# Finish

> Finish a Linear issue — check off requirements, add completion comment, commit/push, mark Ready For Release. Autonomous mode via the `auto` token (every prompt resolves to the conservative default — abort, never override; used by /auto). Use when the user says 'finish issue', 'done with this issue', 'complete PL-XX', or invokes /finish.

- Skill: `alienfast/finish` (Agent Skill)
- Install (CLI): `npx skillmds@latest add alienfast/finish`
- Raw SKILL.md: https://api.skillmd.com/api/skills/alienfast/finish/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: alienfast (https://skillmd.com/u/alienfast)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/alienfast/finish

---


# Finish Issue

Automates the post-completion workflow for a Linear issue using the `linear-cli` CLI. The mechanical steps (worktree-mode detection, issue-ID resolution, Linear posts, git commit/push) are delegated to scripts in `~/.claude/scripts/`; this skill is the orchestrator + LLM-judgment surface (reading the description, composing the completion comment).

## Arguments

- Issue identifier (e.g., `PL-12`) — optional, auto-detected from branch/commit
- `no push` / `don't push` / `skip push` — optional, skips the git push step (commit still happens)
- `merge` — only meaningful inside a `/start wt` worktree. **Default when in a worktree.** Merge the worktree branch back into its recorded source branch, then remove the worktree.
- `pr` — open a pull request for the current branch (works from **any** branch). Inside a `/start wt` worktree the base is the recorded source branch and the worktree is left in place; otherwise the base is the repo's GitHub default branch (in-place — no worktree touched). Optionally apply labels with `with label X` / `label X, Y`. The issue stays `In Progress` (the PR is open, not yet shipped).

- `auto` — optional token (case-insensitive, position-agnostic). **Autonomous mode**: every user prompt in this skill resolves to its conservative default instead of asking — abort rather than override, proceed-without rather than create. Each prompt site below documents its auto default inline. Passed through by `/full auto` for the `/auto` loop; the `/auto` invocation is the commit/push grant (see standards/git.md).

Examples: `/finish`, `/finish PL-12`, `/finish no push`, `/finish PL-12 no push`, `/finish merge`, `/finish auto PL-12 merge`, `/finish pr PL-12`, `/finish pr with label pr-deploy`

## Invariant

**`pnpm check` must pass before committing or pushing code.** Check failures are always CRITICAL — never "pre-existing", never "out of scope", never deferred. Fix them before proceeding. Turborepo caching makes repeated runs cheap.

## Workflow

### Preflight: Exit Plan Mode If Active

If the session is in plan mode when `/finish` is invoked, call `ExitPlanMode` **before any other step**. Every step from Step 0 onward needs Bash, Write, or Edit — all blocked in plan mode — so `finish-detect-mode.sh` would fail on its first call otherwise.

**Detection.** Use the harness's plan-mode indicator visible at skill entry (the same signal that was gating tool calls just before this skill loaded). If that indicator is ambiguous or unavailable, attempt Step 0; if `finish-detect-mode.sh` fails with a plan-mode block, return here, call `ExitPlanMode`, then retry Step 0. Do NOT speculatively call `ExitPlanMode` when plan mode is not active — it raises a spurious approval prompt the user must dismiss.

**Plan body.** Pass a one-line plan summarizing what `/finish` is about to do for the resolved issue. There is nothing to design — `/finish` is a fixed mechanical workflow — but `ExitPlanMode` is the only way to leave plan mode and it requires a plan body. For the `<ISSUE-ID>` substitution: only inline a user-supplied token if it matches `^[A-Z]+-[0-9]+$` (case-insensitive, uppercase it before substituting); otherwise use `the current branch's issue`. This keeps malformed tokens (e.g., `PL13`, stray `merge`/`pr` keywords) out of the plan body and out of any rejection-terminator that echoes the same value.

- Approved by the user: proceed to Step 0.
- Rejected by the user: proceed to the rejection terminator below.
- Tool-error / harness failure (not a user rejection — the tool itself returns an error, or the harness reports `ExitPlanMode` failed for a non-user-cancel reason): surface the error verbatim and stop with `BLOCKED-ON-REVIEW: <ISSUE-ID or "current branch"> — ExitPlanMode failed: <first line of error>. No state change.` Do NOT continue to Step 0; plan mode is still active and Step 0 will compound the failure.

**On user rejection** via the approval UI, treat as an abort and stop with:

```text
BLOCKED-ON-REVIEW: <ISSUE-ID or "current branch"> — user rejected /finish at the plan-mode preflight. No state change.
```

Do not retry, do not re-prompt, do not run any subsequent step. **Skip this preflight only when plan mode is NOT active** — `/finish` is normally invoked from a non-plan session after `/start` or manual development, in which case this section is a no-op.

### Step 0: Detect Worktree Mode

Normalize the user's args before calling the script:

