baldart 4.57.0 → 4.59.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.
@@ -6,7 +6,9 @@
6
6
 
7
7
  **Why this exists**: FEAT-0006 incident left the main repo with an unpushed orphan commit (`c9d41f9`) and `develop` diverged from `origin/develop` because `/new` started work on a dirty/diverged repo without surfacing the situation. This phase reconciles the main repo state BEFORE any worktree is created, and snapshots any in-progress user work so Phase 6c can restore it after the batch.
8
8
 
9
- **Auto Mode does NOT override this phase.** Every `AskUserQuestion` below is non-bypassable — the model has no authority to pick an option on the user's behalf.
9
+ **Auto Mode does NOT override this phase.** Every `AskUserQuestion` below is non-bypassable in the interactive path — the model has no authority to pick an option on the user's behalf.
10
+
11
+ > **When AUTONOMOUS** (per § AUTONOMOUS RESOLUTION RULE + § AUTONOMOUS SAFETY BOUNDARY, SKILL.md), the orchestrator still runs every Phase 0 gate — it does NOT skip workspace hygiene — but resolves each by the per-action disposition instead of `AskUserQuestion`: reversible/local choices (stash with a labeled message, ff-pull, push genuine unpushed commits) are AUTO-RUN; anything needing human judgment (a genuine divergence with no safe default, a missing required card field) materializes a follow-up card / HALTs rather than guessing; and the **Migration Gate's remote schema deploy is treated per § AUTONOMOUS SAFETY BOUNDARY** — see step 1b. Each auto-resolution is logged to `## Issues & Flags` as `AUTO: <gate> → <chosen> (<evidence>)`.
10
12
 
11
13
  **Steps:**
12
14
 
@@ -24,6 +26,7 @@
24
26
  3. **(declared) Verify the artifacts exist on disk.** For each path in `migration_plan.artifacts`, check it exists under `$MAIN`. If **any is missing** → do **NOT** apply or prompt; write `## Migration\ndeclared but artifact(s) missing: <paths> — degraded to deferred (author the migration first)` to the tracker and **proceed to step 2** (the migration falls back to the current end-of-batch owner-gated deferral — no regression). *(Auto-generating a missing artifact is out of scope; the migration must already be authored to be applied up-front.)*
25
27
  4. **(declared + artifacts present) Assemble the apply modalities**, in this source order, de-duplicating by `id`: (a) `migration_plan.apply_modalities` from the epic block; (b) a `## Migration modalities` section in `.baldart/overlays/new.md` if present; (c) any project-memory note on how this project applies migrations; (d) the built-in tail `["Già applicata — prosegui", "Abort"]`. Each modality is `{ id, label, command? }`.
26
28
  5. **`AskUserQuestion`** — `"La epic dichiara una migrazione DB (<summary>). Va applicata PRIMA del batch così le card downstream verificano contro lo schema reale. Come procedo?"` with up to 4 of the assembled modalities (always include "Già applicata — prosegui" and "Abort" as the last options). This is a legitimate Phase 0 question (Auto Mode does not override it).
29
+ **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE + § AUTONOMOUS SAFETY BOUNDARY (SKILL.md): a *command* modality is a remote schema deploy (PRE-AUTH action). category=outward; recommended=DEFER — do NOT execute the command modality. Degrade the front-load to the end-of-batch owner-gated deferral exactly as the missing-artifact / off-trunk-guard branches do: write `## Migration\ndeclared but AUTONOMOUS without AUTO_SHIP authorization — front-load deferred (apply after merge / from trunk)` and proceed to step 2. A "Già applicata — prosegui" disposition (no deploy) stays available — pick it only when a deterministic signal (the optional `verify` probe green) proves the schema is already live; otherwise DEFER. EXECUTE the command modality ONLY when `AUTO_SHIP` is set AND `git.auto_deploy` lists this deploy target/intent AND the `schema_deploy_from_trunk_only` guard (step 6) still passes — `git.auto_deploy` layers ON TOP of the branch invariant, never replaces it.**
27
30
  6. **Execute the choice in `$MAIN`** (project env):
28
31
  - **Schema-deploy-from-trunk-only guard** (when `stack.schema_deploy_from_trunk_only: true`): a **command** modality is a remote schema deploy, so it may run only from trunk. Before executing one, check `git -C "$MAIN" rev-parse --abbrev-ref HEAD`; if it is **not** `git.trunk_branch`, do **NOT** apply or prompt for a command modality — write `## Migration\ndeclared but $MAIN is on '<branch>' (not trunk '<trunk>') — front-load deferred per schema_deploy_from_trunk_only (apply from trunk, then re-run)` to the tracker and **proceed to step 2** (the migration falls back to the end-of-batch owner-gated deferral — no regression). "Già applicata — prosegui" stays available (it executes no deploy). When the key is `false`/unset this guard is a no-op.
29
32
  - a **command** modality → run it with output to disk (`<cmd> > /tmp/migration-<FIRST-CARD-ID>.log 2>&1`), surface only the exit code; on exit 0 run the optional `migration_plan.verify` probe and require it green too. On **failure** → surface a bounded extract (`tail -n 30`) and re-ask (re-offer the modalities + Abort); never silently proceed against a non-live schema. **Never run a command without the user having selected it.**
@@ -59,6 +62,7 @@
59
62
  **Why**: Phase 8 copies the batch tracker into `$METRICS/archive/` and appends to `$METRICS/skill-runs.jsonl` *after* the merge, and never commits them. And `baldart update`/`add` regenerate `.baldart/generated/*` + bump `.baldart/state.json` on every framework sync — left uncommitted until the next reconcile. If either class tripped this gate, **every batch would block on the previous update's own output** — a self-inflicted loop. They are not user work and the user is never asked about them.
60
63
  - If `$PORCELAIN` is non-empty but `$USER_DIRTY` is **empty** (only telemetry under `$METRICS` and/or BALDART-managed `.baldart/` artifacts are dirty) → do **NOT** gate. Write `Dirty: framework-managed-only (auto-ignored)` to the tracker and proceed to step 4. No `AskUserQuestion`.
61
64
  - If `$USER_DIRTY` is **non-empty** (genuine uncommitted user work) → invoke `AskUserQuestion`:
65
+ - **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=reversible; recommended=option 1 "Stash automatico (restore in Phase 6c)" — AUTO-RUN (reversible, the labeled stash is restored post-batch); log `AUTO: dirty-tree → stashed (<label>)`. NEVER option 3 "Procedi senza stash" autonomously (it accepts responsibility a human must own).**
62
66
  - Question: `"Main repo ($MAIN) ha modifiche non committate prima di partire con la batch. Come procedo?"`
63
67
  - Options (max 4):
