From c228ae16407ed3038ec1066bb628f75e46c43291 Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 13 May 2026 09:03:25 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20has=5Fcontent=5Fconflict=5Ffailure=20rea?= =?UTF-8?q?ds=20wrong=20CRDT=20key=20=E2=80=94=20auto-spawn=20mergemaster?= =?UTF-8?q?=20never=20fires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function was calling `read_content(story_id)`, which returns the story's *description* text (e.g. "Bug: Coder exits code 0 with uncommitted work — force a commit-only respawn..."). It then scanned that for "Merge conflict" / "CONFLICT (content):", which obviously never matched, so the auto-spawn-mergemaster-on-content-conflict guard in `pool/auto_assign/merge.rs` always saw `false` and skipped. The actual gate output (where the merge runner stores the failure message including conflict markers) lives at `format!("{story_id}:gate_output")` — that's the key `pipeline/advance/mod.rs:207` writes to. Read from there instead. Witnessed: 954's merge hit a real `CONFLICT (content)` in tests_regression.rs at 08:57:40, no mergemaster spawned, story stayed in MergeFailure. Co-Authored-By: Claude Opus 4.7 (1M context) --- server/src/agents/pool/auto_assign/story_checks.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/agents/pool/auto_assign/story_checks.rs b/server/src/agents/pool/auto_assign/story_checks.rs index e2b106c0..fbf4e0b2 100644 --- a/server/src/agents/pool/auto_assign/story_checks.rs +++ b/server/src/agents/pool/auto_assign/story_checks.rs @@ -72,9 +72,11 @@ pub(super) fn has_content_conflict_failure( if !is_merge_failure { return false; } - // The projection does not carry the reason string; read the raw content - // from the CRDT content store and scan for conflict markers. - crate::db::read_content(story_id) + // The projection does not carry the reason string; read the gate output + // (where the merge runner persists the failure message) and scan for + // conflict markers. NB: the key is `{story_id}:gate_output`, not `{story_id}` + // — the latter is the story's *description* text and would never match. + crate::db::read_content(&format!("{story_id}:gate_output")) .map(|content| { content.contains("Merge conflict") || content.contains("CONFLICT (content):") })