Skip to content

Commit 6d242b4

Browse files
authored
feat(skill): add /resolve skill for context-aware conflict resolution (#1524)
* feat(skill): add /resolve skill for context-aware conflict resolution * fix(skill/resolve): dynamic repo detection, safe temp names, merge guard, lint portability (#1524)
1 parent 0f7d3a0 commit 6d242b4

1 file changed

Lines changed: 97 additions & 22 deletions

File tree

.claude/skills/resolve/SKILL.md

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,27 @@ if ! echo "$PR_NUMBER" | grep -qE '^[0-9]+$'; then
4848
exit 1
4949
fi
5050

51+
# Guard against a previously aborted merge left in place — if MERGE_HEAD exists but
52+
# .codegraph/resolve/ was already cleaned, the next `git merge` call would fail immediately.
53+
if git rev-parse --verify MERGE_HEAD > /dev/null 2>&1; then
54+
echo "ERROR: An in-progress merge (MERGE_HEAD) already exists."
55+
echo "Run: git merge --abort && rm -rf .codegraph/resolve"
56+
echo "Then re-run /resolve $PR_NUMBER"
57+
exit 1
58+
fi
59+
60+
# Detect repo slug dynamically so the skill works in any fork or renamed org
61+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null \
62+
|| git remote get-url origin | sed -E 's|.*github\.com[:/](.+)(\.git)?$|\1|')
63+
if [ -z "$REPO" ]; then
64+
echo "ERROR: could not detect GitHub repo slug — ensure 'gh' is authenticated or 'origin' points to GitHub"
65+
exit 1
66+
fi
67+
echo "Detected repo: $REPO"
68+
5169
# Verify PR exists and is open
52-
gh pr view "$PR_NUMBER" --repo optave/codegraph --json number,state,headRefName,baseRefName \
70+
gh pr view "$PR_NUMBER" --repo "$REPO" --json number,state,headRefName,baseRefName \
71+
5372
--jq '"PR #\(.number) [\(.state)] \(.headRefName) → \(.baseRefName)"' \
5473
|| { echo "ERROR: PR #$PR_NUMBER not found or inaccessible"; exit 1; }
5574
```
@@ -58,18 +77,36 @@ Persist PR metadata for use in later phases (shell variables don't survive acros
5877

5978
```bash
6079
PR_NUMBER=$(echo "${ARGUMENTS:-}" | tr -d '[:space:]#')
80+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null \
81+
|| git remote get-url origin | sed -E 's|.*github\.com[:/](.+)(\.git)?$|\1|')
6182
mkdir -p .codegraph/resolve
62-
gh pr view "$PR_NUMBER" --repo optave/codegraph \
83+
# Trap ensures .codegraph/resolve/ is always cleaned up on non-zero exit, even after a crash.
84+
# This prevents stale state from corrupting a future re-run (Phase 1 reads head-branch / base-branch
85+
# from here, and an aborted merge would see stale values).
86+
trap 'rm -rf .codegraph/resolve' ERR
87+
echo "$REPO" > .codegraph/resolve/repo
88+
gh pr view "$PR_NUMBER" --repo "$REPO" \
89+
6390
--json number,headRefName,baseRefName,title,body \
6491
> .codegraph/resolve/pr-meta.json \
6592
|| { echo "ERROR: failed to fetch PR #$PR_NUMBER metadata"; exit 1; }
6693
jq -r '.headRefName' .codegraph/resolve/pr-meta.json > .codegraph/resolve/head-branch
6794
jq -r '.baseRefName' .codegraph/resolve/pr-meta.json > .codegraph/resolve/base-branch
6895
echo "$PR_NUMBER" > .codegraph/resolve/pr-number
96+
97+
HEAD_BRANCH=$(cat .codegraph/resolve/head-branch)
98+
BASE_BRANCH=$(cat .codegraph/resolve/base-branch)
99+
if [ "$HEAD_BRANCH" = "$BASE_BRANCH" ]; then
100+
echo "ERROR: PR head and base are the same branch ($HEAD_BRANCH) — nothing to merge"
101+
rm -rf .codegraph/resolve
102+
exit 1
103+
fi
104+
69105
echo "Pre-flight passed. PR: $(jq -r '.title' .codegraph/resolve/pr-meta.json)"
70106
```
71107

72-
**Exit condition:** Git repo confirmed, `gh` and `jq` available, PR exists, metadata written to `.codegraph/resolve/`.
108+
**Exit condition:** Git repo confirmed, `gh` and `jq` available, repo slug detected, PR exists, metadata written to `.codegraph/resolve/`.
109+
73110

74111
---
75112

@@ -81,9 +118,11 @@ Check out the PR branch and run `git merge` to surface all conflicts.
81118
HEAD_BRANCH=$(cat .codegraph/resolve/head-branch)
82119
BASE_BRANCH=$(cat .codegraph/resolve/base-branch)
83120
PR_NUMBER=$(cat .codegraph/resolve/pr-number)
121+
REPO=$(cat .codegraph/resolve/repo)
84122

85123
echo "Checking out PR branch: $HEAD_BRANCH"
86-
gh pr checkout "$PR_NUMBER" --repo optave/codegraph \
124+
gh pr checkout "$PR_NUMBER" --repo "$REPO" \
125+
87126
|| { echo "ERROR: failed to check out PR #$PR_NUMBER"; exit 1; }
88127

89128
echo "Fetching latest origin/$BASE_BRANCH..."
@@ -141,6 +180,13 @@ For each conflicting file, identify **which commits and PRs on the base branch**
141180
BASE_BRANCH=$(cat .codegraph/resolve/base-branch)
142181
ORIG_HEAD=$(cat .codegraph/resolve/orig-head)
143182
MERGE_HEAD=$(cat .codegraph/resolve/merge-head)
183+
REPO=$(cat .codegraph/resolve/repo)
184+
185+
# file_key: collision-resistant short name derived from file path
186+
# Uses printf+sha1sum to handle paths with underscores (src/foo_bar.ts and src/foo/bar.ts
187+
# both become different keys, unlike tr '/' '_')
188+
file_key() { printf '%s' "$1" | sha1sum | cut -c1-16; }
189+
144190

145191
# Find all commits that came in from the base branch and touched conflicting files
146192
mkdir -p .codegraph/resolve/incoming-prs
@@ -151,28 +197,36 @@ i=0
151197
while IFS= read -r FILE; do
152198
i=$((i + 1))
153199
echo "[$i/$TOTAL] Tracing incoming changes: $FILE"
200+
KEY=$(file_key "$FILE")
201+
154202

155203
# Commits from origin/<base> that are not yet in the PR branch (what came in from main)
156204
# 2>/dev/null: suppress git's "unknown revision" if ORIG_HEAD or MERGE_HEAD are temporarily unavailable — the empty file case is handled below
157205
git log --oneline "$ORIG_HEAD..$MERGE_HEAD" -- "$FILE" \
158-
> ".codegraph/resolve/incoming-commits-$(echo "$FILE" | tr '/' '_').txt" 2>/dev/null || true
206+
> ".codegraph/resolve/incoming-commits-$KEY.txt" 2>/dev/null || true
159207

160-
if [ ! -s ".codegraph/resolve/incoming-commits-$(echo "$FILE" | tr '/' '_').txt" ]; then
208+
if [ ! -s ".codegraph/resolve/incoming-commits-$KEY.txt" ]; then
161209
echo " (no direct commit history for $FILE on incoming side — likely an add/add conflict)"
162210
else
163211
echo " Incoming commits touching $FILE:"
164-
cat ".codegraph/resolve/incoming-commits-$(echo "$FILE" | tr '/' '_').txt"
212+
cat ".codegraph/resolve/incoming-commits-$KEY.txt"
213+
165214
fi
166215
done < .codegraph/resolve/conflicting-files.txt
167216
```
168217

