clearotron 0.3.0-beta.10 → 0.3.0-beta.2

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 (75) hide show
  1. package/.env.example +3 -13
  2. package/INSTALL.md +6 -110
  3. package/README.md +21 -31
  4. package/bin/connect.mjs +26 -71
  5. package/bin/disconnect.mjs +11 -16
  6. package/bin/example.mjs +2 -14
  7. package/bin/key.mjs +5 -23
  8. package/bin/onboard.mjs +44 -191
  9. package/bin/start.mjs +33 -197
  10. package/bin/status.mjs +3 -23
  11. package/bin/stop.mjs +8 -23
  12. package/bin/update.mjs +6 -38
  13. package/build-info.json +2 -2
  14. package/demo/full-country-search/run/status.json +1 -1
  15. package/demo/global-preliminary-search/run/status.json +1 -1
  16. package/demo/knockout-search/run/email-body.md +1 -1
  17. package/demo/knockout-search/run/status.json +2 -2
  18. package/demo/multi-country-focus-search/run/status.json +1 -1
  19. package/docs/architecture/04-configuration-reference.md +1 -2
  20. package/driver/CHANGELOG.md +8 -107
  21. package/driver/compose-read.mjs +2 -21
  22. package/driver/contract-e3-backlog.mjs +1 -1
  23. package/driver/demo-container.mjs +2 -65
  24. package/driver/driver.config.mjs +0 -5
  25. package/driver/engine/anthropic-agent.mjs +17 -34
  26. package/driver/engine/jx-turn.mjs +1 -4
  27. package/driver/engine/openai-agent.mjs +2 -60
  28. package/driver/enqueue-schema.mjs +1 -3
  29. package/driver/gateway.mjs +15 -88
  30. package/driver/package.json +1 -1
  31. package/driver/portal-local-auth.mjs +3 -35
  32. package/driver/portal-service.mjs +22 -91
  33. package/driver/portal-upstream.mjs +1 -20
  34. package/driver/profile-service.mjs +3 -31
  35. package/driver/publish/index.mjs +9 -12
  36. package/driver/publish/knockout.mjs +2 -4
  37. package/driver/publish/office-record-links.mjs +21 -56
  38. package/driver/recipe-service.mjs +5 -14
  39. package/driver/record-origins.mjs +0 -14
  40. package/driver/search-policy.mjs +2 -7
  41. package/driver/suite-census.json +46 -154
  42. package/driver/systemd/render-units.mjs +13 -7
  43. package/mcp-server/CHANGELOG.md +2 -36
  44. package/mcp-server/CONNECT.md +2 -3
  45. package/mcp-server/lib/driver.mjs +0 -2
  46. package/mcp-server/lib/knockout.mjs +2 -2
  47. package/mcp-server/lib/options.mjs +5 -20
  48. package/mcp-server/package.json +1 -1
  49. package/package.json +1 -1
  50. package/portal-ui/dist/assets/{index-CwPAS0we.js → index-CcFjgM78.js} +383 -525
  51. package/portal-ui/dist/assets/{index-Cv-E_agg.css → index-CsCuPshD.css} +86 -199
  52. package/portal-ui/dist/index.html +2 -2
  53. package/portal-ui/package.json +1 -1
  54. package/providers/clarivate/src/core.js +0 -5
  55. package/providers/oauth-mcp-bridge/CHANGELOG.md +0 -32
  56. package/providers/oauth-mcp-bridge/package.json +1 -1
  57. package/scripts/e2e-unread-terminals.mjs +1 -1
  58. package/scripts/e2e.mjs +20 -197
  59. package/scripts/engine-probe.mjs +2 -2
  60. package/scripts/env-classify.mjs +0 -3
  61. package/scripts/revisit-render-check.mjs +4 -22
  62. package/scripts/travelling-predicates.mjs +1 -1
  63. package/shared/access-audience.mjs +6 -7
  64. package/shared/brand.mjs +3 -7
  65. package/shared/client-door.mjs +0 -39
  66. package/shared/connect-clients.mjs +321 -290
  67. package/shared/env-file-merge.mjs +0 -24
  68. package/shared/invocation.mjs +2 -49
  69. package/shared/names-in-force.mjs +0 -2
  70. package/shared/stdio-connect.mjs +14 -95
  71. package/shared/store-in-repo.mjs +2 -50
  72. package/shared/verb-shim.mjs +1 -10
  73. package/shared/permanent-install.mjs +0 -237
  74. package/shared/running-start.mjs +0 -73
  75. package/shared/wsl.mjs +0 -23
package/bin/onboard.mjs CHANGED
@@ -63,13 +63,11 @@ import { createInterface } from "node:readline/promises";
63
63
  import { stdin as input, stdout as output } from "node:process";
64
64
  import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, chmodSync } from "node:fs"; // read the process table here; moved that to shared/process-table.mjs
65
65
  import { homedir, userInfo } from "node:os";
