clearotron 0.2.4 → 0.3.0-beta.1

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 (235) hide show
  1. package/.env.example +13 -2
  2. package/CONTRIBUTING.md +1 -1
  3. package/INSTALL.md +62 -38
  4. package/bin/brandowner.mjs +18 -169
  5. package/bin/clearotron.mjs +3 -1
  6. package/bin/connect.mjs +28 -19
  7. package/bin/disconnect.mjs +3 -3
  8. package/bin/example.mjs +7 -7
  9. package/bin/framework-preflight.mjs +49 -0
  10. package/bin/grant.mjs +151 -93
  11. package/bin/onboard.mjs +220 -138
  12. package/bin/start.mjs +146 -137
  13. package/bin/stop.mjs +2 -2
  14. package/bin/update.mjs +1 -1
  15. package/build-info.json +2 -2
  16. package/docs/CLIENT-MCP.md +2 -2
  17. package/docs/E2E.md +12 -2
  18. package/docs/ONBOARDING.md +1 -1
  19. package/docs/PORTAL.md +14 -13
  20. package/docs/SECURITY.md +23 -24
  21. package/docs/architecture/04-configuration-reference.md +12 -5
  22. package/docs/architecture/05-config-governance.md +7 -7
  23. package/docs/architecture/07-quality-and-audit.md +1 -1
  24. package/docs/architecture/08-development-guide.md +5 -0
  25. package/docs/configuration.md +118 -0
  26. package/docs/decisions/0004-documentation-structure.md +2 -2
  27. package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
  28. package/driver/CHANGELOG.md +70 -0
  29. package/driver/ask-ledger.mjs +2 -2
  30. package/driver/cancel.mjs +27 -0
  31. package/driver/case-law-sources.mjs +3 -3
  32. package/driver/company-bundle.mjs +261 -0
  33. package/driver/compare.mjs +1 -1
  34. package/driver/compose-read.mjs +2 -2
  35. package/driver/config-inventory.mjs +2 -2
  36. package/driver/contract-audit.mjs +1 -1
  37. package/driver/contract-e3-backlog.mjs +1 -1
  38. package/driver/contract-vocabulary.mjs +1 -1
  39. package/driver/declination-call.mjs +1 -1
  40. package/driver/deliver-trigger.sh +3 -3
  41. package/driver/dev-portal.mjs +3 -1
  42. package/driver/digest-queue.mjs +1 -1
  43. package/driver/disposition-tool.mjs +1 -1
  44. package/driver/doc-constants.mjs +1 -1
  45. package/driver/drain-posture.mjs +2 -2
  46. package/driver/drainer-identity.mjs +1 -1
  47. package/driver/driver.config.mjs +44 -9
  48. package/driver/effective-scope.mjs +30 -1
  49. package/driver/effort-model.mjs +6 -6
  50. package/driver/engine/CONTRACT.md +2 -2
  51. package/driver/engine/anthropic-agent.mjs +11 -11
  52. package/driver/engine/jx-turn.mjs +1 -1
  53. package/driver/engine/mcp/gather-config.mjs +29 -4
  54. package/driver/engine/mcp/recording-server.mjs +73 -1
  55. package/driver/engine/openai-agent.mjs +1 -1
  56. package/driver/engine/probe.mjs +28 -4
  57. package/driver/enqueue-schema.mjs +23 -3
  58. package/driver/findings-model.mjs +2 -2
  59. package/driver/flag-snapshot.mjs +2 -2
  60. package/driver/floor-duty.mjs +2 -2
  61. package/driver/frame-diff-model.mjs +1 -1
  62. package/driver/framework-preflight.mjs +143 -0
  63. package/driver/gateway.mjs +9 -1
  64. package/driver/hit-list.mjs +1 -1
  65. package/driver/jx-lanes.mjs +1 -1
  66. package/driver/jx.mjs +1 -1
  67. package/driver/knockout-assess-record.mjs +1 -1
  68. package/driver/knockout-review-record.mjs +435 -0
  69. package/driver/order-probe.mjs +1 -1
  70. package/driver/outbox-backoff.mjs +2 -2
  71. package/driver/owner-use-check.mjs +2 -2
  72. package/driver/package.json +1 -1
  73. package/driver/pipeline-knockout.mjs +105 -8
  74. package/driver/pipeline.mjs +81 -30
  75. package/driver/plain-register.mjs +77 -3
  76. package/driver/portal-access.mjs +141 -74
  77. package/driver/portal-config-view.mjs +59 -70
  78. package/driver/portal-report.mjs +4 -4
  79. package/driver/portal-service.mjs +348 -141
  80. package/driver/portal-upstream.mjs +105 -17
  81. package/driver/predelivery-lint.mjs +43 -18
  82. package/driver/product-rows.mjs +1 -1
  83. package/driver/products.mjs +1 -1
  84. package/driver/profile-page.html +30 -5
  85. package/driver/profile-service.mjs +197 -26
  86. package/driver/profiles.mjs +48 -1
  87. package/driver/publish/index.mjs +31 -13
  88. package/driver/publish/knockout.mjs +9 -5
  89. package/driver/publish/office-record-links.mjs +189 -0
  90. package/driver/publish/parse.mjs +3 -3
  91. package/driver/publish/publish-inputs.mjs +26 -0
  92. package/driver/publish/render-knockout.mjs +42 -42
  93. package/driver/publish/render.mjs +29 -4
  94. package/driver/publish/report-data.mjs +2 -2
  95. package/driver/publish/seed-pool.mjs +1 -1
  96. package/driver/publish/templates/report.css +8 -8
  97. package/driver/publish/xlsx.mjs +49 -7
  98. package/driver/queue-watch-verdict.mjs +2 -2
  99. package/driver/recipe-service.mjs +1 -1
  100. package/driver/record-carry.mjs +1 -1
  101. package/driver/reference-score.mjs +1 -1
  102. package/driver/reference-strip-signatures.mjs +1 -1
  103. package/driver/register-availability.mjs +4 -3
  104. package/driver/register-count.mjs +3 -3
  105. package/driver/register-records.mjs +1 -1
  106. package/driver/repair-composers.mjs +1 -1
  107. package/driver/repairs.mjs +3 -3
  108. package/driver/replay-archive.mjs +1 -1
  109. package/driver/report-card-record.mjs +1 -1
  110. package/driver/result-noun-fields.mjs +5 -0
  111. package/driver/roster-verdict.mjs +48 -5
  112. package/driver/run-activity.mjs +1 -1
  113. package/driver/run-requirements.mjs +18 -5
  114. package/driver/runner.mjs +24 -15
  115. package/driver/search-policy.mjs +8 -8
  116. package/driver/senior-rights.mjs +1 -1
  117. package/driver/skills/prelim-search/delivery-contract.md +1 -1
  118. package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
  119. package/driver/stages-knockout.mjs +72 -6
  120. package/driver/stages.mjs +10 -10
  121. package/driver/suite-census.json +238 -70
  122. package/driver/synthesis-record.mjs +2 -2
  123. package/driver/systemd/clearotron-client-mcp.service +3 -3
  124. package/driver/systemd/clearotron-deploy.service +2 -2
  125. package/driver/systemd/clearotron-mcp-face.service +1 -1
  126. package/driver/systemd/clearotron-portal.service +3 -3
  127. package/driver/systemd/clearotron-worker.service +5 -5
  128. package/driver/systemd/install-census.mjs +1 -1
  129. package/driver/systemd/render-units.mjs +9 -9
  130. package/driver/terminal-clamp.mjs +1 -1
  131. package/driver/trigger-cap.mjs +18 -2
  132. package/driver/unit-inventory.mjs +8 -8
  133. package/driver/usage-ledger.mjs +5 -3
  134. package/driver/verify-knockout.mjs +7 -7
  135. package/driver/verify.mjs +5 -5
  136. package/driver/whatif-memo-run.mjs +1 -1
  137. package/driver/whatif-queue.mjs +3 -3
  138. package/driver/whatif-worker.mjs +2 -2
  139. package/examples/README.md +1 -1
  140. package/examples/grants.example.json +25 -24
  141. package/mcp-server/CHANGELOG.md +10 -0
  142. package/mcp-server/http-server.mjs +3 -3
  143. package/mcp-server/key-socket.mjs +1 -1
  144. package/mcp-server/lib/audit-view.mjs +3 -3
  145. package/mcp-server/lib/brief.mjs +3 -3
  146. package/mcp-server/lib/driver.mjs +1 -1
  147. package/mcp-server/lib/events.mjs +1 -1
  148. package/mcp-server/lib/http-handler.mjs +2 -2
  149. package/mcp-server/lib/instructions.mjs +2 -2
  150. package/mcp-server/lib/knockout.mjs +1 -1
  151. package/mcp-server/lib/ops.mjs +6 -3
  152. package/mcp-server/lib/options.mjs +15 -4
  153. package/mcp-server/lib/plan.mjs +7 -6
  154. package/mcp-server/lib/runs.mjs +10 -0
  155. package/mcp-server/lib/whatif.mjs +5 -5
  156. package/mcp-server/package.json +1 -1
  157. package/mcp-server/packs/README.md +1 -1
  158. package/mcp-server/remote/client-mcp-apikey.service +1 -1
  159. package/mcp-server/remote/client-mcp.service +2 -2
  160. package/mcp-server/remote/trademark-artifacts-http.service +1 -1
  161. package/mcp-server/server.mjs +38 -24
  162. package/package.json +2 -2
  163. package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
  164. package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
  165. package/portal-ui/dist/index.html +2 -2
  166. package/portal-ui/package.json +1 -1
  167. package/providers/_shared/lane-probe.mjs +9 -3
  168. package/providers/jx-subclass/lookup.mjs +1 -1
  169. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  170. package/providers/oauth-mcp-bridge/package.json +1 -1
  171. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  172. package/scripts/citation-drift-report.mjs +1 -1
  173. package/scripts/citation-line-check.mjs +2 -2
  174. package/scripts/drive-env-check.mjs +1 -1
  175. package/scripts/e2e.mjs +5 -5
  176. package/scripts/env-audit.mjs +13 -1
  177. package/scripts/headless-page.mjs +5 -5
  178. package/scripts/live-surface-check.mjs +26 -2
  179. package/scripts/mint-names-in-force.mjs +19 -5
  180. package/scripts/mint-reference-strip-backlog.mjs +1 -1
  181. package/scripts/mint-suite-census.mjs +37 -10
  182. package/scripts/pack-publishable.mjs +1 -1
  183. package/scripts/preinstall-node-check.mjs +1 -1
  184. package/scripts/release-await-cut.mjs +3 -3
  185. package/scripts/release-cut-decision.mjs +1 -1
  186. package/scripts/release-dist-tag.mjs +1 -1
  187. package/scripts/release-install-check.mjs +1 -1
  188. package/scripts/release-notes-lint.mjs +1 -1
  189. package/scripts/release-publish-guard.mjs +1 -1
  190. package/scripts/release-version-pr-checks.mjs +2 -2
  191. package/scripts/release-version.mjs +61 -5
  192. package/scripts/render-brand-banner.mjs +1 -1
  193. package/scripts/render-check.mjs +2 -2
  194. package/scripts/repo-writes.mjs +1 -1
  195. package/scripts/report-frame-check.mjs +1 -1
  196. package/scripts/report-screenshot.mjs +2 -2
  197. package/scripts/retire-bare-refs.mjs +1 -1
  198. package/scripts/revisit-render-check.mjs +1 -1
  199. package/scripts/score.mjs +1 -1
  200. package/scripts/strip-titles-and-attributions.mjs +389 -0
  201. package/scripts/strip-tracker-citations.mjs +122 -5
  202. package/scripts/test-run.mjs +4 -4
  203. package/scripts/third-party-notices.mjs +1 -1
  204. package/scripts/verify-publishable.mjs +1 -1
  205. package/shared/access-audience.mjs +2 -2
  206. package/shared/anon-overlay.mjs +1 -1
  207. package/shared/brand.mjs +15 -1
  208. package/shared/bundle-freshness.mjs +1 -1
  209. package/shared/bundle-rebuild.mjs +1 -1
  210. package/shared/checkout-move.mjs +2 -2
  211. package/shared/client-door.mjs +8 -8
  212. package/shared/connect-clients.mjs +7 -7
  213. package/shared/connector-signin-probe.mjs +1 -1
  214. package/shared/env-aliases.mjs +1 -1
  215. package/shared/env-local.mjs +5 -5
  216. package/shared/grants-edit.mjs +76 -0
  217. package/shared/install-auth.mjs +1 -1
  218. package/shared/listen.mjs +3 -3
  219. package/shared/mcp-challenge.mjs +1 -1
  220. package/shared/names-in-force.mjs +2 -1
  221. package/shared/onboarding-store.mjs +19 -2
  222. package/shared/reference-guard-classes.mjs +44 -2
  223. package/shared/register-selection.mjs +1 -1
  224. package/shared/scope.mjs +223 -56
  225. package/shared/secret-file.mjs +1 -1
  226. package/shared/server-units.mjs +1 -1
  227. package/shared/staff-domain.mjs +45 -78
  228. package/shared/summary-blocks.mjs +2 -2
  229. package/shared/systemd-failure.mjs +3 -3
  230. package/shared/tracked-files.mjs +1 -1
  231. package/shared/trigger-lane.mjs +1 -1
  232. package/shared/tty-style.mjs +1 -1
  233. package/shared/usage-block.mjs +1 -1
  234. package/shared/vacuous-pass.mjs +1 -1
  235. package/shared/verb-shim.mjs +1 -1
