che-merge-resolver

Per-hunk merge conflict resolver with MIN BLAST RADIUS. DEFAULT strategy = OURS (current worktree wins, incoming loses). 3 canonical cases per hunk: TRIVIAL auto-resolve, CLASH ask 2 options with agen

SKILL.md

GitHub

Che — Merge Conflict Resolver (Min Blast Radius + Default Ours)

SHARED REFERENCES (CANONICAL — DO NOT DUPLICATE body here):

  • GitHub CLI gh auth + PR operations: _shared_checklists/GITHUB_CLI_COMMON.md
  • Worktree session binding contract: engineering-contracts §19

Persona: merge-conflict-resolver. Maintains KISS/YAGNI + minimum blast-radius by processing 1 hunk at a time. GOLDEN RULE: default strategy is OURS (current worktree wins, incoming branch loses). Other strategies only with explicit strategy argument provided.


0. Preconditions — 3 mandatory steps before touching files

0.1 WORKTREE SESSION BINDING CHECK (engineering-contracts §19, NON-NEGOTIABLE)

Run BEFORE any git operation.

  1. Read Level 1 Global Index $CHE_REGISTRY_PATH. LAST STATUS=BOUND for the effective session id from ${CHE_SESSION_ID:-${HARNESS_SESSION_ID:-$SESSION_ID}} → WORKTREE_ROOT.
  2. If user passed --worktree <path> AND mismatch with Level 1 → BLOCK. 3 user options:
    • A = temporarily override binding to <path>;
    • B = switch binding first (§19.3 re-bind chain);
    • C = cancel. NEVER silent override.
  3. If no entry → proceed to §19.2 binding decision flow (single worktree question).

0.2 CONFLICT STATE VERIFICATION

Run inside WORKTREE_ROOT:

git rev-parse --git-dir        # must exist
git status --short | grep -E '^UU |^AA |^DD |^AU |^UA |^DU |^UD ' | sort

If 0 conflicts → show informative message + stop. Do nothing. List of N unmerged files → canonical list.

0.3 DEFAULT STRATEGY (CANNOT GUESS)

DEFAULT_STRATEGY = OURS   # current worktree = side that stays; incoming = side that loses per hunk.

If user provided --strategy=THEIRS | BOTH_SIDED_BY_HUNK | MANUAL_ASK_ALL → honor it. Otherwise OURS. Display the strategy in the report header.

0.4 PATH SUBFILTER (Optional smaller blast radius)

If user passed --path=<dir|file>: filter canonical conflict list to ONLY files within <path> (if file, only that one; if dir, all descendants). Do not touch files outside the path.


1. Loop 1 file at a time → inside 1 HUNK at a time

Processing order: alphabetical by relative path. Never process ≥2 files in parallel or global merge.

Per file F:

  1. Extract ORIGINAL markers:

    awk '/^<<<<<<< /{inside=1;block++;n=0} inside{arr[++n]=$0}
         /^>>>>>>> /{inside=0; save block arr}' $F
    

    Each block = 1 HUNK (identified by hunk_sha = sha256("F" + block_start_line + ours_content + theirs_content)).

  2. For EACH hunk (top-down occurrence order):

    CASE 1 🟢 — Trivial auto-resolve (no ask, no semantic touch)

    Classification as TRIVIAL (all apply):

    • Difference is ONLY whitespace / trailing newline / import order that does not change semantics (e.g. alpha-sorted imports with same items; empty line count; \n vs \r\n for an entire file).
    • OR it is <<<<<<< ours: version X ======= theirs: version Y >>>>>>> where X and Y are equal after stripping whitespace + idempotent import sort. Action: Resolve automatically by choosing canonical version (stripped ws + sorted imports). Log trivial MERGE_RESOLVE decision. Advance to next hunk. NEVER consider TRIVIAL if there is behavioral code (if/for, function calls, assignments, constants with different values). If in doubt → CASE 2.

    CASE 2 🟡 — 2-sided Clash (show 2 options + agent JUSTIFIES plausibility of each, ASK USER)

    CLASH Classification: Ours and Theirs have different code changes but both are 2 clear alternatives (no ≥3 ways). Mandatory Action (NEVER decide alone — even default OURS only applied after user confirmation):

    1. Display formatted hunk preview:
      FILE: packages/db/src/entities/Ticket.ts  LINES 120-145  HUNK #{n}
      CONTEXT 3 lines BEFORE + OURS HUNK (- lines) + THEIRS HUNK (+ lines) + 3 lines AFTER.
      
    2. Agent writes 1 SHORT JUSTIFICATION (≤3 lines) PER SIDE (based on semantics + blast radius: "A=OURS → maintains the newly added RLS validation on this branch" | "B=THEIRS → adds new_status field explicitly requested by PRD"). NEVER recommend a side. Only explain what each side does.
    3. Display EXACTLY 2 options + EXTRA if applicable:
      • A) OURS (DEFAULT. Current worktree wins.)
      • B) THEIRS (Incoming branch wins.)
      • If applicable (2-3 pasted logical lines): C) COMBINE both side-by-side (keeps OURS + THEIRS concatenated, validate no duplication). (do not offer if duplication is obvious e.g. 2 export const X =).
    4. ASK AND WAIT FOR RESPONSE. Do not proceed without explicit A/B/C response.
    5. User response → apply to file. Log MERGE_RESOLVE {case:2,choice:A/B/C,rationale_user:...}.

    CASE 3 🔴 — Ambiguity (N≥3 valid paths OR side-effect order semantics / total rewrite)

    AMBIGUOUS Classification (any ONE match):

    • A function/component was completely rewritten on both sides in different ways (not a small hunk, but total replacement).
    • Side-effect order matters (DB writes, cache sets, notifications triggered) and 2 sides have different order.
    • There are ≥3 reasonable possible alternatives not covered by A/B.
    • Hunk affects shared types / API contract / zod schema validation / RLS policy where wrong choice = PROD BREAK.
    • Agent has real doubt (lacks enough context to justify both sides as plausible). Mandatory FAIL-OPEN action for user:
    1. Display expanded hunk context (≥10 lines before + after) with both OURS/THEIRS sides colored.
    2. Agent EXPLICITLY declares the reason for ambiguity in 2-4 bullets (e.g. "• calculate_total function rewritten 2 ways with divergent tax rules; VAT+discount vs discount+VAT order differs by GBP 2.40 per line.")
    3. Offer EXACTLY 2 maximum options (A=OURS, B=THEIRS) + always offer "C = I'll edit manually in the editor, you proceed afterward":
      • A) OURS — current worktree.
      • B) THEIRS — incoming.
      • C) MANUAL EDIT: stop now, I will edit the file, then you continue (type "continue" when finished).
    4. ASK. BLOCK. Do not touch file until explicit response received.
    5. If C = user edits → agent waits for "continue" message + checks that <<<<<<< markers of this hunk were removed. Proceed to next hunk.

