clearotron 0.3.0-beta.1 → 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 (63) hide show
  1. package/.env.example +13 -2
  2. package/CONTRIBUTING.md +1 -1
  3. package/INSTALL.md +12 -10
  4. package/README.md +3 -2
  5. package/bin/brandowner.mjs +94 -1
  6. package/bin/onboard.mjs +156 -55
  7. package/bin/passphrase.mjs +23 -4
  8. package/bin/start.mjs +177 -38
  9. package/build-info.json +2 -2
  10. package/docs/architecture/04-configuration-reference.md +5 -2
  11. package/docs/architecture/05-config-governance.md +3 -2
  12. package/driver/CHANGELOG.md +95 -0
  13. package/driver/demo-posture.mjs +59 -8
  14. package/driver/driver.config.mjs +36 -7
  15. package/driver/engine/child-record.mjs +93 -0
  16. package/driver/findings-model.mjs +6 -1
  17. package/driver/knockout-assess-record.mjs +6 -3
  18. package/driver/package.json +1 -1
  19. package/driver/portal-local-auth.mjs +98 -3
  20. package/driver/portal-service.mjs +65 -29
  21. package/driver/publish/knockout.mjs +12 -1
  22. package/driver/publish/office-record-links.mjs +61 -10
  23. package/driver/publish/render-knockout.mjs +31 -11
  24. package/driver/publish/xlsx.mjs +33 -1
  25. package/driver/register-records.mjs +6 -0
  26. package/driver/run-requirements.mjs +24 -1
  27. package/driver/runner.mjs +10 -5
  28. package/driver/skills/knockout-assess/SKILL.md +1 -1
  29. package/driver/suite-census.json +174 -30
  30. package/driver/unit-inventory.mjs +109 -5
  31. package/driver/updater-identity.mjs +178 -0
  32. package/driver/usage-ledger.mjs +5 -5
  33. package/mcp-server/CHANGELOG.md +6 -0
  34. package/mcp-server/http-server.mjs +16 -13
  35. package/mcp-server/lib/driver.mjs +7 -0
  36. package/mcp-server/lib/knockout.mjs +14 -2
  37. package/mcp-server/lib/ops.mjs +38 -16
  38. package/mcp-server/package.json +2 -2
  39. package/mcp-server/server.mjs +1 -1
  40. package/package.json +1 -1
  41. package/portal-ui/dist/assets/{index-CWTHP0sH.js → index-CcFjgM78.js} +32 -17
  42. package/portal-ui/dist/assets/{index-KpytsmNH.css → index-CsCuPshD.css} +7 -2
  43. package/portal-ui/dist/index.html +2 -2
  44. package/portal-ui/package.json +3 -3
  45. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  46. package/providers/oauth-mcp-bridge/package.json +2 -2
  47. package/scripts/changelog-plain-language.mjs +7 -30
  48. package/scripts/e2e.mjs +13 -2
  49. package/scripts/env-audit.mjs +10 -3
  50. package/scripts/env-classify.mjs +205 -12
  51. package/scripts/live-surface-check.mjs +74 -2
  52. package/scripts/plain-language-rules.mjs +103 -0
  53. package/scripts/release-note-required.mjs +118 -24
  54. package/scripts/release-notes-lint.mjs +22 -43
  55. package/scripts/release-version.mjs +59 -0
  56. package/scripts/revisit-render-check.mjs +6 -2
  57. package/scripts/text-difference.mjs +22 -0
  58. package/shared/env-local.mjs +25 -2
  59. package/shared/names-in-force.mjs +1 -0
  60. package/shared/reap-on-exit.mjs +27 -14
  61. package/shared/store-in-repo.mjs +147 -0
  62. package/shared/withheld-paths-access.mjs +6 -6
  63. package/shared/yes-no-echo.mjs +35 -0
@@ -27,7 +27,7 @@
27
27
  // facts an operator needs and none of them a secret.
28
28
 
29
29
  import { existsSync, rmSync } from "node:fs";
30
- import { credentialPathFor, establishCredential, readLocalCredential } from "../driver/portal-local-auth.mjs";
30
+ import { defaultInstallBase, establishCredential, installCredential, readLocalCredential } from "../driver/portal-local-auth.mjs";
31
31
  // — the form a reader can actually type, derived from how THIS process started
32
32
  // rather than hardcoded. A hardcoded `npx ` tells a global installer their install is somehow lesser;
33
33
  // a hardcoded bare name sends an npx reader to `command not found`.
@@ -39,6 +39,10 @@ const USAGE = ` ${P}clearotron passphrase — report or reset the portal's loca
39
39
  ${P}clearotron passphrase where the credential is, whether it exists, whose it is
40
40
  ${P}clearotron passphrase --reset mint a NEW passphrase and print it once
41
41
  ${P}clearotron passphrase --help this text. Changes nothing.
42
+ ${P}clearotron passphrase --base <dir> any of the above, for an install set up somewhere other than ~/trademark
43
+
44
+ The credential is the one this install's portal reads: the install's own file in its directory, or
45
+ the shared ~/.cordillera one that an install which has been signing in with it keeps.
42
46
 
43
47
  A reset invalidates the current passphrase immediately. Anyone signed in keeps their session until
44
48
  it expires; the old passphrase stops working the moment the new one is written.`;
