Skip to content

vt-d-5-finalize

Phase 5 - Final quality verification and merge readiness using the finalization-orchestrator. Runs security audit, migration checks, and compliance validation.

Plugin: vt-product-dev
Category: Development Workflow
Command: /vt-d-5-finalize


Phase 5: Finalize - Quality Verification & Merge Readiness

Verify your code is safe to merge with security audits, migration checks, and compliance validation.

Workflow Position

/activate → /vt-d-2-plan → /vt-d-3-build → /vt-d-4-review → [ /vt-d-5-finalize ] → /vt-d-complete
                                                                ▲ YOU ARE HERE
                                                                ↑ deliverable_types determines gate enforcement

Invocation

/vt-d-5-finalize                     # Standard finalization check
/vt-d-5-finalize --env production    # Specify environment
/vt-d-5-finalize --migration         # Include migration safety check

Intent Constraints

This skill MUST NOT: - Proceed without both .review-gate.md (status: PASS) and .test-gate.md (status: PASS) - Override a NO-GO decision without explicit user approval and documented rationale - Skip the security scan, even for urgent deployments - Modify any source code, tests, or configuration (verification only) - Downgrade the severity of any finding to achieve a GO decision

This skill MUST STOP and surface to the user when: - A finalization check returns an unexpected or ambiguous result - npm audit reveals critical vulnerabilities with no available fix - The review gate branch does not match the current branch - The test gate branch does not match the current branch (when code is a deliverable type) - The review gate's head differs from the current HEAD and the commits since then change more than the review record (this spec's review-triage.md and state.yaml, plus docs/review-history.md — exact filter in Step 0) - The test gate's head differs from the current HEAD and the commits since then change more than the review record (when code is a deliverable type) - Migration safety analysis identifies potential data loss

Execution Instructions

This command invokes the finalization-orchestrator agent.

Step 0a: Read Phase Checkpoint

If .claude-checkpoint.md exists in the project root: 1. Read the file 2. Display: Resuming from checkpoint: {completed_phase} completed at {timestamp} 3. Display the "Context for Next Phase" section content 4. If next_phase is 4-review (review not yet completed): HARD BLOCK — display "Code review required before finalization. Run /vt-d-4-review first." and stop. 5. If next_phase doesn't match 5-finalize (other mismatch): warn (advisory, not blocking)

If no checkpoint exists: proceed silently.

Step 0: Review Gate Check (Mandatory)

Before any finalization work, verify that code review has been completed and passed.

Note: After this gate check passes, proceed to Step 0.5 before continuing.

Step 0.5: Project Registration Check

Run the shared registration check — single source of truth. Do NOT inline the logic here (duplicating it re-introduces the cross-copy drift SPEC-150 removed):

bash ~/.claude/skills/vt-c-project-register/scripts/check_registration.sh

Act on the emitted status line (stdout). This phase runs as an isolated sub-agent (context: fork), so it MUST NOT prompt — an AskUserQuestion here is swallowed and the sub-agent dies with no output (see the fork rules later in this file). Report, never ask: - REGISTERED, TOOLKIT_REPO, NOT_GIT, or DEGRADED … → continue silently. - UNREGISTERED skips=N escalate=<bool> → add one non-interactive line to your final report: "Note: this project isn't registered in the toolkit, so its learnings won't reach the intake pipeline — run /vt-c-project-register from an interactive phase." Then continue. Do NOT call AskUserQuestion, and do NOT --record-skip here (a skip means the user declined registration, which only an interactive phase can establish).

Step 0.7: Read Deliverable Types

  1. Read deliverable_types from specs/[N]-feature/spec.md YAML frontmatter
  2. If present and non-empty: use as-is
  3. If missing: warn "No deliverable_types in spec frontmatter — defaulting to [code]" and treat as [code]
  4. Display: "Deliverable types: {list}"

The detected types determine which gates are enforced and which finalization checks are run.

  1. Read .review-gate.md from the project root

  2. If file is missing:

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    BLOCKED: No Review Gate Found
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    Code review has not been run. Finalization requires a passing review.
    
    Run /vt-d-4-review first, then retry /vt-d-5-finalize.
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    STOP. Do not proceed with finalization.

2b. If completeness: INCOMPLETE (SPEC-192 FR-6) — check this BEFORE status:

The gate says the review did not finish, or finished without measuring. Its verdict — whatever it says — rests on an incomplete pass, so it must block with the same weight as a missing gate. The gate records four different causes, and they need different remedies, so print the part that applies.

⚠ It was two until pass 14, and that was a hole in this exact message (pass 14, document-quality H1 / data-integrity M-4). reviewers_deferred and reviewers_failed have been uncovered states in the gate FILE since pass 8 — validate-review-gate.sh derives uncovered over all four — but this block enumerated two, so a gate that is INCOMPLETE only because a reviewer was stood down or failed printed a BLOCKED banner with no cause and no remedy: "every dispatched reviewer is accounted for, and yet incomplete" is the shape this spec calls a violation, and it was in the spec's own operator message. It still STOPped, so nothing was ever finalized wrongly; what failed was the requirement that the gate be legible without the spec. Enumerate all four here, or the next state added silently reopens this.

  • reviewers_unmeasured is non-empty — those reviewers returned but could not run what they needed. Read degraded_reason from the gate BODY and print it verbatim; a generic "review incomplete" tells the operator nothing they can act on, and the whole content of SC-2 is that they learn which command was refused and which rule would grant it.
  • reviewers_outstanding is non-empty — those reviewers never returned. There is no degraded_reason for them (the review writes it only for unmeasured reviewers), and no rule to grant: the remedy is to re-run the review. Never print an empty or invented degraded_reason for this case — a placeholder in a BLOCKED message sends the operator looking for a permission problem that does not exist.

  • reviewers_deferred is non-empty — those reviewers were deliberately stood down for this pass. There is no permission to grant and nothing failed: the remedy is a decision, not a fix — either dispatch them and re-run, or record why a pass without them is acceptable.

  • reviewers_failed is non-empty — those reviewers were dispatched and did not complete. Unlike an unmeasured reviewer there is no refused command to grant, and unlike an outstanding one the failure is known: print whatever the gate records about it, then re-run the review for those reviewers.

