clearotron 0.3.2-beta.11 → 0.3.2-beta.13

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 (122) hide show
  1. package/.env.example +1 -1
  2. package/CONTRIBUTING.md +6 -5
  3. package/INSTALL.md +3 -4
  4. package/README.md +2 -1
  5. package/bin/clearotron.mjs +14 -0
  6. package/bin/connect.mjs +68 -3
  7. package/bin/example.mjs +12 -1
  8. package/bin/key.mjs +6 -1
  9. package/bin/onboard.mjs +23 -3
  10. package/bin/passphrase.mjs +4 -2
  11. package/bin/start.mjs +18 -6
  12. package/build-info.json +2 -2
  13. package/docs/CLIENT-MCP.md +6 -6
  14. package/docs/DELIVERY.md +3 -3
  15. package/docs/ONBOARDING.md +1 -1
  16. package/docs/PORTAL.md +3 -3
  17. package/docs/RELEASES.md +1 -1
  18. package/docs/SECURITY.md +1 -1
  19. package/docs/architecture/04-configuration-reference.md +4 -4
  20. package/docs/architecture/05-config-governance.md +10 -10
  21. package/docs/architecture/06-operations-runbook.md +3 -3
  22. package/docs/architecture/07-quality-and-audit.md +1 -1
  23. package/docs/architecture/08-development-guide.md +2 -2
  24. package/docs/architecture/09-security-and-data.md +1 -1
  25. package/docs/decisions/0002-no-dark-functionality.md +1 -1
  26. package/docs/decisions/0006-what-the-public-repository-carries.md +3 -3
  27. package/driver/CHANGELOG.md +29 -0
  28. package/driver/ask-ledger.mjs +1 -1
  29. package/driver/band-shape.mjs +1 -1
  30. package/driver/bundled-demos.mjs +2 -2
  31. package/driver/card-budget.mjs +2 -2
  32. package/driver/case-law-ledger.mjs +2 -2
  33. package/driver/connotation-search.mjs +5 -5
  34. package/driver/contract-e3-backlog.mjs +13 -13
  35. package/driver/contract-vocabulary.mjs +5 -5
  36. package/driver/coverage-form.mjs +2 -2
  37. package/driver/demo-container.mjs +26 -2
  38. package/driver/disposition-tool.mjs +2 -2
  39. package/driver/engine/CONTRACT.md +3 -3
  40. package/driver/engine/mcp/gather-config.mjs +1 -1
  41. package/driver/engine/mcp/recording-server.mjs +1 -1
  42. package/driver/engine/mcp/supplemental.mjs +22 -5
  43. package/driver/engine/openai-agent.mjs +2 -2
  44. package/driver/findings-model.mjs +21 -6
  45. package/driver/gateway.mjs +1 -1
  46. package/driver/knockout-next-step.mjs +72 -0
  47. package/driver/named-band.mjs +1 -1
  48. package/driver/package.json +1 -1
  49. package/driver/pipeline-knockout.mjs +27 -1
  50. package/driver/pipeline.mjs +4 -4
  51. package/driver/placement-union.mjs +1 -1
  52. package/driver/portal-mcp-client.mjs +1 -1
  53. package/driver/portal-report.mjs +25 -3
  54. package/driver/portal-service.mjs +25 -13
  55. package/driver/predelivery-lint.mjs +9 -4
  56. package/driver/progress.mjs +37 -1
  57. package/driver/publish/render-knockout.mjs +14 -9
  58. package/driver/publish/render.mjs +6 -6
  59. package/driver/record-discard.mjs +1 -1
  60. package/driver/register-availability.mjs +1 -1
  61. package/driver/register-count.mjs +1 -1
  62. package/driver/register-digest-record.mjs +1 -1
  63. package/driver/register-plan.mjs +81 -11
  64. package/driver/report-card-record.mjs +2 -2
  65. package/driver/result-noun-fields.mjs +3 -1
  66. package/driver/roster-verdict.mjs +2 -2
  67. package/driver/search-policy.mjs +1 -1
  68. package/driver/skeptic-record.mjs +1 -1
  69. package/driver/stages-knockout.mjs +1 -1
  70. package/driver/stages.mjs +1 -1
  71. package/driver/suite-census.json +72 -24
  72. package/driver/systemd/README.md +1 -1
  73. package/driver/unit-inventory.mjs +2 -2
  74. package/driver/verify-knockout.mjs +0 -27
  75. package/driver/verify.mjs +1 -1
  76. package/mcp-server/CHANGELOG.md +8 -0
  77. package/mcp-server/CONNECT.md +8 -8
  78. package/mcp-server/lib/runs.mjs +1 -1
  79. package/mcp-server/package.json +1 -1
  80. package/mcp-server/serve.mjs +27 -0
  81. package/package.json +1 -1
  82. package/portal-ui/dist/assets/{index-CtvwLCti.css → index-7Lq-dXDV.css} +12 -9
  83. package/portal-ui/dist/assets/{index-DXSRxPV_.js → index-w8GFZftk.js} +110 -69
  84. package/portal-ui/dist/index.html +2 -2
  85. package/portal-ui/package.json +1 -1
  86. package/providers/free-tier/src/capabilities.js +2 -2
  87. package/providers/jx/src/core.js +3 -3
  88. package/providers/jx/src/turn-envelope.mjs +1 -1
  89. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  90. package/providers/oauth-mcp-bridge/package.json +1 -1
  91. package/providers/perplexity/README.md +1 -1
  92. package/providers/signa/src/capabilities.js +2 -2
  93. package/providers/signa/src/core.js +2 -2
  94. package/providers/uspto-local/src/core.js +1 -1
  95. package/providers/uspto-local/src/sync.js +1 -1
  96. package/scripts/README.md +2 -6
  97. package/scripts/ask-ai-render-check.mjs +26 -1
  98. package/scripts/citation-anchor-report.mjs +1 -1
  99. package/scripts/citation-line-check.mjs +3 -3
  100. package/scripts/dead-names.mjs +17 -16
  101. package/scripts/e2e.mjs +1 -1
  102. package/scripts/env-audit.mjs +7 -1
  103. package/scripts/env-classify.mjs +1 -1
  104. package/scripts/pack-publishable.mjs +1 -1
  105. package/scripts/release-artifact-seal.mjs +2 -2
  106. package/scripts/repo-writes.mjs +42 -0
  107. package/scripts/report-sections-render-check.mjs +245 -0
  108. package/scripts/report-theme-render-check.mjs +31 -3
  109. package/scripts/settings-render-check.mjs +15 -8
  110. package/scripts/strip-tracker-citations.mjs +4 -4
  111. package/scripts/test-run.mjs +108 -6
  112. package/shared/browser-temp-root.mjs +10 -3
  113. package/shared/client-door.mjs +38 -6
  114. package/shared/connect-clients.mjs +2 -0
  115. package/shared/identifier-scan.mjs +2 -2
  116. package/shared/invocation.mjs +1 -1
  117. package/shared/parent-watch.mjs +33 -0
  118. package/shared/reference-guard-classes.mjs +5 -3
  119. package/shared/running-start.mjs +14 -3
  120. package/shared/scope.mjs +22 -3
  121. package/shared/stdio-connect.mjs +39 -18
  122. package/shared/writing-standard-classes.mjs +2 -3
