@junghanacs/entwurf 0.22.0 → 0.23.1

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 (135) hide show
  1. package/AGENTS.md +4 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +514 -0
  4. package/DELIVERY.md +10 -2
  5. package/README.md +43 -4
  6. package/VERIFY.md +47 -0
  7. package/docs/acp-backend-rail.md +96 -17
  8. package/docs/setup-clean-host.md +11 -1
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +41 -41
  29. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  30. package/package.json +9 -8
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  33. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  34. package/pi-extensions/lib/acp/backend.ts +4 -4
  35. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  36. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  37. package/pi-extensions/lib/codex-declaration.js +612 -0
  38. package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
  39. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  40. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  41. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  42. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  43. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  44. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  45. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  46. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  47. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  48. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  49. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  50. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  51. package/pi-extensions/lib/herdr-placement.ts +276 -0
  52. package/pi-extensions/lib/mux-fresh-call.ts +95 -301
  53. package/run.sh +588 -204
  54. package/scripts/check-acp-usage-accounting.ts +9 -9
  55. package/scripts/check-codex-birth-hook.ts +187 -0
  56. package/scripts/check-entwurf-control-rpc.ts +138 -0
  57. package/scripts/check-entwurf-fact-provider.ts +4 -2
  58. package/scripts/check-entwurf-peers-surface.ts +3 -1
  59. package/scripts/check-entwurf-self-address.ts +68 -0
  60. package/scripts/check-fresh-call-dispatch.ts +301 -0
  61. package/scripts/check-gate-qualification.ts +17 -4
  62. package/scripts/check-herdr-activation.ts +1293 -0
  63. package/scripts/check-herdr-fresh-call.ts +1250 -0
  64. package/scripts/check-herdr-placement.ts +383 -0
  65. package/scripts/check-herdr-plugin-build.ts +842 -0
  66. package/scripts/check-herdr-plugin-profile.ts +371 -0
  67. package/scripts/check-herdr-plugin.ts +523 -0
  68. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  69. package/scripts/check-herdr-sandbox.ts +456 -0
  70. package/scripts/check-herdr-supply.ts +184 -0
  71. package/scripts/check-mux-launcher-fence.ts +138 -0
  72. package/scripts/check-peer-facts.ts +331 -0
  73. package/scripts/check-pi-launch.ts +358 -0
  74. package/scripts/check-release-gate-outcomes.ts +19 -0
  75. package/scripts/check-typing-call-fence.ts +296 -0
  76. package/scripts/codex-birth-doctor.sh +159 -16
  77. package/scripts/codex-birth-install.sh +178 -72
  78. package/scripts/codex-birth-uninstall.sh +162 -7
  79. package/scripts/fixtures/herdr-supply.json +29 -0
  80. package/scripts/herdr-activation.mjs +536 -0
  81. package/scripts/herdr-plugin-activate.mjs +270 -0
  82. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  83. package/scripts/herdr-runtime.mjs +1203 -0
  84. package/scripts/install-herdr-ci.sh +96 -0
  85. package/scripts/inventory-verification-surface.ts +86 -10
  86. package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
  87. package/scripts/lib/tree-digest.ts +97 -0
  88. package/scripts/meta-bridge-install.sh +19 -2
  89. package/scripts/meta-bridge-state.py +167 -11
  90. package/scripts/mutants/codex-caller-seat.json +6 -8
  91. package/scripts/mutants/codex-native.json +195 -29
  92. package/scripts/mutants/control-socket-disconnect.json +57 -0
  93. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  94. package/scripts/mutants/herdr-activation.json +305 -0
  95. package/scripts/mutants/herdr-fresh-call.json +448 -0
  96. package/scripts/mutants/herdr-placement.json +161 -0
  97. package/scripts/mutants/herdr-plugin-build.json +160 -0
  98. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  99. package/scripts/mutants/herdr-plugin.json +133 -0
  100. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  101. package/scripts/mutants/herdr-supply.json +133 -0
  102. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  103. package/scripts/mutants/mux-fresh-call.json +104 -17
  104. package/scripts/mutants/mux-launcher-fence.json +13 -0
  105. package/scripts/mutants/omp-fresh.json +4 -4
  106. package/scripts/mutants/peer-facts.json +98 -0
  107. package/scripts/mutants/pi-launch.json +113 -0
  108. package/scripts/mutants/resume-args.json +6 -6
  109. package/scripts/mutants/self-address.json +33 -0
  110. package/scripts/mutants/typing-call-fence.json +17 -0
  111. package/scripts/mutants/v2-surface.json +2 -2
  112. package/scripts/mutants/v2-visible-resume.json +17 -17
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  117. package/scripts/smoke-codex-birth.sh +281 -12
  118. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  119. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  120. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  121. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  122. package/scripts/tsconfig.json +5 -0
  123. package/scripts/check-entwurf-facts.ts +0 -385
  124. package/scripts/check-entwurf-resume-args.ts +0 -122
  125. package/scripts/check-entwurf-v2-decider.ts +0 -657
  126. package/scripts/check-entwurf-v2-lock.ts +0 -371
  127. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  128. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  129. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  130. package/scripts/check-entwurf-v2-release.ts +0 -151
  131. package/scripts/check-entwurf-v2-runner.ts +0 -448
  132. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  133. package/scripts/check-entwurf-v2-send.ts +0 -458
  134. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  135. package/scripts/check-socket-discovery.ts +0 -401
