mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-21 05:06:44 +00:00
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>
This commit is contained in:
@@ -14,6 +14,7 @@ use crate::{validate_packet, TaskPacket, TaskPacketValidationError};
|
||||
pub enum TaskStatus {
|
||||
Created,
|
||||
Running,
|
||||
Blocked,
|
||||
Completed,
|
||||
Failed,
|
||||
Stopped,
|
||||
@@ -24,6 +25,7 @@ impl std::fmt::Display for TaskStatus {
|
||||
match self {
|
||||
Self::Created => write!(f, "created"),
|
||||
Self::Running => write!(f, "running"),
|
||||
Self::Blocked => write!(f, "blocked"),
|
||||
Self::Completed => write!(f, "completed"),
|
||||
Self::Failed => write!(f, "failed"),
|
||||
Self::Stopped => write!(f, "stopped"),
|
||||
@@ -43,6 +45,54 @@ pub struct Task {
|
||||
pub messages: Vec<TaskMessage>,
|
||||
pub output: String,
|
||||
pub team_id: Option<String>,
|
||||
pub heartbeat: Option<LaneHeartbeat>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LaneFreshness {
|
||||
Healthy,
|
||||
Stalled,
|
||||
TransportDead,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LaneHeartbeat {
|
||||
pub observed_at: u64,
|
||||
pub transport_alive: bool,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl LaneHeartbeat {
|
||||
#[must_use]
|
||||
pub fn freshness_at(&self, now: u64, stalled_after_secs: u64) -> LaneFreshness {
|
||||
if !self.transport_alive {
|
||||
return LaneFreshness::TransportDead;
|
||||
}
|
||||
if now.saturating_sub(self.observed_at) > stalled_after_secs {
|
||||
return LaneFreshness::Stalled;
|
||||
}
|
||||
LaneFreshness::Healthy
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LaneBoardEntry {
|
||||
pub task_id: String,
|
||||
pub prompt: String,
|
||||
pub status: TaskStatus,
|
||||
pub team_id: Option<String>,
|
||||
pub heartbeat: Option<LaneHeartbeat>,
|
||||
pub freshness: LaneFreshness,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LaneBoard {
|
||||
pub generated_at: u64,
|
||||
pub active: Vec<LaneBoardEntry>,
|
||||
pub blocked: Vec<LaneBoardEntry>,
|
||||
pub finished: Vec<LaneBoardEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -114,6 +164,7 @@ impl TaskRegistry {
|
||||
messages: Vec::new(),
|
||||
output: String::new(),
|
||||
team_id: None,
|
||||
heartbeat: None,
|
||||
};
|
||||
inner.tasks.insert(task_id, task.clone());
|
||||
task
|
||||
@@ -134,6 +185,67 @@ impl TaskRegistry {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_heartbeat(&self, task_id: &str, heartbeat: LaneHeartbeat) -> Result<(), String> {
|
||||
let mut inner = self.inner.lock().expect("registry lock poisoned");
|
||||
let task = inner
|
||||
.tasks
|
||||
.get_mut(task_id)
|
||||
.ok_or_else(|| format!("task not found: {task_id}"))?;
|
||||
task.heartbeat = Some(heartbeat);
|
||||
task.updated_at = now_secs();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn lane_board(&self, stalled_after_secs: u64) -> LaneBoard {
|
||||
let now = now_secs();
|
||||
self.lane_board_at(now, stalled_after_secs)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn lane_board_at(&self, now: u64, stalled_after_secs: u64) -> LaneBoard {
|
||||
let inner = self.inner.lock().expect("registry lock poisoned");
|
||||
let mut board = LaneBoard {
|
||||
generated_at: now,
|
||||
active: Vec::new(),
|
||||
blocked: Vec::new(),
|
||||
finished: Vec::new(),
|
||||
};
|
||||
|
||||
for task in inner.tasks.values() {
|
||||
let freshness = task
|
||||
.heartbeat
|
||||
.as_ref()
|
||||
.map_or(LaneFreshness::Unknown, |heartbeat| {
|
||||
heartbeat.freshness_at(now, stalled_after_secs)
|
||||
});
|
||||
let entry = LaneBoardEntry {
|
||||
task_id: task.task_id.clone(),
|
||||
prompt: task.prompt.clone(),
|
||||
status: task.status,
|
||||
team_id: task.team_id.clone(),
|
||||
heartbeat: task.heartbeat.clone(),
|
||||
freshness,
|
||||
};
|
||||
|
||||
match task.status {
|
||||
TaskStatus::Running | TaskStatus::Created => board.active.push(entry),
|
||||
TaskStatus::Blocked => board.blocked.push(entry),
|
||||
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Stopped => {
|
||||
board.finished.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
board
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn lane_status_json_at(&self, now: u64, stalled_after_secs: u64) -> serde_json::Value {
|
||||
serde_json::to_value(self.lane_board_at(now, stalled_after_secs))
|
||||
.expect("lane board should serialize")
|
||||
}
|
||||
|
||||
pub fn stop(&self, task_id: &str) -> Result<Task, String> {
|
||||
let mut inner = self.inner.lock().expect("registry lock poisoned");
|
||||
let task = inner
|
||||
@@ -260,9 +372,20 @@ mod tests {
|
||||
repo: "claw-code-parity".to_string(),
|
||||
branch_policy: "origin/main only".to_string(),
|
||||
acceptance_tests: vec!["cargo test --workspace".to_string()],
|
||||
acceptance_criteria: vec!["task is inspectable".to_string()],
|
||||
resources: vec![crate::TaskResource {
|
||||
kind: "module".to_string(),
|
||||
value: "runtime/task system".to_string(),
|
||||
}],
|
||||
model: Some("gpt-5.5".to_string()),
|
||||
provider: Some("openai".to_string()),
|
||||
permission_profile: Some("workspace-write".to_string()),
|
||||
commit_policy: "single commit".to_string(),
|
||||
reporting_contract: "print commit sha".to_string(),
|
||||
reporting_targets: vec!["leader".to_string()],
|
||||
escalation_policy: "manual escalation".to_string(),
|
||||
recovery_policy: Some("retry once".to_string()),
|
||||
verification_plan: vec!["cargo test --workspace".to_string()],
|
||||
};
|
||||
|
||||
let task = registry
|
||||
@@ -340,6 +463,68 @@ mod tests {
|
||||
assert_eq!(output, "line 1\nline 2\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lane_board_groups_active_blocked_finished_and_reports_freshness() {
|
||||
let registry = TaskRegistry::new();
|
||||
let active = registry.create("active", None);
|
||||
let blocked = registry.create("blocked", None);
|
||||
let finished = registry.create("finished", None);
|
||||
|
||||
registry
|
||||
.set_status(&active.task_id, TaskStatus::Running)
|
||||
.expect("running status");
|
||||
registry
|
||||
.set_status(&blocked.task_id, TaskStatus::Blocked)
|
||||
.expect("blocked status");
|
||||
registry
|
||||
.set_status(&finished.task_id, TaskStatus::Completed)
|
||||
.expect("completed status");
|
||||
registry
|
||||
.update_heartbeat(
|
||||
&active.task_id,
|
||||
LaneHeartbeat {
|
||||
observed_at: 100,
|
||||
transport_alive: true,
|
||||
status: "running".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("heartbeat");
|
||||
registry
|
||||
.update_heartbeat(
|
||||
&blocked.task_id,
|
||||
LaneHeartbeat {
|
||||
observed_at: 10,
|
||||
transport_alive: true,
|
||||
status: "waiting".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("heartbeat");
|
||||
registry
|
||||
.update_heartbeat(
|
||||
&finished.task_id,
|
||||
LaneHeartbeat {
|
||||
observed_at: 100,
|
||||
transport_alive: false,
|
||||
status: "done".to_string(),
|
||||
},
|
||||
)
|
||||
.expect("heartbeat");
|
||||
|
||||
let board = registry.lane_board_at(110, 30);
|
||||
|
||||
assert_eq!(board.active.len(), 1);
|
||||
assert_eq!(board.active[0].freshness, LaneFreshness::Healthy);
|
||||
assert_eq!(board.blocked.len(), 1);
|
||||
assert_eq!(board.blocked[0].freshness, LaneFreshness::Stalled);
|
||||
assert_eq!(board.finished.len(), 1);
|
||||
assert_eq!(board.finished[0].freshness, LaneFreshness::TransportDead);
|
||||
|
||||
let json = registry.lane_status_json_at(110, 30);
|
||||
assert_eq!(json["active"][0]["status"], "running");
|
||||
assert_eq!(json["blocked"][0]["freshness"], "stalled");
|
||||
assert_eq!(json["finished"][0]["freshness"], "transport_dead");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigns_team_and_removes_task() {
|
||||
let registry = TaskRegistry::new();
|
||||
@@ -375,6 +560,7 @@ mod tests {
|
||||
let cases = [
|
||||
(TaskStatus::Created, "created"),
|
||||
(TaskStatus::Running, "running"),
|
||||
(TaskStatus::Blocked, "blocked"),
|
||||
(TaskStatus::Completed, "completed"),
|
||||
(TaskStatus::Failed, "failed"),
|
||||
(TaskStatus::Stopped, "stopped"),
|
||||
@@ -392,6 +578,7 @@ mod tests {
|
||||
vec![
|
||||
("created".to_string(), "created"),
|
||||
("running".to_string(), "running"),
|
||||
("blocked".to_string(), "blocked"),
|
||||
("completed".to_string(), "completed"),
|
||||
("failed".to_string(), "failed"),
|
||||
("stopped".to_string(), "stopped"),
|
||||
@@ -478,6 +665,7 @@ mod tests {
|
||||
assert!(task.messages.is_empty());
|
||||
assert!(task.output.is_empty());
|
||||
assert_eq!(task.team_id, None);
|
||||
assert_eq!(task.heartbeat, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user