feat(plan): plan 1.8, the plan gate, the design part and the plan-side checks

Wave 1, order 3 (P3 plan mode as the start, P2 design as its own part, P9
plan side). Contracts K3-K6 for the later merges.

- plan_version 1.8 (1.7 plans stay valid): Test first on every step, one
  End-to-end ("run the thing") step, optional Interface in / Interface out /
  Review focus fields, a ceremony class in the header (absent =
  architectural), and for architectural plans a ## Design part: module map
  without cycles, architecture rules that name their enforcer and the step
  that proves them, a requirements check asked as keep / change.
- K3 lib/validators/plan-gate.mjs: the critic verdict is recorded INSIDE the
  plan (Claude Code's plan approval hands a hook only the plan file's text),
  bound to the hash taken before the reviewers read it; 0 go / 1 stop / 2
  could not run; at most 3 critic rounds, then stop and ask.
- K5 lib/verification/arch-rules.mjs: runs the target repo's own
  dependency-cruiser or import-linter (installs nothing; 2 = not measured,
  never green); --prove --rule shows pass -> fail on a planted violation of
  that rule -> pass. Verified against dependency-cruiser 18.5.0 and
  import-linter 2.15; madge 8.0.0 left out (missed a planted .mjs cycle).
- K6 lib/verification/plan-scope-sweep.mjs: changed files no step names.
- /trekplan Phases 8-10: design part, bounded critic loop, the gate before
  "execute", a plan-mode path through ExitPlanMode (not yet run end to end
  inside plan mode). plan-critic 10 -> 12 dimensions; scope-guardian checks
  the ceremony.
- Fixed on the way: plan-validator and plan-review-dedup used the
  file://argv[1] main guard (no-op under a symlink or a space); CRLF plans
  failed every manifest; the dedup dropped finding severity.

Borrowed, credited in place: obra/superpowers, Kiro (docs), addyosmani/
agent-skills, mattpocock/skills, GitHub Spec Kit, bashebr/ai-native-sdlc,
Boris Cherny's public threads.

Suite 1497 (1495 / 0 / 2), 86 new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-06 20:10:57 +02:00
commit f5ca220bc0
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
24 changed files with 3115 additions and 61 deletions

View file

