clearotron 0.3.2-beta.7 → 0.3.2-beta.9

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 (272) hide show
  1. package/.env.example +58 -26
  2. package/CONTRIBUTING.md +8 -8
  3. package/INSTALL.md +148 -81
  4. package/README.md +3 -3
  5. package/SECURITY.md +3 -3
  6. package/bin/brandowner.mjs +3 -3
  7. package/bin/framework-preflight.mjs +1 -1
  8. package/bin/onboard.mjs +637 -216
  9. package/bin/start.mjs +151 -27
  10. package/bin/update.mjs +58 -11
  11. package/build-info.json +2 -2
  12. package/docs/DELIVERY.md +2 -1
  13. package/docs/INTAKE.md +1 -1
  14. package/docs/ONBOARDING.md +1 -1
  15. package/docs/architecture/03-run-lifecycle.md +6 -6
  16. package/docs/architecture/04-configuration-reference.md +32 -14
  17. package/docs/architecture/05-config-governance.md +23 -8
  18. package/docs/architecture/05-customer-profiles.md +2 -2
  19. package/docs/architecture/06-operations-runbook.md +3 -3
  20. package/docs/architecture/08-development-guide.md +6 -6
  21. package/docs/configuration.md +5 -5
  22. package/docs/decisions/0003-credential-model.md +1 -1
  23. package/docs/writing-standard.md +4 -0
  24. package/driver/CHANGELOG.md +124 -0
  25. package/driver/README.md +3 -3
  26. package/driver/band-size.mjs +59 -0
  27. package/driver/binding-layers.mjs +1 -1
  28. package/driver/citation-census.json +3 -3
  29. package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
  30. package/driver/common-law-receipts.mjs +2 -2
  31. package/driver/company-bundle.mjs +3 -3
  32. package/driver/compose-read.mjs +8 -14
  33. package/driver/config-inventory.mjs +112 -9
  34. package/driver/consumption-ledger.mjs +2 -2
  35. package/driver/contract-arm2-baseline.json +2 -5
  36. package/driver/contract-dictation-registry.mjs +19 -19
  37. package/driver/contract-e3-backlog.mjs +43 -43
  38. package/driver/contract-e3-baseline.json +14 -14
  39. package/driver/contract-vocabulary.mjs +68 -27
  40. package/driver/deliver-trigger.sh +16 -16
  41. package/driver/demo-container.mjs +3 -3
  42. package/driver/dev-portal.mjs +3 -3
  43. package/driver/disposition-call.mjs +1 -1
  44. package/driver/door-gates.mjs +41 -7
  45. package/driver/doubt-ledger.mjs +2 -2
  46. package/driver/drainer-identity.mjs +34 -8
  47. package/driver/driver.config.mjs +367 -104
  48. package/driver/engine/CONTRACT.md +10 -3
  49. package/driver/engine/README.md +2 -2
  50. package/driver/engine/anthropic-agent.mjs +77 -21
  51. package/driver/engine/auth.mjs +129 -10
  52. package/driver/engine/jx-turn.mjs +7 -6
  53. package/driver/engine/mcp/README.md +1 -1
  54. package/driver/engine/mcp/dispositions-server.mjs +3 -3
  55. package/driver/engine/mcp/gather-config.mjs +9 -9
  56. package/driver/engine/mcp/perplexity-server.mjs +2 -2
  57. package/driver/engine/mcp/recording-server.mjs +18 -5
  58. package/driver/engine/openai-agent.mjs +4 -2
  59. package/driver/engine/probe.mjs +110 -23
  60. package/driver/enqueue-schema.mjs +6 -2
  61. package/driver/findings-model.mjs +6 -3
  62. package/driver/flag-snapshot.mjs +34 -8
  63. package/driver/form-neighbourhood.mjs +54 -7
  64. package/driver/framework.mjs +4 -4
  65. package/driver/gateway.mjs +36 -24
  66. package/driver/jx-lanes.mjs +23 -4
  67. package/driver/jx-units.mjs +7 -4
  68. package/driver/jx.mjs +34 -4
  69. package/driver/knockout-review-record.mjs +56 -4
  70. package/driver/known-conflicts.mjs +1 -1
  71. package/driver/matter-frame-record.mjs +90 -1
  72. package/driver/named-band.mjs +1 -1
  73. package/driver/ordinary-words.mjs +51 -0
  74. package/driver/outbox-backoff.mjs +31 -16
  75. package/driver/package.json +1 -1
  76. package/driver/partial-payload-baseline.json +2 -2
  77. package/driver/phase0.mjs +3 -3
  78. package/driver/pipeline-knockout.mjs +5 -5
  79. package/driver/pipeline.mjs +396 -81
  80. package/driver/placement-form.mjs +77 -1
  81. package/driver/placement-model.mjs +1 -1
  82. package/driver/portal-config-view.mjs +30 -1
  83. package/driver/portal-report.mjs +107 -6
  84. package/driver/portal-service.mjs +80 -14
  85. package/driver/portal-upstream.mjs +1 -1
  86. package/driver/predelivery-lint.mjs +12 -2
  87. package/driver/preserve-merge.mjs +3 -3
  88. package/driver/product-rows.mjs +2 -2
  89. package/driver/products.mjs +1 -1
  90. package/driver/profiles/README.md +3 -3
  91. package/driver/profiles/demo-brand-owner.json +2 -2
  92. package/driver/profiles.mjs +55 -17
  93. package/driver/progress.mjs +18 -8
  94. package/driver/provider-usage.mjs +8 -8
  95. package/driver/publish/index.mjs +154 -8
  96. package/driver/publish/knockout.mjs +39 -5
  97. package/driver/publish/pool-admin.mjs +1 -1
  98. package/driver/publish/publish-inputs.mjs +18 -2
  99. package/driver/publish/render-knockout.mjs +184 -31
  100. package/driver/publish/render.mjs +323 -93
  101. package/driver/publish/report-data.mjs +4 -1
  102. package/driver/publish/report-topbar.mjs +58 -0
  103. package/driver/publish/search-depth.mjs +133 -4
  104. package/driver/publish/templates/report.css +78 -4
  105. package/driver/publish/xlsx.mjs +20 -1
  106. package/driver/queue-order.mjs +2 -2
  107. package/driver/recording-agreement.mjs +1 -1
  108. package/driver/reference-score.mjs +1 -1
  109. package/driver/register-availability.mjs +2 -2
  110. package/driver/register-count.mjs +50 -5
  111. package/driver/register-coverage.mjs +161 -1
  112. package/driver/register-digest-record.mjs +236 -11
  113. package/driver/register-grant-vocabulary.mjs +1 -1
  114. package/driver/register-plan.mjs +189 -2
  115. package/driver/registry-fidelity.mjs +3 -3
  116. package/driver/repair-composers.mjs +1 -1
  117. package/driver/repair-contract.mjs +1 -1
  118. package/driver/replay-archive.mjs +6 -6
  119. package/driver/report-overview-record.mjs +2 -2
  120. package/driver/result-noun-fields.mjs +2 -2
  121. package/driver/run-economics.mjs +41 -10
  122. package/driver/run-requirements.mjs +173 -9
  123. package/driver/runner.mjs +5 -5
  124. package/driver/scope-facts.mjs +20 -5
  125. package/driver/scope-ledger.mjs +5 -5
  126. package/driver/search-policy.mjs +22 -12
  127. package/driver/skills/README.md +15 -15
  128. package/driver/skills/blind-frame/SKILL.md +2 -2
  129. package/driver/skills/case-law-citation/SKILL.md +4 -4
  130. package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
  131. package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
  132. package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
  133. package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
  134. package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
  135. package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
  136. package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
  137. package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
  138. package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
  139. package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
  140. package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
  141. package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
  142. package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
  143. package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
  144. package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
  145. package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
  146. package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
  147. package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
  148. package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
  149. package/driver/skills/frame-diff/SKILL.md +1 -1
  150. package/driver/skills/knockout-assess/SKILL.md +10 -7
  151. package/driver/skills/matter-frame/SKILL.md +3 -3
  152. package/driver/skills/narrative-refutation/SKILL.md +9 -9
  153. package/driver/skills/placement-inquiry/SKILL.md +5 -5
  154. package/driver/stage-context.mjs +1 -1
  155. package/driver/stages-knockout.mjs +4 -4
  156. package/driver/stages.mjs +65 -61
  157. package/driver/status-snapshot.mjs +2 -2
  158. package/driver/suite-census.json +340 -136
  159. package/driver/surface-exit-verdict.mjs +58 -0
  160. package/driver/systemd/README.md +9 -6
  161. package/driver/systemd/clearotron-worker.service +1 -1
  162. package/driver/terminal-clamp.mjs +109 -1
  163. package/driver/tokens.mjs +169 -3
  164. package/driver/unit-environment.mjs +42 -15
  165. package/driver/unit-inventory.mjs +19 -2
  166. package/driver/usage-ledger.mjs +1 -1
  167. package/driver/variant-manifest-model.mjs +4 -4
  168. package/driver/verify-knockout.mjs +27 -0
  169. package/driver/verify.mjs +94 -6
  170. package/driver/whatif-queue.mjs +1 -1
  171. package/driver/wordlists/en.txt +63906 -0
  172. package/mcp-server/CHANGELOG.md +8 -0
  173. package/mcp-server/README.md +1 -1
  174. package/mcp-server/lib/README.md +1 -1
  175. package/mcp-server/lib/options.mjs +8 -7
  176. package/mcp-server/lib/plan.mjs +18 -2
  177. package/mcp-server/lib/runs.mjs +1 -1
  178. package/mcp-server/lib/usage.mjs +3 -3
  179. package/mcp-server/lib/whatif.mjs +1 -1
  180. package/mcp-server/package.json +1 -1
  181. package/mcp-server/server.mjs +18 -1
  182. package/package.json +12 -11
  183. package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
  184. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
  185. package/portal-ui/dist/index.html +2 -2
  186. package/portal-ui/package.json +1 -1
  187. package/providers/README.md +1 -1
  188. package/providers/_shared/enumerate.mjs +6 -6
  189. package/providers/_shared/execute-plan.mjs +3 -3
  190. package/providers/_shared/ledger.mjs +119 -5
  191. package/providers/_shared/provider-text.mjs +2 -2
  192. package/providers/_shared/screen.mjs +2 -2
  193. package/providers/_shared/script-form.mjs +3 -3
  194. package/providers/_shared/territory-codes.mjs +23 -3
  195. package/providers/clarivate/README.md +1 -1
  196. package/providers/clarivate/src/capabilities.js +12 -12
  197. package/providers/clarivate/src/core.js +37 -43
  198. package/providers/corsearch/README.md +1 -1
  199. package/providers/corsearch/src/capabilities.js +5 -5
  200. package/providers/corsearch/src/core.js +3 -3
  201. package/providers/jx/README.md +2 -1
  202. package/providers/jx/src/turn-envelope.mjs +8 -3
  203. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  204. package/providers/oauth-mcp-bridge/package.json +1 -1
  205. package/providers/perplexity/src/core.js +1 -1
  206. package/providers/signa/README.md +1 -1
  207. package/providers/signa/src/capabilities.js +42 -49
  208. package/providers/signa/src/core.js +106 -29
  209. package/providers/uspto-local/README.md +1 -1
  210. package/providers/uspto-local/src/sync.js +1 -1
  211. package/scripts/README.md +1 -0
  212. package/scripts/ask-ai-render-check.mjs +127 -1
  213. package/scripts/authority-boundary-probe.mjs +8 -6
  214. package/scripts/backfill-started-at.mjs +2 -2
  215. package/scripts/census-merge-driver.mjs +33 -2
  216. package/scripts/citation-anchor-report.mjs +181 -0
  217. package/scripts/dead-names.mjs +1 -1
  218. package/scripts/deprecate-below.mjs +66 -8
  219. package/scripts/drain-preflight.mjs +1 -1
  220. package/scripts/e2e.mjs +174 -0
  221. package/scripts/env-audit.mjs +39 -6
  222. package/scripts/env-classify.mjs +20 -2
  223. package/scripts/freeze-example-run.mjs +61 -18
  224. package/scripts/generated-files-are-current.mjs +69 -4
  225. package/scripts/live-surface-check.mjs +124 -41
  226. package/scripts/markdown-link-check.mjs +1 -1
  227. package/scripts/merge-shape-check.mjs +242 -0
  228. package/scripts/mint-names-in-force.mjs +5 -5
  229. package/scripts/mint-offered-territories.mjs +72 -0
  230. package/scripts/mint-public-residue.mjs +2 -2
  231. package/scripts/mint-reference-strip-backlog.mjs +2 -2
  232. package/scripts/mint-suite-census.mjs +75 -2
  233. package/scripts/mint-writing-standard-backlog.mjs +2 -2
  234. package/scripts/purge-runs.mjs +7 -7
  235. package/scripts/reconcile-runs.mjs +2 -2
  236. package/scripts/release-approve-parked.mjs +20 -2
  237. package/scripts/release-await-cut.mjs +120 -1
  238. package/scripts/release-note-required.mjs +76 -8
  239. package/scripts/report-header-render-check.mjs +164 -0
  240. package/scripts/settings-render-check.mjs +75 -2
  241. package/scripts/test-full.mjs +96 -3
  242. package/scripts/test-run.mjs +10 -0
  243. package/shared/brand.mjs +27 -0
  244. package/shared/connect-clients.mjs +39 -11
  245. package/shared/deployment-box.mjs +7 -2
  246. package/shared/driver-dir.mjs +1 -1
  247. package/shared/env-aliases.mjs +1 -1
  248. package/shared/identifier-scan.mjs +65 -9
  249. package/shared/identifier-sentinels.mjs +22 -0
  250. package/shared/names-in-force.mjs +4 -2
  251. package/shared/offered-territories.json +738 -0
  252. package/shared/pre-rename-spellings.mjs +53 -0
  253. package/shared/reference-guard-classes.mjs +40 -2
  254. package/shared/stdio-connect.mjs +39 -4
  255. package/shared/tree-commit.mjs +48 -0
  256. /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
  257. /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
  258. /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
  259. /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
  260. /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
  261. /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
  262. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
  263. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
  264. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
  265. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
  266. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
  267. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
  268. /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
  269. /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
  270. /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
  271. /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
  272. /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
