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
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ // pre-rename-spellings.mjs — names the identifier rename moved that an install still carries on disk.
4
+ //
5
+ // The internal identifier `prelim` became `clearance`. Where that identifier is only a name in the code, the
6
+ // rename is complete. Where it names something an install has already WRITTEN — a directory its runs live
7
+ // under, the session-key prefix its fetched records are filed by — the new name finds nothing, and nothing
8
+ // says so. Each such name is read in both spellings here, the old one as the new.
9
+ //
10
+ // ── The studio segment ──
11
+ //
12
+ // THE SEGMENT IS A PLACE ON DISK, NOT A NAME. Every run an install has made — its slug directories, its
13
+ // `archive/`, its queue, its matter ledger — sits under `<workspace>/studio/<segment>/`. The identifier
14
+ // rename changed the segment the code computes from `prelim-search` to `clearance-search`, which moves
15
+ // nothing on disk: it points every reader at an empty directory, and an empty directory reads as "no runs"
16
+ // and "no queued jobs", never as an error. So an install keeps the segment it has: the old spelling
17
+ // wherever that directory exists, and the new one only for an install that has no other.
18
+ //
19
+ // One definition, imported by every site that builds or recognises the path, including the scripts and
20
+ // servers that deliberately do not load the driver's configuration module.
21
+
22
+ import { existsSync } from "node:fs";
23
+ import { join } from "node:path";
24
+
25
+ /** The two spellings, the one every pre-rename install wrote under first: it wins wherever it exists. */
26
+ export const STUDIO_SEGMENTS = Object.freeze(["prelim-search", "clearance-search"]);
27
+
28
+ /** A regex source matching either spelling, for code that recognises a studio path rather than builds one. */
29
+ export const STUDIO_SEGMENT_RE = "(?:prelim|clearance)-search";
30
+
31
+ /** The segment the install under `workspaceDir` uses. */
32
+ export function studioSegmentFor(workspaceDir) {
33
+ try { if (existsSync(join(String(workspaceDir ?? ""), "studio", STUDIO_SEGMENTS[0]))) return STUDIO_SEGMENTS[0]; }
34
+ catch { /* unreadable ⇒ the new spelling, whose read then fails loudly where it happens */ }
35
+ return STUDIO_SEGMENTS[1];
36
+ }
37
+
38
+ /** `<workspaceDir>/studio/<segment>`. */
39
+ export const studioDirFor = (workspaceDir) => join(String(workspaceDir ?? ""), "studio", studioSegmentFor(workspaceDir));
40
+
41
+ // ── The run's session-key prefix ──
42
+ //
43
+ // A run's register calls and fetched record bodies are filed under `<prefix><slug>-<codename>-…`, and the
44
+ // run's own readers filter by that prefix. A run started before the rename and resumed after it has rows
45
+ // under `prelim-` and is now asked about `clearance-`: its records would read as never fetched, and the
46
+ // delivery check fails the run on records it holds.
47
+ /** Every spelling of one run prefix: `[prefix, the same run under the other identifier]`. PURE. */
48
+ export function runPrefixSpellings(runPrefix) {
49
+ const p = String(runPrefix ?? "");
50
+ if (p.startsWith("clearance-")) return [p, `prelim-${p.slice("clearance-".length)}`];
51
+ if (p.startsWith("prelim-")) return [p, `clearance-${p.slice("prelim-".length)}`];
52
+ return [p];
53
+ }
@@ -32,7 +32,7 @@
32
32
  //
33
33
  // ── WHAT IS DELIBERATELY NOT HERE ────────────────────────────────────────────────────────────────
34
34
  //
35
- // PRODUCT VOCABULARY. `lane`, `round`, `box`, `ruling`, `prelim`, `knockout`, `seat` and `jx` are this
35
+ // PRODUCT VOCABULARY. `lane`, `round`, `box`, `ruling`, `clearance`, `knockout`, `seat` and `jx` are this
36
36
  // product's own nouns. A guard refusing them fires thousands of times, and a guard that fires on
37
37
  // correct prose is one whose next reader deletes from the workflow.
38
38
  //
