clearotron 0.3.2-beta.7 → 0.3.2-beta.9

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 (272) hide show
  1. package/.env.example +58 -26
  2. package/CONTRIBUTING.md +8 -8
  3. package/INSTALL.md +148 -81
  4. package/README.md +3 -3
  5. package/SECURITY.md +3 -3
  6. package/bin/brandowner.mjs +3 -3
  7. package/bin/framework-preflight.mjs +1 -1
  8. package/bin/onboard.mjs +637 -216
  9. package/bin/start.mjs +151 -27
  10. package/bin/update.mjs +58 -11
  11. package/build-info.json +2 -2
  12. package/docs/DELIVERY.md +2 -1
  13. package/docs/INTAKE.md +1 -1
  14. package/docs/ONBOARDING.md +1 -1
  15. package/docs/architecture/03-run-lifecycle.md +6 -6
  16. package/docs/architecture/04-configuration-reference.md +32 -14
  17. package/docs/architecture/05-config-governance.md +23 -8
  18. package/docs/architecture/05-customer-profiles.md +2 -2
  19. package/docs/architecture/06-operations-runbook.md +3 -3
  20. package/docs/architecture/08-development-guide.md +6 -6
  21. package/docs/configuration.md +5 -5
  22. package/docs/decisions/0003-credential-model.md +1 -1
  23. package/docs/writing-standard.md +4 -0
  24. package/driver/CHANGELOG.md +124 -0
  25. package/driver/README.md +3 -3
  26. package/driver/band-size.mjs +59 -0
  27. package/driver/binding-layers.mjs +1 -1
  28. package/driver/citation-census.json +3 -3
  29. package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
  30. package/driver/common-law-receipts.mjs +2 -2
  31. package/driver/company-bundle.mjs +3 -3
  32. package/driver/compose-read.mjs +8 -14
  33. package/driver/config-inventory.mjs +112 -9
  34. package/driver/consumption-ledger.mjs +2 -2
  35. package/driver/contract-arm2-baseline.json +2 -5
  36. package/driver/contract-dictation-registry.mjs +19 -19
  37. package/driver/contract-e3-backlog.mjs +43 -43
  38. package/driver/contract-e3-baseline.json +14 -14
  39. package/driver/contract-vocabulary.mjs +68 -27
  40. package/driver/deliver-trigger.sh +16 -16
  41. package/driver/demo-container.mjs +3 -3
  42. package/driver/dev-portal.mjs +3 -3
  43. package/driver/disposition-call.mjs +1 -1
  44. package/driver/door-gates.mjs +41 -7
  45. package/driver/doubt-ledger.mjs +2 -2
  46. package/driver/drainer-identity.mjs +34 -8
  47. package/driver/driver.config.mjs +367 -104
  48. package/driver/engine/CONTRACT.md +10 -3
  49. package/driver/engine/README.md +2 -2
  50. package/driver/engine/anthropic-agent.mjs +77 -21
  51. package/driver/engine/auth.mjs +129 -10
  52. package/driver/engine/jx-turn.mjs +7 -6
  53. package/driver/engine/mcp/README.md +1 -1
  54. package/driver/engine/mcp/dispositions-server.mjs +3 -3
  55. package/driver/engine/mcp/gather-config.mjs +9 -9
  56. package/driver/engine/mcp/perplexity-server.mjs +2 -2
  57. package/driver/engine/mcp/recording-server.mjs +18 -5
  58. package/driver/engine/openai-agent.mjs +4 -2
  59. package/driver/engine/probe.mjs +110 -23
  60. package/driver/enqueue-schema.mjs +6 -2
  61. package/driver/findings-model.mjs +6 -3
  62. package/driver/flag-snapshot.mjs +34 -8
  63. package/driver/form-neighbourhood.mjs +54 -7
  64. package/driver/framework.mjs +4 -4
  65. package/driver/gateway.mjs +36 -24
  66. package/driver/jx-lanes.mjs +23 -4
  67. package/driver/jx-units.mjs +7 -4
  68. package/driver/jx.mjs +34 -4
  69. package/driver/knockout-review-record.mjs +56 -4
  70. package/driver/known-conflicts.mjs +1 -1
  71. package/driver/matter-frame-record.mjs +90 -1
  72. package/driver/named-band.mjs +1 -1
  73. package/driver/ordinary-words.mjs +51 -0
  74. package/driver/outbox-backoff.mjs +31 -16
  75. package/driver/package.json +1 -1
  76. package/driver/partial-payload-baseline.json +2 -2
  77. package/driver/phase0.mjs +3 -3
  78. package/driver/pipeline-knockout.mjs +5 -5
  79. package/driver/pipeline.mjs +396 -81
  80. package/driver/placement-form.mjs +77 -1
  81. package/driver/placement-model.mjs +1 -1
  82. package/driver/portal-config-view.mjs +30 -1
  83. package/driver/portal-report.mjs +107 -6
  84. package/driver/portal-service.mjs +80 -14
  85. package/driver/portal-upstream.mjs +1 -1
  86. package/driver/predelivery-lint.mjs +12 -2
  87. package/driver/preserve-merge.mjs +3 -3
  88. package/driver/product-rows.mjs +2 -2
  89. package/driver/products.mjs +1 -1
  90. package/driver/profiles/README.md +3 -3
  91. package/driver/profiles/demo-brand-owner.json +2 -2
  92. package/driver/profiles.mjs +55 -17
  93. package/driver/progress.mjs +18 -8
  94. package/driver/provider-usage.mjs +8 -8
  95. package/driver/publish/index.mjs +154 -8
  96. package/driver/publish/knockout.mjs +39 -5
  97. package/driver/publish/pool-admin.mjs +1 -1
  98. package/driver/publish/publish-inputs.mjs +18 -2
  99. package/driver/publish/render-knockout.mjs +184 -31
  100. package/driver/publish/render.mjs +323 -93
  101. package/driver/publish/report-data.mjs +4 -1
  102. package/driver/publish/report-topbar.mjs +58 -0
  103. package/driver/publish/search-depth.mjs +133 -4
  104. package/driver/publish/templates/report.css +78 -4
  105. package/driver/publish/xlsx.mjs +20 -1
  106. package/driver/queue-order.mjs +2 -2
  107. package/driver/recording-agreement.mjs +1 -1
  108. package/driver/reference-score.mjs +1 -1
  109. package/driver/register-availability.mjs +2 -2
  110. package/driver/register-count.mjs +50 -5
  111. package/driver/register-coverage.mjs +161 -1
  112. package/driver/register-digest-record.mjs +236 -11
  113. package/driver/register-grant-vocabulary.mjs +1 -1
  114. package/driver/register-plan.mjs +189 -2
  115. package/driver/registry-fidelity.mjs +3 -3
  116. package/driver/repair-composers.mjs +1 -1
  117. package/driver/repair-contract.mjs +1 -1
  118. package/driver/replay-archive.mjs +6 -6
  119. package/driver/report-overview-record.mjs +2 -2
  120. package/driver/result-noun-fields.mjs +2 -2
  121. package/driver/run-economics.mjs +41 -10
  122. package/driver/run-requirements.mjs +173 -9
  123. package/driver/runner.mjs +5 -5
  124. package/driver/scope-facts.mjs +20 -5
  125. package/driver/scope-ledger.mjs +5 -5
  126. package/driver/search-policy.mjs +22 -12
  127. package/driver/skills/README.md +15 -15
  128. package/driver/skills/blind-frame/SKILL.md +2 -2
  129. package/driver/skills/case-law-citation/SKILL.md +4 -4
  130. package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
  131. package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
  132. package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
  133. package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
  134. package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
  135. package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
  136. package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
  137. package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
  138. package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
  139. package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
  140. package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
  141. package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
  142. package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
  143. package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
  144. package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
  145. package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
  146. package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
  147. package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
  148. package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
  149. package/driver/skills/frame-diff/SKILL.md +1 -1
  150. package/driver/skills/knockout-assess/SKILL.md +10 -7
  151. package/driver/skills/matter-frame/SKILL.md +3 -3
  152. package/driver/skills/narrative-refutation/SKILL.md +9 -9
  153. package/driver/skills/placement-inquiry/SKILL.md +5 -5
  154. package/driver/stage-context.mjs +1 -1
  155. package/driver/stages-knockout.mjs +4 -4
  156. package/driver/stages.mjs +65 -61
  157. package/driver/status-snapshot.mjs +2 -2
  158. package/driver/suite-census.json +340 -136
  159. package/driver/surface-exit-verdict.mjs +58 -0
  160. package/driver/systemd/README.md +9 -6
  161. package/driver/systemd/clearotron-worker.service +1 -1
  162. package/driver/terminal-clamp.mjs +109 -1
  163. package/driver/tokens.mjs +169 -3
  164. package/driver/unit-environment.mjs +42 -15
  165. package/driver/unit-inventory.mjs +19 -2
  166. package/driver/usage-ledger.mjs +1 -1
  167. package/driver/variant-manifest-model.mjs +4 -4
  168. package/driver/verify-knockout.mjs +27 -0
  169. package/driver/verify.mjs +94 -6
  170. package/driver/whatif-queue.mjs +1 -1
  171. package/driver/wordlists/en.txt +63906 -0
  172. package/mcp-server/CHANGELOG.md +8 -0
  173. package/mcp-server/README.md +1 -1
  174. package/mcp-server/lib/README.md +1 -1
  175. package/mcp-server/lib/options.mjs +8 -7
  176. package/mcp-server/lib/plan.mjs +18 -2
  177. package/mcp-server/lib/runs.mjs +1 -1
  178. package/mcp-server/lib/usage.mjs +3 -3
  179. package/mcp-server/lib/whatif.mjs +1 -1
  180. package/mcp-server/package.json +1 -1
  181. package/mcp-server/server.mjs +18 -1
  182. package/package.json +12 -11
  183. package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
  184. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
  185. package/portal-ui/dist/index.html +2 -2
  186. package/portal-ui/package.json +1 -1
  187. package/providers/README.md +1 -1
  188. package/providers/_shared/enumerate.mjs +6 -6
  189. package/providers/_shared/execute-plan.mjs +3 -3
  190. package/providers/_shared/ledger.mjs +119 -5
  191. package/providers/_shared/provider-text.mjs +2 -2
  192. package/providers/_shared/screen.mjs +2 -2
  193. package/providers/_shared/script-form.mjs +3 -3
  194. package/providers/_shared/territory-codes.mjs +23 -3
  195. package/providers/clarivate/README.md +1 -1
  196. package/providers/clarivate/src/capabilities.js +12 -12
  197. package/providers/clarivate/src/core.js +37 -43
  198. package/providers/corsearch/README.md +1 -1
  199. package/providers/corsearch/src/capabilities.js +5 -5
  200. package/providers/corsearch/src/core.js +3 -3
  201. package/providers/jx/README.md +2 -1
  202. package/providers/jx/src/turn-envelope.mjs +8 -3
  203. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  204. package/providers/oauth-mcp-bridge/package.json +1 -1
  205. package/providers/perplexity/src/core.js +1 -1
  206. package/providers/signa/README.md +1 -1
  207. package/providers/signa/src/capabilities.js +42 -49
  208. package/providers/signa/src/core.js +106 -29
  209. package/providers/uspto-local/README.md +1 -1
  210. package/providers/uspto-local/src/sync.js +1 -1
  211. package/scripts/README.md +1 -0
  212. package/scripts/ask-ai-render-check.mjs +127 -1
  213. package/scripts/authority-boundary-probe.mjs +8 -6
  214. package/scripts/backfill-started-at.mjs +2 -2
  215. package/scripts/census-merge-driver.mjs +33 -2
  216. package/scripts/citation-anchor-report.mjs +181 -0
  217. package/scripts/dead-names.mjs +1 -1
  218. package/scripts/deprecate-below.mjs +66 -8
  219. package/scripts/drain-preflight.mjs +1 -1
  220. package/scripts/e2e.mjs +174 -0
  221. package/scripts/env-audit.mjs +39 -6
  222. package/scripts/env-classify.mjs +20 -2
  223. package/scripts/freeze-example-run.mjs +61 -18
  224. package/scripts/generated-files-are-current.mjs +69 -4
  225. package/scripts/live-surface-check.mjs +124 -41
  226. package/scripts/markdown-link-check.mjs +1 -1
  227. package/scripts/merge-shape-check.mjs +242 -0
  228. package/scripts/mint-names-in-force.mjs +5 -5
  229. package/scripts/mint-offered-territories.mjs +72 -0
  230. package/scripts/mint-public-residue.mjs +2 -2
  231. package/scripts/mint-reference-strip-backlog.mjs +2 -2
  232. package/scripts/mint-suite-census.mjs +75 -2
  233. package/scripts/mint-writing-standard-backlog.mjs +2 -2
  234. package/scripts/purge-runs.mjs +7 -7
  235. package/scripts/reconcile-runs.mjs +2 -2
  236. package/scripts/release-approve-parked.mjs +20 -2
  237. package/scripts/release-await-cut.mjs +120 -1
  238. package/scripts/release-note-required.mjs +76 -8
  239. package/scripts/report-header-render-check.mjs +164 -0
  240. package/scripts/settings-render-check.mjs +75 -2
  241. package/scripts/test-full.mjs +96 -3
  242. package/scripts/test-run.mjs +10 -0
  243. package/shared/brand.mjs +27 -0
  244. package/shared/connect-clients.mjs +39 -11
  245. package/shared/deployment-box.mjs +7 -2
  246. package/shared/driver-dir.mjs +1 -1
  247. package/shared/env-aliases.mjs +1 -1
  248. package/shared/identifier-scan.mjs +65 -9
  249. package/shared/identifier-sentinels.mjs +22 -0
  250. package/shared/names-in-force.mjs +4 -2
  251. package/shared/offered-territories.json +738 -0
  252. package/shared/pre-rename-spellings.mjs +53 -0
  253. package/shared/reference-guard-classes.mjs +40 -2
  254. package/shared/stdio-connect.mjs +39 -4
  255. package/shared/tree-commit.mjs +48 -0
  256. /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
  257. /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
  258. /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
  259. /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
  260. /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
  261. /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
  262. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
  263. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
  264. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
  265. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
  266. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
  267. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
  268. /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
  269. /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
  270. /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
  271. /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
  272. /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
