1+ //! Lane completion detector — automatically marks lanes as completed when
2+ //! session finishes successfully with green tests and pushed code.
3+ //!
4+ //! This bridges the gap where `LaneContext::completed` was a passive bool
5+ //! that nothing automatically set. Now completion is detected from:
6+ //! - Agent output shows Finished status
7+ //! - No errors/blockers present
8+ //! - Tests passed (green status)
9+ //! - Code pushed (has output file)
10+
11+ use runtime:: {
12+ evaluate, LaneBlocker , LaneContext , PolicyAction , PolicyCondition , PolicyEngine , PolicyRule ,
13+ ReviewStatus ,
14+ } ;
15+
16+ use crate :: AgentOutput ;
17+
18+ /// Detects if a lane should be automatically marked as completed.
19+ ///
20+ /// Returns `Some(LaneContext)` with `completed = true` if all conditions met,
21+ /// `None` if lane should remain active.
22+ pub ( crate ) fn detect_lane_completion (
23+ output : & AgentOutput ,
24+ test_green : bool ,
25+ has_pushed : bool ,
26+ ) -> Option < LaneContext > {
27+ // Must be finished without errors
28+ if output. error . is_some ( ) {
29+ return None ;
30+ }
31+
32+ // Must have finished status
33+ if output. status != "Finished" {
34+ return None ;
35+ }
36+
37+ // Must have no current blocker
38+ if output. current_blocker . is_some ( ) {
39+ return None ;
40+ }
41+
42+ // Must have green tests
43+ if !test_green {
44+ return None ;
45+ }
46+
47+ // Must have pushed code
48+ if !has_pushed {
49+ return None ;
50+ }
51+
52+ // All conditions met — create completed context
53+ Some ( LaneContext {
54+ lane_id : output. agent_id . clone ( ) ,
55+ green_level : 3 , // Workspace green
56+ branch_freshness : std:: time:: Duration :: from_secs ( 0 ) ,
57+ blocker : LaneBlocker :: None ,
58+ review_status : ReviewStatus :: Approved ,
59+ diff_scope : runtime:: DiffScope :: Scoped ,
60+ completed : true ,
61+ reconciled : false ,
62+ } )
63+ }
64+
65+ /// Evaluates policy actions for a completed lane.
66+ pub ( crate ) fn evaluate_completed_lane (
67+ context : & LaneContext ,
68+ ) -> Vec < PolicyAction > {
69+ let engine = PolicyEngine :: new ( vec ! [
70+ PolicyRule :: new(
71+ "closeout-completed-lane" ,
72+ PolicyCondition :: And ( vec![
73+ PolicyCondition :: LaneCompleted ,
74+ PolicyCondition :: GreenAt { level: 3 } ,
75+ ] ) ,
76+ PolicyAction :: CloseoutLane ,
77+ 10 ,
78+ ) ,
79+ PolicyRule :: new(
80+ "cleanup-completed-session" ,
81+ PolicyCondition :: LaneCompleted ,
82+ PolicyAction :: CleanupSession ,
83+ 5 ,
84+ ) ,
85+ ] ) ;
86+
87+ evaluate ( & engine, context)
88+ }
89+
90+ #[ cfg( test) ]
91+ mod tests {
92+ use super :: * ;
93+ use runtime:: { DiffScope , LaneBlocker } ;
94+ use crate :: LaneEvent ;
95+
96+ fn test_output ( ) -> AgentOutput {
97+ AgentOutput {
98+ agent_id : "test-lane-1" . to_string ( ) ,
99+ name : "Test Agent" . to_string ( ) ,
100+ description : "Test" . to_string ( ) ,
101+ subagent_type : None ,
102+ model : None ,
103+ status : "Finished" . to_string ( ) ,
104+ output_file : "/tmp/test.output" . to_string ( ) ,
105+ manifest_file : "/tmp/test.manifest" . to_string ( ) ,
106+ created_at : "2024-01-01T00:00:00Z" . to_string ( ) ,
107+ started_at : Some ( "2024-01-01T00:00:00Z" . to_string ( ) ) ,
108+ completed_at : Some ( "2024-01-01T00:00:00Z" . to_string ( ) ) ,
109+ lane_events : vec ! [ ] ,
110+ current_blocker : None ,
111+ error : None ,
112+ }
113+ }
114+
115+ #[ test]
116+ fn detects_completion_when_all_conditions_met ( ) {
117+ let output = test_output ( ) ;
118+ let result = detect_lane_completion ( & output, true , true ) ;
119+
120+ assert ! ( result. is_some( ) ) ;
121+ let context = result. unwrap ( ) ;
122+ assert ! ( context. completed) ;
123+ assert_eq ! ( context. green_level, 3 ) ;
124+ assert_eq ! ( context. blocker, LaneBlocker :: None ) ;
125+ }
126+
127+ #[ test]
128+ fn no_completion_when_error_present ( ) {
129+ let mut output = test_output ( ) ;
130+ output. error = Some ( "Build failed" . to_string ( ) ) ;
131+
132+ let result = detect_lane_completion ( & output, true , true ) ;
133+ assert ! ( result. is_none( ) ) ;
134+ }
135+
136+ #[ test]
137+ fn no_completion_when_not_finished ( ) {
138+ let mut output = test_output ( ) ;
139+ output. status = "Running" . to_string ( ) ;
140+
141+ let result = detect_lane_completion ( & output, true , true ) ;
142+ assert ! ( result. is_none( ) ) ;
143+ }
144+
145+ #[ test]
146+ fn no_completion_when_tests_not_green ( ) {
147+ let output = test_output ( ) ;
148+
149+ let result = detect_lane_completion ( & output, false , true ) ;
150+ assert ! ( result. is_none( ) ) ;
151+ }
152+
153+ #[ test]
154+ fn no_completion_when_not_pushed ( ) {
155+ let output = test_output ( ) ;
156+
157+ let result = detect_lane_completion ( & output, true , false ) ;
158+ assert ! ( result. is_none( ) ) ;
159+ }
160+
161+ #[ test]
162+ fn evaluate_triggers_closeout_for_completed_lane ( ) {
163+ let context = LaneContext {
164+ lane_id : "completed-lane" . to_string ( ) ,
165+ green_level : 3 ,
166+ branch_freshness : std:: time:: Duration :: from_secs ( 0 ) ,
167+ blocker : LaneBlocker :: None ,
168+ review_status : ReviewStatus :: Approved ,
169+ diff_scope : DiffScope :: Scoped ,
170+ completed : true ,
171+ reconciled : false ,
172+ } ;
173+
174+ let actions = evaluate_completed_lane ( & context) ;
175+
176+ assert ! ( actions. contains( & PolicyAction :: CloseoutLane ) ) ;
177+ assert ! ( actions. contains( & PolicyAction :: CleanupSession ) ) ;
178+ }
179+ }
0 commit comments