clearotron 0.4.0-beta.0 → 0.4.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/.env.example +9 -0
  2. package/INSTALL.md +4 -31
  3. package/README.md +5 -5
  4. package/bin/brandowner.mjs +1 -1
  5. package/bin/onboard.mjs +18 -74
  6. package/bin/start.mjs +58 -14
  7. package/bin/update.mjs +10 -2
  8. package/build-info.json +2 -2
  9. package/demo/full-country-search/run/_driver/search-policy.json +1 -1
  10. package/demo/full-country-search/run/status.json +1 -1
  11. package/demo/global-preliminary-search/run/_driver/search-policy.json +1 -1
  12. package/demo/global-preliminary-search/run/status.json +1 -1
  13. package/demo/knockout-search/run/_driver/search-policy.json +1 -1
  14. package/demo/knockout-search/run/status.json +1 -1
  15. package/demo/multi-country-focus-search/run/_driver/search-policy.json +1 -1
  16. package/demo/multi-country-focus-search/run/status.json +1 -1
  17. package/docs/README.md +1 -0
  18. package/docs/SECURITY-OWASP.md +298 -0
  19. package/docs/SECURITY.md +18 -13
  20. package/docs/architecture/02-architecture.md +5 -5
  21. package/docs/architecture/03-run-lifecycle.md +28 -29
  22. package/docs/architecture/04-configuration-reference.md +5 -10
  23. package/docs/architecture/05-config-governance.md +5 -7
  24. package/docs/architecture/06-operations-runbook.md +0 -12
  25. package/docs/architecture/07-quality-and-audit.md +12 -13
  26. package/docs/architecture/09-security-and-data.md +9 -4
  27. package/driver/CHANGELOG.md +70 -0
  28. package/driver/ask-ledger.mjs +4 -2
  29. package/driver/authority-trees.mjs +90 -8
  30. package/driver/band-shape.mjs +13 -13
  31. package/driver/blind-frame-model.mjs +1 -1
  32. package/driver/case-law-ledger.mjs +19 -3
  33. package/driver/claim-liveness.mjs +30 -1
  34. package/driver/clearance-variants-record.mjs +10 -3
  35. package/driver/common-law-receipts.mjs +64 -14
  36. package/driver/commonlaw-carry.mjs +2 -2
  37. package/driver/compare.mjs +2 -2
  38. package/driver/connotation-search.mjs +33 -104
  39. package/driver/contract-arm2-baseline.json +6 -5
  40. package/driver/contract-e3-backlog.mjs +62 -62
  41. package/driver/contract-vocabulary.mjs +38 -31
  42. package/driver/coverage-form-io.mjs +13 -1
  43. package/driver/coverage-form.mjs +42 -4
  44. package/driver/coverage-ledger.mjs +3 -5
  45. package/driver/cross-check-wait.mjs +43 -0
  46. package/driver/declination-call.mjs +34 -10
  47. package/driver/declination-tool.mjs +27 -15
  48. package/driver/deferral-row.mjs +139 -0
  49. package/driver/degraded-parts.mjs +377 -0
  50. package/driver/deliver-trigger.sh +1 -1
  51. package/driver/demo-container.mjs +4 -1
  52. package/driver/dev-portal.mjs +5 -5
  53. package/driver/dispatch-record.mjs +2 -2
  54. package/driver/driver.config.mjs +193 -96
  55. package/driver/e2e/README.md +1 -1
  56. package/driver/effective-scope.mjs +58 -2
  57. package/driver/engine/anthropic-agent.mjs +103 -27
  58. package/driver/engine/auth.mjs +2 -2
  59. package/driver/engine/child-record.mjs +11 -0
  60. package/driver/engine/cli-version.mjs +4 -3
  61. package/driver/engine/common.mjs +17 -2
  62. package/driver/engine/deny-authority-write.mjs +3 -1
  63. package/driver/engine/engine-env.mjs +174 -0
  64. package/driver/engine/engine-spawn.mjs +179 -0
  65. package/driver/engine/mcp/band-server.mjs +1 -1
  66. package/driver/engine/mcp/clarivate-server.mjs +2 -1
  67. package/driver/engine/mcp/codex-config.mjs +112 -3
  68. package/driver/engine/mcp/corsearch-server.mjs +1 -0
  69. package/driver/engine/mcp/declination-server.mjs +9 -8
  70. package/driver/engine/mcp/dispositions-server.mjs +2 -1
  71. package/driver/engine/mcp/euipo-server.mjs +1 -0
  72. package/driver/engine/mcp/fetch-server.mjs +14 -11
  73. package/driver/engine/mcp/free-tier-server.mjs +1 -1
  74. package/driver/engine/mcp/gather-config.mjs +23 -11
  75. package/driver/engine/mcp/perplexity-server.mjs +97 -17
  76. package/driver/engine/mcp/probe-server.mjs +43 -21
  77. package/driver/engine/mcp/public-fetch.mjs +195 -0
  78. package/driver/engine/mcp/recording-server.mjs +79 -14
  79. package/driver/engine/mcp/signa-server.mjs +1 -0
  80. package/driver/engine/mcp/supplemental.mjs +14 -14
  81. package/driver/engine/mcp/unit-note-server.mjs +50 -1
  82. package/driver/engine/mcp/uspto-local-server.mjs +1 -0
  83. package/driver/engine/openai-agent.mjs +109 -28
  84. package/driver/engine/probe.mjs +152 -19
  85. package/driver/enqueue-schema.mjs +5 -5
  86. package/driver/feedback-issues.mjs +1 -1
  87. package/driver/feedback-store.mjs +1 -1
  88. package/driver/findings-model.mjs +67 -16
  89. package/driver/flag-snapshot.mjs +1 -1
  90. package/driver/floor-duty.mjs +23 -7
  91. package/driver/form-neighbourhood.mjs +52 -17
  92. package/driver/frame-diff-model.mjs +3 -3
  93. package/driver/framework-method.mjs +304 -0
  94. package/driver/gateway.mjs +49 -21
  95. package/driver/hand-off-exits.mjs +129 -0
  96. package/driver/jx-lanes.mjs +1 -1
  97. package/driver/jx.mjs +8 -5
  98. package/driver/knockout-assess-record.mjs +7 -1
  99. package/driver/knockout-frame-record.mjs +96 -34
  100. package/driver/knockout-review-record.mjs +7 -2
  101. package/driver/log.mjs +2 -2
  102. package/driver/matter-frame-record.mjs +31 -2
  103. package/driver/methodology-witness.mjs +6 -3
  104. package/driver/named-band.mjs +1 -1
  105. package/driver/owner-use-check.mjs +45 -10
  106. package/driver/package.json +1 -1
  107. package/driver/partial-payload-baseline.json +4 -0
  108. package/driver/phase0.mjs +2 -2
  109. package/driver/pipeline-knockout.mjs +256 -129
  110. package/driver/pipeline.mjs +417 -614
  111. package/driver/placement-carry.mjs +22 -3
  112. package/driver/placement-form-io.mjs +18 -4
  113. package/driver/placement-form.mjs +53 -4
  114. package/driver/placement-union.mjs +32 -5
  115. package/driver/portal-report.mjs +3 -3
  116. package/driver/portal-service.mjs +6 -5
  117. package/driver/portal-static.mjs +11 -18
  118. package/driver/predelivery-lint.mjs +10 -10
  119. package/driver/profile-page.html +5 -5
  120. package/driver/profiles.mjs +5 -5
  121. package/driver/progress.mjs +6 -4
  122. package/driver/provider-usage.mjs +25 -2
  123. package/driver/publish/index.mjs +97 -25
  124. package/driver/publish/knockout.mjs +136 -14
  125. package/driver/publish/profiles-page.mjs +2 -0
  126. package/driver/publish/publish-inputs.mjs +10 -4
  127. package/driver/publish/render-knockout.mjs +48 -21
  128. package/driver/publish/render.mjs +13 -8
  129. package/driver/publish/report-data.mjs +2 -0
  130. package/driver/publish/search-depth.mjs +77 -4
  131. package/driver/publish/templates/report.css +1 -1
  132. package/driver/publish/xlsx.mjs +34 -15
  133. package/driver/reasoning-tripwires.mjs +2 -160
  134. package/driver/recall-receipt.mjs +27 -0
  135. package/driver/recall-reconciliation.mjs +1 -1
  136. package/driver/record-carry.mjs +14 -14
  137. package/driver/recording-agreement.mjs +2 -2
  138. package/driver/reference-score.mjs +169 -47
  139. package/driver/register-count.mjs +61 -4
  140. package/driver/register-digest-record.mjs +14 -1
  141. package/driver/register-plan.mjs +161 -80
  142. package/driver/register-records.mjs +37 -4
  143. package/driver/registration-scripts.mjs +22 -0
  144. package/driver/registry-fidelity.mjs +4 -4
  145. package/driver/repair-composers.mjs +23 -7
  146. package/driver/repair-contract.mjs +1 -1
  147. package/driver/repairs.mjs +5 -2
  148. package/driver/reviewer-open-points.mjs +33 -0
  149. package/driver/rule-shape.mjs +8 -8
  150. package/driver/run-economics.mjs +3 -3
  151. package/driver/run-integrity.mjs +2 -2
  152. package/driver/runner.mjs +17 -5
  153. package/driver/scope-facts.mjs +89 -12
  154. package/driver/score-redaction.mjs +247 -0
  155. package/driver/screen-gate.mjs +1 -1
  156. package/driver/search-policy.mjs +17 -0
  157. package/driver/skills/README.md +2 -2
  158. package/driver/skills/blind-frame/SKILL.md +2 -2
  159. package/driver/skills/clearance-common-law/SKILL.md +29 -36
  160. package/driver/skills/clearance-common-law/perplexity-prompts.md +8 -30
  161. package/driver/skills/clearance-register/SKILL.md +15 -16
  162. package/driver/skills/clearance-register/digest.md +15 -31
  163. package/driver/skills/clearance-register/register-recipes.md +14 -56
  164. package/driver/skills/clearance-register/unit.md +37 -48
  165. package/driver/skills/clearance-search/SKILL.md +6 -8
  166. package/driver/skills/clearance-search/delivery-contract.md +0 -7
  167. package/driver/skills/clearance-search/firm-wide-reasoning.md +5 -4
  168. package/driver/skills/clearance-search/phase2-execution.md +5 -6
  169. package/driver/skills/clearance-search/report-prose.md +5 -5
  170. package/driver/skills/clearance-search/risk-framework-triage.md +2 -2
  171. package/driver/skills/clearance-search/synthesis-rules.md +9 -9
  172. package/driver/skills/clearance-search/template-formatting.md +2 -2
  173. package/driver/skills/clearance-variants/SKILL.md +8 -8
  174. package/driver/skills/clearance-variants/transliteration-scripts.md +2 -2
  175. package/driver/skills/frame-diff/SKILL.md +2 -2
  176. package/driver/skills/knockout-assess/SKILL.md +40 -10
  177. package/driver/skills/knockout-frame/SKILL.md +42 -10
  178. package/driver/skills/matter-frame/SKILL.md +12 -5
  179. package/driver/skills/matter-frame/watchlist-reference.md +1 -1
  180. package/driver/skills/narrative-refutation/SKILL.md +5 -3
  181. package/driver/skills/placement-inquiry/SKILL.md +5 -1
  182. package/driver/stage-context.mjs +27 -6
  183. package/driver/stages-knockout.mjs +92 -79
  184. package/driver/stages.mjs +111 -68
  185. package/driver/stray-artifacts.mjs +9 -5
  186. package/driver/suite-census.json +696 -150
  187. package/driver/synthesis-record.mjs +26 -4
  188. package/driver/systemd/README.md +5 -6
  189. package/driver/systemd/clearotron-client-mcp.service +24 -0
  190. package/driver/systemd/clearotron-mcp-face.service +24 -0
  191. package/driver/systemd/clearotron-portal.service +24 -0
  192. package/driver/systemd/clearotron-worker.service +29 -0
  193. package/driver/tokens.mjs +10 -9
  194. package/driver/turnaround-bands.mjs +1 -1
  195. package/driver/unit-inventory.mjs +24 -43
  196. package/driver/variant-manifest-model.mjs +22 -3
  197. package/driver/verify-knockout.mjs +163 -4
  198. package/driver/verify.mjs +42 -16
  199. package/driver/web-grid.mjs +150 -0
  200. package/driver/whatif-memo-run.mjs +1 -1
  201. package/driver/withheld-families.mjs +113 -4
  202. package/driver/worker-heartbeat.mjs +13 -2
  203. package/mcp-server/CHANGELOG.md +8 -0
  204. package/mcp-server/README.md +1 -1
  205. package/mcp-server/lib/knockout.mjs +1 -1
  206. package/mcp-server/lib/ops.mjs +10 -10
  207. package/mcp-server/lib/scrub.mjs +1 -1
  208. package/mcp-server/lib/trace.mjs +3 -2
  209. package/mcp-server/package.json +1 -1
  210. package/mcp-server/server.mjs +3 -3
  211. package/package.json +1 -1
  212. package/portal-ui/dist/assets/{index-DVtz44vH.js → index-BPAUjcI0.js} +3 -2
  213. package/portal-ui/dist/assets/{index-5CCwiJG7.css → index-D2wrw9cH.css} +18 -2
  214. package/portal-ui/dist/assets/plus-jakarta-sans-BUCHxqJ-.woff2 +0 -0
  215. package/portal-ui/dist/index.html +2 -15
  216. package/portal-ui/package.json +1 -1
  217. package/providers/_shared/README.md +1 -1
  218. package/providers/_shared/answer-memory.mjs +199 -0
  219. package/providers/_shared/enumerate.mjs +155 -50
  220. package/providers/_shared/execute-plan.mjs +62 -16
  221. package/providers/_shared/ledger-path.mjs +1 -1
  222. package/providers/_shared/ledger.mjs +48 -6
  223. package/providers/_shared/plan-guards.mjs +7 -0
  224. package/providers/_shared/script-form.mjs +24 -5
  225. package/providers/_shared/term-shape.mjs +5 -5
  226. package/providers/clarivate/src/capabilities.js +11 -0
  227. package/providers/clarivate/src/core.js +193 -18
  228. package/providers/corsearch/README.md +1 -2
  229. package/providers/corsearch/src/core.js +2 -2
  230. package/providers/jx-subclass/lookup.mjs +1 -1
  231. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  232. package/providers/oauth-mcp-bridge/README.md +8 -7
  233. package/providers/oauth-mcp-bridge/bridge.mjs +21 -5
  234. package/providers/oauth-mcp-bridge/package.json +1 -1
  235. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  236. package/providers/oauth-mcp-bridge/warm-server.mjs +16 -5
  237. package/providers/perplexity/src/core.js +246 -21
  238. package/providers/signa/src/capabilities.js +24 -0
  239. package/providers/signa/src/core.js +93 -25
  240. package/scripts/README.md +2 -1
  241. package/scripts/added-reference-check.mjs +2 -1
  242. package/scripts/backup-recall-stores.mjs +5 -5
  243. package/scripts/demo-evidence.mjs +2 -1
  244. package/scripts/deprecate-below.mjs +114 -2
  245. package/scripts/drive-env-check.mjs +2 -1
  246. package/scripts/e2e-first-time.mjs +469 -0
  247. package/scripts/e2e-scenario-ops.mjs +441 -0
  248. package/scripts/e2e.mjs +104 -16
  249. package/scripts/env-audit.mjs +8 -0
  250. package/scripts/freeze-example-run.mjs +7 -7
  251. package/scripts/generated-files-are-current.mjs +2 -1
  252. package/scripts/hand-off-exits-probe.mjs +75 -0
  253. package/scripts/import-cycle-check.mjs +2 -1
  254. package/scripts/merge-shape-check.mjs +2 -1
  255. package/scripts/package-size-budget.mjs +2 -1
  256. package/scripts/record-carry-probe.mjs +1 -1
  257. package/scripts/release-entry-catch-up.mjs +77 -8
  258. package/scripts/release-install-check.mjs +4 -1
  259. package/scripts/release-note-required.mjs +139 -9
  260. package/scripts/release-rehearsal-version.mjs +60 -0
  261. package/scripts/release-sbom.mjs +104 -0
  262. package/scripts/release-visible-check.mjs +7 -5
  263. package/scripts/render-check.mjs +5 -2
  264. package/scripts/report-offline-render-check.mjs +169 -0
  265. package/scripts/score.mjs +79 -8
  266. package/scripts/strip-titles-and-attributions.mjs +2 -1
  267. package/scripts/strip-tracker-citations.mjs +2 -1
  268. package/scripts/test-full.mjs +2 -1
  269. package/scripts/test-run.mjs +10 -3
  270. package/scripts/third-party-notices.mjs +3 -1
  271. package/scripts/travelling-predicates.mjs +1 -1
  272. package/scripts/writing-standard-check.mjs +2 -1
  273. package/shared/brand-fonts.mjs +59 -0
  274. package/shared/brand.mjs +6 -6
  275. package/shared/browser-temp-root.mjs +3 -2
  276. package/shared/doctrine-overlay.mjs +1 -1
  277. package/shared/driver-dir.mjs +47 -11
  278. package/shared/fonts/OFL-fira-code.txt +93 -0
  279. package/shared/fonts/OFL-plus-jakarta-sans.txt +93 -0
  280. package/shared/fonts/README.md +37 -0
  281. package/shared/fonts/fira-code.woff2 +0 -0
  282. package/shared/fonts/plus-jakarta-sans.woff2 +0 -0
  283. package/shared/names-in-force.mjs +2 -0
  284. package/shared/npm-cli.mjs +23 -0
  285. package/shared/os-advice.mjs +14 -2
  286. package/shared/path-seps.mjs +42 -0
  287. package/shared/process-table.mjs +71 -3
  288. package/shared/reference-guard-classes.mjs +30 -6
  289. package/shared/root-doc-commands.mjs +6 -2
  290. package/shared/running-start.mjs +24 -7
  291. package/shared/scope.mjs +2 -2
  292. package/shared/wsl.mjs +1 -1
  293. package/driver/known-conflicts.mjs +0 -327