package/.env.example CHANGED
@@ -578,7 +578,7 @@ TRADEMARK_MCP_KEY_SOCKET=
578
578
  # names, read by code that ships.
579
579
 
580
580
  # WHO the chat notice is addressed to: a JSON object of requester email or handle → number, e.g.
581
- # {"lisa@tenant.example":"+41...","jordan":"+41..."}. Before this, the notice went to a map keyed by
581
+ # {"robin@tenant.example":"+41...","jordan":"+41..."}. Before this, the notice went to a map keyed by
582
582
  # AGENT id, and every user of a deployment shares one agent — so the operator was notified about work
583
583
  # other people ordered and the requester was told nothing.
584
584
  #
package/CONTRIBUTING.md CHANGED
@@ -60,9 +60,10 @@ Four more things run for free:
60
60
  | `npx clearotron demo` | Replays `demo/` — a real run on a fictional mark — through the real publisher into `~/trademark-demo/pool` and serves it. No keys, no model, no engine. |
61
61
 
62
62
  **About `npx clearotron demo`.** `npx clearotron demo --no-open --once` publishes the sample and exits without
63
- touching a browser. `npx clearotron demo --pool <dir>`
64
- puts the pool somewhere else, and `npx clearotron demo --run-dir <dir>` replays a finished run of your
65
- own instead of the shipped example.
63
+ touching a browser. Without `--once` it serves the portal until it is stopped: `--no-open` keeps it from
64
+ opening a browser, `--port <n>` moves its three doors to `n`, `n+1` and `n+2`, and `--keep` leaves its folder
65
+ behind when it stops. `npx clearotron demo --pool <dir>` puts the pool somewhere else, and
66
+ `npx clearotron demo --run-dir <dir>` replays a finished run of your own instead of the shipped example.
66
67
 
67
68
  Fix a bug, add a jurisdiction, add a doctrine test case: all of it is reachable from here.
68
69
 
@@ -90,7 +91,7 @@ this tree does not carry has to be declared with its reason, and the publication
90
91
  fails an undeclared one.
91
92
 
92
93
  **`driver/skills/**` is engine input, not documentation.** Those Markdown files are the prompt payload
93
- served to the model at run time — `synthesis-rules.md` is a 16,000-word program. Editing them for
94
+ served to the model at run time — `synthesis-rules.md` is a 12,000-word program. Editing them for
94
95
  brevity, tone or tidiness changes what a clearance concludes. Nothing in this section, and nothing in
95
96
  any writing pass over the documentation, applies to them.
96
97
 
@@ -192,7 +193,7 @@ with no runner assigned and no steps recorded, and every job that gates on it is
192
193
  this repository: two runs on one SHA, thirty minutes apart, the push run green on every job and the
193
194
  scheduled one red having run nothing.
194
195
 
195
- That is **could-not-look**, not a fault, and this tool says so rather than making you open the run and
196
+ That is **a check that could not look**, not a fault, and this tool says so rather than making you open the run and
196
197
  read timings to find out. It does not block on it — nothing ran, so nothing can have regressed.
197
198
 
198
199
  **It also does not call it green.** A run that never ran told you nothing about `main`, and the last
package/INSTALL.md CHANGED
@@ -242,16 +242,15 @@ refuses for every real user.
242
242
 
243
243
  Two further constraints, both of which stop a package being cut from just anywhere:
244
244
 
245
- - **The de-identification scan needs a full clone.** It refuses a shallow one (exit 2 — could-not-look,
246
- not a pass) because it walks history it cannot see.
245
+ - **The de-identification scan needs a full clone.** It refuses a shallow one (exit 2 — a check that could
246
+ not look, not a pass) because it walks history it cannot see.
247
247
  - **It also needs its table**, passed with `--blocklist` (or `CLEAROTRON_IDENTIFIER_BLOCKLIST`), and
248
248
  that table lives in the private config store rather than in this repository. Without it the scan
249
249
  exits 2 rather than arming a weaker rule set quietly.
250
250
 
251
251
  Both refusals are correct and neither is a workaround to route around.
252
252
 
253
- **Every `clearotron` command in this document is written `npx clearotron …`, and that is not a
254
- stylistic choice.** This repository *is* the `clearotron` package, and npm links a package's `bin` into
253
+ This repository *is* the `clearotron` package, and npm links a package's `bin` into
255
254
  `node_modules/.bin` only for its **dependencies** — never for the package itself. So after `npm install`
256
255
  succeeds there is no `clearotron` on your `PATH` and none in `node_modules/.bin`; a bare `clearotron`
257
256
  would be `command not found` with nothing having failed.
package/README.md CHANGED
@@ -33,7 +33,8 @@ the address in your browser. No sign-up, no credentials, no network calls to us.
33
33
  The demo runs for as long as that window stays open, and removes everything it made when you close it —
34
34
  nothing of it is left on the machine, and running it again later starts clean. If you want to keep the
35
35
  sample reports after closing the window, run `npx clearotron demo --keep`; it prints the one command
36
- that removes the folder when you are done with it.
36
+ that removes the folder when you are done with it. `npx clearotron demo --once` publishes the reports and
37
+ exits, and keeps the folder the same way.
37
38
 
38
39
  **Then install it.**
39
40
 
@@ -24,6 +24,7 @@ import { constants as SIG } from "node:os";
24
24
  import { isEntrypoint } from "../shared/is-entrypoint.mjs";