- Look for the `auto` token (case-insensitive, position-agnostic) — it selects autonomous mode for this skill's prompt sites and is NOT passed to the script (its arg contract is `merge|pr|--no-push` only).
- Look for `merge` and `pr` tokens (case-insensitive, position-agnostic) — pass through whichever is present (if both, the script errors). **The token you were dispatched with is authoritative — never substitute the other flow.** Receiving `merge` and then running `finish-detect-mode.sh pr` is a real, repeated defect (measured 2026-08-20: an unattended issue shipped as a PR that way, the reasoning being that recent issues in the repo had shipped as PRs); if the token looks wrong for this repo, say so and STOP rather than switching.
- Look for `no push` / `don't push` / `skip push` — translate to `--no-push` for the script.
- Look for label requests (`with label X`, `label X, Y`, `--label X`) — collect them into a list and **carry it forward to Step 9** (pr mode only). Labels are NOT passed to the script — its arg contract is `merge|pr|--no-push` only. **If labels were requested but the resolved `ACTION` is not `pr`** (i.e., `merge` or the standard flow), labels have no PR to attach to — warn the user once (`Labels apply only to /finish pr; ignoring: <list>.`) rather than silently dropping them.

```bash
~/.claude/scripts/finish-detect-mode.sh [merge|pr] [--no-push]
```

The script probes worktree state, validates incompatible argument combinations, and emits these `KEY=value` lines on stdout: `ACTION`, `SOURCE_BRANCH`, `WORKTREE_BRANCH`, `WT_DIR`, `REPO_ROOT`, `NO_PUSH`, plus `CORRUPTION` and `IDENTITY_SOURCE` (and, only on exit 4, `CORRUPTION_REASON` / `CORRUPTION_OWNER_IS_ME` / `EXPECTED_BRANCH` / `EXPECTED_BASELINE` / `EXPECTED_SOURCE_BRANCH` — see exit 4 above and Step 0.5). **Read those values and carry them forward** — Step 9 substitutes them into bash commands as literal strings (each Bash tool call is a fresh shell).

**Exit codes:**

- 1 — incompatible args (e.g., `merge` + `pr`, or `pr` + `no push`). Surface the error and stop.
- 2 — `merge` requested outside a `/start wt` worktree. Surface and stop. (`pr` is **not** rejected here — it works from any branch; see below.)
- 4 — **worktree identity mismatch.** The worktree no longer matches the identity `/start` stamped (branch swapped, HEAD reset off its stamped baseline, `start.source-branch` config wiped, or — `lineage-clobbered` / `lineage-unauditable` — committed work dropped by a same-lineage rewrite the ancestry check cannot see, or a drop window the preservation audit cannot certify; the clobbered case lists the dropped commits on stderr) while the immune identity sidecar still proves it's a `/start wt` worktree. Two causes look identical to the ancestry check: a parallel-session hijack (the PL-454/PL-460 failure mode) or this session's own un-restamped history rewrite (the BF-505 failure mode) — `CORRUPTION_OWNER_IS_ME=1` marks the latter as likely, but is never proof (a foreign reset while the owner is live reads the same). The script emits `CORRUPTION=1`, `CORRUPTION_REASON`, `CORRUPTION_OWNER_IS_ME`, `IDENTITY_SOURCE`, and `EXPECTED_BRANCH` / `EXPECTED_BASELINE` / `EXPECTED_SOURCE_BRANCH` alongside the usual context. **Do NOT run the normal flow** — merging an unverified tree is the exact risk the stamp exists to stop. Jump straight to **Step 0.5**, carrying those values forward.

When `SOURCE_BRANCH` is set (we're in a worktree), the script defaults `ACTION` to `merge`; `/finish pr` is the way to opt into the PR flow with `base = SOURCE_BRANCH`. Outside a worktree, `pr` is allowed (it emits `ACTION=pr` with an empty `SOURCE_BRANCH`, and Step 9 targets the repo's default branch), while `merge` is rejected (exit 2).

**Register isolation for a standalone background `/finish` (worktree mode only).** When `SOURCE_BRANCH` is set, Step 9 writes into `<WT_DIR>` — the merge-commit message, and any conflict-resolution edits. In the `/full` flow `/start` Step 0 already entered the worktree via `EnterWorktree`, so those Write-tool edits land under the guard (this is what the "editable even under bgIsolation" lines in Step 9 rely on). But a **standalone** `/finish merge` in a **background session** — the manual-recovery path, or a user who backgrounds `/finish` — never ran `/start`, so the worktree is unregistered and those edits would be guard-blocked. If this session did not enter the worktree, register it here at Step 0 — Steps 5, 7, and 9 all Write-tool-write into `<WT_DIR>` (`tmp/linear-description`, `tmp/finish-commit`, the merge-commit message): `cd '<REPO_ROOT>'` first **only if** cwd is currently inside the worktree (to avoid the same-cwd refusal), then `EnterWorktree(path=<WT_DIR>)` — which registers isolation and switches cwd to the worktree. **Never** disable the guard (`worktree.bgIsolation: "none"`) or edit settings to weaken it; if a worktree Write is still blocked, STOP and surface rather than ship a partial merge.

If both `SOURCE_BRANCH` and `ACTION` are empty, this is the standard `/finish` flow.

