huskies: merge 981

This commit is contained in:
dave
2026-05-13 13:54:27 +00:00
parent 51aa649ce4
commit e9a7468d8a
4 changed files with 194 additions and 2 deletions
@@ -1,9 +1,28 @@
//! Merge pipeline runner — start_merge_agent_work and run_merge_pipeline.
use crate::slog;
use crate::slog_error;
use crate::worktree;
use std::path::Path;
use std::sync::Arc;
/// Return `true` when `gate_output` matches a self-evident-fix class of failure
/// that a short fixup coder session can resolve without human intervention.
///
/// Patterns covered: fmt drift (`cargo fmt --check`), clippy warnings promoted
/// to errors (`-D warnings`), and missing doc comments detected by clippy or
/// the source-map-check gate.
fn is_self_evident_fix(gate_output: &str) -> bool {
let patterns: &[&str] = &[
"Diff in ", // cargo fmt --check output
"would reformat", // rustfmt --check output
"error[clippy::", // clippy error
"warning[clippy::", // clippy warning (treated as error via -D warnings)
"missing_doc_comments", // clippy missing-doc lint
"missing-docs direction", // source-map-check gate
];
patterns.iter().any(|p| gate_output.contains(p))
}
use super::super::super::AgentPool;
use super::time::{
decode_server_start_time, encode_server_start_time, server_start_time, unix_now,
@@ -114,9 +133,18 @@ impl AgentPool {
Err(e) => e.clone(),
};
let is_no_commits = reason.contains("no commits to merge");
// Self-evident fix: gate-only failure (no conflicts) whose output matches
// a pattern a fixup coder can resolve in one short session (story 981).
let gate_output = match &report {
Ok(r) if !r.had_conflicts => r.gate_output.clone(),
_ => String::new(),
};
let is_fixup =
!is_no_commits && !gate_output.is_empty() && is_self_evident_fix(&gate_output);
if is_no_commits {
if let Err(e) = crate::agents::lifecycle::transition_to_blocked(&sid, &reason) {
crate::slog_error!("[merge] Failed to transition '{sid}' to Blocked: {e}");
slog_error!("[merge] Failed to transition '{sid}' to Blocked: {e}");
}
let _ = pool
.watcher_tx
@@ -124,6 +152,50 @@ impl AgentPool {
story_id: sid.clone(),
reason,
});
} else if is_fixup {
// Save gate output and mark fixup pending before any state transition
// so that a concurrent auto-assign that fires after the state change
// sees the keys already set.
crate::db::write_content(crate::db::ContentKey::GateOutput(&sid), &gate_output);
crate::db::write_content(crate::db::ContentKey::MergeFixupPending(&sid), "1");
// Merge → MergeFailure → Coding. FixupRequested also sets
// retry_count=1 so maybe_inject_gate_failure injects the gate
// output into --append-system-prompt on the fixup spawn.
let _ = crate::agents::lifecycle::transition_to_merge_failure(&sid, &reason);
match crate::agents::lifecycle::move_story_to_stage(&sid, "current") {
Ok(_) => {
slog!(
"[merge] Self-evident gate fix for '{sid}'; spawning fixup coder"
);
let context = "\n\nYour task is to fix the merge gate failures \
shown above (see --append-system-prompt). \
Run run_tests then commit. Do not explore further.";
if let Err(e) = pool
.start_agent(&root, &sid, None, Some(context), None)
.await
{
slog_error!(
"[merge] Fixup coder spawn failed for '{sid}': {e} \
(auto-assign will retry when a slot opens)"
);
}
}
Err(e) => {
slog_error!(
"[merge] Failed to move '{sid}' back to current for fixup: {e}; \
reverting to MergeFailure"
);
crate::db::delete_content(crate::db::ContentKey::MergeFixupPending(
&sid,
));
let _ = pool.watcher_tx.send(
crate::io::watcher::WatcherEvent::MergeFailure {
story_id: sid.clone(),
reason,
},
);
}
}
} else {
// Transition through the state machine (Merge → MergeFailure).
// Only send the notification when the stage actually changed; if the
@@ -136,7 +208,7 @@ impl AgentPool {
crate::pipeline_state::Stage::MergeFailure { .. }
),
Err(e) => {
crate::slog_error!(
slog_error!(
"[merge] Failed to transition '{sid}' to MergeFailure: {e}"
);
true