mr-watch: detect branch/main conflicts and route to escalate

🔍 Problem

The mr-watch skill (.claude/skills/mr-watch/) watches an MR for reviewer feedback and CI status, but is blind to the branch falling into merge conflict with the target. When main advances and the branch conflicts, the watcher keeps reporting quiet/clean ticks while the MR is unmergeable — the conflict only surfaces when a human notices.

Hit live on !400 (merged): main advanced 20 commits (including a Config proto change), the branch developed a conflict in config.proto / config.pb.go, and nothing in the watch loop flagged it.

Proposed change — detect, then escalate

Add conflict detection to the per-iteration flow and route a positive detection to the existing escalation path. Do not add autonomous conflict resolution.

Detection (safe, cheap)

  • After the exit-gate check, read has_conflicts and detailed_merge_status from the MR payload (glab api .../merge_requests/<iid>) — fields already present, no extra API calls.
  • Surface a Conflicts row in all three report templates (assets/reports/{quiet,action,stop}.md) so even a quiet tick shows "branch conflicts with main."

Escalation (when has_conflicts == true)

  • Post a top-level bot note (or AskUserQuestion in interactive runs) naming the conflicting file set; do not push.
  • Mirror the existing escalate: / ambiguous-blocking: philosophy: stop, summarize, hand to the human.
  • Skip when the watcher has already self-terminated on approvals_left == 0.

Why not autonomous resolution

Conflict resolution is a higher risk class than anything the skill does today (emoji react, threaded reply, single scoped fix):

  1. Semantic merge, not mechanical. The real hazard is the files that do not show conflict markers — a struct both branches edited that auto-merges cleanly but silently drops a change, still compiling and passing tests. Needs judgment, not a rule. On !400 (merged), config.go was exactly this: a shared Config struct that auto-merged, safe only because main's jobs/redis work was proto-only — which required manual verification to confirm.
  2. Resets approvals. Any resolution pushes commits, which revokes approvals. An auto-resolver firing on every main advance would repeatedly invalidate reviews — the opposite of the skill's "keep the MR signal legible" goal.
  3. Generated files need regeneration, not merge. e.g. *.pb.go must be produced by buf generate, not merge-resolved. A generic resolver does not know the per-file regen command.

If autonomous resolution is ever added, gate it behind a tightly-scoped allowlist (a generated file with a declared regen command; a lockfile with a documented strategy). Anything touching hand-written logic → escalate.

🛠️ Implementation pointers

  • Per-iteration flow: add the conflict check after the exit-gate step in .claude/skills/mr-watch/SKILL.md.
  • Templates: add a Conflicts row to .claude/skills/mr-watch/assets/reports/{quiet,action,stop}.md.
  • Commit prefix: chore(skills): (skills are tooling, not product features).
  • Branch from current main: the skill's request-rereview.sh recently took a fix there (6508940).

Acceptance criteria

  • Each iteration reads has_conflicts / detailed_merge_status and reports it.
  • A Conflicts row is present in all three report templates.
  • has_conflicts == true routes to escalation (bot note / AskUserQuestion naming the conflicting files), with no push.
  • Escalation is skipped after self-termination on approvals_left == 0.
  • No autonomous resolution is added — or, if a scoped allowlist class is added, it is documented and tested.