finalize · diff

git:20261027.c9ec8c5 to git:20261027.88cb9bb

6 added, 85 removed. Audit B to B.

---
- description: Finalize (a.k.a. "prep merge") — land prep: verify there's a draft PR, run the quality passes (`/polish`), run the vet suite, merge the base branch, mark ready for review, propose a squash body and settle the PR title it lands under, and post the attestation comment. Pass an optional branch/PR target first (`/finalize <branch|#PR|PR-url>`) to attach to that existing branch before finalizing. `/finalize no vet` is the docs-only mode; `/finalize revoke` withdraws an attestation; `/finalize and merge` squash-merges the PR as well, but only if the run turned up nothing to decide. An operator's ask to merge a branch not yet finalized — "merge", "мерджи", in any words — is `/finalize and merge`, and the reply opens by saying so.
+ description: Finalize (a.k.a. "prep merge") — land prep: verify there's a draft PR, run the quality passes (`/polish`), run the vet suite, merge the base branch, mark ready for review, propose a squash body and settle the PR title it lands under, and post the attestation comment. Pass an optional branch/PR target first (`/finalize <branch|#PR|PR-url>`) to attach to that existing branch before finalizing. `/finalize no vet` is the docs-only mode; `/finalize revoke` withdraws an attestation; `/finalize and merge` squash-merges the PR as well, but only if the run turned up nothing to decide. An operator's ask to merge — "merge", "мерджи", in any words — is `/merge`, which sends a branch not yet finalized here.
---
**Optional branch/PR target**: if the first token of the argument is a branch
name, `#NNN` PR number, or PR URL, then `/finalize <target>` is shorthand for
attaching to that branch first and then finalizing — equivalent to
`/from-branch <target> finalize`. Load `@.claude/skills/from-branch/SKILL.md`
and follow it to attach to `<target>`, then run the finalize steps below. If the
argument has no target token, skip this and finalize the current branch as
usual.
**`no vet` is the docs-only mode.** Pass it when the diff has nothing to verify
— markdown, top-level docs, read-only reference data. It skips the vet run
(there is nothing for it to check) and skips any bucket dispatch. It does
**not** skip step 7: the attestation still goes up, stating that verification
was a deliberate no-op and why. Everything else below still applies. (`no ci`
and `no attest` are accepted spellings of this flag.)
**`revoke` withdraws the attestation instead of making one** — for a branch that
needs work again after it was finalized. It runs
`@.claude/skills/finalize/revoke.md` and nothing else.
**`and merge`** — recognized anywhere in the argument, bare `merge` too — asks
you to squash-merge the PR yourself at step 8, but only if the run gives you no
- reason not to. **An operator's ask to merge** a branch not yet finalized
- ("merge", "мерджи", in any words) is this flag. Either way, load
- `@.claude/skills/finalize/merge.md` before anything else: it says how the reply
- opens, and the step-8 gate is its.
+ reason not to. An operator's ask to merge reaches it through
+ `@.claude/skills/merge/SKILL.md`. Load `@.claude/skills/finalize/merge.md`
+ before anything else: the step-8 gate is its.
**Pre-check**:
- If `HEAD` is detached, create a branch with a meaningful name derived from the
work (e.g., task id or topic) and check it out.
- Run `gh pr view --json isDraft,number,url,baseRefName` on the current branch.
- No PR → push the branch and create a **draft** PR (`gh pr create --draft`).
- PR exists but is non-draft → convert it back to draft
(`gh pr ready --undo`).
- Draft PR exists → continue.
- **Resolve the PR's base branch** from `baseRefName` — this is the branch the
work merges into and is gated against, `<base>` in every step below. It's
usually `main`, but a project with promotion branches or long-lived feature
branches will have PRs based elsewhere. Everywhere a step compares against or
merges from the merge target, use `origin/<base>`, not a hardcoded
`origin/main`. (If there's no PR yet — the no-PR case above — treat `<base>`
as the repo default branch.)
**Two-shot rule**: on any failure, try up to two rounds of fixing, then stop and
ask the user. Commit after each round of fixes.
**What actually gates this PR.** Establish this once, at the top, because
everything below depends on it: check whether any workflow runs on a PR in this
repo (`.github/workflows/` triggers, or `gh pr checks` on an existing PR).
- **If nothing runs on a PR**, _you_ are the entire gate — nothing downstream
will report on this branch before someone merges it. Step 1 is the only thing
that runs the project's checks before the merge, which is why vetting is not
optional on this path even though ordinary development skips it. An empty
Checks tab after step 4 is then the expected end state; do **not** push an
empty commit to "relaunch CI".
- **If a workflow does run on PRs**, the vet run still happens here — it's
faster and it catches things before a reviewer sees red — but step 4 also
means watching that run to green (`@.claude/skills/watch-ci/SKILL.md`), and
step 7 attests to both.
Either way, **step 7's attestation comment is the record.** A reviewer cannot
see from the diff what was run; if it isn't written down it did not happen as
far as anyone else is concerned.
**First, swap in the staged copies.** Run `scripts/staged.sh swap` — a no-op
when nothing is staged — and commit what it `git add`ed
(`chore: swap the staged always-loaded files into place`). A branch edits the
files that load on every turn through copies under `.claude/staged/`
(`@.claude/rules/staging.md`), and everything below reads the real ones: the
passes review them, and the vet run checks them. Where the real file changed
after it was staged, the swap merges; a conflict it stops on is left as markers
in the real file, resolved before anything else runs. `no vet` does not skip
this.
**Then, the quality passes.** Before step 1, load and follow
`@.claude/skills/polish/SKILL.md` over this branch. Land prep is the funnel
every branch reaches whatever route its work took, so this is where a branch
written without `/go` in front of it still gets the passes — and the last point
at which changing the diff is cheap, since the vet run, the base merge, the
squash message and the attestation's SHA are all statements about a diff that
has stopped moving.
It carries no step number because the numbers below are cited from other skills
and stay put; it runs before step 1 regardless. **`no vet` does not skip it** —
a docs-only diff is squarely what both passes are for: `/tend-prose` by
construction, and `/dry` because a constraint stated in two places is a
duplication like any other.
Steps (stop on first unresolved failure):
1. **Vet**: `./scripts/vet.sh`. Fix and rerun until green.
**This step is the branch's whole local verification** — the vet run plus any
check too slow for it that CLAUDE.md § "Vetting" sends here — and step 7
vouches only for a head it ran on. A merge that brings base commits in makes
a head nobody has built, however green both sides were alone, so **after any
such merge this step runs again, all of it, on the merged head** before the
run goes on. `/sync-branch`'s own vet run counts as the re-run's
`./scripts/vet.sh`. `no vet` skips the re-run as it skips this step.
2. **Merge the base branch**: load `@.claude/skills/sync-branch/SKILL.md` and
follow it, passing `no vet` in `no vet` mode. It owns the merge — the
conflicts committed as git left them, the resolution in a commit of its own,
the read of what the base brought in for overlaps and optimizations — and the
vet run over the merged tree. If you can't resolve a conflict, ask. When it
brought commits in, re-run step 1 per the rule there.
3. **Working-artifact cleanup**: some directories ride the branch for in-flight
review but must never land on the base. Sweep the two below **now**, deleting
each **entire** tree (not just the current session's subfolder) and including
the deletions in the **last** pushed commit before step 4.
(`docs/remove-before-merging/` is the exception — it is swept at the **end**
of step 6, after the merge check; see there for why.)
- **`docs/issue/`** and **`docs/pr/`** — issue and PR exports from
`scripts/export-github-item.py`, plus the transient PR body
`scripts/pr-body.py` parks under `docs/pr/<n>/`. If **any**
`docs/issue/<n>/` or `docs/pr/<n>/` tree exists, remove it — `git rm -rf`
what is tracked, plain `rm -rf` what a hook left untracked. Both exports
are committed earlier on purpose, and the two differ in how often:
`@.claude/skills/take-issue/SKILL.md` Step 2 commits an issue once, a
thread being a snapshot any resuming agent (post-compaction handoff,
parallel session) can read instead of re-exporting;
`@.claude/skills/handle/SKILL.md` Step 2 commits a PR export every time it
re-takes one, because the PR moves while the branch works, and the
resulting sequence is what shows a reviewer which state of the PR each turn
was answering. Paired with those adds, the deletion cancels out in the
squash.
- **`docs/plans/`** — file-based plans from `@.claude/skills/plan/SKILL.md`
(the web-session stand-in for plan mode). If **any** `docs/plans/*.md`
exists — in any lifecycle state (`.draft.do-not-implement.md`,
`.in-progress.md`, `.paused.md`, `.completed.md`) —
`git rm -r docs/plans/`. Same rationale: the plan was committed so the
operator could pull and review it, but it's a working artifact — the
add-then-delete pair cancels out in the squash so the plan never reaches
the base branch. **Don't limit any sweep to the current session** — clean
up any other `docs/issue/<m>/`, `docs/pr/<m>/`, or `docs/plans/*.md`
leftovers earlier agents/operators forgot too; there's no reason for these
to ever live on the trunk. Do **not** skip this step — leaving any folder
in means it ships.
4. **Ready for review**: `gh pr ready`. If the repo runs a workflow on PRs,
watch it to green now (`@.claude/skills/watch-ci/SKILL.md`) and fix what it
surfaces under the two-shot rule. If nothing runs on a PR here, there is
nothing to chase — an empty Checks tab is the expected end state, and pushing
an empty commit to "relaunch CI" relaunches nothing.
5. **Reconcile the squash message**: invoke
`@.claude/skills/squash-message/SKILL.md` — load and follow it, passing
`<base>` as the diff base. A proposal normally already exists, posted when
the PR opened and still tracked on the branch, so this is an **edit** to a
live doc in light of the merge, any review comments and the final committed
state — not a fresh composition. It owns the title/body format, the
draft-then-tighten pass, and its outputs (the PR title, the body comment
beside the merge button, and both in the transcript). Do not draft the
message inline here: printing an untightened first draft is the failure mode
that skill exists to prevent.
6. **Dispatch any CI-only bucket the diff reaches, then check the base.**
If the project has hydrated `/test-on-gh`, decide from the diff whether it
reaches a surface the vet run doesn't cover (one that needs real credentials,
real services, or a browser) and dispatch that bucket if so. It's a judgement
call, not a reflex: don't dispatch a bucket the diff never touches, and don't
skip one because the diff "looks fine". A red dispatch is a real failure —
fix it under the two-shot rule. If `/test-on-gh` is still a stub, say so in
the report rather than implying full coverage.
Then, before ending the turn, run `/check-merge`
(`@.claude/skills/check-merge/SKILL.md`) **once**: verification takes a
while, so `origin/<base>` has often advanced while it ran and the target you
just validated against may already be stale. Whatever the outcome, the final
report **must state the `origin/<base>` SHA this check verified against**
(short SHA + subject, e.g. `main @ 8903396 "style: …" as of this check`) —
the operator compares it against current `origin/<base>` at a glance without
re-running `/check-merge`.
- **contained** (no movement) → go to step 7, then **end the turn** so the
session reads as idle; the operator can run `/check-merge` again when they
return.
- **`origin/<base>` advanced** → load and follow
`@.claude/skills/finalize/base-advanced.md`, then step 7.
- **PR merged/closed** → the work landed — nothing more to do. This is
**one** automatic check, not a loop: after acting on it once, end the turn
rather than re-checking — further movement is the operator's `/check-merge`
to catch on return.
- Do NOT merge the PR here — step 8 owns that, and only under `and merge` and
the gate it states. Absent the flag the operator controls the final
title/body and merge.
**Then, once the merge check is done, sweep `docs/remove-before-merging/`** —
after `scripts/staged.sh check --empty` passes, since a copy still under
`.claude/staged/` carries a change that would otherwise land as a stray
duplicate of the file it stands for; a failure means run the swap above,
never delete the copy. Where the cost ledger ships, the session rows go in
the same commit: `python3 .claude/costs/ledger.py backfill HEAD` first,
because a branch that merged the ledger in mid-flight carries rows the
`cost-ledger` branch never received, then `git rm -r .claude/costs/sessions`
— they are display copies, and the trunk carries none. Then `git rm -r` the
**entire** tree, not just the `squash-message.md` that
`@.claude/skills/squash-message/SKILL.md` tracks there: the directory's name
is a standing declaration that nothing inside it may land, so a screenshot or
anything else an agent parked there goes with it. Paired with the earlier
add, the deletion cancels out in the squash. Push it before step 7 so the SHA
the attestation names is the branch's final head.
**Why this tree goes last, after everything else.** The squash proposal is a
live doc for the whole time the PR is in flight: every edit to it — a review
comment, what the base merge brought in, a fix pushed after a red dispatch,
work `/check-merge` just pulled in from an advanced base — is a deliberation
over the text that's already there. Sweeping it at step 3 would leave the
single most important edit, the one that becomes the permanent `git log`
record, as the only one made with the doc gone; sweeping it before
`/check-merge` would do the same to whatever that check drags in.
A directory name is not self-enforcing: upstream, this tree reached the trunk
anyway, carrying a ~1MB deploy-log dump with customer email addresses in it.
So verify the sweep actually ran — and **never end the turn with the tree
still on the branch**, since from here the branch is merge-able: by the
operator, or by step 8 itself.
7. **Attest** — the last act, and the only durable record that this branch was
- verified beyond whatever CI reports. Post it on the PR — or, on a re-run,
- edit it in place; it is sticky, keyed on the `<!-- finalize-attestation`
- marker (posting mechanic below the templates). The marker carries the head's
- full SHA, which the cost ledger's `Stop` hook parses
- (`.claude/costs/lib/finalized.py`) to stop writing display copies onto a
- finalized branch:
-
- ```md
- <!-- finalize-attestation <full-head-sha> -->
-
- ### Attestation
-
- Verified <short-sha> (`<branch>`) against `<base>` @ <base-short-sha> "<base subject>".
-
- - `./scripts/vet.sh` — **green**
- - CI on this PR — **<green (run link) | nothing runs on a PR in this repo>**
- - CI-only buckets — **<dispatched and passed (run link) | not dispatched: the diff doesn't reach them | /test-on-gh is an unhydrated stub>**
-
- <If the head is past the verified commit: "<head-short-sha> merges `<base>` @ <base-short-sha> on top, judged independent of this branch and not re-verified: <why>.">
-
- <If nothing runs on a PR here: "No workflow runs on a PR; this comment is the verification record.">
- ```
-
- `Verified <short-sha>` is the last head step 1 ran on and the marker is the
- final head. They differ only after
- `@.claude/skills/finalize/base-advanced.md` judged a late advance
- independent, so anything that trusts the attestation as a green light
- compares the two.
-
- In `no vet` mode the comment still goes up — same marker, same place — saying
- that there was nothing to verify:
-
- ```md
- <!-- finalize-attestation <full-head-sha> -->
-
- ### Attestation — none required
-
- <short-sha> (`<branch>`) against `<base>` @ <base-short-sha> "<base subject>".
-
- Finalized in `no vet` mode: <why the diff has nothing to verify — e.g. "markdown only, no code path touched">. `./scripts/vet.sh` was skipped rather than run green, and no bucket was dispatched.
-
- <If nothing runs on a PR here: "No workflow runs on a PR; this comment is the verification record.">
- ```
-
- **Posting it: `scripts/pr-comment.sh attestation <n> <file>`**, run from the
- repository root with nothing chained in front, as the script's header
- requires, the file holding the comment as templated above. It edits the
- comment that leads with the marker in place, or posts one when there is none.
- `gh pr comment --edit-last` looks like the tool for this and is not: it
- targets your most recent comment on the PR whatever that happens to be, so on
- the ordinary finalize ordering — squash proposal posted at step 5,
- attestation at step 7 — it overwrites the squash proposal with the
- attestation. Both comments are then one comment, and the loss is silent.
-
- Rules that make it worth trusting:
- - **Attest what you vetted, not what you meant to vet.** If the vet run was
- not green, or a dispatch was skipped for a reason other than "the diff
- doesn't reach it", or you stopped under the two-shot rule — say exactly
- that, in the comment. An honest partial attestation is useful; a tidy one
- that overstates is worse than none. That includes naming a stub:
- "`./scripts/vet.sh` is not implemented in this project yet" is an honest
- line, and it tells the reviewer exactly how much the attestation is worth.
- - **One comment, edited in place.** Re-running `/finalize` after a
- base-advanced re-verify updates it; it must always describe the branch's
- current head, not the first pass. A branch finalized once in `no vet` mode
- and later re-finalized for real replaces the no-op body with the full one.
- - **A no-op is a result, and it gets posted.** Never resolve `no vet` by
- staying silent. An operator coming back to the PR — days later, or from
- another machine — cannot distinguish "verification was skipped on purpose"
- from "nobody ever finalized this" unless one of them is written down, and
- the missing comment looks identical to the forgotten one. Say which it was.
- - **The SHAs go bare, not in code spans** — GitHub only auto-links an
- un-backticked hash, per CLAUDE.md § "GitHub comments".
- - End the body with the attribution footer the repo requires of every GitHub
- comment.
- - **A head that moved only by `.claude/costs/sessions/` is still the head you
- verified.** The cost ledger's `Stop` hook relies on it: it stops writing a
- branch's display copy once the marker's SHA holds, and treats a commit
- touching anything else past that SHA as the branch reopened. Re-attest only
- for a commit that does.
+ verified beyond whatever CI reports. Load
+ `@.claude/skills/finalize/attest.md` and follow it.
8. **Merge — under `and merge` only, and only if the run was uneventful.**
Without the flag, stop at step 7 and report. With it, walk the gate in
`@.claude/skills/finalize/merge.md`, which owns the merge and the report.