clearotron 0.4.0 → 0.4.1-beta.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.
Files changed (132) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/bin/README.md +1 -1
  3. package/bin/clearotron.mjs +2 -0
  4. package/bin/mcp.mjs +80 -0
  5. package/bin/start.mjs +14 -1
  6. package/build-info.json +2 -2
  7. package/demo/README.md +1 -1
  8. package/docs/SECURITY-OWASP.md +21 -13
  9. package/docs/SECURITY.md +20 -14
  10. package/docs/architecture/04-configuration-reference.md +3 -3
  11. package/docs/architecture/05-config-governance.md +1 -1
  12. package/docs/architecture/06-operations-runbook.md +5 -4
  13. package/docs/architecture/08-development-guide.md +3 -1
  14. package/docs/architecture/09-security-and-data.md +16 -11
  15. package/driver/CHANGELOG.md +29 -0
  16. package/driver/binding-layers.mjs +28 -6
  17. package/driver/citation-census.json +3 -3
  18. package/driver/common-law-coverage-status.mjs +52 -0
  19. package/driver/common-law-receipts.mjs +1 -1
  20. package/driver/compare.mjs +1 -1
  21. package/driver/connotation-search.mjs +5 -5
  22. package/driver/contract-audit.mjs +6 -6
  23. package/driver/contract-dictation-registry.mjs +1 -1
  24. package/driver/contract-e3-backlog.mjs +18 -17
  25. package/driver/contract-vocabulary.mjs +145 -132
  26. package/driver/declination-call.mjs +5 -1
  27. package/driver/degraded-parts.mjs +10 -3
  28. package/driver/dev-portal.mjs +2 -1
  29. package/driver/dispatch-record.mjs +19 -0
  30. package/driver/doubt-closure-tool.mjs +2 -2
  31. package/driver/driver.config.mjs +39 -6
  32. package/driver/engagement-receipt.mjs +1 -1
  33. package/driver/engine/CONTRACT.md +41 -1
  34. package/driver/engine/anthropic-agent.mjs +56 -4
  35. package/driver/engine/mcp/codex-config.mjs +8 -3
  36. package/driver/engine/mcp/coverage-server.mjs +1 -1
  37. package/driver/engine/mcp/gather-config.mjs +1 -1
  38. package/driver/engine/mcp/recording-server.mjs +32 -5
  39. package/driver/engine/mcp/stdio-server.mjs +45 -7
  40. package/driver/engine/mcp/supplemental.mjs +26 -1
  41. package/driver/engine/openai-agent.mjs +113 -20
  42. package/driver/engine/session-record.mjs +223 -0
  43. package/driver/enqueue-schema.mjs +2 -2
  44. package/driver/findings-model.mjs +4 -4
  45. package/driver/flag-snapshot.mjs +2 -1
  46. package/driver/form-neighbourhood.mjs +7 -7
  47. package/driver/frame-diff-model.mjs +3 -3
  48. package/driver/framework.mjs +22 -0
  49. package/driver/gateway.mjs +96 -14
  50. package/driver/jx.mjs +1 -1
  51. package/driver/knockout-review-record.mjs +55 -1
  52. package/driver/named-band.mjs +4 -0
  53. package/driver/owner-screen.mjs +7 -4
  54. package/driver/package.json +1 -1
  55. package/driver/phase0.mjs +13 -2
  56. package/driver/pipeline-knockout.mjs +15 -13
  57. package/driver/pipeline.mjs +132 -48
  58. package/driver/portal-families.mjs +3 -3
  59. package/driver/portal-service.mjs +6 -5
  60. package/driver/predelivery-lint.mjs +19 -19
  61. package/driver/products.mjs +1 -1
  62. package/driver/profiles.mjs +4 -3
  63. package/driver/publish/index.mjs +1 -1
  64. package/driver/publish/knockout.mjs +5 -4
  65. package/driver/publish/office-record-links.mjs +17 -2
  66. package/driver/publish/publish-inputs.mjs +3 -3
  67. package/driver/publish/render-knockout.mjs +17 -5
  68. package/driver/publish/render.mjs +4 -2
  69. package/driver/publish/xlsx.mjs +1 -1
  70. package/driver/record-fold.mjs +177 -0
  71. package/driver/register-count.mjs +20 -3
  72. package/driver/register-coverage.mjs +1 -1
  73. package/driver/register-plan.mjs +83 -18
  74. package/driver/register-records.mjs +7 -0
  75. package/driver/registry-fidelity.mjs +2 -2
  76. package/driver/repairs.mjs +1 -1
  77. package/driver/report-card-record.mjs +4 -4
  78. package/driver/result-noun-fields.mjs +3 -3
  79. package/driver/scope-facts.mjs +11 -3
  80. package/driver/scope-rules.mjs +2 -2
  81. package/driver/search-policy.mjs +20 -1
  82. package/driver/skills/clearance-common-law/SKILL.md +22 -22
  83. package/driver/skills/clearance-register/digest.md +3 -3
  84. package/driver/skills/clearance-register/register-recipes.md +11 -11
  85. package/driver/skills/clearance-register/status-rules.md +1 -1
  86. package/driver/skills/clearance-register/unit.md +1 -1
  87. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
  88. package/driver/skills/clearance-search/phase2-execution.md +6 -5
  89. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  90. package/driver/skills/clearance-search/worked-examples.md +39 -39
  91. package/driver/skills/clearance-variants/SKILL.md +16 -16
  92. package/driver/skills/frame-diff/SKILL.md +2 -2
  93. package/driver/stage-context.mjs +36 -1
  94. package/driver/stages-knockout.mjs +6 -5
  95. package/driver/stages.mjs +30 -30
  96. package/driver/suite-census.json +259 -55
  97. package/driver/synthesis-record.mjs +5 -0
  98. package/driver/unit-inventory.mjs +17 -28
  99. package/driver/verify.mjs +7 -7
  100. package/mcp-server/CHANGELOG.md +8 -0
  101. package/mcp-server/CONNECT.md +9 -3
  102. package/mcp-server/demo-mode.mjs +10 -0
  103. package/mcp-server/lib/evidence.mjs +8 -8
  104. package/mcp-server/lib/knockout.mjs +1 -1
  105. package/mcp-server/lib/runs.mjs +7 -1
  106. package/mcp-server/lib/scrub.mjs +1 -1
  107. package/mcp-server/package.json +2 -2
  108. package/mcp-server/packs/README.md +1 -1
  109. package/mcp-server/server.mjs +16 -2
  110. package/package.json +4 -3
  111. package/portal-ui/package.json +3 -3
  112. package/providers/_shared/count.mjs +9 -2
  113. package/providers/_shared/enumerate.mjs +125 -41
  114. package/providers/_shared/execute-plan.mjs +15 -4
  115. package/providers/corsearch/src/capabilities.js +1 -1
  116. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  117. package/providers/oauth-mcp-bridge/package.json +2 -2
  118. package/providers/signa/src/capabilities.js +32 -2
  119. package/providers/signa/src/core.js +262 -36
  120. package/scripts/band-shape-probe.mjs +2 -2
  121. package/scripts/deprecate-below.mjs +28 -4
  122. package/scripts/e2e-first-time.mjs +17 -2
  123. package/scripts/env-audit.mjs +3 -2
  124. package/scripts/freeze-example-run.mjs +10 -10
  125. package/scripts/mcp-registry-listing.mjs +199 -0
  126. package/scripts/mint-plugin-folder.mjs +65 -0
  127. package/scripts/release-version.mjs +44 -0
  128. package/scripts/score.mjs +1 -1
  129. package/shared/register-floor.mjs +19 -0
  130. package/shared/scope.mjs +18 -2
  131. package/shared/trigger-lane.mjs +1 -1
  132. package/skills/clearotron-ops/SKILL.md +81 -24
