feat: 3 person review
This commit is contained in:
@@ -35,6 +35,7 @@ pub fn runtime_loop(
|
||||
}
|
||||
let goal_md = toon::read_markdown(&config.goal_file)?;
|
||||
let standards_md = toon::read_markdown(&config.standards_file)?;
|
||||
refresh_usage_state(&mut state);
|
||||
emit_snapshot(&event_tx, &goal_md, &standards_md, &plan, &state);
|
||||
|
||||
match control_rx.try_recv() {
|
||||
@@ -186,11 +187,12 @@ pub fn runtime_loop(
|
||||
plan.mark_active(&step.id);
|
||||
state.current_step_id = Some(step.id.clone());
|
||||
state.iteration += 1;
|
||||
refresh_usage_state(&mut state);
|
||||
toon::write_plan(&config.plan_file, &plan)?;
|
||||
toon::write_state(&config.state_file, &state)?;
|
||||
emit_snapshot(&event_tx, &goal_md, &standards_md, &plan, &state);
|
||||
|
||||
let exec = executor::implement(&repo_root, &config, &plan, &step, &event_tx)?;
|
||||
let exec = executor::implement(&repo_root, &config, &state, &plan, &step, &event_tx)?;
|
||||
if goal_checker::needs_goal_clarification(&exec) {
|
||||
state.phase = ControllerPhase::Planning;
|
||||
state.set_stop_reason(format!(
|
||||
@@ -253,6 +255,11 @@ pub fn runtime_loop(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh_usage_state(state: &mut crate::model::ControllerState) {
|
||||
let snapshot = crate::process::refresh_usage_snapshot(state);
|
||||
crate::process::persist_usage_snapshot(state, &snapshot);
|
||||
}
|
||||
|
||||
fn emit_snapshot(
|
||||
event_tx: &Sender<AppEvent>,
|
||||
goal_md: &str,
|
||||
@@ -291,12 +298,25 @@ fn recover_stale_execution_state(
|
||||
state: &mut crate::model::ControllerState,
|
||||
event_tx: &Sender<AppEvent>,
|
||||
) -> Result<bool> {
|
||||
if state.current_step_id.is_some() {
|
||||
let current_step_id = state.current_step_id.clone();
|
||||
let has_stale_current_step = if let Some(current_step_id) = ¤t_step_id {
|
||||
!plan.steps.iter().any(|step| {
|
||||
step.id == *current_step_id
|
||||
&& matches!(
|
||||
step.status,
|
||||
StepStatus::Todo | StepStatus::Active | StepStatus::Blocked
|
||||
)
|
||||
})
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !has_stale_current_step && state.current_step_id.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let active_steps = plan.active_step_ids();
|
||||
if active_steps.is_empty() {
|
||||
if !has_stale_current_step && active_steps.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -312,11 +332,28 @@ fn recover_stale_execution_state(
|
||||
state.goal_status = GoalStatus::InProgress;
|
||||
state.clear_stop_reason();
|
||||
state.replan_required = false;
|
||||
let reason = format!(
|
||||
"Recovered stale active step state for {}. Reset {} to todo.",
|
||||
config.controller_id(),
|
||||
active_steps.join(", ")
|
||||
);
|
||||
state.current_step_id = None;
|
||||
|
||||
let reason = if has_stale_current_step && !active_steps.is_empty() {
|
||||
format!(
|
||||
"Recovered stale execution state for {}. Cleared current_step_id {}. Reset {} to todo.",
|
||||
config.controller_id(),
|
||||
current_step_id.unwrap_or_default(),
|
||||
active_steps.join(", ")
|
||||
)
|
||||
} else if has_stale_current_step {
|
||||
format!(
|
||||
"Recovered stale execution state for {}. Cleared current_step_id {}.",
|
||||
config.controller_id(),
|
||||
current_step_id.unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Recovered stale active step state for {}. Reset {} to todo.",
|
||||
config.controller_id(),
|
||||
active_steps.join(", ")
|
||||
)
|
||||
};
|
||||
state.notes.push(reason.clone());
|
||||
toon::write_plan(&config.plan_file, plan)?;
|
||||
toon::write_state(&config.state_file, state)?;
|
||||
@@ -389,6 +426,53 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovers_stale_current_step_reference() {
|
||||
let temp = tempdir().expect("tempdir");
|
||||
let mut config = TaskConfig::default_for("stale-current");
|
||||
let root = temp.path().join(".agent/controllers/stale-current");
|
||||
config.goal_file = root.join("goal.md");
|
||||
config.plan_file = root.join("plan.toon");
|
||||
config.state_file = root.join("state.toon");
|
||||
config.standards_file = root.join("standards.md");
|
||||
|
||||
let mut plan = Plan {
|
||||
version: 1,
|
||||
goal_summary: "goal".to_string(),
|
||||
steps: vec![PlanStep {
|
||||
id: "s1".to_string(),
|
||||
title: "Scope".to_string(),
|
||||
status: StepStatus::Done,
|
||||
..PlanStep::default()
|
||||
}],
|
||||
};
|
||||
let mut state = ControllerState {
|
||||
phase: ControllerPhase::Blocked,
|
||||
goal_status: GoalStatus::Blocked,
|
||||
current_step_id: Some("s1".to_string()),
|
||||
..ControllerState::default()
|
||||
};
|
||||
|
||||
toon::ensure_controller_files(&config).expect("ensure files");
|
||||
let (event_tx, event_rx) = mpsc::channel();
|
||||
|
||||
let recovered = recover_stale_execution_state(&config, &mut plan, &mut state, &event_tx)
|
||||
.expect("recover");
|
||||
|
||||
assert!(recovered);
|
||||
assert!(matches!(state.current_step_id, None));
|
||||
assert!(matches!(state.phase, ControllerPhase::Executing));
|
||||
assert!(matches!(state.goal_status, GoalStatus::InProgress));
|
||||
assert!(state.stop_reason.is_none());
|
||||
let event = event_rx.recv().expect("notice event");
|
||||
match event {
|
||||
AppEvent::Session(entry) => {
|
||||
assert!(entry.body.contains("Cleared current_step_id s1"));
|
||||
}
|
||||
other => panic!("unexpected event: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumable_step_prefers_current_blocked_or_active_step() {
|
||||
let plan = Plan {
|
||||
|
||||
Reference in New Issue
Block a user