@@ -7,13 +7,14 @@
7
7
  // Every value is env-overridable so the identical code runs from a developer's shell and from the
8
8
  // systemd unit on a deployed host.
9
9
 
10
- import { join, dirname, basename, isAbsolute, delimiter } from "node:path";
10
+ import { join, dirname, basename, isAbsolute, win32 } from "node:path";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { readdirSync, existsSync, accessSync, statSync, statfsSync, readFileSync, realpathSync, openSync, readSync, closeSync, constants as FS } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import { isWsl } from "../shared/wsl.mjs"; // — the one answer to "is this Linux under Windows", which decides the /mnt/<drive> skip
15
15
  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
16
16
  import { invoke } from "../shared/invocation.mjs"; // — name a command the reader can actually type
17
+ import { sepClass, notSepClass, hasSep } from "../shared/path-seps.mjs"; // a Windows path is built with "\"
17
18
  import { envFileRead } from "../shared/env-local.mjs"; // — WHICH file to set it in, measured; null for a service that read none
18
19
  import { numericSetting, resolveNumericSetting } from "./numeric-setting.mjs"; import { STUDIO_SEGMENTS, STUDIO_SEGMENT_RE, studioSegmentFor } from "../shared/pre-rename-spellings.mjs"; export { STUDIO_SEGMENTS, STUDIO_SEGMENT_RE, studioSegmentFor }; // the studio segment an install keeps its runs under // — a number, or a refusal that names the variable; never NaN