@@ -41,6 +41,8 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
41
41
  sessionRef: string | null, // opaque resume handle (claude session_id | codex thread_id)
42
42
  modelWire: string | null, // MODEL GAUGE — the served model id this turn observed (§3);
43
43
  // null = nothing was observed, never the requested alias
44
+ providerWire: string | null, // PROVIDER GAUGE — the program's own word for who served the turn
45
+ // ("firstParty", "foundry"); null = not said, or said inconsistently
44
46
  signals: { stalled?, noProgress?, hardWall?, rateLimited?, rateLimitBasis?, resetsAt?,
45
47
  resetsAtBasis?, usageStreamed?, noStreamEvents?, thought: bool|null },
46
48
  // THINKING GAUGE — see below.
@@ -139,8 +141,8 @@ Stages must name **abstract tiers**, not provider aliases. Per-engine maps:
139
141
 
140
142
  | tier | role (stage examples) | anthropic-agent (claude alias) | openai-agent (codex `-m`) |
141
143
  |---|---|---|---|
142
- | `judgment` | matter-frame, register-digest, synthesis, narrative-refutation | `claude-opus-5` (pinned) | `$CLEAROTRON_OPENAI_MODEL_JUDGMENT` |
143
- | `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `claude-sonnet-5` (pinned) | `$CLEAROTRON_OPENAI_MODEL_SWEEP` |
144
+ | `judgment` | matter-frame, register-digest, synthesis, narrative-refutation | `opus` | `$CLEAROTRON_OPENAI_MODEL_JUDGMENT` |
145
+ | `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `sonnet` | `$CLEAROTRON_OPENAI_MODEL_SWEEP` |
144
146
  | `cheap` | saturation-probe | `haiku` | `$CLEAROTRON_OPENAI_MODEL_CHEAP` |
