---
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.
---

**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.

**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, and the marker is how you find it (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, 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. `.claude/settings.json` allows exactly this
   command, so auto mode's classifier never sees the write, which it denies as
   an external one when spelled as inline `gh api`. `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.

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.