@@ -57,6 +57,31 @@ const compactProposal = (p) => ({
57
57
  const inPriority = (p, prio) =>
58
58
  (Array.isArray(p?.nice_classes) ? p.nice_classes : []).some((c) => prio.has(String(c).trim()));
59
59
 
60
+ // A SOUND-ALIKE BATCH GOES AS LISTS where the register takes one (`phoneticOrWidth`, ruled 2026-10-09): up to
61
+ // that many spellings per question, within its word budget, each long enough for the ranked search. A
62
+ // spelling below that floor is its own question, as every name of a batch was before. Each list is one
63
+ // question with one receipt row, so a list that crowds or fails takes no other list down with it. PURE.
64
+ function splitBatch(p, names, capabilities) {
65
+ const { terms: _batch, ...rest } = p;
66
+ const single = (t) => ({ ...rest, term: t });
67
+ const width = Math.floor(Number(capabilities?.phoneticOrWidth));
68
+ if (String(p?.predicate ?? "") !== "phonetic" || !(width >= 2)) return names.map(single);
69
+ const floor = Number(capabilities?.rankedMinLength) || 0;
70
+ const budget = Number(capabilities?.listWordBudget) || Infinity;
71
+ const words = (t) => t.split(/\s+/).filter(Boolean).length;
72
+ const folded = (t) => [...t.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/\s+/g, "")].length;
73
+ const out = [];
74
+ let list = [];
75
+ const close = () => { if (list.length) out.push(list.length === 1 ? single(list[0]) : { ...rest, terms: list }); list = []; };
76
+ for (const t of names) {
77
+ if (folded(t) < floor) { out.push(single(t)); continue; }
78
+ if (list.length >= width || list.reduce((n, x) => n + words(x), 0) + words(t) > budget) close();
79
+ list.push(t);
80
+ }
81
+ close();
82
+ return out;
83
+ }
84
+
60
85
  export function mintSupplementalEntries(axis, proposals, { existingQids = new Set(), perCall = Infinity, axisMax = Infinity, existingCount = 0, capabilities = null, priorityClasses = [] } = {}) {
61
86
  const minted = [], reused = [], rejected = [], enriched = [], narrowed = [];
62
87
  let budget = Math.max(0, axisMax - existingCount);
@@ -82,7 +107,7 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
82
107
  const names = perName && Array.isArray(p?.terms) ? p.terms.map((t) => String(t ?? "").trim()).filter(Boolean) : [];
83
108
  const splitOf = names.length > 1 && names.length <= PLAN_MAX_OR_WIDTH
84
109
  ? fingerprint({ axis, predicate: String(p?.predicate ?? "default"), names: [...names].sort(), i }) : null;
85
- const byName = splitOf ? names.map((t) => { const { terms: _batch, ...rest } = p; return { ...rest, term: t }; }) : [p ?? {}];
110
+ const byName = splitOf ? splitBatch(p, names, capabilities) : [p ?? {}];
86
111
  return byName.flatMap((q) => {
87
112
  const words = perWord && Array.isArray(q?.goods_words) ? q.goods_words.map((w) => String(w ?? "").trim()).filter(Boolean) : [];
88
113
  return words.length > 1
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { mkdtempSync, mkdirSync, writeFileSync, copyFileSync, existsSync, rmSync, readFileSync, readdirSync, statSync, symlinkSync, lstatSync, realpathSync } from "node:fs";
26
26
  import { everyToolCallRefused } from "./tool-refusal.mjs";
27
+ import { newSessionRecord, noteCodexEvent, sessionSummary, countTool, streamSink, codexEnding } from "./session-record.mjs"; // what the session went through, beside how it ended
27
28
  import { writeSecretFile } from "../../shared/secret-file.mjs"; // the rotated login goes back the way every credential is written
28
29
  import { tmpdir, homedir } from "node:os";
29
30
  import { join, dirname, sep } from "node:path";
@@ -38,11 +39,28 @@ import { resolveEngineProgram } from "../driver.config.mjs"; // — the one pl
38
39
  const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return r.resolved ?? r.bin; };
39
40
 
40
41
  // tier/alias → codex `-m` model id. opus/sonnet/haiku are the driver's abstract tiers (CONTRACT §3). The
41
- // GPT ids are ENV-OVERRIDABLE and default to three DISTINCT rungs of the codex ladder — `gpt-5.6-sol`,
42
- // `gpt-5.6-terra`, `gpt-5.6-luna`, all present in the codex CLI as installed — so an out-of-the-box run
42
+ // GPT ids are ENV-OVERRIDABLE and default to the vendor's current models — `gpt-6.1-sol` for the judgment
43
+ // and sweep tiers, `gpt-6-luna` for the cheap tier (the GPT-6 ladder, below) — so an out-of-the-box run
43
44
  // uses REAL, current ids rather than invented guesses, and a stage's declared tier means the same thing
44
- // on both engines. `sol` remains what codex itself reports as its own default (live-probed 2026-07-27:
45
- // `model: gpt-5.6-sol, provider: openai`), which is why the judgment tier keeps it.
45
+ // on both engines. Until 2026-10-09 they were `gpt-5.6-sol`, `gpt-5.6-terra` and `gpt-5.6-luna`; `sol`
46
+ // was what codex itself reported as its own default (live-probed 2026-07-27: `model: gpt-5.6-sol,
47
+ // provider: openai`), which is why the judgment tier kept it.
48
+ //
49
+ // ── THE GPT-6 LADDER (ruling 2026-10-09) ────────────────────────────────────────────────────────────────
50
+ //
51
+ // The owner asked for the latest models on both engines, cheaper and faster. The vendor's price list,
52
+ // read 2026-10-09 (standard tier, per million tokens in / out): `gpt-6.1-sol` $2 / $10, `gpt-6-luna`
53
+ // $0.10 / $0.50, `gpt-6-astra` $10 / $50. The 5.6 ladder it replaces was sol $4 / $20, terra $2 / $12,
54
+ // luna $0.20 / $1.20. There is no GPT-6 middle model and Sol 6.1 costs less than Terra 5.6, so the sweep
55
+ // tier takes Sol as well; Astra is the premium model, neither cheaper nor faster, and is not a rung.
56
+ //
57
+ // THE CODEX FLOOR MOVED WITH IT. On ChatGPT-account sign-in (subscription billing) codex 0.156.1, 0.157.0
58
+ // and 0.158.0 refuse `gpt-6.1-sol` with a 400 — "not supported when using Codex with a ChatGPT account" —
59
+ // and 0.159.0 to 0.162.0 accept it; `gpt-6-luna` is accepted from 0.156.1. Measured 2026-10-09 with
60
+ // one-word turns on the test box. Below 0.159.0 the judgment tier stops at its first stage, so the floor
61
+ // in ENGINE_BINARIES (driver.config.mjs) is 0.159.0.
62
+ //
63
+ // The August measurement below still names the symptom to watch for when a tier moves to a cheaper model.
46
64
  //
47
65
  // ── THE THREE TIERS MAP ONTO THE CODEX LADDER (ruling 2026-09-02,) ──────────────────────────
48
66
  //
@@ -89,9 +107,9 @@ const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return
89
107
  // runs SOMETHING is worse than one that refuses: the arm still produces a number and the number is a
90
108
  // story. They fall through to the throw below, which is where they always should have been. Concrete
91
109
  // gpt-*/o* ids still pass through.
92
- const JUDGMENT = process.env.CLEAROTRON_OPENAI_MODEL_JUDGMENT || "gpt-5.6-sol"; // opus — unchanged
93
- const SWEEP = process.env.CLEAROTRON_OPENAI_MODEL_SWEEP || "gpt-5.6-terra"; // sonnet — was sol
94
- const CHEAP = process.env.CLEAROTRON_OPENAI_MODEL_CHEAP || "gpt-5.6-luna"; // haiku — was sol
110
+ const JUDGMENT = process.env.CLEAROTRON_OPENAI_MODEL_JUDGMENT || "gpt-6.1-sol"; // opus — was gpt-5.6-sol
111
+ const SWEEP = process.env.CLEAROTRON_OPENAI_MODEL_SWEEP || "gpt-6.1-sol"; // sonnet — was gpt-5.6-terra
112
+ const CHEAP = process.env.CLEAROTRON_OPENAI_MODEL_CHEAP || "gpt-6-luna"; // haiku — was gpt-5.6-luna
95
113
  const OPENAI_MODEL = { opus: JUDGMENT, sonnet: SWEEP, haiku: CHEAP };
96
114
  export function openaiModel(model) {
97
115
  if (!model) return undefined;
@@ -311,8 +329,10 @@ export function parseCodexEvent(line, ev) {
311
329
  switch (e?.type) {
312
330
  case "thread.started": if (e.thread_id) ev.threadId = e.thread_id; break;
313
331
  case "turn.completed": ev.turnCompleted = true; if (e.usage) ev.usage = e.usage; break;
314
- case "turn.failed": ev.turnFailed = e.error?.message || "turn.failed"; break;
315
- case "error": ev.streamError = e.message || "stream error"; break;
332
+ // The program's own words are kept apart from the placeholders, so the record never shows a word of
333
+ // ours where it said nothing (session-record.mjs, codexEnding).
334
+ case "turn.failed": ev.turnFailed = e.error?.message || "turn.failed"; ev.turnFailedText = typeof e.error?.message === "string" ? e.error.message : null; break;
335
+ case "error": ev.streamError = e.message || "stream error"; ev.streamErrorText = typeof e.message === "string" ? e.message : null; break;
316
336
  case "item.completed":
317
337
  if (e.item?.type === "agent_message" && typeof e.item.text === "string") ev.agentText = e.item.text;
318
338
  // A FILE CHANGE CODEX ITSELF REPORTS AS FAILED. Measured on codex-cli 0.156.1 with its sandbox unable
@@ -330,6 +350,7 @@ export function parseCodexEvent(line, ev) {
330
350
  ev.commandFailures.push({ command: String(e.item.command ?? ""), output: String(e.item.aggregated_output ?? "") });
331
351
  }
332
352
  noteMcpToolCall(e.item, ev);
353
+ noteToolItem(e.item, ev);
333
354
  break;
334
355
  // ── — AN MCP CALL THAT WAS REFUSED IS NOT A CALL NOBODY MADE ──────────
335
356
  //
@@ -352,6 +373,7 @@ export function parseCodexEvent(line, ev) {
352
373
  // would double every call.
353
374
  case "item.started":
354
375
  noteMcpToolCall(e.item, ev);
376
+ noteToolItem(e.item, ev);
355
377
  break;
356
378
  default: break;
357
379
  }
@@ -377,6 +399,57 @@ export function noteMcpToolCall(item, ev) {
377
399
  });
378
400
  }
379
401
 
402
+ // The item kinds codex reports a tool call as, besides its MCP calls. Its own words, measured on
403
+ // codex-cli 0.150–0.158: a shell command, a file edit, a web search.
404
+ const TOOL_ITEM_KINDS = new Set(["command_execution", "file_change", "web_search"]);
405
+
406
+ /**
407
+ * Remember one tool-shaped item by id, for the per-name count. Codex writes each item twice (started,
408
+ * then its terminal state) under one id, so the id is the unit, exactly as for the MCP gauge.
409
+ */
410
+ export function noteToolItem(item, ev) {
411
+ if (!item?.id) return;
412
+ const name = item.type === "mcp_tool_call" ? `mcp__${item.server ?? "?"}__${item.tool ?? "?"}`
413
+ : TOOL_ITEM_KINDS.has(item.type) ? item.type : null;
414
+ if (!name) return;
415
+ (ev.toolItems ??= new Map()).set(item.id, name);
416
+ }
417
+
418
+ /**
419
+ * The turn's tool calls by name, in the shape anthropic-agent records: an MCP call under
420
+ * `mcp__<server>__<tool>`, and codex's own tool items under its own word for them. Counted once per item
421
+ * id. An OBJECT is a measurement (`{}` = none observed).
422
+ */
423
+ export function codexToolCallsByName(ev) {
424
+ const byName = Object.create(null);
425
+ for (const name of ev?.toolItems?.values() ?? []) countTool(byName, name);
426
+ return { ...byName };
427
+ }
428
+
429
+ /**
430
+ * The commands codex ran this turn: its `command_execution` items, counted once per item id, a failed one
431
+ * included, since a command that exited non-zero still ran. A NUMBER is a measurement (0: none ran). PURE.
432
+ */
433
+ export function codexCommandCalls(ev) {
434
+ let n = 0;
435
+ for (const name of ev?.toolItems?.values() ?? []) if (name === "command_execution") n += 1;
436
+ return n;
437
+ }
438
+
439
+ /**
440
+ * The tool-server calls of this turn that came back as an error, by tool name, in the shape
441
+ * anthropic-agent records: a call that reached its server and `failed` there, which codex writes with no
442
+ * `error.message` (a call codex refused itself carries one, and `toolCallsRefused` counts those). Counted
443
+ * once per item id. An OBJECT is a measurement (`{}`: none). PURE.
444
+ */
445
+ export function codexToolServerErrors(ev) {
446
+ const byName = Object.create(null);
447
+ for (const c of ev?.mcpCalls?.values() ?? []) {
448
+ if (c.status === "failed" && !c.message) countTool(byName, `mcp__${c.server ?? "?"}__${c.tool ?? "?"}`);
449
+ }
450
+ return { ...byName };
451
+ }
452
+
380
453
  /**
381
454
  * The MCP tool-call gauge for this turn: what completed, what was REFUSED, and why.
382
455
  *
@@ -540,11 +613,16 @@ function settleTuple({ r, ev, resumeRef }) {
540
613
  const failed = overflow || killed || !ev.turnCompleted || !!ev.turnFailed;
541
614
  const usage = mapUsage(ev.usage);
542
615
  const text = ev.agentText || "";
543
- const json = buildEnvelope({
544
- text, ok: !failed, killed, usage,
545
- summary: ev.turnFailed ? "failed" : (ev.turnCompleted ? "success" : undefined),
546
- runId: ev.threadId,
547
- });
616
+ const json = {
617
+ ...buildEnvelope({
618
+ text, ok: !failed, killed, usage,
619
+ summary: ev.turnFailed ? "failed" : (ev.turnCompleted ? "success" : undefined),
620
+ runId: ev.threadId,
621
+ }),
622
+ // What ended the turn, in the program's own words when it failed (session-record.mjs, codexEnding):
623
+ // the same three fields the Claude adapter writes. Recorded only; `ok` above is unchanged.
624
+ ...codexEnding(ev),
625
+ };
548
626
  const stderrOut = overflow
549
627
  ? (r.stderr + `\nopenai-agent output overflow: stdout/stderr exceeded ${r.maxBuffer} chars — killed the tree and failed the turn (truncated tail never parsed)`)
550
628
  : (stallKill ? (r.stderr + "\nrequest timed out (openai-agent stall-watchdog: 0 streamed tokens)") : r.stderr);
@@ -585,10 +663,14 @@ function settleTuple({ r, ev, resumeRef }) {
585
663
  // still cannot report a whole-turn tool count, so `toolCalls` stays null, which is the house rule
586
664
  // for "this engine does not report" rather than "it called nothing".
587
665
  ...mcpToolGauge(ev),
588
- // The refused count under the name the attempt row reads on both engines. Codex keeps its shell, and this
589
- // adapter reads its commands only for failures (below), so it reports no command-tool count: null, never zero.
666
+ // The refused count under the name the attempt row reads on both engines. Codex keeps its shell, and every
667
+ // command it runs is a `command_execution` item: counted once per item id, failed ones included.
590
668
  toolCallsRefused: mcpToolGauge(ev).mcpToolCallsRefused,
591
- commandToolCalls: null,
669
+ commandToolCalls: codexCommandCalls(ev),
670
+ // The per-name split of the calls codex reports (see codexToolCallsByName), what the session went
671
+ // through (session-record.mjs: every turn ending and stream error, in order), and the raw stream.
672
+ toolCallsByName: codexToolCallsByName(ev), toolCallsErroredByName: codexToolServerErrors(ev),
673
+ session: sessionSummary(ev.session), stream: ev.stream ?? null,
592
674
  writesFailed: ev.writesFailed ?? 0,
593
675
  commandsFailed: ev.commandsFailed ?? 0,
594
676
  commandFailures: ev.commandFailures ?? [],
@@ -624,6 +706,8 @@ function errResult(t0, e, resumeRef) {
624
706
  code: 1, killed: false, wall: (Date.now() - t0) / 1000, stdout: "",
625
707
  stderr: `openai-agent error: ${e?.message ?? e}`, laneWaitMs: 0,
626
708
  json: null, usage: null, modelWire: null, sessionRef: resumeRef ?? null,
709
+ // No session ran: nothing to have been interrupted, no stream.
710
+ toolCallsByName: null, toolCallsErroredByName: null, session: null, stream: null,
627
711
  };
628
712
  }
629
713
 
@@ -652,7 +736,7 @@ export const openaiAgentEngine = {
652
736
  // `thread/resume failed: no rollout found for thread id ... (code -32600)`, exit 1 — with the control
653
737
  // (same id, same binary, its own home) resuming cleanly. So every warm-patch retry and every
654
738
  // form-repair sub-turn on the codex arm has been failing.
655
- async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, stallSec, codexHome: providedHome } = {}) {
739
+ async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, stallSec, codexHome: providedHome, streamFile = null } = {}) {
656
740
  if (!message) throw new Error("openai-agent.runTurn: message is required");
657
741
  const t0 = Date.now();
658
742
  let codexHome;
@@ -703,16 +787,25 @@ export const openaiAgentEngine = {
703
787
  // Stamped HERE, not at function entry: everything above is file writes that can take a moment, and
704
788
  // a floor set too early would let a previous turn's rollout back in on a shared home.
705
789
  const spawnedAtMs = Date.now();
706
- const ev = { threadId: null, usage: null, agentText: "", turnCompleted: false, turnFailed: null, streamError: null, model: null, mcpCalls: new Map() };
790
+ const ev = { threadId: null, usage: null, agentText: "", turnCompleted: false, turnFailed: null, streamError: null, model: null, mcpCalls: new Map(),
791
+ session: newSessionRecord() };
792
+ // The raw stream, one file per session beside its dispatch; every line codex wrote on stdout.
793
+ const sink = streamSink(streamFile);
707
794
  const r = await runStreamingChild({
708
795
  bin: codexBin(), args, input, runDir,
709
796
  // — the run dir, not a shared tmpdir. codex makes cwd a workspace root, writable under the
710
797
  // stage's profile, so this tightens the writable surface onto the run rather than widening it.
711
798
  cwd: resolveSpawnCwd({ cwd, runDir }),
712
799
  env, stallSec, timeoutSec,
713
- onStdoutLine: (line) => parseCodexEvent(line, ev),
800
+ onStdoutLine: (line) => {
801
+ sink.write(`${line}\n`);
802
+ parseCodexEvent(line, ev);
803
+ let e; try { e = JSON.parse(line); } catch { return; }
804
+ noteCodexEvent(ev.session, e, Date.now() - t0);
805
+ },
714
806
  stderrIsLiveness: true, // codex streams progress on STDERR → it is liveness for the stall watchdog
715
807
  });
808
+ ev.stream = sink.close();
716
809
  if (r.spawnError) return errResult(t0, r.spawnError, resumeRef);
717
810
  // The stream said no model (it never does — see readServedModel); the rollout under THIS run's
718
811
  // CODEX_HOME did. Read here, inside the try, because the finally below deletes that dir.
@@ -0,0 +1,223 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ // engine/session-record.mjs — what a model session went through, kept beside what it ended with.
4
+ //
5
+ // ── why this exists ──────────────────────────────────────────────────────────────────────────────────
6
+ //
7
+ // Both engines read the program's stream in memory and kept the LAST result. A session the vendor's
8
+ // classifier cut off mid-answer, or one the API refused and a restart recovered, ended in an ordinary
9
+ // successful result and was recorded as a clean session. A cut drops the tool calls that had not
10
+ // finished, so a stage that lost calls mid-answer looked exactly like one that finished.
11
+ //
12
+ // Measured on the bench's saved-data replays (2026-09-28, 50 Claude sessions): 23 were cut and carried
13
+ // on; in 8 the API then refused; one of the 8 still answered, because a background helper restarted the
14
+ // session and the last result was a success. The program's own words for each event, read off a saved
15
+ // stream with the content left out:
16
+ //
17
+ // cut a `user` event, `isSynthetic: true`, whose text block says the response "was stopped by
18
+ // a safety classifier"
19
+ // refusal a `system` event, subtype `model_refusal_no_fallback`, carrying `api_refusal_category`
20
+ // and `original_model`; then an `assistant` message with `stop_reason: "refusal"`
21
+ // restart a second `system` / `init` event in the same stream
22
+ // results one `result` event per answer, each with `result_index`, `is_error`, `stop_reason`,
23
+ // `terminal_reason` and, for one a helper's notice started, `origin.kind`
24
+ //
25
+ // So nothing here is inferred: every row is an event the program wrote, with when it arrived. The
26
+ // record answers "was this session interrupted, refused or restarted, and when" from the run directory,
27
+ // and it never decides anything — the retry policy reads the last result exactly as before.
28
+ //
29
+ // ── what is NOT recorded ─────────────────────────────────────────────────────────────────────────────
30
+ //
31
+ // No text in the session record. The cut message, the refusal's explanation and every result's answer
32
+ // carry the model's or the vendor's words about client matter; a row carries the event's kind, its time
33
+ // and its codes. The raw stream, which does carry the words, goes to its own file in the run directory
34
+ // (streamSink below), where the dispatch text already sits.
35
+ //
36
+ // ONE EXCEPTION, and it sits beside the record rather than in it: when a session ENDED IN AN ERROR, the
37
+ // program's own words for that error go on the attempt row (`errorText`, see "how the session ended"
38
+ // below). An expired sign-in recorded `stop_sequence` six times and nothing else, while the program had
39
+ // said in plain words that the session had expired. On a healthy ending that field is the model's answer
40
+ // and stays off the row.
41
+
42
+ import { mkdirSync, openSync, writeSync, closeSync, statfsSync } from "node:fs";
43
+ import { dirname } from "node:path";
44
+
45
+ /** The program's words for a classifier cut, as it writes them into the session. Matched, never quoted. */
46
+ export const CLASSIFIER_CUT_RE = /stopped by a safety classifier/i;
47
+
48
+ // A row list is a record, not a log: a session that is cut fifty times has said what it needs to say by
49
+ // the fiftieth, and the count beside the list keeps saying how many there were.
50
+ const ROWS_CAP = 50;
51
+
52
+ /** An empty record. `starts` counts the program's own session starts (`system`/`init`). */
53
+ export function newSessionRecord() {
54
+ return { starts: 0, results: [], classifierCuts: [], refusals: [], refusalStops: 0, dropped: 0 };
55
+ }
56
+
57
+ const push = (rec, list, row) => {
58
+ if (list.length < ROWS_CAP) list.push(row);
59
+ else rec.dropped += 1;
60
+ };
61
+
62
+ /** The text a `user` event carries outside its tool results: a string, or its `text` blocks. */
63
+ function userText(content) {
64
+ if (typeof content === "string") return content;
65
+ if (!Array.isArray(content)) return "";
66
+ return content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
67
+ }
68
+
69
+ /**
70
+ * Fold one parsed Claude stream event into the record. `atMs` is milliseconds since the spawn. PURE
71
+ * over its arguments; the caller owns the clock.
72
+ */
73
+ export function noteClaudeEvent(rec, ev, atMs) {
74
+ if (!rec || !ev || typeof ev !== "object") return;
75
+ if (ev.type === "system" && ev.subtype === "init") { rec.starts += 1; return; }
76
+ if (ev.type === "system" && ev.subtype === "model_refusal_no_fallback") {
77
+ push(rec, rec.refusals, {
78
+ atMs, category: typeof ev.api_refusal_category === "string" ? ev.api_refusal_category : null,
79
+ model: typeof ev.original_model === "string" ? ev.original_model : null,
80
+ });
81
+ return;
82
+ }
83
+ if (ev.type === "assistant") {
84
+ if (ev.message?.stop_reason === "refusal") rec.refusalStops += 1;
85
+ return;
86
+ }
87
+ if (ev.type === "user") {
88
+ if (CLASSIFIER_CUT_RE.test(userText(ev.message?.content))) push(rec, rec.classifierCuts, { atMs, synthetic: ev.isSynthetic === true });
89
+ return;
90
+ }
91
+ if (ev.type === "result") {
92
+ push(rec, rec.results, {
93
+ atMs,
94
+ index: Number.isFinite(ev.result_index) ? ev.result_index : rec.results.length + rec.dropped,
95
+ subtype: typeof ev.subtype === "string" ? ev.subtype : null,
96
+ isError: ev.is_error === true,
97
+ stopReason: typeof ev.stop_reason === "string" ? ev.stop_reason : null,
98
+ terminalReason: typeof ev.terminal_reason === "string" ? ev.terminal_reason : null,
99
+ origin: typeof ev.origin?.kind === "string" ? ev.origin.kind : null,
100
+ });
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Fold one parsed Codex `exec --json` event into the record. Codex states no refusal category and no
106
+ * classifier cut on its stream (measured, codex-cli 0.150–0.158), so what it can say is said: how the
107
+ * turn ended, every time it ended, and every stream error.
108
+ */
109
+ export function noteCodexEvent(rec, ev, atMs) {
110
+ if (!rec || !ev || typeof ev !== "object") return;
111
+ if (ev.type === "thread.started") { rec.starts += 1; return; }
112
+ if (ev.type === "turn.completed" || ev.type === "turn.failed" || ev.type === "error") {
113
+ push(rec, rec.results, {
114
+ atMs, index: rec.results.length + rec.dropped, subtype: ev.type,
115
+ isError: ev.type !== "turn.completed", stopReason: null, terminalReason: null, origin: null,
116
+ });
117
+ }
118
+ }
119
+
120
+ /** True when any result the session reached was an error, whatever the last one said. */
121
+ export const anyResultErrored = (rec) => Array.isArray(rec?.results) && rec.results.some((r) => r?.isError === true);
122
+
123
+ /**
124
+ * The record as the attempt row carries it. `interrupted` is the one-word answer a reader filters on: the
125
+ * session was cut, refused, restarted, or reached an error result, at least once.
126
+ */
127
+ export function sessionSummary(rec) {
128
+ if (!rec) return null;
129
+ const interrupted = rec.classifierCuts.length > 0 || rec.refusals.length > 0 || rec.refusalStops > 0
130
+ || rec.starts > 1 || anyResultErrored(rec);
131
+ return {
132
+ interrupted, starts: rec.starts, results: rec.results.map((r) => ({ ...r })),
133
+ classifierCuts: rec.classifierCuts.map((r) => ({ ...r })), refusals: rec.refusals.map((r) => ({ ...r })),
134
+ refusalStops: rec.refusalStops, ...(rec.dropped ? { rowsDropped: rec.dropped } : {}),
135
+ };
136
+ }
137
+
138
+ // ── how the session ended, in the program's own words ──────────────────────────────────────────────────
139
+ //
140
+ // The stop reason alone misleads on an error. Measured on an expired sign-in: the Claude program reported
141
+ // `subtype: "success"`, `is_error: true`, `stop_reason: "stop_sequence"` (the stop of its OWN synthetic
142
+ // message, whose model reads `<synthetic>`) and `terminal_reason: "api_error"`, with the reason in the
143
+ // result text: "Failed to authenticate: OAuth session expired and could not be refreshed". Six retries of
144
+ // one stage journalled six identical `stop_sequence` rows and said nothing about why. The stop reason stays
145
+ // verbatim, because on an error it describes the program's synthetic message and rewriting it would be
146
+ // inventing; these three go beside it, on both engines:
147
+ //
148
+ // terminalReason the kind of ending, as the program states it. null when it states none (Codex never does).
149
+ // apiErrorStatus the vendor's status when it gave one. null when it did not, which is not zero.
150
+ // errorText the program's words for the error, ONLY when the session ended in one, cut to a length a
151
+ // person reads. Never a word of ours in their place: an error that carried none reads null.
152
+ //
153
+ // The verdict does not move: `fail`, the retry ladder and the envelope's status read exactly what they
154
+ // read before. These record; they decide nothing.
155
+ export const ERROR_TEXT_CAP = 300;
156
+
157
+ const words = (t) => (typeof t === "string" && t.trim() ? t.slice(0, ERROR_TEXT_CAP) : null);
158
+
159
+ /** Claude: from the session's LAST result event (the one the verdict reads), or nulls when none arrived. */
160
+ export function claudeEnding(resultEvent) {
161
+ const r = resultEvent && typeof resultEvent === "object" ? resultEvent : null;
162
+ return {
163
+ terminalReason: typeof r?.terminal_reason === "string" ? r.terminal_reason : null,
164
+ apiErrorStatus: r?.api_error_status ?? null,
165
+ errorText: r?.is_error === true ? words(r.result) : null,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Codex: no terminal kind and no status on its stream. Its words are the failed turn's own message, or the
171
+ * stream error when the turn never completed; an `error` it recovered from (a reconnect, then a completed
172
+ * turn) is not why it ended. Reads the raw messages `parseCodexEvent` keeps, never its placeholders.
173
+ */
174
+ export function codexEnding(ev) {
175
+ const failed = ev?.turnFailedText ?? null, stream = ev?.streamErrorText ?? null;
176
+ const said = ev?.turnFailed ? words(failed) : (ev?.turnCompleted ? null : words(stream));
177
+ return { terminalReason: null, apiErrorStatus: null, errorText: said };
178
+ }
179
+
180
+ /** The three as an attempt row carries them: written every time, null where the engine said nothing. */
181
+ export function endingFields(json) {
182
+ return { terminalReason: json?.terminalReason ?? null, apiErrorStatus: json?.apiErrorStatus ?? null, errorText: json?.errorText ?? null };
183
+ }
184
+
185
+ /** A per-tool-name call count, folded one name at a time. */
186
+ export function countTool(byName, name) {
187
+ const k = String(name ?? "?") || "?";
188
+ byName[k] = (byName[k] ?? 0) + 1;
189
+ }
190
+
191
+ // ── the raw stream ───────────────────────────────────────────────────────────────────────────────────
192
+ //
193
+ // Every byte the program wrote on its protocol channel, one file per session, beside the dispatch that
194
+ // started it. Kept while the disk holding the run has room: below STREAM_FLOOR_BYTES free it is not
195
+ // written, and the record says so rather than leaving a file that stops halfway. A write that fails
196
+ // mid-session closes the file and says so too. Neither ever fails the turn.
197
+ export const STREAM_FLOOR_BYTES = 5 * 1024 ** 3;
198
+
199
+ /**
200
+ * Open a sink for `file`. Returns `{ write(chunk), close() }`; `close()` returns what landed:
201
+ * `{ file, bytes, present: true }`, or `{ file, present: false, reason }`. A null file is a null sink.
202
+ */
203
+ export function streamSink(file, { floorBytes = STREAM_FLOOR_BYTES, freeBytes = null } = {}) {
204
+ if (!file) return { write() {}, close: () => null };
205
+ let fd = null, bytes = 0, reason = null;
206
+ try {
207
+ mkdirSync(dirname(file), { recursive: true });
208
+ const free = freeBytes ?? (() => { const s = statfsSync(dirname(file)); return Number(s.bavail) * Number(s.bsize); })();
209
+ if (free < floorBytes) reason = `not written: the disk holding the run had ${Math.round(free / 1024 ** 2)} MiB free, under the ${Math.round(floorBytes / 1024 ** 3)} GiB floor`;
210
+ else fd = openSync(file, "w", 0o600);
211
+ } catch (e) { reason = `not written: ${String(e?.message ?? e).slice(0, 120)}`; }
212
+ return {
213
+ write(chunk) {
214
+ if (fd === null) return;
215
+ try { bytes += writeSync(fd, typeof chunk === "string" ? chunk : Buffer.from(chunk)); }
216
+ catch (e) { try { closeSync(fd); } catch { /* already gone */ } fd = null; reason = `cut short after ${bytes} bytes: ${String(e?.message ?? e).slice(0, 120)}`; }
217
+ },
218
+ close() {
219
+ if (fd !== null) { try { closeSync(fd); } catch { /* the bytes are on disk or they are not; bytes says which */ } fd = null; }
220
+ return reason ? { file, present: bytes > 0, bytes, reason } : { file, present: true, bytes };
221
+ },
222
+ };
223
+ }
@@ -730,8 +730,8 @@ export function validateJob(job, { atClaim = false } = {}) {
730
730
  warnings.push(...fit.warnings);
731
731
  }
732
732
  // platforms — marketplaces to sweep IN ADDITION to the account's own. ADDITIVE ONLY, and enforced as
733
- // such where the union happens: a client's platforms are a client MANDATE (the 2026-07-18 Racers
734
- // Paradise run searched house platforms instead of the client's), so a per-run list can widen the grid
733
+ // such where the union happens: a client's platforms are a client MANDATE (the 2026-07-18 Drivers
734
+ // Haven run searched house platforms instead of the client's), so a per-run list can widen the grid
735
735
  // and can never shrink it. Same per-entry rules as a profile's, shared via platformEntryErrors.
736
736
  if (job.platforms != null) {
737
737
  if (!Array.isArray(job.platforms)) errs.push("platforms must be an array of bare store domains (e.g. [\"gnc.com\"]) — they are ADDED to the account's marketplaces, never a replacement for them");
@@ -1780,7 +1780,7 @@ function validateNet(f, ord, mode) {
1780
1780
  // `findings?_` and would aim the repair turn at register-coverage-ledger.json — forbidding the model
1781
1781
  // to rewrite the only file that could fix it.
1782
1782
  // It reaches the wire as a validator `reason` (verify.mjs checkFindingsSibling), which is the only shape
1783
- // gateway.mjs:525 mints as `invalid_file:<path>:<token>`; a bespoke throw would match nothing.
1783
+ // gateway.mjs:1200 judgeArtifacts mints as `invalid_file:<path>:<token>`; a bespoke throw would match nothing.
1784
1784
  // The precedent is one line up the file: findings_mark_assessment_* was named plural for the same
1785
1785
  // routing reason.
1786
1786
  //
@@ -1942,14 +1942,14 @@ function recordUrlOrigin(value) {
1942
1942
  *. `checkRecordUrlHost` asks whether a link points at the wrong register, and `recordUrlOrigin`
1943
1943
  * returns null for anything with no host — so the host gate returns CLEAN for `#`. That is right for its
1944
1944
  * own purpose and it means nothing in the findings path was asking whether the link is a link. A value of
1945
- * `#` then composes `- Source: [EUIPO · 018575624](#)` — the delivered R5 shape is about, byte for
1945
+ * `#` then composes `- Source: [EUIPO · 099999992](#)` — the delivered R5 shape is about, byte for
1946
1946
  * byte, reached through a path nothing rejected.
1947
1947
  *
1948
1948
  * ABSENT IS NOT DEAD, and the distinction is the whole predicate. `null` and `""` are the sanctioned way
1949
1949
  * to say "this provider publishes no per-record page" — validateSource documents `""` in its own message
1950
1950
  * and renderSourceBullet composes nothing for it. Rejecting those would break the legitimate case this
1951
1951
  * issue exists to protect. Anything else that is not an http(s) URL is a value a seat typed in place of a
1952
- * link: `#`, `#details/trademarks/018575624`, a bare path, or whitespace that only looks like a value.
1952
+ * link: `#`, `#details/trademarks/099999992`, a bare path, or whitespace that only looks like a value.
1953
1953
  *
1954
1954
  * IT ENUMERATES RATHER THAN ASKING "IS THIS AN http(s) URL", and a shipped test is why. ` an
1955
1955
  * unparseable or non-http value is not a host claim and is left to the shape rules` deliberately ACCEPTS
@@ -2179,7 +2179,7 @@ export function projectCoverageJudgment(cj) {
2179
2179
  const rows = Array.isArray(cj.rows) ? cj.rows : [];
2180
2180
  if (!rows.length) return cj;
2181
2181
  // — the READER's name leads, and falls back to the identifier when the driver had nothing to
2182
- // add. This string is printed on report.html ("Coverage read"), which is why it reads the label.
2182
+ // add. renderHtml receives this string and leaves it off report.html, so no page prints it; the label leads anyway.
2183
2183
  const shown = rows.slice(0, COVERAGE_JUDGMENT_ROW_CAP);
2184
2184
  const hidden = rows.length - shown.length;
2185
2185
  const rowText = shown.map((r) => `${trimDot(r.areaLabel || r.area)}: ${trimDot(r.note)}`).join("; ");
@@ -425,7 +425,8 @@ export function registerCanCountFor(snapshot) {
425
425
  *
426
426
  * The trap is `covered ?? []`, which turns both "unrestricted" and "unknown" into "nothing" and offers
427
427
  * a client zero territories on a production deployment. It is written down at
428
- * providers/corsearch/src/capabilities.js:66-68 as well: "Never read as 'covers nothing'".
428
+ * the `covered` entry of CAPABILITIES in providers/corsearch/src/capabilities.js as well: "Never read as
429
+ * 'covers nothing'".
429
430
  */
430
431
  export function registerTerritoriesFor(snapshot) {
431
432
  if (!snapshot?.register || !("territories" in snapshot.register)) return undefined;
@@ -288,8 +288,8 @@ export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_S
288
288
 
289
289
  // exactQueries = the strings dispatched as OR-stacked exact/near name searches (deduped, original excluded).
290
290
  //
291
- // Deduped ON THE DIACRITIC FOLD, not the raw string. The registers fold diacritics, so `paradisè`,
292
- // `paradisé`, `paradisê`, `paradisë` and `paradiše` are ONE query, not five — and one already covered by the
291
+ // Deduped ON THE DIACRITIC FOLD, not the raw string. The registers fold diacritics, so `havensidè`,
292
+ // `havensidé`, `havensidê`, `havensidë` and `havenšide` are ONE query, not five — and one already covered by the
293
293
  // element itself. Drivers Haven 2026-07-17 dispatched exactly those five as distinct terms; each returned
294
294
  // the same 424 hits, together they exhausted the run's record budget, all five stayed `unenumerated`, and
295
295
  // `coverage_clean_unverified_incomplete:primary-sweep` then blocked delivery six times on a gate that could
@@ -399,8 +399,8 @@ export function coverageGaps(band, { dispatched = [], explained = [] } = {}) {
399
399
  // `model` is the VALIDATED variant-manifest.json (variant-manifest-model.mjs). When supplied it is
400
400
  // AUTHORITATIVE for the dominant element and the prose is used only for the formative root — because the prose
401
401
  // parse is unsafe. `dominantElementFromManifest` captures to the first `.`/`;`/`(`/newline, so a manifest that
402
- // writes "Dominant element: HYDRA — the stem a family of marks shares" yields the whole clause, which
403
- // normalizeElement then compacts to `hydrathestemafamilyofmarksshares`. That pseudo-element got a full
402
+ // writes "Dominant element: AQUA — the stem a family of marks shares" yields the whole clause, which
403
+ // normalizeElement then compacts to `aquathestemafamilyofmarksshares`. That pseudo-element got a full
404
404
  // exhaustive edit-1 neighbourhood: 1,736 junk exact queries on AquaPlus 2026-07-17, 4,524 on the 07-16 run.
405
405
  // Measured 2026-07-18: 10 of 20 recent runs carried one of these. The JSON field is a validated scalar and
406
406
  // cannot swallow a sentence.
@@ -478,7 +478,7 @@ export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts
478
478
  // line variantCompletenessGaps' per-script floor draws when it says it VALIDATES, NEVER GENERATES.
479
479
 
480
480
  // A seed longer than this is a prose-parse artifact, not an element. The measured shape is the
481
- // swallowed clause — "hydrathestemafamilyofmarksshares", 32-36 characters against a 5-char element —
481
+ // swallowed clause — "aquathestemafamilyofmarksshares", 31-35 characters against a 4-char element —
482
482
  // which took a full exhaustive edit-1 neighbourhood and put 1,736 junk exact queries on the wire
483
483
  // (AquaPlus 2026-07-17). isStemOfNamedElement already rejects that shape for the formative root by
484
484
  // measuring it against the named elements; this is the same measure for a seed with nothing to
@@ -691,7 +691,7 @@ export function variantFloorFamilies(elements, { mark = "", droppedAxes = [], mi
691
691
  // MEASURED, not assumed (2026-08-03): the register compiler's `norm` and the shared `formKey`
692
692
  // both strip separators, so every member of this family keys IDENTICALLY to the mark and is
693
693
  // already searched as the mark's own exact entry — which is why compileRegisterPlan does not
694
- // re-dispatch them, and why doing so would be the PARADISE shape (five spellings, one result
694
+ // re-dispatch them, and why doing so would be the Drivers Haven shape (five spellings, one result
695
695
  // set, the run's record budget spent). Enumerated here because the family is genuinely
696
696
  // mechanical and the surfaces that do NOT fold spacing — the common-law grid, the connotation
697
697
  // sweep — are real; those consumers are NOT wired to it by this change.
@@ -794,7 +794,7 @@ export function mergeVariantFloor(floorFamilies, modelVariants, { rejectedSeeds
794
794
  /**
795
795
  * Is `root` a plausible formative root, or a prose-parse artifact?
796
796
  *
797
- * A stem is never LONGER than the element it stems from — VOLEM(5) for ZOLEMA(6), HYDR(4) for HYDRA(5),
797
+ * A stem is never LONGER than the element it stems from — VOLEM(5) for ZOLEMA(6), AQU(3) for AQUA(4),
798
798
  * VELTRI(6) for VELTRIN(7). Substring containment is NOT the test: a real root can differ in its leading
799
799
  * character (ZOLEMA → VOLEM reaches the VOLEMA family), which is exactly the widening the root exists for.
800
800
  * The swallowed-sentence artifacts are always the element PLUS a clause — 32-36 characters against a 5-char
@@ -70,7 +70,7 @@ export function parseFrameDiff(raw) {
70
70
  if (d.remedy != null) out.remedy = parseRemedy(d.remedy);
71
71
  // ── THE ASK CONTRACT AT ASK-RAISE (charter P2e) ───────────────────────────────────────────────
72
72
  // A firing directive is a REQUEST TO SEARCH SOMETHING. Until now the parser accepted one whose
73
- // item was a display label ("TAKIS (famous cpg snack, …)") with no remedy, and the refusal came
73
+ // item was a display label ("WAVA (famous cpg snack, …)") with no remedy, and the refusal came
74
74
  // hours later at reopen — by which time the session that could have restated it had exited. Four
75
75
  // of the five directives on the 2026-07-29 run died exactly there: loud (coverage rows, the clamp,
76
76
  // the audit trail) but terminal, with the head noun of a perfect one-query term sitting in the
@@ -143,7 +143,7 @@ export function undispatchableThrow(offenders) {
143
143
  * Every FIRING directive in a frame-diff document that cannot be dispatched — read LENIENTLY, from
144
144
  * raw text or an already-parsed object. This is the enumeration the gateway hint and the pipeline's
145
145
  * exhausted-ladder disclosure both run against the artifact ON DISK, because the fail string they
146
- * would otherwise scrape is cut at 160 characters (verify.mjs:381) and cannot carry more than the
146
+ * would otherwise scrape is cut at 160 characters (checkJson() in verify.mjs) and cannot carry more than the
147
147
  * first offender. Lenient on purpose: the artifact reaching these two callers has ALREADY failed
148
148
  * validation, so it may also carry an unknown key or a bad gap flag — refusing to enumerate then
149
149
  * would lose exactly the disclosures this exists to keep. Returns [{item, layer, severity, why}].
@@ -190,7 +190,7 @@ export function firingDirectivesLenient(rawOrDoc) {
190
190
  *
191
191
  * SCOPE IS DELIBERATELY THE VARIANT LAYER (plus any layer's own remedy):
192
192
  * * VARIANT — the evidenced class. The directive says "search this near-form", so its item stands
193
- * where a mark term stands, and a label there ("TAKIS (famous cpg snack, …)") is the whole defect:
193
+ * where a mark term stands, and a label there ("WAVA (famous cpg snack, …)") is the whole defect:
194
194
  * it dispatches as a nil search that reads CLEAN, or it dies unasked at reopen.
195
195
  * * FIELD — untouched. A class-gap directive whose item names no parseable class already has a
196
196
  * principled ending: deriveDirectiveRemedy returns null and the directive is DISCLOSED rather