clearotron 0.3.2-beta.7 → 0.3.2-beta.8

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 (85) hide show
  1. package/.env.example +24 -23
  2. package/INSTALL.md +142 -75
  3. package/README.md +3 -3
  4. package/bin/onboard.mjs +637 -216
  5. package/bin/start.mjs +133 -23
  6. package/bin/update.mjs +58 -11
  7. package/build-info.json +2 -2
  8. package/docs/architecture/04-configuration-reference.md +26 -11
  9. package/docs/architecture/05-config-governance.md +17 -7
  10. package/driver/CHANGELOG.md +76 -0
  11. package/driver/band-size.mjs +59 -0
  12. package/driver/config-inventory.mjs +112 -9
  13. package/driver/contract-arm2-baseline.json +1 -3
  14. package/driver/contract-e3-backlog.mjs +26 -26
  15. package/driver/contract-vocabulary.mjs +44 -10
  16. package/driver/door-gates.mjs +41 -7
  17. package/driver/driver.config.mjs +272 -59
  18. package/driver/engine/CONTRACT.md +10 -3
  19. package/driver/engine/README.md +2 -2
  20. package/driver/engine/anthropic-agent.mjs +77 -21
  21. package/driver/engine/auth.mjs +129 -10
  22. package/driver/engine/jx-turn.mjs +7 -6
  23. package/driver/engine/mcp/recording-server.mjs +13 -0
  24. package/driver/engine/openai-agent.mjs +4 -2
  25. package/driver/engine/probe.mjs +110 -23
  26. package/driver/findings-model.mjs +1 -1
  27. package/driver/flag-snapshot.mjs +28 -5
  28. package/driver/gateway.mjs +24 -18
  29. package/driver/jx-lanes.mjs +21 -2
  30. package/driver/jx-units.mjs +6 -3
  31. package/driver/jx.mjs +4 -2
  32. package/driver/matter-frame-record.mjs +90 -1
  33. package/driver/named-band.mjs +34 -2
  34. package/driver/package.json +1 -1
  35. package/driver/pipeline.mjs +200 -23
  36. package/driver/portal-config-view.mjs +30 -1
  37. package/driver/portal-report.mjs +15 -1
  38. package/driver/portal-service.mjs +46 -6
  39. package/driver/predelivery-lint.mjs +12 -2
  40. package/driver/publish/index.mjs +46 -5
  41. package/driver/publish/knockout.mjs +10 -1
  42. package/driver/publish/render-knockout.mjs +69 -7
  43. package/driver/publish/render.mjs +170 -59
  44. package/driver/publish/report-data.mjs +4 -1
  45. package/driver/publish/report-topbar.mjs +58 -0
  46. package/driver/publish/templates/report.css +18 -1
  47. package/driver/publish/xlsx.mjs +13 -1
  48. package/driver/register-availability.mjs +2 -2
  49. package/driver/register-coverage.mjs +94 -1
  50. package/driver/register-digest-record.mjs +236 -11
  51. package/driver/register-plan.mjs +170 -0
  52. package/driver/result-noun-fields.mjs +2 -2
  53. package/driver/run-economics.mjs +41 -10
  54. package/driver/run-requirements.mjs +173 -9
  55. package/driver/runner.mjs +3 -3
  56. package/driver/stages.mjs +12 -8
  57. package/driver/suite-census.json +142 -64
  58. package/driver/systemd/README.md +7 -4
  59. package/driver/terminal-clamp.mjs +107 -1
  60. package/driver/tokens.mjs +169 -3
  61. package/driver/unit-environment.mjs +42 -15
  62. package/driver/unit-inventory.mjs +19 -2
  63. package/driver/verify.mjs +27 -0
  64. package/mcp-server/CHANGELOG.md +4 -0
  65. package/mcp-server/package.json +1 -1
  66. package/mcp-server/server.mjs +15 -1
  67. package/package.json +1 -1
  68. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
  69. package/portal-ui/dist/index.html +1 -1
  70. package/portal-ui/package.json +1 -1
  71. package/providers/jx/README.md +2 -1
  72. package/providers/jx/src/turn-envelope.mjs +8 -3
  73. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  74. package/providers/oauth-mcp-bridge/package.json +1 -1
  75. package/providers/uspto-local/README.md +1 -1
  76. package/scripts/authority-boundary-probe.mjs +4 -2
  77. package/scripts/env-audit.mjs +12 -6
  78. package/scripts/freeze-example-run.mjs +49 -16
  79. package/scripts/generated-files-are-current.mjs +69 -4
  80. package/scripts/settings-render-check.mjs +75 -2
  81. package/scripts/test-full.mjs +96 -3
  82. package/scripts/test-run.mjs +10 -0
  83. package/shared/deployment-box.mjs +7 -2
  84. package/shared/driver-dir.mjs +1 -1
  85. package/shared/names-in-force.mjs +1 -1
@@ -14,11 +14,14 @@
14
14
  // the provider's four separately-priced token kinds.)
15
15
  //
16
16
  // ── WHAT A "DISPATCH" IS ──────────────────────────────────────────────────────────────────────────
