Files
claude-code/rust/crates/tools/src/lane_completion.rs
bellman f7235ca932 Make G006 task policy state machine executable
Typed task packets, policy decisions, lane board status, and session liveness now have concrete runtime contracts and focused regressions for Stream 4.

Constraint: G006 requires task/lane operation without pane scraping while preserving legacy task packet callers.
Rejected: waiting on stale worker worktrees | all G006 worker worktrees remained at main with no commits, so leader integrated the verified slice directly.
Confidence: high
Scope-risk: moderate
Directive: Keep task packet serde defaults when adding fields so older packets continue to deserialize.
Tested: git diff --check; cargo fmt --manifest-path rust/Cargo.toml --all -- --check; cargo check --manifest-path rust/Cargo.toml -p runtime -p tools -p rusty-claude-cli; cargo test --manifest-path rust/Cargo.toml -p runtime task_packet -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime policy_engine -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime task_registry -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p runtime session_heartbeat -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools run_task_packet_creates_packet_backed_task -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p tools lane_completion -- --nocapture; cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli status_json_surfaces -- --nocapture
Not-tested: full workspace test suite; PR/issue reconciliation deferred to G011/G012

Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-15 09:29:26 +09:00

194 lines
5.7 KiB
Rust

//! 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.
#[allow(dead_code)]
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.eq_ignore_ascii_case("completed")
&& !output.status.eq_ignore_ascii_case("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
green_contract_satisfied: true,
branch_freshness: std::time::Duration::from_secs(0),
blocker: LaneBlocker::None,
review_status: ReviewStatus::Approved,
diff_scope: runtime::DiffScope::Scoped,
completed: true,
reconciled: false,
retry_count: 0,
retry_limit: 1,
rebase_required: false,
stale_cleanup_required: false,
approval_token: None,
})
}
/// Evaluates policy actions for a completed lane.
#[allow(dead_code)]
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};
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![],
derived_state: "working".to_string(),
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,
green_contract_satisfied: true,
branch_freshness: std::time::Duration::from_secs(0),
blocker: LaneBlocker::None,
review_status: ReviewStatus::Approved,
diff_scope: DiffScope::Scoped,
completed: true,
reconciled: false,
retry_count: 0,
retry_limit: 1,
rebase_required: false,
stale_cleanup_required: false,
approval_token: None,
};
let actions = evaluate_completed_lane(&context);
assert!(actions.contains(&PolicyAction::CloseoutLane));
assert!(actions.contains(&PolicyAction::CleanupSession));
}
}