@@ -57,6 +57,40 @@ fn default_log_dir() -> String {
5757 "~/.stakpak/watch/logs" . to_string ( )
5858}
5959
60+ /// Determines which check script exit codes trigger the agent.
61+ #[ derive( Debug , Clone , Copy , Default , PartialEq , Eq , Serialize , Deserialize ) ]
62+ #[ serde( rename_all = "lowercase" ) ]
63+ pub enum CheckTriggerOn {
64+ /// Trigger agent only on exit code 0 (default behavior).
65+ #[ default]
66+ Success ,
67+ /// Trigger agent on any non-zero exit code (1+).
68+ Failure ,
69+ /// Trigger agent regardless of exit code (only timeout/error prevents trigger).
70+ Any ,
71+ }
72+
73+ impl CheckTriggerOn {
74+ /// Returns true if the given exit code should trigger the agent.
75+ pub fn should_trigger ( & self , exit_code : i32 ) -> bool {
76+ match self {
77+ CheckTriggerOn :: Success => exit_code == 0 ,
78+ CheckTriggerOn :: Failure => exit_code != 0 ,
79+ CheckTriggerOn :: Any => true ,
80+ }
81+ }
82+ }
83+
84+ impl std:: fmt:: Display for CheckTriggerOn {
85+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
86+ match self {
87+ CheckTriggerOn :: Success => write ! ( f, "success" ) ,
88+ CheckTriggerOn :: Failure => write ! ( f, "failure" ) ,
89+ CheckTriggerOn :: Any => write ! ( f, "any" ) ,
90+ }
91+ }
92+ }
93+
6094/// Default values applied to triggers when not specified.
6195#[ derive( Debug , Clone , Serialize , Deserialize ) ]
6296pub struct WatchDefaults {
@@ -84,6 +118,13 @@ pub struct WatchDefaults {
84118 /// When true, the agent will pause and exit with code 10 when tools need approval.
85119 #[ serde( default = "default_pause_on_approval" ) ]
86120 pub pause_on_approval : bool ,
121+
122+ /// Determines which check script exit codes trigger the agent.
123+ /// - "success" (default): trigger on exit 0
124+ /// - "failure": trigger on non-zero exit codes (1+)
125+ /// - "any": trigger regardless of exit code
126+ #[ serde( default ) ]
127+ pub check_trigger_on : CheckTriggerOn ,
87128}
88129
89130impl Default for WatchDefaults {
@@ -95,6 +136,7 @@ impl Default for WatchDefaults {
95136 enable_slack_tools : false ,
96137 enable_subagents : false ,
97138 pause_on_approval : default_pause_on_approval ( ) ,
139+ check_trigger_on : CheckTriggerOn :: default ( ) ,
98140 }
99141 }
100142}
@@ -133,6 +175,13 @@ pub struct Trigger {
133175 #[ serde( default , with = "option_humantime_serde" ) ]
134176 pub check_timeout : Option < Duration > ,
135177
178+ /// Determines which check script exit codes trigger the agent.
179+ /// Falls back to defaults.check_trigger_on if not specified.
180+ /// - "success" (default): trigger on exit 0
181+ /// - "failure": trigger on non-zero exit codes (1+)
182+ /// - "any": trigger regardless of exit code
183+ pub check_trigger_on : Option < CheckTriggerOn > ,
184+
136185 /// Prompt to pass to the agent when triggered.
137186 pub prompt : String ,
138187
@@ -177,6 +226,11 @@ impl Trigger {
177226 self . check_timeout . unwrap_or ( defaults. check_timeout )
178227 }
179228
229+ /// Get the effective check_trigger_on, falling back to defaults.
230+ pub fn effective_check_trigger_on ( & self , defaults : & WatchDefaults ) -> CheckTriggerOn {
231+ self . check_trigger_on . unwrap_or ( defaults. check_trigger_on )
232+ }
233+
180234 /// Get the effective enable_slack_tools, falling back to defaults.
181235 pub fn effective_enable_slack_tools ( & self , defaults : & WatchDefaults ) -> bool {
182236 self . enable_slack_tools
@@ -627,6 +681,134 @@ prompt = "Test"
627681[[triggers]]
628682name = "test"
629683schedule = "0 * * * *"
684+ "# ;
685+
686+ let result = WatchConfig :: parse ( config_str) ;
687+ assert ! ( result. is_err( ) ) ;
688+ assert ! ( matches!( result. unwrap_err( ) , ConfigError :: ParseError ( _) ) ) ;
689+ }
690+
691+ #[ test]
692+ fn test_check_trigger_on_should_trigger ( ) {
693+ // Success mode: only exit 0 triggers
694+ assert ! ( CheckTriggerOn :: Success . should_trigger( 0 ) ) ;
695+ assert ! ( !CheckTriggerOn :: Success . should_trigger( 1 ) ) ;
696+ assert ! ( !CheckTriggerOn :: Success . should_trigger( 2 ) ) ;
697+ assert ! ( !CheckTriggerOn :: Success . should_trigger( -1 ) ) ;
698+
699+ // Failure mode: any non-zero triggers
700+ assert ! ( !CheckTriggerOn :: Failure . should_trigger( 0 ) ) ;
701+ assert ! ( CheckTriggerOn :: Failure . should_trigger( 1 ) ) ;
702+ assert ! ( CheckTriggerOn :: Failure . should_trigger( 2 ) ) ;
703+ assert ! ( CheckTriggerOn :: Failure . should_trigger( -1 ) ) ;
704+
705+ // Any mode: all exit codes trigger
706+ assert ! ( CheckTriggerOn :: Any . should_trigger( 0 ) ) ;
707+ assert ! ( CheckTriggerOn :: Any . should_trigger( 1 ) ) ;
708+ assert ! ( CheckTriggerOn :: Any . should_trigger( 2 ) ) ;
709+ assert ! ( CheckTriggerOn :: Any . should_trigger( -1 ) ) ;
710+ }
711+
712+ #[ test]
713+ fn test_check_trigger_on_default ( ) {
714+ assert_eq ! ( CheckTriggerOn :: default ( ) , CheckTriggerOn :: Success ) ;
715+ }
716+
717+ #[ test]
718+ fn test_check_trigger_on_display ( ) {
719+ assert_eq ! ( CheckTriggerOn :: Success . to_string( ) , "success" ) ;
720+ assert_eq ! ( CheckTriggerOn :: Failure . to_string( ) , "failure" ) ;
721+ assert_eq ! ( CheckTriggerOn :: Any . to_string( ) , "any" ) ;
722+ }
723+
724+ #[ test]
725+ fn test_check_trigger_on_parsing ( ) {
726+ // Test parsing from TOML
727+ let config_str = r#"
728+ [[triggers]]
729+ name = "success-trigger"
730+ schedule = "0 * * * *"
731+ prompt = "Test"
732+ check_trigger_on = "success"
733+
734+ [[triggers]]
735+ name = "failure-trigger"
736+ schedule = "0 * * * *"
737+ prompt = "Test"
738+ check_trigger_on = "failure"
739+
740+ [[triggers]]
741+ name = "any-trigger"
742+ schedule = "0 * * * *"
743+ prompt = "Test"
744+ check_trigger_on = "any"
745+
746+ [[triggers]]
747+ name = "default-trigger"
748+ schedule = "0 * * * *"
749+ prompt = "Test"
750+ "# ;
751+
752+ let config = WatchConfig :: parse ( config_str) . expect ( "Should parse check_trigger_on values" ) ;
753+ assert_eq ! ( config. triggers. len( ) , 4 ) ;
754+
755+ assert_eq ! (
756+ config. triggers[ 0 ] . check_trigger_on,
757+ Some ( CheckTriggerOn :: Success )
758+ ) ;
759+ assert_eq ! (
760+ config. triggers[ 1 ] . check_trigger_on,
761+ Some ( CheckTriggerOn :: Failure )
762+ ) ;
763+ assert_eq ! (
764+ config. triggers[ 2 ] . check_trigger_on,
765+ Some ( CheckTriggerOn :: Any )
766+ ) ;
767+ assert_eq ! ( config. triggers[ 3 ] . check_trigger_on, None ) ;
768+ }
769+
770+ #[ test]
771+ fn test_check_trigger_on_defaults_fallback ( ) {
772+ let config_str = r#"
773+ [defaults]
774+ check_trigger_on = "failure"
775+
776+ [[triggers]]
777+ name = "uses-default"
778+ schedule = "0 * * * *"
779+ prompt = "Test"
780+
781+ [[triggers]]
782+ name = "overrides-default"
783+ schedule = "0 * * * *"
784+ prompt = "Test"
785+ check_trigger_on = "success"
786+ "# ;
787+
788+ let config =
789+ WatchConfig :: parse ( config_str) . expect ( "Should parse check_trigger_on with defaults" ) ;
790+
791+ // First trigger should use default (failure)
792+ assert_eq ! (
793+ config. triggers[ 0 ] . effective_check_trigger_on( & config. defaults) ,
794+ CheckTriggerOn :: Failure
795+ ) ;
796+
797+ // Second trigger should override to success
798+ assert_eq ! (
799+ config. triggers[ 1 ] . effective_check_trigger_on( & config. defaults) ,
800+ CheckTriggerOn :: Success
801+ ) ;
802+ }
803+
804+ #[ test]
805+ fn test_check_trigger_on_invalid_value ( ) {
806+ let config_str = r#"
807+ [[triggers]]
808+ name = "invalid"
809+ schedule = "0 * * * *"
810+ prompt = "Test"
811+ check_trigger_on = "invalid"
630812"# ;
631813
632814 let result = WatchConfig :: parse ( config_str) ;
0 commit comments