145
147
 
146
148
  **AN UNHONOURED OVERRIDE IS AN ERROR, NOT A SUBSTITUTION** ( corruption 3, 2026-08-03). This
@@ -150,7 +152,12 @@ anthropic engine, "grade-moving, validated only in the paid A/B". The substituti
150
152
  and could not be: the telemetry logged the alias that was ASKED FOR, so an arm run at gemini reported
151
153
  gemini and ran sonnet. Both tiers are gone — the failover chain was deleted in and both stages
152
154
  declare an anthropic tier in `STAGES` — and every engine's model map now **refuses** an alias it cannot
153
- run (`claudeModel`, `openaiModel`). A concrete provider id passes through; anything else throws.
155
+ run (`claudeModel`, `openaiModel`). On the anthropic engine a tier goes as the vendor's alias, a catalog
156
+ id in the table (`anthropic/claude-opus-5`) goes as itself, a bare or dated `claude-*` id goes as its
157
+ family's alias, and anything else throws. To hold a tier on one model, set the vendor's own
158
+ `ANTHROPIC_DEFAULT_OPUS_MODEL` / `_SONNET_MODEL` / `_HAIKU_MODEL`, or `ANTHROPIC_DEFAULT_FABLE_MODEL` for
159
+ `fable`, which no stage asks for unless an override names it, as `CLEAROTRON_SYNTHESIS_MODEL=fable` does; each
160
+ reaches the CLI through the stage's environment.
154
161
 
155
162
  **Model provenance — two fields, never collapsed.** Every dispatch row (`_driver/<stage>.jsonl`) and
156
163
  every `attempt` row (`_driver/run.jsonl`) carries:
@@ -13,7 +13,7 @@ does not exist — that is the design, and [`CONTRACT.md`](CONTRACT.md) is the d
13
13
  | [`CONTRACT.md`](CONTRACT.md) | **The adapter contract.** What an engine must implement, the model-tier map, and what a turn is allowed to assume. Read this before either adapter |
14
14
  | `anthropic-agent.mjs` | Spawns `claude -p`. Skill-reference absolutization, `--add-dir` grants, rate-limit and no-progress handling |
15
15
  | `openai-agent.mjs` | Spawns `codex exec`. A per-run `CODEX_HOME` carrying a rendered `config.toml`, and the session-rollout reader that recovers the turn's usage |
16
- | `auth.mjs` | `resolveAuthMode()` — subscription or API key, resolved once per turn and stamped on the telemetry |
16
+ | `auth.mjs` | `resolveAuthMode()` — subscription, API key or (Claude only) cloud account, resolved once per turn and stamped on the telemetry |
17
17
  | `probe.mjs` | Drives one cheap turn through whichever adapter is configured, to prove the engine can complete a turn at all. What `npm run setup` spends |
18
18
  | `common.mjs` | Helpers both adapters share |
19
19
  | `deny-authority-write.mjs` | A PreToolUse hook. `--add-dir` has no read-only form, so the read-only intent over the skills tree is enforced here |
@@ -27,7 +27,7 @@ succeeding on the other one. A run's manifest records which engine served it.
27
27
 
28
28
  ## Billing mode is resolved here, and it fails loud
29
29
 
30
- `resolveAuthMode()` is the single place "subscription or API key" is decided, and it is deliberately
30
+ `resolveAuthMode()` is the single place a turn's billing mode (subscription, API key or cloud account) is decided, and it is deliberately
31
31
  unforgiving in one direction:
32
32
 
