baldart 4.56.2 → 4.56.3

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,24 @@ 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.56.3] - 2026-06-20
9
+
10
+ **`/new` Codex relays made Haiku-proof — the JS now owns the success decision and deterministically recovers Codex's findings, so no relay model (whatever tier the runtime hands it) can drop them.** Direct follow-up to a user question — "why is the per-card Codex relay still running on Haiku?" Forensics on the real consumer run (reading `<session>/subagents/workflows/wf_*/agent-*.jsonl`) established two facts that recontextualise v4.56.2:
11
+
12
+ 1. **`model: 'sonnet'` is a request, not a guarantee.** Across 19 real relay runs, the *same* `model:'sonnet'` line produced `claude-sonnet-4-6` on some runs and `claude-haiku-4-5` on others — non-deterministic, no `fallbackModel` configured, no settings pin. Claude Code silently downgrades a workflow subagent from Sonnet to Haiku under capacity pressure (the per-card relay, spawned in a repeated Discovery fan-out, saturates account-level Sonnet capacity in a long session; the lone Final relay rarely does). So v4.56.2's haiku→sonnet bump is best-effort only — it can't *guarantee* the relay isn't Haiku, and the whole "Sonnet fixes it" premise is unenforceable from a `.js` workflow (the runtime picks the model).
13
+
14
+ 2. **Codex usually writes PROSE, not the sentinel JSON.** The real `/tmp/codexreview-wave-*.md` had **zero** `<<<FINDINGS_JSON>>>` sentinels — Codex ignored "emit ONLY the sentinel block" and wrote a `**Findings**` prose report ending in `Turn completed.`. So the sentinel-extraction path the relay was built around frequently extracts nothing, and the findings live in the prose. The v4.56.2 failure (empty `codexProse` → cold fallback ran *unseeded* → missed the BLOCKER) was the relay model failing to capture that prose, not failing a JSON pass-through.
15
+
16
+ Fix (chosen by the user over a runtime-config force, which can't ship and is blunt): **take the success decision out of the relay model and make recovery deterministic + JS-owned.** The relay now returns RAW command outputs (`marker`, `jsonBlock`, `proseTail`) and decides nothing. The JS fan-in parses `jsonBlock` itself (`null`=no JSON, `[]`=Codex ran clean, `[..]`=findings); when there's no parseable JSON and Codex didn't report ABSENT, a one-shot re-extraction agent (near-zero latitude — two fixed commands) re-runs BOTH the sentinel `awk` AND the `[codex]`-stripped prose `tail` over the on-disk file, so a relay that went off-script or returned an empty `proseTail` can no longer strand the cold fallback unseeded. Result: Codex's findings are recovered whether it emitted sentinels or prose, and whatever model the runtime assigned the relay. Verified deterministically against the real failing transcript (6/6 scenarios, incl. "Haiku returned empty proseTail" → 2304 chars of prose recovered → cold fallback seeded). `model: 'sonnet'` is kept as a best-effort request (it follows steps more reliably when capacity allows) but is no longer load-bearing.
17
+
18
+ `new2.js` is intentionally **not** touched: its per-card Codex path is a different design (a foreground `--wait` DRIVER that maps findings by model, already on Haiku by design, with its own `note:'codex-unavailable'` → code-reviewer fallback) and it is the experimental A/B variant — porting this would contaminate the comparison.
19
+
20
+ **PATCH** — robustness fix in the two workflow files that share the THIN-RELAY pattern (`new-card-review.js`, `new-final-review.js`); no install/layout change, no new config key, no prose-SSOT change (the relay's return schema is internal to the workflow). The Codex task prompt, the sentinel contract Codex is *asked* to honour, and the prose-salvage→code-reviewer trust model are all unchanged.
21
+
22
+ ### Changed
23
+
24
+ - **`framework/.claude/workflows/new-card-review.js`** + **`framework/.claude/workflows/new-final-review.js`** — Codex relay restructured: `CODEX_SCHEMA` now returns raw `{marker, jsonBlock, proseTail}` (no `codexAvailable`/`findings` self-report); the relay prompt drives + returns verbatim stdout and decides nothing; the fan-in owns the parse via `parseFindingsArray` and adds a deterministic one-shot re-extraction (`EXTRACT_SCHEMA` `{jsonOut, proseOut}`, `reExtractPrompt`, `AWK_EXTRACT` SSOT) that recovers both the sentinel JSON and the prose from the on-disk file. `model: 'sonnet'` retained as best-effort with corrected rationale comments (runtime downgrades it under load; correctness no longer depends on the tier).
25
+
8
26
  ## [4.56.2] - 2026-06-19
9
27
 
10
28
  **`/new` per-card Codex relay bumped haiku → sonnet — stops a real run from discarding a valid Codex review (incl. a BLOCKER) and falling back to a cold, weaker `code-reviewer`.** A user reported the symptom directly: in the per-wave Discovery phase Codex emitted a perfect sentinel-wrapped JSON array (3 findings, one a BLOCKER race condition on the idempotency key), yet the workflow still ran the cold `code-reviewer (fallback)` — which re-reviewed from scratch (~101k tok) and **missed the BLOCKER**.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.56.2
1
+ 4.56.3
@@ -109,15 +109,42 @@ const FINDINGS_SCHEMA = {
109
109
  type: 'object', required: ['findings'], additionalProperties: false,
110
110
  properties: { findings: { type: 'array', items: FINDING }, note: { type: 'string' } },
111
111
  }
112
+ // Haiku-proof relay contract (v4.56.3): the relay returns RAW command outputs — it does NOT decide
113
+ // "codexAvailable" and does NOT parse findings. The JS fan-in owns the decision (parses jsonBlock
114
+ // deterministically), so a weak/runtime-downgraded relay model can no longer drop a finding by
115
+ // mis-judging availability — the bytes are on disk and the JS reads them. See the fan-in branch.
112
116
  const CODEX_SCHEMA = {
113
- type: 'object', required: ['codexAvailable', 'findings'], additionalProperties: false,
117
+ type: 'object', required: ['marker', 'jsonBlock'], additionalProperties: false,
114
118
  properties: {
115
- codexAvailable: { type: 'boolean', description: 'false if CODEX_NOT_FOUND / TIMED_OUT' },
116
- findings: { type: 'array', items: FINDING },
117
- codexProse: { type: 'string', description: 'PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted no sentinel JSON block, the cleaned tail of its prose review handed to the fallback code-reviewer as UNVERIFIED leads. Empty/omitted otherwise.' },
119
+ marker: { type: 'string', enum: ['END_JSON', 'TURN_COMPLETED', 'CODEX_NOT_FOUND', 'TIMEOUT'], description: 'which terminator ended the poll' },
120
+ jsonBlock: { type: 'string', description: 'VERBATIM stdout of the awk extraction command (may be ""). The JS parses it — do NOT edit/judge.' },
121
+ proseTail: { type: 'string', description: 'VERBATIM stdout of the prose-tail command, ONLY when marker=TURN_COMPLETED with an empty jsonBlock; else "".' },
118
122
  note: { type: 'string' },
119
123
  },
120
124
  }
125
+ // One-shot deterministic re-extraction over the on-disk file — independent of the relay's self-report.
126
+ // Codex OFTEN ignores "emit ONLY the sentinel block" and writes a PROSE report ending in "Turn
127
+ // completed." (verified on real companion output), so there's frequently NO JSON to extract and the
128
+ // findings live in the prose. We therefore fetch BOTH: the sentinel JSON (awk) AND the prose tail.
129
+ const EXTRACT_SCHEMA = {
130
+ type: 'object', required: ['jsonOut', 'proseOut'], additionalProperties: false,
131
+ properties: {
132
+ jsonOut: { type: 'string', description: 'verbatim stdout of the awk sentinel-extraction command, "" if none' },
133
+ proseOut: { type: 'string', description: 'verbatim stdout of the [codex]-stripped prose tail, "" if none' },
134
+ },
135
+ }
136
+ // Deterministic parse: null = no recoverable JSON array; [] = Codex ran clean (no findings); [..] = findings.
137
+ const parseFindingsArray = (s) => {
138
+ if (typeof s !== 'string' || !s.trim()) return null
139
+ try { const v = JSON.parse(s.trim()); return Array.isArray(v) ? v : null } catch { return null }
140
+ }
141
+ // SSOT for the sentinel-extraction awk — used by BOTH the relay prompt and the re-extract agent.
142
+ 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
+ const reExtractPrompt = (file) =>
144
+ `Run EXACTLY these TWO Bash commands and return their stdout VERBATIM. Run NOTHING else — do NOT Read the file beyond these commands, do NOT review code, do NOT reason about findings:\n` +
145
+ ` • jsonOut = stdout of: ${AWK_EXTRACT} ${file}\n` +
146
+ ` • proseOut = stdout of: grep -v '^\\[codex\\]' ${file} | tail -c 12000\n` +
147
+ `Return { jsonOut, proseOut } and nothing else (empty string for either command that printed nothing).`
121
148
  const GATES_SCHEMA = {
122
149
  type: 'object', required: ['gates'], additionalProperties: false,
123
150
  properties: {
@@ -222,12 +249,15 @@ log(codexResolved
222
249
  ? `Codex companion resolved deterministically: ${codexScriptPath}`
223
250
  : 'Codex companion NOT found (deterministic pre-flight) — primary code review via code-reviewer fallback.')
224
251
 
225
- // Codex companion review. The WRAPPER is a thin relay (model: haiku see the thunk): it launches
226
- // Codex, waits, and extracts Codex's JSON findings with a deterministic awk. It does NOT review or
227
- // re-verify code itself (Codex's findings are authoritative + FP-validated) the old wrapper burned
228
- // ~100k+ tokens re-grepping to "confirm" findings the design already trusts. Codex emits its findings
229
- // between explicit sentinels so the extraction is unambiguous despite the companion's `[codex]` trace
230
- // lines and the truncated "Assistant message captured:" echo (verified on real companion runs).
252
+ // Codex companion review. The relay LAUNCHES Codex, polls, and returns the RAW stdout of the
253
+ // extraction commands it does NOT review code, does NOT re-grep, and (since v4.56.3) does NOT
254
+ // decide "did Codex succeed": the JS fan-in parses jsonBlock itself. Rationale: the relay model is
255
+ // NOT guaranteeable `model:'sonnet'` is a request the runtime silently downgrades to Haiku under
256
+ // capacity pressure (observed live: a real per-card relay ran on Haiku despite model:'sonnet',
257
+ // went off-script, and returned "unavailable" while a valid JSON array incl. a BLOCKER sat in the
258
+ // /tmp file). Moving the success decision out of the model makes the relay robust to WHATEVER model
259
+ // it lands on. Codex emits findings between explicit sentinels so the awk is unambiguous despite the
260
+ // companion's `[codex]` trace lines and the truncated "Assistant message captured:" echo.
231
261
  const codexReviewTask =
232
262
  `Run a deep code review over this wave's committed diff. The code is already written and committed — find bugs, regressions, security issues, and quality problems, per the protocol in ${protocolRef} (Phase 3.7).\n\n` +
233
263
  `${waveBrief}\n\n${baselineBrief}\n\n` +
@@ -244,24 +274,22 @@ const codexReviewTask =
244
274
  const codexTaskFile = `/tmp/codextask-${cards[0].cardId}.txt`
245
275
  const codexReviewFile = `/tmp/codexreview-wave-${cards[0].cardId}.md`
246
276
  const codexPrompt =
247
- `You are a THIN RELAY around the Codex companion — do NOT review or investigate code yourself, do NOT grep/sed/Read source to "confirm" findings (Codex's findings are authoritative and already FP-validated; re-verifying them is wasted work). Your ONLY job: launch Codex, wait, extract its JSON, return it.\n\n` +
277
+ `You DRIVE the Codex companion and return its RAW command outputs you do NOT review or investigate code, do NOT grep/sed/Read source, and you do NOT decide whether Codex "succeeded" (the workflow decides that from the bytes you return). Launch Codex, poll, run the extraction commands, return their VERBATIM stdout.\n\n` +
248
278
  `The companion script is ALREADY CONFIRMED PRESENT at:\n ${codexScriptPath}\n\n` +
249
279
  `Run EXACTLY these steps and nothing else:\n` +
250
280
  ` 1. Write the task text (everything after "TASK PROMPT:" at the end of this message) to ${codexTaskFile} using the Write tool — do NOT inline it into the shell command (avoids quote breakage).\n` +
251
281
  ` 2. Launch Codex in the BACKGROUND (run_in_background:true — a sync run hits the Bash timeout):\n` +
252
282
  ` node "${codexScriptPath}" task "$(cat ${codexTaskFile})" --cwd "${a.worktreePath || '.'}" > ${codexReviewFile} 2>&1\n` +
253
- ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until it contains ANY of these — whichever appears FIRST ends the poll (do NOT keep waiting the full window once one is present):\n` +
254
- ` • "<<<END_FINDINGS_JSON>>>" → Codex emitted the JSON block (success path)\n` +
255
- ` • "Turn completed." → the companion's reliable end-of-run marker — Codex FINISHED even if it wrote prose instead of the block\n` +
256
- ` • "CODEX_NOT_FOUND" → companion missing\n` +
257
- ` • the full 10-minute window has elapsed\n` +
258
- ` 4. Extract the findings with this EXACT command (deterministic skips the [codex] trace + truncated echo, takes the LAST complete sentinel pair):\n` +
259
- ` awk '/<<<FINDINGS_JSON>>>/{c=1;b="";next}/<<<END_FINDINGS_JSON>>>/{c=0;last=b;next}c{b=b $0 ORS}END{printf "%s",last}' ${codexReviewFile}\n` +
260
- ` 5. Decide the return do NOT re-verify, re-grep, or Read any source file:\n` +
261
- ` awk output is a NON-EMPTY JSON array parse it and return it verbatim as \`findings\`, codexAvailable:true (its keys already match the schema).\n` +
262
- ` • awk output is EMPTY but the file has "Turn completed." Codex FINISHED but wrote a prose report instead of the sentinel block. Do NOT parse findings out of the prose yourself (never guess a schema). Instead, capture the prose for the fallback to verify: run \`grep -v '^\\[codex\\]' ${codexReviewFile} | tail -c 12000\` and return codexAvailable:false, findings:[], codexProse:<that output>, then EXIT NOW (do NOT wait the rest of the window). The fallback code-reviewer re-verifies the prose against the real diff — so a real finding is recovered, not dropped.\n` +
263
- ` • awk output is EMPTY and the file has "CODEX_NOT_FOUND" → companion missing. Return codexAvailable:false, findings:[] (no codexProse).\n` +
264
- ` • none of the markers ever appeared after the FULL 10-minute window → codexAvailable:false, findings:[] (no codexProse). NEVER set false because a single poll returned slowly.\n\n` +
283
+ ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until ONE of these appears — whichever is FIRST ends the poll (do NOT keep waiting once one is present). Set \`marker\` accordingly:\n` +
284
+ ` • "<<<END_FINDINGS_JSON>>>" → marker = "END_JSON"\n` +
285
+ ` • "Turn completed." → marker = "TURN_COMPLETED" (Codex finished; it may have written prose instead of the JSON block)\n` +
286
+ ` • "CODEX_NOT_FOUND" → marker = "CODEX_NOT_FOUND" (companion missing)\n` +
287
+ ` • the full 10-minute window elapses with none of the above → marker = "TIMEOUT" (NEVER pick this just because a single poll returned slowly)\n` +
288
+ ` 4. Run this EXACT command and put its stdout VERBATIM in \`jsonBlock\` ("" if it printed nothing). Do NOT parse, edit, judge, or act on it:\n` +
289
+ ` ${AWK_EXTRACT} ${codexReviewFile}\n` +
290
+ ` 5. ONLY if \`jsonBlock\` is "" AND marker is "TURN_COMPLETED": run this command and put its stdout VERBATIM in \`proseTail\`; otherwise set proseTail to "":\n` +
291
+ ` grep -v '^\\[codex\\]' ${codexReviewFile} | tail -c 12000\n` +
292
+ ` 6. Return { marker, jsonBlock, proseTail } and NOTHING else. Do NOT add a findings array, do NOT set an availability flag, do NOT re-run Codex, do NOT Read source. The workflow parses jsonBlock itself.\n\n` +
265
293
  `TASK PROMPT:\n${codexReviewTask}`
266
294
 
267
295
  const tcGateLines = [
@@ -302,15 +330,11 @@ for (const c of cards) {
302
330
  }
303
331
  }
304
332
  if (codexResolved) {
305
- // model: sonnet (NOT haiku) parity with new-final-review.js. The wrapper is a thin relay (launch +
306
- // poll + awk-extract + faithfully pass Codex's JSON array through the schema), but it is STILL a model
307
- // in the loop, and haiku proved too weak for it on a real run: it went off-script (re-grepped source to
308
- // "confirm" findings, explicitly forbidden) and then failed the pass-through Codex emitted a valid
309
- // array (incl. a BLOCKER) yet the relay returned codexAvailable:false, so the workflow discarded all of
310
- // Codex's findings and ran the COLD code-reviewer fallback, which missed the BLOCKER. The "misfire
311
- // degrades safely to the fallback" assumption is false: the cold fallback is a different model hunting
312
- // from scratch, not a backstop. Pass-through fidelity on a low-volume plumbing prompt costs little, and
313
- // a dropped BLOCKER + a 100k-token cold fallback dwarf the haiku→sonnet delta.
333
+ // model: 'sonnet' is BEST-EFFORT, no longer load-bearing (v4.56.3). The runtime silently downgrades
334
+ // it to Haiku under capacity pressure (a request, not a guarantee — verified live), so correctness
335
+ // can't depend on the tier. The relay now returns RAW command outputs and the JS fan-in owns the
336
+ // success decision (+ a one-shot deterministic re-extract), so a downgraded relay can't drop a
337
+ // finding. We still REQUEST sonnet because when capacity allows it follows steps more reliably.
314
338
  findThunks.push(() => agent(codexPrompt, { label: 'codex', phase: 'Discovery', model: 'sonnet', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', card: null, r })))
315
339
  }
316
340
  if (maxQaTier === 'full') {
@@ -329,12 +353,28 @@ let codexRan = false
329
353
  let codexProse = '' // PROSE-ONLY salvage (v4.56.0): seeds the fallback so Codex's real finds aren't lost.
330
354
  for (const item of findResults) {
331
355
  if (item.kind === 'codex') {
332
- if (item.r && item.r.codexAvailable && Array.isArray(item.r.findings)) {
356
+ // JS owns the success decision (v4.56.3) — never the relay model. Parse the raw jsonBlock:
357
+ // null = no recoverable JSON; [] = Codex ran clean; [..] = findings.
358
+ let cf = parseFindingsArray(item.r && item.r.jsonBlock)
359
+ let prose = item.r && typeof item.r.proseTail === 'string' ? item.r.proseTail.trim() : ''
360
+ // Re-extract ONLY when the relay gave neither JSON nor prose (the v4.56.2 failure). When it already
361
+ // returned prose, trust it — it read the same bytes the re-extract would (saves an agent per wave).
362
+ if (cf === null && !prose && item.r && item.r.marker !== 'CODEX_NOT_FOUND') {
363
+ // Re-extract deterministically from the on-disk file before conceding —
364
+ // grabbing BOTH the sentinel JSON and the prose (Codex usually writes prose, not the block). This
365
+ // makes prose capture JS-owned: a relay that returned empty proseTail can no longer strand the
366
+ // cold fallback unseeded (the v4.56.2 failure: empty codexProse → BLOCKER missed by cold review).
367
+ const re = await agent(reExtractPrompt(codexReviewFile), { label: 'codex-reextract', phase: 'Discovery', model: 'sonnet', schema: EXTRACT_SCHEMA })
368
+ cf = parseFindingsArray(re && re.jsonOut)
369
+ if (cf) log('Discovery: Codex JSON recovered by deterministic re-extraction.')
370
+ if (!cf && re && typeof re.proseOut === 'string' && re.proseOut.trim()) prose = re.proseOut.trim()
371
+ }
372
+ if (Array.isArray(cf)) { // parsed (incl. empty []) → Codex ran
333
373
  codexRan = true
334
- raw.push(...item.r.findings.map((f) => ({ ...f, source: 'codex', preValidated: true })))
335
- } else {
336
- if (item.r && typeof item.r.codexProse === 'string' && item.r.codexProse.trim()) codexProse = item.r.codexProse.trim()
337
- log(`Discovery: Codex companion resolved but its review did not complete${codexProse ? ' (prose-only — salvaging its findings as fallback leads)' : ''} — falling back to code-reviewer.`)
374
+ raw.push(...cf.map((f) => ({ ...f, source: 'codex', preValidated: true })))
375
+ } else { // no JSON → seed the cold fallback with the prose leads
376
+ if (prose) codexProse = prose
377
+ log(`Discovery: Codex relay returned no parseable JSON (marker=${item.r && item.r.marker})${codexProse ? ' — salvaging its prose as fallback leads' : ''} — falling back to code-reviewer.`)
338
378
  }
339
379
  } else if (item.kind === 'qa') {
340
380
  gateTable = (item.r && item.r.gates) || []
@@ -80,15 +80,42 @@ const FINDINGS_SCHEMA = {
80
80
  type: 'object', required: ['findings'], additionalProperties: false,
81
81
  properties: { findings: { type: 'array', items: FINDING }, note: { type: 'string' } },
82
82
  }
83
+ // Haiku-proof relay contract (v4.56.3): the relay returns RAW command outputs — it does NOT decide
84
+ // "codexAvailable" and does NOT parse findings. The JS fan-in owns the decision (parses jsonBlock
85
+ // deterministically), so a weak/runtime-downgraded relay model can no longer drop a finding by
86
+ // mis-judging availability — the bytes are on disk and the JS reads them. See the fan-in branch.
83
87
  const CODEX_SCHEMA = {
84
- type: 'object', required: ['codexAvailable', 'findings'], additionalProperties: false,
88
+ type: 'object', required: ['marker', 'jsonBlock'], additionalProperties: false,
85
89
  properties: {
86
- codexAvailable: { type: 'boolean', description: 'false if CODEX_NOT_FOUND / TIMED_OUT' },
87
- findings: { type: 'array', items: FINDING },
88
- codexProse: { type: 'string', description: 'PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted no sentinel JSON block, the cleaned tail of its prose review handed to the fallback code-reviewer as UNVERIFIED leads. Empty/omitted otherwise.' },
90
+ marker: { type: 'string', enum: ['END_JSON', 'TURN_COMPLETED', 'CODEX_NOT_FOUND', 'TIMEOUT'], description: 'which terminator ended the poll' },
91
+ jsonBlock: { type: 'string', description: 'VERBATIM stdout of the awk extraction command (may be ""). The JS parses it — do NOT edit/judge.' },
92
+ proseTail: { type: 'string', description: 'VERBATIM stdout of the prose-tail command, ONLY when marker=TURN_COMPLETED with an empty jsonBlock; else "".' },
89
93
  note: { type: 'string' },
90
94
  },
91
95
  }
96
+ // One-shot deterministic re-extraction over the on-disk file — independent of the relay's self-report.
97
+ // Codex OFTEN ignores "emit ONLY the sentinel block" and writes a PROSE report ending in "Turn
98
+ // completed." (verified on real companion output), so there's frequently NO JSON to extract and the
99
+ // findings live in the prose. We therefore fetch BOTH: the sentinel JSON (awk) AND the prose tail.
100
+ const EXTRACT_SCHEMA = {
101
+ type: 'object', required: ['jsonOut', 'proseOut'], additionalProperties: false,
102
+ properties: {
103
+ jsonOut: { type: 'string', description: 'verbatim stdout of the awk sentinel-extraction command, "" if none' },
104
+ proseOut: { type: 'string', description: 'verbatim stdout of the [codex]-stripped prose tail, "" if none' },
105
+ },
106
+ }
107
+ // Deterministic parse: null = no recoverable JSON array; [] = Codex ran clean (no findings); [..] = findings.
108
+ const parseFindingsArray = (s) => {
109
+ if (typeof s !== 'string' || !s.trim()) return null
110
+ try { const v = JSON.parse(s.trim()); return Array.isArray(v) ? v : null } catch { return null }
111
+ }
112
+ // SSOT for the sentinel-extraction awk — used by BOTH the relay prompt and the re-extract agent.
113
+ 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}'`
114
+ const reExtractPrompt = (file) =>
115
+ `Run EXACTLY these TWO Bash commands and return their stdout VERBATIM. Run NOTHING else — do NOT Read the file beyond these commands, do NOT review code, do NOT reason about findings:\n` +
116
+ ` • jsonOut = stdout of: ${AWK_EXTRACT} ${file}\n` +
117
+ ` • proseOut = stdout of: grep -v '^\\[codex\\]' ${file} | tail -c 12000\n` +
118
+ `Return { jsonOut, proseOut } and nothing else (empty string for either command that printed nothing).`
92
119
  const GATES_SCHEMA = {
93
120
  type: 'object', required: ['gates'], additionalProperties: false,
94
121
  properties: {
@@ -176,12 +203,14 @@ log(codexResolved
176
203
  ? `Codex companion resolved deterministically: ${codexScriptPath}`
177
204
  : 'Codex companion NOT found (deterministic pre-flight) — primary review via code-reviewer fallback.')
178
205
 
179
- // Codex companion review. The WRAPPER is a thin relay (model: sonnet see the thunk): it launches
180
- // Codex, waits, and extracts Codex's JSON findings with a deterministic awk; it does NOT review or
181
- // re-verify code itself (Codex's findings are authoritative + FP-validated). Codex emits findings
182
- // between explicit sentinels so the extraction is unambiguous despite the companion's `[codex]` trace
183
- // and the truncated "Assistant message captured:" echo. Sonnet (not haiku) here because the Final is
184
- // the last cross-model gate before merge no downstream Codex pass backstops a wrapper misfire.
206
+ // Codex companion review. The relay LAUNCHES Codex, polls, and returns the RAW stdout of the
207
+ // extraction commands it does NOT review code, does NOT re-grep, and (since v4.56.3) does NOT
208
+ // decide "did Codex succeed": the JS fan-in parses jsonBlock itself. The relay model is NOT
209
+ // guaranteeable `model:'sonnet'` is a request the runtime silently downgrades to Haiku under
210
+ // capacity pressure — and the Final is the last cross-model gate before merge, so a dropped finding
211
+ // here is unrecoverable. Moving the success decision out of the model (+ a one-shot deterministic
212
+ // re-extract) makes the relay robust to WHATEVER model it lands on. Codex emits findings between
213
+ // explicit sentinels so the awk is unambiguous despite the `[codex]` trace + truncated echo.
185
214
  const codexReviewTask =
186
215
  `Run a deep code review over this batch diff, per the /codexreview protocol in ${protocolRef} (Step F.3). The code is already written and committed — find bugs, regressions, security issues, and quality problems.\n\n` +
187
216
  `${scopeBrief}\n\n${baselineBrief}\n\n` +
@@ -198,24 +227,22 @@ const codexReviewTask =
198
227
  const codexTaskFile = `/tmp/codextask-batch-${a.firstCardId || 'batch'}.txt`
199
228
  const codexReviewFile = `/tmp/codexreview-batch-${a.firstCardId || 'batch'}.md`
200
229
  const codexPrompt =
201
- `You are a THIN RELAY around the Codex companion — do NOT review or investigate code yourself, do NOT grep/sed/Read source to "confirm" findings (Codex's findings are authoritative and already FP-validated). Your ONLY job: launch Codex, wait, extract its JSON, return it.\n\n` +
230
+ `You DRIVE the Codex companion and return its RAW command outputs you do NOT review or investigate code, do NOT grep/sed/Read source, and you do NOT decide whether Codex "succeeded" (the workflow decides that from the bytes you return). Launch Codex, poll, run the extraction commands, return their VERBATIM stdout.\n\n` +
202
231
  `The companion script is ALREADY CONFIRMED PRESENT at:\n ${codexScriptPath}\n\n` +
203
232
  `Run EXACTLY these steps and nothing else:\n` +
204
233
  ` 1. Write the task text (everything after "TASK PROMPT:" at the end of this message) to ${codexTaskFile} using the Write tool — do NOT inline it into the shell command (avoids quote breakage).\n` +
205
234
  ` 2. Launch Codex in the BACKGROUND (run_in_background:true — a sync run hits the Bash timeout):\n` +
206
235
  ` node "${codexScriptPath}" task "$(cat ${codexTaskFile})" --cwd "${a.worktreePath || '.'}" > ${codexReviewFile} 2>&1\n` +
207
- ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until it contains ANY of these — whichever appears FIRST ends the poll (do NOT keep waiting the full window once one is present):\n` +
208
- ` • "<<<END_FINDINGS_JSON>>>" → Codex emitted the JSON block (success path)\n` +
209
- ` • "Turn completed." → the companion's reliable end-of-run marker — Codex FINISHED even if it wrote prose instead of the block\n` +
210
- ` • "CODEX_NOT_FOUND" → companion missing\n` +
211
- ` • the full 10-minute window has elapsed\n` +
212
- ` 4. Extract the findings with this EXACT command (deterministic skips the [codex] trace + truncated echo, takes the LAST complete sentinel pair):\n` +
213
- ` awk '/<<<FINDINGS_JSON>>>/{c=1;b="";next}/<<<END_FINDINGS_JSON>>>/{c=0;last=b;next}c{b=b $0 ORS}END{printf "%s",last}' ${codexReviewFile}\n` +
214
- ` 5. Decide the return do NOT re-verify, re-grep, or Read any source file:\n` +
215
- ` awk output is a NON-EMPTY JSON array parse it and return it verbatim as \`findings\`, codexAvailable:true (its keys already match the schema).\n` +
216
- ` • awk output is EMPTY but the file has "Turn completed." Codex FINISHED but wrote a prose report instead of the sentinel block. Do NOT parse findings out of the prose yourself (never guess a schema). Instead, capture the prose for the fallback to verify: run \`grep -v '^\\[codex\\]' ${codexReviewFile} | tail -c 12000\` and return codexAvailable:false, findings:[], codexProse:<that output>, then EXIT NOW (do NOT wait the rest of the window). The fallback code-reviewer re-verifies the prose against the real diff — so a real finding is recovered, not dropped.\n` +
217
- ` • awk output is EMPTY and the file has "CODEX_NOT_FOUND" → companion missing. Return codexAvailable:false, findings:[] (no codexProse).\n` +
218
- ` • none of the markers ever appeared after the FULL 10-minute window → codexAvailable:false, findings:[] (no codexProse). NEVER set false because a single poll returned slowly.\n\n` +
236
+ ` 3. POLL ${codexReviewFile} (BashOutput / repeated reads) until ONE of these appears — whichever is FIRST ends the poll (do NOT keep waiting once one is present). Set \`marker\` accordingly:\n` +
237
+ ` • "<<<END_FINDINGS_JSON>>>" → marker = "END_JSON"\n` +
238
+ ` • "Turn completed." → marker = "TURN_COMPLETED" (Codex finished; it may have written prose instead of the JSON block)\n` +
239
+ ` • "CODEX_NOT_FOUND" → marker = "CODEX_NOT_FOUND" (companion missing)\n` +
240
+ ` • the full 10-minute window elapses with none of the above → marker = "TIMEOUT" (NEVER pick this just because a single poll returned slowly)\n` +
241
+ ` 4. Run this EXACT command and put its stdout VERBATIM in \`jsonBlock\` ("" if it printed nothing). Do NOT parse, edit, judge, or act on it:\n` +
242
+ ` ${AWK_EXTRACT} ${codexReviewFile}\n` +
243
+ ` 5. ONLY if \`jsonBlock\` is "" AND marker is "TURN_COMPLETED": run this command and put its stdout VERBATIM in \`proseTail\`; otherwise set proseTail to "":\n` +
244
+ ` grep -v '^\\[codex\\]' ${codexReviewFile} | tail -c 12000\n` +
245
+ ` 6. Return { marker, jsonBlock, proseTail } and NOTHING else. Do NOT add a findings array, do NOT set an availability flag, do NOT re-run Codex, do NOT Read source. The workflow parses jsonBlock itself.\n\n` +
219
246
  `TASK PROMPT:\n${codexReviewTask}`
220
247
 
221
248
  const docPrompt =
@@ -258,8 +285,10 @@ if (!slimDoc) {
258
285
  }
259
286
  // Codex thunk runs ONLY when the pre-flight resolved the companion (else: no wasted agent).
260
287
  if (codexResolved) {
261
- // model: sonnet thin relay (launch + poll + awk-extract), no self-investigation; sonnet (not haiku)
262
- // because this is the last cross-model gate before merge (no downstream Codex pass backstops a misfire).
288
+ // model: 'sonnet' is BEST-EFFORT (v4.56.3) the runtime silently downgrades it to Haiku under load,
289
+ // so correctness can't depend on the tier. The relay returns RAW outputs and the JS fan-in owns the
290
+ // success decision (+ a one-shot deterministic re-extract), so a downgraded relay can't drop a finding.
291
+ // We still REQUEST sonnet: this is the last cross-model gate before merge and a dropped find is final.
263
292
  reviewThunks.unshift(() => agent(codexPrompt, { label: 'codex', phase: 'Review', model: 'sonnet', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', r })))
264
293
  }
265
294
  // api-perf-cost-auditor: skipped when no API/data files OR when it already ran per-card (slimApi).
@@ -281,13 +310,28 @@ let codexRan = false
281
310
  let codexProse = '' // PROSE-ONLY salvage (v4.56.0): seeds the fallback so Codex's real finds aren't lost.
282
311
  for (const item of reviewResults) {
283
312
  if (item.kind === 'codex') {
284
- if (item.r && item.r.codexAvailable && Array.isArray(item.r.findings)) {
313
+ // JS owns the success decision (v4.56.3) — never the relay model. Parse the raw jsonBlock:
314
+ // null = no recoverable JSON; [] = Codex ran clean; [..] = findings.
315
+ let cf = parseFindingsArray(item.r && item.r.jsonBlock)
316
+ let prose = item.r && typeof item.r.proseTail === 'string' ? item.r.proseTail.trim() : ''
317
+ // Re-extract ONLY when the relay gave neither JSON nor prose (the v4.56.2 failure). When it already
318
+ // returned prose, trust it — it read the same bytes the re-extract would (saves an agent per batch).
319
+ if (cf === null && !prose && item.r && item.r.marker !== 'CODEX_NOT_FOUND') {
320
+ // Re-extract deterministically from the on-disk file before conceding —
321
+ // grabbing BOTH the sentinel JSON and the prose (Codex usually writes prose, not the block). This
322
+ // makes prose capture JS-owned: a relay that returned empty proseTail can no longer strand the
323
+ // cold fallback unseeded (the v4.56.2 failure: empty codexProse → BLOCKER missed by cold review).
324
+ const re = await agent(reExtractPrompt(codexReviewFile), { label: 'codex-reextract', phase: 'Review', model: 'sonnet', schema: EXTRACT_SCHEMA })
325
+ cf = parseFindingsArray(re && re.jsonOut)
326
+ if (cf) log('Review: Codex JSON recovered by deterministic re-extraction.')
327
+ if (!cf && re && typeof re.proseOut === 'string' && re.proseOut.trim()) prose = re.proseOut.trim()
328
+ }
329
+ if (Array.isArray(cf)) { // parsed (incl. empty []) → Codex ran
285
330
  codexRan = true
286
- raw.push(...item.r.findings.map((f) => ({ ...f, source: 'codex', preValidated: true })))
287
- } else {
288
- // Resolved but the run itself failed (CODEX_NOT_FOUND written / empty / prose-only after timeout).
289
- if (item.r && typeof item.r.codexProse === 'string' && item.r.codexProse.trim()) codexProse = item.r.codexProse.trim()
290
- log(`Review: Codex companion resolved but its review did not complete${codexProse ? ' (prose-only — salvaging its findings as fallback leads)' : ''} — falling back to code-reviewer.`)
331
+ raw.push(...cf.map((f) => ({ ...f, source: 'codex', preValidated: true })))
332
+ } else { // no JSON → seed the cold fallback with the prose leads
333
+ if (prose) codexProse = prose
334
+ log(`Review: Codex relay returned no parseable JSON (marker=${item.r && item.r.marker})${codexProse ? ' — salvaging its prose as fallback leads' : ''} falling back to code-reviewer.`)
291
335
  }
292
336
  } else if (item.kind === 'qa') {
293
337
  gateTable = (item.r && item.r.gates) || []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.56.2",
3
+ "version": "4.56.3",
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"