@junghanacs/entwurf 0.41.2 → 0.50.0

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 (122) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +110 -1
  3. package/CHANGELOG.md +328 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/ENTWURF-CLI.json +1229 -0
  6. package/ENTWURF.json +284 -0
  7. package/FAQ.md +4 -4
  8. package/README.md +125 -184
  9. package/VERIFY.md +2 -2
  10. package/docs/external-mcp-host.md +3 -3
  11. package/docs/setup-clean-host.md +37 -12
  12. package/mcp/entwurf-bridge/dist/ENTWURF.json +284 -0
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
  29. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -60
  30. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
  31. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
  33. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  34. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
  35. package/mcp/entwurf-bridge/src/index.ts +149 -327
  36. package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
  37. package/native/win32-x64/manifest.json +1 -1
  38. package/package.json +8 -4
  39. package/pi/pi-durable/bootstrap.mjs +179 -6
  40. package/pi/pi-durable/carrier/runtime.js +108 -9
  41. package/pi/pi-durable/carrier/session-name.js +11 -0
  42. package/pi/pi-durable/carrier/sessions.js +96 -20
  43. package/pi/pi-durable/overlay/upstream-pin.json +11 -5
  44. package/pi-extensions/lib/agent-guidance.ts +341 -0
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
  46. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
  47. package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
  48. package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
  49. package/pi-extensions/lib/herdr-resume-call.ts +810 -0
  50. package/pi-extensions/lib/meta-session.ts +13 -8
  51. package/pi-extensions/lib/mux-fresh-call.ts +23 -26
  52. package/pi-extensions/lib/mux-resume-call.ts +95 -23
  53. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
  54. package/pi-extensions/lib/pi-durable-location.ts +446 -0
  55. package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
  56. package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
  57. package/pi-extensions/lib/remote-pair-send.ts +7 -4
  58. package/pi-extensions/lib/resume-launch-identity.ts +31 -12
  59. package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
  60. package/run.sh +417 -654
  61. package/scripts/build-bridge.sh +10 -0
  62. package/scripts/check-capability-bundle-reach.ts +86 -0
  63. package/scripts/check-claude-launch.ts +341 -0
  64. package/scripts/check-entwurf-self-address.ts +3 -3
  65. package/scripts/check-entwurf-v2-surface.ts +94 -98
  66. package/scripts/check-fresh-call-dispatch.ts +70 -9
  67. package/scripts/check-fresh-cut-gate.sh +23 -9
  68. package/scripts/check-gate-qualification.ts +24 -17
  69. package/scripts/check-herdr-activation.ts +301 -23
  70. package/scripts/check-herdr-fresh-call.ts +161 -25
  71. package/scripts/check-herdr-plugin-build.ts +173 -1
  72. package/scripts/check-herdr-plugin.ts +28 -2
  73. package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
  74. package/scripts/check-install-surface.ts +1236 -6
  75. package/scripts/check-meta-doctor-oracle.sh +1 -1
  76. package/scripts/check-meta-facts.ts +20 -1
  77. package/scripts/check-mux-resume-call.ts +48 -3
  78. package/scripts/check-peer-facts.ts +39 -2
  79. package/scripts/cli-help.mjs +222 -0
  80. package/scripts/dev-bin.sh +39 -2
  81. package/scripts/herdr-activation.mjs +28 -6
  82. package/scripts/herdr-operator-link.mjs +389 -0
  83. package/scripts/herdr-plugin-activate.mjs +5 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +18 -5
  85. package/scripts/herdr-rails.mjs +43 -7
  86. package/scripts/herdr-runtime.mjs +27 -0
  87. package/scripts/meta-bridge-fresh-cut.ts +1 -27
  88. package/scripts/meta-bridge-prune.ts +1 -1
  89. package/scripts/meta-facts.ts +1 -1
  90. package/scripts/mutants/agent-guidance.json +160 -0
  91. package/scripts/mutants/bridge-boot-resume.json +6 -8
  92. package/scripts/mutants/capability-bundle-reach.json +124 -0
  93. package/scripts/mutants/claude-launch.json +142 -0
  94. package/scripts/mutants/cli-help-source.json +420 -0
  95. package/scripts/mutants/control-send-receipt.json +21 -4
  96. package/scripts/mutants/dev-gate-dispatch.json +109 -0
  97. package/scripts/mutants/fresh-call-dispatch.json +38 -2
  98. package/scripts/mutants/fresh-cut.json +15 -0
  99. package/scripts/mutants/herdr-activation.json +128 -0
  100. package/scripts/mutants/herdr-fresh-call.json +278 -5
  101. package/scripts/mutants/herdr-operator-link.json +318 -0
  102. package/scripts/mutants/herdr-plugin-build.json +55 -0
  103. package/scripts/mutants/herdr-plugin.json +17 -0
  104. package/scripts/mutants/herdr-resume-call.json +288 -0
  105. package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
  106. package/scripts/mutants/meta-facts.json +15 -0
  107. package/scripts/mutants/mux-fresh-call.json +14 -3
  108. package/scripts/mutants/mux-resume-call.json +89 -3
  109. package/scripts/mutants/peer-facts.json +15 -0
  110. package/scripts/mutants/pi-durable.json +996 -6
  111. package/scripts/mutants/remote-pair-send.json +21 -2
  112. package/scripts/mutants/self-address.json +10 -10
  113. package/scripts/mutants/source-install.json +35 -0
  114. package/scripts/mutants/v2-surface.json +50 -64
  115. package/scripts/mutants/v2-visible-resume.json +266 -6
  116. package/scripts/peer-facts.ts +1 -1
  117. package/scripts/smoke-agy-install-state.sh +66 -0
  118. package/scripts/smoke-meta-prune.sh +23 -0
  119. package/scripts/smoke-omp-fresh-live.ts +12 -3
  120. package/scripts/smoke-omp-receive-live.ts +7 -1
  121. package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
  122. package/team.json.example +39 -0
@@ -17,7 +17,10 @@
17
17
  * compiled twin declared in tsconfig.build.json. The exact miss that killed the three.
18
18
  * S3a no npm bin points at a raw .ts at all; S3b every .sh bin that execs one branches.
19
19
  * S4 dev-only gates are NOT emitted — the tarball ships operator surfaces, not 70+ gate
20
- * leaves (a build bloat regression is as real as a missing artifact).
20
+ * leaves (a build bloat regression is as real as a missing artifact). A gate with no
21
+ * explicit arm is dispatched by its own name (`check-x` → `check_x`, run.sh's default
22
+ * arm), so S4 reads those from the function definitions; a case-label reader alone would
23
+ * lose every one of them and stay green on what it no longer sees.
21
24
  * S5 no offline smoke carries an obvious write to the operator's real $HOME, and any
22
25
  * smoke that swaps HOME into a sandbox swaps XDG_DATA_HOME with it — moving HOME
23
26
  * alone still writes real install-state below the inherited XDG root, which is
@@ -39,6 +42,41 @@
39
42
  * absent. S7c also pins that the tracked settings carry NO packages key — entwurf's own
