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
@@ -57,7 +57,7 @@
57
57
  //
58
58
  // ── AND WHEN EACH ONE IS ASKED FOR, WHICH IS A SECOND AXIS AND NOT THE SAME ONE ─────────────────────
59
59
  //
60
- // Owner ruling 2026-09-06, in session: "someone can install and select key later so it should still
60
+ // Ruling 2026-09-06, in session: "someone can install and select key later so it should still
61
61
  // start." So a hosted install comes up with no register configured — the doors answer, the portal
62
62
  // answers, and the box is a working install waiting for one value.
63
63
  //
@@ -186,13 +186,26 @@ export function missingRequirements(env = {}, tables = {}) {
186
186
  *
187
187
  * Returns null when nothing blocks, so a caller cannot mistake "configured" for "could not look".
188
188
  */
189
- export function orderTimeRefusal(env = {}, tables = {}, { envFile = null } = {}) {
189
+ export function orderTimeRefusal(env = {}, tables = {}, { envFile = null, readFile = null } = {}) {
190
190
  const missing = missingRequirements(env, tables).atOrder;
191
191
  if (!missing.length) return null;
192
192
  const names = missing.map((r) => r.name);
193
- const where = envFile
194
- ? ` Set them in ${envFile} and restart, or run \`clearotron install\` in a terminal, which writes them for you.`
195
- : " Run `clearotron install` in a terminal to configure them, or set them in the file this install's units read.";
193
+ // EVERY FILE THAT REACHES THIS RUN, EACH NAMED BY WHAT READS IT — the rule `bin/start.mjs` already
194
+ // follows, for the reason it gives: two copies of this sentence is how one of them comes to name a file
195
+ // the reader cannot use. This copy named only `envFile`, the file background units read. A runner
196
+ // started from a terminal on a box with no units reads the install's own file instead, so its operator
197
+ // was told to create a file nothing on that box reads — and the one actually read went unnamed.
198
+ // `readFile` is the file THIS process loaded at start; it is null under a unit, where the unit's own
199
+ // EnvironmentFile is the only configuration and `envFile` alone is the true answer.
200
+ const cmd = "`clearotron install` in a terminal, which writes them for you";
201
+ const both = readFile && envFile && readFile !== envFile;
202
+ const one = readFile || envFile;
203
+ const where = both
204
+ ? ` Set them in either of these — both reach a run:\n ${readFile} — the file this runner read when it started\n`
205
+ + ` ${envFile} — the file background units read\n then restart, or run ${cmd}.`
206
+ : one
207
+ ? ` Set them in ${one} and restart, or run ${cmd}.`
208
+ : " Run `clearotron install` in a terminal to configure them, or set them in the file this install's units read.";
196
209
  return {
197
210
  names,
198
211
  operator: `this installation cannot run a search yet — it is installed but not configured:\n`
package/driver/runner.mjs CHANGED
@@ -29,7 +29,7 @@ import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs"; // —
29
29
  import { isLiveQueueMarker, PROSE_PARTS, CLAIM_SIDECAR_SUFFIXES, TERMINAL_QUEUE_SUFFIXES } from "./queue-markers.mjs";
30
30
  import { matterLedgerPath } from "./usage-ledger.mjs"; // ONE ledger-path calculation, shared with the portal pre-check
31
31
  import { orderTimeRefusal } from "./run-requirements.mjs"; // one authority for what a run needs, and when it is asked for
32
- import { unitEnvPath } from "../shared/env-local.mjs"; // the file the units read, named by its one author
32
+ import { unitEnvPath, envFileRead } from "../shared/env-local.mjs"; // the file the units read, named by its one author
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { config, preflightDeploymentUrls } from "./driver.config.mjs";
35
35
  import { deriveSlug, todayISO, mintFreshCodename } from "./phase0.mjs";
@@ -237,10 +237,15 @@ function recordMatter(qdir, entry) {
237
237
  // any account that needs a different number says so in its own runCaps block.
238
238
  export const DEFAULT_CLIENT_DAILY_RUNS = 20;
239
239
 
240
- export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, now = Date.now(), clientRun = false }) {
241
- // `generic` is the neutral no-customer profile and stays exempt: it is not a brand owner, and capping it
242
- // would put a ceiling on staff/email work that was never client-attributed.
243
- if (!account || account === "generic") return null;
240
+ export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, now = Date.now(), clientRun = false, organisation = null }) {
241
+ // GENERIC IS CAPPED LIKE ANY COMPANY, one lane per organisation (ruling 2026-09-10: "every
242
+ // organisation's Generic lane carries the daily cap; the exemption goes"). A Generic run counts in the
243
+ // lane of the organisation it was filed under and nowhere else, so one organisation's day cannot use up
244
+ // another's. What stays uncapped is what was never client-attributed — a job with no `clientRun` stamp:
245
+ // a person with access to everything, the email door, the CLI — exactly as for a company.
246
+ if (!account) return null;
247
+ const inLane = (key, org) => (key ?? null) === account && (account !== "generic" || (org ?? null) === organisation);
248
+ const lane = account === "generic" ? (organisation ? `Generic for ${organisation}` : "Generic") : account;
244
249
  // The default applies to the DAILY allowance only. maxQueued/monthlyRuns stay opt-in per profile.
245
250
  const eff = Number.isInteger(caps?.dailyRuns) ? caps : { ...(caps ?? {}), dailyRuns: DEFAULT_CLIENT_DAILY_RUNS };
246
251
  caps = eff;
@@ -252,7 +257,7 @@ export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, no
252
257
  let files = [];
253
258
  try { files = readdirSync(qd).filter(isLiveQueueMarker); } catch { continue; }
254
259
  for (const f of files) {
255
- try { if ((JSON.parse(readFileSync(join(qd, f), "utf8")).profileKey ?? null) === account) queued++; }
260
+ try { const j = JSON.parse(readFileSync(join(qd, f), "utf8")); if (inLane(j.profileKey, j.tenant)) queued++; }
256
261
  catch { /* mid-rename / prose sidecar — skip */ }
257
262
  }
258
263
  }
@@ -261,7 +266,7 @@ export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, no
261
266
  // the uniform +1 admitted untagged jobs one over the cap).
262
267
  const inFlight = queued + (inHandTagged ? 0 : 1);
263
268
  if (inFlight > caps.maxQueued) {
264
- return `admission cap: ${account} already has ${inFlight - 1} run(s) queued/in-flight (runCaps.maxQueued=${caps.maxQueued}) — wait for the backlog to drain or cancel one (stop_run), then re-send`;
269
+ return `admission cap: ${lane} already has ${inFlight - 1} run(s) queued/in-flight (runCaps.maxQueued=${caps.maxQueued}) — wait for the backlog to drain or cancel one (stop_run), then re-send`;
265
270
  }
266
271
  }
267
272
  // dailyRuns — the beta allowance, and the ONLY cap that counts a subset of runs rather than all of
@@ -292,23 +297,23 @@ export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, no
292
297
  // So a client whose runs keep failing is bounded by the MONTHLY cap, not the daily one — which
293
298
  // is why a monthlyRuns value is worth setting on any account this matters for.
294
299
  if (e.failed === true) continue;
295
- if (e.profileKey === account && e.clientPrincipal === true
300
+ if (inLane(e.profileKey, e.organisation) && e.clientPrincipal === true
296
301
  && typeof e.ts === "number" && new Date(e.ts).toISOString().slice(0, 10) === dayKey) today++;
297
302
  }
298
303
  }
