clearotron 0.3.0-beta.4 → 0.3.0-beta.6

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 (39) hide show
  1. package/.env.example +9 -0
  2. package/INSTALL.md +13 -0
  3. package/bin/connect.mjs +3 -1
  4. package/bin/onboard.mjs +80 -28
  5. package/bin/start.mjs +82 -18
  6. package/bin/update.mjs +38 -6
  7. package/build-info.json +2 -2
  8. package/docs/architecture/04-configuration-reference.md +1 -0
  9. package/driver/CHANGELOG.md +36 -10
  10. package/driver/contract-e3-backlog.mjs +1 -1
  11. package/driver/demo-container.mjs +30 -2
  12. package/driver/engine/openai-agent.mjs +60 -2
  13. package/driver/enqueue-schema.mjs +3 -1
  14. package/driver/gateway.mjs +12 -0
  15. package/driver/package.json +1 -1
  16. package/driver/portal-local-auth.mjs +35 -3
  17. package/driver/portal-service.mjs +80 -18
  18. package/driver/portal-upstream.mjs +20 -1
  19. package/driver/search-policy.mjs +7 -2
  20. package/driver/suite-census.json +71 -23
  21. package/mcp-server/CHANGELOG.md +12 -2
  22. package/mcp-server/CONNECT.md +3 -2
  23. package/mcp-server/lib/options.mjs +13 -5
  24. package/mcp-server/package.json +1 -1
  25. package/package.json +1 -1
  26. package/portal-ui/dist/assets/{index-DWYCsOCJ.js → index-DBUVdQT-.js} +6 -0
  27. package/portal-ui/dist/index.html +1 -1
  28. package/portal-ui/package.json +1 -1
  29. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  30. package/providers/oauth-mcp-bridge/package.json +1 -1
  31. package/scripts/e2e.mjs +84 -12
  32. package/scripts/engine-probe.mjs +2 -2
  33. package/scripts/env-classify.mjs +3 -0
  34. package/shared/connect-clients.mjs +8 -0
  35. package/shared/names-in-force.mjs +1 -0
  36. package/shared/permanent-install.mjs +167 -0
  37. package/shared/stdio-connect.mjs +32 -14
  38. package/shared/verb-shim.mjs +10 -1
  39. package/shared/wsl.mjs +23 -0
package/.env.example CHANGED
@@ -399,6 +399,15 @@ CLEAROTRON_DEMO=
399
399
  # effect: deployment
400
400
  # CLEAROTRON_INVOKED_AS=
401
401
 
402
+ # Marks an install that has already moved itself out of npm's npx cache. `npx clearotron install` first
403
+ # installs the same version under ~/.local and then runs that copy's install, with this set to 1, so the
404
+ # copy does not try to move itself a second time. Unset, an install running from npx's cache moves.
405
+ # Set by bin/onboard.mjs on the one process it starts, never by an operator, so the row is left
406
+ # commented. It is listed for the reason CLEAROTRON_DEMO is: the catalogue is about what an operator can
407
+ # FIND. Read by bin/onboard.mjs.
408
+ # effect: deployment
409
+ # CLEAROTRON_RELOCATED=
410
+
402
411
  # How many client what-ifs the runner drains at once (default 1). A what-if re-runs ONE stage of a run
403
412
  # that already exists, in a sandbox, at a client's request — the owner opened it to clients on
404
413
  # 2026-08-27 and ruled SPEND controls out with it ("ignore the call spend"). This is not one.
package/INSTALL.md CHANGED
@@ -64,6 +64,15 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
64
64
  npx clearotron install
65
65
  ```
66
66
 
67
+ Run from `npx`, the install first installs Clearotron under `~/.local`, as `npm install -g --prefix
68
+ ~/.local` would, and finishes from there. That way the `clearotron` command and your assistant's connection
69
+ do not point into npm's temporary cache, which npm replaces on an update and deletes when it cleans up.
70
+ Later, the `update` command updates that copy in place.
71
+
72
+ On WSL, a program on the Windows side can hold a port that WSL reports as free, and the browser reaches
73
+ it first. VS Code's Remote-SSH port forwarding is the common case. If the page that opens is not this
74
+ install's sign-in, run the same command again with `--port 28802`, or any free number.
75
+
67
76
  A *hosted* deployment needs Linux for one further thing, the systemd outbox trigger —
68
77
  [driver/systemd/README.md](driver/systemd/README.md).
69
78
  - **A reasoning CLI on your `PATH`, signed in.** This is the prerequisite people miss. Every stage runs
@@ -749,9 +758,13 @@ Use the demo to see what this system produces. Use `npx clearotron start` to run
749
758
 
750
759
  - Generates `PORTAL_SECRET` and `TRADEMARK_MCP_TOKEN_SECRET` and **appends** them to `~/.config/clearotron/.env` at
751
760
  mode 600. Append, never rewrite: that file also holds the credentials `npx clearotron install` collected.
761
+ It also records where saved searches are kept, `CLEAROTRON_RECIPES_DIR` and `RECIPE_REPO_ROOT`, so
762
+ `clearotron doctor` and a connected assistant read the same saved searches as the portal.
752
763
  - Creates `~/trademark/` — `pool/`, `workspace/`, `queue/`, `outbox/`, `locks/`, an empty grants file,
753
764
  and a small git repository for saved searches. Same base directory `npx clearotron install` uses, so whichever
754
765
  of the two you ran first, the other finds the same install. Move it with `npx clearotron start --base <dir>`.
766
+ That does not move anything the env file already names: the saved-search lines above, and the data
767
+ directories `npx clearotron install` wrote, keep pointing at the old place until you edit them.
755
768
  - Mints your sign-in passphrase and **prints it once**. Write it down. It is stored as a scrypt digest in
756
769
  `~/trademark/portal-local-credential.json`, nothing can read it back, and no later start reprints it. To
757
770
  get a new one, run `clearotron passphrase --reset`. An install that has been signing in with
package/bin/connect.mjs CHANGED
@@ -47,6 +47,7 @@ import { execFileSync } from "node:child_process";
47
47
  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
+ import { isWsl } from "../shared/wsl.mjs";
50
51
  import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
51
52
  import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
52
53
  import { envFrom } from "../shared/env-aliases.mjs";
@@ -205,7 +206,8 @@ function deploymentHas(env = process.env) {
205
206
  return {
206
207
  // EVERY SHAPE, RESOLVED ONCE. A row picks its own; nothing here knows a client's name.
207
208
  stdioRoutes: Object.fromEntries(Object.keys(STDIO_SHAPES).map((shape) =>
208
- [shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null })])),
209
+ [shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null, reportsDir: env.CLEAROTRON_REPORTS_DIR || null })])),
210
+ wsl: isWsl({ env }),
209
211
  // WHERE THE DOOR BINDS — not an address handed to any assistant. It is the loopback address the
210
212
  // unit listens on, and `enablePlan` needs it to write the unit. It used to be passed to the
211
213
  // resolver as `localAddress` and served to Cowork as somewhere to connect, which is the false
package/bin/onboard.mjs CHANGED
@@ -68,6 +68,8 @@ import { nodeFloorVerdict } from "../shared/node-floor.mjs"; // — the floor
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
71
73
  import { styleFor, banner } from "../shared/tty-style.mjs"; // — weight where the meaning is
72
74
  import { bracketAsciiCells, BRAND } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
73
75
  // THE REFUSALS ABOUT THE SIGN-IN ADDRESS ITSELF, shared with `bin/start.mjs`. Two copies would be a
@@ -99,7 +101,15 @@ import { entrypointOf } from "../driver/systemd/install-census.mjs"; //
99
101
  import { overlayReport, renderOverlayReport } from "../shared/doctrine-overlay.mjs"; // — the doctor reports the overlay
100
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
101
103
  import { engineInventory, engineMode, ENGINE_MODES } from "../driver/config-inventory.mjs"; //
102
- import { probeEngineTurn, probeFailureText, PROBE_MODEL, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.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)?`;
103
113
  import { runRequiredNames, missingRequirements, REGISTER_ENV, ENGINE_ENV } from "../driver/run-requirements.mjs"; // the order-time gate's own question, asked here rather than restated
