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
@@ -54,8 +54,8 @@
54
54
  // The one place the real adapter is exercised, it is pointed at `driver/test/mock-claude.mjs` through
55
55
  // `CLEAROTRON_CLAUDE_PATH` — the same offline fixture the engine tests already spawn.
56
56
 
57
- import { ENGINE_BINARIES, DEFAULT_ENGINE_ID, engineAdapterSpecifier } from "../driver.config.mjs";
58
- import { resolveAuthMode } from "./auth.mjs";
57
+ import { ENGINE_BINARIES, DEFAULT_ENGINE_ID, engineAdapterSpecifier, resolveEngineProgram } from "../driver.config.mjs";
58
+ import { resolveAuthMode, CLOUD_SETTINGS, CLOUD_CREDENTIAL_CHECK } from "./auth.mjs";
59
59
 
60
60
  /** Six words. Short enough to be free in practice, and it still requires a real completed turn. */
61
61
  export const PROBE_PROMPT = "Reply with the single word: ok.";
@@ -90,44 +90,97 @@ const tail = (s) => {
90
90
  return t.length > DETAIL_CHARS ? `…${t.slice(-DETAIL_CHARS)}` : t;
91
91
  };
92
92
 
93
+ /**
94
+ * An engine instruction that names its program (`run \`claude\` once…`, `claude setup-token`), rewritten to
95
+ * name the copy that will actually run when that copy is the one Clearotron installed. That copy is not on
96
+ * PATH, so for it the bare word is a command the reader's shell cannot
97
+ * find, at the one step nobody can do for them. Any other copy is on PATH or named by path already, and
98
+ * the text is returned unchanged.
99
+ *
100
+ * HERE, AND RE-EXPORTED BY SETUP. Setup's own screens used it and the probe's sign-in advice, which doctor
101
+ * and the run door print, did not, so after setup's install doctor told the reader to run a `claude` their
102
+ * shell does not have. The probe cannot import the wizard, so the one copy lives on this side.
103
+ */
104
+ export function namingProgram(text, eng, bin) {
105
+ if (!text || bin?.source !== "installed" || !bin.path) return text;
106
+ const program = /\s/.test(bin.path) ? `"${bin.path}"` : bin.path;
107
+ return String(text).replace(new RegExp(`(^|\`)${eng.fallback}(?=[\\s\`]|$)`, "g"), (_, before) => `${before}${program}`);
108
+ }
109
+
93
110
  // THE HEADLESS ROUTE, WHERE THE ENGINE HAS ONE. The interactive sign-in is the one thing a server with no
94
111
  // browser cannot do, and it was the only remedy this offered — including to a box that had configured the
95
112
  // route built for servers. The engine table already carries that route; this reads it rather than a copy.
