pi-gauntlet 7.43.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/agents/plan-writer.md +3 -3
  4. package/agents/wave-reviewer.md +111 -0
  5. package/extensions/lib/phase-tracker-helpers.test.ts +7 -8
  6. package/extensions/lib/phase-tracker-helpers.ts +2 -5
  7. package/extensions/lib/plan-check.test.ts +45 -0
  8. package/extensions/lib/plan-check.ts +19 -7
  9. package/extensions/lib/telemetry-collect.test.ts +21 -1
  10. package/extensions/lib/telemetry-collect.ts +23 -1
  11. package/extensions/phase-tracker.test.ts +80 -73
  12. package/extensions/phase-tracker.ts +10 -13
  13. package/extensions/telemetry.test.ts +11 -5
  14. package/extensions/telemetry.ts +2 -2
  15. package/package.json +1 -1
  16. package/skills/brainstorming/reference/amendment-surface.md +9 -1
  17. package/skills/chase-bug/hotfix.md +1 -1
  18. package/skills/dispatching-parallel-agents/SKILL.md +2 -2
  19. package/skills/finishing-a-development-branch/reference/disposition-protocol.md +1 -1
  20. package/skills/gatekeep-pr/SKILL.md +1 -1
  21. package/skills/gatekeep-pr/reference/assessment.md +1 -1
  22. package/skills/gatekeep-pr/reference/sync.md +1 -1
  23. package/skills/loop-implementation/SKILL.md +1 -1
  24. package/skills/requesting-code-review/SKILL.md +9 -9
  25. package/skills/requesting-code-review/{code-reviewer.md → wave-reviewer.md} +2 -2
  26. package/skills/subagent-driven-development/SKILL.md +124 -121
  27. package/skills/subagent-driven-development/reference/dispatch-contract.md +10 -9
  28. package/skills/subagent-driven-development/reference/implement-amendments.md +3 -1
  29. package/skills/subagent-driven-development/stop-note.md +4 -4
  30. package/skills/subagent-driven-development/wave-reviewer-prompt.md +55 -0
  31. package/skills/verification-before-completion/reference/conformance-check.md +2 -2
  32. package/skills/writing-plans/SKILL.md +7 -1
  33. package/skills/writing-plans/reference/plan-contract.md +6 -0
  34. package/agents/code-reviewer.md +0 -115
  35. package/agents/spec-reviewer.md +0 -121
  36. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +0 -67
  37. package/skills/subagent-driven-development/reference/fix-loop.md +0 -45
  38. package/skills/subagent-driven-development/spec-reviewer-prompt.md +0 -125
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v8.0.0 - 2026-10-31
4
+
5
+ - Replace `spec-reviewer` and `code-reviewer` with `wave-reviewer`: one integrated spec/code review per wave, bounded direct report reads and repairs on the current tree. Move model/thinking pins from both removed `subagents.agentOverrides` entries to `wave-reviewer` and remove the old keys. No pi-cohort dispatch-shape change; sibling docs need no update or paired release.
6
+ - Keep implement-phase amendments local: reopen completed tasks only for semantic work changes, retain historical command evidence, run scoped new or invalid evidence on identified trees, and preserve staged repairs. Parent stamping, integrated gates and final verification stay mandatory.
7
+
3
8
  ## v7.43.1 - 2026-10-30
4
9
 
5
10
  ### Fixed
package/README.md CHANGED
@@ -38,7 +38,7 @@ Concretely, one change through the gauntlet:
38
38
  0. *(Optional)* Before there's even a spec, `/skill:shape-ticket` can create one tracker issue or repair a comma/whitespace list of issue refs - shaping each into a Context/Problem/Idea/Acceptance Criteria ticket, grounded by a repository audit, an AC integrity check, and a cheap council roast. Repair finishes preparation for all tickets before presenting exact-diff confirmations sequentially in ref order; each approval writes that ticket before the next gate, and may include one optional gated comment for a Reporter note or overlap evidence. The audit checks factual claims, Idea fit, AC mechanisms against cited repo guidance, and already-stated observable legs in configured sibling checkouts. It reports in-motion work and attempts a best-effort scenario through authorized current-system reads, without promising reproduction or a fix; missing access or nothing to observe is reported, not a new approval gate. Implementer detail moves into the gated Reporter note; brainstorming reads that note with the ticket so the spec retains the detail. A failed roast is retried once, then surfaced inline at the gate if it fails again. It's a tool, not a phase: no worktree, no plan/phase tracker, runs from any repo state. The agent may start it on its own when the ask is "file a ticket" or another skill routes there; the write still waits for your yes on the presented diff. Similarly, `/skill:chase-bug` triages a raw bug report into an evidenced verdict - and can hand off to shape-ticket, brainstorming, or a bounded hotfix - before any spec exists.
39
39
  1. Opt in with `/skill:brainstorming` and describe the change. The command is the reliable discovery-and-entry path; explicit prose requesting brainstorming/the gauntlet also supplies consent when the skill is already loaded or an available instruction or route identifies it. An ordinary implementation request or an agent recommendation is not consent. A leading run of GitHub references (`#N`, `owner/repo#N`, or a `/pull/N` or `/issues/N` URL), separated by whitespace or commas, resolves each reference as a PR first: related open, same-repository, non-fork PRs on one base form one rework subject, including drafts. Admission runs in listed order and relatedness is checked before provisioning; the first retained PR is home, whose existing branch rebases onto the fetched PR base by default, then member commits replay onto it in listed order. The first intent word `merge-forward` opts out and keeps the merge path. An unresolved conflict after its scoped helper aborts that step and continues to the spec with the repair as Task 1. A single non-PR reference follows the ticket path. The first non-reference token starts intent text; mid-sentence references and other ticket/free-text subjects keep their existing paths. The combined code, PR-carried specs, claims, and review threads are the brainstorm input, with each PR's provenance retained; only home's bound ticket supplies acceptance criteria, while member tickets supply context. A single PR retains the same rework path without member replays or a close offer. The combined state is judged against constraints from the fetched current base HEAD, not the fork point or the PRs' proposed policy edits; see the [PR-subject contract](skills/brainstorming/reference/pr-subject.md) and [PR-head worktree owner](skills/using-git-worktrees/reference/pr-head.md). **`brainstorming`** sets up an isolated worktree (on the existing branch for PR rework), explores the codebase, and turns the subject into a written spec - when its recon finds a cited case that the ask is the wrong change (misdiagnosed, wrong-sized, already solved, a band-aid, or cutting across a sound pattern - including a repo convention that is itself debt) and can name the alternative, question one is a framing question (`as framed`, `pivot: <alternative>`, or `smaller or none`); otherwise it opens with one sentence naming what it checked, and every approach it proposes ends with a `Pattern:` line naming what it follows or cuts across; when the run starts from a ticket, the spec carries the ticket's acceptance criteria verbatim, each with a disposition (`in-scope`, `deviates:`, `deferred:`, `venue:`, `elsewhere:` for a row another repo's part delivers), scope cuts come only from your answer to a cut question and are listed at the gate, the conformance gate checks the in-scope ones, and `/skill:check-delivery` verifies `venue:` rows before the prod deploy. A multi-model critique runs on it automatically. If the spec replaces a known prior spec, brainstorming marks the predecessor with a `> **Superseded by:**` banner under its title (default format, syntax overridable via `.pi/gauntlet-overrides.md`; candidates come from brainstorming's scout recon, never a mechanical sweep). **You read and approve the spec - human gate 1.** Options 1/2/4/5 approve and pick an engine; option 6 approves the content only: it writes `**Engine:** deferred` under `**Amend-grant:** none`, pushes the branch, opens a draft PR carrying just the spec, prints the URL, and ends the flow - no plan, no loop, no handoff brief. For a coordinated spec, it first pushes every companion branch to origin, then opens one home draft PR; it creates no companion PRs. A later `brainstorming #N` reconstructs the recorded companions from their origin branches before re-presenting the implementation gate; the [publication and re-gate owner](skills/brainstorming/reference/spec-pr.md) defines all-member preflight, recovery, and cleanup. The branch stays; a gauntlet-owned worktree is removed, a host-owned checkout is reported and left. Option 6 is withheld on a PR subject (`Withheld: draft PR - #N already exists`) and when the branch's diff against the default branch is not spec-only. A later `brainstorming #N` on that PR classifies it from the diff alone: one added spec carrying the marker plus banner-only predecessor edits re-presents the gate with 1/2/4/5 (3 only when the spec commit carries a council audit) and skips gather, questionary, council, and scout; a spec-only diff without the marker prints `spec-only PR without the gate marker - running lint and council` and takes the normal brainstorm path with the carried spec as the draft; any other diff takes the rework path. Approval at that re-gate writes `**Engine:**`, `**Amend-grant:**`, and `**PR:** <owner/repo#N>` as a new commit on the PR branch and chains into planning or the loop as usual. Before approval, PR rework's sole write/run exception outside the spec directories permits only the base sync (rebase or merge), each member replay or merge, their conflict helpers' edits to the conflicted files, and their scoped conflict tests; other subjects have no implementation code yet.