17
- // One model invocation: one row in `_driver/<stage>.jsonl` carrying a `model` (gateway.mjs writes one
18
- // per ATTEMPT, so retries are separate dispatches and retry waste is counted, not averaged away). The
19
- // direct-API jx lanes bypass the gateway and write `_driver/jx-completions.jsonl` in the same
20
- // {model, usage} shape; they are dispatches too, under stage `jx-completions`. `run.jsonl` is skipped
21
- // (run events, not dispatches) — same file selection as tokens.mjs, deliberately.
17
+ // One model invocation: one row in `_driver/<stage>.jsonl` that tokens.mjs's `isAttemptRow` counts as
18
+ // a provider attempt (gateway.mjs writes one per ATTEMPT, so retries are separate dispatches and retry
19
+ // waste is counted, not averaged away). The direct-API jx lanes bypass the gateway and write
20
+ // `_driver/jx-completions.jsonl` in the same {model, usage} shape; they are dispatches too, under stage
21
+ // `jx-completions`. `run.jsonl` is skipped (run events, not dispatches) — same file selection as
22
+ // tokens.mjs, deliberately, and the SAME ROW TEST as tokens.mjs, imported rather than copied: a jx row
23
+ // whose turn ran and named no model carries no `model`, only `modelActual: null`, and a census that still
24
+ // asked for a `model` counted no dispatch and no tokens for a turn the token rollup counted with both.
22
25
  //
23
26
  // ── ZERO SEMANTICS: THE THING THIS MODULE EXISTS TO GET RIGHT ─────────────────────────────────────
24
27
  // tokens.mjs sums `usage` with `u.output || 0`, so a dispatch whose usage is null contributes zero AND
@@ -32,7 +35,7 @@
32
35
  // measured — the dispatch journalled a usage object (from the provider's own result envelope)
33
36
  // streamed — usage present but RECONSTRUCTED from the stream (`signals.usageStreamed`), because the
34
37
  // turn died before its result event. A real measurement, a weaker one, counted apart.
35
- // unmeasured — the row is a dispatch (it has a model) and carries no usage at all. THE KILLED TURNS.
38
+ // unmeasured — the row is a dispatch and carries no usage at all. THE KILLED TURNS.
36
39
  // `tokensComplete` is false whenever `unmeasured > 0`, at run level and per stage, and
37
40
  // `unmeasuredDispatches[]` names which ones so a reader can see what the total is missing.
38
41
  //
@@ -92,14 +95,18 @@
92
95
  // so a per-record basis is the only honest shape. `tokens.mjs` keys its rollup on the requested alias
93
96
  // for the same reason and is likewise untouched.
94
97
  //
95
- // Pure by contract: `runEconomics()` reads the run dir and nothing else — no env, no config, no network,
96
- // no driver imports. `stampRunEconomics()` is the only part that writes.
98
+ // Pure by contract: `runEconomics()` reads the run dir and nothing else — no env, no config, no network.
99
+ // Its only driver import is the attempt-row test it shares with tokens.mjs; the log and status writers
100
+ // serve `stampRunEconomics()`, the only part that writes.
97
101
 
98
102
  import { readdirSync, readFileSync, writeFileSync } from "node:fs";
99
103
  import { join } from "node:path";
100
104
  import { driverDir } from "../shared/driver-dir.mjs"; //
101
105
  import { runLog, note } from "./log.mjs";
102
106
  import { writeRunStatus } from "./progress.mjs";
107
+ // tokens.mjs imports this module too (isCodeSide, stampRunEconomics). The cycle is safe because each side
108
+ // reads the other's bindings only inside functions, never while the module is loading.
109
+ import { isAttemptRow } from "./tokens.mjs";
103
110
 
104
111
  /**
105
112
  * The provider's separately-priced token kinds, in the driver's own `usage` vocabulary (gateway.mjs /
@@ -178,7 +185,23 @@ function billingKeyOf(rec) {
178
185
  // is rather than dragged into "unknown" beside genuinely unstamped legacy rows.
179
186
  const engine = isCodeSide(rec) ? "code" : String(rec.engine ?? "unknown");
180
187
  const authMode = isCodeSide(rec) ? "not-provider-billed" : String(rec.authMode ?? "unknown");
181
- const model = String(rec.modelUsed ?? rec.model ?? "unknown");
188
+ // A TURN THAT NAMED NO MODEL (a jx row with `modelActual: null` and no `model`) is keyed the way the
189
+ // token rollup keys it (modelKey in tokens.mjs): `<engine>/no-model-reported`, a name that says the
190
+ // model is missing. Read through the old `?? "unknown"` it landed beside legacy rows nobody stamped,
191
+ // and its byBilling bucket named a different model from the rollup's byModel for the same turn.
192
+ //
193
+ // A COPY OF modelKey's RULE, NOT A SHARED ONE: tokens.mjs does not export it. So the test for "no stamp"
194
+ // is modelKey's own, a non-empty string `modelUsed`, and not `modelUsed == null`: under that looser test
195
+ // a row stamped `modelUsed: ""` keyed its bucket as the empty string while the rollup keyed the same
196
+ // turn `<engine>/no-model-reported`. Two copies of one rule drifting apart is how the census and the
197
+ // rollup came to disagree about what an attempt is, so the tests hold these two copies to each other on
198
+ // the rows the engine writes. They still part on a row no writer produces: no model and no engine, or
199
+ // engine `anthropic-agent`. This key names the missing model there, while modelKey resolves the absent
200
+ // model through the catalog before it asks whether one exists, and buckets the row as `undefined`.
201
+ const stamped = typeof rec.modelUsed === "string" && rec.modelUsed;
202
+ const model = !stamped && typeof rec.model !== "string"
203
+ ? `${typeof rec.engine === "string" && rec.engine ? rec.engine : "unknown"}/no-model-reported`
204
+ : String(rec.modelUsed ?? rec.model ?? "unknown");
182
205
  return { engine, authMode, model, key: `${engine}|${authMode}|${model}` };
183
206
  }
184
207
 
@@ -223,11 +246,19 @@ function foldBilling(bucketMap, rec, cls) {
223
246
  // future engine whose name began that way, which is how a vendor claim becomes a guess. An engine this
224
247
  // table does not know is reported BY NAME and blocks the single-vendor claim, because "I do not know who
225
248
  // billed this" and "one vendor" are different answers and only one of them is safe to print.
249
+ //
250
+ // THE NATIVE-LANGUAGE ROWS STAMP THE VENDOR ITSELF. jxBillingStamp (jx-lanes.mjs) writes as the row's
251
+ // engine the provider the engine door resolved, `anthropic` or `openai` (engine/auth.mjs), so those two
252
+ // names are engines this table must place. Without them an Anthropic-only run with a native-language turn
253
+ // named `anthropic` as an engine that bills to no vendor, in the same sentence that named anthropic as its
254
+ // one vendor. Two exact names, still a closed table.
226
255
  export const ENGINE_VENDORS = Object.freeze({
227
256
  "anthropic-agent": "anthropic",
228
257
  "anthropic-direct": "anthropic",
229
258
  "anthropic-completions": "anthropic",
230
259
  "openai-agent": "openai",
260
+ "anthropic": "anthropic",
261
+ "openai": "openai",
231
262
  });
232
263
  /** The vendor an engine bills to, or null when the table does not name one. */
