baldart 4.53.0 → 4.53.2

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to BALDART will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.53.2] - 2026-06-18
9
+
10
+ **`/new` completeness/AC verification no longer treats a quote-broken grep as proof an AC is unimplemented.** Diagnosed from a real `/new FEAT-0035` team-mode L1 run: the orchestrator spot-checked AC-3 by grepping `data/products.ts` for the `mode:'catalog'|'picker'` token, the pattern's embedded single-quotes + pipe broke the shell/regex parse inside the `echo "=== … ===" && grep` block → **0 matches even though `mode` was fully implemented** (`ListProductsMode` type + `mode??'catalog'` + picker branch all present) → a false "AC-3 not implemented" alarm. It self-recovered (the orchestrator noticed the grep contradicted the coder's completion report and re-grepped correctly), but a false "AC absent" can otherwise trigger a needless gap-fix coder re-spawn — the same waste class as v4.53.1. Two compounding causes, both addressed: (1) grep-as-AC-evidence over **code-shaped tokens** is brittle (the verification phase is prose-mandated to "grep changed files for the implementation"), and (2) `0 matches` was conflated with "absent". Added a **Grep-verification discipline** to the completeness SSOT: search literals with `rg -F` (fixed-string, one fully-single-quoted pattern per call — never folded into an `echo "===…" && grep` composition), and a `0-match` that CONTRADICTS the coder's structured completion report is a likely broken grep → re-confirm by Reading the cited `file:line` BEFORE flagging or re-spawning (never re-spawn a coder on a 0-match alone). **PATCH** (verification-guidance hygiene to existing prose; no code/behaviour change in the workflows, no `baldart.config.yml` key).
11
+
12
+ ### Fixed
13
+
14
+ - **`framework/.claude/skills/new/references/completeness.md`** — Phase 2.5 gains a "Grep-verification discipline" MUST note governing every grep check in the phase (step 0 binary-outcome branches, step 4 symbol verification, the `data_fields` checks): `rg -F` for code-shaped literals, one pattern per call; `0 matches` ≠ absent; a 0-match contradicting the coder report → re-confirm at the cited `file:line` before classifying Missing/Partial or spawning a gap-fix agent.
15
+ - **`framework/.claude/skills/new/references/team-mode.md`** — the post-completion (D.1+) on-disk spot-check step now cites that discipline, so a quote-broken AC/symbol grep in team mode cannot trigger a needless fix-coder.
16
+
17
+ ## [4.53.1] - 2026-06-18
18
+
19
+ **Review workflows no longer spawn fixers for verified-but-no-action findings, and a `finding_id` collision that turned applied fixes into false residuals is closed.** Diagnosed from a real `/new FEAT-0035` team-mode run where the per-wave review cluster spawned `security-reviewer` (Sonnet) + `coder` (Opus) for ~110k tokens to apply ~3 LOW fixes and **no-op ~8 non-actionable findings** — finders (simplify / security / codex / api-perf / doc) emit *verified observations that need NO change* (`minimal_fix_direction` = "No fix required" / "acceptable as-is" / "verified non-issue") as findings; these survive the false-positive check (they are TRUE, not false positives), are `preValidated` → skip Verify → classified `VERIFIED` → flood the fix partitions. The system conflated **VERIFIED with actionable**. Fix (two fail-safe layers): the `FINDING` schema gains an optional `requires_action` flag, every finder prompt is told to set it `false` (or simply not emit) a no-change observation, and a deterministic `isActionable()` gate — honoured for MED/LOW only, a **BLOCKER/HIGH is always actionable** so a cross-wave merge-ordering HIGH still surfaces as residual — segregates no-action findings into a recorded/counted bucket (`summary.noAction`, `decision=skipped reason=no-action`) that **never reaches a writer and is never silently dropped**. A regex backstop on the fix direction covers a finder that omits the flag. **Second defect (root cause of an observed empty-coder re-spawn):** finders number `finding_id` (`F###`) independently, so a security `F003` and a migration `F003` collide; the Fix-phase bookkeeping (`appliedIds`/`unresolvedIds`/`codeResidual`/`bucket`) keys on `finding_id` with flat Sets, so one finder's `unresolved` id dragged another finder's *applied* same-id fix into residual → the skill then re-spawned a coder that found nothing to do. Fixed by making `finding_id` globally unique at the fan-in (prefix `source` + index). Verified with a deterministic test that extracts the real gate from source and runs it over the actual FEAT-0035 findings (4 no-action LOW suppressed incl. a mid-prose "no fix required", 3 real LOW + the cross-wave HIGH kept) plus a collision regression (bug reproduces pre-fix, gone post-fix). Applied to **both** review workflows + their prose-SSOT (so the inline fallback benefits) + (opt-in) the finder agents' system prompts. **PATCH** (economy/quality refinement of existing workflows; no new agent/skill/command, no install-layout change, no `baldart.config.yml` key).
20
+
21
+ ### Fixed
22
+
23
+ - **`framework/.claude/workflows/new-card-review.js`** — `FINDING` schema gains `requires_action`; finder prompts (codex / simplify / security / code-reviewer fallback) instructed to flag no-action observations; new `NO_ACTION_RE` + `isNoAction()` gate (MED/LOW only) segregates a `noAction` partition excluded from `securityFix`/`actionable`/`docResidual` and logged; `finding_id` made globally unique at the fan-in (fixes the cross-finder `appliedIds`/`unresolvedIds`/`codeResidual` contamination); coder fix-pass prompt clarifies `applied` = edit made even if the build stays red for out-of-scope reasons, `unresolved` = the edit itself could not be made; `summary.noAction` added.
24
+ - **`framework/.claude/workflows/new-final-review.js`** — same `requires_action` schema field + finder-prompt instruction (codex / doc / api-perf / code-reviewer fallback) + `finding_id` uniqueness at the fan-in; no-action VERIFIED MED/LOW findings segregated out of the returned `findings` into a new `noActionFindings` field + `summary.noAction`, so the skill's F.5 writer partition never routes them.
25
+ - **`framework/.claude/skills/new/references/review-cycle.md`** / **`final-review.md`** — drift-note + Phase-2.55 step-4 / F.4 classification + F.5 partition updated to document the actionability gate and the `finding_id` uniqueness (the inline fallback path mirrors the workflow). `codex-gate.md` unchanged (its per-card gate only auto-fixes BLOCKER/HIGH, which are never no-action).
26
+ - **`framework/.claude/agents/code-reviewer.md`**, **`api-perf-cost-auditor.md`**, **`security-reviewer.md`**, **`doc-reviewer.md`** — an Actionability clause after the FP challenge: a verified-safe / already-accurate observation is a cleared concern, not an actionable finding (set `requires_action:false` or omit; MED/LOW only) — defense-in-depth that also benefits standalone `/codexreview`, `/prd`, `/bug`.
27
+ - **`framework/.claude/workflows/new2.js`** / **`new2-resolve.js`** — unchanged (justified): they route only BLOCKER/HIGH findings, which the severity invariant always keeps actionable, so no-action MED/LOW never reach them; they group by file/area, not `finding_id`.
28
+
8
29
  ## [4.53.0] - 2026-06-18
