clearotron 0.3.2-beta.8 → 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 (238) hide show
  1. package/.env.example +34 -3
  2. package/CONTRIBUTING.md +8 -8
  3. package/INSTALL.md +6 -6
  4. package/SECURITY.md +3 -3
  5. package/bin/brandowner.mjs +3 -3
  6. package/bin/framework-preflight.mjs +1 -1
  7. package/bin/start.mjs +18 -4
  8. package/build-info.json +2 -2
  9. package/docs/DELIVERY.md +2 -1
  10. package/docs/INTAKE.md +1 -1
  11. package/docs/ONBOARDING.md +1 -1
  12. package/docs/architecture/03-run-lifecycle.md +6 -6
  13. package/docs/architecture/04-configuration-reference.md +6 -3
  14. package/docs/architecture/05-config-governance.md +6 -1
  15. package/docs/architecture/05-customer-profiles.md +2 -2
  16. package/docs/architecture/06-operations-runbook.md +3 -3
  17. package/docs/architecture/08-development-guide.md +6 -6
  18. package/docs/configuration.md +5 -5
  19. package/docs/decisions/0003-credential-model.md +1 -1
  20. package/docs/writing-standard.md +4 -0
  21. package/driver/CHANGELOG.md +48 -0
  22. package/driver/README.md +3 -3
  23. package/driver/binding-layers.mjs +1 -1
  24. package/driver/citation-census.json +3 -3
  25. package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
  26. package/driver/common-law-receipts.mjs +2 -2
  27. package/driver/company-bundle.mjs +3 -3
  28. package/driver/compose-read.mjs +8 -14
  29. package/driver/consumption-ledger.mjs +2 -2
  30. package/driver/contract-arm2-baseline.json +2 -3
  31. package/driver/contract-dictation-registry.mjs +19 -19
  32. package/driver/contract-e3-backlog.mjs +19 -19
  33. package/driver/contract-e3-baseline.json +14 -14
  34. package/driver/contract-vocabulary.mjs +24 -17
  35. package/driver/deliver-trigger.sh +16 -16
  36. package/driver/demo-container.mjs +3 -3
  37. package/driver/dev-portal.mjs +3 -3
  38. package/driver/disposition-call.mjs +1 -1
  39. package/driver/doubt-ledger.mjs +2 -2
  40. package/driver/drainer-identity.mjs +34 -8
  41. package/driver/driver.config.mjs +95 -45
  42. package/driver/engine/mcp/README.md +1 -1
  43. package/driver/engine/mcp/dispositions-server.mjs +3 -3
  44. package/driver/engine/mcp/gather-config.mjs +9 -9
  45. package/driver/engine/mcp/perplexity-server.mjs +2 -2
  46. package/driver/engine/mcp/recording-server.mjs +5 -5
  47. package/driver/enqueue-schema.mjs +6 -2
  48. package/driver/findings-model.mjs +5 -2
  49. package/driver/flag-snapshot.mjs +6 -3
  50. package/driver/form-neighbourhood.mjs +54 -7
  51. package/driver/framework.mjs +4 -4
  52. package/driver/gateway.mjs +12 -6
  53. package/driver/jx-lanes.mjs +2 -2
  54. package/driver/jx-units.mjs +1 -1
  55. package/driver/jx.mjs +30 -2
  56. package/driver/knockout-review-record.mjs +56 -4
  57. package/driver/known-conflicts.mjs +1 -1
  58. package/driver/named-band.mjs +1 -33
  59. package/driver/ordinary-words.mjs +51 -0
  60. package/driver/outbox-backoff.mjs +31 -16
  61. package/driver/package.json +1 -1
  62. package/driver/partial-payload-baseline.json +2 -2
  63. package/driver/phase0.mjs +3 -3
  64. package/driver/pipeline-knockout.mjs +5 -5
  65. package/driver/pipeline.mjs +206 -68
  66. package/driver/placement-form.mjs +77 -1
  67. package/driver/placement-model.mjs +1 -1
  68. package/driver/portal-report.mjs +92 -5
  69. package/driver/portal-service.mjs +34 -8
  70. package/driver/portal-upstream.mjs +1 -1
  71. package/driver/preserve-merge.mjs +3 -3
  72. package/driver/product-rows.mjs +2 -2
  73. package/driver/products.mjs +1 -1
  74. package/driver/profiles/README.md +3 -3
  75. package/driver/profiles/demo-brand-owner.json +2 -2
  76. package/driver/profiles.mjs +55 -17
  77. package/driver/progress.mjs +18 -8
  78. package/driver/provider-usage.mjs +8 -8
  79. package/driver/publish/index.mjs +110 -5
  80. package/driver/publish/knockout.mjs +29 -4
  81. package/driver/publish/pool-admin.mjs +1 -1
  82. package/driver/publish/publish-inputs.mjs +18 -2
  83. package/driver/publish/render-knockout.mjs +115 -24
  84. package/driver/publish/render.mjs +153 -34
  85. package/driver/publish/search-depth.mjs +133 -4
  86. package/driver/publish/templates/report.css +60 -3
  87. package/driver/publish/xlsx.mjs +8 -1
  88. package/driver/queue-order.mjs +2 -2
  89. package/driver/recording-agreement.mjs +1 -1
  90. package/driver/reference-score.mjs +1 -1
  91. package/driver/register-count.mjs +50 -5
  92. package/driver/register-coverage.mjs +67 -0
  93. package/driver/register-grant-vocabulary.mjs +1 -1
  94. package/driver/register-plan.mjs +19 -2
  95. package/driver/registry-fidelity.mjs +3 -3
  96. package/driver/repair-composers.mjs +1 -1
  97. package/driver/repair-contract.mjs +1 -1
  98. package/driver/replay-archive.mjs +6 -6
  99. package/driver/report-overview-record.mjs +2 -2
  100. package/driver/run-requirements.mjs +3 -3
  101. package/driver/runner.mjs +2 -2
  102. package/driver/scope-facts.mjs +20 -5
  103. package/driver/scope-ledger.mjs +5 -5
  104. package/driver/search-policy.mjs +22 -12
  105. package/driver/skills/README.md +15 -15
  106. package/driver/skills/blind-frame/SKILL.md +2 -2
  107. package/driver/skills/case-law-citation/SKILL.md +4 -4
  108. package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
  109. package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
  110. package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
  111. package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
  112. package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
  113. package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
  114. package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
  115. package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
  116. package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
  117. package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
  118. package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
  119. package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
  120. package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
  121. package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
  122. package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
  123. package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
  124. package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
  125. package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
  126. package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
  127. package/driver/skills/frame-diff/SKILL.md +1 -1
  128. package/driver/skills/knockout-assess/SKILL.md +10 -7
  129. package/driver/skills/matter-frame/SKILL.md +3 -3
  130. package/driver/skills/narrative-refutation/SKILL.md +9 -9
  131. package/driver/skills/placement-inquiry/SKILL.md +5 -5
  132. package/driver/stage-context.mjs +1 -1
  133. package/driver/stages-knockout.mjs +4 -4
  134. package/driver/stages.mjs +53 -53
  135. package/driver/status-snapshot.mjs +2 -2
  136. package/driver/suite-census.json +218 -92
  137. package/driver/surface-exit-verdict.mjs +58 -0
  138. package/driver/systemd/README.md +2 -2
  139. package/driver/systemd/clearotron-worker.service +1 -1
  140. package/driver/terminal-clamp.mjs +2 -0
  141. package/driver/usage-ledger.mjs +1 -1
  142. package/driver/variant-manifest-model.mjs +4 -4
  143. package/driver/verify-knockout.mjs +27 -0
  144. package/driver/verify.mjs +67 -6
  145. package/driver/whatif-queue.mjs +1 -1
  146. package/driver/wordlists/en.txt +63906 -0
  147. package/mcp-server/CHANGELOG.md +4 -0
  148. package/mcp-server/README.md +1 -1
  149. package/mcp-server/lib/README.md +1 -1
  150. package/mcp-server/lib/options.mjs +8 -7
  151. package/mcp-server/lib/plan.mjs +18 -2
  152. package/mcp-server/lib/runs.mjs +1 -1
  153. package/mcp-server/lib/usage.mjs +3 -3
  154. package/mcp-server/lib/whatif.mjs +1 -1
  155. package/mcp-server/package.json +1 -1
  156. package/mcp-server/server.mjs +3 -0
  157. package/package.json +12 -11
  158. package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
  159. package/portal-ui/dist/assets/{index-6jzO9HiX.js → index-EVaSo5-g.js} +1459 -482
  160. package/portal-ui/dist/index.html +2 -2
  161. package/portal-ui/package.json +1 -1
  162. package/providers/README.md +1 -1
  163. package/providers/_shared/enumerate.mjs +6 -6
  164. package/providers/_shared/execute-plan.mjs +3 -3
  165. package/providers/_shared/ledger.mjs +119 -5
  166. package/providers/_shared/provider-text.mjs +2 -2
  167. package/providers/_shared/screen.mjs +2 -2
  168. package/providers/_shared/script-form.mjs +3 -3
  169. package/providers/_shared/territory-codes.mjs +23 -3
  170. package/providers/clarivate/README.md +1 -1
  171. package/providers/clarivate/src/capabilities.js +12 -12
  172. package/providers/clarivate/src/core.js +37 -43
  173. package/providers/corsearch/README.md +1 -1
  174. package/providers/corsearch/src/capabilities.js +5 -5
  175. package/providers/corsearch/src/core.js +3 -3
  176. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  177. package/providers/oauth-mcp-bridge/package.json +1 -1
  178. package/providers/perplexity/src/core.js +1 -1
  179. package/providers/signa/README.md +1 -1
  180. package/providers/signa/src/capabilities.js +42 -49
  181. package/providers/signa/src/core.js +106 -29
  182. package/providers/uspto-local/src/sync.js +1 -1
  183. package/scripts/README.md +1 -0
  184. package/scripts/ask-ai-render-check.mjs +127 -1
  185. package/scripts/authority-boundary-probe.mjs +4 -4
  186. package/scripts/backfill-started-at.mjs +2 -2
  187. package/scripts/census-merge-driver.mjs +33 -2
  188. package/scripts/citation-anchor-report.mjs +181 -0
  189. package/scripts/dead-names.mjs +1 -1
  190. package/scripts/deprecate-below.mjs +66 -8
  191. package/scripts/drain-preflight.mjs +1 -1
  192. package/scripts/e2e.mjs +174 -0
  193. package/scripts/env-audit.mjs +27 -0
  194. package/scripts/env-classify.mjs +20 -2
  195. package/scripts/freeze-example-run.mjs +20 -10
  196. package/scripts/live-surface-check.mjs +124 -41
  197. package/scripts/markdown-link-check.mjs +1 -1
  198. package/scripts/merge-shape-check.mjs +242 -0
  199. package/scripts/mint-names-in-force.mjs +5 -5
  200. package/scripts/mint-offered-territories.mjs +72 -0
  201. package/scripts/mint-public-residue.mjs +2 -2
  202. package/scripts/mint-reference-strip-backlog.mjs +2 -2
  203. package/scripts/mint-suite-census.mjs +75 -2
  204. package/scripts/mint-writing-standard-backlog.mjs +2 -2
  205. package/scripts/purge-runs.mjs +7 -7
  206. package/scripts/reconcile-runs.mjs +2 -2
  207. package/scripts/release-approve-parked.mjs +20 -2
  208. package/scripts/release-await-cut.mjs +120 -1
  209. package/scripts/release-note-required.mjs +76 -8
  210. package/scripts/report-header-render-check.mjs +164 -0
  211. package/shared/brand.mjs +27 -0
  212. package/shared/connect-clients.mjs +39 -11
  213. package/shared/env-aliases.mjs +1 -1
  214. package/shared/identifier-scan.mjs +65 -9
  215. package/shared/identifier-sentinels.mjs +22 -0
  216. package/shared/names-in-force.mjs +3 -1
  217. package/shared/offered-territories.json +738 -0
  218. package/shared/pre-rename-spellings.mjs +53 -0
  219. package/shared/reference-guard-classes.mjs +40 -2
  220. package/shared/stdio-connect.mjs +39 -4
  221. package/shared/tree-commit.mjs +48 -0
  222. /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
  223. /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
  224. /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
  225. /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
  226. /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
  227. /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
  228. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
  229. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
  230. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
  231. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
  232. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
  233. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
  234. /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
  235. /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
  236. /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
  237. /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
  238. /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