@@ -44,6 +44,9 @@
44
44
  // BLOCKING — without these nothing runs at all, in any product. The register and its credential (the
45
45
  // driver throws by name at the first stage), the engine and the binary it drives, and the pool the
46
46
  // report is written into. This is the set whose absence produced the outcome at the top of this file.
47
+ // And what the billing word needs, because the run door refuses without it before any turn: under
48
+ // `cloud` the switch of the cloud it pays through (or the gateway address), under `api-key` the
49
+ // engine's key; and the billing word itself, whenever the run door refuses the way it is set.
47
50
  //
48
51
  // NARROWING — `PERPLEXITY_API_KEY`. Its absence does NOT crash a run and does not deliver a false
49
52
  // notice: the three clearance searches carry the common-law grid and cannot switch it off, so they
@@ -71,13 +74,16 @@
71
74
  // Refusing here is refusing over OUR bug, and there is nothing for a reader to go and set.
72
75
  //
73
76
  // at:"order" the value an OPERATOR supplies — the register, its credential, the engine and the
74
- // binary it drives. Absent, the install is not finished. The doors come up and every run is refused
75
- // AT ORDER TIME, before a stage dispatches and before anything is spent, naming what is missing.
77
+ // binary it drives, and what the billing word needs (above). Absent, the install is not finished.
78
+ // The doors come up and every run is refused AT ORDER TIME, before a stage dispatches and before
79
+ // anything is spent, naming what is missing.
76
80
  //
77
81
  // The axis is on the ROW, not in the caller, for the reason the whole module exists: a start that
78
82
  // decides for itself which names are its own and a runner that decides separately are two opinions
79
83
  // about one list, and they drift in the direction where the second asks for less.
80
84
 
85
+ import { BILLING_MODES, CLOUD_SWITCH, CLOUD_SETTINGS, CLOUD_CREDENTIAL_CHECK, billingMode, cloudsSwitchedOn, resolveAuthMode } from "./engine/auth.mjs"; // — the billing row asks the one resolver which ways an engine can be paid for, and the cloud rows read its own lists
86
+
81
87
  /** The pool a report is written into. Named once; the supervisor already writes it. */
82
88
  export const POOL_ENV = "CLEAROTRON_REPORTS_DIR";
83
89
  /** The register selection, and the engine selection. */
@@ -85,6 +91,9 @@ export const REGISTER_ENV = "CLEAROTRON_DATABASE";
85
91
  export const ENGINE_ENV = "CLEAROTRON_AI";
