huskies: merge 881_bug_inject_prior_gate_failure_output_into_retry_agent_s_system_prompt
This commit is contained in:
@@ -199,7 +199,13 @@ impl AgentPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else
|
} else {
|
||||||
|
// Persist gate_output so the retry spawn can inject it into
|
||||||
|
// --append-system-prompt (story 881).
|
||||||
|
crate::db::write_content(
|
||||||
|
&format!("{story_id}:gate_output"),
|
||||||
|
&completion.gate_output,
|
||||||
|
);
|
||||||
// Increment retry count and check if blocked.
|
// Increment retry count and check if blocked.
|
||||||
if let Some(reason) =
|
if let Some(reason) =
|
||||||
should_block_story(story_id, config.max_retries, "coder")
|
should_block_story(story_id, config.max_retries, "coder")
|
||||||
@@ -236,6 +242,7 @@ impl AgentPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
PipelineStage::Qa => {
|
PipelineStage::Qa => {
|
||||||
if completion.gates_passed {
|
if completion.gates_passed {
|
||||||
// Run coverage gate in the QA worktree before advancing to merge.
|
// Run coverage gate in the QA worktree before advancing to merge.
|
||||||
@@ -318,8 +325,14 @@ impl AgentPool {
|
|||||||
slog_error!("[pipeline] Failed to restart qa for '{story_id}': {e}");
|
slog_error!("[pipeline] Failed to restart qa for '{story_id}': {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(reason) = should_block_story(story_id, config.max_retries, "qa")
|
} else {
|
||||||
{
|
// Persist gate_output so the retry spawn can inject it into
|
||||||
|
// --append-system-prompt (story 881).
|
||||||
|
crate::db::write_content(
|
||||||
|
&format!("{story_id}:gate_output"),
|
||||||
|
&completion.gate_output,
|
||||||
|
);
|
||||||
|
if let Some(reason) = should_block_story(story_id, config.max_retries, "qa") {
|
||||||
// Story has exceeded retry limit — do not restart.
|
// Story has exceeded retry limit — do not restart.
|
||||||
let _ = self.watcher_tx.send(WatcherEvent::StoryBlocked {
|
let _ = self.watcher_tx.send(WatcherEvent::StoryBlocked {
|
||||||
story_id: story_id.to_string(),
|
story_id: story_id.to_string(),
|
||||||
@@ -341,6 +354,7 @@ impl AgentPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
PipelineStage::Mergemaster => {
|
PipelineStage::Mergemaster => {
|
||||||
// Bug 529: Guard against stale mergemaster advances. If the story
|
// Bug 529: Guard against stale mergemaster advances. If the story
|
||||||
// has already reached done or archived (e.g. a previous mergemaster
|
// has already reached done or archived (e.g. a previous mergemaster
|
||||||
|
|||||||
@@ -26,6 +26,57 @@ use super::super::super::{
|
|||||||
use super::super::AgentPool;
|
use super::super::AgentPool;
|
||||||
use super::super::types::StoryAgent;
|
use super::super::types::StoryAgent;
|
||||||
|
|
||||||
|
/// Maximum bytes of gate output to include in the `--append-system-prompt`
|
||||||
|
/// failure section. The tail is preserved (where errors appear).
|
||||||
|
const GATE_OUTPUT_PROMPT_BYTES: usize = 3_000;
|
||||||
|
|
||||||
|
/// Truncate `output` to at most [`GATE_OUTPUT_PROMPT_BYTES`], keeping the tail.
|
||||||
|
fn truncate_for_system_prompt(output: &str) -> &str {
|
||||||
|
if output.len() <= GATE_OUTPUT_PROMPT_BYTES {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
let start = output.len() - GATE_OUTPUT_PROMPT_BYTES;
|
||||||
|
let mut adjusted = start;
|
||||||
|
while !output.is_char_boundary(adjusted) {
|
||||||
|
adjusted += 1;
|
||||||
|
}
|
||||||
|
&output[adjusted..]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inject a gate-failure section into the `--append-system-prompt` CLI arg.
|
||||||
|
///
|
||||||
|
/// If a `--append-system-prompt` pair already exists, appends to its value.
|
||||||
|
/// Otherwise adds a new `--append-system-prompt <section>` pair.
|
||||||
|
fn inject_gate_failure_section(args: &mut Vec<String>, gate_output: &str) {
|
||||||
|
let section = format!(
|
||||||
|
"Your previous run's quality gates failed:\n{}",
|
||||||
|
truncate_for_system_prompt(gate_output)
|
||||||
|
);
|
||||||
|
if let Some(pos) = args.iter().position(|a| a == "--append-system-prompt")
|
||||||
|
&& let Some(val) = args.get_mut(pos + 1)
|
||||||
|
{
|
||||||
|
val.push_str("\n\n");
|
||||||
|
val.push_str(§ion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
args.push("--append-system-prompt".to_string());
|
||||||
|
args.push(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On retry spawns (retry_count > 0), read the stored gate_output from the DB
|
||||||
|
/// and inject it into `--append-system-prompt` so the agent always sees the
|
||||||
|
/// prior failure context, even when session-resuming (story 881).
|
||||||
|
pub(super) fn maybe_inject_gate_failure(args: &mut Vec<String>, story_id: &str) {
|
||||||
|
let retry_count = crate::crdt_state::read_item(story_id)
|
||||||
|
.and_then(|item| item.retry_count)
|
||||||
|
.unwrap_or(0);
|
||||||
|
if retry_count > 0
|
||||||
|
&& let Some(gate_output) = crate::db::read_content(&format!("{story_id}:gate_output"))
|
||||||
|
{
|
||||||
|
inject_gate_failure_section(args, &gate_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the background worktree-creation + agent-launch flow.
|
/// Run the background worktree-creation + agent-launch flow.
|
||||||
///
|
///
|
||||||
/// Caller (`AgentPool::start_agent`) wraps this in `tokio::spawn` and stores
|
/// Caller (`AgentPool::start_agent`) wraps this in `tokio::spawn` and stores
|
||||||
@@ -131,7 +182,7 @@ pub(super) async fn run_agent_spawn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (command, args, mut prompt) = match config_clone.render_agent_args(
|
let (command, mut args, mut prompt) = match config_clone.render_agent_args(
|
||||||
&wt_path_str,
|
&wt_path_str,
|
||||||
&sid,
|
&sid,
|
||||||
Some(&aname),
|
Some(&aname),
|
||||||
@@ -160,6 +211,10 @@ pub(super) async fn run_agent_spawn(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// On retry spawns (retry_count > 0), inject prior gate failure output into
|
||||||
|
// --append-system-prompt so the agent always sees the failure context (story 881).
|
||||||
|
maybe_inject_gate_failure(&mut args, &sid);
|
||||||
|
|
||||||
// Append project-local prompt content (.huskies/AGENT.md) to the
|
// Append project-local prompt content (.huskies/AGENT.md) to the
|
||||||
// baked-in prompt so every agent role sees project-specific guidance
|
// baked-in prompt so every agent role sees project-specific guidance
|
||||||
// without any config changes. The file is read fresh each spawn;
|
// without any config changes. The file is read fresh each spawn;
|
||||||
@@ -430,3 +485,101 @@ pub(super) async fn run_agent_spawn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// AC1 + AC3 (story 881): when retry_count=1 and gate_output is stored in
|
||||||
|
/// the DB, `maybe_inject_gate_failure` injects a failure section beginning
|
||||||
|
/// `Your previous run's quality gates failed:` into `--append-system-prompt`.
|
||||||
|
#[test]
|
||||||
|
fn gate_failure_injected_into_system_prompt_on_retry() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
|
||||||
|
let story_id = "9960_story_gate_injection_881";
|
||||||
|
crate::db::write_item_with_content(story_id, "2_current", "---\nname: Test\n---\n");
|
||||||
|
crate::crdt_state::set_retry_count(story_id, 1);
|
||||||
|
|
||||||
|
let gate_output =
|
||||||
|
"error[E0308]: mismatched types\n --> src/lib.rs:5:10\n = expected i32, found &str";
|
||||||
|
crate::db::write_content(&format!("{story_id}:gate_output"), gate_output);
|
||||||
|
|
||||||
|
let mut args: Vec<String> = vec!["--verbose".to_string()];
|
||||||
|
maybe_inject_gate_failure(&mut args, story_id);
|
||||||
|
|
||||||
|
let pos = args
|
||||||
|
.iter()
|
||||||
|
.position(|a| a == "--append-system-prompt")
|
||||||
|
.expect("--append-system-prompt must be present after retry injection");
|
||||||
|
let value = &args[pos + 1];
|
||||||
|
assert!(
|
||||||
|
value.starts_with("Your previous run's quality gates failed:"),
|
||||||
|
"--append-system-prompt must begin with the failure marker; got: {value}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value.contains("mismatched types"),
|
||||||
|
"--append-system-prompt must contain a snippet of gate_output; got: {value}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AC2 (story 881): first-attempt spawn (retry_count == 0) must NOT add the
|
||||||
|
/// failure section to `--append-system-prompt`.
|
||||||
|
#[test]
|
||||||
|
fn gate_failure_not_injected_on_first_attempt() {
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
|
||||||
|
let story_id = "9961_story_no_gate_injection_881";
|
||||||
|
crate::db::write_item_with_content(story_id, "2_current", "---\nname: Test\n---\n");
|
||||||
|
// retry_count is 0 (default — never bumped).
|
||||||
|
|
||||||
|
crate::db::write_content(&format!("{story_id}:gate_output"), "some previous output");
|
||||||
|
|
||||||
|
let mut args: Vec<String> = vec!["--verbose".to_string()];
|
||||||
|
maybe_inject_gate_failure(&mut args, story_id);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!args.iter().any(|a| a == "--append-system-prompt"),
|
||||||
|
"no --append-system-prompt should be added when retry_count is 0; args: {args:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Injection appends to an existing `--append-system-prompt` value rather
|
||||||
|
/// than adding a duplicate flag.
|
||||||
|
#[test]
|
||||||
|
fn gate_failure_appends_to_existing_system_prompt() {
|
||||||
|
let gate_output = "test failure output";
|
||||||
|
let mut args = vec![
|
||||||
|
"--append-system-prompt".to_string(),
|
||||||
|
"base prompt".to_string(),
|
||||||
|
];
|
||||||
|
inject_gate_failure_section(&mut args, gate_output);
|
||||||
|
|
||||||
|
// Only one --append-system-prompt flag.
|
||||||
|
let count = args
|
||||||
|
.iter()
|
||||||
|
.filter(|a| a.as_str() == "--append-system-prompt")
|
||||||
|
.count();
|
||||||
|
assert_eq!(count, 1, "must not duplicate --append-system-prompt");
|
||||||
|
|
||||||
|
let pos = args
|
||||||
|
.iter()
|
||||||
|
.position(|a| a == "--append-system-prompt")
|
||||||
|
.unwrap();
|
||||||
|
let value = &args[pos + 1];
|
||||||
|
assert!(
|
||||||
|
value.contains("base prompt"),
|
||||||
|
"original prompt must be preserved"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value.contains("Your previous run's quality gates failed:"),
|
||||||
|
"failure section must be appended"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value.contains("test failure output"),
|
||||||
|
"gate_output must appear in value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user