19
20
 
@@ -50,8 +51,9 @@ const home = () => homedir();
50
51
  // CLEAROTRON_SKILLS_STORE_STRICT (skills-store-provenance.mjs). There was a fourth, CLEAROTRON_SELFTEST_PATHS_ONLY,
51
52
  // and it died with driver/selftest.mjs. The first two are here because they LIED, the third because it was written
52
53
  // after this note and a new gate has no excuse to spell itself a fifth way. Roughly
53
- // FIVE gates are still inline `!== "0"`: CLEAROTRON_BAND_TRUTH_GATE, CLEAROTRON_REGISTER_GAP_CLAMP,
54
- // CLEAROTRON_RECALL_TRIPWIRE, CLEAROTRON_WARM_RETRY, CLEAROTRON_SATPROBE_CODESIDE. Those are CORRECT — `0`
54
+ // FOUR gates are still inline `!== "0"`: CLEAROTRON_BAND_TRUTH_GATE, CLEAROTRON_REGISTER_GAP_CLAMP,
55
+ // CLEAROTRON_WARM_RETRY, CLEAROTRON_SATPROBE_CODESIDE. (CLEAROTRON_RECALL_TRIPWIRE was a fifth, and went
56
+ // with the recall store on 2026-09-24.) Those are CORRECT — `0`
55
57
  // disables each, which is what they document — they simply do not accept `off`/`false`/`no`.
56
58
  //
57
59
  // IT WAS TEN. Three went with their knobs: CLEAROTRON_TAINT_CHAIN, CLEAROTRON_XCHECK and
@@ -60,9 +62,10 @@ const home = () => homedir();
60
62
  // rollback. The ones that remain stay because the test corpus sets them, and two of them are how a
61
63
  // hermetic suite keeps from dialling a real provider.
62
64
  //
63
- // CLEAROTRON_RECALL_PROBES was nearly a fourth and is the cautionary one: five test files disable it with
64
- // `||=`, and the census that cleared it for deletion looked for a plain `=` after the name. Deleting
65
- // it armed the probes in all five. A measurement that cannot see a form reports clean.
65
+ // CLEAROTRON_RECALL_PROBES (also gone with the recall store) was nearly a fourth and is the cautionary
66
+ // one: five test files disabled it with `||=`, and the census that cleared it for deletion looked for a
67
+ // plain `=` after the name. Deleting it armed the probes in all five. A measurement that cannot see a form
68
+ // reports clean.
66
69
  //
67
70
  // Anyone extending the accessors to the rest must update the note in .env.example's conventions header
68
71
  // in the same change — a header that claims more coverage than the code has is the defect this whole
