huskies: merge 982

This commit is contained in:
dave
2026-05-13 15:30:03 +00:00
parent e6d051d016
commit 91fbad568a
15 changed files with 357 additions and 117 deletions
+31 -25
View File
@@ -129,32 +129,35 @@ impl AgentPool {
// On any failure: record merge_failure in CRDT and emit notification.
if !success {
let reason = match &report {
let kind = match &report {
Ok(r) => {
if r.had_conflicts {
format!(
"Merge conflict: {}",
r.conflict_details
.as_deref()
.unwrap_or("conflicts detected")
crate::pipeline_state::MergeFailureKind::ConflictDetected(
r.conflict_details.clone(),
)
} else {
format!("Quality gates failed: {}", r.gate_output)
crate::pipeline_state::MergeFailureKind::GatesFailed(
r.gate_output.clone(),
)
}
}
Err(e) => e.clone(),
Err(e) => crate::pipeline_state::MergeFailureKind::Other(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_no_commits = matches!(
&kind,
crate::pipeline_state::MergeFailureKind::Other(r) if r.contains("no commits to merge")
);
// Self-evident fix: gate-only failure whose output matches a pattern
// a fixup coder can resolve in one short session (story 981).
let fixup_output = match &kind {
crate::pipeline_state::MergeFailureKind::GatesFailed(o) => o.as_str(),
_ => "",
};
let is_fixup =
!is_no_commits && !gate_output.is_empty() && is_self_evident_fix(&gate_output);
!is_no_commits && !fixup_output.is_empty() && is_self_evident_fix(fixup_output);
if is_no_commits {
let reason = kind.display_reason();
if let Err(e) = crate::agents::lifecycle::transition_to_blocked(&sid, &reason) {
slog_error!("[merge] Failed to transition '{sid}' to Blocked: {e}");
}
@@ -165,15 +168,16 @@ impl AgentPool {
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);
// Mark fixup pending before any state transition so a concurrent
// auto-assign that fires after the state change sees the key set.
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);
// retry_count=1 so maybe_inject_gate_failure injects gate output
// into --append-system-prompt on the fixup spawn.
// transition_to_merge_failure also writes ContentKey::GateOutput.
let display = kind.display_reason();
let _ =
crate::agents::lifecycle::transition_to_merge_failure(sid.as_str(), kind);
match crate::agents::lifecycle::move_story_to_stage(&sid, "current") {
Ok(_) => {
slog!(
@@ -203,7 +207,7 @@ impl AgentPool {
let _ = pool.watcher_tx.send(
crate::io::watcher::WatcherEvent::MergeFailure {
story_id: sid.clone(),
reason,
reason: display,
},
);
}
@@ -212,8 +216,10 @@ impl AgentPool {
// Transition through the state machine (Merge → MergeFailure).
// Only send the notification when the stage actually changed; if the
// story was already in MergeFailure (self-loop), suppress the duplicate.
let display = kind.display_reason();
let should_notify = match crate::agents::lifecycle::transition_to_merge_failure(
&sid, &reason,
sid.as_str(),
kind,
) {
Ok(fired) => !matches!(
fired.before,
@@ -231,7 +237,7 @@ impl AgentPool {
pool.watcher_tx
.send(crate::io::watcher::WatcherEvent::MergeFailure {
story_id: sid.clone(),
reason,
reason: display,
});
}
}