baldart 4.16.2 → 4.17.1

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,33 @@ 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.17.1] - 2026-06-08
9
+
10
+ **`new2` post-refactor: tre falle di routing/detection emerse al secondo run reale (card FEAT-0019-followup).** Dopo l'hardening v4.17.0, gli agent principali sono correttamente specializzati, ma il *resolve* e il *final review* mostravano tre errori di instradamento. **PATCH** (bugfix di orchestrazione nei workflow `new2-resolve`/`new-final-review`; nessuna nuova capability, nessun cambio layout, **nessuna chiave `baldart.config.yml`** → schema-propagation N/A).
11
+
12
+ > **Why.** Tutti e tre i bug condividono la stessa radice: una decisione che dovrebbe essere **deterministica** (quale agent per quale dominio / esiste Codex) era affidata o a una mappa incompleta o al self-report soft di un subagent. Il dominio freeform `documentation` non matchava la chiave `doc`, il companion Codex (installato e funzionante) veniva dichiarato assente per un timeout di una Bash-call sincrona, e le follow-up card nascevano da un Haiku general-purpose anziché dal loro owner.
13
+
14
+ ### Fixed
15
+
16
+ - **`framework/.claude/workflows/new2-resolve.js` — routing per dominio robusto (F-038/F-024).** Le review agent emettono `domain` come **stringa libera** (`documentation`, `frontend`, …); la mappa fixer keyava solo su `doc`/`ui`, così `documentation` cadeva sul costoso `coder` (Opus) per un finding di pura documentazione. Aggiunto `normDomain()` che normalizza ai bucket di routing (`doc|ui|security|migration|perf|test|code`) **prima** di scegliere fixer e judge, e mappa fixer estesa (`doc→doc-reviewer`, `ui→ui-expert`, `security→security-reviewer`, resto→`coder`) — reviewer-owns-its-domain anche in fase di *apply*, non solo di *judge*.
17
+ - **`framework/.claude/workflows/new2-resolve.js` — follow-up card via `prd-card-writer` (F-039).** La materializzazione della follow-up usava `general-purpose` + Haiku scrivendo uno stub YAML minimale; ora delega all'agent **`prd-card-writer`** (owner del `card-template`, Rule C per `review_profile`, `owner_agent`, traceability) — le card di follow-up rispettano le stesse regole SSOT di quelle native.
18
+ - **`framework/.claude/workflows/new-final-review.js` — detection Codex deterministica + background (F-040).** Codex risultava "non disponibile" pur essendo installato e funzionante: il workflow delegava l'intera invocazione a un subagent generico che la eseguiva **in modo sincrono** dentro una Bash-call e auto-dichiarava `codexAvailable` — un run reale di Codex (minuti) superava il timeout della call → falso negativo → fallback a `code-reviewer`. Ora un **pre-flight deterministico** (un Haiku risolve SOLO il glob del companion e ritorna il path: la decisione di esistenza è tolta al review agent), poi Codex gira in **background con poll su file** e finestra 10 min (allineato a `references/final-review.md` F.3/F.4); `codexAvailable:false` è ammesso solo per `CODEX_NOT_FOUND` reale o file vuoto a fine timeout. Un **unico** fallback deterministico a `code-reviewer` copre sia "companion assente" sia "run non completato".
19
+ - **`new2.js` + `new-final-review.js` — final review snella per batch mono-card (F-041).** Un batch da 1 sola card eseguiva la batch final review **per intero** (`doc-reviewer` + `api-perf-cost-auditor` + `qa-sentinel`), **duplicando** la review per-card (Phase 3) che aveva già girato gli stessi agent sugli stessi file — e senza alcun conflitto cross-card da trovare (~300k token ripetuti su un run reale da 1,28M). Ora `new2.js` passa `singleCard: committed.length === 1`; con quel flag la final review tiene **solo** il passaggio cross-model Codex (valore unico: modello diverso → bug diversi) + i gate `qa-sentinel` (letti dall'integrity gate di merge), e salta i duplicati Claude. Batch multi-card invariati.
20
+ - **`new2.js` + `new2-resolve.js` — fan-out Tier-2 inutile su finding doc (F-038/F-033/F-036).** Un singolo merge-blocker doc-domain già **risolto al Tier-1 e confermato dal judge** (`best:1`) faceva comunque partire **3 `doc-reviewer` paralleli (~150k token)** — con rischio di follow-up spuria per un problema già chiuso. Tre cause composte, tutte corrette: **(A)** la resolve riceveva come MAY-EDIT i file di **codice** della card (`reviewScopeFiles`), così il fixer doc editava `docs/` "fuori ownership" — ora `domainMayEdit()` dà ai finding doc la loro territory reale (`docs_dir`/`references_dir`/`wiki_dir`/`prd_dir`); **(B)** la guardia anti-fabbricazione del judge usava `.every()` (rigetto se *anche un solo* file confermato cade fuori MAY-EDIT) — ma il judge lista naturalmente i file adiacenti (il codice che una doc descrive); ora `someInScope()` (`.some()`) rigetta solo un verdetto che non conferma **nessun** file di proprietà (phantom puro); **(C)** il fan-out a 3 angoli (`root-cause`/`call-site`/`reuse-utility`) è code-bug-shaped → per domini deterministici non-code (doc/test) ora un **singolo** retry mirato invece di 3.
21
+
22
+ ## [4.17.0] - 2026-06-07
23
+
24
+ **`new2` hardening single-wave: 36 finding dal primo run reale risolti in un colpo.** Il primo run reale di `new2` (batch FEAT-0021, 9 card) sotto un outage 529 prolungato è costato **7.4M token / 100 agent / 5h08m producendo solo 6/9 card reali**, con 2 stati `DONE` falsi nel backlog, codice security RLS-bypass non revisionato finito su `develop` via un "safety commit", e 1 follow-up persa. Una raccolta diagnostica end-to-end (36 finding verificati su codice + telemetria reale) ha isolato cause **strutturali**; questo rilascio le risolve tutte in un'unica wave, riportando il costo atteso a ~1.5-2.5M e — prioritario — azzerando le violazioni di integrità/sicurezza sotto degradazione. **MINOR** (cambio sostanziale di capacità del workflow esperimentale `new2`; nessuna rimozione/cambio layout d'install; **nessuna nuova chiave `baldart.config.yml`** → schema-propagation N/A).
25
+
26
+ > **Why.** `new2` è un A/B di `/new` per misurare l'economia di contesto del workflow-hosting. Il run reale ha mostrato che, oltre al costo, l'autonomia zero-ask degradava in modo **non sicuro** sotto outage (DONE falsi, codice non revisionato su trunk, follow-up persa) — esattamente il recovery che `/new` (gated, main-loop) avrebbe fermato. Decisioni utente vincolanti: *Decision-auto, infra-deferred* (niente `BALDART_AUTONOMOUS=1`; azioni infra owner-gated sempre policy-deferred) e *agenti specializzati come `/new`* (no general-purpose dove conta la qualità).
27
+
28
+ ### Changed
29
+ - **`framework/.claude/workflows/new2.js` — scheduler a DAG + resilienza + integrità.** Il for-loop sequenziale e il ramo team-mode sono sostituiti da uno **scheduler dependency-gated** (semantica single-worktree): una card parte solo quando TUTTE le dipendenze in-batch sono `committed`; i dipendenti di una dep fallita vanno `blocked` (niente resolve doomed, F-017/F-021); fallimento transitorio → `pending` + re-invocazione (cap); outage sostenuto → **degrada pulito** (`degraded:true` + worktree clean + return-as-ledger, F-012/F-019/F-020). Implement usa l'**owner_agent** della card; review è un **fan-out specializzato** sfrondato dal `review_profile` (F-024/F-025); commit è **Haiku** con `git status` reconcile, mai `git add -A` (F-023). Skip-completed idempotente su commit-sha+receipt+no-open-followup, NON sul flag `DONE` (F-026). Merge **integrity-gated**: mai force-DONE (F-029), mai commit di codice non revisionato (F-030), mai merge di batch incompleto/degradato (F-031). Telemetria veritiera: `total_tokens` via `budget.spent()`, `agent_count`, `degraded`, `cards_real_done` vs `cards_force_done`=0 (F-032). Parse-guard `args` (F-001).
30
+ - **`framework/.claude/workflows/new2-resolve.js` — short-circuit + anti-fabbricazione + batching.** Verdetto **terminale** che salta il multi-attempt quando il problema è impossibile-by-definition (`out-of-ownership` verificato in JS contro MAY-EDIT; altri reason ratificati dal judge → niente escape-hatch, F-008). **Judge avversariale OBBLIGATORIO** su ogni claim `verified`, con **cross-check JS** dei file confermati indipendentemente dal judge (defeats fabricated success, beccata 2× nel run reale — F-015/F-033). Lista **`findings` batchata** (1 resolve per fix-area, F-007/F-035/F-037). Fixer/judge **per dominio** (doc→doc-reviewer, ui→ui-expert, security→security-reviewer judge, perf→api-perf-cost-auditor judge — F-024). `outOfScopeFindings` non più persi (F-022). Follow-up su Haiku, **offline-safe** (deferita al skill se nessun agente può scrivere, F-020). Parse-guard `args` (F-004).
31
+ - **`framework/.claude/skills/new2/SKILL.md` — reconcile + resume + telemetria reale.** Step 4 passa `ts` (il workflow non ha clock). Step 5: materializza le follow-up dei `residuals` non ancora su disco (offline-safe — il skill ha il filesystem → chiude "nothing dropped" sotto outage, F-020), **rilancia** con `resumeFromRunId` se `degraded` (idempotente via skip-completed), registra telemetria veritiera (`wall_clock_s`, `followups_on_disk` contando i file — F-032).
32
+ - **`framework/.claude/agents/prd-card-writer.md` — Rule A.1 (AC↔ownership consistency).** Fix a monte del difetto ricorrente "AC fuori dal MAY-EDIT" (F-016): ogni AC dev'essere soddisfabile editando i file della propria card.
33
+ - **`framework/docs/WORKFLOWS.md`** — righe `new2`/`new2-resolve` aggiornate al comportamento v4.17.0.
34
+
8
35
  ## [4.16.2] - 2026-06-06