86
92
  /** Narrowing, never blocking — see the header. */
87
93
  export const RESEARCH_ENV = "PERPLEXITY_API_KEY";
94
+ /** The settings that decide WHICH cloud a Claude turn goes to: each cloud's switch, and the gateway address.
95
+ * Any one of them answers the cloud billing word; start compares all of them against the services' file. */
96
+ export const CLOUD_ROUTES = Object.freeze([...Object.values(CLOUD_SWITCH), "ANTHROPIC_BASE_URL"]);
88
97
 
89
98
  /** WHEN a blocking value is asked for. `START` is what `clearotron start` writes itself; `ORDER` is what
90
99
  * an operator configures, and its absence refuses a RUN rather than an install. See the header. */
@@ -93,6 +102,35 @@ export const ORDER = "order";
93
102
 
94
103
  const val = (env, name) => String(env?.[name] ?? "").trim();
95
104
 
105
+ // THE WAYS AN ENGINE CAN BE PAID FOR, AS THE BILLING ROW SAYS THEM. The row named all three for every engine,
106
+ // and Codex refuses a cloud account (engine/auth.mjs, resolveAuthMode), so a Codex install was offered a way
107
+ // to pay its run door refuses.
108
+ const PAY_WORDS = Object.freeze({ subscription: "subscription", "api-key": "API key", cloud: "cloud account" });
109
+
110
+ /**
111
+ * The billing words `engineId` can be paid by, in BILLING_MODES order: each one asked of the resolver, the
112
+ * one authority on it, with what that word needs set (the engine's key for `api-key`, a cloud's switch for
113
+ * `cloud`), so a word the resolver accepts only when its settings are present is not read as refused. An
114
+ * import of a leaf with no imports of its own, so this module stays pure.
115
+ */
116
+ export function payWays(engineId, engine = {}) {
117
+ return BILLING_MODES.filter((mode) => {
118
+ const env = { [engine.authEnv ?? "CLEAROTRON_AI_BILLING"]: mode,
119
+ ...(mode === "api-key" && engine.apiKeyEnv ? { [engine.apiKeyEnv]: "set" } : {}),
120
+ ...(mode === "cloud" ? { [CLOUD_SWITCH.vertex]: "1" } : {}) };
121
+ try { resolveAuthMode({ engineName: engineId, env }); return true; } catch { return false; }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * A billing refusal from the run door, in words that carry no value. Every refusal names settings and the
127
+ * billing word, except the one for a word that is not a billing mode, which quotes the word as it is set;
128
+ * that quote is replaced by the setting's name. PURE. Any other message comes back unchanged.
129
+ */
130
+ export function billingRefusalWords(message) {
131
+ return String(message ?? "").replace(/^([A-Z][A-Z0-9_]*)=[\s\S]*? is not a billing mode/, "$1 is set to a word that is not a billing mode");
132
+ }
133
+
96
134
  /**
97
135
  * Every environment name this box's configuration says a clearance needs, with the reason each one is
98
136
  * there and whether its absence blocks or narrows.
@@ -101,9 +139,9 @@ const val = (env, name) => String(env?.[name] ?? "").trim();
101
139
  * environment in hand: the composer has the supervisor's, and the guard has the one it just wrote into
102
140
  * the unit file. A function that read the ambient environment would answer about neither.
103
141
  *
104
- * PURE.
142
+ * PURE, apart from the engine resolver a caller passes in `tables.resolveEngine` (see the engine row).
105
143
  */
106
- export function runRequirements(env = {}, { registers = [], engines = {}, defaultEngine = null } = {}) {
144
+ export function runRequirements(env = {}, { registers = [], engines = {}, defaultEngine = null, resolveEngine = null } = {}) {
107
145
  const out = [];
108
146
  const push = (name, blocking, why, at = ORDER) =>
109
147
  out.push({ name, blocking, why, at, present: Boolean(val(env, name)) });
@@ -128,11 +166,135 @@ export function runRequirements(env = {}, { registers = [], engines = {}, defaul
128
166
 
129
167
  // ── THE ENGINE, AND THE BINARY IT DRIVES ─────────────────────────────────────────────────────────
130
168
  push(ENGINE_ENV, true, "which reasoning engine runs the stages");
131
- const engine = (engines ?? {})[val(env, ENGINE_ENV) || defaultEngine || ""];
132
- if (engine?.env)
169
+ const engineId = val(env, ENGINE_ENV) || defaultEngine || "";
170
+ const engine = (engines ?? {})[engineId];
171
+ if (engine?.env) {
133
172
  push(engine.env, true, `the path to the ${engine.vendor} CLI this engine drives — a stage cannot dispatch without it`);
134
- if (engine?.authEnv)
135
- push(engine.authEnv, false, "how the engine bills — subscription or key; the adapter refuses before spending if the sign-in it names is absent");
173
+ // FOUND IS WHAT COUNTS, NOT SET. A program on this environment's PATH, or the copy installed with
174
+ // Clearotron, needs no path written anywhere, and asking only whether the variable was set refused an
175
+ // install whose engine the run door would have started. The resolver arrives through the tables, like
176
+ // everything else this module knows about the install; a caller that passes no resolver gets the
177
+ // variable's own answer, which is all it can see. The module's one import is engine/auth.mjs, for the
178
+ // billing row below: a driver leaf with no imports of its own, pure, and the one authority on which
179
+ // billing words an engine takes, so importing it keeps this module pure and reaches nothing in `bin/`.
180
+ const row = out[out.length - 1];
181
+ if (!row.present && typeof resolveEngine === "function") {
182
+ try { row.present = Boolean(resolveEngine(engineId, { env })?.resolved); } catch { /* not found is not present */ }
183
+ }
184
+ }
185
+ if (engine?.authEnv) {
186
+ const ways = payWays(engineId, engine);
187
+ const said = ways.map((m) => PAY_WORDS[m]);
188
+ push(engine.authEnv, false, `how the engine is paid for — ${said.length > 1 ? `${said.slice(0, -1).join(", ")} or ${said.at(-1)}` : said[0]}; `
189
+ + `unset means the subscription, and the adapter refuses before spending if the ${ways.includes("cloud") ? "key or cloud account" : "key"} it names is absent`);
190
+ const billingRow = out[out.length - 1];
191
+ const billingRows = out.length;
192
+ const word = billingMode(env);
193
+ const refused = "without it the engine refuses every search before spending";
194
+ // ── THE KEY, WHEN THE WORD IS `api-key` ──────────────────────────────────────────────────────────
195
+ //
196
+ // The same defect as the cloud below, one billing word over: the word travelled to the services' file
197
+ // and the key did not, so the run door refused every search after intake ("api-key but
198
+ // ANTHROPIC_API_KEY is not set"), while start and doctor reported nothing. Blocking at order time, like
199
+ // the switch: an operator's value, and the run door's own refusal without it.
200
+ if (word === "api-key" && engine.apiKeyEnv && ways.includes("api-key"))
201
+ push(engine.apiKeyEnv, true, `the key ${engine.authEnv}=api-key bills every turn to — ${refused}`);
202
+ // ── THE LONG-LIVED SIGN-IN, WHEN THE WORD IS THE SUBSCRIPTION ────────────────────────────────────
203
+ //
204
+ // Setup captures it on a machine that cannot complete a sign-in, which is the server a background
205
+ // install runs on, and the program reads it from its environment. Carried when set and never asked
206
+ // for: a machine signed in through the program itself needs none.
207
+ const tokenEnv = engine.headless?.tokenEnv;
208
+ if (word === "subscription" && tokenEnv && val(env, tokenEnv))
209
+ push(tokenEnv, false, `the long-lived sign-in the ${engine.vendor} CLI uses for the subscription on a machine it cannot sign in on; carried as set, and not asked for`);
210
+ // ── A CLOUD ACCOUNT, AND THE SETTINGS THAT REACH IT ──────────────────────────────────────────────
211
+ //
212
+ // The billing word travelled and the cloud did not. A machine set up to pay through a cloud account and
213
+ // started as background services wrote CLEAROTRON_AI_BILLING=cloud into the services' file and none of
214
+ // the cloud's own settings, so the run door refused every search: "none of CLAUDE_CODE_USE_VERTEX, …
215
+ // is set". Measured 2026-09-15 on a Microsoft Foundry configuration.
216
+ //
217
+ // ONLY WHEN THE WORD IS `cloud` AND THE ENGINE TAKES ONE — the resolver's answer through `payWays`, not
218
+ // a list of engines here. Under subscription or api-key nothing of a cloud is carried: a switch left on
219
+ // in a shell and written into the services' file sends Claude to that cloud, and the run door refuses
220
+ // a switch beside either word. Codex refuses a cloud account outright, so its rows do not change.
221
+ if (word === "cloud" && ways.includes("cloud")) {
222
+ const on = cloudsSwitchedOn(env);
223
+ const payWord = `${engine.authEnv}=cloud`;
224
+ // BLOCKING, AT ORDER TIME. Without the switch the run door refuses (engine/auth.mjs), so this is the
225
+ // same class as the engine and its program: an operator's value, whose absence refuses a run at
226
+ // intake and never a start.
227
+ for (const c of on)
228
+ push(CLOUD_SWITCH[c], true, `sends Claude to ${CLOUD_CREDENTIAL_CHECK[c].who}, the account ${payWord} pays through — ${refused}`);
229
+ if (!on.length && val(env, "ANTHROPIC_BASE_URL"))
230
+ push("ANTHROPIC_BASE_URL", true, `the gateway ${payWord} pays through — ${refused}`);
231
+ if (!on.length && !val(env, "ANTHROPIC_BASE_URL")) {
232
+ // NOTHING SAYS WHICH CLOUD, and a row has one name. Any one of four settings satisfies it. The row is
233
+ // NAMED by one real setting, Google's switch, the representative `payWays` above already uses for "a
234
+ // cloud account", because every reader of a row treats its name as a variable: start and doctor print
235
+ // it, the runner logs it, and a composite name read as four more items in doctor's list of what is
236
+ // missing. The REASON names all four, with whose each is. And the row carries all four as `anyOf`,
237
+ // which `runRequiredNames` hands out as names to read: doctor fills its view of the services by name,
238
+ // and reading only Google's switch reported an Azure machine's switch, held by its services, as
239
+ // missing.
240
+ //
241
+ // A SWITCH THAT IS SET AND NOT ON IS SAID, because it reads as on to a person and as off to the
242
+ // program: `CLAUDE_CODE_USE_FOUNDRY=0` on an Azure machine was answered with Google's switch
243
+ // and nothing else, and the reader was left to work out why the one they set did not count.
244
+ // And it is not an alternative to read or carry: `anyOf` is handed to the composer too, and the
245
+ // set-and-off line would travel through it.
246
+ const off = Object.values(CLOUD_SWITCH).filter((k) => val(env, k));
247
+ out.push({ name: CLOUD_SWITCH.vertex, anyOf: CLOUD_ROUTES.filter((k) => !off.includes(k)), blocking: true, at: ORDER, present: false,
248
+ why: `${payWord} pays through a cloud account and nothing names which one — set `
249
+ + `${["vertex", "foundry", "bedrock"].map((c) => `${CLOUD_SWITCH[c]}=1 for ${CLOUD_CREDENTIAL_CHECK[c].who}`).join(", ")}, `
250
+ + `or ANTHROPIC_BASE_URL for a gateway`
251
+ + (off.length ? ` (${off.join(" and ")} ${off.length > 1 ? "are" : "is"} set, but not on: a switch is on at 1, true, yes or on)` : "")
252
+ + `; ${refused}` });
253
+ }
254
+ // EVERY OTHER CLOUD SETTING THAT IS SET, CARRIED AND NEVER ASKED FOR. Which of them a cloud needs
255
+ // depends on how the machine signs in to it — an Azure key or the Azure sign-in, an AWS profile, an
256
+ // instance role or keys, gcloud or a key file — and the model pins are optional everywhere. The run
257
+ // door asks for none of them; the cloud refuses a missing one at the first turn. So a row is made
258
+ // only for a name that is set: present by construction, it can never be reported missing or refuse
259
+ // anything, and exists so the composer carries it. Names on CLOUD_SETTINGS only — never the rest of
260
+ // the environment, which would put every secret in a shell into the services' file.
261
+ //
262
+ // NEVER A SWITCH. One that is on is a row above. One that is set and not on switches nothing, and
263
+ // carried into the services' file it held that line there, where the add-only merge kept a later
264
+ // `=1` out of it for good.
265
+ const who = on.length === 1 ? CLOUD_CREDENTIAL_CHECK[on[0]].who : !on.length && val(env, "ANTHROPIC_BASE_URL") ? "the gateway" : "the cloud account";
266
+ const named = new Set(out.flatMap((r) => r.anyOf ?? [r.name]));
267
+ for (const k of CLOUD_SETTINGS)
268
+ if (!named.has(k) && !Object.values(CLOUD_SWITCH).includes(k) && val(env, k))
269
+ push(k, false, `one of the settings Claude reads to reach and pay ${who}; carried as set, and not asked for, because which ones a cloud needs depends on how this machine signs in to it`);
270
+ }
271
+ // ── AND ANY OTHER WAY THE RUN DOOR REFUSES HOW THIS IS PAID FOR ──────────────────────────────────
272
+ //
273
+ // The rows above name what is MISSING. The run door also refuses what is set wrongly: two clouds
274
+ // switched on, a switch left on beside `subscription` or `api-key`, a word that is not a billing mode,
275
+ // a cloud account on Codex. Each passed start's guard, the order wall and doctor, and the run was then
276
+ // refused after intake. Measured 2026-09-15: a services' file holding Azure's switch from one start
277
+ // and Google's from the next read clean everywhere and refused every search.
278
+ //
279
+ // ASKED OF THE RUN DOOR ITSELF, `resolveAuthMode`, over the environment being judged, and its refusal
280
+ // is the reason: one authority, so this check can be neither stricter nor laxer than the door. Its
281
+ // words carry names and the billing word, never a key. The row that turns blocking is the billing
282
+ // word's own, so no new name is handed to the composer: a switch beside `subscription` stays out of
283
+ // the services' file, as the cloud rows above intend. `present` on a row means SATISFIED, which is
284
+ // what every reader of it asks; for this row, set is not enough. When a row above already names what
285
+ // is missing, that row is the answer and this adds nothing, because the door's refusal is the same one.
286
+ //
287
+ // ONE OF THE DOOR'S REFUSALS QUOTES A VALUE: the word that is not a billing mode. A key pasted into the
288
+ // billing word by mistake is that value, and this reason is printed by start, by doctor and in the
289
+ // runner's log. So it is said by name, through billingRefusalWords below.
290
+ if (!out.slice(billingRows).some((r) => r.blocking && !r.present)) {
291
+ try { resolveAuthMode({ engineName: engineId, env }); } catch (e) {
292
+ if (e?.billingRefusal)
293
+ Object.assign(billingRow, { blocking: true, present: false, at: ORDER,
294
+ why: `every search is refused before spending, over how this machine is set to pay: ${billingRefusalWords(e.message)}` });
295
+ }
296
+ }
297
+ }
136
298
 
137
299
  push(RESEARCH_ENV, false, "the three clearance searches carry the common-law grid and refuse at preflight without it; a Knockout search still runs and discloses the half it skipped");
138
300
 
@@ -147,7 +309,9 @@ export function runRequirements(env = {}, { registers = [], engines = {}, defaul
147
309
  * a box whose operator configured them, which is the same shape of defect one size down.
148
310
  */
149
311
  export function runRequiredNames(env = {}, tables = {}) {
150
- return runRequirements(env, tables).map((r) => r.name);
312
+ // A row any one of several settings satisfies names them in `anyOf`, and each is a name to carry or read.
313
+ // Each name once: a name handed out twice is read twice and reported twice.
314
+ return [...new Set(runRequirements(env, tables).flatMap((r) => r.anyOf ?? [r.name]))];
151
315
  }
152
316
 
153
317
  /**
package/driver/runner.mjs CHANGED
@@ -21,7 +21,7 @@
21
21
  import "../shared/env-local.mjs"; // side effect: apply <repo>/.env when THIS file is the CLI entry (never on library import)
22
22
  import "./engine/mcp/http-dispatcher.mjs"; // side effect: raise undici headersTimeout (code-side fetches)
23
23
  import { readdirSync, renameSync, existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, rmSync, statSync } from "node:fs";
24
- import { join, dirname, basename } from "node:path";
24
+ import { join, dirname, basename } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
25
25
  import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
26
26
  // The queue's filename vocabulary, in ONE place — the rule, extended by to the prose-sidecar and
27
27
  // claim-sidecar names, because a harness check retyped four of them from memory and false-alarmed on the
@@ -774,15 +774,15 @@ async function backstopFailureNotice({ res, job, agentId, base, codename, studio
774
774
  // starts, so nothing is spent and nothing is promised.
775
775
 
776
776
  let __runTables = null;
777
- async function runTables() {
777
+ export async function runTables() { // exported for the requirement-check wiring test
778
778
  // AT CALL TIME, never a static import. `driver/run-requirements.mjs`'s header states the reason and it
779
779
  // is load-bearing: the register SELECTION table lives in `bin/onboard.mjs`, a CLI entry point, and a
780
780
  // static import from `driver/` would point the driver at `bin/` — the cycle that makes `clearotron
781
781
  // doctor` exit 13 after printing most of a report.
782
782
  if (!__runTables) {
783
783
  const { PROVIDERS } = await import("../bin/onboard.mjs");
784
- const { ENGINE_BINARIES, DEFAULT_ENGINE_ID } = await import("./driver.config.mjs");
785
- __runTables = { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: DEFAULT_ENGINE_ID };
784
+ const { ENGINE_BINARIES, DEFAULT_ENGINE_ID, resolveEngineProgram } = await import("./driver.config.mjs");
785
+ __runTables = { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: DEFAULT_ENGINE_ID, resolveEngine: resolveEngineProgram };
786
786
  }
787
787
  return __runTables;
788
788
  }
@@ -1548,7 +1548,7 @@ export function agentStudioRoots() {
1548
1548
  try {
1549
1549
  for (const name of readdirSync(config.workspaceRoot)) {
1550
1550
  if (config.agentIdFromWorkspaceName(name) == null) continue;
1551
- const s = join(config.workspaceRoot, name, "studio", "prelim-search");
1551
+ const s = studioDirFor(join(config.workspaceRoot, name));
1552
1552
  if (existsSync(s)) out.push(s);
1553
1553
  }
1554
1554
  } catch { /* workspaceRoot absent in some envs */ }
@@ -26,6 +26,7 @@
26
26
  // keys only on the neutral plan/band vocabulary, never a vendor name or vendor-shaped field.
27
27
 
28
28
  import { classTokensFromScopeText } from "./coverage-ledger.mjs";
29
+ import { capabilitiesFor } from "./register-capabilities.mjs";
29
30
 
30
31
  const clsStr = (c) => String(c ?? "").trim();
31
32
 
@@ -144,10 +145,11 @@ export function deriveScopeFacts({ instructedScope = null, plan = null, planExec
144
145
 
145
146
  const searched_jurisdictions = Array.isArray(plan?.regions) ? plan.regions.map(String) : [];
146
147
  const scope_basis = plan?.scope_basis === "worldwide" ? "worldwide" : null;
148
+ const register_service = registerServiceOf(plan);
147
149
 
148
150
  const classes_line = instructedClasses.length ? instructedClasses.join(", ") : null;
149
151
  const coverage_line = instructedClasses.length && entries.length
150
- ? buildCoverageLine(instructedClasses, per_class, { searched_jurisdictions, scope_basis, instructedScope })
152
+ ? buildCoverageLine(instructedClasses, per_class, { searched_jurisdictions, scope_basis, instructedScope, register_service })
151
153
  : null;
152
154
 
153
155
  return {
@@ -184,7 +186,7 @@ export function deriveScopeFacts({ instructedScope = null, plan = null, planExec
184
186
  // clause is word-for-word the same are stated ONCE, over the classes they are about. Nothing is pooled
185
187
  // and no number moves: the grouping is on the rendered text, so two classes only share a line when the
186
188
  // line they would each have printed is already the same string.
187
- function buildCoverageLine(classes, per_class, { searched_jurisdictions = [], scope_basis = null, instructedScope = null } = {}) {
189
+ function buildCoverageLine(classes, per_class, { searched_jurisdictions = [], scope_basis = null, instructedScope = null, register_service = null } = {}) {
188
190
  const sorted = [...classes].sort((a, b) => (Number(a) || 0) - (Number(b) || 0));
189
191
  const byBody = new Map();
190
192
  for (const c of sorted) {
@@ -197,7 +199,7 @@ function buildCoverageLine(classes, per_class, { searched_jurisdictions = [], sc
197
199
  // bodies A, B, A renders "Classes 5 and 32: A; Class 9: B", not A twice.
198
200
  const groups = [...byBody.entries()].map(([body, classes]) => ({ body, classes }));
199
201
  const head = (cs) => (cs.length === 1 ? `Class ${cs[0]}` : `Classes ${joinAnd(cs.map(String))}`);
200
- return `${groups.map((g) => `${head(g.classes)}: ${g.body}`).join("; ")}${jurisdictionTail({ searched_jurisdictions, scope_basis, instructedScope })}`;
202
+ return `${groups.map((g) => `${head(g.classes)}: ${g.body}`).join("; ")}${jurisdictionTail({ searched_jurisdictions, scope_basis, instructedScope, register_service })}`;
201
203
  }
202
204
 
203
205
  // One plain-language clause per class, routed on the COUNTS (state-agnostic), not on the state label.
@@ -263,11 +265,24 @@ const joinAnd = (parts) => (parts.length <= 1 ? parts.join("") : `${parts.slice(
263
265
  // EU US CH WO doesn't make sense". A worldwide-scoped plan (scope_basis, or a worldwide token riding
264
266
  // an instructed list) collapses the tail to the one word; only a genuinely named list is listed.
265
267
  const WORLDWIDE_TOKEN_RE = /^(worldwide|world|ww|global)$/i;
266
- function jurisdictionTail({ searched_jurisdictions = [], scope_basis = null, instructedScope = null } = {}) {
268
+
269
+ // WORLDWIDE NAMES THE SERVICE THAT SEARCHED IT (owner, 2026-09-18). "Worldwide" is eleven registers on one
270
+ // installation and 186 on another, and a reader can only tell which by being told the service. The name is
271
+ // the provider's own label from its capabilities contract — the same name the order form shows — read off
272
+ // the provider the frozen plan records. A plan that records none, or one this build does not know, keeps
273
+ // the bare word: a guessed name would be worse than none. Never a list of offices, never a count.
274
+ function registerServiceOf(plan) {
275
+ if (!plan?.provider) return null;
276
+ try { return capabilitiesFor(plan.provider).label ?? null; } catch { return null; }
277
+ }
278
+
279
+ function jurisdictionTail({ searched_jurisdictions = [], scope_basis = null, instructedScope = null, register_service = null } = {}) {
267
280
  const list = (searched_jurisdictions.length
268
281
  ? searched_jurisdictions
269
282
  : (Array.isArray(instructedScope?.jurisdictions) ? instructedScope.jurisdictions : [])).map((j) => String(j).trim()).filter((j) => j);
270
- if (scope_basis === "worldwide" || list.some((j) => WORLDWIDE_TOKEN_RE.test(j))) return " · registers: worldwide";
283
+ if (scope_basis === "worldwide" || list.some((j) => WORLDWIDE_TOKEN_RE.test(j))) {
284
+ return ` · registers: worldwide${register_service ? ` (${register_service})` : ""}`;
285
+ }
271
286
  if (!list.length) return "";
272
287
  // 2–3-letter office codes display uppercase ("us" reads as a pronoun, "US" as a jurisdiction);
273
288
  // longer names pass through untouched.
@@ -1,12 +1,12 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
- // scope-ledger.mjs — the machine mirror of the prelim-variants `### Scope ledger` (the frame-omission
3
+ // scope-ledger.mjs — the machine mirror of the clearance-variants `### Scope ledger` (the frame-omission
4
4
  // design: the blind-pass framing fix, approved into this file — the code is its own record).
5
5
  //
6
- // prelim-variants emits a prose `### Scope ledger` table — one row per variant / field / source the
6
+ // clearance-variants emits a prose `### Scope ledger` table — one row per variant / field / source the
7
7
  // run CONSIDERED and DROPPED (or applied), each carrying the observation that should REOPEN it. The
8
8
  // driver CODE-DERIVES scope-ledger.json from that prose (renderScopeLedgerJson, called after
9
- // prelim-variants validates) so the JSON is authored by the driver, never the model, and matches the
9
+ // clearance-variants validates) so the JSON is authored by the driver, never the model, and matches the
10
10
  // prose BY CONSTRUCTION — exactly the coverage-ledger.mjs pattern. The blind frame-diff reads the
11
11
  // dropped set + reopen triggers to decide which omissions to escalate.
12
12
  //
@@ -115,7 +115,7 @@ export function parseScopeLedgerJson(raw) {
115
115
 
116
116
  /**
117
117
  * CODE-DERIVE the JSON scope ledger FROM the prose `### Scope ledger` table (the driver calls this after
118
- * prelim-variants validates; never-killed at the call site). PURE. Throws `scope_ledger_unparseable`
118
+ * clearance-variants validates; never-killed at the call site). PURE. Throws `scope_ledger_unparseable`
119
119
  * when the prose carries a Scope ledger heading but yields no classifiable row, so the caller's catch
120
120
  * routes to the manifest-prose-fallback path (the run still delivers). Returns a JSON ARRAY string that
121
121
  * round-trips through parseScopeLedgerJson.
@@ -136,7 +136,7 @@ export function renderScopeLedgerJson(md) {
136
136
  * MARKDOWN TABLE back out of a prose manifest a model had typed, on fixed column positions. A machine
137
137
  * artifact the downstream depends on, recovered from a table.
138
138
  *
139
- * `record_prelim_variants` now receives those rows TYPED, so the driver can serialise them directly. Both
139
+ * `record_clearance_variants` now receives those rows TYPED, so the driver can serialise them directly. Both
140
140
  * paths call THIS function, which is what makes the recorded and the archived ledger byte-identical for
141
141
  * the same rows by construction rather than by an assertion someone has to maintain — and it is why the
142
142
  * prose parse can stay for archives (the anchor rule: a new way in, never a replacement) without becoming
@@ -15,10 +15,10 @@
15
15
  // ── THE DEPTH LADDER IS GONE ─────────────────────────────────────────────────────────────────────────────────
16
16
  //
17
17
  // There used to be a second vocabulary here: a closed set of LEVELS (`knockout`, `knockout-register`,
18
- // `prelim`, `prelim-jx`) with a display face reading "Depth 1"…"Depth 5", ordered by effort. A client
18
+ // `clearance`, `clearance-jx`) with a display face reading "Depth 1"…"Depth 5", ordered by effort. A client
19
19
  // bought a level; the thing they were sold — a knockout, a worldwide preliminary, a single-country deep
20
20
  // dive — was a different word that appeared on no wire. The two disagreed in the one place it mattered:
21
- // `prelim` named THREE products depending on where it pointed, and the composer's own footer had to
21
+ // `clearance` named THREE products depending on where it pointed, and the composer's own footer had to
22
22
  // invent labels ("Deep dive — United States", "Full clearance") for distinctions "the registry has no
23
23
  // word for". The registry now has the word. The level menu, its numbering, its ordering and the
24
24
  // `searchLevel` wire field are DELETED — not deprecated, not hidden behind the product name.
@@ -61,7 +61,7 @@ export const ORDERABLE_PRODUCTS = PRODUCT_IDS;
61
61
  //
62
62
  // This is the reading that squares "no old mechanism survives its replacement" with an archive: the
63
63
  // delete rule governs the ORDERABLE path, and naming a run that already happened is not that path.
64
- export const RETIRED_PRODUCTS = ["knockout", "knockout-register", "prelim-register-only", "prelim", "prelim-jx"];
64
+ export const RETIRED_PRODUCTS = ["knockout", "knockout-register", "clearance-register-only", "clearance", "clearance-jx"];
65
65
 
66
66
  /** Saved searches dropped by the last `loadRecipes` because their base is a retired product. Rewritten on
67
67
  * every load, so it describes the CURRENT store and never accumulates. Read it to tell a reader why a
@@ -304,9 +304,9 @@ export const RETIRED_POLICIES = {
304
304
  // Retired 2026-08-06: the clearance shape with its unregistered-use half removed. That basis
305
305
  // still composes into riskStatement, and the workbook's own gate still inverts on it, because an
306
306
  // archived run of it must not re-render claiming a sweep that never ran.
307
- "prelim-register-only": { product: "prelim-register-only", stageLabel: "Depth 3", pipeline: "clearance", report: { template: "clearance", identity: "Preliminary clearance — register only" }, components: { registerProbe: false, jxLanes: false, commonLawGrid: false } },
308
- "prelim": { product: "prelim", stageLabel: "Depth 4", pipeline: "clearance", report: { template: "clearance", identity: "Preliminary clearance" }, components: { registerProbe: false, jxLanes: false, commonLawGrid: true } },
309
- "prelim-jx": { product: "prelim-jx", stageLabel: "Depth 5", pipeline: "clearance", report: { template: "clearance", identity: "Preliminary clearance with jurisdiction deep-dive" }, components: { registerProbe: false, jxLanes: true, commonLawGrid: true } },
307
+ "clearance-register-only": { product: "clearance-register-only", stageLabel: "Depth 3", pipeline: "clearance", report: { template: "clearance", identity: "Preliminary clearance — register only" }, components: { registerProbe: false, jxLanes: false, commonLawGrid: false } },
308
+ "clearance": { product: "clearance", stageLabel: "Depth 4", pipeline: "clearance", report: { template: "clearance", identity: "Preliminary clearance" }, components: { registerProbe: false, jxLanes: false, commonLawGrid: true } },
309
+ "clearance-jx": { product: "clearance-jx", stageLabel: "Depth 5", pipeline: "clearance", report: { template: "clearance", identity: "Preliminary clearance with jurisdiction deep-dive" }, components: { registerProbe: false, jxLanes: true, commonLawGrid: true } },
310
310
  };
311
311
 
312
312
  /** The report identity to print on a run's document: `{ template, identity, stageLabel, banner }`.
@@ -464,13 +464,23 @@ export function isRegisterOnly(policy) {
464
464
  * search asked for. Asking it "does this exist" is asking the wrong question — the orderability test is
465
465
  * always `ORDERABLE_PRODUCTS.includes(...)`, and a typo must fail closed against that positive list. */
466
466
  export function policyFor(product) {
467
- const k = String(product ?? "").trim().toLowerCase();
467
+ const k = productKeyAsRenamed(product);
468
468
  return PRODUCT_POLICIES[k] ?? RETIRED_POLICIES[k] ?? null;
469
469
  }
470
+ // THREE RETIRED KEYS WERE RENAMED WITH THE IDENTIFIER, and records written before it still carry the old
471
+ // spelling: an archived run's product, a saved search's base in a client's store. Read the old key as the
472
+ // new wherever a key is looked up, or an archived report loses its name and a store's saved searches fail
473
+ // to load over a base this build still knows as retired.
474
+ const PRE_RENAME_PRODUCT_KEYS = Object.freeze({ "prelim": "clearance", "prelim-register-only": "clearance-register-only", "prelim-jx": "clearance-jx" });
475
+ /** A product key, lower-cased, with a pre-rename spelling read as its current one. PURE. */
476
+ export function productKeyAsRenamed(product) {
477
+ const k = String(product ?? "").trim().toLowerCase();
478
+ return PRE_RENAME_PRODUCT_KEYS[k] ?? k;
479
+ }
470
480
 
471
481
  // What THIS build can actually execute. A resolution onto machinery a build does not carry must CLARIFY
472
482
  // at admission (never silently run the wrong-priced product — a knockout request running as a $40
473
- // prelim, or a 1.5 request running as a plain Stage 1, is the exact silent-substitution this file exists
483
+ // clearance, or a 1.5 request running as a plain Stage 1, is the exact silent-substitution this file exists
474
484
  // to forbid). These flags are flipped in CODE as each lane lands, never from the environment: there are
475
485
  // no runtime env kill switches — see the note below.
476
486
  // EXPORTED because availability is now asked about in two places, not one. The runner asks "may this
@@ -498,7 +508,7 @@ export const BUILT = { knockout: true, jxLanes: true, registerProbe: true };
498
508
  // They were not merely dead, they were actively harmful, and in exactly the way the old comment here
499
509
  // predicted: a caller outside the engine's environment reads every switch as unset, and unset was
500
510
  // indistinguishable from off. The portal was given a snapshot to work around it. The ops-MCP was not,
501
- // so `describe_options` and `plan_run` told clients that knockout, knockout-register and prelim-jx were
511
+ // so `describe_options` and `plan_run` told clients that knockout, knockout-register and clearance-jx were
502
512
  // "Not switched on for this account yet" while the engine would have run all three (2026-07-27). That is
503
513
  // the second time this service has lied for want of an environment variable — the first was the profiles
504
514
  // dir answering with a demo roster. Deleting the switch is the fix that cannot recur; plumbing a
@@ -830,7 +840,7 @@ export const CAPABILITY_SKIPPED_CAUSE = {
830
840
  * now honoured wherever it resolves, and built machinery is never "off".
831
841
  *
832
842
  * The RESOLVED policy is measured, never `policyFor(resolved.level)`: a recipe can turn jxLanes on over a
833
- * base of `prelim`, and the base registry entry would answer "available" for a resolution that is not.
843
+ * base of `clearance`, and the base registry entry would answer "available" for a resolution that is not.
834
844
  * Delegating the built arms to productAvailability is what makes the null-equivalence with
835
845
  * gateResolvedPolicy structural rather than a coincidence two edits can break (search-policy.test.mjs
836
846
  * pins it over the whole matrix).
@@ -1115,7 +1125,7 @@ export function loadRecipes({ dir = process.env.CLEAROTRON_RECIPES_DIR || null,
1115
1125
  // NOT SILENT, because unlike a retired extra this one is observable: a saved search disappears. The
1116
1126
  // skip is recorded so a caller can say why rather than leaving a lawyer's saved search gone with no
1117
1127
  // reason. Everything else still throws — a config error is still load-blocking.
1118
- if (RETIRED_PRODUCTS.includes(String(r?.base ?? "").trim().toLowerCase())) {
1128
+ if (RETIRED_PRODUCTS.includes(productKeyAsRenamed(r?.base))) {
1119
1129
  retiredBaseSkips.push({ customer, slug, base: String(r.base), why: "base names a retired search — the saved search cannot be ordered" });
1120
1130
  continue;
1121
1131
  }
@@ -1216,7 +1226,7 @@ export function checkMarkBudget(job, policy) {
1216
1226
  // THE SCOPE ITSELF.
1217
1227
  //
1218
1228
  // THAT LAST RUNG IS THE OFFERING, AND IT IS NOT A CONSTANT. The Generic default used to be the literal
1219
- // level `prelim`, which named three different products depending on where it pointed. A clearance that
1229
+ // level `clearance`, which named three different products depending on where it pointed. A clearance that
1220
1230
  // names no product IS whichever product its territories make it — `productFor(pipeline, scope)`, the one
1221
1231
  // function that answers that question anywhere — so the default is derived, not picked. The caller hands
1222
1232
  // in the RESOLVED scope (`territories`) because resolving it needs the profile and the project overlay,
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Every file here is prompt payload served to the model at runtime.** A stage is one engine turn told to read named
4
4
  files and follow them exactly, so an edit for brevity or tone changes what a clearance concludes —
5
- `prelim-search/synthesis-rules.md` is 1,025 lines of program. Never touch this tree in a documentation or tidy-up
5
+ `clearance-search/synthesis-rules.md` is 1,025 lines of program. Never touch this tree in a documentation or tidy-up
6
6
  task, and never let a mass edit reach it — the [ADR-0005](../../docs/decisions/0005-comments-carry-reasoning.md)
7
7
  comment sweep is required to exclude this tree, and that script does not exist yet, so nothing enforces it but you.
8
8
 
@@ -20,21 +20,21 @@ One directory per skill. `SKILL.md` is the entry point — YAML frontmatter (`na
20
20
  procedure. Every other file beside it is a reference the SKILL.md or the stage message names explicitly, with six
21
21
  exceptions: the four `risk-framework*.manifest.json` sidecars, whose paths the code DERIVES from the deck
22
22
  (`manifestPathFor()`, `../framework.mjs`) so validators and the renderer read band vocabulary without parsing prose;
23
- `prelim-search/risk-framework-triage.md`, the knockout lane's default ladder, named in code only
23
+ `clearance-search/risk-framework-triage.md`, the knockout lane's default ladder, named in code only
24
24
  (`../pipeline-knockout.mjs`) because the knockout stage message hands the seat the frozen `_driver/framework.json`
25
- instead; and `prelim-search/templates/search-request-form.html`, named only in `../publish/index.mjs`.
25
+ instead; and `clearance-search/templates/search-request-form.html`, named only in `../publish/index.mjs`.
26
26
 
27
27
  | Skill | What the stage does |
28
28
  |---|---|
29
29
  | `matter-frame` | Phase 0. Writes the matter's commercial context — sector, customer base, channels of trade, jurisdictions that materially matter, off-field sectors, watchlist seeds — before any search runs. `watchlist-reference.md` is enrichment, not authority. |
30
- | `prelim-variants` | Classifies the mark into one of six archetypes, derives a risk theory from that, emits the variant manifest both execution skills read. Non-Latin scripts: `transliteration-scripts.md`. |
30
+ | `clearance-variants` | Classifies the mark into one of six archetypes, derives a risk theory from that, emits the variant manifest both execution skills read. Non-Latin scripts: `transliteration-scripts.md`. |
31
31
  | `blind-frame`, `frame-diff` | Re-derives the threat model from the raw instruction alone, deliberately starved of the matter frame, then diffs that model against what the run actually scoped and emits reopen directives the driver acts on. Something has to test the frame instead of reasoning inside it. |
32
- | `prelim-common-law` | The marketplace / web / social sweep, as structured Perplexity research over the platform list the stage dictates. Prompt templates: `perplexity-prompts.md`. |
33
- | `prelim-register` | Register execution in the two modes a spawn selects: `unit.md` (the funnel — enumerate one axis to completion, decide nothing) and `digest.md` (judgment over the merged band). Plus `register-recipes.md`, `status-rules.md`, `stealth-filer-indicators.md`, `providers/`. |
32
+ | `clearance-common-law` | The marketplace / web / social sweep, as structured Perplexity research over the platform list the stage dictates. Prompt templates: `perplexity-prompts.md`. |
33
+ | `clearance-register` | Register execution in the two modes a spawn selects: `unit.md` (the funnel — enumerate one axis to completion, decide nothing) and `digest.md` (judgment over the merged band). Plus `register-recipes.md`, `status-rules.md`, `stealth-filer-indicators.md`, `providers/`. |
34
34
  | `placement-inquiry` | Applies commercial relevance per candidate — headline-candidate / sheet-2 / watchlist-annex / out-of-scope-filtered — before any tiering runs. |
35
35
  | `case-law-citation` | Grounds risk-relevant findings in precedent fetched in-session, never from memory. One thin adapter per source in `sources/`; `evals.md` defines what working means. |
36
36
  | `narrative-refutation` | Reads the finished narrative against the underlying findings files and returns CLEAR / CONDITIONAL / BLOCKING with itemised flags. Delivery is gated on the verdict. |
37
- | `prelim-search` | The doctrine the synthesis and delivery stages are held to: `synthesis-rules.md`, the `risk-framework*.md` ladders with their `.manifest.json` band vocabularies, `delivery-contract.md`, `report-prose.md`, `worked-examples.md`, `phase2-execution.md`, `field-doctrine-pharma.md`. |
37
+ | `clearance-search` | The doctrine the synthesis and delivery stages are held to: `synthesis-rules.md`, the `risk-framework*.md` ladders with their `.manifest.json` band vocabularies, `delivery-contract.md`, `report-prose.md`, `worked-examples.md`, `phase2-execution.md`, `field-doctrine-pharma.md`. |
38
38
  | `knockout-frame`, `knockout-assess` | Stages A and C of the knockout doctrine — a broad kill/no-kill triage screen over several candidate names at once (Stage B is a code-side research sweep). Not a clearance. |
39
39
 
40
40
  ## What reads it
@@ -53,25 +53,25 @@ instead; and `prelim-search/templates/search-request-form.html`, named only in `
53
53
 
54
54
  ## The provider boundary
55
55
 
56
- Operator syntax and vendor tool tokens like `corsearch_*` live only in `prelim-register/providers/`; the spine names
56
+ Operator syntax and vendor tool tokens like `corsearch_*` live only in `clearance-register/providers/`; the spine names
57
57
  the `register_*` tools, whichever vendor `CLEAROTRON_DATABASE` selects.
58
58
  `../test/provider-neutral-prose.test.mjs` holds three of that boundary's lines: no vendor-prefixed tool token
59
59
  outside `providers/` and the `engine/mcp/<provider>-server.mjs` glue; no provider record host in a provider-agnostic
60
60
  skill file (banned hosts are read FROM the provider docs, so a sixth provider is covered the day its doc lands);
61
61
  every register server registering only `register_*`, under the MCP name `register`. Field paths are the open edge —
62
62
  the matcher requires `<vendor>_` and cannot see camelCase, so no test fails on one, and
63
- `prelim-register/status-rules.md` still instructs off Corsearch's own field names (`corsearchStatusCode`,
63
+ `clearance-register/status-rules.md` still instructs off Corsearch's own field names (`corsearchStatusCode`,
64
64
  `onomaticsJurisdictionsStatuses`, the `owners[0].*` owner chain), plus one line of `register-recipes.md` off
65
- `onomaticsOppositions[]`. `prelim-register/SKILL.md`, `unit.md`, `digest.md` and everything under `prelim-search/`
65
+ `onomaticsOppositions[]`. `clearance-register/SKILL.md`, `unit.md`, `digest.md` and everything under `clearance-search/`
66
66
  are clean. The authoring rules and the empirical-verification checklist a new provider doc must pass sit in
67
- `prelim-register/providers/README.md`.
67
+ `clearance-register/providers/README.md`.
68
68
 
69
69
  ## Where to start
70
70
 
71
- `prelim-search/SKILL.md` — the orchestrator's own skill, and the one file describing the whole workflow end to end.
72
- Then `prelim-register/SKILL.md`, the more elaborated of the two execution skills: a spine plus per-mode files
73
- (`unit.md`, `digest.md`) and per-provider files (`providers/`). The other, `prelim-common-law`, is one spine plus
71
+ `clearance-search/SKILL.md` — the orchestrator's own skill, and the one file describing the whole workflow end to end.
72
+ Then `clearance-register/SKILL.md`, the more elaborated of the two execution skills: a spine plus per-mode files
73
+ (`unit.md`, `digest.md`) and per-provider files (`providers/`). The other, `clearance-common-law`, is one spine plus
74
74
  `perplexity-prompts.md`, its two grid modes (deterministic `grid_spec_path` dispatch vs. the legacy authored
75
75
  program) being sections inside it — the per-mode split and `providers/` are register-only, not a shape every
76
- execution skill shares. `prelim-search/phase2-execution.md` is methodology, not sequencing — the pipeline is
76
+ execution skill shares. `clearance-search/phase2-execution.md` is methodology, not sequencing — the pipeline is
77
77
  sequenced in code, in `../stages.mjs` and `../pipeline.mjs`.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: blind-frame
3
- description: The frame-STARVED independent re-derivation for the v3 preliminary trademark search workflow. **Invoked exclusively by the `prelim-search` orchestrator/driver**, in parallel with the gather sweeps — do not call directly. Reads ONLY the raw instruction (mark, goods, classes, territories, manner of use) and NOT the matter frame, then re-derives the threat model cold across four layers — element + neighbours both directions, field by goods-overlap, sources by real channel, ranking by goods-overlap — and emits a structured model the frame-diff stage diffs against what the run actually scoped. Its job is to test the frame, not to reason inside it.
3
+ description: The frame-STARVED independent re-derivation for the v3 preliminary trademark search workflow. **Invoked exclusively by the `clearance-search` orchestrator/driver**, in parallel with the gather sweeps — do not call directly. Reads ONLY the raw instruction (mark, goods, classes, territories, manner of use) and NOT the matter frame, then re-derives the threat model cold across four layers — element + neighbours both directions, field by goods-overlap, sources by real channel, ranking by goods-overlap — and emits a structured model the frame-diff stage diffs against what the run actually scoped. Its job is to test the frame, not to reason inside it.
4
4
  ---
5
5
 
6
6
  ## Purpose
7
7
 
8
- Every other stage in a clearotron run reasons *inside* a frame that was set early (at `matter-frame` / `prelim-variants`): the variants to chase, the field that counts as on-field, the sources worth searching. Verification then runs *on* that frame — nothing tests the frame itself. When the frame is mis-scoped, the whole run inherits the miss and the skeptic, reasoning from the same frame, certifies it.
8
+ Every other stage in a clearotron run reasons *inside* a frame that was set early (at `matter-frame` / `clearance-variants`): the variants to chase, the field that counts as on-field, the sources worth searching. Verification then runs *on* that frame — nothing tests the frame itself. When the frame is mis-scoped, the whole run inherits the miss and the skeptic, reasoning from the same frame, certifies it.
9
9
 
10
10
  You are the antidote. You are **deliberately starved of the frame**: you receive only the raw instruction, exactly as the requester wrote it, and you re-derive the threat model **cold**. Because you never see the run's conclusions, you cannot anchor to them. Your output is later **diffed** against what the run actually scoped — the gaps in that diff are the omissions the frame missed.
11
11
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: case-law-citation
3
- description: Grounds risk-relevant trademark findings in cited case law and decisions instead of asserting them. Invoked by the prelim-search orchestrator at Step 4.5 to profile an aggressive enforcer or test a likelihood-of-confusion question against precedent, and usable inline in chat for ad-hoc conflict/enforcement questions. Queries live legal sources — CourtListener (US federal incl. CAFC), EUR-Lex (EU CJEU + General Court judgments, verbatim), and Legal Data Hunter (108-country statutes and case law) — and returns, per finding, on-point authorities (case or decision, forum, date, one-line holding, stable identifier) with relevance notes, or an explicit no-precedent result. Cites only from documents fetched in the session, never from memory. Use when grounding watchlist or aggressive-enforcer hits, or when a trademark conflict, confusability, or enforcement-history question needs precedent.
3
+ description: Grounds risk-relevant trademark findings in cited case law and decisions instead of asserting them. Invoked by the clearance-search orchestrator at Step 4.5 to profile an aggressive enforcer or test a likelihood-of-confusion question against precedent, and usable inline in chat for ad-hoc conflict/enforcement questions. Queries live legal sources — CourtListener (US federal incl. CAFC), EUR-Lex (EU CJEU + General Court judgments, verbatim), and Legal Data Hunter (108-country statutes and case law) — and returns, per finding, on-point authorities (case or decision, forum, date, one-line holding, stable identifier) with relevance notes, or an explicit no-precedent result. Cites only from documents fetched in the session, never from memory. Use when grounding watchlist or aggressive-enforcer hits, or when a trademark conflict, confusability, or enforcement-history question needs precedent.
4
4
  ---
5
5
 
6
6
  # Case-law citation
@@ -40,17 +40,17 @@ Companion files (one level deep — read the one you need):
40
40
 
41
41
  Still-future adapter (drops in here with no change to this file): `sources/euipo.md` (EUIPO
42
42
  Boards-of-Appeal decisions — no free API today). EUIPO *register* lookups are a different layer
43
- (`prelim-register`), not this skill.
43
+ (`clearance-register`), not this skill.
44
44
 
45
45
  ## Trigger
46
46
 
47
- Called by `prelim-search` after synthesis flags the risk-relevant findings (Step 4.5). Also triggers in
47
+ Called by `clearance-search` after synthesis flags the risk-relevant findings (Step 4.5). Also triggers in
48
48
  chat when the user raises a trademark conflict, likelihood-of-confusion, or enforcement-history question.
49
49
  Gated and optional: if this skill is absent, clearotron skips Step 4.5 and delivers normally.
50
50
 
51
51
  ## Model
52
52
 
53
- **Sonnet, eval-gated** when spawned from the `prelim-search` Step 4.5 seam. The work is extraction
53
+ **Sonnet, eval-gated** when spawned from the `clearance-search` Step 4.5 seam. The work is extraction
54
54
  from documents fetched **this session** — the fetch-before-cite discipline (cite only what you
55
55
  fetched; read the holding from the fetched text; refuse to invent when sources are empty), not model
56
56
  recall, is what keeps citations honest. That makes it a Sonnet-tier task **provided** Sonnet reliably
@@ -65,7 +65,7 @@ successfully fetch and verify.
65
65
  would close this gap; none is wired today.)
66
66
  - **Not** the EUIPO administrative layer: **EUIPO Boards-of-Appeal decisions** have no free API and are
67
67
  not searched — report that as a coverage gap. EUIPO *register* lookups are a different layer
68
- (`prelim-register`), not this skill.
68
+ (`clearance-register`), not this skill.
69
69
 
70
70
  ## Citing convention
71
71