64
68
  1. **"Stash automatico (restore in Phase 6c)"** — `$MAIN` is the main checkout (NOT a worktree), so a stash here is safe. Run `git -C "$MAIN" stash push -u -m "baldart-new-phase0-<FIRST-CARD-ID>-<timestamp>"` and write the **stash MESSAGE label** (`baldart-new-phase0-<FIRST-CARD-ID>-<timestamp>`) to the tracker under `## Workspace Snapshot: <message-label> (created <timestamp>)`. Phase 6c resolves the stash by this message — NEVER by a positional `stash@{N}`, because other stashes pushed during the batch (on the main checkout) would shift the index. Do NOT record `stash@{0}`.
@@ -95,6 +99,8 @@
95
99
  ```
96
100
  `$FW_SKIPPED` framework-management commits are auto-ignored (log them — see below). Branch on `$BEHIND` and the **genuine** ahead count `$GENUINE_AHEAD` (NOT the raw `$AHEAD`):
97
101
 
102
+ > **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE + § AUTONOMOUS SAFETY BOUNDARY (SKILL.md) at each branch below:** behind-only → ff-pull (AUTO-RUN, only when `$MAIN` HEAD == `$TRUNK`); ahead-only → push the genuine commits to `origin/$TRUNK` (AUTO-RUN, FF only) — `Cherry-pick` / `Reset --hard` are NOT auto-selected (reset-hard is HARD-NEVER); diverged-both-ways → no safe default → materialize a follow-up card and proceed without reconciling (NEVER auto-rebase/reset). Log `AUTO: divergence → <chosen> (<evidence>)`. `Abort` is never auto-selected (an unattended run does not silently abort a hygiene gate it can defer).
103
+
98
104
  - **`BEHIND=0` and `GENUINE_AHEAD=0`** — synchronized for `/new`'s purposes. If `$FW_SKIPPED > 0`, write `Divergence: $FW_SKIPPED framework-management commits ahead (auto-ignored — baldart update/subtree)` to the tracker. **No `AskUserQuestion`.** Proceed to step 5.
99
105
  - **`BEHIND>0` and `GENUINE_AHEAD=0`** (only behind) — invoke `AskUserQuestion`:
100
106
  - Question: `"Local $TRUNK è behind origin/$TRUNK di $BEHIND commit. Come procedo?"`
@@ -132,7 +138,7 @@
132
138
  **1b-i — HALT on non-derivable fields (ask the user).** Verify the fields that cannot be safely synthesized are present and non-empty:
133
139
  - `requirements` (>=1), `acceptance_criteria` (>=1), `files_likely_touched` (>=1 file), and `scope`.
134
140
  - `scope_boundaries` is **NOT** in this set (it is conditional — legitimately omitted for standalone cards with no siblings; see card-schema.md).
135
- If any card fails: log the specific missing fields in `## Issues & Flags`, ask the user to fill them in before proceeding with that card, and continue pre-flight for any remaining valid cards.
141
+ If any card fails: log the specific missing fields in `## Issues & Flags`, ask the user to fill them in before proceeding with that card, and continue pre-flight for any remaining valid cards. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (non-derivable fields cannot be fabricated — SC-4 "missing fields → SKIP+report, never fabricate") → skip that card from the batch and log `AUTO: card-baseline → skipped <CARD-ID> (missing <fields>)`; continue with the valid cards. NEVER invent requirements/AC/scope.**
136
142
 
137
143
  **1b-ii — Back-fill deterministically-computable fields (compute → persist → log).** For each non-epic card missing a derivable field, compute it and **write it back to the card on disk in `$MAIN/${paths.backlog_dir}`** (the main repo path resolved at Phase 0 step 1 — NOT the worktree copy; same F-040 discipline `new2` uses for follow-ups):
138
144
  - `review_profile` absent → compute via `prd-card-writer.md § Rule C` (the SSOT), write it.
@@ -142,10 +148,10 @@
142
148
  - **Commit discipline:** after all cards are processed, if ANY back-fill was written, commit them together in `$MAIN` with the COMMIT_LOCK + doc-freshness precautions of `merge-cleanup.md` Phase 6b (clear stale `COMMIT_LOCK`; `git add` only the back-filled card YAMLs; if the doc-freshness hook blocks, stage `ssot-registry.md`; ≤2 retries): `git commit -m "chore(backlog): normalise card baseline [BACKFILL]"`. Do NOT leave `$MAIN` dirty for the batch duration — an uncommitted backlog edit would surface as noise in the Phase 3/4 git-diff gates.
143
149
  - `canonical_docs` / `links.prd` absent on a standalone/non-PRD card → **WARN** only (log, do not block, do not back-fill).
144
150
 
145
- **1b-iii — Conformance validation.** Run the framework validator `node .framework/framework/scripts/validate-card-baseline.js <card-yaml-path>` (or the path resolved for your install) against each card. It is profile-aware (epic/child/standalone) and catches what 1b-i/1b-ii could not — invalid enum values (`review_profile` ∉ `{skip,light,balanced,deep}`, `owner_agent` ∉ Rule A enum), or a REQUIRED field still missing. Exit 1 → display the per-field errors in `## Issues & Flags` and HALT that card (continue others). If the validator is not reachable in this install, skip with a one-line note (the 1b-i/1b-ii gates still applied).
151
+ **1b-iii — Conformance validation.** Run the framework validator `node .framework/framework/scripts/validate-card-baseline.js <card-yaml-path>` (or the path resolved for your install) against each card. It is profile-aware (epic/child/standalone) and catches what 1b-i/1b-ii could not — invalid enum values (`review_profile` ∉ `{skip,light,balanced,deep}`, `owner_agent` ∉ Rule A enum), or a REQUIRED field still missing. Exit 1 → display the per-field errors in `## Issues & Flags` and HALT that card (continue others). **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a conformance error is a data defect the orchestrator must not fabricate around) → skip that card from the batch and log `AUTO: card-conformance → skipped <CARD-ID>`; continue with the valid cards.** If the validator is not reachable in this install, skip with a one-line note (the 1b-i/1b-ii gates still applied).
146
152
 
147
153
  1c. **Field Registry Validation (pre-flight gate)** — for each card with a `data_fields` block, run the project's field-validation tool if available (path listed in `.baldart/overlays/new.md`; typically `python3 tools/validate-card-fields.py <card-yaml-path>`).
148
- - If exit 1: display the field errors in `## Issues & Flags` and HALT — ask the user to fix the card before proceeding. Do not start implementation until the card passes validation.
154
+ - If exit 1: display the field errors in `## Issues & Flags` and HALT — ask the user to fix the card before proceeding. Do not start implementation until the card passes validation. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a field-registry error is a data defect the orchestrator must not fabricate around) → skip that card from the batch and log `AUTO: field-validation → skipped <CARD-ID>`; continue with the valid cards.**
149
155
  - If the card has DB-index signals (`db_indexes` — or legacy `firestore_indexes` — or `data.collections`/`data.tables`) but NO `data_fields` block: log WARNING in `## Issues & Flags` — "Card `<ID>` touches the persistence layer but has no `data_fields` block. Field/column names are unvalidated."