169218
For each incoming commit, find the PR that introduced it:
170219

171220
```bash
221+
REPO=$(cat .codegraph/resolve/repo)
172222
mkdir -p .codegraph/resolve/source-prs
173223

224+
file_key() { printf '%s' "$1" | sha1sum | cut -c1-16; }
225+
174226
while IFS= read -r FILE; do
175-
COMMIT_FILE=".codegraph/resolve/incoming-commits-$(echo "$FILE" | tr '/' '_').txt"
227+
KEY=$(file_key "$FILE")
228+
COMMIT_FILE=".codegraph/resolve/incoming-commits-$KEY.txt"
229+
176230
[ -f "$COMMIT_FILE" ] || continue
177231

178232
while IFS= read -r COMMIT_LINE; do
@@ -188,7 +242,8 @@ while IFS= read -r FILE; do
188242
echo "Looking up PR for commit $SHA..."
189243
# gh api returns empty array if commit is not associated with a PR
190244
# 2>/dev/null: suppress gh's "HTTP 422" or network error output — the || clause writes an empty array so later steps handle it gracefully
191-
gh api "repos/optave/codegraph/commits/$SHA/pulls" \
245+
gh api "repos/$REPO/commits/$SHA/pulls" \
246+
192247
--jq '[.[] | {number: .number, title: .title, body: .body, state: .state}]' \
193248
> "$PR_FILE" 2>/dev/null || echo '[]' > "$PR_FILE"
194249

@@ -205,6 +260,8 @@ done < .codegraph/resolve/conflicting-files.txt
205260
Fetch full descriptions and diffs for each source PR:
206261

207262
```bash
263+
REPO=$(cat .codegraph/resolve/repo)
264+
208265
mkdir -p .codegraph/resolve/source-pr-diffs
209266