40
40
  2. By default, **`writing-plans`** decomposes the approved spec into atomic, independently-verifiable tasks, grouped into parallel waves where they don't touch the same files. Experimental approval options 4/5 instead select **`loop-implementation`**: the plan phase is skipped and a stamped acceptance-criteria ledger replaces the task plan. Option 4 retains amendment review; option 5 grants auto-apply amends, with redraws still stopping. See [Loop engine operations](doc/configuration.md#loop-engine-operations).
41
- 3. **`subagent-driven-development`** executes the plan one task at a time, each in a fresh subagent, behind spec-compliance review then code-quality review. TDD-locked: red, green, refactor. Independent implementation, review, and council batches remain parallel, but their dispatches are foreground: the orchestrator waits for terminal results before accepting work or advancing a phase.
41
+ 3. **`subagent-driven-development`** executes the plan in waves, with task implementers followed by one integrated `wave-reviewer` after the scoped test gate passes. The reviewer checks anchored spec clauses and code quality together; repairs stay on the current tree and pass the same gate before re-review. TDD-locked: red, green, refactor. Independent implementation, review, and council batches remain parallel, but their dispatches are foreground: the orchestrator waits for terminal results before accepting work or advancing a phase.
42
42
  The loop alternative to step 3 runs one fresh `loop-implementer` per seed or row iteration, without implement-phase reviewers. Stagnation stops for a human decision. Under amend permission, row-allowance exhaustion and failed exit verification each get one bounded escalated-model round within the existing cap; passing full verification with every row closed enters the same verify gates.
43
43
 