66
- import { invocationPrefix, installRoute, reachableCommand } from "../shared/invocation.mjs"; // — one rule for how the reader invokes us
66
+ import { invocationPrefix } from "../shared/invocation.mjs"; // — one rule for how the reader invokes us
67
67
  import { nodeFloorVerdict } from "../shared/node-floor.mjs"; // — the floor is package.json engines, not a constant here
68
68
  import { invocationForm } from "../shared/invocation.mjs"; // — and WHY that form
69
69
  import { standFrom } from "../shared/invocation.mjs"; // is this tree one npm replaces?
70
70
  import { installShim } from "../shared/verb-shim.mjs"; // — the verb goes on PATH
71
- import { relocationPlan } from "../shared/permanent-install.mjs"; // — and the program out of npx's cache
72
- import { isWsl } from "../shared/wsl.mjs"; // — one answer to "is this WSL", shared with the connect lines
73
71
  import { styleFor, banner } from "../shared/tty-style.mjs"; // — weight where the meaning is
74
72
  import { bracketAsciiCells, BRAND } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
75
73
  // THE REFUSALS ABOUT THE SIGN-IN ADDRESS ITSELF, shared with `bin/start.mjs`. Two copies would be a
@@ -99,17 +97,9 @@ import { processTable } from "../shared/process-table.mjs"; // — /proc is no
99
97
  import { programsFromAnotherCheckout } from "../shared/checkout-move.mjs";
100
98
  import { entrypointOf } from "../driver/systemd/install-census.mjs"; // one ExecStart parser
101
99
  import { overlayReport, renderOverlayReport } from "../shared/doctrine-overlay.mjs"; // — the doctor reports the overlay
102
- import { whereSavesGo, storeCommitRefusal, storeInRepo, storeOutsideRepoMessage, resolveStoreRepoRoot } from "../shared/store-in-repo.mjs"; // — doctor says where a portal save goes once it is committed, and why saved searches are off
100
+ import { whereSavesGo } from "../shared/store-in-repo.mjs"; // — doctor says where a portal save goes once it is committed
103
101
  import { engineInventory, engineMode, ENGINE_MODES } from "../driver/config-inventory.mjs"; //