299
304
  if (today + 1 > caps.dailyRuns) {
300
- return `daily allowance: ${account} has started ${today} search(es) today (runCaps.dailyRuns=${caps.dailyRuns}) — the allowance resets at midnight UTC, or ask your ${BRAND.name} contact to run this one for you`;
305
+ return `daily allowance: ${lane} has started ${today} search(es) today (runCaps.dailyRuns=${caps.dailyRuns}) — the allowance resets at midnight UTC, or ask your ${BRAND.name} contact to run this one for you`;
301
306
  }
302
307
  }
303
308
  if (Number.isInteger(caps.monthlyRuns)) {
304
309
  let month = 0;
305
310
  for (const qd of queueDirs) {
306
311
  for (const e of readMatterLedger(qd)) {
307
- if (e.profileKey === account && typeof e.ts === "number" && new Date(e.ts).toISOString().slice(0, 7) === monthKey) month++;
312
+ if (inLane(e.profileKey, e.organisation) && typeof e.ts === "number" && new Date(e.ts).toISOString().slice(0, 7) === monthKey) month++;
308
313
  }
309
314
  }
310
315
  if (month + 1 > caps.monthlyRuns) {
311
- return `admission cap: ${account} has started ${month} run(s) this month (runCaps.monthlyRuns=${caps.monthlyRuns}) — the cap resets at month end; raise it in the customer profile if this is intended growth`;
316
+ return `admission cap: ${lane} has started ${month} run(s) this month (runCaps.monthlyRuns=${caps.monthlyRuns}) — the cap resets at month end; raise it in the customer profile if this is intended growth`;
312
317
  }
313
318
  }
314
319
  return null;