### Step 0.5: Worktree Corruption Recovery (only when `finish-detect-mode.sh` exits 4)

The worktree's identity no longer verifies. Either a parallel `/start wt` session reset it out from under us (PL-454/PL-460, ~8 `/full wt` in parallel), or this session's own deliberate history rewrite detached the stamp without a restamp (BF-505). For the hijack case the intended work is typically uncommitted edits that survived the reset, recoverable by `finish-recover.sh`: it salvages that work to a patch, re-forks a fresh branch off the **current** source tip, re-applies, gates on `pnpm check`, commits, and merges.

**Posture: detect-and-stop with ONE confirmation.** Recovery infers which-files-are-mine heuristically when the branch was reset, so never run it unattended. **Auto mode:** do not ask, do not recover, do not restamp — stop immediately with `BLOCKED-ON-RECOVERY: <ISSUE-ID> — worktree identity mismatch (<CORRUPTION_REASON>); recovery requires a human. Worktree preserved at <WT_DIR>.` (The sanctioned restamp belongs at rewrite time in the session that performed the rewrite — `wt-restamp.sh` run at finish time on a mismatch would self-authorize exactly the signal a live-owner hijack forges.)

1. **Surface the mismatch** from Step 0's output: `CORRUPTION_REASON`, `CORRUPTION_OWNER_IS_ME`, `IDENTITY_SOURCE`, `EXPECTED_BRANCH` vs the current `WORKTREE_BRANCH`, `EXPECTED_BASELINE`, `EXPECTED_SOURCE_BRANCH`. State plainly what recovery will do (salvage → fresh `.claude/worktrees/<id>-recovered` off `EXPECTED_SOURCE_BRANCH` → re-apply → `pnpm check` → commit → merge → retire the corrupted worktree).

2. **Self-rewrite fast path (interactive only).** When `CORRUPTION_OWNER_IS_ME=1`, `CORRUPTION_REASON=baseline-detached`, and `EXPECTED_BRANCH` matches `WORKTREE_BRANCH`, the likely cause is a deliberate rebase/reset by this session or its user — confirm that with them, and on confirmation run `~/.claude/scripts/wt-restamp.sh '<WT_DIR>'` and re-run Step 0's detection. **What licenses continuing is the RE-RUN reporting `CORRUPTION=0`** — the restamp's own exit 0 is necessary but not sufficient, so never skip the re-run or read its verdict off the restamp. On a clean re-run, continue the **normal** flow — no recovery, no salvage; the tree was never foreign-touched. If the restamp exits **non-zero** (2, 3, 4, 5, or an undocumented code), or the re-run still reports `CORRUPTION=1`, fall through to the recovery question below, surfacing the script's stderr verbatim. **Exit 5** specifically means the rewrite dropped commits the branch used to carry (the script lists them) — a foreign reset and a deliberate drop look identical here, so only after the user explicitly confirms those drops were intentional may the fast path re-run once as `~/.claude/scripts/wt-restamp.sh --acknowledge-lost '<WT_DIR>'`. Never pass that flag unprompted, and never in auto mode.

   **`lineage-clobbered` with `CORRUPTION_OWNER_IS_ME=1` takes the same fast path via `--acknowledge-lost` — and the audit can fire on an orphaned MERGE alone.** The preservation audit checks content and reachability separately: `git cherry` compares patch-ids and never sees a merge commit, so `git rev-list --merges` is the only check that can. Measured: `git commit --amend` on a merge leaves `cherry` reporting nothing and `rev-list --merges` reporting the pre-amend merge, with the two trees identical — `WTID_AUDIT_LOST` empty, `WTID_AUDIT_LOST_MERGES` not. Read the two stderr headings separately (`commits … HEAD no longer contains:` vs `merge commits no longer reachable:`) and tell the user which fired and what it lists. Only after they explicitly confirm the drops were deliberate, run `~/.claude/scripts/wt-restamp.sh --acknowledge-lost '<WT_DIR>'` and re-run Step 0's detection — the RE-RUN reporting `CORRUPTION=0` licenses continuing, exactly as above; never unprompted, never in auto mode. Exit 3 means the restamp could not match a session id (owner attribution falls back to harness-pid equality when an id is missing on either side; the restamp never does): fall through to the recovery question, which for this reason must carry the flatten warning in item 3.

   **Never carry `wt-restamp.sh` into a handoff.** Ownership is proven by session-id equality alone, so it refuses every caller that is not the stamped owning session (exit 3, `cannot prove this is the owning session`) — a human at a terminal presents no session id at all and can never clear it. A blocked message is by definition read by someone else, so naming restamp in one sends its reader to a command that cannot run. Route to the recovery question in item 3 instead.

