diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f84fe7b0..bd35aa93 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -227,8 +227,8 @@ { "name": "pr-cycle", "source": "./commands/pr-cycle", - "description": "/pr-cycle — one-shot PR cycle command. Pushes the current branch (`git push -u origin HEAD`), `gh pr create --fill`, then SELF-MERGES with ` && gh pr merge --squash --admin --delete-branch` in the SAME command block — one command does create → merge. Refuses on main/master. Pass extra gh flags as args. 0.4.1 — command appends the merge ITSELF instead of relying on the `pr-cycle-hook` PreToolUse hook: slash-command `!`-exec does NOT route through the Bash-tool PreToolUse hook, so the hook never fired for /pr-cycle (PRs left created-but-unmerged); self-merge fixes it, idempotent with the hook (skips any command already containing `gh pr merge`). Command half of the pr-cycle split (routing hook = `pr-cycle-hook`, still serves agent Bash-tool `gh pr create`).", - "version": "0.4.1" + "description": "/pr-cycle — one-shot PR cycle command. Pushes the current branch (`git push -u origin HEAD`), `gh pr create --fill`, SELF-MERGES with `gh pr merge --squash --admin --delete-branch`, then SWEEPS merged agent worktrees in the SAME command block — one command does create → merge → clean. Refuses on main/master. Pass extra gh flags as args. 0.5.0 — POST-MERGE WORKTREE SWEEP closes the leak where `--delete-branch` removed the remote branch but left the `.claude/worktrees/agent-*` DIRECTORY piling up across /cycle-bg runs. After a successful merge the command cd's to the MAIN worktree and removes every linked agent worktree whose upstream is `[gone]` (the squash-safe 'merged + branch deleted' signal — `--is-ancestor` cannot detect a squash merge, but `git push -u` then `--delete-branch` reliably leaves the upstream gone), plus `git branch -D` + `git worktree prune`. cd-to-main first means even the just-merged CURRENT worktree (when /pr-cycle ran inside one) is swept. SAFETY: never touches a worktree with a live OR absent (`track=none`) upstream — those may hold un-pushed work — nor `locked` worktrees (another checkout); only paths under `/.claude/worktrees/` are eligible. 0.4.1 — command appends the merge ITSELF instead of relying on the `pr-cycle-hook` PreToolUse hook: slash-command `!`-exec does NOT route through the Bash-tool PreToolUse hook, so the hook never fired for /pr-cycle (PRs left created-but-unmerged); self-merge fixes it, idempotent with the hook (skips any command already containing `gh pr merge`). Command half of the pr-cycle split (routing hook = `pr-cycle-hook`, still serves agent Bash-tool `gh pr create`).", + "version": "0.5.0" }, { "name": "pr-cycle-hook", diff --git a/CHANGELOG.md b/CHANGELOG.md index bb00518f..9008f48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ For the full audit trail, see `git log`. --- +## 2026-06-07 — 🧹 pr-cycle 0.5.0 — 머지 후 worktree sweep (누적 누수 차단) + +`/cycle-bg` 를 돌릴수록 `.claude/worktrees/agent-*` worktree 디렉터리가 계속 쌓이던 +누수를 차단. `gh pr merge --delete-branch` 는 **원격 브랜치만** 지우고 worktree +디렉터리는 남겼는데, pr-cycle 에 정리 단계가 없어 누적됐음 (pr→merge→clean 의 clean 부재). + +- 🧹 **머지 후 sweep** — 머지 성공 후 MAIN worktree 로 cd → upstream 이 `[gone]` 인 + `.claude/worktrees/agent-*` worktree 만 `git worktree remove --force` + `git branch -D` + + `git worktree prune`. MAIN 으로 먼저 cd 하므로 /pr-cycle 가 worktree **안에서** + 돌았어도 방금 머지된 자기 worktree 까지 정리됨. +- 🔑 **squash-safe 신호** — squash 머지는 worktree 커밋이 origin/main 의 조상이 안 돼 + `--is-ancestor` 로 "머지됨" 판정 불가. 대신 `git push -u` → `--delete-branch` 후 + upstream 이 `[gone]` 으로 변하는 걸 신호로 사용. +- 🔒 **안전** — upstream 이 살아있거나(`track=none` 미푸시 작업 가능성) `locked`(다른 + 체크아웃) worktree 는 절대 건드리지 않음. `/.claude/worktrees/` 경로만 대상. +- 검증: sh 문법 · 4-상태 worktree 분류(merged/`[gone]`·미푸시·locked·main) 보존 로직 · + g22 lockstep(plugin 0.5.0 ↔ marketplace 0.5.0). + ## 2026-06-07 — 🎓 domain 0.12.0 — 자동사용(USE) 절반 복원 · skillopt-hook 의 SessionStart 주입을 domain 안으로 0.11.0 통합에서 빠졌던 **자동사용 훅**을 domain 플러그인 내부에 포팅 — `activate` 가 diff --git a/commands/pr-cycle/.claude-plugin/plugin.json b/commands/pr-cycle/.claude-plugin/plugin.json index 31136dc0..beec89a1 100644 --- a/commands/pr-cycle/.claude-plugin/plugin.json +++ b/commands/pr-cycle/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pr-cycle", - "description": "/pr-cycle — one-shot PR cycle command. Pushes the current branch (`git push -u origin HEAD`), `gh pr create --fill`, then SELF-MERGES with ` && gh pr merge --squash --admin --delete-branch` in the SAME command block — one command does create → merge. Refuses on main/master. Pass extra gh flags as args (e.g. --title \"...\" --body \"...\"). 0.4.1 — the command now appends the merge ITSELF instead of relying on the `pr-cycle-hook` PreToolUse hook: the command body executes via slash-command `!`-exec, which does NOT route through the Bash-tool PreToolUse hook, so the hook never fired for /pr-cycle and PRs were left created-but-unmerged. Self-merging fixes it and is idempotent with the hook (the hook skips any command already containing `gh pr merge`); the hook still serves agent-issued `gh pr create` Bash-tool calls + cross-repo/worktree cleanup. Command half of the pr-cycle split.", - "version": "0.4.1", + "description": "/pr-cycle — one-shot PR cycle command. Pushes the current branch (`git push -u origin HEAD`), `gh pr create --fill`, SELF-MERGES with `gh pr merge --squash --admin --delete-branch`, then SWEEPS merged agent worktrees in the SAME command block — one command does create → merge → clean. Refuses on main/master. Pass extra gh flags as args (e.g. --title \"...\" --body \"...\"). 0.5.0 — POST-MERGE WORKTREE SWEEP closes the leak where `--delete-branch` removed the remote branch but left the `.claude/worktrees/agent-*` DIRECTORY piling up across /cycle-bg runs. After a successful merge the command cd's to the MAIN worktree and removes every linked agent worktree whose upstream is `[gone]` (the squash-safe 'merged + branch deleted' signal — `--is-ancestor` cannot detect a squash merge, but `git push -u` then `--delete-branch` reliably leaves the upstream gone), plus `git branch -D` + `git worktree prune`. cd-to-main first means even the just-merged CURRENT worktree (when /pr-cycle ran inside one) is swept. SAFETY: never touches a worktree with a live OR absent (`track=none`) upstream — those may hold un-pushed work — nor `locked` worktrees (another checkout); only paths under `/.claude/worktrees/` are eligible. 0.4.1 — the command now appends the merge ITSELF instead of relying on the `pr-cycle-hook` PreToolUse hook: the command body executes via slash-command `!`-exec, which does NOT route through the Bash-tool PreToolUse hook, so the hook never fired for /pr-cycle and PRs were left created-but-unmerged. Self-merging fixes it and is idempotent with the hook (the hook skips any command already containing `gh pr merge`); the hook still serves agent-issued `gh pr create` Bash-tool calls + cross-repo/worktree cleanup. Command half of the pr-cycle split.", + "version": "0.5.0", "author": { "name": "dancinlab" }, "repository": "https://github.com/dancinlab/sidecar", "license": "MIT", diff --git a/commands/pr-cycle/commands/pr-cycle.md b/commands/pr-cycle/commands/pr-cycle.md index 092f2733..759efb4a 100644 --- a/commands/pr-cycle/commands/pr-cycle.md +++ b/commands/pr-cycle/commands/pr-cycle.md @@ -1,5 +1,5 @@ --- -description: /pr-cycle — one-shot PR cycle. Pushes the current branch (git push -u origin HEAD), runs gh pr create --fill, then SELF-MERGES with ` && gh pr merge --squash --admin --delete-branch` IN THE SAME command block. 0.4.1 — the merge is appended by the command itself (not left to the pr-cycle PreToolUse hook): the command body runs via slash-command `!`-exec, which does NOT route through the Bash-tool PreToolUse hook, so relying on the hook left PRs created-but-unmerged; self-merging fixes that and stays idempotent (the hook skips any command already containing `gh pr merge`). Refuses on main/master. Pass extra gh flags as args (e.g. --title "..." --body "..."). +description: /pr-cycle — one-shot PR cycle. Pushes the current branch (git push -u origin HEAD), runs gh pr create --fill, SELF-MERGES with `gh pr merge --squash --admin --delete-branch`, then SWEEPS merged agent worktrees IN THE SAME command block. 0.5.0 — post-merge worktree sweep: after a successful merge it cd's to the MAIN worktree and removes every `.claude/worktrees/agent-*` linked worktree whose upstream is `[gone]` (the squash-safe "merged + branch deleted" signal — `--is-ancestor` can't detect squash merges, but `git push -u` then `--delete-branch` reliably leaves the upstream gone), plus `git branch -D` + `git worktree prune`. Fixes the leak where `--delete-branch` removed the remote branch but left the worktree DIRECTORY piling up across /cycle-bg runs. NEVER touches a worktree with a live or absent (`track=none`) upstream — those may hold un-pushed work — nor `locked` worktrees (another checkout). 0.4.1 — the merge is appended by the command itself (not the PreToolUse hook): the body runs via slash-command `!`-exec, which does NOT route through the Bash-tool PreToolUse hook, so relying on the hook left PRs created-but-unmerged; self-merging fixes that and stays idempotent. Refuses on main/master. Pass extra gh flags as args (e.g. --title "..." --body "..."). argument-hint: "[gh pr create flags, e.g. --title \"...\" --body \"...\"]" allowed-tools: Bash --- @@ -12,6 +12,34 @@ if [ "$BR" = "main" ] || [ "$BR" = "master" ]; then echo " (git switch -c feat/)" exit 0 fi -echo "▸ /pr-cycle on '$BR' — push + create + self-merge" +echo "▸ /pr-cycle on '$BR' — push + create + self-merge + worktree sweep" git push -u origin HEAD 2>&1 | tail -2 -gh pr create --fill $ARGUMENTS && gh pr merge --squash --admin --delete-branch` +if gh pr create --fill $ARGUMENTS && gh pr merge --squash --admin --delete-branch; then + # ── post-merge worktree sweep ────────────────────────────────────────────── + # Remove harness agent worktrees whose upstream is [gone] (= merged + branch + # deleted). Squash-safe: a squashed branch is NOT an ancestor of main, so we + # key off the deleted upstream, not --is-ancestor. cd to MAIN first so even the + # just-merged CURRENT worktree (if /pr-cycle ran inside one) becomes sweepable. + MAIN=$(git worktree list --porcelain | awk 'NR==1 && /^worktree /{print $2; exit}') + [ -n "$MAIN" ] && cd "$MAIN" 2>/dev/null || true + git fetch -p origin >/dev/null 2>&1 || true + git worktree list --porcelain | awk ' + /^worktree /{wt=$2; br=""; lk=""} + /^branch /{br=$2; sub("refs/heads/","",br)} + /^locked/{lk="L"} + /^$/{if(wt!=""){print wt"|"br"|"lk; wt=""}} + END{if(wt!=""){print wt"|"br"|"lk}}' | while IFS="|" read -r wt br lk; do + case "$wt" in *"/.claude/worktrees/"*) ;; *) continue ;; esac # only harness agent worktrees + [ "$wt" = "$MAIN" ] && continue # never the main checkout + [ -n "$lk" ] && continue # never locked (another checkout) + [ -n "$br" ] || continue + track=$(git for-each-ref --format='%(upstream:track)' "refs/heads/$br" 2>/dev/null) + [ "$track" = "[gone]" ] || continue # only merged+deleted (squash-safe) + if git worktree remove --force "$wt" 2>/dev/null; then + git branch -D "$br" 2>/dev/null || true + echo " 🧹 swept merged worktree: $wt" + fi + done + git worktree prune 2>/dev/null || true + echo "✓ /pr-cycle done — PR merged + merged worktrees swept" +fi`