@@ -0,0 +1,358 @@
1
+ /**
2
+ * check-pi-launch — deterministic gate for the MANAGED pi launch (`entwurf pi`, #118 홉 2).
3
+ * Hermetic: no real pi, no network, no model turn, no write outside its own temp root.
4
+ *
5
+ * SAME ORACLE AS check-copilot-launch, and deliberately the same one: the subject is a
6
+ * process REPLACEMENT, so the truth is a FAKE VENDOR — a real executable named `pi` on a
7
+ * sandbox PATH that reports the argv, environment and pid it was actually handed. Nothing
8
+ * below is asserted by reading the launcher's source, and the launcher is driven through
9
+ * its PUBLIC address (`run.sh pi`) because the dispatcher's own argv handling is part of
10
+ * the contract. Reusing that harness shape rather than importing it is the honest split:
11
+ * the two launchers share a posture, not a body, and a shared fixture would have to grow a
12
+ * vendor-name parameter that neither production path has.
13
+ *
14
+ * WHY THE PRECONDITION SET IS EMPTY, measured rather than assumed. `entwurf copilot` checks
15
+ * six receiver-state facts before it execs, because setting its flag is a PROMISE of a
16
+ * doorbell that may not exist. `entwurf pi` promises nothing pi does not already own: on a
17
+ * host where the entwurf extension is not registered, `pi --entwurf-control` refuses itself
18
+ * with `Error: Unknown option: --entwurf-control` and exit 1 (measured, pi 0.85.1). A
19
+ * pre-check here would duplicate that refusal and go stale the day pi renames it.
20
+ *
21
+ * WHY ONE RECURSION FENCE AND NOT TWO. The sentinel closes the only real loop — a PATH
22
+ * executable named `pi` that shells back to `entwurf pi`. Copilot's second fence (resolve
23
+ * the binary, refuse our own entrypoints) closes a case that is NOT a loop here, and the
24
+ * cell below measures that rather than asserting it: a PATH `pi` symlinked to run.sh is
25
+ * exec'd as `run.sh --entwurf-control …`, an unknown verb this dispatcher already refuses.
26
+ *
27
+ * WHY THE FLAG IS ADDED AND NEVER DEDUPLICATED. `pit`/`pius`-style operator wrappers already
28
+ * pass `--entwurf-control`; passing it twice was measured byte-identical to passing it once
29
+ * (pi 0.85.1, extension registered). The launcher therefore injects exactly ONE and leaves
30
+ * the operator's copies alone — an argv scan would be code earning nothing.
31
+ */
32
+
33
+ import assert from "node:assert/strict";
34
+ import { execFileSync, spawnSync } from "node:child_process";
35
+ import {
36
+ chmodSync,
37
+ existsSync,
38
+ mkdirSync,
39
+ mkdtempSync,
40
+ readFileSync,
41
+ rmSync,
42
+ symlinkSync,
43
+ writeFileSync,
44
+ } from "node:fs";
45
+ import { tmpdir } from "node:os";
46
+ import * as path from "node:path";
47
+ import { fileURLToPath } from "node:url";
48
+
49
+ const REPO = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
50
+ const RUN_SH = path.join(REPO, "run.sh");
51
+
52
+ let passed = 0;
53
+ function ok(label: string, cond: boolean, detail = ""): void {
54
+ assert.ok(cond, detail ? `${label}\n${detail}` : label);
55
+ console.log(` ok ${label}`);
56
+ passed++;
57
+ }
58
+
59
+ // ── the flag SSOT, read as text ──────────────────────────────────────────────
60
+ // The shell cannot import a TypeScript constant, so the launcher spells the flag as a
61
+ // literal and this gate is what keeps the two in step. The SSOT is the constant; every
62
+ // argv-composing site below must carry exactly its value.
63
+ const storeSrc = readFileSync(path.join(REPO, "pi-extensions", "lib", "acp", "session-store.ts"), "utf8");
64
+ const flagMatch = /export const ENTWURF_CONTROL_FLAG = "([^"]+)";/.exec(storeSrc);
65
+ assert.ok(flagMatch, "ENTWURF_CONTROL_FLAG is not declared in pi-extensions/lib/acp/session-store.ts");
66
+ const FLAG = flagMatch[1];
67
+
68
+ const root = mkdtempSync(path.join(tmpdir(), "entwurf-pi-launch."));
69
+ try {
70
+ const bin = path.join(root, "bin");
71
+ mkdirSync(bin, { recursive: true });
72
+
73
+ // The fake vendor, installed under the REAL name `pi`. There is no "which command
74
+ // stands in for the vendor" switch: a production env seam that can redirect an exec is
75
+ // an authority, not a test convenience.
76
+ const vendor = path.join(bin, "pi");
77
+ writeFileSync(
78
+ vendor,
79
+ `#!/usr/bin/env bash
80
+ echo "PID=$$"
81
+ echo "SENTINEL=[\${ENTWURF_PI_LAUNCH_ACTIVE-<unset>}]"
82
+ for a in "$@"; do printf 'ARG<%s>\\n' "$a"; done
83
+ exit "\${FAKE_PI_EXIT:-0}"
84
+ `,
85
+ );
86
+ chmodSync(vendor, 0o755);
87
+
88
+ // A PATH with no `pi` anywhere on it — built by dropping every real entry that holds
89
+ // one, rather than by emptying PATH (the launcher still needs bash and friends).
90
+ // The sandbox is TOTAL, the same way check-copilot-launch's is. The launcher writes
91
+ // nothing itself, but it EXECS a vendor, and a fixture that leaves the operator's HOME
92
+ // and XDG roots reachable is one careless vendor away from touching them. Naming every
93
+ // root explicitly is also what #119 asks of each test rather than of each gate.
94
+ const sandboxEnv = {
95
+ HOME: path.join(root, "home"),
96
+ XDG_DATA_HOME: path.join(root, "xdg", "data"),
97
+ XDG_CONFIG_HOME: path.join(root, "xdg", "config"),
98
+ XDG_STATE_HOME: path.join(root, "xdg", "state"),
99
+ XDG_CACHE_HOME: path.join(root, "xdg", "cache"),
100
+ PI_CODING_AGENT_DIR: path.join(root, "pi-agent"),
101
+ };
102
+ for (const d of Object.values(sandboxEnv)) mkdirSync(d, { recursive: true });
103
+
104
+ const pathWithoutVendor = (process.env.PATH ?? "")
105
+ .split(":")
106
+ .filter((d) => d !== "" && !existsSync(path.join(d, "pi")))
107
+ .join(":");
108
+
109
+ interface Run {
110
+ status: number | null;
111
+ out: string;
112
+ args: string[];
113
+ sentinel: string;
114
+ pid: string;
115
+ }
116
+ function launch(args: string[], extraEnv: Record<string, string> = {}, withVendor = true): Run {
117
+ const r = spawnSync("bash", [RUN_SH, "pi", ...args], {
118
+ cwd: root,
119
+ encoding: "utf8",
120
+ env: {
121
+ ...process.env,
122
+ ...sandboxEnv,
123
+ PATH: withVendor ? `${bin}:${pathWithoutVendor}` : pathWithoutVendor,
124
+ ENTWURF_PI_LAUNCH_ACTIVE: undefined as unknown as string,
125
+ FAKE_PI_EXIT: undefined as unknown as string,
126
+ ...extraEnv,
127
+ },
128
+ });
129
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
130
+ const argv: string[] = [];
131
+ for (const line of out.split("\n")) {
132
+ const m = /^ARG<([\s\S]*)>$/.exec(line);
133
+ if (m) argv.push(m[1]);
134
+ }
135
+ return {
136
+ status: r.status,
137
+ out,
138
+ args: argv,
139
+ sentinel: /^SENTINEL=\[(.*)\]$/m.exec(out)?.[1] ?? "<no-launch>",
140
+ pid: /^PID=(.*)$/m.exec(out)?.[1] ?? "",
141
+ };
142
+ }
143
+
144
+ // ── 1. the flag literal agrees with its SSOT, everywhere argv is composed ──
145
+ {
146
+ // CODE only. The branch explains itself at length and quotes the flag four times in
147
+ // prose; a literal check that read those goes green on a launcher whose actual exec
148
+ // line carries a typo — measured, and it is why the comments are dropped first.
149
+ const branch = /^ {2}pi\)$[\s\S]*?^ {4};;$/m.exec(readFileSync(RUN_SH, "utf8"))?.[0] ?? "";
150
+ const launcher = branch
151
+ .split("\n")
152
+ .filter((line) => !/^\s*#/.test(line))
153
+ .join("\n");
154
+ // Deliberately narrow: this cell owns the LITERAL and nothing else. Asserting the
155
+ // whole exec line here would swallow every other claim below — a mutation of the
156
+ // argv order, the quoting or the exec itself would all die on this assertion first,
157
+ // and each mutant is supposed to name exactly one broken thing. The token boundary
158
+ // is what makes a near-miss like `--entwurf-controll` a failure rather than a
159
+ // substring hit.
160
+ ok(
161
+ "[QK:PILAUNCH-FLAG-SSOT] the run.sh launcher spells the flag exactly as ENTWURF_CONTROL_FLAG declares it — the shell cannot import the constant, so the literal is checked as a whole token",
162
+ new RegExp(`(^|\\s)${FLAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "m").test(launcher),
163
+ `FLAG=${FLAG}\n--- launcher branch, comments dropped ---\n${launcher}`,
164
+ );
165
+ // EXHAUSTIVE, not a list. The first cut named five files and claimed "every
166
+ // argv-composing site"; cross-review found eight more that the list did not know
167
+ // about. They all happened to be spelled correctly, so there was no false success —
168
+ // but a drift OUTSIDE the list would have passed silently, which is the same gate
169
+ // rot in a slower form. So the sweep walks every tracked source file and requires
170
+ // that every `--entwurf-…` token it finds IS this flag. A typo, a rename, or a
171
+ // second spelling anywhere becomes a named red, and a new call site needs no edit
172
+ // here to be covered.
173
+ //
174
+ // Prose is excluded and that exclusion is the load-bearing part: comments and
175
+ // markdown quote the flag constantly, and a checker that read them would be a spell
176
+ // checker for documentation rather than a guard on what pi actually receives. Only
177
+ // lines that can compose argv are read.
178
+ const tracked = execFileSync(
179
+ "git",
180
+ ["ls-files", "--", "scripts", "pi-extensions", "mcp", "demo", "plugins", "run.sh"],
181
+ {
182
+ cwd: REPO,
183
+ encoding: "utf8",
184
+ },
185
+ )
186
+ .split("\n")
187
+ .filter((f) => f.length > 0 && /\.(ts|mjs|js|sh)$/.test(f));
188
+
189
+ // Narrowed to the DRIFT class, and the first run is why. A pattern of `--entwurf-…`
190
+ // swept up `--entwurf-bootstrap` (scripts/check-omp-birth-hook.ts), which is a real and
191
+ // unrelated flag — this cell is not the registry of every entwurf flag. What it owns is
192
+ // that no VARIANT of the control flag exists: `--entwurf-controll`, `--entwurf-control-x`
193
+ // and friends. A wholesale rename to a different word is a different change, and the SSOT
194
+ // cell above catches it by reading the constant.
195
+ const FLAGLIKE = new RegExp(`${FLAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[A-Za-z0-9-]*`, "g");
196
+ const wrong: string[] = [];
197
+ let sites = 0;
198
+ for (const rel of tracked) {
199
+ const body = readFileSync(path.join(REPO, rel), "utf8");
200
+ body.split("\n").forEach((line, i) => {
201
+ // A whole-line comment in any of the three syntaxes these files use. An inline
202
+ // trailing comment is NOT stripped: `#` and `//` both appear inside real argv
203
+ // strings here, and guessing where a comment starts would drop live code.
204
+ if (/^\s*(\/\/|#|\*|\/\*)/.test(line)) return;
205
+ for (const m of line.matchAll(FLAGLIKE)) {
206
+ sites++;
207
+ if (m[0] !== FLAG) wrong.push(`${rel}:${i + 1}: ${m[0]}`);
208
+ }
209
+ });
210
+ }
211
+ ok(
212
+ `[QK:PILAUNCH-FLAG-SITES] every control-flag PREFIX VARIANT in tracked code is exactly the constant (${sites} occurrences swept, prose excluded)`,
213
+ wrong.length === 0,
214
+ `FLAG=${FLAG}\ndisagreeing:\n${wrong.join("\n")}`,
215
+ );
216
+ // A sweep that found nothing would also report zero disagreements, so the count is
217
+ // asserted too: the launcher alone guarantees at least one.
218
+ ok(
219
+ "[QK:PILAUNCH-FLAG-SWEEP-REACHES] the sweep actually reached code that names the flag",
220
+ sites > 1,
221
+ `sites=${sites}`,
222
+ );
223
+ }
224
+
225
+ // ── 2. the dispatcher verb must not reach the vendor ──────────────────────
226
+ {
227
+ const r = launch(["--model", "x"]);
228
+ ok(
229
+ "[QK:PILAUNCH-VERB-DROPPED] the dispatcher verb never reaches the vendor — without the shift, `pi` arrives as a prompt argument",
230
+ r.status === 0 && !r.args.includes("pi"),
231
+ `argv: ${JSON.stringify(r.args)}`,
232
+ );
233
+ ok(
234
+ "[QK:PILAUNCH-FLAG-FIRST] the injected flag is the FIRST token, so the control socket is stood up before anything the operator asked for",
235
+ r.args[0] === FLAG,
236
+ `argv: ${JSON.stringify(r.args)}`,
237
+ );
238
+ ok(
239
+ "[QK:PILAUNCH-NOTHING-ELSE-INJECTED] the launcher adds the flag and NOTHING else — operator taste is not a launcher decision (Rule 9)",
240
+ JSON.stringify(r.args) === JSON.stringify([FLAG, "--model", "x"]),
241
+ `argv: ${JSON.stringify(r.args)}`,
242
+ );
243
+ }
244
+
245
+ // ── 3. operator argv crosses byte-identical ───────────────────────────────
246
+ {
247
+ const operator = ["--model", "a b", "", "-", "--", "한글 인자", " spaced "];
248
+ const r = launch(operator);
249
+ ok(
250
+ "[QK:PILAUNCH-ARGV-BYTES] every operator element crosses byte-identical — empty strings, embedded spaces, a bare dash, the terminator and non-ASCII",
251
+ JSON.stringify(r.args) === JSON.stringify([FLAG, ...operator]),
252
+ `expected: ${JSON.stringify([FLAG, ...operator])}\nactual: ${JSON.stringify(r.args)}`,
253
+ );
254
+ }
255
+
256
+ // ── 4. the flag is ADDED, never deduplicated ──────────────────────────────
257
+ {
258
+ const r = launch([FLAG, "--model", "x"]);
259
+ ok(
260
+ "[QK:PILAUNCH-FLAG-NOT-DEDUPED] an operator who already passes the flag gets it twice, because pi was measured to eat the duplicate and a scan would earn nothing",
261
+ JSON.stringify(r.args) === JSON.stringify([FLAG, FLAG, "--model", "x"]),
262
+ `argv: ${JSON.stringify(r.args)}`,
263
+ );
264
+ }
265
+
266
+ // ── 5. the vendor's exit status is this command's exit status ─────────────
267
+ // THIS CELL CARRIES NO MUTANT, and the reason is measured rather than conceded: run.sh
268
+ // is `set -euo pipefail` (run.sh:14), so every mutation that stops the vendor status
269
+ // from reaching the caller also stops `exec` from replacing the process. The non-exec
270
+ // form still exits 7, because set -e aborts before a planted `exit 0` can run. A mutant
271
+ // that kills two claims is not an attributable mutant, so this stands on its assertion
272
+ // alone until that coupling changes.
273
+ // Asserted BEFORE the pid cell on purpose: dropping `exec` for a plain call still
274
+ // propagates the status, while running the vendor and then exiting 0 breaks both. With
275
+ // the status checked first each of those two mutations lands on exactly one claim.
276
+ {
277
+ const r = launch([], { FAKE_PI_EXIT: "7" });
278
+ ok(
279
+ "[QK:PILAUNCH-EXIT-PASSTHROUGH] the vendor's exit status is the launch's exit status",
280
+ r.status === 7,
281
+ `status=${r.status}`,
282
+ );
283
+ }
284
+
285
+ // ── 6. exec, not fork: the pid survives ───────────────────────────────────
286
+ {
287
+ const r = spawnSync("bash", ["-c", `echo "SHELL_PID=$$"; exec bash "${RUN_SH}" pi`], {
288
+ cwd: root,
289
+ encoding: "utf8",
290
+ env: {
291
+ ...process.env,
292
+ ...sandboxEnv,
293
+ PATH: `${bin}:${pathWithoutVendor}`,
294
+ ENTWURF_PI_LAUNCH_ACTIVE: undefined as unknown as string,
295
+ },
296
+ });
297
+ const shellPid = /^SHELL_PID=(\d+)$/m.exec(r.stdout ?? "")?.[1];
298
+ const vendorPid = /^PID=(\d+)$/m.exec(r.stdout ?? "")?.[1];
299
+ ok(
300
+ "[QK:PILAUNCH-EXEC-KEEPS-PID] the vendor REPLACES this process — a fork would leave run.sh as a parent that supervises nothing",
301
+ Boolean(shellPid) && shellPid === vendorPid,
302
+ `shell=${shellPid} vendor=${vendorPid}`,
303
+ );
304
+ }
305
+
306
+ // ── 7. an absent vendor is a NAMED refusal, never a quiet success ─────────
307
+ {
308
+ const r = launch([], {}, false);
309
+ ok(
310
+ "[QK:PILAUNCH-VENDOR-ABSENT] with no `pi` on PATH the launch refuses by name and non-zero, and never reports success",
311
+ r.status !== 0 && r.out.includes("no 'pi' executable found on PATH") && r.pid === "",
312
+ `status=${r.status}\n${r.out}`,
313
+ );
314
+ }
315
+
316
+ // ── 8. the recursion fence, and the sentinel that carries it ──────────────
317
+ {
318
+ const armed = launch([]);
319
+ ok(
320
+ "[QK:PILAUNCH-SENTINEL-EXPORTED] the sentinel is EXPORTED, so a PATH `pi` that shells back here sees it on re-entry",
321
+ armed.sentinel === "1",
322
+ `sentinel=${armed.sentinel}`,
323
+ );
324
+ const r = launch([], { ENTWURF_PI_LAUNCH_ACTIVE: "1" });
325
+ ok(
326
+ "[QK:PILAUNCH-RECURSION-FENCE] a re-entered launch refuses by name instead of spinning forever behind a hung terminal",
327
+ r.status !== 0 && r.out.includes("recursive managed launch detected") && r.pid === "",
328
+ `status=${r.status}\n${r.out}`,
329
+ );
330
+ }
331
+
332
+ // ── 9. the case copilot's SECOND fence covers, measured here instead ───────
333
+ {
334
+ const loopBin = path.join(root, "loopbin");
335
+ mkdirSync(loopBin, { recursive: true });
336
+ symlinkSync(RUN_SH, path.join(loopBin, "pi"));
337
+ const r = spawnSync("bash", [RUN_SH, "pi"], {
338
+ cwd: root,
339
+ encoding: "utf8",
340
+ timeout: 30_000,
341
+ env: {
342
+ ...process.env,
343
+ ...sandboxEnv,
344
+ PATH: `${loopBin}:${pathWithoutVendor}`,
345
+ ENTWURF_PI_LAUNCH_ACTIVE: undefined as unknown as string,
346
+ },
347
+ });
348
+ ok(
349
+ "[QK:PILAUNCH-SYMLINK-NOT-A-LOOP] a PATH `pi` symlinked to run.sh is exec'd as an unknown verb and fails closed — this is why the second fence copilot carries is not needed here",
350
+ r.status !== 0 && r.signal === null,
351
+ `status=${r.status} signal=${r.signal}`,
352
+ );
353
+ }
354
+
355
+ console.log(`\ncheck-pi-launch: ${passed} checks passed`);
356
+ } finally {
357
+ rmSync(root, { recursive: true, force: true });
358
+ }
@@ -304,6 +304,25 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
304
304
  // NOT waived: its direct call stays required for a Cortex-rail cut, and running
305
305
  // it without the connection still reports protocol SKIP rather than a pass.
306
306
  "smoke-acp-cortex-live": ["VERIFY.md", "The release aggregate does not re-certify Cortex"],
307
+ // #116 C4: first evidence precedes promotion. The herdr rail's LIVE acceptance exists and
308
+ // runs on demand, but a rail whose first real acceptance run is days old has not earned a
309
+ // place in the aggregate that blocks every cut — and herdr is an OPTIONAL rail, so a cut
310
+ // on a host without it would block for a capability the product does not require.
311
+ "smoke-herdr-fresh-call-live": ["VERIFY.md", "The release aggregate does not yet require the herdr rail"],
312
+ // #116 M3-b3, same rail and the same reason: the plugin build smoke drives a REAL
313
+ // `herdr plugin install` over the network. herdr is optional, so an aggregate that required it
314
+ // would block a cut on a host that does not have the rail at all. Its first LIVE acceptance
315
+ // landed 2026-09-16 (5 assertions, 0 skipped) — one run is evidence the axis works, not the run
316
+ // history that earns a place in the gate blocking every cut. On-demand, with the exclusion
317
+ // stated where an operator reads it.
318
+ "smoke-herdr-plugin-build-live": ["VERIFY.md", "The release aggregate does not require the plugin build smoke"],
319
+ // #118 홉 1, third on the same rail and for the same two reasons: herdr is OPTIONAL, so an
320
+ // aggregate that required this would block a cut on a host that does not have the rail at
321
+ // all; and first evidence precedes promotion — this smoke's acceptance runs are hours old,
322
+ // which is evidence the axis works rather than the run history that earns a place in the
323
+ // gate blocking every cut. It also needs a Docker daemon and the public network, neither of
324
+ // which the aggregate owns. On-demand, with the exclusion stated where an operator reads it.
325
+ "smoke-herdr-raw-install-live": ["VERIFY.md", "The plugin's FIRST USER PATH is its own on-demand axis"],
307
326
  };
308
327
 
309
328
  const allLive = globSync("scripts/smoke-*live*.{ts,sh}", { cwd: REPO_DIR })