9
36
 
10
37
  **`baldart update` non si blocca più all'infinito su un edit `.framework/` ormai assorbito a monte.** Un edit locale a un file framework (es. `agents/ui-expert.md`) committato con un subject che *non* contiene la parola "baldart" sfuggiva alla classificazione `chore-wrapper` (rumore) e veniva marcato `custom-overlay-able`. Quel commit resta per sempre ancestor di `HEAD` ma mai di `FETCH_HEAD`, quindi `git log FETCH_HEAD..HEAD -- .framework` lo ripesca a **ogni rilascio successivo** — e il flusso seamless si fermava sul gate `divergence-capture-incomplete` perché l'overlay (additivo) non riproduceva byte-a-byte il file `.framework`, anche quando il contenuto era già identico all'upstream e la personalizzazione viveva correttamente nell'overlay. Risultato: lo stesso commit "fantasma" rifaceva muro a ogni `update`, senza via d'uscita via overlay (riconciliare un overlay additivo non può mai far passare il round-trip stretto). **PATCH** (bugfix del detector di divergenza + reset-safety degli overlay additivi; nessun cambio di capability, nessuna chiave `baldart.config.yml`).
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.16.2
1
+ 4.17.1
@@ -136,6 +136,31 @@ that list — not this one — is the source of truth.
136
136
  **Epic cards** use `owner_agent: ""` (empty string) because they are