96
- const signInLine = (engine) => {
113
+ //
114
+ // `program` is the copy that ran ({ source, path }, or null), and every command run ON THIS MACHINE is named
115
+ // through `namingProgram`. The token route's command can run on any machine, so it keeps the bare word,
116
+ // with this machine's copy named beside it, as setup names it.
117
+ const signInLine = (engine, program = null) => {
97
118
  const spec = ENGINE_BINARIES[engine];
98
- const base = spec?.signIn ?? "sign the CLI in";
119
+ const base = spec?.signIn ? namingProgram(spec.signIn, spec, program) : "sign the CLI in";
99
120
  const h = spec?.headless;
100
121
  // Both forms the wizard already offers, read off the same table: a TOKEN route is run elsewhere and
101
- // carried here by variable; a DEVICE route is run on this box and signs it in directly.
122
+ // carried here by variable; a DEVICE route is run on this machine and signs it in directly. The route is
123
+ // named by where a sign-in can be completed, as INSTALL.md's sign-in table names it.
102
124
  if (!h?.cmd) return base;
125
+ const here = namingProgram(h.cmd, spec, program);
103
126
  return h.tokenEnv
104
- ? `${base} — or, on a box with no browser, run \`${h.cmd}\` on any machine you can sign in on and set the token it prints as ${h.tokenEnv} in this install's environment file`
105
- : `${base} — or, on a box with no browser, run \`${h.cmd}\` here`;
127
+ ? `${base} — or, on a machine you cannot complete a sign-in on, run \`${h.cmd}\` on any machine you can sign in on${here !== h.cmd ? ` (on this one, \`${here}\`)` : ""} and set the token it prints as ${h.tokenEnv} in this install's environment file`
128
+ : `${base} — or, on a machine you cannot complete a sign-in on, run \`${here}\` here`;
106
129
  };
107
130
 
131
+ /**
132
+ * Who refused the credentials and what to check, for a turn paid through an API key or a cloud account:
133
+ * `{ who, what, check }`, or null on a subscription, whose remedy is the sign-in above. `auth` is the
134
+ * resolver's answer for the turn ({ mode, cloud }). Names only, never a value.
135
+ *
136
+ * THE SIGN-IN IS A SUBSCRIPTION'S REMEDY AND NO OTHER'S. A cloud that refuses the credentials, or a key the
137
+ * vendor refuses, answers 401 or 403 like a signed-out program, and the advice was to run the program once
138
+ * and sign in: nothing to sign in to on a cloud, and a sign-in the adapter would not use under a key.
139
+ */
140
+ export function credentialCheck(engine, auth) {
141
+ if (auth?.mode === "cloud") {
142
+ const c = CLOUD_CREDENTIAL_CHECK[auth.cloud];
143
+ return c ? { who: c.who, what: "the credentials", check: c.check } : null;
144
+ }
145
+ const spec = ENGINE_BINARIES[engine];
146
+ if (auth?.mode === "api-key" && spec?.apiKeyEnv) return { who: spec.vendor, what: "the API key", check: spec.apiKeyEnv };
147
+ return null;
148
+ }
149
+ const capitalised = (s) => s.charAt(0).toUpperCase() + s.slice(1);
150
+
108
151
  /**
109
152
  * One verdict from one turn. PURE — no clock, no filesystem, no process.
110
153
  *
111
154
  * `tuple` is the engine's normalized return (engine/CONTRACT.md §1); `error` is a THROW, which is a
112
155
  * distinct class and not a returned failure: `openai-agent.runTurn` throws for both of its auth shapes
113
156
  * (resolveAuthMode on api-key-without-key, and the auth.json refusal) rather than settling a tuple.
157
+ *
158
+ * `auth` is how the turn was paid for (resolveAuthMode's `{ mode, cloud }`) and `program` the copy that ran
159
+ * (`{ source, path }`); both only shape the advice, never the mode, so the run door refuses exactly what it
160
+ * refused before. Absent, the advice is the subscription's, naming the bare program word.
114
161
  */
115
- export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC } = {}) {
162
+ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC, auth = null, program = null } = {}) {
116
163
  const id = String(engine ?? "").trim().toLowerCase();
117
164
  const v = (mode, basis, headline, fix, extra = {}) =>
118
165
  ({ ok: false, engine: id, mode, basis, headline, fix, detail: null, ...extra });
119
166
 
120
167
  // ── a THROW ────────────────────────────────────────────────────────────────────────────────────────
121
- // The thrown text is relayed VERBATIM as the fix wherever the thrower already says what to do. Those
122
- // messages were written by the module that owns the decision (auth.mjs owns the billing refusal, the
123
- // codex adapter owns `codex login`); paraphrasing them here creates a second wording that drifts.
168
+ // The thrown text is relayed as the fix wherever the thrower already says what to do. Those messages
169
+ // were written by the module that owns the decision (auth.mjs owns the billing refusal, the codex
170
+ // adapter owns `codex login`); paraphrasing them here creates a second wording that drifts.
171
+ //
172
+ // ONE CHANGE ONLY, AND ONLY TO A SIGN-IN: the program's name. The codex adapter refuses a subscription
173
+ // with no sign-in by throwing "run `codex login`" before it starts anything, which is the commonest way a
174
+ // signed-out Codex reaches this line. Relayed as thrown, that told a reader whose only copy is the one
175
+ // setup installed, which is not on PATH, to run a command their shell does not have. `namingProgram`
176
+ // rewrites the bare word for that copy and leaves every other copy's text as it was thrown.
124
177
  if (error) {
125
178
  const msg = String(error?.message ?? error);
126
- if (/=api-key but/i.test(msg))
179
+ if (error?.billingRefusal === true || /=api-key but/i.test(msg)) // auth.mjs marks every billing refusal
127
180
  return v("auth-misconfigured", "config",
128
- `${id} cannot start: the billing mode this box declares has no key`, msg, { detail: null });
181
+ `${id} cannot start: ${/=api-key but/i.test(msg) ? "the billing mode this box declares has no key" : "the billing setting this box declares is refused"}`, msg, { detail: null });
129
182
  if (SIGNED_OUT_RE.test(msg))
130
- return v("signed-out", "config", `${id} is not signed in`, msg);
183
+ return v("signed-out", "config", `${id} is not signed in`, ENGINE_BINARIES[id] ? namingProgram(msg, ENGINE_BINARIES[id], program) : msg);
131
184
  if (TIER_RE.test(msg))
132
185
  return v("tier-unavailable", "config", `${id} cannot reach the model it was asked for`, tierFix(id, msg));
133
186
  return v("failed", "throw", `${id} could not run a turn`, msg);
@@ -144,7 +197,11 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
144
197
  // which pipe this happened to look at.
145
198
  const detail = tail(tuple.stderr) ?? tail(tuple.stdout);
146
199
 
147
- if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null };
200
+ // A completed turn names what served it, the model and the provider as the program reported them, and
201
+ // null where it named neither, so a proof says which model and whose account it proved.
202
+ if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null,
203
+ served: typeof tuple.modelWire === "string" && tuple.modelWire ? tuple.modelWire : null,
204
+ provider: typeof tuple.providerWire === "string" && tuple.providerWire ? tuple.providerWire : null };
148
205
 
149
206
  // spawn itself failed. The filesystem preflight normally catches this first; when it does not, say so
150
207
  // in the binary's own vocabulary rather than as a mysterious engine fault.
@@ -163,9 +220,15 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
163
220
  { resetsAt: s.resetsAt ?? null, detail });
164
221
  }
165
222
 
223
+ // THE MODE STAYS `signed-out` UNDER EVERY WAY OF PAYING, because the run door refuses on the mode and a
224
+ // refused credential is as much this machine's to fix as a signed-out program. Only the words follow how
225
+ // the turn is paid for.
226
+ const refused = credentialCheck(id, auth);
166
227
  if (SIGNED_OUT_RE.test(text))
167
- return v("signed-out", "text-match", `${id} is not signed in`,
168
- `Sign in: ${signInLine(id)}, then run this again. Setup does not do it for you — the CLI owns its own login.`, { detail });
228
+ return refused
229
+ ? v("signed-out", "text-match", `${capitalised(refused.who)} refused ${refused.what}`, `check ${refused.check}, then run this again.`, { detail })
230
+ : v("signed-out", "text-match", `${id} is not signed in`,
231
+ `Sign in: ${signInLine(id, program)}, then run this again. Setup does not do it for you — the CLI owns its own login.`, { detail });
169
232
 
170
233
  if (TIER_RE.test(text))
171
234
  return v("tier-unavailable", "text-match", `${id} cannot reach the model it was asked for`, tierFix(id, text), { detail });
@@ -179,7 +242,9 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
179
242
  // from the shape rather than read from a message.
180
243
  if (s.noStreamEvents)
181
244
  return v("signed-out", "startup-class", `${id} exited before it produced anything`,
182
- `That is the signed-out shape, so start there: ${signInLine(id)}, then run this again. The engine's stderr below is the diagnosis if it is something else.`, { detail });
245
+ refused
246
+ ? `That is what refused credentials look like, so start there: check ${refused.check}, then run this again. The engine's stderr below is the diagnosis if it is something else.`
247
+ : `That is the signed-out shape, so start there: ${signInLine(id, program)}, then run this again. The engine's stderr below is the diagnosis if it is something else.`, { detail });
183
248
 
184
249
  return v("failed", "nonzero-exit", `${id} ran but the turn failed (exit ${tuple.code})`,
185
250
  "The engine's stderr below is the whole story; a turn that starts and fails is not a configuration this check can name.", { detail });
@@ -292,11 +357,15 @@ export function probeWeatherWarning(verdict) {
292
357
  * and a caller building the environment it hands this module fills from it. `doctor` kept a second copy
293
358
  * that had dropped both credentials, so it filled a probe environment without the very token it then
294
359
  * reported missing. Exported so there is nothing left to copy.
360
+ *
361
+ * The cloud a Claude turn is sent to and paid through is on it too (`CLOUD_SETTINGS`, from auth.mjs).
362
+ * Without those names a cloud answered in setup would pass the resolver, which reads the caller's
363
+ * environment, and never reach the turn, which reads this process's: a proof of an account no run bills.
295
364
  */
296
365
  export function engineEnvKeys() {
297
366
  return [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
298
367
  .flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
299
- .filter(Boolean)])];
368
+ .filter(Boolean), ...CLOUD_SETTINGS])];
300
369
  }
301
370
 