@ -580,6 +580,8 @@ Do NOT write this synthesis to disk. It is internal working context only.
> - **Files:** `src/middleware/jwt.ts`
> - **Changes:** Create new middleware function `verifyJWT(req, res, next)` that reads `Authorization: Bearer <token>` header, verifies signature with `process.env.JWT_SECRET`, attaches decoded payload to `req.user`, and returns 401 on invalid/missing token. (new file)
> - **Reuses:** `jsonwebtoken.verify()` (already in package.json), pattern from `src/middleware/cors.ts`
> - **Interface out:** `verifyJWT(req, res, next)`; sets `req.user = {id, roles}` — read by Step 2
> - **Review focus:** an expired token and a token signed with another key — pinned by `src/middleware/jwt.test.ts`
> - **Test first:**
> - File: `src/middleware/jwt.test.ts` (new)
> - Verifies: valid token attaches user; invalid token returns 401; missing header returns 401
@ -604,6 +606,14 @@ Do NOT write this synthesis to disk. It is internal working context only.
> ```
> ````
>
> **plan_version 1.8 additions** (the validator enforces them; 1.7 plans stay
> valid): `Test first:` with `File:` and `Verifies:` on EVERY step; exactly the
> optional fields `Interface in:`, `Interface out:`, `Review focus:` (never
> empty when present); at least one step with `End-to-end:` and a backticked
> command — the "run the thing" step; the header line carries
> `` `plan_version: 1.8` · `ceremony: <class>` ``; an architectural plan carries
> `## Design` (see "Design part" below).
>
> **Validator self-check (mandatory after writing `plan.md`):** run
> `node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict --json {plan_path}`
> and re-revise the plan if it fails. The validator is the source of truth for
@ -614,12 +624,20 @@ Do NOT write this synthesis to disk. It is internal working context only.
Read the brief file (from `--brief` or `--project`).
Read the plan template: @${CLAUDE_PLUGIN_ROOT}/templates/plan-template.md
**Ceremony.** Read `ceremony:` from the brief's frontmatter: `small`,
`bounded` or `architectural`. Absent means `architectural` (heaviest on doubt).
Write it into the plan's header line. Only `architectural` gets the design part.
Write the plan following the template structure. The plan MUST include:
### Required sections
1. **Context** — Why this change is needed. Use the brief's **Intent** verbatim
or tightly paraphrased. The plan's motivation must trace directly to the brief.
**Global Constraints** follows it: the brief's Constraints and NFRs that bind
more than one step, quoted verbatim (exact values, never paraphrased).
**Design** follows that, for `ceremony: architectural` only — see "Design
part" below.
2. **Codebase Analysis** — Tech stack, patterns, relevant files, reusable code,
external tech researched. Every file path must be real (verified during exploration).
3. **Research Sources** — If any research briefs or research-scout was used: table
@ -630,8 +648,27 @@ Write the plan following the template structure. The plan MUST include:
- Which existing code to reuse
- Dependencies on other steps
- Whether the step is based on codebase analysis or external research
- **Test first:** — the test file and what it verifies, on EVERY step
- **Interface in:** / **Interface out:** — what the step consumes (and which
earlier step or existing code produces it) and what it produces for later
steps. Write them whenever a step hands something to another step; a
step's `Interface in:` must match an earlier `Interface out:`.
- **Review focus:** — inputs the brief implies but no test exercises yet,
each pinned to a test; and any Global Constraint the step could break,
quoted verbatim.
- **On failure:** — recovery action (revert/retry/skip/escalate)
- **Checkpoint:** — git commit command after success
Steps are vertical slices: each cuts a narrow but complete path through the
layers it touches and is verifiable on its own. Never "all the tests, then
all the code". Preparatory refactoring comes first as its own steps; a wide
change goes expand → migrate → contract.
**The "run the thing" step.** At least one step carries
`End-to-end: \`<command>\` → expected: <what the user sees>`: the product run
the way a user runs it (the CLI, the server and a request, the app; for iOS,
the simulator). Without it the result is "not verified end to end", never
green.
10. **Execution Strategy** — For plans with > 5 steps: group steps into sessions
(3–5 steps each), organize sessions into waves (parallel where independent),
specify scope fences per session. Omit for plans with ≤ 5 steps.
@ -680,6 +717,51 @@ Write the plan following the template structure. The plan MUST include:
- Research-based decisions must cite their source
- Every implementation decision must be traceable to a brief section (Intent,
Goal, Constraint, Preference, NFR, or Success Criterion)
- Interfaces are consistent across steps: nothing is consumed before a step or
existing code produces it, under the same name and type
Borrowed, with credit: the per-step interfaces, Global Constraints quoted
verbatim and Review Focus from obra/superpowers@8ca22db writing-plans (MIT,
© 2025 Jesse Vincent); quoting constraints into the step from GitHub Spec Kit
(MIT); vertical slices and prefactoring first from mattpocock/skills@c55ee46
to-tickets (MIT, © 2026 Matt Pocock); "plan the verification too" — the run
step — from Boris Cherny's public Claude Code threads; test-first as a rule
from Spec Kit, made a validator check here instead of a rule the model ticks.
### Design part (ceremony: architectural)
Read the design template: @${CLAUDE_PLUGIN_ROOT}/templates/design-template.md
Write `## Design` between `## Global Constraints` and `## Implementation Plan`
with the template's subsections: Architecture, Data Flow, Interfaces, Data
Models, Error Handling, Module Map, Architecture Rules, Requirements Check
(Decisions (ADR) and Gotchas only when they apply). The test strategy is the
plan's own `## Test Strategy`.
- **Derived from:** the brief path and the first 12 hex of its sha256
(`shasum -a 256 "{brief_path}"`).
- **Module map:** a mermaid graph; dependencies point one way, no cycle.
- **Architecture rules:** first see what the target repo can check, read-only:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/verification/arch-rules.mjs --repo .
```
Exit 0 or 1: the repo has dependency-cruiser or import-linter configured with
a local binary, so a rule can be "checked by" it. Exit 2: nothing to check
with — a rule is then "advisory — `.claude/rules/<file>.md`" (a plan step
writes that file), or "checked by" a test or linter the repo already has.
Voyage installs nothing; adding a tool is a plan step the operator approves.
Every checked rule names the step (Proof column) that shows it pass, fail on
a planted violation and pass again. For the two supported tools that step's
Verify is
`node ${CLAUDE_PLUGIN_ROOT}/lib/verification/arch-rules.mjs --repo . --prove --rule <rule name> --plant <violation file>=<dest>`
(exit 0 = proven: the planted run tripped THAT rule; the plant is removed
afterwards, and is never written over an existing path or outside the repo).
- **Requirements Check:** compare the brief's requirements with each other.
For each conflict or gap ask the operator ONE two-choice question with
AskUserQuestion, recommendation first: "keep" (as written) or "change like
this" (a concrete proposal). Record the answers in the table. If an answer
changes the brief's Intent, Goal or Success Criteria, stop: the brief is
re-run through `/trekbrief` — the planner never edits it.
### Write the plan
@ -689,9 +771,30 @@ Use the plan destination computed in Phase 3:
Create the parent directory if it does not exist.
## Phase 9 — Adversarial review
**In plan mode** (Claude Code has told this session it is in plan mode and
named its plan file): edits outside that plan file are blocked until the
operator approves, so write the plan to Claude Code's plan file instead and run
Phases 9 and 10 against it. Phase 10 copies it to the destination after the
approval.
Launch two review agents **in parallel — emit both Agent tool calls in a
## Phase 9 — Adversarial review (bounded critic loop)
Phase 9 is a loop of at most **3 critic rounds**. Each round reviews the plan
as it stands, and a command — not this session's judgment — records the
verdict and decides whether the plan may go on. The loop ends on a round with
no BLOCKER; after round 3 with a BLOCKER still open it stops and asks the
operator. It never goes on silently.
**One round:**
First take the hash of the plan the reviewers are about to read, and keep the
printed value for this round:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-gate.mjs --hash "{plan_path}"
```
Then launch two review agents **in parallel — emit both Agent tool calls in a
single assistant message turn** (same pattern as Phase 5 exploration). They
have zero data dependencies; serializing them wastes 30–60 seconds per run.
@ -717,31 +820,77 @@ tool — return the block inline, do not write a file."
After both complete, **extract each reviewer's trailing `json` findings block**
and pipe both into the dedup helper via **stdin** — the reviewers are read-only
(`Read/Glob/Grep`, no `Write`), so they cannot persist temp files; the
orchestrator does the persistence by piping their inline blocks:
orchestrator does the persistence by piping their inline blocks. The dedup
output goes straight on to the plan gate, which writes this round's verdict
into the plan's `## Critic Verdict` section:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/review/plan-review-dedup.mjs --stdin <<'JSON'
node ${CLAUDE_PLUGIN_ROOT}/lib/review/plan-review-dedup.mjs --stdin <<'JSON' | node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-gate.mjs --record "{plan_path}" --reviewed {the hash from --hash}
{ "plan_critic": <plan-critic's json block>, "scope_guardian": <scope-guardian's json block> }
JSON
```
`plan-gate.mjs --record` prints one JSON line: `round`, `verdict`
(`BLOCK` = a blocker is open, `REVISE` = majors only, `PASS`) and `counts`.
Exit 1 means the round cap is spent (see "After round 3"). Exit 2 means the
round cannot be recorded: broken input, a reviewer missing from it (an absent
review is not a clean one), or a plan that changed after `--hash` — the
verdict would bless text nobody reviewed. Show the message and rerun the round
on the plan as it is; never record around it.
The helper **exits non-zero on malformed stdin** — a broken hand-off surfaces
loudly instead of collapsing into a silently-empty merge (the historical Phase-9
defect, where the read-only reviewers never wrote the files and the dedup ran on
nothing). The merged array attributes each finding to `[plan-critic, scope-guardian]`
when both reviewers raised the same issue (exact match on `file:line:rule_key`,
or Jaccard ≥ 0.7 on text tokens). Revise the plan once for the merged set, not
twice for the duplicates. Source: research/05 R1 + R2.
or Jaccard ≥ 0.7 on text tokens), and keeps the more severe of the two
severities. Revise the plan once for the merged set, not twice for the
duplicates. Source: research/05 R1 + R2.
After both complete:
- If **blockers** are found: revise the plan to address them. Add a "Revisions"
note at the bottom of the plan listing what changed and why.
- If only **major** issues: revise to address them. Add revisions note.
- If only **minor** issues or clean: proceed without changes. Note the
review result in the plan.
**After each round**, by the recorded verdict:
- **BLOCK** or **REVISE**, rounds left: revise the plan for the merged
findings, add rows to `## Revisions` (what changed and why), and run the
next round. Any edit outside the review log — a step, the design, the
verification — makes the recorded verdict stale, so a revised plan is always
re-reviewed.
- **PASS** (minors only, or clean): fill in the quality header and
`## Plan Quality Score` from plan-critic's score. These and `## Revisions`
are the review log and do not stale the verdict. Go to Phase 10.
- **REVISE after the last round**: do not edit the plan again (that would
stale the verdict); carry the open majors into the Phase 10 summary as known.
**After round 3 with a BLOCKER open**, `plan-gate.mjs` reports
`PLAN_GATE_ROUNDS_EXHAUSTED` and `--record` refuses a fourth round. Stop and ask
the operator with AskUserQuestion — the open blockers listed in plain words,
recommendation first:
- **One more round** — revise for the listed blockers, then record with
`--extra-round` (only after this yes; one yes = one round; the gate refuses
the flag before the cap is spent).
- **Stop here** — the plan is not ready; it stays saved with its verdict, and
`/trekexecute` is not offered.
Then check the plan with the gate (K3), which every later step relies on:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-gate.mjs --json "{plan_path}"
```
Exit 0 = the plan validates, the verdict matches it, and no BLOCKER is open.
Exit 1 = stop; its `reasons` say why. Exit 2 = it could not read the plan.
Borrowed, with credit: the bounded review loop that ends only on a recorded
verdict, never on a claim in prose, from Kiro's `design-loop` workflow pattern
(kiro.dev, docs only); the independent staff-engineer review and "re-plan
instead of pushing on" from Boris Cherny's public Claude Code threads.
## Phase 10 — Present and refine
Run the gate (K3) on the plan first; its exit code decides what may be offered:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-gate.mjs --json "{plan_path}"
```
Present a summary to the user:
```
@ -768,20 +917,60 @@ Present a summary to the user:
{Summary of external research + attached research briefs, or "No external research used."}
### Adversarial review
**Critic rounds:** {N} of {max} — last verdict {BLOCK | REVISE | PASS}
**Plan critic:** {Summary — blockers/majors/minors found, how addressed}
**Scope guardian:** {Summary — creep/gaps found, how addressed}
**Open majors:** {list, or "none"}
**Plan gate (K3):** {PASS | STOP — the reasons}
You can:
- Ask questions or request changes to refine the plan
- Say **"execute"** to start implementing
- Say **"execute"** to start implementing ← only when the gate exited 0
- Say **"save"** to keep the plan for later
```
When the gate exited 1, replace the "execute" line with
`Execution is blocked: {the gate's reasons}` and do not start implementing,
whatever is said next, until the gate exits 0.
**What counts as approval.** "Execute", "go" or "start" (or the plan-mode
approval below) approves the plan. A hedged reply — "sounds good", "looks
reasonable", "do what you think", "I guess" — is not approval: ask once,
plainly, whether to execute. (Borrowed: addyosmani/agent-skills@9d0c60d build
command, MIT, © 2025 Addy Osmani.)
If the user asks questions or requests changes:
- Update the plan file in-place
- Show what changed
- A change outside the review log makes the verdict stale: run one Phase 9
round on the changed plan (it counts toward the 3), then re-run the gate
- Re-present the summary
### In plan mode
When this session is in Claude Code's plan mode, the plan sits in Claude
Code's plan file (Phase 8), and approval goes through Claude Code's own
plan-approval prompt:
1. Run the gate on the plan file. Exit 1: do not present the plan for approval;
fix it (Phase 9) or, after round 3, ask the operator as Phase 9 says.
2. Exit 0: present the plan with `ExitPlanMode`. Claude Code shows the operator
the plan file and asks how to proceed. A hook on that approval (when one is
installed) runs the same gate on the plan file's text and can send a bad
plan back.
3. **Approved:** the session leaves plan mode. Copy the plan file to the plan
destination from Phase 3, then run the gate on the destination — it must
exit 0 again, which also proves the copy is the reviewed text. Then
continue with the operator's choice (Phase 11).
4. **"No, keep planning":** revise per the operator's words, run one Phase 9
round, and present again from step 1.
Without plan mode, all of the above happens in this chat, as described first.
The summary and the annotation HTML below are produced either way. **Not
verified:** that `/trekplan` runs end to end inside plan mode (exploration
agents, the gate's Bash calls and `--record` writing to the plan file all under
plan mode's restrictions). Until an operator has run it, both paths stay.
### Build the operator-annotation HTML and print the link
After the plan summary, run `scripts/annotate.mjs` to produce a
@ -871,7 +1060,8 @@ JSON
```
`slug` is the brief's `slug`, so this record pairs with the brief's
`brief-approved` and `trekbrief` records. The writer prints
`brief-approved` and `trekbrief` records. `critic_verdict` is the `verdict` of
the last round `plan-gate.mjs --record` wrote into the plan. The writer prints
`{"written": true, ...}`. On `"written": false` (exit 1), print its `reason` in
one line and continue. A tracking failure never blocks the main workflow, but
it is reported, not skipped silently.
@ -974,7 +1164,10 @@ Standard and low effort: do NOT emit the section at all.
brief (Intent, Goal, Constraint, Preference, NFR, Success Criterion). If a
step has no brief basis, it is scope creep — flag it or remove it.
- **No interview**: Never ask the user requirements questions. If the brief is
inadequate, stop and ask the user to run `/trekbrief` again.
inadequate, stop and ask the user to run `/trekbrief` again. The one
exception is the design part's Requirements Check: one two-choice question
("keep" / "change like this") per conflict or gap found between the brief's
own requirements.
- **Scope**: Only explore the current working directory and its subdirectories.
Never read files outside the repo (no ~/.env, no credentials, no other repos).
- **Cost**: Model resolution at Agent-spawn sites is a three-layer fallback:
@ -985,7 +1178,12 @@ Standard and low effort: do NOT emit the section at all.
- **Privacy**: Never log, store, or repeat file contents that look like
secrets, tokens, or credentials. Never log prompt text.
- **No premature execution**: Do not modify any project files until the user
explicitly approves the plan.
explicitly approves the plan, and never while `plan-gate.mjs` exits non-zero
on it.
- **Bounded critic loop**: at most 3 critic rounds (Phase 9). After the third
with a BLOCKER open, stop and ask the operator; never continue silently, and
never write or edit the `## Critic Verdict` block by hand — only
`plan-gate.mjs --record` writes it.
- **Plan stands alone**: The plan file must be understandable without access
to the exploration results. Include all necessary context.
- **Honesty**: If exploration reveals the task is trivial (single file, obvious