@@ -742,7 +747,7 @@ async function backstopFailureNotice({ res, job, agentId, base, codename, studio
742
747
  // parked/failed-at-intake, or the claim was lost.
743
748
  // ── THE ORDER-TIME REFUSAL, AND WHY IT IS HERE ──────────────────────────────────
744
749
  //
745
- // A hosted install now STARTS with no register configured (owner ruling 2026-09-06: "someone can install
750
+ // A hosted install now STARTS with no register configured (ruling 2026-09-06: "someone can install
746
751
  // and select key later so it should still start"). The protection that used to live in
747
752
  // `clearotron start --background` moves here — it does not go away.
748
753
  //
@@ -851,7 +856,7 @@ async function claimAndPrep(jsonFile, qdir, agentId) {
851
856
  }
852
857
  // ── IS THIS BOX CONFIGURED TO SEARCH AT ALL? See the header above claimAndPrep.
853
858
  {
854
- const refusal = orderTimeRefusal(process.env, await runTables(), { envFile: unitEnvPath() });
859
+ const refusal = orderTimeRefusal(process.env, await runTables(), { envFile: unitEnvPath(), readFile: envFileRead() });
855
860
  if (refusal) {
856
861
  note(`[runner] ${base} REFUSED at order time — this install is not configured to run a search: ${refusal.names.join(", ")}`);
857
862
  await failAtIntake(procPath, qdir, base, agentId, job,
@@ -977,7 +982,8 @@ async function claimAndPrep(jsonFile, qdir, agentId) {
977
982
  // clarifies before any spend and before the matter is recorded.
978
983
  if (!effProfile) note(`[runner] ${base} run-caps skipped — profile resolution errored, caps not evaluable for this admission (fail-open by doctrine, logged for visibility)`);
979
984
  const capMsg = checkRunCaps({ account: effProfile?.key ?? null, caps: effProfile?.runCaps, queueDirs: config.queueDirs,
980
- inHandTagged: Boolean(job.profileKey), clientRun: job.clientPrincipal === true });
985
+ inHandTagged: Boolean(job.profileKey), clientRun: job.clientPrincipal === true,
986
+ organisation: typeof job.tenant === "string" && job.tenant ? job.tenant : null });
981
987
  if (capMsg) {
982
988
  note(`[runner] ${base} ${capMsg}`);
983
989
  await failAtIntake(procPath, qdir, base, agentId, job, { classify: "clarify", errors: [capMsg] }, [capMsg]);
@@ -1005,7 +1011,10 @@ async function claimAndPrep(jsonFile, qdir, agentId) {
1005
1011
  }
1006
1012
  }
1007
1013
  recordMatter(qdir, { sig, conversationId, msgId: job.msgId, id: job.id, ts: dupNow,
1008
- ...(effProfile?.key && effProfile.key !== "generic" ? { profileKey: effProfile.key } : {}),
1014
+ // Generic is tagged too, with the organisation whose lane it counts in. An untagged Generic row
1015
+ // counted towards nothing, which was the exemption the owner's 2026-09-10 ruling removed.
1016
+ ...(effProfile?.key ? { profileKey: effProfile.key } : {}),
1017
+ ...(effProfile?.key === "generic" && typeof job.tenant === "string" && job.tenant ? { organisation: job.tenant } : {}),
1009
1018
  // positive-only, and it is what tomorrow's daily count reads. A run that omits it is a staff,
1010
1019
  // email or CLI run and never consumes a client's allowance.
1011
1020
  ...(job.clientPrincipal === true ? { clientPrincipal: true } : {}),
@@ -263,8 +263,8 @@ export const PRODUCT_POLICIES = {
263
263
  // not run is a search that did not happen, and this row was the shortest of the three.
264
264
  //
265
265
  // WORLDWIDE (global-preliminary-search) IS DELIBERATELY LEFT AT 1 and that is now an inversion: the
266
- // broader product gets the weaker recall follow-up. The ruling named this product, and widening an
267
- // owner ruling to the most expensive product is not mine to do. Raised as a question rather than
266
+ // broader product gets the weaker recall follow-up. The ruling named this product, and widening a
267
+ // ruling to the most expensive product is not mine to do. Raised as a question rather than
268
268
  // assumed —.
269
269
  recallFollowupMax: 2,
270
270
  envelopeRounds: "as-today",
@@ -584,7 +584,7 @@ export function gateResolvedPolicy(resolved, { built = BUILT, registerTerritorie
584
584
  */
585
585
  export function productAvailability(policy, { built = BUILT, registerCanCount = null, registerTerritories = undefined, geography = null, demo = false } = {}) {
586
586
  if (!policy) return "unbuilt";
587
- // ── — A DEMO NO LONGER REFUSES A PRODUCT (owner ruling, 2026-08-31) ──────
587
+ // ── — A DEMO NO LONGER REFUSES A PRODUCT (ruling, 2026-08-31) ──────
588
588
  //
589
589
  // `demo` used to return FIRST, before every other cause, and greyed all four products with a sentence
590
590
  // about credentials. That implemented the ruling of 14:44 that day, which the owner superseded at
@@ -615,7 +615,7 @@ export function productAvailability(policy, { built = BUILT, registerCanCount =
615
615
  // or unrestricted coverage returns null there — this arm cannot fire on a snapshot that says nothing.
616
616
  if (geography) {
617
617
  const cause = registerCoverageCause(geography, registerTerritories);
618
- // ── — COVERAGE IS DISCLOSED, NEVER REFUSED (owner ruling, 2026-08-31) ──
618
+ // ── — COVERAGE IS DISCLOSED, NEVER REFUSED (ruling, 2026-08-31) ──
619
619
  //
620
620
  // "A user could still run global and just be aware of the limitations — I prefer that than switch
621
621
  // it off." The gate removed the product instead, and it was the ONE place in this system that
@@ -632,7 +632,7 @@ export function productAvailability(policy, { built = BUILT, registerCanCount =
632
632
  //
633
633
  // `register-cannot-count` is ruled explicitly out of scope on that issue: a register that cannot
634
634
  // return counts cannot produce the search's core output, which is a capability gap.
635
- // ── D6 (owner ruling, 2026-09-02) — AND THE OTHER COVERAGE CAUSE TOO ────
635
+ // ── D6 (ruling, 2026-09-02) — AND THE OTHER COVERAGE CAUSE TOO ────
636
636
  //
637
637
  // "Disclosure yes, in line with the picker." The picker offers a territory the register cannot
638
638
  // reach and says so at the control; a product refused for the same fact was the last place the two
@@ -678,7 +678,7 @@ export function productAvailability(policy, { built = BUILT, registerCanCount =
678
678
  */
679
679
  export const UNAVAILABLE_NOTE = {
680
680
  unbuilt: "Not part of the current release.",
681
- // `demo` IS DELETED FROM THIS MAP (owner ruling 2026-08-31 14:47), and
681
+ // `demo` IS DELETED FROM THIS MAP (ruling 2026-08-31 14:47), and
682
682
  // deleting it is what makes the reversal structural rather than a filter somebody can undo by
683
683
  // accident. Every client-facing surface renders a product's refusal as `UNAVAILABLE_NOTE[cause]`;
684
684
  // while a sentence sat here for a demo, the greyed control was one `return "demo"` away from coming
@@ -692,7 +692,7 @@ export const UNAVAILABLE_NOTE = {
692
692
  // a baked-in provider name, no CLEAROTRON_* string in scope here, and it says the one thing a reader can
693
693
  // act on — this is a coverage limit, so a newer version will not fix it.
694
694
  //
695
- // `register-not-worldwide` IS DELETED FROM THIS MAP (owner ruling
695
+ // `register-not-worldwide` IS DELETED FROM THIS MAP (ruling
696
696
  // 2026-08-31), and deleting it is what makes the ruling structural rather than a filter somebody can
697
697
  // reverse by accident. Every client-facing surface renders a product's refusal as
698
698
  // `UNAVAILABLE_NOTE[cause]`; while a sentence sat here for that cause, the ruling held only as long as
@@ -701,7 +701,7 @@ export const UNAVAILABLE_NOTE = {
701
701
  // visible edit to a map with this paragraph above it. The cause itself still exists and is still
702
702
  // computed: `coverageDisclosure` below keys on it, and says what the register DOES reach.
703
703
  //
704
- // `register-coverage` IS DELETED TOO (owner ruling D6, 2026-09-02: "disclosure yes, in line with the
704
+ // `register-coverage` IS DELETED TOO (ruling D6, 2026-09-02: "disclosure yes, in line with the
705
705
  // picker"). It was the last place a coverage fact could still refuse a product, which is the
706
706
  // disagreement between the two controls that the ruling on this issue forbids. Same structural move
707
707
  // as its sibling and for the same reason: with no sentence here there is nothing for a door to
@@ -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
- // senior-rights.mjs — "verify the right that matters" (WP-receipts, 2026-07-05 owner steer).
3
+ // senior-rights.mjs — "verify the right that matters" (WP-receipts, 2026-07-05 steer).
4
4
  //
5
5
  // The VENZY reference case: the lead conflict was ONE finding backed by NINE registrations of the same
6
6
  // mark (Türkiye, UAE, Saudi, …). The run fetched exactly one leg — the top-scoring UAE 2015 filing —
@@ -44,7 +44,7 @@ never even given. The card's FRAME (the head, `- ord:`, `- group:`, `- source:`,
44
44
  already the driver's from the 2026-08-16 frame conversion, and the entries above described it for a
45
45
  reader rather than ordering anyone to type it.
46
46
 
47
- **What did NOT move, and must not be finished by a later reader:** the card's PROSE. Owner ruling S2
47
+ **What did NOT move, and must not be finished by a later reader:** the card's PROSE. Ruling S2
48
48
  (2026-08-13) re-scoped the proposal rather than adopting it — the mechanical fields move now, and whether the
49
49
  analysis itself should be rendered is decided by EVIDENCE, one matter built both ways and read side by
50
50
  side. The transport carries the seat's sentences; it does not write them.
@@ -10,21 +10,24 @@
10
10
  ## Band meanings (triage grain)
11
11
 
12
12
  ### Very High
13
- Obvious major blocker.
13
+
14
+ - **Triage read.** Obvious major blocker.
14
15
 
15
16
  ### High
16
- Significant conflicts — same format as Medium, more urgent. Reserved for dominant, well-resourced,
17
- known-to-enforce rights holders.
17
+
18
+ - **Triage read.** Significant conflicts — same format as Medium, more urgent. Reserved for dominant, well-resourced, known-to-enforce rights holders.
18
19
 
19
20
  ### Medium
20
- Material findings that need attention. List adverse parties with dispute context.
21
+
22
+ - **Triage read.** Material findings that need attention. List adverse parties with dispute context.
21
23
 
22
24
  ### Manageable
23
- Minor findings, no strong claims. Flag major competitors/brand owners if relevant. The floor for any
24
- common English phrase.
25
+
26
+ - **Triage read.** Minor findings, no strong claims. Flag major competitors/brand owners if relevant. The floor for any common English phrase.
25
27
 
26
28
  ### Low
27
- No issues found. Available only for coined/fanciful terms (calibration rule 3).
29
+
30
+ - **Triage read.** No issues found. Available only for coined/fanciful terms (calibration rule 3).
28
31
 
29
32
  ## Calibration rules (they override gut instinct — reviewer-derived)
30
33
 
@@ -14,13 +14,14 @@ import { kebab } from "./search-policy.mjs";
14
14
  // re-deriving it. A third copy is what put the knockout frame a class behind the intake.
15
15
  import { requestNamesClasses } from "./enqueue-schema.mjs";
16
16
  import { knockoutAssessChunkFile } from "./knockout-assess-record.mjs";
17
+ import { knockoutReviewFile, validateKnockoutReviewFile } from "./knockout-review-record.mjs";
17
18
  export { kebab }; // one definition (search-policy) — re-exported for the lane's existing imports
18
19
 
19
20
  const lines = (...xs) => xs.filter(Boolean).join("\n");
20
21
  const reads = (skillReads) => `First, read and follow exactly: ${skillReads.join(", ")}.`;
21
22
 
22
23
  /**
23
- * One line per owner the driver looked up, for the assess dispatch (tracker issue 276).
24
+ * One line per owner the driver looked up, for the assess dispatch.
24
25
  *
25
26
  * BOTH OUTCOMES ARE NAMED, and that is the point. A row whose search answered gets its payload path; a
26
27
  * row whose search did NOT answer says so in the dispatch itself. Listing only the answered ones would
@@ -51,6 +52,9 @@ export function koPaths(runDir) {
51
52
  plan: join(runDir, "knockout-plan.json"),
52
53
  findings: join(runDir, "knockout-findings.json"),
53
54
  assessment: join(runDir, "knockout-assessment.md"),
55
+ // The reviewing pass's record of what it rewrote and what it let stand. At the RUN ROOT, like the
56
+ // assess chunks and for the same reason: it is a model output, and `_driver/` is behind a deny hook.
57
+ review: knockoutReviewFile(runDir),
54
58
  // Depth 2: the code-authoritative count sidecar + its per-call receipts. Both live under
55
59
  // _driver/ — they are the driver's own measurements, never a model's output.
56
60
  registerCounts: driverDir(runDir, "register-counts.json"),
@@ -63,8 +67,7 @@ export function koPaths(runDir) {
63
67
  // rows were left exactly as they were rather than gaining a matching marker: their shape is pinned
64
68
  // byte-for-byte by register-count.test.mjs, and a discriminator only one side needs is enough.
65
69
  registerRecords: driverDir(runDir, "register-records.json"),
66
- // The scoped owner lookups this run owes its promoted filings, and their receipts (tracker issue
67
- // 276). Both under `_driver/` because both are the DRIVER's own measurement of a call it made — the
70
+ // The scoped owner lookups this run owes its promoted filings, and their receipts. Both under `_driver/` because both are the DRIVER's own measurement of a call it made — the
68
71
  // rule stated fourteen lines above. The seat reads the payloads under research/, never these.
69
72
  ownerChecks: driverDir(runDir, "owner-checks.json"),
70
73
  ownerCheckLedger: driverDir(runDir, "owner-check.jsonl"),
@@ -98,6 +101,9 @@ export function koPaths(runDir) {
98
101
  // back to this and nothing ever writes it again. Delete once no resumable run predates the move.
99
102
  assessChunkLegacy: (n) => driverDir(runDir, `knockout-assess-${n}.json`),
100
103
  instructedScope: driverDir(runDir, "instructed-scope.json"),
104
+ // as on the clearance lane, and apart from instructed-scope.json for the same reason: the frame
105
+ // quotes that file's values verbatim. Written at intake, read by no stage.
106
+ defaultTerritories: driverDir(runDir, "default-territories.json"),
101
107
  emailBody: join(runDir, "email-body.md"),
102
108
  };
103
109
  }
@@ -318,7 +324,7 @@ export const KO_STAGES = {
318
324
  existsSync(K.registerRecords)
319
325
  ? `THE REGISTER FILINGS THIS RUN ALREADY FETCHED — ${K.registerRecords}. Real records, retrieved before you started: owner, jurisdiction, status, classes, dates. Read them and WEIGH them for the marks you are rating. A registration on this list is evidence about the name it names — treat its owner, its scope and its vulnerability the way the firm-wide reasoning tells you to (revocability above the lowest band; an enforcer's portfolio profile; a crowd as a mitigant under its gating precondition). What you may NOT do is describe the lane that fetched them — see the coverage rule below.`
320
326
  : "",
321
- // ── WHAT THE OWNER ACTUALLY SELLS, ALREADY SEARCHED (tracker issue 276) ────────────────────────
327
+ // ── WHAT THE OWNER ACTUALLY SELLS, ALREADY SEARCHED ────────────────────────────────────────────
322
328
  //
323
329
  // The defect this closes is not that the seat lacked an instruction — it is that the seat had no
324
330
  // way to know. It was handed an owner's NAME and its class numbers and nothing else, so a read
@@ -386,7 +392,7 @@ export const KO_STAGES = {
386
392
  // source, never a regex over rendered prose.
387
393
  // ── THE PER-MARK OPENING ASSESSMENT ──────────────────────────────────────
388
394
  //
389
- // Owner ruling, 2026-08-26: a batch client gets the same kind of opening paragraph a single-mark
395
+ // Ruling, 2026-08-26: a batch client gets the same kind of opening paragraph a single-mark
390
396
  // client gets, MODEL-AUTHORED and at full length — "the most useful piece of pre-triage; I don't
391
397
  // see why we constrain it; 4× longer like the single mark seems fine."
392
398
  //
@@ -397,7 +403,7 @@ export const KO_STAGES = {
397
403
  // ADDITIONAL TO `chunkSummary`, NOT A REPLACEMENT. The cross-mark paragraph still exists and still
398
404
  // owns the grouped page — the two are different documents for different readers, and collapsing
399
405
  // them is what produced the blank.
400
- // ── STRUCTURE, AND A SCALE THAT HOLDS (tracker issues 1934 and 2056) ──────────────────────────
406
+ // ── STRUCTURE, AND A SCALE THAT HOLDS ─────────────────────────────────────────────────────────
401
407
  //
402
408
  // Owner, 2026-08-31, reading a delivered run: the per-mark opening was "now HUGE and formatted
403
409
  // even worse — no newlines, borderline not a summary". Measured on that run: 2,875 characters,
@@ -485,4 +491,64 @@ export const KO_STAGES = {
485
491
  : `As a CONTINUATION chunk you send \`marks\` and \`chunkSummary\` only — batch and framework came from chunk 1.`,
486
492
  ),
487
493
  },
494
+
495
+ // ── THE REVIEWING PASS ──────────────────────────────────────────────────────
496
+ //
497
+ // The third stage, after assess. It rewrites the lines a reader meets before opening anything, in the
498
+ // words that reader already owns, and it does nothing else.
499
+ //
500
+ // SMALLER THAN THE STAGE IT COPIES, DELIBERATELY. The clearance lane's refutation stage re-derives the
501
+ // headline, audits the plan and grades corrections; this one is handed a list of lines and returns
502
+ // replacements for the ones it agrees are wrong. It has no verdict, no band, no evidence and no
503
+ // decision. Copying that stage's SHAPE was the design; copying its size would have made a rewrite pass
504
+ // into a second reviewer, on a product that screens rather than clears.
505
+ //
506
+ // NOT THE PRE-DELIVERY LINT, AND THIS IS THE REASON. That lint is deterministic and its own header
507
+ // states what happens to a failing check: it ships as a visible flag at the top of the client's report.
508
+ // That is a disclosure, and putting a note about the lawyer's vocabulary on the client's own page is
509
+ // the opposite of the intent, on the surface the rule exists to protect. The lint is the right
510
+ // instrument for a mechanical defect and the wrong one for a rewrite.
511
+ //
512
+ // MODEL AND EFFORT. Opus at high, like the two stages before it. A rewrite that keeps every fact while
513
+ // changing the register is the same judgment the rating seats make, and the whole value of the pass is
514
+ // that the replacement reads better than what it replaced. A cheaper rung would produce replacements
515
+ // nobody would ship, which is a pass that runs and achieves nothing.
516
+ "knockout-review": {
517
+ model: "opus", thinking: "high", timeoutSec: 600, stallSec: 420,
518
+ // DECLARATIVE ONLY on this lane, exactly as the assess stage records: nothing in koStage reads
519
+ // `skillReads`, and the effective read is the `reads([...])` call in the message below. The two are
520
+ // edited together and the asymmetry is written down rather than left to be discovered.
521
+ skillReads: ["skills/prelim-search/report-prose.md"],
522
+ out: (K) => K.review,
523
+ validate: validateKnockoutReviewFile,
524
+ // ── THE MESSAGE PROMISES A TABLE ONLY WHEN IT HAS ONE ──────────────────────────
525
+ //
526
+ // `evidenceLines` is the driver's measurement, passed at dispatch. Two things follow, and the arms
527
+ // found both: the defaults must not be absent — a message that throws on a ctx without them cannot
528
+ // be composed by anything except the production call site, which is how a dispatch goes unexamined
529
+ // — and the promise has to branch. A stage message that says "the lines are below" and then lists
530
+ // none has handed the seat an absence dressed as a pass, which is the reading this whole rule
531
+ // exists to stop. The driver does not dispatch on an empty measurement; the branch is here because
532
+ // "it cannot happen" is not a property a message should depend on.
533
+ message: ({ K, evidenceLines = [], exclusionNote = "" }) => lines(
534
+ reads(["skills/prelim-search/report-prose.md"]),
535
+ `You are the REVIEWING PASS on a knockout screen. The batch is already rated and nothing about that rating is yours to change. Your job is the words.`,
536
+ `THE RULE. A line a reader meets before opening anything carries no legal or engine vocabulary and no sentence longer than 25 words. Inside a fold the lawyer's words are allowed where a plain one would lose precision; on a default-visible line they are not. The reader here is the client of the lawyer who ordered this screen, and they read the same page.`,
537
+ `The rated record: ${K.findings}. Read it — the lines named below are quoted from it and you are rewriting them in place.`,
538
+ evidenceLines.length
539
+ ? `THE DRIVER MEASURED THESE LINES. Each row is the line, the ADDRESS that names it, and what a deterministic read found. THIS IS EVIDENCE, NOT A VERDICT: judge each in context and pass over any where the word is the subject rather than the profession's shorthand. ${exclusionNote}`
540
+ : `THE DRIVER'S READ FLAGGED NO LINE ON THIS RECORD, so there is no table below and nothing here is evidence of a fault. Read the record yourself against the rule above; if it is already plain, send one \`declined\` row saying so rather than inventing a rewrite to fill the call.`,
541
+ ...evidenceLines,
542
+ // ── THE DICTATION NAMES THE FIELD, NOT AN EFFECT ────────────────────────────────
543
+ //
544
+ // "Make the report plainer" is read and not applied, and nothing refuses — it simply does not
545
+ // happen. The address IS the field, so every rewrite this stage returns names the exact line it
546
+ // replaces and the driver writes it there.
547
+ `HAND YOUR REWRITES BACK BY CALLING \`record_knockout_review\`. THERE IS NO FILE FOR YOU TO WRITE and this dispatch names none for you to write to. Send \`rewrites\`: one row per line you are replacing, each carrying the \`at\` address COPIED VERBATIM from the row above and \`text\`, the replacement prose. The driver writes your text into that field exactly as you send it.`,
548
+ `A REWRITE KEEPS EVERY FACT. You are changing the register, never the content: the same band, the same names, the same reasons, the same conclusion, in the words the reader already owns. Splitting one long sentence into two short ones is the commonest right answer. Dropping the reason to make a line shorter is the commonest wrong one.`,
549
+ `SEND \`declined\` FOR A LINE YOU ARE LEAVING ALONE, with \`why\`. A word that is a mark being screened, a client's own product name, or a term with no plainer form that keeps the meaning — say so and leave it. A line you neither rewrite nor decline is indistinguishable afterwards from a line nobody read.`,
550
+ `YOU MOVE NOTHING ELSE. No band, no finding, no ordinal, no count, no piece of evidence and no caveat the engine wrote. There is no field for any of them and a payload naming one is refused.`,
551
+ `IF YOU CALL AGAIN, SEND ONLY WHAT YOU ARE CORRECTING. The driver merges BY ADDRESS onto what it already accepted, so a row you omit keeps what you sent for it before.`,
552
+ ),
553
+ },
488
554
  };
package/driver/stages.mjs CHANGED
@@ -279,7 +279,7 @@ export function paths(runDir) {
279
279
  // What was DECIDED about the receipt's deferrals, and when — kept apart from plan-execution.json,
280
280
  // which is an execution-FACTS receipt several validators fail-closed on. See envelope-settle.mjs.
281
281
  envelopeDecision: p(driverRel("envelope-decision.json")),
282
- instructedScope: p(driverRel("instructed-scope.json")),
282
+ instructedScope: p(driverRel("instructed-scope.json")), defaultTerritories: p(driverRel("default-territories.json")), // the stored account defaults the engine cannot search — written at intake, read by no stage (pipeline.mjs writes it)
283
283
  // compute-don't-author — the per-class scope truth (scope-facts.mjs): instructed × frozen plan ×
284
284
  // band states × coverage ledger. The masthead classes/coverage_line are stamped FROM this sidecar.
285
285
  scopeFacts: p(driverRel("scope-facts.json")),
@@ -991,7 +991,7 @@ export const pharmaMatter = (job) =>
991
991
  // The reach is pinned by report-prose-standard.test.mjs, because nothing FAILS if these rules never
992
992
  // arrive: the stage simply writes to model defaults and every other test still passes.
993
993
  const REPORT_PROSE = "skills/prelim-search/report-prose.md";
994
- // THE SPINE RIDES WITH synthesis-rules.md EVERYWHERE IT IS READ (tracker issue 1926). 412 lines MOVED
994
+ // THE SPINE RIDES WITH synthesis-rules.md EVERYWHERE IT IS READ. 412 lines MOVED
995
995
  // out of that file into `firm-wide-reasoning.md` so the knockout lane can read the same copy instead of
996
996
  // carrying its own retired transcription. A move, not a copy — which means every stage that read
997
997
  // synthesis-rules.md for that material must now read both, or it silently LOSES doctrine it has had all
@@ -2093,7 +2093,7 @@ export const STAGES = {
2093
2093
  },
2094
2094
  "per-axis prose digest — the SHORT AUDIT NOTE at register-units/<axis>.md": {
2095
2095
  class: "judgment", tokens: ["too_short"],
2096
- why: "#850 rules the per-axis prose digest J (keep) and I do not re-litigate. The only arm that speaks about it is nonEmpty at 40 or 80 chars, in validators.registerUnit — the branch reading `return /not applicable|n\\/a|no .*(hits|results)/i.test(c) ? nonEmpty(c, 40) : nonEmpty(c, 80)`. CONVERTED at tracker issue 1893 and the ruling STANDS: the judgment half — is this axis a null result, and the one observation the counts cannot carry — is still the seat's, sent as values through `record_unit_note`. What left is the part that was never judgment: the three COUNTS (queries enumerated, incomplete blocks, records carried forward) are aggregates over the tool-written band, so the driver derives them and the note cannot disagree with the material it describes. The floor is checked at the call now as well as by the validator, so a short note surfaces as a refusal the seat can act on rather than as a stage failure.",
2096
+ why: "#850 rules the per-axis prose digest J (keep) and I do not re-litigate. The only arm that speaks about it is nonEmpty at 40 or 80 chars, in validators.registerUnit — the branch reading `return /not applicable|n\\/a|no .*(hits|results)/i.test(c) ? nonEmpty(c, 40) : nonEmpty(c, 80)`. CONVERTED, and the ruling STANDS: the judgment half — is this axis a null result, and the one observation the counts cannot carry — is still the seat's, sent as values through `record_unit_note`. What left is the part that was never judgment: the three COUNTS (queries enumerated, incomplete blocks, records carried forward) are aggregates over the tool-written band, so the driver derives them and the note cannot disagree with the material it describes. The floor is checked at the call now as well as by the validator, so a short note surfaces as a refusal the seat can act on rather than as a stage failure.",
2097
2097
  },
2098
2098
  "escalation judgement — the CROWD BOUND: attempt each dangerous-category slice once class-scoped, gate on the RESULT, stop terminal on a crowd, and write a block only for the distinctive anchor": {
2099
2099
  class: "judgment", tokens: [],
@@ -2474,7 +2474,7 @@ export const STAGES = {
2474
2474
  },
2475
2475
  "the Sheet-1 findings row's identifier cells — URI, Mark, Owner, Country, Classes, Status, Filed, Expiry": {
2476
2476
  class: "mechanical:tool-written", tokens: ["registerdigest_uri_missing", "registerdigest_uri_unknown"],
2477
- why: "CONVERTED (conversion 11, tracker issue 1893): the seat sends the position's `uri` and the driver renders every cell from the band record it names — record_id, mark_text, classes, status, owner_name, owner_country, application_date, registration_date, expiry_date. The join is now the check: a uri no band record carries is refused AT THE CALL, where restating it costs nothing, instead of producing a plausible row of retyped cells that fails downstream or nowhere. The DECISION that a position earns a row stays judgment (element above); the cells were never anything but transcription.",
2477
+ why: "CONVERTED (conversion 11): the seat sends the position's `uri` and the driver renders every cell from the band record it names — record_id, mark_text, classes, status, owner_name, owner_country, application_date, registration_date, expiry_date. The join is now the check: a uri no band record carries is refused AT THE CALL, where restating it costs nothing, instead of producing a plausible row of retyped cells that fails downstream or nowhere. The DECISION that a position earns a row stays judgment (element above); the cells were never anything but transcription.",
2478
2478
  },
2479
2479
  "the full clickable record URL, composed from providers/<name>.md 'Record base host' plus the record `uri`": {
2480
2480
  class: "mechanical:tool-written", tokens: [],
@@ -2618,8 +2618,8 @@ export const STAGES = {
2618
2618
  `YOU DO NOT SEND — AND MUST NOT RETYPE — any record's Mark, Owner, Country, Classes, Status, Filed or Expiry, the clickable record URL, the register name, the summary counts, the audit trail or the record ids you read. Every one of those is rendered from the band record your \`uri\` names or from this run's own receipts. The uri IS the join: a uri no band record carries is refused on the call, naming it, rather than rendered as a row of blank cells.`,
2619
2619
  // ── — THE SEAT MARKS THE LIST, AND THE RULE NAMES THE FIELD IT SENDS ─────────
2620
2620
  //
2621
- // Stated as a FIELD ON THE CALL, deliberately. The failure this avoids is measured: tracker issue
2622
- // 1955 ruled a rule that described a downstream effect — "the string you sweep" — the seat opened
2621
+ // Stated as a FIELD ON THE CALL, deliberately. The failure this avoids is measured: an earlier
2622
+ // ruling covered a rule that described a downstream effect — "the string you sweep" — the seat opened
2623
2623
  // the file, and it reached one variant of thirty-six, because the seat authors rows and had no
2624
2624
  // field to put the effect in. A rule the seat cannot map onto something it sends produces nothing
2625
2625
  // and no refusal.
@@ -3222,7 +3222,7 @@ export const STAGES = {
3222
3222
  // lane's own prose contract. Two of the four longest sentences in the delivered report were
3223
3223
  // coverage/gap prose, and NOTHING governed it: the code-stamped `coverage_line:` front-matter
3224
3224
  // (scope-facts.mjs,) is EXCLUDED from predelivery-lint's prose scan by design
3225
- // (stripFrontMatterBlock, predelivery-lint.mjs:376), so a coverage number re-typed into prose
3225
+ // (stripFrontMatterBlock in predelivery-lint.mjs), so a coverage number re-typed into prose
3226
3226
  // beside it is caught only if the prose form itself trips SCOPE_NUMBER_RE — which the narrative's
3227
3227
  // own phrasings routinely dodge. Prompt-only per §7; the fix is to stop authoring the duplicate.
3228
3228
  `COVERAGE PROSE (the lane that runs longest — hold it to the house budgets): the register coverage line a reader sees is COMPUTED from this run's own record and STAMPED BY CODE as front-matter (the proportion, the class states, the searched registers). Do NOT re-type its numbers anywhere in prose — not in a coverage[] note, not in the coverage_judgment reason, not in the narrative. Nothing catches the duplicate for you (the code-stamped line sits outside the prose checks precisely because ITS numbers are the authoritative ones), so a re-typed count does not disagree with the record — it silently drifts from it a redelivery later. Carry the SUBSTANCE and drop the number: "the remaining forms are non-Latin script" says the useful half; the code says how many. State each coverage fact ONCE, in ONE place — an area's state belongs in its coverage[] row, the sufficiency read belongs in coverage_judgment.reason, and neither is re-narrated in the other or in the findings. And say WHICH KIND of negative you hold every time: a source this run actually queried and got nothing from reads "searched — none found"; a source it did not reach reads "not searched this run" or "could not be searched — <the reason>". The same source must never wear both readings in one report.`,
@@ -3870,7 +3870,7 @@ export const STAGES = {
3870
3870
  reads(reportCardSkillReads({ profile })),
3871
3871
  // ── CONVERSION 5. THE FIRST FAN-OUT TRANSPORT ────────────────────────────────────
3872
3872
  //
3873
- // What moves is the SHAPE, not the analysis. Owner ruling S2 (2026-08-13) keeps the prose the
3873
+ // What moves is the SHAPE, not the analysis. Ruling S2 (2026-08-13) keeps the prose the
3874
3874
  // model's pending a side-by-side reading of one matter built both ways — so this dispatch still
3875
3875
  // asks for the seat's judgment, bullet by bullet, and stops asking it to hit line templates a
3876
3876
  // parser then re-reads.
@@ -4561,11 +4561,11 @@ export function assertTierSanity() {
4561
4561
  return true;
4562
4562
  }
4563
4563
 
4564
- // ── WHO THE COMPLETION NOTICE IS FOR (tracker issue 289) ─────────────────────────────────────────────
4564
+ // ── WHO THE COMPLETION NOTICE IS FOR ─────────────────────────────────────────────────────────────────
4565
4565
  //
4566
4566
  // It went to whoever runs the agent, never to whoever asked. `AGENT_WHATSAPP` is keyed by AGENT ID, and
4567
4567
  // every user of this deployment shares the one agent, so on every run the operator was paged for work
4568
- // somebody else ordered and the person who ordered it was told nothing. Owner ruling, 2026-09-07: route
4568
+ // somebody else ordered and the person who ordered it was told nothing. Ruling, 2026-09-07: route
4569
4569
  // to the requester where a number is held for them, and keep the operator's number as a copy the
4570
4570
  // operator can switch off.
4571
4571
  //