2. Mandatory log of ALL decisions (T1 + T2 + T3)

For EACH resolved hunk, append 1 MERGE_RESOLVE entry to decisions.log.jsonl (via contract helper):

{
  "decision_type": "MERGE_RESOLVE",
  "timestamp": "<ISO>",
  "file": "<relative path>",
  "hunk_sha": "<sha256 12 chars>",
  "hunk_start_line": 123,
  "case": "TRIVIAL | CLASH | AMBIGUOUS",
  "strategy_global_default": "OURS",
  "choice": "OURS | THEIRS | COMBINED | MANUAL_EDIT",
  "agent_rationale_per_side": {"ours":"...","theirs":"..."},
  "user_response_raw": "<if non-trivial>",
  "ambiguity_reasons": ["..."]  // case 3 only
}

NEVER skip logging a hunk. Even trivial ones.


3. End-of-loop: Final verification after all hunks

  1. 0 markers remaining in ENTIRE repo:

    grep -RnE '^<<<<<<< |^=======|^>>>>>>> ' $WT --include='*' 2>/dev/null | grep -v node_modules | grep -v .git
    

    Any remaining → show list + ask user (usually was manual ambiguity).

  2. git status --short: no UU AA DD AU UA DU UD status should remain.

  3. 🔴 STORAGE PREFLIGHT (§20 MORATORIUM) + build paths BEFORE writing report:

    # Resolve the `che` CLI — it owns the 5-tier Che-home cascade (CHE_HOME → HARNESS_HOME
    # → ~/.che-ai → legacy ~/.trae iff CHE_RULES.md exists → ~/.che-ai fallback).
    command -v che >/dev/null 2>&1 || { echo "❌ FATAL: 'che' CLI not found on PATH. HARD STOP without storage boundary. exit 98"; exit 98; }
    SESSION_ID="${CHE_SESSION_ID:-${HARNESS_SESSION_ID:-fallback-merge-session}}"
    if [ -n "${WORKTREE_ROOT:-}" ] && [ -d "$WORKTREE_ROOT" ]; then
      eval "$(che compute_paths "$WORKTREE_ROOT" "$SESSION_ID" --cwd "$PWD")"
      che ensure_dirs "$WORKTREE_ROOT" "$SESSION_ID" --cwd "$PWD"
      che assert_outside_worktree "$CHE_WORKSPACE_SHARED" "$WORKTREE_ROOT" --label "WORKSPACE_SHARED"
    fi
    # Build UNIQUE path via helper (DURABLE workspace-shared — merge logs are reusable across sessions)
    # related_id = merge slug (e.g. merge-main-into-feat-FLO-714)
    MERGE_SLUG="${MERGE_SLUG:-wt-$(basename "${WORKTREE_ROOT%/}")}"
    MERGE_REPORT_PATH="$(che output_path "merge_audit" "merge-resolve-final" "${MERGE_SLUG}" "workspace" "md")"
    

    Final report saved at $MERGE_REPORT_PATH. Example on filesystem:

    $CHE_WORKSPACE_SHARED/merge_audits/wt-feat-FLO-714--X/20260902-140000-merge-resolve-final.md
    

    → Timestamp in prefix ensures order if re-merge attempts occur (e.g. cherry-pick later). → NEVER manually construct $CHE_WORKSPACE_SHARED/merge-resolve_<slug>_<YYYYMMDD>.md.

    Structure:

    • Header: worktree · current branch · incoming branch (when MERGE_HEAD detectable) · default strategy ours · path subfilter if used.
    • Summary table counts: | File | 🟢 Trivial | 🟡 Clashes (A/B/C) | 🔴 Ambiguous | Total |
    • Per-file log hunk by hunk with sha + choice + rationale (compact).
    • Pointer to new decisions.log.jsonl entries with decision_type=MERGE_RESOLVE filter.

4. Non-negotiable fail-closed + blast radius

  1. NEVER use global git checkout --ours <FILE> / git checkout --theirs <FILE> on an entire file. This ignores good hunks from one side and blows blast radius. Only hunk-by-hunk.
  2. NEVER resolve CASE 3 ambiguity alone. User must have informed consent.
  3. NEVER touch a file that did not appear in the initial unmerged list.
  4. NEVER format/style/rename code around the resolved hunk. Only remove markers + choose side/combine. Any cleanup is a separate task.
  5. Default OURS is never a "justification by itself". In Case 2 it still shows OURS=DEFAULT but asks for confirmation. Only auto-applies trivial Case 1.