fix: post-squash compile errors reclassify as semantic merge conflicts

When deterministic-merge produces a clean git squash but the post-squash
compile fails (typical when master gained a Stage payload field after the
feature branch forked — e.g. story 1018 hit `error[E0063]: missing field
plan` after 1010's PlanState landed), the failure is morally a merge
conflict that git's diff3 missed: the conflicting literal lives in a
different file from the type definition that changed on master. Routing
it as GatesFailed left mergemaster idle and the story stuck.

Changes:
- gates.rs GateFailureKind::classify: detect rustc compile errors
  (`error[E\d+]`) as Build instead of falling through to Test. Clippy
  errors (`error[clippy::...]`) still classify as Lint.
- agents/merge/mod.rs: new MergeResult::to_merge_failure_kind() method.
  GateFailure with failure_kind=Build maps to ConflictDetected (so the
  existing 998 subscriber auto-spawns mergemaster). Other gate failures
  stay GatesFailed.
- agents/pool/pipeline/merge/runner.rs: replace the inline match with a
  call to the new method.

Tests: 6 new unit tests covering the classifier branch and every
to_merge_failure_kind arm. All 2932 tests pass.
This commit is contained in:
Timmy
2026-05-14 10:18:33 +01:00
parent e3f5875b8e
commit 8b2ba1c810
3 changed files with 149 additions and 19 deletions
@@ -138,25 +138,7 @@ impl AgentPool {
// On any failure: record merge_failure in CRDT and emit notification.
if !success {
let kind = match &report {
Ok(r) => match &r.result {
crate::agents::merge::MergeResult::NoCommits { .. } => {
crate::pipeline_state::MergeFailureKind::NoCommits
}
crate::agents::merge::MergeResult::Conflict { details, .. } => {
crate::pipeline_state::MergeFailureKind::ConflictDetected(
details.clone(),
)
}
crate::agents::merge::MergeResult::GateFailure { output, .. } => {
crate::pipeline_state::MergeFailureKind::GatesFailed(output.clone())
}
crate::agents::merge::MergeResult::Other { output, .. } => {
crate::pipeline_state::MergeFailureKind::Other(output.clone())
}
crate::agents::merge::MergeResult::Success { .. } => {
unreachable!("success branch is guarded by !success above")
}
},
Ok(r) => r.result.to_merge_failure_kind(),
Err(e) => crate::pipeline_state::MergeFailureKind::Other(e.clone()),
};
let is_no_commits =