@@ -46,9 +50,23 @@ const USAGE = ` ${P}clearotron passphrase — report or reset the portal's loca
46
50
  const args = process.argv.slice(2);
47
51
  if (args.includes("--help") || args.includes("-h")) { console.log(`\n${USAGE}\n`); process.exit(0); }
48
52
 
49
- const path = credentialPathFor();
53
+ // THE SAME DECISION THE PORTAL'S SUPERVISOR MAKES (installCredential), so the file this verb reports and
54
+ // resets is the file the portal reads: the install's own when it has one, or the shared default an older
55
+ // install keeps using. `--base` names the install, exactly as it does for `clearotron start`.
56
+ const baseAt = args.indexOf("--base");
57
+ const base = baseAt >= 0 ? args[baseAt + 1] : defaultInstallBase();
58
+ if (baseAt >= 0 && (!base || base.startsWith("-"))) {
59
+ console.error(`clearotron passphrase: --base needs a directory\n\n${USAGE}\n`);
60
+ process.exit(2);
61
+ }
62
+ const { path, source } = installCredential({ base, env: process.env });
50
63
  const reset = args.includes("--reset");
51
- const unknown = args.filter((a) => !["--reset", "--help", "-h"].includes(a));
64
+ const unknown = args.filter((a, i) => !["--reset", "--help", "-h", "--base"].includes(a) && !(baseAt >= 0 && i === baseAt + 1));
65
+ const WHICH = {
66
+ configured: "the file PORTAL_LOCAL_CREDENTIAL names",
67
+ install: "this install's own",
68
+ shared: "the shared default: an install with no credential of its own signs in with it",
69
+ };
52
70
  if (unknown.length) {
53
71
  console.error(`clearotron passphrase: unrecognised argument${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}\n\n${USAGE}\n`);
54
72
  process.exit(2);
@@ -71,11 +89,12 @@ catch (e) {
71
89
 
72
90
  if (!reset) {
73
91
  console.log(`\n credential: ${path}`);
92
+ console.log(` which: ${WHICH[source]}`);
74
93
  console.log(existing
75
94
  ? ` exists: yes — for ${existing.email}, created ${existing.createdAt ?? "(no date recorded)"}`
76
95
  : ` exists: NO — the portal will mint one on its next start and print it to that start's output`);
77
96
  console.log(existing
78
- ? `\n The passphrase itself cannot be shown: what is stored is a digest, not the secret.\n To get a working one, run: ${P}clearotron passphrase --reset\n`
97
+ ? `\n The passphrase itself cannot be shown: what is stored is a digest, not the secret.\n To get a working one, run: ${P}clearotron passphrase --reset${baseAt >= 0 ? ` --base ${base}` : ""}\n`
79
98
  : `\n Nothing to reset yet.\n`);
80
99
  process.exit(0);
81
100
  }
package/bin/start.mjs CHANGED
@@ -120,7 +120,7 @@ import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "
120
120
  import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
121
121
  import { createServer } from "node:net";
122
122
  import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
123
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
123
+ import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
124
124
  import { randomBytes } from "node:crypto";
125
125
  import { invocationPrefix, invoke } from "../shared/invocation.mjs"; // — the banner names the verb
126
126
  import { unitEnvPath } from "../shared/env-local.mjs"; // — the file the units read, named once
@@ -138,7 +138,7 @@ import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // never serve
138
138
  import { addressRefusal } from "../shared/staff-domain.mjs";
139
139
  // The grants file's editors, shared with `clearotron grant` and the portal's People page: the installer's
140
140
  // entry is written through them, so this command cannot produce a shape of its own.
141
- import { withPerson, withOrganisation } from "../shared/grants-edit.mjs";
141
+ import { withPerson, withOrganisation, withCompany } from "../shared/grants-edit.mjs";
142
142
  import { assertGrantsShape, resolvePerson } from "../shared/scope.mjs";
143
143
  import { backgroundManager } from "../shared/os-advice.mjs";
144
144
  import { frontingVariablesSet } from "../shared/install-auth.mjs"; // — one owner for what counts as a proxy in front of a door
@@ -375,6 +375,10 @@ export function installPaths(base) {
375
375
  credential: join(base, "portal-local-credential.json"),
376
376
  configStore: join(base, "config"),
377
377
  recipes: join(base, "config", "recipes"),
378
+ // The customer store, inside the config store's repository so a save commits where it lands. Only a
379
+ // demo is pointed at it (childEnv): a real install keeps whatever its settings name, and pointing it
380
+ // here would move a live store.
381
+ profiles: join(base, "config", "profiles"),
378
382
  };
379
383
  }
380
384
 