9
30
 
10
31
  **Worktree setup for `/new` / `/nw` / `new2` is now a deterministic SSOT script — no model in the loop for pure plumbing.** Diagnosed from a real `/new FEAT-0036 -full` run where the Phase-0 worktree-setup subagent **created the worktree but went idle before `npm install`** (node_modules missing, no registry entry), forcing the orchestrator to burn turns probing → messaging the agent → waiting → cleaning the half-built orphan → falling back to inline `/nw` (which reloads the ~1200-line skill body into the orchestrator — the exact cost the delegation was meant to avoid). Same root cause as the earlier haiku *fabrication* (a well-formed `baseline: pass` with nothing on disk, 2/2): a **model in the loop for mechanical work** — it either fabricates the expected return or stalls mid-execution. The fix removes the model: the entire build sequence (worktree add → sync untracked cards → install → copy `stack.env_files` → allocate port → write registry → baseline) now lives **once** in `framework/.claude/skills/worktree-manager/scripts/setup-worktree.sh` and is invoked identically by all three callers as a background `Bash`. A deterministic script cannot fabricate or stall: it does the work or fails honestly with a log + a structured manifest. The orchestrator's disk-verification gate (setup.md step 6a) and git-authoritative idempotency pre-check (step 4a2) are unchanged — defence-in-depth survives the redesign. Non-interactive HARD (stdin closed, `CI=1`, build under `timeout 600`); port allocation is atomic under a `.worktrees/.wt-setup.lock` mutex (mirrors `allocate-id.sh`) so concurrent `/nw` runs never collide; the registry is written via `node` (atomic tmp+rename, no jq dependency); the script never self-repairs a failing baseline (role boundary — the coder does, downstream). Verified end-to-end on a throwaway repo: happy path (worktree + env+PORT + card-sync + `buildVerified:true` + manifest), collision (loud fail, exit 3), build-fail (`baseline:fail`, port held, `buildVerified:false`), and second-worktree port allocation (3001+3002 reserved → 3003). Opt-in-with-fallback: a pre-this-release subtree without the script falls back to inline `/nw`. **MINOR** (new shipped SSOT asset + behaviour change to existing skills; backward-compatible, no install-layout or command change, no `baldart.config.yml` key — `stack.env_files` / `toolchain.commands.*` are reused).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.53.0
1
+ 4.53.2
@@ -202,6 +202,10 @@ Consider:
202
202
 
203
203
  **Exception**: never-demote items above are never false positives in this project — do not suppress them.
204
204
 
205
+ ### Actionability Pass (after the FP challenge)
206
+
207
+ A finding that survives the FP challenge can still need NO change — the pattern is acceptable as-is (e.g. an admin path where the optimization is genuinely not worth it). That is a cleared concern, **not an actionable finding**: reporting it spawns a downstream fixer to no-op it. Either omit it, or — when a consumer schema exposes a `requires_action` field — set `requires_action:false` (recorded, never routed to a writer). Record cleared concerns under a separate "Verified, no action" list. Guard: MEDIUM/LOW only (a BLOCKER/HIGH is never no-action); a change needed but out of your scope is NOT no-action — report it (it surfaces as residual).
208
+
205
209
  ## Load Simulation Pass (MANDATORY — execute mentally before findings)
206
210
 
207
211
  Walk the changed handler/cron/query as if it were running under realistic production load. For each entry point in scope:
@@ -288,6 +288,17 @@ Consider:
288
288
 
289
289
  **Never-demote items above are never false positives — do not suppress them.**
290
290
 
291
+ ### Actionability Pass (after the FP challenge)
292
+
293
+ A finding that survives the FP challenge can still need NO change — the code is fine as-is, and the only honest "fix" is "no fix required" / "acceptable as-is" / "verified safe". That is **not a finding**: it is a cleared concern. Do NOT report it as an actionable finding (a downstream fixer would be spawned to no-op it). Either omit it, or — when a consumer schema exposes a `requires_action` field — set `requires_action:false` so it is recorded but never routed to a writer. Record cleared concerns separately:
294
+
295
+ <details>
296
+ <summary>Verified, no action (N items)</summary>
297
+ - **Title** — why it is safe as-is
298
+ </details>
299
+
300
+ Guard: this applies to MEDIUM/LOW only — a BLOCKER/HIGH is never "no action". And if a change IS needed but is out of your scope, that is NOT no-action: report it as a real finding (it will surface as residual).
301
+
291
302
  ## Diff Simulation Pass (MANDATORY — execute mentally before findings)
292
303
 
293
304
  Walk the diff as if you were the runtime. For each non-trivial changed function/handler:
@@ -233,6 +233,7 @@ Key points:
233
233
  - Fast Mode: verdict + one-liner only.
234
234
 
235
235
  ## Constraints
236
+ - **Actionability (when pooled as a finder, e.g. in the final-review fan-out):** a doc you VERIFIED is already accurate / up-to-date is a cleared concern, **not a finding** — do not emit it as a `LOW`/`MEDIUM` finding (a downstream fixer would be spawned to no-op it). Either omit it, or — when the pooled schema exposes a `requires_action` field — set `requires_action: false` (recorded, never routed to a writer). In your own write mode this is moot (you simply make no edit). A `BLOCKER`/`HIGH` is never no-action.
236
237
  - For **feature documentation**: WRITE missing docs directly. You are fully responsible -- do not defer to other agents.
237
238
  - For **general audits**: PROPOSE structural changes, but WRITE fixes for factual gaps, stale content, and missing entries.