150
156
  - Cards with no `data_fields` and no storage signals: skip silently.
151
157
  - If the project does not ship a field-validation tool: skip this step.
@@ -259,7 +265,7 @@
259
265
  ```
260
266
  Parse PASS / `CODEX_NOT_FOUND` / conflict-findings from that filtered output only. When logging to the tracker, write the **distilled findings** (one line per conflict: cards + type + one-line fix) — never paste the raw block, and never the trace.
261
267
  - Read the exact `$AUDIT_FILE` path recorded in the tracker after the background command completes (always via the trace-stripping filter above).
262
- - If the file contains **`CODEX_NOT_FOUND`** (Codex unavailable): do NOT silently proceed — **fallback**: spawn the `code-reviewer` agent over the full batch's card YAMLs + file-ownership map with the same cross-card conflict questions (FILE_CONFLICT / IMPLICIT_DEP / ORDER_RISK / STATE_MUTATION). If `code-reviewer` is also unavailable, surface `CAPABILITY_UNAVAILABLE: cross-card-review` to the user and ask whether to proceed without cross-card detection or halt. Never run a multi-card batch with cross-card review silently skipped.
268
+ - If the file contains **`CODEX_NOT_FOUND`** (Codex unavailable): do NOT silently proceed — **fallback**: spawn the `code-reviewer` agent over the full batch's card YAMLs + file-ownership map with the same cross-card conflict questions (FILE_CONFLICT / IMPLICIT_DEP / ORDER_RISK / STATE_MUTATION). If `code-reviewer` is also unavailable, surface `CAPABILITY_UNAVAILABLE: cross-card-review` to the user and ask whether to proceed without cross-card detection or halt. Never run a multi-card batch with cross-card review silently skipped. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=use the `code-reviewer` fallback (the deterministic safe default); only if BOTH Codex and `code-reviewer` are unavailable → proceed without cross-card detection but log `AUTO: cross-card-review → unavailable, proceeded (CAPABILITY_UNAVAILABLE)` to `## Issues & Flags` (an unattended run cannot halt indefinitely on a missing capability; the per-card + Final gates remain).**
263
269
  - If **PASS** or file empty: proceed normally.
264
270
  - If **conflicts found**: log in tracker under `## Cross-Card Conflicts (Codex)` and present to user. For each conflict:
265
271
  - `FILE_CONFLICT` / `ORDER_RISK` → force the conflicting cards sequential (update file-ownership map).
@@ -6,6 +6,8 @@
6
6
 
7
7
  When the complexity assessment (step 3c) selects team mode, the orchestrator changes role: instead of executing each card's pipeline sequentially, it coordinates parallel coder agents — each with its OWN isolated context window.
8
8
 
9
+ > **When AUTONOMOUS, team-mode is FORCED to sequential single-worktree** (per § AUTONOMOUS RESOLUTION RULE, SKILL.md — cost/predictability: a wide parallel fan-out saturates the shared API rate-limit pool and kills coders mid-work, paying twice; an unattended run cannot afford that non-determinism). So under `-auto`/`--auto`/`BALDART_AUTONOMOUS`/`CI`, step 3c's team-mode decision is overridden to `sequential` regardless of card count / group width, and the per-card pipeline (the rest of `/new`) runs instead of this section. Log `AUTO: execution-mode → sequential (AUTONOMOUS forces single-worktree)` in `## Execution Mode`. This section applies ONLY to the non-AUTONOMOUS interactive path.
10
+
9
11
  **Key principle**: the orchestrator stays LEAN. It holds only:
10
12
  - The tracker file path
11
13
  - Parallel group status (pending/active/done)
@@ -154,12 +156,12 @@ For each completed agent:
154
156
  `D="$(ls -dt ~/.claude/projects/*/${CLAUDE_SESSION_ID:-*}/subagents 2>/dev/null | head -1)"; f="$(grep -l '"<teammate-label>"' "$D"/*.meta.json 2>/dev/null | head -1 | sed 's/\.meta\.json$/.jsonl/')"; tail -c 4000 "$f"`
155
157
  (if `$CLAUDE_SESSION_ID` is unset, the most recent `subagents/` dir under this project is the live session's.) If the transcript truly cannot be located, fall back to whatever the `Agent` tool result surfaced — but do NOT default to "genuine empty-result" without having looked. **State the classification you reached + the evidence in your narration** (e.g. "03 last event = `Rate limited` → transient").
156
158
  - **Transient infra failure (the common cause of a silent empty rest in a WIDE parallel wave).** The agent's last event is an API error — signatures: `isApiErrorMessage:true`, `Rate limited`, `Server is temporarily limiting requests`, `overload`, `(not your usage limit)` — i.e. the background teammate was **killed mid-flight by API rate-limiting** (it had done real work — reads, a plan — then died), NOT a model fabrication or "decided nothing to do". The runtime does NOT auto-resume a dead teammate, so it rests with no report + no diff. **Re-spawn it, but STAGGERED with backoff — do NOT re-fire several transient-failed agents in the same parallel burst** (that re-saturates the API and kills them again). Space them out (one at a time, or after a brief pause). This still costs a fresh run (the killed agent's reads are lost — the runtime cannot resume it), so **prevention beats recovery**: if a wave repeatedly hits rate limits, NARROW the parallel fan-out width for that wave (spawn fewer coders at once) rather than re-firing the full wave. A transient failure does NOT consume the Step-B genuine-failure budget.
157
- - **Genuine empty-result / fabrication.** The agent rested CLEANLY (no error in its last event), with no completion report and no diff. THIS is the model-fault case: take the one Step-B re-spawn below (same cap), re-briefing the coder with an explicit "write your ownership files and confirm them on disk before reporting done" mandate. A second empty result → `AskUserQuestion` (skip/abandon) — do not re-spawn a third time.
159
+ - **Genuine empty-result / fabrication.** The agent rested CLEANLY (no error in its last event), with no completion report and no diff. THIS is the model-fault case: take the one Step-B re-spawn below (same cap), re-briefing the coder with an explicit "write your ownership files and confirm them on disk before reporting done" mandate. A second empty result → `AskUserQuestion` (skip/abandon) — do not re-spawn a third time. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a card whose coder produced nothing twice has no implementation) → skip the card AND materialize a follow-up card carrying its spec (NEVER mark it DONE / no-op).** (Classify the cause first — a transient rate-limit death is re-spawned staggered, not counted against this budget.)
158
160
 
159
161
  **If an agent fails** (status: failed after 3 retries — the central repair cap):