33
33
  ```
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // Contract: engine/CONTRACT.md. runTurn() returns the registry-standard normalized tuple
8
8
  // (`{code, killed, wall, stdout, stderr, laneWaitMs, json, usage, reads, readsTruncated, modelWire,
9
- // sessionRef, signals}`), with a SYNTHESIZED `json` envelope in the classifier's shape so every downstream
9
+ // providerWire, sessionRef, signals}`), with a SYNTHESIZED `json` envelope in the classifier's shape so every downstream
10
10
  // classifier in gateway.mjs (payloadText, json.status check, isEmbeddedFallback, isTimeout,
11
11
  // isLaneWedge) works unchanged.
12
12
  //
@@ -21,12 +21,18 @@ import { tmpdir } from "node:os";
21
21
  import { join, dirname } from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { resolveSpawnCwd, spawnGraceMs } from "./common.mjs";
24
- import { envFrom } from "../../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
24
+ import { resolveEngineProgram } from "../driver.config.mjs"; // — the one place that finds the program; it reads every spelling of the setting
25
25
  import { authorityTrees } from "../authority-trees.mjs";
26
26
  import { recordEngineChild, clearEngineChild } from "./child-record.mjs"; //
27
+ import { billingMode } from "./auth.mjs"; // — the one parse of the billing word (see spawnEnv)
27
28
 
28
29
  // Read per-call (not module-level) so tests can drive a short stall timeout / a mock binary.
29
- const claudeBin = () => envFrom(process.env, "CLEAROTRON_CLAUDE_PATH") || "claude";
30
+ // ONE place knows how to find the program (driver.config.mjs resolveEngineProgram): the explicit setting,
31
+ // then PATH, then the copy Clearotron installed. What it found is spawned by ABSOLUTE path, because a
32
+ // bare word lets spawn(2) walk PATH on its own and never reach the installed copy. When nothing resolved,
33
+ // what was asked for is spawned unchanged, so that failure reads exactly as it always has; the run door
34
+ // (preflightEngineBinary) refuses that case before any stage runs.
35
+ const claudeBin = () => { const r = resolveEngineProgram("anthropic-agent"); return r.resolved ?? r.bin; };
30
36
 
31
37
  // AUTH TOGGLE (config, not code). The subscription path is the cost-saving default: claude -p with NO
32
38
  // ANTHROPIC_API_KEY in its env falls back to the OAuth subscription credentials (apiKeySource:"none" →
@@ -34,10 +40,15 @@ const claudeBin = () => envFrom(process.env, "CLEAROTRON_CLAUDE_PATH") || "claud
34
40
  // .env, and a present API key OVERRIDES the subscription — so we must STRIP it from the claude subprocess
35
41
  // env. CLEAROTRON_AI_BILLING=api-key keeps the key (the standing fallback for when the subscription is
36
42
  // revoked — Anthropic's advance notice = today's per-call API cost). Default = subscription.
43
+ //
44
+ // `cloud` strips it too, as that mode's acceptance asks: a key has no part in a turn the vendor's switches
45
+ // send to the reader's cloud account, and dropping it means a leftover key can never be what bills. A
46
+ // gateway's credential is its own ANTHROPIC_AUTH_TOKEN, which rides through like every other name. The
47
+ // word is parsed by auth.mjs (billingMode), the one place that reads it. This never validates and never
48
+ // throws, because the doors that do (the top of runStage, the probe, the jx runner) have already run.
37
49
  export function spawnEnv(base = process.env) {
38
50
  const env = { ...base };
39
- const mode = (base.CLEAROTRON_AI_BILLING || "subscription").toLowerCase();
40
- if (mode !== "api-key") delete env.ANTHROPIC_API_KEY; // subscription: force OAuth/subscription billing
51
+ if (billingMode(base) !== "api-key") delete env.ANTHROPIC_API_KEY; // subscription and cloud
41
52
  return env;
42
53
  }
43
54
  // 120s of ZERO streamed output = the silent-provider-stall abort. A healthy turn
@@ -110,15 +121,20 @@ const killEscalateMs = () => Math.max(50, Number(process.env.CLEAROTRON_KILL_ESC
110
121
  // clock, so the watchdog never trips). Default 64MB (gateway parity); CLEAROTRON_ENGINE_MAX_BUFFER shrinks it for tests.
111
122
  const engineMaxBufferChars = () => Math.max(1024, Number(process.env.CLEAROTRON_ENGINE_MAX_BUFFER || 64 * 1024 * 1024));
112
123
 
113
- // tier/alias → claude -p model alias. haiku passes straight through (claude understands the alias —
114
- // the 2026-06-16 capture used `--model haiku`). opus and sonnet are PINNED to their full model names
115
- // (claude-opus-5 / claude-sonnet-5) rather than the bare "opus"/"sonnet" aliases, so they no longer
116
- // silently drift to whatever Anthropic/the CLI currently calls "opus"/"sonnet" — matching the driver's
117
- // reproducibility conventions (warm-resume same-model, PURE-FILE replay). opus was bumped off the
118
- // floating "opus" alias to the pinned claude-opus-5 on 2026-07-27: the bare alias still resolved to
119
- // claude-opus-4-8 on the live CLI (2.1.209) at the time, so this is a real, GRADE-MOVING model change
120
- // validated in the paid A/B (CONTRACT §3), never on $0 replay — same price as 4.8 ($5/$25). The
121
- // non-anthropic tiers (gemini skeptic, deepseek refutation, azure) have no claude equivalent →
124
+ // tier/alias → claude -p model alias. EVERY TIER GOES AS THE VENDOR'S OWN ALIAS — opus, sonnet, haiku,
125
+ // fable — so the CLI serves the newest model of that family, and a new one arrives with no edit here.
126
+ // opus and sonnet were pinned to claude-opus-5 / claude-sonnet-5 from 2026-07-27, when the bare "opus"
127
+ // still resolved to Opus 4.8 on the live CLI (2.1.209). The pin was reversed on 2026-09-14, for two
128
+ // reasons. A hand-pinned id is a silent downgrade on every clearance from the day a better model ships.
129
+ // And on Bedrock, Vertex and Foundry the CLI resolves an alias through the vendor's own
130
+ // ANTHROPIC_DEFAULT_OPUS_MODEL / _SONNET_MODEL / _HAIKU_MODEL / _FABLE_MODEL, which an exact id bypasses:
131
+ // a cloud with no deployment of that exact name refuses the turn. The cost is that a model can move under a
132
+ // clearance without a test; the witness is the id the CLI reports, recorded on every attempt row
133
+ // (`modelActual`) and on the published run. To hold a tier still, set the vendor's variable in the env file
134
+ // (ANTHROPIC_DEFAULT_OPUS_MODEL=<id>): the stage's environment is the driver's, so it reaches the CLI
135
+ // with no setting of Clearotron's own. ANTHROPIC_DEFAULT_FABLE_MODEL holds fable, which no stage asks for
136
+ // unless an override names it, as CLEAROTRON_SYNTHESIS_MODEL=fable does. A catalog id a caller names
137
+ // (anthropic/claude-opus-5) still goes as that exact id. The non-anthropic tiers (gemini skeptic, deepseek refutation, azure) have no claude equivalent →
122
138
  // substituted with an anthropic model (also GRADE-MOVING, A/B-only); their bare-alias substitutes
123
139
  // (e.g. deepseek → "opus") are legacy aliases no stage names today, intentionally left un-pinned. They
124
140
  // stay registered so a stage that names one is SUBSTITUTED loudly rather than caught by the regex
@@ -141,7 +157,7 @@ const engineMaxBufferChars = () => Math.max(1024, Number(process.env.CLEAROTRON_
141
157
  // non-GPT id. That is the issue's requirement in one line: an unhonoured model override is an error,
142
158
  // not a substitution.
143
159
  const CLAUDE_MODEL = {
144
- opus: "claude-opus-5", sonnet: "claude-sonnet-5", haiku: "haiku", fable: "fable",
160
+ opus: "opus", sonnet: "sonnet", haiku: "haiku", fable: "fable",
145
161
  "anthropic/claude-opus-5": "claude-opus-5", "anthropic/claude-sonnet-5": "claude-sonnet-5",
146
162
  "anthropic/claude-sonnet-4-6": "sonnet", "anthropic/claude-haiku-4-5": "haiku",
147
163
  };
@@ -552,7 +568,16 @@ export const anthropicAgentEngine = {
552
568
  // It never falls back to the requested alias — a record that says "actual: <what we asked for>"
553
569
  // when nothing was observed is precisely the absence-read-as-a-pass this issue exists to end. The
554
570
  // comparison and the policy live in gateway.mjs; this reports, it does not judge.
555
- let wireModelInit = null, wireModelAssistant = null;
571
+ //
572
+ // A MESSAGE THE CLI WROTE ITSELF NAMES NO MODEL. The CLI labels such a message `<synthetic>` in the
573
+ // model field, measured in testing on Azure Foundry (2026-09-14): with the opus pin naming a
574
+ // deployment that did not exist, the turn exited 1 with the CLI's own error and its assistant event
575
+ // said `<synthetic>`. No model served that turn, so the label is never taken as a served id, and
576
+ // `answeredItself` stops init's answer from standing in for one: init says what the session was
577
+ // configured with, and naming it here would name a model for a turn no model served. A real id on
578
+ // an earlier assistant event of the same turn now stands, because that model did serve a call;
579
+ // before the label was refused, the label that followed overwrote it.
580
+ let wireModelInit = null, wireModelAssistant = null, answeredItself = false;
556
581
  // READS GAUGE (AD-4, 2026-07-30 addendum): which files this turn actually OPENED, from the stream's
557
582
  // completed Read tool_use blocks. The stage prompt OFFERS a set of documents (declared inputs +
558
583
  // skill refs); nothing recorded whether the turn could and did read them — and one review
@@ -703,11 +728,14 @@ export const anthropicAgentEngine = {
703
728
  else if (ev.type === "rate_limit_event") rateLimitEvent = ev;
704
729
  else if (ev.type === "system" && ev.subtype === "init") {
705
730
  // MODEL GAUGE — the session's configured model, the earliest wire statement of what will run.
706
- if (typeof ev.model === "string" && ev.model) wireModelInit ??= ev.model;
731
+ if (typeof ev.model === "string" && ev.model && !isCliLabel(ev.model)) wireModelInit ??= ev.model;
707
732
  }
708
733
  else if (ev.type === "assistant") {
709
734
  // MODEL GAUGE — the model that served THIS API call. Authoritative over init (see above).
710
- if (typeof ev.message?.model === "string" && ev.message.model) wireModelAssistant = ev.message.model;
735
+ if (typeof ev.message?.model === "string" && ev.message.model) {
736
+ if (isCliLabel(ev.message.model)) answeredItself = true;
737
+ else wireModelAssistant = ev.message.model;
738
+ }
711
739
  // THINKING GAUGE — block presence + signature, never the text (display defaults to "omitted",
712
740
  // so an engaged block carries a zero-length `thinking` string). See the declaration above.
713
741
  if (!thought && ev.message?.content?.some?.((b) => b?.type === "thinking")) thought = true;
@@ -1077,8 +1105,13 @@ export const anthropicAgentEngine = {
1077
1105
  toolWaitUnmeasurable: [...unmeasurable],
1078
1106
  // MODEL GAUGE: the id the WIRE reported, or null when the stream never said. Assistant
1079
1107
  // message first (what served the call), init second (what the session was configured with).
1080
- // Never the requested alias — see the declaration above.
1081
- modelWire: wireModelAssistant ?? wireModelInit ?? null,
1108
+ // Never the requested alias, and never init's answer for a turn only the CLI answered — see the
1109
+ // declaration above.
1110
+ modelWire: wireModelAssistant ?? (answeredItself ? null : wireModelInit),
1111
+ // PROVIDER GAUGE: the program's own word for who served the turn, read from the result's per-model
1112
+ // usage ("firstParty" on Anthropic's own API and "foundry" on Azure Foundry, measured 2026-09-14),
1113
+ // or null when the stream never said or its models disagree. Recorded, never inferred from config.
1114
+ providerWire: providerOf(resultEvent),
1082
1115
  sessionRef: resultEvent?.session_id ?? resumeRef ?? null,
1083
1116
  // The raw result event's total_cost_usd is a provider-side field and stays in the provider's
1084
1117
  // own stream; the tuple carries no currency (tokens-only directive 2026-07-11) — `usage` is
@@ -1116,6 +1149,29 @@ function errResult(t0, e, resumeRef) {
1116
1149
  // reads: a spawn error means NO turn ran — [] is the true observation (nothing was read), not a gap.
1117
1150
  // modelWire: null for the opposite reason — no turn ran, so the wire said nothing about a model, and
1118
1151
  // the record must say UNKNOWN rather than inherit the alias that was asked for.
1119
- json: null, usage: null, reads: [], readsTruncated: false, modelWire: null, sessionRef: resumeRef ?? null,
1152
+ json: null, usage: null, reads: [], readsTruncated: false, modelWire: null, providerWire: null, sessionRef: resumeRef ?? null,
1120
1153
  };
1121
1154
  }
1155
+
1156
+ /**
1157
+ * The program's own word for which provider served a turn, from the result event's per-model usage:
1158
+ * `modelUsage[<model>].provider`, "firstParty" on Anthropic's own API and "foundry" on Azure Foundry
1159
+ * (measured 2026-09-14, CLI 2.1.263). One word when every model the turn used names the same provider;
1160
+ * null when none does or they disagree, because a single word would then be a guess.
1161
+ */
1162
+ export function providerOf(resultEvent) {
1163
+ const words = new Set();
1164
+ for (const u of Object.values(resultEvent?.modelUsage ?? {})) {
1165
+ const w = typeof u?.provider === "string" ? u.provider.trim() : "";
1166
+ if (w) words.add(w);
1167
+ }
1168
+ return words.size === 1 ? [...words][0] : null;
1169
+ }
1170
+
1171
+ /**
1172
+ * Whether a model field holds one of the CLI's own bracketed labels, such as `<synthetic>` on a message
1173
+ * it wrote itself, rather than the id of a model. The reports skip the same shape when they name models.
1174
+ */
1175
+ function isCliLabel(id) {
1176
+ return /^<.*>$/.test(String(id).trim());
1177
+ }
@@ -20,28 +20,147 @@
20
20
  // spelling list let the OpenAI half decide how an Anthropic run bills. deleted the old names, so
21
21
  // there is one variable, only the selected engine is ever consulted, and the hazard has no route left.
22
22
  //
23
- // And deliberately written OUT at each site rather than through a helper: the guard in
24
- // `env-governance.test.mjs` finds a product read by the literal `env.NAME`, so a helper taking the
25
- // name as an argument makes both reads invisible to it — measured, it turned them harness-only and
26
- // put both names on the "no longer read by product code" list. The repetition is what keeps them
27
- // visible to the census that has to see them.
23
+ // And deliberately written OUT as a literal rather than passed to a helper as an argument: the env
24
+ // audit finds a product read by the literal `env.NAME`, so a helper taking the name as an argument makes
25
+ // the read invisible to it — measured, it turned the reads harness-only and put the name on the "no
26
+ // longer read by product code" list. `billingMode` below spells the name out, which keeps it visible.
27
+
28
+ // THREE MODES FOR CLAUDE, AND NOTHING ELSE IS A MODE. `cloud` bills Claude through the reader's own
29
+ // Google, Microsoft or Amazon account, or through a gateway in front of one (ANTHROPIC_BASE_URL). The
30
+ // vendor's program already routes on its own switches, which reach it because the stage environment is
31
+ // the driver's; what was missing was a billing word that says so. Without it a cloud machine had two
32
+ // choices and both were wrong: `api-key` refused for want of an Anthropic key the machine does not have,
33
+ // and `subscription` ran and stamped every row as billed to a subscription nobody was paying.
34
+ //
35
+ // AN UNKNOWN WORD IS REFUSED. It used to run as `subscription` on both engines, so a typo in the one
36
+ // setting that decides who pays was a quiet bill to the wrong account. It refuses here, at the top of
37
+ // runStage, in the probe and in the jx runner, before any turn runs.
38
+ export const BILLING_MODES = Object.freeze(["subscription", "api-key", "cloud"]);
39
+
40
+ /**
41
+ * The billing word as the environment writes it, normalised and NOT validated: unset or blank reads as
42
+ * the default. The one parse of the word. `resolveAuthMode` validates it; the anthropic adapter's
43
+ * `spawnEnv`, which must never throw, reads it through here rather than parsing it a second way.
44
+ */
45
+ export function billingMode(env = process.env) {
46
+ return String(env.CLEAROTRON_AI_BILLING ?? "").trim().toLowerCase() || "subscription";
47
+ }
48
+
49
+ // The vendor's own switches, read the way its program reads them: "1", "true", "yes" or "on" switches
50
+ // one on. Spelled out one per line for the reason given above. The order is only the order a refusal
51
+ // names them in.
52
+ const switchedOn = (v) => ["1", "true", "yes", "on"].includes(String(v ?? "").trim().toLowerCase());
53
+ export const CLOUD_SWITCH = Object.freeze({ vertex: "CLAUDE_CODE_USE_VERTEX", foundry: "CLAUDE_CODE_USE_FOUNDRY", bedrock: "CLAUDE_CODE_USE_BEDROCK" });
54
+
55
+ // EVERY NAME THE PROGRAM READS TO REACH AND PAY A CLOUD, as setup writes them and the install page lists
56
+ // them: each cloud's switch and its least settings, the gateway pair, and the four model pins a cloud
57
+ // deployment is named by. A run takes every line of its settings file, so it has these already. Setup's
58
+ // proof turn and doctor read the file name by name, and carry these so a check proves the account a run
59
+ // bills rather than whatever the shell happened to hold.
60
+ //
61
+ // THE FABLE PIN IS ON IT, THOUGH SETUP NEVER ASKS FOR IT. The program reads a pin for every tier it takes as
62
+ // an alias, fable included, and no stage asks for fable unless an override names it. On Foundry that
63
+ // alias resolves to nothing unless the pin names a deployment, so a reader who sets the override sets the
64
+ // pin by hand, and doctor, setup's proof turn and a background start must carry it like the other three.
65
+ //
66
+ // THE STANDARD AWS KEY VARIABLES ARE ON IT. A machine with no AWS profile and no instance role keeps its
67
+ // Amazon keys in the settings file, and a search reads them from there. Without these three names doctor's
68
+ // proof turn ran without the keys and reported a fault on a machine whose searches worked.
69
+ export const CLOUD_SETTINGS = Object.freeze([
70
+ ...Object.values(CLOUD_SWITCH),
71
+ "ANTHROPIC_VERTEX_PROJECT_ID", "CLOUD_ML_REGION", "GOOGLE_APPLICATION_CREDENTIALS",
72
+ "ANTHROPIC_FOUNDRY_RESOURCE", "ANTHROPIC_FOUNDRY_API_KEY",
73
+ "AWS_REGION", "AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN",
74
+ "ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN",
75
+ "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "ANTHROPIC_DEFAULT_FABLE_MODEL",
76
+ ]);
77
+
78
+ // THE ONES THAT HOLD A SECRET, by name. Wherever a cloud setting is shown, one of these is shown as set and
79
+ // never with its value. Named rather than matched by suffix: AWS_ACCESS_KEY_ID ends like the Google project
80
+ // id beside it, and only one of the two is a credential.
81
+ export const CLOUD_SECRETS = Object.freeze([
82
+ "ANTHROPIC_FOUNDRY_API_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "ANTHROPIC_AUTH_TOKEN",
83
+ ]);
84
+
85
+ // WHAT A READER CHECKS WHEN A CLOUD REFUSES THE CREDENTIALS, per cloud: who refused, by the name a reader
86
+ // knows it by, and the settings and sign-in that decide it. Names only, never a value. The remedy for a
87
+ // subscription, run the program once and sign in, means nothing on a machine that pays through a cloud, and
88
+ // it was the only remedy the checks gave. Every setting named here is on CLOUD_SETTINGS, so doctor and
89
+ // setup's proof turn carry what this tells a reader to check.
90
+ export const CLOUD_CREDENTIAL_CHECK = Object.freeze({
91
+ vertex: Object.freeze({ who: "Google Cloud",
92
+ check: "ANTHROPIC_VERTEX_PROJECT_ID, CLOUD_ML_REGION, and the Google sign-in on this machine (gcloud's, or the key GOOGLE_APPLICATION_CREDENTIALS names)" }),
93
+ foundry: Object.freeze({ who: "Microsoft Azure",
94
+ check: "ANTHROPIC_FOUNDRY_API_KEY, or the Azure sign-in on this machine, and ANTHROPIC_FOUNDRY_RESOURCE" }),
95
+ bedrock: Object.freeze({ who: "Amazon Bedrock",
96
+ check: "AWS_REGION and the AWS credentials on this machine (a profile, an instance role, or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)" }),
97
+ gateway: Object.freeze({ who: "the gateway", check: "ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN" }),
98
+ });
99
+
100
+ export function cloudsSwitchedOn(env = process.env) {
101
+ const on = [];
102
+ if (switchedOn(env.CLAUDE_CODE_USE_VERTEX)) on.push("vertex");
103
+ if (switchedOn(env.CLAUDE_CODE_USE_FOUNDRY)) on.push("foundry");
104
+ if (switchedOn(env.CLAUDE_CODE_USE_BEDROCK)) on.push("bedrock");
105
+ return on;
106
+ }
107
+
108
+ // Every refusal below carries `billingRefusal: true`, so a reader classifies it by what it is rather than
109
+ // by its wording: the probe does, and a sign-in error that also names this setting is not one of these.
110
+ const refuse = (message) => Object.assign(new Error(message), { billingRefusal: true });
111
+ const notAMode = (mode, modes) => refuse(
112
+ `CLEAROTRON_AI_BILLING=${mode} is not a billing mode — refusing to guess, because the guess would bill ` +
113
+ `the subscription. One of: ${modes.join(", ")}.`);
28
114
 
29
115
  export function resolveAuthMode({ engineName, env = process.env } = {}) {
30
116
  const name = String(engineName || "").toLowerCase();
31
117
 
32
118
  if (name === "anthropic-agent") {
33
- const mode = (env.CLEAROTRON_AI_BILLING || "subscription").toLowerCase() === "api-key" ? "api-key" : "subscription";
119
+ const mode = billingMode(env);
34
120
  if (mode === "api-key" && !env.ANTHROPIC_API_KEY)
35
- throw new Error(
121
+ throw refuse(
36
122
  `CLEAROTRON_AI_BILLING=api-key but ANTHROPIC_API_KEY is not set — refusing to silently bill the ` +
37
123
  `subscription instead. Set the key, or use CLEAROTRON_AI_BILLING=subscription.`);
38
- return { provider: "anthropic", mode, apiBilled: mode === "api-key" };
124
+ const on = cloudsSwitchedOn(env);
125
+ // A cloud's switch sends the program to that cloud whatever the billing word says. Measured on Foundry,
126
+ // 2026-09-14: with the switch on and the word unset, the program reported Foundry as its provider and
127
+ // the row was stamped as the subscription's. So a switch beside `subscription` or `api-key` is refused,
128
+ // after a missing key, which is the fault the config page names first.
129
+ if ((mode === "subscription" || mode === "api-key") && on.length)
130
+ throw refuse(
131
+ `${on.map((c) => CLOUD_SWITCH[c]).join(" and ")} ${on.length > 1 ? "are" : "is"} on, which sends Claude to ` +
132
+ `that cloud account, while CLEAROTRON_AI_BILLING says ${mode} — refusing rather than record the wrong ` +
133
+ `account. Use CLEAROTRON_AI_BILLING=cloud, or ${on.length > 1 ? "turn them off" : "turn the switch off"}.`);
134
+ if (mode === "subscription") return { provider: "anthropic", mode, apiBilled: false };
135
+ if (mode === "api-key") return { provider: "anthropic", mode, apiBilled: true };
136
+ if (mode === "cloud") {
137
+ if (on.length > 1)
138
+ throw refuse(
139
+ `CLEAROTRON_AI_BILLING=cloud but more than one cloud is switched on ` +
140
+ `(${on.map((c) => CLOUD_SWITCH[c]).join(", ")}) — set exactly one, so the run can say which account it bills.`);
141
+ // A switch names the cloud. ANTHROPIC_BASE_URL alone is the gateway form: a cloud reached through the
142
+ // reader's own proxy. With a switch also set, the switch is what the program routes on.
143
+ const cloud = on[0] ?? (env.ANTHROPIC_BASE_URL ? "gateway" : null);
144
+ if (!cloud)
145
+ throw refuse(
146
+ `CLEAROTRON_AI_BILLING=cloud but none of CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_FOUNDRY, ` +
147
+ `CLAUDE_CODE_USE_BEDROCK or ANTHROPIC_BASE_URL is set — refusing to silently bill the subscription ` +
148
+ `instead. Set the one for your cloud (INSTALL.md), or use CLEAROTRON_AI_BILLING=subscription.`);
149
+ return { provider: "anthropic", mode, apiBilled: true, cloud };
150
+ }
151
+ throw notAMode(mode, BILLING_MODES);
39
152
  }
40
153
 
41
154
  if (name === "openai-agent") {
42
- const mode = (env.CLEAROTRON_AI_BILLING || "subscription").toLowerCase() === "api-key" ? "api-key" : "subscription";
155
+ const mode = billingMode(env);
156
+ if (mode === "cloud")
157
+ throw refuse(
158
+ `CLEAROTRON_AI_BILLING=cloud bills Claude through a cloud account, and this machine runs the Codex ` +
159
+ `engine — refusing rather than billing the ChatGPT subscription instead. Use subscription or api-key ` +
160
+ `with Codex, or CLEAROTRON_AI=anthropic-agent for a cloud account.`);
161
+ if (mode !== "subscription" && mode !== "api-key") throw notAMode(mode, ["subscription", "api-key"]);
43
162
  if (mode === "api-key" && !env.CODEX_API_KEY)
44
- throw new Error(
163
+ throw refuse(
45
164
  `CLEAROTRON_AI_BILLING=api-key but CODEX_API_KEY is not set — refusing to silently bill the ChatGPT ` +
46
165
  `subscription instead. Set the key, or use CLEAROTRON_AI_BILLING=subscription.`);
47
166
  return { provider: "openai", mode, apiBilled: mode === "api-key" };
@@ -68,7 +68,7 @@ export function turnText(tuple) {
68
68
  * Read one normalized tuple into the shape the jx lanes consume. PURE, so every branch is assertable
69
69
  * from a literal rather than from a spawned CLI.
70
70
  */
71
- export function readJxTuple(tuple, { vendor, authMode, engine }) {
71
+ export function readJxTuple(tuple, { vendor, authMode, cloud = null, engine }) {
72
72
  // CANONICAL Usage, whole (engine/CONTRACT.md §2: {input, output, cacheRead, cacheWrite, total}). The old
73
73
  // Messages-API rows carried input/output only because that is all the API returned; keeping only those
74
74
  // two now would drop cache and total tokens from the rollup on the very lanes this change puts on the
@@ -82,7 +82,7 @@ export function readJxTuple(tuple, { vendor, authMode, engine }) {
82
82
  // the receipt names who did the native-language work, and an alias does not name anyone. Both adapters
83
83
  // populate it (anthropic-agent from the assistant/init events, openai-agent from `ev.model`).
84
84
  const model = tuple?.modelWire ?? null;
85
- const base = { model, vendor, authMode, usage, engine };
85
+ const base = { model, vendor, authMode, cloud, usage, engine };
86
86
  // WHETHER TRUNCATION IS OBSERVABLE IS A FACT ABOUT THE ADAPTER, NOT ABOUT THIS TURN. It is keyed on the
87
87
  // engine deliberately: `anthropic-agent` writes `stopReason: r?.stop_reason` unconditionally, so the
88
88
  // KEY is present on every one of its turns whether or not the wire said anything — testing for the key
@@ -108,7 +108,7 @@ export function readJxTuple(tuple, { vendor, authMode, engine }) {
108
108
  /**
109
109
  * A `turn` runner for the jx lanes, bound to the run's engine and billing mode.
110
110
  *
111
- * Returns `{ turn, vendor, authMode, engine }`, or `{ error }` when the configuration refuses — the
111
+ * Returns `{ turn, vendor, authMode, cloud, engine }`, or `{ error }` when the configuration refuses — the
112
112
  * caller degrades the lane with that cause rather than this throwing into a pipeline stage.
113
113
  */
114
114
  export async function makeJxTurnRunner({
@@ -149,16 +149,17 @@ export async function makeJxTurnRunner({
149
149
 
150
150
  const vendor = auth.provider;
151
151
  const authMode = auth.mode;
152
+ const cloud = auth.cloud ?? null; // which cloud account bills, under the cloud mode; null otherwise
152
153
  return {
153
- vendor, authMode, engine: id,
154
+ vendor, authMode, cloud, engine: id,
154
155
  async turn({ prompt }) {
155
156
  let tuple;
156
157
  try { tuple = await runTurn({ message: prompt, model, thinking, timeoutSec, stallSec }); }
157
158
  catch (e) {
158
159
  return { ok: false, cause: `the engine turn threw: ${String(e?.message ?? e).slice(0, 200)}`,
159
- model: null, vendor, authMode, engine: id, usage: { input: 0, output: 0 }, truncationObservable: false };
160
+ model: null, vendor, authMode, cloud, engine: id, usage: { input: 0, output: 0 }, truncationObservable: false };
160
161
  }
161
- return readJxTuple(tuple, { vendor, authMode, engine: id });
162
+ return readJxTuple(tuple, { vendor, authMode, cloud, engine: id });
162
163
  },
163
164
  };
164
165
  }
@@ -14,7 +14,7 @@ The active register provider is mounted under the single, neutral server key `re
14
14
  named `register_*` — so the namespaced ids the prompts and allowlists carry (`mcp__register__register_enumerate`)
15
15
  stay stable across a provider swap. What is PINNED is the vendor's TOOL tokens, not its name:
16
16
  `../../test/provider-neutral-prose.test.mjs` walks `driver/` (skipping `fixtures/`) and fails any `<vendor>_<tool>`
17
- outside the six `<provider>-server.mjs` files and `../../skills/prelim-register/providers/`, exempting a core's own
17
+ outside the six `<provider>-server.mjs` files and `../../skills/clearance-register/providers/`, exempting a core's own
18
18
  `ERROR: … HTTP` diagnostics. The plain vendor NAME is unrestricted: `REGISTER_SERVERS` is keyed by it, and 134 other
19
19
  files under `driver/` carry `corsearch`.
20
20
 
@@ -12,7 +12,7 @@
12
12
  // held by FOUR stages — `common-law`, `common-law-half`, `narrative-refutation`, `synthesis`. Because
13
13
  // `allowedToolsFor` enumerates every tool on every entry a group resolves to, all four carried
14
14
  // `mcp__perplexity__record_dispositions`, while every doctrinal mention of the tool is common-law's:
15
- // `driver/skills/prelim-common-law/SKILL.md`, and the two common-law stage dictations in `stages.mjs`.
15
+ // `driver/skills/clearance-common-law/SKILL.md`, and the two common-law stage dictations in `stages.mjs`.
16
16
  // Zero occurrences in synthesis's dictation block, zero in narrative-refutation's doctrine.
17
17
  //
18
18
  // That is GRANTED-BUT-NEVER-ORDERED, the defect class, in its mirror form: not a stage ordered to
@@ -75,8 +75,8 @@ async function record_dispositions(params) {
75
75
  let spec;
76
76
  try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
77
77
  catch (err) { return { isError: true, text: `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.` }; }
78
- if (!/\/studio\/prelim-search\//.test(spec.output_path))
79
- return { isError: true, text: `ERROR: grid spec.output_path must be within a studio/prelim-search run dir; got ${spec.output_path}` };
78
+ if (!/\/studio\/(?:prelim|clearance)-search\//.test(spec.output_path)) // either spelling: an install keeps the studio segment it has
79
+ return { isError: true, text: `ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}` };
80
80
  // NEVER THROWN PAST THIS POINT. An exception surfaces to the seat as a tool error naming no row, which
81
81
  // tells it nothing about what to fix — the failure mode this transport exists to end.
82
82
  try {
@@ -23,7 +23,7 @@ const BRIDGE = process.env.CLEAROTRON_OAUTH_BRIDGE || join(MCP_DIR, "..", "..",
23
23
  // ── The register surface is PROVIDER-NEUTRAL ────────────────────────────────────────────────────
24
24
  // The active register provider is mounted under the single server key `register`, and every register
25
25
  // tool is named `register_*`. The vendor's name survives ONLY inside its own *-server.mjs file and in
26
- // skills/prelim-register/providers/<provider>.md (the vocabulary doc the spawns are told to read).
26
+ // skills/clearance-register/providers/<provider>.md (the vocabulary doc the spawns are told to read).
27
27
  //
28
28
  // Why neutral names rather than interpolating `${PROVIDER}_enumerate` into the prose: the register
29
29
  // instructions in stages.mjs / pipeline.mjs / gateway.mjs are then correct BY CONSTRUCTION for every
@@ -275,7 +275,7 @@ const RECORDING = Object.freeze({
275
275
  + "DISCOVERY over the run dir (O3c: 21 calls, 1 write / 15 attempts, `ls`/`find`/`cat`), not the "
276
276
  + "enumerable pair frame-diff's Read grant covers",
277
277
  },
278
- // FIFTH — prelim-variants, conversion 3. Classes 3 + 2, and the CLASS 3 half is what makes it
278
+ // FIFTH — clearance-variants, conversion 3. Classes 3 + 2, and the CLASS 3 half is what makes it
279
279
  // different from every conversion before it: O3c measured 9 Bash calls with 4 WRITES across 15
280
280
  // attempts, and the shape is `python3 -c` over `variant-manifest.json` — the seat PRE-CHECKING its own
281
281
  // JSON before saving it. That is a constraint the transport can check, so under the design's Class 3
@@ -287,12 +287,12 @@ const RECORDING = Object.freeze({
287
287
  // enumerable — it derives the manifest from material already on disk that the dictation names — so the
288
288
  // seeded `Read` grant carries them. Granting a search tool to a stage whose reads can be listed is
289
289
  // what the sanctioned-equivalents design refuses. Same ruling as frame-diff, opposite to matter-frame.
290
- "prelim-variants": {
290
+ "clearance-variants": {
291
291
  seatWrites: false,
292
- tools: Object.freeze(["record_prelim_variants"]),
292
+ tools: Object.freeze(["record_clearance_variants"]),
293
293
  reason: "hands back the variant manifest — mark, dominant element, elements, variants with their "
294
294
  + "romanisations, incumbent classes, watchlist owners and the scope-ledger rows — through "
295
- + "record_prelim_variants instead of hand-formatting a JSON skeleton the driver already "
295
+ + "record_clearance_variants instead of hand-formatting a JSON skeleton the driver already "
296
296
  + "strict-parses and a prose twin that restates it; the driver serialises variant-manifest.json, "
297
297
  + "renders variant-manifest.md, and derives scope-ledger.json from the same values rather than by "
298
298
  + "re-parsing a markdown table the seat typed. Gains NO retrieval server and NO search tool (its "
@@ -320,7 +320,7 @@ const RECORDING = Object.freeze({
320
320
  // NO `search_run_artifacts`: the dictation names this stage's ONLY two inputs — the settled narrative
321
321
  // and findings.json — and already trimmed its declared reads to exactly those two. A read set that
322
322
  // short is enumerable by definition, and the design forbids handing a search tool to a stage whose
323
- // reads can be listed. Same ruling as frame-diff and prelim-variants.
323
+ // reads can be listed. Same ruling as frame-diff and clearance-variants.
324
324
  "report-overview": {
325
325
  seatWrites: false,
326
326
  tools: Object.freeze(["record_report_overview"]),
@@ -671,7 +671,7 @@ const LOCAL = {
671
671
  //
672
672
  // GRANTED BY EXACTLY ONE LANE'S GROUP LIST — `common-law` and its `common-law-half` variant, through
673
673
  // the prefix branch in toolGroupsForStage. Every doctrinal mention of the tool is theirs:
674
- // driver/skills/prelim-common-law/SKILL.md and the two common-law stage dictations in stages.mjs.
674
+ // driver/skills/clearance-common-law/SKILL.md and the two common-law stage dictations in stages.mjs.
675
675
  //
676
676
  // NOT an allowlist growing by a token this time: it SHRINKS synthesis's and narrative-refutation's
677
677
  // argv and RENAMES the token on common-law's (`mcp__perplexity__record_dispositions` →
@@ -1222,8 +1222,8 @@ export const RECORDING_TOOLS = Object.freeze({
1222
1222
  // so deriving it would compare a value with itself. matter-frame carries the search tool for the same
1223
1223
  // reason skeptic does, on its OWN key — a shared key would hand skeptic a writer into the frame.
1224
1224
  // BY HAND, like every row here — O1 compares the resolved grant against it, so a derived row would
1225
- // compare a value with itself. prelim-variants carries NO search tool: enumerable inputs, Read serves.
1226
- "prelim-variants": Object.freeze(["Read", "mcp__recording-prelim-variants__record_prelim_variants"]),
1225
+ // compare a value with itself. clearance-variants carries NO search tool: enumerable inputs, Read serves.
1226
+ "clearance-variants": Object.freeze(["Read", "mcp__recording-clearance-variants__record_clearance_variants"]),
1227
1227
  "matter-frame": Object.freeze(["Read", "mcp__recording-matter-frame__record_matter_frame",
1228
1228
  "mcp__recording-matter-frame__search_run_artifacts"]),
1229
1229
  skeptic: Object.freeze(["Read", "mcp__recording-skeptic__record_skeptic",
@@ -154,8 +154,8 @@ async function research(params) {
154
154
  let spec;
155
155
  try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
156
156
  catch (err) { return `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.`; }
157
- if (!/\/studio\/prelim-search\//.test(spec.output_path))
158
- return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/prelim-search run dir; got ${spec.output_path}`, { spec, gridSpecPath: grid_spec_path });
157
+ if (!/\/studio\/(?:prelim|clearance)-search\//.test(spec.output_path)) // either spelling: an install keeps the studio segment it has
158
+ return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}`, { spec, gridSpecPath: grid_spec_path });
159
159
  // — already recorded and complete? Answer from the ledger; do not buy the grid twice.
160
160
  const already = recordedLedgerFor(spec);
161
161
  if (already) {
@@ -44,7 +44,7 @@ import { VARIANT_DIRECTIONS, RANKING_BASES } from "../../blind-frame-model.mjs";
44
44
  import { recordSkeptic } from "../../skeptic-record.mjs";
45
45
  import { recordFrameDiff } from "../../frame-diff-record.mjs";
46
46
  import { recordMatterFrame, INTAKE_ASK_OWNERS, SCOPE_BASES } from "../../matter-frame-record.mjs";
47
- import { recordPrelimVariants, SCOPE_LAYERS, SCOPE_STATUS } from "../../prelim-variants-record.mjs";
47
+ import { recordClearanceVariants, SCOPE_LAYERS, SCOPE_STATUS } from "../../clearance-variants-record.mjs";
48
48
  import { recordReportOverview } from "../../report-overview-record.mjs";
49
49
  import { recordReportCard } from "../../report-card-record.mjs"; // conversion 5 — the fan-out transport
50
50
  import { recordRefutation, REVIEW_VERDICTS } from "../../narrative-refutation-record.mjs"; // conversion 9
@@ -125,12 +125,12 @@ async function record_knockout_review(params) {
125
125
  return recordKnockoutReview(runDir, params);
126
126
  }
127
127
 
128
- async function record_prelim_variants(params) {
128
+ async function record_clearance_variants(params) {
129
129
  const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
130
130
  if (!runDir) {
131
131
  return { error: "this server was started without a run — the driver wires it per run; there is no parameter for it and this tool never guesses one" };
132
132
  }
133
- return recordPrelimVariants(runDir, params);
133
+ return recordClearanceVariants(runDir, params);
134
134
  }
135
135
 
136
136
  async function record_report_card(params) {
@@ -444,7 +444,7 @@ serve({
444
444
  //
445
445
  // `scope_ledger` is the one genuinely new field, and it is what deletes a derivation: those rows used
446
446
  // to reach the driver only by re-parsing a markdown table out of the prose manifest.
447
- name: "record_prelim_variants",
447
+ name: "record_clearance_variants",
448
448
  description:
449
449
  "Hand back the variant manifest as VALUES. The driver serialises variant-manifest.json, renders " +
450
450
  "variant-manifest.md and writes scope-ledger.json from what you send, so you never format JSON, " +
@@ -516,7 +516,7 @@ serve({
516
516
  },
517
517
  },
518
518
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
519
- handler: record_prelim_variants,
519
+ handler: record_clearance_variants,
520
520
  }, {
521
521
  // ── CONVERSION 4 — THE REPORT SHELL, AND THE FIRST ARTIFACT A CLIENT READS ────────────────────
522
522
  //
@@ -627,6 +627,19 @@ serve({
627
627
  },
628
628
  },
629
629
  },
630
+ batch: {
631
+ type: "integer",
632
+ minimum: 1,
633
+ description:
634
+ "The batch of records this call accounts for, when the dispatch splits the band into " +
635
+ "batches. Send one call per batch, carrying its number. Every record in THAT batch must end " +
636
+ "in this call — a findings row, an incumbent row, a Negative-results drop, or a " +
637
+ "Disagreement resolution — and the call is refused naming any that end nowhere; the records " +
638
+ "in every other batch are not this call's business. A batch call MERGES onto what you have " +
639
+ "already recorded, so earlier batches are kept without re-sending them, and a record ended " +
640
+ "under one batch cannot be ended again under another. Omit it only when you are sending the " +
641
+ "whole band in one call, which the dispatch tells you when it is.",
642
+ },
630
643
  patch: {
631
644
  type: "boolean",
632
645
  description: