clearotron 0.3.2-beta.6 → 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 (98) 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 +82 -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 +83 -0
  11. package/driver/band-size.mjs +59 -0
  12. package/driver/config-inventory.mjs +112 -9
  13. package/driver/connotation-search.mjs +45 -0
  14. package/driver/contract-arm2-baseline.json +1 -3
  15. package/driver/contract-e3-backlog.mjs +29 -29
  16. package/driver/contract-vocabulary.mjs +59 -25
  17. package/driver/door-gates.mjs +41 -7
  18. package/driver/driver.config.mjs +272 -59
  19. package/driver/engine/CONTRACT.md +10 -3
  20. package/driver/engine/README.md +2 -2
  21. package/driver/engine/anthropic-agent.mjs +77 -21
  22. package/driver/engine/auth.mjs +129 -10
  23. package/driver/engine/jx-turn.mjs +7 -6
  24. package/driver/engine/mcp/recording-server.mjs +13 -0
  25. package/driver/engine/openai-agent.mjs +4 -2
  26. package/driver/engine/probe.mjs +110 -23
  27. package/driver/findings-model.mjs +1 -1
  28. package/driver/flag-snapshot.mjs +28 -5
  29. package/driver/gateway.mjs +30 -21
  30. package/driver/jx-lanes.mjs +21 -2
  31. package/driver/jx-units.mjs +6 -3
  32. package/driver/jx.mjs +4 -2
  33. package/driver/matter-frame-record.mjs +90 -1
  34. package/driver/named-band.mjs +34 -2
  35. package/driver/package.json +1 -1
  36. package/driver/pipeline.mjs +391 -26
  37. package/driver/portal-config-view.mjs +30 -1
  38. package/driver/portal-report.mjs +15 -1
  39. package/driver/portal-service.mjs +46 -6
  40. package/driver/predelivery-lint.mjs +12 -2
  41. package/driver/publish/index.mjs +46 -5
  42. package/driver/publish/knockout.mjs +10 -1
  43. package/driver/publish/render-knockout.mjs +69 -7
  44. package/driver/publish/render.mjs +170 -59
  45. package/driver/publish/report-data.mjs +4 -1
  46. package/driver/publish/report-topbar.mjs +58 -0
  47. package/driver/publish/templates/report.css +28 -2
  48. package/driver/publish/xlsx.mjs +13 -1
  49. package/driver/register-availability.mjs +2 -2
  50. package/driver/register-coverage.mjs +94 -1
  51. package/driver/register-digest-record.mjs +236 -11
  52. package/driver/register-plan.mjs +170 -0
  53. package/driver/result-noun-fields.mjs +7 -4
  54. package/driver/run-economics.mjs +41 -10
  55. package/driver/run-requirements.mjs +173 -9
  56. package/driver/runner.mjs +3 -3
  57. package/driver/stages.mjs +12 -8
  58. package/driver/suite-census.json +162 -72
  59. package/driver/systemd/README.md +7 -4
  60. package/driver/terminal-clamp.mjs +107 -1
  61. package/driver/tokens.mjs +169 -3
  62. package/driver/unit-environment.mjs +42 -15
  63. package/driver/unit-inventory.mjs +19 -2
  64. package/driver/verify.mjs +50 -5
  65. package/mcp-server/CHANGELOG.md +8 -0
  66. package/mcp-server/http-server.mjs +4 -0
  67. package/mcp-server/lib/audit.mjs +11 -1
  68. package/mcp-server/lib/http-handler.mjs +6 -2
  69. package/mcp-server/package.json +1 -1
  70. package/mcp-server/server.mjs +16 -2
  71. package/package.json +1 -1
  72. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
  73. package/portal-ui/dist/index.html +1 -1
  74. package/portal-ui/package.json +1 -1
  75. package/providers/clarivate/src/capabilities.js +5 -5
  76. package/providers/clarivate/src/core.js +1 -1
  77. package/providers/corsearch/src/core.js +2 -2
  78. package/providers/jx/README.md +2 -1
  79. package/providers/jx/src/turn-envelope.mjs +8 -3
  80. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  81. package/providers/oauth-mcp-bridge/package.json +1 -1
  82. package/providers/perplexity/src/core.js +3 -3
  83. package/providers/signa/src/capabilities.js +5 -6
  84. package/providers/signa/src/core.js +1 -1
  85. package/providers/uspto-local/README.md +1 -1
  86. package/scripts/authority-boundary-probe.mjs +4 -2
  87. package/scripts/env-audit.mjs +12 -6
  88. package/scripts/freeze-example-run.mjs +49 -16
  89. package/scripts/generated-files-are-current.mjs +69 -4
  90. package/scripts/release-duplicate-notes.mjs +246 -0
  91. package/scripts/release-publish-guard.mjs +64 -6
  92. package/scripts/report-print-check.mjs +194 -0
  93. package/scripts/settings-render-check.mjs +75 -2
  94. package/scripts/test-full.mjs +96 -3
  95. package/scripts/test-run.mjs +10 -0
  96. package/shared/deployment-box.mjs +7 -2
  97. package/shared/driver-dir.mjs +1 -1
  98. package/shared/names-in-force.mjs +1 -1
package/bin/start.mjs CHANGED
@@ -84,8 +84,10 @@ import { writeSecretFile } from "../shared/secret-file.mjs"; // one atomic wri
84
84
  // below: to COMPOSE the units' environment and to GUARD it before this command reports success. The
85
85
  // tables are handed in rather than imported by it, because the register table lives in a CLI entry and
86
86
  // the driver must not point at `bin/`.
87
- import { runRequiredNames, missingRequirements } from "../driver/run-requirements.mjs";
88
- import { ENGINE_BINARIES, DEFAULT_ENGINE_ID as RUN_DEFAULT_ENGINE } from "../driver/driver.config.mjs";
87
+ import { runRequiredNames, missingRequirements, CLOUD_ROUTES } from "../driver/run-requirements.mjs";
88
+ import { CLOUD_SWITCH, cloudsSwitchedOn } from "../driver/engine/auth.mjs"; // a switch compared the way the program reads it: on or off
89
+ import { ENGINE_BINARIES, DEFAULT_ENGINE_ID as RUN_DEFAULT_ENGINE, resolveEngineProgram } from "../driver/driver.config.mjs";
90
+ import { unitEnvironment, unitValue } from "../driver/unit-environment.mjs"; // the PATH the worker unit will run with, read the way doctor reads it
89
91
 
90
92
  /**
91
93
  * The tables the requirements authority needs — resolved at CALL time, never at module scope.
@@ -104,9 +106,9 @@ import { ENGINE_BINARIES, DEFAULT_ENGINE_ID as RUN_DEFAULT_ENGINE } from "../dri
104
106
  * driver/test/a-backgrounded-install-can-actually-run-a-clearance.test.mjs refuses a static import of
105
107
  * onboard from this file, so the cycle cannot come back quietly.
106
108
  */
107
- async function runTables() {
109
+ export async function runTables() {
108
110
  const { PROVIDERS } = await import("./onboard.mjs");
109
- return { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: RUN_DEFAULT_ENGINE };
111
+ return { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: RUN_DEFAULT_ENGINE, resolveEngine: resolveEngineProgram };
110
112
  }
111
113
  import { spawn, spawnSync, execFileSync } from "node:child_process";
112
114
  import { storeInRepo, storeOutsideRepoMessage, storeCommitRefusal } from "../shared/store-in-repo.mjs"; //
@@ -188,6 +190,89 @@ export function homeEnvUpdate(homeText, union) {
188
190
  return mergeEnvFile(homeText, union, { refresh: LAUNCHER_MINTED });
189
191
  }
190
192
 
193
+ /**
194
+ * The services' settings file as this start will leave it, and the run's settings in it that differ from
195
+ * the configuration this command holds. PURE, and the one reading `--background` uses for both its guard
196
+ * and its write, so a test drives start's own answer rather than a copy of it.
197
+ *
198
+ * THE GUARD CHECKED A MERGE THE FILE DOES NOT PERFORM. It read `{ ...file, ...union }`, where this
199
+ * command's value wins, while the add-only merge keeps the file's line, an empty one included. So a file
200
+ * holding `CLAUDE_CODE_USE_FOUNDRY=` passed the guard on this command's `=1`, kept its empty line, and
201
+ * every search was refused at the order wall. `reads` is the merged text, parsed: what the units read.
202
+ *
203
+ * AND WHAT THE FILE KEEPS IS SAID, never replaced. The merge adds only what the file lacks, and that is
204
+ * deliberate: the file is the running product's configuration, an operator edits it to change what the
205
+ * services do, and a start that rewrote it from this command's configuration would undo that edit. What
206
+ * was wrong was the silence. A machine moved from one cloud to another, or holding a rotated key, started
207
+ * again and reported the file complete while the services kept the old account.
208
+ * `differ` names the run settings on which the two disagree, so start can say which. Names only, never a
209
+ * value.
210
+ *
211
+ * WHICH DISAGREEMENTS, BY THE KIND OF SETTING, and never by which side holds the value. A healthy install
212
+ * can keep its register key, its engine's path and its research key in the units' file alone, because
213
+ * that file is where start tells an operator to change what the services use; this command's shell then
214
+ * holds none of them, and naming them on every start would warn about a working install. So a setting
215
+ * counts only when this command holds a value the file contradicts: a rotated key, or a line kept empty.
216
+ * The billing word and the settings that pick a cloud are the exception, and count in either direction,
217
+ * because the file holding one this command does not is exactly the machine billing an account nobody
218
+ * chose any more. They are compared as the program reads them: an unset billing word is the subscription,
219
+ * and a switch is on or off, so `1` beside `true` is not a difference.
220
+ *
221
+ * BUT ONLY WHERE THIS COMMAND HOLDS AN OPINION. A configuration that sets no billing word says nothing
222
+ * about how the services pay: an api-key, Codex api-key or cloud install whose billing lives in the units'
223
+ * file alone, where start's own remedy offers to put it, was warned about its billing word and its switch
224
+ * on every start. So with no billing word here, neither the word nor the cloud settings are compared. Under
225
+ * `cloud` with no cloud named here, which cloud is not compared either. And under `subscription` or
226
+ * `api-key` a cloud setting this command holds counts as unset, because the run door refuses a switch
227
+ * beside either word and start never carries one: naming it sent the operator to put into the file a
228
+ * switch every search would then be refused over. A cloud the FILE holds under those words still counts.
229
+ */
230
+ export function unitsFileAfterStart(homeText, union, { config = {}, tables = {} } = {}) {
231
+ const merged = homeEnvUpdate(homeText, union);
232
+ const reads = parseEnvFile(merged.text);
233
+ const ours = (k) => String(union[k] ?? config[k] ?? "").trim();
234
+ const theirs = (k) => String(reads[k] ?? "").trim();
235
+ const words = new Set(Object.values(tables.engines ?? {}).map((e) => e?.authEnv).filter(Boolean));
236
+ const switches = Object.values(CLOUD_SWITCH);
237
+ const as = (k, v) => words.has(k) ? (v.toLowerCase() || "subscription")
238
+ : switches.includes(k) ? cloudsSwitchedOn({ [k]: v }).length > 0 : v;
239
+ const ourWord = ([...words].map(ours).find(Boolean) ?? "").toLowerCase();
240
+ const ourRoutes = Object.fromEntries(CLOUD_ROUTES.map((k) => [k, ours(k)]));
241
+ const ourCloud = cloudsSwitchedOn(ourRoutes).length > 0 || ourRoutes.ANTHROPIC_BASE_URL !== "";
242
+ const route = (k) => CLOUD_ROUTES.includes(k);
243
+ const opinion = (k) => ourWord !== "" && (!route(k) || ourWord !== "cloud" || ourCloud);
244
+ const oursAs = (k) => as(k, route(k) && ourWord !== "cloud" ? "" : ours(k));
245
+ const eitherWay = (k) => words.has(k) || route(k);
246
+ const names = new Set([...runRequiredNames(config, tables), ...runRequiredNames(reads, tables), ...CLOUD_ROUTES]);
247
+ const differ = [...names].filter((k) => !LAUNCHER_MINTED.includes(k)
248
+ && (eitherWay(k) ? opinion(k) && oursAs(k) !== as(k, theirs(k)) : ours(k) !== "" && ours(k) !== theirs(k)));
249
+ return { merged, reads, differ };
250
+ }
251
+
252
+ /**
253
+ * What start says about `differ`, the settings on which the units' file and this command's configuration
254
+ * disagree. PURE, so the words are driven rather than read from source. Names only, never a value.
255
+ *
256
+ * BOTH PLACES, because the add-only merge makes one of them a trap. This said to edit the units' file and
257
+ * restart. An operator who moved the services from one cloud to another that way, with this command's
258
+ * configuration still naming the first, had the first cloud's switch added back on the next start, since
259
+ * the file no longer held its line: two clouds on, and every search refused. Deleting that line, as the
260
+ * notice said, only repeated it. So it names the file the units read and the configuration start adds from,
261
+ * `cliEnv` (envFileRead(), or null when this command read no file and its shell is the configuration).
262
+ */
263
+ export function keptSettingsNotice(differ, { homeEnv, cliEnv = null } = {}) {
264
+ if (!differ?.length) return [];
265
+ const one = differ.length === 1;
266
+ return [
267
+ ` ⚠ ${homeEnv} and this command's configuration differ on ${one ? "this setting" : "these settings"}, and the units use what the file says:`,
268
+ ` ${differ.join(", ")}`,
269
+ ` This command only adds a setting the file has no line for; it never replaces one. To change what the units`,
270
+ ` use, change ${one ? "it" : "them"} in both places, then restart them:`,
271
+ ` ${homeEnv}\n the file the units read`,
272
+ ` ${cliEnv ?? "this command's environment"}\n what this command adds from: a line the file above lacks is added back from here on the next start`,
273
+ ];
274
+ }
275
+
191
276
  /**
192
277
  * What a `--background` run has ALREADY DONE when systemd refuses to enable a unit.
193
278
  *
@@ -1709,22 +1794,19 @@ if (isMain) {
1709
1794
  // said everything was fine, and a lawyer's search that died at its first stage with a stack trace,
1710
1795
  // delivering "nothing was delivered. Clearotron has been notified" on a box that notified nobody.
1711
1796
  //
1712
- // CHECKED AGAINST `union`, WHICH IS WHAT THE FILE WILL SAY — plus what the file ALREADY says, since
1713
- // `mergeEnvFile` is add-only and an operator's existing line wins. Checking `process.env` here would
1797
+ // CHECKED AGAINST WHAT THE FILE WILL SAY: the file's own lines, with `union` added where the file has
1798
+ // no line, because `mergeEnvFile` is add-only and an existing line wins, an empty one included. That
1799
+ // is `unitsFileAfterStart`, the same reading the write below uses. Checking `process.env` here would
1714
1800
  // measure this shell rather than the units, and pass on exactly the box that fails.
1801
+ let homeText = "";
1802
+ try { homeText = readFileSync(HOME_ENV, "utf8"); } catch (e) { if (e.code !== "ENOENT") fatal(`${HOME_ENV} exists but could not be read (${e.code}).`); }
1803
+ const unitsFile = unitsFileAfterStart(homeText, union, { config: process.env, tables: RUN_TABLES });
1715
1804
  //
1716
1805
  // BLOCKING REFUSES; NARROWING IS SAID OUT LOUD AND STARTS ANYWAY. A research key this box does not
1717
1806
  // hold means the three clearance searches refuse at preflight and a Knockout search still runs and
1718
1807
  // discloses what it skipped — so refusing to start over it would take a box that can serve a real
1719
1808
  // product and make it serve none. The operator is told which products this install can fill.
1720
1809
  {
1721
- const already = {};
1722
- try {
1723
- for (const line of readFileSync(HOME_ENV, "utf8").split("\n")) {
1724
- const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
1725
- if (m) already[m[1]] = m[2];
1726
- }
1727
- } catch { /* no file yet — the union is the whole of it */ }
1728
1810
  // ONE COMPOSER for "where do I set these", used by the start-time refusal and by the order-time
1729
1811
  // announcement below it. Two copies of this sentence is how one of them comes to name a file the
1730
1812
  // reader cannot use.
@@ -1739,7 +1821,29 @@ if (isMain) {
1739
1821
  : `Set them in ${homeEnv} — the file the units read. This command read no environment file of `
1740
1822
  + `its own, so that is the address. \`${invoke("install")}\` writes them for you IN A TERMINAL.`;
1741
1823
  };
1742
- const willRead = { ...already, ...union };
1824
+ const willRead = { ...unitsFile.reads };
1825
+ // ── AND THE PATH THE WORKER WILL SEARCH FOR THE ENGINE'S PROGRAM ─────────────────────────────
1826
+ //
1827
+ // The check asks the engine resolver, and a resolver handed no PATH finds only a path setting or
1828
+ // the copy setup installed. So a `claude` on the units' PATH was announced as "every run is
1829
+ // refused", and the run found it and ran. The PATH is the worker unit's own, read from the file
1830
+ // placed below: the worker runs the clearance, and every shipped unit sets
1831
+ // `Environment=PATH=%h/…`, which the unit reader expands to this home. Never this shell's PATH,
1832
+ // which the units do not inherit.
1833
+ //
1834
+ // THE SETTINGS FILE IS READ WITH IT, because on systemd a PATH in that file wins over the unit's own
1835
+ // `Environment=PATH=` line, wherever the two lines sit (systemd.exec(5)), and the unit reader
1836
+ // applies that rule. Nothing Clearotron writes puts a PATH there, but a hand-edited file can, and
1837
+ // then the worker searches that PATH and not the unit's. A file that is not there yet reads as
1838
+ // empty: this command is about to write it, and the unit's PATH is then the one in force.
1839
+ {
1840
+ let text = null;
1841
+ try { text = readFileSync(join(REPO, "driver", "systemd", "clearotron-worker.service"), "utf8"); } catch { /* the install loop below names a missing unit */ }
1842
+ const settingsFile = (p) => { if (p !== HOME_ENV) return ""; try { return readFileSync(p, "utf8"); } catch { return ""; } };
1843
+ const workerPath = unitValue(unitEnvironment({ units: [{ name: "clearotron-worker.service", text }],
1844
+ readEnvFile: settingsFile, home: homedir() }), "PATH").value;
1845
+ if (workerPath) willRead.PATH = workerPath;
1846
+ }
1743
1847
  const miss = missingRequirements(willRead, RUN_TABLES);
1744
1848
  // ── — WHICH HALF OF `blocking` MAY REFUSE A START ─────────────────────────
1745
1849
  //
@@ -1769,9 +1873,9 @@ if (isMain) {
1769
1873
  //
1770
1874
  // BOTH FILES, and that is the difference from the port refusals, which say `~/.env` "is NOT read
1771
1875
  // here". They are right: nothing in that file reaches a port decision. Here both are true. The
1772
- // block above reads HOME_ENV into `already` and merges it into `willRead`, so a value set there
1773
- // satisfies this check on the next run; and the CLI's own file reaches it too, through the
1774
- // `runRequiredNames(process.env, …)` loop that copies its values into `union`. Driven rather than
1876
+ // block above reads HOME_ENV into `willRead`, with its lines winning as they do in the file, so a
1877
+ // value set there satisfies this check on the next run; and the CLI's own file reaches it too,
1878
+ // through the `runRequiredNames(process.env, …)` loop that copies its values into `union`. Driven rather than
1775
1879
  // read: three blocking names cleared from the CLI's file alone, and the refusal came back naming
1776
1880
  // a fourth that the first three had newly required.
1777
1881
  //
@@ -1817,26 +1921,32 @@ if (isMain) {
1817
1921
  // it"), not about which gate happened to print it, and the arms in
1818
1922
  // `a-refusal-names-the-file-when-the-command-it-offers-cannot-be-run.test.mjs` measure it here now.
1819
1923
  //
1820
- // BOTH FILES, because both genuinely reach the check: `~/.env` is merged into `already` above,
1821
- // and this command's own file reaches it through the carry loop. `envFileRead()` for the second,
1822
- // never a path composed here — null means this process read no file of its own (a systemd start,
1924
+ // BOTH FILES, because both genuinely reach the check: `~/.env` is read into `willRead` above,
1925
+ // and this command's own file reaches it through the carry loop where `~/.env` has no line.
1926
+ // `envFileRead()` for the second, never a path composed here — null means this process read no file of its own (a systemd start,
1823
1927
  // or CLEAROTRON_NO_ENV_FILE=1) and then HOME_ENV is the only honest address there is.
1824
1928
  say(` Nothing has been installed that cannot run, and nothing has been spent.`);
1825
1929
  say(` ${orderRemedy(HOME_ENV)}`);
1826
1930
  }
1827
1931
  for (const r of miss.narrowing)
1828
1932
  say(` ⚠ ${r.name} is not set — ${r.why}`);
1933
+ // ── WHAT THE FILE KEEPS, SAID BY NAME ──────────────────────────────────────────────────────────
1934
+ //
1935
+ // The merge never replaces a line (see `unitsFileAfterStart`), so a setting changed in this
1936
+ // command's configuration after the first start does not reach the units. A machine moved from one
1937
+ // cloud to another went on billing the first, and a rotated key stayed at its first value, while this
1938
+ // command reported the file complete. Said here, by name and never by value, naming both places the
1939
+ // setting lives (see `keptSettingsNotice` for why one is not enough).
1940
+ for (const line of keptSettingsNotice(unitsFile.differ, { homeEnv: HOME_ENV, cliEnv: envFileRead() })) say(line);
1829
1941
  }
1830
1942
 
1831
- let homeText = "";
1832
- try { homeText = readFileSync(HOME_ENV, "utf8"); } catch (e) { if (e.code !== "ENOENT") fatal(`${HOME_ENV} exists but could not be read (${e.code}).`); }
1833
1943
  // THE TRIGGER KEY IS MINTED HERE, SO IT IS REWRITTEN HERE. Everything else in this union is
1834
1944
  // collected — the operator's credentials, their paths — and add-only is what stops a launcher from
1835
1945
  // losing them. The trigger key is the opposite: this process mints it, thirty days at a time, and a
1836
1946
  // value written once and never again runs down to expiry on a server nobody has touched. When it
1837
1947
  // lapses every Start stops, and the failure arrives as an upstream refusal that reads like an engine
1838
1948
  // fault rather than an expired key card.
1839
- const merged = homeEnvUpdate(homeText, union);
1949
+ const { merged } = unitsFile;
1840
1950
  if (merged.added.length || merged.refreshed.length) {
1841
1951
  writeSecretFile(HOME_ENV, merged.text);
1842
1952
  const parts = [];
package/bin/update.mjs CHANGED
@@ -51,7 +51,7 @@ import { existsSync } from "node:fs";
51
51
  import { join, dirname, resolve } from "node:path";
52
52
  import { fileURLToPath } from "node:url";
53
53
 
54
- import { config } from "../driver/driver.config.mjs";
54
+ import { config, ENGINE_BINARIES, enginesFolder, engineInstallArgs } from "../driver/driver.config.mjs";
55
55
  import { isInsideCheckout } from "../shared/inside-checkout.mjs"; // — one copy of the rule
56
56
  import { overlayReport, renderOverlayReport, treeFiles } from "../shared/doctrine-overlay.mjs";
57
57
  import { liveRunHolds } from "../driver/deploy-live-run-guard.mjs"; // — one live-run test, shared with deploy-preflight
@@ -180,6 +180,34 @@ export function isGitCheckout(repo = REPO, exists = existsSync) {
180
180
  return exists(join(repo, ".git"));
181
181
  }
182
182
 
183
+ /**
184
+ * The engine programs setup installed for this user, which `clearotron update` refreshes: every engine
185
+ * whose package is in the engines folder. Setup installs only the one the reader chose, so this is usually
186
+ * one, and none where the machine's own copy was found first or the reader declined the install.
187
+ */
188
+ export function enginesToRefresh({ dir = enginesFolder(), exists = existsSync } = {}) {
189
+ return Object.values(ENGINE_BINARIES)
190
+ .filter((e) => e.package && exists(join(dir, "node_modules", ...e.package.split("/"), "package.json")));
191
+ }
192
+
193
+ /**
194
+ * Run setup's install again for each of them, in the same folder. The same command, because re-running an
195
+ * install with a `>=` range moves the program to the newest its vendor publishes (driver.config.mjs, above
196
+ * engineInstallArgs). Returns 0, or the first failing exit code.
197
+ */
198
+ function refreshEngines(engines, dir = enginesFolder()) {
199
+ if (!engines.length) return 0;
200
+ say(`\n Refreshing the engine program Clearotron installed in ${dir}.`);
201
+ for (const e of engines) {
202
+ const rc = runInCheckout("npm", engineInstallArgs(e, dir));
203
+ if (rc !== 0) {
204
+ console.error(`\n npm could not refresh ${e.package}. \`clearotron doctor\` says which copy a run would use now.`);
205
+ return rc;
206
+ }
207
+ }
208
+ return 0;
209
+ }
210
+
183
211
  function runInCheckout(cmd, args) {
184
212
  say(`\n $ ${cmd} ${args.join(" ")}`);
185
213
  const r = spawnSync(cmd, args, { cwd: REPO, stdio: "inherit" });
@@ -301,22 +329,38 @@ export async function update(argv = process.argv.slice(2)) {
301
329
  }
302
330
 
303
331
  if (packaged) {
332
+ // ONE NPM RUN, WITH THE LAUNCHER PUT BACK AFTER IT, and both branches below install through it, so
333
+ // neither can run npm and forget the second half. npm puts its own link back at
334
+ // `<prefix>/bin/clearotron` on every install, over the launcher the install wrote, and that link runs
335
+ // whichever `node` is first on PATH. Put the launcher back, but only over npm's link or our own:
336
+ // anything else there was not ours before this update either.
337
+ const reinstall = () => {
338
+ const rc = runInCheckout("npm", packaged.npmArgs);
339
+ if (rc !== 0) return rc;
340
+ const kind = inspectShim(shimPath()).kind;
341
+ if (kind === "npm-link" || kind === "ours" || kind === "ours-other-install") {
342
+ const shim = installShim();
343
+ if (!shim.ok) console.error(`\n The update worked, but the launcher at ${shim.path ?? "~/.local/bin/clearotron"} could not be written back: ${shim.detail}.`);
344
+ }
345
+ return 0;
346
+ };
304
347
  if (packaged.current) {
305
- say(`\n This install is ${packaged.installed}, and nothing newer is published (${packaged.tag}: ${packaged.version}). Nothing was touched.\n`);
348
+ say(`\n This install is ${packaged.installed}, and nothing newer is published (${packaged.tag}: ${packaged.version}).`);
349
+ // CURRENT STILL REFRESHES THE ENGINE PROGRAM setup installed: it moves on its vendor's schedule, not
350
+ // this package's. A machine's own copy on PATH updates itself and is used first.
351
+ const engines = enginesToRefresh();
352
+ if (!engines.length) { say(" Nothing was touched.\n"); return 0; }
353
+ const rc = refreshEngines(engines);
354
+ if (rc !== 0) return rc;
355
+ say("\n Clearotron itself was already current; restart the services so they use the refreshed program.\n");
306
356
  return 0;
307
357
  }
308
358
  if (packaged.unread) say(`\n npm did not say which versions are published, so this follows the ${packaged.tag} channel.`);
309
359
  say(`\n Updating this install at ${packaged.prefix} from ${packaged.installed ?? "an unreadable version"} to clearotron@${packaged.spec}.`);
310
- const rc = runInCheckout("npm", packaged.npmArgs);
360
+ const rc = reinstall();
311
361
  if (rc !== 0) return rc;
312
- // npm puts its own link back at `<prefix>/bin/clearotron` on every install, over the launcher the
313
- // install wrote, and that link runs whichever `node` is first on PATH. Put the launcher back, but only
314
- // over npm's link or our own: anything else there was not ours before this update either.
315
- const kind = inspectShim(shimPath()).kind;
316
- if (kind === "npm-link" || kind === "ours" || kind === "ours-other-install") {
317
- const shim = installShim();
318
- if (!shim.ok) console.error(`\n The update worked, but the launcher at ${shim.path ?? "~/.local/bin/clearotron"} could not be written back: ${shim.detail}.`);
319
- }
362
+ const refreshed = refreshEngines(enginesToRefresh());
363
+ if (refreshed !== 0) return refreshed;
320
364
  say("\n Updated. An assistant starts the new version the next time it launches Clearotron; restart the services for the portal.\n");
321
365
  return 0;
322
366
  }
@@ -346,6 +390,30 @@ export async function update(argv = process.argv.slice(2)) {
346
390
  return installed;
347
391
  }
348
392
 
393
+ // ── AND THE STAMP THE PULL CANNOT MOVE ─────────────────────────────────────────────────────────────
394
+ //
395
+ // `build-info.json` names the commit an archive was packed from. It is written by `prepack`, which this
396
+ // route never runs, and it is untracked, so the pull above cannot bring it forward either. A checkout
397
+ // that was ever packed by hand keeps whatever that pack wrote, and the file then ages while the tree
398
+ // moves: measured on the test box 2026-09-16, it named a commit five days and two minor versions behind
399
+ // the tree it was sitting in.
400
+ //
401
+ // NOTHING THE PRODUCT READS IS FOOLED BY IT, which is why this stamps rather than refuses. `engineCommit`
402
+ // takes git first and never lets a packed stamp override a live checkout, so no run has been
403
+ // misattributed and no client has seen a wrong version. The reader it misleads is a person — checking on
404
+ // the box which build is deployed, answered confidently and wrongly, with nothing on the file to say it
405
+ // is stale. This verb is what moves the commit on this route, so it is the cheapest place to keep the
406
+ // answer true.
407
+ //
408
+ // A failure here does not fail the update. The code and its dependencies are already correct, and
409
+ // reporting a half-done deploy because a note could not be rewritten would be the louder lie.
410
+ const stamped = runInCheckout("node", ["scripts/write-build-info.mjs"]);
411
+ if (stamped !== 0) {
412
+ console.error("\n The update succeeded, but build-info.json could not be re-stamped. A reader of that");
413
+ console.error(" file on this box would be told the commit of an earlier pack rather than this one.");
414
+ console.error(" What this install RUNS is unaffected — that is named from the checkout itself.\n");
415
+ }
416
+
349
417
  // ── THE BUNDLE THE PULL COULD NOT UPDATE ─────────────────────────────────────────────────────────
350
418
  //
351
419
  // `portal-ui/dist` is untracked on the public tree, so `git pull` above can never bring it forward.
@@ -369,6 +437,9 @@ export async function update(argv = process.argv.slice(2)) {
369
437
  console.error(` the custom instructions could not be read — ${e.message}`);
370
438
  }
371
439
 
440
+ const refreshed = refreshEngines(enginesToRefresh());
441
+ if (refreshed !== 0) return refreshed;
442
+
372
443
  say("\n Up to date.\n");
373
444
  return 0;
374
445
  }
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "91905bd9b7ffd04985537f799d2ef49359e3d0e6",
3
- "version": "0.3.2-beta.6"
2
+ "commit": "7596846c65b0bd1a4dcc291732803da5fa8e9ccb",
3
+ "version": "0.3.2-beta.8"
4
4
  }
@@ -29,7 +29,7 @@ time, so all of them honour a mid-process env flip — the file's own NOTE besid
29
29
  `./driver.config.mjs` resolves to ONE cached module instance across the offline test fleet, so
30
30
  import-time captures silently pinned every test to the first test's env. What IS frozen at first import
31
31
  is the module-level declarations beside it — the consts `REGISTER_PROVIDER` and
32
- `UNREACHABLE_SENIOR_POLICY`, and `MODELS.azure`, a plain property reading `CLEAROTRON_AZURE_MODEL`.
32
+ `UNREACHABLE_SENIOR_POLICY`.
33
33
  Because the driver runs as a systemd **oneshot** (a fresh process per activation), editing the
34
34
  deployment's `.env` takes effect on the next queue-triggered run with no deploy and no restart — that is the
35
35
  supported way to change caps and A/B toggles. One trap: `synthesis.model` reads
@@ -130,17 +130,22 @@ through anything containing `/`):
130
130
  | gemini | `google/gemini-3.1-pro-preview` |
131
131
  | gemini-flash | `google/gemini-3-flash-preview` |
132
132
  | deepseek-v4-pro | `together/deepseek-ai/DeepSeek-V4-Pro` |
133
- | azure | `CLEAROTRON_AZURE_MODEL` (default `azure-openai/gpt-5.4`) |
133
+ | azure | `azure-openai/gpt-5.4` |
134
134
 
135
135
  The bottom four are **legacy names that no stage declares and no engine can run** — they resolve at
136
136
  level 1 and then throw at level 2 (below). They are catalogue entries, not available tiers.
137
137
 
138
138
  **Level 2 — the active engine** maps aliases to the CLI's own model names. On `anthropic-agent`
139
- (`CLAUDE_MODEL` in `engine/anthropic-agent.mjs`): **opus and sonnet are pinned** to `claude-opus-5`
140
- and `claude-sonnet-5` so neither drifts with what the CLI currently calls "opus"/"sonnet"; `haiku`
141
- and `fable` pass through as aliases. A bare or dated Anthropic id (`claude-haiku-4-5-20251001`)
142
- still resolves to its family — that is a naming form of a model the CLI can run, not a substitution
143
- of a different one.
139
+ (`CLAUDE_MODEL` in `engine/anthropic-agent.mjs`): `opus`, `sonnet`, `haiku` and `fable` pass through
140
+ as aliases, so each tier follows the vendor's newest model; to hold one still, set
141
+ `ANTHROPIC_DEFAULT_OPUS_MODEL` (or `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`,
142
+ `ANTHROPIC_DEFAULT_FABLE_MODEL`).
143
+ The catalog ids `anthropic/claude-opus-5` and `anthropic/claude-sonnet-5` are passed as those
144
+ concrete models; `anthropic/claude-haiku-4-5` goes over as the `haiku` alias, so it follows the
145
+ vendor the same way. A bare or dated Anthropic id (`claude-haiku-4-5-20251001`) still resolves to
146
+ its family — that is a naming form of a model the CLI can run, not a substitution of a different
147
+ one. Telemetry keeps the level-1 catalog id as the model asked for, and the attempt row records the
148
+ id the program reports it served.
144
149
 
145
150
  **Anything else throws.** There is no regex fall-through to sonnet and no cross-provider
146
151
  substitution: the `gemini`/`gemini-flash`/`deepseek-v4-pro`/`azure` mappings are gone with the
@@ -170,8 +175,8 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
170
175
  | Setting | Values | Effect |
171
176
  |---|---|---|
172
177
  | `CLEAROTRON_AI` | `anthropic-agent` (default) \| `openai-agent` | Compute engine (a registered provider adapter). `anthropic-agent` = `claude -p`; `openai-agent` = `codex exec` (both off-gateway, same normalized contract). An **unregistered** value — a typo, or the gateway-runtime adapter removed in the extraction — **fails loud**: no silent wrong-provider run. Production default is unchanged. |
173
- | `CLEAROTRON_AI_BILLING` | `subscription` (default) \| `api-key` | Billing mode for the selected engine. ONE variable for both, and only the LIVE engine's setting is read — it fills each engine's billing knob, and the engine that is not selected is never consulted. **`anthropic-agent`:** subscription **deletes `ANTHROPIC_API_KEY` from the child env** so `claude -p` uses OAuth subscription credentials (a present key would override them); `api-key` keeps the key — the scale setting and standing fallback. **`openai-agent`:** subscription seeds `auth.json` into the per-run `CODEX_HOME` from `CLEAROTRON_OPENAI_AUTH_FILE` (default `~/.codex/auth.json`) and strips API keys; `api-key` keeps `CODEX_API_KEY`. **Fail-loud either way:** `api-key` with no `ANTHROPIC_API_KEY` / `CODEX_API_KEY` throws, and never silently bills the subscription. The resolved mode is stamped on every stage telemetry row (`engine`, `authMode`, `apiBilled`). |
174
- | `CLEAROTRON_CLAUDE_PATH` · `CLEAROTRON_CODEX_PATH` | `claude` / `codex` on `PATH` | The engine binary to spawn, ONE PER ENGINE: `CLEAROTRON_CLAUDE_PATH` under `anthropic-agent`, `CLEAROTRON_CODEX_PATH` under `openai-agent`. Only the live engine's is read, and a box that runs both sets both — they were one variable until it met a box needing two different paths. Give it an **absolute** path if the binary is not on `PATH` — stage subprocesses run with cwd set to the run directory, so a relative path does not resolve there, and `npx clearotron doctor` refuses one. |
178
+ | `CLEAROTRON_AI_BILLING` | `subscription` (default) \| `api-key` \| `cloud` | Billing mode for the selected engine. ONE variable for both, and only the LIVE engine's setting is read — it fills each engine's billing knob, and the engine that is not selected is never consulted. **`anthropic-agent`:** subscription **deletes `ANTHROPIC_API_KEY` from the child env** so `claude -p` uses OAuth subscription credentials (a present key would override them); `api-key` keeps the key — the scale setting and standing fallback; **`cloud`** bills Claude per use through the reader's own cloud account, named by the Claude program's own switch — `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY` or `CLAUDE_CODE_USE_BEDROCK`, or `ANTHROPIC_BASE_URL` alone for a gateway (see the credentials table below) — and deletes `ANTHROPIC_API_KEY` as subscription does. **`openai-agent`:** subscription seeds `auth.json` into the per-run `CODEX_HOME` from `CLEAROTRON_OPENAI_AUTH_FILE` (default `~/.codex/auth.json`) and strips API keys; `api-key` keeps `CODEX_API_KEY`; `cloud` is refused. **Fail-loud, never a guess, never the subscription in its place:** `api-key` with no `ANTHROPIC_API_KEY` / `CODEX_API_KEY` throws; on `anthropic-agent`, `cloud` with two switches on, or with none and no `ANTHROPIC_BASE_URL`, throws, and so does a switch left on under `subscription` or `api-key`, because the program bills that cloud whatever this says; a value that is none of the three throws on both engines. `clearotron start` and `clearotron doctor` report each refusal against the settings the services read, naming a value that is not a billing mode by its setting and never quoting it, and the runner refuses the search when it is ordered, before anything is spent. The resolved mode is stamped on every stage telemetry row (`engine`, `authMode`, `apiBilled`, and `cloud`, null when no cloud bills), and the attempt row also carries `providerReported`, the program's own word for who served the turn. |
179
+ | `CLEAROTRON_CLAUDE_PATH` · `CLEAROTRON_CODEX_PATH` | `claude` / `codex` on `PATH`, then the copy Clearotron installed | The engine binary to spawn, ONE PER ENGINE: `CLEAROTRON_CLAUDE_PATH` under `anthropic-agent`, `CLEAROTRON_CODEX_PATH` under `openai-agent`. Only the live engine's is read, and a machine that runs both sets both — they were one variable until it met a machine needing two different paths. A path set here must be **absolute**: stage subprocesses run with cwd set to the run directory, so a relative path does not resolve there, and `npx clearotron doctor` refuses one. **Unset**, or set to the bare word `claude` / `codex` (which means the same), the engine uses the program on `PATH` when the machine has one, and otherwise the copy Clearotron installed in `~/.local/share/clearotron/engines`: `clearotron install` offers to put the chosen engine's program there (`@anthropic-ai/claude-code` or `@openai/codex`, this platform's build only), and `clearotron update` refreshes it. Nothing is bundled into the package. A value naming anything else is used as given and never falls back. `clearotron doctor` says which copy it found, and its version when npm installed that copy. |
175
180
  | `CLEAROTRON_OPENAI_AUTH_FILE` | `~/.codex/auth.json` | The credentials file seeded into the per-run `CODEX_HOME` under subscription billing. Was exempted from the August 2026 rename as an `openai-agent` internal rather than an install-surface name; the owner's 2026-09-04 ruling **reversed that exemption** and renamed the whole namespace, so it carries the house prefix like everything else. One spelling, no exceptions. |
176
181
  | `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
177
182
  | ↳ why they are three, and what the old measurement still says | superseded 2026-09-02, not erased | The three defaulted to `sol` alone on a MEASUREMENT, never a placeholder: on 2026-08-11/12, with `SWEEP=gpt-5.6-terra` / `CHEAP=gpt-5.6-luna`, every codex clearance died on structural-output gates — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` — over 2 scenarios × 3 stage families, **byte-identically on retry**, so no retry budget rescued it. That measurement stands for the code and the codex CLI **of that date**, and it is why the judgment tier keeps `sol`. Three weeks of stage-contract work and several codex minor versions sit between it and the 2026-09-02 ruling that split the three. **The symptom to match against this cause is unchanged**: those same three gates, as a fail row in`_driver/<stage>.jsonl`, inside the first few dispatches of a clearance and identical on every retry — a model incapable of a contract is not flaky at it. It reads as an engine bug when the only fault is this configuration. A cheap-tier experiment still belongs in the three constants in `driver/engine/openai-agent.mjs` where a reviewer sees it, not in an untraceable env line. |
@@ -207,6 +212,7 @@ deployment may override (verify live values per deployment).
207
212
  | `USPTO_LOCAL_DB` | **none — set it to use `uspto-local`** | The local USPTO index (`node:sqlite` + FTS5) that `bin/uspto-sync.mjs` builds and the free US register reads. Named in `.env.example`; this is the reference row. |
208
213
  | `CLEAROTRON_JX_SUBCLASS_DB` | unset ⇒ the lane refuses by name | Path to the built similar-group database the JX subclass lane reads, produced by `providers/jx-subclass/load-public.mjs` from the committed public tables. It names WHERE the table lives; what the table says is the build's. Absence is a refusal rather than an empty answer — a clearance that silently found no similar groups is indistinguishable from one where the file was missing. |
209
214
  | `CLEAROTRON_SUITE_TELEMETRY_DIR` | unset ⇒ the box's ledger | Redirects the provider telemetry ledger into a suite run's own temp root, so a suite never writes the box's ledger and never inherits it. Sits BELOW an explicitly-named ledger file: a test naming its own path is being deliberate, and this exists for the runs that name nothing. |
215
+ | `CLEAROTRON_ENGINES_DIR` | unset ⇒ `~/.local/share/clearotron/engines` | The folder `clearotron install` installs the chosen engine's program into, `clearotron update` refreshes, and the engine resolver reads as its last step after the explicit setting and `PATH`: an npm project holding `node_modules/@anthropic-ai/claude-code` or `node_modules/@openai/codex`, where an empty directory means there is none. The test suite points it at an empty directory, so a test never reaches a program the developer's own setup installed. |
210
216
  | `FEEDBACK_GH_TOKEN` | unset ⇒ `gh`'s own auth | GitHub token for the unattended feedback minter (`scripts/feedback-mint.mjs`), for a service account with no `gh auth` login. Present ⇒ exported to `gh` as `GH_TOKEN` for that call only. A credential: it is not a placeholder value and must not be committed. |
211
217
  | `USPTO_API_KEY` | unset ⇒ the download path refuses | API key for the USPTO bulk download path in `bin/uspto-sync.mjs`. The INGEST path needs no key and says so; only the download half reads this. A credential. |
212
218
 
@@ -317,7 +323,13 @@ cannot be read as one list.
317
323
 
318
324
  | Var | Consumer |
319
325
  |---|---|
320
- | `ANTHROPIC_API_KEY` | Engine child env in `api-key` mode only (deleted in subscription mode). |
326
+ | `ANTHROPIC_API_KEY` | Engine child env in `api-key` mode only (deleted in subscription and cloud modes). |
327
+ | `CLAUDE_CODE_USE_VERTEX` / `CLAUDE_CODE_USE_FOUNDRY` / `CLAUDE_CODE_USE_BEDROCK` | **Read by the Claude program**, which sends every turn to that cloud; `1`, `true`, `yes` or `on` switches one on. Clearotron reads them to name the cloud under `CLEAROTRON_AI_BILLING=cloud` and to refuse a switch left on under `subscription` or `api-key`. Under `CLEAROTRON_AI_BILLING=cloud`, `clearotron start --background` carries the switch that is on, and every setting in the five rows below that is set, the model pins included, into `~/.env` (a switch that is set and not on stays behind); under `subscription` or `api-key` it carries none. It adds only a line `~/.env` lacks, and names any of these on which that file and its own configuration differ. A missing switch is reported by `clearotron start` and `clearotron doctor`, and refuses a search when it is ordered. Doctor and setup's proof turn carry these and the five rows below from the settings file a search reads, which is the one at the old location while an install is still configured there (`CLOUD_SETTINGS` in `driver/engine/auth.mjs`). A value in the shell wins over the file's: setup's proof turn keeps a blank one, as a run does, where doctor passes over it and takes the file's. In setup, an answer given wins over both. A run takes the whole file. |
328
+ | `ANTHROPIC_VERTEX_PROJECT_ID` / `CLOUD_ML_REGION` / `GOOGLE_APPLICATION_CREDENTIALS` | Vertex AI's project, region and service-account key, read by the Claude program. Without the key file it uses gcloud's sign-in. |
329
+ | `ANTHROPIC_FOUNDRY_RESOURCE` / `ANTHROPIC_FOUNDRY_API_KEY` | The Foundry resource and its key, read by the Claude program. Without the key it uses the machine's Azure sign-in. |
330
+ | `AWS_REGION` / `AWS_PROFILE` / `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` | Bedrock's region, optionally the AWS profile, and the standard AWS key variables (the session token only for temporary credentials), read by the Claude program. Without the keys it uses the machine's other AWS credentials, such as an instance role. |
331
+ | `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` | A gateway in front of a cloud, and its token, read by the Claude program. With no cloud switch, `ANTHROPIC_BASE_URL` is the gateway form of `CLEAROTRON_AI_BILLING=cloud`; the gateway's credential must then be the token, because `ANTHROPIC_API_KEY` is deleted. |
332
+ | `ANTHROPIC_DEFAULT_OPUS_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL` / `ANTHROPIC_DEFAULT_HAIKU_MODEL` / `ANTHROPIC_DEFAULT_FABLE_MODEL` | The model each tier's alias resolves to, read by the Claude program: on Foundry, the deployment names; anywhere, a way to hold a tier still. Set the fable one to your Fable deployment's name if you set `CLEAROTRON_SYNTHESIS_MODEL=fable`; setup does not ask for it. |
321
333
  | `OPENAI_API_KEY` | **Read only to be DELETED.** The `openai-agent` adapter strips it from the `codex exec` environment under subscription billing, so an unrelated key exported on the box cannot spoil a clean subscription bill. It is never the api-key credential for this product — that is `CODEX_API_KEY`. Listed because a reader who has one set needs to know it is removed. |
322
334
  | `CORSEARCH_SESSION_KEY` | Register provider auth (session cookie). **Preflighted at run start — a missing credential fails fast before any model spend.** |
323
335
  | `CLARIVATE_API_KEY` / `CLARIVATE_API_BASE` | Clarivate adapter — on the engine path: `clarivate-server.mjs` in `gather-config.mjs`'s stage-grant table, plus driver-side `recordFetch` / `countHits` / `listRecords` / `executePlan`. `CLARIVATE_API_BASE` overrides the core's `DEFAULT_BASE`. |
@@ -341,7 +353,6 @@ cannot be read as one list.
341
353
  | `CLEAROTRON_SYNTHESIS_MODEL` | unset (⇒ opus) | Stage-specific synthesis model override — the live A/B toggle (e.g. `fable`). Alias must be registered in the engine map or the dispatch REFUSES by name (; it used to run sonnet silently and log the alias asked for). Read at module load; effective per fresh oneshot process. |
342
354
  | `CLEAROTRON_MEANING_SEAT_MODEL` | unset (⇒ `haiku`) | The common-law MEANING seat's model (`COMMON_LAW_SEAT_TIER[MEANING_SEAT]`, `stages.mjs`). The default moved sonnet → haiku on measured evidence: 2 attempts / 423 s against 1 attempt / 1674 s for the same outcome. **Margin:** sufficient at every load the test suite exercises, but at its densest scenario haiku used the last rung of the retry ladder — suspect this variable first if a dense matter's meaning seat goes terminal. Set`sonnet` to roll back with no code change. The thinking budget is NOT overridable (one variable, by design). |
343
355
  | `CLEAROTRON_STAGE_THINKING` | unset (⇒ each stage's declared tier) | Per-stage thinking-tier override, `<stage>=<tier>[,…]` (e.g. `register-digest=high`) — the A/B instrument, so a suite arm needs no code fork or redeploy between runs. Thinking only: models are deliberately not overridable here, so one arm can never move two variables. **The env override is a dev/test instrument**; a permanent change edits the tier in `stages.mjs` and ships. Unknown stage or tier **throws** — `effortFor()` falls back to `medium`, so a typo would otherwise run a stage at a tier nobody chose and every number measured against it would be wrong. Read per call, so an arm can flip mid-process. |
344
- | `CLEAROTRON_AZURE_MODEL` | `azure-openai/gpt-5.4` | Target of the `azure` alias — a legacy catalogue entry no engine can run (see model tiers above). |
345
356
  | `CLEAROTRON_DUMP_JSON` | unset | Dump each attempt's raw engine envelope to `_driver/<stage>.attempt<N>.rawjson.json`. Opt-in: any value except `0`/`off`/`false`/`no`/empty arms it. |
346
357
  | `CLEAROTRON_DISPATCH_RECORD` | **on** | Write the verbatim message of every stage dispatch to `_driver/<stage>.attempt<N>[.repair<M>].dispatch.txt`, with `{file, sha, bytes, chars, kind}` on the attempt row. **Default ON** — `0`/`off`/`false`/`no` disarms it. Unlike `CLEAROTRON_DUMP_JSON` beside it, this is opt-OUT: the question it answers ("was the model given this?") is asked *after* the run that raised it, so a flag someone had to remember would be off on exactly the run that needed it. The files carry the company's identity verbatim and are deliberately not in the artifact table. |
347
358
  | `CLEAROTRON_GATHER_SESSION_KEY` / `CLEAROTRON_GATHER_AGENT` / `CLEAROTRON_GATHER_SESSION_ID` | set per stage | Telemetry attribution into the provider-call ledger (set by the gather config; not operator-set). |
@@ -356,8 +367,12 @@ The settings below were deleted. Nothing in any environment set them, so each be
356
367
  had always resolved to. **They are listed because an operator whose `.env` still carries one needs to
357
368
  know it is inert** — an unread setting is indistinguishable from a setting that works.
358
369
 
370
+ `CLEAROTRON_AZURE_MODEL` left for a different reason, on 2026-09-15: it retargeted the `azure` alias,
371
+ which no stage names and no engine runs, so no run ever used its value.
372
+
359
373
  | Was | Now fixed at |
360
374
  |---|---|
375
+ | `CLEAROTRON_AZURE_MODEL` | `azure-openai/gpt-5.4`, the target of the `azure` alias (see model tiers above) |
361
376
  | `CLEAROTRON_BAND_SHAPE_PART_CHARS` | 70000 characters per shape part |
362
377
  | `CLEAROTRON_DEDUP_WINDOW_HOURS` | 24 hours, and the window can no longer be disabled |
363
378
  | `CLEAROTRON_FETCH_MAX_CHARS` | 200000 characters per fetched page |
@@ -159,13 +159,19 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
159
159
  ### 5.2 Engine & models — T3
160
160
 
161
161
  `CLEAROTRON_AI` (anthropic-agent | openai-agent; default anthropic-agent), `CLEAROTRON_AI_BILLING`
162
- (subscription|api-key), `CLEAROTRON_AI_BILLING` (subscription|api-key), `CLEAROTRON_CODEX_PATH`,
162
+ (subscription|api-key|cloud; cloud is Claude only), `CLEAROTRON_CODEX_PATH`,
163
163
  `CLEAROTRON_OPENAI_AUTH_FILE`, `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` /
164
164
  `CLEAROTRON_OPENAI_MODEL_CHEAP` (all gpt-5.6-sol),
165
- `CLEAROTRON_CLAUDE_PATH` (claude), `CLEAROTRON_AZURE_MODEL`,
165
+ `CLEAROTRON_CLAUDE_PATH` (claude on PATH, then the copy Clearotron installed),
166
166
  `CLEAROTRON_SYNTHESIS_MODEL` (opus), `CLEAROTRON_KNOCKOUT_MODEL` (opus),
167
167
  `CLEAROTRON_KNOCKOUT_PRESET` (pro-search), `CLEAROTRON_MAX_BUDGET_USD` (unset).
168
168
 
169
+ The Claude program's own cloud settings — `CLAUDE_CODE_USE_FOUNDRY`, `CLAUDE_CODE_USE_VERTEX` and
170
+ `CLAUDE_CODE_USE_BEDROCK`, each cloud's own settings, the gateway pair and the model pins — are the
171
+ vendor's names, not this tier's; the configuration reference's credentials table lists them. Written out
172
+ rather than as one wildcard: a guard reads these documents for the names they govern, and a trailing `*`
173
+ matches nothing it can check, so a name hidden behind one reads as governed while being invisible.
174
+
169
175
  ### 5.3 Concurrency, admission, retries, walls — T3 (walls are load-bearing; change deliberately)
170
176
 
171
177
  `CLEAROTRON_MAX_CONCURRENT_RUNS` (2), `CLEAROTRON_GATHER_CONCURRENCY` (7), `CLEAROTRON_CARD_CONCURRENCY` (8),
@@ -472,6 +478,12 @@ It must be **asked for**: a real deployment behind its upstream still reports be
472
478
  blank value is not a declaration, and the doctor names this variable in its output when it obeys it. Set
473
479
  it in CI and nowhere else — on a deployed box it silences the one check that notices the box is stale.
474
480
 
481
+ Engines folder: `CLEAROTRON_ENGINES_DIR` — where setup installs the chosen engine's program, where
482
+ `clearotron update` refreshes it, and where the engine resolver in `driver/driver.config.mjs` looks for it
483
+ after the explicit setting and `PATH`. Unset on a deployment, where it is
484
+ `~/.local/share/clearotron/engines`. The suite runner points it at an empty directory, so a test never
485
+ reaches a program the developer's own setup installed.
486
+
475
487
  > **Write every name out. No `*`, no `{A,B}`, no `/_SUFFIX`.** The enforcement test matches a name on
476
488
  > a word boundary, so shorthand documents a variable to a human and hides it from the guard. This row
477
489
  > is where that was found: `*_AUTH_DISABLED`, `*_DEV` and `CLEAROTRON_REPLAY_SNAPSHOT`/`_ROOTS` left five
@@ -483,11 +495,9 @@ it in CI and nowhere else — on a deployed box it silences the one check that n
483
495
  > see `providers/oauth-mcp-bridge/README.md`) were listed there for years and read by nothing, which
484
496
  > made a reader configure a variable and get no behaviour.
485
497
  >
486
- > `COURTLISTENER_TOKEN` is gone. The four`AZURE_OPENAI_*` names are **still there and stay**:
487
- > they are a reconstructed external contract, the file says so in place and tells a reader to verify
488
- > the spellings against the platform that consumes them. Naming a foreign contract is not the same
489
- > defect as inviting someone to set a variable this product reads — the rule above is about the
490
- > second.
498
+ > Both are gone. The four `AZURE_OPENAI_*` names left `.env.example` on 2026-09-15: on a page that
499
+ > explains paying for Claude through an Azure account, four Azure variables nothing in Clearotron reads
500
+ > looked like that setup's settings, and they are not.
491
501
 
492
502
  `portal-ui/` has **zero** env config (no `VITE_*`, no `import.meta.env`) — the SPA talks to its
493
503
  origin; all portal config lives server-side in portal-service.