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.mdskills/che-merge-resolver/SKILL.md
GitHubChe — 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.
- 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. - 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.
- A = temporarily override binding to
- 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:
Extract ORIGINAL markers:
awk '/^<<<<<<< /{inside=1;block++;n=0} inside{arr[++n]=$0} /^>>>>>>> /{inside=0; save block arr}' $FEach block = 1 HUNK (identified by
hunk_sha = sha256("F" + block_start_line + ours_content + theirs_content)).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;
\nvs\r\nfor 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):
- 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. - 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.
- 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. 2export const X =).
- ASK AND WAIT FOR RESPONSE. Do not proceed without explicit A/B/C response.
- 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:
- Display expanded hunk context (≥10 lines before + after) with both OURS/THEIRS sides colored.
- 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.")
- 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).
- ASK. BLOCK. Do not touch file until explicit response received.
- If C = user edits → agent waits for "continue" message + checks that
<<<<<<<markers of this hunk were removed. Proceed to next hunk.
- Difference is ONLY whitespace / trailing newline / import order that does not change semantics (e.g. alpha-sorted imports with same items; empty line count;
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
0 markers remaining in ENTIRE repo:
grep -RnE '^<<<<<<< |^=======|^>>>>>>> ' $WT --include='*' 2>/dev/null | grep -v node_modules | grep -v .gitAny remaining → show list + ask user (usually was manual ambiguity).
git status --short: noUU AA DD AU UA DU UDstatus should remain.🔴 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_HEADdetectable) · 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.jsonlentries withdecision_type=MERGE_RESOLVEfilter.
- Header: worktree · current branch · incoming branch (when
4. Non-negotiable fail-closed + blast radius
- 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. - NEVER resolve CASE 3 ambiguity alone. User must have informed consent.
- NEVER touch a file that did not appear in the initial unmerged list.
- NEVER format/style/rename code around the resolved hunk. Only remove markers + choose side/combine. Any cleanup is a separate task.
- 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.