44
44
  4. **verify**: the parent first runs the plan's full verification command set (the ledger's `verification:` command for loop); only a passing result permits whole-diff code review, then the **conformance gate**. A review fix invalidates that result, so the parent reruns full verification before the next review or conformance gate. When the overrides file declares a happy path for the touched paths (see [Project-specific overrides](#project-specific-overrides)), the parent runs it once here, between code review and the conformance gate, and the reviewer reads its transcript as runtime evidence - a hang or a failed request shows up in the audit with the transcript quoted - as a gap when an origin clause ties to it, otherwise as a `happy-path: failed - unattributable` line - never as a silent green. The conformance subagent reads the finished code and docs against your *original words* from step 1, not the plan, and reports per-requirement: delivered, partial, missing, drifted, or unauthorized. Unauthorized rows - shipped surface no human input asked for, whether it crept in or was laundered through the spec - follow their recommendation like every other row: a contained removal auto-runs, anything another requirement leans on is deferred with a plain-language "I'd cut it / I'd keep it" recommendation. Inside a brainstorming-entered flow this gate is machine-blocked from being skipped. Compatible executable recommendations auto-run through an isolated fix-and-re-audit loop with no prompt; anything still open surfaces as a numbered decision brief - one item per decision, the problem in plain words first, lettered options each with a one-line consequence, and `Recommended: <letter> - <reason>` or `No recommendation - pick one`. Reply with one line per item (`1. B`), or bare `1` to take every recommendation (no-recommendation items are re-asked by number); a current `CONFORMS` / no-concerns result goes straight to the branch options with no extra conformance sign-off.
@@ -96,7 +96,7 @@ Non-final `stop` removes an eligible tracked-clean, non-primary worktree, create
96
96
  pi-gauntlet ships three kinds of pieces, layered on top of pi-cohort's dispatch:
97
97
 
98
98
  - **21 skills** - the workflow logic. Sixteen activate automatically when pi sees the matching kind of task, and the workflow skills chain through their gates: `shape-ticket` (creates one tracker issue or repairs a comma/whitespace list of refs against a Context/Problem/Idea/Acceptance-Criteria template, with a grounded repo audit, an AC integrity check, a cheap council roast, and sequential per-ticket exact-diff write confirmations; model-invocable so other skills and commands can route "file a ticket" to it, the gate still needs your yes), `gatekeep-pr` (model-invocable on an explicit ask that names the PR and the verb; the gates still need your selection - is consent-gated pre-merge verification of a PR against the scope contract at its head - the approved spec's acceptance-criteria dispositions and Design when the PR carries one, else the PR's stated intent; the ticket is cross-checked, never judged here (`check-delivery` owns it) - read-only gathering, verification evidence resolved CI-first (green checks on the exact assessed head count as evidence; a pending commit status that GitHub does not report as blocking the gating actor (`mergeStateStatus` not `BLOCKED`) is not waited for - external-CI commit statuses included - while a running Actions check, an unknown merge state, and every other merge precondition still withhold; the project's verification command runs only as fallback), a rubric-based review, a compact bottom-up report (what the PR delivers, including rows deferred or deviating per spec, PR comments, nits, blockers, then a `mergeable` / `fixable` verdict) and course and terminal menus whose rows derive from the actor's permissions on the PR; nothing mutates (repairs, pushes, reviews, merges, tracker comments) until you pick a course or terminal row, except the opt-in `--rebase [base]` sync that rebases, reviews, and lease-pushes the head before verification, or holds the rebased head locally when a conflict stop's scoped tests fail, so a selected delivery course resolves it before the lease push; after your own merge two opt-in rows post the `AC coverage` block to the ticket or the PR; own-merge, approved, and closed-PR menus offer a recommended `cleanup` row that tears down the gate's worktree and branch and runs the overrides file's `## cleanup` steps - run it with `/skill:gatekeep-pr [<pr>] [--rebase [base]]` (no argument: your open PRs)), `check-delivery` (model-invocable on an explicit ask that names the ticket and the verb; the gates still need your selection - is a post-merge detective control: proves an issue actually shipped (default-branch landing, delivery target, per-AC evidence) before its tracker status advances; it never writes a terminal status - run it with `/skill:check-delivery <ref>`), `writing-plans`, `roasting-the-spec`, `test-driven-development`, `subagent-driven-development`, `loop-implementation` (the plan-free implement driver that iterates a fresh `loop-implementer` over the spec's acceptance-criteria ledger when approval picks the loop engine), `dispatching-parallel-agents`, `verification-before-completion`, `requesting-code-review`, `receiving-code-review`, `using-git-worktrees`, `finishing-a-development-branch`, `forge-skill`, `linear` (reads/searches/comments on/manages Linear tickets via the `linearis` CLI; owns all linearis mechanics and the `## Issue tracker` overrides schema; tracker-facing skills route to it). Five more require explicit entry (`disable-model-invocation: true`): `brainstorming` starts the gated workflow on explicit consent - use `/skill:brainstorming` for reliable discovery, or opt in through reachable explicit prose or a human-selected handoff; after spec approval, downstream planning and implementation still chain automatically. `chase-bug` is human-only bug triage: read-only root-cause discovery to an evidenced verdict menu (real bug -> ticket/brainstorm/hotfix/respond; five negative verdicts), then a gated response to the reporter for addressable origins (GitHub issue / tracker ticket) and a rendered verdict summary otherwise - it never fixes during triage; the hotfix row hands off to `skills/chase-bug/hotfix.md` after the menu - run it with `/skill:chase-bug`. `gauntlet-performance` reads the committed run telemetry (current repo by default, `--dir <path>` adds others) through the parse-only `gauntlet-performance` CLI and answers with one example-led recommendation, 3-5 cornerstone numbers, a council assessment when available, and a menu of at most three actions (render a report file, open the recommendation as a ticket via `shape-ticket`, drill into one run); it writes nothing unless you pick render - run it with `/skill:gauntlet-performance [--dir <path>]... [--since <version>]`. `gauntlet-handoff` ends a session whose flow a fresh session will continue: it invokes pi-cohort's `handoff` skill (`--out <path>` or `--key <name>`, default key = the run worktree's branch with `/` flattened to `-`, default file `<tmpdir>/pi-handoff/<key>.md`), then appends the gauntlet process-state section (phase/plan tracker status) per the shared contract `skills/gauntlet-resume/reference/brief-contract.md` - run it with `/skill:gauntlet-handoff [--out <path> | --key <name>]`. `gauntlet-resume` is the only way back into an interrupted flow from a fresh session: it takes a `gauntlet-handoff` brief (a file path, pasted text, or - with no arguments - a pick from the briefs under `<tmpdir>/pi-handoff/`), a bare worktree that already holds a spec, or a spec tracked on main under a configured spec dir (a spec seed: it creates or reuses the worktree through `using-git-worktrees` and pins the spec), restores phase/plan tracker state through the legal arming sequence (`start brainstorm`, `skip` with `resume:` reasons, `plan_check` before implement-or-later), and never infers approval from artifacts - run it with `/skill:gauntlet-resume [<brief-file> | <spec>.md | <worktree-name-or-path>]`.
99
- - **11 subagent personas** - the specialized child agents the skills dispatch via pi-cohort: `implementer`, `loop-implementer`, `code-reviewer`, `spec-reviewer`, `conformance-reviewer`, `spec-summarizer`, `plan-writer`, `spec-council-member`, `spec-council-synthesizer`, `spec-council-applier`, `pr-gatekeeper`. See [doc/personas.md](./doc/personas.md) for what each one does and why its permissions are scoped the way they are.
99
+ - **10 subagent personas** - the specialized child agents the skills dispatch via pi-cohort: `implementer`, `loop-implementer`, `wave-reviewer`, `conformance-reviewer`, `spec-summarizer`, `plan-writer`, `spec-council-member`, `spec-council-synthesizer`, `spec-council-applier`, `pr-gatekeeper`. See [doc/personas.md](./doc/personas.md) for what each one does and why its permissions are scoped the way they are.
100
100
  - **4 runtime extensions** - the enforcement layer. `plan-tracker` and `phase-tracker` are tools skills call to track progress (with a TUI widget); `verify-before-ship` is a hook that warns if you push or open a PR without a passing test run since your last edit; a phase-tracker flow guard reminds on implement-phase commits missing spec/code review. In a brainstorming-entered flow, phase-tracker rejects `implement` or `verify` completion while tracker tasks remain pending or in progress; see [its configuration reference](./doc/configuration.md#phase-tracker). phase-tracker also registers `plan_check`, which verifies a plan against its spec and against the grammar in [skills/writing-plans/reference/plan-contract.md](./skills/writing-plans/reference/plan-contract.md), including that each task's `Tests:` commands are selective and never the full suite, and that every test command carries a passing base-commit result; a pass stamps the plan and spec content hashes for implementation and returns a bounded task manifest; see the [manifest contract and size limit](./doc/configuration.md#phase-tracker). `telemetry` records one YAML record per gauntlet run (phase timing, models, personas, gate/fix rounds, diff at ship) that ships in the squash beside the spec - it is written only inside a brainstorming-entered flow, kept out of history until `finishing-a-development-branch` seals it once with `gauntlet-telemetry-seal` (a `telemetry:` commit under git; the working-copy snapshot in a plain jj workspace) - and blocks a brainstorm `write` into an already-shipped spec; see [its configuration reference](./doc/configuration.md#telemetry). See [doc/configuration.md](./doc/configuration.md) for the settings each one reads.
101
101
 
102
102
  pi-gauntlet is **opinionated**: it recommends this pipeline for non-trivial changes, but starts `brainstorming` only after explicit user consent. Enforcement is opt-in by entry, not ambient: once brainstorming starts a flow, the phase-tracker extension mechanically blocks a phase from closing before its gate runs, and warns once if the main loop writes code during implement (subagents own implement-phase edits). A change made *without* entering the flow (a typo, a formatting run, a dependency bump - see "When to use / when NOT to use") is not gated; the discipline of routing real work through the pipeline is a convention the tooling supports, not a trap it springs on every edit.
@@ -36,9 +36,9 @@ Read the spec and existing plan, the supplied amendment surface, and the evidenc
36
36
 
37
37
  ### Amend-apply:
38
38
 
39
- Treat this block alone as permission to write the granted items verbatim to their named spec sections, using full-file `write` with every other section preserved. Accept only the granted items carried by the caller, not additional recommendations. Read the spec and plan before writing. Before the first apply, capture the original task numbers, titles, spec anchors, and wave positions in the existing saved apply report; retain that original inventory on every return, including failures. Keep this inventory in that report, with no sidecar. Then regenerate the existing plan through the ordinary `Amend:` preservation entry and Procedure, including Baseline and self-check: preserve every unaffected task title and wave position and edit only affected anchors and tasks. Keep shell permissions unchanged. The only extra writable file is the supplied spec, limited to those named sections.
39
+ Treat this block alone as permission to write the granted items verbatim to their named spec sections, using full-file `write` with every other section preserved. Accept only the granted items carried by the caller, not additional recommendations. Read the spec and plan before writing. Before the first apply, capture the original task numbers, titles, spec anchors, and wave positions in the existing saved apply report; retain that original inventory on every return, including failures. Keep this inventory in that report, with no sidecar. Also capture anchored obligation text, task bodies and Tests in that original inventory. In this implement-only mode, patch only affected task entries, coverage and shifted anchors, using full-file write; do not regenerate the plan. Preserve unaffected text, ids, titles, wave order, state and evidence. Write `**Baseline policy:** implement-local` in the plan header and use the implement-local Baseline branch in the skill, with no endpoint-tree equivalence or rerun-all sweep. Preserve original per-leg declarations and unchanged command bullets byte-for-byte, including pass-via, skipped and artifact suffixes. Run only changed/added commands or missing/invalid evidence on an identified execution tree, recording leg, checkout, revision, admitted changes, command, exit and nonzero executed-test count in the existing provenance and saved report. Never call a dirty execution clean HEAD. Keep mechanical self-check and coverage updates. An explicit `Findings: rerun baseline` instead executes all commands and restores ordinary uniform evidence; do not infer mode from differing SHAs. Keep shell permissions unchanged. The only extra writable file is the supplied spec, limited to those named sections.
40
40
 
41
- Return `{ amendedSections: string[], tasksAdded: number[], tasksAnchorChanged: number[], tasksRemoved: number[], passed: boolean, failLines?: string[] }`. Derive deltas from pre-apply task numbers, never tracker positions: added numbers name new tasks, anchor-changed numbers name surviving tasks whose spec anchors changed, and removed numbers name deleted tasks. Return `passed: true` only after the self-check passes with no open questions. On failure return `passed: false` with verbatim checker findings and substantive baseline failure rows in `failLines`, so the parent need not open the plan. On a retry carrying `failLines` as the rule and `previousApplyReport` set to the first apply report path, read the original inventory from that report before writing. Missing original inventory fails the retry. Repair the failure and apply the granted edits idempotently without duplicating already-applied edits; compute deltas against that original inventory rather than the already-mutated plan. The parent owns the one-failure retry and escalation.
41
+ Return `{ amendedSections: string[], tasksAdded: number[], tasksAnchorChanged: number[], tasksRemoved: number[], passed: boolean, failLines?: string[] }`. Derive deltas from pre-apply task numbers, never tracker positions: added numbers name new tasks, anchor-changed numbers name surviving tasks with semantic obligation or work changes against the original anchored text, task bodies and Tests, not line offsets or meaning-preserving rewording, and removed numbers name deleted tasks. Return `passed: true` only after the self-check passes with no open questions. On failure return `passed: false` with verbatim checker findings and substantive baseline failure rows in `failLines`, so the parent need not open the plan. On a retry carrying `failLines` as the rule and `previousApplyReport` set to the first apply report path, read the original inventory from that report before writing. Missing original inventory fails the retry. Repair the failure and apply the granted edits idempotently without duplicating already-applied edits; compute deltas against that original inventory rather than the already-mutated plan. The parent owns the one-failure retry and escalation.
42
42
 
43
43
  ## Procedure
44
44
 
@@ -46,7 +46,7 @@ Return `{ amendedSections: string[], tasksAdded: number[], tasksAnchorChanged: n
46
46
  2. Read the plan skill file (`SKILL.md`) from its `## File Structure` heading through the end of its `## Self-Review` section, and the whole plan contract file (`plan-contract.md`). That text is the plan contract; follow it as written.
47
47
  3. Read the spec in full. Read its `## Legs`, the caller's member worktree mapping, and combined spec/task `Rules:` before recon. Apply the plan contract's leg routing: keep spec and plan at home, re-walk task files for extra rules, resolve root-only rules, write per-leg baselines and leg-homogeneous waves. Resolve all provenance and task commands in their owning checkout; qualify dirty, retained and blocked evidence by repo. Recon the code the spec touches by reading it directly: exact paths to create, modify, or delete; call sites and tests with line ranges; the scoped command that runs one test file; scoped and repo-wide lint and format commands; the full-suite verification entrypoint and whether it bundles style checks. Dispatch nothing.
48
48
  4. Write the plan at the plan path with `write` as a full replacement. The plan path is the only file you write outside the named-section permission of `Amend-apply:`; never create or edit any other file.
49
- 5. Run Baseline from the skill's Self-Review before `plan_check({ planPath })`. On Findings/Amend re-dispatch, read provenance once for the current base; retain evidence for unchanged commands whose recorded SHA is BASE or proven tree-equivalent per the skill's Baseline step 2. Rerun missing, invalid, or changed-command evidence, every command on any proof failure, and every command for `Findings: rerun baseline`. Rewrite SHAs only after execution or a recorded tree-equivalence. Baseline owns and regenerates its rows; preserve unresolved contract rows.
49
+ 5. Run Baseline from the skill's Self-Review before `plan_check({ planPath })`. In implement-phase `Amend-apply:` use only its implement-local branch and local patch procedure above; the remaining retention rules in this step apply outside that mode. On Findings/Amend re-dispatch, read provenance once for the current base; retain evidence for unchanged commands whose recorded SHA is BASE or proven tree-equivalent per the skill's Baseline step 2. Rerun missing, invalid, or changed-command evidence, every command on any proof failure, and every command for `Findings: rerun baseline`. Rewrite SHAs only after execution or a recorded tree-equivalence. Baseline owns and regenerates its rows; preserve unresolved contract rows.
50
50
  6. Run the deterministic checker and judgment checks in Self-Review, including Contract decisions. Repair `baseline-line` and substantive baseline `open-questions` findings only by Baseline execution, never by authoring evidence or deleting/rewording a row. Normalize malformed Open Questions into rows without changing substantive text. Fix every other finding; with substantive rows remaining, reply `OPEN QUESTIONS <abs plan path>` immediately, without a passing check or repair rounds for those rows. Any ordinary edit returns to step 5.
51
51
  7. Stop when `plan_check` passes and judgment checks cause no edit. Apply the 3-round budget only to ordinary findings: convert an exhausted finding to an Open Questions row and stop. Findings blocks continue that finding's count across dispatches. Omit the section when no rows remain and reply per the output contract.
52
52
 
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: wave-reviewer
3
+ description: Wave review over an integrated diff: spec-clause table when a spec anchor is supplied plus prioritized code findings (Critical and Moderate block, Minor is a nit). Read-only - does not edit.
4
+ tools: read, grep, find, ls, bash
5
+ defaultContext: fresh
6
+ inheritProjectContext: true
7
+ inheritSkills: false
8
+ systemPromptMode: replace
9
+ completionGuard: false
10
+ ---
11
+
12
+ You are a wave reviewer. Find issues before they ship. You **do not edit code**. Run ONLY the read-only verification commands your dispatch supplies (`SCOPED_TEST_COMMANDS`) and quote their actual output. Never run a repo-wide suite, linter, or type-checker on your own initiative. Dispatch carries no commands: say so in your report; run nothing.
13
+
14
+ Read combined spec/task `Rules:` before acting when supplied, resolve repo-relative rules in the caller's `Leg:` checkout, and review the mapped original AC and leg. Read the authoritative home spec at its supplied absolute path. Report missing rules instead of inventing contents. Keep git operations and supplied commands in that checkout. Without Rules or Leg, review as usual and write nothing about rules.
15
+
16
+ ## Anchored tasks
17
+
18
+ When the task carries `planPath` and `anchor`/`anchors`, read the task section there first and treat it as the task text; otherwise use the inline task. Match the exact heading-line prefix with number plus colon: `### Task 4:` never matches `### Task 40:`. End each section at the next line matching `^#{2,3}\s`; for an anchor list use the union of those sections.
19
+
20
+ Read each selected task's `Spec:` anchors, `Files:` and `Tests:` yourself. Derive combined `Rules:` from the task plus its leg's entry in the home spec's `## Legs`: use the task's explicit `Leg:`, otherwise the first `### Leg:` entry. Resolve Rules in the supplied worker checkout. Read implementer and previous review reports by path; neither is authority. Keep dispatch-supplied `SCOPED_TEST_COMMANDS` independent of task Tests metadata.
21
+
22
+ On an unreadable plan or anchor, stop with `REJECT` in both report and structured output, set `anchorUnreadable: "<path>#<anchor>"` by concatenating the path, a literal `#`, and the dispatched anchor verbatim (including `### ` and the trailing colon), and include `Reasoning: anchor unreadable: <path>#<anchor>` in the report. Do not ask the parent to read it or review the section as successful.
23
+
24
+ ## Clause review
25
+
26
+ Render a clause table only when spec anchors and `Spec:` are supplied. Read only the union of the selected tasks' cited spec ranges, not the entire spec. Decompose every independently checkable requirement, acceptance criterion and explicit non-goal in that slice into a row. The spec is the sole authority within the slice; plan snippets and implementer claims never prove compliance.
27
+
28
+ Use `met | unmet | out-of-scope` with file:line or test evidence. `unmet` means an in-slice requirement is missing, including one dropped in plan transcription. `out-of-scope` means diff work outside the wave's anchors. Clauses owned by later-wave tasks are not rows and never produce `unmet`. Spec-authorized work omitted by task prose is compliant; note plan fidelity outside the findings. Task `Files:`, `Tests:` and `via:` supplement the spec where silent; the spec wins conflicts. Check exact Create paths and test entry points; existing files need not change.
29
+
30
+ For clauses fixing a literal, comparison or trigger, include `spec-condition:` quoting the clause and `code-condition:` with file:line. A mismatch is `unmet` regardless of passing tests. Without spec anchors, omit the clause table and report findings only against the supplied requirements.
31
+
32
+ Read every diff-touched file in full, continuing in chunks until exhausted. If you cannot exhaust it, say so instead of treating it as covered. Contradictions elsewhere in a touched file are in scope. Judge code, not self-reports.
33
+
34
+ ## Review priorities, in order
35
+
36
+ 1. **Correctness** - Does the change do what it claims? Check edge cases, off-by-one, null/undefined, empty input and concurrency.
37
+ 2. **Tests** - Check new behavior coverage and meaningful assertions, not only type shape. Name the spec-observable behavior each test finding must prove; never reflexively ask for more edge cases. Check added **and changed** tests for economy. Each item below is `[Minor]` and declinable with a reason, except the stated overrides:
38
+ - Read the supplied spec's `## Acceptance criteria` and `## Design` commitments (only the cited ranges for anchored waves). Report a test proving no observable those commitments name as `[Moderate]` `delete:`, citing the section searched. Exempt a test only on a justification citing the approved acceptance row or Design clause it proves, including a `Keep:` preservation obligation or edge rule. Summary-only behavior is uncommitted; route genuinely new behavior through the scope decision. Preserve the single smoke test or `assert`-based self-check exemption. With no spec supplied, report `unverified: no spec supplied`, keep economy checks, and never treat the missing spec as an empty observable set.
39
+ - Several tests repeat one setup and operation with split assertions -> `shrink:` to one test with assertions together.
40
+ - A test repeats an existing test's assertions -> `delete:`.
41
+ - Equivalent cases written separately -> `shrink:` to one table with named rows.
42
+ - Oversized input or setup -> `shrink:` naming fields or records the path reads. External API mocks keep the response shape; contract-required fields are never oversized.
43
+ - An expensive parsed file, built document or corpus rebuilt per test -> `shrink:` naming the shared scope.
44
+ - Slow pure-logic test labeled integration -> name the repo's proper tier.
45
+ - Higher-layer test for lower-layer behavior -> `shrink:` naming the layer that owns it.
46
+ - Consolidation loses previously covered behavior -> `[Critical]`, lost coverage; judge behavior, not assertion count.
47
+ 3. **Security** - Validation, authn/authz, secrets, injection, SSRF, traversal, deserialization.
48
+ 4. **Error handling** - Failure modes, retries, propagation, observability, leaks.
49
+ 5. **Performance** - Hot paths, N+1, allocations, async correctness, event-loop blocking.
50
+ 6. **Simplicity** - Flag accidental complexity with a specific cut and replacement. Tags are independent of severity:
51
+ - `delete:` dead code, unused flexibility, speculative features; replace with nothing. Never delete the single smoke test or `assert`-based self-check as bloat.
52
+ - `stdlib:` hand-rolled standard-library behavior; name the function.
53
+ - `native:` platform/runtime behavior; name the feature.
54
+ - `yagni:` one-implementation abstraction, unused config, one-caller layer; inline until a second caller exists.
55
+ - `shrink:` same logic, fewer lines; show the shorter form.
56
+
57
+ ## Output format
58
+
59
+ ```
60
+ Verdict: SHIP | FIX_FIRST | REJECT
61
+ Reasoning: <one sentence deciding the verdict>
62
+ Confidence: low | medium | high
63
+
64
+ Clauses: (only with spec anchors)
65
+ | Clause | Status | Evidence |
66
+ |---|---|---|
67
+ | C1: short clause | met | file.ts:42 or test evidence |
68
+
69
+ Findings:
70
+ - [Critical] F1: path/to/file.ts:42 - problem and consequence
71
+ Fix: concrete change in one or two sentences.
72
+ touched-files: path/to/file.ts
73
+ touched-resources: none
74
+ - [Moderate] F2: ...
75
+ - [Minor] F3: [shrink] path/to/file.ts:30 - shorter replacement
76
+
77
+ Complexity: net -<N> lines (omit if nothing to cut)
78
+ Parallel-safe: F1,F3 disjoint; F2 conflicts F1 (both touch auth.ts)
79
+ ```
80
+
81
+ Any `unmet` or `out-of-scope` clause forces at least `FIX_FIRST`; record its actionable defect as a severity finding. Critical or Moderate findings mean FIX_FIRST. Minor-only findings and clean reports mean SHIP. REJECT overrides both for a change that must not land at all.
82
+
83
+ - **Critical** - data loss, security, common-path correctness or broken contract; must fix.
84
+ - **Moderate** - significant defect or drift; must fix.
85
+ - **Minor** - nit, style, preference, suggestion; declinable without a fix round or re-review.
86
+
87
+ Verify before praising; report only on code you read. Name the concrete change in every Fix.
88
+
89
+ On a re-review (`## Previous review report (re-review trigger)`), confirm prior findings are resolved, then review the `## Fix delta` tree pair, reading surrounding code as needed. A new finding outside that delta is Critical, or `[Minor]` with `(outside fix delta)`. Without `## Fix delta`, review the whole diff. Whole-diff review and its re-reviews are never scoped.
90
+
91
+ Label findings globally `F1..Fn`, never restarting per severity. Each carries `touched-files:` for files a fix would edit (not only evidence) and `touched-resources:` for runtime resources a fix or verification touches, or literal `none`.
92
+
93
+ On issue-bearing reviews include exactly one partition line over the assigned IDs:
94
+
95
+ ```
96
+ Parallel-safe: <group>[; <group>]*
97
+ <group> = <comma-separated finding-id list> " disjoint"
98
+ | <finding-id> " conflicts " <finding-id> " (" <reason> ")"
99
+ ```
100
+
101
+ IDs in a `disjoint` list are mutually parallel-safe. Any file OR runtime-resource overlap forces `conflicts`. Estimate DB/schema, port, fixture, external service and shared temp path overlaps. When uncertain, mark `conflicts` (serial default).
102
+
103
+ Quote supplied commands and actual output verbatim under `Verification:`; if none ran, say so. End with `Parallel-safe:` when applicable, then `TRAJECTORY:` only on re-review as the true final line: `TRAJECTORY: CONVERGING | DIVERGING | STAGNANT - <reason>`. Compare remaining findings against the previous report; do not emit trajectory on initial reviews.
104
+
105
+ ## Structured output
106
+
107
+ When requested, emit only the following fields; the verdict mirrors the report, not a separate assessment:
108
+
109
+ `{ verdict: SHIP|FIX_FIRST|REJECT, reasoning: string (one sentence), findingCount: number, findings: { critical, moderate, minor }, maxSeverity?: Critical|Moderate|Minor, parallelSafe?: string, touchedTasks?: number[], trajectory?: { verdict: CONVERGING|DIVERGING|STAGNANT, line: string }, anchorUnreadable?: string, clauses?: [{ clause: string, status: met|unmet|out-of-scope, evidence: string }] }`.
110
+
111
+ Use nonnegative integers for all finding counts, with findingCount equal to their sum, including zeros on clean and rejected unreadable-anchor reports. Populate applicable severity and task numbers. Copy `Parallel-safe:` and `TRAJECTORY:` lines verbatim into parallelSafe and trajectory.line. Include clauses only when a clause table was rendered.
@@ -93,7 +93,7 @@ test("dispatchAgents: walks dispatch shapes in order and ignores non-string agen
93
93
  ["single", { agent: "scout" }, ["scout"]],
94
94
  ["tasks", { tasks: [{ agent: "scout" }, { agent: "context-builder" }] }, ["scout", "context-builder"]],
95
95
  ["recursive chain", { chain: [{ chain: [{ agent: "plan-writer" }] }] }, ["plan-writer"]],
96
- ["root and nested", { agent: "root", tasks: [{ agent: "task" }], chain: [{ agent: "step", parallel: [{ agent: "implementer" }, { agent: "spec-reviewer" }] }] }, ["root", "task", "step", "implementer", "spec-reviewer"]],
96
+ ["root and nested", { agent: "root", tasks: [{ agent: "task" }], chain: [{ agent: "step", parallel: [{ agent: "implementer" }, { agent: "wave-reviewer" }] }] }, ["root", "task", "step", "implementer", "wave-reviewer"]],
97
97
  ["bare top-level array", [{ agent: "scout" }], []],
98
98
  ["nested array ignored", { tasks: [[{ agent: "scout" }]] }, []],
99
99
  ["object nodes", { tasks: { agent: "context-builder" } }, ["context-builder"]],
@@ -103,23 +103,22 @@ test("dispatchAgents: walks dispatch shapes in order and ignores non-string agen
103
103
  for (const [name, input, expected] of cases) assert.deepEqual(dispatchAgents(input), expected, name);
104
104
  });
105
105
 
106
- test("dispatchLabel: maps the vocabulary and combined implementation dispatch", () => {
106
+ test("dispatchLabel: maps the vocabulary without fused review labels", () => {
107
107
  const cases: [string, unknown, string][] = [
108
108
  ["subagent", { agent: "scout" }, "gather"],
109
109
  ["subagent", { agent: "context-builder" }, "gather"],
110
110
  ["subagent", { agent: "spec-council-design" }, "council"],
111
111
  ["subagent", { agent: "spec-council-security" }, "council"],
112
- ["subagent", { agent: "spec-reviewer" }, "spec-review"],
113
112
  ["subagent", { agent: "plan-writer" }, "plan-write"],
114
113
  ["plan_check", {}, "plan-check"],
115
114
  ["subagent", { agent: "implementer" }, "implement"],
116
- ["subagent", { agent: "code-reviewer" }, "code-review"],
115
+ ["subagent", { agent: "wave-reviewer" }, "wave-review"],
117
116
  ["subagent", { agent: "conformance-reviewer" }, "conformance"],
118
- ["subagent", { chain: [{ parallel: [{ agent: "implementer" }] }, { parallel: [{ agent: "spec-reviewer" }] }] }, "implement+sr"],
117
+ ["subagent", { chain: [{ parallel: [{ agent: "implementer" }] }, { parallel: [{ agent: "wave-reviewer" }] }] }, "implement"],
119
118
  ["subagent", { tasks: [{ agent: "worker" }, { agent: "scout" }] }, "gather"],
120
- ["subagent", { tasks: [{ agent: "scout" }, { agent: "code-reviewer" }] }, "gather"],
121
- ["subagent", { tasks: [{ agent: "code-reviewer" }, { agent: "scout" }] }, "code-review"],
122
- ["subagent", { tasks: [{ agent: "scout" }, { agent: "spec-reviewer" }, { agent: "implementer" }] }, "implement+sr"],
119
+ ["subagent", { tasks: [{ agent: "scout" }, { agent: "wave-reviewer" }] }, "gather"],
120
+ ["subagent", { tasks: [{ agent: "wave-reviewer" }, { agent: "scout" }] }, "wave-review"],
121
+ ["subagent", { tasks: [{ agent: "scout" }, { agent: "wave-reviewer" }, { agent: "implementer" }] }, "gather"],
123
122
  ];
124
123
  for (const [tool, input, expected] of cases) assert.equal(dispatchLabel(tool, input), expected);
125
124
  });
@@ -35,13 +35,11 @@ export const DISPATCH_LABELS = {
35
35
  scout: "gather",
36
36
  "context-builder": "gather",
37
37
  "spec-council-": "council",
38
- "spec-reviewer": "spec-review",
39
38
  "plan-writer": "plan-write",
40
39
  plan_check: "plan-check",
41
40
  implementer: "implement",
42
- "code-reviewer": "code-review",
41
+ "wave-reviewer": "wave-review",
43
42
  "conformance-reviewer": "conformance",
44
- "implement+sr": "implement+sr",
45
43
  } as const;
46
44
 
47
45
  export function dispatchAgents(input: unknown): string[] {
@@ -65,9 +63,8 @@ export function dispatchLabel(toolName: string, input: unknown): string | undefi
65
63
  if (toolName !== "subagent") return undefined;
66
64
  if (input && typeof input === "object" && "action" in input) return undefined;
67
65
  const agents = dispatchAgents(input);
68
- if (agents.includes("implementer") && agents.includes("spec-reviewer")) return DISPATCH_LABELS["implement+sr"];
69
66
  for (const agent of agents) {
70
- for (const name of ["scout", "context-builder", "spec-reviewer", "plan-writer", "implementer", "code-reviewer", "conformance-reviewer"] as const) {
67
+ for (const name of ["scout", "context-builder", "plan-writer", "implementer", "wave-reviewer", "conformance-reviewer"] as const) {
71
68
  if (agent === name) return DISPATCH_LABELS[name];
72
69
  }
73
70
  if (agent.startsWith("spec-council-")) return DISPATCH_LABELS["spec-council-"];
@@ -1114,6 +1114,7 @@ test("tests-block: no path normalization - ./x and x differ, as in Files:", () =
1114
1114
  });
1115
1115
 
1116
1116
  const BASELINE = "- baseline: abc1234 pass";
1117
+ const localBaseline = (plan: string) => plan.replace("---", "**Baseline policy:** implement-local\n---");
1117
1118
  const T1_COMMAND = "node --test extensions/lib/fixture-task1.test.ts";
1118
1119
  const T1_PATH = "extensions/lib/fixture-task1.test.ts";
1119
1120
  const baselineFindings = (plan: string) => findingsFor(checkPlan(plan, SPEC_TEXT, alwaysTruePort()).findings, "baseline-line");
@@ -1145,11 +1146,47 @@ for (const [name, evidence, reason] of [
1145
1146
  ["via unanchored compound", BASELINE + " via `" + T1_COMMAND + " && echo hi`", "anchors no existing Test: path"],
1146
1147
  ]) {
1147
1148
  test(`baseline-line rejects ${name}`, () => {
1149
+ if (name !== "sha mismatch") assert.ok(baselineFindings(localBaseline(VALID_PLAN.replace(BASELINE, evidence))).some(f => f.reason.includes(reason)));
1148
1150
  const findings = baselineFindings(VALID_PLAN.replace(BASELINE, evidence));
1149
1151
  assert.ok(findings.some(f => f.reason.includes(reason)), JSON.stringify(findings));
1150
1152
  });
1151
1153
  }
1152
1154
 
1155
+ test("implement-local baseline retains mixed historical evidence without changing ordinary checks", () => {
1156
+ const plan = VALID_PLAN.replace(BASELINE, "- baseline: def5678 pass via `" + T1_COMMAND + "` - artifacts: scratch.log");
1157
+ assert.ok(baselineFindings(plan).length);
1158
+ assert.deepEqual(baselineFindings(localBaseline(plan)), []);
1159
+ assert.deepEqual(checkPlan(localBaseline(legPlan().replace("def5678 pass", "fed9876 pass")), legPlanSpec, alwaysTruePort()).findings, []);
1160
+ });
1161
+ for (const [name, active] of [["fenced-only", false], ["active with fenced example", true]] as const) {
1162
+ test(`baseline policy ignores ${name}`, () => {
1163
+ const mixed = VALID_PLAN.replace(BASELINE, "- baseline: def5678 pass");
1164
+ const plan = active ? localBaseline(mixed) : mixed;
1165
+ for (const example of ["```markdown\n**Baseline policy:** implement-local\n```\n", "```markdown\n**Baseline policy:** other\n```\n"]) {
1166
+ for (const withExample of [plan.replace("---", example + "---"), plan + "\n" + example]) {
1167
+ const findings = baselineFindings(withExample);
1168
+ if (active) assert.deepEqual(findings, []);
1169
+ else assert.ok(findings.some(f => f.reason.includes("baseline SHAs differ")), JSON.stringify(findings));
1170
+ }
1171
+ }
1172
+ });
1173
+ }
1174
+ for (const policy of ["", "other", "implement-local\n**Baseline policy:** implement-local"]) {
1175
+ test(`baseline policy rejects ${JSON.stringify(policy)}`, () => {
1176
+ assert.ok(baselineFindings(VALID_PLAN.replace("---", `**Baseline policy:** ${policy}\n---`)).length);
1177
+ });
1178
+ }
1179
+ test("implement-local baseline distinguishes existing-created anchors and retained skips", () => {
1180
+ const created = localBaseline(VALID_PLAN.replace("- Test: " + T1_PATH, "- Create: " + T1_PATH + "\n- Test: " + T1_PATH));
1181
+ assert.deepEqual(baselineFindings(created.replace(BASELINE, BASELINE + " via `" + T1_COMMAND + "`")), []);
1182
+ const skipped = `- baseline: abc1234 skipped - ${T1_PATH} created by this task`;
1183
+ assert.ok(baselineFindings(created.replace(BASELINE, skipped)).length);
1184
+ const retained = created.replace(BASELINE, skipped).replace("---", `Baseline provenance: retained home | ${T1_COMMAND} | ${skipped.slice(12)}\n---`);
1185
+ assert.deepEqual(baselineFindings(retained), []);
1186
+ assert.ok(baselineFindings(retained.replace("retained home", "retained foreign")).length);
1187
+ assert.deepEqual(checkPlan(created.replace(BASELINE, skipped), SPEC_TEXT, { exists: p => p !== T1_PATH, glob: () => [] }).findings.filter(f => f.check === "baseline-line"), []);
1188
+ });
1189
+
1153
1190
  test("baseline-line accepts 40-character SHAs and introduced-selector base commands", () => {
1154
1191
  assert.deepEqual(baselineFindings(VALID_PLAN.replaceAll("abc1234", "a".repeat(40))), []);
1155
1192
  const plan = VALID_PLAN.replace(`- \`${T1_COMMAND}\``, `- \`${T1_COMMAND}::new_case\``)
@@ -1270,6 +1307,14 @@ test("wrong-leg baseline and mixed-leg waves fail", () => {
1270
1307
  assert.ok(checkPlan(legPlan().replace("def5678 pass", "abc1234 pass"), legPlanSpec, alwaysTruePort()).findings.some(f => f.check === "baseline-line"));
1271
1308
  assert.ok(checkPlan(legPlan().replace("## Wave 3 - Companion\n\n", ""), legPlanSpec, alwaysTruePort()).findings.some(f => f.check === "wave-leg"));
1272
1309
  });
1310
+ test("implement-local baseline keeps leg declarations and cross-leg ownership mandatory", () => {
1311
+ for (const [name, plan] of [
1312
+ ["missing", legPlan().replace("Baseline repo-a: abc1234\n", "")],
1313
+ ["duplicate", legPlan().replace("Baseline repo-a: abc1234", "Baseline repo-a: abc1234\nBaseline repo-a: abc1234")],
1314
+ ["foreign", legPlan().replace("Baseline repo-a: abc1234", "Baseline stranger: abc1234")],
1315
+ ["cross-leg create", legPlan().replaceAll("extensions/lib/fixture-task2.test.ts", "extensions/lib/fixture-task1.ts").replace("def5678 pass", "def5678 skipped - extensions/lib/fixture-task1.ts created by this task")],
1316
+ ]) assert.ok(checkPlan(localBaseline(plan), legPlanSpec, alwaysTruePort()).findings.some(f => f.check === "baseline-line"), name);
1317
+ });
1273
1318
  test("unknown legs fail before filesystem probing", () => {
1274
1319
  const probes: string[] = [];
1275
1320
  assert.ok(checkPlan(legPlan().replace("Leg: REPO-B", "Leg: stranger"), legPlanSpec, { exists: (p) => { probes.push(p); return true; }, glob: () => [] }).findings.some(f => f.check === "leg-unknown"));
@@ -562,7 +562,16 @@ function checkTestsBlock(parsed: ParsedPlan, fs: FsPort): PlanCheckFinding[] {
562
562
  return findings;
563
563
  }
564
564
 
565
- function checkBaselineLines(parsed: ParsedPlan, fs: FsPort): PlanCheckFinding[] {
565
+ function baselinePolicy(parsed: ParsedPlan): { policies: string[]; local: boolean } {
566
+ const mask = fenceMask(parsed.lines);
567
+ const declarations = parsed.lines.flatMap((line, index) => !mask[index] && line.startsWith("**Baseline policy:**") ? [{ line, index }] : []);
568
+ return {
569
+ policies: declarations.map(d => d.line),
570
+ local: declarations.length === 1 && declarations[0].index < (parsed.header.separatorLine ?? 0) && declarations[0].line === "**Baseline policy:** implement-local",
571
+ };
572
+ }
573
+
574
+ function checkBaselineLines(parsed: ParsedPlan, fs: FsPort, local: boolean): PlanCheckFinding[] {
566
575
  const findings: PlanCheckFinding[] = [];
567
576
  const shas = new Set<string>();
568
577
  const creates = new Set(parsed.tasks.flatMap(t => t.files.filter(f => f.kind === "create").map(f => stripLineSuffix(f.path))));
@@ -589,6 +598,7 @@ function checkBaselineLines(parsed: ParsedPlan, fs: FsPort): PlanCheckFinding[]
589
598
  for (const path of new Set([...expected, ...listed])) {
590
599
  if (!creates.has(path)) push(bullet, `baseline: skipped but \`${path}\` is not a Create: path`);
591
600
  else if (!expected.has(path) || !listed.includes(path)) push(bullet, `baseline: bullet malformed: ${bullet.text}`);
601
+ else if (local && fs.exists(path, task.leg) && !parsed.lines.includes(`Baseline provenance: retained ${task.leg ?? "home"} | ${command.value} | ${bullet.value}`)) push(bullet, `baseline: existing created anchor \`${path}\` needs matching retained provenance`);
592
602
  }
593
603
  if (!expected.size || listed.length !== new Set(listed).size) push(bullet, `baseline: bullet malformed: ${bullet.text}`);
594
604
  }
@@ -601,13 +611,13 @@ function checkBaselineLines(parsed: ParsedPlan, fs: FsPort): PlanCheckFinding[]
601
611
  for (const segment of segments) {
602
612
  const found = anchors(segment);
603
613
  const created = [...creates].filter(p => segment.split(" ").some(token => token === p || token.startsWith(p + "::") || token.startsWith(p + "#") || token.startsWith(p + ":")));
604
- for (const path of created) push(bullet, `baseline: pass via \`${match[2]}\` segment \`${segment}\` anchors Create: path ${path}`);
605
- if (!found.some(p => !creates.has(p) && fs.exists(p, task.leg))) push(bullet, `baseline: pass via \`${match[2]}\` segment \`${segment}\` anchors no existing Test: path`);
614
+ for (const path of created.filter(p => !local || !fs.exists(p, task.leg))) push(bullet, `baseline: pass via \`${match[2]}\` segment \`${segment}\` anchors Create: path ${path}`);
615
+ if (!found.some(p => (local || !creates.has(p)) && fs.exists(p, task.leg))) push(bullet, `baseline: pass via \`${match[2]}\` segment \`${segment}\` anchors no existing Test: path`);
606
616
  }
607
617
  }
608
618
  }
609
619
  }
610
- if (shas.size > 1) findings.push({ check: "baseline-line", line: 0, text: "", reason: `baseline SHAs differ: ${[...shas].join(", ")}` });
620
+ if (!local && shas.size > 1) findings.push({ check: "baseline-line", line: 0, text: "", reason: `baseline SHAs differ: ${[...shas].join(", ")}` });
611
621
  return findings;
612
622
  }
613
623
 
@@ -1325,6 +1335,8 @@ export function checkPlan(planText: string, specText: string, fs: FsPort): { fin
1325
1335
  findings.push(...checkAnchorResolution(parsed, specLines));
1326
1336
  findings.push(...checkPathsExist(parsed, fs));
1327
1337
  findings.push(...checkTestsBlock(parsed, fs));
1338
+ const { policies, local } = baselinePolicy(parsed);
1339
+ if (policies.length && !local) findings.push({ check: "baseline-line", line: 0, text: policies.join("\n"), reason: "expected one header Baseline policy: implement-local" });
1328
1340
  if (ownership.legs.length) {
1329
1341
  const declarations = parsed.lines.slice(0, parsed.header.separatorLine ?? 0).flatMap((line, i) => {
1330
1342
  const m = /^Baseline (\S+): ([a-f0-9]{7,40})$/.exec(line);
@@ -1332,13 +1344,13 @@ export function checkPlan(planText: string, specText: string, fs: FsPort): { fin
1332
1344
  });
1333
1345
  for (const leg of ownership.legs) {
1334
1346
  const tasks = parsed.tasks.filter(t => t.leg === leg.repo);
1335
- findings.push(...checkBaselineLines({ ...parsed, tasks }, fs));
1347
+ findings.push(...checkBaselineLines({ ...parsed, tasks }, fs, local));
1336
1348
  const declarationsForLeg = declarations.filter(d => d.repo === leg.repo);
1337
1349
  if (declarationsForLeg.length !== 1) findings.push({ check: "baseline-line", line: 0, text: leg.repo, reason: "expected one Baseline declaration per leg" });
1338
- for (const task of tasks) for (const bullet of task.testsBaseline) if (bullet.value.split(" ")[0] !== declarationsForLeg[0]?.sha) findings.push({ check: "baseline-line", line: bullet.line, text: bullet.text, reason: `Task ${task.number}: baseline differs from ${leg.repo} declaration` });
1350
+ for (const task of tasks) for (const bullet of task.testsBaseline) if (!local && bullet.value.split(" ")[0] !== declarationsForLeg[0]?.sha) findings.push({ check: "baseline-line", line: bullet.line, text: bullet.text, reason: `Task ${task.number}: baseline differs from ${leg.repo} declaration` });
1339
1351
  }
1340
1352
  for (const declaration of declarations) if (!ownership.legs.some(l => l.repo === declaration.repo)) findings.push({ check: "baseline-line", line: declaration.line, text: declaration.repo, reason: "unknown baseline leg" });
1341
- } else findings.push(...checkBaselineLines(parsed, fs));
1353
+ } else findings.push(...checkBaselineLines(parsed, fs, local));
1342
1354
  findings.push(...parsed.openQuestions.findings);
1343
1355
  const requiredLiterals = computeRequiredLiteralsPerTask(parsed, specLines);
1344
1356
  findings.push(...checkPlaceholderScan(parsed, requiredLiterals));
@@ -50,7 +50,27 @@ test("textOf concatenates text content blocks", () => {
50
50
  assert.equal(textOf("nope"), "");
51
51
  });
52
52
 
53
+ test("structured findings use only Outputs, including zero counts and repeated Stop summaries", async () => {
54
+ const { countStructuredFindings } = await import("./telemetry-collect.ts");
55
+ const carrier = (value: unknown) => `## Outputs\n### Step 1\n#### Task 1 (wave-reviewer, as review)\n\`\`\`json\n${JSON.stringify({ structured: value })}\n\`\`\`\nOutput saved to: /tmp/review.md`;
56
+ for (const [name, value, expected] of [
57
+ ["counts", { findings: { critical: 1, moderate: 2, minor: 3 } }, { blocker: 1, major: 2, minor: 3 }],
58
+ ["zeros", { findings: { critical: 0, moderate: 0, minor: 0 } }, { blocker: 0, major: 0, minor: 0 }],
59
+ ["absent", {}, undefined],
60
+ ["negative", { findings: { critical: -1, moderate: 2, minor: 3 } }, undefined],
61
+ ["fraction", { findings: { critical: 0.5, moderate: 2, minor: 3 } }, undefined],
62
+ ["missing", { findings: { critical: 1, minor: 3 } }, undefined],
63
+ ] as const) {
64
+ const text = carrier(value);
65
+ assert.deepEqual(countStructuredFindings(`${text}\n## Stop\n${text.replace("## Outputs", "summary")}`), expected, name);
66
+ }
67
+ assert.equal(countStructuredFindings(carrier({ findings: { critical: 1, moderate: 2, minor: 3 } }).replace("## Outputs", "## Other")), undefined);
68
+ assert.equal(countStructuredFindings("## Outputs\n```json\n{invalid}\n```"), undefined);
69
+ assert.deepEqual(countStructuredFindings('## Outputs\n### Step 1\n#### Task 1 (wave-reviewer)\n```json\n{"findings":{"critical":1,"moderate":2,"minor":3}}\n```\nOutput saved to: /tmp/review.md'), { blocker: 1, major: 2, minor: 3 });
70
+ assert.deepEqual(countFindings("spec-reviewer [Critical]\ncode-reviewer [Moderate]"), { blocker: 1, major: 1, minor: 0 });
71
+ });
72
+
53
73
  test("constant sets", () => {
54
- assert.deepEqual([...REVIEWER_AGENTS], ["spec-reviewer", "code-reviewer", "conformance-reviewer"]);
74
+ assert.deepEqual([...REVIEWER_AGENTS], ["wave-reviewer", "conformance-reviewer"]);
55
75
  assert.deepEqual([...COUNTED_USER_PHASES], ["plan", "implement", "verify", "ship"]);
56
76
  });
@@ -30,7 +30,29 @@ export function countOpenGaps(text: string): number | undefined {
30
30
  return gaps.size || undefined;
31
31
  }
32
32
 
33
- export const REVIEWER_AGENTS = new Set(["spec-reviewer", "code-reviewer", "conformance-reviewer"]);
33
+ export function countStructuredFindings(text: string): { blocker: number; major: number; minor: number } | undefined {
34
+ const outputs = /^## Outputs\s*$/m.exec(text);
35
+ if (!outputs) return undefined;
36
+ const body = text.slice(outputs.index + outputs[0].length).split(/^## /m)[0];
37
+ const total = { blocker: 0, major: 0, minor: 0 };
38
+ let found = false;
39
+ for (const match of body.matchAll(/^```json\s*\n([\s\S]*?)^```/gm)) {
40
+ try {
41
+ const value = JSON.parse(match[1]);
42
+ const findings = value?.structured?.findings ?? value?.findings;
43
+ if (!findings || ![findings.critical, findings.moderate, findings.minor].every((n) => Number.isSafeInteger(n) && n >= 0)) continue;
44
+ total.blocker += findings.critical;
45
+ total.major += findings.moderate;
46
+ total.minor += findings.minor;
47
+ found = true;
48
+ } catch {
49
+ // A malformed carrier falls back to the historical severity tags.
50
+ }
51
+ }
52
+ return found ? total : undefined;
53
+ }
54
+
55
+ export const REVIEWER_AGENTS = new Set(["wave-reviewer", "conformance-reviewer"]);
34
56
  export const COUNTED_USER_PHASES = new Set<PhaseKey>(["plan", "implement", "verify", "ship"]);
35
57
 
36
58
  export const textOf = (content: unknown): string =>