137
137
  trackers, not implementation. The dispatcher skips them.
138
138
 
139
+ ### Rule A.1 — AC↔ownership consistency (every AC must be satisfiable inside the card)
140
+
141
+ For EVERY acceptance criterion you write, the file(s) that AC requires editing
142
+ MUST be a subset of that card's `files_likely_touched` (its MAY-EDIT ownership).
143
+ An AC whose ONLY implementation site lives in another card's file is
144
+ **structurally unsatisfiable** by this card — at execution time it can never
145
+ close, so it burns an entire resolve cycle and then defers anyway (and, on
146
+ impossible tasks, *induces* fabricated "fixed" claims). Concretely:
147
+
148
+ - If an AC needs the JSX **render** of a value, the card that owns the render
149
+ file (e.g. `homepage-store.tsx`) must carry that AC — not the data/view-model
150
+ card that merely computes the value.
151
+ - If an AC needs **wiring at a call-site** (e.g. `after(() => dispatch())` inside
152
+ a create funnel in `data.ts`), assign it to the card that owns that file, or
153
+ add that file to this card's `files_likely_touched`, or express the dependency
154
+ explicitly via `depends_on` + a card that owns the site.
155
+ - An AC whose remedy is an **owner-gated infra action** (remote DB push, deploy,
156
+ secret rotation, DNS) is NOT a code AC — phrase it as a tracked manual/ops
157
+ step, never as a per-card code acceptance criterion.
158
+
159
+ Self-check before emitting cards: for each AC, name the file it edits and
160
+ confirm that file is in the same card's `files_likely_touched`. If it isn't,
161
+ move the AC or widen ownership. (This is the upstream fix for the recurring
162
+ "AC outside MAY-EDIT" failure; `new2`/`/new` also re-assert it at pre-flight.)
163
+
139
164
  ### Rule B — UI cards (`ui-expert`) have UI-ONLY scope