package/bin/start.mjs CHANGED
@@ -5,6 +5,9 @@
5
5
  //
6
6
  // npx clearotron start start the portal and the trigger lane; print one URL
7
7
  // npx clearotron start --user you@host the one address that signs in (persisted; asked for once)
8
+ // npx clearotron start --organisation <name>
9
+ // your organisation's name, filed in a grants file that holds
10
+ // none yet (setup asks for it; unset, none is invented)
8
11
  // npx clearotron start --base <dir> where this install keeps its data (default ~/trademark)
9
12
  // npx clearotron start --port <n> the portal's port (default 18802); the engine door takes
10
13
  // n+1 and the client door n+2 unless each is set explicitly
@@ -124,15 +127,19 @@ import { unitEnvPath } from "../shared/env-local.mjs"; // — the file the uni
124
127
  import { homedir, userInfo } from "node:os";
125
128
  import { dirname, join } from "node:path";
126
129
  import { fileURLToPath } from "node:url";
127
- import { usageBlock } from "../shared/usage-block.mjs"; // tracker issues 1861/1882
130
+ import { usageBlock } from "../shared/usage-block.mjs";
128
131
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
129
132
  import { productIdentity } from "../shared/product-identity.mjs"; // AGPL §13 — one answer, three surfaces
130
133
  import { pinEnvAll } from "../shared/env-aliases.mjs"; // — a pin that names one spelling has set nothing that wins