@@ -378,6 +378,40 @@ export function censusOf(files, read) {
378
378
  * @param {string} root
379
379
  * @returns {{files: string[], laid: number} | {error: string}}
380
380
  */
381
+ /**
382
+ * A reader that answers with what HEAD PUBLISHES at a path, not with whatever is sitting there.
383
+ *
384
+ * `publishedOf` asks whether a PATH is in HEAD and never whose bytes are at it. A laid file at a NEW path
385
+ * is correctly excluded; a laid file that SHADOWS a published path passes that filter, and the census then
386
+ * measures the private file as though the public tree carried it. Measured on the beta-8 overlay:
387
+ * `corpus/scripts/report-print-check.mjs` lays over `scripts/report-print-check.mjs`, the private copy
388
+ * carries two bare references in its comments and the published file carries none, and the residue census
389
+ * reported `0 → 2` against the public tree. Three of that run's nine reds were this one cause.
390
+ *
391
+ * The same hole swallows an ordinary uncommitted edit: mint a backlog with unsaved work in the tree and it
392
+ * records what you have not published yet. The path filter cannot see either, because both are a question
393
+ * about bytes.
394
+ *
395
+ * ONLY THE FILES THAT DIFFER ARE FETCHED FROM HEAD. Asking git for every file's blob would spawn a process
396
+ * per file over the whole corpus; asking which paths differ costs one call, and for the rest the bytes on
397
+ * disk ARE the published bytes. So the common case stays a plain read and correctness does not depend on
398
+ * how fast it is.
399
+ *
400
+ * A file that differs and cannot be read out of HEAD is not silently read from disk: that is the permissive
401
+ * answer and the one that reinstates the defect. It throws, and `censusOf` skips a file it cannot read —
402
+ * which undercounts rather than miscounts, and undercounting a floor is the safe direction.
403
+ */
404
+ export function publishedReader(root, read) {
405
+ let differs = new Set();
406
+ try {
407
+ differs = new Set(execFileSync("git", ["-C", root, "diff", "--name-only", "HEAD"],
408
+ { encoding: "utf8", maxBuffer: 1 << 28 }).split("\n").map((s) => s.trim()).filter(Boolean));
409
+ } catch { differs = new Set(); }
410
+ return (f) => (differs.has(f)
411
+ ? execFileSync("git", ["-C", root, "show", `HEAD:${f}`], { encoding: "utf8", maxBuffer: 1 << 28 })
412
+ : read(f));
413
+ }
414
+
381
415
  export function publishedOf(trackedList, root) {
382
416
  let head;
383
417
  try {
@@ -389,5 +423,9 @@ export function publishedOf(trackedList, root) {
389
423
  return { error: `could not read HEAD in ${root}: ${String(e.message).split("\n")[0]}` };
390
424
  }
391
425
  const files = trackedList.filter((f) => head.has(f));
392
- return { files, laid: trackedList.length - files.length };
426
+ // THE PATHS, not only how many. A count tells a reader that something was not counted and leaves them
427
+ // to find out what; every caller that acts on `laid` needs to name them, and deriving the list again at
428
+ // each call site is three chances to derive it differently.
429
+ const laidPaths = trackedList.filter((f) => !head.has(f));
430
+ return { files, laid: laidPaths.length, laidPaths };
393
431
  }
@@ -61,8 +61,15 @@ export function stdioConnectCommand({ installRoot = stableInstallRoot({ installR
61
61
  * descriptions of one thing drift into three different promises about what it does.
62
62
  */
63
63
  export function stdioConnectOffer(opts = {}) {
64
+ // ASKED OF THE SAME COMPOSER THE PAGE ASKS, so a terminal under WSL offers the two sides the page
65
+ // offers and neither surface has to know how a launcher is built. `command` is what it has always
66
+ // been — off WSL the one line, and under WSL the Windows-side one, which is the row that leads there
67
+ // too — and `variants` is null unless the caller named a WSL target, so no surface grows a second
68
+ // line because this file changed.
69
+ const both = stdioConnectFor("claude-cli", opts);
64
70
  return {
65
- command: stdioConnectCommand(opts),
71
+ command: both.text,
72
+ variants: both.variants,
66
73
  name: STDIO_SERVER_NAME,
67
74
  // Deliberately states the two facts a reader needs to judge it: no network, and it only works from a
68
75
  // machine with this install on its disk. Both are why it is offered to staff and not to a client.
@@ -315,13 +322,41 @@ export function remoteConnectFor(shape, { address = null } = {}) {
315
322
  * absence is a finding: a row naming a shape nobody implemented should surface as missing, and the
316
323
  * table's own arm refuses such a row outright.
317
324
  */
325
+ /**
326
+ * THE TWO SIDES OF A WSL INSTALL, in the product's own words and in ONE place.
327
+ *
328
+ * An install inside WSL can be reached by an assistant on the Windows side — Claude Desktop, Claude Code
329
+ * in PowerShell — through the `wsl.exe` wrapper, and by an assistant running inside the distribution
330
+ * with the plain `node` line. They are different commands and the reader has to be told which is which;
331
+ * the owner pasted the Windows one into Claude Code inside WSL and got a closed connection.
332
+ *
333
+ * Ruled 2026-09-17: these words exactly, and no other new sentence. They are exported rather than spelled
334
+ * at each surface so the page and the terminal cannot drift apart — one author, as everything else here.
335
+ */
336
+ export const WSL_ROW_HEADINGS = Object.freeze({ fromWindows: "From Windows", insideWsl: "Inside WSL" });
337
+
318
338
  export function stdioConnectFor(shape, { installRoot = stableInstallRoot({ installRoot: INSTALL_ROOT }), workDir = null, reportsDir = null, platform = process.platform, wsl = null } = {}) {
319
339
  const spec = Object.hasOwn(STDIO_SHAPES, String(shape ?? "")) ? STDIO_SHAPES[shape] : null;
320
340
  if (!spec) return null;
321
341
  const server = join(installRoot, "mcp-server", "server.mjs");
342
+ const render = (target) => spec.render({ server, workDir, reportsDir, platform, wsl: target });
343
+ const base = { shape, kind: spec.kind, where: spec.where, after: spec.after, name: STDIO_SERVER_NAME };
344
+ // OFF WSL NOTHING CHANGES: one launcher, no variants, and `variants: null` rather than an empty array
345
+ // so a consumer cannot read "this install has no sides" as "this install has two sides, both missing".
346
+ if (!wsl) return { ...base, text: render(null), variants: null };
347
+ // ON WSL, TWO, AND THE SECOND IS NOT WRITTEN TWICE. The inside-WSL launcher is exactly what this shape
348
+ // renders for an install that is not under WSL at all — the same author, asked with the target taken
349
+ // away — so it is the line every off-WSL install already offers and every arm already covers, rather
350
+ // than a second composition that can drift from it.
351
+ //
352
+ // `text` stays the Windows-side one. Every consumer that reads a single `text` today keeps the answer
353
+ // it has always had, and only the surfaces that ask for `variants` draw the pair.
322
354
  return {
323
- shape, kind: spec.kind, where: spec.where, after: spec.after,
324
- text: spec.render({ server, workDir, reportsDir, platform, wsl }),
325
- name: STDIO_SERVER_NAME,
355
+ ...base,
356
+ text: render(wsl),
357
+ variants: [
358
+ { heading: WSL_ROW_HEADINGS.fromWindows, text: render(wsl) },
359
+ { heading: WSL_ROW_HEADINGS.insideWsl, text: render(null) },
360
+ ],
326
361
  };
327
362
  }
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // tree-commit.mjs — the tree a path belongs to, and the commit that tree carries.
5
+ //
6
+ // A CHECKOUT ANSWERS THROUGH GIT; A PACKAGED INSTALL ANSWERS THROUGH ITS build-info.json. An install
7
+ // from the registry is a copy of the package, not a clone: there is no git there and there never will
8
+ // be, so asking git about it fails every time, and a deploy check that treats that failure as a fault
9
+ // reports every packaged box as broken. What a packaged install does carry is `build-info.json`, written
10
+ // by `prepack`, naming the commit it was packed from — read here through the one reader of that file.
11
+ //
12
+ // Git is asked first, and only its failure sends the question to the stamp: a checkout's HEAD is the
13
+ // truth about a checkout, and a stale stamp left in one must not outvote it.
14
+ import { dirname } from "node:path";
15
+ import { execFileSync } from "node:child_process";
16
+ import { packagedBuild } from "./packaged-build.mjs";
17
+
18
+ /** `git -C <repo> <args>`, keeping git's own sentence when it fails. */
19
+ export function gitTry(repo, ...args) {
20
+ try {
21
+ return { ok: true, out: execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(), err: null };
22
+ } catch (e) {
23
+ return { ok: false, out: null, err: String(e?.stderr || e?.message || e).replace(/\s+/g, " ").trim().slice(0, 120) };
24
+ }
25
+ }
26
+
27
+ /**
28
+ * `{ root, head, source, why }` for the tree holding `path`. `source` is "git" or "build-info"; `root`
29
+ * null means neither could say, and `why` names both failures. Readers are injected so an arm can drive
30
+ * a packaged install without building one.
31
+ */
32
+ export function treeOf(path, { git = gitTry, build = packagedBuild } = {}) {
33
+ const top = git(path, "rev-parse", "--show-toplevel");
34
+ if (top.ok) {
35
+ const head = git(top.out, "rev-parse", "HEAD");
36
+ return { root: top.out, head: head.ok ? head.out : null, source: "git",
37
+ why: head.ok ? null : `git could not read HEAD in ${top.out}: ${head.err}` };
38
+ }
39
+ // WALK UP to the package root: a unit's WorkingDirectory or a running module sits somewhere inside
40
+ // the installed package, and the stamp is at its top.
41
+ for (let d = path; ; d = dirname(d)) {
42
+ const b = build(d);
43
+ if (b) return { root: d, head: b.commit, source: "build-info", why: null };
44
+ if (dirname(d) === d) break;
45
+ }
46
+ return { root: null, head: null, source: null,
47
+ why: `git could not read ${path} (${top.err}), and no build-info.json above it names a commit` };
48
+ }