@@ -304,8 +307,8 @@ export const config = {
304
307
 
305
308
  // ── Per-agent paths ───────────────────────────────────────────────────────
306
309
  // A clearotron job is enqueued into the FORWARDING agent's OWN workspace queue (that agent's `write` tool
307
- // is sandboxed there), and we run the whole pipeline as that same agent so the reply tool
308
- // (`clawdi_send_<user>`), the run-dir, and the agent's memory all line up. The queue LOCATION therefore
310
+ // is sandboxed there), and we run the whole pipeline as that same agent so the reply tool,
311
+ // the run-dir, and the agent's memory all line up. The queue LOCATION therefore
309
312
  // encodes the agent identity — the driver derives {agentId, studioRoot} from where it claimed the job,
310
313
  // so there is no forwarder→agent map to keep in sync. Delivery is still centralized (poolRoot below).
311
314
  // The per-agent directory prefix under workspaceRoot. Inherited as a bare `workspace-` layout; now ONE
@@ -339,9 +342,10 @@ export const config = {
339
342
  return join(this.studioRootForAgent(agentId), "archive");
340
343
  },
341
344
  // …/<prefix><id>/studio/<segment>/queue → "<id>"; null if the path isn't an agent queue dir. Either
342
- // spelling of the segment, because an install keeps the one it has.
343
- agentIdFromQueueDir(qdir) {
344
- const m = new RegExp(`(?:^|/)${this.workspacePrefixRe}([^/]+)/studio/${STUDIO_SEGMENT_RE}/queue/?$`).exec(qdir);
345
+ // spelling of the segment, because an install keeps the one it has, and either separator on Windows.
346
+ agentIdFromQueueDir(qdir, { platform = process.platform } = {}) {
347
+ const [S, N] = [sepClass(platform), notSepClass(platform)];
348
+ const m = new RegExp(`(?:^|${S})${this.workspacePrefixRe}(${N}+)${S}studio${S}${STUDIO_SEGMENT_RE}${S}queue${S}?$`).exec(qdir);
345
349
  return m ? m[1] : null;
346
350
  },
347
351
  // Every agent workspace's clearotron queue. The systemd `.path` watches these and the runner drains ALL of
@@ -369,38 +373,45 @@ export const config = {
369
373
  }
370
374
  }
371
375
  } catch { /* workspaceRoot may not exist in some test envs — fall through to the canonical queue */ }
372
- // THE ONE AGENT NAME NO CONFIGURATION REMOVES, and it is deliberate. row 5.
376
+ // THE INSTALL'S OWN QUEUE IS ALWAYS WATCHED, AND IT USED TO BE ONE PARTICULAR AGENT'S. `this.queueDir`
377
+ // was `queueDirForAgent("<the old default>")` — a LITERAL, not `defaultAgent` — so every deployment,
378
+ // however configured, watched that one directory as well as its own. It was back-compat for a real
379
+ // legacy queue, and its own note said removing it was a SEQUENCED MIGRATION rather than an edit:
380
+ // drain that queue, or name it explicitly with CLEAROTRON_QUEUE_DIR, and only then drop the literal.
373
381
  //
374
- // `this.queueDir` is `queueDirForAgent("clawdi")` — a LITERAL, not `defaultAgent` — so every
375
- // deployment, however configured, watches `<workspacePrefix>clawdi/studio/clearance-search/queue`.
376
- // Setting CLEAROTRON_DEFAULT_AGENT does not remove it (prod runs `ops`, dev runs `dev`, and both still
377
- // watch this); nor does CLEAROTRON_WORKSPACE_PREFIX. An installer who copies `.env.example` gets a
378
- // neutral default agent AND this directory.
382
+ // BOTH PRECONDITIONS WERE MEASURED BEFORE IT WENT (2026-09-27). Every install that runs a clearance
383
+ // unit sets CLEAROTRON_QUEUE_DIR, and holds nothing in that legacy directory. One retired deployment
384
+ // still has 23 files there; classified against this product's own live-marker rule, NONE is live —
385
+ // ten results, ten done markers and three hand-made probe files, newest a month old — that deployment
386
+ // runs no clearance unit, and the path resolved per-install anyway, so no other deployment could ever
387
+ // have reached it.
379
388
  //
380
- // WHY IT STAYS. It is back-compat for a real legacy queue that still holds work. Dropping the
381
- // literal stops the deployment that owns that queue draining it — silently, because an unwatched
382
- // queue looks exactly like an empty one. Removing it is therefore a SEQUENCED MIGRATION (drain the
383
- // legacy queue, or name it explicitly via CLEAROTRON_QUEUE_DIR, then drop this), not an edit: it needs
384
- // the box, and no repository-only change can do it safely.
385
- //
386
- // Written down because a sweep of this platform name will find it again and read it as residue.
387
- // It is not residue; it is the one instance whose removal has a cost outside this repository.
389
+ // What remains is the honest version of what the literal was for: the queue of the agent this
390
+ // install actually runs as, appended so a deployment whose own queue is not among the scanned
391
+ // workspaces still drains it.
388
392
  const canonical = this.queueDir;
389
393
  if (!dirs.includes(canonical)) dirs.push(canonical);
390
394
  return [...new Set(dirs)]; // CLEAROTRON_QUEUE_DIR may equal a scanned workspace queue
391
395
  },
392
396
 
393
- // Back-compat defaults for the legacy agent (selftest / docs / CLI without a derived agent), pinned
394
- // for the reason written at `queueDirs` above. Per-run code uses the *ForAgent helpers above with the
395
- // agent derived from the claimed queue dir.
397
+ // Defaults for a caller with no derived agent (selftest, docs, a CLI invocation): the agent this
398
+ // install runs as, which is what CLEAROTRON_DEFAULT_AGENT says and `localagent` when it says nothing.
399
+ // They named one particular agent as a literal until 2026-09-27; see `queueDirs` above for what that
400
+ // was for and what was measured before it went. Per-run code uses the *ForAgent helpers with the agent
401
+ // derived from the claimed queue dir, and is unaffected either way.
402
+ //
403
+ // THIS MOVES NO PATH ON A DEPLOYMENT THAT SETS ITS OWN AGENT, which is the point: the two installs
404
+ // whose default agent is the id this replaced resolve to exactly the directories they resolved to
405
+ // before, because that is what their own configuration says. An install that sets nothing moves to its
406
+ // own default, where its runs already are.
396
407
  get studioRoot() {
397
- return this.studioRootForAgent("clawdi");
408
+ return this.studioRootForAgent(this.defaultAgent);
398
409
  },
399
410
  get queueDir() {
400
- return this.queueDirForAgent("clawdi");
411
+ return this.queueDirForAgent(this.defaultAgent);
401
412
  },
402
413
  get archiveRoot() {
403
- return this.archiveRootForAgent("clawdi");
414
+ return this.archiveRootForAgent(this.defaultAgent);
404
415
  },
405
416
 
406
417
  // Delivery pool: the deterministic publish step writes report.html + audit.xlsx here; Caddy serves it