131
134
  import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
132
135
  import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // never serve a bundle older than its sources
133
- // ONE CLASSIFIER FOR WHAT A STAFF RULE ADMITS, shared with the setup wizard. Two copies of this
134
- // judgement would be a wizard that asks about one rule and a launcher that writes another.
135
- import { classifyStaffDomain, staffDomainRefusal, staffGrantSentence } from "../shared/staff-domain.mjs";
136
+ // THE REFUSALS ABOUT THE SIGN-IN ADDRESS ITSELF, shared with the setup wizard so the two cannot send
137
+ // back different addresses.
138
+ import { addressRefusal } from "../shared/staff-domain.mjs";
139
+ // The grants file's editors, shared with `clearotron grant` and the portal's People page: the installer's
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";
142
+ import { assertGrantsShape, resolvePerson } from "../shared/scope.mjs";
136
143
  import { backgroundManager } from "../shared/os-advice.mjs";
137
144
  import { frontingVariablesSet } from "../shared/install-auth.mjs"; // — one owner for what counts as a proxy in front of a door
138
145
 
@@ -175,7 +182,7 @@ export function homeEnvUpdate(homeText, union) {
175
182
  }
176
183
 
177
184
  /**
178
- * What a `--background` run has ALREADY DONE when systemd refuses to enable a unit — tracker issue 203.
185
+ * What a `--background` run has ALREADY DONE when systemd refuses to enable a unit.
179
186
  *
180
187
  * THE READER'S QUESTION IS NOT THAT A STEP FAILED. It is whether they now have a half-installed
181
188
  * deployment, and whether to run this again, undo it, or leave it alone. This step is the worst-placed
@@ -214,7 +221,7 @@ const HOST = "127.0.0.1";
214
221
  // ── pure helpers (exported for driver/test/start-command.test.mjs) ───────────────────────────────────
215
222
 
216
223
  /**
217
- * WHOSE socket is on the client door's port — tracker issue 228.
224
+ * WHOSE socket is on the client door's port.
218
225
  *
219
226
  * `clearotron stop` removes three units and DELIBERATELY leaves the client door running, because a
220
227
  * product stop must not silently revoke an assistant's connection. `start` then manages that same door
@@ -294,7 +301,7 @@ export function resolvePorts(env = {}) {
294
301
  }
295
302
 
296
303
  /**
297
- * Apply `--port <n>` to the three doors — tracker issue 166.
304
+ * Apply `--port <n>` to the three doors.
298
305
  *
299
306
  * PURE, AND EXPORTED, because the inline version of this could only be tested by spawning a supervisor
300
307
  * and reading `ss`. The defect it fixes was found by a reader watching a log line, which is exactly
@@ -371,74 +378,43 @@ export function installPaths(base) {
371
378
  };
372
379
  }
373
380
 
374
- /** A staff rule this command will not build. Carries the sentence the operator reads, and why. */
375
- export class StaffDomainRefused extends Error {
376
- constructor(domain, message) { super(message); this.name = "StaffDomainRefused"; this.domain = domain; }
377
- }
378
-
379
381
  /**
380
- * The one address that signs in, and the staff domain derived from it.
382
+ * The grants file after this start has made sure the person who installed can use the install.
381
383
  *
382
- * Derived, never asked twice. In local mode the portal admits EXACTLY ONE address, so a domain-wide
383
- * staff rule built from that address admits exactly that address and nobody else — which is what makes
384
- * it safe here and would not be safe on a hosted instance. Without it the sign-in succeeds and every
385
- * page 403s, because signing in is not being enrolled (portal-access.mjs decides that, and it is
386
- * deliberately blind to which door you came through).
384
+ * THE PERSON WHO INSTALLS IS THE FIRST PERSON: Run, Manage, access to everything. That used to be derived
385
+ * from the part of their address after the `@` — a domain rule that admitted exactly one identity on
386
+ * `<account>@localhost` and a whole employer on anything else. The rule is deleted. The installer is an
387
+ * entry of their own under `people`, and nothing about a domain admits anyone.
387
388
  *
388
- * ── AND THAT PARAGRAPH IS ONLY TRUE OF `<account>@localhost` ─────────────────────────────────────────
389
+ * TWO CONDITIONS, DECIDED SEPARATELY:
389
390
  *
390
- * "admits exactly that address and nobody else" is a property of the DOMAIN, not of the mode. It holds
391
- * for `localhost`. It does not hold for a domain other people have addresses at: the rule this line
392
- * produces from `alex@a-firm.example-tld` says *anyone at that domain*, the settings page then reports
393
- * it back in those words, and on a deployment where identity arrives from an external login system the
394
- * domain is the whole of the check. Nobody was asked, and there is nothing on the box recording that a
395
- * grant was made.
391
+ * the installer's entry written when the file names nobody under `people` — a fresh file, and the
392
+ * `{"tenants":{}}` this command wrote before `people` existed, whose local user
393
+ * was admitted by the domain rule and would otherwise be locked out of their own
394
+ * install on the first start after an upgrade. A file that names anybody is
395
+ * somebody's decision, and it is left exactly as it is.
396
+ * the first organisation created when the file holds no organisation and a name is known. Absent a
397
+ * name, none is invented: a person with access to everything resolves without
398
+ * any organisation, and Generic then files under none, which is how every
399
+ * Generic run was filed before organisations existed.
396
400
  *
397
- * So the derivation now refuses the domains that can never be a staff rule — a webmail provider, a
398
- * documentation domain — and hands anything wider than one machine back to the caller to state and
399
- * confirm. `shared/staff-domain.mjs` is the one classifier, shared with the setup wizard, so the
400
- * question the wizard asks and the rule this command builds cannot drift apart.
401
- *
402
- * An UNPARSEABLE address still returns `""`, and that is not the same answer as a refusal: nothing was
403
- * classified, so nothing was decided. The caller already refuses a non-address one line earlier.
401
+ * PURE, so the policy can be DRIVEN rather than read — the reason `homeEnvUpdate` is extracted: the start
402
+ * that writes this binds ports and spawns children. `unadmitted` says the address resolves to no access
403
+ * in the result; the caller reports that rather than repairing it.
404
404
  */