140
165
 
141
166
  When you assign `owner_agent: ui-expert` to a card, its scope MUST cover
@@ -99,30 +99,55 @@ Workflow({ name: 'new2', args: {
99
99
  metricsDir, // $METRICS
100
100
  refModulesBase, // .claude/skills/new/references (semantic SSOT)
101
101
  config, // the parsed baldart.config.yml (paths.*/stack.*/features.*/git.*)
102
+ ts, // ISO timestamp NOW — the workflow has no clock (Date.now() unavailable there)
102
103
  flags: { stats, effort, full }
103
104
  }})
104
105
  ```
105
106
 
107
+ Record the kickoff time (`ts`) before the call — Step 5 uses it to stamp the
108
+ real wall-clock duration (the workflow cannot read a clock).
109
+
106
110
  The workflow runs the entire batch in the background. **Do not poll it** — it
107
111
  returns when the batch is done. It returns:
108
112
 
109
113
  ```
110
- { report, perCardResults, gateLedger, residualFollowups, telemetry }
114
+ { report, perCardResults, gateLedger, residualFollowups, residuals,
115
+ degraded, degradationReasons, telemetry }
111
116
  ```
112
117
 
113
- `report` is a ready-to-show markdown batch summary (files changed per card, review
114
- findings, QA, merge hash, any auto-created follow-up cards). `residualFollowups`
115
- lists every follow-up card `new2-resolve` materialised for an unresolved
116
- gate/scope-expansion (nothing was silently dropped). `telemetry` is the Phase-8
117
- record (same schema as `/new`, with `variant: "new2"`).
118
-
119
- ### Step 5 Present + record
120
-
121
- - Print `report` to the user verbatim (it is already the single batch summary).
122
- - If `residualFollowups` is non-empty, surface them prominently ("queste AC/finding
123
- non risolti autonomamente sono stati tracciati come follow-up card: …") — this is
124
- the post-run review that replaced the ~25 mid-run questions.
125
- - Append `telemetry` (with `variant: "new2"`) to `${metricsDir}/skill-runs.jsonl`
126
- and, when `flags.stats`, run the session token-telemetry script the same way
127
- `/new` Phase 8 does (`-stats`), so the A/B comparison has real `usage` data for
128
- both variants. Do NOT re-summarise the cards yourselfthe workflow already did.
118
+ - `report` ready-to-show markdown batch summary.
119
+ - `residuals` the **OFFLINE-SAFE ledger of record**: every residual the workflow
120
+ could not finish, each `{ card, kind, evidence, materialized }`. A residual with
121
+ `materialized:false` has NO follow-up card on disk yet (e.g. the workflow hit an
122
+ outage where no agent could write the file). **You (the skill) must reconcile it.**
123
+ - `degraded` / `degradationReasons` — the batch stopped early under a sustained
124
+ outage (or another degradation). The batch is NOT complete; it must be resumed.
125
+ - `telemetry` — the Phase-8 record (`variant:"new2"`).
126
+
127
+ ### Step 5 Reconcile, resume, present, record
128
+
129
+ 1. **Materialise missing follow-ups (offline-safe you have filesystem access, the
130
+ workflow does not).** For every `residuals[]` entry with `materialized:false`,
131
+ write a follow-up card `${paths.backlog_dir}/<card>-followup-<kind>.yml` (status:
132
+ TODO; ≥1 requirement derived from the residual; acceptance_criteria = the verbatim
133
+ residual; files_likely_touched from the card's ownership)it MUST pass the `/new`
134
+ pre-flight field check. This is the layer that guarantees **nothing is ever
135
+ dropped, even when every agent was dead** during the run.
136
+ 2. **Resume if degraded.** If `degraded` is true, re-invoke the workflow with
137
+ `Workflow({ scriptPath, resumeFromRunId })` (same `args` + the new `ts`). The
138
+ per-card **skip-completed** guard makes the resume idempotent — already-committed
139
+ cards are skipped, only the incomplete/blocked ones run. Repeat until `degraded`
140
+ is false (or the same cards stall twice → surface to the user).
141
+ 3. **Present.** Print `report` verbatim. Surface `residuals` prominently
142
+ ("questi residui sono tracciati come follow-up: …") — the post-run review that
143
+ replaced the ~25 mid-run questions. If `degraded`, say so plainly (the run was
144
+ incomplete and resumed).
145
+ 4. **Record truthful telemetry.** Before appending `telemetry` to
146
+ `${metricsDir}/skill-runs.jsonl`, fill the fields the workflow could not compute:
147
+ `wall_clock_s` (now − kickoff `ts`) and `followups_on_disk` (count the actual
148
+ follow-up files on disk, NOT `residualFollowups.length` — which double-counts).
149
+ `total_tokens`/`agent_count` come from the workflow (`budget.spent()` delta +
150
+ spawn counter); if `total_tokens` is null, run the `/new` Phase-8 `-stats` script
151
+ to backfill real `usage`. Keep `degraded`/`degradation_reasons` in the record so
152
+ the A/B comparison can exclude or weight degraded runs. Do NOT re-summarise the
153
+ cards — the workflow already did.
@@ -116,11 +116,40 @@ if (baselinePaths.length) {
116
116
  // ───────────────────────────────────────────────────────────────────────────
117
117
  phase('Review')
118
118
 
119
+ // F-040 — DETERMINISTIC Codex pre-flight (fixes the false-negative where Codex is
120
+ // installed+working but the review subagent reported codexAvailable:false because a
121
+ // synchronous inline run hit its Bash timeout). The workflow sandbox has no Bash, so a
122
+ // minimal Haiku agent runs ONLY the resolution glob and returns the path — the existence
123
+ // decision is thus taken OUT of the review agent's hands. Found → Codex runs in the
124
+ // BACKGROUND with file-poll (canonical final-review.md F.3/F.4). Not found → straight to
125
+ // the code-reviewer fallback, no wasted Codex agent.
126
+ const PREFLIGHT_SCHEMA = {
127
+ type: 'object', required: ['codexScriptPath'], additionalProperties: false,
128
+ properties: { codexScriptPath: { type: 'string', description: 'absolute path to codex-companion.mjs, or "" if none' } },
129
+ }
130
+ let codexScriptPath = ''
131
+ try {
132
+ const pf = await agent(
133
+ `Resolve the Codex companion script path. Run EXACTLY this one Bash command and nothing else:\n` +
134
+ " ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex/scripts/codex-companion.mjs ~/.claude/plugins/cache/openai-codex/codex/*/scripts/codex-companion.mjs 2>/dev/null | sort -V | tail -1\n" +
135
+ `Return codexScriptPath = the trimmed stdout (a single absolute path), or "" if the command printed nothing. Do NOT run Codex, do NOT read any other file, do NOT reason about availability — just report the command's stdout.`,
136
+ { label: 'codex-preflight', phase: 'Review', model: 'haiku', schema: PREFLIGHT_SCHEMA }
137
+ )
138
+ codexScriptPath = (pf && typeof pf.codexScriptPath === 'string') ? pf.codexScriptPath.trim() : ''
139
+ } catch (_) { codexScriptPath = '' }
140
+ const codexResolved = /codex-companion\.mjs$/.test(codexScriptPath)
141
+ log(codexResolved
142
+ ? `Codex companion resolved deterministically: ${codexScriptPath}`
143
+ : 'Codex companion NOT found (deterministic pre-flight) — primary review via code-reviewer fallback.')
144
+
119
145
  const codexPrompt =