210267
for PR_FILE in .codegraph/resolve/source-prs/*.json; do
@@ -220,7 +277,8 @@ for PR_FILE in .codegraph/resolve/source-prs/*.json; do
220277
if [ ! -f "$META_FILE" ]; then
221278
echo "Fetching source PR #$SOURCE_PR description..."
222279
# 2>/dev/null: suppress gh's auth/network error text — the || clause writes a stub so the skill continues gracefully
223-
gh pr view "$SOURCE_PR" --repo optave/codegraph \
280+
gh pr view "$SOURCE_PR" --repo "$REPO" \
281+
224282
--json number,title,body,baseRefName,headRefName \
225283
> "$META_FILE" 2>/dev/null \
226284
|| { echo "WARN: could not fetch PR #$SOURCE_PR metadata — skipping"; echo '{}' > "$META_FILE"; }
@@ -229,7 +287,8 @@ for PR_FILE in .codegraph/resolve/source-prs/*.json; do
229287
if [ ! -f "$DIFF_FILE" ]; then
230288
echo "Fetching source PR #$SOURCE_PR diff..."
231289
# 2>/dev/null: suppress gh's auth/network error text — the || clause creates an empty file so the skill continues gracefully
232-
gh pr diff "$SOURCE_PR" --repo optave/codegraph \
290+
gh pr diff "$SOURCE_PR" --repo "$REPO" \
291+
233292
> "$DIFF_FILE" 2>/dev/null \
234293
|| { echo "WARN: could not fetch PR #$SOURCE_PR diff — skipping"; touch "$DIFF_FILE"; }
235294
fi
@@ -309,6 +368,9 @@ MERGE_HEAD=$(cat .codegraph/resolve/merge-head)
309368
TOTAL=$(wc -l < .codegraph/resolve/conflicting-files.txt | tr -d '[:space:]')
310369
i=0
311370

371+
file_key() { printf '%s' "$1" | sha1sum | cut -c1-16; }
372+
373+
312374
while IFS= read -r FILE; do
313375
i=$((i + 1))
314376
echo "=== [$i/$TOTAL] Resolving: $FILE ==="
@@ -329,7 +391,9 @@ while IFS= read -r FILE; do
329391
echo ""
330392
echo "--- Source PR context for $FILE ---"
331393
# Show which source PRs touched this file (from Phase: Conflict Archaeology)
332-
COMMIT_FILE=".codegraph/resolve/incoming-commits-$(echo "$FILE" | tr '/' '_').txt"
394+
KEY=$(file_key "$FILE")
395+
COMMIT_FILE=".codegraph/resolve/incoming-commits-$KEY.txt"
396+
333397
if [ -s "$COMMIT_FILE" ]; then
334398
while IFS= read -r COMMIT_LINE; do
335399
SHA=$(echo "$COMMIT_LINE" | awk '{print $1}')
@@ -435,31 +499,41 @@ Run tests and lint to confirm the resolution didn't break anything.
435499

436500
```bash
437501
echo "=== Detecting test runner ==="
438-
if [ -f "pnpm-lock.yaml" ]; then TEST_CMD="pnpm test"
502+
if [ -f "pnpm-lock.yaml" ] && command -v pnpm > /dev/null 2>&1; then TEST_CMD="pnpm test"
503+
elif [ -f "pnpm-lock.yaml" ]; then TEST_CMD="npx pnpm test"
504+
439505
elif [ -f "yarn.lock" ] && command -v yarn > /dev/null 2>&1; then TEST_CMD="yarn test"
440506
elif [ -f "package.json" ]; then TEST_CMD="npm test"
441507
else
442508
echo "WARN: No recognised test runner found — skipping tests"
443-
TEST_CMD="true"
509+
TEST_CMD=""
444510
fi
445-
echo "Running: $TEST_CMD"
446-
$TEST_CMD || { echo "ERROR: tests failed after conflict resolution — fix before committing"; exit 1; }
511+
if [ -n "$TEST_CMD" ]; then
512+
echo "Running: $TEST_CMD"
513+
eval "$TEST_CMD" || { echo "ERROR: tests failed after conflict resolution — fix before committing"; exit 1; }
514+
fi
515+
447516
```
448517

449518
```bash
450519
echo "=== Detecting lint runner ==="
451520
if [ -f "biome.json" ] && command -v npx > /dev/null 2>&1; then
452-
LINT_CMD="npx biome check --reporter=summary src/ tests/"
453-
elif find . -maxdepth 1 -name "eslint.config.*" | grep -q .; then
521+
# Point biome at . and let biome.json's files.include/files.ignore govern scope
522+
LINT_CMD="npx biome check --reporter=summary ."
523+
elif ls eslint.config.* > /dev/null 2>&1; then
524+
454525
LINT_CMD="npx eslint ."
455526
elif [ -f "package.json" ] && grep -q '"lint"' package.json; then
456527
LINT_CMD="npm run lint"
457528
else
458529
echo "WARN: No recognised lint runner found — skipping lint"
459-
LINT_CMD="true"
530+
LINT_CMD=""
531+
fi
532+
if [ -n "$LINT_CMD" ]; then
533+
echo "Running: $LINT_CMD"
534+
eval "$LINT_CMD" || { echo "ERROR: lint failed after conflict resolution — fix before committing"; exit 1; }
460535
fi
461-
echo "Running: $LINT_CMD"
462-
$LINT_CMD || { echo "ERROR: lint failed after conflict resolution — fix before committing"; exit 1; }
536+
463537
```
464538

465539
**Exit condition:** Tests and lint pass (or were not applicable). No new failures introduced by the resolution.
@@ -524,4 +598,5 @@ echo "Conflict resolution complete."
524598
- **Run tests and lint before pushing.** A clean merge that breaks tests is not ready to push.
525599
- **No co-author lines** in commit messages.
526600
- **No Claude Code references** in commit messages or comments.
527-
- **Cleanup:** `.codegraph/resolve/` is created during the skill and removed after a successful push. If the skill exits early (due to ambiguity or error), remove it with `rm -rf .codegraph/resolve` before re-running.
601+
- **Cleanup:** `.codegraph/resolve/` is created during the skill, removed automatically on any error exit (via `trap`), and removed after a successful push. If a crash left both `.codegraph/resolve/` stale **and** an in-progress merge (MERGE_HEAD), run `git merge --abort && rm -rf .codegraph/resolve` before re-running — Phase 0 will detect the leftover MERGE_HEAD and remind you.
602+

0 commit comments

Comments
 (0)