405
- export function staffDomainFor(email) {
406
- const at = String(email ?? "").lastIndexOf("@");
407
- const domain = at > 0 ? String(email).slice(at + 1).toLowerCase() : "";
408
- if (!domain) return "";
409
- const refusal = staffDomainRefusal(domain);
410
- if (refusal) throw new StaffDomainRefused(domain, refusal);
411
- return domain;
412
- }
413
-
414
- /**
415
- * What to say when the derived domain is a real one — wider than this machine, and never assumed.
416
- *
417
- * A REFUSAL RATHER THAN A PROMPT, and the reason is that this command has no reader. `start` runs from
418
- * a script, from `--background`, and from a service manager as often as from a terminal; a question
419
- * asked there either hangs or is answered by whatever happens to be on stdin. Every other decision on
420
- * this boundary already refuses instead — an unset access file, a multi-`@` identity, local mode off
421
- * loopback — so this is the idiom the rest of the door speaks.
422
- *
423
- * The consent itself lives where a person is definitionally present: `clearotron install` asks for the
424
- * address, shows this same sentence, and writes the answer down as an explicit `PORTAL_STAFF_DOMAINS`.
425
- * After that the `||` above this call short-circuits and the derivation is never reached again.
426
- */
427
- export function wideStaffDomainRefusal({ user, domain, envPath }) {
428
- return `${user} would make every address at ${domain} an administrator of this install.\n`
429
- + "\n"
430
- + ` The rule that would be written is: ${staffGrantSentence(domain, { staffLabel: `${BRAND.name} staff` })}\n`
431
- + "\n"
432
- + " That is a grant to a group, and this command will not make one on your behalf. Pick the one\n"
433
- + " that is true here:\n"
434
- + "\n"
435
- + ` · only you use this machine — start without --user, or use ${String(user).slice(0, String(user).lastIndexOf("@"))}@localhost.\n`
436
- + ` The rule is then this machine and nobody else, and nothing is granted to anyone.\n`
437
- + ` · your colleagues at ${domain} should all be administrators — say so once, in writing:\n`
438
- + ` PORTAL_STAFF_DOMAINS=${domain}\n`
439
- + ` in ${envPath}, or in the environment. The settings page then names that file as where the\n`
440
- + " rule came from, so whoever reads it later can undo it.\n"
441
- + ` · run \`${invocationPrefix()}clearotron install\`, which asks for the address and this question with it.`;
405
+ export function installerGrants(existing, { user, organisation = null }) {
406
+ let grants = existing ?? { tenants: {} };
407
+ const changed = [];
408
+ if (!Object.keys(grants.people ?? {}).length) {
409
+ grants = withPerson(grants, { email: user, switches: { run: true, manage: true, everything: true } });
410
+ changed.push("person");
411
+ }
412
+ const name = String(organisation ?? "").trim();
413
+ if (name && !Object.keys(grants.tenants ?? {}).length) {
414
+ ({ grants } = withOrganisation(grants, { name }));
415
+ changed.push("organisation");
416
+ }
417
+ return { grants, changed, unadmitted: resolvePerson(user, grants) === null };
442
418
  }
443
419
 
444
420
  /**
@@ -495,7 +471,7 @@ export const BACKGROUND_RETIRED = Object.freeze([]);
495
471
  export const BACKGROUND_EXCLUDED = Object.freeze({
496
472
  // ── `clearotron-client-mcp.service` LEFT THIS TABLE ON 2026-09-03, AND SAYING SO IS THE POINT ────
497
473
  //
498
- // It was the rebuild-seam gate of record (owner rulings, tracker issues 1976/2082): starting the unit
474
+ // It was the rebuild-seam gate of record (rulings): starting the unit
499
475
  // WAS the on-demand consent, because starting it turned on client-account access, so an enable list
500
476
  // that included it would have made that consent meaningless. That reasoning was right under that
501
477
  // ruling and the exclusion was not an oversight.
@@ -514,7 +490,7 @@ export const BACKGROUND_EXCLUDED = Object.freeze({
514
490
  "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",
515
491
  });
516
492
 
517
- export function childEnv({ ports, paths, user, staffDomains, portalSecret, tokenSecret, opsToken, host = HOST, localWorker = false, demo = false, clientFence = null, env = process.env }) {
493
+ export function childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken, host = HOST, localWorker = false, demo = false, clientFence = null, env = process.env }) {
518
494
  // ONE AUTHOR FOR THIS EXPRESSION. The hosted install path composes the same
519
495
  // origin, and the near-miss is specific: this is an ORIGIN, the portal's client appends `/mcp`
520
496
  // itself, and a second author writing the endpoint form produces a doubled path — a 404 at submit
@@ -577,7 +553,7 @@ export function childEnv({ ports, paths, user, staffDomains, portalSecret, token
577
553
  worker: { ...shared },
578
554
  // ── THE CLIENT DOOR, ON BOTH PATHS ( — F26) ──────────────────────────────
579
555
  //
580
- // Owner ruling, restated several times in session: START BOTH. It already held on the systemd path
556
+ // Ruling, restated several times in session: START BOTH. It already held on the systemd path
581
557
  // — the door is in SERVER_INSTALL_SET on the 2148 ruling that the door auto-starts and the
582
558
  // PER-ACCOUNT KEY is the gate — and it did not hold on the foreground path, with nothing saying
583
559
  // which of the two you were on. The owner spent the leg believing MCP had not started at all; it
@@ -628,7 +604,6 @@ export function childEnv({ ports, paths, user, staffDomains, portalSecret, token
628
604
  ...shared,
629
605
  PORTAL_AUTH_MODE: "local",
630
606
  PORTAL_LOCAL_USER: user,
631
- PORTAL_STAFF_DOMAINS: staffDomains,
632
607
  PORTAL_SECRET: portalSecret,
633
608
  PORTAL_SERVICE_HOST: host,
634
609
  PORTAL_SERVICE_PORT: String(ports.portal),
@@ -637,7 +612,7 @@ export function childEnv({ ports, paths, user, staffDomains, portalSecret, token
637
612
  PORTAL_OPS_TOKEN: opsToken,
638
613
  // — the demo runs THIS portal, not a second one. What that difference IS
639
614
  // has changed: the products are orderable and the confirmation resolves to a report that already
640
- // exists (owner ruling 2026-08-31 14:47, superseding the greyed-control ruling of 14:44).
615
+ // exists (ruling 2026-08-31 14:47, superseding the greyed-control ruling of 14:44).
641
616
  //
642
617
  // The FLAG moved to `shared` and is `CLEAROTRON_DEMO`, because the
643
618
  // portal is no longer the only process that has to know: the MCP door prints a boot warning aimed
@@ -687,7 +662,7 @@ if (isMain) {
687
662
  const markStateWritten = () => { wroteState = true; };
688
663
  // `stated` is a caller that has already said what stands, in terms this generic line cannot reach —
689
664
  // which unit refused, which ones are up, what re-running does. Without it the systemd refusal printed
690
- // both, and the pair read as two different answers to the reader's one question (tracker issue 203).
665
+ // both, and the pair read as two different answers to the reader's one question.
691
666
  const fatal = (msg, { stated = false } = {}) => {
692
667
  err(`\nstart: ${msg}\n`);
693
668
  if (wroteState && !stated) err(" This run had already written state (env file, data directories, grants, seeded example).\n Every one of those writes is idempotent — re-running `clearotron start` is safe and nothing needs undoing.\n");
@@ -749,7 +724,7 @@ if (isMain) {
749
724
  // be the same silent downgrade refused to build.
750
725
  // ── A BOX WITH THE SERVICES INSTALLED HAS ONE CONFIGURATION, AND IT IS NOT THIS ONE ───────────────
751
726
  //
752
- // Owner ruling,: "whatever is cleanest, simplest and industry-standard expected" —
727
+ // Ruling,: "whatever is cleanest, simplest and industry-standard expected" —
753
728
  // and yes to refusing outright. A box carrying the shipped units is a SERVER: its configuration is the
754
729
  // units' `EnvironmentFile` (`%h/.env`), and this command's is `<repo>/.env`, the hand-run laptop file.
755
730
  //
@@ -826,7 +801,7 @@ if (isMain) {
826
801
  // product's own, and the confirmation resolves to a finished report that
827
802
  // already exists — no engine turn, no register call, no queue entry, no run
828
803
  // directory. A product the demo carries no report for refuses and names which.
829
- // (Owner ruling 2026-08-31 14:47, superseding the greyed-control ruling of
804
+ // (Ruling 2026-08-31 14:47, superseding the greyed-control ruling of
830
805
  // 14:44 the same day: "a demo that shows four finished reports and a dead
831
806
  // button demonstrates the output and hides the thing a buyer is deciding
832
807
  // about". The greyed control was "a viewer creeping back in".)
@@ -838,11 +813,11 @@ if (isMain) {
838
813
  // bound anywhere else in any mode. Sign-in is untouched — the demo signs in like any first start, and
839
814
  // the portal mints and prints its passphrase exactly as it does for a real one.
840
815
  const DEMO = argv.includes("--demo");
841
- // THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (owner
842
- // ruling, 2026-09-08), so the demo account is refused from the roster unless somebody asked for it.
816
+ // THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (ruling,
817
+ // 2026-09-08), so the demo account is refused from the roster unless somebody asked for it.
843
818
  // Asked here, once and visibly, rather than at each site that happens to read a roster.
844
819
  if (DEMO) process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
845
- // ── `--port` MOVES EVERY DOOR IT OPENS (tracker issue 166) ───────────────────────────────────────
820
+ // ── `--port` MOVES EVERY DOOR IT OPENS ───────────────────────────────────────────────────────────
846
821
  //
847
822
  // It used to move ONE of the three. `resolvePorts` reads three independent variables with three
848
823
  // fixed defaults, and this line set only the portal's — so `demo --port 18860` put the portal on
@@ -865,7 +840,7 @@ if (isMain) {
865
840
  Object.assign(ports, moved);
866
841
  }
867
842
 
868
- // ── A BUNDLE OLDER THAN ITS SOURCES IS REBUILT, NOT SERVED (tracker issue 160) ───────────────────
843
+ // ── A BUNDLE OLDER THAN ITS SOURCES IS REBUILT, NOT SERVED ───────────────────────────────────────
869
844
  //
870
845
  // Only reachable on a source checkout: `portal-ui/dist` is untracked there, so a `git pull` that
871
846
  // changed `portal-ui/src` leaves the bundle behind and every surface still reads healthy. A packaged
@@ -879,45 +854,32 @@ if (isMain) {
879
854
 
880
855
  // The ADDRESS is not asked for here: a local install has one user, this machine already knows their
881
856
  // name, and the address never leaves the machine — `--user` is there for a reader who wants their real
882
- // one, and whatever is resolved here is written to `.env` so the question is never put twice. What IS
883
- // asked for, and used not to be, is the staff rule the address implies once it is a real one; that is
884
- // the block below the address, and `clearotron install` is where the question is actually put.
857
+ // one, and whatever is resolved here is written to `.env` so the question is never put twice. It is the
858
+ // first person on the install (`installerGrants`, below); the part of it after the `@` decides nothing
859
+ // about anybody else.
885
860
  let whoami = "user";
886
861
  try { whoami = userInfo().username || "user"; } catch { /* a container with no passwd entry */ }