@@ -396,13 +400,15 @@ export function installPaths(base) {
396
400
  * the first organisation created when the file holds no organisation and a name is known. Absent a
397
401
  * name, none is invented: a person with access to everything resolves without
398
402
  * any organisation, and Generic then files under none, which is how every
399
- * Generic run was filed before organisations existed.
403
+ * Generic run was filed before organisations existed. `companies`, the
404
+ * accounts an install brings with it (only the demo has any), are filed under
405
+ * that organisation as it is created, and never afterwards.
400
406
  *
401
407
  * PURE, so the policy can be DRIVEN rather than read — the reason `homeEnvUpdate` is extracted: the start
402
408
  * that writes this binds ports and spawns children. `unadmitted` says the address resolves to no access
403
409
  * in the result; the caller reports that rather than repairing it.
404
410
  */
405
- export function installerGrants(existing, { user, organisation = null }) {
411
+ export function installerGrants(existing, { user, organisation = null, companies = [] }) {
406
412
  let grants = existing ?? { tenants: {} };
407
413
  const changed = [];
408
414
  if (!Object.keys(grants.people ?? {}).length) {
@@ -411,12 +417,56 @@ export function installerGrants(existing, { user, organisation = null }) {
411
417
  }
412
418
  const name = String(organisation ?? "").trim();
413
419
  if (name && !Object.keys(grants.tenants ?? {}).length) {
414
- ({ grants } = withOrganisation(grants, { name }));
420
+ let key;
421
+ ({ grants, key } = withOrganisation(grants, { name }));
422
+ // A company sits in exactly one organisation, so what the install brings is filed under the one it
423
+ // starts with: the demo's company under the demo's organisation.
424
+ for (const account of companies) grants = withCompany(grants, { tenant: key, account });
415
425
  changed.push("organisation");
416
426
  }
417
427
  return { grants, changed, unadmitted: resolvePerson(user, grants) === null };
418
428
  }
419
429
 
430
+ /**
431
+ * THE DEMO'S ORGANISATION — the one `examples/grants.example.json` already names, holding the demo account.
432
+ *
433
+ * A demo whose grants file holds no organisation offers no Generic in its switcher, and a company created
434
+ * there has nowhere to belong: 0.3.0-beta.1's demo filed none.
435
+ */
436
+ export const DEMO_ORGANISATION = "Demo Org";
437
+
438
+ /** The accounts the demo brings: every bundled profile marked demo data, read rather than listed. */
439
+ export function demoAccounts(dir = join(REPO, "driver", "profiles")) {
440
+ const keys = [];
441
+ for (const f of readdirSync(dir).filter((n) => n.endsWith(".json")).sort()) {
442
+ try {
443
+ const p = JSON.parse(readFileSync(join(dir, f), "utf8"));
444
+ if (p?.demoData === true && p?.testFixture !== true) keys.push(f.slice(0, -".json".length));
445
+ } catch { /* an unreadable profile is the loader's to refuse, and it does so by name */ }
446
+ }
447
+ return keys;
448
+ }
449
+
450
+ /**
451
+ * Copy the demo's accounts into the demo's own store: each profile, its context pack and its projects.
452
+ *
453
+ * The demo's children read that store and, of the bundle, only `generic.json`, so an account left in the
454
+ * bundle is an account the demo does not have. ONLY WHAT THE STORE DOES NOT HOLD is copied: a visitor's
455
+ * edit survives the next start, and a stale demo is one directory to remove. Returns the keys copied.
456
+ */
457
+ export function seedDemoStore({ from, to, accounts }) {
458
+ mkdirSync(to, { recursive: true });
459
+ const copied = [];
460
+ for (const key of accounts) {
461
+ if (existsSync(join(to, `${key}.json`))) continue;
462
+ cpSync(join(from, `${key}.json`), join(to, `${key}.json`));
463
+ for (const extra of [`${key}.context.md`, join("projects", key)])
464
+ if (existsSync(join(from, extra))) cpSync(join(from, extra), join(to, extra), { recursive: true });
465
+ copied.push(key);
466
+ }
467
+ return copied;
468
+ }
469
+
420
470
  /**
421
471
  * The environment each child is started with — and the ONLY place a port becomes a string or a URL.
422
472
  *
@@ -490,7 +540,7 @@ export const BACKGROUND_EXCLUDED = Object.freeze({
490
540
  "profile-service.service": "the portal constructs the profile service IN-PROCESS (driver/portal-service.mjs); the standalone unit is the separate-editor deployment shape and running both double-serves the store",
491
541
  });
492
542
 
493
- export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken, host = HOST, localWorker = false, demo = false, clientFence = null, env = process.env }) {
543
+ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken, host = HOST, localWorker = false, demo = false, clientFence = null, credential = null, env = process.env }) {
494
544
  // ONE AUTHOR FOR THIS EXPRESSION. The hosted install path composes the same
495
545
  // origin, and the near-miss is specific: this is an ORIGIN, the portal's client appends `/mcp`
496
546
  // itself, and a second author writing the endpoint form produces a doubled path — a 404 at submit
@@ -523,6 +573,31 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
523
573
  "CLEAROTRON_OUTBOX_DIR": paths.outbox,
524
574
  "CLEAROTRON_RUN_LOCK_DIR": paths.locks,
525
575
  "CLEAROTRON_ACCESS_FILE": paths.grants,
576
+
577
+ // THE SAVED-SEARCHES STORE, for every child on every install. The portal lists and saves searches in
578
+ // it; the MCP door lists them and plans runs from them through `loadRecipes()`, which reads no store
579
+ // when it is handed no directory and answers that the install has none. Outside a demo this reached
580
+ // the portal alone, so an assistant connected to a local install could neither see nor plan a saved
581
+ // search its portal showed. Unset, saved searches are not "off" in any visible way either:
582
+ // `/portal/api/config/searches` answers 404 and a settings panel renders an error. So the store is
583
+ // named here and created by this command, and where an operator has set their own, `paths` holds it.
584
+ "CLEAROTRON_RECIPES_DIR": paths.recipes,
585
+ "RECIPE_REPO_ROOT": paths.configStore,
586
+
587
+ // THE DEMO'S OWN STORE, and every name that chooses a store pinned to it, for every child and not
588
+ // only the portal. Inherited, a CLEAROTRON_CUSTOMERS_DIR, CLEAROTRON_INSTRUCTIONS_DIR or
589
+ // PROFILE_REPO_ROOT hands the demo's children the reader's real config store, and a company created
590
+ // in the demo is written into it. Empty is unset to every reader. The demo overrides no instruction,
591
+ // so the product's own are read (the portal derives no overlay in a demo), and the two audit logs and
592
+ // the feedback directory fall back to their places inside the demo's own directories.
593
+ ...(demo ? {
594
+ "CLEAROTRON_CUSTOMERS_DIR": paths.profiles,
595
+ "CLEAROTRON_INSTRUCTIONS_DIR": "",
596
+ "PROFILE_REPO_ROOT": paths.configStore,
597
+ "PROFILE_AUDIT": "",
598
+ "RECIPE_AUDIT": "",
599
+ "CLEAROTRON_FEEDBACK_DIR": "",
600
+ } : {}),
526
601
  });
527
602
  return {
528
603
  url: `http://${host}:${ports.portal}/portal`,
@@ -628,16 +703,18 @@ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsTok
628
703
  // sign-in screen they cannot pass. Measured by driving it. It is also
629
704
  // what makes "removing the demo is one directory" true rather than nearly true.
630
705
  PORTAL_LOCAL_CREDENTIAL: paths.credential,
706
+ } : credential ? {
707
+ // A LIVE INSTALL IS HANDED THE CREDENTIAL START CHOSE FOR IT when that is not the shared default:
708
+ // its own file, on its first start or once it has one, or the operator's own setting. Unset, it
709
+ // signs in with the shared default, which an install that has been using it keeps
710
+ // (installCredential says why). Never `paths.credential` unconditionally, as the demo has it: that
711
+ // would move a live install's credential and lock out whoever holds its passphrase.
712
+ PORTAL_LOCAL_CREDENTIAL: credential,
631
713
  } : {}),
632
714
  // — ONLY set when this launcher is supervising a worker. It is what licenses the portal to say
633
715
  // "waiting for a worker": a deployed instance drains via systemd and writes no heartbeat, so without
634
716
  // this the portal must keep saying "waiting to start" rather than invent an alarm.
635
717
  ...(localWorker ? { PORTAL_LOCAL_WORKER: "1" } : {}),
636
- // Unset, saved searches are not "off" in any visible way — `/portal/api/config/searches` simply
637
- // answers 404 and a panel in the settings surface renders an error. A panel degraded to a string
638
- // is the failure this command exists to remove, so the store is named and created.
639
- CLEAROTRON_RECIPES_DIR: paths.recipes,
640
- RECIPE_REPO_ROOT: paths.configStore,
641
718
  },
642
719
  };
643
720
  }
@@ -764,16 +841,33 @@ if (isMain) {
764
841
  let ports;
765
842
  try { ports = resolvePorts(process.env); } catch (e) { fatal(String(e.message)); }
766
843
 
844
+ // Decided before any path is, because in a demo every path below is the demo's own. The posture
845
+ // itself is described at the DEMO block further down.
846
+ const DEMO = argv.includes("--demo");
767
847
  // The same base `npm run setup` writes under, so whichever of the two a reader ran first, the other
768
848
  // finds the same install rather than a second one beside it.
769
- const paths = installPaths(flag("--base", join(homedir(), argv.includes("--demo") ? "trademark-demo" : "trademark")));
849
+ const paths = installPaths(flag("--base", join(homedir(), DEMO ? "trademark-demo" : "trademark")));
770
850
  // Whatever the environment already says wins over the base-derived default, for every path — a reader
771
851
  // who ran `npm run setup` has these in .env already and this must not move their data.
772
- for (const [k, name] of [["pool", "CLEAROTRON_REPORTS_DIR"], ["workspace", "CLEAROTRON_WORK_DIR"], ["queue", "CLEAROTRON_QUEUE_DIR"],
773
- ["outbox", "CLEAROTRON_OUTBOX_DIR"], ["locks", "CLEAROTRON_RUN_LOCK_DIR"], ["grants", "CLEAROTRON_ACCESS_FILE"],
774
- ["recipes", "CLEAROTRON_RECIPES_DIR"]]) if (process.env[name]) paths[k] = process.env[name];
775
- if (process.env.RECIPE_REPO_ROOT) paths.configStore = process.env.RECIPE_REPO_ROOT;
776
- if (process.env.PORTAL_AUDIT) paths.audit = process.env.PORTAL_AUDIT;
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
+ }
863
+ // ── THIS INSTALL'S FIRST START, read before this start writes either file that answers it ────────────
864
+ //
865
+ // The grants file and the config store's repository are both written further down, on every start
866
+ // since the first public release, so both absent means nothing has ever started this install. It
867
+ // decides one thing: whether this install mints a sign-in credential of its own, or keeps the shared
868
+ // one an earlier start of it has been using (installCredential says why). Asked here, before anything
869
+ // is written, so no later line can make the answer "no" on the run that is the first.
870
+ const firstStartOfThisInstall = !existsSync(paths.grants) && !existsSync(join(paths.configStore, ".git"));
777
871
  // recipe-service SAVES by committing, so the store has to live inside the repository it commits to.
778
872
  // Said at boot rather than discovered on the first Save, where the message is about `git add`.
779
873
  // — the shared statement, so the launcher, the two services and the portal cannot describe the
@@ -812,7 +906,8 @@ if (isMain) {
812
906
  // The loopback rule needs no copying: HOST above is a literal, not a default, so neither door can be
813
907
  // bound anywhere else in any mode. Sign-in is untouched — the demo signs in like any first start, and
814
908
  // the portal mints and prints its passphrase exactly as it does for a real one.
815
- const DEMO = argv.includes("--demo");
909
+ // `DEMO` itself is decided above the paths, which it keeps the demo's own.
910
+ //
816
911
  // THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (ruling,
817
912
  // 2026-09-08), so the demo account is refused from the roster unless somebody asked for it.
818
913
  // Asked here, once and visibly, rather than at each site that happens to read a roster.
@@ -874,12 +969,14 @@ if (isMain) {
874
969
  // half-made — has one answer.
875
970
  const refusal = addressRefusal(user);
876
971
  if (refusal) fatal(`${refusal}\n This address came from ${flag("--user") ? "--user" : `PORTAL_LOCAL_USER, in the environment or ${ENV_PATH}`}.`);
877
- // YOUR ORGANISATION'S NAME, which `clearotron install` asks for directly after the address and writes
878
- // as CLEAROTRON_ORGANISATION_NAME. It is read only to file the first organisation into a grants file
972
+ // YOUR ORGANISATION'S NAME, which `clearotron install` asks for and writes as
973
+ // CLEAROTRON_ORGANISATION_NAME. It is read only to file the first organisation into a grants file
879
974
  // that holds none (`installerGrants`); after that the grants file is where the name lives, and renaming
880
975
  // it is an edit there. A DEMO TAKES NONE FROM THE SETTING, for the reason it takes no address from it:
881
- // the reader's real install must not decide what the demo shows.
882
- const organisation = String(flag("--organisation", DEMO ? "" : (process.env.CLEAROTRON_ORGANISATION_NAME ?? "")) ?? "").trim();
976
+ // the reader's real install must not decide what the demo shows. It files its own instead
977
+ // (DEMO_ORGANISATION) holding the demo's company, so the switcher offers that company and its
978
+ // organisation's Generic, and a company created in the demo has an organisation to belong to.
979
+ const organisation = String(flag("--organisation", DEMO ? DEMO_ORGANISATION : (process.env.CLEAROTRON_ORGANISATION_NAME ?? "")) ?? "").trim();
883
980
 
884
981
  say("");
885
982
  say(` ${BRAND.name} ${BRAND.product.toLowerCase()} — local install`);
@@ -1064,9 +1161,10 @@ if (isMain) {
1064
1161
  // here, where the reader can still fix it, rather than as a child that exits after the doors are up.
1065
1162
  //
1066
1163
  // A DEMO WRITES ONLY ITS OWN FILE. The demo visitor is simply the first person on a box holding only
1067
- // demo data, so it gets the installer's entry — in the demo's base and nowhere else. An environment
1068
- // pointing CLEAROTRON_ACCESS_FILE elsewhere names a real install's roster, and a demo identity with
1069
- // access to everything does not belong in it.
1164
+ // demo data, so it gets the installer's entry — in the demo's base and nowhere else. It cannot be
1165
+ // pointed at another: a demo's paths come from its base and never from the environment (above), so a
1166
+ // real install's roster is out of its reach rather than warned about. It files its own organisation,
1167
+ // holding the demo's company.
1070
1168
  {
1071
1169
  let existing = null;
1072
1170
  if (existsSync(paths.grants)) {
@@ -1078,14 +1176,9 @@ if (isMain) {
1078
1176
  + " The portal refuses to start on it as well. Fix that entry by hand, or set CLEAROTRON_ACCESS_FILE to the file you mean.");
1079
1177
  }
1080
1178
  }
1081
- if (DEMO && paths.grants !== installPaths(paths.base).grants) {
1082
- err(` WARNING: CLEAROTRON_ACCESS_FILE points this demo at ${paths.grants}, outside ${paths.base}. The demo `
1083
- + `writes nothing there, so ${user} has no access unless that file already gives it some`
1084
- + `${existing ? "" : " — and it does not exist, so the portal will refuse to start"}. `
1085
- + "Unset CLEAROTRON_ACCESS_FILE to run the demo on its own file.");
1086
- } else {
1179
+ {
1087
1180
  let seeded;
1088
- try { seeded = installerGrants(existing, { user, organisation }); }
1181
+ try { seeded = installerGrants(existing, { user, organisation, companies: DEMO ? demoAccounts() : [] }); }
1089
1182
  catch (e) { fatal(`could not add ${user} to the grants file at ${paths.grants} (${String(e?.message ?? e)}).`); }
1090
1183
  if (!existing || seeded.changed.length) {
1091
1184
  // ATOMIC, because a --background refresh runs beside units that read this file per request, and a
@@ -1161,6 +1254,33 @@ if (isMain) {
1161
1254
  }
1162
1255
  }
1163
1256
 
1257
+ // ── THE DEMO'S OWN STORE, WITH ITS COMPANY IN IT ────────────────────────────────────────────────
1258
+ //
1259
+ // A demo's children are pointed at <base>/config (childEnv), so a company created in the demo is
1260
+ // written there and a later real install never sees it. The demo's company is copied into the store
1261
+ // with its projects (`seedDemoStore`), only when the store does not already hold it. It overrides no
1262
+ // instruction, so it has no instruction overlay: the product's own are read, as on any fresh install.
1263
+ if (DEMO) {
1264
+ try {
1265
+ // The store before anything is copied into it: the children are pointed at it whether or not the
1266
+ // copy below succeeds, and a store that does not exist fails every roster read rather than showing
1267
+ // Generic alone.
1268
+ mkdirSync(paths.profiles, { recursive: true });
1269
+ const copied = seedDemoStore({ from: join(REPO, "driver", "profiles"), to: paths.profiles, accounts: demoAccounts() });
1270
+ if (copied.length) {
1271
+ // Committed, so the store is identifiable like any other. A failure leaves the files readable,
1272
+ // and the first save commits them with its own change.
1273
+ try {
1274
+ execFileSync("git", ["-C", paths.configStore, "add", "-A", "--", "profiles"], { stdio: "ignore" });
1275
+ execFileSync("git", ["-C", paths.configStore, "commit", "-q", "-m", "the demo's company"], { stdio: "ignore" });
1276
+ } catch { /* see above */ }
1277
+ say(` demo store ${paths.profiles} — ${copied.join(", ")}`);
1278
+ }
1279
+ } catch (e) {
1280
+ err(` WARNING: could not set up the demo's store at ${paths.profiles} (${String(e?.message ?? e)}). Its switcher will show no demo company.`);
1281
+ }
1282
+ }
1283
+
1164
1284
  // — AN INSTALL COMES UP WITH SOMETHING IN IT.