160
162
  - Log failure in tracker `## Issues & Flags`.
161
163
  - Other agents in the group continue unaffected.
162
- - After group completes, ask user: retry failed card or skip?
164
+ - After group completes, ask user: retry failed card or skip? **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=take the one Step-B re-entry below (the central re-try cap); if it fails again → skip the card AND materialize a follow-up card (NEVER drop it silently).**
163
165
  - **Retry re-entry (defined path):** on "retry", re-run the failed card from **Step B** (re-spawn the autonomous coder with a fresh briefing) — NOT from Step A, because the Step A arch context for the group is already in hand and is reused. The retried card then rejoins the group's Step D pipeline and MUST pass the SAME mandatory per-card sub-steps as its peers (D.3a AC-Closure → D.3b Simplify → D.3c E2E → D.4 QA → D.4a doc → D.4b codex → D.5 commit → D.6 backlog); there is no shortcut. The Step D coverage assertion will block Step E until the retried card has all mandatory tracker entries — so a retried card cannot silently skip a gate. **Cap: one Step-B re-entry per card** (consistent with the central cap discipline); on a second failure, re-ask with only skip/abandon so the loop cannot recur unbounded.
164
166
 
165
167
  #### Step D: Post-group review + QA (ALL sub-steps MANDATORY)
@@ -228,7 +230,7 @@ After ALL agents in the group complete successfully:
228
230
  residual → targeted `security-reviewer`; else unconverged code/perf residual → targeted `coder`).
229
231
  Apply the **same BLOCKING QA-gate consumption**:
230
232
  `gateTable` with any `status:"FAIL"` OR `summary.checksFailed` → coder fix (≤2 retries) then