233
264
  export const vendorOf = (engine) => ENGINE_VENDORS[String(engine ?? "")] ?? null;
@@ -469,7 +500,7 @@ export function runEconomics(runDir, { now = null, bytesPerOutputToken = BYTES_P
469
500
  let sawDeclaredNoOutput = false;
470
501
 
471
502
  for (const rec of rows) {
472
- if (!rec || typeof rec.model !== "string") continue; // only dispatch rows carry a model
503
+ if (!isAttemptRow(rec)) continue; // only provider attempts are dispatches (tokens.mjs, isAttemptRow)
473
504
  const cls = classesOf(rec.usage);
474
505
  const streamed = cls != null && rec.signals?.usageStreamed === true;
475
506
 
@@ -44,6 +44,9 @@
44
44
  // BLOCKING — without these nothing runs at all, in any product. The register and its credential (the
45
45
  // driver throws by name at the first stage), the engine and the binary it drives, and the pool the
46
46
  // report is written into. This is the set whose absence produced the outcome at the top of this file.
47
+ // And what the billing word needs, because the run door refuses without it before any turn: under
48
+ // `cloud` the switch of the cloud it pays through (or the gateway address), under `api-key` the
49
+ // engine's key; and the billing word itself, whenever the run door refuses the way it is set.
47
50
  //
48
51
  // NARROWING — `PERPLEXITY_API_KEY`. Its absence does NOT crash a run and does not deliver a false
49
52
  // notice: the three clearance searches carry the common-law grid and cannot switch it off, so they
@@ -71,13 +74,16 @@
71
74
  // Refusing here is refusing over OUR bug, and there is nothing for a reader to go and set.
72
75
  //
73
76
  // at:"order" the value an OPERATOR supplies — the register, its credential, the engine and the
74
- // binary it drives. Absent, the install is not finished. The doors come up and every run is refused
75
- // AT ORDER TIME, before a stage dispatches and before anything is spent, naming what is missing.
77
+ // binary it drives, and what the billing word needs (above). Absent, the install is not finished.
78
+ // The doors come up and every run is refused AT ORDER TIME, before a stage dispatches and before
79
+ // anything is spent, naming what is missing.
76
80
  //
77
81
  // The axis is on the ROW, not in the caller, for the reason the whole module exists: a start that
78
82
  // decides for itself which names are its own and a runner that decides separately are two opinions
79
83
  // about one list, and they drift in the direction where the second asks for less.
80
84
 
85
+ import { BILLING_MODES, CLOUD_SWITCH, CLOUD_SETTINGS, CLOUD_CREDENTIAL_CHECK, billingMode, cloudsSwitchedOn, resolveAuthMode } from "./engine/auth.mjs"; // — the billing row asks the one resolver which ways an engine can be paid for, and the cloud rows read its own lists
86
+
81
87
  /** The pool a report is written into. Named once; the supervisor already writes it. */
82
88
  export const POOL_ENV = "CLEAROTRON_REPORTS_DIR";
83
89
  /** The register selection, and the engine selection. */
@@ -85,6 +91,9 @@ export const REGISTER_ENV = "CLEAROTRON_DATABASE";
85
91
  export const ENGINE_ENV = "CLEAROTRON_AI";
86
92
  /** Narrowing, never blocking — see the header. */
87
93
  export const RESEARCH_ENV = "PERPLEXITY_API_KEY";
94
+ /** The settings that decide WHICH cloud a Claude turn goes to: each cloud's switch, and the gateway address.
95
+ * Any one of them answers the cloud billing word; start compares all of them against the services' file. */
96
+ export const CLOUD_ROUTES = Object.freeze([...Object.values(CLOUD_SWITCH), "ANTHROPIC_BASE_URL"]);
88
97
 
89
98
  /** WHEN a blocking value is asked for. `START` is what `clearotron start` writes itself; `ORDER` is what
90
99
  * an operator configures, and its absence refuses a RUN rather than an install. See the header. */
@@ -93,6 +102,35 @@ export const ORDER = "order";
93
102
 
94
103
  const val = (env, name) => String(env?.[name] ?? "").trim();
95
104
 
105
+ // THE WAYS AN ENGINE CAN BE PAID FOR, AS THE BILLING ROW SAYS THEM. The row named all three for every engine,
106
+ // and Codex refuses a cloud account (engine/auth.mjs, resolveAuthMode), so a Codex install was offered a way
107
+ // to pay its run door refuses.
108
+ const PAY_WORDS = Object.freeze({ subscription: "subscription", "api-key": "API key", cloud: "cloud account" });
109
+
110
+ /**
111
+ * The billing words `engineId` can be paid by, in BILLING_MODES order: each one asked of the resolver, the
112
+ * one authority on it, with what that word needs set (the engine's key for `api-key`, a cloud's switch for
113
+ * `cloud`), so a word the resolver accepts only when its settings are present is not read as refused. An
114
+ * import of a leaf with no imports of its own, so this module stays pure.
115
+ */
116
+ export function payWays(engineId, engine = {}) {
117
+ return BILLING_MODES.filter((mode) => {
118
+ const env = { [engine.authEnv ?? "CLEAROTRON_AI_BILLING"]: mode,
119
+ ...(mode === "api-key" && engine.apiKeyEnv ? { [engine.apiKeyEnv]: "set" } : {}),
120
+ ...(mode === "cloud" ? { [CLOUD_SWITCH.vertex]: "1" } : {}) };
121
+ try { resolveAuthMode({ engineName: engineId, env }); return true; } catch { return false; }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * A billing refusal from the run door, in words that carry no value. Every refusal names settings and the
127
+ * billing word, except the one for a word that is not a billing mode, which quotes the word as it is set;
128
+ * that quote is replaced by the setting's name. PURE. Any other message comes back unchanged.
129
+ */
130
+ export function billingRefusalWords(message) {
131
+ return String(message ?? "").replace(/^([A-Z][A-Z0-9_]*)=[\s\S]*? is not a billing mode/, "$1 is set to a word that is not a billing mode");
132
+ }
133
+
96
134
  /**
97
135
  * Every environment name this box's configuration says a clearance needs, with the reason each one is
98
136
  * there and whether its absence blocks or narrows.
@@ -101,9 +139,9 @@ const val = (env, name) => String(env?.[name] ?? "").trim();
101
139
  * environment in hand: the composer has the supervisor's, and the guard has the one it just wrote into
102
140
  * the unit file. A function that read the ambient environment would answer about neither.
103
141
  *
104
- * PURE.
142
+ * PURE, apart from the engine resolver a caller passes in `tables.resolveEngine` (see the engine row).
105
143
  */
106
- export function runRequirements(env = {}, { registers = [], engines = {}, defaultEngine = null } = {}) {
144
+ export function runRequirements(env = {}, { registers = [], engines = {}, defaultEngine = null, resolveEngine = null } = {}) {
107
145
  const out = [];
108
146
  const push = (name, blocking, why, at = ORDER) =>
109
147
  out.push({ name, blocking, why, at, present: Boolean(val(env, name)) });
@@ -128,11 +166,135 @@ export function runRequirements(env = {}, { registers = [], engines = {}, defaul
128
166
 
129
167
  // ── THE ENGINE, AND THE BINARY IT DRIVES ─────────────────────────────────────────────────────────
130
168
  push(ENGINE_ENV, true, "which reasoning engine runs the stages");
131
- const engine = (engines ?? {})[val(env, ENGINE_ENV) || defaultEngine || ""];
132
- if (engine?.env)
169
+ const engineId = val(env, ENGINE_ENV) || defaultEngine || "";
170
+ const engine = (engines ?? {})[engineId];
171
+ if (engine?.env) {
133
172
  push(engine.env, true, `the path to the ${engine.vendor} CLI this engine drives — a stage cannot dispatch without it`);
134
- if (engine?.authEnv)
135
- push(engine.authEnv, false, "how the engine bills — subscription or key; the adapter refuses before spending if the sign-in it names is absent");
173
+ // FOUND IS WHAT COUNTS, NOT SET. A program on this environment's PATH, or the copy installed with
174
+ // Clearotron, needs no path written anywhere, and asking only whether the variable was set refused an
175
+ // install whose engine the run door would have started. The resolver arrives through the tables, like
176
+ // everything else this module knows about the install; a caller that passes no resolver gets the
177
+ // variable's own answer, which is all it can see. The module's one import is engine/auth.mjs, for the
178
+ // billing row below: a driver leaf with no imports of its own, pure, and the one authority on which
179
+ // billing words an engine takes, so importing it keeps this module pure and reaches nothing in `bin/`.
180
+ const row = out[out.length - 1];
181
+ if (!row.present && typeof resolveEngine === "function") {
182
+ try { row.present = Boolean(resolveEngine(engineId, { env })?.resolved); } catch { /* not found is not present */ }
183
+ }
184
+ }
185
+ if (engine?.authEnv) {
186
+ const ways = payWays(engineId, engine);
187
+ const said = ways.map((m) => PAY_WORDS[m]);
188
+ push(engine.authEnv, false, `how the engine is paid for — ${said.length > 1 ? `${said.slice(0, -1).join(", ")} or ${said.at(-1)}` : said[0]}; `
189
+ + `unset means the subscription, and the adapter refuses before spending if the ${ways.includes("cloud") ? "key or cloud account" : "key"} it names is absent`);
190
+ const billingRow = out[out.length - 1];
191
+ const billingRows = out.length;
192
+ const word = billingMode(env);
193
+ const refused = "without it the engine refuses every search before spending";
194
+ // ── THE KEY, WHEN THE WORD IS `api-key` ──────────────────────────────────────────────────────────
195
+ //
196
+ // The same defect as the cloud below, one billing word over: the word travelled to the services' file
197
+ // and the key did not, so the run door refused every search after intake ("api-key but
198
+ // ANTHROPIC_API_KEY is not set"), while start and doctor reported nothing. Blocking at order time, like
199
+ // the switch: an operator's value, and the run door's own refusal without it.
200
+ if (word === "api-key" && engine.apiKeyEnv && ways.includes("api-key"))
201
+ push(engine.apiKeyEnv, true, `the key ${engine.authEnv}=api-key bills every turn to — ${refused}`);
202
+ // ── THE LONG-LIVED SIGN-IN, WHEN THE WORD IS THE SUBSCRIPTION ────────────────────────────────────
203
+ //
204
+ // Setup captures it on a machine that cannot complete a sign-in, which is the server a background
205
+ // install runs on, and the program reads it from its environment. Carried when set and never asked
206
+ // for: a machine signed in through the program itself needs none.
207
+ const tokenEnv = engine.headless?.tokenEnv;
208
+ if (word === "subscription" && tokenEnv && val(env, tokenEnv))
209
+ push(tokenEnv, false, `the long-lived sign-in the ${engine.vendor} CLI uses for the subscription on a machine it cannot sign in on; carried as set, and not asked for`);
210
+ // ── A CLOUD ACCOUNT, AND THE SETTINGS THAT REACH IT ──────────────────────────────────────────────
211
+ //
212
+ // The billing word travelled and the cloud did not. A machine set up to pay through a cloud account and
213
+ // started as background services wrote CLEAROTRON_AI_BILLING=cloud into the services' file and none of
214
+ // the cloud's own settings, so the run door refused every search: "none of CLAUDE_CODE_USE_VERTEX, …
215
+ // is set". Measured 2026-09-15 on a Microsoft Foundry configuration.
216
+ //
217
+ // ONLY WHEN THE WORD IS `cloud` AND THE ENGINE TAKES ONE — the resolver's answer through `payWays`, not
218
+ // a list of engines here. Under subscription or api-key nothing of a cloud is carried: a switch left on
219
+ // in a shell and written into the services' file sends Claude to that cloud, and the run door refuses
220
+ // a switch beside either word. Codex refuses a cloud account outright, so its rows do not change.
221
+ if (word === "cloud" && ways.includes("cloud")) {
222
+ const on = cloudsSwitchedOn(env);
223
+ const payWord = `${engine.authEnv}=cloud`;
224
+ // BLOCKING, AT ORDER TIME. Without the switch the run door refuses (engine/auth.mjs), so this is the
225
+ // same class as the engine and its program: an operator's value, whose absence refuses a run at
226
+ // intake and never a start.
227
+ for (const c of on)
228
+ push(CLOUD_SWITCH[c], true, `sends Claude to ${CLOUD_CREDENTIAL_CHECK[c].who}, the account ${payWord} pays through — ${refused}`);
229
+ if (!on.length && val(env, "ANTHROPIC_BASE_URL"))
230
+ push("ANTHROPIC_BASE_URL", true, `the gateway ${payWord} pays through — ${refused}`);
231
+ if (!on.length && !val(env, "ANTHROPIC_BASE_URL")) {
232
+ // NOTHING SAYS WHICH CLOUD, and a row has one name. Any one of four settings satisfies it. The row is
233
+ // NAMED by one real setting, Google's switch, the representative `payWays` above already uses for "a
234
+ // cloud account", because every reader of a row treats its name as a variable: start and doctor print
235
+ // it, the runner logs it, and a composite name read as four more items in doctor's list of what is
236
+ // missing. The REASON names all four, with whose each is. And the row carries all four as `anyOf`,
237
+ // which `runRequiredNames` hands out as names to read: doctor fills its view of the services by name,
238
+ // and reading only Google's switch reported a Microsoft machine's switch, held by its services, as
239
+ // missing.
240
+ //
241
+ // A SWITCH THAT IS SET AND NOT ON IS SAID, because it reads as on to a person and as off to the
242
+ // program: `CLAUDE_CODE_USE_FOUNDRY=0` on a Microsoft machine was answered with Google's switch
243
+ // and nothing else, and the reader was left to work out why the one they set did not count.
244
+ // And it is not an alternative to read or carry: `anyOf` is handed to the composer too, and the
245
+ // set-and-off line would travel through it.
246
+ const off = Object.values(CLOUD_SWITCH).filter((k) => val(env, k));
247
+ out.push({ name: CLOUD_SWITCH.vertex, anyOf: CLOUD_ROUTES.filter((k) => !off.includes(k)), blocking: true, at: ORDER, present: false,
248
+ why: `${payWord} pays through a cloud account and nothing names which one — set `
249
+ + `${["vertex", "foundry", "bedrock"].map((c) => `${CLOUD_SWITCH[c]}=1 for ${CLOUD_CREDENTIAL_CHECK[c].who}`).join(", ")}, `
250
+ + `or ANTHROPIC_BASE_URL for a gateway`
251
+ + (off.length ? ` (${off.join(" and ")} ${off.length > 1 ? "are" : "is"} set, but not on: a switch is on at 1, true, yes or on)` : "")
252
+ + `; ${refused}` });
253
+ }
254
+ // EVERY OTHER CLOUD SETTING THAT IS SET, CARRIED AND NEVER ASKED FOR. Which of them a cloud needs
255
+ // depends on how the machine signs in to it — an Azure key or the Azure sign-in, an AWS profile, an
256
+ // instance role or keys, gcloud or a key file — and the model pins are optional everywhere. The run
257
+ // door asks for none of them; the cloud refuses a missing one at the first turn. So a row is made
258
+ // only for a name that is set: present by construction, it can never be reported missing or refuse
259
+ // anything, and exists so the composer carries it. Names on CLOUD_SETTINGS only — never the rest of
260
+ // the environment, which would put every secret in a shell into the services' file.
261
+ //
262
+ // NEVER A SWITCH. One that is on is a row above. One that is set and not on switches nothing, and
263
+ // carried into the services' file it held that line there, where the add-only merge kept a later
264
+ // `=1` out of it for good.
265
+ const who = on.length === 1 ? CLOUD_CREDENTIAL_CHECK[on[0]].who : !on.length && val(env, "ANTHROPIC_BASE_URL") ? "the gateway" : "the cloud account";
266
+ const named = new Set(out.flatMap((r) => r.anyOf ?? [r.name]));
267
+ for (const k of CLOUD_SETTINGS)
268
+ if (!named.has(k) && !Object.values(CLOUD_SWITCH).includes(k) && val(env, k))
269
+ push(k, false, `one of the settings Claude reads to reach and pay ${who}; carried as set, and not asked for, because which ones a cloud needs depends on how this machine signs in to it`);
270
+ }
271
+ // ── AND ANY OTHER WAY THE RUN DOOR REFUSES HOW THIS IS PAID FOR ──────────────────────────────────
272
+ //
273
+ // The rows above name what is MISSING. The run door also refuses what is set wrongly: two clouds
274
+ // switched on, a switch left on beside `subscription` or `api-key`, a word that is not a billing mode,
275
+ // a cloud account on Codex. Each passed start's guard, the order wall and doctor, and the run was then
276
+ // refused after intake. Measured 2026-09-15: a services' file holding Microsoft's switch from one start
277
+ // and Google's from the next read clean everywhere and refused every search.
278
+ //
279
+ // ASKED OF THE RUN DOOR ITSELF, `resolveAuthMode`, over the environment being judged, and its refusal
280
+ // is the reason: one authority, so this check can be neither stricter nor laxer than the door. Its
281
+ // words carry names and the billing word, never a key. The row that turns blocking is the billing
282
+ // word's own, so no new name is handed to the composer: a switch beside `subscription` stays out of
283
+ // the services' file, as the cloud rows above intend. `present` on a row means SATISFIED, which is
284
+ // what every reader of it asks; for this row, set is not enough. When a row above already names what
285
+ // is missing, that row is the answer and this adds nothing, because the door's refusal is the same one.
286
+ //
287
+ // ONE OF THE DOOR'S REFUSALS QUOTES A VALUE: the word that is not a billing mode. A key pasted into the
288
+ // billing word by mistake is that value, and this reason is printed by start, by doctor and in the
289
+ // runner's log. So it is said by name, through billingRefusalWords below.
290
+ if (!out.slice(billingRows).some((r) => r.blocking && !r.present)) {
291
+ try { resolveAuthMode({ engineName: engineId, env }); } catch (e) {
292
+ if (e?.billingRefusal)
293
+ Object.assign(billingRow, { blocking: true, present: false, at: ORDER,
294
+ why: `every search is refused before spending, over how this machine is set to pay: ${billingRefusalWords(e.message)}` });
295
+ }
296
+ }
297
+ }
136
298
 
137
299
  push(RESEARCH_ENV, false, "the three clearance searches carry the common-law grid and refuse at preflight without it; a Knockout search still runs and discloses the half it skipped");
138
300
 
@@ -147,7 +309,9 @@ export function runRequirements(env = {}, { registers = [], engines = {}, defaul
147
309
  * a box whose operator configured them, which is the same shape of defect one size down.
148
310
  */
149
311
  export function runRequiredNames(env = {}, tables = {}) {
150
- return runRequirements(env, tables).map((r) => r.name);
312
+ // A row any one of several settings satisfies names them in `anyOf`, and each is a name to carry or read.
313
+ // Each name once: a name handed out twice is read twice and reported twice.
314
+ return [...new Set(runRequirements(env, tables).flatMap((r) => r.anyOf ?? [r.name]))];
151
315
  }
152
316
 
153
317
  /**
package/driver/runner.mjs CHANGED
@@ -774,15 +774,15 @@ async function backstopFailureNotice({ res, job, agentId, base, codename, studio
774
774
  // starts, so nothing is spent and nothing is promised.
775
775
 
776
776
  let __runTables = null;
777
- async function runTables() {
777
+ export async function runTables() { // exported for the requirement-check wiring test
778
778
  // AT CALL TIME, never a static import. `driver/run-requirements.mjs`'s header states the reason and it
779
779
  // is load-bearing: the register SELECTION table lives in `bin/onboard.mjs`, a CLI entry point, and a
780
780
  // static import from `driver/` would point the driver at `bin/` — the cycle that makes `clearotron
781
781
  // doctor` exit 13 after printing most of a report.
782
782
  if (!__runTables) {
783
783
  const { PROVIDERS } = await import("../bin/onboard.mjs");
784
- const { ENGINE_BINARIES, DEFAULT_ENGINE_ID } = await import("./driver.config.mjs");
785
- __runTables = { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: DEFAULT_ENGINE_ID };
784
+ const { ENGINE_BINARIES, DEFAULT_ENGINE_ID, resolveEngineProgram } = await import("./driver.config.mjs");
785
+ __runTables = { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: DEFAULT_ENGINE_ID, resolveEngine: resolveEngineProgram };
786
786
  }
787
787
  return __runTables;
788
788
  }
package/driver/stages.mjs CHANGED
@@ -1260,7 +1260,7 @@ export const STAGES = {
1260
1260
  },
1261
1261
  "scope_jurisdictions / excluded_jurisdictions / scope_basis — typed fields the driver renders": {
1262
1262
  class: "mechanical:code-rendered", tokens: ["frame_scope_missing"],
1263
- why: "CLASS ALIGNED WITH `## Instructed scope` in this same stage — #850 rules that row \"Code stamps the section from _driver/instructed-scope.json\", and the instructed jurisdictions are in that same file. Not pre-bound: no form carries them. On the instructed branch the driver already holds the list and hands it over; the model retypes it into a shape the driver dictates. verify.mjs:1171 string-compares it back — `add(\"jurisdictions\", scope.jurisdictions)` — failing frame_scope_missing:jurisdictions, so this half IS policed, unlike the campaign-shape twin above. [citation unverified]",
1263
+ why: "CLASS ALIGNED WITH `## Instructed scope` in this same stage — #850 rules that row \"Code stamps the section from _driver/instructed-scope.json\", and the instructed jurisdictions are in that same file. Not pre-bound: no form carries them. On the instructed branch the driver already holds the list and hands it over; the model retypes it into a shape the driver dictates. verify.mjs checkFindingsSibling string-compares it back — `add(\"jurisdictions\", scope.jurisdictions)` — failing frame_scope_missing:jurisdictions, so this half IS policed, unlike the campaign-shape twin above.",
1264
1264
  },
1265
1265
  "Scope reasoning — search-wide/cite-narrow, the in-scope-by-reach routes, and the reopen trigger behind each exclusion": {
1266
1266
  class: "judgment", tokens: [],
@@ -2482,8 +2482,12 @@ export const STAGES = {
2482
2482
  class: "judgment", tokens: ["too_short", "missing"],
2483
2483
  why: "'The only relevance judge' — the funnel pre-gated nothing (prelim-register SKILL.md, `## Coverage = the band blocks`). #850 keeps findings prose / relevance gate / opposition / Option-D / position rows as J.",
2484
2484
  },
2485
+ "every record the run carried into this stage ends somewhere — a findings row, a Negative-results drop, or a Disagreement resolution": {
2486
+ class: "judgment", tokens: ["registerdigest_unaccounted_records", "registerdigest_nothing_judged", "registerdigest_accounting_unreadable", "registerdigest_model_missing", "registerdigest_batch_unknown", "registerdigest_double_counted", "registerdigest_model_write_failed"],
2487
+ why: "A record that simply goes unmentioned is a silent recall loss — the one failure this stage's output exists to prevent, and the one no reader of the document can see, because a band judged in part looks exactly like a band judged in full. Armed by an era stamp, so archived runs replay to the verdicts they always had. It is checked at the CALL, scoped to the batch that call was handed, and again at the EXIT over the union: the call-time scope is what lets a dense band be recorded at all, and the exit is where that concession is paid for. Four of these are driver-written and say so — a run stamped for accounting whose own facts or stored model are missing is this driver's bug, not a model defect, and telling a seat to re-state cannot fix it.",
2488
+ },
2485
2489
  "the Sheet-1 findings row's identifier cells — URI, Mark, Owner, Country, Classes, Status, Filed, Expiry": {
2486
- class: "mechanical:tool-written", tokens: ["registerdigest_uri_missing", "registerdigest_uri_unknown"],
2490
+ class: "mechanical:tool-written", tokens: ["registerdigest_uri_missing", "registerdigest_uri_unknown", "registerdigest_flag_reason_missing", "registerdigest_verify_invalid"],
2487
2491
  why: "CONVERTED (conversion 11): the seat sends the position's `uri` and the driver renders every cell from the band record it names — record_id, mark_text, classes, status, owner_name, owner_country, application_date, registration_date, expiry_date. The join is now the check: a uri no band record carries is refused AT THE CALL, where restating it costs nothing, instead of producing a plausible row of retyped cells that fails downstream or nowhere. The DECISION that a position earns a row stays judgment (element above); the cells were never anything but transcription.",
2488
2492
  },
2489
2493
  "the full clickable record URL, composed from providers/<name>.md 'Record base host' plus the record `uri`": {
@@ -2495,7 +2499,7 @@ export const STAGES = {
2495
2499
  why: "CONVERTED (conversion 11): the driver stamps the provider and its environment word into the rendered document from the run config it already holds, and the tool takes no field for either. 'Exactly one register per run' (prelim-register SKILL.md, `## Provider`) and digest.md's `## Provider` note conceded 'the tag is constant across the findings file' — a constant the seat was retyping onto every record.",
2496
2500
  },
2497
2501
  "Negative-results drop rows — the Notes cell carrying URI, screen_verdict, class and status": {
2498
- class: "mechanical:tool-written", tokens: ["registerdigest_uri_unknown", "registerdigest_drop_reason_missing"],
2502
+ class: "mechanical:tool-written", tokens: ["registerdigest_uri_unknown", "registerdigest_drop_reason_missing", "registerdigest_drop_ground_invalid", "registerdigest_drop_ground_contradicted"],
2499
2503
  why: "CONVERTED (conversion 11): the seat sends the dropped record's `uri` and its one-line reason; the driver renders the Notes cell's four provenance fields from the band record. This is the element the conversion most clearly repays — the acceptance gate used to parse those fields back out to check the model's retyping against material the driver already held, which is a guard comparing a value with a copy of itself. The DROP DECISION and its why stay judgment (the findings-prose element), and a drop with no stated reason is refused: a batch-dropped candidate with no row is a silent recall loss.",
2500
2504
  },
2501
2505
  "## Summary counts — total queries executed (search + detail-fetch), enumerated records across N axes, crowd-descriptor count, candidates past the gate, surfaced count, open-verification-flag count": {
@@ -2507,7 +2511,7 @@ export const STAGES = {
2507
2511
  why: "CONVERTED (conversion 11): the driver renders the Audit trail table from the same artifacts as the Summary counts, plus `_query` which digest.md:388 says 'the driver stamps at merge' — carrying that forward was transcription of a driver stamp. The judgment half — flagging a unit that shortcut its axis — stays in the findings-prose element.",
2508
2512
  },
2509
2513
  "INSTRUCTED CHECKS — the answer to each requester ask the register owns": {
2510
- class: "judgment", tokens: [],
2514
+ class: "judgment", tokens: ["registerdigest_instructed_incomplete"],
2511
2515
  why: "Answering a lawyer's question from the frozen band, including the honest 'the frozen material cannot answer this' that becomes an open coverage row. No artifact holds it. (No token here: intake_ask_unanswered lives on validators.narrative, not registerFindings.)",
2512
2516
  },
2513
2517
  "the record ids read while answering each instructed check": {
@@ -2515,7 +2519,7 @@ export const STAGES = {
2515
2519
  why: "CONVERTED (conversion 11): the driver renders the ids beneath each instructed check from its own reading audit, and the tool takes {ask, answer} only. Every band_shape / band_lookup / band_record call lands in reading-log.jsonl with its args (the pattern #850 names as already existing for the band tools), so the driver held this list the whole time the seat was being asked to reproduce it.",
2516
2520
  },
2517
2521
  "adopt-or-override each placement by engaging its reason, and the `### Disagreement resolutions` rows (one per surfaced disagreement and per borderline:true, each ADOPTED/OVERRODE in writing)": {
2518
- class: "judgment", tokens: [],
2522
+ class: "judgment", tokens: ["registerdigest_adjudication_invalid", "registerdigest_adjudication_incomplete"],
2519
2523
  why: "Answering the promotion question the other way, in writing, against a reason another stage authored. #850 keeps it J. The row's SUBJECT is handed over as data (the driver appends the PLACEMENT RULINGS TAIL block, pipeline.mjs:3584), so nothing here is a fetch. [citation unverified]",
2520
2524
  },
2521
2525
  // ── REWRITTEN, NEVER DELETED (the ruling) — AND THE ROW THAT COST THIS CONVERSION A DESIGN ──
@@ -2843,8 +2847,8 @@ export const STAGES = {
2843
2847
  // 20-min override on the VELTRIPHEN run). NOTE 2026-06-17: Opus fast mode was REMOVED here and everywhere
2844
2848
  // (it ~2.5x'd subscription usage → 5h-cap 429s); HIGH effort retained.
2845
2849
  // CLEAROTRON_SYNTHESIS_MODEL (2026-07-10): stage-specific override for a live A/B test (Fable vs Opus 4.8) on
2846
- // just this stage — the driver's only other env override (CLEAROTRON_AZURE_MODEL, driver.config.mjs) is
2847
- // tier-wide, which would retarget all 6 opus stages. Unset ⇒ unchanged default "opus". Toggled live in
2850
+ // just this stage — stage-specific because a tier-wide override would retarget all 6 opus stages.
2851
+ // Unset ⇒ unchanged default "opus". Toggled live in
2848
2852
  // the service's EnvironmentFile (a oneshot unit — no restart needed, takes effect on the next
2849
2853
  // queue-triggered run), never hardcoded here.
2850
2854
  model: process.env.CLEAROTRON_SYNTHESIS_MODEL || "opus", thinking: "high", timeoutSec: 2500, stallSec: 900,
@@ -3516,7 +3520,7 @@ export const STAGES = {
3516
3520
  },
3517
3521
  "the `[on: N, M]` flag ordinals — which findings each flag names": {
3518
3522
  class: "mechanical:code-extracted", tokens: [],
3519
- why: "#850: selection against the finding index the driver already holds; targetsOf's normalised prose join is the fallback that already fails (6 of 9 flags resolved to nothing on a delivered run). NO TOKEN: parseOn exists at verify.mjs:583 and validators.seniorEyeReview never calls it — the skill file itself says \"either every flag has one or none of them do any work\", and nothing checks which state a review is in [citation unverified]",
3523
+ why: "#850: selection against the finding index the driver already holds; targetsOf's normalised prose join is the fallback that already fails (6 of 9 flags resolved to nothing on a delivered run). NO TOKEN: parseOn exists at verify.mjs commonLawHalfEvidence and validators.seniorEyeReview never calls it — the skill file itself says \"either every flag has one or none of them do any work\", and nothing checks which state a review is in",
3520
3524
  },
3521
3525
  "the section titled exactly \"PLAN-EXECUTION CHECK\"": {
3522
3526
  class: "mechanical:code-rendered", tokens: ["plan_audit_missing"],