1165
1285
  //
1166
1286
  // A fresh start used to produce a working portal over an empty archive: nothing to look at, and the
@@ -1261,8 +1381,13 @@ if (isMain) {
1261
1381
  // process's resolved environment, which is where `<repo>/.env` has already been applied, so a
1262
1382
  // decision recorded in that file survives a start rather than being silently overwritten with "1".
1263
1383
  const declaredFence = String(process.env.CLIENT_MCP_ACCOUNT_ACCESS ?? "").trim();
1384
+ // WHICH SIGN-IN CREDENTIAL THIS INSTALL USES, decided by the function `clearotron passphrase` asks too, so
1385
+ // the file the verb resets is the file the portal reads. A demo keeps its own, by layout (childEnv).
1386
+ const { installCredential } = await import("../driver/portal-local-auth.mjs");
1387
+ const signIn = DEMO ? null : installCredential({ base: paths.base, env: process.env, firstStart: firstStartOfThisInstall });
1264
1388
  const envs = childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken,
1265
- localWorker: wantWorker, demo: DEMO, clientFence: declaredFence || null });
1389
+ localWorker: wantWorker, demo: DEMO, clientFence: declaredFence || null,
1390
+ credential: signIn && signIn.source !== "shared" ? signIn.path : null });
1266
1391
 