302
371
  function applyEngineEnv(env) {
@@ -346,6 +415,12 @@ async function defaultLoadAdapter(engine) {
346
415
  /**
347
416
  * Run the probe and return a verdict. Never throws for a configuration fault — a caller that wants a
348
417
  * refusal calls `preflightEngineTurn`, and a caller that wants to report calls this.
418
+ *
419
+ * `program` is the copy the caller has already found and is running (`{ source, path }`), for a caller
420
+ * that knows more about it than its setting says. Setup pins the program setting to the absolute path of
421
+ * the copy it proves, so the turn runs exactly that copy; resolved from that setting alone, the copy setup
422
+ * installed reads as a path the reader set, and the advice then named the bare word that copy does not
423
+ * answer to. Without it, the probe resolves the copy itself.
349
424
  */
350
425
  export async function probeEngineTurn({
351
426
  env = process.env,
@@ -354,6 +429,7 @@ export async function probeEngineTurn({
354
429
  loadAdapter = defaultLoadAdapter,
355
430
  timeoutSec = PROBE_TIMEOUT_SEC,
356
431
  stallSec = PROBE_STALL_SEC,
432
+ program: knownProgram = null,
357
433
  } = {}) {
358
434
  const id = String(env.CLEAROTRON_AI || DEFAULT_ENGINE_ID).trim().toLowerCase();
359
435
  if (!ENGINE_BINARIES[id]) {
@@ -366,8 +442,10 @@ export async function probeEngineTurn({
366
442
  };
367
443
  }
368
444
 
369
- // The billing-mode door, before anything spawns — a fail-loud config error must not cost a turn.
370
- try { resolveAuthMode({ engineName: id, env }); }
445
+ // The billing-mode door, before anything spawns — a fail-loud config error must not cost a turn. Its
446
+ // answer is kept: a refusal from a cloud or of a key is advised on differently from a signed-out program.
447
+ let auth;
448
+ try { auth = resolveAuthMode({ engineName: id, env }); }
371
449
  catch (e) { return classifyProbe({ engine: id, error: e, timeoutSec }); }
372
450
 
373
451
  let turn = injectedRunTurn;
@@ -377,11 +455,20 @@ export async function probeEngineTurn({
377
455
  }
378
456
 
379
457
  const restore = applyEngineEnv(env);
458
+ let program = null;
380
459
  try {
460
+ // THE COPY THAT RUNS, resolved the way the adapter resolves it: by the one resolver, inside the
461
+ // environment the turn runs in. The sign-in advice names it when it is the copy Clearotron installed,
462
+ // which is not on PATH. AFTER applyEngineEnv, NEVER BEFORE: the resolver reads this process's
463
+ // environment, and the caller's program setting is only there from that line until `restore()`, so above
464
+ // it this would name whatever copy the shell happened to point at. Inside the try, so the `finally` puts
465
+ // the environment back whatever it does. A resolver that cannot answer leaves the bare word.
466
+ if (knownProgram?.path) program = { source: knownProgram.source ?? null, path: knownProgram.path };
467
+ else try { const r = resolveEngineProgram(id); program = r.resolved ? { source: r.source, path: r.resolved } : null; } catch { /* the bare word */ }
381
468
  const tuple = await turn({ message: PROBE_PROMPT, model: PROBE_MODEL, thinking: PROBE_THINKING, timeoutSec, stallSec });
382
- return classifyProbe({ engine: id, tuple, timeoutSec });
469
+ return classifyProbe({ engine: id, tuple, timeoutSec, auth, program });
383
470
  } catch (e) {
384
- return classifyProbe({ engine: id, error: e, timeoutSec });
471
+ return classifyProbe({ engine: id, error: e, timeoutSec, auth, program });
385
472
  } finally {
386
473
  restore();
387
474
  }
@@ -1732,7 +1732,7 @@ function validateNet(f, ord, mode) {
1732
1732
  // This file's header says `findings_` = a top-level shape defect, `finding_` = a specific finding/field,
1733
1733
  // and by that rule this token would be `finding_net_chained` alongside finding_net_missing /
1734
1734
  // finding_net_invalid / finding_net_prescriptive. It is `findings_net_chained` instead, because the
1735
- // convention is about ROUTING and routing disagrees. pipeline.mjs:3161 reads:
1735
+ // convention is about ROUTING and routing disagrees. pipeline.mjs reads:
1736
1736
  //
1737
1737
  // const eligible = /^invalid_file:/.test(fail) && /:finding_[a-z]/.test(fail) && !/:findings_/.test(fail);
1738
1738
  //
@@ -129,7 +129,7 @@ const truthy = (v) => ["1", "true", "yes", "on"].includes(String(v ?? "").trim()
129
129
  * `capturedAt` is supplied rather than read from the clock so this stays testable and so a caller can
130
130
  * stamp it from the same instant it stamps everything else.
131
131
  */
132
- export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, registerCanCount = null, registerTerritories = undefined, engine = undefined, providers = undefined }) {
132
+ export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, registerLabel = null, registerCanCount = null, registerTerritories = undefined, engine = undefined, providers = undefined }) {
133
133
  const flags = {};
134
134
  // Written unconditionally, true or false to a count: a reader must be able to tell "this snapshot
135
135
  // tracks no flags" from "this snapshot lost its flags", and `flags: {}` alone cannot say which.
@@ -181,6 +181,12 @@ export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, re
181
181
  register: registerProvider
182
182
  ? {
183
183
  provider: registerProvider,
184
+ // The register's own DISPLAY label ("Signa"), beside the key the engine switches on ("signa").
185
+ // A door that names the register to a client must not print the key, and capabilities — where
186
+ // the label lives — is a provider module this process may not be able to import. Same split as
187
+ // `territories` directly below: the writer runs in the engine environment and resolves it once.
188
+ // Omitted rather than guessed when the writer had none, and every reader falls back to the key.
189
+ ...(registerLabel ? { label: registerLabel } : {}),
184
190
  canCount: registerCanCount,
185
191
  ...(registerTerritories === undefined ? {} : { territories: registerTerritories }),
186
192
  }
@@ -335,9 +341,9 @@ export function postureDisagreement(snapshot, live) {
335
341
  // the better answer for a reader: "found" against "not found" says it without a legend.
336
342
  const found = (v) => (v === true ? "found" : v === false ? "not found" : null);
337
343
  differ("engine program", found(snapshot.engine?.binaryPresent), found(live.engine?.binaryPresent),
338
- "whether a NEW search can start — the engine that last ran and this deployment do not agree that the "
339
- + "engine program can be found, so one screen offers a search the other refuses. Restart the engine "
340
- + "service so it re-reads its PATH, or install the CLI where the service can see it");
344
+ "whether a NEW search can start — the services, when they last started, and this deployment do not agree "
345
+ + "that the engine program can be found, so one screen offers a search the other refuses. Restart the "
346
+ + "services so they look again; if they still disagree, `clearotron doctor` says which side to fix and how");
341
347
 
342
348
  // Flags: compare only names BOTH sides declare, for the same reason `differ` skips absent values —
343
349
  // a build that adds a flag must not read as every older capture disagreeing with it.
@@ -425,6 +431,22 @@ export function registerTerritoriesFor(snapshot) {
425
431
  return Array.isArray(v) ? v.filter((n) => typeof n === "string") : undefined;
426
432
  }
427
433
 
434
+ /**
435
+ * The wired register's DISPLAY label, for a sentence a client reads — `null` when the snapshot does not
436
+ * carry one, which every snapshot written before this shipped does not.
437
+ *
438
+ * FALLS BACK TO THE PROVIDER KEY rather than to nothing: a door that names the register is better off
439
+ * saying "signa" than saying nothing at all, and the caller decides whether a key is good enough to
440
+ * print. It is deliberately NOT title-cased on the way out — "uspto-local" title-cased is worse prose
441
+ * than the key, and inventing a display name is the provider module's job, not this reader's.
442
+ */
443
+ export function registerLabelFor(snapshot) {
444
+ const l = snapshot?.register?.label;
445
+ if (typeof l === "string" && l.trim()) return l.trim();
446
+ const p = snapshot?.register?.provider;
447
+ return typeof p === "string" && p.trim() ? p.trim() : null;
448
+ }
449
+
428
450
  /**
429
451
  * What this instance searches. THREE answers, exactly as `registerTerritoriesFor` above:
430
452
  *
@@ -535,11 +557,12 @@ export async function livePosture({ env = process.env } = {}) {
535
557
  const { REGISTER_PROVIDER } = await import("./driver.config.mjs");
536
558
  const { capabilitiesFor } = await import("./register-capabilities.mjs");
537
559
  const canCount = (() => { try { return capabilitiesFor(REGISTER_PROVIDER).countProbe !== "none"; } catch { return null; } })();
560
+ const label = (() => { try { return capabilitiesFor(REGISTER_PROVIDER).label ?? null; } catch { return null; } })();
538
561
  const { coveredTerritoryNames } = await import("./register-coverage.mjs");
539
562
  const territories = await (async () => { try { return await coveredTerritoryNames(capabilitiesFor(REGISTER_PROVIDER)); } catch { return undefined; } })();
540
563
  const { engineInventory, providerInventory } = await import("./config-inventory.mjs");
541
564
  return buildFlagSnapshot(env, {
542
- capturedAt: new Date().toISOString(), registerProvider: REGISTER_PROVIDER, registerCanCount: canCount,
565
+ capturedAt: new Date().toISOString(), registerProvider: REGISTER_PROVIDER, registerLabel: label, registerCanCount: canCount,
543
566
  registerTerritories: territories,
544
567
  engine: engineInventory(env), providers: providerInventory(env),
545
568
  });
@@ -817,7 +817,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
817
817
  followup = false, // #5b: this run is a warm-resume / followup (escalation, envelope close, frame-reopen
818
818
  // sweep) — a hard-wall timeout breaks after ONE attempt (a 1.5× extension can't fit
819
819
  // an already-over-budget resume; the caller records the coverage-limited deferral).
820
- excludeTools, bandSize, // copper-lattice re-route: tool names dropped from this stage's allowedTools
820
+ excludeTools, bandSize, derivedLimit = null, // copper-lattice re-route: tool names dropped from this stage's allowedTools
821
821
  } = opts;
822
822
  if (!message) throw new Error(`runStage(${name}): message is required`);
823
823
  if (!sessionKey) throw new Error(`runStage(${name}): sessionKey is required`);
@@ -1145,17 +1145,17 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1145
1145
  // it; the log was already saying what the code believed.
1146
1146
  //
1147
1147
  // TWO FIELDS, NEVER COLLAPSED INTO ONE:
1148
- // modelUsed — the requested resolution. Unchanged in meaning and unchanged in value, because
1149
- // run-economics.mjs and tokens.mjs both read it and a field that quietly changes
1150
- // what it means is its own corruption.
1151
- // modelActual — the id the WIRE reported (engine tuple `modelWire`), or NULL when the stream
1152
- // never said: an engine that does not emit one (codex), a turn killed before any
1153
- // event, a spawn error. It NEVER falls back to the requested alias.
1154
- // `modelBasis` names which of the two the row can defend: "actual" or "unknown". There is no third
1155
- // state in which a requested value is dressed as an observed one.
1148
+ // modelUsed — the requested resolution, unchanged in meaning and value: run-economics.mjs and
1149
+ // tokens.mjs both read it, and a field that quietly changes meaning is its own corruption.
1150
+ // modelActual — the id the WIRE reported (engine tuple `modelWire`), or NULL when the stream never
1151
+ // said: an engine that does not emit one (codex), a turn killed before any event, a
1152
+ // spawn error. It NEVER falls back to the requested alias, and `modelBasis` ("actual" or
1153
+ // "unknown") never dresses a requested value as an observed one. `providerReported` is
1154
+ // the provider word the same stream gave (tuple `providerWire`), null on the same terms.
1156
1155
  const modelRequested = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model);
1157
1156
  const modelActual = (typeof turn.modelWire === "string" && turn.modelWire) ? turn.modelWire : null;
1158
1157
  const modelBasis = modelActual ? "actual" : "unknown";
1158
+ const providerReported = (typeof turn.providerWire === "string" && turn.providerWire) ? turn.providerWire : null;
1159
1159
  // WHETHER THE OBSERVED ID NAMES A FIXED BUILD. `modelBasis: "actual"` says the provider answered,
1160
1160
  // not that the answer is pinned: two of the three tiers come back as undated aliases the provider
1161
1161
  // may repoint, and recorded beside a dated one they read identically. null when there is nothing to
@@ -1171,9 +1171,13 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1171
1171
  // "written before anybody asked", which is the distinction the field exists for. One spawn per
1172
1172
  // binary per process; a probe never throws, because taking down a dispatch to record a version
1173
1173
  // would be a worse defect than the gap it closes.
1174
+ // `source` says WHICH copy served (explicit / path / installed), and it is attached here, outside the
1175
+ // probe's cache: that cache is keyed by the file, and one file can be reached by more than one route.
1174
1176
  const cli = (() => {
1175
- try { return probeCliVersion(preflightEngineBinary(process.env)?.resolved ?? null); }
1176
- catch (e) { return { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160) }; }
1177
+ try {
1178
+ const pre = preflightEngineBinary(process.env);
1179
+ return { ...probeCliVersion(pre?.resolved ?? null), source: pre?.source ?? null };
1180
+ } catch (e) { return { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160), source: null }; }
1177
1181
  })();
1178
1182
  if (modelActual) lastModelWire = modelActual; // — never overwritten with null
1179
1183
  // The comparison is by FAMILY (driver.config modelFamily), because `--model haiku` legitimately comes
@@ -1294,7 +1298,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1294
1298
  attempt, key, agent, model,
1295
1299
  modelUsed: (lastModelUsed = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model)),
1296
1300
  // Same billing stamp as the attempt row, written on the same terms — see the note there.
1297
- engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true,
1301
+ engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
1298
1302
  code: rt.code, wall: rt.wall, timeoutSec: effTimeout,
1299
1303
  // — same rename as the attempt row above. This row already carries the driver's verdict as
1300
1304
  // `repairOutcome` (only "repaired" is success), so it needs no `ok`; what it lacked was any mark
@@ -1638,8 +1642,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1638
1642
  // modelMismatch — true/false when both sides name a family, null when either does not.
1639
1643
  // Written even on the rows where they are null, so "this engine cannot report" stays visibly
1640
1644
  // different from "this record predates the gauge".
1641
- modelActual, modelBasis, modelSnapshot, modelMismatch,
1642
- cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
1645
+ modelActual, modelBasis, modelSnapshot, modelMismatch, providerReported,
1646
+ cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}), cliSource: cli.source,
1643
1647
  // W3 billing telemetry: which engine ran + the RESOLVED billing mode (subscription vs api-key). This
1644
1648
  // records INTENT (the mode the engine was configured to bill under), not independent billing evidence
1645
1649
  // — the actual proof is the provider console (claude's stream also reports apiKeySource; codex does
@@ -1653,7 +1657,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1653
1657
  // "every telemetry field is written unconditionally, so 'did not happen' stays distinguishable
1654
1658
  // from 'not recorded'" (instrumentation-house-rule.test.mjs). A run must be able to STATE that it
1655
1659
  // billed subscription, not merely fail to state that it billed API.
1656
- engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true,
1660
+ engine: engine.name, writeBoundary: writeBoundaryOf(engine), authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
1657
1661
  // build 2 — THIS attempt is the fresh dispatch bought by discarding a warm session that
1658
1662
  // reproduced its own failure. Exact, not cumulative: a `warmEscalatedAt > 0` test would mark
1659
1663
  // every later attempt too the moment the ladder is deepened, and the row would stop meaning
@@ -1743,6 +1747,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1743
1747
  // null (not 0, and not absent) on an engine that cannot report them, so "this adapter does not
1744
1748
  // measure" stays visibly different from "this turn called no tools" — see toolGauge.
1745
1749
  ...toolGauge(turn), band: bandSize ?? undefined,
1750
+ inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
1746
1751
  // AD-4 emitted-vs-landed, UNCONDITIONAL (was success-only, which made a failed attempt's mid-write
1747
1752
  // artifact invisible): `output` = what LANDED on disk after this attempt (null when the stage has no
1748
1753
  // expected file); `wrote` = whether THIS attempt emitted it (see the computation above the runDir
@@ -1776,8 +1781,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1776
1781
  event: "attempt", stage: name, attempt, of: maxRetries + 1, ok: !fail, fail: fail ?? null,
1777
1782
  //: the spine carries the same pair as the per-stage log, or the two disagree about what
1778
1783
  // ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
1779
- model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch,
1780
- cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
1784
+ model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch, providerReported,
1785
+ cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}), cliSource: cli.source,
1781
1786
  wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
1782
1787
  rescued: rescued ?? undefined, killed: killed || undefined,
1783
1788
  quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
@@ -1789,7 +1794,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1789
1794
  // archived runs actually reads, and the question "has this box ever billed API" could not be
1790
1795
  // answered from it because the pair was only ever on the per-stage log. Written unconditionally,
1791
1796
  // like everything else here: a subscription run states `false`.
1792
- authMode: auth.mode, apiBilled: auth.apiBilled === true,
1797
+ authMode: auth.mode, apiBilled: auth.apiBilled === true, cloud: auth.cloud ?? null,
1793
1798
  //: the spine carries the POINTER and the sha, not the text — enough to find the file and
1794
1799
  // to tell two attempts apart without opening either.
1795
1800
  dispatch: dispatch?.file ?? null, dispatchSha: dispatch?.sha ?? null,
@@ -1806,6 +1811,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1806
1811
  wall, outputTokens: usage?.output ?? null, tokensPerSec: tokensPerSec(usage, wall),
1807
1812
  // — the spine carries them too, or a round has to join two files to ask why a stage was slow.
1808
1813
  ...toolGauge(turn), band: bandSize ?? undefined,
1814
+ inputBytes: derivedLimit?.inputBytes ?? undefined, derivedLimitSec: derivedLimit?.sec ?? undefined,
1809
1815
  formRepairs: formRepairsThisAttempt || undefined, //, see the stage row above
1810
1816
  });
1811
1817
  } catch { /* telemetry best-effort — never fail a turn over a journal line */ }
@@ -280,6 +280,25 @@ export const jxBillingStamp = (executorSource, result = null) => {
280
280
  // NO VENDOR ON THE RESULT MEANS NO DISPATCH HAPPENED. A fixture, an injected executor, or a
281
281
  // configuration the engine door refused all return without one, and none of them is provider-billed.
282
282
  // Saying so in its own words beats "unknown", which is indistinguishable from an unstamped legacy row.
283
- if (executorSource !== "engine" || !result?.vendor) return { engine: "not-provider-billed", authMode: "not-provider-billed" };
284
- return { engine: result.vendor, authMode: result.authMode ?? "not-provider-billed" };
283
+ if (executorSource !== "engine" || !result?.vendor) return { engine: "not-provider-billed", authMode: "not-provider-billed", cloud: null };
284
+ return { engine: result.vendor, authMode: result.authMode ?? "not-provider-billed", cloud: result.cloud ?? null }; // cloud: which account a cloud mode bills
285
+ };
286
+
287
+ // ── What a jx ledger row says about the model ─────────────────────────────────────────────────────────
288
+ // The `model` on these rows is the id the program reported for the turn (jx-turn.mjs reads it off the
289
+ // wire), not a tier asked for, so a row that names one also carries it as `modelActual`, the name every
290
+ // attempt row uses for a served id. That is what the report's list of models reads; without it, a model
291
+ // that did only this work was left off the report.
292
+ //
293
+ // A TURN THAT RAN AND NAMED NO MODEL STILL RAN. The Claude program answered it itself, or the stream never
294
+ // said, or the turn was killed first: the program reports no id. Such a row used to carry neither field,
295
+ // and every reader took a row with no `model` for a call that was never made, so the turn's attempt and
296
+ // any tokens it reported fell out of the run's totals, and a run made only of such turns read as one where
297
+ // nothing was looked at. It now records `modelActual: null`, the stage rows' own words for "a turn ran and
298
+ // named no model", and tokens.mjs counts it as an attempt. Only a real dispatch writes it, by the rule
299
+ // jxBillingStamp states: no vendor on the result means no dispatch, so a fixture, an injected executor or
300
+ // a configuration the engine door refused writes neither field.
301
+ export const jxModelFields = (executorSource, result = null) => {
302
+ if (result?.model) return { model: result.model, modelActual: result.model };
303
+ return jxBillingStamp(executorSource, result).engine === "not-provider-billed" ? {} : { modelActual: null };
285
304
  };
@@ -34,7 +34,7 @@
34
34
  import { readFileSync, writeFileSync, renameSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
35
35
  import { join } from "node:path";
36
36
  import { driverDir } from "../shared/driver-dir.mjs"; //
37
- import { LANGUAGE_LANES, SERP_LANES, isMirrorHost, canonicalTerm, jxBillingStamp } from "./jx-lanes.mjs";
37
+ import { LANGUAGE_LANES, SERP_LANES, isMirrorHost, canonicalTerm, jxBillingStamp, jxModelFields } from "./jx-lanes.mjs";
38
38
  import { jxKey, MAX_LANE_ATTEMPTS } from "./jx.mjs";
39
39
  import { abbrev } from "./repair-contract.mjs";
40
40
  import { kebab } from "./search-policy.mjs";
@@ -137,6 +137,9 @@ function foldRetryable(ctx, lane) {
137
137
  // spend real tokens that no per-run total ever sees (they did, until 2026-07-28).
138
138
  //
139
139
  // …and the BILLING PATH rides with them — see jxBillingStamp in jx-lanes.mjs.
140
+ //
141
+ // …and so does what the turn said about the model: the id it reported, or that it ran and named none —
142
+ // see jxModelFields in jx-lanes.mjs.
140
143
  function ledgerRow(runDir, row) {
141
144
  try { appendFileSync(driverDir(runDir, "jx-completions.jsonl"), JSON.stringify(row) + "\n"); } catch { /* receipts best-effort */ }
142
145
  }
@@ -392,7 +395,7 @@ export async function runJxSerpGrid(ctx, job, opts = {}, { runLog = () => {}, no
392
395
  ledgerRow(run.runDir, { ts: new Date().toISOString(), lane, mark: markName, unit: "serp-judge", executor: judgeSource,
393
396
  ...jxBillingStamp(judgeSource, jr),
394
397
  took_ms: jr?.tookMs ?? (Date.now() - started), ok: Boolean(jr?.ok), judged: jr?.ok ? (jr.judgments?.length ?? 0) : 0,
395
- ...(jr?.model ? { model: jr.model } : {}),
398
+ ...jxModelFields(judgeSource, jr),
396
399
  ...(jr?.usage ? { usage: jr.usage } : {}), ...(jr?.ok ? {} : { cause: String(jr?.cause ?? "unknown").slice(0, 300) }) });
397
400
  if (!jr?.ok) { judgeDegraded = String(jr?.cause ?? "unknown").slice(0, 200); break; }
398
401
  const byId = new Map(jr.judgments.map((j) => [j.id, j]));
@@ -524,7 +527,7 @@ export async function runJxNativeread(ctx, job, opts = {}, { runLog = () => {},
524
527
  ledgerRow(run.runDir, { ts: new Date().toISOString(), lane, mark: markName, unit: "nativeread", executor: source,
525
528
  ...jxBillingStamp(source, r),
526
529
  took_ms: r?.tookMs ?? (Date.now() - started), ok: Boolean(r?.ok), items: r?.ok ? (r.items?.length ?? 0) : 0,
527
- ...(r?.model ? { model: r.model } : {}),
530
+ ...jxModelFields(source, r),
528
531
  ...(r?.usage ? { usage: r.usage } : {}), ...(r?.ok ? {} : { cause: String(r?.cause ?? "unknown").slice(0, 300) }) });
529
532
  if (!r?.ok) {
530
533
  degradeUnit(run.runDir, key, st.attempts, r?.cause ?? "unknown");
package/driver/jx.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  import { readFileSync, writeFileSync, renameSync, appendFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { driverDir } from "../shared/driver-dir.mjs"; //
17
- import { decideJxLanes, candidateRefusal, canonicalTerm, romanizationSpellings, LANGUAGE_LANES, jxBillingStamp } from "./jx-lanes.mjs";
17
+ import { decideJxLanes, candidateRefusal, canonicalTerm, romanizationSpellings, LANGUAGE_LANES, jxBillingStamp, jxModelFields } from "./jx-lanes.mjs";
18
18
  import { cnipaSubgroupsForClasses, cnipaEditionLabel } from "./jx-subclass.mjs"; // — replaces the hand-written seed table
19
19
  import { kebab } from "./search-policy.mjs";
20
20
  import { JX_PROVIDERS } from "./driver.config.mjs";
@@ -512,7 +512,9 @@ export async function runJxCandidateFold(ctx, job, opts = {}, { runLog = () => {
512
512
  const row = { ts: new Date().toISOString(), lane, mark: markName, executor: source, ...jxBillingStamp(source, r),
513
513
  took_ms: r?.tookMs ?? (Date.now() - started), ok: Boolean(r?.ok),
514
514
  candidates: r?.ok ? (r.candidates?.length ?? 0) : 0,
515
- ...(r?.model ? { model: r.model } : {}),
515
+ // the model the turn reported, as `model` and `modelActual`, or `modelActual: null` for a turn
516
+ // that ran and named none — see jxModelFields.
517
+ ...jxModelFields(source, r),
516
518
  ...(r?.usage ? { usage: r.usage } : {}), ...(r?.ok ? {} : { cause: String(r?.cause ?? "unknown").slice(0, 300) }) };
517
519
  try { appendFileSync(ledgerPath, JSON.stringify(row) + "\n"); } catch { /* receipts best-effort */ }
518
520
  // Before the degraded-lane `continue` below: a lane that FAILED still ran a turn, and the model