25
25
  import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // — one floor, read from package.json
26
26
  import { invocationPrefix } from "../shared/invocation.mjs"; // — print a command the reader can type
27
+ import { watchParent } from "../shared/parent-watch.mjs";
27
28
 
28
29
  export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
29
30
 
@@ -229,6 +230,19 @@ const [verb, ...rest] = process.argv.slice(2);
229
230
  };
230
231
  process.on(sig, forward);
231
232
  }
233
+
234
+ // ── AND THE DEMO STOPS WHEN WHATEVER STARTED IT IS GONE ─────────────────────────────────────────────
235
+ //
236
+ // The forwarding above covers a signal sent to THIS pid. Through npx it is not this pid a reader holds:
237
+ // npm → `sh -c` → this launcher, and a TERM to npm dies at that `sh` (shared/parent-watch.mjs). This
238
+ // launcher is then reparented, and the demo runs on holding its three ports while the reader believes
239
+ // it stopped. So the demo takes its parent's death as a TERM, and stops everything it started.
240
+ //
241
+ // THE DEMO ONLY. A foreground `start` or a `run` outliving the shell that started it can be what the
242
+ // reader meant: `nohup clearotron start &` on a server they are about to log out of, or an hours-long
243
+ // clearance they will not babysit. The demo is a replay nobody leaves running on purpose, and its own
244
+ // banner already says it lasts only as long as the command that started it.
245
+ if (verb === "demo") watchParent(() => { try { child.kill("SIGTERM"); } catch { /* already gone */ } });
232
246
  }
233
247
 
234
248
  // RESOLVE BOTH SIDES THROUGH SYMLINKS. `npm install` puts a symlink at node_modules/.bin/clearotron, so
package/bin/connect.mjs CHANGED
@@ -40,7 +40,7 @@ import { createInterface } from "node:readline/promises";
40
40
  import { requireInteractive } from "../shared/invocation.mjs"; // — a prompt with nobody to answer it
41
41
  import { stdin, stdout } from "node:process";
42
42
  import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from "node:fs";
43
- import { join, dirname } from "node:path";
43
+ import { join, dirname, resolve } from "node:path";
44
44
  import { homedir, userInfo } from "node:os";
45
45
  import { fileURLToPath } from "node:url";
46
46
  import { execFileSync } from "node:child_process";
@@ -48,7 +48,11 @@ import { createServer } from "node:net";
48
48
  import { CONNECT_CLIENTS, WHERE_FLAG, clientById, leadRouteFor, plainStep, whatItNeeds } from "../shared/connect-clients.mjs";
49
49
  import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
50
50
  import { isWsl, wslTarget } from "../shared/wsl.mjs"; // — and which distribution a row should start the server in
51
- import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
51
+ import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT, demoTokenSecretPath, keyPurposeLine, demoConnectCommand } from "../shared/client-door.mjs";
52
+ import { readRunning } from "../shared/running-start.mjs";
53
+ import { readLocalCredential, passphraseResetCommand, INSTALL_CREDENTIAL_FILE } from "../driver/portal-local-auth.mjs";
54
+ import { invocationPrefix } from "../shared/invocation.mjs";
55
+ import { BRAND } from "../shared/brand.mjs";
52
56
  import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
53
57
  import { envFrom } from "../shared/env-aliases.mjs";
54
58
  import { atomicWrite } from "../driver/progress.mjs";
@@ -654,6 +658,59 @@ function printSteps(offer, key) {
654
658
  }
655
659
  }
656
660
 