887
862
  // In a demo the address is the demo's own and is never written anywhere: see the DEMO block above.
888
863
  const user = String(flag("--user", DEMO ? "demo@localhost" : (process.env.PORTAL_LOCAL_USER || `${whoami}@localhost`))).trim().toLowerCase();
889
864
  if (!user.includes("@") || user.indexOf("@") !== user.lastIndexOf("@"))
890
865
  fatal(`--user "${user}" is not a single email address. It is the one identity that signs in here, and the portal refuses a multi-@ identity outright.`);
891
- // ── THE STAFF RULE THIS ADDRESS IMPLIES, STATED BEFORE IT IS WRITTEN ──────────────────────────────
892
- //
893
- // An explicit `PORTAL_STAFF_DOMAINS` is a decision somebody already made in writing, and it wins
894
- // untouched — including a domain the classifier would otherwise refuse, because an operator who
895
- // typed it has said what they mean and this command does not overrule that.
896
- //
897
- // Absent one, the rule is DERIVED from the single address, and that derivation is the defect this
898
- // block exists to close. Three outcomes:
866
+ // ── THE REFUSALS ABOUT THE ADDRESS ITSELF, the wizard's own (shared/staff-domain.mjs) ──────────────
899
867
  //
900
- // `localhost` (or any bare hostname) — one machine, no second person, nothing to ask. Silent, as
901
- // it has always been. This is the default path and the only
902
- // one a laptop ever reaches.
903
- // a webmail or documentation domain — refused by `staffDomainFor` itself; there is no yes that
904
- // makes it right.
905
- // any other real domain — refused HERE, with the rule quoted in the words the
906
- // settings page uses and the three ways out named.
868
+ // A public provider or a documentation domain is sent back in the words `clearotron install` uses for
869
+ // the same address, with where this one came from, because that is the value the reader changes.
907
870
  //
908
871
  // BEFORE THIS RUN CHANGES THE BOX, deliberately — it sits above the state-written divide further
909
872
  // down, with the units gate, the auth-mode gate and the port probe. A refused start has written no
910
873
  // `.env`, minted no secret and placed no unit, so the reader's only question — is my install
911
874
  // half-made — has one answer.
912
- let staffDomains = process.env.PORTAL_STAFF_DOMAINS;
913
- if (!staffDomains) {
914
- let derived = "";
915
- try { derived = staffDomainFor(user); }
916
- catch (e) { fatal(e.message); }
917
- if (derived && classifyStaffDomain(derived) === "wide")
918
- fatal(wideStaffDomainRefusal({ user, domain: derived, envPath: ENV_PATH }));
919
- staffDomains = derived;
920
- }
875
+ const refusal = addressRefusal(user);
876
+ 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
879
+ // that holds none (`installerGrants`); after that the grants file is where the name lives, and renaming
880
+ // 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();
921
883
 