104
- import { probeEngineTurn, probeFailureText, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
105
-
106
- // THE PROVING SENTENCES NAME NO MODEL. They printed the driver's tier word, which is an Anthropic model's
107
- // name, on both engines, so a codex user was told setup was about to spend on a model family they do not
108
- // use. "Its cheapest model" is true of either engine and brands neither.
109
- export const probingLine = (engineId) =>
110
- `Probing ${engineId} with one turn on its cheapest model (this SPENDS; ${PROBE_TIMEOUT_SEC}s ceiling)…`;
111
- export const proveQuestion = ({ engineId, lane }) =>
112
- `Prove ${engineId} on the ${lane} lane now with one turn on its cheapest model (a few tokens, ${PROBE_TIMEOUT_SEC}s ceiling)?`;
102
+ import { probeEngineTurn, probeFailureText, PROBE_MODEL, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
113
103
  import { runRequiredNames, missingRequirements, REGISTER_ENV, ENGINE_ENV } from "../driver/run-requirements.mjs"; // the order-time gate's own question, asked here rather than restated
114
104
  import { pinEnv, envFrom } from "../shared/env-aliases.mjs";
115
105
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
@@ -130,12 +120,6 @@ function readIfPresent(path) {
130
120
  }
131
121
 
132
122
  const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
133
- // A PACKAGED INSTALL IS NOT A CHECKOUT. doctor says which one it is talking to, and names only commands
134
- // that run there: npm's scripts and a checkout's tree are not where a package's reader stands.
135
- const PACKAGED = installRoute(REPO) === "packaged";
136
- const THIS_TREE = PACKAGED ? "the installed package" : "this checkout";
137
- // The example job, by the path it has in THIS install, so the printed command runs from any directory.
138
- const EXAMPLE_JOB = join(REPO, "examples", "job.euipo.json");
139
123
  const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
140
124
  // WHAT IS READ IS NOT ALWAYS WHERE THE NEXT WRITE GOES. An install configured before the move still
141
125
  // has its file at the old path, and the loader still reads it — so every READ here asks
@@ -711,8 +695,23 @@ export function resolveEngineBin(bin, { env = process.env, wsl = null, onWindows
711
695
  /** A path on a Windows drive as WSL mounts it. */
712
696
  export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
713
697
 
714
- /** Whether this is a Linux running under Windows: the one answer, from shared/wsl.mjs. */
715
- export { isWsl };
698
+ /**
699
+ * Whether this is a Linux running under Windows.
700
+ *
701
+ * BOTH SIGNALS INJECTABLE, for the reason `platformEngineRefusal` gives: the readers this protects
702
+ * are the ones who cannot run this suite to find out, so a Linux runner has to be able to drive both
703
+ * answers rather than read the source and agree with it.
704
+ *
705
+ * A READ THAT FAILS ANSWERS "NOT WSL", and that is the direction that changes nothing: it leaves the
706
+ * resolution exactly as it was before this existed. Claiming WSL on a could-not-read would start
707
+ * refusing candidates under /mnt on an ordinary Linux box with an ordinary mount.
708
+ */
709
+ export function isWsl({ env = process.env, procVersion = null } = {}) {
710
+ if (String(env.WSL_DISTRO_NAME ?? "").trim()) return true;
711
+ if (String(env.WSL_INTEROP ?? "").trim()) return true;
712
+ const v = procVersion ?? (() => { try { return readFileSync("/proc/version", "utf8"); } catch { return ""; } })();
713
+ return /microsoft|wsl/i.test(v);
714
+ }
716
715
 
717
716
  /**
718
717
  * What to say about candidates passed over because they sit on a Windows drive — or `null` when none
@@ -1111,10 +1110,7 @@ export async function runCheck() {
1111
1110
  say(" Restart whatever supervises them; this command does not, because which supervisor owns");
1112
1111
  say(" them is a property of your deployment and not of this checkout.");
1113
1112
  } else if (running.state === "unknown") {
1114
- // A PACKAGED INSTALL HAS NO TREE TO BE BEHIND. npm replaces the whole package on an update, so this
1115
- // question belongs to a checkout, and on a package it is a `!` nothing can ever clear.
1116
- if (PACKAGED) info("running programs against a checkout's tree: not applicable to a packaged install");
1117
- else warn(`could not tell whether running programs are on the current tree: ${running.detail}`);
1113
+ warn(`could not tell whether running programs are on the current tree: ${running.detail}`);
1118
1114
  }
1119
1115
 
1120
1116
  // ── AND THE MORE DANGEROUS ANSWER: A DIFFERENT TREE, NOT AN OLDER ONE ──────────────────────────────
@@ -1146,8 +1142,7 @@ export async function runCheck() {
1146
1142
  if (elsewhere.programs.length > 6) say(` … and ${elsewhere.programs.length - 6} more`);
1147
1143
  say(" Either point the install back at the tree they run from, or restart them onto this one.");
1148
1144
  } else if (elsewhere.state === "unknown") {
1149
- if (PACKAGED && !configuredTree) info("running programs on a different checkout: not applicable to a packaged install");
1150
- else warn(`could not tell whether running programs are on a different checkout: ${elsewhere.detail}`);
1145
+ warn(`could not tell whether running programs are on a different checkout: ${elsewhere.detail}`);
1151
1146
  } else if (elsewhere.state === "unplaced") {
1152
1147
  // PRINTED, because the alternative is silence that reads as a clean answer. Nothing here was placed
1153
1148
  // on a tree, which is not the same as everything being on the right one, and the reader is the only
@@ -1211,7 +1206,7 @@ export async function runCheck() {
1211
1206
  // deliberate — a second reader would drift from this one exactly as the composer and the checker did
1212
1207
  // in F41, and the drift is invisible because both sides keep passing their own arms.
1213
1208
  const unitDir = join(homedir(), ".config", "systemd", "user");
1214
- const { BACKGROUND_UNITS, startPaths } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
1209
+ const { BACKGROUND_UNITS } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
1215
1210
  const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
1216
1211
  const unitEnv = hosted
1217
1212
  ? unitEnvironment({
@@ -1353,7 +1348,7 @@ export async function runCheck() {
1353
1348
  const installMode = engineMode(engineInventory(invEnv));
1354
1349
  if (installMode === ENGINE_MODES.DEMO) {
1355
1350
  info("MODE: demo — everything works except starting a NEW search. The example report, its audit trail "
1356
- + `and the MCP connection are live right now; \`${reachableCommand("demo")}\` needs no engine.`);
1351
+ + "and the MCP connection are live right now; `npm run example` needs no engine.");
1357
1352
  // NAMES THE COMMAND, AND THE RESTART. The settings page says both now, and a doctor that named
1358
1353
  // the program but not how to install it — or that left out the restart, which is what actually
1359
1354
  // unsticks a reader who has just installed it — would be the third opinion this issue exists to
@@ -1445,7 +1440,7 @@ export async function runCheck() {
1445
1440
  } else if (!bin.executable || bin.relative) {
1446
1441
  info("not probed — there is no usable binary to probe. Fix the line above first.");
1447
1442
  } else {
1448
- say(`\n ${probingLine(engineId)}`);
1443
+ say(`\n Probing ${engineId} with one ${PROBE_MODEL}-tier turn (this SPENDS; ${PROBE_TIMEOUT_SEC}s ceiling)…`);
1449
1444
  // The engine as a RUN would see it: environment first, .env behind it. Only the engine-selection
1450
1445
  // keys — the probe must bill exactly the way this box bills and moves no other variable.
1451
1446
  const probeEnv = { ...process.env };
@@ -1503,9 +1498,6 @@ export async function runCheck() {
1503
1498
  } else if (form.form === "shim-path") {
1504
1499
  warn(`${form.dir} is not on this shell's PATH, so the bare \`clearotron\` will not resolve here`);
1505
1500
  info(`add it with: export PATH="${form.dir}:$PATH" — or open a new login shell`);
1506
- } else if (form.form === "npx-pinned") {
1507
- info(`this is running from npm's npx cache, so the commands below name this version through npx (\`${form.prefix.trim()}\`), `
1508
- + `which works from any directory and after npm cleans its cache; \`${invoke("install")}\` puts \`clearotron\` on your PATH`);
1509
1501
  } else if (form.shimKind === "ours-other-install") {
1510
1502
  warn(`${form.shim} is a shim for a DIFFERENT install (${form.otherInstall})`);
1511
1503
  info(`re-run \`${invoke("install")}\` to point the bare name at this one`);
@@ -1561,9 +1553,9 @@ export async function runCheck() {
1561
1553
  {
1562
1554
  const inCheckout = (p) => isInsideCheckout(p, REPO);
1563
1555
  for (const [name, what] of [
1564
- ["CLEAROTRON_CUSTOMERS_DIR", `customers resolve to the bundled demo roster IN ${THIS_TREE}`],
1565
- ["CLEAROTRON_INSTRUCTIONS_DIR", `doctrine resolves to the bundled files IN ${THIS_TREE}`],
1566
- ["PROFILE_REPO_ROOT", `the portal's profile editor commits INTO ${THIS_TREE}`],
1556
+ ["CLEAROTRON_CUSTOMERS_DIR", "customers resolve to the bundled demo roster IN this checkout"],
1557
+ ["CLEAROTRON_INSTRUCTIONS_DIR", "doctrine resolves to the bundled files IN this checkout"],
1558
+ ["PROFILE_REPO_ROOT", "the portal's profile editor commits INTO this checkout"],
1567
1559
  ]) {
1568
1560
  // — WHICH NAME THE READER IS TOLD IS NOT ONE QUESTION, IT IS TWO.
1569
1561
  //
@@ -1636,13 +1628,6 @@ export async function runCheck() {
1636
1628
  // because a reader debugging `clearotron run` by hand IS this process.
1637
1629
  info(`this command's own process ${where} — a CLI is not started by the units' EnvironmentFile, so `
1638
1630
  + "that differs by design and is not what a run uses");
1639
- } else if (!hosted && svcCustomers?.from === serviceEnvLabel) {
1640
- // NAMED IN THE ENV FILE ALONE, ON AN INSTALL WITH NO UNITS. `clearotron start` loads that file and
1641
- // its services inherit the store; this command does not load it, so its own process fell back to
1642
- // the bundled roster and printed that as the answer, one line below a ✓ for the same variable.
1643
- ok(`the services resolve profiles from ${svcCustomers.v} (${svcCustomers.from})`);
1644
- info(`this command's own process ${where} — it does not load ${serviceEnvFile}, so that differs by `
1645
- + `design and is not what \`${invoke("start")}\` hands the services`);
1646
1631
  } else {
1647
1632
  if (r.situation === "overlay" && !r.findings.length) ok(`${where} — the configured store`);
1648
1633
  else if (r.situation === "bundled-fallback") info(`${where} — THE BUNDLED DEMO ROSTER, because CLEAROTRON_CUSTOMERS_DIR is unset. Legitimate on a generic-defaults install; a fallback either way, and it is what a misconfigured deployment also looks like`);
@@ -1653,8 +1638,7 @@ export async function runCheck() {
1653
1638
  info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
1654
1639
  + "what the services resolve is not judged here, and the line above is this process's own answer");
1655
1640
  info("what a RUN used is its own `profile-store` journal line — this command reports what the "
1656
- + (hosted ? "units' file says, which the running services read at their own start"
1657
- : `environment you are typing in and your environment file say, which \`${invoke("start")}\` reads at its own start`));
1641
+ + (hosted ? "units' file says, which the running services read at their own start" : "environment you are typing in says"));
1658
1642
  // ── AND WHO IS ACTUALLY IN IT ─────────────────────────────────────────
1659
1643
  //
1660
1644
  // The line above names the STORE. An operator who has just configured one wants to know their
@@ -1815,57 +1799,6 @@ export async function runCheck() {
1815
1799
  }
1816
1800
  }
1817
1801
 
1818
- // — SAVED SEARCHES, JUDGED BY THE RULE THE PORTAL APPLIES WHEN IT STARTS. The portal switches them off,
1819
- // and answers every saved-search route "not found", when the store is unset or sits outside the
1820
- // repository its saves are committed in. Its boot log said so and nothing an operator runs did, so an
1821
- // install whose Custom searches could never load passed this command. Same resolver, same containment
1822
- // check, read from the services' environment for the reason the roster lines give; when that could not
1823
- // be read, the lines above already said so and nothing is judged here. A store that is configured but
1824
- // holds a file that cannot be read fails every company's saved searches, in the portal and the
1825
- // connector alike, so that is read here too.
1826
- //
1827
- // WITH NO UNITS, THE SERVICES ARE `clearotron start`'s CHILDREN, and it hands every one of them a store
1828
- // whether or not the env file names it. So "off" is only ever true of a hosted box. This read the env
1829
- // file alone, and on a local install started before `start` wrote the store there, it told a portal
1830
- // that was listing saved searches that they were off.
1831
- if (!hosted || serviceKnown) {
1832
- // Layered as effectiveForService layers it — this command's environment over the services' file — so
1833
- // the repository root is read from the same place the store directory is.
1834
- let storeEnv = { ...(serviceFileEnv ?? {}), ...process.env };
1835
- let recipesSet = effectiveForService("CLEAROTRON_RECIPES_DIR");
1836
- let handedBy = "";
1837
- if (!hosted && !recipesSet?.v) {
1838
- const handed = startPaths({ env: storeEnv });
1839
- recipesSet = { v: handed.recipes, from: "clearotron start", name: "CLEAROTRON_RECIPES_DIR" };
1840
- storeEnv = { ...storeEnv, RECIPE_REPO_ROOT: handed.configStore };
1841
- handedBy = ` — where \`${invoke("start")}\` puts them`;
1842
- }
1843
- const recipesDir = recipesSet?.v || null;
1844
- if (!recipesDir) {
1845
- info("saved searches are off: CLEAROTRON_RECIPES_DIR is not set, so Custom searches in the portal and the "
1846
- + "connector offer none. Name a directory inside a git repository to switch them on");
1847
- } else if (handedBy && !existsSync(join(storeEnv.RECIPE_REPO_ROOT, ".git"))) {
1848
- // NOT CREATED YET IS NOT UNREADABLE. The store does not exist until the first start makes it, and
1849
- // reading it now would report a missing directory as a broken one.
1850
- info(`saved searches switch on at the first \`${invoke("start")}\`, which creates their store in ${recipesDir}`);
1851
- } else {
1852
- const resolved = resolveStoreRepoRoot({ names: ["RECIPE_REPO_ROOT", "PROFILE_REPO_ROOT"], fallback: REPO, env: storeEnv });
1853
- const reach = storeInRepo(recipesDir, resolved.root);
1854
- if (!reach.ok) {
1855
- warn(`saved searches are OFF: ${storeOutsideRepoMessage({ storeVar: "CLEAROTRON_RECIPES_DIR", storeDir: reach.store, repoVar: "RECIPE_REPO_ROOT", repoRoot: reach.repo })} `
1856
- + `The repository came from ${resolved.from}. Until this is fixed the portal answers every saved-search request as not found`);
1857
- } else {
1858
- const { loadRecipes } = await import("../driver/search-policy.mjs");
1859
- let unreadable = null;
1860
- try { loadRecipes({ dir: recipesDir, force: true }); } catch (e) { unreadable = String(e?.message ?? e).split("\n")[0]; }
1861
- if (unreadable) {
1862
- warn(`saved searches cannot be read from ${recipesDir}: ${unreadable}. Every company's saved searches fail `
1863
- + "to load, in the portal and the connector, until it is fixed");
1864
- } else ok(`saved searches are read from ${recipesDir}${handedBy}, and saves are committed in ${reach.repo}`);
1865
- }
1866
- }
1867
- }
1868
-
1869
1802
  // — WHICH DOCTRINE FILES THIS INSTALL OVERRIDES, AND WHETHER OURS HAVE MOVED UNDER THEM.
1870
1803
  //
1871
1804
  // The report itself shipped in and worked, reachable only as `npm run doctrine-report` — a name
@@ -1904,8 +1837,7 @@ export async function runCheck() {
1904
1837
  info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
1905
1838
  + "whether the services read an overlay is not judged here");
1906
1839
  }
1907
- if (report.ok && report.overlayConfigured)
1908
- say(`\n Full detail: ${PACKAGED ? `node ${join(REPO, "scripts", "doctrine-report.mjs")}` : "npm run doctrine-report"}`);
1840
+ if (report.ok && report.overlayConfigured) say("\n Full detail: npm run doctrine-report");
1909
1841
  } catch (e) {
1910
1842
  // An unreadable overlay THROWS by design (config.resolveSkillPath refuses rather than falling back
1911
1843
  // to the product's copy). Surfaced here rather than allowed to abort the whole check: the doctor's
@@ -1914,21 +1846,7 @@ export async function runCheck() {
1914
1846
  }
1915
1847
 
1916
1848
  say("\n Register provider");
1917
- // WHAT THE SERVICES WILL SEARCH, READ AS THE ORDER-TIME CHECK BELOW READS IT. This section asked the
1918
- // shell and this command's .env, so in a fresh terminal on a hosted box it told a working install that
1919
- // no register was selected, one screen above "nothing a search is refused for is missing from the units'
1920
- // environment": one install, two answers, and the fix it named was already set. effectiveForService is
1921
- // the reader that check asks, so the two lines cannot disagree; where it cannot read the units, this
1922
- // says it could not look rather than reporting the register absent.
1923
- // Only a HOSTED box has a second environment to read; with no units, the services are started from
1924
- // this command's own file, and `effective` has always read and labelled exactly that.
1925
- const serviceValue = hosted ? effectiveForService : effective;
1926
- const prov = serviceKnown ? serviceValue("CLEAROTRON_DATABASE") : null;
1927
- if (!serviceKnown) {
1928
- info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
1929
- + "which register the services search, and whether its keys are set, is not judged here: a failure to look, not a finding");
1930
- }
1931
- else
1849
+ const prov = effective("CLEAROTRON_DATABASE");
1932
1850
  // `blocking`, not `warn` and not `problem`. The exit status is a CONTRACT — an absence reports and
1933
1851
  // exits 0, a misconfiguration exits 1, and onboard-wizard.test.mjs holds it — so this cannot become a
1934
1852
  // `problem` however much it stops the reader: an install that has not chosen a register yet is
@@ -1947,7 +1865,7 @@ export async function runCheck() {
1947
1865
  else {
1948
1866
  ok(`${spec.id} — ${spec.label} (${prov.from})`);
1949
1867
  for (const k of spec.credentials) {
1950
- const c = serviceValue(k);
1868
+ const c = effective(k);
1951
1869
  // issue 1871 — SET, not WORKING, and the line now says which. An operator reads a tick as "this
1952
1870
  // works"; this one is equally true of a valid key, an expired key, a key scoped to the wrong
1953
1871
  // account and forty characters of nonsense. --probe-providers is what settles it.
@@ -1958,7 +1876,7 @@ export async function runCheck() {
1958
1876
  // and never as a problem, but never silently either: the reader has to know which offices this
1959
1877
  // box will not reach before they read a report that says nothing was found there.
1960
1878
  for (const k of spec.optionalCredentials ?? []) {
1961
- const c = serviceValue(k);
1879
+ const c = effective(k);
1962
1880
  if (c) ok(`${k} present (${c.from}) — presence only; add --probe-providers to prove it retrieves`);
1963
1881
  else info(`${k} is NOT set — ${spec.id} will run without it and DISCLOSE the offices it cannot reach as deferred coverage. Set it to search them.`);
1964
1882
  }
@@ -1966,10 +1884,8 @@ export async function runCheck() {
1966
1884
  }
1967
1885
 
1968
1886
  say("\n Research provider");
1969
- // The same reader as the register, for the same reason: this is a key the services use.
1970
- const px = serviceKnown ? serviceValue("PERPLEXITY_API_KEY") : null;
1971
- if (!serviceKnown) info("the units' environment could not be read, so whether the services hold PERPLEXITY_API_KEY is not judged here — a failure to look, not a finding");
1972
- else if (px) ok(`PERPLEXITY_API_KEY present (${px.from}) — presence only; add --probe-providers to prove it answers`);
1887
+ const px = effective("PERPLEXITY_API_KEY");
1888
+ if (px) ok(`PERPLEXITY_API_KEY present (${px.from}) — presence only; add --probe-providers to prove it answers`);
1973
1889
  else info("PERPLEXITY_API_KEY is not set — the three clearance searches carry the common-law grid and cannot switch it off, so a clearance stops before it starts, and names the missing key; a Knockout search still runs and discloses the half it skipped");
1974
1890
 
1975
1891
  // ── — THE LANES A PRODUCT DECLARES IT NEEDS, BEFORE A REPORT NAMES THEM ──
@@ -2238,26 +2154,15 @@ export async function runCheck() {
2238
2154
  say("\n Portal sign-in");
2239
2155
  {
2240
2156
  const { authView } = await import("../driver/portal-config-view.mjs");
2241
- // A BOX WITH NO UNITS RUNS THE PORTAL `clearotron start` LAUNCHES, as the line below says, and start
2242
- // hands that portal `PORTAL_AUTH_MODE=local` and refuses any other declared mode (bin/start.mjs). So on
2243
- // such a box the door is start's, not the bare service's fronted default. Reading the bare default told
2244
- // a fresh home that the portal would refuse to start and that nobody could use it; `start` then came
2245
- // up on the local sign-in and created the grants file (measured on a published beta, 2026-09-11).
2246
- const startLaunched = !hosted;
2247
- const declaredAuth = String(effectiveForService("PORTAL_AUTH_MODE")?.v ?? "").trim();
2248
2157
  const door = authView({
2249
- mode: startLaunched ? "local" : declaredAuth,
2158
+ mode: effectiveForService("PORTAL_AUTH_MODE")?.v ?? "",
2250
2159
  oidcIssuer: effectiveForService("PORTAL_OIDC_ISSUER")?.v ?? "",
2251
2160
  team: effectiveForService("CF_ACCESS_TEAM")?.v ?? "",
2252
2161
  jwksUrl: effectiveForService("PORTAL_JWKS_URL")?.v ?? "",
2253
2162
  emailClaim: effectiveForService("PORTAL_EMAIL_CLAIM")?.v ?? "",
2254
2163
  authHeader: effectiveForService("PORTAL_AUTH_HEADER")?.v ?? "",
2255
2164
  });
2256
- const typed = declaredAuth ? `PORTAL_AUTH_MODE=${declaredAuth}`
2257
- : startLaunched ? "PORTAL_AUTH_MODE is unset, and `start` runs the local sign-in" : "PORTAL_AUTH_MODE is unset";
2258
- if (startLaunched && declaredAuth && declaredAuth.toLowerCase() !== "local")
2259
- problem(`${typed}: \`${reachableCommand("start")}\` refuses it, because start is the local install `
2260
- + "and runs the local sign-in; that mode belongs to a hosted deployment's units");
2165
+ const typed = door.declared ? `PORTAL_AUTH_MODE=${door.declared}` : "PORTAL_AUTH_MODE is unset";
2261
2166
  // NOT A TICK, AND THAT IS THE POINT. This reads the environment THIS command is
2262
2167
  // typed in. `bin/start.mjs` INJECTS `PORTAL_AUTH_MODE: "local"` into the portal's own environment,
2263
2168
  // and a systemd unit's EnvironmentFile can name a third thing — so a green tick here was a
@@ -2322,13 +2227,7 @@ export async function runCheck() {
2322
2227
  const { makePrincipal } = await import("../driver/portal-access.mjs");
2323
2228
  // ASKED OF THE SERVICE'S OWN ENVIRONMENT. Reading this command's file here is what produced a hard
2324
2229
  // ✗ claiming nobody could use a portal that was admitting its operator on every request.
2325
- // ON A BOX `start` RUNS, an unset grants file is the one start creates on its first run, admitting
2326
- // the person who started it (`paths.grants` in bin/start.mjs). Before that run it does not exist,
2327
- // which is a first start still to come, not a portal nobody can use.
2328
- const namedGrants = effectiveForService("CLEAROTRON_ACCESS_FILE")?.v ?? "";
2329
- const startGrants = startLaunched && !namedGrants ? startPaths({ env: {} }).grants : null;
2330
- const beforeFirstStart = Boolean(startGrants) && !existsSync(startGrants);
2331
- const grantsFile = namedGrants || (beforeFirstStart ? "" : startGrants ?? "");
2230
+ const grantsFile = effectiveForService("CLEAROTRON_ACCESS_FILE")?.v ?? "";
2332
2231
  let grants = null, unreadable = null;
2333
2232
  if (grantsFile) {
2334
2233
  try { grants = JSON.parse(readFileSync(grantsFile, "utf8")); }
@@ -2341,10 +2240,7 @@ export async function runCheck() {
2341
2240
  // A FAILURE TO LOOK IS NOT A LOCKOUT. On a hosted box whose unit environment
2342
2241
  // could not be read, every name above resolves empty — which is indistinguishable from a box that
2343
2242
  // has genuinely configured nothing, and would print the loudest ✗ in this command on no evidence.
2344
- if (beforeFirstStart) {
2345
- info(`no grants file yet: the first \`${reachableCommand("start")}\` creates ${startGrants} and gives the person `
2346
- + "who runs it access to everything");
2347
- } else if (!serviceKnown) {
2243
+ if (!serviceKnown) {
2348
2244
  info("who may use this portal is not judged here: the units' environment could not be read, so a "
2349
2245
  + "grants file configured there would be invisible to this check");
2350
2246
  } else if (unreadable) {
@@ -2954,39 +2850,6 @@ if (!input.isTTY) {
2954
2850
  process.exit(2);
2955
2851
  }
2956
2852
 
2957
- // ── OUT OF NPX'S CACHE, BEFORE ANYTHING IS WRITTEN ─────────────────────────────────────────────────────
2958
- //
2959
- // Run from npx, this program lives in npm's cache, and everything below would be wired to a directory npm
2960
- // deletes: the launcher, and the connect line an assistant is registered with. So the install first puts
2961
- // this same version somewhere permanent (shared/permanent-install.mjs) and runs itself from there. Nothing
2962
- // has been written yet, so a failure here costs nothing, and it stops rather than carrying on: an install
2963
- // finished from the cache is the defect, not a fallback.
2964
- const move = relocationPlan();
2965
- if (move && !move.skip && process.env.CLEAROTRON_RELOCATED !== "1") {
2966
- say(`\n This is running from npm's temporary npx cache. Installing clearotron ${move.version} to ${move.prefix}`);
2967
- say(" first, so the launcher and your assistants keep working after npm cleans that cache or you update.\n");
2968
- // The npm that launched this, when npm says which: no second npm is guessed at.
2969
- const npmCli = process.env.npm_execpath;
2970
- const r = npmCli && existsSync(npmCli)
2971
- ? spawnSync(process.execPath, [npmCli, ...move.npmArgs], { stdio: "inherit" })
2972
- : spawnSync("npm", move.npmArgs, { stdio: "inherit" });
2973
- if (r.status !== 0 || !existsSync(move.entry)) {
2974
- console.error(`\n Could not install clearotron to ${move.prefix}${r.error ? ` (${r.error.message})` : ""}. Nothing was installed, and nothing of yours was changed.`);
2975
- console.error(` Run \`npm install --global --prefix ${move.prefix} clearotron@${move.version}\`, then \`${join(move.prefix, "bin", "clearotron")} install\`.\n`);
2976
- process.exit(1);
2977
- }
2978
- // THE REST OF THE INSTALL RUNS FROM THE PERMANENT COPY, with npm's marks of an npx arrival taken off, so
2979
- // it prints the commands of the install it now is.
2980
- const env = { ...process.env, CLEAROTRON_RELOCATED: "1" };
2981
- for (const k of ["npm_command", "npm_lifecycle_event", "npm_execpath"]) delete env[k];
2982
- const moved = spawnSync(process.execPath, [move.entry, "install", ...process.argv.slice(2)], { stdio: "inherit", env });
2983
- process.exit(moved.status ?? 1);
2984
- }
2985
- if (move?.skip) {
2986
- console.error(`\n Note: this is running from npm's temporary npx cache and cannot be moved out of it here (${move.skip}).`);
2987
- console.error(" It will stop working when npm cleans that cache. `npm install -g clearotron` installs it permanently.\n");
2988
- }
2989
-
2990
2853
  // A credential typed at a prompt is echoed by the terminal and then sits in scrollback, in tmux history,
2991
2854
  // in whatever the reader pastes into a bug report. So the echo is muted while a secret is being typed:
2992
2855
  // the output stream readline writes through drops everything while `muted` is set.
@@ -3367,7 +3230,7 @@ try {
3367
3230
  // skipping it costs an hour later.
3368
3231
  info("A file that exists is not an engine that works — so setup tries one before writing anything.");
3369
3232
  info("It takes a few seconds here. Skipped, a broken engine surfaces an hour into a real search.");
3370
- if (!await confirm(proveQuestion({ engineId: pick.id, lane: authPick.id }), true)) {
3233
+ if (!await confirm(`Prove ${pick.id} on the ${authPick.id} lane now with one ${PROBE_MODEL}-tier turn (a few tokens, ${PROBE_TIMEOUT_SEC}s ceiling)?`, true)) {
3371
3234
  info("Not proven, so not written. Pick again — the last row configures no engine at all.");
3372
3235
  continue;
3373
3236
  }
@@ -3781,14 +3644,6 @@ try {
3781
3644
  candidate["CLEAROTRON_CUSTOMERS_DIR"] = join(cfg, "profiles");
3782
3645
  candidate.PROFILE_REPO_ROOT = cfg; // no alias row — this name is current
3783
3646
  for (const k of ["CLEAROTRON_CUSTOMERS_DIR", "PROFILE_REPO_ROOT"]) ok(`${k}=${candidate[k]}`);
3784
- // A REPOSITORY ALREADY THERE IS ASKED NOW, while the operator is still here, whether it can record a
3785
- // save. `clearotron start` gives a store it creates an identity of its own; one made by hand has none
3786
- // unless somebody set it, and on a machine with no global identity the first company created in the
3787
- // portal is refused. Said here, with the command, rather than discovered on that first company.
3788
- if (existsSync(join(cfgAbs, ".git"))) {
3789
- const cannot = storeCommitRefusal(cfgAbs);
3790
- if (cannot) warn(`${cannot.message}. Until then, creating a company in the portal is refused.`);
3791
- }
3792
3647
 
3793
3648
  // CLEAROTRON_INSTRUCTIONS_DIR IS DELIBERATELY NOT WRITTEN (found in review).
3794
3649
  //
@@ -3979,13 +3834,11 @@ try {
3979
3834
 
3980
3835
  say(`\n ${style.bold("Start here:")}\n`);
3981
3836
  say(` ${invocationPrefix()}clearotron start\n`);
3982
- say(" Starts the portal and the engine door and prints one address to open in your browser. That");
3983
- say(" address is the product: you order a clearance from it and read the report there.\n");
3837
+ say(" Starts the portal and the engine door, prints one address, and opens it. That address is");
3838
+ say(" the product: you order a clearance from it and read the report there.\n");
3984
3839
  say(` ${style.dim(`Also: \`${invocationPrefix()}clearotron demo\` replays a finished report with no keys and no model calls;`)}`);
3985
- say(` ${style.dim(`\`${invocationPrefix()}clearotron run --job ${/\s/.test(EXAMPLE_JOB) ? `"${EXAMPLE_JOB}"` : EXAMPLE_JOB}\` runs a first real clearance on the EU register.`)}`);
3986
- // THE OLD WAY IS A CHECKOUT'S. A package has no npm scripts where its reader stands.
3987
- if (!PACKAGED) say(` ${style.dim("Each still works the old way too — `npm start`, `npm run example`, `node driver/pipeline.mjs`.")}`);
3988
- say("");
3840
+ say(` ${style.dim(`\`${invocationPrefix()}clearotron run --job examples/job.euipo.json\` runs a first real clearance on the EU register.`)}`);
3841
+ say(` ${style.dim("Each still works the old way too — `npm start`, `npm run example`, `node driver/pipeline.mjs`.")}\n`);
3989
3842
 
3990
3843
  // WHY THOSE LINES LOOK THE WAY THEY DO, when they are not the bare verb.
3991
3844
  //