104
114
  import { pinEnv, envFrom } from "../shared/env-aliases.mjs";
105
115
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
@@ -695,23 +705,8 @@ export function resolveEngineBin(bin, { env = process.env, wsl = null, onWindows
695
705
  /** A path on a Windows drive as WSL mounts it. */
696
706
  export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
697
707
 
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
- }
708
+ /** Whether this is a Linux running under Windows: the one answer, from shared/wsl.mjs. */
709
+ export { isWsl };
715
710
 
716
711
  /**
717
712
  * What to say about candidates passed over because they sit on a Windows drive — or `null` when none
@@ -1206,7 +1201,7 @@ export async function runCheck() {
1206
1201
  // deliberate — a second reader would drift from this one exactly as the composer and the checker did
1207
1202
  // in F41, and the drift is invisible because both sides keep passing their own arms.
1208
1203
  const unitDir = join(homedir(), ".config", "systemd", "user");
1209
- const { BACKGROUND_UNITS } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
1204
+ const { BACKGROUND_UNITS, startPaths } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
1210
1205
  const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
1211
1206
  const unitEnv = hosted
1212
1207
  ? unitEnvironment({
@@ -1440,7 +1435,7 @@ export async function runCheck() {
1440
1435
  } else if (!bin.executable || bin.relative) {
1441
1436
  info("not probed — there is no usable binary to probe. Fix the line above first.");
1442
1437
  } else {
1443
- say(`\n Probing ${engineId} with one ${PROBE_MODEL}-tier turn (this SPENDS; ${PROBE_TIMEOUT_SEC}s ceiling)…`);
1438
+ say(`\n ${probingLine(engineId)}`);
1444
1439
  // The engine as a RUN would see it: environment first, .env behind it. Only the engine-selection
1445
1440
  // keys — the probe must bill exactly the way this box bills and moves no other variable.
1446
1441
  const probeEnv = { ...process.env };
@@ -1628,6 +1623,13 @@ export async function runCheck() {
1628
1623
  // because a reader debugging `clearotron run` by hand IS this process.
1629
1624
  info(`this command's own process ${where} — a CLI is not started by the units' EnvironmentFile, so `
1630
1625
  + "that differs by design and is not what a run uses");
1626
+ } else if (!hosted && svcCustomers?.from === serviceEnvLabel) {
1627
+ // NAMED IN THE ENV FILE ALONE, ON AN INSTALL WITH NO UNITS. `clearotron start` loads that file and
1628
+ // its services inherit the store; this command does not load it, so its own process fell back to
1629
+ // the bundled roster and printed that as the answer, one line below a ✓ for the same variable.
1630
+ ok(`the services resolve profiles from ${svcCustomers.v} (${svcCustomers.from})`);
1631
+ info(`this command's own process ${where} — it does not load ${serviceEnvFile}, so that differs by `
1632
+ + `design and is not what \`${invoke("start")}\` hands the services`);
1631
1633
  } else {
1632
1634
  if (r.situation === "overlay" && !r.findings.length) ok(`${where} — the configured store`);
1633
1635
  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`);
@@ -1638,7 +1640,8 @@ export async function runCheck() {
1638
1640
  info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
1639
1641
  + "what the services resolve is not judged here, and the line above is this process's own answer");
1640
1642
  info("what a RUN used is its own `profile-store` journal line — this command reports what the "
1641
- + (hosted ? "units' file says, which the running services read at their own start" : "environment you are typing in says"));
1643
+ + (hosted ? "units' file says, which the running services read at their own start"
1644
+ : `environment you are typing in and your environment file say, which \`${invoke("start")}\` reads at its own start`));
1642
1645
  // ── AND WHO IS ACTUALLY IN IT ─────────────────────────────────────────
1643
1646
  //
1644
1647
  // The line above names the STORE. An operator who has just configured one wants to know their
@@ -1807,16 +1810,32 @@ export async function runCheck() {
1807
1810
  // be read, the lines above already said so and nothing is judged here. A store that is configured but
1808
1811
  // holds a file that cannot be read fails every company's saved searches, in the portal and the
1809
1812
  // connector alike, so that is read here too.
1813
+ //
1814
+ // WITH NO UNITS, THE SERVICES ARE `clearotron start`'s CHILDREN, and it hands every one of them a store
1815
+ // whether or not the env file names it. So "off" is only ever true of a hosted box. This read the env
1816
+ // file alone, and on a local install started before `start` wrote the store there, it told a portal
1817
+ // that was listing saved searches that they were off.
1810
1818
  if (!hosted || serviceKnown) {
1811
- const recipesSet = hosted ? effectiveForService("CLEAROTRON_RECIPES_DIR") : effective("CLEAROTRON_RECIPES_DIR");
1819
+ // Layered as effectiveForService layers it this command's environment over the services' file — so
1820
+ // the repository root is read from the same place the store directory is.
1821
+ let storeEnv = { ...(serviceFileEnv ?? {}), ...process.env };
1822
+ let recipesSet = effectiveForService("CLEAROTRON_RECIPES_DIR");
1823
+ let handedBy = "";
1824
+ if (!hosted && !recipesSet?.v) {
1825
+ const handed = startPaths({ env: storeEnv });
1826
+ recipesSet = { v: handed.recipes, from: "clearotron start", name: "CLEAROTRON_RECIPES_DIR" };
1827
+ storeEnv = { ...storeEnv, RECIPE_REPO_ROOT: handed.configStore };
1828
+ handedBy = ` — where \`${invoke("start")}\` puts them`;
1829
+ }
1812
1830
  const recipesDir = recipesSet?.v || null;
1813
1831
  if (!recipesDir) {
1814
1832
  info("saved searches are off: CLEAROTRON_RECIPES_DIR is not set, so Custom searches in the portal and the "
1815
1833
  + "connector offer none. Name a directory inside a git repository to switch them on");
1834
+ } else if (handedBy && !existsSync(join(storeEnv.RECIPE_REPO_ROOT, ".git"))) {
1835
+ // NOT CREATED YET IS NOT UNREADABLE. The store does not exist until the first start makes it, and
1836
+ // reading it now would report a missing directory as a broken one.
1837
+ info(`saved searches switch on at the first \`${invoke("start")}\`, which creates their store in ${recipesDir}`);
1816
1838
  } else {
1817
- // Layered as effectiveForService layers it — this command's environment over the services' file — so
1818
- // the repository root is read from the same place the store directory above was.
1819
- const storeEnv = { ...(serviceFileEnv ?? {}), ...process.env };
1820
1839
  const resolved = resolveStoreRepoRoot({ names: ["RECIPE_REPO_ROOT", "PROFILE_REPO_ROOT"], fallback: REPO, env: storeEnv });
1821
1840
  const reach = storeInRepo(recipesDir, resolved.root);
1822
1841
  if (!reach.ok) {
@@ -1828,8 +1847,8 @@ export async function runCheck() {
1828
1847
  try { loadRecipes({ dir: recipesDir, force: true }); } catch (e) { unreadable = String(e?.message ?? e).split("\n")[0]; }
1829
1848
  if (unreadable) {
1830
1849
  warn(`saved searches cannot be read from ${recipesDir}: ${unreadable}. Every company's saved searches fail `
1831
- + "to load, in the portal and the connector, until that file is fixed");
1832
- } else ok(`saved searches are read from ${recipesDir}, and saves are committed in ${reach.repo}`);
1850
+ + "to load, in the portal and the connector, until it is fixed");
1851
+ } else ok(`saved searches are read from ${recipesDir}${handedBy}, and saves are committed in ${reach.repo}`);
1833
1852
  }
1834
1853
  }
1835
1854
  }
@@ -2901,6 +2920,39 @@ if (!input.isTTY) {
2901
2920
  process.exit(2);
2902
2921
  }
2903
2922
 
2923
+ // ── OUT OF NPX'S CACHE, BEFORE ANYTHING IS WRITTEN ─────────────────────────────────────────────────────
2924
+ //
2925
+ // Run from npx, this program lives in npm's cache, and everything below would be wired to a directory npm
2926
+ // deletes: the launcher, and the connect line an assistant is registered with. So the install first puts
2927
+ // this same version somewhere permanent (shared/permanent-install.mjs) and runs itself from there. Nothing
2928
+ // has been written yet, so a failure here costs nothing, and it stops rather than carrying on: an install
2929
+ // finished from the cache is the defect, not a fallback.
2930
+ const move = relocationPlan();
2931
+ if (move && !move.skip && process.env.CLEAROTRON_RELOCATED !== "1") {
2932
+ say(`\n This is running from npm's temporary npx cache. Installing clearotron ${move.version} to ${move.prefix}`);
2933
+ say(" first, so the launcher and your assistants keep working after npm cleans that cache or you update.\n");
2934
+ // The npm that launched this, when npm says which: no second npm is guessed at.
2935
+ const npmCli = process.env.npm_execpath;
2936
+ const r = npmCli && existsSync(npmCli)
2937
+ ? spawnSync(process.execPath, [npmCli, ...move.npmArgs], { stdio: "inherit" })
2938
+ : spawnSync("npm", move.npmArgs, { stdio: "inherit" });
2939
+ if (r.status !== 0 || !existsSync(move.entry)) {
2940
+ console.error(`\n Could not install clearotron to ${move.prefix}${r.error ? ` (${r.error.message})` : ""}. Nothing was installed, and nothing of yours was changed.`);
2941
+ console.error(` Run \`npm install --global --prefix ${move.prefix} clearotron@${move.version}\`, then \`${join(move.prefix, "bin", "clearotron")} install\`.\n`);
2942
+ process.exit(1);
2943
+ }
2944
+ // THE REST OF THE INSTALL RUNS FROM THE PERMANENT COPY, with npm's marks of an npx arrival taken off, so
2945
+ // it prints the commands of the install it now is.
2946
+ const env = { ...process.env, CLEAROTRON_RELOCATED: "1" };
2947
+ for (const k of ["npm_command", "npm_lifecycle_event", "npm_execpath"]) delete env[k];
2948
+ const moved = spawnSync(process.execPath, [move.entry, "install", ...process.argv.slice(2)], { stdio: "inherit", env });
2949
+ process.exit(moved.status ?? 1);
2950
+ }
2951
+ if (move?.skip) {
2952
+ console.error(`\n Note: this is running from npm's temporary npx cache and cannot be moved out of it here (${move.skip}).`);
2953
+ console.error(" It will stop working when npm cleans that cache. `npm install -g clearotron` installs it permanently.\n");
2954
+ }
2955
+
2904
2956
  // A credential typed at a prompt is echoed by the terminal and then sits in scrollback, in tmux history,
2905
2957
  // in whatever the reader pastes into a bug report. So the echo is muted while a secret is being typed:
2906
2958
  // the output stream readline writes through drops everything while `muted` is set.
@@ -3281,7 +3333,7 @@ try {
3281
3333
  // skipping it costs an hour later.
3282
3334
  info("A file that exists is not an engine that works — so setup tries one before writing anything.");
3283
3335
  info("It takes a few seconds here. Skipped, a broken engine surfaces an hour into a real search.");
3284
- 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)) {
3336
+ if (!await confirm(proveQuestion({ engineId: pick.id, lane: authPick.id }), true)) {
3285
3337
  info("Not proven, so not written. Pick again — the last row configures no engine at all.");
3286
3338
  continue;
3287
3339
  }
package/bin/start.mjs CHANGED
@@ -300,6 +300,22 @@ export function resolvePorts(env = {}) {
300
300
  client: one("CLIENT_MCP_HTTP_PORT", clientDoorPort({})) };
301
301
  }
302
302
 
303
+ /**
304
+ * WHAT TO DO WHEN THE PAGE THAT OPENS IS NOT OURS. Printed under every "Open" line.
305
+ *
306
+ * A port can be free where this runs and taken where the browser runs: on WSL, a Windows-side listener
307
+ * (VS Code's Remote-SSH forwarding is the one measured, 2026-09-11) answers 127.0.0.1 before WSL does. The
308
+ * doors bind cleanly, the in-use detection has nothing to see, and the browser shows somebody else's page
309
+ * with nothing on this screen saying so. `--port` already moves all three doors; the reader has to be told
310
+ * about it at the moment the address is handed over, which is here.
311
+ */
312
+ export function foreignPageHint(verb) {
313
+ return [
314
+ "If the page that opens is not this install's sign-in, another program on this machine holds that",
315
+ `port from outside this environment. Run \`${invoke(verb)} --port 28802\` (or any free number) instead.`,
316
+ ];
317
+ }
318
+
303
319
  /**
304
320
  * Apply `--port <n>` to the three doors.
305
321
  *
@@ -355,6 +371,50 @@ export function defaultGrantsPath({ env = process.env, demo = false } = {}) {
355
371
  return installPaths(join(homedir(), demo ? "trademark-demo" : "trademark")).grants;
356
372
  }
357
373
 
374
+ /**
375
+ * The paths `clearotron start` hands its services: the install's layout under `base`, with whatever the
376
+ * environment already names winning over the layout's default, as `start` has always applied it.
377
+ *
378
+ * EXPORTED FOR THE ONE OTHER READER THAT MUST AGREE WITH IT. On an install with no background units,
379
+ * `clearotron doctor` has no unit file to read the services' environment from: the services are this
380
+ * command's children and get the saved-search store from here. An install started before the store was
381
+ * written to the env file has no line there to read, so doctor asks this function for it, and the
382
+ * answer it prints and the one the services were given have one author.
383
+ *
384
+ * NOT IN A DEMO. Nothing the environment says about an install is the demo's: with the reader's settings
385
+ * in force, 0.3.0-beta.1's demo seeded its example reports into their real archive.
386
+ */
387
+ export function startPaths({ env = process.env, base = join(homedir(), "trademark"), demo = false } = {}) {
388
+ const paths = installPaths(base);
389
+ if (demo) return paths;
390
+ for (const [k, name] of [["pool", "CLEAROTRON_REPORTS_DIR"], ["workspace", "CLEAROTRON_WORK_DIR"], ["queue", "CLEAROTRON_QUEUE_DIR"],
391
+ ["outbox", "CLEAROTRON_OUTBOX_DIR"], ["locks", "CLEAROTRON_RUN_LOCK_DIR"], ["grants", "CLEAROTRON_ACCESS_FILE"],
392
+ ["recipes", "CLEAROTRON_RECIPES_DIR"]]) if (env[name]) paths[k] = env[name];
393
+ if (env.RECIPE_REPO_ROOT) paths.configStore = env.RECIPE_REPO_ROOT;
394
+ if (env.PORTAL_AUDIT) paths.audit = env.PORTAL_AUDIT;
395
+ return paths;
396
+ }
397
+
398
+ /**
399
+ * The saved-search store, as the env file must record it for every reader OUTSIDE this command's tree.
400
+ *
401
+ * The services are handed the store by `childEnv`. A connector an assistant launches, `clearotron doctor`
402
+ * and every other hand-run command are not this command's children: they read the env file, and the
403
+ * file never named the store. So on a local install the portal listed a company's saved searches, the
404
+ * connected assistant was told there were none, and doctor said saved searches were off. Written
405
+ * add-only beside the secrets, as `npx clearotron install` writes the reports and work directories: a
406
+ * line an operator wrote wins.
407
+ */
408
+ export function storesForOtherReaders(paths) {
409
+ return { CLEAROTRON_RECIPES_DIR: paths.recipes, RECIPE_REPO_ROOT: paths.configStore };
410
+ }
411
+
412
+ // Written above each line, so a reader who moves the install sees that these two move with it.
413
+ const STORE_NOTES = {
414
+ CLEAROTRON_RECIPES_DIR: "Where saved searches are kept. `clearotron start` created it; edit both lines if you move the install.",
415
+ RECIPE_REPO_ROOT: "The git repository saved searches are committed in, which holds the directory above.",
416
+ };
417
+
358
418
  /** Everything this install keeps on disk, under one base directory. */
359
419
  export function installPaths(base) {
360
420
  return {
@@ -846,20 +906,11 @@ if (isMain) {
846
906
  const DEMO = argv.includes("--demo");
847
907
  // The same base `npm run setup` writes under, so whichever of the two a reader ran first, the other
848
908
  // finds the same install rather than a second one beside it.
849
- const paths = installPaths(flag("--base", join(homedir(), DEMO ? "trademark-demo" : "trademark")));
850
909
  // Whatever the environment already says wins over the base-derived default, for every path — a reader
851
- // who ran `npm run setup` has these in .env already and this must not move their data.
852
- //
853
- // NOT IN A DEMO. Nothing the environment says about an install is the demo's: with the reader's
854
- // settings in force, 0.3.0-beta.1's demo seeded its example reports into their real archive. Not read,
855
- // rather than deleted from the environment, so a real start cannot be reached by this branch at all.
856
- if (!DEMO) {
857
- for (const [k, name] of [["pool", "CLEAROTRON_REPORTS_DIR"], ["workspace", "CLEAROTRON_WORK_DIR"], ["queue", "CLEAROTRON_QUEUE_DIR"],
858
- ["outbox", "CLEAROTRON_OUTBOX_DIR"], ["locks", "CLEAROTRON_RUN_LOCK_DIR"], ["grants", "CLEAROTRON_ACCESS_FILE"],
859
- ["recipes", "CLEAROTRON_RECIPES_DIR"]]) if (process.env[name]) paths[k] = process.env[name];
860
- if (process.env.RECIPE_REPO_ROOT) paths.configStore = process.env.RECIPE_REPO_ROOT;
861
- if (process.env.PORTAL_AUDIT) paths.audit = process.env.PORTAL_AUDIT;
862
- }
910
+ // who ran `npm run setup` has these in .env already and this must not move their data. Not in a demo,
911
+ // which `startPaths` says why. One author, because `doctor` asks the same function what the services
912
+ // were handed.
913
+ const paths = startPaths({ env: process.env, base: flag("--base", join(homedir(), DEMO ? "trademark-demo" : "trademark")), demo: DEMO });
863
914
  // ── THIS INSTALL'S FIRST START, read before this start writes either file that answers it ────────────
864
915
  //
865
916
  // The grants file and the config store's repository are both written further down, on every start
@@ -1117,14 +1168,19 @@ if (isMain) {
1117
1168
  const portalSecret = secretFor("PORTAL_SECRET");
1118
1169
  const tokenSecret = secretFor("TRADEMARK_MCP_TOKEN_SECRET");
1119
1170
  if (!process.env.PORTAL_LOCAL_USER) generated.PORTAL_LOCAL_USER = user;
1171
+ const stores = DEMO ? {} : storesForOtherReaders(paths);
1120
1172
 
1121
1173
  // A DEMO WRITES NO SECRETS AND NO ADDRESS. They are generated per run and live in memory only, which
1122
1174
  // is the same posture the ops key already has here — and it is what makes "removing the demo is one
1123
1175
  // directory" true rather than nearly true.
1124
- if (Object.keys(generated).length && !DEMO) {
1176
+ //
1177
+ // THE STORES ARE WRITTEN EVEN WHEN NO SECRET IS. An install whose secrets are already in the file is
1178
+ // exactly the install whose connector and doctor could not see its saved searches, and a gate on the
1179
+ // secrets alone would never reach it.
1180
+ if ((Object.keys(generated).length || Object.keys(stores).length) && !DEMO) {
1125
1181
  let existing = "";
1126
1182
  try { existing = readFileSync(ENV_PATH, "utf8"); } catch (e) { if (e.code !== "ENOENT") fatal(`${ENV_PATH} exists but could not be read (${e.code}).`); }
1127
- const merged = mergeEnvFile(existing, generated);
1183
+ const merged = mergeEnvFile(existing, { ...generated, ...stores }, { notes: STORE_NOTES });
1128
1184
  if (merged.added.length) {
1129
1185
  // ONE WRITER FOR EVERY FILE THAT HOLDS CREDENTIALS, and it creates the directory. This site had its
1130
1186
  // own copy of the write and did not learn what the wizard's copy learned when `.env` moved under
@@ -1135,7 +1191,7 @@ if (isMain) {
1135
1191
  } catch (e) {
1136
1192
  fatal(`could not write ${ENV_PATH} (${String(e?.message ?? e)}).`);
1137
1193
  }
1138
- // NAMES only. This file is where the credentials are.
1194
+ // NAMES only. Some of what this file holds is credentials.
1139
1195
  say(` wrote ${merged.added.join(", ")} to ${ENV_PATH} (mode 600)`);
1140
1196
  }
1141
1197
  }
@@ -1330,8 +1386,12 @@ if (isMain) {
1330
1386
  // SEEDED FROM A COPY, for the reason the player publishes from one: republishing writes a receipt
1331
1387
  // into the run directory it reads, and `demo/` is tracked. This is the path a reader actually takes
1332
1388
  // — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
1333
- const { publishSource } = await import("../driver/demo-container.mjs");
1389
+ const { publishSource, seedDemoRuns } = await import("../driver/demo-container.mjs");
1334
1390
  const seed = await seedPool({ pool: paths.pool, examplesDir: publishSource(join(REPO, "demo"), { repoRoot: REPO }), republish: republishRun });
1391
+ // AND AS RUNS, so the assistant this demo's connect line wires has them to list, brief and open. Under
1392
+ // the demo's own workspace only: nothing of it reaches an install started afterwards.
1393
+ const runs = seedDemoRuns({ workspace: paths.workspace, examplesDir: join(REPO, "demo") });
1394
+ if (runs.seeded.length) say(` runs ${runs.seeded.length} sample run(s) your assistant can list, brief and open`);
1335
1395
  // WHAT WAS ALREADY THERE IS SAID TOO. This branch used to run only when the pool
1336
1396
  // was empty; it now tops a stale pool up to the package's set, so "seeded 1" on an upgrade is a fact
1337
1397
  // about what was MISSING and says nothing on its own about how many are now listed.
@@ -1839,6 +1899,7 @@ if (isMain) {
1839
1899
  }
1840
1900
  say("");
1841
1901
  say(` Open: ${envs.url}`);
1902
+ for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
1842
1903
  say(" This SURVIVES the terminal — close the window, the product keeps running.");
1843
1904
  say(` Stop it: ${invoke("stop")} (stops and removes the units; issued connect keys survive — \`${invoke("disconnect")}\` revokes those)`);
1844
1905
  say(` Is it up? ${invoke("status")}`);
@@ -2072,6 +2133,7 @@ if (isMain) {
2072
2133
 
2073
2134
  say("");
2074
2135
  say(` Open ${envs.url}`);
2136
+ for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
2075
2137
  say("");
2076
2138
  // ── TWO DOORS, TWO AUDIENCES, BOTH NAMED ( — F26) ─────────────────────────
2077
2139
  //
@@ -2169,7 +2231,9 @@ if (isMain) {
2169
2231
  //
2170
2232
  // The string comes from the ONE composer, not from a literal here: three surfaces state this route
2171
2233
  // and a line of instruction with more than one author drifts silently.
2172
- const connect = stdioConnectOffer({ workDir: process.env.CLEAROTRON_WORK_DIR || null });
2234
+ // THE WORKSPACE AND POOL THE SERVICES WERE HANDED, not this process's environment: a demo reads no env
2235
+ // file, so its own line named no workspace and the connector fell back to the real install's.
2236
+ const connect = stdioConnectOffer({ workDir: paths.workspace, reportsDir: paths.pool });
2173
2237
  say(" Connect your assistant to this install — one line, no address and no sign-in:");
2174
2238
  say("");
2175
2239
  say(` ${connect.command}`);
package/bin/update.mjs CHANGED
@@ -59,6 +59,8 @@ import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-p
59
59
  import { readEnvFile } from "./onboard.mjs";
60
60
  import { invoke, invocationPrefix } from "../shared/invocation.mjs"; // — name a command the reader can actually type
61
61
  import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // a pull cannot update an untracked bundle
62
+ import { packagedUpdate } from "../shared/permanent-install.mjs"; // — a packaged install updates at its own prefix
63
+ import { installShim, inspectShim, shimPath } from "../shared/verb-shim.mjs"; // — npm's link replaces the launcher
62
64
 
63
65
  const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
64
66
  const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
@@ -246,13 +248,22 @@ export async function update(argv = process.argv.slice(2)) {
246
248
  // not succeed` — true, useless, and pointing at the wrong thing entirely. `update` is the verb a
247
249
  // stranger reaches for, so the one install we expect most people to have must not be answered with
248
250
  // a git error about a directory that was never a repository.
251
+ //
252
+ // A PACKAGED INSTALL NOW UPDATES ITSELF, the way it was installed: npm, at the prefix it lives under, on
253
+ // the channel it came from (shared/permanent-install.mjs). It runs AFTER the live-run refusal below,
254
+ // because npm replaces the program's files as surely as `npm ci` does. Only a layout that cannot be
255
+ // named is still refused.
256
+ let packaged = null;
249
257
  if (!isGitCheckout()) {
250
- console.error("\n This install is not a git checkout, so there is nothing to pull.");
251
- console.error(" It was installed from a package rather than cloned, which is the ordinary way.");
252
- console.error("\n Update it the way it was installed:\n");
253
- console.error(" npm install -g clearotron@latest\n");
254
- console.error(" Nothing was touched.");
255
- return 4;
258
+ packaged = packagedUpdate();
259
+ if (!packaged) {
260
+ console.error("\n This install is not a git checkout, so there is nothing to pull.");
261
+ console.error(" It was installed from a package rather than cloned, which is the ordinary way.");
262
+ console.error("\n Update it the way it was installed:\n");
263
+ console.error(" npm install -g clearotron@latest\n");
264
+ console.error(" Nothing was touched.");
265
+ return 4;
266
+ }
256
267
  }
257
268
 
258
269
  // ── AND THE SECOND REFUSAL: NOT OVER A LIVE RUN ──────────────────────────────────────────────
@@ -289,6 +300,27 @@ export async function update(argv = process.argv.slice(2)) {
289
300
  return 4;
290
301
  }
291
302
 
303
+ if (packaged) {
304
+ if (packaged.current) {
305
+ say(`\n This install is ${packaged.installed}, and nothing newer is published (${packaged.tag}: ${packaged.version}). Nothing was touched.\n`);
306
+ return 0;
307
+ }
308
+ if (packaged.unread) say(`\n npm did not say which versions are published, so this follows the ${packaged.tag} channel.`);
309
+ say(`\n Updating this install at ${packaged.prefix} from ${packaged.installed ?? "an unreadable version"} to clearotron@${packaged.spec}.`);
310
+ const rc = runInCheckout("npm", packaged.npmArgs);
311
+ if (rc !== 0) return rc;
312
+ // npm puts its own link back at `<prefix>/bin/clearotron` on every install, over the launcher the
313
+ // install wrote, and that link runs whichever `node` is first on PATH. Put the launcher back, but only
314
+ // over npm's link or our own: anything else there was not ours before this update either.
315
+ const kind = inspectShim(shimPath()).kind;
316
+ if (kind === "npm-link" || kind === "ours" || kind === "ours-other-install") {
317
+ const shim = installShim();
318
+ if (!shim.ok) console.error(`\n The update worked, but the launcher at ${shim.path ?? "~/.local/bin/clearotron"} could not be written back: ${shim.detail}.`);
319
+ }
320
+ say("\n Updated. An assistant starts the new version the next time it launches Clearotron; restart the services for the portal.\n");
321
+ return 0;
322
+ }
323
+
292
324
  say("\n Configuration store is outside the checkout. Updating the product.");
293
325
  for (const e of entries) say(` ${e.name}=${e.value} (${e.from})`);
294
326
  if (!entries.length) {
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "0fb023f5822c9458f9c63bf993876c3a3a8053e3",
3
- "version": "0.3.0-beta.4"
2
+ "commit": "d96db92fd1989500c9b107df8e7f7a8b7f715500",
3
+ "version": "0.3.0-beta.6"
4
4
  }
@@ -291,6 +291,7 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
291
291
  | `PORTAL_URL` | `http://127.0.0.1:18802`, or built from `PORTAL_SERVICE_HOST`/`PORTAL_SERVICE_PORT` | Where the deploy tick's live-surface check expects to reach the portal. |
292
292
  | `PORTAL_OPS_TOKEN_FILE` | `~/.config/systemd/user/trademark-portal.service.d/secrets.conf` | The systemd drop-in the live-surface check reads `PORTAL_OPS_TOKEN` out of. It reads the FILE rather than the environment so a check run by hand sees the same token the service does. |
293
293
  | `CLEAROTRON_INVOKED_AS` | unset (⇒ the verb's own `argv[1]`) | How the reader typed the command, so every command a verb prints for them to type next is spelled the way they type it: `clearotron …` after a global install, `npx clearotron …` otherwise. The dispatcher runs each verb as a process of its own, whose `argv[1]` is always `bin/<verb>.mjs`, so `bin/clearotron.mjs` passes its own `argv[1]` down in this name. **Set by the dispatcher, never by an operator.** Effect class `deployment`; the full contract is its row in `.env.example`. |
294
+ | `CLEAROTRON_RELOCATED` | unset (⇒ an install running from npx's cache moves itself to `~/.local` first) | Marks an install that has already made that move. `npx clearotron install` installs the same version under `~/.local`, then runs that copy's install with this set to `1`, so the copy does not try to move itself again. **Set by the install on the one process it starts, never by an operator.** Effect class `deployment`; the full contract is its row in `.env.example`. |
294
295
  | `CLEAROTRON_REQUIRE_EXPLICIT_PORTS` | unset | `1` makes a service that would listen on a built-in default port refuse to start instead of warning. For a box that runs more than one instance, where one instance's default is another's port on the day that other instance is down. Unset, a service on a default port still says so as it starts. Effect class `deployment`. |
295
296
  | `CLEAROTRON_UPDATER_STAMP` | `_updater-identity.json` beside the update script, in the directory the updater runs from | The full path of the file in which the updater that deploys this box records which copy of itself ran. The updater writes it and deploy health reads it under this one name, so a box that moves the stamp sets it once for both. On a box with no updater unit, setting it says an updater exists elsewhere and is to be judged. Effect class `deployment`. |
296
297
  | `CLEAROTRON_CUT_REF` | `HEAD` | Which ref the cut decision reads the version from. **Read only by the release workflow, never set on a deployment.** The jobs that ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref and `HEAD` there is that ref rather than the branch they are deciding about. A job that asks the wrong subject gets a confident wrong answer. |
@@ -1,5 +1,31 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.0-beta.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 2ca849d: Fixed: When codex's sign-in can no longer be refreshed, the search stops and says to run `codex login`. It used to retry with a bare exit code.
8
+ - a81279f: Fixed: When the portal address opens someone else's page, for example a port forwarded from outside WSL, Clearotron now says so and points you to `--port`.
9
+ - a81279f: Fixed: The sign-in page clears a session left by another Clearotron on the same address and says so. A refusal that is not about the passphrase now says what it is about.
10
+ - f133f7d: Fixed: A passphrase pasted with a space or line break at either end now signs in, instead of being refused as wrong.
11
+ - 2ca849d: Fixed: A manager who does not run the installation no longer sees server file paths when a new company cannot be recorded or filed.
12
+ - a81279f: Fixed: The local sign-in page no longer invites the browser to fill in a saved password from another install.
13
+ - a81279f: Fixed: `npx clearotron install` now installs Clearotron permanently under `~/.local` before setting up. The `clearotron` command and your assistants' connections keep working after npm cleans its cache.
14
+ - 2ca849d: Fixed: When the saved-search store exists but cannot be read, the connector and `clearotron doctor` now say so, instead of reporting no saved searches.
15
+ - a81279f: Fixed: On WSL, the "on this computer" connect steps now say to run them inside WSL. The Claude Code line registers Clearotron for every project and works in Windows PowerShell.
16
+ - a81279f: New: An assistant connected to `clearotron demo` can now list, brief and open the demo's four sample runs. They stay inside the demo's own folder.
17
+ - a81279f: Fixed: The sign-in page's reset line now runs for a demo started with npx, and resets that demo's own passphrase.
18
+ - a81279f: New: `clearotron update` now updates an npm-installed copy itself, and a beta install moves on to the release once it is published.
19
+
20
+ ## 0.3.0-beta.5
21
+
22
+ ### Patch Changes
23
+
24
+ - 5f7e295: Fixed: A Clearotron install signed in to OpenAI's `codex` with a subscription keeps working after codex refreshes its login. Before, every search after the first refresh failed within seconds until you signed in again.
25
+ - 2878809: Fixed: When the company store cannot record a new company, the New company page now says why and what fixes it, instead of "Try again shortly". Someone who does not run the installation is told to ask whoever does.
26
+ - e7cd9e1: Fixed: On an install run with `clearotron start`, a connected assistant now lists the same saved searches as the portal, including Generic's. `clearotron doctor` no longer says working saved searches are off, and it names one place profiles come from.
27
+ - e7cd9e1: Fixed: When setup offers to try your engine, it no longer names an Anthropic model to someone who chose OpenAI's `codex`.
28
+
3
29
  ## 0.3.0-beta.4
4
30
 
5
31
  ### Minor Changes
@@ -23,9 +49,9 @@
23
49
  - a782aad: Fixed: Creating a company is refused, with nothing left behind, when the configuration store cannot record it. The company used to be created anyway, with no record of who made it or when, and its organisation was given access to it.
24
50
 
25
51
  A store with no git identity is the usual cause on a new machine, and the refusal names the command that fixes it. Setup and `clearotron start` now check a store they adopt for this straight away.
26
- - d1ef225: Fixed: A stage stopped at its time limit now records the output it actually produced. It used to record a small fraction, so a stage that was working read as one that had stalled.
52
+ - d1ef225: Fixed: A search step stopped at its time limit now records the output it actually produced. It used to record a small fraction, so a step that was working read as one that had stalled.
27
53
 
28
- The token totals `clearotron tokens` reports for runs with a stopped stage now include that output.
54
+ The token totals `clearotron tokens` reports for runs with a stopped step now include that output.
29
55
  - 0ff42d1: Fixed: `clearotron doctor` now says when saved searches are switched off and why, and when a saved search file cannot be read.
30
56
 
31
57
  An assistant asking for saved searches is told when they could not be read, instead of being told there are none.
@@ -79,11 +105,11 @@
79
105
 
80
106
  Fixed: A search now records any of your default territories that the engine cannot search, in the record of that search. A mistyped default no longer narrows a search silently.
81
107
  - eacfce4: Fixed: An assistant connected to a local install now sees the saved searches the portal shows, and can plan a run from one of them. Before, outside the demo, only the portal was told where saved searches are kept, so an assistant was told the install had none.
82
- - 34cc1c7: For operators: A health check that could not look now fails instead of reporting success. Two halves of the unit check can go quiet. One goes quiet when the installation does not say which installation it is; the other when the walk over the unit files does not finish. Both used to note that they had not run and then pass.
108
+ - 34cc1c7: For operators: A deployment health check that could not finish now fails instead of reporting success. Two parts of the service check could skip themselves. One skipped when the installation did not say which installation it is; the other when the scan of the service files did not finish. Both used to note that they had not run and then pass.
83
109
 
84
110
  For operators: Set `CLEAROTRON_BOX` to `prod` or `test`. Those are the only two values the check accepts; anything else, including any other name, reads as unnamed and fails. The failure names the setting and says what went unchecked.
85
111
 
86
- For operators: The half that goes quiet is the one that notices a service that has stopped and stayed stopped. The other half lists what is running, so it cannot see something that is no longer there. While that half is suppressed, a service can disappear without the check saying anything.
112
+ For operators: The part that could skip itself is the one that notices a service that has stopped and stayed stopped. The other part lists what is running, so it cannot see something that is no longer there. While the first part is skipped, a service can disappear without the check saying anything.
87
113
  - b45a5cf: Fixed: `clearotron doctor` now reports everything a search would be refused for, checked against the environment the search will run in. So an installation it passes is one that can run a search.
88
114
 
89
115
  Fixed: A token set in the installation's own configuration file now reaches the engine check. A headless server set up the documented way proves its engine instead of reporting it signed out.
@@ -128,7 +154,7 @@
128
154
  Fixed: When a feature is switched off on your installation, the screen says so and what to change. It used to suggest trying again shortly.
129
155
 
130
156
  Fixed: The link to the risk-framework guide opens in a new tab and goes straight to the section on writing your own. It also appears on the Company profile screen.
131
- - ad088d6: Fixed: The audit workbook's "What was searched" sheet is now in plain words. Its Result and Note columns carry the search log's own notes. Engine vocabulary could reach them: a receipt "deferred", a full web address, a bare HTTP code. Those words are now replaced as the workbook is built, a republished report included. A search recorded as not run still reads as not searched. Search terms and names stay exactly as written, because they record what was searched.
157
+ - ad088d6: Fixed: The audit workbook's "What was searched" sheet is now in plain words. Its Result and Note columns carry the search log's own notes. Internal terms could reach them: a status such as "deferred", a full web address, a bare HTTP code. Those words are now replaced as the workbook is built, a republished report included. A search recorded as not run still reads as not searched. Search terms and names stay exactly as written, because they record what was searched.
132
158
  - ff16a3b: For operators: The deployment health check now reports whether the component that updates an installation is itself up to date. It was the one part of a deployment the check could not identify. An installation kept current by an out-of-date updater could report healthy while serving stale code.
133
159
 
134
160
  For operators: An updater that cannot be identified is now reported as a failure rather than passed over. A copy old enough to predate this reporting writes nothing at all. That silence is the case worth knowing about, so it is treated as a finding.
@@ -158,7 +184,7 @@
158
184
 
159
185
  The product now says company throughout. It used to say brand owner, account, client and customer for the same thing. The firm running the installation is named separately, in the top bar.
160
186
 
161
- Companies created through the settings page were saved without a risk framework. Their matters were then rated under the house default, with nothing on screen saying so. Every company created now carries one, and says which.
187
+ Companies created through the settings page were saved without a risk framework. Their matters were then rated under the default risk framework, with nothing on screen saying so. Every company created now carries one, and says which.
162
188
  - f75266d: New: The setup wizard now asks for the organisation's name after the sign-in address, and the person who installs starts with access to everything.
163
189
 
164
190
  `clearotron grant add` sets a person's two permissions with `--run` and `--manage`; with neither, the person can look and start nothing.
@@ -180,18 +206,18 @@
180
206
 
181
207
  Getting the deck's shape wrong used to fail quietly. The profile screen showed the framework's title and your band colours, and silently omitted the box saying what the bands mean. The new command answers that question directly, using the screen's own read of the deck.
182
208
 
183
- Fixed: a framework served from the product's own files, while you have a configuration store set, now says so.
209
+ Fixed: When you have a configuration store set and a risk framework comes from the product's own files instead, the product now says so.
184
210
 
185
211
  Your store is looked in first, and the product's files answer when it is silent. The product ships decks under names you may also have chosen. So a deck that went missing from your store was replaced by ours rather than reported absent. Same band words, different rubric, nothing raised anywhere. The profile screen now writes one line naming what happened, and the new command reports it.
186
212
 
187
- Fixed: the house triage ladder's profile page explains its bands again.
213
+ Fixed: The built-in triage framework's profile page explains its bands again.
188
214
 
189
215
  Its band sections stated their meanings as plain paragraphs, which the screen does not read. Every company without a framework of its own saw band colours and no explanation. The wording is unchanged.
190
- - cfb9a9f: Fixed: `clearotron doctor` now says which companies your portal's trigger key can start.
216
+ - cfb9a9f: Fixed: `clearotron doctor` now lists the companies your portal's key may start runs for.
191
217
 
192
218
  The key carries a list of the companies it may start runs for. A company added after the key was minted is outside it. Doctor reported the key's expiry and never its coverage. So the one command whose job is to tell you what a machine is configured for said nothing about it.
193
219
 
194
- It reads the roster the services read, not the one a command-line process resolves, and it says which. Those two can disagree, and when they do the difference is the whole answer.
220
+ It reads the company list the background services read, not the one a command run in a terminal would find, and it says which. The two can differ, and when they do, that difference is the answer.
195
221
 
196
222
  The line is a note, not a failure. Your portal takes a fresh credential at the start of every call, so a company outside the key is not normally refused. It is refused when the portal cannot take a fresh one, and the line says so and gives you the command to widen the key.
197
223
  - ba2899b: Fixed: On reports searched through Signa, each register finding now links to the office's own page for that record, where the office publishes one. Singapore publishes no such page, so its registrations are cited by number, and the report says why. A number that an office's page cannot take is cited the same way.