238
239
  - **ALWAYS identify the macro feature** -- even for small bug fixes or one-off cards.
@@ -144,6 +144,13 @@ Map your severity labels to the pooled enum: Critical → `BLOCKER`, High → `H
144
144
  `# Security Review Summary` (scope / overall risk / main attack surfaces / most critical concern) so
145
145
  the orchestrator has a header. Do NOT emit the long markdown finding blocks in this mode.
146
146
 
147
+ **Actionability**: a control you have VERIFIED as safe-as-is (the honest recommendation is "no fix
148
+ required" / "verified safe") is a cleared concern, **not a finding** — emitting it as a `LOW`/`MEDIUM`
149
+ finding makes the orchestrator spawn a fixer to no-op it. Either omit it, or — when the pooled schema
150
+ exposes a `requires_action` field — set `requires_action: false` (recorded for the audit trail, never
151
+ routed to a writer). A `BLOCKER`/`HIGH` is never no-action; a fix needed but out of your ownership IS a
152
+ real finding (report it as residual, not as no-action).
153
+
147
154
  ### Mode B — Standalone review (when invoked directly by a human)
148
155
 
149
156
  When invoked directly (no pooling orchestrator), use this human-readable structure:
@@ -6,6 +6,10 @@
6
6
 
7
7
  Before triggering any review, you MUST verify that the coder agent implemented **every requirement and acceptance criterion** in the backlog card. This is a blocking gate — do NOT proceed to Phase 3 with unimplemented items.
8
8
 
9
+ > **Grep-verification discipline (MUST — applies to every grep check in this phase: step 0, step 4, the `data_fields` checks).** The completeness checks below confirm an item by grepping the changed files for a token. Two failure modes turn a *false* "not found" into a *false* "Missing/Partial":
10
+ > 1. **Quote-broken pattern.** Code-shaped tokens contain shell/regex metacharacters — a TS union literal `mode:'catalog'|'picker'`, an object key, anything with `'` `"` `|` `[]` `?` `(`. Embedding such a pattern in an `echo "=== … ===" && grep …` block or an unquoted `-E` arg breaks the shell/regex parse → the grep matches **nothing even though the code is there**. **Search literals with `rg -F '<exact string>'` (fixed-string, no regex), single-quote the WHOLE pattern, and run ONE pattern per `rg` call** — never fold the search token into a quoted header/echo composition.
11
+ > 2. **`0 matches` ≠ absent.** A zero-match result is ambiguous (true miss vs. broken pattern / wrong file / wrong escaping). Do NOT classify an item `Missing`/`Partial` on a 0-match alone. **If the 0-match CONTRADICTS the coder completion report's claim that the item is implemented, treat it as a likely broken grep — re-confirm by reading the report's cited `file:line` (or a corrected `rg -F`) BEFORE flagging or spawning a gap-fix agent.** Never re-spawn a coder on a 0-match alone. When unsure, a targeted `Read` at the report's `file:line` is more reliable than a hand-rolled grep.
12
+
9
13
  **Step-by-step**:
10
14
 
11
15
  0. **Conditional requirements pre-scan** (BLOCKING — before building the main checklist):
@@ -124,7 +124,12 @@ that is a **gate violation**: log it as
124
124
  > *orchestration shape* of F.2–F.4; its agent briefs cite THIS module for the
125
125
  > review *semantics*, so the SSOT for what each reviewer checks stays here. When
126
126
  > you change the F.2–F.4 reviewer set, gates, or classification rules, update the
127
- > workflow's phase/agent wiring to match.
127
+ > workflow's phase/agent wiring to match. Since v4.53.1 the workflow also (a) makes every
128
+ > `finding_id` globally unique at the fan-in (finders number `F###` independently → a collision
129
+ > corrupts downstream finding_id-keyed routing/telemetry), and (b) runs an **actionability gate** that
130
+ > segregates VERIFIED-but-no-change findings (`requires_action:false` / regex backstop, MED/LOW only)
131
+ > into `noActionFindings` + `summary.noAction`, keeping them out of the returned `findings` the skill
132
+ > routes to writers in F.5. Keep both mirrored with the inline F.4 classification above.
128
133
 
129
134
  ### Step F.2 — Architecture baseline
130
135
 
@@ -189,6 +194,10 @@ that is a **gate violation**: log it as
189
194
  - minimal_fix_direction: what to change
190
195
  5. Run mandatory false-positive check: for each finding, ask 'What is the strongest
191
196
  argument this is a false positive?' Suppress if the FP argument is convincing.
197
+ 5.5 Actionability: an observation you have VERIFIED needs NO change anywhere (the honest direction
198
+ would be 'no fix required' / 'acceptable as-is' / 'verified non-issue') is not work — set
199
+ requires_action:false on it (recorded, never routed to a fixer), or simply do not report it.
200
+ This is for MEDIUM/LOW only; a BLOCKER/HIGH is never no-action.
192
201
  6. Classify surviving findings as VERIFIED, FALSE_POSITIVE, or NEEDS_MANUAL_CONFIRMATION.
193
202
 
194
203
  Return ONLY verified findings. If zero verified bugs: state 'No verified bugs found.'
@@ -248,12 +257,12 @@ that is a **gate violation**: log it as
248
257
  - **Each domain specialist OWNS its lane end-to-end** — it runs its OWN false-positive check in the finding pass (doc-reviewer for doc, api-perf-cost-auditor for api/perf/data, and the Codex/`code-reviewer`-fallback engine for code), so its surviving findings are **already validated**. Do **NOT** cross-validate a doc or api finding by spawning `code-reviewer` (that is the WRONG specialist judging another domain), and do **NOT** re-run the same specialist over its own findings (self-judge — no model diversity). Codex findings are likewise already FP-validated (Step F.3 protocol includes it).
249
258
  - **Residual only:** a finding the originating specialist explicitly leaves UNRESOLVED (`confidence < 80`) is routed to the finding's **domain specialist** (by `domain`: doc→doc-reviewer, api/perf→api-perf-cost-auditor, security/migration→security-reviewer, test→qa-sentinel, else code-reviewer) over the cited file:line. If that domain specialist **is** the originating finder (it already had its pass), classify `NEEDS_MANUAL_CONFIRMATION` instead of re-spawning it — never self-judge, never silently drop.
250
259
  - Classify: `VERIFIED` | `FALSE_POSITIVE` | `NEEDS_MANUAL_CONFIRMATION`.
251
- - `VERIFIED` findings proceed to fixes. **`NEEDS_MANUAL_CONFIRMATION` findings are NOT discarded** — list them in `## Issues & Flags` and surface them to the user via `AskUserQuestion` (treat as VERIFIED, treat as FALSE_POSITIVE, or hand off) before merge. Only `FALSE_POSITIVE` are dropped.
260
+ - `VERIFIED` findings proceed to fixes — **EXCEPT no-action ones** (`requires_action:false`, or a "no fix required" / "acceptable as-is" / "verified non-issue" direction; MEDIUM/LOW only). A no-action finding is TRUE but needs no change: record it (`decision=skipped | reason=no-action`) and do NOT route it to a writer. (The delegated `new-final-review` workflow returns these separately as `noActionFindings` + `summary.noAction`; the inline path classifies them here.) **`NEEDS_MANUAL_CONFIRMATION` findings are NOT discarded** — list them in `## Issues & Flags` and surface them to the user via `AskUserQuestion` (treat as VERIFIED, treat as FALSE_POSITIVE, or hand off) before merge. Only `FALSE_POSITIVE` are dropped.
252
261
 
253
262
  ### Step F.5 — Apply fixes and final build
254
263
 
255
264
  10. **Persist verified findings** to `/tmp/batch-final-review-<FIRST-CARD-ID>.md`.
256
- 11. **Merge-blocking gate (mirrors the per-card Phase 3.7 gate this final pass backstops):** if any VERIFIED **BLOCKER or HIGH** finding exists, it MUST be resolved before Phase 6 merge. Apply fixes by **domain owner** (since v3.40.0 — same Domain-Override routing as the per-card phases), then re-verify; if a BLOCKER/HIGH cannot be resolved in a single apply + one retry, log it in `## Issues & Flags` and invoke `AskUserQuestion` (override with reason / escalate to a follow-up card / halt) — do NOT proceed to Phase 6 with an unresolved BLOCKER or HIGH. VERIFIED findings of severity MEDIUM are also applied (advisory below that). Partition the verified findings by the **Domain-Override match rules** ("Domain-Override Domains"):
265
+ 11. **Merge-blocking gate (mirrors the per-card Phase 3.7 gate this final pass backstops):** if any VERIFIED **BLOCKER or HIGH** finding exists, it MUST be resolved before Phase 6 merge. Apply fixes by **domain owner** (since v3.40.0 — same Domain-Override routing as the per-card phases), then re-verify; if a BLOCKER/HIGH cannot be resolved in a single apply + one retry, log it in `## Issues & Flags` and invoke `AskUserQuestion` (override with reason / escalate to a follow-up card / halt) — do NOT proceed to Phase 6 with an unresolved BLOCKER or HIGH. VERIFIED findings of severity MEDIUM are also applied (advisory below that). Partition the verified findings — **excluding the no-action ones segregated in F.4** (`requires_action:false` / no-change direction, MED/LOW; they are recorded, never fixed) — by the **Domain-Override match rules** ("Domain-Override Domains"):
257
266
  - **`doc`-domain findings** (file path matching the `doc` match rule — `*.md` under `${paths.references_dir}`/`${paths.prd_dir}`, `CHANGELOG.md`, `ssot-registry.md`) → invoke the **doc-reviewer** agent once in write mode to apply them. NEVER route doc fixes to coder.
258
267
  - **`security`-domain findings** (path in `paths.high_risk_modules`, or RLS-policy SQL) → route to **security-reviewer** in write mode (canonical writer map v4.26.1 — it owns the security-invariant contract a coder lacks; NEVER route security fixes to coder). **`migration`-domain findings** (SQL under the migrations dir) → route to **coder**. For both, apply the Sub-agent failure protocol's STOP-on-crash rule (never inline-fallback on a security/migration fix). These are NOT collapsed into a generic "everything else" bucket.
259
268
  - **All remaining findings** (other code, perf, test) → invoke the **coder** agent once to apply them in a single pass.
@@ -74,6 +74,13 @@ so it surfaces in telemetry.
74
74
  `gateTable` is all PASS/SKIP and `checksFailed` is false** — a delegated QA FAIL must block exactly
75
75
  as the inline path does. (`gateTable` is **wave-scoped**, not per-card — qa-sentinel runs once over
76
76
  the worktree; a FAIL applies to the whole wave's commit.)
77
+ - **No-action findings (since v4.53.1)**: the workflow already segregated VERIFIED findings that
78
+ need NO change (finder `requires_action:false`, or a regex backstop on a "no fix required" /
79
+ "acceptable as-is" / "verified non-issue" direction — honoured for MED/LOW only; BLOCKER/HIGH are
80
+ always actionable). They are **counted in `summary.noAction`** and **excluded from both the writer
81
+ passes and `residual`** — so they never spawn a fixer and never surface as user-facing residual.
82
+ Append one `## Fix Application Log` row each as `decision=skipped | reason=no-action` (audit trail,
83
+ no silent drop). Do NOT re-route them to a writer.
77
84
  - **Telemetry**: append the workflow's `fixesApplied` + `residual` to `## Fix Application Log`
78
85
  (`decision=workflow`). Derive the Phase-8 producers from the return: `qa_first_attempt` ←
79
86
  `summary.qaRan ? (summary.failingGates.length === 0 && !summary.checksFailed ? "pass" : "fail") : "n/a"`
@@ -90,7 +97,12 @@ so it surfaces in telemetry.
90
97
  > review cluster; its agent briefs cite THIS module for the review *semantics*, so the SSOT for what each
91
98
  > finder checks stays here. When you change the Simplify lenses, the qa contract, the Codex/Step-A
92
99
  > profile rule, or the FP-check/routing, update the workflow's phase/agent wiring to match. The
93
- > delegated path runs the doc-review (Phase 3) on the FINAL post-fix/post-E2E code; the inline path runs
100
+ > workflow also (since v4.53.1) (a) makes every `finding_id` globally unique at the fan-in finders
101
+ > number `F###` independently and the Fix-phase bookkeeping keys on `finding_id`, so a collision used
102
+ > to drag one finder's applied fix into another's residual; and (b) runs an **actionability gate** that
103
+ > keeps VERIFIED-but-no-change findings (finder `requires_action:false` / regex backstop, MED/LOW only)
104
+ > out of the writer passes. Keep both in sync with the inline path below (step 4 skip-classification).
105
+ > The delegated path runs the doc-review (Phase 3) on the FINAL post-fix/post-E2E code; the inline path runs
94
106
  > it at its current position — both are doc-relevance-gated and both are backstopped by the Final FULL
95
107
  > doc-reviewer.
96
108
 
@@ -114,6 +126,7 @@ After completeness is verified, clean up the implementation before it reaches re
114
126
  - **Valid AND in a Domain-Override domain** (the finding's target file matches the `doc`, `security`, or `migration` match rule in "Domain-Override Domains") → do NOT apply inline. Delegate to the domain **writer** (canonical writer map v4.26.1): `doc` → `doc-reviewer` (write mode), `security` → `security-reviewer` (write mode — it owns the security-invariant contract a coder lacks), `migration` → `coder`. Even a one-line efficiency fix in `paths.high_risk_modules` (security) or a migration file goes to the owning agent — the orchestrator lacks that domain's invariant contract.
115
127
  - **Valid AND not in a Domain-Override domain** → fix directly (apply edits inline).
116
128
  - **False positive / not worth addressing** → skip, BUT record it (see telemetry). If the skip rests on a "covered by X" / "redundant" / "not needed" rationalization (the same family the AC-Closure Gate guards against), do NOT discard silently — verify the rationale by reading `X`, and if it does not hold, treat the finding as valid.
129
+ - **Verified but no action needed** (the finding is TRUE but its only honest direction is "no fix required" / "acceptable as-is" / "verified safe" — a cleared concern, not work) → do NOT apply and do NOT delegate; record it with `decision=skipped | reason=no-action`. This is distinct from a false positive (the observation holds) and from "not worth addressing" (a judgement call). Guard: this is for `MEDIUM`/`LOW` only — a `BLOCKER`/`HIGH` is never "no action". If a change IS needed but not here (out of this card's scope), it is NOT no-action — keep it valid and let it surface as residual. (This mirrors the delegated workflow's `requires_action` gate.)
117
130
 
118
131
  **Telemetry (Fix Application Log)** — for EVERY finding (valid OR skipped) append one row to the tracker's `## Fix Application Log` section per the schema above. Use `domain=simplify-{reuse|quality|efficiency}` matching the originating agent. Include the `severity` trailing key. Inline: `decision=inline | applied_by=orchestrator | est_lines=<n> | severity=<HIGH|MEDIUM> | finding=<1-line>`. Delegated (domain-override): `decision=<coder|doc-reviewer> | applied_by=<coder|doc-reviewer> | est_lines=<n> | severity=<...> | finding=<1-line>`. Skipped: `decision=skipped | applied_by=- | est_lines=0 | reason=<false-positive|not-worth-addressing>`.
119
132
 
@@ -152,6 +152,8 @@ For each completed agent:
152
152
 
153
153
  After ALL agents in the group complete successfully:
154
154
 
155
+ > **On-disk spot-checks follow the grep-verification discipline** (`references/completeness.md` § "Grep-verification discipline"): when you grep the worktree to confirm a coder implemented an AC / symbol / field, search code-shaped tokens with `rg -F '<exact string>'` (one pattern per call, fully single-quoted — never inside an `echo "===…" && grep` block), and treat a `0-match` that CONTRADICTS the coder's completion report as a likely broken grep → re-confirm by reading the cited `file:line` BEFORE alarming or re-spawning. A false "AC not implemented" from a quote-broken grep must not trigger a needless fix-coder.
156
+
155
157
  1. **D.1 — Build verification (group)** — Run `npm run build` (when `has_toolchain`, the configured `toolchain.commands.build` verbatim — § "Toolchain gates") in the worktree to verify combined changes compile (redirect to `/tmp/build-group.txt` per § "Context economy"; surface only exit code + bounded extract on failure). If build fails, identify which card's changes broke it (from `git diff --name-only` per card), spawn a targeted fix-coder for those files only.
156
158
 
157
159
  1.5. **D.1.5 — Effective per-card review profile (compute ONCE; drives D.2 + D.4b)** — For EACH card in the group, compute its **effective codex profile** with the SAME deterministic rule the sequential Phase 3.7 Step C uses, so the two paths never disagree:
@@ -80,6 +80,7 @@ const FINDING = {
80
80
  evidence: { type: 'string', description: 'exact file:line + code quote' },
81
81
  minimal_fix_direction: { type: 'string' },
82
82
  domain: { enum: ['doc', 'security', 'migration', 'code', 'perf', 'test'], description: 'Domain-Override routing bucket' },
83
+ requires_action: { type: 'boolean', description: 'false = a VERIFIED observation that needs NO change anywhere (concern cleared / safe-as-is / advisory) — recorded for audit, NEVER routed to a writer. Omit (⇒ actionable) when a concrete edit is needed. If a change is needed but not by you (out of scope), keep true and say so in minimal_fix_direction (it surfaces as residual).' },
83
84
  },
84
85
  }
85
86
  const FINDINGS_SCHEMA = {
@@ -200,7 +201,8 @@ const codexPrompt =
200
201
  `Return codexAvailable:false ONLY if $REVIEW_FILE ends up containing "CODEX_NOT_FOUND" or stays empty after the FULL 10-minute window — NEVER because a single Bash call returned slowly.\n\n` +
201
202
  `${waveBrief}\n\n${baselineBrief}\n\n` +
202
203
  `For each finding return: finding_id, title, severity (BLOCKER|HIGH|MEDIUM|LOW), confidence (0-100), evidence (exact file:line + code quote), minimal_fix_direction, and domain (doc|security|migration|code|perf|test). ` +
203
- `Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated). Set codexAvailable:true when the review ran.`
204
+ `Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated). ` +
205
+ `Then the actionability check: an observation you have VERIFIED needs NO change anywhere (the fix direction would be "no fix required" / "acceptable as-is" / "verified non-issue") is not work — set requires_action:false on it (it is recorded but never sent to a fixer), or simply do not emit it. Emit requires_action true/omitted ONLY when a concrete edit is needed. Set codexAvailable:true when the review ran.`
204
206
 
205
207
  const tcGateLines = [
206
208
  ['lint', tcCmds.lint], ['typecheck', tcCmds.typecheck], ['test', tcCmds.test],
@@ -218,12 +220,14 @@ function simplifyPrompt(c) {
218
220
  ` • Quality — redundant state, parameter sprawl, copy-paste with slight variation, leaky abstractions, stringly-typed code where enums exist, unnecessary JSX nesting, WHAT-comments / narration.\n` +
219
221
  ` • Efficiency — redundant computation, duplicate API calls, N+1, missed concurrency, hot-path bloat, missing change-detection guards, unbounded structures.\n\n` +
220
222
  `${cardScopeBrief(c)}\n\n${baselineBrief}\n\n` +
221
- `Run a false-positive check on every finding and SUPPRESS the unconvincing ones (your surviving findings are treated as validated). Return findings with domain in {code, perf}. Flag only a finding you genuinely cannot resolve as confidence < 80.`
223
+ `Run a false-positive check on every finding and SUPPRESS the unconvincing ones (your surviving findings are treated as validated). ` +
224
+ `Then the actionability check: an observation you have VERIFIED needs NO change (the fix direction would be "no fix required" / "acceptable as-is" / "optional") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it. Return findings with domain in {code, perf}. Flag only a finding you genuinely cannot resolve as confidence < 80.`
222
225
  }
223
226
 
224
227
  function securityPrompt(c) {
225
228
  return `AppSec review (read-only) over ONE card's committed diff — auth, permissions, secrets, webhooks, file upload, infra, multi-tenant isolation, injection. Security-sensitive paths: ${highRisk.join(', ') || '(none configured)'}.\n\n${cardScopeBrief(c)}\n\n${baselineBrief}\n\n` +
226
- `You OWN the security domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as validated and are NOT re-judged by another agent. Flag only a finding you genuinely cannot resolve as confidence < 80. Return findings with domain in {security, migration}.`
229
+ `You OWN the security domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as validated and are NOT re-judged by another agent. ` +
230
+ `Then the actionability check: a control you have VERIFIED as safe-as-is needs NO change (the fix direction would be "no fix required" / "verified safe") — set requires_action:false on it (recorded for the security audit trail, never sent to a fixer), or do not emit it. Flag only a finding you genuinely cannot resolve as confidence < 80. Return findings with domain in {security, migration}.`
227
231
  }
228
232
 
229
233
  // Build the finder fan-out. Per-card: simplify (if runSimplify), security (if hasSecurityFiles).
@@ -273,12 +277,19 @@ for (const item of findResults) {
273
277
  if (!codexRan) {
274
278
  codexEngine = 'code-reviewer (fallback)'
275
279
  const fb = await agent(
276
- `Codex was unavailable for this wave's code review. Run the FULL code review yourself over the wave diff, per ${protocolRef} (Phase 3.7).\n\n${waveBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied (your findings are treated as validated).`,
280
+ `Codex was unavailable for this wave's code review. Run the FULL code review yourself over the wave diff, per ${protocolRef} (Phase 3.7).\n\n${waveBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied (your findings are treated as validated). An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.`,
277
281
  { label: 'code-reviewer (fallback)', phase: 'Discovery', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
278
282
  )
279
283
  if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: true })))
280
284
  }
281
285
 
286
+ // Globally-unique finding_id at the fan-in. Finders number F### INDEPENDENTLY, so a Codex F003 and
287
+ // a security F003 collide. The Fix-phase bookkeeping (appliedIds / unresolvedIds / codeResidual /
288
+ // bucket) keys on finding_id with FLAT Sets, so a collision cross-contaminates: one finder's
289
+ // `unresolved` id drags another finder's APPLIED same-id finding into residual (the empty-coder
290
+ // re-spawn bug). Prefix source + running index → uniqueness without losing the human-readable id.
291
+ raw.forEach((f, i) => { f.finding_id = `${f.source || 'src'}#${i}:${f.finding_id || ('F' + i)}` })
292
+
282
293
  // ───────────────────────────────────────────────────────────────────────────
283
294
  // Phase Verify — specialist-owned validation (parity with new-final-review.js F.4)
284
295
  // ───────────────────────────────────────────────────────────────────────────
@@ -330,16 +341,29 @@ const isDoc = (f) => /doc|wiki|ssot|readme/.test(String(f.domain).toLowerCase())
330
341
  // migration/test → coder), so match the exact 'security' domain, not the broader verifier regex.
331
342
  const isSecurity = (f) => String(f.domain).toLowerCase() === 'security'
332
343
  const isManual = (f) => f.classification === 'NEEDS_MANUAL_CONFIRMATION'
344
+ // Actionability gate: a VERIFIED finding that needs NO change must NOT reach a writer (it floods the
345
+ // fix pass with no-ops). Honoured ONLY for MED/LOW — a BLOCKER/HIGH is always actionable (the flag
346
+ // can never suppress a blocker; a cross-wave HIGH like "don't merge before the consumer cards" stays
347
+ // actionable → surfaces as residual). Signal: finder flag requires_action:false, OR a conservative
348
+ // regex backstop on the fix direction for when the model omitted the flag. Never a silent drop —
349
+ // no-action findings are logged + counted, just excluded from the writer partitions.
350
+ const NO_ACTION_RE = /(^|[.\s(])(no\s+(fix|change|action)\s+(is\s+)?(required|needed)|none\s+(needed|required)|not\s+a\s+(defect|bug|correctness\s+bug)|acceptable\s+as[-\s]is|verified\s+(safe|non[-\s]issue)|no\s+action\s+(needed|required))([.\s,;)]|$)/i
351
+ const isNoAction = (f) => (f.severity !== 'BLOCKER' && f.severity !== 'HIGH') &&
352
+ (f.requires_action === false || NO_ACTION_RE.test(String(f.minimal_fix_direction || '')))
333
353
  // Partition `surviving` (= VERIFIED + NEEDS_MANUAL; FALSE_POSITIVE already dropped) with NO overlap:
334
354
  // securityFix = VERIFIED security → security-reviewer applies (it owns the security invariants).
335
355
  // actionable = VERIFIED non-doc non-security → the coder fixes these.
336
356
  // docResidual = VERIFIED doc → the skill runs doc-reviewer post-E2E on final code.
337
357
  // manualResidual= NEEDS_MANUAL any → human gate, owned by the skill (a doc-manual must NOT be
338
358
  // silently auto-re-reviewed: it carries its needs-manual classification out).
339
- const securityFix = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && isSecurity(f))
340
- const actionable = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && !isSecurity(f))
341
- const docResidual = surviving.filter((f) => f.classification === 'VERIFIED' && isDoc(f))
359
+ // No-action VERIFIED findings: recorded + counted, excluded from every writer partition.
360
+ const noAction = surviving.filter((f) => f.classification === 'VERIFIED' && isNoAction(f))
361
+ const noActionSet = new Set(noAction)
362
+ const securityFix = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && isSecurity(f) && !noActionSet.has(f))
363
+ const actionable = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && !isSecurity(f) && !noActionSet.has(f))
364
+ const docResidual = surviving.filter((f) => f.classification === 'VERIFIED' && isDoc(f) && !noActionSet.has(f))
342
365
  const manualResidual = surviving.filter(isManual)