@@ -105,14 +105,40 @@ export function drainerVerdict({ stamp, headCommit, isAlive, processes, ppidOf =
105
105
  const nameThem = (ps) => ps.map((p) => `pid ${p.pid}`).join(", ");
106
106
 
107
107
  if (!stamp) {
108
- // COULD NOT LOOK, AND THAT IS A FAILURE. The deploy must not report a build live having never
109
- // established what the executing process holds — "no stamp" is the exact state the incident's
110
- // orphaned drainer was in, so treating it as a skip would pass the very box this arm exists for.
111
- const extra = seen === null ? "and the process table could not be read either"
112
- : seen.length ? `while ${seen.length} drainer-shaped process(es) ARE running (${nameThem(seen)}) — unstamped, so what build they hold is unknown`
113
- : "and no drainer-shaped process is running, so nothing is executing runs on this box";
114
- return { state: "fail", message: `no drainer identity stamp at ${STAMP_BASENAME}: the build held by the process that `
115
- + `executes runs was NOT established, ${extra}. This is a failure to look, never a pass.${postureNote}` };
108
+ // THREE SITUATIONS LIVED IN ONE FAIL, AND ONE OF THEM IS NOT A FINDING.
109
+ //
110
+ // All three begin the same way — no stamp, so what the executing process holds was never
111
+ // established. What separates them is the PROCESS TABLE, and the old message ran the answers
112
+ // together: it asserted "nothing is executing runs on this box", a finding, and closed with "This is
113
+ // a failure to look, never a pass", which is the opposite claim. A reader could act on either.
114
+ //
115
+ // The module's ruling about postures says what each exit's posture IS. It does not license reporting
116
+ // two different facts through one verdict, and a deployment check is exactly where that is
117
+ // expensive: redeploying fixes an idle box and does nothing at all for an unreadable process table.
118
+ //
119
+ // — NOTHING WAS ESTABLISHED IN EITHER DIRECTION. No stamp and no process table: this arm did not
120
+ // find an idle box, it failed to look at one. Skipped and marked, never passed — the caller counts
121
+ // it toward a different exit code, and the box could be perfectly healthy or completely wedged.
122
+ if (seen === null) {
123
+ return { state: "skip", blocked: true,
124
+ message: `no drainer identity stamp at ${STAMP_BASENAME} AND the process table could not be read: what the `
125
+ + "process that executes runs holds was not established, and neither was whether one is running at all. "
126
+ + `This is a failure to look — redeploying would change nothing, because nothing was compared.${postureNote}` };
127
+ }
128
+ // — THE INCIDENT'S OWN SHAPE. Drainer-shaped processes are running and none of them stamped
129
+ // anything, so what build they hold is unknown. This is the state the orphaned drainer was in and
130
+ // the reason this arm exists; it is a finding and stays one.
131
+ if (seen.length) {
132
+ return { state: "fail", message: `no drainer identity stamp at ${STAMP_BASENAME} while ${seen.length} `
133
+ + `drainer-shaped process(es) ARE running (${nameThem(seen)}) — unstamped, so what build they hold is `
134
+ + `unknown. The process table was read; this is what it said.${postureNote}` };
135
+ }
136
+ // — A FINDING, and the plainest one here. The process table WAS read and holds no drainer, so
137
+ // nothing is executing runs on this box. Saying "failure to look" about a table that answered was
138
+ // the contradiction at the centre of this.
139
+ return { state: "fail", message: `no drainer identity stamp at ${STAMP_BASENAME} and no drainer-shaped process `
140
+ + "is running, so nothing is executing runs on this box. The process table was read, and this is what "
141
+ + `it said.${postureNote}` };
116
142
  }
117
143
 
118
144
  const pid = Number(stamp.pid) || 0;
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
- // Central paths + tunables for the prelim-search deterministic driver.
3
+ // Central paths + tunables for the clearance-search deterministic driver.
4
4
  //
5
5
  // The driver runs as an ordinary UNIX service account (launched by systemd), NOT as an LLM agent.
6
6
  // The agent exec-deny is a gateway agent-tool restriction; it does not apply to this OS process.
@@ -15,7 +15,7 @@ import { isWsl } from "../shared/wsl.mjs"; // — the one answer to "is this L
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
17
  import { envFileRead } from "../shared/env-local.mjs"; // — WHICH file to set it in, measured; null for a service that read none
18
- import { numericSetting, resolveNumericSetting } from "./numeric-setting.mjs"; // — a number, or a refusal that names the variable; never NaN
18
+ 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
19
 
20
20
  const { X_OK } = FS;
21
21
 
@@ -111,6 +111,23 @@ export const envGateOn = (name) => {
111
111
  // so import-time captures silently pinned every test to the FIRST test's env (workspace root, pool,
112
112
  // retries) — the root of the intermittent cross-test contamination flake. Getters make "set env, then
113
113
  // run" mean what it says, in tests and in prod alike.
114
+ // The four skill folders the identifier rename moved. A store outside the product may still hold them,
115
+ // and a profile may still name them, under the old spelling.
116
+ const RENAMED_SKILL_DIRS = Object.freeze([
117
+ ["clearance-search", "prelim-search"], ["clearance-register", "prelim-register"],
118
+ ["clearance-common-law", "prelim-common-law"], ["clearance-variants", "prelim-variants"],
119
+ ]);
120
+ /** `[rel, the same path under the folder's other spelling | null]`. PURE. */
121
+ export function skillSpellings(rel) {
122
+ const m = /^skills\/([a-z-]+)(\/.*)?$/.exec(String(rel ?? ""));
123
+ if (!m) return [rel, null];
124
+ for (const [now, before] of RENAMED_SKILL_DIRS) {
125
+ if (m[1] === now) return [rel, `skills/${before}${m[2] ?? ""}`];
126
+ if (m[1] === before) return [rel, `skills/${now}${m[2] ?? ""}`];
127
+ }
128
+ return [rel, null];
129
+ }
130
+
114
131
  export const config = {
115
132
  // / — A BLANK VALUE IS NOT A CONFIGURED VALUE. `process.env.X || default` treats " " as
116
133
  // configured, because a whitespace-only string is TRUTHY in JavaScript — so a variable set to spaces
@@ -212,21 +229,8 @@ export const config = {
212
229
  * still fails loudly against the canonical location rather than silently against the overlay.
213
230
  */
214
231
  resolveSkillPath(relFromSkillsRoot) {
215
- const rel = String(relFromSkillsRoot ?? "").replace(/^\/+/, "");
216
- const overlay = this.skillsOverlayDir;
217
- if (overlay) {
218
- // FAIL LOUD ON AN UNREADABLE OVERLAY. existsSync() answers false for a permission error just as it
219
- // does for a missing file, so a config store the process cannot read would silently resolve EVERY
220
- // file to the repo — swapping a customer's own risk framework for the Generic default with nothing
221
- // in the log to say so. A configured-but-unreadable overlay is a deploy defect, not a fallback.
222
- if (!existsSync(overlay))
223
- throw new Error(`skills_overlay_unreadable:${overlay} (CLEAROTRON_INSTRUCTIONS_DIR names it, set by the operator or derived by the portal from PROFILE_REPO_ROOT, but this process cannot see it — customer-specific skills would silently fall back to the repo defaults)`);
224
- const p = join(dirname(overlay), rel);
225
- if (existsSync(p)) return p;
226
- }
227
- return join(dirname(this.skillsBaseDir), rel);
232
+ return this.resolveSkillPathReport(relFromSkillsRoot).path;
228
233
  },
229
-
230
234
  /**
231
235
  * WHICH LAYER ANSWERED, as a fact rather than a path — `resolveSkillPath` with its reasoning shown.
232
236
  *
@@ -252,13 +256,24 @@ export const config = {
252
256
  */
253
257
  resolveSkillPathReport(relFromSkillsRoot) {
254
258
  const rel = String(relFromSkillsRoot ?? "").replace(/^\/+/, "");
255
- const basePath = join(dirname(this.skillsBaseDir), rel);
259
+ // EITHER SPELLING OF A RENAMED SKILL FOLDER, in either direction. A store that has not moved its
260
+ // folders still holds `skills/prelim-search/…`, and a profile written before the rename still NAMES
261
+ // it; the product ships only the new names. Tried in the overlay first — a client's own doctrine must
262
+ // never be silently replaced by ours because a folder name changed — and the base answers only under
263
+ // the name it ships.
264
+ const [asNamed, other] = skillSpellings(rel);
265
+ const shipped = rel === asNamed && other && /^skills\/prelim-/.test(asNamed) ? other : asNamed;
266
+ const basePath = join(dirname(this.skillsBaseDir), shipped);
256
267
  const overlay = this.skillsOverlayDir;
257
268
  if (!overlay) return { path: basePath, rel, layer: existsSync(basePath) ? "base-only" : "missing", overlayPath: null, basePath };
258
269
  if (!existsSync(overlay))
259
270
  throw new Error(`skills_overlay_unreadable:${overlay} (CLEAROTRON_INSTRUCTIONS_DIR names it, set by the operator or derived by the portal from PROFILE_REPO_ROOT, but this process cannot see it — customer-specific skills would silently fall back to the repo defaults)`);
260
- const overlayPath = join(dirname(overlay), rel);
271
+ const overlayPath = join(dirname(overlay), asNamed);
261
272
  if (existsSync(overlayPath)) return { path: overlayPath, rel, layer: "overlay", overlayPath, basePath };
273
+ if (other) {
274
+ const otherPath = join(dirname(overlay), other);
275
+ if (existsSync(otherPath)) return { path: otherPath, rel, layer: "overlay", overlayPath: otherPath, basePath };
276
+ }
262
277
  return { path: basePath, rel, layer: existsSync(basePath) ? "base" : "missing", overlayPath, basePath };
263
278
  },
264
279
 
@@ -276,7 +291,7 @@ export const config = {
276
291
  return roots;
277
292
  },
278
293
 
279
- // The base that a profile's "skills/prelim-search/<file>.md" path is relative to — i.e. the PARENT
294
+ // The base that a profile's "skills/clearance-search/<file>.md" path is relative to — i.e. the PARENT
280
295
  // of skillsDir. Everything the DRIVER reads itself (framework manifests, band-meaning extraction)
281
296
  // must join against this, exactly as the agent resolves the same relative paths against the
282
297
  // skillsDir it is handed (gateway.mjs engineSkillsDir).
@@ -305,15 +320,17 @@ export const config = {
305
320
  },
306
321
  // Escaped prefix for the reverse regexes below (a custom prefix may carry regex metachars).
307
322
  get workspacePrefixRe() { return this.workspacePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); },
308
- // `prelim-search` IS NOT A PRODUCT NAME HERE and does not follow the product rename. It is a
309
- // directory segment on disk, and every archived run — its slug dirs, its `archive/`, its matter
310
- // ledger — was written under it. Renaming the segment does not move those runs; it points the
311
- // reader somewhere empty, and an empty directory reads as "no runs" rather than as an error.
312
- // The rule the tree already enforces elsewhere: a token that is read back out of an archive keeps
313
- // its old spelling, or the code refuses its own archive. Thirteen sites compute this segment and
314
- // all thirteen stay.
323
+ // THE STUDIO SEGMENT IS NOT A PRODUCT NAME and does not follow the product rename. It is a directory
324
+ // segment on disk, and every archived run — its slug dirs, its `archive/`, its queue, its matter ledger —
325
+ // was written under it. Renaming it does not move those runs; it points the reader somewhere empty, and
326
+ // an empty directory reads as "no runs" rather than as an error. The identifier rename rewrote this
327
+ // comment together with the thirteen sites it protected, and an upgraded install lost sight of its whole
328
+ // archive. So an install keeps the segment it has: `prelim-search` wherever that directory exists, and
329
+ // `clearance-search` only for an install that has no other. Every site asks `studioSegmentFor`.
330
+ studioSegment(workspaceDir) { return studioSegmentFor(workspaceDir); },
315
331
  studioRootForAgent(agentId) {
316
- return join(this.workspaceRoot, this.workspaceDirName(agentId), "studio", "prelim-search");
332
+ const ws = join(this.workspaceRoot, this.workspaceDirName(agentId));
333
+ return join(ws, "studio", studioSegmentFor(ws));
317
334
  },
318
335
  queueDirForAgent(agentId) {
319
336
  return join(this.studioRootForAgent(agentId), "queue");
@@ -321,9 +338,10 @@ export const config = {
321
338
  archiveRootForAgent(agentId) {
322
339
  return join(this.studioRootForAgent(agentId), "archive");
323
340
  },
324
- // …/<prefix><id>/studio/prelim-search/queue → "<id>"; null if the path isn't an agent queue dir.
341
+ // …/<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.
325
343
  agentIdFromQueueDir(qdir) {
326
- const m = new RegExp(`(?:^|/)${this.workspacePrefixRe}([^/]+)/studio/prelim-search/queue/?$`).exec(qdir);
344
+ const m = new RegExp(`(?:^|/)${this.workspacePrefixRe}([^/]+)/studio/${STUDIO_SEGMENT_RE}/queue/?$`).exec(qdir);
327
345
  return m ? m[1] : null;
328
346
  },
329
347
  // Every agent workspace's clearotron queue. The systemd `.path` watches these and the runner drains ALL of
@@ -343,14 +361,18 @@ export const config = {
343
361
  try {
344
362
  for (const name of readdirSync(root)) {
345
363
  if (this.agentIdFromWorkspaceName(name) == null) continue;
346
- const q = join(root, name, "studio", "prelim-search", "queue");
347
- if (existsSync(q)) dirs.push(q);
364
+ // BOTH spellings are drained where both exist: a job queued under either is a job somebody is
365
+ // waiting on, and an unwatched queue looks exactly like an empty one.
366
+ for (const seg of STUDIO_SEGMENTS) {
367
+ const q = join(root, name, "studio", seg, "queue");
368
+ if (existsSync(q)) dirs.push(q);
369
+ }
348
370
  }
349
371
  } catch { /* workspaceRoot may not exist in some test envs — fall through to the canonical queue */ }
350
372
  // THE ONE AGENT NAME NO CONFIGURATION REMOVES, and it is deliberate. row 5.
351
373
  //
352
374
  // `this.queueDir` is `queueDirForAgent("clawdi")` — a LITERAL, not `defaultAgent` — so every
353
- // deployment, however configured, watches `<workspacePrefix>clawdi/studio/prelim-search/queue`.
375
+ // deployment, however configured, watches `<workspacePrefix>clawdi/studio/clearance-search/queue`.
354
376
  // Setting CLEAROTRON_DEFAULT_AGENT does not remove it (prod runs `ops`, dev runs `dev`, and both still
355
377
  // watch this); nor does CLEAROTRON_WORKSPACE_PREFIX. An installer who copies `.env.example` gets a
356
378
  // neutral default agent AND this directory.
@@ -540,7 +562,7 @@ export const config = {
540
562
  // directory, so renaming the default moves the install to an empty one and nothing migrates. Here
541
563
  // the orphaned files are run-slot locks, so a live run's slot goes unseen and the global cap is
542
564
  // silently exceeded rather than enforced. Ruling.
543
- get runLockDir() { return this.envValue("CLEAROTRON_RUN_LOCK_DIR") || join(this.workspaceRoot, "prelim-run-locks"); },
565
+ get runLockDir() { return this.envValue("CLEAROTRON_RUN_LOCK_DIR") || [join(this.workspaceRoot, "prelim-run-locks"), join(this.workspaceRoot, "clearance-run-locks")].find((d, i) => i === 1 || existsSync(d)); }, // the name the install already has wins
544
566
 
545
567
  // Delivery outbox (Workstream B). On a handoff-mode finish the driver drops <runId>.pending here (naming
546
568
  // the forwarder agent); the systemd-user prelim-outbox.path unit fires an INSTANT clearotron-deliver wake off
@@ -552,7 +574,31 @@ export const config = {
552
574
  // directory, so renaming the default moves the install to an empty one and nothing migrates. Here
553
575
  // the orphaned files are requester-facing events — delivered, run-failed, intake-rejected — so the
554
576
  // visible failure is a requester never told their run finished. Ruling.
555
- get outboxDir() { return this.envValue("CLEAROTRON_OUTBOX_DIR") || join(this.workspaceRoot, "prelim-outbox"); },
577
+ get outboxDir() { return this.envValue("CLEAROTRON_OUTBOX_DIR") || join(this.workspaceRoot, "clearance-outbox"); },
578
+
579
+ // ── THE DIRECTORY THIS DEFAULT USED TO NAME, AND WHY IT IS STILL READ ──────────────────────────────
580
+ //
581
+ // This default was `clearance-outbox` until the identifier was renamed. A deployment that never pinned
582
+ // `CLEAROTRON_OUTBOX_DIR` has its `<runId>.pending` markers sitting in the old directory, and moving
583
+ // the default without reading the old one orphans every one of them. That failure is silent and it is
584
+ // the expensive kind: a marker is a report a client is OWED, an unread outbox is indistinguishable
585
+ // from nothing to send, and the run that produced it has already recorded itself as delivered-pending.
586
+ //
587
+ // NULL WHEN THE VARIABLE IS SET, because then the operator named the directory and there is no old
588
+ // default in play. Writers use `outboxDir` alone; only readers consult this, which is the same posture
589
+ // the run records take — new work uses the new name, old work is still understood.
590
+ //
591
+ // THE OLD SPELLING IS THE WHOLE POINT OF THIS ACCESSOR, and a sweep took it. The identifier rename
592
+ // moved the directory from `prelim-outbox` to `clearance-outbox`, and the follow-up that renamed the
593
+ // directory's 85 occurrences rewrote this literal along with them — leaving the accessor that exists to
594
+ // name the OLD directory naming the new one. Both getters then answered the same path: the drain read
595
+ // the new directory twice, never read the old one, and every marker under it was listed TWICE. So a box
596
+ // that never pinned the variable got exactly the failure the block above describes — orphaned markers,
597
+ // silently — plus duplicate work on the ones it could see. It is written once, here, and pinned by a
598
+ // test that asserts the two are different.
599
+ get legacyOutboxDir() {
600
+ return this.envValue("CLEAROTRON_OUTBOX_DIR") ? null : join(this.workspaceRoot, "prelim-outbox");
601
+ },
556
602
 
557
603
  // ── Delivery/comms (Phase 2, standalone product) ─────────────────────────────────────────────────
558
604
  // THERE IS ONE MODE AND IT IS NOT A SETTING. The driver SENDS NOTHING: every requester-facing event
@@ -570,7 +616,7 @@ export const config = {
570
616
  // LOCATION (see studioRootForAgent / agentIdFromQueueDir above).
571
617
  //
572
618
  // THE DEFAULT IS PART OF A PATH, so changing it moves where an install looks for its own runs:
573
- // every run dir is `<workspaceRoot>/workspace-<agent>/studio/prelim-search/…`. An install created
619
+ // every run dir is `<workspaceRoot>/workspace-<agent>/studio/clearance-search/…`. An install created
574
620
  // before this default changed keeps its runs under the old id and must pin it — both spellings,
575
621
  // because the gather servers read their own variable:
576
622
  //
@@ -939,7 +985,7 @@ export const PROVIDERS = {
939
985
  id: "corsearch",
940
986
  label: "Corsearch",
941
987
  credEnv: "CORSEARCH_SESSION_KEY",
942
- skillDoc: "skills/prelim-register/providers/corsearch.md",
988
+ skillDoc: "skills/clearance-register/providers/corsearch.md",
943
989
  hasPublicRecordUrl: true,
944
990
  // WP-receipts W2: the public per-record origin (publicRecordOrigin + /mark/<jur>/<id> is a working
945
991
  // link) — replaces the fragile resolved-link-origin inference at render for receipt-carrying runs.
@@ -1013,7 +1059,7 @@ export const PROVIDERS = {
1013
1059
  id: "clarivate",
1014
1060
  label: "Clarivate Compumark",
1015
1061
  credEnv: "CLARIVATE_API_KEY",
1016
- skillDoc: "skills/prelim-register/providers/clarivate.md",
1062
+ skillDoc: "skills/clearance-register/providers/clarivate.md",
1017
1063
  hasPublicRecordUrl: false, // Compumark Content has no public record URL — cite the office register
1018
1064
  //, ruling 2026-08-20 — WHAT A CARD SHOWS WHERE A LINK CANNOT GO. A UI exists for this
1019
1065
  // provider and we do not know its per-record URL, so the card says so and says it is unfinished.
@@ -1155,7 +1201,7 @@ export const PROVIDERS = {
1155
1201
  id: "signa",
1156
1202
  label: "Signa",
1157
1203
  credEnv: "SIGNA_API_KEY",
1158
- skillDoc: "skills/prelim-register/providers/signa.md",
1204
+ skillDoc: "skills/clearance-register/providers/signa.md",
1159
1205
  hasPublicRecordUrl: false, // Signa exposes no per-record public URL — cite the office register
1160
1206
  //, ruling 2026-08-20 — no register UI exists to link to at all, so the card points at
1161
1207
  // the artifact that DOES carry the record: the audit workbook. Naming it is the whole of this
@@ -1210,8 +1256,12 @@ export const PROVIDERS = {
1210
1256
  // on the search response, so this no longer has to answer `present` and nothing else. It is
1211
1257
  // still null whenever the vendor would only approximate it — and null there means UNKNOWN,
1212
1258
  // which is the whole reason the field may never be filled in with a figure from anywhere else.
1259
+ // `floor` rides beside `approximate` because the two are one fact: the register answered, and the
1260
+ // answer is "more than this". Without the number the disclosure is not usable — "approximate"
1261
+ // alone says no more than "unknown" does, which is the state this replaces.
1213
1262
  return { ok: true, total: Number.isFinite(p.total_hits) ? p.total_hits : null,
1214
- approximate: p.total_approximate === true, present: p.present === true, note: p.note };
1263
+ approximate: p.total_approximate === true, floor: Number.isFinite(p.total_floor) ? p.total_floor : null,
1264
+ present: p.present === true, note: p.note };
1215
1265
  } catch (e) { return { ok: false, cause: `countHits threw: ${e.message}` }; }
1216
1266
  },
1217
1267
  // `reason`, not `cause`, on every refusal: the listing reads `reason` (register-records.mjs), so a
@@ -1301,7 +1351,7 @@ export const PROVIDERS = {
1301
1351
  // list — without this, an instance holding the id and no secret passes preflight and dies on the
1302
1352
  // first token request, after model spend and reported as a provider fault.
1303
1353
  credEnvAlso: ["EUIPO_CLIENT_SECRET"],
1304
- skillDoc: "skills/prelim-register/providers/euipo.md",
1354
+ skillDoc: "skills/clearance-register/providers/euipo.md",
1305
1355
  hasPublicRecordUrl: true,
1306
1356
  publicRecordOrigin: "https://euipo.europa.eu",
1307
1357
  async recordFetch(uri, { agentId, sessionKey, recordLog = null }) {
@@ -1367,7 +1417,7 @@ export const PROVIDERS = {
1367
1417
  id: "uspto-local",
1368
1418
  label: "USPTO (local index)",
1369
1419
  credEnv: "USPTO_LOCAL_DB",
1370
- skillDoc: "skills/prelim-register/providers/uspto-local.md",
1420
+ skillDoc: "skills/clearance-register/providers/uspto-local.md",
1371
1421
  hasPublicRecordUrl: true,
1372
1422
  // TSDR publishes a page per serial, so a finding can cite an address the reader can open. The
1373
1423
  // record ref is /mark/us/<serial>, and the core builds the full statusSearch link on the record.
@@ -1471,7 +1521,7 @@ export const PROVIDERS = {
1471
1521
  label: "Free tier (EUIPO + USPTO local index)",
1472
1522
  credEnv: "EUIPO_CLIENT_ID",
1473
1523
  credEnvAlso: ["EUIPO_CLIENT_SECRET"],
1474
- skillDoc: "skills/prelim-register/providers/free-tier.md",
1524
+ skillDoc: "skills/clearance-register/providers/free-tier.md",
1475
1525
  hasPublicRecordUrl: true,
1476
1526
  // NULL, deliberately: the two members have DIFFERENT public origins (euipo.europa.eu and the USPTO),
1477
1527
  // so a single origin string here would stamp one office's host onto the other's citations. The
@@ -1821,7 +1871,7 @@ export function preflightCredentials(env = process.env) {
1821
1871
  // paid for. This door is that same failure, moved in front of the spend.
1822
1872
  //
1823
1873
  // GATED ON THE COMPONENT, NEVER ON THE PIPELINE. `pipeline === "clearance"` is the wrong predicate and
1824
- // fails in the expensive direction: `prelim-register-only` is a clearance that carries
1874
+ // fails in the expensive direction: `clearance-register-only` is a clearance that carries
1825
1875
  // `commonLawGrid: false`, searches no unregistered-use half by design, and would be refused for a
1826
1876
  // credential its lane never reads. The component IS the question — search-policy.mjs calls it "the
1827
1877
  // clearance's unregistered-use half" in as many words.
@@ -2332,7 +2382,7 @@ export function preflightDeploymentUrls(env = process.env) {
2332
2382
  const RUN_FREE_BYTES_FLOOR = 500e6;
2333
2383
 
2334
2384
  /** Nearest ancestor of `p` that exists. statfs needs a real path, and on a first run NONE of
2335
- * …/workspace-<agent>/studio/prelim-search exists yet — measuring the leaf would throw ENOENT and land
2385
+ * …/workspace-<agent>/studio/clearance-search exists yet — measuring the leaf would throw ENOENT and land
2336
2386
  * in the unmeasurable branch, which would disable this check on exactly the fresh installs it is for. */
2337
2387
  function nearestExistingDir(p) {
2338
2388
  let dir = p;
@@ -2374,7 +2424,7 @@ export function freeSpacePlan({ freeBytes, needBytes, path }) {
2374
2424
  * shape of run that can proceed without one, so there is no exemption to write.
2375
2425
  *
2376
2426
  * MEASURES THE FILESYSTEM THAT WILL HOLD THE BYTES, which is the workspace root's, not `/` and not the
2377
- * repo's. Run directories live under config.studioRoot (…/workspace-<agent>/studio/prelim-search); the
2427
+ * repo's. Run directories live under config.studioRoot (…/workspace-<agent>/studio/clearance-search); the
2378
2428
  * published report goes to poolRoot and the packet to outboxDir, which on a laptop are different
2379
2429
  * filesystems again. A check aimed at the wrong mount passes while the right one is full, which is the
2380
2430
  * silent-pass this exists to prevent — so the path is taken from the caller's studioRoot when the runner
@@ -14,7 +14,7 @@ The active register provider is mounted under the single, neutral server key `re
14
14
  named `register_*` — so the namespaced ids the prompts and allowlists carry (`mcp__register__register_enumerate`)
15
15
  stay stable across a provider swap. What is PINNED is the vendor's TOOL tokens, not its name:
16
16
  `../../test/provider-neutral-prose.test.mjs` walks `driver/` (skipping `fixtures/`) and fails any `<vendor>_<tool>`
17
- outside the six `<provider>-server.mjs` files and `../../skills/prelim-register/providers/`, exempting a core's own
17
+ outside the six `<provider>-server.mjs` files and `../../skills/clearance-register/providers/`, exempting a core's own
18
18
  `ERROR: … HTTP` diagnostics. The plain vendor NAME is unrestricted: `REGISTER_SERVERS` is keyed by it, and 134 other
19
19
  files under `driver/` carry `corsearch`.
20
20
 
@@ -12,7 +12,7 @@
12
12
  // held by FOUR stages — `common-law`, `common-law-half`, `narrative-refutation`, `synthesis`. Because
13
13
  // `allowedToolsFor` enumerates every tool on every entry a group resolves to, all four carried
14
14
  // `mcp__perplexity__record_dispositions`, while every doctrinal mention of the tool is common-law's:
15
- // `driver/skills/prelim-common-law/SKILL.md`, and the two common-law stage dictations in `stages.mjs`.
15
+ // `driver/skills/clearance-common-law/SKILL.md`, and the two common-law stage dictations in `stages.mjs`.
16
16
  // Zero occurrences in synthesis's dictation block, zero in narrative-refutation's doctrine.
17
17
  //
18
18
  // That is GRANTED-BUT-NEVER-ORDERED, the defect class, in its mirror form: not a stage ordered to
@@ -75,8 +75,8 @@ async function record_dispositions(params) {
75
75
  let spec;
76
76
  try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
77
77
  catch (err) { return { isError: true, text: `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.` }; }
78
- if (!/\/studio\/prelim-search\//.test(spec.output_path))
79
- return { isError: true, text: `ERROR: grid spec.output_path must be within a studio/prelim-search run dir; got ${spec.output_path}` };
78
+ if (!/\/studio\/(?:prelim|clearance)-search\//.test(spec.output_path)) // either spelling: an install keeps the studio segment it has
79
+ return { isError: true, text: `ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}` };
80
80
  // NEVER THROWN PAST THIS POINT. An exception surfaces to the seat as a tool error naming no row, which
81
81
  // tells it nothing about what to fix — the failure mode this transport exists to end.
82
82
  try {
@@ -23,7 +23,7 @@ const BRIDGE = process.env.CLEAROTRON_OAUTH_BRIDGE || join(MCP_DIR, "..", "..",
23
23
  // ── The register surface is PROVIDER-NEUTRAL ────────────────────────────────────────────────────
24
24
  // The active register provider is mounted under the single server key `register`, and every register
25
25
  // tool is named `register_*`. The vendor's name survives ONLY inside its own *-server.mjs file and in
26
- // skills/prelim-register/providers/<provider>.md (the vocabulary doc the spawns are told to read).
26
+ // skills/clearance-register/providers/<provider>.md (the vocabulary doc the spawns are told to read).
27
27
  //
28
28
  // Why neutral names rather than interpolating `${PROVIDER}_enumerate` into the prose: the register
29
29
  // instructions in stages.mjs / pipeline.mjs / gateway.mjs are then correct BY CONSTRUCTION for every
@@ -275,7 +275,7 @@ const RECORDING = Object.freeze({
275
275
  + "DISCOVERY over the run dir (O3c: 21 calls, 1 write / 15 attempts, `ls`/`find`/`cat`), not the "
276
276
  + "enumerable pair frame-diff's Read grant covers",
277
277
  },
278
- // FIFTH — prelim-variants, conversion 3. Classes 3 + 2, and the CLASS 3 half is what makes it
278
+ // FIFTH — clearance-variants, conversion 3. Classes 3 + 2, and the CLASS 3 half is what makes it
279
279
  // different from every conversion before it: O3c measured 9 Bash calls with 4 WRITES across 15
280
280
  // attempts, and the shape is `python3 -c` over `variant-manifest.json` — the seat PRE-CHECKING its own
281
281
  // JSON before saving it. That is a constraint the transport can check, so under the design's Class 3
@@ -287,12 +287,12 @@ const RECORDING = Object.freeze({
287
287
  // enumerable — it derives the manifest from material already on disk that the dictation names — so the
288
288
  // seeded `Read` grant carries them. Granting a search tool to a stage whose reads can be listed is
289
289
  // what the sanctioned-equivalents design refuses. Same ruling as frame-diff, opposite to matter-frame.
290
- "prelim-variants": {
290
+ "clearance-variants": {
291
291
  seatWrites: false,
292
- tools: Object.freeze(["record_prelim_variants"]),
292
+ tools: Object.freeze(["record_clearance_variants"]),
293
293
  reason: "hands back the variant manifest — mark, dominant element, elements, variants with their "
294
294
  + "romanisations, incumbent classes, watchlist owners and the scope-ledger rows — through "
295
- + "record_prelim_variants instead of hand-formatting a JSON skeleton the driver already "
295
+ + "record_clearance_variants instead of hand-formatting a JSON skeleton the driver already "
296
296
  + "strict-parses and a prose twin that restates it; the driver serialises variant-manifest.json, "
297
297
  + "renders variant-manifest.md, and derives scope-ledger.json from the same values rather than by "
298
298
  + "re-parsing a markdown table the seat typed. Gains NO retrieval server and NO search tool (its "
@@ -320,7 +320,7 @@ const RECORDING = Object.freeze({
320
320
  // NO `search_run_artifacts`: the dictation names this stage's ONLY two inputs — the settled narrative
321
321
  // and findings.json — and already trimmed its declared reads to exactly those two. A read set that
322
322
  // short is enumerable by definition, and the design forbids handing a search tool to a stage whose
323
- // reads can be listed. Same ruling as frame-diff and prelim-variants.
323
+ // reads can be listed. Same ruling as frame-diff and clearance-variants.
324
324
  "report-overview": {
325
325
  seatWrites: false,
326
326
  tools: Object.freeze(["record_report_overview"]),
@@ -671,7 +671,7 @@ const LOCAL = {
671
671
  //
672
672
  // GRANTED BY EXACTLY ONE LANE'S GROUP LIST — `common-law` and its `common-law-half` variant, through
673
673
  // the prefix branch in toolGroupsForStage. Every doctrinal mention of the tool is theirs:
674
- // driver/skills/prelim-common-law/SKILL.md and the two common-law stage dictations in stages.mjs.
674
+ // driver/skills/clearance-common-law/SKILL.md and the two common-law stage dictations in stages.mjs.
675
675
  //
676
676
  // NOT an allowlist growing by a token this time: it SHRINKS synthesis's and narrative-refutation's
677
677
  // argv and RENAMES the token on common-law's (`mcp__perplexity__record_dispositions` →
@@ -1222,8 +1222,8 @@ export const RECORDING_TOOLS = Object.freeze({
1222
1222
  // so deriving it would compare a value with itself. matter-frame carries the search tool for the same
1223
1223
  // reason skeptic does, on its OWN key — a shared key would hand skeptic a writer into the frame.
1224
1224
  // BY HAND, like every row here — O1 compares the resolved grant against it, so a derived row would
1225
- // compare a value with itself. prelim-variants carries NO search tool: enumerable inputs, Read serves.
1226
- "prelim-variants": Object.freeze(["Read", "mcp__recording-prelim-variants__record_prelim_variants"]),
1225
+ // compare a value with itself. clearance-variants carries NO search tool: enumerable inputs, Read serves.
1226
+ "clearance-variants": Object.freeze(["Read", "mcp__recording-clearance-variants__record_clearance_variants"]),
1227
1227
  "matter-frame": Object.freeze(["Read", "mcp__recording-matter-frame__record_matter_frame",
1228
1228
  "mcp__recording-matter-frame__search_run_artifacts"]),
1229
1229
  skeptic: Object.freeze(["Read", "mcp__recording-skeptic__record_skeptic",
@@ -154,8 +154,8 @@ async function research(params) {
154
154
  let spec;
155
155
  try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
156
156
  catch (err) { return `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.`; }
157
- if (!/\/studio\/prelim-search\//.test(spec.output_path))
158
- return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/prelim-search run dir; got ${spec.output_path}`, { spec, gridSpecPath: grid_spec_path });
157
+ if (!/\/studio\/(?:prelim|clearance)-search\//.test(spec.output_path)) // either spelling: an install keeps the studio segment it has
158
+ return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}`, { spec, gridSpecPath: grid_spec_path });
159
159
  // — already recorded and complete? Answer from the ledger; do not buy the grid twice.
160
160
  const already = recordedLedgerFor(spec);
161
161
  if (already) {
@@ -44,7 +44,7 @@ import { VARIANT_DIRECTIONS, RANKING_BASES } from "../../blind-frame-model.mjs";
44
44
  import { recordSkeptic } from "../../skeptic-record.mjs";
45
45
  import { recordFrameDiff } from "../../frame-diff-record.mjs";
46
46
  import { recordMatterFrame, INTAKE_ASK_OWNERS, SCOPE_BASES } from "../../matter-frame-record.mjs";
47
- import { recordPrelimVariants, SCOPE_LAYERS, SCOPE_STATUS } from "../../prelim-variants-record.mjs";
47
+ import { recordClearanceVariants, SCOPE_LAYERS, SCOPE_STATUS } from "../../clearance-variants-record.mjs";
48
48
  import { recordReportOverview } from "../../report-overview-record.mjs";
49
49
  import { recordReportCard } from "../../report-card-record.mjs"; // conversion 5 — the fan-out transport
50
50
  import { recordRefutation, REVIEW_VERDICTS } from "../../narrative-refutation-record.mjs"; // conversion 9
@@ -125,12 +125,12 @@ async function record_knockout_review(params) {
125
125
  return recordKnockoutReview(runDir, params);
126
126
  }
127
127
 
128
- async function record_prelim_variants(params) {
128
+ async function record_clearance_variants(params) {
129
129
  const runDir = String(process.env.CLEAROTRON_BAND_RUN_DIR ?? "");
130
130
  if (!runDir) {
131
131
  return { error: "this server was started without a run — the driver wires it per run; there is no parameter for it and this tool never guesses one" };
132
132
  }
133
- return recordPrelimVariants(runDir, params);
133
+ return recordClearanceVariants(runDir, params);
134
134
  }
135
135
 
136
136
  async function record_report_card(params) {
@@ -444,7 +444,7 @@ serve({
444
444
  //
445
445
  // `scope_ledger` is the one genuinely new field, and it is what deletes a derivation: those rows used
446
446
  // to reach the driver only by re-parsing a markdown table out of the prose manifest.
447
- name: "record_prelim_variants",
447
+ name: "record_clearance_variants",
448
448
  description:
449
449
  "Hand back the variant manifest as VALUES. The driver serialises variant-manifest.json, renders " +
450
450
  "variant-manifest.md and writes scope-ledger.json from what you send, so you never format JSON, " +
@@ -516,7 +516,7 @@ serve({
516
516
  },
517
517
  },
518
518
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
519
- handler: record_prelim_variants,
519
+ handler: record_clearance_variants,
520
520
  }, {
521
521
  // ── CONVERSION 4 — THE REPORT SHELL, AND THE FIRST ARTIFACT A CLIENT READS ────────────────────
522
522
  //
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
- // Job-file shape for studio/prelim-search/queue/<id>.json (written by email-loop on a prelim-search request).
3
+ // Job-file shape for studio/clearance-search/queue/<id>.json (written by email-loop on a clearance-search request).
4
4
  // The job id = sanitized email message-id so a re-delivered webhook overwrites the same file (no duplicate run).
5
5
  //
6
6
  // Blocking semantics follow change-spec v3 §B2: the ONLY content reason a search may not start is the
@@ -10,7 +10,7 @@
10
10
  // with a `noref<hash>` slug (see phase0.mjs deriveSlug). The intake confirmation brief (email-loop §6)
11
11
  // resolves ambiguity BEFORE enqueue; this validator is the runner-side mechanical backstop.
12
12
 
13
- import { loadProfiles, loadProjects, resolveProfile, applicantMatchesProfile, recipeProseGuard, platformEntryErrors } from "./profiles.mjs";
13
+ import { loadProfiles, loadProjects, resolveProfile, applicantMatchesProfile, recipeProseGuard, platformEntryErrors , unreadableProfiles } from "./profiles.mjs";
14
14
  import { demoRunShape } from "./demo-run-agreement.mjs";
15
15
  import { ORDERABLE_PRODUCTS, policyFor, checkMarkBudget, checkScopeAgainstPolicy, loadRecipes, kebabCollisions, resolveSearchPolicy } from "./search-policy.mjs";
16
16
  // — the §B2 gate resolves the subject through the SAME ladder the run uses. See the gate itself.
@@ -776,6 +776,10 @@ export function validateJob(job, { atClaim = false } = {}) {
776
776
  profiles = loadProfiles({ force: true });
777
777
  known = profiles.has(key);
778
778
  }
779
+ // A company whose own file would not load is a known company that could not be READ — the same
780
+ // failure to look as a store that cannot be read, and handled the same way below, never "no such
781
+ // customer". The file's reason travels with the run's own refusal (resolveProfile).
782
+ if (!known) known = new Set(unreadableProfiles(profiles).map((u) => u.key)).has(key);
779
783
  roster = [...profiles.keys()].sort();
780
784
  } catch { known = true; }
781
785
  // NAME THE ROSTER THIS PROCESS CAN SEE, always.
@@ -590,8 +590,11 @@ export function riskStatement({ tier, verdict, reasons, basis, clauses } = {}) {
590
590
  const basisNote = registerOnly ? " Register findings only — no common-law or marketplace search was run." : "";
591
591
  if (v === "BLOCKING") return `On hold — the reviewing lawyer's open questions must be resolved before any recommendation.${basisNote}`;
592
592
  if (v === "CONDITIONAL") {
593
- const conds = (Array.isArray(reasons) ? reasons : []).map((r) => String(r ?? "").trim()).filter(Boolean);
594
- const cls = (Array.isArray(clauses) ? clauses : []).map((c) => String(c ?? "").trim()).filter(Boolean);
593
+ // A clause stored as explicit null is a condition ruled to the run record alone: it is not the lede,
594
+ // it is not counted, and its reason is never the fallback text. `undefined` (legacy/short) is not null.
595
+ const cs = Array.isArray(clauses) ? clauses : [];
596
+ const conds = (Array.isArray(reasons) ? reasons : []).filter((r, i) => cs[i] !== null).map((r) => String(r ?? "").trim()).filter(Boolean);
597
+ const cls = cs.map((c) => String(c ?? "").trim()).filter(Boolean);
595
598
  const lede = cls[0] ?? conds[0] ?? "the open conditions carried in the report";
596
599
  const first = clipClause(sentenceCaseLead(lede), STATEMENT_CLAUSE_MAX);
597
600
  const n = conds.length || cls.length;
@@ -41,7 +41,7 @@
41
41
  // than being unable to ask. Failing closed here would mean a file-read error takes the whole portal
42
42
  // down — trading a rare wrong-greyed-out option for a total outage.
43
43
 
44
- import { writeFileSync, readFileSync, mkdirSync } from "node:fs";
44
+ import { writeFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
45
45
  import { dirname, join } from "node:path";
46
46
  import { BUILT } from "./search-policy.mjs";
47
47
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
@@ -369,7 +369,7 @@ export function postureDisagreement(snapshot, live) {
369
369
 
370
370
  /** Where the snapshot lives. Beside the pool, so it shares the pool's lifecycle and backup. */
371
371
  export function snapshotPath(poolRoot) {
372
- return join(poolRoot, "_state", "prelim-flag-snapshot.json");
372
+ return join(poolRoot, "_state", "clearance-flag-snapshot.json");
373
373
  }
374
374
 
375
375
  /**
@@ -383,7 +383,10 @@ export function readFlagSnapshot(poolRoot) {
383
383
  // reads as intentional.
384
384
  if (!poolRoot) return null;
385
385
  try {
386
- const raw = JSON.parse(readFileSync(snapshotPath(poolRoot), "utf8"));
386
+ // The file's pre-rename name is read when the new one is not there yet, so an upgraded install does
387
+ // not read as "no snapshot" until its first write under the new name.
388
+ const legacy = join(poolRoot, "_state", "prelim-flag-snapshot.json");
389
+ const raw = JSON.parse(readFileSync(existsSync(snapshotPath(poolRoot)) || !existsSync(legacy) ? snapshotPath(poolRoot) : legacy, "utf8"));
387
390
  if (!raw || typeof raw !== "object" || typeof raw.flags !== "object") return null;
388
391
  return raw;
389
392
  } catch {