922
884
  say("");
923
885
  say(` ${BRAND.name} ${BRAND.product.toLowerCase()} — local install`);
@@ -958,9 +920,9 @@ if (isMain) {
958
920
  // down, which is what the paragraph above says this check is for.
959
921
  // The file to send the reader to, MEASURED rather than composed — `envFileRead()` reports what
960
922
  // shared/env-local.mjs actually did in this process, and answers null when it read nothing. Its own
961
- // header carries the reasoning (tracker issue 200).
923
+ // header carries the reasoning.
962
924
  const portFile = envFileRead();
963
- // Set when the probe met OUR OWN client door on its own port (tracker issue 228). Carried out of this
925
+ // Set when the probe met OUR OWN client door on its own port. Carried out of this
964
926
  // loop because both paths below have to act on it: the background path must RESTART the door it
965
927
  // adopted, and the foreground path must not spawn a second one beside it.
966
928
  let adoptedClientDoor = false;
@@ -987,7 +949,7 @@ if (isMain) {
987
949
  // The narrow carve above already proved every installed unit is ours.
988
950
  if (backgroundRefresh) break;
989
951
  const code = await probe(port);
990
- // ── OUR OWN DOOR IS NOT A COLLISION (tracker issue 228) ──────────────────
952
+ // ── OUR OWN DOOR IS NOT A COLLISION ──────────────────────────────────────
991
953
  //
992
954
  // `stop` leaves this door up on purpose and says so; refusing here made `stop`'s own "plain
993
955
  // `clearotron start` works in a terminal from here" a lie, and left `clearotron demo` unrunnable on
@@ -1006,7 +968,7 @@ if (isMain) {
1006
968
  // on a privileged port and from an address this host does not have, and names the way out of each;
1007
969
  // the launcher having its own shorter sentence for one of the three would mean a user meets two
1008
970
  // different answers to the same question depending on which door refused first.
1009
- // ── A PORT NOBODY CHOSE IS MOVED RATHER THAN REFUSED (owner ruling, 2026-09-09) ─────────────
971
+ // ── A PORT NOBODY CHOSE IS MOVED RATHER THAN REFUSED (ruling, 2026-09-09) ─────────────
1010
972
  //
1011
973
  // Three conditions, and each is a different reason:
1012
974
  // · the address is genuinely taken — anything else is not this case;
@@ -1092,13 +1054,60 @@ if (isMain) {
1092
1054
  for (const d of [paths.pool, paths.workspace, paths.queue, paths.outbox, paths.locks, paths.recipes]) {
1093
1055
  try { mkdirSync(d, { recursive: true }); } catch (e) { fatal(`could not create ${d} (${String(e?.message ?? e)}).`); }
1094
1056
  }
1095
- if (!existsSync(paths.grants)) {
1096
- // An empty roster is the correct starting state: it admits the staff domain above and grants no
1097
- // client anything. What is not legitimate is having no file — the portal refuses to start without
1098
- // one, because with no grants file every admitted identity resolves to every account.
1099
- try { writeFileSync(paths.grants, `${JSON.stringify({ tenants: {} }, null, 2)}\n`); }
1100
- catch (e) { fatal(`could not create the grants file at ${paths.grants} (${String(e?.message ?? e)}).`); }
1101
- say(` created ${paths.grants} (an empty roster — one staff address, no clients yet)`);
1057
+ // ── THE GRANTS FILE, AND THE PERSON WHO INSTALLED IN IT ─────────────────────────────────────────────
1058
+ //
1059
+ // Having no file is not legitimate — the portal refuses to start without one, because with no grants
1060
+ // file every admitted identity resolves to every account. `installerGrants` holds the policy for what
1061
+ // goes in it; this block reads, writes and says what happened.
1062
+ //
1063
+ // READ AND CHECKED FIRST, with the portal's own shape check: a file the portal will refuse is named
1064
+ // here, where the reader can still fix it, rather than as a child that exits after the doors are up.
1065
+ //
1066
+ // 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.
1070
+ {
1071
+ let existing = null;
1072
+ if (existsSync(paths.grants)) {
1073
+ try {
1074
+ existing = JSON.parse(readFileSync(paths.grants, "utf8"));
1075
+ assertGrantsShape(existing, paths.grants);
1076
+ } catch (e) {
1077
+ fatal(`the grants file at ${paths.grants} cannot be used: ${String(e?.message ?? e)}\n`
1078
+ + " 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
+ }
1080
+ }
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 {
1087
+ let seeded;
1088
+ try { seeded = installerGrants(existing, { user, organisation }); }
1089
+ catch (e) { fatal(`could not add ${user} to the grants file at ${paths.grants} (${String(e?.message ?? e)}).`); }
1090
+ if (!existing || seeded.changed.length) {
1091
+ // ATOMIC, because a --background refresh runs beside units that read this file per request, and a
1092
+ // reader catching half of it gets malformed JSON.
1093
+ const { atomicWrite } = await import("../driver/progress.mjs");
1094
+ try { atomicWrite(paths.grants, `${JSON.stringify(seeded.grants, null, 2)}\n`); }
1095
+ catch (e) { fatal(`could not write the grants file at ${paths.grants} (${String(e?.message ?? e)}).`); }
1096
+ }
1097
+ if (!existing) say(` created ${paths.grants} — ${user}: Run, Manage, access to everything`);
1098
+ else if (seeded.changed.includes("person"))
1099
+ say(` grants ${paths.grants} named nobody, so ${user} was added: Run, Manage, access to everything`);
1100
+ if (seeded.changed.includes("organisation")) say(` organisation "${organisation}", filed there — rename it there too`);
1101
+ else if (seeded.changed.includes("person") && !Object.keys(seeded.grants.tenants ?? {}).length)
1102
+ say(` no organisation named yet — \`${invocationPrefix()}clearotron start --organisation "<name>"\` files the first one`);
1103
+ // REPORTED, NOT REPAIRED. A file that names other people is somebody's decision, and adding this
1104
+ // address to it would give access to everything to an address nobody enrolled.
1105
+ if (seeded.unadmitted)
1106
+ err(` WARNING: ${paths.grants} gives ${user} no access, so it signs in and every page refuses it.\n`
1107
+ + " The file already names other people, so this command leaves it as it is. Add under \"people\":\n"
1108
+ + ` "${user}": { "run": true, "manage": true, "everything": true }\n`
1109
+ + " or start with --user set to an address that file gives access.");
1110
+ }
1102
1111
  }
1103
1112
  // THE REVOCATION LIST, CREATED — not merely named (found in review).
1104
1113
  //
@@ -1165,7 +1174,7 @@ if (isMain) {
1165
1174
  // ONE sample ships today (a multi-country focus search). Three of the four products have no finished
1166
1175
  // run anywhere to freeze — see — so this seeds what exists and picks the rest up unchanged when
1167
1176
  // they are captured.
1168
- // ── F23 — A REAL INSTALL STARTS EMPTY (OWNER RULING, 2026-09-04) ────────
1177
+ // ── F23 — A REAL INSTALL STARTS EMPTY (RULING, 2026-09-04) ────────
1169
1178
  //
1170
1179
  // Owner, in session, on his first real start: "critical, it started and I still see a demo report in
1171
1180
  // the actual product. Should not be there — should ONLY be in demo. Proper product should have no
@@ -1197,7 +1206,7 @@ if (isMain) {
1197
1206
  // — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
1198
1207
  const { publishSource } = await import("../driver/demo-container.mjs");
1199
1208
  const seed = await seedPool({ pool: paths.pool, examplesDir: publishSource(join(REPO, "demo"), { repoRoot: REPO }), republish: republishRun });
1200
- // WHAT WAS ALREADY THERE IS SAID TOO (tracker issue 277). This branch used to run only when the pool
1209
+ // WHAT WAS ALREADY THERE IS SAID TOO. This branch used to run only when the pool
1201
1210
  // was empty; it now tops a stale pool up to the package's set, so "seeded 1" on an upgrade is a fact
1202
1211
  // about what was MISSING and says nothing on its own about how many are now listed.
1203
1212
  if (seed.already?.length) {
@@ -1252,7 +1261,7 @@ if (isMain) {
1252
1261
  // process's resolved environment, which is where `<repo>/.env` has already been applied, so a
1253
1262
  // decision recorded in that file survives a start rather than being silently overwritten with "1".
1254
1263
  const declaredFence = String(process.env.CLIENT_MCP_ACCOUNT_ACCESS ?? "").trim();
1255
- const envs = childEnv({ ports, paths, user, staffDomains, portalSecret, tokenSecret, opsToken,
1264
+ const envs = childEnv({ ports, paths, user, portalSecret, tokenSecret, opsToken,
1256
1265
  localWorker: wantWorker, demo: DEMO, clientFence: declaredFence || null });
1257
1266
 
1258
1267
  // ── 3b. the configuration snapshot, so the portal can name this install's MODE ────────────────────
@@ -1300,8 +1309,8 @@ if (isMain) {
1300
1309
  // the processes and thats it its done". The foreground supervisor dies with the terminal — his portal
1301
1310
  // was gone between two of his own sessions and every screen spun. The honest fix on a machine with
1302
1311
  // systemd is not a detach flag and a pid file; it is the same user units a server runs, installed for
1303
- // exactly the pinned BACKGROUND_UNITS set (the census arm keeps that pin honest). Since tracker issue
1304
- // 2148 settled point 2 that set INCLUDES the client door, which is why the block below writes the
1312
+ // exactly the pinned BACKGROUND_UNITS set (the census arm keeps that pin honest). Since the owner
1313
+ // settled point 2 that set INCLUDES the client door, which is why the block below writes the
1305
1314
  // door's settings before any unit is placed.
1306
1315
  //
1307
1316
  // THE UNITS READ %h/.env, NOT THIS PROCESS'S ENV. Everything the foreground supervisor would have
@@ -1310,7 +1319,7 @@ if (isMain) {
1310
1319
  // what makes re-running safe, and the same idempotence sentence the foreground fatal carries applies.
1311
1320
  if (wantBackground) {
1312
1321
  // ONE AUTHOR for this path — `driver/runner.mjs` names the same file in the same words since
1313
- // tracker issue 216 moved the run-configuration refusal there, and two composers of one path
1322
+ // A later change moved the run-configuration refusal there, and two composers of one path
1314
1323
  // fail quietly: a refusal that sends an operator to edit a file the units do not read.
1315
1324
  const HOME_ENV = unitEnvPath();
1316
1325
  const union = { ...envs.mcp, ...envs.portal, ...(envs.worker ?? {}),
@@ -1402,9 +1411,9 @@ if (isMain) {
1402
1411
  };
1403
1412
  const willRead = { ...already, ...union };
1404
1413
  const miss = missingRequirements(willRead, RUN_TABLES);
1405
- // ── — WHICH HALF OF `blocking` MAY REFUSE A START (tracker issue 216) ─────
1414
+ // ── — WHICH HALF OF `blocking` MAY REFUSE A START ─────────────────────────
1406
1415
  //
1407
- // Owner ruling 2026-09-06, in session: "someone can install and select key later so it should
1416
+ // Ruling 2026-09-06, in session: "someone can install and select key later so it should
1408
1417
  // still start." So the register, its credential, the engine and the engine's binary NO LONGER
1409
1418
  // refuse here. They refuse AT ORDER TIME — `driver/runner.mjs`'s intake wall, before a stage
1410
1419
  // dispatches and before anything is spent — and `doctor` and the portal report the box as
@@ -1426,7 +1435,7 @@ if (isMain) {
1426
1435
  // of them, and offered `install` as the route. `install` REFUSES a non-terminal — "this is an
1427
1436
  // interactive wizard and stdin is not a terminal", rc 2 — so the one reader who arrives here by a
1428
1437
  // scripted or hosted install was handed a route they cannot take and no address for the route
1429
- // they can. Same defect as naming a variable and not the file, one level up (tracker issue 202).
1438
+ // they can. Same defect as naming a variable and not the file, one level up.
1430
1439
  //
1431
1440
  // BOTH FILES, and that is the difference from the port refusals, which say `~/.env` "is NOT read
1432
1441
  // here". They are right: nothing in that file reaches a port decision. Here both are true. The
@@ -1456,7 +1465,7 @@ if (isMain) {
1456
1465
  + `client's first search, which is where this surfaced before: as a failed run and a notice saying `
1457
1466
  + `they had been notified.`);
1458
1467
  }
1459
- // ── THE BOX STARTS AND SAYS WHAT IT CANNOT DO YET (tracker issue 216) ──
1468
+ // ── THE BOX STARTS AND SAYS WHAT IT CANNOT DO YET ──────────────────────
1460
1469
  //
1461
1470
  // An install that comes up unconfigured must not come up SILENTLY unconfigured — that is the
1462
1471
  // failure one step along from the one being fixed: a reader who is told nothing concludes they are
@@ -1466,13 +1475,13 @@ if (isMain) {
1466
1475
  say(` the doors and the portal are up, and every run is refused at order time, before`);
1467
1476
  say(` anything is spent, naming what is missing.`);
1468
1477
  for (const r of miss.atOrder) say(` ${r.name} — ${r.why}`);
1469
- // ── THE SAME REMEDY THE REFUSAL USED TO CARRY, AND THE SAME TWO FILES (tracker issue 202) ──
1478
+ // ── THE SAME REMEDY THE REFUSAL USED TO CARRY, AND THE SAME TWO FILES ──────────────────────
1470
1479
  //
1471
1480
  // 202's subject was a refusal on THESE values that named no file, on a product with two of them,
1472
1481
  // and offered `install` — a wizard that refuses a non-terminal, so the one reader who arrives
1473
1482
  // here by a scripted or hosted install was handed a route they cannot take.
1474
1483
  //
1475
- // tracker issue 216 moved that refusal to order time, so this screen is where these values are
1484
+ // A later change moved that refusal to order time, so this screen is where these values are
1476
1485
  // now named to an operator. The remedy travels with them RATHER THAN BEING DELETED WITH THE
1477
1486
  // REFUSAL — 202's property is about the class ("a message about a value names the file that sets
1478
1487
  // it"), not about which gate happened to print it, and the arms in
@@ -1518,7 +1527,7 @@ if (isMain) {
1518
1527
  say(` ${ENV_PATH} — the CLI reads this one when you type a command in a shell.`);
1519
1528
  say(` Editing one does not change the other. To change what the RUNNING product does, edit the`);
1520
1529
  say(` first and restart the units.`);
1521
- // PORTS ARE THE EXCEPTION, and leaving it unsaid is what tracker issue 200 was filed about. The
1530
+ // PORTS ARE THE EXCEPTION, and leaving it unsaid is what that gap was. The
1522
1531
  // sentence above is true — a unit takes its port from the EnvironmentFile like everything else —
1523
1532
  // but THIS command probes for a collision using the value it read from the CLI file, before any
1524
1533
  // unit exists. A reader who has just been told to edit the first file, and whose next run refuses
@@ -1541,7 +1550,7 @@ if (isMain) {
1541
1550
  writeFileSync(join(UNIT_DIR, u), renderUnit(text, { ...process.env, ...union }));
1542
1551
  }
1543
1552
  // STDERR IS CAPTURED, not discarded. `stdio: "ignore"` threw systemd's own explanation away before
1544
- // anyone could read it, which is half of what tracker issue 121 fixed in `connect` and was never
1553
+ // anyone could read it, which is half of what was fixed in `connect` and was never
1545
1554
  // done here. The two-cause remedy below is right and stays; what was missing was the sentence
1546
1555
  // systemd itself wrote.
1547
1556
  //
@@ -1587,7 +1596,7 @@ if (isMain) {
1587
1596
  //
1588
1597
  // This loop ran uncaught. When systemd declined to enable a unit the whole output was
1589
1598
  // `node:internal/errors:983`, a Node stack trace and a status code — no name for what failed, and no
1590
- // statement of what had happened to the install (tracker issue 203). It is the worst-placed refusal
1599
+ // statement of what had happened to the install. It is the worst-placed refusal
1591
1600
  // in this command: by here the env file, the data directories, the grants roster and the seeded
1592
1601
  // example are written and every unit file is rendered, so the reader is left not knowing whether
1593
1602
  // they have a half-installed deployment. Every other refusal in this command says whether anything
@@ -1620,7 +1629,7 @@ if (isMain) {
1620
1629
  if (backgroundRefresh) for (const u of unitsToRestartOnRefresh(BACKGROUND_UNITS, unitTypeOf)) {
1621
1630
  try { execFileSync("systemctl", ["--user", "restart", u], { stdio: "ignore" }); } catch { /* health check below reports it */ }
1622
1631
  }
1623
- // ── AN ADOPTED DOOR IS RESTARTED, OR IT KEEPS RUNNING THE OLD TREE (tracker issue 228) ──────────
1632
+ // ── AN ADOPTED DOOR IS RESTARTED, OR IT KEEPS RUNNING THE OLD TREE ──────────────────────────────
1624
1633
  //
1625
1634
  // `enable --now` is a NO-OP on an already-active unit — the finding `unitsToRestartOnRefresh` exists
1626
1635
  // for, measured on the test box as an unchanged MainPID. So a door this run adopted rather than
@@ -1910,7 +1919,7 @@ if (isMain) {
1910
1919
  // supported state and a useful one; an install that refuses to come up at all because a door could
1911
1920
  // not bind is not. The door's own refusal is loud and carries its remedy — measured and recorded as
1912
1921
  // working — so a reader sees why in its output rather than losing the portal along with it.
1913
- // ADOPTED, NOT RE-SPAWNED (tracker issue 228). On a box where `stop` left the door unit up, spawning a
1922
+ // ADOPTED, NOT RE-SPAWNED. On a box where `stop` left the door unit up, spawning a
1914
1923
  // second door here would bind-fail against the first and print a refusal about a port the reader's
1915
1924
  // assistant is correctly connected to. The unit IS the door; this path just does not add another.
1916
1925
  const clientDoor = adoptedClientDoor
@@ -1937,7 +1946,7 @@ if (isMain) {
1937
1946
  // is reaped, so it is the same fact without the race. The failure mode being avoided is this
1938
1947
  // finding's own defect relocated into its failure path: a dead door announced as one a client's
1939
1948
  // assistant connects to, which is worse than the silence F26 replaced.
1940
- // AN ADOPTED DOOR IS RUNNING (tracker issue 228). Without this the banner reported "NOT RUNNING …
1949
+ // AN ADOPTED DOOR IS RUNNING. Without this the banner reported "NOT RUNNING …
1941
1950
  // its output above says why" about a door that is up and serving, and pointed the reader at output
1942
1951
  // that does not exist — the one sentence on this screen a reader would act on, and false.
1943
1952
  const doorRunning = adoptedClientDoor || clientDoor?.child?.exitCode === null;
package/bin/stop.mjs CHANGED
@@ -27,7 +27,7 @@ import { invoke } from "../shared/invocation.mjs";
27
27
  const UNIT_DIR = join(homedir(), ".config", "systemd", "user");
28
28
  const say = (s = "") => console.log(s);
29
29
  // ON STDERR, because a refusal that scrolls past in the same stream as the success lines is a
30
- // refusal a script cannot act on and a reader skims (tracker issue 270).
30
+ // refusal a script cannot act on and a reader skims.
31
31
  const err = (s = "") => console.error(s);
32
32
 
33
33
  const argv = process.argv.slice(2);
@@ -49,7 +49,7 @@ for (const u of BACKGROUND_UNITS) {
49
49
  const file = join(UNIT_DIR, u);
50
50
  if (!existsSync(file)) continue;
51
51
  found++;
52
- // ── THE STOP IS VERIFIED, AND THE FILE GOES ONLY IF IT WORKED (tracker issue 270) ────────────────
52
+ // ── THE STOP IS VERIFIED, AND THE FILE GOES ONLY IF IT WORKED ────────────────────────────────────
53
53
  //
54
54
  // This was `catch { /* already down */ }` — a COMMENT standing in for a check. The comment guessed why
55
55
  // the call failed, the next line deleted the unit file regardless, and the line after that announced
package/bin/update.mjs CHANGED
@@ -314,7 +314,7 @@ export async function update(argv = process.argv.slice(2)) {
314
314
  return installed;
315
315
  }
316
316
 
317
- // ── THE BUNDLE THE PULL COULD NOT UPDATE (tracker issue 160) ─────────────────────────────────────
317
+ // ── THE BUNDLE THE PULL COULD NOT UPDATE ─────────────────────────────────────────────────────────
318
318
  //
319
319
  // `portal-ui/dist` is untracked on the public tree, so `git pull` above can never bring it forward.
320
320
  // A pull that changed `portal-ui/src` therefore leaves the built bundle behind, and every surface
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "3c2277f7fab7452490c86d19802dd60a886a2b06",
3
- "version": "0.2.4"
2
+ "commit": "3709addd55b282b4c38625c841b86b7486d178b8",
3
+ "version": "0.3.0-beta.1"
4
4
  }