366
+ for (const f of noAction) log(`Review: no-action finding recorded (not routed to a writer): [${f.finding_id}] ${f.severity} ${f.domain} — ${f.title} (${f.requires_action === false ? 'requires_action:false' : 'no-action-inferred'})`)
343
367
 
344
368
  const SKIP_CHECKS = { lint: 'SKIP', tsc: 'SKIP', build: 'SKIP' }
345
369
 
@@ -360,7 +384,7 @@ async function applyFixPass(findings, writer, label, role) {
360
384
  findings.map((f) => `- [${f.finding_id}] (${f.card || '?'} / ${f.domain} / ${f.severity}) ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n') +
361
385
  `\n\nAfter applying: run \`${tc('lint', 'npm run lint')}\` and (when the project uses typescript) \`${tc('typecheck', 'npx tsc --noEmit')}\` and \`${tc('build', 'npm run build')}\` in the worktree. If a check fails because of an edit you made, fix the regression — at most 2 retries — staying within the allowed files. ` +
362
386
  `Do NOT commit. Do NOT git stash (refs/stash is shared across worktrees). ` +
363
- `Return: applied (finding_ids you fixed), unresolved (finding_ids you could NOT fix within the allowed files / 2 retries), and checks (PASS/FAIL/SKIP for lint, tsc, build).`
387
+ `Return: applied (finding_ids you fixed — list a finding here if you MADE the edit, even when the build stays red for reasons OUTSIDE this finding / your ownership, e.g. a known cross-wave cascade; the build status is reported separately in checks), unresolved (finding_ids you could NOT fix within the allowed files / 2 retries — i.e. the EDIT itself could not be made), and checks (PASS/FAIL/SKIP for lint, tsc, build).`
364
388
  const r = await agent(fixBrief, { label, phase: 'Fix', agentType: writer, schema: FIX_SCHEMA })
365
389
  // Normalize: the agent may die (null) or return a truthy object missing fields.
366
390
  const res = (r && typeof r === 'object') ? r : { applied: [], unresolved: findings.map((f) => f.finding_id), checks: { ...SKIP_CHECKS } }
@@ -407,6 +431,7 @@ const summary = makeSummary({
407
431
  cards: cards.length,
408
432
  totalFindings: raw.length,
409
433
  verified: surviving.filter((f) => f.classification === 'VERIFIED').length,
434
+ noAction: noAction.length, // VERIFIED-but-no-change findings: recorded, never sent to a writer
410
435
  falsePositive: classified.filter((f) => f.classification === 'FALSE_POSITIVE').length,
411
436
  needsManual: manualResidual.length,
412
437
  fixesApplied: appliedIds.size,
@@ -418,7 +443,7 @@ const summary = makeSummary({
418
443
  blockers: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
419
444
  highs: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
420
445
  })
421
- log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
446
+ log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
422
447
 
423
448
  return { codexEngine, perCard, gateTable, summary }
424
449
 
@@ -426,7 +451,7 @@ return { codexEngine, perCard, gateTable, summary }
426
451
  function asArr(x) { return Array.isArray(x) ? x.filter(Boolean) : [] }
427
452
  function dedupe(xs) { return Array.from(new Set(asArr(xs))) }
428
453
  function makeSummary(o) {
429
- return Object.assign({ cards: 0, totalFindings: 0, verified: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
454
+ return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
430
455
  }
431
456
  function slimFinding(f) {
432
457
  return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification, card: f.card }
@@ -54,6 +54,7 @@ const FINDING = {
54
54
  evidence: { type: 'string', description: 'exact file:line + code quote' },
55
55
  minimal_fix_direction: { type: 'string' },
56
56
  domain: { enum: ['doc', 'security', 'migration', 'code', 'perf', 'test'], description: 'Domain-Override routing bucket' },
57
+ requires_action: { type: 'boolean', description: 'false = a VERIFIED observation that needs NO change anywhere (concern cleared / safe-as-is / advisory) — recorded for audit, NEVER routed to a writer. Omit (⇒ actionable) when a concrete edit is needed. If a change is needed but not by you (out of scope), keep true and say so in minimal_fix_direction (it surfaces as residual).' },
57
58
  },
58
59
  }
59
60
  const FINDINGS_SCHEMA = {
@@ -157,16 +158,19 @@ const codexPrompt =
157
158
  `Return codexAvailable:false ONLY if $REVIEW_FILE ends up containing "CODEX_NOT_FOUND" or stays empty after the FULL 10-minute window — NEVER because a single Bash call returned slowly. The script's existence is already verified, so a premature false here is a bug, not a capability gap.\n\n` +
158
159
  `${scopeBrief}\n\n${baselineBrief}\n\n` +
159
160
  `For each finding return: finding_id, title, severity (BLOCKER|HIGH|MEDIUM|LOW), confidence (0-100), evidence (exact file:line + code quote), minimal_fix_direction, and domain (doc|security|migration|code|perf|test). ` +
160
- `Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated). Set codexAvailable:true when the review ran.`
161
+ `Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated). ` +
162
+ `Then the actionability check: an observation you have VERIFIED needs NO change anywhere (the fix direction would be "no fix required" / "acceptable as-is" / "verified non-issue") is not work — set requires_action:false on it (it is recorded but never sent to a fixer), or simply do not emit it. Emit requires_action true/omitted ONLY when a concrete edit is needed. Set codexAvailable:true when the review ran.`
161
163
 
162
164
  const docPrompt =
163
165
  `Cross-card documentation + SSOT-registry review over the batch diff, per ${protocolRef} Step F.3 (doc-reviewer row). Check doc consistency, ssot-registry completeness, and invariants across the changed files.\n\n${scopeBrief}\n\n${baselineBrief}\n\n` +
164
- `You OWN the doc domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as already validated and are NOT re-judged by another agent (a generic code-reviewer judging prose would be cross-domain). Flag only a finding you genuinely cannot resolve as confidence < 80.\n\n` +
166
+ `You OWN the doc domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as already validated and are NOT re-judged by another agent (a generic code-reviewer judging prose would be cross-domain). ` +
167
+ `Then the actionability check: an observation you VERIFIED needs NO change (the fix direction would be "no fix required" / "already accurate") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it. Flag only a finding you genuinely cannot resolve as confidence < 80.\n\n` +
165
168
  `Return findings (domain almost always "doc"). Use the finding schema fields.`
166
169
 
167
170
  const apiPrompt =
168
171
  `API / data-model / performance / cost defect review over the batch diff, per ${protocolRef} Step F.3 (api-perf-cost-auditor row). Look for unbounded reads, N+1, missing pagination, contract drift, and cost regressions.\n\n${scopeBrief}\n\n${baselineBrief}\n\n` +
169
- `You OWN the api/perf/data domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as already validated and are NOT re-judged by another agent. Flag only a finding you genuinely cannot resolve as confidence < 80.\n\n` +
172
+ `You OWN the api/perf/data domain end-to-end: run the mandatory false-positive check on every finding yourself and SUPPRESS the unconvincing ones — your surviving findings are treated as already validated and are NOT re-judged by another agent. ` +
173
+ `Then the actionability check: an observation you VERIFIED needs NO change (the fix direction would be "no fix required" / "acceptable as-is" / "optional") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it. Flag only a finding you genuinely cannot resolve as confidence < 80.\n\n` +
170
174
  `Return findings with domain in {code, perf, migration, security}. Use the finding schema fields.`
171
175
 
172
176
  const qaPrompt =
@@ -238,7 +242,7 @@ for (const item of reviewResults) {
238
242
  if (!codexRan) {
239
243
  codexEngine = 'code-reviewer (fallback)'
240
244
  const fb = await agent(
241
- `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied.`,
245
+ `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied. An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.`,
242
246
  { label: 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
243
247
  )
244
248
  // the fallback code-reviewer IS the primary code-review engine here and applied its own FP
@@ -247,6 +251,12 @@ if (!codexRan) {
247
251
  if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: true })))
248
252
  }
249
253
 
254
+ // Globally-unique finding_id at the fan-in (parity with new-card-review.js). Finders number F###
255
+ // INDEPENDENTLY → a Codex F003 and a doc F003 collide; downstream telemetry/routing (new2 ledger,
256
+ // the skill's F.5 partition) keys on finding_id, so a collision conflates them. Prefix source +
257
+ // running index → uniqueness without losing the human-readable id.
258
+ raw.forEach((f, i) => { f.finding_id = `${f.source || 'src'}#${i}:${f.finding_id || ('F' + i)}` })
259
+
250
260
  // ───────────────────────────────────────────────────────────────────────────
251
261
  // Phase Verify (F.4) — specialist-owned validation (NOT a generic code-reviewer re-judge).
252
262
  // Each domain specialist owns its lane end-to-end: it FP-checks its OWN findings in the
@@ -292,20 +302,31 @@ async function verifyFinding(f) {
292
302
  return { ...f, classification: (v && v.classification) || 'NEEDS_MANUAL_CONFIRMATION' }
293
303
  }
294
304
 
295
- // ---- Result: drop FALSE_POSITIVE, keep VERIFIED + NEEDS_MANUAL (F.4 step 9) --
296
- const findings = classified.filter((f) => f.classification !== 'FALSE_POSITIVE')
305
+ // ---- Result: drop FALSE_POSITIVE, segregate no-action, keep actionable VERIFIED + NEEDS_MANUAL --
306
+ // Actionability gate (parity with new-card-review.js): a VERIFIED finding needing NO change must NOT
307
+ // reach the skill's F.5 writer partition. Honoured ONLY for MED/LOW — BLOCKER/HIGH always actionable.
308
+ // No silent drop: no-action findings are returned separately (noActionFindings) + counted, so the
309
+ // skill can log them; only the actionable set flows into `findings` (what F.5 routes to writers).
310
+ const NO_ACTION_RE = /(^|[.\s(])(no\s+(fix|change|action)\s+(is\s+)?(required|needed)|none\s+(needed|required)|not\s+a\s+(defect|bug|correctness\s+bug)|acceptable\s+as[-\s]is|verified\s+(safe|non[-\s]issue)|no\s+action\s+(needed|required))([.\s,;)]|$)/i
311
+ const isNoAction = (f) => f.classification === 'VERIFIED' && (f.severity !== 'BLOCKER' && f.severity !== 'HIGH') &&
312
+ (f.requires_action === false || NO_ACTION_RE.test(String(f.minimal_fix_direction || '')))
313
+ const noActionFindings = classified.filter(isNoAction)
314
+ const noActionSet = new Set(noActionFindings)
315
+ const findings = classified.filter((f) => f.classification !== 'FALSE_POSITIVE' && !noActionSet.has(f))
316
+ for (const f of noActionFindings) log(`Final review: no-action finding recorded (not routed to a writer): [${f.finding_id}] ${f.severity} ${f.domain} — ${f.title} (${f.requires_action === false ? 'requires_action:false' : 'no-action-inferred'})`)
297
317
  const summary = {
298
318
  total: raw.length,
299
319
  verified: classified.filter((f) => f.classification === 'VERIFIED').length,
320
+ noAction: noActionFindings.length,
300
321
  falsePositive: classified.filter((f) => f.classification === 'FALSE_POSITIVE').length,
301
322
  needsManual: classified.filter((f) => f.classification === 'NEEDS_MANUAL_CONFIRMATION').length,
302
323
  blockers: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
303
324
  highs: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
304
325
  failingGates: gateTable.filter((g) => g.status === 'FAIL').map((g) => g.gate),
305
326
  }
306
- log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s). Engine: ${codexEngine}.`)
327
+ log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s). Engine: ${codexEngine}.`)
307
328
 
308
- return { codexEngine, findings, gateTable, summary }
329
+ return { codexEngine, findings, noActionFindings, gateTable, summary }
309
330
 
310
331
  function emptySummary() {
311
332
  return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.53.0",
3
+ "version": "4.53.2",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"