@@ -617,20 +628,20 @@ export const config = {
617
628
  //
618
629
  // THE DEFAULT IS PART OF A PATH, so changing it moves where an install looks for its own runs:
619
630
  // every run dir is `<workspaceRoot>/workspace-<agent>/studio/clearance-search/…`. An install created
620
- // before this default changed keeps its runs under the old id and must pin it — both spellings,
621
- // because the gather servers read their own variable:
622
- //
623
- // CLEAROTRON_DEFAULT_AGENT=clawdi
624
- // CLEAROTRON_GATHER_AGENT=clawdi
631
+ // before this default changed keeps its runs under the id it was created with, and must set both
632
+ // CLEAROTRON_DEFAULT_AGENT and CLEAROTRON_GATHER_AGENT to that id — both, because the gather servers
633
+ // read their own variable. Unpinned, such an install starts a fresh empty workspace and its existing
634
+ // runs read as absent rather than as an error.
625
635
  //
626
- // Unpinned, such an install starts a fresh empty workspace and its existing runs read as absent
627
- // rather than as an error. Said again where upgrades are described (INSTALL.md, ops runbook).
636
+ // THE ID IS NO LONGER WRITTEN DOWN ANYWHERE FOR THEM (ruling 563, 2026-09-27): what this tree knows is
637
+ // that the id is a path segment, not which id any particular install was created with. An operator who
638
+ // needs it reads the name of the workspace directory their runs are in. The release note for that
639
+ // ruling carries the instruction; this comment carries the reason.
628
640
  //
629
- // THE ID IS ALSO A KEY, not only a path segment. `AGENT_WHATSAPP` in stages.mjs is keyed by agent
630
- // id, so the demo roster has to carry this value or the reference deployment resolves no operator
631
- // number and the hole reads as "no copy configured" rather than as a miss. Both ids are in that
632
- // roster for the same reason the pin above exists. (The roster stays one line: 32 backlog rows
633
- // cite line numbers in stages.mjs, so a comment added there repoints all of them.)
641
+ // THE ID IS ALSO A KEY, not only a path segment. `AGENT_WHATSAPP` in stages.mjs is keyed by agent id,
642
+ // so a deployment whose agent is not in that roster resolves no operator number and the hole reads as
643
+ // "no copy configured" rather than as a miss. (The roster stays one line: 32 backlog rows cite line
644
+ // numbers in stages.mjs, so a comment added there repoints all of them.)
634
645
  get defaultAgent() { return process.env.CLEAROTRON_DEFAULT_AGENT || "localagent"; },
635
646
 
636
647
  // Per-stage retry budget (fresh session key per retry).
@@ -668,13 +679,12 @@ export const config = {
668
679
  // Owner's ruling, 2026-09-23: record the tier. What a run asked for is a tier, what it was served is
669
680
  // recorded separately and already is, and the report names the model that ran — none of that moves.
670
681
  //
671
- // WHAT IT COSTS, RULED ON AND ACCEPTED RATHER THAN DISCOVERED LATER. Per-model totals are keyed on what
672
- // was ASKED for, and the direct-API lanes must name a version because they call the API rather than the
673
- // program — the API takes model ids, not tier words. So one model reached by both routes now lands in
674
- // two buckets: `anthropic/claude-haiku` from a stage, `anthropic/claude-haiku-4-5` from those lanes.
675
- // That is a real split in a per-model total and it was accepted with the ruling: the two are genuinely
676
- // different requests, and keying the totals on what actually SERVED each turn is the change that would
677
- // fix it properly, which is larger than this and not what was ruled.
682
+ // WHAT IT COSTS, RULED ON AND ACCEPTED RATHER THAN DISCOVERED LATER. A stage's rows are keyed on the
683
+ // tier it asked for, and a native-language lane's rows on the model that served them (`modelKey` in
684
+ // tokens.mjs). So one model reached both ways lands in two buckets: `anthropic/claude-haiku` from a
685
+ // stage, `anthropic/claude-haiku-4-5` from a native-language lane. That is a real split in a per-model
686
+ // total and it was accepted with the ruling; keying every row on what served it is the change that
687
+ // would close it, which is larger than this and not what was ruled.
678
688
  export const MODELS = {
679
689
  haiku: "anthropic/claude-haiku",
680
690
  sonnet: "anthropic/claude-sonnet",
@@ -1319,7 +1329,15 @@ export const PROVIDERS = {
1319
1329
  // The office's own numbers come off the full record Signa returns on search, through the
1320
1330
  // provider's own normaliser, so publish can address the office's page for each filing
1321
1331
  // (publish/office-record-links.mjs) instead of showing the handle.
1322
- return { ok: true, records: (Array.isArray(p.results) ? p.results : []).map((row) => {
1332
+ //
1333
+ // THE TOTAL RIDES THE SAME ANSWER and is carried, not dropped: `include_total` puts the register's
1334
+ // own count for this exact question on every search, so the listing of a term already holds the
1335
+ // figure the count lane would ask for separately. An approximation stays one: no number in
1336
+ // `total`, the floor beside it — the count lane's rule, unchanged.
1337
+ return { ok: true,
1338
+ total: Number.isFinite(p.total_hits) ? p.total_hits : null,
1339
+ ...(p.total_approximate === true ? { approximate: true, floor: Number.isFinite(p.total_floor) ? p.total_floor : null } : {}),
1340
+ records: (Array.isArray(p.results) ? p.results : []).map((row) => {
1323
1341
  const rec = row?.raw && typeof row.raw === "object" ? core.normalizeRecord(row.raw, row.office || null) : null;
1324
1342
  return {
1325
1343
  record_id: row?.record_id ?? null,
@@ -1660,12 +1678,16 @@ export function activeProvider(env = null) {
1660
1678
 
1661
1679
 
1662
1680
  // ── Research providers (Phase 2a, knockout lane) — the same lazy-import + cred-guard idiom as
1663
- // PROVIDERS, for CODE-side research calls (no model in the data path). The knockout sweep's live
1664
- // executor; tests inject their own, the dev instance uses CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES. Knobs:
1665
- // PERPLEXITY_API_KEY — required for live sweeps (absent ⇒ every call degrades, loud)
1666
- // CLEAROTRON_KNOCKOUT_PRESET — perplexity preset (default "pro-search"; "deep-research" = deeper/costlier)
1667
- // CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES — dir of <mark-kebab>.md canned payloads ($0 dev/e2e)
1681
+ // PROVIDERS, for CODE-side research calls (no model in the data path). The knockout's live executors:
1682
+ // `grid` runs one mark's web search as a dictated grid, `research` asks one question (the owner lookups).
1683
+ // Tests inject their own, the dev instance uses CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES. Knobs:
1684
+ // PERPLEXITY_API_KEY — required for live calls (absent ⇒ the web half is skipped, loud)
1685
+ // CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES — dir of canned answers ($0 dev/e2e): <mark-kebab>.grid.json, the
1686
+ // grid program's printed ledger for a mark, and <mark-kebab>.md,
1687
+ // the answer to a question asked about that mark
1668
1688
  // CLEAROTRON_KNOCKOUT_MODEL — LLM override for the frame/assess turns (default opus)
1689
+ // The preset and the reasoning setting are not knobs: they are pinned in stages-knockout.mjs, so no
1690
+ // environment can quietly move them.
1669
1691
  export const RESEARCH_PROVIDERS = {
1670
1692
  perplexity: {
1671
1693
  // The vendor's own name, for the staff config page's provider inventory (config-inventory.mjs).
@@ -1689,8 +1711,11 @@ export const RESEARCH_PROVIDERS = {
1689
1711
  const body = core.buildRequestBody({ task, preset });
1690
1712
  const data = await core.callAgentAPI(process.env.PERPLEXITY_API_KEY, body, { retries: core.retriesForPreset(preset) });
1691
1713
  const text = core.formatResponse(data, preset);
1692
- if (!text || !String(text).trim()) return { ok: false, cause: "empty research response" };
1693
- return { ok: true, text: String(text), bytes: Buffer.byteLength(String(text)), tookMs: Date.now() - started };
1714
+ // The searches behind the answer, kept for the caller to write into the run: the answer alone
1715
+ // cannot say which pages were retrieved.
1716
+ const raw = core.rawResultItems(data);
1717
+ if (!text || !String(text).trim()) return { ok: false, cause: "empty research response", raw };
1718
+ return { ok: true, text: String(text), bytes: Buffer.byteLength(String(text)), tookMs: Date.now() - started, raw };
1694
1719
  } catch (e) {
1695
1720
  // OUTAGE-SHAPED OR NOT — the caller cannot tell from prose, so say it here.
1696
1721
  //
@@ -1716,6 +1741,38 @@ export const RESEARCH_PROVIDERS = {
1716
1741
  };
1717
1742
  }
1718
1743
  },
1744
+ /**
1745
+ * ONE DICTATED GRID, THROUGH THE CLEARANCE GRID'S OWN FUNCTIONS: the program task built from the spec,
1746
+ * the sandbox call, and the capture that reconciles what the program printed against every cell the
1747
+ * spec asked for. Nothing is summarised; the ledger is what came back.
1748
+ *
1749
+ * The outage rule is `research`'s, for the same reason: 429 and 5xx park, anything else is loud.
1750
+ */
1751
+ async grid(spec, { preset = "pro-search", reasoning = null } = {}) {
1752
+ if (!process.env.PERPLEXITY_API_KEY) return { ok: false, cause: "PERPLEXITY_API_KEY absent from driver env" };
1753
+ let core;
1754
+ try { core = await import("../providers/perplexity/src/core.js"); }
1755
+ catch (e) { return { ok: false, cause: `perplexity core unavailable: ${e.message}` }; }
1756
+ const started = Date.now();
1757
+ try {
1758
+ const body = core.buildRequestBody({ task: core.buildGridProgramTask(spec), preset, enableSandbox: true, reasoning });
1759
+ const data = await core.callAgentAPI(process.env.PERPLEXITY_API_KEY, body, { retries: core.retriesForPreset(preset) });
1760
+ const raw = core.rawResultItems(data);
1761
+ const cap = core.captureGridFromResponse(data, spec);
1762
+ if (!cap.ok) return { ok: false, cause: `grid: ${String(cap.error ?? "capture failed").slice(0, 200)}`, raw, tookMs: Date.now() - started };
1763
+ return { ok: true, ledgerJson: cap.ledgerJson, present: cap.present, requested: cap.requested, missing: cap.missing.length,
1764
+ bytes: Buffer.byteLength(cap.ledgerJson), tookMs: Date.now() - started, raw };
1765
+ } catch (e) {
1766
+ const status = Number.isInteger(e?.status) ? e.status : null;
1767
+ return {
1768
+ ok: false,
1769
+ cause: `grid call threw: ${String(e?.message ?? e).slice(0, 200)}`,
1770
+ status,
1771
+ outage: status === 429 || (status >= 500 && status <= 599),
1772
+ tookMs: Date.now() - started,
1773
+ };
1774
+ }
1775
+ },
1719
1776
  },
1720
1777
  };
1721
1778
 
@@ -2024,9 +2081,10 @@ export const ENGINE_BINARIES = {
2024
2081
  // npm, NOT the vendor's shell installer: a command this product executes on someone's box has to be
2025
2082
  // one they can read in full before they answer, and a piped remote script is not.
2026
2083
  // — the documented headless ending. `claude setup-token` walks the sign-in and
2027
- // prints a long-lived token; the stage subprocess env is a spread of the driver's — spawnEnv
2028
- // strips ONLY the API key under subscription — so a token in the env file reaches the CLI
2029
- // untouched, and that inheritance is the whole mechanism (armed in engine.anthropic.test.mjs). The wizard
2084
+ // prints a long-lived token; the stage subprocess env is a list (engine/engine-env.mjs) that carries the
2085
+ // vendor's own `CLAUDE_*` names — the API key is the one name it keeps only under `api-key` — so a token
2086
+ // in the env file reaches the CLI untouched, and that is the whole mechanism (armed in
2087
+ // engine.anthropic.test.mjs). The wizard
2030
2088
  // captures it by paste: the vendor's stream layout is not ours to guess at, and a paste works
2031
2089
  // whatever it prints where.
2032
2090
  headless: { cmd: "claude setup-token", tokenEnv: "CLAUDE_CODE_OAUTH_TOKEN" },
@@ -2171,7 +2229,17 @@ export function engineAdapterSpecifier(engine) {
2171
2229
  /** A path on a Windows drive as WSL mounts it. */
2172
2230
  export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
2173
2231
 
2174
- const isExecFile = (p) => { try { return statSync(p).isFile() && (accessSync(p, X_OK), true); } catch { return false; } };
2232
+ // ON WINDOWS THE EXECUTE BIT DOES NOT EXIST: `X_OK` there answers whether the file exists, so npm's
2233
+ // extensionless `claude` shim, a shell script Windows cannot start, passed this check and the spawn failed
2234
+ // with ENOENT (seen on a real Windows run, 2026-09-09). There a program is what Windows starts itself, an
2235
+ // `.exe` or `.com`, or a JavaScript file, which runs through the Node running now (engine/engine-spawn.mjs).
2236
+ const WINDOWS_PROGRAM = /\.(?:exe|com|[cm]?js)$/i;
2237
+ const isExecFile = (p, platform = process.platform) => {
2238
+ try {
2239
+ if (!statSync(p).isFile()) return false;
2240
+ return platform === "win32" ? WINDOWS_PROGRAM.test(p) : (accessSync(p, X_OK), true);
2241
+ } catch { return false; }
2242
+ };
2175
2243
  const realOrNull = (p) => { try { return realpathSync(p); } catch { return null; } };
2176
2244
  const readPackage = (dir) => { try { return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")); } catch { return null; } };
2177
2245
 
@@ -2229,9 +2297,52 @@ function installedProgram(spec, root) {
2229
2297
  return rel ? join(dir, rel) : null;
2230
2298
  }
2231
2299
 
2300
+ /**
2301
+ * PATH as Windows spells it. Windows names are not case-sensitive, and Node copies the environment with
2302
+ * the case the system gave it, which is `Path`. A lookup of `PATH` alone reads nothing there.
2303
+ */
2304
+ function pathOf(env, platform) {
2305
+ if (platform !== "win32") return env.PATH;
2306
+ const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH");
2307
+ return key ? env[key] : undefined;
2308
+ }
2309
+
2310
+ /**
2311
+ * The program a PATH directory holds under `name` on Windows, or null.
2312
+ *
2313
+ * WINDOWS FINDS A PROGRAM BY ITS EXTENSION, in the order PATHEXT lists them, so `claude` is `claude.exe`
2314
+ * or `claude.cmd` and never the extensionless file. An `.exe` or `.com` is the program itself: the
2315
+ * vendor's own installer puts `claude.exe` on PATH. A `.cmd` or `.bat` is npm's shim for a global
2316
+ * install, and Node refuses to start a batch file without a shell. So the shim is read as what it stands
2317
+ * for: the vendor's package in `node_modules` beside it, and the program that package's `bin` names. A
2318
+ * name that already ends in one of those extensions, as a setting of `claude.exe` does, is looked for as
2319
+ * written, as Windows looks for it.
2320
+ */
2321
+ function windowsProgramIn(dir, name, spec, env) {
2322
+ const key = Object.keys(env).find((k) => k.toUpperCase() === "PATHEXT");
2323
+ const exts = String((key && env[key]) || ".COM;.EXE;.BAT;.CMD").split(";").map((x) => x.trim().toLowerCase())
2324
+ .filter((x) => [".com", ".exe", ".bat", ".cmd"].includes(x));
2325
+ const own = /\.(?:com|exe|bat|cmd)$/i.exec(name)?.[0]?.toLowerCase();
2326
+ const tries = own ? [[name, own]] : exts.map((ext) => [`${name}${ext}`, ext]);
2327
+ for (const [file, ext] of tries) {
2328
+ const p = join(dir, file);
2329
+ let found = false;
2330
+ try { found = statSync(p).isFile(); } catch { /* not here */ }
2331
+ if (!found) continue;
2332
+ if (ext === ".exe" || ext === ".com") return p;
2333
+ return installedProgram(spec, dir);
2334
+ }
2335
+ return null;
2336
+ }
2337
+
2232
2338
  /** Can this candidate be spawned as the engine? `{ok: true, version}` or `{ok: false, why}`. */
2233
- function engineCandidate(p, spec) {
2234
- if (!isExecFile(p)) return { ok: false, why: "not an executable file (it is missing, is a directory, or lacks the execute bit for this user)" };
2339
+ /** Why a candidate the lookup cannot start is refused, in the platform's own terms: Windows has no execute bit. */
2340
+ const notStartable = (platform) => platform === "win32"
2341
+ ? "not a program Windows can start (it is missing, is a folder, or is not an .exe file)"
2342
+ : "not an executable file (it is missing, is a directory, or lacks the execute bit for this user)";
2343
+
2344
+ function engineCandidate(p, spec, platform = process.platform) {
2345
+ if (!isExecFile(p, platform)) return { ok: false, why: notStartable(platform) };
2235
2346
  const own = owningPackage(p);
2236
2347
  const vendor = Boolean(spec.package) && own?.name === spec.package;
2237
2348
  if (vendor && !runsDirectly(p)) {
@@ -2258,7 +2369,8 @@ function engineCandidate(p, spec) {
2258
2369
  * way, because the services run as user units under the same home. `wsl` and `onWindowsDrive` are
2259
2370
  * injectable for the reason shared/wsl.mjs gives.
2260
2371
  */
2261
- export function resolveEngineProgram(engine, { env = process.env, enginesDir = undefined, wsl = null, onWindowsDrive = null } = {}) {
2372
+ export function resolveEngineProgram(engine, { env = process.env, enginesDir = undefined, wsl = null, onWindowsDrive = null,
2373
+ platform = process.platform } = {}) {
2262
2374
  const id = String(engine ?? "").trim().toLowerCase();
2263
2375
  const spec = ENGINE_BINARIES[id];
2264
2376
  const out = { engine: id, binEnv: spec?.env ?? null, bin: null, explicit: false, relative: false,
@@ -2270,14 +2382,14 @@ export function resolveEngineProgram(engine, { env = process.env, enginesDir = u
2270
2382
  const underWsl = wsl ?? isWsl({ env });
2271
2383
  const onDrive = onWindowsDrive ?? ((p) => ON_A_WINDOWS_DRIVE.test(p));
2272
2384
  const take = (p, source) => {
2273
- const c = engineCandidate(p, spec);
2385
+ const c = engineCandidate(p, spec, platform);
2274
2386
  if (!c.ok) { out.rejected.push({ path: p, why: c.why, source }); return false; }
2275
2387
  Object.assign(out, { resolved: p, source, version: c.version });
2276
2388
  return true;
2277
2389
  };
2278
2390
 
2279
- if (out.explicit && out.bin.includes("/")) {
2280
- if (!isAbsolute(out.bin)) { out.relative = true; return out; }
2391
+ if (out.explicit && hasSep(out.bin, platform)) {
2392
+ if (!(platform === "win32" ? win32.isAbsolute(out.bin) : isAbsolute(out.bin))) { out.relative = true; return out; }
2281
2393
  out.windowsShim = underWsl && onDrive(out.bin);
2282
2394
  take(out.bin, "explicit");
2283
2395
  return out;
@@ -2287,9 +2399,9 @@ export function resolveEngineProgram(engine, { env = process.env, enginesDir = u
2287
2399
  // tears a Windows PATH at every drive letter; the wizard's walk already used the delimiter.
2288
2400
  const installed = out.explicit ? null : installedProgram(spec, enginesDir !== undefined ? enginesDir : enginesFolder());
2289
2401
  const installedReal = installed ? realOrNull(installed) : null;
2290
- for (const dir of String(env.PATH ?? "").split(delimiter).filter(Boolean)) {
2291
- const p = join(dir, out.bin);
2292
- if (!isExecFile(p)) continue;
2402
+ for (const dir of String(pathOf(env, platform) ?? "").split(platform === "win32" ? ";" : ":").filter(Boolean)) {
2403
+ const p = platform === "win32" ? windowsProgramIn(dir, out.bin, spec, env) : join(dir, out.bin);
2404
+ if (!p || !isExecFile(p, platform)) continue;
2293
2405
  // UNDER WSL THE WINDOWS PATH IS APPENDED TO THIS ONE, so `claude` on a fresh WSL2 Ubuntu resolves to the
2294
2406
  // Windows build before any Linux install. It is executable, and it fails the proof turn as "not signed
2295
2407
  // in" because the credential it looks for is the Linux one. Passed over, and named for the caller to say.
@@ -2313,26 +2425,11 @@ export function resolveEngineProgram(engine, { env = process.env, enginesDir = u
2313
2425
  * registry's.
2314
2426
  */
2315
2427
  export function preflightEngineBinary(env = process.env, { platform = process.platform, enginesDir = undefined, wsl = null, onWindowsDrive = null } = {}) {
2316
- // item 3 — NATIVE WINDOWS REFUSES BY NAME, BEFORE ANYTHING READS PATH.
2317
- //
2318
- // INSTALL.md promises a native-Windows run "refuses at preflight". Nothing implemented it, so what a
2319
- // Windows user actually got was a PATH walk that split on ":", tore `C:\Users\…` in half at the drive
2320
- // letter, and reported their `claude.cmd` as "not on PATH". The walk now splits on the platform's own
2321
- // delimiter, and the refusal stands anyway, because its real grounds were never the lookup: a stage runs
2322
- // as its own process group and is stopped by signalling that group, an immediate stop identifies the
2323
- // process from /proc or ps, and the write-boundary hook is quoted for a POSIX shell. Native Windows has
2324
- // none of that, and where the program is found changes none of it.
2325
- //
2326
- // This fires FIRST so that no message about a PATH reaches a reader whose platform is the answer.
2327
- //
2328
- // `platform` is injectable so the refusal is testable off win32 — the population this protects is the
2329
- // one that cannot run this suite to find out.
2330
- if (platform === "win32") {
2331
- throw new Error("[preflight] this engine does not run on native Windows. Each stage runs as its own process "
2332
- + "group and is stopped by signalling that group, which native Windows cannot do, so a stopped stage would "
2333
- + "leave the tools it started still running. Run it under WSL2, or in the devcontainer (.devcontainer/), "
2334
- + "where the documented install path applies unchanged.");
2335
- }
2428
+ // NATIVE WINDOWS RUNS. This door refused it by name, on three grounds, and each now has a Windows
2429
+ // answer: a stage is stopped as a process tree rather than a group (engine/engine-spawn.mjs), the
2430
+ // immediate stop reads start times through PowerShell (claim-liveness.mjs), and the write-boundary hook
2431
+ // runs with no shell (anthropic-agent.mjs, writeBoundarySettings). `platform` stays a parameter, because
2432
+ // the lookup below answers by it.
2336
2433
  const engine = (env.CLEAROTRON_AI || DEFAULT_ENGINE_ID).trim().toLowerCase();
2337
2434
  const spec = ENGINE_BINARIES[engine];
2338
2435
  if (!spec) return { engine, binEnv: null, bin: null, resolved: null }; // selectEngine owns this refusal
@@ -2352,7 +2449,7 @@ export function preflightEngineBinary(env = process.env, { platform = process.pl
2352
2449
  // `envFrom` is therefore BELT-AND-BRACES, not the repair: it makes this site correct on its own terms
2353
2450
  // rather than correct because something upstream normalised the environment first — a coupling
2354
2451
  // nothing at this site declares and nothing here could notice breaking.
2355
- const r = resolveEngineProgram(engine, { env, enginesDir, wsl, onWindowsDrive }); // injectable for the reason it gives
2452
+ const r = resolveEngineProgram(engine, { env, enginesDir, wsl, onWindowsDrive, platform }); // injectable for the reason it gives
2356
2453
  const bin = r.bin;
2357
2454
  const setTo = String(envFrom(env, spec.env) ?? "").trim();
2358
2455
  const where = `${spec.env}${r.explicit ? "" : setTo
@@ -2361,7 +2458,7 @@ export function preflightEngineBinary(env = process.env, { platform = process.pl
2361
2458
 
2362
2459
  if (r.relative) {
2363
2460
  throw new Error(`[preflight] ${where} is the RELATIVE path "${bin}", which cannot work: the engine is `
2364
- + "spawned with the RUN DIRECTORY as its cwd (#524), not the repo, so a relative command is looked "
2461
+ + "spawned with the RUN DIRECTORY as its cwd, not the repo, so a relative command is looked "
2365
2462
  + `for inside the run. Give an absolute path — e.g. ${join(REPO_ROOT, bin)} — or a bare name on PATH.`);
2366
2463
  }
2367
2464
 
@@ -2375,9 +2472,9 @@ export function preflightEngineBinary(env = process.env, { platform = process.pl
2375
2472
  const windows = r.skipped.length
2376
2473
  ? `. Passed over because they sit on a Windows drive, and a Windows build cannot run a stage here: ${r.skipped.join(", ")}` : "";
2377
2474
  throw new Error(`[preflight] the ${engine} engine cannot run: ${where} names "${bin}", which is `
2378
- + (bin.includes("/")
2379
- ? (r.rejected[0]?.why ?? "not an executable file (it is missing, is a directory, or lacks the execute bit for this user)")
2380
- : `not on PATH as an executable file (PATH=${env.PATH || "(empty)"})`
2475
+ + (hasSep(bin, platform)
2476
+ ? (r.rejected[0]?.why ?? notStartable(platform))
2477
+ : `not on PATH as an executable file (PATH=${pathOf(env, platform) || "(empty)"})`
2381
2478
  + (r.explicit || installedRefused ? "" : ", and Clearotron has not installed one")
2382
2479
  + (passedOver ? `. Passed over: ${passedOver}` : "")
2383
2480
  + windows)
@@ -37,7 +37,7 @@ other, which is the only proof available where no gold set exists.
37
37
 
38
38
  A knockout scenario graded against a similar-marks sheet scores a **structural zero**. Knockout answers
39
39
  one narrow question — how many register filings exist for the exact mark and its close variations in the
40
- named classes — and never retrieves similar marks, so a lawyer's list of TIKI PUNCH and TIKI TROPICS is
40
+ named classes — and never retrieves similar marks, so a lawyer's list of WAVO PUNCH and WAVO TROPICS is
41
41
  unreachable by construction. R3 and R4 scored 0/8 and 0/9 on both free-tier and clarivate on the same
42
42
  day with the same engine, which measured the pairing rather than the engine.
43
43
 
@@ -90,7 +90,7 @@ export const jobJurisdictions = (job) =>
90
90
  /**
91
91
  * — WHAT THE ACCOUNT'S DEFAULT TERRITORIES MAY SAY TO THE MATTER FRAME.
92
92
  *
93
- * Returns 0 or 1 prompt lines, spread into the frame's message by stages.mjs.
93
+ * Returns 0 to 2 prompt lines, spread into the frame's message by stages.mjs.
94
94
  *
95
95
  * THE RULE IS THE LADDER'S RULE, which is why it lives beside the ladder rather than in the prompt: a
96
96
  * default FILLS AN ABSENT SCOPE and never widens a stated one. `resolveTerritories` below has always
@@ -139,9 +139,65 @@ export function defaultTerritoryState(profile) {
139
139
  return { kept, unrecognized: dropped };
140
140
  }
141
141
 
142
+ /**
143
+ * THE MAJOR MARKETS: where a crowded result is narrowed when neither the order nor the account names a
144
+ * market and the customer's field and brand point nowhere in particular. One list, and the manuals that
145
+ * name the major markets name these (ruled 2026-09-26: a generic customer narrows by the order's classes
146
+ * and the major markets). Codes as the territory vocabulary writes them, so GB is the UK.
147
+ */
148
+ export const MAJOR_MARKETS = Object.freeze(["US", "EU", "GB", "CN", "JP"]);
149
+
150
+ /**
151
+ * WHERE A CROWDED RESULT IS NARROWED BY MARKET, in the ruled order (2026-09-26), as far as the driver can
152
+ * know it. An order with a territorial scope (its own countries, or the account's defaults filling an
153
+ * absent one) narrows within that scope, one market at a time: nothing outside it was ordered. A worldwide
154
+ * order, or one with no territory at all, narrows to the customer's priority markets (the account's
155
+ * defaults); else to the markets the customer's field and brand point to, which is the reading step's
156
+ * judgment; else to the major markets.
157
+ * `{ scope, priorities, major }` — arrays of codes; `scope` empty means worldwide. PURE.
158
+ */
159
+ export function narrowingMarkets(job, profile) {
160
+ const { jurisdictions, geographyMode } = resolveTerritories(job, profile);
161
+ const scope = geographyMode === "worldwide" ? [] : jurisdictions.map((t) => String(t).trim()).filter(Boolean);
162
+ if (scope.length) return { scope, priorities: [], major: [] };
163
+ return { scope: [], priorities: defaultTerritoryState(profile).kept, major: [...MAJOR_MARKETS] };
164
+ }
165
+
166
+ /**
167
+ * THE NARROWING ORDER, as the register reading step is told it (ruled 2026-09-26): goods and class
168
+ * first, then the markets in turn, with the markets the driver knows filled in. One line. PURE.
169
+ */
170
+ export function narrowingOrderLine(job, profile, inScopeClasses = null) {
171
+ const { scope, priorities, major } = narrowingMarkets(job, profile);
172
+ const markets = scope.length
173
+ ? `one question per market of the order's scope (${scope.join(", ")}); nothing outside it was ordered`
174
+ : `one question per market: ${[
175
+ priorities.length ? `the customer's priority markets (${priorities.join(", ")})` : "",
176
+ "the markets the customer's field and brand point to (judge them from the matter frame)",
177
+ `the major markets (${major.join(", ")})`,
178
+ ].filter(Boolean).join(", else ")}`;
179
+ return `NARROWING A CROWD, in this order: first the goods and the classes, the client's goods words in `
180
+ + `${inScopeClasses ? `the order's classes [${inScopeClasses}]` : "the order's classes"}, then the classes the crowding owners `
181
+ + `file in; still a crowd, then the markets, ${markets}. From the narrowed lists, read what a lawyer would raise. `
182
+ + `What was counted and not read stays on its coverage row with its count: set aside, never clean.`;
183
+ }
184
+
142
185
  export function defaultJurisdictionsLine(job, profile) {
143
- if (jobJurisdictions(job).length) return [];
144
186
  const { kept } = defaultTerritoryState(profile);
187
+ // THE ORDER'S SCOPE IN ONE LINE, and the account's defaults as the customer's PRIORITIES, never as the
188
+ // scope (ruled 2026-09-26). The ladder's first rung, which this line had never heard of: measured
189
+ // 2026-09-25, a worldwide search under an account with seven default territories was told "the request
190
+ // names none — apply these", framed as those seven, and its crowded identical-mark question was read
191
+ // in those seven only.
192
+ if (job?.geography?.mode === "worldwide") {
193
+ return [
194
+ "Order scope: worldwide (AUTHORITATIVE — every register question is asked worldwide; do NOT narrow the scope, and exclude no market for lying outside the customer's priority markets).",
195
+ ...(kept.length ? [`Customer priority markets (the account's default territories — where a crowded result is narrowed first; never the scope of this order): ${kept.join(", ")}.`] : []),
196
+ ];
197
+ }
198
+ // A named order's own countries are its scope and the first markets a crowd narrows to, so no default
199
+ // appears anywhere in its prompt.
200
+ if (jobJurisdictions(job).length) return [];
145
201
  return kept.length
146
202
  ? [`Customer-default jurisdictions that materially matter (the request names none — apply these): ${kept.join(", ")}.`]
147
203
  : [];