1267
1392
  // ── 3b. the configuration snapshot, so the portal can name this install's MODE ────────────────────
1268
1393
  //
@@ -1849,7 +1974,7 @@ if (isMain) {
1849
1974
  // Captured HERE, immediately before the spawn, rather than beside the sentence that reads it: the
1850
1975
  // check has to sit on the other side of the thing that mints, and the only way to keep that true is
1851
1976
  // for it to be adjacent to the spawn where a reader can see why.
1852
- const { credentialPathFor: credentialPathBeforeStart, newPassphrase, passphraseResetCommand } = await import("../driver/portal-local-auth.mjs");
1977
+ const { credentialPathFor: credentialPathBeforeStart, newPassphrase, passphraseResetCommand, demoCredentialToReplace, laterStartLines, readLocalCredential } = await import("../driver/portal-local-auth.mjs");
1853
1978
  // ASKED ABOUT THE FILE THE PORTAL WILL ACTUALLY USE, not the shared default. `credentialPathFor`
1854
1979
  // reads `PORTAL_LOCAL_CREDENTIAL`, and a demo sets it to a file inside its own base — but this call
1855
1980
  // was made against THIS process's environment, which never carries it. So on any box that already had
@@ -1858,6 +1983,13 @@ if (isMain) {
1858
1983
  // reprinted" over a credential file that was empty. The visitor got a sign-in screen they could not
1859
1984
  // pass — which is the exact failure the demo's own credential path was introduced to prevent, left
1860
1985
  // half-wired because the mint decision was reading a different file from the mint.
1986
+ // A DEMO ALWAYS HAS A WAY IN (demoCredentialToReplace says why): its own credential is replaced on every
1987
+ // start, so this start mints and the frame below prints a passphrase that works. Before the capture,
1988
+ // because the capture is what decides whether to mint.
1989
+ if (DEMO) {
1990
+ const stale = demoCredentialToReplace({ path: credentialPathBeforeStart(envs.portal), ownPath: paths.credential, user });
1991
+ if (stale) unlinkSync(stale);
1992
+ }
1861
1993
  const credentialExisted = existsSync(credentialPathBeforeStart(envs.portal));
1862
1994
 
1863
1995
  // ── MINT HERE SO THE SUMMARY CAN PRINT IT ( — F10) ────────────────────────
@@ -1910,7 +2042,11 @@ if (isMain) {
1910
2042
  // NON-FATAL on purpose. An install with no worker is a supported state (--no-worker), so a worker that
1911
2043
  // dies must leave the portal serving rather than take the whole install down with it.
1912
2044
  const worker = wantWorker
1913
- ? start("the worker", "driver/runner.mjs", envs.worker, { args: ["--watch"], fatal: false })
2045
+ // THE FILE THIS SUPERVISOR READ, HANDED AS A FLAG. A unit's ExecStart is fixed at `--watch` and never
2046
+ // carries it, so a runner holding it was started by this command, and its order-time refusal can name
2047
+ // the file its values came from rather than one nothing on this box reads (driver/runner.mjs,
2048
+ // startEnvFile). Not a variable, and not in `envs.worker`, which `--background` writes to the units.
2049
+ ? start("the worker", "driver/runner.mjs", envs.worker, { args: ["--watch", ...(envFileRead() ? [`--start-env-file=${envFileRead()}`] : [])], fatal: false })
1914
2050
  : null;
1915
2051
 
1916
2052
  // ── 5c. the client door — the OTHER door, on this path too ( — F26) ───────
@@ -1999,9 +2135,12 @@ if (isMain) {
1999
2135
  say(` │ Lost it? ${reset}`);
2000
2136
  say(` └${rule}┘`);
2001
2137
  } else {
2002
- say(` Sign in as ${user}.`);
2003
- say(" The passphrase was minted on an earlier start and is NOT reprinted — it is stored only as a");
2004
- say(` digest. Lost it? Run ${reset} to mint a new one.`);
2138
+ // THE WAY BACK IN FIRST, then which credential, when and for whom: laterStartLines says why. Read for
2139
+ // its date and address only; a file that cannot be read is named by the portal's own boot.
2140
+ const credentialNow = credentialPathBeforeStart(envs.portal);
2141
+ let record = null;
2142
+ try { record = readLocalCredential(credentialNow); } catch { /* the portal's own boot names what is wrong with it */ }
2143
+ for (const line of laterStartLines({ user, reset, credentialPath: credentialNow, source: signIn?.source ?? "install", record })) say(line);
2005
2144
  }
2006
2145
  say("");
2007
2146
  if (worker) {
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "3709addd55b282b4c38625c841b86b7486d178b8",
3
- "version": "0.3.0-beta.1"
2
+ "commit": "f24d87c471bd3073f3a56dbf0edff39d1a640175",
3
+ "version": "0.3.0-beta.2"
4
4
  }
@@ -285,11 +285,14 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
285
285
  | `CLEAROTRON_DEMO` | unset | `1` puts this install in the DEMO posture. **Ordering is real**: the four products are listed and orderable, the form, the plan and the confirmation are the product's own, and the confirmation resolves to a finished report that already exists rather than dispatching — no engine turn, no register call, no queue entry, no run directory (ruling 2026-08-31, superseding the greyed-control ruling of the same day). A product the demo carries no finished report for refuses and names which one. It also re-aims two boot warnings written for an operator of a real deployment at the visitor who is not one, from one place (`driver/demo-posture.mjs`). **Set by `npx clearotron demo`, not by an operator** — it is passed explicitly to the two processes that have a reason to know (the portal and the MCP door; the worker is not told, because a demo never queues anything for it to drain), and those run with `CLEAROTRON_NO_ENV_FILE=1`, so a stray `.env` can neither put a live install into demo mode nor take a demo out of one. Anything but the literal `1` is not a demo. Replaces `PORTAL_DEMO`, which named only one of the processes that has to know. |
286
286
  | `CLEAROTRON_TEST_FIXTURE_PROFILES` | unset | `1` makes the profile loader return the three suite fixtures, which are refused from every roster otherwise. Set by `scripts/test-run.mjs`, never by an operator; an explicit `includeTestFixtures` argument beats it. Effect class `harness`; the full contract is its row in `.env.example`. |
287
287
  | `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo account, which a fresh install does not resolve (ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` accounts either way. Effect class `harness`; the full contract is its row in `.env.example`. |
288
- | `CLEAROTRON_ORGANISATION_NAME` | unset | Your organisation's name, written quoted by `npx clearotron install` directly after the sign-in address. The first `clearotron start` files it as the first organisation in the grants file (`CLEAROTRON_ACCESS_FILE`) when that file holds none; from then on the grants file holds the name, renaming is an edit there, and this is not read. Unset ⇒ no organisation is invented. `clearotron start --organisation <name>` supplies it for one start, and a demo never reads it. Effect class `deployment`; the full contract is its row in `.env.example`. |
289
- | `PORTAL_LOCAL_CREDENTIAL` | `~/.cordillera/portal-local-credential.json` | Where local sign-in keeps its passphrase DIGEST. `npx clearotron demo` points it inside the demo's own base directory, so a demo mints its own passphrase instead of inheriting a digest minted for another address — and removing the demo stays one `rm -rf`. |
288
+ | `CLEAROTRON_ORGANISATION_NAME` | unset | Your organisation's name, written quoted by `npx clearotron install`, which asks for it and not for a sign-in address. The first `clearotron start` files it as the first organisation in the grants file (`CLEAROTRON_ACCESS_FILE`) when that file holds none; from then on the grants file holds the name, renaming is an edit there, and this is not read. Unset ⇒ no organisation is invented. `clearotron start --organisation <name>` supplies it for one start, and a demo never reads it. Effect class `deployment`; the full contract is its row in `.env.example`. |
289
+ | `PORTAL_LOCAL_CREDENTIAL` | `~/.cordillera/portal-local-credential.json` | Where local sign-in keeps its passphrase DIGEST. `clearotron start` points it inside the install's own base directory for a new install, so it mints its own passphrase instead of adopting a digest another install left; an install that has been signing in with the shared file keeps it. `npx clearotron demo` always points it inside the demo's own base directory and mints a new passphrase there on every start, so a demo never inherits a digest minted for another address, and removing the demo stays one `rm -rf`. |
290
290
  | `PORTAL_LOCAL_PASSPHRASE` | unset | **NEVER set this in a file.** An internal one-shot handoff, not an operator control: on a first FOREGROUND start the supervisor mints the passphrase and hands it to the portal it spawns *at the spawn call*, so the closing summary can print the value beside the address rather than sending a first-time reader back into eleven startup log lines for the one value in this product that cannot be read back. It is deliberately absent from the composed child environments, because that composition is what `--background` writes into the units' env file — a passphrase there would be a permanent plaintext copy on disk and the product's own sentence, "it is stored only as a digest", would stop being true. Setting it in any env file recreates exactly that. Lost passphrase: `clearotron passphrase --reset`. |
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
+ | `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_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
+ | `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`. |
293
296
  | `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. |
294
297
  | `CLEAROTRON_RELEASE_WAIT_MS` | 25 minutes | How long a requested cut waits for the version pull request to merge itself before giving up. **Read only by the release workflow.** A rehearsal sets it to `0` so the wiring is exercised without holding a runner. Giving up is a quiet success by design — the scheduled run underneath catches a cut whose wait expired — so this budget failing shows up as a slow job rather than a red one. |
295
298
  | `CLEAROTRON_AGENT_MCP_URL` | unset (⇒ `null`) | The API-key MCP door advertised to a signed-in client. Null until that door is deployed, and the UI keeps its honest empty state rather than inventing a URL. |
@@ -152,7 +152,7 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
152
152
  | `CLEAROTRON_MCP_URL` | fail-closed omit | Staff "Ask your AI" connector base |
153
153
  | `CLEAROTRON_CLIENT_MCP_URL` | fail-closed omit | Client connector base |
154
154
  | `CLEAROTRON_ACCESS_DOMAIN` | omit note | Identity domain in the delivery email access note |
155
- | `CLEAROTRON_BOX` | unset ⇒ the expected-but-absent check is suppressed | Which deployment this is (`prod` \| `test`), for `scripts/live-surface-check.mjs`'s unit inventory. Self-declared, never inferred from the account name: an unrecognised value suppresses the arm rather than reporting every production unit missing |
155
+ | `CLEAROTRON_BOX` | none — required (unset or unrecognised ⇒ the unit-inventory line fails and names this variable) | Which deployment this is (`prod` \| `test`), for `scripts/live-surface-check.mjs`'s unit inventory. Self-declared, never inferred from the account name. Without a recognised value, the half that looks for a unit declared here and not running cannot run, because a guess would report every other deployment's units missing; so the line fails instead of passing with that half unrun |
156
156
  | `CLEAROTRON_BRAND_NAME` / `CLEAROTRON_BRAND_TAGLINE` / `CLEAROTRON_BRAND_PRODUCT` | reference-tenant literals in `shared/brand.mjs` | Tenant brand seam (single-sourced) |
157
157
 
158
158
  ### 5.2 Engine & models — T3
@@ -394,7 +394,8 @@ Local mode adds two values and no third: `PORTAL_LOCAL_USER` is the one email ad
394
394
  in `CLEAROTRON_ACCESS_FILE`, because signing in is not being enrolled), and
395
395
  `PORTAL_LOCAL_CREDENTIAL` optionally relocates the credential file, which otherwise lives at
396
396
  `~/.cordillera/portal-local-credential.json` (mode 0600, never in the repository and never inside the
397
- pool or the archive). `PORTAL_SECRET` is required in BOTH modes and signs both token families — the
397
+ pool or the archive). `clearotron start` gives a new install its own file in its base directory instead,
398
+ and an install that has been signing in with the shared file keeps it. `PORTAL_SECRET` is required in BOTH modes and signs both token families — the
398
399
  confirmation tokens unprefixed, the session cookie prefixed with a domain separator so neither can be
399
400
  replayed as the other. First start in local mode mints a passphrase and prints it once.
400
401
 
@@ -1,5 +1,100 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.0-beta.2
4
+
5
+ ### Minor Changes
6
+
7
+ - cdc7c84: New: `clearotron brandowner framework <key> <path>` points an existing company at a risk framework.
8
+
9
+ Which framework rates a company's matters is set on the command line. Until now the only verb there was `add`, which creates a company. A company made in the browser could not be pointed at its own rubric at all.
10
+
11
+ New: The framework's deck is checked before anything is written. A path that does not resolve, or a manifest that will not load, is refused. The company is left exactly as it was.
12
+
13
+ ### Patch Changes
14
+
15
+ - c7c96f3: Fixed: A key pasted at a yes-or-no question in setup is never shown on screen. Where the question leads to a key, a token or a credential, setup takes what was pasted as the answer. It does not ask for it again.
16
+
17
+ While setup waits for a yes or no, it shows only what you type toward one. Anything else stays off the screen, so a pasted key never reaches the terminal's history.
18
+
19
+ Fixed: The up-arrow at a later question in setup no longer brings back a key, a token or a password typed earlier. Setup keeps no history of its answers.
20
+ - 184fbc8: Fixed: A knockout searched on Signa now shows each filing's owner, classes and filing date. Before, those cells were blank on every filing.
21
+
22
+ Fixed: Each filing in a Signa knockout that carries its office's number now links to the trade mark office's own page for that record. Where it cannot be linked, the report gives the office and the number, and says once why. A filing without a number shows as before. The audit workbook carries the same link or number.
23
+ - 5851a16: Fixed: A knockout no longer refuses a finding that names the register filings it rests on. Before, the assessment was refused and retried, and the delivered findings lost the labels that say where each one came from.
24
+ - 4585112: Fixed: A new install signs in with a passphrase of its own, and its first start prints it. This holds even on a machine where an earlier install left a sign-in behind.
25
+
26
+ Each new install keeps its sign-in inside its own directory, `~/trademark/` by default. An install that already signs in with the shared one under `~/.cordillera/` keeps using it, so no passphrase stops working when you upgrade.
27
+
28
+ Fixed: When a start does not print the passphrase, its first line now says how to get a new one: `clearotron passphrase --reset`. It also says which sign-in file it is using, when that file was created, and for which address.
29
+
30
+ Fixed: The demo always gives you a passphrase that works. Each demo start makes a new one and prints it, instead of reusing one an earlier demo left behind.
31
+
32
+ Fixed: A new install can open its companies' profiles. If its configuration folder overrides no instruction files, the portal no longer answers with an error. Nor does it warn that risk frameworks might be synthetic. Setup also creates the `skills` folder its closing screen tells you to use.
33
+
34
+ Fixed: A search refused because the install is not fully configured now names the settings file `clearotron start` read. It no longer names a file that does not exist on that machine.
35
+ - b45a5cf: New: A screening report's summary, basis lines and conflict sentences now read in plain language.
36
+
37
+ Long sentences are split and the profession's shorthand is replaced with the everyday word. No rating, name or reason is changed.
38
+ - b45a5cf: Fixed: An audit workbook no longer reports every register finding as missing its link when the register publishes no page per record.
39
+
40
+ The registration number and the office are the citation in that case. A finding that carries neither is called out instead.
41
+
42
+ 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.
43
+ - 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.
44
+ - 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.
45
+
46
+ 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.
47
+
48
+ 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.
49
+ - 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.
50
+
51
+ 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.
52
+
53
+ Fixed: When a search is refused because the installation is not configured, the message names every configuration file that reaches the run.
54
+
55
+ Fixed: The signed-out message now offers a sign-in route for a machine with no browser.
56
+ - 332967f: For operators: `clearotron doctor` now says where a company or project saved in the portal goes once it is recorded. Sometimes the settings folder sits in a copy of a repository that other work also pulls and pushes. The next person to do that then publishes those saves. Doctor now warns about this, and counts the saves still waiting to go. Publishing your settings on purpose is still supported: this is only a warning, and it does not change doctor's result.
57
+ - cdc7c84: Fixed: Filtering Clearances to a company with no clearances no longer clears the page.
58
+
59
+ The company buttons and the status filters stay on screen, so you can pick another. They used to disappear along with the list, and they are the only way to change company there.
60
+ - b45a5cf: Fixed: Pressing Stop now prevents the report from being published.
61
+
62
+ A run stopped during its final step used to finish and deliver anyway, after telling you nothing would be delivered.
63
+
64
+ Fixed: The Stop dialog no longer promises that nothing will be delivered when the report is already being written. It says so instead.
65
+ - e3db928: Fixed: "Stop now" ends the step in flight and anything that step started, and says the step has ended only once it has.
66
+
67
+ If the step cannot be ended, the card says so and the run stops at its next step instead.
68
+ - b1dbbd5: Fixed: Pressing "Stop now" no longer says the step in flight has ended. It says a stop was sent, and what happens if the step will not take it.
69
+
70
+ A stop is sent to the step under way. If it takes it the run ends in seconds; if it does not, the run ends at its next step. The old wording promised the first of those every time.
71
+
72
+ Fixed: The notice shown while a run is stopping no longer overlaps the elapsed time on the card.
73
+
74
+ It has its own line under the run, so "1 min so far" stays readable while a stop is in flight.
75
+ - fc3c17d: Fixed: `clearotron demo` no longer reads the settings of a real install on the same computer. Before, it showed that install's companies instead of its own and put its four example reports into that install's archive. A company created in the demo would have been saved into that install's customer list. An assistant connected to the demo could be offered that install's saved searches. The demo now keeps all of this in its own folder.
76
+
77
+ Fixed: The demo's company switcher lists the demo company and Generic. A company you create in the demo belongs to the demo's own organisation.
78
+
79
+ Fixed: If no organisation is set up yet, New company now says so and names the command that sets one up.
80
+
81
+ For operators: On a local install, setup no longer asks for a sign-in address. It uses your computer account's name at `localhost` and shows it once in the summary. An address already in your settings file is kept.
82
+ - cdc7c84: Fixed: A Knockout report no longer carries a line telling the reader to remove the reviewer's notes before it goes to a client.
83
+
84
+ The notes are reference material and the report says so where they sit. An instruction to edit the document was addressed to the lawyer and read by whoever opened it.
85
+ - ad088d6: Fixed: `clearotron doctor`'s register check now tests the register your install is set up for. A register or key kept only in your install's settings file came back as not set, though doctor had just listed it. The check now reads that file too, and a value set in your shell still wins.
86
+ - b45a5cf: Fixed: You can now start a new company from the Company profile screen. It used to be reachable only from the company picker, which disappears as soon as you pick a company.
87
+
88
+ Fixed: Choosing a company on the dashboard now shows only that company's clearances. The buttons above the list used to change nothing.
89
+
90
+ 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.
91
+
92
+ 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.
93
+ - 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.
94
+ - 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.
95
+
96
+ 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.
97
+
3
98
  ## 0.3.0-beta.1
4
99
 
5
100
  ### Minor Changes