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
@@ -27,7 +27,7 @@ import { envFileRead } from "../shared/env-local.mjs"; // side effect: apply t
27
27
  import { envFrom } from "../shared/env-aliases.mjs"; // — a refusal names the name in force
28
28
  import { accessAudience, audienceLabel } from "../shared/access-audience.mjs"; // — F54; jose-free on purpose
29
29
  import { doorPostureVerdict } from "./door-posture.mjs"; // — say when this door's mode came from another door's variables
30
- // The local key door (tracker issue 174): a second listener on a unix socket, so a scoped access key has
30
+ // The local key door: a second listener on a unix socket, so a scoped access key has
31
31
  // a path that no tunnel can forward to and the TCP door never learns about keys.
32
32
  import { keyDoorRefusal, openKeyDoor, KEY_SOCKET_MODE } from "./key-socket.mjs";
33
33
  import { demoPostureLine } from "../driver/demo-posture.mjs"; // — the two mis-aimed warnings answer from one place
@@ -221,7 +221,7 @@ if (isMain) {
221
221
  }
222
222
  }
223
223
 
224
- // ── THE KEY PATH, ON THE POSTURE SURFACE (tracker issue 174) ──────────────────────────────────────
224
+ // ── THE KEY PATH, ON THE POSTURE SURFACE ──────────────────────────────────────────────────────────
225
225
  //
226
226
  // An operator must be able to see that a local key path exists, and with what permissions, WITHOUT
227
227
  // reading a unit file — the acceptance asks for exactly that, and it is the same reasoning as the
@@ -318,7 +318,7 @@ if (isMain) {
318
318
  onReady: ({ port: bound }) => log(`listening on http://${HOST}:${bound}/mcp — READ-ONLY staff surface, firmDomains=[${ALLOWED_DOMAINS.join(", ")}], ${door}`),
319
319
  });
320
320
 
321
- // ── THE SECOND DOOR: A LOCAL KEY PATH ON A UNIX SOCKET (tracker issue 174) ────────────────────────
321
+ // ── THE SECOND DOOR: A LOCAL KEY PATH ON A UNIX SOCKET ────────────────────────────────────────────
322
322
  //
323
323
  // One process, two transports, two handlers. The TCP door above is untouched and still never honours a
324
324
  // key; this one takes a scoped access key and cannot be reached from any network. A tunnel forwards to
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
3
  // key-socket.mjs — the local key door, on a unix socket, beside the TCP door that takes a proxy identity
4
- // (tracker issue 174).
4
+ //.
5
5
  //
6
6
  // ── THE PROBLEM, AND WHY THE OBVIOUS FIX IS NOT ONE ─────────────────────────────────────────────────
7
7
  //
@@ -2,7 +2,7 @@
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
3
  // lib/audit-view.mjs — the AUDIT CHAIN a client account may interrogate, projected.
4
4
  //
5
- // Owner ruling, 2026-08-27: "I don't see why we don't open it or just give it to clients. Ignore the call
5
+ // Ruling, 2026-08-27: "I don't see why we don't open it or just give it to clients. Ignore the call
6
6
  // spend." shared/scope.mjs holds the line that ruling drew and which artifacts it opened; this file is the
7
7
  // half that decides what the four STRUCTURED reads hand over — get_run, trace, decision_timeline,
8
8
  // get_finding, and list_findings' raw block lists.
@@ -311,7 +311,7 @@ export function accountTimeline(result, { brandName = "The firm" } = {}) {
311
311
  };
312
312
  }
313
313
 
314
- // ---- WHAT-IF (owner ruling 2026-08-27) --------------------------------------------------------------
314
+ // ---- WHAT-IF (ruling 2026-08-27) --------------------------------------------------------------
315
315
  //
316
316
  // The counterfactual is the second half of the same ruling, and it meets the same two seals. The PLAN
317
317
  // prints what the change would cost, drawn from the prior run's telemetry — which is precisely the model
