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
@@ -13,7 +13,7 @@ import { readdirSync, existsSync, accessSync, statSync, statfsSync, constants as
13
13
  import { homedir } from "node:os";
14
14
  import { envFrom } from "../shared/env-aliases.mjs"; // — an operator-facing name is the one an operator sets, and it has to work where they set it; — envFrom is the resolver that reads every spelling of it
15
15
  import { invoke } from "../shared/invocation.mjs"; // — name a command the reader can actually type
16
- import { envFileRead } from "../shared/env-local.mjs"; // — WHICH file to set it in, measured; null for a service that read none (tracker issue 202)
16
+ import { envFileRead } from "../shared/env-local.mjs"; // — WHICH file to set it in, measured; null for a service that read none
17
17
  import { numericSetting, resolveNumericSetting } from "./numeric-setting.mjs"; // — a number, or a refusal that names the variable; never NaN
18
18
 
19
19
  const { X_OK } = FS;
@@ -117,7 +117,7 @@ export const config = {
117
117
  // two different things depending on which accessor you asked, and `queueDirs` resolved a RELATIVE
118
118
  // queue directory under the process cwd, which is its own incident shape.
119
119
  //
120
- // Owner ruling 2026-08-19: a whitespace-only value counts as UNSET, everywhere. `X=` already meant
120
+ // Ruling 2026-08-19: a whitespace-only value counts as UNSET, everywhere. `X=` already meant
121
121
  // "not configured" and this is that rule finishing its sentence — not a reversal of it, which
122
122
  // is why the 13 sites relying on empty-means-unset are untouched: they get the same answer they
123
123
  // always did, and only the spaces-shaped near-miss changes.
@@ -226,6 +226,41 @@ export const config = {
226
226
  return join(dirname(this.skillsBaseDir), rel);
227
227
  },
228
228
 
229
+ /**
230
+ * WHICH LAYER ANSWERED, as a fact rather than a path — `resolveSkillPath` with its reasoning shown.
231
+ *
232
+ * THE SILENT CASE IS NOT THE MISSING ONE. A file in neither root resolves to a base path that does not
233
+ * exist, and the read then throws by name, which is loud already. The case nobody hears is a file
234
+ * ABSENT FROM THE OVERLAY AND PRESENT IN THE BASE: a real, readable file is returned, and if that file
235
+ * is a risk framework the matter is rated under somebody else's rubric with nothing in the log. The
236
+ * shipped tree carries `risk-framework.md`, `risk-framework-demo.md` and `risk-framework-triage.md`
237
+ * under the same names customers use for their own, so removing one from the config store swaps the
238
+ * deck rather than emptying it. NAME ONLY DECKS THAT SHIP: `files[]` excludes
239
+ * `risk-framework-*` and re-includes exactly those three, so an example naming any other deck is
240
+ * describing a tree the reader does not have.
241
+ *
242
+ * `layer` is what happened: "overlay" served from the config store, "base" served from the repo while
243
+ * an overlay was configured and did not hold it, "base-only" served from the repo with no overlay
244
+ * configured at all (the ordinary single-tree install — nothing to say about it), and "missing" held by
245
+ * neither, which the caller's own read reports.
246
+ *
247
+ * BEHAVIOUR IS NOT CHANGED HERE and must not be. The overlay-then-base fallback is the migration
248
+ * design: every generic methodology file legitimately falls back today, and making that throw would
249
+ * take the whole product down to make one class of file loud. This reports; the caller decides which
250
+ * layers matter to it.
251
+ */
252
+ resolveSkillPathReport(relFromSkillsRoot) {
253
+ const rel = String(relFromSkillsRoot ?? "").replace(/^\/+/, "");
254
+ const basePath = join(dirname(this.skillsBaseDir), rel);
255
+ const overlay = this.skillsOverlayDir;
256
+ if (!overlay) return { path: basePath, rel, layer: existsSync(basePath) ? "base-only" : "missing", overlayPath: null, basePath };
257
+ if (!existsSync(overlay))
258
+ throw new Error(`skills_overlay_unreadable:${overlay} (CLEAROTRON_INSTRUCTIONS_DIR is set but the process cannot see it — customer-specific skills would silently fall back to the repo defaults)`);
259
+ const overlayPath = join(dirname(overlay), rel);
260
+ if (existsSync(overlayPath)) return { path: overlayPath, rel, layer: "overlay", overlayPath, basePath };
261
+ return { path: basePath, rel, layer: existsSync(basePath) ? "base" : "missing", overlayPath, basePath };
262
+ },
263
+
229
264
  /**
230
265
  * Every skills root handed to the engine's file tools (overlay + base, deduped).
231
266
  *
@@ -377,7 +412,7 @@ export const config = {
377
412
  //
378
413
  // "`install` writes one for you" is the whole remedy this carried, and `install` refuses a
379
414
  // non-terminal. This message reaches `start --background`'s screen, which is the scripted and hosted
380
- // install route, so its reader was being sent to a wizard they cannot open (tracker issue 202).
415
+ // install route, so its reader was being sent to a wizard they cannot open.
381
416
  //
382
417
  // `envFileRead()` rather than a path composed here, for the reason its header gives: this is a
383
418
  // library reached by CLI entries AND by unit-booted services, and a service read no file of its own.
@@ -503,7 +538,7 @@ export const config = {
503
538
  // ON-DISK NAME, NOT A PRODUCT NAME: an install that never set the variable is already using this
504
539
  // directory, so renaming the default moves the install to an empty one and nothing migrates. Here
505
540
  // the orphaned files are run-slot locks, so a live run's slot goes unseen and the global cap is
506
- // silently exceeded rather than enforced. Owner ruling, tracker issue 308.
541
+ // silently exceeded rather than enforced. Ruling.
507
542
  get runLockDir() { return this.envValue("CLEAROTRON_RUN_LOCK_DIR") || join(this.workspaceRoot, "prelim-run-locks"); },
508
543
 
509
544
  // Delivery outbox (Workstream B). On a handoff-mode finish the driver drops <runId>.pending here (naming
@@ -515,7 +550,7 @@ export const config = {
515
550
  // ON-DISK NAME, NOT A PRODUCT NAME: an install that never set the variable is already using this
516
551
  // directory, so renaming the default moves the install to an empty one and nothing migrates. Here
517
552
  // the orphaned files are requester-facing events — delivered, run-failed, intake-rejected — so the
518
- // visible failure is a requester never told their run finished. Owner ruling, tracker issue 308.
553
+ // visible failure is a requester never told their run finished. Ruling.
519
554
  get outboxDir() { return this.envValue("CLEAROTRON_OUTBOX_DIR") || join(this.workspaceRoot, "prelim-outbox"); },
520
555
 
521
556
  // ── Delivery/comms (Phase 2, standalone product) ─────────────────────────────────────────────────
@@ -972,7 +1007,7 @@ export const PROVIDERS = {
972
1007
  credEnv: "CLARIVATE_API_KEY",
973
1008
  skillDoc: "skills/prelim-register/providers/clarivate.md",
974
1009
  hasPublicRecordUrl: false, // Compumark Content has no public record URL — cite the office register
975
- //, owner ruling 2026-08-20 — WHAT A CARD SHOWS WHERE A LINK CANNOT GO. A UI exists for this
1010
+ //, ruling 2026-08-20 — WHAT A CARD SHOWS WHERE A LINK CANNOT GO. A UI exists for this
976
1011
  // provider and we do not know its per-record URL, so the card says so and says it is unfinished.
977
1012
  // "placeholder" is the reader-facing admission; anything else would read as a citation.
978
1013
  recordCitation: "placeholder",
@@ -1109,7 +1144,7 @@ export const PROVIDERS = {
1109
1144
  credEnv: "SIGNA_API_KEY",
1110
1145
  skillDoc: "skills/prelim-register/providers/signa.md",
1111
1146
  hasPublicRecordUrl: false, // Signa exposes no per-record public URL — cite the office register
1112
- //, owner ruling 2026-08-20 — no register UI exists to link to at all, so the card points at
1147
+ //, ruling 2026-08-20 — no register UI exists to link to at all, so the card points at
1113
1148
  // the artifact that DOES carry the record: the audit workbook. Naming it is the whole of this
1114
1149
  // branch; constructing a per-record URL for a provider that publishes none would be a fabricated
1115
1150
  // citation on a legal deliverable, and the ruling forbids it in those words.
@@ -1506,7 +1541,7 @@ export const RESEARCH_PROVIDERS = {
1506
1541
  // and so the page never renders a title-cased guess at an id.
1507
1542
  label: "Perplexity",
1508
1543
  credEnv: "PERPLEXITY_API_KEY",
1509
- // READER-TERMS FIELDS (tracker issues 2072/2089): what its absence costs, in the reader's world,
1544
+ // READER-TERMS FIELDS: what its absence costs, in the reader's world,
1510
1545
  // and where a key comes from. Stated beside the credential like `label`, so setup and doctor can
1511
1546
  // DERIVE their prompts from this table instead of keeping a second hand list — the second list is
1512
1547
  // how SERPAPI_API_KEY went unprompted while the code said "required for live grid cells".
@@ -1561,7 +1596,7 @@ export const RESEARCH_PROVIDERS = {
1561
1596
  //
1562
1597
  // ── / — THE NATIVE-LANGUAGE LANES RUN ON THE PROGRAM THE CUSTOMER CHOSE ────────────────
1563
1598
  //
1564
- // Owner ruling 2026-08-20, verbatim: "one LLM provider only ever, API or auth, no mix." These three
1599
+ // Ruling 2026-08-20, verbatim: "one LLM provider only ever, API or auth, no mix." These three
1565
1600
  // lanes were the mix. They POSTed to the Anthropic Messages API on `ANTHROPIC_API_KEY` at a hardcoded
1566
1601
  // haiku tier no matter which engine the run was configured for — so round 21f9b0ad's receipt carried
1567
1602
  // engine `openai-agent` / SUBSCRIPTION on its agentic stages and `anthropic-direct` / API-KEY on all
@@ -118,9 +118,30 @@ export const jobJurisdictions = (job) =>
118
118
  * carries thirty-odd line-number citations from other files, so inserting explanation into it silently
119
119
  * repoints all of them. Keeping the reasoning where the rule lives costs that file no lines.
120
120
  */
121
+ /**
122
+ * The account's default territories, split by whether the engine can search them.
123
+ *
124
+ * ONE reading of `defaultJurisdictions`, because there were two and they disagreed in silence. The
125
+ * prompt line below dropped what the vocabulary did not recognize; the resolved scope carried the same
126
+ * entries through untouched. So a misspelled default was simultaneously absent from what the model was
127
+ * told and present in what the search planned against — and the person who typed it saw neither.
128
+ *
129
+ * `unrecognized` is what `recognizedTerritories` calls `dropped`, renamed at this boundary because its
130
+ * own docstring states the rule the caller has to keep: it is A FINDING, NEVER A SILENCE. Returning it
131
+ * is what makes that possible; `resolveEffectiveScope` is where it reaches somebody.
132
+ *
133
+ * NOT a filter on what gets searched. What a stored default DOES is a client-outcome question and it is
134
+ * not this function's to answer, and the ruling behind this change is explicit that the request path's
135
+ * tolerance for an unrecognized territory stays exactly as it is. This reports; it does not narrow.
136
+ */
137
+ export function defaultTerritoryState(profile) {
138
+ const { kept, dropped } = recognizedTerritories(profile ? profile.defaultJurisdictions ?? [] : []);
139
+ return { kept, unrecognized: dropped };
140
+ }
141
+
121
142
  export function defaultJurisdictionsLine(job, profile) {
122
143
  if (jobJurisdictions(job).length) return [];
123
- const { kept } = recognizedTerritories(profile ? profile.defaultJurisdictions ?? [] : []);
144
+ const { kept } = defaultTerritoryState(profile);
124
145
  return kept.length
125
146
  ? [`Customer-default jurisdictions that materially matter (the request names none — apply these): ${kept.join(", ")}.`]
126
147
  : [];
@@ -203,6 +224,14 @@ export function resolveEffectiveScope(job = {}, profile = null, resolved = null)
203
224
  // The common-law grid's size follows the platform list, so widening the marketplaces widens the
204
225
  // work. Surfaced because it is the part a requester cannot infer from their own request.
205
226
  gridCellsPerVariant: profile ? derivedFloor(widened) : null,
227
+ // The stored defaults the engine cannot search — named, so the person approving a run can see that
228
+ // the configuration says more than the search will do. Empty on a profile with none, which is every
229
+ // profile written through an editor that refuses them at the point of entry.
230
+ //
231
+ // Reported off the PROFILE rather than off this run's ladder outcome. A default that lost to a
232
+ // request is still a default that will never work, and a reader who only hears about it on the runs
233
+ // where it happened to apply learns about a broken setting at random.
234
+ unrecognizedDefaultTerritories: defaultTerritoryState(profile).unrecognized,
206
235
  };
207
236
  }
208
237
 
@@ -51,7 +51,7 @@
51
51
  * "1.5–2.5 hours" and a knockout from 45 to 15 minutes — and a stored quote is honoured at the version
52
52
  * it was given. A version-2 quote must therefore not be read against this table.
53
53
  * 4 —: the wave multiplier is gone and the turnaround quote is a table lookup with no arithmetic
54
- * (owner ruling 2026-08-26, recorded at `turnaroundBounds`). No weight moved and no unit changed.
54
+ * (ruling 2026-08-26, recorded at `turnaroundBounds`). No weight moved and no unit changed.
55
55
  * The SHAPE did, which is what this counter tracks — but state the size of it honestly: `runCount`
56
56
  * is 1 for every job any door will admit, so the removed term was 1 on every quote that has ever
57
57
  * been stored. No version-3 quote in any run record holds a number this table would not reproduce.
@@ -214,11 +214,11 @@ export const costBand = (i) => Math.max(1, Math.min(5, Math.ceil(effortUnits(i)
214
214
  // ── turnaround ──────────────────────────────────────────────────────────────────────────────────────
215
215
  //
216
216
  // THERE IS NO ARITHMETIC IN THIS SECTION, AND THAT IS THE DESIGN. The quote is a table lookup and
217
- // nothing else — owner ruling 2026-08-26, below. Anything that multiplies, adds to or scales the ruled
217
+ // nothing else — ruling 2026-08-26, below. Anything that multiplies, adds to or scales the ruled
218
218
  // range is the thing this section was rebuilt to remove; do not reintroduce one without a new ruling.
219
219
 
220
220
  /**
221
- * THE QUOTED BOUNDS. ONE SOURCE — owner ruling, 2026-08-23.
221
+ * THE QUOTED BOUNDS. ONE SOURCE — ruling, 2026-08-23.
222
222
  *
223
223
  * WHAT THIS REPLACED, and why a constant beat a model. The old quote was a base plus one adder per lane:
224
224
  * 1.5h, +0.5 for case law, +0.5 for a native-language lane, +0.5 for a single territory. It missed the
@@ -239,12 +239,12 @@ export const costBand = (i) => Math.max(1, Math.min(5, Math.ceil(effortUnits(i)
239
239
  * harness judges against — see reconcileTurnaround.
240
240
  *
241
241
  * KNOCKOUT was quoted 45 min against 4–6 min delivered. Ruled "~15 min" — and SUPERSEDED by a later
242
- * owner ruling to 5-10 minutes, a range. Recorded rather than rewritten,
242
+ * ruling to 5-10 minutes, a range. Recorded rather than rewritten,
243
243
  * because the 4–6 min measurement beside it is the reason 5-10 is the better quote: 15 never sat on it.
244
244
  */
245
245
  export const TURNAROUND_QUOTE = Object.freeze({
246
246
  clearance: Object.freeze({ lowHours: 1.5, highHours: 2.5 }),
247
- // — OWNER RULING: a knockout quotes 5-10 MINUTES, a range, not a flat figure.
247
+ // — RULING: a knockout quotes 5-10 MINUTES, a range, not a flat figure.
248
248
  //
249
249
  // The flat 0.25 came from the "no compute, just say it, keep it simple", and that ruling is
250
250
  // untouched here: this is still one stated figure per pipeline with nothing computed from levers. What
@@ -264,7 +264,7 @@ export const quoteBoundsFor = (l) => TURNAROUND_QUOTE[deriveMode(l) === "knockou
264
264
  /**
265
265
  * The bounds for THIS job. The ruled range, and NOTHING is done to it.
266
266
  *
267
- * — OWNER RULING, 2026-08-26: "No compute. We just say 1.5–2.5 hours for
267
+ * — RULING, 2026-08-26: "No compute. We just say 1.5–2.5 hours for
268
268
  * big reports, period. Keep it simple." The question put to him was which run-slot cap the quote should
269
269
  * divide by, and he removed the division instead of answering it.
270
270
  *
@@ -50,7 +50,7 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
50
50
  // "text-parsed" on resetsAtBasis means it was read out of vendor
51
51
  // prose that stated no timezone, so it resolved in the running
52
52
  // box's zone — a reading, never a provider fact. Absent whenever
53
- // `resetsAt` is absent. (tracker issue 697)
53
+ // `resetsAt` is absent.
54
54
  reads: string[] | absent, // READS GAUGE — file paths this turn's Read tool_use blocks
55
55
  // opened; [] = recorded "read nothing"; key absent = engine cannot
56
56
  // observe reads (openai-agent) → gateway journals `reads: null`
@@ -261,7 +261,7 @@ not extend the budget for it.
261
261
 
262
262
  **The hard ceiling — the third clock, and it measures ACTIVE time.** The last-resort wall fires at the
263
263
  stage's timeout + 60s (`CLEAROTRON_HARD_MS` pins it; unset and with no stage timeout, 660s) — compared not
264
- against elapsed but against elapsed MINUS tool wait, including a call still in flight. Owner ruling: a
264
+ against elapsed but against elapsed MINUS tool wait, including a call still in flight. Ruling: a
265
265
  model always delivers something or fails, so this ceiling exists for the harness's own failure modes,
266
266
  and a turn waiting on a register call is working. Read on elapsed, a stage that spent 74.8% of its wall
267
267
  in tool calls and one that stalled are indistinguishable; read on active time the first is a normal turn
@@ -494,7 +494,7 @@ export const anthropicAgentEngine = {
494
494
  // "omitted" on Opus 5, so an ENGAGED block streams with a zero-length `thinking` string and a real
495
495
  // `signature` — reading the text would report "no thinking" on every production turn.
496
496
  let thought = false;
497
- // MODEL GAUGE (tracker issue 238 corruption 3): which model the PROVIDER says served this turn. Until now
497
+ // MODEL GAUGE (corruption 3): which model the PROVIDER says served this turn. Until now
498
498
  // nothing on any record was a function of the provider's response — `modelUsed` was
499
499
  // `resolveModel(<the alias we asked for>)`, a pure function of the request, so a substitution or a
500
500
  // tier bounce was unobservable by construction. The truth is on the wire twice: `system:init`
@@ -564,7 +564,7 @@ export const anthropicAgentEngine = {
564
564
  // real (two chunks, no gap); keying on `spent === 0` would flag those and miss a stall that
565
565
  // happened to leave 1ms on the clock.
566
566
  let toolAskedChunk = null;
567
- // tracker issue 1828 (the LATENT half, not the stall) — ONE OUTSTANDING ASK WAS ALL THIS COULD
567
+ // THE LATENT HALF, NOT THE STALL — ONE OUTSTANDING ASK WAS ALL THIS COULD
568
568
  // HOLD. `toolAskedAt` was a single slot, so a second assistant message arriving before the first
569
569
  // ask's result OVERWROTE the first ask's start time and its wait was never counted at all.
570
570
  // Measured on the overlap fixture (two asks, 150ms each): the old code reported toolWaitMs=149
@@ -689,7 +689,7 @@ export const anthropicAgentEngine = {
689
689
  .filter((b) => b?.type === "tool_use");
690
690
  const asked = askBlocks.map((b) => String(b?.name ?? "?"));
691
691
  toolAskedAt = asked.length ? Date.now() : null;
692
- toolAskedChunk = asked.length ? chunkSeq : null; // tracker issue 1828
692
+ toolAskedChunk = asked.length ? chunkSeq : null;
693
693
  toolAskedNames = asked.length ? [...new Set(asked)].sort().join("+") : null;
694
694
  if (asked.length) {
695
695
  const now = Date.now();
@@ -776,7 +776,7 @@ export const anthropicAgentEngine = {
776
776
  const watchdog = setInterval(() => {
777
777
  artifactProgress();
778
778
  const now = Date.now();
779
- // THE STARTUP DEBT. tracker issue 1692 widened the deadline below until the first byte but left this
779
+ // THE STARTUP DEBT. An earlier repair widened the deadline below until the first byte but left this
780
780
  // clock's ORIGIN at spawn, so the startup interval stayed on the meter: the moment the grace let
781
781
  // go, progIdle already WAS the whole boot, and any NOPROG shorter than startup had expired before
782
782
  // the child was observed at all. The kill then landed on the next tick, before the ask could reach
@@ -787,9 +787,9 @@ export const anthropicAgentEngine = {
787
787
  // ── STARTUP IS NEITHER SILENCE NOR WORK ──────────────────────────────────────────────
788
788
  // Every clock here USED TO start at SPAWN, so until the child's first byte they were all timing
789
789
  // process startup: an idle clock that had not yet seen silence, and an active clock that had not
790
- // yet seen work. Past tense deliberately — tracker issue 1780 finished this, and the sentence stayed true of
790
+ // yet seen work. Past tense deliberately — that work is finished, and the sentence stayed true of
791
791
  // the byte-stall below (`lastMove`) while being false of the progress clock above, which is
792
- // precisely the gap that let the defect sit here for a day looking like ambient flake. tracker issue 1703 established this for the byte-stall in common.mjs — which this engine does not use,
792
+ // precisely the gap that let the defect sit here for a day looking like ambient flake. This was established for the byte-stall in common.mjs — which this engine does not use,
793
793
  // because it spawns its own child, so that fix never reached this watchdog. Under full-suite load
794
794
  // a starved spawn crossed a 400ms test ceiling before the mock had emitted anything, and the kill
795
795
  // was recorded as a no-progress stall against a turn that had not yet been given a chance to
@@ -802,7 +802,7 @@ export const anthropicAgentEngine = {
802
802
  else if (progIdle >= (started ? NOPROG : Math.max(NOPROG, GRACE))) { stallKill = true; noProgressKill = true; killed = true; killTree(); }
803
803
  // ── THE CEILING MEASURES ACTIVE TIME, NOT ELAPSED ────────────────────────────────────
804
804
  //
805
- // Owner ruling: "there isnt such thing as a hung model. it always delivers something or fails."
805
+ // Ruling: "there isnt such thing as a hung model. it always delivers something or fails."
806
806
  // So this ceiling exists for the harness's own failure modes, not to budget the model, and a turn
807
807
  // still doing work must not die because a tool it was waiting on took a while to answer.
808
808
  //
@@ -855,7 +855,7 @@ export const anthropicAgentEngine = {
855
855
  // STDERR DOES NOT START THE CLOCKS, and this line used to. This engine's protocol is
856
856
  // stream-json on STDOUT; stderr carries node warnings and CLI notices, which a child can emit
857
857
  // before it has done anything at all. Treating one as "the child has spoken" ended the grace and
858
- // — since tracker issue 1780, which made the first byte the progress clock's ORIGIN — started that clock too,
858
+ // — since the change that made the first byte the progress clock's ORIGIN — started that clock too,
859
859
  // so a turn whose real startup was still running got its no-progress ceiling measured from a
860
860
  // deprecation warning. A child that writes ONLY stderr is still bounded: it never starts, so the
861
861
  // byte-stall below fires at max(STALL, GRACE) exactly as it does for a silent spawn.
@@ -893,7 +893,7 @@ export const anthropicAgentEngine = {
893
893
  //
894
894
  // A turn killed mid-tool-call has an OPEN ask that `toolWaitMs` has not closed. Reporting the
895
895
  // unclosed accumulator left `activeMs + toolWaitMs` short of the wall by exactly that gap — and
896
- // only on turns killed during a call, which is the population tracker issue 1111 is about. A guard asserting
896
+ // only on turns killed during a call, which is the population this rule is about. A guard asserting
897
897
  // the identity would have held on every ordinary attempt and quietly not held on the interesting
898
898
  // one. Closed here, ONCE, so both fields are derived from the same number.
899
899
  //
@@ -987,7 +987,7 @@ export const anthropicAgentEngine = {
987
987
  // nothing unmeasurable), following toolWaitByTool's own rule that absence and "cannot report"
988
988
  // must not look alike. `toolWaitMs` and the per-tool split are untouched.
989
989
  toolWaitUnmeasurable: [...unmeasurable],
990
- // MODEL GAUGE (tracker issue 238): the id the WIRE reported, or null when the stream never said. Assistant
990
+ // MODEL GAUGE: the id the WIRE reported, or null when the stream never said. Assistant
991
991
  // message first (what served the call), init second (what the session was configured with).
992
992
  // Never the requested alias — see the declaration above.
993
993
  modelWire: wireModelAssistant ?? wireModelInit ?? null,
@@ -1023,7 +1023,7 @@ function errResult(t0, e, resumeRef) {
1023
1023
  stderr: `anthropic-agent spawn error: ${e?.message ?? e}`, laneWaitMs: 0,
1024
1024
  // reads: a spawn error means NO turn ran — [] is the true observation (nothing was read), not a gap.
1025
1025
  // modelWire: null for the opposite reason — no turn ran, so the wire said nothing about a model, and
1026
- // the record must say UNKNOWN rather than inherit the alias that was asked for (tracker issue 238).
1026
+ // the record must say UNKNOWN rather than inherit the alias that was asked for.
1027
1027
  json: null, usage: null, reads: [], readsTruncated: false, modelWire: null, sessionRef: resumeRef ?? null,
1028
1028
  };
1029
1029
  }
@@ -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
  // engine/jx-turn.mjs — the native-language lanes reach a model the same way every other stage does.
4
4
  //
5
- // ── why ( /, owner ruling 2026-08-20) ─────────────────────────────────────────────────────
5
+ // ── why ( /, ruling 2026-08-20) ─────────────────────────────────────────────────────
6
6
  //
7
7
  // Verbatim: "one LLM provider only ever, API or auth, no mix". The jx lanes did not obey it and could
8
8
  // not: they POSTed to the Anthropic Messages API on `ANTHROPIC_API_KEY` at a hardcoded haiku tier,
@@ -350,7 +350,7 @@ const RECORDING = Object.freeze({
350
350
  // reads are already the seeded `Read` grant's. No `search_run_artifacts`: this stage reads NOTHING from
351
351
  // the run — its finding arrives INLINE in the dispatch, which is the isolation the stage exists for.
352
352
  //
353
- // WHAT DOES NOT MOVE: the card's PROSE. Owner ruling S2 (2026-08-13) re-scoped — the mechanical
353
+ // WHAT DOES NOT MOVE: the card's PROSE. Ruling S2 (2026-08-13) re-scoped — the mechanical
354
354
  // fields move now, the prose half waits on a side-by-side reading of one matter built both ways. The
355
355
  // transport carries the seat's bullets; it does not write them.
356
356
  "report-card": {
@@ -389,7 +389,7 @@ const RECORDING = Object.freeze({
389
389
  // and must take it whole. That is a context cost, not a capability loss, and it is the honest price.
390
390
  // NINTH — narrative-refutation, and the first conversion whose seat KEEPS its retrieval surface.
391
391
  //
392
- // The reviewer is the report's only check. Its output converts; its REACH does not. Owner ruling
392
+ // The reviewer is the report's only check. Its output converts; its REACH does not. Ruling
393
393
  // (relayed 2026-08-26): it keeps the perplexity and band groups, because a reviewer that can only
394
394
  // compare the report against itself is a proofreader, and a prose-consistency read is the shape that
395
395
  // let the false coverage claim through. So this row states plainly what every sibling could take for
@@ -551,6 +551,23 @@ const RECORDING = Object.freeze({
551
551
  + "CALLS, UNMEASURED PRE-CONVERSION — this lane's seats hold no tools today, so the first "
552
552
  + "converted run is where the count comes from",
553
553
  },
554
+ // THE LANE'S THIRD CONVERSION, and the only stage on it that was born typed — there is no
555
+ // pre-conversion knockout-review that wrote its own file, so nothing here is a migration.
556
+ //
557
+ // NOT FANNED. The pass reads the WHOLE merged record in one turn, because the rule it applies is about
558
+ // consistency across the lines a reader meets: two marks' basis lines rewritten in separate turns drift
559
+ // apart in register, which is the defect the pass exists to remove.
560
+ "knockout-review": {
561
+ seatWrites: false,
562
+ tools: Object.freeze(["record_knockout_review"]),
563
+ reason: "hands back rewrites as VALUES, each naming the line it replaces by the typed address the "
564
+ + "driver measured and handed over. The address is what makes this a transport rather than a "
565
+ + "matching problem: prose quoted back has to be found again in the record, and the finder then "
566
+ + "decides where the rewrite lands. Refused by name at the call when an address names no line on "
567
+ + "this run's record, so a seat learns while it can still send a repair turn. EXPECTED FROM THE "
568
+ + "DICTATION'S ORDERED CALLS, UNMEASURED PRE-CONVERSION — this lane's seats hold no tools today, "
569
+ + "so the first converted run is where the count comes from",
570
+ },
554
571
  });
555
572
 
556
573
  // stage → its server key. DERIVED, and the alphabet is checked rather than assumed: an underscore in a
@@ -1094,8 +1111,8 @@ const FANNED_STAGES = Object.freeze({ perAxis: PER_AXIS_STAGES, perChunk: PER_CH
1094
1111
  * The BOUND ORDINAL for a fanned-out recording stage: which member of a per-ordinal artifact this turn
1095
1112
  * is writing. The driver knows, because the driver is what fanned it out.
1096
1113
  *
1097
- * ONE FUNCTION, BOTH SUFFIX FORMS, for the reason `toolGroupsForStage` gives one block to both (tracker
1098
- * issue 2003): a second derivation beside the first is a second place to forget, and forgetting is how
1114
+ * ONE FUNCTION, BOTH SUFFIX FORMS, for the reason `toolGroupsForStage` gives one block to both:
1115
+ * a second derivation beside the first is a second place to forget, and forgetting is how
1099
1116
  * the `#` form came to be unhandled in the first place. It walks SUFFIX_FORMS, so a separator this
1100
1117
  * module knows for grants is a separator it also knows for binding — the two cannot drift apart.
1101
1118
  *
@@ -1247,6 +1264,14 @@ export const RECORDING_TOOLS = Object.freeze({
1247
1264
  // retrieval grant here would contradict its own dispatch. Its reads are the instructed-scope sidecar
1248
1265
  // and its skill doc, which the seeded Read grant serves.
1249
1266
  "knockout-frame": Object.freeze(["Read", "mcp__recording-knockout-frame__record_knockout_frame"]),
1267
+ // THE LANE'S THIRD AND LAST CONVERSION. BY HAND, like every row here — O1 asserts the resolved grant
1268
+ // EQUALS this row, so a derived row would compare a value with itself.
1269
+ //
1270
+ // `Read` and its one record tool, and NO Write: this stage's artifact is the driver's. No search tool
1271
+ // and no register tools — the pass reads lines and rewrites them, and a retrieval grant would invite a
1272
+ // rewrite that adds a fact the rating never weighed. Its one read is the merged record the dispatch
1273
+ // names by path, which the seeded Read grant serves.
1274
+ "knockout-review": Object.freeze(["Read", "mcp__recording-knockout-review__record_knockout_review"]),
1250
1275
  // THE WRITER. BY HAND, like every row here — O1 asserts the resolved grant EQUALS this row, so a
1251
1276
  // derived row would compare a value with itself. This stage keeps three retrieval groups, and O1
1252
1277
  // compares the WHOLE grant, so they are listed: a row naming only the recording half would be a row
@@ -64,6 +64,7 @@ import { REGISTER_AXES } from "../../coverage-ledger.mjs";
64
64
  import { searchRunArtifacts, SEARCH_LIMITS } from "../../skeptic-search.mjs";
65
65
  import { recordKnockoutAssess } from "../../knockout-assess-record.mjs";
66
66
  import { recordKnockoutFrame } from "../../knockout-frame-record.mjs";
67
+ import { recordKnockoutReview } from "../../knockout-review-record.mjs";
67
68
 
68
69
  async function record_blind_frame(params) {
69
70
  const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
@@ -114,6 +115,16 @@ async function record_knockout_frame(params) {
114
115
  return recordKnockoutFrame(runDir, params);
115
116
  }
116
117
 
118
+ async function record_knockout_review(params) {
119
+ const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
120
+ if (!runDir) {
121
+ return { error: "this server was started without a run — the driver wires it per run; there is no parameter for it and this tool never guesses one" };
122
+ }
123
+ // NO BOUND ORDINAL. The pass reads the whole merged record in one turn, so there is nothing for the
124
+ // driver to bind and nothing for a payload to usurp — the same asymmetry knockout-frame states.
125
+ return recordKnockoutReview(runDir, params);
126
+ }
127
+
117
128
  async function record_prelim_variants(params) {
118
129
  const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
119
130
  if (!runDir) {
@@ -686,7 +697,7 @@ serve({
686
697
  }, {
687
698
  // ── CONVERSION 5 — ONE FINDING CARD, AND THE FIRST FAN-OUT TRANSPORT ──────────────────────────
688
699
  //
689
- // The schema carries the seat's JUDGMENT and nothing else. Owner ruling S2 keeps the prose the
700
+ // The schema carries the seat's JUDGMENT and nothing else. Ruling S2 keeps the prose the
690
701
  // model's pending a side-by-side reading, so `text` is the seat's sentence — what the driver takes
691
702
  // over is every SHAPE around it: the bullet, the bold lead-in, the `::p::` position, and the final
692
703
  // `- Source:` line, which is composed from the finding's own record rather than typed from a host
@@ -1170,6 +1181,67 @@ serve({
1170
1181
  },
1171
1182
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1172
1183
  handler: record_knockout_assess,
1184
+ }, {
1185
+ name: "record_knockout_review",
1186
+ description:
1187
+ "Hand back your REWRITES of the lines a reader meets first, as VALUES. The driver writes them into " +
1188
+ "the rated record itself, so you never format JSON and never write a file. Each rewrite names the " +
1189
+ "line it replaces by the `at` address you were handed in the dispatch, copied verbatim — you do " +
1190
+ "not quote the sentence and there is no field for one. An address naming no line on this run's " +
1191
+ "record is refused by name, so a mistyped address costs you a turn rather than landing your text " +
1192
+ "on the wrong line. A repair turn may send only the rows it is correcting: the driver merges BY " +
1193
+ "ADDRESS onto what it already accepted, so rows you omit survive.",
1194
+ inputSchema: {
1195
+ type: "object",
1196
+ additionalProperties: false,
1197
+ properties: {
1198
+ schema_version: { type: "integer", description: "Accepted and ignored — the driver stamps its own." },
1199
+ rewrites: {
1200
+ type: "array",
1201
+ description:
1202
+ "One row per line you are replacing. A rewrite keeps every fact and changes only the " +
1203
+ "register — the same band, names, reasons and conclusion, in the words the reader owns.",
1204
+ items: {
1205
+ type: "object", additionalProperties: false, required: ["at", "text"],
1206
+ properties: {
1207
+ at: {
1208
+ type: "object", additionalProperties: false, required: ["field"],
1209
+ description: "The address, copied VERBATIM from the dispatch row. Do not compose one.",
1210
+ properties: {
1211
+ field: { type: "string", description: "The field this line lives in." },
1212
+ mark: { type: "string", description: "The mark's name, where the field is a mark's." },
1213
+ index: { type: "integer", description: "The position, where the field is a list." },
1214
+ ordinal: { type: "integer", description: "The finding's ordinal, for a conflict's one sentence." },
1215
+ },
1216
+ },
1217
+ text: { type: "string", description: "The replacement prose, finished — written into the field exactly as sent." },
1218
+ why: { type: "string", description: "Optional: what was wrong with the line you replaced." },
1219
+ },
1220
+ },
1221
+ },
1222
+ declined: {
1223
+ type: "array",
1224
+ description:
1225
+ "One row per flagged line you are LEAVING ALONE, with the reason. A line you neither " +
1226
+ "rewrite nor decline cannot be told afterwards from a line nobody read.",
1227
+ items: {
1228
+ type: "object", additionalProperties: false, required: ["at", "why"],
1229
+ properties: {
1230
+ at: {
1231
+ type: "object", additionalProperties: false, required: ["field"],
1232
+ properties: {
1233
+ field: { type: "string" }, mark: { type: "string" },
1234
+ index: { type: "integer" }, ordinal: { type: "integer" },
1235
+ },
1236
+ },
1237
+ why: { type: "string", description: "Why the line stands as written." },
1238
+ },
1239
+ },
1240
+ },
1241
+ },
1242
+ },
1243
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1244
+ handler: record_knockout_review,
1173
1245
  }, {
1174
1246
  name: "record_knockout_frame",
1175
1247
  description:
@@ -39,7 +39,7 @@ const codexBin = () => envFrom(process.env, "CLEAROTRON_CODEX_PATH") || "codex";
39
39
  // on both engines. `sol` remains what codex itself reports as its own default (live-probed 2026-07-27:
40
40
  // `model: gpt-5.6-sol, provider: openai`), which is why the judgment tier keeps it.
41
41
  //
42
- // ── THE THREE TIERS MAP ONTO THE CODEX LADDER (owner ruling 2026-09-02,) ──────────────────────────
42
+ // ── THE THREE TIERS MAP ONTO THE CODEX LADDER (ruling 2026-09-02,) ──────────────────────────
43
43
  //
44
44
  // Every stage declares a tier — opus / sonnet / haiku — engine-independently. The anthropic engine
45
45
  // resolves those three declarations onto three models. Until this ruling codex resolved ALL THREE onto
@@ -90,7 +90,20 @@ const tail = (s) => {
90
90
  return t.length > DETAIL_CHARS ? `…${t.slice(-DETAIL_CHARS)}` : t;
91
91
  };
92
92
 
93
- const signInLine = (engine) => ENGINE_BINARIES[engine]?.signIn ?? "sign the CLI in";
93
+ // THE HEADLESS ROUTE, WHERE THE ENGINE HAS ONE. The interactive sign-in is the one thing a server with no
94
+ // browser cannot do, and it was the only remedy this offered — including to a box that had configured the
95
+ // route built for servers. The engine table already carries that route; this reads it rather than a copy.
96
+ const signInLine = (engine) => {
97
+ const spec = ENGINE_BINARIES[engine];
98
+ const base = spec?.signIn ?? "sign the CLI in";
99
+ const h = spec?.headless;
100
+ // Both forms the wizard already offers, read off the same table: a TOKEN route is run elsewhere and
101
+ // carried here by variable; a DEVICE route is run on this box and signs it in directly.
102
+ if (!h?.cmd) return base;
103
+ return h.tokenEnv
104
+ ? `${base} — or, on a box with no browser, run \`${h.cmd}\` on any machine you can sign in on and set the token it prints as ${h.tokenEnv} in this install's environment file`
105
+ : `${base} — or, on a box with no browser, run \`${h.cmd}\` here`;
106
+ };
94
107
 
95
108
  /**
96
109
  * One verdict from one turn. PURE — no clock, no filesystem, no process.
@@ -273,6 +286,19 @@ export function probeWeatherWarning(verdict) {
273
286
  * again; a credential outliving the check that borrowed it would be a worse defect than the one this
274
287
  * fixed, and a check drives the restore rather than trusting it.
275
288
  */
289
+ /**
290
+ * EVERY VARIABLE THAT DECIDES WHAT AN ENGINE SPAWN IS — selection, binary, billing mode and the
291
+ * credentials, the API key and the headless token alike. ONE list: `applyEngineEnv` below applies it,
292
+ * and a caller building the environment it hands this module fills from it. `doctor` kept a second copy
293
+ * that had dropped both credentials, so it filled a probe environment without the very token it then
294
+ * reported missing. Exported so there is nothing left to copy.
295
+ */
296
+ export function engineEnvKeys() {
297
+ return [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
298
+ .flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
299
+ .filter(Boolean)])];
300
+ }
301
+
276
302
  function applyEngineEnv(env) {
277
303
  if (!env || env === process.env) return () => {};
278
304
  // EVERY VARIABLE THAT DECIDES WHAT THE SPAWN IS, not only the ones that decide WHICH BINARY.
@@ -293,9 +319,7 @@ function applyEngineEnv(env) {
293
319
  // written, so the restore put that back instead of deleting it, and the mode leaked into the rest of
294
320
  // the process. Caught by the arm that exists to prove the restore, which is the only reason widening
295
321
  // this list was safe to do at all.
296
- const keys = [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
297
- .flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
298
- .filter(Boolean)])];
322
+ const keys = engineEnvKeys();
299
323
  const saved = new Map();
300
324
  for (const k of keys) {
301
325
  saved.set(k, process.env[k]);
@@ -115,6 +115,8 @@ export const DECLARED_JOB_FIELDS = Object.freeze([
115
115
  "deliveryRoute", "parentRunId",
116
116
  // whose rulebook rates it
117
117
  "profileKey", "projectKey", "customer", "customerUnknown",
118
+ // which organisation's Generic — set by a door only on a Generic run, from the verified principal
119
+ "tenant",
118
120
  // intake fidelity — posture and prose, verbatim from the request
119
121
  "deliverableSpec", "commercialFlexibility", "priorUse", "campaignShape",
120
122
  "upfrontInstructions", "brief", "rawRequest", "deadline", "provider",
@@ -675,7 +677,7 @@ export function validateJob(job, { atClaim = false } = {}) {
675
677
  errs.push(`both product (${JSON.stringify(rawProduct)}) and recipeKey (${JSON.stringify(rawRecipe)}) are set — name ONE selector (a saved search already carries its product)`);
676
678
  if (rawProduct && !ORDERABLE_PRODUCTS.includes(rawProduct))
677
679
  errs.push(unknownProductMessage(job.product));
678
- // caseLaw: NOT A REQUEST FIELD ANY MORE (owner ruling 2026-08-06). It is what a Full country search IS.
680
+ // caseLaw: NOT A REQUEST FIELD ANY MORE (ruling 2026-08-06). It is what a Full country search IS.
679
681
  // REFUSED rather than ignored, and that is the whole point: a flag accepted and dropped is the
680
682
  // "accepted, then quietly narrower" shape — a requester who sent it believes they bought the deep
681
683
  // reading. The sentence names the product that carries it, so the refusal is one edit away from a
@@ -752,8 +754,26 @@ export function validateJob(job, { atClaim = false } = {}) {
752
754
  let known = false;
753
755
  let roster = [];
754
756
  try {
755
- const profiles = loadProfiles();
756
- known = profiles.has(String(job.profileKey).trim());
757
+ const key = String(job.profileKey).trim();
758
+ let profiles = loadProfiles();
759
+ known = profiles.has(key);
760
+ // THE ROSTER MOVES WHILE THIS PROCESS RUNS, and until this re-read it could not see that.
761
+ //
762
+ // `loadProfiles()` answers from a module cache with no expiry and no invalidation hook, filled
763
+ // when this process started. The engine door is a long-lived process of its own, so a company
764
+ // created through the portal AFTER it booted was refused here — "names no known customer" — while
765
+ // its file sat on disk and the portal had already told the person they could search under it.
766
+ // That is the whole of the defect the credential re-mint was meant to remove: the grant wall
767
+ // stopped refusing and this did not, so the person met the same dead end one step further in.
768
+ //
769
+ // ON THE MISS ONLY, so the ordinary request pays nothing: a hit is answered from the cache exactly
770
+ // as before, and a directory read happens just when the alternative is a wrong refusal. This is
771
+ // what "the door checks the company exists on disk now" has to mean for a process that has been
772
+ // up for a week.
773
+ if (!known) {
774
+ profiles = loadProfiles({ force: true });
775
+ known = profiles.has(key);
776
+ }
757
777
  roster = [...profiles.keys()].sort();
758
778
  } catch { known = true; }
759
779
  // NAME THE ROSTER THIS PROCESS CAN SEE, always.