3. **Ask the user (single message, then wait):**

   > Worktree for `<ISSUE-ID>` no longer matches its stamped identity (`<CORRUPTION_REASON>`; owner-is-me=`<CORRUPTION_OWNER_IS_ME>`). Recover automatically? Reply `yes` to run `finish-recover.sh`, or `abort` to stop and inspect manually.

   **When `CORRUPTION_REASON` is `lineage-clobbered`, the question must say what `yes` costs.** `finish-recover.sh` salvages by flattening the branch to one `git diff <fork>` patch replayed as ONE commit on a fresh fork, so every merge commit and its message is destroyed — a real loss when the audit's only drops are orphaned merges whose tree HEAD already carries (history is broken; the tree is not). Add the listed drops to the question, state that `yes` flattens them away and that `abort` leaves the worktree for a human to repair reachability by hand, and on `abort` carry those SHAs in the `BLOCKED-ON-RECOVERY` line. **Never merge with `_WT_SKIP_IDENTITY_CHECK=1` instead.** It has been done once, and the landed tree even matched a prior read-only `git merge-tree` prediction — which is exactly what makes it look safe. That variable is recovery-internal — `finish-merge.sh` honours it only because recovery re-invokes the merge on a freshly stamped worktree — and it switches off `wt_identity_verify` along with the audit, demands no ownership proof, and re-opens the detect→merge window a parallel session can hijack across, the reason `finish-merge.sh` re-runs both checks at all. It is strictly weaker than the `--acknowledge-lost` path it would stand in for. (`_FM_SKIP_OURS_GUARD` is a different kind of skip: the guard it bypasses has already printed exactly what the bypass discards; nothing enumerates what this one gives up.)

   On `abort`: stop with `BLOCKED-ON-RECOVERY: <ISSUE-ID> — worktree identity mismatch (<CORRUPTION_REASON>); recovery declined. Worktree preserved at <WT_DIR>.` No state change. Do not run any further step.