120
146
  `Run a deep, cross-model code review over this batch diff, following the /codexreview protocol summarized in ${protocolRef} (Step F.3). The code is already written and committed — find bugs, regressions, security issues, and quality problems.\n\n` +
121
- `Resolve and invoke the Codex companion (a non-Anthropic frontier model) via Bash:\n` +
122
- " CODEX_SCRIPT=\"$(ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex/scripts/codex-companion.mjs ~/.claude/plugins/cache/openai-codex/codex/*/scripts/codex-companion.mjs 2>/dev/null | sort -V | tail -1)\"\n" +
123
- `If no companion script is found, OR the review does not complete, return { "codexAvailable": false, "findings": [] } — do NOT fabricate findings.\n\n` +
147
+ `The Codex companion script is ALREADY CONFIRMED PRESENT at:\n ${codexScriptPath}\n` +
148
+ `Launch it in the BACKGROUND and poll for completion — do NOT run it synchronously (a sync run would hit the Bash tool timeout and is the exact bug this design fixes):\n` +
149
+ ` REVIEW_FILE=a unique /tmp file (e.g. /tmp/codexreview-batch-${a.firstCardId || 'batch'}-$$.md)\n` +
150
+ ` • node "${codexScriptPath}" task "<your review instructions>" > "$REVIEW_FILE" 2>&1 — run this with run_in_background:true (the launching call returns immediately).\n` +
151
+ ` • Then POLL $REVIEW_FILE (BashOutput / repeated reads) until it holds a terminal result, up to a full 10-minute window.\n` +
152
+ `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` +
124
153
  `${scopeBrief}\n\n${baselineBrief}\n\n` +
