Conversation
The merge queue derives `main`'s squash message from repo settings (`squash_merge_commit_message: COMMIT_MESSAGES`) and its enqueue API carries no message fields, so only a single-commit PR merges with a written message instead of bulleted commit soup. That invariant was maintained by three hand-rolled copies which had already diverged, and `pr-review` broke it outright — it pushed an auto-fix commit and forbade the force-push needed to restore one commit. Add `.claude/skills/collapse-pr/` as the single owner of the mechanics and the rationale; `open-pr`, `revise-pr` and `pr-review` now each keep only their own decision about *whether* to collapse. Correctness properties the skill enforces: - resolve an existing PR's head from `origin` and rewrite it in a throwaway worktree; the local checkout is never assumed to be the target. A dry run against PR #842 found the checkout holding a superseded copy of a commit, one fewer than origin — collapsing from it would have destroyed a fix that existed only on origin - `--in-place` lets a caller that owns the checkout say so, rather than inferring mode from whether a PR happens to exist; that inference silently discarded local work when `open-pr` was re-run - assert the collapsed tree is byte-identical to the pre-collapse head. That assertion, and only that assertion, licenses skipping the managed `pre-push` hook — and only in a throwaway worktree, never in place, where the hook is the sole local verification - five gates: stale approval and open threads are soft (`--yes` proceeds); branch ownership and the push lease are hard - gate 3e resolves a diverged checkout losslessly instead of asking a human. Whether local work is unique or merely superseded is not mechanically decidable — `git cherry` and reverse-apply both report "unique" for an amended copy — so commits are preserved at a `collapse-pr-backup/<branch>-<utc>` ref before any reset, and the backup is a hard precondition of it `pr-review` now collapses between its auto-fix push and its review post, which is the only ordering that works: the collapse rewrites every SHA, so inline anchors must be derived after it, and `dismiss_stale_reviews` means collapsing after the approval would dismiss it. It preserves the author's commit message via `--message-file` rather than substituting one the reviewer wrote, and emits suggestion blocks where it cannot push. Closes #849
Deploying offlinecv with
|
| Latest commit: |
f860dae
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://54222e7a.offlinecv.pages.dev |
| Branch Preview URL: | https://gh-849.offlinecv.pages.dev |
s-annam
left a comment
There was a problem hiding this comment.
An extremely thorough and maintainer-grade implementation of the /collapse-pr skill and its integration across the PR lifecycle.
Reviewed by: Gemini 3.5 Flash (high)
| # per row 3 first (so nothing is lost), then replay just the fix onto the | ||
| # remote head inside $WORKDIR, which is already detached at origin/$HEAD_REF. | ||
| FIX_SHA="$(git -C "$WT" rev-parse HEAD)" | ||
| # ... row 3's backup of $FIX_SHA runs here, with its own `||` refusal ... |
There was a problem hiding this comment.
This comment suggests running the backup commands of Row 3 here. It would make the skill more self-contained and robust to explicitly include the Row 3 backup commands (specialized for $FIX_SHA) inside the else block instead of a placeholder comment.
s-annam
left a comment
There was a problem hiding this comment.
Reviewed the whole diff against #849's acceptance criteria, not against the description — which I read last, per this skill's own ordering rule. All 17 ACs of #849 are met, the regime/gate probes all behave live as documented, and the description is accurate on every claim I could check. The three blockers below are all in the ~230 lines of round 3 that no prior review has seen, and all three sit inside collapse-pr gate 3e row 1 — the code the ## Review focus section correctly named as the least-reviewed in the diff.
Verdict rule applied: normally ≥1 Blocking → REQUEST_CHANGES. This review posts COMMENT under an explicit maintainer override reserving the verdict for themselves on this PR. The same override disabled Step 5.5 (no auto-fix, no commit, no push, no /collapse-pr invocation — the collapse code is what this PR changes, and running it for the first time on the PR that contains it is not a risk worth taking) and forbade any enqueue or auto-merge. Nothing was merged, approved, or enqueued; the branch is exactly as I found it.
Prior rounds — confirmed still fixed
- Round 1 (5 blocking): Step 5b is guarded by
if [ "$MODE" = worktree ](L986);OFFLINECV_SKIP_HOOKS=1is worktree-mode-only via theSKIP=()array (L915); thetrapis gone and replaced with an explicit "run Step 5b on every exit path" contract (L162-166);pr-reviewstep 5 opens with "Only if step 4 pushed"; the collapsed message comes from--message-fileholdinggit log -1 --format=%B "$PRE_FIX_SHA". ✅ - Round 2 (7 blocking): the backup ref is chained with
||and re-verified byrev-parsebefore the reset (L738-749); 3e's mode restriction is now real code in 7 places, not prose;--authored-worktreescopes the whitelist and thewhile IFS= read -r WTloop exists; row 1's push is ancestry-tested rather than a barepush HEAD; the rejection-triage table has three causes;--untracked-files=nois used everywhere (.git/info/exclude:18is indeed!/node_modules— verified);--in-placeexists andopen-prpasses it. ✅
I re-report none of these.
Blocking
Three findings, all one code path: collapse-pr gate 3e row 1.
B1 — Row 1 has no "did we author anything?" precondition, so it publishes the user's local-only commit
Row 1's table entry fires on "uncommitted changes this run authored", but the executable blocks carry no such condition — and the iterate block (L524-529) explicitly says the rows are the loop body, run for every $WT. So on a matched worktree that is clean but diverged, row 1 still executes:
$WT != $AUTHORED_WORKTREE→DIRTY_COUNTis0→ no refusal (L573-580).git addstages nothing;git commitfails "nothing to commit" — and L760-762 of this same file states that an unchained nonzero exit is silently discarded in this execution model.WT_PRE_FIX_SHAis therefore the user's local-only commit.merge-base --is-ancestor "$WT_PRE_FIX_SHA" "origin/$HEAD_REF"→ false → else branch.FIX_SHA= the user's commit →git -C "$WORKDIR" cherry-pick "$FIX_SHA"→push origin "HEAD:$HEAD_REF".
The user's unreviewed local commit is published into someone else's PR, and the Step 6 report describes that checkout as row 3 "preserved + reset" — it never says the commit was also pushed.
This reproduces on the skill's own flagship scenario. In the #842 worked example (L794-803) the main checkout was clean and held exactly one local-only commit, 6ba92e9. Under this code that commit is cherry-picked onto origin's head and pushed into PR #842. Round 2 finding 4 narrowed this hazard from "all local-only commits" to "the tip local-only commit" — it did not remove it, because row 1 still runs on worktrees it has no business touching.
Fix: give row 1 a real precondition, in code, mirroring the mode guard's own "code, not prose" standard:
[ "$MODE" = worktree ] || exit 1
[ -n "${AUTHORED_WORKTREE:-}" ] && [ "$WT" = "$AUTHORED_WORKTREE" ] || { echo "row 1: not the authored checkout — skipping to row 2"; ROW1=skip; }
DIRTY_COUNT="$(git -C "$WT" status --porcelain --untracked-files=no -z | tr -dc '\0' | wc -c | tr -d ' ')"
[ "$DIRTY_COUNT" != 0 ] || { echo "row 1: nothing dirty in $WT — no fold-in needed"; ROW1=skip; }and make the commit/ancestry/push blocks no-ops when ROW1=skip. The non-authored-worktree branch should fall through to rows 3/4, never into the commit-and-publish machinery.
B2 — The cherry-pick path's push is rejected 100% of the time on this repo, and the triage table misdiagnoses it
git -C "$WORKDIR" cherry-pick "$FIX_SHA"
git -C "$WORKDIR" push origin "HEAD:$HEAD_REF"$WORKDIR is the throwaway worktree from git worktree add --detach under mktemp -d. Worktrees share .git/hooks (core.hooksPath is unset — verified), so .git/hooks/pre-push fires and runs npm run verify in a directory with a package.json and no node_modules. That is the exact failure Step 0b describes at L199-202 as the reason the bypass exists — but this push carries no OFFLINECV_SKIP_HOOKS=1 and no --no-verify.
The rejection-triage table then sends the reader the wrong way (L688):
| the rejection came from
.git/hooks/pre-push| row 1's push is a plain push from a real checkout, so the managed hook fires … | fix the failing gate. Never setOFFLINECV_SKIP_HOOKS=1here |
"a real checkout" is true of the if branch only. In the else branch the push comes from the throwaway worktree, the gate is not red — the dependencies are simply absent — and "fix the failing gate" is unactionable. The run wedges after a backup ref has already been created, with no defined next step.
Fix: decide the licence explicitly for this push. The cherry-picked content is new relative to origin, so Step 0b's tree-identity licence does not cover it — either npm install in $WORKDIR before this push, or push the fix from $WT on a temporary branch and fast-forward, or state that this path requires a bootstrapped worktree. Then split the triage row into "if-branch (real checkout)" and "else-branch ($WORKDIR)" with different answers.
B3 — In the cherry-pick path the backup ref is a comment, not code, and the prose then tells row 3 not to create one
L646:
# ... row 3's backup of $FIX_SHA runs here, with its own `||` refusal ...That placeholder is the only thing standing between this path and an unbacked reset --hard, and L659-662 then instructs:
Row 3 is already half-done for this
$WT. The backup above covered the local-only commits and the fix commit on top of them, so when row 3 reaches this checkout it must not mint a second ref — only thereset --hardremains.
But row 3's code (L721-752) has no "already backed up" check. So an agent gets one of two outcomes, and neither is the intended one: it follows the code and mints a second ref (harmless, but contradicts the report contract), or it follows the prose, skips row 3's backup, and — because L646 was never expanded into real commands — runs git reset --hard "origin/$HEAD_REF" with no backup ref at all. That is precisely the irreversible-loss shape round 2 finding 1 was blocking for, on the one path where the loss cannot be undone.
This is also the same defect class as round 2 finding 2: a safety property stated in prose inside a code block instead of being executable.
Fix: inline the actual backup commands (the while name-search, git branch … || refuse, and the rev-parse verification) into the else branch, and give row 3 an explicit [ -n "${BACKUP:-}" ] short-circuit so it resets without re-minting. Report the one ref, as L659-662 intends.
Secondary
-
collapse-prL579 — the row-2 refusal does not run Step 5b in code.[ "$DIRTY_COUNT" = 0 ] || { echo …; exit 1; }carries a comment saying "Run Step 5b before stopping, like every refusal in this skill" — but the block just exits. This fires inworktreemode, the one mode where Step 5b is not a no-op, so it orphans both the linked worktree and$WORKDIR_PARENTunder/tmp. Every other refusal in the skill states the teardown in surrounding prose; this one contradicts itself inline. -
pr-reviewL581-587 — thecut -c4-subtraction errs in the unsafe direction, not the safe one. The prose claims: "it errs by over-subtracting, which drops a path to row 2 and refuses. That is the safe direction." It does the opposite.git status --porcelain -zemits a rename as two records —R <new>\0then a bare<orig>\0with noXYprefix (verified).cut -c4-chops three characters off that bare second field, sosrc/foo.tsbecomes/foo.tsand fails to subtractsrc/foo.tsfrom$FIXED_PATHS. The path stays whitelisted, reaches 3e as--authored-path, and the user's in-progress rename can be committed into the PR — the exact outcome the paragraph exists to prevent. Narrow trigger (the reviewer must have edited a path the user is mid-rename on), but the stated safety argument is inverted. Fix: parse the same waycollapse-prL586-594 already does, or drop only entries whose record began with a status prefix. -
pr-reviewL560-565 — the partial-failure recovery rationale does not hold. "if step 4 committed nothing, or committed and failed to push, the fixes are sitting uncommitted and 3e can only fold them in if it knows they are ours." If step 4 committed and the push failed, the fixes are committed, not uncommitted — 3e row 1 never sees them, row 3 backs them up andreset --hards them out of the checkout. And that state cannot arise in-run anyway, because step 5 opens with "Only if step 4 pushed." On an actual re-run the leftover commit is local-only, so row 3 quietly relocates the previous run's fix to acollapse-pr-backup/…ref and both skills report success. Either describe that outcome or add a "leftover fix commit" check to step 1. -
collapse-prL111 — swallowed error decides the destructive mode.PR_NUM="${PR_NUM:-$(gh pr view --json number -q .number 2>/dev/null || true)}": an auth failure, rate limit, or network blip yields an emptyPR_NUM, and L113's[ -z "$PR_NUM" ]then selectsMODE=inplace— the mode that rewrites the user's own checkout. This is the "swallowed errors —2>/dev/null || truethat hides auth/rate-limit, not just 'already exists'" antipattern frompr-reviewgate 3e, applied to a mode decision. Distinguish "no PR exists" (exit 1 with theno pull requests foundmessage) from "the call failed" and refuse on the latter. -
collapse-prL598-600 — the normal path runs a command that errors. In the documented happy case (pr-reviewstep 4 already committed$FIXED_PATHS, so "gate 3e sees them clean") row 1 still reachesgit commit -m …on a clean tree and fails. Harmless in outcome, but the skill's own execution model has noset -e, so the run continues past a nonzero exit on its main path — and an agent that does stop on it aborts a collapse that should have succeeded. Same root cause as B1; fixing B1's precondition fixes this.
Nits
collapse-prL508 —MATCHES_FILE(andMSG_FILEat L859 when it is amktemp) are never removed; Step 5b tears down the worktree but not the temp files.collapse-prL729-732 — the backup-name search is TOCTOU against a concurrent run: two runs can both find-1free, one losesgit branch, and the||then prints "Fix the ref store and re-run", which misdiagnoses a name race as a broken ref store. Worth a distinct message forexit 128/ "already exists".collapse-prL886-890 — the net-no-change branch exits1.pr-reviewstep 5 reads any non-zero as "a hard gate refused" and reports it as such. A distinct exit code, or a note inpr-review, would keep the report honest.docs/CONTRIBUTING-PROCESS.md§Squash messages still carries a full copy of the merge-queue derivation, whilecollapse-prL64 declares itself "the single source of truth for the merge-queue rationale" and the test plan says "onlycollapse-prrestates" it. Both are defensible (the doc is the rationale file, and the same diff sayscollapse-pr"holds the canonical one"), but the three statements do not quite agree.collapse-prL584-585 — "git quotes paths containing spaces" is given as a reason to avoidcut -c4-. In-zmode git never quotes; the rename-mangling argument alone carries the point.
AC checklist — #849
All 17 acceptance criteria met. Spot-verified live rather than read: regime detection (mergeQueue.configuration.mergeMethod → SQUASH, squash_merge_commit_title → COMMIT_OR_PR_TITLE, squash_merge_commit_message → COMMIT_MESSAGES), gate 3a's dual probe (branch protection returns true; the ruleset probe returns unknown, and the documented first | if . == null guard is what keeps that from reading as false), the --in-place mode contract, the tree-identity assertion, 3e's losslessness, --dry-run holding 3e's writes back, and the dogfood criterion (the PR holds exactly 1 commit).
The one place implementation departs from the issue is pr-review 5.5's ordering: #849 §3 sketched fix → commit → collapse → push, the PR ships fix → commit → plain push → collapse. That is a correct departure, not a gap — /collapse-pr reads the head from origin, so a still-local fix commit would not be in the collapse — and the AC as written ("collapses before posting the review, with --force-with-lease, falls back to a plain push when the lease fails") is satisfied.
Description accuracy (gate 3f) — accurate
Every checkable claim round-tripped to the diff and held:
npm run verifygreen —verify,fallow, and Cloudflare Pages allSUCCESS. ✅- Four in-scope frontmatters parse as strict YAML and
pr-readydoes not — verified with a strict parser;pr-readyfails exactly as described (argument-hintflow sequence), and it is untouched by this diff. ✅ open-prandrevise-prcarry zeroreset --soft— confirmed; the only remaining occurrence repo-wide ispr-review's unrelated auto-fix revert. ✅- Cross-referenced step and row numbers resolve — spot-checked every cross-file reference in the diff (
3d/Step 5/3e row 3/Step 3.6/5.1/5.5/Step 6); all resolve. ✅ - Exactly one commit. ✅
## Review focus was honest and correct: items 1 and 2 named gate 3e and row 1's cherry-pick path as the least-reviewed code, and that is where all three blockers are. Under this skill's own rules a focus section that points at the right hunk is a point in the author's favour — I reviewed the other four files and the rest of collapse-pr regardless, and found nothing above Nit there.
On focus item 4 (length): 1083 lines is a lot, but I would not cut it. The three blockers above all come from a step that was described in prose but not written as code — which is the failure mode that shrinking this file would multiply, not reduce. The prose that is actually load-bearing here is the kind that gets promoted into if statements.
Gates run
| Gate | Result |
|---|---|
| 3a fixture PII | skipped — no fixture binaries in the diff |
| 3b design-system / reuse | skipped — no src/components/** changes |
| 3c style tokens | skipped — no src/** changes |
3d dead code (fallow) |
green on CI (fallow check SUCCESS); not re-run locally |
| 3e command-level bugs in skill/script files | the whole review — B1, B2, B3, S1-S5, N1-N3 |
| 3f description accuracy | run; accurate |
Head SHA reviewed: f860dae. 👀 was posted before the review began (Step 0.6).
Reviewed by: Claude Opus 5 (high)
| ```bash | ||
| [ "$MODE" = worktree ] || exit 1 # 3e never runs in inplace mode (above) | ||
|
|
||
| if git -C "$WT" merge-base --is-ancestor "$WT_PRE_FIX_SHA" "origin/$HEAD_REF"; then |
There was a problem hiding this comment.
Blocking (B1). This ancestry test decides on $WT_PRE_FIX_SHA, which row 1 sets unconditionally at L598 — including on worktrees where nothing was authored and nothing was dirty. On a clean-but-diverged $WT, git commit above failed ("nothing to commit", silently discarded per L760-762), so $WT_PRE_FIX_SHA is the user's local-only commit, this test goes false, and the else branch cherry-picks and pushes it into the PR.
That is the #842 worked example at L794-803: a clean main checkout holding 6ba92e9. Round 2 finding 4 narrowed this from "every local-only commit" to "the tip one"; it did not close it.
Row 1 needs a real precondition — $WT is --authored-worktree and the tree is dirty — expressed in code, the same standard the mode guard holds itself to.
| # per row 3 first (so nothing is lost), then replay just the fix onto the | ||
| # remote head inside $WORKDIR, which is already detached at origin/$HEAD_REF. | ||
| FIX_SHA="$(git -C "$WT" rev-parse HEAD)" | ||
| # ... row 3's backup of $FIX_SHA runs here, with its own `||` refusal ... |
There was a problem hiding this comment.
Blocking (B3). This is a comment, not code — and it is the only backup standing between this path and the reset --hard. L659-662 then tells row 3 not to mint a second ref for this $WT, but row 3's code (L721-752) has no "already backed up" short-circuit. So an agent either mints two refs (contradicting the report contract) or, following the prose, skips row 3's backup and resets with no backup ref at all — the irreversible-loss shape round 2 finding 1 was blocking for.
Inline the real commands here (name search → git branch … || refuse → rev-parse verification) and give row 3 an explicit [ -n "${BACKUP:-}" ] skip.
| FIX_SHA="$(git -C "$WT" rev-parse HEAD)" | ||
| # ... row 3's backup of $FIX_SHA runs here, with its own `||` refusal ... | ||
| git -C "$WORKDIR" cherry-pick "$FIX_SHA" | ||
| git -C "$WORKDIR" push origin "HEAD:$HEAD_REF" |
There was a problem hiding this comment.
Blocking (B2). This push comes from $WORKDIR — the throwaway worktree — which shares .git/hooks with the main clone (core.hooksPath unset, verified). pre-push runs npm run verify there with no node_modules, so this push is rejected every time on this repo. That is exactly the failure Step 0b cites at L199-202 as the reason the bypass exists, but there is no OFFLINECV_SKIP_HOOKS=1 and no --no-verify here.
The triage table at L688 then misreads it: "row 1's push is a plain push from a real checkout" is true of the if branch only, and "fix the failing gate" is unactionable when the gate is not red — the dependencies are absent. The run wedges after a backup ref already exists.
Step 0b's tree-identity licence does not cover this push (the cherry-picked content is new relative to origin), so pick one deliberately: npm install in $WORKDIR first, or publish the fix from $WT and fast-forward. Then split that triage row per branch.
| DIRTY_COUNT="$(git -C "$WT" status --porcelain --untracked-files=no -z \ | ||
| | tr -dc '\0' | wc -c | tr -d ' ')" | ||
| # Row 2. Run Step 5b before stopping, like every refusal in this skill. | ||
| [ "$DIRTY_COUNT" = 0 ] || { echo "row 2: $WT is dirty and is not --authored-worktree" >&2; exit 1; } |
There was a problem hiding this comment.
Secondary. The comment says "Run Step 5b before stopping, like every refusal in this skill" — but the block exits without doing it. This refusal only fires in worktree mode, which is the one mode where Step 5b is not a no-op, so it orphans the linked worktree and $WORKDIR_PARENT under /tmp.
Either call the teardown here or move the instruction into surrounding prose, the way every other refusal in this file does.
|
|
||
| WT_PRE_FIX_SHA="$(git -C "$WT" rev-parse HEAD)" # BEFORE the commit — see the push below | ||
| git -C "$WT" add -- ${AUTHORED_PATHS[@]+"${AUTHORED_PATHS[@]}"} # by path, never `git add -A`/`.` | ||
| git -C "$WT" commit -m "${AUTHORED_MESSAGE:-chore: fold in run-authored changes (collapse-pr gate 3e)}" |
There was a problem hiding this comment.
Secondary. On the documented happy path this errors. pr-review step 4 has already committed $FIXED_PATHS, so — as L612 puts it — "gate 3e sees them clean", and git commit on a clean tree exits non-zero with "nothing to commit". The skill runs without set -e, so the run continues past it (into B1); an agent that does stop aborts a collapse that should have succeeded.
Fixing B1's row-1 precondition fixes this too.
|
|
||
| ```bash | ||
| REPO="${REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" # offlinecv/OfflineCV | ||
| PR_NUM="${PR_NUM:-$(gh pr view --json number -q .number 2>/dev/null || true)}" |
There was a problem hiding this comment.
Secondary. 2>/dev/null || true makes an auth failure, rate limit, or network blip indistinguishable from "no PR exists" — and L113 then reads the empty value as a licence for MODE=inplace, the mode that rewrites the user's own checkout. A swallowed error is choosing the destructive branch.
This is the "swallowed errors" item from pr-review gate 3e applied to a mode decision. Separate the two: match gh's "no pull requests found" and refuse on any other non-zero exit.
| ```bash | ||
| # Drop from FIXED_PATHS anything that appears in PRE_DIRTY_FILE. | ||
| comm -23 <(printf '%s\n' ${FIXED_PATHS[@]+"${FIXED_PATHS[@]}"} | sort -u) \ | ||
| <(tr '\0' '\n' < "$PRE_DIRTY_FILE" | cut -c4- | sort -u) |
There was a problem hiding this comment.
Secondary. This errs in the unsafe direction, not the safe one that L586 claims.
git status --porcelain -z emits a rename as two records: R <new>\0 and then a bare <orig>\0 with no XY prefix (verified locally). cut -c4- chops three characters off that bare field, so src/foo.ts → /foo.ts and src/foo.ts is not subtracted from $FIXED_PATHS. It stays whitelisted, goes out as --authored-path, and the user's in-progress rename can be committed into the PR — the outcome this paragraph exists to prevent.
Parse it the way collapse-pr L586-594 already does, or drop only records that actually carried a status prefix.
| **Pass `--authored-worktree` and one `--authored-path` per surviving entry of | ||
| `$FIXED_PATHS`.** In the normal flow they change nothing — step 4 already committed | ||
| those paths, so gate 3e sees them clean — but they are what makes a **re-run after a | ||
| partial failure** work: if step 4 committed nothing, or committed and failed to push, |
There was a problem hiding this comment.
Secondary. If step 4 committed and the push failed, the fixes are committed, not uncommitted — 3e row 1 never sees them, and row 3 backs them up and reset --hards them out of the checkout. And that state cannot arise inside one run: step 5 opens with "Only if step 4 pushed."
On a real re-run the leftover commit is local-only, so row 3 relocates the previous run's fix to a collapse-pr-backup/… ref and both skills report success. Either describe that as the recovery, or add a leftover-fix-commit check to step 1.
| ```bash | ||
| # 3e is worktree-mode ONLY. The guard produces an EMPTY match list in inplace | ||
| # mode, which is what makes every block below a no-op there — prose cannot. | ||
| MATCHES_FILE="$(mktemp -t collapse-pr-3e)" |
There was a problem hiding this comment.
Nit. $MATCHES_FILE is never removed (nor $MSG_FILE at L859 when it comes from mktemp). Step 5b tears down the worktree but leaves the temp files behind on every path.
| # a free name rather than failing on one — a retry loop would otherwise hit the | ||
| # refusal below every single time. | ||
| i=0 | ||
| while git -C "$WT" show-ref --verify --quiet "refs/heads/$BACKUP"; do |
There was a problem hiding this comment.
Nit. TOCTOU against a concurrent run: two runs in the same second can both see -1 free, and the loser's git branch then trips the || at L738, which prints "Fix the ref store and re-run" — a name race misdiagnosed as a broken ref store. Worth distinguishing exit 128 / "already exists" from a genuine ref-store failure, since the message is what a user acts on.
|
Adversarial review of the round-3 additions. 6 Blocking, 4 Nits. Head reviewed: Four of the six blockers sit in the ~230 lines the PR body flags as unreviewed (the cherry-pick path and the
Blocking1.
|
…hing local work (#852) Round-4 review of #851 found three blocking defects, all in gate 3e row 1 — the ~230 lines added in round 3 that no prior review had seen. All three share one root cause: a precondition stated in the row table as prose while the executable blocks carried no condition at all. - row 1 is now opt-in in code. `ROW1=run|skip` gates the commit, ancestry and publish blocks. Previously the rows ran for every matched worktree, so a checkout that was clean but held a local-only commit fell through the dirty check, `git commit` failed and was silently discarded, and the user's unreviewed commit was cherry-picked and pushed into the PR. That reproduced on the skill's own #842 worked example. - the cherry-pick branch no longer pushes. It pushed from the throwaway worktree, which shares .git/hooks and has no node_modules, so the pre-push hook failed on every run — and Step 0b's bypass licence does not cover it, because cherry-picked content is new relative to origin. The fix now stays in $WORKDIR and Step 5's collapse publishes it under the lease already pinned in gate 3d. The re-pin block is scoped to the push branch, which would otherwise reset --hard away the cherry-pick. - the cherry-pick branch's backup ref is real code, not a placeholder comment. It was the only thing between row 3's reset --hard and unrecoverable loss, and the prose then told row 3 not to mint one. Row 3 short-circuits on $BACKUP. Also fixed, same class — a status check whose result was discarded: - Step 5 and row 1 capture the push status with `|| PUSH_FAILED=1` and chain the rollback. There is no `set -e`, so an unchained failure was discarded and Step 6 would report a collapse that never landed. Documents why the push must not be piped through `tail`: `$?` then belongs to the filter, and `$PIPESTATUS` is bash-only — under zsh it expands empty and fires the rollback on a push that succeeded. - the cherry-pick is chained; an unchained conflict fell through to a push of $WORKDIR's unchanged head, dropping the fix while reporting success. - row 2's refusal runs the teardown in code instead of a comment saying it should. - PR discovery distinguishes "no PR exists" from "the call failed". An empty PR_NUM selects in-place mode, so an auth expiry or rate limit silently redirected the rewrite at the user's own checkout. - row 1 skips on a clean authored worktree — the documented happy path, which previously reached `git commit` on a clean tree and failed. pr-review: - the pre-existing-dirty subtraction parses porcelain records properly. `cut -c4-` mangles a rename's bare second field, so it *under*-subtracted and left the user's in-progress rename whitelisted — the opposite of the safety direction the surrounding prose claimed. - the partial-failure rationale now matches what the code does, and names the inherited-state case it does not cover. Nits: temp-file cleanup in Step 5b, a distinct exit 2 for the no-net-change no-op so pr-review stops reading it as a refused gate, a TOCTOU-aware message on backup-ref collision, and the -z quoting claim dropped from the cut -c4- rationale. Verified by collapsing PR #842 for real: 3aeae1b + 72358ab -> c5eeec8, tree 56a742c identical on both sides, zero diff, lease pinned to 72358ab and accepted. Refs #849 Refs #851
Summary
mainmerges through a merge queue whose enqueue API carries no commit-message fields, so GitHub derives the squash message fromsquash_merge_commit_message: COMMIT_MESSAGES. Only a single-commit PR merges with a written message; anything else lands inmainas*-bulleted commit soup.That invariant was maintained by three hand-rolled copies that had already diverged — and
pr-reviewbroke it outright, pushing an auto-fix commit while explicitly forbidding the force-push needed to restore one commit.This extracts
.claude/skills/collapse-pr/as the single owner of both the mechanics and the rationale.open-pr,revise-prandpr-reviewkeep only their own decision about whether to collapse.Closes #849.
What the skill enforces
originand rewritten in a throwaway worktree; the local checkout is never assumed to be the target.--in-placelets a caller that genuinely owns the checkout say so, instead of inferring mode from whether a PR happens to exist.pre-pushhook, and only in a throwaway worktree, never in place where the hook is the sole local verification.--yesproceeds); branch ownership and the push lease are hard and never overridable.collapse-pr-backup/<branch>-<utc>ref before any reset, and creating that ref is a hard precondition of the reset.pr-reviewordering. Collapse sits between the auto-fix push and the review post — the only ordering that works, since the collapse rewrites every SHA (so anchors must come after) anddismiss_stale_reviews: truemeans collapsing after an approval would dismiss it. The author's message is preserved via--message-filerather than replaced with one the reviewer wrote.Review focus
collapse-prgate 3e (Step 3). The most destructive code here — it runsgit reset --hardon a checkout. Check the backup ref is an unconditional precondition on every path, and that the[ "$MODE" = worktree ]guards cannot be bypassed.push HEADfrom publishing local-only commits. Least-reviewed code in the diff.pr-reviewStep 5.5 ordering — that no route reaches a force-push after an abandoned auto-fix.collapse-pris 1083 lines. Growth is gates and guarded paths; a reviewer may reasonably disagree about how much prose earns its place.Test plan
npm run verifygreen (also enforced by thepre-pushhook on this branch — not bypassed).pr-readydoes not — pre-existing, filed as pr-ready SKILL.md frontmatter is invalid YAML (unquoted argument-hint flow sequence) #850, deliberately untouched here.)collapse-prrestates the merge-queue rationale;open-prandrevise-prcarry zeroreset --soft.56a742c98fb9d6afe1370ab5f9432c82abc6c5d3identical on both sides, zero diff.--force-with-leaseverified on both paths — valid lease accepted, stale lease refused with the remote unchanged.Adversarial review
Three rounds ran before this PR was opened. Findings and dispositions:
Round 1 — 5 blocking, all fixed
git worktree remove --forcewas unguarded; ininplacemode$WORKDIRis the user's checkout, and linked worktrees delete cleanly. Would have deleted the worktree it was working in.OFFLINECV_SKIP_HOOKS=1was applied on both push paths — silently removingnpm run verifyfromopen-prentirely, since the hook was its only local verification.trap … EXITcannot span separate tool calls, so cleanup either fired immediately or never; two prose claims asserted otherwise.pr-reviewstep 5 ran the collapse on paths that had pushed nothing.Round 2 — 7 blocking, all fixed
reset --hardwere unconditional siblings; a failedgit branchfell through to irreversible loss (the skill does not run underset -e).worktree-only restriction was prose with zeroMODEreferences in any of its five code blocks — repeating round 1's lesson.--authored-pathwhitelists path names, but provenance is content in a specific checkout; a global list applied across worktrees could publish a user's unrelated edits. No iteration construct existed for multiple matches.push HEADpublished every local-only commit — the fix(heuristics): recover dropped roles, titles and skills sections #842 hazard, ungated.pre-pushhook, are likelier causes.status --porcelainincludes??, and.git/info/excludeun-ignoresnode_modules, so gate 3e would have refused 100% of the time on this repo.open-prafter a PR exists flipped mode toworktree, collapsed origin's head, and discarded local commits while reporting success.Plus 7 nits (rename/space-safe
statusparsing, bash 3.2set -uarray expansion, teardown naming on exit paths, and others).Round 3 added ~230 lines (cherry-pick path,
--authored-worktree,--in-place, real mode guards) which no subagent review has seen — reviewing that here is the point of opening the PR.A note on process: a solo verification pass returned "clean" before each of rounds 1 and 2, which then found 5 and 7 blocking defects respectively. Treat the sections above as the areas where direct inspection has been demonstrably unreliable.