4. **On `yes`:** Write the work-commit message (must contain the issue ID, e.g. `PL-13: <summary>`) to `<REPO_ROOT>/tmp/finish-commit-<issue-id-lowercased>.md`. **In a worktree-isolated session the guard blocks this `REPO_ROOT` write** — write it via the shell (heredoc / `printf` redirect), not the Write tool: it is gitignored, run-scoped bookkeeping owned by this workflow (the same pattern as `/auto`'s state file), and never a reason to touch `worktree.bgIsolation`. Then run from the MAIN checkout:

   ```bash
   cd '<REPO_ROOT from Step 0>'
   ~/.claude/scripts/finish-recover.sh '<WT_DIR>' '<EXPECTED_BASELINE>' '<EXPECTED_SOURCE_BRANCH>' '<EXPECTED_BRANCH>' '<REPO_ROOT>/tmp/finish-commit-<issue-id-lowercased>.md'
   ```

   Route on `finish-recover.sh`'s exit code (it prints `RECOVER_DIFF_STRATEGY=<strategy>` on stderr — quote it in the closing line):

   - **0** — recovered + merged. The merge owns the Ready-For-Release transition (as in Step 9 exit-0): run `~/.claude/scripts/mark-ready-for-release.sh <ISSUE-ID>`, then emit `SHIPPED-MERGE: <ISSUE-ID> — worktree identity mismatch recovered; work salvaged (<strategy>), re-forked off <EXPECTED_SOURCE_BRANCH>, merged, Ready For Release.` Terminal.
   - **2** — conflict applying/merging in `.claude/worktrees/<id>-recovered`. Resolve there (read the conflicted files listed on stderr, fix, `git -C '.claude/worktrees/<id>-recovered' add <files>`, `pnpm check`), then **re-run the same `finish-recover.sh` line** (it resumes the recovered worktree). If genuinely unresolvable, emit `BLOCKED-ON-RECOVERY: <ISSUE-ID> — recovery conflict in .claude/worktrees/<id>-recovered; resolve and re-run finish-recover.sh.`
   - **3** — merge deferred to the queue (transient). Emit `DEFERRED-MERGE: <ISSUE-ID> — recovered work queued (<reason>); will retry automatically. Check with /merge-queue.` (The drainer marks Ready For Release when it lands.)
   - **4** — `pnpm check` failed in the recovered worktree. Emit `BLOCKED-ON-RECOVERY: <ISSUE-ID> — pnpm check failed in the recovered worktree; fix in .claude/worktrees/<id>-recovered and re-run finish-recover.sh.`
   - **1** — setup failure (source branch gone, nothing salvageable, or the identity stamp on the recovered worktree failed). Surface the script's stderr and emit `BLOCKED-ON-RECOVERY: <ISSUE-ID> — recovery setup failed: <first stderr line>. Inspect <WT_DIR> manually.` **If the failure was the identity stamp**, the salvage already succeeded: the work is committed on the recovery branch in `.claude/worktrees/<id>-recovered` (not `<WT_DIR>`, which is the corrupted original and holds nothing new), and the script resumes from that committed state — say so, point at the recovered worktree, and **re-run the same `finish-recover.sh` line** once the cause is cleared (a concurrent config writer, or a read-only `.git`).

Step 0.5 is terminal for the recovery case — do NOT continue to Steps 1–9. (The work being recovered is the same code `/quality-review` already passed in `/start`, so no separate verdict gate runs here; the user's `yes` is the gate.) The one exception is item 2's self-rewrite fast path: a successful restamp re-verified by a clean Step 0 re-run exits Step 0.5 entirely and continues the normal flow from Step 1 — nothing was recovered, so every normal gate (including Step 1.5's verdict read and Step 8) still applies.

### Step 1: Identify the Issue

```bash
~/.claude/scripts/detect-issue-id.sh [--input <USER-SUPPLIED-ID>]
```

The script tries `--input` → current branch → latest commit subject, in that order. Pass `--input` only when the user typed an explicit ID (e.g., `/finish PL-12`). On exit 1, ask the user for the identifier explicitly.

**Cross-worktree sanity check (standard-flow only).** After the issue ID is resolved, if the standard flow was detected in Step 0 (`ACTION` empty — no worktree config and no explicit `pr`) but the issue's branch exists in a known linked worktree of this repo (`git worktree list` shows a path whose basename or branch contains `<issue-id-lowercased>`, e.g., `pl-13`), warn the user before continuing:

> Issue `<ISSUE-ID>` appears to live in worktree `<path>`. Are you running `/finish` from the wrong cwd? Reply `yes` to proceed here anyway, or `abort` and `cd` into the worktree first.

Continue only on explicit `yes`. **Auto mode:** never proceed here — stop with `BLOCKED-ON-REVIEW: <ISSUE-ID> — issue appears to live in worktree <path>; wrong cwd for an unattended /finish. No state change.` This catches the case where `/start wt` created a worktree, the user opened a fresh terminal in the main checkout, and ran `/finish PL-13` from there — which would otherwise push/commit on the wrong branch. Skip the check entirely when Step 0 detected a worktree (in which case `SOURCE_BRANCH` is set and we're already in the right place), when `ACTION` is `pr` (an explicit `/finish pr` is a deliberate choice to open a PR for the current branch — never a wrong-cwd accident), or when no issue ID was resolved (nothing to check against).

### Step 1.5: Read Quality-Review Verdict + Sub-issues

```bash
~/.claude/scripts/finish-read-verdict.sh PL-12
```

Emits seven `KEY=value` lines: `VERDICT_FILE`, `VERDICT`, `CYCLES`, `SUB_ISSUES`, `SUB_ISSUES_ERROR`, `VERDICT_STALE`, `VERDICT_STALE_REASON`. **Read those values and carry them forward** — Step 4 embeds them in the completion comment, Step 8 gates the `Ready For Release` transition on `VERDICT` and `VERDICT_STALE`.

`VERDICT` is one of:

- `passed-clean` / `passed-after-fixes` — `/quality-review` converged cleanly. Step 8 proceeds without prompting.
- `terminated-with-open-items` / `escalated-to-architect` — non-passing. Step 8 hard-refuses by default (override prompt; see Step 8).
- `malformed` — verdict file exists but cannot be parsed (no `Verdict:` line, the line contains the pipe-separated schema example, or the value is not one of the four recognized enums). Step 8 hard-refuses; the user clearly ran `/quality-review` but the handoff is broken, so silently passing the gate would defeat the safety check.
- `none-found` — no verdict file exists at either the current worktree's `tmp/` or the main checkout's `tmp/`. `/quality-review` was either never run for this issue or was run from a different repo. Step 8 hard-refuses by default (override prompt; see Step 8).

`SUB_ISSUES` is the parent issue's **current `children` array from Linear** — i.e., every sub-issue that exists under this parent right now, not necessarily ones filed by this `/quality-review` run. Step 4 surfaces this list as context (labeled accordingly), not as a "filed this run" claim.

`SUB_ISSUES_ERROR` is populated only if `linear-cli issues get` failed (CLI unauthenticated, missing issue, network blip). Step 1.5 does NOT abort on this — `linear-cli auth oauth` is offered in Step 2's error handling if needed, and the rest of `/finish` can proceed without sub-issue context. Surface the warning text in chat once when populated.

`VERDICT_STALE=1` means the verdict file's mtime predates HEAD's commit time — additional commits landed AFTER `/quality-review` ran, so the verdict does not reflect current code. Step 8 escalates passing-but-stale to refuse-with-override (same shape as `malformed`), preventing the gate from sailing through on an out-of-date verdict. The `VERDICT_STALE_REASON` field carries diagnostic text for the override prompt.

### Step 2: Get Issue Details

```bash
linear-cli issues get PL-12
```

Read the description carefully. Note:

- Requirement checkboxes (`- [ ]` items)
- Success criteria checkboxes
- Any "Nice to Have" vs "Must Have" distinctions

### Step 3: Read Current Description as JSON

```bash
linear-cli issues get PL-12 --output json
```

Identify each `- [ ]` checkbox and decide which were completed this session. Don't post anything yet — Step 5 sends the updated description and the completion comment together.

### Step 4: Generate Completion Comment

Write a markdown comment summarizing the work. **Every `<...>` token below is a substitution site — replace each one with the resolved value before posting; never emit a literal `<placeholder>` to Linear.** Template:

```markdown
## Implementation Complete

Branch: `<actual branch name>`

### What was done
- Bullet points of key changes (files created/modified, features implemented)

### Design decisions
- Key technical choices and why they were made

### Verification
- What was verified (type checks, tests, dev server, etc.)

### Adversarial review
- Verdict: <VERDICT value from Step 1.5> (cycles: <CYCLES value from Step 1.5>)
- Sub-issues (current children of this issue): <comma-list of SUB_ISSUES from Step 1.5, or the bare word `none` (no quotes) when empty>
- Open items: <text extracted from VERDICT_FILE's Open items: section, only when VERDICT=terminated-with-open-items, escalated-to-architect, or malformed>

### Notes
- Any unchecked items with explanation of why
- Any follow-up work identified
```

Omit sections that have no content (e.g., skip "Notes" if everything was completed). Omit the **Adversarial review** section entirely when `VERDICT=none-found` (no `/quality-review` ran). When the verdict is passing, drop the `Open items` bullet but keep the other two.

**Reconcile the plan's filing promises before posting.** Re-read this issue's plan comment and any checkpoint comments for promised filings — "a follow-up will be filed", an out-of-scope census "recorded, not skipped", work explicitly named for later. Every such promise must resolve to an issue ID named in this comment, or be filed now (via `/quality-review` Step 6 sub-step 6's recipe, search-before-filing included) before the comment posts. A promise without an ID is a silent leak: nothing downstream ever re-reads the plan, so an unfiled promise vanishes with the session (measured on BF-894: the plan's out-of-scope census promised a follow-up covering four surfaces; BF-1171/BF-1172 covered two, and `ApprovalSignerAccessLoader` + `Types::Tenant#incoming`/`#outgoing` were silently dropped, surfacing only at a later keeper audit).

**An unimplemented `### Nice to Have` requirement: one line in Notes, and stop — it never files, never blocks, never carries forward.** The tier means target of opportunity (`standards/issue-spec.md`, the spec-template section): implement one only when it fell out easily from the work in hand; declining the opportunity is the normal outcome and creates no obligation. Do not file a follow-up — not even when a dependency gates it: a gated opportunity is simply not opportune, and filing it is feature creep — an optional brainstorm line converted into tracked work nobody prioritized (BF-1182, canceled at keeper review: a skipped notify-others line filed `human` behind a Customer.io dependency on BF-1123's precedent — but BF-1123 gates a **Must Have**, and the analogy fails on the tier). For this tier specifically, this overrides the reconciliation rule above: a plan comment that promised a follow-up for a Nice to Have was wrong to promise (`/start`'s planning rule forbids it), and the correction is the Notes line recording the decline — never the filing the promise asked for. The only path by which a skipped Nice to Have becomes work is a human promoting it into `### Must Have` through `/spec`.

### Step 5: Post Description Update + Completion Comment

Write both files:

1. `tmp/linear-description-<issue-id-lowercased>.md` (e.g., `tmp/linear-description-pl-12.md`) — full description with `- [ ]` flipped to `- [x]` for completed items. Preserve everything else exactly.
2. `tmp/linear-comment-<issue-id-lowercased>.md` (e.g., `tmp/linear-comment-pl-12.md`) — completion-comment body from Step 4.

Then post both in one call:

```bash
~/.claude/scripts/finish-post-update.sh PL-12 tmp/linear-description-pl-12.md tmp/linear-comment-pl-12.md
```

Exit codes: 1 (validation — missing/empty files), 2 (Linear API failure).

### Step 6: Verify Check Passes

Run `pnpm check` as a hard gate before committing:

```bash
pnpm check
```

If it **fails**: this is CRITICAL. Do not commit or push. Fix the failures first, then re-run until it passes. **Auto mode:** bound to **2** fix delegations; if still red, stop with `BLOCKED-ON-REVIEW: <ISSUE-ID> — pnpm check failing at /finish gate after 2 unattended fix attempts. Nothing committed.`

If it **passes**: proceed to commit.

### Step 7: Git Commit & Push

1. Stage relevant files by name (`git add <files>`). Never `git add -A` / `git add .` (per CLAUDE.md). **Derive the list from a fresh `git status --short` run now, at commit time — never from a file list captured earlier in the session.** Review-fix cycles routinely touch files beyond the original implementation set; staging from a stale list ships a commit whose halves don't match (local `pnpm check` passes on the working tree while the committed tree fails CI's type check). After staging, confirm `git status --short` shows no remaining unstaged *tracked* modifications that belong to this issue.
2. Write the commit message to `tmp/finish-commit-<issue-id-lowercased>.md` (e.g., `tmp/finish-commit-pl-13.md`). The issue ID **must** appear in the message (the script enforces it for Linear auto-linking). **Only `<ISSUE-ID>` may follow a Linear close keyword** — Linear scans commit messages as well as PR titles and bodies, so a body sentence like "follow-up to PL-14" is safe while "fixes PL-14" moves PL-14 on merge; see [`standards/git.md`](../../standards/git.md) § Linear auto-close keywords for the keyword set and the safe forms. This bites hardest unattended, where the commit body is the only prose an `/auto` run writes:

   ```text
   PL-13: <short imperative summary>

   <optional body explaining the why>
   ```

3. Run the commit script:

```bash
~/.claude/scripts/finish-commit.sh PL-13 tmp/finish-commit-pl-13.md [--no-push]
```

**`--no-push` is required in TWO cases — easy to miss the second:**

1. The user requested `no push` / `don't push` / `skip push` (Step 0 translates these to `NO_PUSH=1`).
2. **`ACTION=merge`** — the temp branch is about to be merged into source and deleted locally; pushing it pollutes origin with abandoned branches. The merge commit reaches origin only when a human pushes the source branch — deliberately manual, no skill or script owns that push (keeper ruling 2026-08-16), so origin lags Linear until the keeper pushes.

If either condition holds, pass `--no-push`. The script does NOT enforce this rule (it has no awareness of `ACTION`), so the orchestrator MUST gate on `NO_PUSH=1 OR ACTION=merge`.

The script handles all three states: pre-staged changes (commit + push), already-committed-but-ahead (push only), already-synced (no-op). If staging is missing for an unstaged-only state, it errors with exit 2 — go back and `git add` the files.

### Step 8: Mark Issue as Ready For Release

**Skip when `ACTION == "pr"`.** In PR mode, the work is not yet shipped — review and merge are still pending. Leave the issue in `In Progress`; the transition to `Ready For Release` happens after the PR merges (manually, or via a follow-up `/finish` once the worktree branch is merged into source).

**Auto mode — the gate below never prompts.** Every refuse-with-override branch (`VERDICT_STALE=1`, `terminated-with-open-items`, `escalated-to-architect`, `malformed`, `none-found`) resolves to `abort`: emit that branch's `abort` terminator (`BLOCKED-ON-REVIEW: ... — <reason>, auto mode refused the override. No state change.`) and stop. Never override, never re-run unattended. **`none-found` keeps its own abort terminator** — unattended, "no review artifact" means unreviewed code, which never ships (`BLOCKED-ON-REVIEW: <ISSUE-ID> — no /quality-review artifact; unattended runs never ship unreviewed. Run /quality-review then /finish manually.`). The failing tag is `/auto`'s signal to count a failure and surface the issue to a human.

In all other cases (no worktree, or `ACTION == "merge"`), gate the transition on the `VERDICT` from Step 1.5. **Every `<...>` token in the prompt and comment bodies below is a substitution site** — replace each with the resolved value before emitting; never write a literal `<placeholder>` to chat or to Linear. The Step 4 substitution rule applies here too.

**Who performs the `Ready For Release` transition (read before any branch below).** The transition belongs to whoever *completes the lifecycle*, so Linear never shows `Ready For Release` for code that is not yet merged:

- **Standard flow (`ACTION` empty):** Step 8 runs `linear-cli issues update --state "Ready For Release"` inline (the commands in the branches below) — there is no merge to wait for.
- **`ACTION == "merge"`:** Step 8 runs the verdict **gate only** (the proceed / abort / override decisions below) and does **NOT** run `linear-cli issues update`. The merge owns the transition: Step 9 runs it via `~/.claude/scripts/mark-ready-for-release.sh <ISSUE-ID>` **only after `finish-merge.sh` exits 0** (the merge actually landed), and the launchd drainer runs the same script when it lands an async deferral. So wherever a branch below says "proceed with the state update", in `merge` mode that means **proceed to Step 9 without changing Linear state** — the gate passed; the merge (now, or later via the queue) transitions it. On a deferred merge (exit 3) the issue therefore stays **In Progress**, which is the truth: it is not released until it is merged. **Override-comment wording in merge mode:** the override comments in the branches below — posted when the user accepts a stale/failing verdict — still post here at gate time, but in `merge` mode they must record *authorization*, not a completed transition. Replace the literal `marked Ready For Release` in any such comment body with `authorized Ready For Release (the merge applies it when it lands)` — otherwise the comment re-tells the very lie this ordering exists to prevent (asserting a release state while the code is unmerged and the issue is still In Progress). The standard flow (`ACTION` empty) keeps `marked Ready For Release` — there the state really is changed here.
- **`ACTION == "pr"`:** Step 8 is skipped entirely (handled above).

**Step 8 termination contract — applies to ALL branches below.** Per `standards/lifecycle-tags.md`, every terminal path of `/finish` Step 8 ends with exactly one tagged final line. (The Preflight has its own independent terminator — `BLOCKED-ON-REVIEW` on plan-mode rejection or `ExitPlanMode` tool failure — and never reaches Step 8.) Mechanical mapping (do not skip):

- A branch that completed `linear-cli issues update --state "Ready For Release"` AND `ACTION` from Step 0 is empty (standard flow, no Step 9 to follow) → emit `RELEASED: <ISSUE-ID> — <one-line summary>` as the last LLM-authored line.
- A branch that passed the verdict gate AND `ACTION == "merge"` → do NOT change Linear state and do NOT emit a tag here (per "Who performs the transition" above, merge mode defers the state update). Step 9 owns BOTH the Ready-For-Release transition (after the merge lands) and the terminal line (`SHIPPED-MERGE:` on a completed merge, or `DEFERRED-MERGE:` when `finish-merge.sh` exits 3 and the merge is queued — the issue stays In Progress until it lands). (`ACTION == "pr"` never reaches a state update — Step 8 is skipped for it — so it isn't in these bullets; Step 9 owns `SHIPPED-PR:`. Discriminate on `ACTION`, not `SOURCE_BRANCH`: a non-worktree `pr` has an empty `SOURCE_BRANCH` yet still flows to Step 9.)
- A branch that exited via the user picking `abort` at the gate prompt → emit `BLOCKED-ON-REVIEW: <ISSUE-ID> — <one-line reason>` as the last LLM-authored line. State was NOT changed. (`re-run` never exits — it dispatches and resumes; see the block below.)
- **A branch where `linear-cli issues update` itself failed** (API error, auth dropped mid-session, team's terminal state name differs from `Ready For Release`) → see the **State-update failure** section below for the recovery + terminator rule. **This bullet supersedes bullets 1 and 2 whenever the state update doesn't succeed** — never emit `RELEASED:` on a failed update.

The per-branch instructions below indicate which terminator each branch uses; trust the contract above for the literal tag wording.

**`re-run` dispatches the review — it is never a stop (applies to every gate prompt below).** Each prompt offers `re-run` as "invoke `/quality-review`", and the reply means exactly that: the user asked this session to run the review, not to be told to run it themselves (measured 2026-08-27: a session answered the none-found prompt's recommended "Run /quality-review first" and got a stop instructing it to run the review by hand). On `re-run`:

1. Invoke `/quality-review <ISSUE-ID>` via the Skill tool and let it run to completion.
2. Resume at Step 1.5 — run `finish-read-verdict.sh` again and carry the fresh `VERDICT`/`VERDICT_STALE`/`SUB_ISSUES` forward. Do NOT repeat Steps 2–5: the description update and completion comment already posted.
3. Re-run Steps 6–7 — the review's fix loop may have left uncommitted edits (`/quality-review` holds no commit grant), and `finish-commit.sh` no-ops when the tree is already synced. The same `--no-push` gate applies. Reading the verdict before committing the fix delta mirrors the normal Step 1.5 → Step 7 ordering, so the carried `VERDICT_STALE=0` stays honest.
4. Post a short comment with the fresh verdict via `~/.claude/scripts/linear-post.sh` (reuse Step 4's Adversarial-review section shape) — the completion comment posted before this review ran, so its verdict line is stale, or omitted entirely on the `none-found` path.
5. Re-enter this gate on the fresh values. A `re-run` reply has no terminator of its own — the resumed gate (or Step 9) ends the flow, and a still-non-passing fresh verdict simply prompts again.

Auto mode never reaches this block — every refuse-with-override branch resolves to `abort` unattended (above).

**State-update failure recovery (applies to every branch that attempts `linear-cli issues update --state "Ready For Release"`).** If the call exits non-zero:

1. Inspect the error. If it's a "no such state" rejection (the team uses a different terminal state name), apply this probe-and-match fallback — analogous to `/start` Step 8.5's CANCELED/ABANDONED fallback and `/quality-review` sub-step 6's fallback:
   - Derive the team key from the issue ID prefix (e.g., `PL-13` → team `PL`). Then probe: `linear-cli statuses list -t PL`.
   - Pick the first state whose name matches `/^ready[ _-]?for[ _-]?(release|deploy|ship)$/i` (exact match — NOT a prefix match — to avoid latching onto `Ready For Review`; the `[ _-]?` separator class matches `Ready For Release`, `Ready_For_Release`, `Ready-For-Release`, `ReadyForRelease`).
   - If found, retry `linear-cli issues update <ISSUE-ID> --state "<matched-name>"`. If it succeeds, emit the standard-flow terminator `RELEASED:`. **This whole recovery applies only to the standard flow** (`ACTION` empty) — per "Who performs the transition" above, `ACTION == "merge"` does not run `linear-cli issues update` in Step 8 at all, so there is no Step-8 update to recover here; the merge owns the transition (Step 9 / the drainer via `mark-ready-for-release.sh`, which carries this same fallback).
   - If no match, OR if the retry also fails, fall through to step 2.
   - **Note on bare `Ready`:** the regex deliberately requires `Ready For <release|deploy|ship>` and does NOT match a bare `Ready` state. A team's `Ready` state is too ambiguous (could mean ready-for-review, ready-for-QA, etc.) to auto-route into — the issue falls through to step 2's BLOCKED-ON-REVIEW. To use bare `Ready` as a release state, rename it to `Ready For Release` or add canonical config.
2. Surface the error to the user and emit `BLOCKED-ON-REVIEW: <ISSUE-ID> — linear-cli issues update failed: <rea

…(truncated)