231
- `AskUserQuestion`; **D.5 commit MUST NOT happen until `gateTable` is PASS/SKIP and `checksFailed` is
233
+ `AskUserQuestion` (**when AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE, SKILL.md: category=blocker; recommended=none safe → follow-up card, do NOT commit the wave's affected card; NEVER force-proceed past a FAIL**); **D.5 commit MUST NOT happen until `gateTable` is PASS/SKIP and `checksFailed` is
232
234
  false** (a delegated QA FAIL blocks exactly as inline D.4 / Phase 3.5 would — `gateTable` is
233
235
  wave-scoped). Append the workflow `fixesApplied`/`residual` to `## Fix Application Log`
234
236
  (`decision=workflow`, with `card=<ID>`).
@@ -263,14 +265,14 @@ After ALL agents in the group complete successfully:
263
265
 
264
266
  3. **D.3 — Apply fixes (group)** — If D.2 findings exist, spawn ONE fix-coder to apply all fixes in a single pass. Run build + lint after.
265
267
 
266
- 3a. **D.3a — Phase 2.5b AC-Closure Gate (per-card, BLOCKING — non-skippable)** — For EACH card in the group, **sequentially**, invoke the full Phase 2.5b gate as documented in `### Phase 2.5b — AC-Closure Gate (BLOCKING — Scope Closure Discipline)`. This includes: build the AC Closure Ledger from the card YAML, run the rationalization scan, invoke `AskUserQuestion` one-per-deferred-AC, run the `implementation_notes` deferral audit, and persist the ledger in the tracker. Until EVERY card in the group exits PASS, do NOT proceed to D.3b. Cards exiting with `not_implemented` ACs that the user routes to "Implementa adesso" must finish their fix-coder loop and re-pass the gate before D.3b starts for the next card. Log under `## AC Closure Ledger — <CARD-ID>` per card.
268
+ 3a. **D.3a — Phase 2.5b AC-Closure Gate (per-card, BLOCKING — non-skippable)** — For EACH card in the group, **sequentially**, invoke the full Phase 2.5b gate as documented in `### Phase 2.5b — AC-Closure Gate (BLOCKING — Scope Closure Discipline)`. (Its Step-4 user gate carries the AUTONOMOUS pointer in `references/completeness.md` — under AUTONOMOUS a deferred AC becomes a follow-up card, never an auto-approved deferral.) This includes: build the AC Closure Ledger from the card YAML, run the rationalization scan, invoke `AskUserQuestion` one-per-deferred-AC, run the `implementation_notes` deferral audit, and persist the ledger in the tracker. Until EVERY card in the group exits PASS, do NOT proceed to D.3b. Cards exiting with `not_implemented` ACs that the user routes to "Implementa adesso" must finish their fix-coder loop and re-pass the gate before D.3b starts for the next card. Log under `## AC Closure Ledger — <CARD-ID>` per card.
267
269
 
268
270
  3b. **D.3b — Phase 2.55 Simplify (per-card, FANNED OUT across the group)** — The Simplify agents are **read-only analysis on file-disjoint per-card diffs** (the orchestrator applies the fixes afterward), so there is NO reason to run them one card at a time. **Spawn the per-card Simplify analysis for ALL eligible cards in PARALLEL** — in a SINGLE message, fire each card's Phase 2.55 trio (Reuse / Quality / Efficiency) against that card's diff captured to `/tmp/diff-<CARD-ID>.txt` (per Phase 2.55 step 2 — pass each trio the **path**, scoped to the card's File Ownership Map; never inline the diff). Per-card (not group-aggregate) so findings stay attributable. When all analyses return, **apply fixes per card** (file-disjoint → no write conflict), then re-run `npm run lint` and `npx tsc --noEmit` (when `has_toolchain`, the configured `toolchain.commands.{lint,typecheck}` verbatim — § "Toolchain gates") on the worktree ONCE for the whole group (redirect to disk per § "Context economy"). (Concurrency is capped by the platform; passing N cards is safe — excess agents queue.)
269
271
  - **Gate (enumerated, `TRIVIAL_CARDS`-driven)**: SKIP D.3b for a card in **`TRIVIAL_CARDS`** (the set already computed at D.1.5 — `review_profile == skip` AND 0 Step-A triggers AND **non-source diff**), aligning team mode with sequential Phase 2.55's `IS_TRIVIAL` re-confirmation on the ACTUAL diff and with team-mode's own D.1.5 SSOT. A trivial card has no substantive diff to simplify, and Simplify is quality-only (no merge-gate coverage to lose). Log `simplify: SKIPPED (trivial — non-source diff)`. **A card with `review_profile == skip` whose committed diff DID touch a source file is NOT in `TRIVIAL_CARDS` → run D.3b for it** (exactly as sequential 2.55 does — `skip` is the floor, the real diff is the deciding check). For `light`/`balanced`/`deep` cards D.3b runs unchanged. This is the ONLY enumerated skip — never skip D.3b "for time" on a non-trivial card. (Skipped cards are simply omitted from the parallel fan-out.)
270
272
 
271
273
  3c. **D.3c — Phase 2.6 E2E-Review (per-card)** — First, evaluate the existing Gate table for EVERY card at once (skip when `features.has_e2e_review: false`, backend-only diff per the diff predicate documented in Phase 2.6, or card type in the Phase 2.6 skip set — `backend`/`api`/`db`/`infra`/`docs`/`chore`/`config`). In practice most cards in a group skip this gate (backend/db/api), so the eligible set is usually 0–1. For the cards that PASS the gate, invoke `/e2e-review` in programmatic mode with that card's payload. Each `/e2e-review` keeps its own isolated state dir (`.baldart/e2e-review/<CARD-ID>/`), so multiple runs do not clobber each other's artifacts.
272
274
  - **Parallel-when-safe**: if **two or more** cards pass the gate, you MAY fan them out in parallel ONLY when they hit **disjoint routes/pages** — the genuinely shared resource is the worktree's single dev-server port, and concurrent Playwright sessions against one server are fine for disjoint routes. If the eligible cards touch overlapping routes (or `/e2e-review` spins its own server and a port clash is possible), run those **sequentially** to avoid a flaky cross-test. Default to parallel for the common disjoint case; fall back to sequential on any contention.
273
- - BLOCKING per-card: if `/e2e-review` returns `"blocked"` or `"error"`, surface to the user via the same `AskUserQuestion` documented in Phase 2.6 — do NOT proceed to D.4 with an unresolved card. Skips are logged with the documented gate reason, never with `"time budget"` or similar.
275
+ - BLOCKING per-card: if `/e2e-review` returns `"blocked"` or `"error"`, surface to the user via the same `AskUserQuestion` documented in Phase 2.6 — do NOT proceed to D.4 with an unresolved card. Skips are logged with the documented gate reason, never with `"time budget"` or similar. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md) exactly as the Phase 2.6 `blocked`/`error` rows do (`references/review-cycle.md`): category=blocker; `blocked` → escalate to a follow-up card (never auto-override); `error` → retry once then skip-E2E + follow-up card.**
274
276
 
275
277
  4. **D.4 — QA gate (group; since v4.7.0 — runs only for `deep` / risk-escalation, else deferred to Final)** — Read each card's `review_profile` field and the D.1.5 Step-A trigger result. **Run the group qa-sentinel (once, at FULL) iff** the group's MAX profile is `deep` **OR** any card in the group had a Step-A risk escalation. **Otherwise (max profile ≤ `balanced`, no escalation) → DEFER the group QA to the Final Review F.3 qa-sentinel** (FULL suite + build + audit over the entire batch — the unconditional merge gate); log `D.4 QA: DEFERRED to Final FULL gate (group max=balanced, no risk escalation)`. This mirrors sequential step 21b. When it DOES run, invoke qa-sentinel at FULL (the combined group diff must be validated together) using the same prompt contract as Phase 3.5 step 22. **Granularity (INTENTIONAL — not a drift)**: the predicate is identical to sequential 21b (`deep`/risk-escalation → FULL, else defer to the Final FULL gate), but it is evaluated **per-GROUP** here whereas sequential evaluates it per-CARD. This is by design — qa-sentinel runs once over the group's combined diff (the wave-batched model gives it no per-card granularity), so the gate is deliberately the more conservative side: a single `deep`/risk card escalates the WHOLE group to FULL rather than just itself. No correctness gap — every card still reaches a FULL suite no later than the unconditional Final gate. ⚠️ A balanced-only group runs its first suite at the Final gate; merge safety is preserved because Final is FULL over the whole batch.
276
278
 
@@ -280,7 +282,7 @@ After ALL agents in the group complete successfully:
280
282
  - **`LIGHT_CARDS \ TRIVIAL_CARDS` → run `/codexreview` at `profile: light`** (since v4.18.0): **Codex is the sole finder** + the Step 3 FP-gate (`code-reviewer` + `codebase-architect` validate); no api-perf/doc/CoVe. This is where these cards' per-card code review now lives (D.2 no longer code-reviews them). On Codex-unavailable, `light` degrades to `code-reviewer` finder. Log `codex-review: <verdict> (light — Codex finder + FP-gate)`.
281
283
  - **`TRIVIAL_CARDS` → SKIP D.4b** (non-source diff — nothing for Codex to review; covered by the doc-reviewer group pass + the Final FULL gate). Log `codex-review: SKIPPED (trivial — non-source diff; covered by Final FULL gate)`.
282
284
  - **`FULL_CARDS` → run `/codexreview` at `profile: full`** (Codex adversarial + `code-reviewer` finder + CoVe + FP-gate). Keep the sequential one-at-a-time loop (avoid N concurrent `/codexreview`, each of which fans out multiple sub-agents → rate-limit risk). ⚠️ Since light cards now also run here, the D.4b loop is longer per group — accepted cost of moving the diff sweep onto Codex.
283
- Apply the same fix sub-loop as sequential Phase 3.7 Step C.4: if the consolidated report shows verified BLOCKER/HIGH findings, spawn a fix-coder and — **re-writing the consumed-once lean contract first** — re-invoke `/codexreview` via the Skill tool with `args: <CARD-ID>` (max 2 retries per card; a bare prose mention or a missing card-ID would let the retry review the wrong card). If still BLOCKER/HIGH after retries, ask the user before proceeding to D.5. The D.5 commits MUST NOT happen until every card in the group has a PASS verdict (or explicit user override via `AskUserQuestion`). Log results in the tracker under `## Pre-Merge Codex Review` per card. **Lean + profile (since v3.35.0)**: before each card's `/codexreview`, apply the same Review Profile Selector and write the same `/tmp/codexreview-lean-<CARD-ID>.json` contract as sequential Phase 3.7 Step C — `arch_baseline_path` pointing at `/tmp/arch-baseline-group-<FIRST-CARD-ID>.md`, `skip_doc_reviewer: true`, `skip_api_perf_auditor: true` (since v4.7.0 — api-perf deferred to the Final F.3 batch-wide auditor), and `profile` resolved from D.1.5 (`light` cards → `light`, `full` cards → `full`; do NOT recompute). The post-batch **Final-review FULL gate** (a single FULL `/codexreview` over the entire batch diff, which team mode reaches via "Post-batch — same as sequential mode") remains the cross-card safety net for every card including the trivial ones.
285
+ Apply the same fix sub-loop as sequential Phase 3.7 Step C.4: if the consolidated report shows verified BLOCKER/HIGH findings, spawn a fix-coder and — **re-writing the consumed-once lean contract first** — re-invoke `/codexreview` via the Skill tool with `args: <CARD-ID>` (max 2 retries per card; a bare prose mention or a missing card-ID would let the retry review the wrong card). If still BLOCKER/HIGH after retries, ask the user before proceeding to D.5. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=route the residual BLOCKER/HIGH to the writer (security-reviewer/coder); if still unresolved → follow-up card and do NOT commit that card (NEVER force a PASS verdict).** The D.5 commits MUST NOT happen until every card in the group has a PASS verdict (or explicit user override via `AskUserQuestion`). Log results in the tracker under `## Pre-Merge Codex Review` per card. **Lean + profile (since v3.35.0)**: before each card's `/codexreview`, apply the same Review Profile Selector and write the same `/tmp/codexreview-lean-<CARD-ID>.json` contract as sequential Phase 3.7 Step C — `arch_baseline_path` pointing at `/tmp/arch-baseline-group-<FIRST-CARD-ID>.md`, `skip_doc_reviewer: true`, `skip_api_perf_auditor: true` (since v4.7.0 — api-perf deferred to the Final F.3 batch-wide auditor), and `profile` resolved from D.1.5 (`light` cards → `light`, `full` cards → `full`; do NOT recompute). The post-batch **Final-review FULL gate** (a single FULL `/codexreview` over the entire batch diff, which team mode reaches via "Post-batch — same as sequential mode") remains the cross-card safety net for every card including the trivial ones.
284
286
 
285
287
  5. **D.5 — Commit** — One commit per card using explicit staging from the ownership map. **NO stash in worktrees** (stashes are globally shared via `refs/stash` — see Phase 4 WORKTREE COMMIT RULE):
286
288
  ```bash
@@ -294,7 +296,7 @@ After ALL agents in the group complete successfully:
294
296
  git commit -m "[CARD-ID] Brief description"
295
297
  fi
296
298
  ```
297
- If `git commit` itself fails with a non-empty staging area (a pre-commit / lint-staged hook rejected it), apply the SAME bounded handler as sequential Phase 4 step 27: clear a stale `COMMIT_LOCK`, re-stage explicitly, retry at most **2 times**, then log `[COMMIT-BLOCKED]` and escalate via `AskUserQuestion` — never loop unbounded and never silently leave a card uncommitted.
299
+ If `git commit` itself fails with a non-empty staging area (a pre-commit / lint-staged hook rejected it), apply the SAME bounded handler as sequential Phase 4 step 27: clear a stale `COMMIT_LOCK`, re-stage explicitly, retry at most **2 times**, then log `[COMMIT-BLOCKED]` and escalate via `AskUserQuestion` — never loop unbounded and never silently leave a card uncommitted. (Under AUTONOMOUS this inherits the same § AUTONOMOUS RESOLUTION RULE disposition as sequential Phase 4 step 27 — see `references/commit.md`: a persistent hook rejection has no safe default → follow-up card, never force-commit / never mark DONE.)
298
300
 
299
301
  6. **D.6 — Update backlog (MANDATORY — do NOT skip)** — For EACH card in the group:
300
302
  a. Edit the backlog YAML (`${paths.backlog_dir}/<CARD-ID>.yml`): set `status: DONE`, add `completed_date: <today>`, add implementation notes (NEVER include `[USER-APPROVED DEFERRAL]` lines that didn't actually pass through D.3a's gate).
@@ -42,7 +42,7 @@ If there are no git changes at all, review the most recently modified files that
42
42
 
43
43
  Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
44
44
 
45
- > **Author-time prevention mirror:** `coder` now applies an author-time subset of the Quality + Efficiency taxonomy below WHILE writing (`framework/.claude/agents/coder.md § Author-Time Simplicity Discipline`), so the diff this pass reviews should already be cleaner on those domains. This pass remains the **independent net** — its load-bearing value is the cross-codebase **Reuse** detection (exact/near duplicate, inline reimplementation) that the author cannot see from inside the task. This Step 2 taxonomy is the SSOT; if you change it, keep the coder mirror in sync.
45
+ > **Author-time prevention mirror:** `coder` now applies an author-time subset of the Quality + Efficiency taxonomy below WHILE writing (`framework/.claude/agents/coder.md § Author-Time Simplicity Discipline`), so the diff this pass reviews should already be cleaner on those domains. This pass remains the **independent net** — its load-bearing value is the cross-codebase **Reuse** detection (exact/near duplicate, inline reimplementation, dependency-reimplements-native) that the author cannot see from inside the task. This Step 2 taxonomy is the SSOT; if you change it, keep the coder mirror in sync.
46
46
 
47
47
  ### Agent 1: Duplication & Reuse Review
48
48
 
@@ -61,12 +61,14 @@ For each new piece of code in the diff:
61
61
  reverse-impact set from the graph (and `graphify-out/GRAPH_REPORT.md` surfaces
62
62
  duplicated god-node clusters) — see `framework/agents/code-graph-protocol.md`.
63
63
  4. **Flag components that wrap the same underlying element** with minor style/prop differences.
64
+ 5. **For any newly added third-party dependency** in the diff (a new entry in the package manifest, or a first import of a package the project did not use before), check whether the language/stdlib/runtime or platform already provides the capability — prefer the native primitive over pulling in a dependency for it (e.g. `Intl`/`Temporal`/`Array`/`fetch`/`structuredClone` vs a util or date library).
64
65
 
65
66
  Classify each finding as:
66
67
 
67
68
  - **Exact duplicate** — same logic exists elsewhere. Use the existing one. Delete the new one.
68
69
  - **Near duplicate** — similar logic with minor differences. Extract a shared version with parameters.
69
70
  - **Inline reimplementation** — hand-rolled logic that an existing utility already handles. Replace with the utility call.
71
+ - **Dependency reimplements native** — a newly added third-party dependency duplicates a capability the language/stdlib/runtime or platform already provides. Prefer the native primitive; flag the dependency as removable. (A genuinely needed new dependency is separately an ADR trigger, not a simplify removal.)
70
72
 
71
73
  ### Agent 2: Code Quality Review
72
74
 
@@ -118,8 +120,9 @@ Collect results from all three agents. For each finding, categorize as:
118
120
  1. **Exact duplicate** — same logic exists elsewhere. Use the existing one. Delete the new one.
119
121
  2. **Near duplicate** — similar logic with minor differences. Extract a shared version with parameters.
120
122
  3. **Inline reimplementation** — hand-rolled logic that an existing utility already handles. Replace with the utility call.
121
- 4. **Quality issue** — hacky pattern from Agent 2. Fix directly.
122
- 5. **Efficiency issue** — performance problem from Agent 3. Fix directly.
123
+ 4. **Dependency reimplements native** — a new third-party dependency duplicates a stdlib/platform capability. Replace with the native primitive; remove the dependency.
124
+ 5. **Quality issue** — hacky pattern from Agent 2. Fix directly.
125
+ 6. **Efficiency issue** — performance problem from Agent 3. Fix directly.
123
126
 
124
127
  If no issues are found, say so and stop. Do not invent problems.
125
128
 
@@ -130,6 +133,7 @@ For each finding, apply the fix directly:
130
133
  - **Exact duplicates**: Delete the new code, import the existing version, update all references.
131
134
  - **Near duplicates**: Extract a shared function/component/hook to the project's shared directory resolved from `baldart.config.yml` (see "When extracting shared code" below). Update all call sites to use the shared version.
132
135
  - **Inline reimplementations**: Replace inline code with calls to the existing utility. Add imports.
136
+ - **Dependency reimplements native**: Replace the dependency's usages with the native primitive, remove the package from the manifest, and update imports. If the removal is non-trivial or the dependency is shared by other code, flag it instead of forcing the removal in this pass.
133
137
  - **Quality issues**: Apply the fix inline (remove redundant state, simplify JSX nesting, replace string literals with constants, etc.).
134
138
  - **Efficiency issues**: Apply the fix inline (parallelize independent calls, add change-detection guards, batch reads, etc.).
135
139
 
@@ -138,6 +138,19 @@ const parseFindingsArray = (s) => {
138
138
  if (typeof s !== 'string' || !s.trim()) return null
139
139
  try { const v = JSON.parse(s.trim()); return Array.isArray(v) ? v : null } catch { return null }
140
140
  }
141
+ // Collision-safe: map a writer-returned token to the canonical prefixed finding_id.
142
+ // Accept exact equality OR a UNIQUE colon-delimited tail match (id.endsWith(':'+token)).
143
+ // A bare suffix (e.g. '001' vs 'CR-001') must NOT match → only ':'+token. 0 or >1 → return
144
+ // the token unchanged (caller logs the mismatch; never silently mis-attribute across cards).
145
+ function makeResolveId(canonIds) {
146
+ return (returned) => {
147
+ const t = String(returned == null ? '' : returned).trim()
148
+ if (!t) return returned
149
+ if (canonIds.includes(t)) return t
150
+ const hits = canonIds.filter((id) => id === t || id.endsWith(':' + t))
151
+ return hits.length === 1 ? hits[0] : returned
152
+ }
153
+ }
141
154
  // SSOT for the sentinel-extraction awk — used by BOTH the relay prompt and the re-extract agent.
142
155
  const AWK_EXTRACT = `awk '/<<<FINDINGS_JSON>>>/{c=1;b="";next}/<<<END_FINDINGS_JSON>>>/{c=0;last=b;next}c{b=b $0 ORS}END{printf "%s",last}'`
143
156
  const reExtractPrompt = (file) =>
@@ -304,7 +317,7 @@ const qaPrompt =
304
317
 
305
318
  function simplifyPrompt(c) {
306
319
  return `Simplify analysis (read-only — you do NOT edit, the workflow applies fixes afterward) over ONE card's committed diff, per ${protocolRef} (Phase 2.55). Cover all THREE lenses and return findings:\n` +
307
- ` • Reuse — newly written code that duplicates an existing util/helper; inline logic that could use existing code.\n` +
320
+ ` • Reuse — newly written code that duplicates an existing util/helper; inline logic that could use existing code; a NEW third-party dependency that reimplements a capability the language/stdlib/runtime/platform already provides (prefer the native primitive).\n` +
308
321
  ` • 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` +
309
322
  ` • Efficiency — redundant computation, duplicate API calls, N+1, missed concurrency, hot-path bloat, missing change-detection guards, unbounded structures.\n\n` +
310
323
  `${cardScopeBrief(c)}\n\n${baselineBrief}\n\n` +
@@ -385,22 +398,37 @@ for (const item of findResults) {
385
398
  }
386
399
 
387
400
  // ONE deterministic fallback: pre-flight found no companion, or it ran but did not complete.
388
- // PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted prose (no sentinel JSON), seed the
389
- // fallback with that prose as UNVERIFIED leads. The relay never parses prose into findings; the
390
- // fallback code-reviewer (a real reviewer WITH code access) re-verifies each lead against the actual
391
- // diff and emits the schema itself so Codex's cross-model finds are recovered, not discarded, and
392
- // the trust model is unchanged (output is validated by a reviewer over real code). Net vs the old
393
- // cold fallback: same full pass, but seeded recovers the finds, typically cheaper (confirm vs hunt).
401
+ // Two sub-paths (SC-3):
402
+ // SEEDED (Codex FINISHED but emitted PROSE, no sentinel JSON): run a CHEAP CONVERTER that turns the
403
+ // prose into the finding schema WITHOUT an independent re-hunt Codex already did the cross-model
404
+ // review, re-hunting the whole diff is the wasted second pass. The converted findings are source
405
+ // 'codex' but NON-preValidated, so the existing Verify phase validates each over real code (trust
406
+ // model unchanged: Verify validates). Layered coverage stays per-card simplify/security + qa +
407
+ // Verify + the batch Final pass keep hunting; only the duplicate full re-hunt is removed.
408
+ // COLD (no prose at all): no Codex signal to convert → run today's FULL code-reviewer (preValidated).
394
409
  if (!codexRan) {
395
- codexEngine = codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)'
396
- const seedBlock = codexProse
397
- ? `\n\nCodex (a DIFFERENT model) reviewed this exact diff and reported the following findings in PROSE — it failed to emit machine-readable JSON, so these are UNVERIFIED leads, NOT validated findings. VERIFY each against the actual diff at its cited file:line, DROP any that don't hold, then run your own full independent review for anything Codex missed. Treat the prose as hints to check, never as ground truth:\n<codex-prose>\n${codexProse}\n</codex-prose>`
398
- : ''
399
- const fb = await agent(
400
- `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.${seedBlock}`,
401
- { label: codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)', phase: 'Discovery', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
402
- )
403
- if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: true })))
410
+ if (codexProse) {
411
+ codexEngine = 'codex (prose-converted)'
412
+ const conv = await agent(
413
+ `Convert the Codex PROSE findings below into the structured finding schema. Codex (a DIFFERENT model) already reviewed this exact diff; your job is ONLY transcription, not review.\n\n` +
414
+ `For each finding in the prose: keep the cited file:line + severity + a one-line minimal_fix_direction. Do a LIGHT plausibility check — DROP a lead whose cited file:line is obviously wrong/empty/missing. Do NOT perform an independent code review, do NOT hunt for new issues, do NOT re-read the whole diff.\n\n` +
415
+ `${waveBrief}\n\n<codex-prose>\n${codexProse}\n</codex-prose>\n\n` +
416
+ `Return findings[] using the schema fields (domain in {doc, security, migration, code, perf, test}).`,
417
+ { label: 'codex (prose-converted)', phase: 'Discovery', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
418
+ )
419
+ // NON-preValidated (no preValidated:true) → verifyFinding routes each to the domain specialist.
420
+ // Clamp confidence < 80: the converter transcribes Codex's (often high) self-confidence, and
421
+ // verifyFinding short-circuits to VERIFIED at confidence >= 80 — which would skip the very Verify
422
+ // pass SC-3 relies on. Capping forces the domain-specialist validation over real code.
423
+ if (conv && Array.isArray(conv.findings)) raw.push(...conv.findings.map((f) => ({ ...f, source: 'codex', confidence: Math.min(Number(f.confidence) || 0, 70) })))
424
+ } else {
425
+ codexEngine = 'code-reviewer (fallback)'
426
+ const fb = await agent(
427
+ `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.`,
428
+ { label: 'code-reviewer (fallback)', phase: 'Discovery', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
429
+ )
430
+ if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: true })))
431
+ }
404
432
  }