40
43
  * checkout self-registers through user scope alone (#110).
41
44
  * S6 tracked first-party text sources are NUL-free (a stray NUL makes git show nothing).
45
+ * S8 the by-name dispatch answers only for a gate function run.sh itself defined: a run.sh
46
+ * function that is not a gate (`usage`), a gate name nothing defines, one only an
47
+ * exported BASH_FUNC_* defines, and one only a comment/heredoc spells all stay the
48
+ * Unknown command; an admitted gate runs once, with no argv, its streams and exact exit
49
+ * status intact, and stops at its first failure.
50
+ * S9 `entwurf --help`, `entwurf help all` and `entwurf help <command>` render root ENTWURF-CLI.json
51
+ * through scripts/cli-help.mjs. The root forms and the Unknown path print the short root page,
52
+ * byte for byte a reviewed literal fixture (pinned, written before the data and never generated
53
+ * from it); `entwurf help all` prints the full catalog — the ce92dc7 usage bytes (a fixture
54
+ * outside the tarball, pinned) with explicit amendments only, the `entwurf` prefix and the tool
55
+ * count EXPECTED_TOOLS holds — and starts only node on the renderer with `--all`. Every command
56
+ * the root page names is described, dispatchable and outside the debt, and an example naming
57
+ * anything else fails the source by name. The source is read beside the renderer; of the PATH tools
58
+ * it watches, a help run starts only node on that renderer (a PATH watch, not an exec audit —
59
+ * the renderer's node:fs-only reach is read directly); a missing or malformed source fails by
60
+ * name with no fallback text, exit 1 on every form, apart from a refused help request's 2; an
61
+ * early-closed reader ends the page quietly, both pipe sides 0 under pipefail; the committed
62
+ * bytes are canonical — the only guard against a duplicated JSON property, which JSON.parse
63
+ * accepts. A help preflight between `cmd=${1:-}`
64
+ * and the dispatch case answers `help <command>` and `<command> -h|--help` before any arm: on a
65
+ * byte copy that stops at the case, every command run.sh has is asked — described ones print
66
+ * their page, undescribed ones are refused (exit 2) and never run, the five vendor launchers
67
+ * pass through — and extra argv, the `--help <command>` alias and odd names are refused on one
68
+ * line. fresh-cut's longer text matches its ce92dc7 source literal minus the old invocation.
69
+ * The root page is the #153 literal with three explicit #157 amendments (pinned). `entwurf help
70
+ * team` / `help remote-pair` print guide topics byte for byte reviewed literal fixtures (pinned,
71
+ * written before the data; remote-pair with one explicit review amendment, pinned too); only
72
+ * `help <name>` reaches them, through node on the renderer with
73
+ * `--name`; asked as a command (`team --help`) they are refused, their names are no command's,
74
+ * `all` or each other's, and the package files they name are shipped by exact files entries.
75
+ * Coverage reads owners independently of the data: operator commands are the labels of a case
76
+ * byte-pinned to the reviewed ce92dc7 one (evidence for this change, not a ban on later work),
77
+ * gate commands are the functions bash itself defines from run.sh — so a definition left only
78
+ * as heredoc text is no command — and every description names one of them while the
79
+ * undescribed are exactly the ce92dc7 debt.
42
80
  *
43
81
  * HONEST SCOPE — what a green run does and does not mean. S1-S4 are structural: they read the
44
82
  * dispatch graph and the build manifest, so they hold for any entrypoint written in this repo's
@@ -50,13 +88,34 @@
50
88
  * and not claimed here. Every S was mutation-checked, including bypasses found in
51
89
  * review — do not add an S without proving it fails on the bug it names.
52
90
  *
53
- * Read-only: parses sources, spawns nothing.
91
+ * Read-only for the repo: parses sources; spawns git, and S8's bounded bash probes — the
92
+ * checkout's run.sh on the Unknown path, a byte copy of run.sh in a tmp dir with one fixture gate
93
+ * defined ahead of the unchanged dispatch, and `bash -c` witnesses that an exported function
94
+ * really imports; S9's root-help runs of the checkout's run.sh and of tmp copies carrying a broken
95
+ * help source, under a PATH whose leading shims record what a help run starts, byte copies that
96
+ * stop before the dispatch to list the functions bash defined (scratch HOME/XDG, the same refusing
97
+ * shims), and a byte copy whose dispatch case is preceded by a sentinel exit, on which every command
98
+ * is asked for its help without any arm being reachable. Each probe gets PATH plus at most the BASH_FUNC_* or scratch HOME/XDG its cell names,
99
+ * never the host environment.
54
100
  */
55
101
 
56
- import { execFileSync } from "node:child_process";
57
- import { existsSync, readdirSync, readFileSync } from "node:fs";
102
+ import { execFileSync, spawnSync } from "node:child_process";
103
+ import { createHash } from "node:crypto";
104
+ import {
105
+ copyFileSync,
106
+ existsSync,
107
+ mkdirSync,
108
+ mkdtempSync,
109
+ readdirSync,
110
+ readFileSync,
111
+ rmSync,
112
+ writeFileSync,
113
+ } from "node:fs";
114
+ import os from "node:os";
58
115
  import path from "node:path";
59
116
  import { fileURLToPath } from "node:url";
117
+ import { FRESH_CUT_EXIT } from "../pi-extensions/lib/meta-session.ts";
118
+ import { EXPECTED_TOOLS } from "./probe-bridge-command.ts";
60
119
 
61
120
  const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
62
121
  const RUN_SH = readFileSync(path.join(REPO, "run.sh"), "utf8");
@@ -64,6 +123,18 @@ const BUILD_TSCONFIG = readFileSync(path.join(REPO, "mcp/entwurf-bridge/tsconfig
64
123
  const PACKAGE_JSON = JSON.parse(readFileSync(path.join(REPO, "package.json"), "utf8")) as {
65
124
  bin?: Record<string, string>;
66
125
  };
126
+ // Bash parameter expansion, escaped so the string holds the literal `${1:-}` run.sh carries: the
127
+ // line the top-level dispatch starts at.
128
+ const DISPATCH = `\ncmd=\${1:-}\n`;
129
+ const CLI_JSON = "ENTWURF-CLI.json";
130
+ const CLI_RENDERER = "scripts/cli-help.mjs";
131
+
132
+ /** Copy the help source and its renderer beside a fixture run.sh, where its usage() reads them. */
133
+ function stageHelp(dir: string, json: string = readFileSync(path.join(REPO, CLI_JSON), "utf8")): void {
134
+ mkdirSync(path.join(dir, "scripts"), { recursive: true });
135
+ copyFileSync(path.join(REPO, CLI_RENDERER), path.join(dir, CLI_RENDERER));
136
+ writeFileSync(path.join(dir, CLI_JSON), json);
137
+ }
67
138
 
68
139
  let failed = 0;
69
140
  function ok(label: string, cond: boolean, detail?: string): void {
@@ -171,6 +242,13 @@ function subcommandTargets(): Map<string, string[]> {
171
242
 
172
243
  const targets = subcommandTargets();
173
244
  const isDevGate = (cmd: string): boolean => cmd.startsWith("check-") || cmd.startsWith("smoke-");
245
+
246
+ /** Gates no explicit arm claims: run.sh's default arm reaches them by name, check-x → check_x. */
247
+ const byNameGates = new Map<string, string[]>();
248
+ for (const [fn, ts] of functionTargets()) {
249
+ const cmd = fn.replace(/_/g, "-");
250
+ if (/^(?:check|smoke)_[a-z0-9_]+$/.test(fn) && !targets.has(cmd)) byNameGates.set(cmd, ts);
251
+ }
174
252
  const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.length > 0);
175
253
 
176
254
  {
@@ -188,16 +266,1168 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
188
266
  .join("\n\n"),
189
267
  );
190
268
 
191
- const bloat = [...targets].flatMap(([cmd, tss]) =>
269
+ // Counted from the raw text, not through functionTargets, so a reader that stopped seeing gate
270
+ // functions shrinks this set against an independent number instead of passing on fewer rows.
271
+ const gateFnDefs = [...RUN_SH.matchAll(/^((?:check|smoke)_[a-z0-9_]+)\(\)\s*\{/gm)].map((m) => m[1] as string);
272
+ const unaccounted = gateFnDefs.filter((fn) => {
273
+ const cmd = fn.replace(/_/g, "-");
274
+ return !targets.has(cmd) && !byNameGates.has(cmd);
275
+ });
276
+ ok(
277
+ `[QK:DEV-GATE-BY-NAME-READ] S4 reads every gate function: ${byNameGates.size} dispatched by name + the rest under an explicit arm`,
278
+ byNameGates.size > 0 && unaccounted.length === 0,
279
+ unaccounted.length ? `gate functions S4 cannot see: ${unaccounted.join(", ")}` : "no gate is dispatched by name",
280
+ );
281
+
282
+ // Names alone are not coverage: a reader that kept every name but lost its leaves would pass
283
+ // the bloat check on empty rows. Two gates the by-name arm reaches, with the leaf each one's
284
+ // run.sh function runs (read off run.sh by hand, not off this reader).
285
+ const knownLeaves: [cmd: string, leaf: string][] = [
286
+ ["check-peer-facts", "check-peer-facts"],
287
+ ["smoke-herdr-plugin-build-live", "smoke-herdr-plugin-build-live"],
288
+ ];
289
+ const lostLeaves = knownLeaves.filter(([cmd, leaf]) => !(byNameGates.get(cmd) ?? []).includes(leaf));
290
+ ok(
291
+ "[QK:DEV-GATE-BY-NAME-LEAVES] S4 reads the run_ts leaf of a by-name gate, not only its name",
292
+ lostLeaves.length === 0,
293
+ lostLeaves.map(([cmd, leaf]) => `'${cmd}' should reach scripts/${leaf}.ts`).join("\n"),
294
+ );
295
+
296
+ const bloat = [...targets, ...byNameGates].flatMap(([cmd, tss]) =>
192
297
  isDevGate(cmd) ? tss.filter((ts) => emitted.has(ts)).map((ts) => [cmd, ts]) : [],
193
298
  );
194
299
  ok(
195
- "S4: dev-only gates are not emitted into the tarball",
300
+ "[QK:DEV-GATE-BY-NAME-NOT-EMITTED] S4: dev-only gates are not emitted into the tarball (explicit arms and by-name gates)",
196
301
  bloat.length === 0,
197
302
  bloat.map(([cmd, ts]) => `'${cmd}' is a dev gate but scripts/${ts}.ts is in the build include`).join("\n"),
198
303
  );
199
304
  }
200
305
 
306
+ // ─── S8 — the by-name dispatch answers only for a defined gate function ──────────────
307
+ {
308
+ // Only PATH crosses: the probes end on the Unknown path, and no host identity or secret has a
309
+ // reason to reach them.
310
+ const probe = (cmd: string) =>
311
+ spawnSync("bash", [path.join(REPO, "run.sh"), cmd], {
312
+ cwd: REPO,
313
+ encoding: "utf8",
314
+ env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
315
+ });
316
+ // The Unknown path prints the root page: its first line, with its LF, read from the reviewed
317
+ // literal (S9 owns the whole-page comparison), marks that page here.
318
+ const UNKNOWN_PAGE_START = `${readFileSync(path.join(REPO, "test/fixtures/cli-help/root-compact-153.txt"), "utf8").split("\n", 1)[0]}\n`;
319
+ const isUnknown = (cmd: string): boolean => {
320
+ const r = probe(cmd);
321
+ return r.status === 1 && r.stderr === `Unknown command: ${cmd}\n` && r.stdout.startsWith(UNKNOWN_PAGE_START);
322
+ };
323
+ ok(
324
+ "[QK:BY-NAME-GATE-PREFIX] S8a a run.sh function that is not a gate (`usage`) is still the Unknown command, never run by name",
325
+ isUnknown("usage"),
326
+ );
327
+ ok(
328
+ "[QK:BY-NAME-GATE-DEFINED] S8b a gate name no function defines is still the Unknown command (rc 1 + usage), never a bash command-not-found",
329
+ isUnknown("check-no-such-gate"),
330
+ );
331
+
332
+ // A byte copy of run.sh with one fixture gate defined ahead of the dispatch, which stays
333
+ // exactly the production text. The gate fails at its third line, so a dispatcher that loses
334
+ // errexit runs the marker after it; a comment and a heredoc spell two more gate names that
335
+ // nothing defines.
336
+ const dir = mkdtempSync(path.join(os.tmpdir(), "entwurf-by-name-"));
337
+ try {
338
+ if (RUN_SH.split(DISPATCH).length !== 2)
339
+ throw new Error(`S8: run.sh no longer has exactly one \`cmd=\${1:-}\` line`);
340
+ const fixture = [
341
+ "check_dispatch_probe() {",
342
+ " printf 'argc=%s\\n' \"$#\"",
343
+ " echo probe-stderr >&2",
344
+ " (exit 7)",
345
+ " echo MARK-AFTER-FAILURE",
346
+ "}",
347
+ "# check_comment_only() { :; }",
348
+ ": <<'TEXT_ONLY'",
349
+ "check_heredoc_only() { :; }",
350
+ "TEXT_ONLY",
351
+ ].join("\n");
352
+ const copy = path.join(dir, "run.sh");
353
+ writeFileSync(copy, RUN_SH.replace(DISPATCH, `\n${fixture}${DISPATCH}`));
354
+ stageHelp(dir); // the Unknown path renders the usage, which reads beside run.sh
355
+ const run = (argv: string[], extraEnv: Record<string, string> = {}) =>
356
+ spawnSync("bash", [copy, ...argv], {
357
+ cwd: dir,
358
+ encoding: "utf8",
359
+ env: { PATH: process.env.PATH ?? "/usr/bin:/bin", ...extraEnv },
360
+ });
361
+ const unknownIn = (cmd: string, extraEnv: Record<string, string> = {}): boolean => {
362
+ const r = run([cmd], extraEnv);
363
+ return r.status === 1 && r.stderr === `Unknown command: ${cmd}\n` && r.stdout.startsWith(UNKNOWN_PAGE_START);
364
+ };
365
+ const probe = run(["check-dispatch-probe", "extra-a", "extra-b"]);
366
+ ok(
367
+ "[QK:BY-NAME-GATE-RUNS] S8c an admitted gate runs exactly once: its stdout/stderr pass through and its exact nonzero status (7) is run.sh's",
368
+ probe.status === 7 && probe.stdout.split("argc=").length === 2 && probe.stderr === "probe-stderr\n",
369
+ `rc=${probe.status} stdout=${JSON.stringify(probe.stdout)} stderr=${JSON.stringify(probe.stderr)}`,
370
+ );
371
+ ok(
372
+ "[QK:BY-NAME-GATE-NO-ARGV] S8d the gate is called with no arguments, as the explicit arm it replaced did",
373
+ probe.stdout.startsWith("argc=0\n"),
374
+ `stdout=${JSON.stringify(probe.stdout)}`,
375
+ );
376
+ ok(
377
+ "[QK:BY-NAME-GATE-STOPS] S8e the gate stops at its first failure: the line after it never runs",
378
+ !probe.stdout.includes("MARK-AFTER-FAILURE") && probe.status === 7,
379
+ `rc=${probe.status} stdout=${JSON.stringify(probe.stdout)}`,
380
+ );
381
+ // An exported function for `fn`, plus the witness that bash really imports that spelling —
382
+ // without it, a malformed variable would leave no function to refuse and a cell would pass
383
+ // on nothing.
384
+ const exportFn = (fn: string): { env: Record<string, string>; imported: boolean } => {
385
+ const env = { [`BASH_FUNC_${fn}%%`]: "() { echo env-function-ran\n}" };
386
+ const w = spawnSync("bash", ["-c", fn], {
387
+ encoding: "utf8",
388
+ env: { PATH: process.env.PATH ?? "/usr/bin:/bin", ...env },
389
+ });
390
+ return { env, imported: w.stdout === "env-function-ran\n" };
391
+ };
392
+ const envOnly = exportFn("check_env_only");
393
+ ok(
394
+ "[QK:BY-NAME-GATE-DEFINED-HERE] S8f a gate only an exported environment function defines is the Unknown command",
395
+ envOnly.imported && unknownIn("check-env-only", envOnly.env),
396
+ `import witness: ${envOnly.imported}`,
397
+ );
398
+ // The same refusal for a name run.sh spells only in a comment or a heredoc, with a real
399
+ // exported function of that name present — the text must not vouch for it.
400
+ const textOnly = ["check_comment_only", "check_heredoc_only"].map((fn) => {
401
+ const e = exportFn(fn);
402
+ return { fn, imported: e.imported, unknown: unknownIn(fn.replace(/_/g, "-"), e.env) };
403
+ });
404
+ ok(
405
+ "S8g an exported function whose name run.sh spells only in a comment or a heredoc is the Unknown command",
406
+ textOnly.every((c) => c.imported && c.unknown),
407
+ JSON.stringify(textOnly),
408
+ );
409
+ const shadowed = run(["check-dispatch-probe"], { "BASH_FUNC_check_dispatch_probe%%": "() { exit 0\n}" });
410
+ ok(
411
+ "S8h an environment function with the name of a gate run.sh defines does not replace it",
412
+ shadowed.status === 7 && shadowed.stdout === "argc=0\n",
413
+ `rc=${shadowed.status} stdout=${JSON.stringify(shadowed.stdout)}`,
414
+ );
415
+ } finally {
416
+ rmSync(dir, { recursive: true, force: true });
417
+ }
418
+ }
419
+
420
+ // ─── S9 — `entwurf --help` renders ENTWURF-CLI.json, which describes only real commands ─
421
+ {
422
+ // The independent oracle is what ce92dc7's usage() heredoc actually printed, kept byte for byte
423
+ // outside the tarball; the pin fences the fixture itself so neither can drift alone. It is the
424
+ // CONTENT oracle of the full catalog — the page `entwurf help all` prints, which was the root page
425
+ // until the root became the short page below — and no expected catalog text or inventory is
426
+ // derived from the description data. The command pages further down are checked against a page
427
+ // built from the data entry — a routing and format oracle; their content is bound by this catalog
428
+ // fixture (every synopsis and summary also prints in the catalog) and by fresh-cut's own fixture.
429
+ const OLD_USAGE_FIXTURE = "test/fixtures/cli-help/usage-ce92dc7.txt";
430
+ const OLD_USAGE_SHA256 = "e67f1cb97e5022b865f525d8fb37e3cd0fd0adac927323efd2f27beb9c9e23e6";
431
+ const oldUsage = readFileSync(path.join(REPO, OLD_USAGE_FIXTURE), "utf8");
432
+ const sha256 = (s: string): string => createHash("sha256").update(s).digest("hex");
433
+ if (sha256(oldUsage) !== OLD_USAGE_SHA256) throw new Error(`S9: ${OLD_USAGE_FIXTURE} is not the ce92dc7 usage`);
434
+ // The root page's own oracle: a reviewed literal, pinned, written before the data that renders it
435
+ // and never generated from it. Moving the root forms from the catalog bytes to these is the
436
+ // intended change; the catalog keeps its own oracle above, observed through `help all`.
437
+ const COMPACT_FIXTURE = "test/fixtures/cli-help/root-compact-153.txt";
438
+ const COMPACT_SHA256 = "30176fa0a2e8598a2ffca98c0e2c4ac773f57dc3428042e15286c87e2b8832ae";
439
+ const compact153 = readFileSync(path.join(REPO, COMPACT_FIXTURE), "utf8");
440
+ if (sha256(compact153) !== COMPACT_SHA256) {
441
+ throw new Error(`S9: ${COMPACT_FIXTURE} is not the reviewed root page`);
442
+ }
443
+ // #157 (main 0922fd, approved for GLG): the help row names a command or a topic, and one footer
444
+ // line points at the two guides; #157 stage 4 adds the Claude Code launch row. Exactly these three
445
+ // literal edits of the #153 page, each exactly once, and the result pinned too — the sha below was
446
+ // taken from the amended literal before the data changed.
447
+ const ROOT_AMENDMENTS_157: { old: string; next: string }[] = [
448
+ {
449
+ old: " entwurf help [<command>] this overview, or one command's full description\n",
450
+ next: " entwurf help [<name>] this overview, or command/topic help\n",
451
+ },
452
+ {
453
+ old: "not commands here. Advanced maintenance and development gates: entwurf help all\n",
454
+ next: "not commands here. Advanced maintenance and development gates: entwurf help all\nGuides: entwurf help team | entwurf help remote-pair\n",
455
+ },
456
+ // #157 stage 4 (GLG, relayed by main 07697d): the Claude Code launch row, right after
457
+ // pi-durable's — receipt: the run.sh `claude)` arm. Pinned below with the other two.
458
+ {
459
+ old: " entwurf pi-durable [args...] the packaged durable pi launcher\n",
460
+ next: " entwurf pi-durable [args...] the packaged durable pi launcher\n entwurf claude [args...] Claude Code itself; args go to claude unchanged\n",
461
+ },
462
+ ];
463
+ let expectedCompact = compact153;
464
+ for (const a of ROOT_AMENDMENTS_157) {
465
+ if (expectedCompact.split(a.old).length !== 2) throw new Error(`S9: root amendment not exactly once: ${a.old}`);
466
+ expectedCompact = expectedCompact.replace(a.old, a.next);
467
+ }
468
+ if (sha256(expectedCompact) !== "24d41fb481b910d2c60a9c14d92dc75f200b7d1214c104b6f26e61a85a8b0949") {
469
+ throw new Error("S9: the amended root page is not the reviewed #157 page");
470
+ }
471
+ // The guide topics' oracles: reviewed literals, pinned, written before the data and never
472
+ // generated from it. Each names the package files it sends the reader to (S9w checks they ship).
473
+ const TOPIC_FIXTURES: { name: string; file: string; sha: string; ships: string[] }[] = [
474
+ {
475
+ name: "team",
476
+ file: "test/fixtures/cli-help/topic-team-157.txt",
477
+ sha: "c5691a0db1cb2dcccac0042fb1cf0439dd472e87623ea6af8419825062b2f672",
478
+ ships: ["team.json.example"],
479
+ },
480
+ {
481
+ name: "remote-pair",
482
+ file: "test/fixtures/cli-help/topic-remote-pair-157.txt",
483
+ sha: "eb60224e5b588b9c6cfed1cd43d816ec0f9a0d10abc2c6da8decc6f3e0808052",
484
+ ships: ["docs/setup-clean-host.md"],
485
+ },
486
+ ];
487
+ const topicPage = new Map<string, string>();
488
+ for (const t of TOPIC_FIXTURES) {
489
+ const text = readFileSync(path.join(REPO, t.file), "utf8");
490
+ if (sha256(text) !== t.sha) throw new Error(`S9: ${t.file} is not the reviewed ${t.name} guide`);
491
+ topicPage.set(t.name, text);
492
+ }
493
+ // #157 review amendment (H1-AMEND-A1): the remote-pair guide's path rule is per host. Exactly this
494
+ // one literal edit of its baseline fixture, exactly once, and the result pinned — the sha below
495
+ // was taken by the coordinator before the data changed. The baseline fixture itself is unchanged.
496
+ const REMOTE_PAIR_AMENDMENT = {
497
+ old: " moves the store; the file follows the store. The sending bridge and the\n SSH receive side must resolve the same path.\n",
498
+ next: " moves the store; the file follows the store. On each host, its bridge and\n its incoming SSH receive process must resolve the same local opt-in path.\n The paths on the two hosts need not be identical.\n",
499
+ };
500
+ const remoteBaseline = topicPage.get("remote-pair") ?? "";
501
+ if (remoteBaseline.split(REMOTE_PAIR_AMENDMENT.old).length !== 2) {
502
+ throw new Error("S9: the remote-pair amendment is not exactly once in its baseline fixture");
503
+ }
504
+ const remoteAmended = remoteBaseline.replace(REMOTE_PAIR_AMENDMENT.old, REMOTE_PAIR_AMENDMENT.next);
505
+ if (sha256(remoteAmended) !== "2c53d9050a3c76fdc73c43ac1339b82808dc0f45548ef73903b7e105a7b6780e") {
506
+ throw new Error("S9: the amended remote-pair guide is not the reviewed #157 page");
507
+ }
508
+ topicPage.set("remote-pair", remoteAmended);
509
+
510
+ // The catalog today is that ce92dc7 fixture with exactly these explicit amendments applied, each naming
511
+ // the source that justifies it — never a text regenerated from the new data. The prefix: a command
512
+ // line names the product, not the checkout's script; columns are absolute, so the character the
513
+ // prefix loses becomes one more space before `#`. The literals: an old phrase that is no longer
514
+ // true, replaced where it stands.
515
+ const respell = (l: string): string =>
516
+ l.replace(/^ {2}\.\/run\.sh (\S.*?)( +)# /, (_m, syn, gap) => ` entwurf ${syn}${gap} # `);
517
+ const toolCountWord = ({ 9: "nine" } as Record<number, string>)[EXPECTED_TOOLS.length];
518
+ const literalAmendments: { old: string; next: string; receipt: string }[] = [
519
+ {
520
+ old: "EXACTLY the eight shipped garden verbs",
521
+ next: `EXACTLY the ${toolCountWord} shipped garden verbs`,
522
+ receipt:
523
+ "scripts/probe-bridge-command.ts EXPECTED_TOOLS; check-entwurf-bridge-boot G1f checks the live tools/list",
524
+ },
525
+ {
526
+ old: "roots and BOTH component inverses read-only first; mutates only when every preflight is green; then Claude -> Pi user scope -> runtime -> ledger LAST. A component failure stops with the runtime and ledger intact as retry authority. With NO ledger it removes nothing: a clean host exits 0, and anything found at the plugin's journal/runtime/cache addresses is named",
527
+ next: "roots, BOTH component inverses and the operator link (#153) read-only first; mutates only when every preflight is green; then the operator link -> Claude -> Pi user scope -> runtime -> ledger LAST. A component failure stops with the runtime and ledger intact as retry authority. With NO ledger it removes nothing: a clean host exits 0, and anything found at the plugin's journal/runtime/cache addresses or its operator-link receipt is named",
528
+ receipt:
529
+ "scripts/herdr-plugin-deactivate.mjs: preflightOperatorLink beside the component preflights, removeOperatorLink before the components, operatorLinkLeftovers with no ledger (#153)",
530
+ },
531
+ {
532
+ old: "--continue (this directory's newest session) and --resume (choose one of its sessions) take none of them.",
533
+ next: "--continue (this directory's newest session), --resume (choose one of its sessions), and --session <native id> (reopen exactly that session of this directory) take none of them and are mutually exclusive.",
534
+ receipt:
535
+ "pi/pi-durable/bootstrap.mjs parseArgs: --session in VALUE_FLAGS; session-continue-exclusive, session-resume-exclusive and fresh-only-argument-with-session beside resume-continue-exclusive and the --resume/--continue fresh-only refusals (#157 G3 B-L1)",
536
+ },
537
+ ];
538
+ const respelled = oldUsage.split("\n").filter((l) => respell(l) !== l).length;
539
+ let expectedCatalog = oldUsage.split("\n").map(respell).join("\n");
540
+ const amendmentProblems: string[] = [];
541
+ if (toolCountWord === undefined)
542
+ amendmentProblems.push(`EXPECTED_TOOLS has ${EXPECTED_TOOLS.length} tools, no reviewed word`);
543
+ for (const a of literalAmendments) {
544
+ if (expectedCatalog.split(a.old).length !== 2) amendmentProblems.push(`not exactly once in the fixture: ${a.old}`);
545
+ expectedCatalog = expectedCatalog.replace(a.old, a.next);
546
+ }
547
+ // A line the old usage never had: a debt name described since, inserted right after the fixture
548
+ // line it belongs beside. The text is a reviewed literal whose receipt is the arm it describes.
549
+ const insertedLines: { after: string; line: string; receipt: string }[] = [
550
+ {
551
+ after: " entwurf pi-durable [args...] # ",
552
+ line: " entwurf pi [args...] # #118: the MANAGED pi launch. exec()s the pi that `type -P` finds on PATH (a file, never a shell function or alias) in THIS terminal, with --entwurf-control added ahead of your argv, which passes through untouched. Refuses by name when ENTWURF_PI_LAUNCH_ACTIVE is non-empty (it exports that to the pi it starts), when no pi is on PATH (it launches the pi coding agent, it does not install one), or when that path is not an executable regular file",
553
+ receipt:
554
+ "run.sh `pi)` arm: type -P pi, the non-empty ENTWURF_PI_LAUNCH_ACTIVE refusal, the missing and non-regular refusals, exec with --entwurf-control",
555
+ },
556
+ {
557
+ after: " entwurf pi [args...] # ",
558
+ line: " entwurf claude [args...] # #157: the Claude Code launch. exec()s the claude that `type -P` finds on PATH (a file, never a shell function or alias) in THIS terminal and directory; your argv, --help included, reaches it untouched with nothing added, and its exit status and signals are its own. It sets ENTWURF_CLAUDE_LAUNCH_ACTIVE and removes an inherited PI_SESSION_ID/PI_AGENT_ID pair (another citizen's identity); no other variable is decided here. A garden citizen comes from the meta-bridge plugin and hooks setup installed, which this launch neither checks nor supplies: without them it is plain Claude Code. Refuses by name when ENTWURF_CLAUDE_LAUNCH_ACTIVE is non-empty, when no claude is on PATH (it launches Claude Code, it does not install one), when that path is not an executable regular file, or when it resolves to this run.sh",
559
+ receipt:
560
+ "run.sh `claude)` arm: type -P claude, the non-empty ENTWURF_CLAUDE_LAUNCH_ACTIVE refusal, the missing, non-regular and resolves-to-run.sh refusals, unset PI_SESSION_ID PI_AGENT_ID, export of the sentinel, exec with the operator argv alone (#157 stage 4)",
561
+ },
562
+ {
563
+ after: " entwurf check-codex-app-server-launch # ",
564
+ line: " entwurf check-claude-launch # #157 stage 4 gate: the Claude Code launch `entwurf claude`, driven through its public address against a FAKE VENDOR on a sandbox PATH. The verb dropped, the argv byte-identical with nothing injected, --help/-h reaching the vendor, the caller's cwd, the sentinel exported, an inherited PI_SESSION_ID/PI_AGENT_ID pair removed and no other environment change, the exit status, pid (exec) and signal passed through, and named refusals for an absent vendor, a non-regular PATH hit, the sentinel and a PATH claude that resolves to run.sh. Hermetic: no real Claude Code, no network, no model turn",
565
+ receipt: "run.sh check_claude_launch -> scripts/check-claude-launch.ts (#157 stage 4)",
566
+ },
567
+ {
568
+ after: " entwurf herdr-plugin-deactivate # ",
569
+ line: " entwurf herdr-plugin-expose-operator --bin-dir <abs> # #153 the ONE explicit operator link: creates exactly <abs>/entwurf -> the Herdr runtime's own node_modules/.bin/entwurf, recorded by a receipt beside the activation ledger (raised to schema 3 first, so an artifact that cannot undo the link refuses the host). Runs only as the active runtime's package, only from a settled runtime with an active ledger, only into a physical directory that is an entry of the caller's PATH with no earlier entwurf on it; an existing file there without a receipt is someone else's and is refused. Never edits a profile or picks a directory",
570
+ receipt: "run.sh `herdr-plugin-expose-operator)` arm -> scripts/herdr-operator-link.mjs expose (#153)",
571
+ },
572
+ {
573
+ after: " entwurf herdr-plugin-expose-operator --bin-dir <abs> # ",
574
+ line: " entwurf herdr-plugin-withdraw-operator # #153 the opt-out of herdr-plugin-expose-operator: removes exactly the receipt's link — only while it is still that link — and then the receipt; the ledger stays at schema 3. Runs only as the active runtime's package. herdr-plugin-deactivate removes the same link first, before any component",
575
+ receipt: "run.sh `herdr-plugin-withdraw-operator)` arm -> scripts/herdr-operator-link.mjs withdraw (#153)",
576
+ },
577
+ ];
578
+ for (const ins of insertedLines) {
579
+ const rows = expectedCatalog.split("\n");
580
+ const at = rows.flatMap((l, i) => (l.startsWith(ins.after) ? [i] : []));
581
+ if (at.length !== 1) amendmentProblems.push(`insertion anchor not exactly once: ${ins.after}`);
582
+ else rows.splice(at[0] + 1, 0, ins.line);
583
+ expectedCatalog = rows.join("\n");
584
+ }
585
+
586
+ const helpEnv = { PATH: process.env.PATH ?? "/usr/bin:/bin" };
587
+ const runHelp = (runSh: string, argv: string[], cwd: string, env: Record<string, string> = helpEnv) =>
588
+ spawnSync("bash", [runSh, ...argv], { cwd, encoding: "utf8", env });
589
+ const rootForms: string[][] = [[], ["-h"], ["--help"], ["help"]];
590
+ /** Recording shims for the tools a help run must not start; `node` gets `nodeTail` instead. */
591
+ const makeShims = (dir: string, nodeTail: string): { bin: string; trace: string } => {
592
+ const bin = path.join(dir, "shims");
593
+ const trace = path.join(dir, "trace");
594
+ mkdirSync(bin);
595
+ writeFileSync(trace, "");
596
+ const watched = [
597
+ "python3",
598
+ "pnpm",
599
+ "npm",
600
+ "git",
601
+ "tmux",
602
+ "herdr",
603
+ "pi",
604
+ "claude",
605
+ "codex",
606
+ "copilot",
607
+ "ssh",
608
+ "curl",
609
+ ];
610
+ for (const name of ["node", ...watched]) {
611
+ writeFileSync(
612
+ path.join(bin, name),
613
+ `#!/bin/sh\nprintf '%s' "${name}" >>'${trace}'; printf ' %s' "$@" >>'${trace}'; echo >>'${trace}'\n${name === "node" ? nodeTail : "exit 99"}\n`,
614
+ { mode: 0o755 },
615
+ );
616
+ }
617
+ return { bin, trace };
618
+ };
619
+ const rendersRoot = (runSh: string, cwd: string): string[] => {
620
+ const bad: string[] = [];
621
+ for (const argv of rootForms) {
622
+ const r = runHelp(runSh, argv, cwd);
623
+ if (r.status !== 0 || r.stderr !== "" || r.stdout !== expectedCompact) {
624
+ bad.push(`${JSON.stringify(argv)}: rc=${r.status} stdout-sha=${sha256(r.stdout)} stderr=${r.stderr.trim()}`);
625
+ }
626
+ }
627
+ const u = runHelp(runSh, ["no-such-command"], cwd);
628
+ if (u.status !== 1 || u.stderr !== "Unknown command: no-such-command\n" || u.stdout !== expectedCompact) {
629
+ bad.push(`unknown: rc=${u.status} stdout-sha=${sha256(u.stdout)} stderr=${u.stderr.trim()}`);
630
+ }
631
+ return bad;
632
+ };
633
+ const rootBad = rendersRoot(path.join(REPO, "run.sh"), REPO);
634
+ ok(
635
+ `[QK:CLI-HELP-ROOT-COMPACT] S9a the root help (no argv, -h, --help, help) and the Unknown path print exactly the reviewed root page (${COMPACT_FIXTURE} with the three #157 amendments)`,
636
+ rootBad.length === 0,
637
+ rootBad.join("\n"),
638
+ );
639
+ // The catalog the root page used to be: `entwurf help all`, against the ce92dc7 oracle.
640
+ const allPage = runHelp(path.join(REPO, "run.sh"), ["help", "all"], REPO);
641
+ ok(
642
+ "[QK:CLI-HELP-ALL-BYTES] S9a* `entwurf help all` prints exactly the ce92dc7 usage bytes with the reviewed amendments, on stdout, exit 0, nothing on stderr",
643
+ allPage.status === 0 && allPage.stderr === "" && allPage.stdout === expectedCatalog,
644
+ `rc=${allPage.status} stdout-sha=${sha256(allPage.stdout)} stderr=${allPage.stderr.trim()}`,
645
+ );
646
+ const catalogNow = allPage.stdout;
647
+ ok(
648
+ "[QK:CLI-HELP-AMENDMENTS] S9a' each amendment is exact in the catalog `help all` prints: every command line is spelled `entwurf`, the tool count is the one EXPECTED_TOOLS holds, and no replaced phrase survives",
649
+ amendmentProblems.length === 0 &&
650
+ respelled === 189 &&
651
+ !/^ {2}\.\/run\.sh /m.test(catalogNow) &&
652
+ literalAmendments.every((a) => catalogNow.split(a.next).length === 2 && !catalogNow.includes(a.old)),
653
+ [...amendmentProblems, `respelled lines: ${respelled}`].join("\n"),
654
+ );
655
+ // pi left the description debt: its catalog line is the reviewed literal, once, beside
656
+ // pi-durable's, and `entwurf help pi` prints that same summary as its page.
657
+ const piRows = catalogNow.split("\n");
658
+ const piLine = insertedLines[0].line;
659
+ const piAt = piRows.flatMap((l, i) => (l === piLine ? [i] : []));
660
+ const piPage = runHelp(path.join(REPO, "run.sh"), ["help", "pi"], REPO);
661
+ const piSummary = piLine.slice(piLine.indexOf("# ") + 2);
662
+ ok(
663
+ "[QK:CLI-HELP-PI-DESCRIBED] S9a'' `pi` is described by the reviewed literal: one catalog line, right after pi-durable's, and `entwurf help pi` prints that summary",
664
+ piAt.length === 1 &&
665
+ piRows[piAt[0] - 1].startsWith(insertedLines[0].after) &&
666
+ piPage.status === 0 &&
667
+ piPage.stderr === "" &&
668
+ piPage.stdout === `Usage: entwurf pi [args...]\n\n${piSummary}\n`,
669
+ JSON.stringify({ lines: piAt.length, rc: piPage.status, stderr: piPage.stderr, page: piPage.stdout.slice(0, 200) }),
670
+ );
671
+ // #157 — each guide topic, asked `entwurf help <name>` on the real run.sh, prints its reviewed
672
+ // literal: stdout only, exit 0, nothing on stderr.
673
+ const topicBad = TOPIC_FIXTURES.flatMap((t) => {
674
+ const r = runHelp(path.join(REPO, "run.sh"), ["help", t.name], REPO);
675
+ return r.status === 0 && r.stderr === "" && r.stdout === topicPage.get(t.name)
676
+ ? []
677
+ : [`help ${t.name}: rc=${r.status} stdout-sha=${sha256(r.stdout)} stderr=${r.stderr.trim()}`];
678
+ });
679
+ ok(
680
+ `[QK:CLI-HELP-TOPIC-BYTES] S9a''' \`entwurf help team\` and \`entwurf help remote-pair\` print exactly their reviewed guide pages (team: its fixture; remote-pair: its fixture with the one pinned review amendment), on stdout, exit 0, nothing on stderr`,
681
+ topicBad.length === 0,
682
+ topicBad.join("\n"),
683
+ );
684
+ // The files a guide sends its reader to must reach every install: each is an EXACT entry of the
685
+ // package.json files array (no glob reading here — the tarball proof is check-pack's), exists in
686
+ // this checkout, and is named by its guide's reviewed literal.
687
+ const packageFiles = (JSON.parse(readFileSync(path.join(REPO, "package.json"), "utf8")) as { files: string[] }).files;
688
+ const unshipped = TOPIC_FIXTURES.flatMap((t) =>
689
+ t.ships.flatMap((f) =>
690
+ packageFiles.includes(f) && existsSync(path.join(REPO, f)) && (topicPage.get(t.name) ?? "").includes(f)
691
+ ? []
692
+ : [`${t.name} -> ${f}: files entry=${packageFiles.includes(f)} exists=${existsSync(path.join(REPO, f))}`],
693
+ ),
694
+ );
695
+ ok(
696
+ "[QK:CLI-HELP-TOPIC-SHIPPED] S9w every package file a guide topic names (team.json.example, docs/setup-clean-host.md) is an exact package.json files entry, exists, and is named by that guide",
697
+ unshipped.length === 0,
698
+ unshipped.join("\n"),
699
+ );
700
+
701
+ const scratch = mkdtempSync(path.join(os.tmpdir(), "entwurf-cli-help-"));
702
+ try {
703
+ // A decoy source in the caller's cwd and HOME: the renderer must read the bytes beside itself.
704
+ const decoy = path.join(scratch, "decoy");
705
+ mkdirSync(path.join(decoy, "scripts"), { recursive: true });
706
+ writeFileSync(path.join(decoy, CLI_JSON), "{");
707
+ const r = runHelp(path.join(REPO, "run.sh"), ["--help"], decoy, { ...helpEnv, HOME: decoy });
708
+ const rAll = runHelp(path.join(REPO, "run.sh"), ["help", "all"], decoy, { ...helpEnv, HOME: decoy });
709
+ ok(
710
+ "[QK:CLI-HELP-MODULE-RELATIVE] S9b the help source is read beside the renderer, never from the caller's cwd or HOME",
711
+ r.status === 0 && r.stdout === expectedCompact && rAll.status === 0 && rAll.stdout === expectedCatalog,
712
+ `root rc=${r.status} stderr=${r.stderr.trim()}; help all rc=${rAll.status} stderr=${rAll.stderr.trim()}`,
713
+ );
714
+
715
+ // Production run.sh under a PATH whose first entries record what a help run starts, among the
716
+ // tools shimmed here: node is wrapped and then really runs, the rest refuse. This is a PATH
717
+ // watch, not a full exec audit — an absolute-path exec or a module node imports is not seen. The
718
+ // renderer's own reach is read directly: it imports node:fs alone and reads one module-relative
719
+ // file. Within that scope a help run starts one thing: node on the shipped renderer, no argument.
720
+ const { bin: shims, trace } = makeShims(scratch, `exec '${process.execPath}' "$@"`);
721
+ const traced = { PATH: `${shims}:${helpEnv.PATH}` };
722
+ for (const argv of [...rootForms, ["no-such-command"]]) runHelp(path.join(REPO, "run.sh"), argv, REPO, traced);
723
+ const want = `node ${path.join(REPO, CLI_RENDERER)}\n`.repeat(rootForms.length + 1);
724
+ const got = readFileSync(trace, "utf8");
725
+ ok(
726
+ "[QK:CLI-HELP-ONLY-RENDERER] S9c of the PATH tools watched (node, python3, pnpm, npm, git, tmux, herdr, pi, claude, codex, copilot, ssh, curl), a help run starts only node on scripts/cli-help.mjs",
727
+ got === want,
728
+ `trace:\n${got}`,
729
+ );
730
+ // The catalog's own route, watched the same way: `help all` starts node on the renderer with
731
+ // `--all`, the one argv that selects the catalog, and nothing else.
732
+ const allRouteDir = path.join(scratch, "all-route");
733
+ mkdirSync(allRouteDir);
734
+ const allShims = makeShims(allRouteDir, `exec '${process.execPath}' "$@"`);
735
+ runHelp(path.join(REPO, "run.sh"), ["help", "all"], REPO, { PATH: `${allShims.bin}:${helpEnv.PATH}` });
736
+ const allTrace = readFileSync(allShims.trace, "utf8");
737
+ ok(
738
+ "[QK:CLI-HELP-ALL-ROUTE] S9c' of the same watched PATH tools, `entwurf help all` starts exactly node on scripts/cli-help.mjs --all",
739
+ allTrace === `node ${path.join(REPO, CLI_RENDERER)} --all\n`,
740
+ `trace:\n${allTrace}`,
741
+ );
742
+ // A guide's own route, watched the same way: `help <topic>` starts node on the renderer with
743
+ // `--name <topic>` — the one mode that may answer with a topic — and nothing else.
744
+ const topicRouteDir = path.join(scratch, "topic-route");
745
+ mkdirSync(topicRouteDir);
746
+ const topicShims = makeShims(topicRouteDir, `exec '${process.execPath}' "$@"`);
747
+ for (const t of TOPIC_FIXTURES)
748
+ runHelp(path.join(REPO, "run.sh"), ["help", t.name], REPO, { PATH: `${topicShims.bin}:${helpEnv.PATH}` });
749
+ const topicTrace = readFileSync(topicShims.trace, "utf8");
750
+ ok(
751
+ "[QK:CLI-HELP-TOPIC-ROUTE] S9c'' of the same watched PATH tools, `entwurf help <topic>` starts exactly node on scripts/cli-help.mjs --name <topic>",
752
+ topicTrace === TOPIC_FIXTURES.map((t) => `node ${path.join(REPO, CLI_RENDERER)} --name ${t.name}\n`).join(""),
753
+ `trace:\n${topicTrace}`,
754
+ );
755
+
756
+ // A fixture copy whose description source is missing or malformed. Each must fail by name with
757
+ // nothing on stdout — the root forms and the Unknown path alike, no older text behind them.
758
+ // These are the RUNTIME refusals. A duplicated JSON property is not one of them: JSON.parse
759
+ // keeps the last value silently, so only S9g's check-time canonical comparison sees it.
760
+ const good = JSON.parse(readFileSync(path.join(REPO, CLI_JSON), "utf8")) as {
761
+ schemaVersion: number;
762
+ root: Record<string, unknown>;
763
+ topics: Record<string, unknown>[];
764
+ usage: { commandGroups: Record<string, unknown>[][] };
765
+ };
766
+ const variant = (edit: (d: typeof good) => void): string => {
767
+ const d = structuredClone(good);
768
+ edit(d);
769
+ return JSON.stringify(d);
770
+ };
771
+ const first = good.usage.commandGroups[0]?.[0];
772
+ const broken: [label: string, json: string | null][] = [
773
+ ["missing", null],
774
+ ["not JSON", "{"],
775
+ [
776
+ "unsupported schemaVersion",
777
+ variant((d) => {
778
+ d.schemaVersion = 1;
779
+ }),
780
+ ],
781
+ [
782
+ "the previous schemaVersion (2, before the root page)",
783
+ variant((d) => {
784
+ d.schemaVersion = 2;
785
+ }),
786
+ ],
787
+ [
788
+ "the previous schemaVersion (3, before the guide topics)",
789
+ variant((d) => {
790
+ d.schemaVersion = 3;
791
+ }),
792
+ ],
793
+ [
794
+ "a topic with a field the schema does not know",
795
+ variant((d) => {
796
+ d.topics[0]!.run = "x";
797
+ }),
798
+ ],
799
+ [
800
+ "a topic named like a described command",
801
+ variant((d) => {
802
+ d.topics[0]!.name = "setup";
803
+ }),
804
+ ],
805
+ [
806
+ "a topic named all",
807
+ variant((d) => {
808
+ d.topics[0]!.name = "all";
809
+ }),
810
+ ],
811
+ [
812
+ "one topic name twice",
813
+ variant((d) => {
814
+ d.topics[1]!.name = d.topics[0]!.name;
815
+ }),
816
+ ],
817
+ [
818
+ "a root page with a field the schema does not know",
819
+ variant((d) => {
820
+ d.root.run = "x";
821
+ }),
822
+ ],
823
+ [
824
+ "an entry with a field the schema does not know",
825
+ variant((d) => {
826
+ d.usage.commandGroups[0]![0]!.run = "x";
827
+ }),
828
+ ],
829
+ [
830
+ "one command described twice",
831
+ variant((d) => {
832
+ d.usage.commandGroups[1]!.push({ ...first });
833
+ }),
834
+ ],
835
+ [
836
+ "details that start on an empty line",
837
+ variant((d) => {
838
+ d.usage.commandGroups[0]![0]!.details = ["", "text"];
839
+ }),
840
+ ],
841
+ [
842
+ "details that are not single-line strings",
843
+ variant((d) => {
844
+ d.usage.commandGroups[0]![0]!.details = ["one\ntwo"];
845
+ }),
846
+ ],
847
+ ];
848
+ const failures: string[] = [];
849
+ const strictness: Record<string, boolean> = {};
850
+ for (const [label, json] of broken) {
851
+ const dir = path.join(scratch, `broken-${Object.keys(strictness).length}`);
852
+ mkdirSync(dir);
853
+ writeFileSync(path.join(dir, "run.sh"), RUN_SH);
854
+ stageHelp(dir, json ?? "");
855
+ if (json === null) rmSync(path.join(dir, CLI_JSON));
856
+ let refused = true;
857
+ // The root forms, the Unknown path, the catalog and a command help: each must be the metadata
858
+ // failure — exactly 1, never the help refusal's 2 — with nothing on stdout.
859
+ for (const argv of [
860
+ ["--help"],
861
+ ["no-such-command"],
862
+ ["help", "all"],
863
+ ["peer-facts", "--help"],
864
+ ["help", "team"],
865
+ ]) {
866
+ const b = runHelp(path.join(dir, "run.sh"), argv, dir);
867
+ if (b.status !== 1 || b.stdout !== "" || !b.stderr.includes(`entwurf: ${CLI_JSON} `)) {
868
+ refused = false;
869
+ failures.push(
870
+ `${label} ${JSON.stringify(argv)}: rc=${b.status} stdout=${b.stdout.length}B stderr=${b.stderr.trim()}`,
871
+ );
872
+ }
873
+ }
874
+ strictness[label] = refused;
875
+ }
876
+ ok(
877
+ "[QK:CLI-HELP-FAIL-LOUD] S9d a missing or unparseable help source fails help by name, with no stdout and no fallback text",
878
+ strictness.missing === true && strictness["not JSON"] === true,
879
+ failures.join("\n"),
880
+ );
881
+ ok(
882
+ "[QK:CLI-HELP-STRICT-FIELDS] S9e the renderer refuses an old schemaVersion (2 and 3 included), a field the schema does not know in an entry, the root page or a topic, and details that start empty or span lines",
883
+ strictness["unsupported schemaVersion"] === true &&
884
+ strictness["the previous schemaVersion (2, before the root page)"] === true &&
885
+ strictness["the previous schemaVersion (3, before the guide topics)"] === true &&
886
+ strictness["an entry with a field the schema does not know"] === true &&
887
+ strictness["a root page with a field the schema does not know"] === true &&
888
+ strictness["a topic with a field the schema does not know"] === true &&
889
+ strictness["details that start on an empty line"] === true &&
890
+ strictness["details that are not single-line strings"] === true,
891
+ failures.join("\n"),
892
+ );
893
+ ok(
894
+ "[QK:CLI-HELP-UNIQUE] S9f the renderer refuses a command described twice",
895
+ strictness["one command described twice"] === true,
896
+ failures.join("\n"),
897
+ );
898
+ ok(
899
+ "[QK:CLI-HELP-TOPIC-DISJOINT] S9f'' the renderer refuses a topic that takes a described command's name, `all`, or another topic's name — one word, one page",
900
+ strictness["a topic named like a described command"] === true &&
901
+ strictness["a topic named all"] === true &&
902
+ strictness["one topic name twice"] === true,
903
+ failures.join("\n"),
904
+ );
905
+ // The two failure kinds stay apart: a broken source is exit 1 on every form above, while the
906
+ // same command-help shape against an intact source, naming nothing it describes, is the help
907
+ // refusal's exit 2.
908
+ const intactRefusal = runHelp(path.join(REPO, "run.sh"), ["help", "no-such-command"], REPO);
909
+ ok(
910
+ "[QK:CLI-HELP-METADATA-EXIT] S9f' a missing or malformed source is exit 1 on root, Unknown and command help alike — distinct from the exit 2 of a refused help request",
911
+ Object.keys(strictness).length === broken.length &&
912
+ Object.values(strictness).every(Boolean) &&
913
+ intactRefusal.status === 2 &&
914
+ intactRefusal.stdout === "" &&
915
+ intactRefusal.stderr.startsWith("entwurf: no help for "),
916
+ [...failures, `intact refusal: rc=${intactRefusal.status} stderr=${intactRefusal.stderr.trim()}`].join("\n"),
917
+ );
918
+ } finally {
919
+ rmSync(scratch, { recursive: true, force: true });
920
+ }
921
+
922
+ // Check time only: the committed bytes are the canonical serialisation of what they parse to.
923
+ // This is what catches a duplicated JSON property (JSON.parse keeps the last one silently, so it
924
+ // would change the rendering without failing), and it pins one format so a review reads one
925
+ // shape. It says nothing about a file someone edits after install.
926
+ const cliBytes = readFileSync(path.join(REPO, CLI_JSON), "utf8");
927
+ ok(
928
+ "[QK:CLI-HELP-CANONICAL] S9g ENTWURF-CLI.json is its own canonical serialisation (no duplicated property survives the commit)",
929
+ cliBytes === `${JSON.stringify(JSON.parse(cliBytes), null, "\t")}\n`,
930
+ 'rewrite it with JSON.stringify(data, null, "\\t") + "\\n"',
931
+ );
932
+
933
+ // Coverage, read independently of the data. Operator commands are the case labels of the
934
+ // top-level dispatch: the `case "$cmd" in` after the `cmd=${1:-}` line, to the column-0 `esac`
935
+ // that closes it. That is a text read, so it is bound to the reviewed ce92dc7 case (plus its reviewed
936
+ // #153 two-arm amendment) by a byte pin
937
+ // — evidence for this change, not a ban on later dispatch work: a change to the case re-reviews
938
+ // its labels and moves the pin in the same change. The labels are not claimed to be
939
+ // runtime-proven. Gate commands are the check_/smoke_ functions bash itself defines when it reads
940
+ // run.sh — a definition spelled only in a heredoc or a comment defines nothing, and an
941
+ // environment function is not run.sh's.
942
+ const CASE_LINE = 'case "$cmd" in\n';
943
+ const start = RUN_SH.indexOf(DISPATCH);
944
+ const caseAt = RUN_SH.indexOf(`\n${CASE_LINE}`, start) + 1;
945
+ const end = RUN_SH.indexOf("\nesac\n", caseAt) + "\nesac\n".length;
946
+ // Moved for #153: two operator-link arms (herdr-plugin-expose-operator / -withdraw-operator), re-reviewed.
947
+ // Moved for #157 stage 4: one `claude)` arm, nothing removed (133 -> 134 labels, re-reviewed: `claude`
948
+ // is a described entry and outside the debt).
949
+ const REVIEWED_CASE_SHA256 = "bfdafcfad8645ea3651905dd08d73808a724c67e42f89f9721c917f3a494bef9";
950
+ ok(
951
+ "[QK:CLI-HELP-DISPATCH-REVIEWED] S9h the case the operator labels are read from is the reviewed ce92dc7 case plus the reviewed #153 two-arm amendment, byte for byte",
952
+ start > 0 && caseAt > start && sha256(RUN_SH.slice(caseAt, end)) === REVIEWED_CASE_SHA256,
953
+ "the top-level case changed: re-review its labels against ENTWURF-CLI.json and the debt below, then move the pin",
954
+ );
955
+ // The help preflight sits between `cmd=${1:-}` and that case, and nowhere else: the only case is
956
+ // the pinned one, and the preflight's code opens no case, esac or heredoc that could hide a
957
+ // second dispatch or move the pinned one.
958
+ const preflightCode = RUN_SH.slice(start + DISPATCH.length, caseAt)
959
+ .split("\n")
960
+ .filter((l) => !/^\s*#/.test(l))
961
+ .join("\n");
962
+ ok(
963
+ "[QK:CLI-HELP-PREFLIGHT-PLACEMENT] S9h' the help preflight is plain if/elif code between the dispatch's `cmd=` line and the one pinned case",
964
+ RUN_SH.split(CASE_LINE).length === 2 &&
965
+ /\bexec node "\$REPO_DIR\/scripts\/cli-help\.mjs" --command\b/.test(preflightCode) &&
966
+ !/\bcase\b|\besac\b|<</.test(preflightCode),
967
+ preflightCode,
968
+ );
969
+ const labels = [...RUN_SH.slice(caseAt, end).matchAll(/^ {2}([a-z][a-z0-9-]*)\)$/gm)].map((m) => m[1] as string);
970
+
971
+ // A byte copy that stops right before the dispatch and lists what bash defined while reading it.
972
+ // Everything above the dispatch runs — assignments and definitions — so the probe gets a scratch
973
+ // HOME/XDG and the same recording shims as S9c (node included, refusing): nothing may start and
974
+ // nothing may be written.
975
+ const bashDefinedGates = (text: string): { names: Set<string>; problems: string[] } => {
976
+ const dir = mkdtempSync(path.join(os.tmpdir(), "entwurf-gate-fns-"));
977
+ try {
978
+ if (text.split(DISPATCH).length !== 2) throw new Error("S9: the copy has no single dispatch line");
979
+ const home = path.join(dir, "home");
980
+ mkdirSync(home);
981
+ const { bin, trace } = makeShims(dir, "exit 99");
982
+ const copy = path.join(dir, "run.sh");
983
+ const probe = 'shopt -s extdebug\nfor __f in $(compgen -A function); do declare -F "$__f"; done\nexit 0';
984
+ writeFileSync(copy, text.replace(DISPATCH, `\n${probe}${DISPATCH}`));
985
+ const r = spawnSync("bash", [copy], {
986
+ cwd: dir,
987
+ encoding: "utf8",
988
+ env: {
989
+ PATH: `${bin}:${helpEnv.PATH}`,
990
+ HOME: home,
991
+ XDG_CONFIG_HOME: path.join(home, ".config"),
992
+ XDG_DATA_HOME: path.join(home, ".local/share"),
993
+ XDG_STATE_HOME: path.join(home, ".local/state"),
994
+ XDG_CACHE_HOME: path.join(home, ".cache"),
995
+ },
996
+ });
997
+ const problems: string[] = [];
998
+ if (r.status !== 0 || r.stderr !== "") problems.push(`probe rc=${r.status} stderr=${r.stderr.trim()}`);
999
+ const started = readFileSync(trace, "utf8");
1000
+ if (started !== "") problems.push(`the probe started:\n${started}`);
1001
+ if (readdirSync(home, { recursive: true }).length > 0) problems.push("the probe wrote below its HOME");
1002
+ const names = new Set<string>();
1003
+ for (const line of r.stdout.split("\n")) {
1004
+ const m = line.match(/^((?:check|smoke)_[a-z0-9_]+) [1-9][0-9]* (.*)$/);
1005
+ if (m && m[2] === copy) names.add((m[1] as string).replace(/_/g, "-"));
1006
+ }
1007
+ return { names, problems };
1008
+ } finally {
1009
+ rmSync(dir, { recursive: true, force: true });
1010
+ }
1011
+ };
1012
+ const defined = bashDefinedGates(RUN_SH);
1013
+ const inventory = new Set([...labels, ...defined.names]);
1014
+ const described = (
1015
+ JSON.parse(cliBytes) as { usage: { commandGroups: { synopsis: string }[][] } }
1016
+ ).usage.commandGroups.flatMap((g) => g.map((e) => e.synopsis.split(" ", 1)[0] as string));
1017
+ const orphansOf = (inv: Set<string>): string[] => described.filter((n) => !inv.has(n));
1018
+ const orphans = orphansOf(inventory);
1019
+ ok(
1020
+ `[QK:CLI-HELP-NO-ORPHAN] S9i every described command is a reviewed dispatch label or a gate function bash defines from run.sh (${described.length} described, ${labels.length} labels + ${defined.names.size} gate functions)`,
1021
+ labels.length > 0 && defined.names.size > 0 && defined.problems.length === 0 && orphans.length === 0,
1022
+ [...defined.problems, orphans.length ? `described but not reachable: ${orphans.join(", ")}` : ""]
1023
+ .filter(Boolean)
1024
+ .join("\n"),
1025
+ );
1026
+
1027
+ // The negative that makes the owner read worth having: a real, described gate whose definition
1028
+ // survives only as heredoc text. A text reader still finds `check_peer_facts() {` there; bash
1029
+ // defines nothing, so the name leaves the inventory and its description becomes an orphan.
1030
+ const ownerAt = RUN_SH.indexOf("\ncheck_peer_facts() {\n");
1031
+ const ownerEnd = RUN_SH.indexOf("\n}\n", ownerAt) + "\n}".length;
1032
+ if (ownerAt < 0 || ownerEnd <= ownerAt) throw new Error("S9: run.sh no longer defines check_peer_facts");
1033
+ const textOnlyOwner = bashDefinedGates(
1034
+ `${RUN_SH.slice(0, ownerAt)}\n: <<'TEXT_ONLY'${RUN_SH.slice(ownerAt, ownerEnd)}\nTEXT_ONLY${RUN_SH.slice(ownerEnd)}`,
1035
+ );
1036
+ ok(
1037
+ "[QK:CLI-HELP-REAL-OWNER] S9j a described gate whose definition is left only as heredoc text is not a command: it leaves the inventory and its description is an orphan",
1038
+ defined.names.has("check-peer-facts") &&
1039
+ textOnlyOwner.problems.length === 0 &&
1040
+ !textOnlyOwner.names.has("check-peer-facts") &&
1041
+ orphansOf(new Set([...labels, ...textOnlyOwner.names])).includes("check-peer-facts"),
1042
+ JSON.stringify({ problems: textOnlyOwner.problems, stillDefined: textOnlyOwner.names.has("check-peer-facts") }),
1043
+ );
1044
+
1045
+ // The commands that had no line in the ce92dc7 usage, computed from that commit's run.sh with
1046
+ // the reader above. The set only shrinks: a new command arrives with its description, and a
1047
+ // described debt name leaves this list in the same change.
1048
+ const DESCRIPTION_DEBT = new Set([
1049
+ "check-acp-backend-preflight",
1050
+ "check-acp-carrier-augment",
1051
+ "check-acp-config",
1052
+ "check-acp-cortex",
1053
+ "check-acp-event-mapper",
1054
+ "check-acp-foreground-only",
1055
+ "check-acp-launch-namespace",
1056
+ "check-acp-overlay",
1057
+ "check-acp-prompt-builder",
1058
+ "check-acp-prompt-lifecycle",
1059
+ "check-acp-provider-surface",
1060
+ "check-acp-sdk-surface",
1061
+ "check-acp-session-reuse",
1062
+ "check-acp-session-store",
1063
+ "check-acp-set-model-readback",
1064
+ "check-acp-stop-reason",
1065
+ "check-acp-stream-hooks",
1066
+ "check-acp-tool-surface",
1067
+ "check-acp-usage-accounting",
1068
+ "check-acp-vendor-rejection",
1069
+ "check-agent-guidance-beside",
1070
+ "check-auth-boundary",
1071
+ "check-cc-retire-pidfd",
1072
+ "check-cc-retire-request",
1073
+ "check-claude-floor-coherence",
1074
+ "check-control-retire",
1075
+ "check-control-send-receipt",
1076
+ "check-control-socket-path",
1077
+ "check-entwurf-control-rpc",
1078
+ "check-entwurf-resume-args",
1079
+ "check-entwurf-v2-native-push",
1080
+ "check-entwurf-v2-production",
1081
+ "check-entwurf-v2-retire",
1082
+ "check-entwurf-v2-runner",
1083
+ "check-entwurf-v2-send",
1084
+ "check-entwurf-v2-surface",
1085
+ "check-entwurf-v2-visible-resume",
1086
+ "check-env-namespace",
1087
+ "check-fresh-call-dispatch",
1088
+ "check-fresh-call-provider",
1089
+ "check-herdr-fresh-call",
1090
+ "check-herdr-sandbox",
1091
+ "check-herdr-supply",
1092
+ "check-install-preflight",
1093
+ "check-install-surface",
1094
+ "check-mux-fresh-call",
1095
+ "check-mux-launch",
1096
+ "check-mux-launch-tmux",
1097
+ "check-mux-placement",
1098
+ "check-mux-placement-tmux",
1099
+ "check-omp-receive-arm",
1100
+ "check-pack-win32-carrier",
1101
+ "check-pi-import-surface",
1102
+ "check-pi-launch",
1103
+ "check-pi-preflight",
1104
+ "check-pi-queue-oracle",
1105
+ "check-pi-retire-request",
1106
+ "check-pi-runtime-version",
1107
+ "check-probe-bridge-command",
1108
+ "check-retirement-event",
1109
+ "check-tests-beside-behavior",
1110
+ "check-win32-preview",
1111
+ "doctor-pi-provider",
1112
+ "expose-dev-bin",
1113
+ "remove-dev-bin",
1114
+ "smoke-acp-bundled-mcp-live",
1115
+ "smoke-acp-carrier-augment-live",
1116
+ "smoke-acp-cortex-live",
1117
+ "smoke-acp-long-turn-live",
1118
+ "smoke-acp-mcp-live",
1119
+ "smoke-acp-memory-containment-live",
1120
+ "smoke-acp-ordering-probe-live",
1121
+ "smoke-acp-overlay-live",
1122
+ "smoke-acp-provider-live",
1123
+ "smoke-acp-raw-turn-live",
1124
+ "smoke-acp-rgg-live",
1125
+ "smoke-acp-session-reuse-live",
1126
+ "smoke-acp-skill-live",
1127
+ "smoke-acp-socket-citizen-live",
1128
+ "smoke-acp-v2-send-live",
1129
+ "smoke-herdr-fresh-call-live",
1130
+ "smoke-herdr-plugin-build-live",
1131
+ "smoke-herdr-raw-install-live",
1132
+ "smoke-mux-fresh-call-live",
1133
+ "smoke-omp-receive-state",
1134
+ "smoke-pi-provider-state",
1135
+ "wire-agy-bridge",
1136
+ "wire-agy-hooks",
1137
+ "wire-agy-statusline",
1138
+ ]);
1139
+ const undescribed = [...inventory].filter((n) => !described.includes(n));
1140
+ const newDebt = undescribed.filter((n) => !DESCRIPTION_DEBT.has(n));
1141
+ const paid = [...DESCRIPTION_DEBT].filter((n) => !undescribed.includes(n));
1142
+ ok(
1143
+ `[QK:CLI-HELP-DEBT-EXACT] S9k the commands with no description are exactly the ${DESCRIPTION_DEBT.size} the ce92dc7 usage already lacked`,
1144
+ newDebt.length === 0 && paid.length === 0,
1145
+ [
1146
+ newDebt.length ? `no description in ${CLI_JSON}: ${newDebt.join(", ")}` : "",
1147
+ paid.length ? `described or removed — drop from DESCRIPTION_DEBT: ${paid.join(", ")}` : "",
1148
+ ]
1149
+ .filter(Boolean)
1150
+ .join("\n"),
1151
+ );
1152
+
1153
+ // The root page names only real, described commands. Read from the reviewed literal itself — not
1154
+ // from the data — every `entwurf <name>` it shows (rows and examples; `help` is the help verb) must
1155
+ // be a described entry, in the inventory, and outside the debt. Then the guard that keeps the data
1156
+ // honest: one example in an otherwise valid source names an undescribed command, and the source
1157
+ // fails by name at that example — an example prints verbatim, so without the guard it would be
1158
+ // advertised, not crash.
1159
+ const rootNamed = [
1160
+ ...new Set([...expectedCompact.matchAll(/^ {2}entwurf ([a-z][a-z0-9-]*)/gm)].map((m) => m[1] as string)),
1161
+ ].filter((n) => n !== "help");
1162
+ const unreal = rootNamed.filter((n) => !described.includes(n) || !inventory.has(n) || DESCRIPTION_DEBT.has(n));
1163
+ const BAD_NAME = "zz-undescribed";
1164
+ const badExampleDir = mkdtempSync(path.join(os.tmpdir(), "entwurf-root-names-"));
1165
+ let badExample: ReturnType<typeof runHelp>;
1166
+ try {
1167
+ const data = JSON.parse(cliBytes) as { root: { examples: { items: { command: string }[] } } };
1168
+ data.root.examples.items[1]!.command = `entwurf ${BAD_NAME}`;
1169
+ writeFileSync(path.join(badExampleDir, "run.sh"), RUN_SH);
1170
+ stageHelp(badExampleDir, JSON.stringify(data));
1171
+ badExample = runHelp(path.join(badExampleDir, "run.sh"), ["--help"], badExampleDir);
1172
+ } finally {
1173
+ rmSync(badExampleDir, { recursive: true, force: true });
1174
+ }
1175
+ ok(
1176
+ `[QK:CLI-HELP-ROOT-NAMES-REAL] S9k' every command the root page names is described, dispatchable and outside the debt (${rootNamed.length} names), and an example naming an undescribed command fails the source by name at that example`,
1177
+ rootNamed.length > 0 &&
1178
+ unreal.length === 0 &&
1179
+ !inventory.has(BAD_NAME) &&
1180
+ badExample.status === 1 &&
1181
+ badExample.stdout === "" &&
1182
+ badExample.stderr ===
1183
+ `entwurf: ${CLI_JSON} root.examples.items[1].command: ${JSON.stringify(BAD_NAME)} is not a command the catalog describes\n`,
1184
+ JSON.stringify({
1185
+ unreal,
1186
+ rc: badExample.status,
1187
+ stdout: badExample.stdout.slice(0, 120),
1188
+ stderr: badExample.stderr,
1189
+ }),
1190
+ );
1191
+ // ── the help preflight, proven on a copy that cannot dispatch ─────────────────────────
1192
+ // A byte copy of run.sh with one line added right before the unchanged case: whatever gets past
1193
+ // the preflight stops there (stderr DISPATCHED, exit 97) instead of entering an arm, so every
1194
+ // command can be asked safely. The bare root forms and the Unknown path go through that case and
1195
+ // are S9a's, on the real file. Expected pages are built from the entry the data holds; what is
1196
+ // compared is the output of the whole run.sh path — routing and format, not content, which the
1197
+ // catalog and fresh-cut fixtures bind.
1198
+ const VENDORS = ["pi", "pi-durable", "claude", "copilot", "codex-app-server"];
1199
+ type Entry = { synopsis: string; summary: string; details?: string[] };
1200
+ const entryOf = new Map<string, Entry>();
1201
+ for (const g of (JSON.parse(cliBytes) as { usage: { commandGroups: Entry[][] } }).usage.commandGroups)
1202
+ for (const e of g) entryOf.set(e.synopsis.split(" ", 1)[0] as string, e);
1203
+ const page = (e: Entry): string =>
1204
+ `${[`Usage: entwurf ${e.synopsis}`, "", e.summary, ...(e.details ? ["", ...e.details] : [])].join("\n")}\n`;
1205
+ const gated = mkdtempSync(path.join(os.tmpdir(), "entwurf-help-preflight-"));
1206
+ try {
1207
+ const copy = path.join(gated, "run.sh");
1208
+ writeFileSync(copy, RUN_SH.replace(`\n${CASE_LINE}`, `\necho DISPATCHED >&2; exit 97\n${CASE_LINE}`));
1209
+ stageHelp(gated);
1210
+ const run = (argv: string[], env: Record<string, string> = helpEnv) =>
1211
+ spawnSync("bash", [copy, ...argv], { cwd: gated, encoding: "utf8", env });
1212
+ type R = ReturnType<typeof run>;
1213
+ const dispatched = (r: R): boolean => r.status === 97 && r.stderr === "DISPATCHED\n" && r.stdout === "";
1214
+ const refused = (r: R): boolean =>
1215
+ r.status === 2 && r.stdout === "" && /^entwurf: [^\n]*\n$/.test(r.stderr) && !r.stderr.includes("DISPATCHED");
1216
+ const answers =
1217
+ (name: string) =>
1218
+ (r: R): boolean => {
1219
+ const e = entryOf.get(name);
1220
+ return e !== undefined && r.status === 0 && r.stderr === "" && r.stdout === page(e);
1221
+ };
1222
+ const show = (argv: string[], r: R): string =>
1223
+ `${JSON.stringify(argv)}: rc=${r.status} stdout=${r.stdout.length}B stderr=${JSON.stringify(r.stderr.slice(0, 160))}`;
1224
+
1225
+ // Axis 1 — every command run.sh has, asked `--help`, one run each, in order.
1226
+ const t0 = Date.now();
1227
+ const bad = { described: [] as string[], undescribed: [] as string[], vendor: [] as string[] };
1228
+ const count = { described: 0, undescribed: 0, vendor: 0 };
1229
+ for (const name of [...inventory].sort()) {
1230
+ const argv = [name, "--help"];
1231
+ const r = run(argv);
1232
+ if (VENDORS.includes(name)) {
1233
+ count.vendor++;
1234
+ if (!dispatched(r)) bad.vendor.push(show(argv, r));
1235
+ } else if (entryOf.has(name)) {
1236
+ count.described++;
1237
+ if (!answers(name)(r)) bad.described.push(show(argv, r));
1238
+ } else {
1239
+ count.undescribed++;
1240
+ if (!(refused(r) && r.stderr.includes(JSON.stringify(name)))) bad.undescribed.push(show(argv, r));
1241
+ }
1242
+ }
1243
+ const axisSeconds = ((Date.now() - t0) / 1000).toFixed(1);
1244
+ ok(
1245
+ `[QK:CLI-HELP-COMMAND] S9l every described command answers \`<command> --help\` with its own page on stdout, exit 0, nothing on stderr, and no arm entered (${count.described} commands, axis ${axisSeconds}s)`,
1246
+ count.described > 0 && bad.described.length === 0,
1247
+ bad.described.join("\n"),
1248
+ );
1249
+ ok(
1250
+ `[QK:CLI-HELP-NO-DISPATCH] S9m every undescribed command asked \`--help\` is refused (one stderr line naming it, exit 2) and never run (${count.undescribed} commands)`,
1251
+ count.undescribed > 0 && bad.undescribed.length === 0,
1252
+ bad.undescribed.join("\n"),
1253
+ );
1254
+
1255
+ // Axis 2 — the other shapes, on a few representative names.
1256
+ const debtGate = [...DESCRIPTION_DEBT].find((n) => defined.names.has(n) && !VENDORS.includes(n)) as string;
1257
+ const shapes = (cells: [string[], (r: R) => boolean][]): string[] =>
1258
+ cells.flatMap(([argv, judge]) => {
1259
+ const r = run(argv);
1260
+ return judge(r) ? [] : [show(argv, r)];
1261
+ });
1262
+ const formsBad = shapes([
1263
+ [["peer-facts", "-h"], answers("peer-facts")],
1264
+ [["help", "peer-facts"], answers("peer-facts")],
1265
+ [["help", "meta-bridge-fresh-cut"], answers("meta-bridge-fresh-cut")],
1266
+ [["help", "copilot"], answers("copilot")],
1267
+ [["help", debtGate], refused],
1268
+ [["help", "pi"], answers("pi")],
1269
+ [["help", "claude"], answers("claude")],
1270
+ [["help", "no-such-command"], refused],
1271
+ [["no-such-command", "--help"], refused],
1272
+ ]);
1273
+ ok(
1274
+ "[QK:CLI-HELP-FORMS] S9n `help <command>` and `<command> -h` answer like `--help`; a vendor launcher's own entry answers `help`; an undescribed or unknown name is refused",
1275
+ formsBad.length === 0,
1276
+ formsBad.join("\n"),
1277
+ );
1278
+ // #157 — a guide is not a command. Its name is no case label, gate function or described entry;
1279
+ // asked the way one asks about something to run (`<topic> --help` / `-h`) it is refused like any
1280
+ // undescribed name, one stderr line naming it, exit 2, nothing run; `help <topic>` still answers.
1281
+ const named = (name: string) => (r: R) => refused(r) && r.stderr.includes(JSON.stringify(name));
1282
+ const notCommandBad = [
1283
+ ...TOPIC_FIXTURES.filter((t) => inventory.has(t.name) || entryOf.has(t.name)).map(
1284
+ (t) => `${t.name} is also a command`,
1285
+ ),
1286
+ ...shapes([
1287
+ [["team", "--help"], named("team")],
1288
+ [["team", "-h"], named("team")],
1289
+ [["remote-pair", "--help"], named("remote-pair")],
1290
+ [["help", "team"], (r) => r.status === 0 && r.stderr === "" && r.stdout === topicPage.get("team")],
1291
+ ]),
1292
+ ];
1293
+ ok(
1294
+ "[QK:CLI-HELP-TOPIC-NOT-COMMAND] S9n' a guide topic is not a command: its name is no dispatch label, gate or described entry, and `<topic> --help` / `-h` is refused like an undescribed name while `help <topic>` answers",
1295
+ notCommandBad.length === 0,
1296
+ notCommandBad.join("\n"),
1297
+ );
1298
+ const extraBad = shapes([
1299
+ [["peer-facts", "--help", "x"], refused],
1300
+ [["peer-facts", "-h", "x"], refused],
1301
+ [["help", "peer-facts", "x"], refused],
1302
+ [["help", "copilot", "x"], refused],
1303
+ [["help", "pi", "x"], refused],
1304
+ [["help", "all", "x"], refused],
1305
+ [["help", "team", "x"], refused],
1306
+ [[debtGate, "--help", "x"], refused],
1307
+ ]);
1308
+ ok(
1309
+ "[QK:CLI-HELP-EXTRA-REFUSED] S9o a help form with more argv after it is refused, exit 2, and runs nothing",
1310
+ extraBad.length === 0,
1311
+ extraBad.join("\n"),
1312
+ );
1313
+ const aliasBad = shapes([
1314
+ [["--help", "peer-facts"], refused],
1315
+ [["-h", "peer-facts"], refused],
1316
+ ]);
1317
+ ok(
1318
+ "[QK:CLI-HELP-ALIAS-REFUSED] S9p `--help <command>` / `-h <command>` are refused, not read as `help <command>`",
1319
+ aliasBad.length === 0,
1320
+ aliasBad.join("\n"),
1321
+ );
1322
+ const vendorBad = [
1323
+ ...bad.vendor,
1324
+ ...shapes([
1325
+ [["copilot", "--help", "x"], dispatched],
1326
+ [["pi", "-h", "x"], dispatched],
1327
+ ]),
1328
+ ];
1329
+ ok(
1330
+ `[QK:CLI-HELP-VENDOR-PASSTHROUGH] S9q the five vendor launchers keep every argv, help flags included: each reaches its arm (${count.vendor} of ${VENDORS.length})`,
1331
+ count.vendor === VENDORS.length && vendorBad.length === 0,
1332
+ vendorBad.join("\n"),
1333
+ );
1334
+ const positionBad = shapes([
1335
+ [["peer-facts", "x", "--help"], dispatched],
1336
+ [["peer-facts", "--", "--help"], dispatched],
1337
+ ]);
1338
+ ok(
1339
+ "[QK:CLI-HELP-NOT-HELP-POSITION] S9r a help flag anywhere but right after the command, or after `--`, is not a help request: it reaches the dispatch as before",
1340
+ positionBad.length === 0,
1341
+ positionBad.join("\n"),
1342
+ );
1343
+ const odd = "bad\nname\u0007";
1344
+ const oddRun = run(["help", odd]);
1345
+ ok(
1346
+ "[QK:CLI-HELP-ONE-LINE] S9s an unknown name with a newline and a control character is quoted onto one stderr line",
1347
+ refused(oddRun) && oddRun.stderr.includes(JSON.stringify(odd)),
1348
+ show(["help", odd], oddRun),
1349
+ );
1350
+
1351
+ // What a command help starts, among the watched PATH tools: node on the renderer, asked for that
1352
+ // one command, and nothing else. #157 amendment: the `help` form asks with `--name`, the mode
1353
+ // that may also answer with a guide topic; the `--help` form keeps `--command`, which may not.
1354
+ const { bin, trace } = makeShims(gated, `exec '${process.execPath}' "$@"`);
1355
+ for (const argv of [
1356
+ ["peer-facts", "--help"],
1357
+ ["help", "peer-facts"],
1358
+ ])
1359
+ run(argv, { PATH: `${bin}:${helpEnv.PATH}` });
1360
+ const traced = readFileSync(trace, "utf8");
1361
+ ok(
1362
+ "[QK:CLI-HELP-COMMAND-TRACE] S9t observed on peer-facts in both forms: of the watched PATH tools, only node on scripts/cli-help.mjs starts — `--command peer-facts` for `--help`, `--name peer-facts` for `help`",
1363
+ traced ===
1364
+ `node ${path.join(gated, CLI_RENDERER)} --command peer-facts\nnode ${path.join(gated, CLI_RENDERER)} --name peer-facts\n`,
1365
+ `trace:\n${traced}`,
1366
+ );
1367
+ } finally {
1368
+ rmSync(gated, { recursive: true, force: true });
1369
+ }
1370
+
1371
+ // fresh-cut's longer text: kept whole as the ce92dc7 source literal of its usage() (the template
1372
+ // slots filled with the constants it named) — read from the source, not captured from a run. The
1373
+ // details carry it with one explicit amendment: the two lines that named the old `node
1374
+ // --experimental-strip-types` invocation and the blank line after them, which the synopsis
1375
+ // `entwurf meta-bridge-fresh-cut` replaces. Nothing else is trimmed. The exit numbers are checked
1376
+ // against the constant the verb itself returns, here in the gate only.
1377
+ const FC_FIXTURE = "test/fixtures/cli-help/fresh-cut-usage-ce92dc7.source-literal.txt";
1378
+ const FC_SHA256 = "f31c766a64fc99705ce81cc413bc4eadc40edd8e031127551d92fa68a8ac35a2";
1379
+ const fcText = readFileSync(path.join(REPO, FC_FIXTURE), "utf8");
1380
+ if (sha256(fcText) !== FC_SHA256) throw new Error(`S9: ${FC_FIXTURE} is not the ce92dc7 source literal`);
1381
+ const fcLines = fcText.slice(0, -1).split("\n"); // the file's final LF is framing, not a line
1382
+ const invocationDropped =
1383
+ fcLines[0]?.startsWith("usage: node --experimental-strip-types ") === true &&
1384
+ fcLines[1]?.trimStart().startsWith("(dev clone:") === true &&
1385
+ fcLines[2] === "";
1386
+ const fcDetails = entryOf.get("meta-bridge-fresh-cut")?.details ?? [];
1387
+ const exitMarks: [keyof typeof FRESH_CUT_EXIT, string][] = [
1388
+ ["COMPLETE", "cut complete:"],
1389
+ ["NO_MOVE", "NOTHING MOVED:"],
1390
+ ["USAGE", "usage."],
1391
+ ["HALF_CUT", "CUT TRANSITION INCOMPLETE:"],
1392
+ ["CLEANUP_INCOMPLETE", "CUT COMPLETE, CLEANUP INCOMPLETE:"],
1393
+ ];
1394
+ const exitsAgree = exitMarks.every(
1395
+ ([k, mark]) => fcDetails.filter((l) => l.startsWith(` ${FRESH_CUT_EXIT[k]} ${mark}`)).length === 1,
1396
+ );
1397
+ ok(
1398
+ "[QK:CLI-HELP-DETAILS-ORACLE] S9u fresh-cut's details are its ce92dc7 usage text minus only the old invocation lines, and every exit number is the one FRESH_CUT_EXIT returns",
1399
+ invocationDropped && JSON.stringify(fcDetails) === JSON.stringify(fcLines.slice(3)) && exitsAgree,
1400
+ `details=${fcDetails.length} lines, oracle=${fcLines.length - 3} lines, exits agree=${exitsAgree}`,
1401
+ );
1402
+
1403
+ // A reader that stops early (`entwurf --help | head`) closes the pipe. Adopted as a UX amendment:
1404
+ // that ends the page quietly with exit 0 — a stated choice, not a claim about what the old `cat`
1405
+ // returned. pipefail is on and both sides' statuses are read back, so the producer's 0 is seen,
1406
+ // not hidden behind head's. Both pages are read: the short root page can fit the pipe buffer
1407
+ // before the reader closes, so the long catalog (`help all`) is where an early close really bites.
1408
+ const runSh = path.join(REPO, "run.sh");
1409
+ const piped = spawnSync(
1410
+ "bash",
1411
+ [
1412
+ "-c",
1413
+ `set -o pipefail; bash '${runSh}' --help | head -c 1 >/dev/null; root="\${PIPESTATUS[0]} \${PIPESTATUS[1]}"; ` +
1414
+ `bash '${runSh}' help all | head -c 1 >/dev/null; all="\${PIPESTATUS[0]} \${PIPESTATUS[1]}"; ` +
1415
+ `bash '${runSh}' help team | head -c 1 >/dev/null; echo "$root $all \${PIPESTATUS[0]} \${PIPESTATUS[1]}"`,
1416
+ ],
1417
+ { cwd: REPO, encoding: "utf8", env: helpEnv },
1418
+ );
1419
+ ok(
1420
+ "[QK:CLI-HELP-EPIPE-QUIET] S9v `entwurf --help | head`, `entwurf help all | head` and `entwurf help team | head` end without an error on stderr",
1421
+ piped.stderr === "",
1422
+ `stderr=${piped.stderr.slice(0, 200)}`,
1423
+ );
1424
+ ok(
1425
+ "[QK:CLI-HELP-EPIPE-EXIT] S9v' under pipefail, the help producer and head both end 0 when the reader stops early, on the root page, the catalog and a guide topic",
1426
+ piped.status === 0 && piped.stdout === "0 0 0 0 0 0\n",
1427
+ `rc=${piped.status} statuses=${JSON.stringify(piped.stdout)}`,
1428
+ );
1429
+ }
1430
+
201
1431
  // ─── S3 — no npm bin can reach a raw .ts when installed ──────────────────────────────
202
1432
  {
203
1433
  // Two ways a bin dies under node_modules: it IS a .ts, or it is a wrapper that execs one