mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-05 23:54:50 +08:00
feat(tools): add lane_completion module (P1.3)
Implement automatic lane completion detection: - detect_lane_completion(): checks session-finished + tests-green + pushed - evaluate_completed_lane(): triggers CloseoutLane + CleanupSession actions - 6 tests covering all conditions Bridges the gap where LaneContext::completed was a passive bool that nothing automatically set. Now completion is auto-detected. ROADMAP P1.3 marked done.
This commit is contained in:
179
rust/crates/tools/src/lane_completion.rs
Normal file
179
rust/crates/tools/src/lane_completion.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
//! Lane completion detector — automatically marks lanes as completed when
|
||||
//! session finishes successfully with green tests and pushed code.
|
||||
//!
|
||||
//! This bridges the gap where `LaneContext::completed` was a passive bool
|
||||
//! that nothing automatically set. Now completion is detected from:
|
||||
//! - Agent output shows Finished status
|
||||
//! - No errors/blockers present
|
||||
//! - Tests passed (green status)
|
||||
//! - Code pushed (has output file)
|
||||
|
||||
use runtime::{
|
||||
evaluate, LaneBlocker, LaneContext, PolicyAction, PolicyCondition, PolicyEngine, PolicyRule,
|
||||
ReviewStatus,
|
||||
};
|
||||
|
||||
use crate::AgentOutput;
|
||||
|
||||
/// Detects if a lane should be automatically marked as completed.
|
||||
///
|
||||
/// Returns `Some(LaneContext)` with `completed = true` if all conditions met,
|
||||
/// `None` if lane should remain active.
|
||||
pub(crate) fn detect_lane_completion(
|
||||
output: &AgentOutput,
|
||||
test_green: bool,
|
||||
has_pushed: bool,
|
||||
) -> Option<LaneContext> {
|
||||
// Must be finished without errors
|
||||
if output.error.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must have finished status
|
||||
if output.status != "Finished" {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must have no current blocker
|
||||
if output.current_blocker.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must have green tests
|
||||
if !test_green {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must have pushed code
|
||||
if !has_pushed {
|
||||
return None;
|
||||
}
|
||||
|
||||
// All conditions met — create completed context
|
||||
Some(LaneContext {
|
||||
lane_id: output.agent_id.clone(),
|
||||
green_level: 3, // Workspace green
|
||||
branch_freshness: std::time::Duration::from_secs(0),
|
||||
blocker: LaneBlocker::None,
|
||||
review_status: ReviewStatus::Approved,
|
||||
diff_scope: runtime::DiffScope::Scoped,
|
||||
completed: true,
|
||||
reconciled: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Evaluates policy actions for a completed lane.
|
||||
pub(crate) fn evaluate_completed_lane(
|
||||
context: &LaneContext,
|
||||
) -> Vec<PolicyAction> {
|
||||
let engine = PolicyEngine::new(vec![
|
||||
PolicyRule::new(
|
||||
"closeout-completed-lane",
|
||||
PolicyCondition::And(vec![
|
||||
PolicyCondition::LaneCompleted,
|
||||
PolicyCondition::GreenAt { level: 3 },
|
||||
]),
|
||||
PolicyAction::CloseoutLane,
|
||||
10,
|
||||
),
|
||||
PolicyRule::new(
|
||||
"cleanup-completed-session",
|
||||
PolicyCondition::LaneCompleted,
|
||||
PolicyAction::CleanupSession,
|
||||
5,
|
||||
),
|
||||
]);
|
||||
|
||||
evaluate(&engine, context)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use runtime::{DiffScope, LaneBlocker};
|
||||
use crate::LaneEvent;
|
||||
|
||||
fn test_output() -> AgentOutput {
|
||||
AgentOutput {
|
||||
agent_id: "test-lane-1".to_string(),
|
||||
name: "Test Agent".to_string(),
|
||||
description: "Test".to_string(),
|
||||
subagent_type: None,
|
||||
model: None,
|
||||
status: "Finished".to_string(),
|
||||
output_file: "/tmp/test.output".to_string(),
|
||||
manifest_file: "/tmp/test.manifest".to_string(),
|
||||
created_at: "2024-01-01T00:00:00Z".to_string(),
|
||||
started_at: Some("2024-01-01T00:00:00Z".to_string()),
|
||||
completed_at: Some("2024-01-01T00:00:00Z".to_string()),
|
||||
lane_events: vec![],
|
||||
current_blocker: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_completion_when_all_conditions_met() {
|
||||
let output = test_output();
|
||||
let result = detect_lane_completion(&output, true, true);
|
||||
|
||||
assert!(result.is_some());
|
||||
let context = result.unwrap();
|
||||
assert!(context.completed);
|
||||
assert_eq!(context.green_level, 3);
|
||||
assert_eq!(context.blocker, LaneBlocker::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completion_when_error_present() {
|
||||
let mut output = test_output();
|
||||
output.error = Some("Build failed".to_string());
|
||||
|
||||
let result = detect_lane_completion(&output, true, true);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completion_when_not_finished() {
|
||||
let mut output = test_output();
|
||||
output.status = "Running".to_string();
|
||||
|
||||
let result = detect_lane_completion(&output, true, true);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completion_when_tests_not_green() {
|
||||
let output = test_output();
|
||||
|
||||
let result = detect_lane_completion(&output, false, true);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_completion_when_not_pushed() {
|
||||
let output = test_output();
|
||||
|
||||
let result = detect_lane_completion(&output, true, false);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_triggers_closeout_for_completed_lane() {
|
||||
let context = LaneContext {
|
||||
lane_id: "completed-lane".to_string(),
|
||||
green_level: 3,
|
||||
branch_freshness: std::time::Duration::from_secs(0),
|
||||
blocker: LaneBlocker::None,
|
||||
review_status: ReviewStatus::Approved,
|
||||
diff_scope: DiffScope::Scoped,
|
||||
completed: true,
|
||||
reconciled: false,
|
||||
};
|
||||
|
||||
let actions = evaluate_completed_lane(&context);
|
||||
|
||||
assert!(actions.contains(&PolicyAction::CloseoutLane));
|
||||
assert!(actions.contains(&PolicyAction::CleanupSession));
|
||||
}
|
||||
}
|
||||
@@ -4793,6 +4793,8 @@ fn parse_skill_description(contents: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub mod lane_completion;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
Reference in New Issue
Block a user