405
433
 
406
434
  // Globally-unique finding_id at the fan-in. Finders number F### INDEPENDENTLY, so a Codex F003 and
@@ -526,8 +554,16 @@ if (!securityFix.length && !actionable.length) log('Fix: no actionable code/perf
526
554
  // Merge the two passes. A FAIL in EITHER pass fails the wave; PASS only when a pass actually ran it.
527
555
  const fixPasses = [secResult, codeFixResult]
528
556
  const allActionable = [...securityFix, ...actionable]
529
- const appliedIds = new Set(fixPasses.flatMap((p) => (p.applied || []).map((x) => x.finding_id)))
530
- const unresolvedIds = new Set(fixPasses.flatMap((p) => p.unresolved || []))
557
+ // Collision-safe id reconciliation (SC-1): a writer often echoes only the human-readable tail
558
+ // (e.g. FEAT-0040-04-CR-001) instead of the full prefixed finding_id, so an exact-Set match would
559
+ // drop an applied fix into residual. resolveId maps each echoed token back to its canonical id.
560
+ const canonIds = allActionable.map((f) => f.finding_id)
561
+ const resolveId = makeResolveId(canonIds)
562
+ const appliedIds = new Set(fixPasses.flatMap((p) => (p.applied || []).map((x) => resolveId(x.finding_id))))
563
+ const unresolvedIds = new Set(fixPasses.flatMap((p) => (p.unresolved || []).map(resolveId)))
564
+ const matched = [...appliedIds].filter((id) => canonIds.includes(id)).length
565
+ const claimed = fixPasses.reduce((n, p) => n + (p.applied || []).length, 0)
566
+ if (matched < claimed) log(`Fix: id-reconciliation — ${claimed - matched} applied id(s) did not map to a canonical finding_id (left as residual; check writer echo).`)
531
567
  const ranChecks = fixPasses.filter((p) => p.ran).map((p) => p.checks)
532
568
  const mergedChecks = ['lint', 'tsc', 'build'].reduce((acc, k) => {
533
569
  acc[k] = ranChecks.some((c) => c[k] === 'FAIL') ? 'FAIL' : (ranChecks.some((c) => c[k] === 'PASS') ? 'PASS' : 'SKIP')
@@ -537,6 +573,24 @@ const mergedChecks = ['lint', 'tsc', 'build'].reduce((acc, k) => {
537
573
  const codeResidual = allActionable.filter((f) => !appliedIds.has(f.finding_id) || unresolvedIds.has(f.finding_id))
538
574
  const checksFailed = ['lint', 'tsc', 'build'].some((k) => mergedChecks[k] === 'FAIL')
539
575
 
576
+ // Light-tier test-regression gate (P1-D). At light tier qa-sentinel was SKIPPED in Discovery and the
577
+ // Fix-phase writers run only lint/tsc/build — so a fix that breaks a test slips through (the full suite
578
+ // is deferred to the Final FULL gate, which runs only at the END of the batch). When the Fix phase
579
+ // ACTUALLY applied code fixes at light tier, run ONE diff-scoped qa-sentinel pass over just the tests
580
+ // that import the changed files (NOT the whole suite) and merge its gates into gateTable, so the
581
+ // skill's existing gateTable-OR gate catches a fix-introduced test regression at the wave boundary.
582
+ const fixPhaseTouchedCode = appliedIds.size > 0
583
+ if (maxQaTier !== 'full' && fixPhaseTouchedCode) {
584
+ const scopedQaPrompt = qaPrompt +
585
+ `\n\nDIFF-SCOPED RUN: do NOT run the whole test suite. Scope tests to the files that import the changed files above (the post-fix blast radius) — run only those, plus lint/type-check on the changed files. If the test runner cannot select a diff-scoped subset, SKIP the test gate gracefully (return status SKIP with a one-line reason) rather than running the full suite.`
586
+ const scopedQa = await agent(scopedQaPrompt, { label: 'qa-sentinel (light-tier post-fix)', phase: 'Fix', agentType: 'qa-sentinel', schema: GATES_SCHEMA })
587
+ const scopedGates = (scopedQa && Array.isArray(scopedQa.gates)) ? scopedQa.gates : []
588
+ if (scopedGates.length) {
589
+ gateTable = [...gateTable, ...scopedGates]
590
+ log(`Fix: light-tier diff-scoped qa-sentinel ran after applying code fixes — ${scopedGates.filter((g) => g.status === 'FAIL').length} failing gate(s) merged into gateTable.`)
591
+ }
592
+ }
593
+
540
594
  // ───────────────────────────────────────────────────────────────────────────
541
595
  // Phase Doc — doc-reviewer audit-and-apply on the wave diff (since v4.56.0).
542
596
  // MOVED OFF the orchestrator (was /new Phase 3) so doc-reviewer's analysis +
@@ -574,7 +628,10 @@ if ((docSurfaceTouched || docFindings.length)) {
574
628
  const dres = (dr && typeof dr === 'object') ? dr : {}
575
629
  docApplied = Array.isArray(dres.applied) ? dres.applied : []
576
630
  const docUnresolved = Array.isArray(dres.unresolved) ? dres.unresolved : []
577
- const docAppliedIds = new Set(docApplied.map((x) => x && x.finding_id).filter(Boolean))
631
+ // Collision-safe id reconciliation (SC-1) for doc echoes too. Synthetic ids (doc-audit#i) with no
632
+ // canonical match return unchanged — correct (they don't map to a routed finding).
633
+ const resolveDoc = makeResolveId(docFindings.map((f) => f.finding_id))
634
+ const docAppliedIds = new Set(docApplied.map((x) => resolveDoc(x && x.finding_id)).filter(Boolean))
578
635
  // Verified doc findings NOT applied → residual (skill runs a targeted Phase 3 doc pass / routes code-rooted drift).
579
636
  docResidualOut = docFindings.filter((f) => !docAppliedIds.has(f.finding_id))
580
637
  // doc-reviewer's own unresolved audit items (no matching finding) → slim residual notes.