Any combination can be true at once; then print every part that applies.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BLOCKED: Review Gate INCOMPLETE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[only if reviewers_unmeasured is non-empty]
Returned without measuring: [reviewers_unmeasured]
[the gate's degraded_reason text, verbatim — never a paraphrase and never a placeholder]
→ Grant the rule named above (or apply its stated remedy), then re-run /vt-d-4-review.

[only if reviewers_outstanding is non-empty]
Never returned: [reviewers_outstanding]
→ Re-run /vt-d-4-review; these reviewers produced no report to grade.

[only if reviewers_deferred is non-empty]
Stood down for this pass: [reviewers_deferred]
→ Dispatch them and re-run /vt-d-4-review, or record the decision to grade without them.

[only if reviewers_failed is non-empty]
Dispatched and did not complete: [reviewers_failed]
→ Re-run /vt-d-4-review for these reviewers; there is no rule to grant here.

`"$CLAUDE_PLUGIN_ROOT/../vt-base/bin/validate-review-gate.sh" .review-gate.md` checks that this ledger
is consistent. On this gate it exits 0 and prints `finalizable=no`: exit 0 means the
ledger is well-formed, never that finalization may proceed.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If $CLAUDE_PLUGIN_ROOT is not set, locate the script relative to this file at ../../../vt-base/bin/validate-review-gate.sh. The ../vt-base/ hop is the ONLY arm — there is no primary in front of it — and it is deliberate; do not drop it. It is what resolves in a consumer project, where the repo-relative path measures No such file or directory, rc=127, which this step treats as STOP (SPEC-192 pass 8, P8-C1). $CLAUDE_PLUGIN_ROOT/../vt-base/ is correct from BOTH plugins: from vt-product-dev it hops sideways, and from vt-base it collapses to itself.

⚠ RE-INSTATED, and this is the THIRD turn of this same paragraph (BUG-340). It once forbade the repo-relative path outright. SPEC-192 H-9 then made that path primary, because it is the only spelling a permission rule can pin. BUG-340 MEASURED that spelling failing in a real consumer project on 2026-09-29 — "No such file", which reads to an operator as "the toolkit has no validator". The prohibition is back, and this time it rests on a measurement rather than on an argument.

The grant from Decision 9 buys nothing at this call site, and that is accepted: no rule can match a $CLAUDE_PLUGIN_ROOT path, so this MANDATORY step will prompt. A command that cannot RUN is worse than one that prompts — ungrantable and correct beats grantable and broken.

STOP. Do not proceed with finalization.

⚠ Order is load-bearing. This branch sits before the status: FAIL branch on purpose: that branch ends in STOP, so anything placed after it is unreachable and FR-6 would never fire. An INCOMPLETE gate is usually also a FAIL, which is exactly why the more specific message has to come first.

  1. If status: FAIL:
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    BLOCKED: Review Gate FAILED
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    The last code review did not pass.
    Critical: [critical_count] | High: [high_count]
    
    Fix the issues listed in the review, then re-run /vt-d-4-review.
    Only after the review passes can /vt-d-5-finalize proceed.
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    STOP. Do not proceed with finalization.

3b. Validate the collection-barrier ledger (BUG-202) — MANDATORY, mechanical:

```bash
bash "$CLAUDE_PLUGIN_ROOT/../vt-base/bin/validate-review-gate.sh" .review-gate.md
```

If `$CLAUDE_PLUGIN_ROOT` is not set, locate the script relative to this skill file at
`../../../vt-base/bin/validate-review-gate.sh`, resolving the skill file's real path first
(deployed skills are symlinks into the toolkit checkout).

**Never call it by a repo-relative path such as `plugins/vt-base/bin/…` (BUG-340).** This skill
runs in the project being finalized, not in the toolkit, so such a path exists only when the
project *is* the toolkit. It did for months, and every other project got "No such file", which
reads as "the toolkit has no validator". The `../vt-base/` hop is the same one Step 0c uses, for
the same reason (BUG-037): `$CLAUDE_PLUGIN_ROOT` names the calling plugin, `vt-product-dev`, and
the script lives in `vt-base`. If the file does not resolve, that is a **toolkit defect**:
report it as `FAIL` with the path tried, and do not skip this step.

**On a branch that does not name the spec — above all `main`, where Step 0b item 3c sends a
cross-repository test gate — write `.active-spec` before this call.** The validator's rule R9
asks `lane-detect.sh status` which spec the run belongs to, and the helper finds one in exactly
two ways: the ID on the first line of `.active-spec` in the repository root (an ID registered
in `.design-state.yaml` whose `specs_dir` holds a `spec.md`), or a branch named
`feature/spec-NNN-*`. On `main` only the file is left. `.active-spec` is local state and
usually gitignored, so a fresh checkout has none, and R9 then exits 1 with
`spec-less gate (lane-detect.sh status: none)` although the spec exists (F055 SPEC-010,
2026-10-11). Write the ID of the spec the review gate belongs to:
```bash
printf '%s\n' "[SPEC-ID]" > .active-spec
```
An ID that is not registered, or whose `specs_dir` has no `spec.md`, still answers `none`.
Never write an ID to get a branch that has no spec past R9 — that refusal is the rule (SPEC-164 D9).

Exit 0 **and the OK line says `finalizable=yes`** → continue. Exit 0 alone is NOT a go — it
means the ledger is well-formed, and a well-formed ledger can still record an INCOMPLETE or a
FAIL pass. **Exit 1 → STOP**, and print the validator's own output verbatim;
it names the reviewers that were dispatched and never accounted for.

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BLOCKED: Review Gate Ledger Invalid
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[validator output — names the unaccounted reviewers]

The review did not account for every reviewer it dispatched, so its verdict
covers fewer reviewers than it claims. Re-run /vt-d-4-review.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

⚠ **This step is mechanical on purpose, and that is the whole point of it.** The barrier
it enforces was already written in `/vt-d-4-review` Step 2.9 on 2026-08-10, the gate
schema already carried `reviewers_returned` / `reviewers_outstanding` / `completeness`,
and the invariant was already stated — as a YAML *comment*. On 2026-08-13, three days
later, a review dispatched 9 of 11 reviewers and graded PASS anyway. Both missing
reviewers held a finding no other reviewer reached, and a gate on the nine would have
been clean (BUG-202).

A rule that nothing evaluates is a comment. Do not replace this call with a reading of
the gate — the reading is what failed.

**On that STOP, first print the most specific block below that applies** — step 6 when
`status` is absent or anything other than `PASS`/`FAIL` — and only fall back to the
"Ledger Invalid" block above when none does. Step 6 is reached **from here**, not by falling
through to it.

> ⚠ **Why that sentence exists (pass 13, H-5).** `finalizable=yes` requires
> `completeness: COMPLETE` **and** `status: PASS`, so it is false for every gate with no
> `status` — the validator reports that shape as `status=n/a finalizable=no`. Without this
> clause a statusless gate stopped here under the heading "Review Gate Ledger Invalid", which
> is a **misdiagnosis**: the ledger may be perfectly well-formed, and the actual defect is that
> no verdict was reached. `promote/SKILL.md` and `finalization-orchestrator.md` both carry this
> remedy and this file did not — two of three consumers, which is the recurring shape pass 13
> named. **Before adding any STOP to this step, state which branches below it you have just
> made dead.**

> ⚠ **Which gates reach 3b, stated plainly.** Only those that are neither `INCOMPLETE` nor
> `FAIL`: steps 2b and 3 both end in STOP and both come first, on purpose — the more specific
> diagnosis wins, and the "Order is load-bearing" note above 3 is the rule that says so. So
> "MANDATORY" here means *mandatory on every gate that could still finalize*, not *on every
> gate*. Do **not** "fix" this by moving 3b above 2b: `finalizable=no` is true for every
> INCOMPLETE and every FAIL gate, so 3b would swallow both specific blocks — which is exactly
> the pass-9 reorder that `promote/SKILL.md` records in its "Order is load-bearing" block —
> not at the `:76` this line cited until pass 14, which is the `2b.` header (Low) — as moving
> the defect instead of removing it.
  1. If branch doesn't match current branch:
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    BLOCKED: Review Gate Is For A Different Branch
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    Review gate was for branch '[gate-branch]' but you are on '[current-branch]'.
    The review does not describe this branch. Re-run /vt-d-4-review on this branch,
    then retry /vt-d-5-finalize.
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    STOP. Do not proceed with finalization. This is the MUST STOP case listed under Intent Constraints. It used to say "display warning but allow the user to decide" — which contradicted that list, and which this phase cannot do anyway: it runs as an isolated sub-agent (context: fork) and has no way to receive the user's decision (SPEC-205 W4.3 a).

4b. If head is present and doesn't match the current HEAD (BUG-014):

Record-only exception (owner decision 2026-10-01, SPEC-205 review pass 6, P6-PAT-1). /vt-d-4-review writes the gate and then commits its own record (triage, state.yaml), so HEAD always moves past the gate. Before blocking, run both checks:

git merge-base --is-ancestor [gate-head] HEAD
git diff --no-renames --no-relative --name-only [gate-head]..HEAD | grep -vE '^([specs-dir]/[spec-dir]/(review-triage\.md|state\.yaml|\.review-gate\.md|\.claude-checkpoint\.md|metrics\.json)|docs/review-history\.md|\.test-gate\.md)$'
[specs-dir] is the repository's spec root — specs_directory from .design-state.yaml without its trailing slash, specs when none is declared (for example 05-specs) — and [spec-dir] is the directory of the spec being finalized, both written literally (for example 205-virtual-rolf-supervised-autonomy) — never [^/]+: that let another spec's state.yaml or triage pass as this review's record (P10-SEC-6, owner decision 2026-10-04). If the first succeeds AND the second prints nothing, every commit since the gate only records the review: proceed and report ✓ Review gate at [gate-head]; since then only review records changed. Any other file — or a gate head that is not an ancestor — falls through to the block below. The list is exactly these seven paths; widen it only by owner decision. docs/review-history.md was added by owner decision 2026-10-03 (SPEC-205 review pass 8, P7-PAT M-1): Step 7a of /vt-d-4-review is marked MUST run and writes it, so without it the documented review path always ended in a BLOCKED finalize. The other files that step sometimes writes — critical-patterns.md, metrics.json, bugs/<N>/review-triage.md — were deliberately NOT added; they are occasional, and each one widens the control further. Widened by owner decision 2026-10-09 (BUG-394). First, [specs-dir] replaced a hard-coded specs/: in a repo whose spec root is 05-specs/ no record could ever match, so even the three paths above never passed. Second, .review-gate.md, .claude-checkpoint.md and metrics.json of [spec-dir] and the root .test-gate.md joined. A repo that TRACKS its gates — visifair-marker-platform keeps each spec's review gate in the spec directory because its root .review-gate.md belongs to another spec — has /vt-d-4-review commit all four after the gate, so every finalize there blocked, and a re-review only moved the gate again (measured on SPEC-033, 2026-10-09). None of the four is code. A gate file changed after its own head: is read as the gate it now is; this check never vouched for a gate's content, only for the code since it. metrics.json is taken back out of the deliberately-excluded list above; critical-patterns.md and bugs/<N>/review-triage.md stay out. --no-renames is required: by default a rename prints only its NEW path, so moving any file to specs/x/state.yaml passed (P7-SEC-S1, measured). --no-relative keeps diff.relative from hiding files outside the current directory (P7-SEC-W2).

Otherwise:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BLOCKED: Review Gate Is For An Older Commit
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Review gate was taken at [gate-head] but HEAD is now [current-head].
Commits have landed since the review; nobody has reviewed them.
List them with: git log --oneline [gate-head]..HEAD
Re-run /vt-d-4-review on this HEAD, then retry /vt-d-5-finalize.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STOP. Do not proceed with finalization. Owner decision 2026-09-30 (SPEC-205 review pass 5, finding P5-SEC-S-1): the earlier wording "report this and continue" let commits added after a PASS review reach finalize unreviewed, and nobody had decided that. This phase runs as an isolated sub-agent (context: fork) and cannot ask, so "continue with a note" and "continue" are the same thing. The way out is cheap: review again. The only exception is the record-only check above.

⚠ Absent head: is NOT staleness — skip the check silently. Gate files written before this field existed carry no head:, and treating that as stale would block every pre-existing gate. Only a head: that is present AND differs is a signal.

⚠ This is why no hook watches for branch switches. Comparing the recorded SHA to the current one answers staleness wherever the checkout came from — human, IDE, another session, or agent — instead of pattern-matching Bash command strings, which read a file restore as a branch switch and deleted tracked files for weeks (BUG-014). A stale gate record is more useful than an absent one: it names which branch and which SHA it covered.

  1. If status: PASS and branch matches: Display confirmation and proceed.

    ✓ Review gate: PASS (reviewed [date] on branch [branch])
      Critical: 0 | High: [high_count] | Findings: todos/
    

  2. If status is anything else — including ABSENT: STOP. Do not proceed with finalization. Reached from 3b's STOP path, not by falling through — the validator reports this shape as status=n/a finalizable=no, so 3b stops first and prints this block.

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    BLOCKED: Review Gate Carries No PASS
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    The review gate does not record `status: PASS` (found: [the value, or ABSENT]).
    Finalization requires an explicit PASS, not the absence of a FAIL.
    
    Run /vt-d-4-review, then retry /vt-d-5-finalize.
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    Added at pass 13 (H-5): this skill had no arm for a statusless gate while promote/SKILL.md (step 6) and finalization-orchestrator.md (Step 0.5 arm 5) both did. SPEC-192 Step 2.9 item 4 tells /vt-d-4-review to write exactly this shape when a dispatched reviewer never returned, so it is the routine output of the workflow this step gates — not an exotic case.

Provenance check (SPEC-189 FR-4) — attached to the PASS branch only. After reading status, read validator. If it is not agent — including absent, which reads as unknown and never as agent (FR-6) — display:

⚠ This gate's PASS was NOT produced by its prescribed validator (validator: <value>).
  Did not run: <prescribed − validators_run>, from the writing skill's `requires_agents`.
  When no prescribed set is declared, name what DID run instead and say so.

⚠ This is a BLOCKING region, so the notice carries no "Continuing" line — printing "this is a notice, not a block" beside a BLOCKED/STOP would be worse than printing nothing. FR-5 restated for this region: the notice changes no branch. It must never be read as licence to soften a block into a warning.

Step 0b: Test Gate Check

After verifying the review gate, also verify that automated tests have passed.

When code is NOT in deliverable_types: Accept .test-gate.md with status: NOT_RUN. Display "Test gate: NOT_RUN (accepted — no code deliverables)" and proceed. If the file is missing entirely, this is also acceptable for non-code specs — display the same message and proceed.

When code in deliverable_types (existing behavior):

  1. Read .test-gate.md from the project root

  2. If file is missing:

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    BLOCKED: No Test Gate Found
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    Automated tests were not run. Finalization requires passing tests.
    
    Run /vt-d-4-review first (which runs the test suite), then retry /vt-d-5-finalize.
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    STOP. Do not proceed with finalization.

  3. If status: FAIL:

    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    BLOCKED: Test Gate FAILED
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    [failed] of [total] tests failed. Finalization requires all tests passing.
    (source: ci — print instead of the line above: "CI run(s) failed for head [head]:" then each
    failing ci_runs url)
    
    Fix failing tests and re-run /vt-d-4-review.
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    STOP. Do not proceed with finalization.

3c. If source: ci and the gate's commit is not in this repository (cross-repository test gate, BUG-396):

Some projects keep specs and gates in a documentation repository and the code in another. The test gate is then measured in the code repository by ci-test-gate.py and copied here, so its branch: and head: name the CODE repository and can never match this repository's branch and HEAD: items 4 and 4b below would block every finalize, however often the gate is renewed. Detect the case mechanically, for every ci_runs[].head_sha (full SHA):

git cat-file -e "[run-head-sha]^{commit}"
If every one exists here, this is not a cross-repository gate: skip this item and continue with item 4. If none exists here, items 4 and 4b are replaced by a gate-to-gate check: the review gate read in Step 0 must name the code commit it reviewed in code_head: (a full 40-hex SHA), and every ci_runs[].head_sha must equal it exactly. Both gates then describe the same code, and the review gate's own branch and head checks (Step 0, items 4/4b) still hold this repository to the review. Run finalize therefore on a checkout of this repository that is on the review gate's branch: (usually main); a feature branch or worktree fails Step 0 item 4. On that branch the spec is found only through .active-spec, which Step 0 item 3b needs BEFORE this item is reached — write it first, as described there. Report ✓ Test gate (cross-repository) at [code_head] = review gate's code_head and continue with item 5. Anything else — code_head: absent or not 40 hex, any run on another commit, or some runs found here and some not — gets:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BLOCKED: Test Gate Is For Another Code Commit
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

The CI test gate names [run-head-sha], a commit of another repository, but the review
gate names code_head: [code_head, or ABSENT]. The tested code is not the reviewed code.
Renew the test gate for the reviewed commit (ci-test-gate.py in the code repository),
or review the tested commit, then retry /vt-d-5-finalize.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STOP. Do not proceed with finalization. Owner decision 2026-10-11 (BUG-396): the alternative — accept any gate head that is an ancestor of the code repository's main — was rejected, because it lets commits after the review through unreviewed (P5-SEC-S-1). This item applies to source: ci only; a source: local gate is always measured in this repository and keeps items 4 and 4b.

  1. If branch doesn't match current branch:
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    BLOCKED: Test Gate Is For A Different Branch
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    Test gate was for branch '[gate-branch]' but you are on '[current-branch]'.
    The recorded test run does not describe this branch. Re-run /vt-d-4-review on this branch,
    then retry /vt-d-5-finalize.
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    
    STOP. Do not proceed with finalization. A PASS recorded for another branch is not a PASS for this one, so proceeding would break the first MUST NOT under Intent Constraints. This step used to say "Display warning (advisory, same as review gate)" — stale since the review gate's Step 4 began to block (SPEC-205 W4.3 a), and a warning here reached nobody for the same reason: this phase runs as context: fork (SPEC-205 review pass 3, PAT-2).

4b. If head is present and doesn't match current HEAD:

Record-only exception (owner decision 2026-10-01, SPEC-205 review pass 6, P6-PAT-1). /vt-d-4-review writes the gate and then commits its own record (triage, state.yaml), so HEAD always moves past the gate. Before blocking, run both checks:

git merge-base --is-ancestor [gate-head] HEAD
git diff --no-renames --no-relative --name-only [gate-head]..HEAD | grep -vE '^([specs-dir]/[spec-dir]/(review-triage\.md|state\.yaml|\.review-gate\.md|\.claude-checkpoint\.md|metrics\.json)|docs/review-history\.md|\.test-gate\.md)$'
[specs-dir] is the repository's spec root — specs_directory from .design-state.yaml without its trailing slash, specs when none is declared (for example 05-specs) — and [spec-dir] is the directory of the spec being finalized, both written literally (for example 205-virtual-rolf-supervised-autonomy) — never [^/]+: that let another spec's state.yaml or triage pass as this review's record (P10-SEC-6, owner decision 2026-10-04). If the first succeeds AND the second prints nothing, every commit since the gate only records the review: proceed and report ✓ Test gate at [gate-head]; since then only review records changed. Any other file — or a gate head that is not an ancestor — falls through to the block below. The list is exactly these seven paths; widen it only by owner decision. docs/review-history.md was added by owner decision 2026-10-03 (SPEC-205 review pass 8, P7-PAT M-1): Step 7a of /vt-d-4-review is marked MUST run and writes it, so without it the documented review path always ended in a BLOCKED finalize. The other files that step sometimes writes — critical-patterns.md, metrics.json, bugs/<N>/review-triage.md — were deliberately NOT added; they are occasional, and each one widens the control further. Widened by owner decision 2026-10-09 (BUG-394). First, [specs-dir] replaced a hard-coded specs/: in a repo whose spec root is 05-specs/ no record could ever match, so even the three paths above never passed. Second, .review-gate.md, .claude-checkpoint.md and metrics.json of [spec-dir] and the root .test-gate.md joined. A repo that TRACKS its gates — visifair-marker-platform keeps each spec's review gate in the spec directory because its root .review-gate.md belongs to another spec — has /vt-d-4-review commit all four after the gate, so every finalize there blocked, and a re-review only moved the gate again (measured on SPEC-033, 2026-10-09). None of the four is code. A gate file changed after its own head: is read as the gate it now is; this check never vouched for a gate's content, only for the code since it. metrics.json is taken back out of the deliberately-excluded list above; critical-patterns.md and bugs/<N>/review-triage.md stay out. --no-renames is required: by default a rename prints only its NEW path, so moving any file to specs/x/state.yaml passed (P7-SEC-S1, measured). --no-relative keeps diff.relative from hiding files outside the current directory (P7-SEC-W2).

Otherwise:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BLOCKED: Test Gate Is For An Older Commit
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Test gate was taken at [gate-head] but HEAD is now [current-head].
The commits since then have not been tested. Re-run the test suite on this HEAD,
then retry /vt-d-5-finalize.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STOP. Do not proceed with finalization. Same rule as the review gate's Step 4b (owner decision 2026-09-30, P5-SEC-S-1): this text said "advisory, same as the review gate", and the review gate now blocks. Absent head: is skipped silently.

  1. If status: PASS and branch matches: Display confirmation and proceed. Branch on source: — absent reads as local (gates written before SPEC-221). The branch, head and provenance checks above and below apply to both sources unchanged (SPEC-221 FR-7).
  2. source: local:
    ✓ Test gate: PASS ([total] tests, [passed] passed, [duration])
    
  3. source: ci — written by ci-test-gate.py from the CI runs for the head it names (item 4b warns when that is not HEAD); it has no local test counts, so never read their absence as zero tests — reason: a CI PASS means every listed workflow passed; show each ci_runs url:
    ✓ Test gate: PASS (from CI, head [head])
      [workflow] ([os]): [conclusion] — [url]
    

Provenance check (SPEC-189 FR-4) — attached to the PASS branch only. After reading status, read validator. If it is not agent — including absent, which reads as unknown and never as agent (FR-6) — display:

⚠ This gate's PASS was NOT produced by its prescribed validator (validator: <value>).
  Did not run: <prescribed − validators_run>, from the writing skill's `requires_agents`.
  When no prescribed set is declared, name what DID run instead and say so.

⚠ This is a BLOCKING region, so the notice carries no "Continuing" line — printing "this is a notice, not a block" beside a BLOCKED/STOP would be worse than printing nothing. FR-5 restated for this region: the notice changes no branch. It must never be read as licence to soften a block into a warning.

Step 0c: Version Gate Check

When code is NOT in deliverable_types: Skip the version gate entirely. Display "Version gate: SKIPPED (no code deliverables)" and proceed to Step 0a.

When code in deliverable_types (existing behavior):

After verifying review and test gates, verify that plugin version bumps and changelogs are in order.

  1. Run the version gate script using the Bash tool:
    "$CLAUDE_PLUGIN_ROOT/../vt-base/scripts/check-finalize-versions.sh"
    
    If $CLAUDE_PLUGIN_ROOT is not set, locate the script relative to this skill file at ../../../vt-base/scripts/check-finalize-versions.sh.

The ../vt-base/ hop is deliberate — do not "simplify" it to $CLAUDE_PLUGIN_ROOT/scripts/. $CLAUDE_PLUGIN_ROOT names the calling plugin, which for this skill is vt-product-dev, and vt-product-dev has no scripts/ directory. The script lives in vt-base, the required base plugin every other plugin depends on. Both paths above are siblings under plugins/, so they resolve to the same file.

This was BUG-037: SPEC-163 W5 moved this skill from core-standards to vt-product-dev while the script stayed in the base plugin. Both paths had been correct before the split, and neither was updated — so the version gate silently found nothing for every release from 5.0.0 onward.

  1. If output starts with VERSION_GATE: FAIL:

Auto-fix instead of blocking. Determine and apply the version bump automatically:

2a. Determine bump type:

# Check if new files were added under plugins/*/skills/ or plugins/*/agents/
git diff --name-status main...HEAD -- 'plugins/*/skills/' 'plugins/*/agents/'
- If any lines start with A (added): bump type = MINOR (new capability) - If only M (modified): bump type = PATCH (improvement/fix)

2b. Compute new version: - Read current version from plugins/registry.json (parse JSON, extract version for affected plugin) - For MINOR: increment middle number (3.21.0 → 3.22.0) - For PATCH: increment last number (3.21.0 → 3.21.1)

2c. Count current skills:

# Every plugin, not just the base — SPEC-163 spread the skills across eight of them, so
# counting vt-base alone reports 15 against a real 132. And match manifest ROWS, not the
# literal " -> ": each manifest's `# Format: name -> directory` comment contains that
# string, so `grep -c " -> "` overcounts by one per file.
cat plugins/*/.claude-plugin/skill-symlinks.manifest \
  | grep -cE '^[^#[:space:]]+[[:space:]]*->'

2d. Apply auto-fix:

  1. Write the new version into EVERY mirror at once (Bash tool):

    python3 plugins/vt-base/lib/version_mirrors.py set . {name} {new-version}
    
    It prints one line per mirror it wrote — plugin.json, registry.json, the marketplace.json entry (and metadata.version for vt-base), and the | Version | row in docs/plugins/{name}.md. Read the lines: the count is the number of mirrors this repo has for the plugin, and a mirror it skips as absent is one this repo genuinely lacks.

    ⚠ Do not replace this with hand edits of registry.json and plugin.json. That is what this step said until 2026-09-05, and it is BUG-211: those two are what the OLD gate checked, guard 22 checks five, and four bumps in a row (SPEC-169, SPEC-170, BUG-031, BUG-207) passed the gate here and failed guard 22 on CI. The list of mirrors lives in that one module — the gate in Step 1 reads it, guard 22 reads it, and this command writes it. Adding a sixth mirror to the module is the whole change; this step does not need to know. 2. If the skill count changed, update components.skills in plugins/{name}/.claude-plugin/plugin.json (Edit tool) — the version is already written. 3. Generate CHANGELOG entry: - Read active spec title from specs/[N]-feature/spec.md frontmatter (title: field) - Read branch commits: git log main..HEAD --oneline - Write a ## [new-version] - YYYY-MM-DD section at the top of plugins/{name}/CHANGELOG.md:

    ## [3.22.0] - 2026-03-24
    
    ### Added
    
    - [Summarize from spec title and commit messages] (SPEC-NNN)
    
    - If a section for this version already exists in CHANGELOG: skip CHANGELOG generation 4. Re-run the gate script from Step 1. It must now print VERSION_GATE: PASS for this plugin; if it still lists a mirror, the set command above skipped something — stop and read its output rather than editing the file it named by hand. 5. Stage exactly the files the set command listed, plus the changelog:
    git add plugins/registry.json plugins/*/.claude-plugin/plugin.json \
            .claude-plugin/marketplace.json docs/plugins/*.md plugins/*/CHANGELOG.md
    
    6. Commit:
    git commit -m "chore: bump vt-base to [version] (SPEC-NNN)"
    

2e. Report and continue:

✓ Version auto-bumped: 3.21.0 → 3.22.0 (MINOR — new skill added)
  - 5 mirrors written: plugin.json, registry.json, marketplace.json (entry + metadata), docs/plugins/vt-base.md
  - plugin.json skill count updated (104 → 105)
  - CHANGELOG.md entry added
  - version gate re-run: PASS
Continue finalization — do NOT stop or require re-run.

Exception — MAJOR bump: If the script output mentions breaking changes or the spec title contains "breaking" or "remove", do NOT auto-bump. Instead display:

⚠ Potential breaking change detected — manual version bump required.
  Review the changes and bump the MAJOR version manually if appropriate.
STOP only in this case.

  1. If output starts with VERSION_GATE: WARN:

    ⚠ Version gate: WARN
      [List each WARNINGS line from script output]
      [List VERSION_CHANGES lines from script output]
    
    Display warning but proceed with finalization.

  2. If output starts with VERSION_GATE: PASS:

    ✓ Version gate: PASS
    
    If VERSION_CHANGES lines are present, also display them:
    ✓ Version gate: PASS ([list VERSION_CHANGES summaries])
    
    If the NOTE line says "No plugin code changes detected", display:
    ✓ Version gate: PASS (no plugin code changes)
    

  3. If the script does not run, distinguish the two causes — they are not the same failure.

Check first whether the file is there:

ls "$CLAUDE_PLUGIN_ROOT/../vt-base/scripts/check-finalize-versions.sh"

5a. The file is missing (path does not resolve) — this is a TOOLKIT DEFECT, not an environment problem. Report it as FAIL, name the path that was tried, and do not silently proceed:

✗ Version gate: FAIL (script not found at <path tried>)
  This is a toolkit defect, not a local environment issue — the skill is pointing at a path
  that does not exist. File a bug. Do not treat this run as version-gated.
A version gate that cannot find itself has not passed. Record it as a failed check in the finalize gate, not a skipped one, so the GO/NO-GO decision reflects that nothing was verified.

5b. The file exists but errored while running (Python3 missing, permissions, a crash):

⚠ Version gate: SKIPPED (script error — <first line of stderr>)
Display the warning and proceed. Do not block finalization on genuine local-environment issues.

Why these are split. Until BUG-037 both rendered as one "script error — not available" message. That message is indistinguishable from a missing local dependency, so a path broken by the SPEC-163 split read as an environment hiccup the operator was told to ignore — and the gate enforced nothing for every release from 5.0.0 onward without one run reporting a problem. A graceful-degradation branch must never be able to absorb a toolkit defect silently.

Important: Save the VERSION_CHANGES output from this step for use in the TL;DR summary (Step 5).

Step 0a: Check for Spec-Driven Mode

If .specify/ directory exists:

  1. Load constitution - .specify/memory/constitution.md for principles
  2. Derive active spec from branch — check .active-spec file first, then parse branch name (feature/spec-NNN-* → SPEC-NNN), then look up specs_dir in .design-state.yaml
  3. Read completion status from specs/[N]-feature/state.yaml
  4. Load spec - specs/[N]-feature/spec.md for completeness verification
  5. Display status:
    ✓ SpecKit detected - Constitution compliance verification enabled
    • Finalization will verify constitution principles
    • Spec coverage will be checked
    

If NO .specify/:

Standard finalization checks (no spec validation).

Step 1: Load Institutional Knowledge

Before finalization checks:

  1. Load docs/solutions/patterns/critical-patterns.md - Check for known issues
  2. Load docs/runbooks/ - Reference existing procedures
  3. Check recent incidents that might affect this release
  4. If SpecKit: Load constitution and spec from Step 0

Step 2: Run Finalization Checks

The finalization-orchestrator coordinates:

Check Tool Purpose
Repo Governance /vt-c-repo-audit --ci-mode Zero context bleed, LFS compliance
Dependency Audit npm audit / bundle audit Known vulnerabilities
Security Scan security-sentinel Final security review
Migration Safety data-integrity-guardian Zero-downtime migrations
Compliance compliance-checklist GDPR/SOC2 requirements

Step 2 (non-code): Content Finalization Checks

When non-code types present in deliverable_types, run these additional checks:

  1. Link validation — All internal and external links in deliverables resolve correctly
  2. Format verification — Consistent heading hierarchy, proper list formatting, no skipped heading levels
  3. Placeholder detection — No TODO, FIXME, TBD, [fill in], or other placeholder content remains

When document or presentation in types, also run delivery checks (absorbed from kw-4-publish):

  1. Delivery format checklist:
  2. [ ] Correct file format (Markdown, PDF, PPTX as specified)
  3. [ ] File names follow conventions
  4. [ ] Version/date stamp present
  5. [ ] Access permissions appropriate

  6. Stakeholder readiness checklist:

  7. [ ] Executive summary or key highlights prepared
  8. [ ] Key points are prominent and easy to find
  9. [ ] Next steps clearly identified
  10. [ ] Stakeholder communication template generated:
    Subject: [Deliverable name] — Ready for Review
    Body: [Summary of what was delivered, key highlights, next steps]
    

Step 2a: Repo Governance Audit (if .repo-manifest.yaml exists)

Run /vt-c-repo-audit --ci-mode before other checks:

  • FAIL if any context bleed detected (context artifacts in deployable paths or build output)
  • FAIL if oversized untracked binaries found in deployable paths
  • WARN (non-blocking) for traceability gaps or stale sandbox files
  • SKIP if no .repo-manifest.yaml present

This is the "Finalization Auditor" from the Unified Repo Governance model — ensures zero leakage of design, prompt, or prototype files into the production bundle.

Step 2b: State Documentation Maintenance Gate (advisory)

Check whether UI state documentation may need updating based on changed files.

  1. Check for state matrix existence: Look for 01-docs/deliverables/implementation-readiness/ui-state-matrix.md in the project. If the file does not exist → skip this step silently.

  2. Get changed files:

    git diff --name-only $(git merge-base HEAD main) HEAD
    
    If the git command fails → display "State documentation check: SKIPPED (git diff unavailable)" and continue.

  3. Grep for state-affecting patterns in changed file names and content:

  4. Filename patterns: *state*, *State*, *guard*, *route*
  5. Content patterns: enum.*State, interface.*State, type.*State, ngIf, ngSwitch, @if, @switch, [hidden], canActivate

  6. If matches found (advisory, non-blocking):

    State documentation advisory:
    ─────────────────────────────────────────────────────────────────
    UI logic changes detected in {N} file(s). Verify that the state matrix
    (01-docs/deliverables/implementation-readiness/ui-state-matrix.md)
    is still current.
    
    Changed files with state-affecting patterns:
    • {file1}
    • {file2}
    ─────────────────────────────────────────────────────────────────
    

  7. If no matches found → continue silently.

Step 2.5: User Manual Build Gate

Skip silently if docs/user-manual/mkdocs.yml does not exist.

When docs/user-manual/mkdocs.yml exists:

  1. Build the manual:

    cd docs/user-manual && python3 -m mkdocs build --strict -d /tmp/user-manual-build-test 2>&1
    

  2. If build fails (exit code != 0):

    [FAIL] User Manual Build Gate
          docs/user-manual/ failed mkdocs build --strict:
          [error output]
    
    BLOCK finalization — broken documentation must not ship.

  3. If build succeeds:

    [PASS] User Manual Build Gate — docs/user-manual/ builds successfully
    

  4. If mkdocs is not installed (command not found):

    [WARN] User Manual Build Gate — mkdocs not installed, skipping build validation
    
    Continue without blocking — do not fail finalization due to missing local tooling.

  5. Draft marker detection:

    grep -rl "<!-- DRAFT" docs/user-manual/docs/**/*.md 2>/dev/null
    
    If matches found, report as Medium severity (non-blocking):
    [WARN] User Manual: N page(s) contain unreviewed draft content
          • docs/user-manual/docs/features/feature-a.md
          • docs/user-manual/docs/features/feature-b.md
    

  6. Documentation coverage report:

  7. Scan all specs/*/state.yaml files for docs_page fields
  8. For each docs_page, check if the file exists
  9. Report:
    Documentation coverage: N/M features documented
      DOCUMENTED: feature-a, feature-b, feature-c
      MISSING: feature-d (specs/4-feature-d/state.yaml → docs/user-manual/docs/features/feature-d.md)
      ORPHANED: getting-started.md (no spec reference — manual addition)
    
    Missing pages are reported as Low severity (advisory). Orphaned pages are informational only.

Step 3: Produce Finalization Report

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase 5: Quality Verification & Merge Readiness
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Environment: [production/staging]
Branch: [current branch]
Last Commit: [commit hash and message]

Check Results:
─────────────────────────────────────────────────────────────────
[✓/✗] Dependency Security Audit
      • High vulnerabilities: X
      • Critical vulnerabilities: X

[✓/✗] Final Security Scan
      • Issues found: X

[✓/✗] Migration Safety (if applicable)
      • Migrations: X pending
      • Zero-downtime compatible: Yes/No

[✓/✗] Compliance Verification
      • GDPR: Compliant/Issues found
      • Data handling: Reviewed

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Step 3b: Built-In Security Review Gate

When code is NOT in deliverable_types: Skip the security review gate. Display "Security review: SKIPPED (no code deliverables)" and proceed to Step 4.

When code in deliverable_types:

After producing the finalization report, run a final automated security pass before the Go/No-Go decision, via the toolkit's /vt-d-security-scan skill (which dispatches security-sentinel). It replaced the Claude Code built-in /security-review, which is not consistently available across installations — see processed proposal 2026-04-15-finalize-security-scan-naming-mismatch.md.

The pass is on a deadline, and a pass that does not return has not passed. This is the same rule Step 0c 5a already applies to the version gate, for the same reason. Read that rule before changing anything here.

1. Dispatch, and keep the transcript path

Dispatch the pass scoped to the branch diff, and put the budget in the prompt as well, so the agent has its own chance to self-limit:

Budget: 10 minutes. Return a partial, honest report rather than continuing past it. A partial report is the correct outcome; producing nothing is a failure.

Keep the dispatched command set narrow (see §5). Capture the output_file path the dispatch returns — that is <transcript> below.

2. Bound the wait mechanically

Do not decide "it is taking too long" by judgement — that is the failure this step exists to fix, and twice it produced a 15-minute wait that was written down as success. Run the watchdog:

bash "$CLAUDE_PLUGIN_ROOT/../vt-base/bin/security-pass-watchdog.sh" \
  --transcript "<transcript>" --budget-s 900 --idle-s 180

Run it in the background. It returns only when a bound is hit — it has no success verdict, because it cannot see success:

Verdict Exit Meaning
STALLED 3 Transcript stopped growing for the idle window.
DEADLINE 4 Absolute budget elapsed.
USAGE 2 Bad arguments or the transcript path never appeared — toolkit defect, treat as STALLED.

The completion notification is authoritative, and it is checked FIRST. A pass that finishes also stops writing, so a transcript that stopped growing means "hung" and "done" alike — bytes cannot tell them apart. Therefore:

  • The dispatch reported completion → the pass returned. Go to §4 and grade its report. Discard the watchdog's verdict, whatever it says.
  • The watchdog returned while the dispatch is still running → that, and only that, is the failure recorded in §3.

Measured 2026-09-21: the watchdog returned STALLED on a pass that had delivered a full report 60 seconds earlier. Grading by the watchdog alone would have recorded a false FAIL.

⚠ Never read the transcript's contents. It is a full JSONL agent log and reading it overflows the finalize context. The watchdog reads its size only. The output_file path is a symlink, and the watchdog follows it (stat -L). Plain stat returns the length of the link's target path — a constant — which is how "the transcript never grew past 188 bytes" came to be misread as "the pass produced nothing".

3. Record the outcome — a stall is a FAILED check, never a caution

On STALLED, DEADLINE, or USAGE while the dispatch is still running (see §2 — never on a pass that has completed), print exactly:

✗ Security pass (security-sentinel): FAIL (<verdict> — <watchdog stdout>)
  The final security pass did not return. Nothing was verified by it.
  This is a failed check, not a caution: record it in checks_failed and carry it into
  the Step 4 blocking issues.

Then:

  • Increment checks_failed. Do not increment checks_passed.
  • Add final security pass did not return — security surface unverified to the Step 4 blocking issues list.
  • Write it to finalize_gate in the spec's state.yaml as a failed check with its verdict — not under cautions:.

Why it may not be downgraded to a caution. On SPEC-203 this pass stalled twice, two days apart (2026-09-18 PR 1, 2026-09-20 PR 2). Both times it was recorded under cautions: and both gates shipped checks_failed: 0. The release decision silently fell back to an earlier review round while the gate asserted that every check passed. An unreturned pass is weaker evidence than a known HIGH finding, not stronger — a HIGH at least tells you what is wrong.

The only exit is re-running the pass, not waiving it. If a stalled pass could be accepted with a note, this step would be a control that trains its own bypass — the recurring high-severity shape in this repo. Re-run it, or fix what hangs it, or ship a NO-GO.

4. If the pass returns normally

  1. HIGH severity findings unresolved → blocking issues for the NO-GO decision. All HIGH findings must be enumerated individually (EC4).
  2. Returned but with no findings section at all → treat as STALLED per §3. A report that says nothing has verified nothing.
  3. Merge results into the finalization report alongside the Step 2 security-sentinel results.

Quality gates checklist item:

[ ] Security review passed — /vt-d-security-scan returned, HIGH findings resolved

5. If /vt-d-security-scan is genuinely unavailable

Only when the skill does not resolve at all (toolkit installed, skill not exposed):

⚠ Security pass: SKIPPED (/vt-d-security-scan not exposed in this installation)

Continue to Step 4. This branch is for a missing skill, and it is deliberately narrow — it must never absorb a pass that was dispatched and then failed to return. Those are §3, and §3 fails.

Keep the dispatched pass cheap. Each of the pass's tool calls is expensive at this nesting depth (BUG-261 measured ~7 minutes of dispatch latency between two trivial commands in a depth-2 sub-agent). Scope it to the branch diff, ask for a bounded number of targeted greps rather than an open-ended sweep, and prefer one compound command over several round trips. Fewer calls is not a style preference here — it is what keeps the pass inside its budget.


Step 3c: Definition of Done Confirmation (SPEC-160, FR-3)

Confirm the active spec's own Definition of Done before the Go/No-Go decision. This is a reporting + reuse step — it reads gates/ledger/orphan-scan already present in finalize and adds no new gate (SPEC-160 NFR-1).

If the active spec has no ## Definition of Done section (EC-2 — a legacy spec authored before SPEC-160, not retro-mutated): display Definition of Done: not present (legacy spec — not retro-authored) and proceed. Do not synthesize one.

Otherwise:

  1. Parse the spec's ## Definition of Done section — the canonical items plus any item marked N/A — <reason> inline (SPEC-160 Decision 1). The canonical item list and each item's mechanical/narrative classification come from docs/concepts/definition-of-ready-and-done.md — confirm against that list, not the author's prose, so the bar cannot be lowered per-spec.

  2. Hard-verify the mechanical items (deterministic — an unmet one is a blocking issue fed into the Step 4 NO-GO decision):

  3. DoD-1 .review-gate.md present and PASS — already checked in Step 0; reuse that result.
  4. DoD-2 .test-gate.md PASS, or accepted NOT_RUN/N/A for non-code deliverables — already checked in Step 0b; reuse that result. Honor an inline N/A — <reason> and print the reason.
  5. DoD-3 docs/change-ledger.md updated for this spec — grep the ledger for this spec's ID. If present, PASS. If absent, mark pending — satisfied by Step 4.5 (doc-sync --log, GO only); this is not a blocker before Step 4.5 because the ledger append is a GO-only post-decision step.

  6. Report the narrative items as non-blocking advisory (LLM judgement — never blocks complete, SPEC-160 Decision 4):

  7. DoD-4 All acceptance criteria met — check the spec's ACs against the implementation.
  8. DoD-5 No orphaned artifacts — reuse the orphan/state-doc scan already run in finalize.

  9. Print each item with its status and source — never a bare "DoD checked" that conflates deterministic and judgement signals:

Definition of Done — SPEC-NNN
─────────────────────────────────────────────────────────────────
Mechanical (hard-verified):
  [✓] DoD-1 .review-gate.md PASS            (Step 0)
  [✓] DoD-2 .test-gate.md PASS              (Step 0b)   [or: N/A — no code deliverable]
  [~] DoD-3 change-ledger updated           pending — Step 4.5 (doc-sync --log)
Narrative (advisory, non-blocking):
  [✓] DoD-4 all acceptance criteria met     (advisory)
  [✓] DoD-5 no orphaned artifacts           (advisory)
─────────────────────────────────────────────────────────────────

An unmet mechanical item (e.g. .review-gate.md not PASS, or an N/A with no justification) is added to the Step 4 blocking issues. Narrative items are reported for the author's judgement and never block. If the SPEC-160 sections predate the spec, this step is a no-op per the EC-2 branch above.


Step 4: Go/No-Go Decision

If GO:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DECISION: GO - Safe to Merge
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

All checks passed. You may proceed with merging.

Rollback Plan:
─────────────────────────────────────────────────────────────────
Previous version: [version/vt-c-commit]
Rollback command: [specific rollback instructions]
Monitoring: Check [service] for errors post-merge

Post-Merge Steps:
1. Documentation sync (Step 4.5 — runs automatically next)
2. Monitor error rates for 15 minutes
3. Verify key user flows work
4. Check logs for unexpected errors

TIP: Pre-PR simplify ritual (optional):
  1. git commit (save current work)
  2. /simplify (run 3 parallel quality agents)
  3. git diff (review improvements)
  4. git commit -m "refactor: simplify" (commit improvements)
  See references/simplify-guide.md for focused mode and skip conditions.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEXT: /vt-d-complete (mark spec done), then merge PR to main
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

After /vt-d-complete and successful merge, Phase 6 (/vt-d-6-operate) covers:
• Bug investigation if issues arise
• Incident response procedures
• Continuous improvement

If NO-GO:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DECISION: NO-GO - Blocking Issues Found
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Finalization blocked due to:

[List of blocking issues with details...]

Required Actions:
─────────────────────────────────────────────────────────────────
1. [Specific action to fix issue 1]
2. [Specific action to fix issue 2]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ACTION: Fix issues, then run /vt-d-5-finalize again
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Recommended: Address the blocking issues listed above, then re-run /vt-d-5-finalize.


Step 4.5: Documentation Sync (GO only)

Only execute this step if the decision was GO or GO WITH CAUTION.

4.5a: Capture change ledger entry

Invoke /vt-t-doc-sync --log to interactively record what changed: - What was added, modified, or removed - Why (the spec/feature that drove it) - Key files affected

This appends an entry to docs/change-ledger.md.

4.5b: Apply mechanical documentation fixes

Invoke /vt-t-doc-sync --fix to automatically update: - Component counts in README.md, docs/index.md, macros.py, plugin.json - Version references against plugins/registry.json - Deprecated terminology - What's New section (regenerated from change ledger) - MkDocs nav entries if needed

4.5c: Report result

Documentation Sync:
─────────────────────────────────────────────────────────────────
Change ledger: Entry added for [spec/feature name]
Counts updated: [N] files fixed
What's New: Regenerated
Remaining drift: [0 | N items — list them]

If /vt-t-doc-sync --fix reports remaining manual-fix items, list them as advisories (non-blocking — finalization continues).


Step 4.7: Non-code Delivery Summary

Only when non-code types present in deliverable_types.

Synthesize a delivery summary from journal entries and deliverable content:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Delivery Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Goals achieved:
• [goal 1 from spec, with status]
• [goal 2 from spec, with status]

Key decisions:
• [decision 1 from journal entries]
• [decision 2 from journal entries]

Lessons learned:
• [lesson 1 from journal entries]

Post-delivery actions:
─────────────────────────────────────────────────────────────────
1. Run /vt-c-consolidate to archive session learnings
2. Capture stakeholder feedback via /vt-c-journal "Feedback: ..."
3. Consider /vt-c-compound if this solved a reusable problem
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If no journal entries exist, base the summary on the spec goals and deliverable content only.


Step 4.8: Persist Finalize Gate in state.yaml

After the GO/NO-GO decision, persist the finalize result for committed audit trail:

  1. Derive the active spec from the branch name (feature/spec-NNN-* → SPEC-NNN)
  2. Look up specs_dir in .design-state.yaml
  3. Read specs/[N]-feature/state.yaml
  4. Add or update the finalize_gate section:
    finalize_gate:
      status: GO|NO-GO
      date: "YYYY-MM-DD"
      version: "[bumped version or N/A]"
      checks_passed: N
      checks_failed: N
    
  5. Write the updated state.yaml
  6. Stage: git add specs/[N]-feature/state.yaml

If no active spec or no state.yaml: skip silently. If write is denied (context: fork): skip silently — the GO/NO-GO decision in the TL;DR output is sufficient for the current session.

Step 4.9: Write Phase Checkpoint

Write .claude-checkpoint.md to the project root: - completed_phase: 5-finalize - next_phase: complete - next_command: /vt-d-complete - branch: current git branch - active_spec: derived from branch or .active-spec - clear_recommended: false - clear_reason: "Completion is brief — merge and mark done" - Key Outcomes: GO/NO-GO decision, version bump, checks passed - Context for Next Phase: merge instructions, post-merge steps

Display the clear recommendation per the phase-checkpoint protocol.


Step 5: TL;DR Summary (Final Output)

This is the final reported output. The run may continue afterwards only with work that writes no file, performs no git operation, changes no gate or state record and has no external effect (no network call, no publish); anything beyond that belongs in the report, above this summary (docs/reference/skill-frontmatter.md, Fork Context Requirements). Max 10 lines between delimiters.

After ALL previous steps (including the GO/NO-GO decision and any file writes), output a concise summary using the format below. Fill in values from Steps 2–4.

If GO:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TL;DR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Decision: GO
Checks: [N] passed | [N] failed | [N] warnings
Constitution: COMPLIANT
Version: [VERSION_CHANGES from Step 0c, e.g. "vt-base 3.0.0 → 3.1.0 (MINOR)"]
Docs: Synced (change ledger + counts updated)
Branch: [branch-name]

Next: /vt-d-complete (mark spec done), then merge PR to main
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If GO WITH CAUTION:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TL;DR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Decision: GO WITH CAUTION
Checks: [N] passed | [N] failed | [N] warnings
Constitution: COMPLIANT
Version: [VERSION_CHANGES from Step 0c, e.g. "vt-base 3.0.0 → 3.1.0 (MINOR)"]
Spec Coverage: [Z]% (some requirements pending)
Docs: Synced (change ledger + counts updated)
Branch: [branch-name]

Next: /vt-d-complete (mark spec done), then merge PR; track pending requirements in backlog
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If NO-GO:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TL;DR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Decision: NO-GO
Checks: [N] passed | [N] failed | [N] warnings
Constitution: [COMPLIANT / VIOLATIONS]
Version: [VERSION_CHANGES from Step 0c, or "BLOCKED" if version gate failed]
Branch: [branch-name]

Blockers: [list of blocking issues]
Next: Address blockers, then re-run /vt-d-5-finalize
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If stale gates detected:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TL;DR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Decision: NO-GO (stale gates)
Branch: [branch-name]
Stale: [review-gate/test-gate] from branch [other-branch]

Next: Re-run /vt-d-4-review on current branch, then /vt-d-5-finalize
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Notes: - Omit the "Constitution" line if SpecKit is not active (no .specify/ directory) - Omit the "Blockers" line if there are no blocking issues - The "Version" line uses the VERSION_CHANGES from Step 0c. If no plugin changes were detected, use "No plugin changes". If the version gate was skipped, omit the line. - The TL;DR summarizes Steps 2–4; do not repeat the full report


Error Handling (context: fork)

This skill runs with context: fork — it executes as an isolated sub-agent that returns only its output to the parent conversation. Fork sub-agents CANNOT interact with the user.

Critical rules for fork context:

  1. NEVER ask interactive questions. No "Would you like...?", no "Should I...?", no AskUserQuestion calls. The question will be swallowed, the sub-agent will die, and the parent conversation receives no output at all.

  2. If a file write fails, report it inline and continue. Use this format:

    Warning — file not persisted: Could not write {filename}. The full content is included in the output above. To persist, copy the content manually or re-run with write permissions.

  3. Always produce the full formatted output as the primary deliverable. The formatted finalization report returned to the parent conversation is the primary output. File persistence is secondary. Complete all analysis, checks, and formatting BEFORE attempting any file writes.

  4. Finalization report is the primary output: The GO/NO-GO decision with full check results MUST always be returned to the parent conversation as formatted text, regardless of whether any file writes succeed.

Finalization Checks Detail

Dependency Security Audit

  • Runs npm audit or bundle audit
  • Identifies known vulnerabilities in dependencies
  • Flags high/critical issues as blocking

Final Security Scan

  • Last-pass security review
  • Checks for any issues missed in Phase 4
  • Validates authentication/authorization

Migration Safety

If database migrations are pending: - Checks for zero-downtime compatibility - Validates rollback capability - Checks for data loss risks - Verifies large table handling

Compliance Verification

  • GDPR data handling review
  • SOC2 logging requirements
  • PII protection validation
  • Audit trail completeness

Constitution Compliance (When SpecKit Active)

If .specify/ exists, finalization checks include:

Constitution Verification

Constitution Compliance Check:
─────────────────────────────────────────────────────────────────
Verifying release meets .specify/memory/constitution.md principles...

[✓/✗] [Principle 1 from constitution]
[✓/✗] [Principle 2 from constitution]
[✓/✗] [Principle 3 from constitution]
...

Constitution Status: COMPLIANT / VIOLATIONS FOUND

Source of truth (SPEC-143): specs/[N]-feature/state.yaml.plan_gate if present. Reader accepts status: PASS (post-SPEC-143) and legacy status: COMPLETE as equivalent compliant markers — both are considered compliant. Use principles_checked and violations from plan_gate to populate the verification block rather than re-parsing plan.md prose.

Provenance check (SPEC-189 FR-4). After reading status, read validator. If it is not agent — including absent, which reads as unknown and never as agent (FR-6) — display:

⚠ This gate's <status> was NOT produced by its prescribed validator (validator: <value>).
  Did not run: <prescribed − validators_run>, from the writing skill's `requires_agents`.
  When no prescribed set is declared, name what DID run instead and say so — never drop the
  line, and never read an empty `prescribed` as "none were required".
  Continuing — this is a notice, not a block.

Non-blocking by FR-5: the notice changes no branch.

Spec Coverage Verification

Spec Coverage Check:
─────────────────────────────────────────────────────────────────
Verifying specs/[N]-feature/spec.md requirements are implemented...

Total Requirements: X
Implemented: Y
Coverage: Z%

Unimplemented (if any):
• [Requirement not yet implemented]

Finalization Decision with SpecKit

GO with full spec compliance:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DECISION: GO - Spec Compliant
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ All constitution principles respected
✓ Spec requirements implemented
✓ Standard checks passed

CAUTION with partial spec coverage:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DECISION: GO WITH CAUTION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ Constitution principles respected
⚠ Spec coverage: X% (some requirements pending)
✓ Standard checks passed

Pending requirements should be tracked in backlog.

Feature Flag Recommendation

For risky changes, consider:

TIP: Use feature flags for gradual rollout

The feature-flags skill provides patterns for:
• Percentage-based rollouts
• User segment targeting
• Kill switches for quick rollback

This allows deploying code with features disabled,
then gradually enabling for users.

Open Brain Capture (Optional)

After producing the Go/No-Go decision, if the capture_thought MCP tool is available, capture the finalization outcome.

When to capture: After every completed finalization (GO, NO-GO, or GO WITH CAUTION).

How: 1. Check if capture_thought tool is available. If not: skip silently. 2. Call capture_thought with:

thought: "Release decision: {GO/NO-GO/GO WITH CAUTION} for {spec title} on branch {branch}. Checks passed: {N}/{total}. Constitution: {COMPLIANT/VIOLATION}. Key concern: {top issue or 'none'}."
3. On timeout or error: log debug message and continue. Never fail the skill.

Next actions

Close the Go/No-Go decision with the concrete follow-up:

  • GO → merge the branch, then run /vt-d-complete to mark the spec done.
  • GO WITH CAUTION → address the noted concern, then merge.
  • NO-GO → fix the listed blockers and re-run /vt-d-5-finalize.