huskies: merge 822
This commit is contained in:
@@ -13,6 +13,26 @@ use tokio::sync::broadcast;
|
|||||||
use super::super::super::{CompletionReport, PipelineStage, agent_config_stage, pipeline_stage};
|
use super::super::super::{CompletionReport, PipelineStage, agent_config_stage, pipeline_stage};
|
||||||
use super::super::{AgentPool, StoryAgent};
|
use super::super::{AgentPool, StoryAgent};
|
||||||
|
|
||||||
|
/// Maximum number of bytes of gate output to include in the failure context
|
||||||
|
/// injected into the resumed session. Keeps the injected message focused —
|
||||||
|
/// the tail of the output (where errors appear) is always preserved.
|
||||||
|
const MAX_GATE_OUTPUT_BYTES: usize = 8_000;
|
||||||
|
|
||||||
|
/// Truncate gate output to [`MAX_GATE_OUTPUT_BYTES`], keeping the **tail**
|
||||||
|
/// (where compiler errors and test failures are reported).
|
||||||
|
fn truncate_gate_output(output: &str) -> &str {
|
||||||
|
if output.len() <= MAX_GATE_OUTPUT_BYTES {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
let start = output.len() - MAX_GATE_OUTPUT_BYTES;
|
||||||
|
// Advance to the next valid UTF-8 char boundary.
|
||||||
|
let mut adjusted = start;
|
||||||
|
while !output.is_char_boundary(adjusted) {
|
||||||
|
adjusted += 1;
|
||||||
|
}
|
||||||
|
&output[adjusted..]
|
||||||
|
}
|
||||||
|
|
||||||
impl AgentPool {
|
impl AgentPool {
|
||||||
/// Pipeline advancement: after an agent completes, move the story to
|
/// Pipeline advancement: after an agent completes, move the story to
|
||||||
/// the next pipeline stage and start the appropriate agent.
|
/// the next pipeline stage and start the appropriate agent.
|
||||||
@@ -197,7 +217,7 @@ impl AgentPool {
|
|||||||
"\n\n---\n## Previous Attempt Failed\n\
|
"\n\n---\n## Previous Attempt Failed\n\
|
||||||
The acceptance gates failed with the following output:\n{}\n\n\
|
The acceptance gates failed with the following output:\n{}\n\n\
|
||||||
Please review the failures above, fix the issues, and try again.",
|
Please review the failures above, fix the issues, and try again.",
|
||||||
completion.gate_output
|
truncate_gate_output(&completion.gate_output)
|
||||||
);
|
);
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.start_agent(
|
.start_agent(
|
||||||
|
|||||||
@@ -770,3 +770,89 @@ async fn gates_failed_with_test_evidence_and_committed_work_advances() {
|
|||||||
"run_tests evidence must be cleared after pipeline advance consumes it"
|
"run_tests evidence must be cleared after pipeline advance consumes it"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── story 822: warm-resume coder on gate failure ──────────────────────────
|
||||||
|
|
||||||
|
/// Story 822 / AC 1 & 4: when a coder fails gates and a prior session ID is
|
||||||
|
/// provided, the pipeline re-spawns the coder so it can warm-resume the prior
|
||||||
|
/// conversation with the failure context injected — rather than starting from
|
||||||
|
/// scratch and re-reading the spec.
|
||||||
|
///
|
||||||
|
/// The test verifies:
|
||||||
|
/// - The coder is re-spawned (Pending/Running) rather than blocked.
|
||||||
|
/// - The retry counter is incremented (AC 3).
|
||||||
|
#[tokio::test]
|
||||||
|
async fn warm_resume_coder_on_gate_failure_with_session_id() {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let root = tmp.path();
|
||||||
|
|
||||||
|
// Set up a project with a fast no-op coder agent.
|
||||||
|
fs::create_dir_all(root.join(".huskies")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
root.join(".huskies/project.toml"),
|
||||||
|
r#"
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder-1"
|
||||||
|
role = "Coder"
|
||||||
|
command = "echo"
|
||||||
|
args = ["noop"]
|
||||||
|
prompt = "test prompt"
|
||||||
|
stage = "coder"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
crate::db::write_item_with_content(
|
||||||
|
"9950_story_warm_resume",
|
||||||
|
"2_current",
|
||||||
|
"---\nname: Warm Resume Test\n---\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let pool = AgentPool::new_test(3001);
|
||||||
|
|
||||||
|
// Simulate a coder failing gates. A prior session ID is provided to
|
||||||
|
// trigger the warm-resume path (--resume <session_id>).
|
||||||
|
pool.run_pipeline_advance(
|
||||||
|
"9950_story_warm_resume",
|
||||||
|
"coder-1",
|
||||||
|
CompletionReport {
|
||||||
|
summary: "Tests failed".to_string(),
|
||||||
|
gates_passed: false,
|
||||||
|
gate_output: "error[E0308]: mismatched types\n --> src/lib.rs:5:10".to_string(),
|
||||||
|
},
|
||||||
|
Some(root.to_path_buf()),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
Some("prior-session-abc123".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// The coder must be re-spawned — Pending or Running.
|
||||||
|
let agents = pool.agents.lock().unwrap();
|
||||||
|
let coder_restarted = agents.values().any(|a| {
|
||||||
|
a.agent_name == "coder-1" && matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
coder_restarted,
|
||||||
|
"Coder must be re-spawned (warm-resumed) when gates fail and prior session ID provided. \
|
||||||
|
Pool: {:?}",
|
||||||
|
agents
|
||||||
|
.iter()
|
||||||
|
.map(|(k, a)| format!("{k}: {} ({})", a.agent_name, a.status))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
drop(agents);
|
||||||
|
|
||||||
|
// Retry counter must have been incremented (AC 3).
|
||||||
|
let content = crate::db::read_content("9950_story_warm_resume")
|
||||||
|
.expect("story must exist in content store");
|
||||||
|
assert!(
|
||||||
|
content.contains("retry_count"),
|
||||||
|
"retry_count must be incremented after warm-resume: {content}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user