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.
|
|
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: ['
|
|
117
|
+
type: 'object', required: ['marker', 'jsonBlock'], additionalProperties: false,
|
|
114
118
|
properties: {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
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
|
|
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
|
|
254
|
-
` • "<<<END_FINDINGS_JSON>>>" →
|
|
255
|
-
` • "Turn completed." →
|
|
256
|
-
` • "CODEX_NOT_FOUND" → companion missing\n` +
|
|
257
|
-
` • the full 10-minute window
|
|
258
|
-
` 4.
|
|
259
|
-
`
|
|
260
|
-
` 5.
|
|
261
|
-
`
|
|
262
|
-
`
|
|
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
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
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
|
-
|
|
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(...
|
|
335
|
-
} else {
|
|
336
|
-
if (
|
|
337
|
-
log(`Discovery: Codex
|
|
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: ['
|
|
88
|
+
type: 'object', required: ['marker', 'jsonBlock'], additionalProperties: false,
|
|
85
89
|
properties: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// and the
|
|
184
|
-
//
|
|
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
|
|
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
|
|
208
|
-
` • "<<<END_FINDINGS_JSON>>>" →
|
|
209
|
-
` • "Turn completed." →
|
|
210
|
-
` • "CODEX_NOT_FOUND" → companion missing\n` +
|
|
211
|
-
` • the full 10-minute window
|
|
212
|
-
` 4.
|
|
213
|
-
`
|
|
214
|
-
` 5.
|
|
215
|
-
`
|
|
216
|
-
`
|
|
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
|
|
262
|
-
//
|
|
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
|
-
|
|
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(...
|
|
287
|
-
} else {
|
|
288
|
-
|
|
289
|
-
|
|
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) || []
|