125
154
  `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). ` +
126
155
  `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.`
@@ -134,16 +163,29 @@ const apiPrompt =
134
163
  const qaPrompt =
135
164
  `Run MECHANICAL GATES ONLY over the batch scope, per ${protocolRef} Step F.3 (qa-sentinel row): lint, type-check, the full test suite, build, dependency audit, and markdownlint as applicable to this project. Do NOT read source for code findings, do NOT emit severities — return only a PASS/FAIL/SKIP gate table.\n\nWorktree: ${a.worktreePath || '(cwd)'}\nChanged files:\n${scope.join('\n')}`
136
165
 
166
+ // F-041 — single-card batch: the per-card review (Phase 3) already ran doc-reviewer +
167
+ // api-perf-cost-auditor over these exact files, and a 1-card batch has NO cross-card
168
+ // conflict to surface. Keep ONLY the cross-model Codex pass (its unique value — a different
169
+ // model finds different bugs) + qa-sentinel gates; skip the Claude-agent duplicates.
170
+ const slim = a.singleCard === true
171
+ // qa-sentinel always runs — the merge integrity gate reads its PASS/FAIL table.
137
172
  const reviewThunks = [
138
- () => agent(codexPrompt, { label: 'codex', phase: 'Review', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', r })),
139
- () => agent(docPrompt, { label: 'doc-reviewer', phase: 'Review', agentType: 'doc-reviewer', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'doc', r })),
140
173
  () => agent(qaPrompt, { label: 'qa-sentinel', phase: 'Review', agentType: 'qa-sentinel', schema: GATES_SCHEMA }).then((r) => ({ kind: 'qa', r })),
141
174
  ]
142
- // api-perf-cost-auditor is skipped when the batch touches no API/data files (F.3 note).
143
- if (a.hasApiDataFiles !== false) {
175
+ if (!slim) {
176
+ reviewThunks.unshift(() => agent(docPrompt, { label: 'doc-reviewer', phase: 'Review', agentType: 'doc-reviewer', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'doc', r })))
177
+ }
178
+ // Codex thunk runs ONLY when the pre-flight resolved the companion (else: no wasted agent).
179
+ if (codexResolved) {
180
+ reviewThunks.unshift(() => agent(codexPrompt, { label: 'codex', phase: 'Review', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', r })))
181
+ }
182
+ // api-perf-cost-auditor: skipped when no API/data files OR on a slim single-card pass.
183
+ if (!slim && a.hasApiDataFiles !== false) {
144
184
  reviewThunks.push(() => agent(apiPrompt, { label: 'api-perf-cost-auditor', phase: 'Review', agentType: 'api-perf-cost-auditor', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'api', r })))
145
185
  } else {
146
- log('Review: api-perf-cost-auditor skipped (no API/data files in scope).')
186
+ log(slim
187
+ ? 'Review: single-card batch — doc-reviewer + api-perf skipped (already run per-card); kept cross-model Codex + qa gates.'
188
+ : 'Review: api-perf-cost-auditor skipped (no API/data files in scope).')
147
189
  }
148
190
 
149
191
  const reviewResults = (await parallel(reviewThunks)).filter(Boolean)
@@ -151,20 +193,16 @@ const reviewResults = (await parallel(reviewThunks)).filter(Boolean)
151
193
  // ---- Fan-in (F.4 step 9): collect findings + Codex fallback branch ----------
152
194
  let raw = []
153
195
  let gateTable = []
154
- let codexEngine = 'codex'
196
+ let codexEngine = codexResolved ? 'codex' : 'code-reviewer (fallback)'
197
+ let codexRan = false
155
198
  for (const item of reviewResults) {
156
199
  if (item.kind === 'codex') {
157
200
  if (item.r && item.r.codexAvailable && Array.isArray(item.r.findings)) {
201
+ codexRan = true
158
202
  raw.push(...item.r.findings.map((f) => ({ ...f, source: 'codex', preValidated: true })))
159
203
  } else {
160
- // CODEX_NOT_FOUND / TIMED_OUT Claude code-reviewer fallback (F.4 step 8).
161
- codexEngine = 'code-reviewer (fallback)'
162
- log('Review: Codex unavailable — falling back to code-reviewer for the primary code review.')
163
- const fb = await agent(
164
- `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.`,
165
- { label: 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
166
- )
167
- if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: false })))
204
+ // Resolved but the run itself failed (CODEX_NOT_FOUND written / empty after timeout).
205
+ log('Review: Codex companion resolved but its review did not complete — falling back to code-reviewer.')
168
206
  }
169
207
  } else if (item.kind === 'qa') {
170
208
  gateTable = (item.r && item.r.gates) || []
@@ -173,6 +211,17 @@ for (const item of reviewResults) {
173
211
  }
174
212
  }
175
213
 
214
+ // F-040 — ONE deterministic fallback: either the pre-flight found no companion, or it ran
215
+ // but did not complete. Either way the primary code review must still happen (F.4 step 8).
216
+ if (!codexRan) {
217
+ codexEngine = 'code-reviewer (fallback)'
218
+ const fb = await agent(
219
+ `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.`,
220
+ { label: 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
221
+ )
222
+ if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: false })))
223
+ }
224
+
176
225
  // ───────────────────────────────────────────────────────────────────────────
177
226
  // Phase Verify (F.4) — adversarial validation of low-confidence findings
178
227
  // Codex findings are pre-FP-validated → VERIFIED. Claude-agent findings with