@@ -324,7 +324,7 @@ export function accountTimeline(result, { brandName = "The firm" } = {}) {
324
324
  // those out and none is added here; this is the same cost/chain line the audit reads draw, applied to the
325
325
  // one tool that would otherwise walk straight through it.
326
326
  //
327
- // THE MEMO KIND ADDS THREE, AND LEAVING THEM OUT BROKE IT SILENTLY (tracker issue 132). This list is
327
+ // THE MEMO KIND ADDS THREE, AND LEAVING THEM OUT BROKE IT SILENTLY. This list is
328
328
  // default-deny, so a plan kind whose fields nobody added here arrives stripped rather than refused. A
329
329
  // memo plan was composed correctly and reached a client missing `kind` (so it could not be told from a
330
330
  // stage plan), `assumption` (so it did not say what it was about) and `parentUntouched` (so it did not
@@ -13,7 +13,7 @@
13
13
  // fallback. That fallback survives — for archived runs published before report-data.json existed — and it
14
14
  // now says so in the brief instead of being indistinguishable from the real thing. The client-summary
15
15
  // branch is deleted, and with it the `**Recommendation:** …` line that only it emitted: the deliverable
16
- // carries prioritized facts and never advice (owner ruling 2026-07-28) — the recipient is a lawyer who
16
+ // carries prioritized facts and never advice (ruling 2026-07-28) — the recipient is a lawyer who
17
17
  // layers advice on top. The line is gone by construction, not suppressed.
18
18
  //
19
19
  // THE PRODUCT NAME IS DERIVED, NEVER STORED AND NEVER HARDCODED. Every run used to announce itself as
@@ -135,8 +135,8 @@ export function buildBrief(run) {
135
135
  lines.push(`- **${m.name}** — ${band}.${d.url ? ` Report: ${d.url}` : ""}`);
136
136
  for (const f of (m.findings ?? [])) {
137
137
  const who = [f.name, f.owner].filter(Boolean).join(" — ");
138
- // A PROMOTED REGISTER FILING SHOWS THE RATING AND THE READ THE SEARCH ACTUALLY MADE (tracker
139
- // issue 274). This line used to print `net` alone, and for a register card `net` carried the
138
+ // A PROMOTED REGISTER FILING SHOWS THE RATING AND THE READ THE SEARCH ACTUALLY MADE. This
139
+ // line used to print `net` alone, and for a register card `net` carried the
140
140
  // stated "no rating of its own" — so the one hard legal right on a page was described here as
141
141
  // unrated even on runs where the assessment had written a full read of that exact filing and
142
142
  // the report was already printing it. The page and this briefing disagreed.
@@ -16,7 +16,7 @@ export {
16
16
 
17
17
  // The KNOCKOUT lane's own run-dir table. `paths` above is the CLEARANCE table and has no entry for any
18
18
  // file this lane writes, which is why every audit projection read a delivered knockout as a run with
19
- // nothing on disk (tracker issue 275). Re-exported rather than re-derived for the reason this whole file
19
+ // nothing on disk. Re-exported rather than re-derived for the reason this whole file
20
20
  // exists: a second copy of a path table drifts, and the drift shows up as an artifact reported missing.
21
21
  export { koPaths } from "../../driver/stages-knockout.mjs";
22
22
 
@@ -112,7 +112,7 @@ function classify(e, st, rat) {
112
112
  // was never written, or was blank — were indistinguishable on a lawyer's timeline from a genuinely
113
113
  // clean gate.
114
114
  //
115
- // OWNER RULING (2026-08-18): when the screening safety-net could not actually inspect anything, the
115
+ // RULING (2026-08-18): when the screening safety-net could not actually inspect anything, the
116
116
  // client's progress view shows "Screening: incomplete — flagged for review". Clear wording appears
117
117
  // ONLY when the gate genuinely ran and found nothing. `CLIENT_INCOMPLETE` below is that string
118
118
  // VERBATIM and is not to be re-worded here — it is owner-approved client-visible text.
@@ -78,8 +78,8 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
78
78
  if (tokenOnly && verify) throw new Error("makeHttpHandler: tokenOnly is for a door with no auth proxy in front — pass verify:null");
79
79
  return async (req, res) => {
80
80
  try {
81
- // THE BASE IS A CONSTANT, AND WHAT THE `Host` HEADER IS USED FOR HERE IS: NOTHING (tracker issue
82
- // 1928). Only `pathname` and `searchParams` are read below, so the base exists purely so a bare
81
+ // THE BASE IS A CONSTANT, AND WHAT THE `Host` HEADER IS USED FOR HERE IS: NOTHING. Only
82
+ // `pathname` and `searchParams` are read below, so the base exists purely so a bare
83
83
  // `req.url` parses as a path. Interpolating the caller's `Host` bought a crash and no behaviour:
84
84
  // a value that is not a valid authority makes `new URL` throw, the outer catch answers 500 with a
85
85
  // stack in the log, and every bit of that happens ABOVE the `authenticate FIRST` block — so an
@@ -44,7 +44,7 @@ import { fileURLToPath } from "node:url";
44
44
  const SKILLS = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
45
45
  const SKILL_DIR = Object.freeze({ client: "clearotron-client", account: "clearotron-account", ops: "clearotron-ops" });
46
46
 
47
- // ── A PACK MAY BE MORE THAN ONE FILE (tracker issue 148) ──────────────────────────────────────────
47
+ // ── A PACK MAY BE MORE THAN ONE FILE ──────────────────────────────────────────────────────────────
48
48
  //
49
49
  // The ops SKILL.md tells the assistant twice that delivery "comes back to you as outbox events (see
50
50
  // COURIER.md)" — and this reader only ever opened SKILL.md, so from the assistant's side that document
@@ -105,7 +105,7 @@ function pack(audience) {
105
105
  * engineering tool set neither pack describes". That premise held while ops meant OUR agents, briefed
106
106
  * separately by the Claude Code plugin which installs the same packs as files.
107
107
  *
108
- * Owner ruling (2026-08-27, ruling 7): on a SELF-HOSTED install the customer IS ops. The person who owns
108
+ * Ruling (2026-08-27, ruling 7): on a SELF-HOSTED install the customer IS ops. The person who owns
109
109
  * the box connects over this same connector and is briefed with nothing, while
110
110
  * `skills/clearotron-ops/SKILL.md` sits shipped and undelivered — SKILL_DIR has mapped it the whole
111
111
  * time. So the premise is false for that deployment, and the exclusion went with it.
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
- // lib/knockout.mjs — the KNOCKOUT lane, projected into the audit tools' own shapes (tracker issue 275).
3
+ // lib/knockout.mjs — the KNOCKOUT lane, projected into the audit tools' own shapes.
4
4
  //
5
5
  // THE DEFECT THIS CLOSES, and it is worth stating plainly because the failure mode was a confident wrong
6
6
  // answer rather than an error. Every read-only tool whose job is to show HOW a search reached its answer
@@ -99,6 +99,9 @@ export function buildJob(args = {}, { scope } = {}) {
99
99
  profileKey: args.profileKey || undefined,
100
100
  // the project/engagement under the customer — its overlay rates the matter
101
101
  projectKey: args.projectKey || undefined,
102
+ // which organisation's Generic — `authorize` stamps it on a Generic job from the verified person, and
103
+ // an ops caller is trusted to route it. Dropped here, a Generic run lost the organisation it belongs to.
104
+ tenant: args.tenant || undefined,
102
105
  // per-run SCOPE — where the machinery points, as against the selectors above which choose WHICH
103
106
  // machinery runs. Passed through verbatim: validateJob owns the vocabulary (shape, caps, dedupe,
104
107
  // the bare-string normalization) so this door cannot drift from the CLI or the portal.
@@ -197,7 +200,7 @@ export const START_RUN_JOB_FIELDS = Object.freeze({
197
200
  // and "I said nothing" are two different searches and only a positive instruction can tell them apart.
198
201
  "geography",
199
202
  "product", "recipeKey", "nativeLanguage", "caseLaw", "searchLevel", "deliveryRoute", "parentRunId",
200
- "customer", "profileKey", "projectKey", "customerUnknown",
203
+ "customer", "profileKey", "projectKey", "customerUnknown", "tenant",
201
204
  "upfrontInstructions", "brief", "rawRequest", "deliverableSpec", "commercialFlexibility",
202
205
  "priorUse", "campaignShape", "deadline",
203
206
  "dupOverride", "clientPrincipal", "enqueuedAt", "enqueuedVia",
@@ -208,7 +211,7 @@ export const START_RUN_JOB_FIELDS = Object.freeze({
208
211
  notCarried: Object.freeze({
209
212
  registerFixtures: "a run that reads canned register payloads instead of calling a register. This door "
210
213
  + "starts real work for staff and agents; a fixture round is composed as job files by the e2e harness, "
211
- + "which writes the field directly rather than asking this tool for it (tracker issue 2038).",
214
+ + "which writes the field directly rather than asking this tool for it.",
212
215
  promptParts: "the requester's declaration that the prose rides as SIDECAR files. This door assembles from "
213
216
  + "structured tool input and writes no sidecars, so a job it built cannot be in that shape. Carrying it "
214
217
  + "would make the manifest claim an intake it did not use, and the manifest check would then report sidecars "
@@ -346,7 +349,7 @@ export function stopRun(args = {}, { scope } = {}) {
346
349
 
347
350
  // ── — IMMEDIATE MODE: SENTINEL FIRST, THEN THE SIGNAL ─────────────────────
348
351
  //
349
- // Owner ruling, on his second encounter with the same wait: "a stop is a stop — maybe it should be
352
+ // Ruling, on his second encounter with the same wait: "a stop is a stop — maybe it should be
350
353
  // a 'stop immediately or at next boundary to preserve data' kind of question when you press it."
351
354
  // The boundary stop is unchanged and stays the default; this is the other half of the choice.
352
355
  //
@@ -184,12 +184,20 @@ function accountKeyFor(args, scope) {
184
184
  function accountFor(key, { scope, now }) {
185
185
  if (!key) return null;
186
186
  let profile = null;
187
- try { profile = loadProfiles().get(key) ?? null; } catch { return null; } // unreadable roster ⇒ say nothing
187
+ // ON A MISS THE ROSTER IS READ AGAIN, the way start_run's check does it (driver/enqueue-schema.mjs).
188
+ // The cache behind `loadProfiles()` is filled once per process, so a company created in the portal
189
+ // after the door started was answered with no account at all. A hit costs nothing extra; a miss reads
190
+ // the store, and the new company's projects are then walked against that same fresh roster.
191
+ let fresh = null;
192
+ try {
193
+ profile = loadProfiles().get(key) ?? null;
194
+ if (!profile) { fresh = loadProfiles({ force: true }); profile = fresh.get(key) ?? null; }
195
+ } catch { return null; } // unreadable roster ⇒ say nothing
188
196
  if (!profile || profile.key === "generic") return null;
189
197
 
190
198
  let projects = [];
191
199
  try {
192
- for (const [, ov] of loadProjects()) {
200
+ for (const [, ov] of loadProjects(fresh ? { profiles: fresh, force: true } : {})) {
193
201
  if (ov.archived || ov.customerKey !== key) continue;
194
202
  projects.push({ key: ov.projectKey, name: ov.projectName });
195
203
  }
@@ -268,7 +276,9 @@ function allowanceFor(profile, { scope, now }) {
268
276
  // The queue dirs the runner drains — the ledger sits beside each of them (usage-ledger.mjs).
269
277
  try { usage = accountUsage({ queueDirs: config.queueDirs, account: profile.key, now }); } catch { usage = null; }
270
278
  const shared = {
271
- capped: scope?.kind === "account",
279
+ // Capped means the daily allowance binds this session: an account session, except a person with
280
+ // access to everything, whose jobs are never stamped for the cap (shared/scope.mjs authorize).
281
+ capped: scope?.kind === "account" && scope?.everything !== true,
272
282
  dailyRuns,
273
283
  monthlyRuns: caps?.monthlyRuns ?? null,
274
284
  maxQueued: caps?.maxQueued ?? null,
@@ -311,7 +321,8 @@ export function describeOptions(args = {}, { scope, now = Date.now() } = {}) {
311
321
  const accountsGranted = !account && Array.isArray(granted) && granted.length
312
322
  ? granted.map((k) => {
313
323
  let name = null;
314
- try { name = loadProfiles().get(k)?.name ?? null; } catch { /* roster unreadable ⇒ the key alone */ }
324
+ // a miss re-reads the store, as accountFor does — a company granted since the door started has a name
325
+ try { name = (loadProfiles().get(k) ?? loadProfiles({ force: true }).get(k))?.name ?? null; } catch { /* roster unreadable ⇒ the key alone */ }
315
326
  return { profileKey: k, name };
316
327
  })
317
328
  : null;
@@ -122,10 +122,11 @@ function allowanceFor(profile, { scope, now = Date.now() } = {}) {
122
122
  dailyRunsEffective: dailyRuns,
123
123
  monthlyRuns: caps?.monthlyRuns ?? null,
124
124
  maxQueued: caps?.maxQueued ?? null,
125
- // Only a CLIENT principal is capped — the same positive-only rule the runner applies (checkRunCaps
126
- // bites jobs stamped clientPrincipal:true, and only the account door stamps them). Staff previewing
127
- // for this customer see the counts and are not blocked by them.
128
- capped: scope?.kind === "account",
125
+ // Only a session whose jobs are stamped for the cap is capped — the same positive-only rule the runner
126
+ // applies (checkRunCaps bites jobs stamped clientPrincipal:true, and the account door stamps every job
127
+ // except a person's with access to everything). Anyone else previewing sees the counts and is not
128
+ // blocked by them.
129
+ capped: scope?.kind === "account" && scope?.everything !== true,
129
130
  };
130
131
  if (!usage?.complete) {
131
132
  return { ...shared, complete: false, today: null, thisMonth: null, queued: null, exhausted: false };
@@ -136,7 +137,7 @@ function allowanceFor(profile, { scope, now = Date.now() } = {}) {
136
137
  ...shared,
137
138
  complete: true,
138
139
  today: usage.today, thisMonth: usage.thisMonth, queued: usage.queued,
139
- exhausted: scope?.kind === "account" && usage.today >= dailyRuns,
140
+ exhausted: scope?.kind === "account" && scope?.everything !== true && usage.today >= dailyRuns,
140
141
  };
141
142
  }
142
143
 
@@ -289,7 +290,7 @@ export function planRun(args = {}, { scope, now = Date.now() } = {}) {
289
290
  // ── — AND WHAT THE REGISTER CANNOT REACH, on the door that commits ──────
290
291
  //
291
292
  // The same argument the coverage arm above makes, one rung further along. A worldwide search is
292
- // ORDERABLE on a partial register now (owner ruling 2026-08-31), so it stops being a blocker and
293
+ // ORDERABLE on a partial register now (ruling 2026-08-31), so it stops being a blocker and
293
294
  // becomes something a requester has to be TOLD before they confirm. The portal says it twice — at
294
295
  // the point of choosing and again in the review step — and `describe_options` says it on the menu.
295
296
  // Without it here, an assistant can walk a client through the one door that spends and never
@@ -190,3 +190,13 @@ export function runAccountKey(run) {
190
190
  return p.profileKey ?? p.key ?? null;
191
191
  } catch { return null; }
192
192
  }
193
+
194
+ // A Generic run's organisation, from the same frozen sidecar. Null for a company's run, for one filed
195
+ // before organisations existed, and for an unreadable sidecar — each of which the account gate treats as
196
+ // visible only to a full-grant session.
197
+ export function runOrganisation(run) {
198
+ try {
199
+ const p = JSON.parse(readFileSync(driverDir(run.runDir, "profile.json"), "utf8"));
200
+ return typeof p.organisation === "string" && p.organisation ? p.organisation : null;
201
+ } catch { return null; }
202
+ }
@@ -80,7 +80,7 @@ function completeness(stage) {
80
80
  /** whatIfPlan — pure dry-run. run = resolved Run ({runId, slug, codename, agent, runDir, P, status, location}). */
81
81
  export function whatIfPlan({ run, stage, axis = null, instructions = null, model = null, kind = "stage" }) {
82
82
  if (!run) throw new Error("whatIfPlan: run is required");
83
- // ── PLANNING A MEMO (tracker issue 132) ────────────────────────────────────────────────────────
83
+ // ── PLANNING A MEMO ────────────────────────────────────────────────────────────────────────────
84
84
  //
85
85
  // Without this branch the memo capability is COMPOSED AND UNREACHABLE. whatif-memo.mjs composes one,
86
86
  // whatIfRefusal already admits `kind: "memo"` on a finished run, and decodeOp already validates a memo
@@ -179,7 +179,7 @@ export function decodeOp(confirmationToken, what = "whatIfRun") {
179
179
  }
180
180
 
181
181
  /**
182
- * whatIfEnqueue — the CLIENT path (owner ruling 2026-08-27). Queues the op for the worker instead of
182
+ * whatIfEnqueue — the CLIENT path (ruling 2026-08-27). Queues the op for the worker instead of
183
183
  * running it, because the remote surfaces never spawn the engine and this module's own lazy import of
184
184
  * driver/pipeline.mjs is what keeps that true. Nothing below reaches runExperiment.
185
185
  *
@@ -220,7 +220,7 @@ export async function whatIfRun({ confirmationToken } = {}, deps = {}) {
220
220
  const run = resolveRun(runId);
221
221
  if (!run) throw new Error(`whatIfRun: run "${runId}" not found.`);
222
222
 
223
- // ── A MEMO IS NOT A STAGE RE-RUN, AND THIS IS THE DOOR IT WAS MISSING (tracker issue 132) ────────
223
+ // ── A MEMO IS NOT A STAGE RE-RUN, AND THIS IS THE DOOR IT WAS MISSING ────────────────────────────
224
224
  //
225
225
  // Every piece of the memo capability existed and nothing reached it. whatIfPlan mints the token,
226
226
  // decodeOp validates it on its own terms, whatIfEnqueue queues it and answers the client
@@ -230,7 +230,7 @@ export async function whatIfRun({ confirmationToken } = {}, deps = {}) {
230
230
  // 1. askArchivedRun (driver/whatif-memo-run.mjs, 323 lines with its own arms) was imported by
231
231
  // nothing but its own test. Composed and completely unreachable.
232
232
  // 2. The refusal below was called WITHOUT `kind`, so it defaulted to "stage" and refused every
233
- // memo with "what-if runs on live runs only" — the exact sentence tracker issue 132 was filed
233
+ // memo with "what-if runs on live runs only" — the exact sentence the fix was written
234
234
  // to delete. whatIfEnqueue passes the kind (see its own note); this door did not.
235
235
  //
236
236
  // So a memo was accepted at the front door, PROMISED to the client, and killed in the worker where
@@ -273,7 +273,7 @@ export async function whatIfRun({ confirmationToken } = {}, deps = {}) {
273
273
  if (deriveSlug(job) !== run.slug)
274
274
  throw new Error(`whatIfRun: cannot reconstruct the job for ${runId} (derived slug "${deriveSlug(job)}" != "${run.slug}"). status.json lacks the original ref/markName.`);
275
275
 
276
- // ── THE RATING AUTHORITY TRAVELS WITH THE JOB (tracker issue 135) ──────────────────────────────
276
+ // ── THE RATING AUTHORITY TRAVELS WITH THE JOB ──────────────────────────────────────────────────
277
277
  //
278
278
  // The reconstruction above carries six fields and resolveProfile keys on none of them. It reads
279
279
  // `job.profileKey` first, then falls back to `job.forwarderDomain`; the job has `forwarder` but not
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.2.4",
3
+ "version": "0.3.0-beta.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
@@ -7,7 +7,7 @@ the per-host connection recipes, and worked example prompts. Three audiences, th
7
7
  | Pack | Audience | Surface | Token |
8
8
  |---|---|---|---|
9
9
  | `client/` | report recipients (client legal teams) | plain-language read layer: `brief`, `list_findings`, `read_artifact` (the report — the only artifact a report link may read), one run | the run-scoped link embedded in their report ("Ask your AI") |
10
- | `account/` | a customer's own assistant, across all of that customer's searches | the client layer, the evidence reads (`list_evidence`, `list_searches`, `get_search_coverage`), the audit chain (`read_artifact` over the chain artifacts, `list_findings` raw, `get_finding`, `get_run`, `trace`, `decision_timeline` — owner ruling 2026-08-27), what-if as a QUEUED sandbox job (`what_if_plan`, `what_if_run`, `what_if_result`) and their own run lifecycle (`describe_options`, `plan_run`, `start_run`, `stop_run`) | their sign-in on the client surface, or an account API key (`mint-token.mjs --scope account`) — both refused unless that surface is started with `CLIENT_MCP_ACCOUNT_ACCESS=1` |
10
+ | `account/` | a customer's own assistant, across all of that customer's searches | the client layer, the evidence reads (`list_evidence`, `list_searches`, `get_search_coverage`), the audit chain (`read_artifact` over the chain artifacts, `list_findings` raw, `get_finding`, `get_run`, `trace`, `decision_timeline` — ruling 2026-08-27), what-if as a QUEUED sandbox job (`what_if_plan`, `what_if_run`, `what_if_result`) and their own run lifecycle (`describe_options`, `plan_run`, `start_run`, `stop_run`) | their sign-in on the client surface, or an account API key (`mint-token.mjs --scope account`) — both refused unless that surface is started with `CLIENT_MCP_ACCOUNT_ACCESS=1` |
11
11
  | `ops/` | integrator/operator agents that run searches and courier deliveries | `start_run` / `feed_context` / `stop_run`, the outbox courier verbs, triage reads | a verb-scoped ops token (`mint-token.mjs`, docs/architecture/06-operations-runbook.md) |
12
12
 
13
13
  ## Where the prompt text lives
@@ -51,7 +51,7 @@ RestartSec=10
51
51
  # Hardening: this process only reads run-dirs + serves HTTP on loopback (mirrors client-mcp.service).
52
52
  NoNewPrivileges=true
53
53
  ProtectSystem=strict
54
- # tracker issue 774 — the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
54
+ # the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
55
55
  # by existence (providers/_shared/ledger-path.mjs, mcp-server/lib/audit.mjs): a box that already has
56
56
  # a ledger or an access log under the old path keeps appending to it and needs it writable, while a
57
57
  # fresh box writes to the new one. `-` on both so a missing directory does not fail the unit to
@@ -30,7 +30,7 @@ Environment=CLIENT_MCP_HTTP_HOST=127.0.0.1
30
30
  # to avoid a port collision. The CF Tunnel route for clients-mcp.example.com must point at this port.
31
31
  #
32
32
  # ── AND ON A BOX THAT ALSO RUNS A TEST OR DEV INSTANCE, THE SECOND INSTANCE MUST NOT TAKE THIS. ────
33
- # Measured 2026-08-18 (tracker issue 1147): the deployment box had 18811 and 18812 held by production client faces
33
+ # Measured 2026-08-18: the deployment box had 18811 and 18812 held by production client faces
34
34
  # and no client face for the test instance at all. `http-server-client.mjs` DEFAULTS to 18811, so a
35
35
  # second instance started without an explicit port does not merely collide — on a day production is
36
36
  # down it SUCCEEDS, binds the port production is about to want, and a CLIENT surface ends up pointed at
@@ -59,7 +59,7 @@ RestartSec=10
59
59
  # Hardening: this process only reads run-dirs + serves HTTP on loopback (mirrors trademark-artifacts-http).
60
60
  NoNewPrivileges=true
61
61
  ProtectSystem=strict
62
- # tracker issue 774 — the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
62
+ # the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
63
63
  # by existence (providers/_shared/ledger-path.mjs, mcp-server/lib/audit.mjs): a box that already has
64
64
  # a ledger or an access log under the old path keeps appending to it and needs it writable, while a
65
65
  # fresh box writes to the new one. `-` on both so a missing directory does not fail the unit to
@@ -34,7 +34,7 @@ RestartSec=10
34
34
  # Hardening: this process only reads run-dirs + serves HTTP on loopback.
35
35
  NoNewPrivileges=true
36
36
  ProtectSystem=strict
37
- # tracker issue 774 — the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
37
+ # the telemetry default moved to %h/trademark/telemetry. BOTH are listed because resolution is
38
38
  # by existence (providers/_shared/ledger-path.mjs, mcp-server/lib/audit.mjs): a box that already has
39
39
  # a ledger or an access log under the old path keeps appending to it and needs it writable, while a
40
40
  # fresh box writes to the new one. `-` on both so a missing directory does not fail the unit to
@@ -30,7 +30,7 @@ import { join, basename } from "node:path";
30
30
  import { driverDir } from "../shared/driver-dir.mjs"; //
31
31
  import { fileURLToPath } from "node:url";
32
32
 
33
- import { enumerateRuns, resolveRun, runAccountKey } from "./lib/runs.mjs";
33
+ import { enumerateRuns, resolveRun, runAccountKey, runOrganisation } from "./lib/runs.mjs";
34
34
  import { ORDERABLE_PRODUCTS } from "../driver/search-policy.mjs";
35
35
  import { PRODUCTS } from "../driver/products.mjs";
36
36
 
@@ -48,7 +48,7 @@ import { readEvents, projectTimeline } from "./lib/events.mjs";
48
48
  import { tokenize, scoreLine } from "./lib/lexsearch.mjs";
49
49
  import { artifactToStage, listArtifactVersions, assertDiffRefsSafe } from "./lib/artifacts.mjs";
50
50
  import { authorize, visibleTools, USER_ARTIFACTS, ACCOUNT_ARTIFACTS, accountMayReadArtifact, assertAccountAccess, accountVisible, TOOL_SCOPES, readOnlyFor } from "./lib/scope.mjs";
51
- // The AUDIT-CHAIN projections (owner ruling 2026-08-27). Imported eagerly: it pulls only scrub.mjs, which
51
+ // The AUDIT-CHAIN projections (ruling 2026-08-27). Imported eagerly: it pulls only scrub.mjs, which
52
52
  // this file already loads, so there is nothing here for a lazy import to save.
53
53
  import { accountRun, accountTrace, accountTimeline, accountFinding, accountFindingList,
54
54
  accountWhatIfPlan, accountWhatIfQueued, accountWhatIfResult, CLIENT_FAILURE_NOTE as clientFailureNote } from "./lib/audit-view.mjs";
@@ -56,7 +56,7 @@ import { scrubMarkdown, scrubBody, scrubFrontMatter, scrubCards } from "./lib/sc
56
56
  import { evidenceRecords, searchLog, coverageStatement } from "./lib/evidence.mjs";
57
57
  // The knockout lane's projections. Every audit tool below branches on `isKnockoutRun` because the
58
58
  // clearance projections read artifacts this product does not write, and returned empty rather than saying
59
- // so (tracker issue 275).
59
+ // so.
60
60
  import {
61
61
  isKnockoutRun, knockoutDoc, knockoutArtifacts, knockoutArtifactPath, knockoutFindings,
62
62
  knockoutEvidence, knockoutSearches, knockoutCoverage, traceKnockout, notProducedOnThisProduct,
@@ -89,7 +89,7 @@ function mustRun(runId) {
89
89
  function artifactPath(run, name) {
90
90
  const { P, runDir } = run;
91
91
  if (name === "status.json") return join(runDir, "status.json");
92
- // THE KNOCKOUT TABLE IS TERMINAL ON A KNOCKOUT, and both halves of that matter (tracker issue 275).
92
+ // THE KNOCKOUT TABLE IS TERMINAL ON A KNOCKOUT, and both halves of that matter.
93
93
  //
94
94
  // Resolving FIRST is what fixes `report`: a knockout's report.md is written to the POOL and never into
95
95
  // the run dir, so the clearance table returned the run dir's own `report` slot — a path that does not
@@ -214,9 +214,25 @@ const tools = {
214
214
  // Each customer carries its PROJECTS (engagements) so intake can resolve a projectKey too. A bad
215
215
  // project file must never blank the whole roster, so the project read is best-effort (its own loud failure
216
216
  // surfaces at run time via loadProjects in the driver).
217
+ //
218
+ // READ FRESH ON EVERY CALL. `loadProfiles()` and `loadProjects()` answer from module caches with no
219
+ // expiry, filled by this process's first read — the boot line's — so a company or project created in
220
+ // the portal after the door started was missing here until the door restarted, while start_run, which
221
+ // re-reads on a miss (driver/enqueue-schema.mjs), already accepted it. This is the list an assistant
222
+ // resolves a customer against: a company it cannot see is a company it cannot pick. One read of the
223
+ // roster, handed to the project walk, so the two cannot come from different moments.
224
+ //
225
+ // A COMPANY FILE THAT CANNOT BE READ must not take every other company off this list. Re-reading made
226
+ // that possible: a bad file added after the door started used to go unread, because the door answered
227
+ // from its boot read, and now it would fail every call until fixed. So a failed re-read keeps the
228
+ // roster this process last read, and the reply says so. With no earlier read there is nothing to keep,
229
+ // and the call fails as it always did.
230
+ let roster, storeError = null;
231
+ try { roster = loadProfiles({ force: true }); }
232
+ catch (e) { storeError = String(e?.message ?? e); roster = loadProfiles(); }
217
233
  let byCustomer = new Map();
218
234
  try {
219
- for (const [fq, ov] of loadProjects()) {
235
+ for (const [fq, ov] of loadProjects({ profiles: roster, force: true })) {
220
236
  // An ARCHIVED project is not offered for new work: this list is what the intake AI resolves a
221
237
  // projectKey against, so a name it cannot see is a name it cannot pick. Already-queued and
222
238
  // finished runs are untouched — a run freezes its effective profile at admission.
@@ -226,7 +242,7 @@ const tools = {
226
242
  byCustomer.set(ov.customerKey, list);
227
243
  }
228
244
  } catch { byCustomer = new Map(); }
229
- const clients = [...loadProfiles().values()]
245
+ const clients = [...roster.values()]
230
246
  .filter((p) => p.key !== "generic")
231
247
  .map((p) => ({ key: p.key, name: p.name, industry: p.industry ?? null,
232
248
  projects: (byCustomer.get(p.key) ?? []).sort((a, b) => a.key.localeCompare(b.key)) }))
@@ -235,6 +251,9 @@ const tools = {
235
251
  _note: "Customer roster for intake resolution. Resolve by JUDGMENT — an explicit name, a misspelling of one of the keys below, or an implicit reference (\"our functional-beverage client\") all map to a key. Set the job's profileKey to the chosen customer key; OMIT it for a new/unknown customer (⇒ the neutral generic profile). If the request names a specific PROJECT/engagement under that customer (listed in `projects[]`), also set projectKey to that project's key; OMIT projectKey when no project is meant (⇒ the customer profile). CLARIFY if you cannot tell either. Never pick a profile from the sender's email domain.",
236
252
  clients,
237
253
  genericFallback: "generic",
254
+ // Staff-only tool (TOOL_SCOPES), so the reason may name the file: no client door sees this reply.
255
+ ...(storeError ? { storeUnreadable: `The company store could not be re-read (${storeError}). This is the list as `
256
+ + "last read, so a company added or changed since may be missing until that file is fixed." } : {}),
238
257
  };
239
258
  },
240
259
  async describe_options(args, extra) {
@@ -482,7 +501,7 @@ const tools = {
482
501
  const run = mustRun(runId);
483
502
  const { whatIfPlan } = await import("./lib/whatif.mjs");
484
503
  // `kind` DEFAULTS TO "stage", so every existing caller is unchanged. A memo is the other kind
485
- // (tracker issue 132): a bounded re-read of a DELIVERED run's archived evidence under a stated
504
+ //: a bounded re-read of a DELIVERED run's archived evidence under a stated
486
505
  // assumption, which re-runs no stage and therefore cannot carry one.
487
506
  const asked = kind === "memo" ? "memo" : "stage";
488
507
  // A STAGE PLAN WITH NO STAGE IS A REFUSAL, NOT A THROW. `stage` cannot be schema-required any more:
@@ -575,7 +594,7 @@ const TOOL_DEFS = [
575
594
  { name: "plan_run", description: "FREE PREVIEW — resolve what a search WOULD do and return it, spending nothing and queueing nothing. Takes the SAME arguments as start_run. Use this BEFORE start_run whenever the requester has not already confirmed the specifics: it reports the PRODUCT that resolves and WHERE it came from (their request, the account default, a saved search, or the territories themselves), the territories and marketplaces that would ACTUALLY be searched, the turnaround, and any blockers as questions to put back. Show the result to the requester; then call start_run with the SAME arguments to actually run it. Nothing here reserves or holds anything.", inputSchema: { type: "object", properties: { markName: { type: "string", description: "The mark (single-mark form). REQUIRED unless marks[] is given." }, marks: { type: "array", items: { type: "object", properties: { name: { type: "string" }, classes: { type: "array", items: { type: "number" } }, ref: { type: "string" } }, required: ["name"] } }, classes: { type: "array", items: { type: "number" } }, goods: { type: "string" }, jurisdictions: { type: "array", items: { type: "string" } }, platforms: { type: "array", items: { type: "string" } }, customer: { type: "string" }, profileKey: { type: "string", description: "The customer ACCOUNT key (see list_profiles). REQUIRED for an accounts-scoped session (your grant names the keys — see describe_options)." }, projectKey: { type: "string" }, forwarder: { type: "string", description: "Requester/reply-routing key — same as start_run, so the preview describes the job that would actually be built. Account sessions may omit it — it is stamped from your verified identity." }, forwarderEmail: { type: "string" }, ref: { type: "string" }, product: { type: "string", enum: ORDERABLE_PRODUCTS, description: PRODUCT_DESC }, nativeLanguage: { type: "boolean", description: "OPTIONAL, and only on a multi-country-focus-search: the native-language investigation. Send TRUE or omit the field — FALSE IS REFUSED, not honoured: the toggle only ever added, so there is nothing for false to switch off, and accepting it would let you believe you had removed a reading that runs anyway. It runs AUTOMATICALLY on a full-country-search and is not part of the other two, so asking for it there is refused rather than accepted and ignored. It routes on territory — the scope must name one it covers." }, worldwide: { type: "boolean", description: "Search EVERYWHERE, and refuse to be narrowed by the account's own default territories. This is not the same as omitting jurisdictions, which means \"whatever the account says\" — the two used to be indistinguishable on this wire, which is how a request that bought everywhere ran an account's seven countries. Never send \"Worldwide\" as a jurisdictions entry." }, recipeKey: { type: "string" }, upfrontInstructions: { type: "string" }, deadline: { type: "string" }, deliveryRoute: { type: "string", enum: ["email", "portal"], description: "OPTIONAL: how the delivered packet leaves — email (default). \"portal\" is reserved for the portal delivery lane and CLARIFIES at admission until it ships (never a silent no-op) — so the preview BLOCKS it here rather than describing a job start_run could not run." }, commercialFlexibility: { type: "string" }, priorUse: { type: "string" }, campaignShape: { type: "string", description: "Campaign-shape FACTS from the request/client — how the mark will be deployed: standalone brand vs flavour/sub-brand under a NAMED house mark; seasonal/limited vs permanent; launch scale. Verbatim facts only — the engine records them on the matter frame instead of inferring a launch shape." }, customerUnknown: { type: "boolean" } }, required: ["forwarder"] } },
576
595
  // ---- OPS write-face (requires an ops token; never exposed to a user/internal session) ----
577
596
  { name: "start_run", description: "OPS-ONLY. Enqueue a NEW clearance/search run the driver will pick up. Provide markName (or marks[] for a batch) + forwarder (and ideally classes/goods/customer/profileKey/ref). Returns the queue id + slug; the runId/codename is assigned when the runner claims it — poll list_runs (by markName) or run_changes for progress. Spends real money once it runs.", inputSchema: { type: "object", properties: { agent: { type: "string", description: "Which agent workspace's queue. Omit for the deployment default (CLEAROTRON_QUEUE_DIR or the default agent's queue — docs/INTAKE.md)." }, markName: { type: "string", description: "The mark (single-mark form). Required unless marks[] is provided." }, marks: { type: "array", items: { type: "object", properties: { name: { type: "string" }, classes: { type: "array", items: { type: "number" } }, ref: { type: "string" } }, required: ["name"] }, description: "Batch form: EVERY name of the batch in ONE job. Only a knockout-search reads more than one name at a time; a clearance reads one, and more is refused rather than truncated." }, classes: { type: "array", items: { type: "number" }, description: "Nice classes — whole numbers 1–45 (1–34 goods, 35–45 services). classes OR goods; either suffices." }, goods: { type: "string" }, jurisdictions: { type: "array", items: { type: "string" }, description: "WHERE the search points — and, for a clearance, WHICH search it is: one country is a full-country-search, a region or two-or-more countries a multi-country-focus-search. AUTHORITATIVE (the matter frame is told not to widen past them). Omit ⇒ the project/customer default territories; send worldwide:true to search everywhere instead. Names or codes both read; max 20." }, platforms: { type: "array", items: { type: "string" }, description: "OPTIONAL per-run SCOPE: extra marketplaces to sweep, as bare store domains e.g. [\"gnc.com\"]. ADDED to the account's own marketplaces — additive only, never a replacement (a client's platform list is a mandate). Max 10; \"web\" is implicit and must not be listed." }, customer: { type: "string" }, profileKey: { type: "string", description: "The customer ACCOUNT key (see list_profiles) whose profile rates this run; omit for the neutral generic profile. REQUIRED for an accounts-scoped session (your grant names the keys — see describe_options)." }, forwarder: { type: "string", description: "Requester/reply-routing key — rides the delivery packet so the integrator knows who gets the report (docs/DELIVERY.md). Account sessions may omit it — it is stamped from your verified identity." }, forwarderEmail: { type: "string" }, ref: { type: "string" }, provider: { type: "string" }, brief: { type: "string" }, upfrontInstructions: { type: "string" }, projectKey: { type: "string", description: "OPTIONAL (spec 62): the PROJECT/engagement key UNDER profileKey (see that customer's projects[] in list_profiles) whose overlay rates this run; omit to run on the customer profile. An unknown key clarifies at intake." }, product: { type: "string", enum: ORDERABLE_PRODUCTS, description: PRODUCT_DESC }, nativeLanguage: { type: "boolean", description: "OPTIONAL, and only on a multi-country-focus-search: the native-language investigation. Send TRUE or omit the field — FALSE IS REFUSED, not honoured: the toggle only ever added, so there is nothing for false to switch off, and accepting it would let you believe you had removed a reading that runs anyway. It runs AUTOMATICALLY on a full-country-search and is not part of the other two, so asking for it there is refused rather than accepted and ignored. It routes on territory — the scope must name one it covers." }, worldwide: { type: "boolean", description: "Search EVERYWHERE, and refuse to be narrowed by the account's own default territories. This is not the same as omitting jurisdictions, which means \"whatever the account says\" — the two used to be indistinguishable on this wire, which is how a request that bought everywhere ran an account's seven countries. Never send \"Worldwide\" as a jurisdictions entry." }, recipeKey: { type: "string", description: "OPTIONAL alternative selector: a saved search (recipe) slug for this customer. Mutually exclusive with product — a saved search already carries one." }, deliveryRoute: { type: "string", enum: ["email", "portal"], description: "OPTIONAL: how the delivered packet leaves — email (default). \"portal\" is reserved for the portal delivery lane and CLARIFIES at admission until it ships (never a silent no-op)." }, parentRunId: { type: "string", description: "OPTIONAL escalation lineage: the runId this run escalates from (e.g. a knockout HIGH mark → this clearotron)." }, deliverableSpec: { type: "string" }, commercialFlexibility: { type: "string" }, priorUse: { type: "string" }, campaignShape: { type: "string", description: "Campaign-shape FACTS from the request/client — how the mark will be deployed: standalone brand vs flavour/sub-brand under a NAMED house mark; seasonal/limited vs permanent; launch scale. Verbatim facts only — the engine records them on the matter frame instead of inferring a launch shape." }, deadline: { type: "string", description: "ISO-8601 — drives the deadline envelope." }, customerUnknown: { type: "boolean" }, dupOverride: { type: "boolean", description: "Requester-confirmed force-run past matter dedup." }, clientPrincipal: { type: "boolean", description: "Set by the client portal only: this run was started by a CLIENT and consumes that account's runCaps.dailyRuns allowance. Omit for staff, agent and email-door runs — absence means uncapped, and setting it can only restrict the run that carries it." }, rawRequest: { type: "string" }, forwarderDomain: { type: "string" }, msgId: { type: "string" }, conversationId: { type: "string" }, id: { type: "string" } }, required: ["forwarder"] } },
578
- { name: "stop_run", description: "OPS-ONLY. Cancel a run. Pass id to remove a not-yet-claimed queued job (a real cancel, no spend). Pass runId to file a cancel request on a started run. By default the cancel is COOPERATIVE — the sentinel is written and a turn already in flight is allowed to finish, which has no deadline: a reasoning turn can run for tens of minutes. Add immediate:true to also end that turn, so the run goes terminal in seconds.", inputSchema: { type: "object", properties: { agent: { type: "string" }, id: { type: "string", description: "A queued job id (from start_run) — dequeues before the runner claims it." }, runId: { type: "string", description: "A started run's runId — files a cancel sentinel." }, immediate: { type: "boolean", description: "Stop NOW instead of at the next step boundary (tracker issue 2076). The cancel sentinel is written FIRST and then the run's own recorded engine turn is ended, in that order — the sentinel is what makes the record clean, so the terminal names the stage, the actor and the request time instead of leaving a run that reads as still running. The step in flight is lost; everything already recorded is kept. Falls back to the boundary stop, and SAYS SO in the answer, when there is no live turn to end — a run between steps, or one whose turn has already exited. Never send this expecting a guaranteed instant stop: the answer states which of the two actually happened."}, onBehalfOf: { type: "string", description: "The identifier of a human this caller has itself verified (#1378). RECORDED BESIDE the token's own principal, never instead of it — a stop by the portal for alice@x is written `portal:alice@x`, so an asserted name is never mistaken for a proved one. Bare identifiers only; anything else is dropped." } } } },
597
+ { name: "stop_run", description: "OPS-ONLY. Cancel a run. Pass id to remove a not-yet-claimed queued job (a real cancel, no spend). Pass runId to file a cancel request on a started run. By default the cancel is COOPERATIVE — the sentinel is written and a turn already in flight is allowed to finish, which has no deadline: a reasoning turn can run for tens of minutes. Add immediate:true to also end that turn, so the run goes terminal in seconds.", inputSchema: { type: "object", properties: { agent: { type: "string" }, id: { type: "string", description: "A queued job id (from start_run) — dequeues before the runner claims it." }, runId: { type: "string", description: "A started run's runId — files a cancel sentinel." }, immediate: { type: "boolean", description: "Stop NOW instead of at the next step boundary. The cancel sentinel is written FIRST and then the run's own recorded engine turn is ended, in that order — the sentinel is what makes the record clean, so the terminal names the stage, the actor and the request time instead of leaving a run that reads as still running. The step in flight is lost; everything already recorded is kept. Falls back to the boundary stop, and SAYS SO in the answer, when there is no live turn to end — a run between steps, or one whose turn has already exited. Never send this expecting a guaranteed instant stop: the answer states which of the two actually happened."}, onBehalfOf: { type: "string", description: "The identifier of a human this caller has itself verified (#1378). RECORDED BESIDE the token's own principal, never instead of it — a stop by the portal for alice@x is written `portal:alice@x`, so an asserted name is never mistaken for a proved one. Bare identifiers only; anything else is dropped." } } } },
579
598
  { name: "server_info", description: "The AGPL §13 source offer: this server's name, version, licence, source repository and the COMMIT IT IS RUNNING. Callable by every session kind — the offer is owed to whoever interacts with the service. Reads no run and no account data.", inputSchema: { type: "object", properties: {} } },
580
599
  { name: "list_outbox_events", description: "Integrator discovery read (docs/DELIVERY.md): every pending outbox event — delivered markers plus the self-contained run-failed / intake-rejected / duplicate-skipped / late-bind-ack packets. Route each, then mark_sent (delivered) or ack_event (the rest).", inputSchema: { type: "object", properties: {} } },
581
600
  { name: "get_delivery_packet", description: "The run's send payload: _driver/delivery.json (subject, recipient routing, verbatim emailBodyHtml, whatsapp line) and/or failure.json, plus sendPending/.sent state. NOT exposed to client tokens.", inputSchema: { type: "object", properties: { runId: { type: "string" } }, required: ["runId"] } },
@@ -712,7 +731,7 @@ export function describeForAudience(def, kind) {
712
731
  }
713
732
 
714
733
  /**
715
- * Attach MCP tool annotations — tracker issue 148.
734
+ * Attach MCP tool annotations.
716
735
  *
717
736
  * No tool declared any, so a client could not tell `brief` from `start_run` and asked before every
718
737
  * call. `readOnlyHint` is DERIVED from the scope table's `write` flag (`readOnlyFor`) rather than
@@ -757,7 +776,7 @@ export function attachHandlers(server, { scope = { kind: "ops", runId: null }, l
757
776
  if (Array.isArray(scope?.accounts) && authedArgs?.runId != null) {
758
777
  try {
759
778
  const run = resolveRun(String(authedArgs.runId));
760
- if (run) assertAccountAccess(scope, runAccountKey(run), `run "${authedArgs.runId}"`);
779
+ if (run) assertAccountAccess(scope, runAccountKey(run), `run "${authedArgs.runId}"`, runOrganisation(run));
761
780
  } catch (e) {
762
781
  log(`account deny ${name} [${scope.sub ?? scope.kind}]: ${e.message}`);
763
782
  return { isError: true, content: [{ type: "text", text: `FORBIDDEN (${name}): ${e.message}` }] };
@@ -789,13 +808,13 @@ export function attachHandlers(server, { scope = { kind: "ops", runId: null }, l
789
808
  const runs = scope.kind === "user"
790
809
  ? (() => { const r = scope.runId && resolveRun(scope.runId); return r ? [r] : []; })()
791
810
  : enumerateRuns()
792
- .filter((r) => !Array.isArray(scope?.accounts) || accountVisible(scope, runAccountKey(r)))
811
+ .filter((r) => !Array.isArray(scope?.accounts) || accountVisible(scope, runAccountKey(r), runOrganisation(r)))
793
812
  .slice(0, 8).map((r) => ({ ...r, P: paths(r.runDir) }));
794
813
  // A user (report-link) token sees ONLY the report (one report — never a second version by another
795
814
  // name), never the internal KEY_ARTIFACTS (narrative/audit/run.jsonl/…) — the same gate authorize()
796
815
  // puts on read_artifact.
797
816
  //
798
- // AND THE TWO CLIENT KINDS NO LONGER AGREE (owner ruling 2026-08-27). They did while both read only
817
+ // AND THE TWO CLIENT KINDS NO LONGER AGREE (ruling 2026-08-27). They did while both read only
799
818
  // the report; the account layer now reads the audit chain and the report link still does not. This
800
819
  // is keyed on the KIND rather than on CLIENT_KINDS for exactly that reason — leaving it collapsed
801
820
  // would have made the tool surface serve an account the audit chain while this door went on sealing
@@ -830,7 +849,7 @@ export function attachHandlers(server, { scope = { kind: "ops", runId: null }, l
830
849
  throw new Error("forbidden: a client may only read the report");
831
850
  const run = resolveRun(reqRunId);
832
851
  if (!run) throw new Error(`run not found: ${m[1]}`);
833
- if (Array.isArray(scope?.accounts)) assertAccountAccess(scope, runAccountKey(run), `run "${reqRunId}"`);
852
+ if (Array.isArray(scope?.accounts)) assertAccountAccess(scope, runAccountKey(run), `run "${reqRunId}"`, runOrganisation(run));
834
853
  const path = artifactPath(run, reqArtifact);
835
854
  if (!path || !existsSync(path)) throw new Error(`artifact not found: ${m[2]}`);
836
855
  // CLIENT VIEW ( R1): the Resources surface is a second door to the same bytes — it applies the
@@ -875,7 +894,7 @@ export function presentForPrincipal(scope, name, result) {
875
894
  const declared = TOOL_SCOPES[name]?.present ?? null;
876
895
  if (declared === null) throw new UndeclaredPresentation(name);
877
896
 
878
- // `project` — THE AUDIT CHAIN (owner ruling 2026-08-27). lib/audit-view.mjs holds the allowlist over
897
+ // `project` — THE AUDIT CHAIN (ruling 2026-08-27). lib/audit-view.mjs holds the allowlist over
879
898
  // each result's structure and the prose transform; nothing about what travels is decided here.
880
899
  //
881
900
  // The four tools are accountSafe and NOT clientSafe, so a `user` (report-link) token never reaches this
@@ -949,32 +968,27 @@ export function filterByAccounts(scope, name, result) {
949
968
  // one status-scan per call builds runId → account for the array filters
950
969
  const accountOf = () => {
951
970
  const map = new Map();
952
- for (const r of enumerateRuns()) map.set(r.runId, runAccountKey(r));
971
+ for (const r of enumerateRuns()) map.set(r.runId, { account: runAccountKey(r), organisation: runOrganisation(r) });
953
972
  return map;
954
973
  };
974
+ const seen = (map, id) => { const o = id != null ? map.get(id) : null; return o != null && accountVisible(scope, o.account, o.organisation); };
955
975
  if (name === "list_profiles" && result && Array.isArray(result.clients))
956
976
  return { ...result, clients: result.clients.filter((c) => scope.accounts.includes(c.key)) };
957
977
  if ((name === "list_runs" || name === "search_runs") && Array.isArray(result)) {
958
978
  const map = accountOf();
959
- return result.filter((r) => {
960
- const id = r.runId ?? r.id ?? null;
961
- return id != null && accountVisible(scope, map.get(id) ?? null);
962
- });
979
+ return result.filter((r) => seen(map, r.runId ?? r.id ?? null));
963
980
  }
964
981
  // search_runs answers an OBJECT ({query, mode, scope, runsScanned, hits, truncated}) — the array
965
982
  // guard above never matched it, so scoped sessions saw EVERY hit — a real cross-account content
966
983
  // leak. Filter the hits by their run's account like the array shapes.
967
984
  if (name === "search_runs" && result && Array.isArray(result.hits)) {
968
985
  const map = accountOf();
969
- const hits = result.hits.filter((h) => {
970
- const id = h.runId ?? h.id ?? null;
971
- return id != null && accountVisible(scope, map.get(id) ?? null);
972
- });
986
+ const hits = result.hits.filter((h) => seen(map, h.runId ?? h.id ?? null));
973
987
  return { ...result, hits, ...(typeof result.count === "number" ? { count: hits.length } : {}) };
974
988
  }
975
989
  if (name === "list_outbox_events" && result && Array.isArray(result.events)) {
976
990
  const map = accountOf();
977
- const events = result.events.filter((ev) => ev.runId != null && accountVisible(scope, map.get(ev.runId) ?? null));
991
+ const events = result.events.filter((ev) => seen(map, ev.runId ?? null));
978
992
  return { ...result, events, count: events.length };
979
993
  }
980
994
  return result;