661
+ /**
662
+ * ── A RUNNING DEMO, NAMED BY ITS FOLDER (owner, 2026-09-19) ─────────────────────────────────────────
663
+ *
664
+ * The demo is how a newcomer meets the product, and the next thing they ask an assistant is to connect.
665
+ * The route that works is the demo's own client door with an account key, and reaching it took a key
666
+ * issued by hand and an address read out of the startup log. This does both: it finds the demo that is
667
+ * running from that folder, mints a key with the demo's own secret for the demo's one user, and prints
668
+ * the address and the key, and nothing else is changed. No unit is written and no setting touched: the
669
+ * demo's door is already open, and everything of the demo goes when it stops.
670
+ *
671
+ * The address route's steps are not printed. They are written for a door behind a sign-in service,
672
+ * reached over the web, and a demo's door is on this machine and takes the key instead.
673
+ */
674
+ async function connectDemo({ base, dryRun, clientName = null, running = readRunning() } = {}) {
675
+ const at = resolve(base);
676
+ const prefix = invocationPrefix();
677
+ say("");
678
+ if (clientName) { say(` ${clientName}`); say(""); }
679
+ const demo = running.find((r) => r.demo && resolve(String(r.base ?? "")) === at);
680
+ if (!demo) {
681
+ say(` Not available here — no demo is running from ${at}.`);
682
+ say(` What would change it: start it with \`${prefix}clearotron demo --base ${/\s/.test(at) ? `"${at}"` : at}\`, then run this again.`);
683
+ return 1;
684
+ }
685
+ if (!demo.ports?.client) {
686
+ say(` Not available here — the demo running from ${at} has no client door open.`);
687
+ say(" What would change it: stop the demo and start it again; its output says why the door did not start.");
688
+ return 1;
689
+ }
690
+ const address = `http://${!demo.host || demo.host === "0.0.0.0" ? "127.0.0.1" : demo.host}:${demo.ports.client}/mcp`;
691
+ const credentialPath = join(at, INSTALL_CREDENTIAL_FILE);
692
+ let email = "demo@localhost";
693
+ try { email = readLocalCredential(credentialPath)?.email || email; } catch { /* the demo's own user stands */ }
694
+ if (dryRun) {
695
+ say(" (dry run — nothing was changed)");
696
+ say(` Address: ${address}`);
697
+ return 0;
698
+ }
699
+ let secret = "";
700
+ try { secret = readFileSync(demoTokenSecretPath(at), "utf8").trim(); } catch { /* said below */ }
701
+ if (!secret) {
702
+ say(` Not available here — the demo running from ${at} keeps no key secret this command can read.`);
703
+ say(" What would change it: run this as the user who started the demo.");
704
+ return 1;
705
+ }
706
+ const key = withSecret(secret, () => mintToken({ scope: "account", sub: email, ttlSec: 90 * 24 * 3600 }));
707
+ say(` ${keyPurposeLine({ email, brand: BRAND.name, reset: passphraseResetCommand({ prefix, credentialPath }) })}`);
708
+ say("");
709
+ say(` Address: ${address}`);
710
+ say(` Key: ${key}`);
711
+ return 0;
712
+ }
713
+
657
714
  async function main() {
658
715
  const argv = process.argv.slice(2);
659
716
  // `--help` IS ANSWERED, not rejected. Running this bare drops the reader into a question, so "run it
@@ -674,6 +731,7 @@ async function main() {
674
731
  say(" internet, with a key made for you now");
675
732
  say(" --list the assistants this build knows");
676
733
  say(" --dry-run say what would change, change nothing");
734
+ say(" --base <dir> a running demo's folder: mint a key for its client door, and print both");
677
735
  say(" --allow-checkout-move");
678
736
  say(" write this checkout's path even though services are running from");
679
737
  say(" another one. Every unit's ExecStart follows that value, so the ones");
@@ -681,7 +739,7 @@ async function main() {
681
739
  say("");
682
740
  return 0;
683
741
  }
684
- const known = new Set(["--client", "--where", "--list", "--dry-run", "--allow-checkout-move", "--help", "-h"]);
742
+ const known = new Set(["--client", "--where", "--list", "--dry-run", "--base", "--allow-checkout-move", "--help", "-h"]);
685
743
  const unknown = argv.filter((a) => a.startsWith("--") && !known.has(a));
686
744
  if (unknown.length) {
687
745
  console.error(`connect: unrecognised flag(s): ${unknown.join(", ")}`);
@@ -689,6 +747,13 @@ async function main() {
689
747
  process.exit(2);
690
748
  }
691
749
  const dryRun = argv.includes("--dry-run");
750
+ const b = argv.indexOf("--base");
751
+ if (b >= 0) {
752
+ const base = argv[b + 1];
753
+ if (!base || base.startsWith("--")) { console.error("connect: --base needs the demo's folder."); process.exit(2); }
754
+ const c = argv.indexOf("--client");
755
+ return await connectDemo({ base, dryRun, clientName: c >= 0 ? clientById(argv[c + 1])?.name ?? null : null });
756
+ }
692
757
  const w = argv.indexOf("--where");
693
758
  if (w >= 0 && !Object.hasOwn(WHERE_FLAG, argv[w + 1] ?? "")) {
694
759
  console.error(`connect: --where takes one of: ${Object.keys(WHERE_FLAG).join(", ")}`);
package/bin/example.mjs CHANGED
@@ -47,7 +47,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
47
47
  import { spawn } from "node:child_process";
48
48
  import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
49
49
  import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
50
- import { isFrozen, demoChildren, demoInventory, prepareSample } from "../driver/demo-container.mjs"; // — one definition of what a frozen demo is, for the player AND the gate
50
+ import { isFrozen, demoChildren, demoInventory, prepareSample, releaseDemoCopies } from "../driver/demo-container.mjs"; // — one definition of what a frozen demo is, for the player AND the gate
51
51
  import { ensureDemoProgram, demoProgramEnv } from "../shared/permanent-install.mjs";
52
52
 
53
53
  const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -168,6 +168,14 @@ if (!sampleDirs.length || !isFrozen(sampleDir)) {
168
168
  // rest are published. An unreadable file inside one sample used to throw out of the copy here and take
169
169
  // every demo down with a stack trace (measured on a published beta, 2026-09-11).
170
170
  const failures = ALL ? inventory.unusable.map((u) => ({ name: u.name, why: u.why })) : [];
171
+ // A STOP WHILE THE SAMPLES ARE COPIED OR PUBLISHED still takes the copies with it. The copies go on exit
172
+ // (driver/demo-container.mjs), and a signal's default ends the process without one. Taken off again once
173
+ // the copies are gone, before the portal's own handlers are installed.
174
+ const stopEarly = [["SIGINT", 2], ["SIGTERM", 15], ["SIGHUP", 1]].map(([sig, no]) => {
175
+ const h = () => process.exit(128 + no);
176
+ process.once(sig, h);
177
+ return [sig, h];
178
+ });
171
179
  const samples = [];
172
180
  for (const dir of sampleDirs) {
173
181
  const r = prepareSample(dir, { repoRoot: REPO });
@@ -277,6 +285,9 @@ for (const s0 of samples) {
277
285
  failures.push({ name: s0.name, why: String(e?.message ?? e) });
278
286
  }
279
287
  }
288
+ // PUBLISHED, SO THE COPIES THEY WERE PUBLISHED FROM GO NOW, whatever comes next.
289
+ releaseDemoCopies();
290
+ for (const [sig, h] of stopEarly) process.off(sig, h);
280
291
  if (!results.length) {
281
292
  die(`demo: no demo could be replayed.`, "", ...failures.map((f) => ` ${f.name}: ${f.why}`));
282
293
  }
package/bin/key.mjs CHANGED
@@ -28,9 +28,10 @@
28
28
  // question would be two answers, and the wrong one would be the one nobody read.
29
29
  import "../shared/env-local.mjs"; // side effect: apply the install's .env — FIRST, before anything reads process.env
30
30
  import { invocationPrefix } from "../shared/invocation.mjs";
31
+ import { BRAND } from "../shared/brand.mjs";
31
32
  import { existsSync, readFileSync } from "node:fs";
32
33
  import { defaultGrantsPath, installPaths } from "./start.mjs";
33
- import { demoTokenSecretPath } from "../shared/client-door.mjs";
34
+ import { demoTokenSecretPath, keyPurposeLine } from "../shared/client-door.mjs";
34
35
  import { resolvePerson } from "../shared/scope.mjs"; // the door's own reading of the guest list, so one answer serves both
35
36
  import { mintFromOptions } from "../mcp-server/mint-token.mjs";
36
37
 
@@ -96,6 +97,10 @@ try {
96
97
  die(e.message);
97
98
  }
98
99
 
100
+ // WHAT THIS KEY IS FOR, FIRST, in the owner's words (2026-09-19): a person who found this verb before the
101
+ // passphrase minted a key and could not sign in to the portal with it. Standard error, as every line here
102
+ // is, so the token stays alone on standard output.
103
+ console.error(keyPurposeLine({ email, brand: BRAND.name, reset: `${p}clearotron passphrase --reset` }));
99
104
  for (const line of minted.notes) console.error(line);
100
105
 
101
106
  // — found in review. A KEY FOR AN IDENTITY ON NO LIST IS A KEY THAT 403s. This command
package/bin/onboard.mjs CHANGED
@@ -2818,6 +2818,9 @@ export async function runCheck() {
2818
2818
  info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
2819
2819
  + "the door verdicts below are withheld rather than guessed, because a failure to look is not a finding");
2820
2820
  if (door.shape === "local") {
2821
+ // WHO THAT ONE USER IS, in the owner's words (2026-09-19), resolved as `start` resolves it.
2822
+ const oneUser = String(effectiveForService("PORTAL_LOCAL_USER")?.v || `${userInfo().username}@localhost`).trim().toLowerCase();
2823
+ say(` · Portal sign-in: one user, ${oneUser}, by passphrase.`);
2821
2824
  say(` · local passphrase door (${typed}) — one operator, one passphrase, no identity provider`);
2822
2825
  info(`a lost passphrase is recoverable: ${invocationPrefix()}clearotron passphrase --reset`);
2823
2826
  } else if (door.shape === "fronted") {
@@ -3194,7 +3197,10 @@ export async function runCheck() {
3194
3197
  try { const s = statSync(p); if (s.size > 0) { wrote = p; break; } } catch { /* not this one */ }
3195
3198
  }
3196
3199
  if (wrote) {
3197
- ok(`the client door's access log is being written: ${wrote}`);
3200
+ // WHAT WAS READ, AND NOTHING MORE. This said "is being written" from any non-empty file — a claim
3201
+ // about activity made from a file's size, printed over a log whose only line was a test's
3202
+ // (measured 2026-09-19). Reworded as the owner ruled, 2026-09-19.
3203
+ ok(`the client door's access log has entries: ${wrote}`);
3198
3204
  } else {
3199
3205
  // NOT-YET-WRITTEN IS NOT UNWRITABLE. A door nobody has called through has no log, and saying it
3200
3206
  // is broken would be the same lie as calling an unreachable unit inactive.
@@ -3338,8 +3344,22 @@ export async function runCheck() {
3338
3344
  sock.once("error", (e) => done(e.code === "ECONNREFUSED" ? false : null));
3339
3345
  });
3340
3346
  } catch { /* stays null — nobody could ask */ }
3347
+ // WHOSE LISTENER. Only this install's own evidence counts: its unit running (which binds this very
3348
+ // port), or a live start of this install whose record names the port. Anything else on the port is
3349
+ // somebody's process, and on a shared box usually another account's door.
3350
+ let doorOwnListener = null;
3351
+ if (doorListening === true) {
3352
+ if (doorActive === true) doorOwnListener = true;
3353
+ else {
3354
+ try {
3355
+ const { clientDoorPort: portOf } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
3356
+ const { readRunning } = await import(pathToFileURL(join(REPO, "shared", "running-start.mjs")).href);
3357
+ if (readRunning().some((r) => r?.ports?.client === portOf(doorEnv))) doorOwnListener = true;
3358
+ } catch { /* stays null — an unread record is not a claim either way */ }
3359
+ }
3360
+ }
3341
3361
  const door = doorState({ env: doorEnv, unitDir, exists: existsSync, active: doorActive,
3342
- listening: doorListening, activeState: doorActiveState, subState: doorSubState });
3362
+ listening: doorListening, ownListener: doorOwnListener, activeState: doorActiveState, subState: doorSubState });
3343
3363
  // EVERY COMMAND THROUGH `invoke`. Doctor's own guard runs every command doctor
3344
3364
  // prints from a directory that is not the install; a literal `clearotron start` in that text is
3345
3365
  // `command not found` for a reader with no shim, and the guard caught exactly that the moment this
@@ -4266,7 +4286,7 @@ try {
4266
4286
  // Enter yield the empty string, `present("")` is false, and askValue loops with "A value is needed
4267
4287
  // here." — so a reader taking the header at its word ("Enter takes the default in brackets") on the
4268
4288
  // one prompt that advertises empty as an answer could not leave it. Driven on the merged tree:
4269
- // 60 enters, killed at the cap, every cycle this prompt. Same defect class as 's
4289
+ // 60 enters, killed at the cap, every cycle this prompt. Same defect class as the
4270
4290
  // engine-menu loop, arriving one commit after it was fixed, in a prompt this branch's own sibling
4271
4291
  // added.
4272
4292
  //
@@ -32,6 +32,7 @@ import { defaultInstallBase, establishCredential, installCredential, readLocalCr
32
32
  // rather than hardcoded. A hardcoded `npx ` tells a global installer their install is somehow lesser;
33
33
  // a hardcoded bare name sends an npx reader to `command not found`.
34
34
  import { invocationPrefix } from "../shared/invocation.mjs";
35
+ import { BRAND } from "../shared/brand.mjs";
35
36
 
36
37
  const P = invocationPrefix();
37
38
  const USAGE = ` ${P}clearotron passphrase — report or reset the portal's local sign-in
@@ -117,8 +118,9 @@ catch (e) {
117
118
  process.exit(1);
118
119
  }
119
120
 
120
- console.log(`\n A NEW passphrase has been minted for ${email}.`);
121
- console.log(` The previous one no longer works.\n`);
121
+ // THE OWNER'S WORDS, 2026-09-19: what the value is for comes before it, and names the one user it signs in.
122
+ console.log(`\n New passphrase for this ${BRAND.name}'s one user, ${email}. Paste it on the portal's sign-in page:\n`);
122
123
  console.log(` PASSPHRASE: ${minted.passphrase}\n`);
124
+ console.log(` The previous one no longer works.`);
123
125
  console.log(` Write it down now. It is stored only as a digest, so this line is the only copy that will`);
124
126
  console.log(` ever exist — re-run this command if you lose it.\n`);
package/bin/start.mjs CHANGED
@@ -121,7 +121,7 @@ import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "
121
121
  // — the door --background now INSTALLS, and the one authority for the settings
122
122
  // it refuses to start without. (Until 2026-09-03 this import read "the one unit --background may
123
123
  // tolerate and never manage"; settled point 2 superseded that.)
124
- import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort, demoTokenSecret, demoTokenSecretPath, keyIssueCommand } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
124
+ import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort, demoTokenSecret, demoTokenSecretPath, keyIssueCommand, demoConnectCommand } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
125
125
  import { createServer } from "node:net";
126
126
  import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
127
127
  import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
@@ -805,6 +805,10 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
805
805
  // DNS-rebinding protection is keyed off this list being non-empty, so it is derived from the same
806
806
  // port the listener is given rather than left to whoever remembers.
807
807
  TRADEMARK_MCP_ALLOWED_HOSTS: `${host}:${ports.mcp},localhost:${ports.mcp}`,
808
+ // WHERE THE OTHER DOOR IS, so a key refused here is told where it belongs (shared/scope.mjs
809
+ // otherDoor). Read by nothing else in this process.
810
+ CLIENT_MCP_HTTP_HOST: host,
811
+ CLIENT_MCP_HTTP_PORT: String(ports.client),
808
812
  },
809
813
  // The worker needs the install's PATHS and nothing else — no ports, no secrets, no door config. It
810
814
  // talks to the queue and the pool, not to either listener.
@@ -830,6 +834,9 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
830
834
  // written in one place and an allow-list in another drift into a door that starts and turns every
831
835
  // request away, which reads as a dead door rather than as a misconfiguration.
832
836
  CLIENT_MCP_ALLOWED_HOSTS: `${host}:${ports.client},localhost:${ports.client}`,
837
+ // The staff door's place, for the mirror refusal (shared/scope.mjs otherDoor). Read by nothing else here.
838
+ TRADEMARK_MCP_HTTP_HOST: host,
839
+ TRADEMARK_MCP_HTTP_PORT: String(ports.mcp),
833
840
  CLIENT_MCP_ACCOUNT_ACCESS: String(clientFence ?? "1"),
834
841
  // TOKEN-ONLY, AND WITHOUT IT THE DOOR DOES NOT START. Found by driving it rather than by reading
835
842
  // the diff: with this unset the door demands an OIDC audience plus a Cloudflare Access team or
@@ -1603,9 +1610,12 @@ if (isMain) {
1603
1610
  // — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
1604
1611
  // ONE SAMPLE AT A TIME: one whose files cannot be read is left out and named below, and the others
1605
1612
  // seed. Copied in one call, a single unreadable file emptied the whole archive.
1606
- const { publishContainer, seedDemoRuns } = await import("../driver/demo-container.mjs");
1613
+ const { publishContainer, seedDemoRuns, releaseDemoCopies } = await import("../driver/demo-container.mjs");
1607
1614
  const container = publishContainer(join(REPO, "demo"), { repoRoot: REPO });
1608
- const seed = await seedPool({ pool: paths.pool, examplesDir: container.dir, republish: republishRun });
1615
+ let seed;
1616
+ // THE COPY GOES AS SOON AS THE POOL IS SEEDED FROM IT, and on the way out if seeding throws.
1617
+ try { seed = await seedPool({ pool: paths.pool, examplesDir: container.dir, republish: republishRun }); }
1618
+ finally { releaseDemoCopies(); }
1609
1619
  // AND AS RUNS, so the assistant this demo's connect line wires has them to list, brief and open. Under
1610
1620
  // the demo's own workspace only: nothing of it reaches an install started afterwards. Their report
1611
1621
  // links are stamped with this portal's address, the one the Open line prints.
@@ -2421,7 +2431,8 @@ if (isMain) {
2421
2431
  // The output used to name neither as a door, so an owner watching one of them start concluded MCP had
2422
2432
  // not come up. Both are printed with who each is for, because "MCP is running" is ambiguous on a box
2423
2433
  // that has two of them and the ambiguity is what cost the leg.
2424
- say(` Engine door http://${HOST}:${ports.mcp}/mcp — the portal's Start button calls this. Staff.`);
2434
+ // THE CLIENT DOOR LEADS (owner, 2026-09-19). It is the one an assistant uses, and with the engine door
2435
+ // printed first a demo's reader handed the staff address to their assistant, which refused its key.
2425
2436
  // THE SYNCHRONOUS TRUTH, NOT THE ASYNC FLAG ( — F26, review finding).
2426
2437
  //
2427
2438
  // `rec.alive` is flipped by the child's exit handler, which is async: reading it here asks "has the
@@ -2440,11 +2451,12 @@ if (isMain) {
2440
2451
  if (adoptedClientDoor)
2441
2452
  say(` Already running as ${CLIENT_DOOR_UNIT}; this start kept it, so existing keys still work.`);
2442
2453
  else
2443
- say(` It refuses every caller until a key is issued: ${keyIssueCommand({ prefix: invocationPrefix(), demo: DEMO, user, base: paths.base, defaultBase: join(homedir(), "trademark") })}`);
2454
+ say(` It refuses every caller until a key is issued: ${DEMO ? demoConnectCommand({ prefix: invocationPrefix(), base: paths.base }) : keyIssueCommand({ prefix: invocationPrefix(), demo: DEMO, user, base: paths.base, defaultBase: join(homedir(), "trademark") })}`);
2444
2455
  } else {
2445
2456
  say(` Client door NOT RUNNING on ${HOST}:${ports.client} — its output above says why. The portal and`);
2446
2457
  say(" the engine door are unaffected; a client assistant cannot connect until it is up.");
2447
2458
  }
2459
+ say(` Engine door http://${HOST}:${ports.mcp}/mcp — the portal's Start button calls this. Staff.`);
2448
2460
  say("");
2449
2461
  // ── WHAT `status` AND `stop` READ ABOUT THIS START ───────────────────────────────────────────────
2450
2462
  //
@@ -2556,7 +2568,7 @@ if (isMain) {
2556
2568
  // THE HEADINGS COME WITH THE PAIR, from the composer. Nothing is written here: the page prints these
2557
2569
  // same two words above these same two commands, and a second author is how the two surfaces drift.
2558
2570
  if (connect.variants) {
2559
- for (const v of connect.variants) { say(` ${v.heading}`); say(` ${v.text}`); say(""); }
2571
+ for (const v of connect.variants) { say(` ${v.heading}`); say(` ${v.text}`); if (v.hint) say(` ${v.hint}`); say(""); }
2560
2572
  } else {
2561
2573
  say(` ${connect.command}`);
2562
2574
  say("");
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "aa58558b3b09fbf5b1c83010291d340167d438d8",
3
- "version": "0.3.2-beta.11"
2
+ "commit": "f107c38f60f6cd5536cd30edb3d2b3ad8a1d1d97",
3
+ "version": "0.3.2-beta.13"
4
4
  }
@@ -46,10 +46,10 @@ uses). Enrolment is therefore the portal's: no second credential to mint, rotate
46
46
  access revokes this with it. **Off unless `CLIENT_MCP_ACCOUNT_ACCESS=1`.**
47
47
 
48
48
  **Who turns that on. The installer, since 2026-09-03** — ruling, settled
49
- point 2. `render-units.mjs --apply` and `npx clearotron start --background` both write the settings this
49
+ point 2. `render-units.mjs --apply` and `clearotron start --background` both write the settings this
50
50
  door refuses to start without and then place and enable `clearotron-client-mcp.service`. The settings
51
51
  come from one authority, `enablePlan` in `shared/client-door.mjs`, which is also what
52
- `npx clearotron connect` calls.
52
+ `clearotron connect` calls.
53
53
 
54
54
  **This supersedes the 2026-08-31 ruling** *"On demand is fine"*, under which nothing at install and no
55
55
  rebuild's enable list could start this unit, because starting it WAS the consent that opened
@@ -57,10 +57,10 @@ access for each company's people. The owner changed the posture knowingly: **the
57
57
  gate, not whether a process runs.** A door with no key issued refuses everything, which is the same
58
58
  protection by a mechanism that does not depend on a reader finding a verb.
59
59
 
60
- **`npx clearotron disconnect` therefore revokes a person, not a service** (Q3). It writes the caller's key
60
+ **`clearotron disconnect` therefore revokes a person, not a service** (Q3). It writes the caller's key
61
61
  ids to the denylist and strikes them from the record; it does not stop the unit and does not touch
62
62
  `CLIENT_MCP_ACCOUNT_ACCESS`, which is the whole install's setting. Cutting everyone off is
63
- `npx clearotron disconnect --everyone`, which states how many keys and how many people that is before
63
+ `clearotron disconnect --everyone`, which states how many keys and how many people that is before
64
64
  acting — and does not stop the service either.
65
65
 
66
66
  An `account` principal reaches **eighteen** tools, for its own companies only — everything carrying
@@ -117,8 +117,8 @@ Four things about it are worth knowing before you offer it:
117
117
  - **The caller cannot choose the model.** The tier is cost and method both, and it is the one argument on
118
118
  the one tool that spends. Express the change with `instructions`.
119
119
  - **Nothing bounds the spend, by ruling.** `start_run` is stamped `clientPrincipal: true` at the
120
- chokepoint so `runCaps.dailyRuns` bites it; a what-if job carries no such stamp, because the owner
121
- ruled spend controls out ("ignore the call spend"). Every experiment records what it spent; no door
120
+ chokepoint so `runCaps.dailyRuns` bites it; a what-if job carries no such stamp, because spend controls
121
+ were deliberately left out of it. Every experiment records what it spent; no door
122
122
  refuses the next one. Concurrency IS bounded — `CLEAROTRON_WHATIF_MAX_CONCURRENT`, default 1 — because
123
123
  letting a free experiment occupy the box while a paid clearance waits is a different question from
124
124
  spend, and the ruling did not touch it.
package/docs/DELIVERY.md CHANGED
@@ -284,12 +284,12 @@ So the checks that run are exactly those whose whole input is model-authored tex
284
284
  **permission-prose**, **scope-numbers-in-prose**, **counting-consistency**,
285
285
  **wipo-designation-language**, **prescription-prose**. Every other clearance check is listed in the
286
286
  receipt under `notApplicable` with the reason its input does not exist here
287
- (`KNOCKOUT_ABSENT_BY_DESIGN` in `driver/predelivery-lint.mjs`). That is a third state (/),
287
+ (`KNOCKOUT_ABSENT_BY_DESIGN` in `driver/predelivery-lint.mjs`). That is a third state,
288
288
  and it is the point: a check that cannot apply is recorded as absent, never as a silent pass, and
289
289
  never as a `pass:false` that would project to the lawyer as a defect.
290
290
 
291
- **Why the lane cannot simply skip the lint.** The lint receipt IS the workbook's QC record
292
- (/), so a lane that writes no`_driver/predelivery-lint.json` produces an *empty* QC record
291
+ **Why the lane cannot simply skip the lint.** The lint receipt IS the workbook's QC record,
292
+ so a lane that writes no`_driver/predelivery-lint.json` produces an *empty* QC record
293
293
  rather than a deliberately-absent one — and an empty record reads as "not evaluated", never
294
294
  "passed". And since recorded defects reach the reviewing lawyer only through the projected
295
295
  machine-check sheet on the audit workbook, a lane with nothing to project is a lane whose
@@ -21,7 +21,7 @@ installer.
21
21
  ## The command
22
22
 
23
23
  ```
24
- npx clearotron brandowner add <key> --name "<legal name>" [options]
24
+ clearotron brandowner add <key> --name "<legal name>" [options]
25
25
  ```
26
26
 
27
27
  `<key>` is the bundle's filename and its identity everywhere else. It is validated on the way in, so a
package/docs/PORTAL.md CHANGED
@@ -99,7 +99,7 @@ There were two ways in and only one of them proved anything: the auth-proxy edge
99
99
  and handed every caller the same synthetic address. **Both switches are deleted**, along with
100
100
  `PORTAL_DEV_EMAIL`. Local mode replaces them with a real sign-in.
101
101
 
102
- **Use `npx clearotron start`.** It is the documented way to run this product on one machine
102
+ **Use `clearotron start`.** It is the documented way to run this product on one machine
103
103
  ([INSTALL.md](../INSTALL.md) §6) and it assembles everything below for you: the portal, the MCP face the Start button
104
104
  calls, a minted ops key, the grants file, the saved-search store and the data-plane paths — one command,
105
105
  one URL, `Ctrl-C` stops both processes. Nothing here needs an `*_AUTH_DISABLED` variable, and the
@@ -143,7 +143,7 @@ its dev bypass to provide that**:`TRADEMARK_MCP_AUTH_MODE=token` runs it with a
143
143
  access key and no auth proxy — loopback only, and refused outright alongside
144
144
  `TRADEMARK_MCP_AUTH_DISABLED`, which authenticates nobody. Mint the key with
145
145
  `mint-token.mjs --scope ops --sub portal --verbs start_run,stop_run --accounts foxglade`, or let
146
- `npx clearotron start` mint one in memory at every start and never write it down.
146
+ `clearotron start` mint one in memory at every start and never write it down.
147
147
 
148
148
  ## Putting your own login provider in front
149
149
 
@@ -177,7 +177,7 @@ the model.
177
177
  1. Put the proxy in front of the portal's port and make it require sign-in. The portal must not be
178
178
  reachable except through it — an origin anyone can reach directly is an origin with no door.
179
179
  2. Set `PORTAL_AUTH_MODE=auth-proxy` and the four values above in `.env`.
180
- 3. `npx clearotron doctor` — the **Portal door** section reports which door is configured and which of
180
+ 3. `clearotron doctor` — the **Portal door** section reports which door is configured and which of
181
181
  the four values are present, by name. It never prints their values.
182
182
  4. Sign in through the proxy once and confirm the portal shows your address rather than a passphrase
183
183
  box.
package/docs/RELEASES.md CHANGED
@@ -16,7 +16,7 @@ npm install -g clearotron@beta # newest — cut when there is something wort
16
16
  | | `latest` (stable) | `beta` |
17
17
  |---|---|---|
18
18
  | **Version looks like** | `0.2.0` | `0.2.1-beta.4` |
19
- | **Cut when** | a beta has passed a full clearance run and a from-scratch install by somebody who has never seen the product, and the owner says go | when a change lands that is worth testing, or while a stable is being prepared |
19
+ | **Cut when** | a beta has passed a full clearance run and a from-scratch install by somebody who has never seen the product, and the maintainers decide to ship it | when a change lands that is worth testing, or while a stable is being prepared |
20
20
  | **Promises** | it installed and ran a real clearance end to end before it was published | it built, and the automated suite passed |
21
21
  | **Use it if** | you are running this for real work | you want a fix that landed today, or you are helping test |
22
22
 
package/docs/SECURITY.md CHANGED
@@ -123,7 +123,7 @@ what the mechanism guarantees.*
123
123
  - One OPERATOR issuance path: `mint-token.mjs` (prints once, stores nothing; `sub` names the
124
124
  principal in every audit line; the `jti` printed at mint time is the revocation handle). Two
125
125
  automatic minters sit beside it on the same `mintToken`: the clearance publisher mints the report
126
- link's run-bound `user` token at publish, and `npx clearotron start` mints the portal's verb-scoped,
126
+ link's run-bound `user` token at publish, and `clearotron start` mints the portal's verb-scoped,
127
127
  company-capped ops token in memory at every start. Neither prints, and neither is written down.
128
128
  - **Revocation**: denylist file checked on every verification; missing file = nothing revoked (the
129
129
  denylist can never take all auth down). **Rotation**: two-secret window, flag-day-free.
@@ -176,11 +176,11 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
176
176
  |---|---|---|
177
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. |
178
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. |
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. |
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 `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. |
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; a decision of 2026-09-04 **reversed that exemption** and renamed the whole namespace, so it carries the house prefix like everything else. One spelling, no exceptions. |
181
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. |
182
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. |
183
- | `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose ( — the removed`corsearch` fallback named a vendor the deployment did not choose). Three are paid global sweeps; `euipo` (EU) and `uspto-local` (US) are free single-office sources, and `free-tier` composes those two as one register. Choosing a free value makes every territory outside its coverage a disclosed *deferred* row. Unknown ids throw loudly, and the gather layer throws at stage time for any provider without a built MCP server. |
183
+ | `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose (the removed `corsearch` fallback named a vendor the deployment did not choose). Three are paid global sweeps; `euipo` (EU) and `uspto-local` (US) are free single-office sources, and `free-tier` composes those two as one register. Choosing a free value makes every territory outside its coverage a disclosed *deferred* row. Unknown ids throw loudly, and the gather layer throws at stage time for any provider without a built MCP server. |
184
184
  | `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox helper bypassed, for hosts where that helper cannot spawn. It removes a defence: with it set, the run dir is writable by the seat and only the deny-hook stands between a stage and `_driver/`. Set it because the host forces it, never for convenience. |
185
185
 
186
186
  ## Environment variable reference
@@ -225,7 +225,7 @@ deployment may override (verify live values per deployment).
225
225
  | `CLEAROTRON_RUN_LOCK_POLL_MS` | 15000 | Run-slot acquire poll cadence. |
226
226
  | `CLEAROTRON_ADMISSION_BUDGET_MS` | 7200000 (2 h) | Runner stops claiming new jobs after this per activation; leftovers re-trigger a fresh activation. |
227
227
  | `CLEAROTRON_QUEUE_SCAN_MS` | 10000 (min 1000) | Mid-drain re-scan for newly arrived jobs. |
228
- | `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many queued what-ifs the runner drains at once. Deliberately NOT a run-slot: an experiment that took one from `CLEAROTRON_MAX_CONCURRENT_RUNS` could block an admitted paid clearance rather than merely share the box with it. This is a concurrency bound and not a spend control — the owner ruled spend controls out when he opened what-if to each company's people (2026-08-27), and nothing here refuses the next experiment. |
228
+ | `CLEAROTRON_WHATIF_MAX_CONCURRENT` | 1 (min 1) | How many queued what-ifs the runner drains at once. Deliberately NOT a run-slot: an experiment that took one from `CLEAROTRON_MAX_CONCURRENT_RUNS` could block an admitted paid clearance rather than merely share the box with it. This is a concurrency bound and not a spend control — spend controls were deliberately left out when what-if opened to each company's people (2026-08-27), and nothing here refuses the next experiment. |
229
229
  | `CLEAROTRON_STOP_GRACE_MS` | 60000 | Grace after first SIGTERM before exit(1). |
230
230
  | `CLEAROTRON_MAX_CLAIM_AGE_MS` | 172800000 (48 h; 0 disables) | Hard ceiling on a claim's age (from the `.pid` sidecar mtime) — beyond it, re-claim regardless of liveness. |
231
231
  | `CLEAROTRON_KNOCKOUT_VARIANT_CAP` | unset (⇒ the lane's own cap) | Ceiling on variants a knockout screens per name. Set only to bound an unusually wide batch; absent means the lane decides. |