@junghanacs/entwurf 0.13.0 → 0.14.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 (193) hide show
  1. package/AGENTS.md +52 -17
  2. package/BASELINE.md +60 -206
  3. package/CHANGELOG.md +64 -0
  4. package/CONTRIBUTING.md +14 -10
  5. package/DELIVERY.md +118 -261
  6. package/README.md +91 -431
  7. package/VERIFY.md +74 -95
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +236 -0
  12. package/docs/external-mcp-host.md +131 -0
  13. package/docs/fresh-cut-policy.md +99 -0
  14. package/docs/setup-clean-host.md +124 -328
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  38. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  39. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  40. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  41. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  42. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  43. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  44. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  45. package/mcp/entwurf-bridge/src/index.ts +176 -67
  46. package/mcp/entwurf-bridge/start.sh +2 -2
  47. package/mcp/entwurf-bridge/test.sh +23 -9
  48. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  49. package/package.json +25 -11
  50. package/pi-extensions/entwurf-control.ts +220 -71
  51. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  52. package/pi-extensions/lib/acp/augment.ts +42 -3
  53. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  54. package/pi-extensions/lib/acp/backend.ts +366 -38
  55. package/pi-extensions/lib/acp/config.ts +1 -1
  56. package/pi-extensions/lib/acp/engraving.ts +43 -1
  57. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  58. package/pi-extensions/lib/acp/models.ts +4 -4
  59. package/pi-extensions/lib/acp/overlay.ts +11 -9
  60. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  61. package/pi-extensions/lib/entwurf-core.ts +15 -15
  62. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  63. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  64. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  65. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  66. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  67. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  68. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  69. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  70. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  71. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  72. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  73. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  74. package/pi-extensions/lib/meta-session.ts +93 -5
  75. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  76. package/pi-extensions/lib/mux-launch.ts +267 -0
  77. package/pi-extensions/lib/mux-placement.ts +387 -0
  78. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  79. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  80. package/pi-extensions/lib/session-id.js +8 -5
  81. package/pi-extensions/lib/socket-discovery.ts +3 -3
  82. package/prompts/engraving.md +13 -5
  83. package/run.sh +688 -282
  84. package/scripts/agy-bridge-config.py +5 -1
  85. package/scripts/check-acp-backend-preflight.ts +1 -1
  86. package/scripts/check-acp-carrier-augment.ts +246 -10
  87. package/scripts/check-acp-cortex.ts +5 -5
  88. package/scripts/check-acp-overlay.ts +13 -3
  89. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  90. package/scripts/check-acp-stop-reason.ts +342 -0
  91. package/scripts/check-acp-stream-hooks.ts +504 -0
  92. package/scripts/check-elapsed.sh +25 -0
  93. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  94. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  95. package/scripts/check-entwurf-control-rpc.ts +4 -3
  96. package/scripts/check-entwurf-resume-args.ts +72 -70
  97. package/scripts/check-entwurf-session-identity.ts +14 -10
  98. package/scripts/check-entwurf-v2-contract.ts +34 -59
  99. package/scripts/check-entwurf-v2-decider.ts +17 -177
  100. package/scripts/check-entwurf-v2-lock.ts +5 -2
  101. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  102. package/scripts/check-entwurf-v2-production.ts +2 -91
  103. package/scripts/check-entwurf-v2-release.ts +10 -105
  104. package/scripts/check-entwurf-v2-runner.ts +4 -85
  105. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  106. package/scripts/check-entwurf-v2-send.ts +0 -28
  107. package/scripts/check-entwurf-v2-surface.ts +198 -128
  108. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  109. package/scripts/check-fresh-cut-gate.sh +1 -1
  110. package/scripts/check-gate-qualification.ts +101 -7
  111. package/scripts/check-install-container.sh +10 -2
  112. package/scripts/check-install-surface.ts +1 -1
  113. package/scripts/check-keyset-overlap.py +1 -1
  114. package/scripts/check-meta-facts.ts +249 -0
  115. package/scripts/check-meta-identity-consumers.ts +1 -1
  116. package/scripts/check-meta-session.ts +169 -0
  117. package/scripts/check-mux-launch-tmux.ts +316 -0
  118. package/scripts/check-mux-launch.ts +288 -0
  119. package/scripts/check-mux-launcher-fence.ts +264 -0
  120. package/scripts/check-mux-parent-artifact.ts +195 -0
  121. package/scripts/check-mux-placement-tmux.ts +322 -0
  122. package/scripts/check-mux-placement.ts +323 -0
  123. package/scripts/check-mux-resume-call.ts +282 -0
  124. package/scripts/check-probe-cli-shim.ts +25 -22
  125. package/scripts/check-probe-ordering.ts +121 -81
  126. package/scripts/check-release-gate-outcomes.ts +452 -0
  127. package/scripts/check-resume-launch-identity.ts +244 -0
  128. package/scripts/check-socket-discovery.ts +1 -1
  129. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  130. package/scripts/inventory-verification-surface.ts +349 -0
  131. package/scripts/lib/claude-launcher-fence.ts +322 -0
  132. package/scripts/lib/live-skip.ts +33 -0
  133. package/scripts/lib/mutation-qualify.ts +109 -3
  134. package/scripts/lib/probe-acp-turn.ts +33 -12
  135. package/scripts/lib/step-outcome.sh +88 -0
  136. package/scripts/meta-bridge-doctor.sh +6 -8
  137. package/scripts/meta-facts.ts +60 -0
  138. package/scripts/mutants/acp-augment.json +106 -0
  139. package/scripts/mutants/acp-cortex.json +2 -2
  140. package/scripts/mutants/acp-overlay.json +17 -0
  141. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  142. package/scripts/mutants/acp-stop-reason.json +80 -0
  143. package/scripts/mutants/acp-stream-hooks.json +158 -0
  144. package/scripts/mutants/bridge-boot-resume.json +45 -0
  145. package/scripts/mutants/meta-facts.json +50 -0
  146. package/scripts/mutants/meta-identity.json +36 -0
  147. package/scripts/mutants/mux-boundary.json +196 -0
  148. package/scripts/mutants/mux-fresh-call.json +185 -0
  149. package/scripts/mutants/mux-launcher-fence.json +123 -0
  150. package/scripts/mutants/mux-parent-artifact.json +39 -0
  151. package/scripts/mutants/mux-resume-call.json +148 -0
  152. package/scripts/mutants/probe-ordering.json +6 -1021
  153. package/scripts/mutants/release-gate.json +140 -0
  154. package/scripts/mutants/resume-args.json +76 -0
  155. package/scripts/mutants/resume-launch-identity.json +96 -0
  156. package/scripts/mutants/v2-surface.json +74 -23
  157. package/scripts/mutants/v2-visible-resume.json +215 -0
  158. package/scripts/new-session-id.ts +9 -4
  159. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  160. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  161. package/scripts/smoke-acp-cortex-live.ts +8 -14
  162. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  163. package/scripts/smoke-acp-mcp-live.ts +2 -2
  164. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  165. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  166. package/scripts/smoke-acp-overlay-live.ts +2 -2
  167. package/scripts/smoke-acp-provider-live.ts +2 -2
  168. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  169. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  170. package/scripts/smoke-acp-skill-live.ts +2 -2
  171. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  172. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  173. package/scripts/smoke-agy-native-push-live.ts +10 -20
  174. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  175. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  176. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
  177. package/scripts/smoke-meta-honesty.sh +1 -1
  178. package/scripts/smoke-meta-install-state.sh +4 -0
  179. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  180. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  181. package/scripts/smoke-pi-attach.ts +1 -1
  182. package/scripts/smoke-user-scope-citizen.sh +1 -1
  183. package/scripts/tsconfig.json +1 -0
  184. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  185. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  186. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  187. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  188. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  189. package/scripts/check-acp-sdk-surface.ts +0 -236
  190. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  191. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  192. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  193. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
@@ -0,0 +1,282 @@
1
+ /**
2
+ * check-mux-resume-call — deterministic gate for the resume placement composition
3
+ * (`pi-extensions/lib/mux-resume-call.ts`).
4
+ *
5
+ * Same scope discipline as `check-mux-launch` and `check-mux-fresh-call`: only what is decidable
6
+ * without tmux. There is no fake tmux in this repo and this gate does not introduce one — the
7
+ * real-window axis belongs to the LIVE lifecycle smoke.
8
+ *
9
+ * Every cwd claim below is a MEASURED tmux 3.6a behaviour, not a precaution. They were taken on a
10
+ * private server on 2026-08-06, and each one is a way a resume would look successful while being
11
+ * wrong:
12
+ *
13
+ * MUXRESUME-CWD-MISSING-REFUSED a nonexistent `-c` does NOT fail: tmux exits 0, opens the
14
+ * window, and the child lands in $HOME. Nothing downstream can
15
+ * catch it — the launch receipt is well-formed either way.
16
+ * MUXRESUME-CWD-FORMAT-REFUSED `-c` is FORMAT-EXPANDED. `<dir>/#{pane_id}` became `<dir>/%0`,
17
+ * and a `#(…)` value was observed running its command.
18
+ * MUXRESUME-CWD-WHITESPACE-OK whitespace is SAFE (argv is an array) — so the refusal set
19
+ * stays at two rules and no quoting/escaping layer is owed.
20
+ * MUXRESUME-ARGV-HAS-CWD the `-c` actually reaches tmux, after the append shape
21
+ * MUXRESUME-ARGV-RUNTIME-AFTER-DASHDASH the runtime and its flags are exec'd, never parsed as
22
+ * tmux options or run through a shell
23
+ * MUXRESUME-ARGV-CARRIER-FREE nothing beyond append + `-c` + runtime + the caller's flags
24
+ * MUXRESUME-NO-IDENTITY this module never learns which citizen it is reopening
25
+ * MUXRESUME-SURFACE-SEAM both surfaces hand the launcher IN; neither lets the v2
26
+ * composition import mux
27
+ */
28
+
29
+ import assert from "node:assert/strict";
30
+ import fs from "node:fs";
31
+ import os from "node:os";
32
+ import path from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+ import type { Placement } from "../pi-extensions/lib/mux-placement.ts";
35
+ import {
36
+ buildResumeCallArgs,
37
+ classifyResumeCwd,
38
+ RESUME_CALL_REJECT_HINT,
39
+ RESUME_CALL_RUNTIME,
40
+ type ResumeCallRejectReason,
41
+ resumeCall,
42
+ } from "../pi-extensions/lib/mux-resume-call.ts";
43
+
44
+ let passed = 0;
45
+ function ok(label: string, cond: boolean): void {
46
+ assert.ok(cond, label);
47
+ console.log(` ok ${label}`);
48
+ passed++;
49
+ }
50
+
51
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
52
+ const read = (rel: string): string => fs.readFileSync(path.join(ROOT, rel), "utf8");
53
+
54
+ const PLACEMENT: Placement = {
55
+ serverPid: "4242",
56
+ sessionId: "$0",
57
+ windowId: "@1",
58
+ windowIndex: "1",
59
+ paneId: "%1",
60
+ panePid: "4243",
61
+ };
62
+
63
+ function main(): void {
64
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "check-mux-resume-call-"));
65
+ const realDir = path.join(tmp, "project");
66
+ const spaceDir = path.join(tmp, "with space");
67
+ const filePath = path.join(tmp, "a-file");
68
+ fs.mkdirSync(realDir);
69
+ fs.mkdirSync(spaceDir);
70
+ fs.writeFileSync(filePath, "");
71
+ // `assertLaunchTarget` proves the runtime is real BEFORE argv is built, so the argv
72
+ // assertions need an executable that exists rather than a plausible-looking path.
73
+ const runtimeDir = path.join(tmp, "bin");
74
+ fs.mkdirSync(runtimeDir);
75
+ const RUNTIME = path.join(runtimeDir, "pi");
76
+ fs.writeFileSync(RUNTIME, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
77
+
78
+ try {
79
+ // ── cwd classification ────────────────────────────────────────────────────────
80
+ ok("an existing absolute directory is accepted", classifyResumeCwd(realDir) === null);
81
+
82
+ ok(
83
+ "[QK:MUXRESUME-CWD-MISSING-REFUSED] a cwd that no longer exists is refused HERE, because tmux would not refuse it — measured: rc=0, the window opens, and the child silently falls back to $HOME, so the resume would land a visible citizen in the wrong project and look successful",
84
+ classifyResumeCwd(path.join(tmp, "deleted-project")) === "cwd-missing",
85
+ );
86
+ ok(
87
+ "a path that exists but is a FILE is refused as its own cause, not as missing",
88
+ classifyResumeCwd(filePath) === "cwd-not-directory",
89
+ );
90
+ ok(
91
+ "[QK:MUXRESUME-CWD-FORMAT-REFUSED] a cwd containing '#' is refused unrun — measured: tmux FORMAT-EXPANDS the -c value, so `<dir>/#{pane_id}` silently became `<dir>/%0` and a `#(…)` value was observed executing its command; a path is data and tmux reads it as a format",
92
+ classifyResumeCwd(path.join(tmp, "#{pane_id}")) === "cwd-format-token" &&
93
+ classifyResumeCwd(path.join(tmp, "#(touch x)")) === "cwd-format-token",
94
+ );
95
+ ok(
96
+ "the '#' refusal precedes the filesystem question, whose answer would be about a path tmux is not going to use",
97
+ classifyResumeCwd(path.join(realDir, "#nope")) === "cwd-format-token",
98
+ );
99
+ ok(
100
+ "a relative cwd is refused before anything touches the filesystem",
101
+ classifyResumeCwd("project") === "cwd-not-absolute",
102
+ );
103
+ ok(
104
+ "[QK:MUXRESUME-CWD-WHITESPACE-OK] a cwd containing whitespace is ACCEPTED — measured: argv is an array, tmux does not re-split it, and the directory arrived intact; inventing a quoting grammar here would refuse real project paths for a danger that was measured not to exist",
105
+ classifyResumeCwd(spaceDir) === null,
106
+ );
107
+
108
+ // ── argv shape ────────────────────────────────────────────────────────────────
109
+ const runtimeArgs = ["--entwurf-control", "--session", "/s.jsonl", "--model", "m"] as const;
110
+ const args = buildResumeCallArgs(PLACEMENT, realDir, RUNTIME, runtimeArgs);
111
+
112
+ ok(
113
+ "[QK:MUXRESUME-ARGV-HAS-CWD] the recorded cwd reaches tmux as `-c <dir>` — without it the window inherits the CALLER's directory and the citizen comes back in someone else's project",
114
+ args.includes("-c") && args[args.indexOf("-c") + 1] === realDir,
115
+ );
116
+ ok(
117
+ "the append shape is the leaf's: detached, appended at {end} of the caller's own session, printing the shared handle format",
118
+ args[0] === "new-window" &&
119
+ args.includes("-d") &&
120
+ args.includes("-a") &&
121
+ args[args.indexOf("-t") + 1] === "$0:{end}",
122
+ );
123
+ ok(
124
+ "[QK:MUXRESUME-ARGV-RUNTIME-AFTER-DASHDASH] the runtime is preceded by `--` and every caller flag follows it, so tmux execs pi directly instead of reading `--session` as a tmux option or running the line through a shell",
125
+ args[args.indexOf("--") + 1] === RUNTIME &&
126
+ args.slice(args.indexOf("--") + 2).join(" ") === runtimeArgs.join(" "),
127
+ );
128
+ // Scoped to the TMUX prefix, before `--`. Everything after that belongs to pi, and a real
129
+ // resume argv legitimately carries `-e <bridge>` there — asserting `-e` is absent from the
130
+ // whole line would be green only because this fixture has no extensions, and would go red
131
+ // on the recorded-ACP shape that actually ships.
132
+ const tmuxPrefix = args.slice(0, args.indexOf("--"));
133
+ ok(
134
+ "[QK:MUXRESUME-ARGV-CARRIER-FREE] the TMUX half of the argv is exactly the append shape plus -c — no -n window name, no -e env carrier, no -b; the runtime's own flags live after `--` and are not tmux's business",
135
+ !tmuxPrefix.includes("-n") && !tmuxPrefix.includes("-e") && !tmuxPrefix.includes("-b"),
136
+ );
137
+ ok(
138
+ "a cwd that classification refuses can never be built into argv either — the builder re-checks rather than trusting its caller",
139
+ (() => {
140
+ try {
141
+ buildResumeCallArgs(PLACEMENT, path.join(tmp, "#x"), RUNTIME, runtimeArgs);
142
+ return false;
143
+ } catch {
144
+ return true;
145
+ }
146
+ })(),
147
+ );
148
+ ok(
149
+ "a non-native session selector is refused before tmux is invoked",
150
+ (() => {
151
+ try {
152
+ buildResumeCallArgs({ ...PLACEMENT, sessionId: "entwurf; kill-server" }, realDir, RUNTIME, []);
153
+ return false;
154
+ } catch {
155
+ return true;
156
+ }
157
+ })(),
158
+ );
159
+
160
+ // ── refusals happen BEFORE any mutation ───────────────────────────────────────
161
+ // No tmux context at all: a bad cwd must still be the reason, because it is decided first
162
+ // and nothing may reach tmux while it is wrong.
163
+ const noTmux = resumeCall({ cwd: path.join(tmp, "gone"), runtimeArgs: [] }, {});
164
+ ok(
165
+ "an unusable cwd refuses even outside tmux — the cwd question is settled before placement, so no window can exist by the time it is answered",
166
+ !noTmux.ok && noTmux.reason === "cwd-missing",
167
+ );
168
+ const outsideTmux = resumeCall({ cwd: realDir, runtimeArgs: [] }, { PATH: "/nonexistent" });
169
+ ok(
170
+ "with a good cwd and no runtime on PATH, the refusal is the runtime — preconditions are proven before placement is even read",
171
+ !outsideTmux.ok && outsideTmux.reason === "runtime-unresolved",
172
+ );
173
+
174
+ // ── every reason is answerable ────────────────────────────────────────────────
175
+ const reasons: ResumeCallRejectReason[] = [
176
+ "no-tmux-context",
177
+ "anchor-malformed",
178
+ "anchor-unresolved",
179
+ "anchor-mismatch",
180
+ "cwd-not-absolute",
181
+ "cwd-format-token",
182
+ "cwd-missing",
183
+ "cwd-not-directory",
184
+ "runtime-unresolved",
185
+ "runtime-not-absolute",
186
+ "runtime-path-whitespace",
187
+ "runtime-missing",
188
+ "runtime-not-regular-file",
189
+ "runtime-not-executable",
190
+ ];
191
+ ok(
192
+ "every reject reason has a hint a caller can act on — a reason without one is a reason they will guess about",
193
+ reasons.every((r) => typeof RESUME_CALL_REJECT_HINT[r] === "string" && RESUME_CALL_REJECT_HINT[r].length > 0),
194
+ );
195
+ ok("the fixed runtime is pi — only a control-socket backend has a same-id resume", RESUME_CALL_RUNTIME === "pi");
196
+
197
+ // ── boundaries, asserted on source ────────────────────────────────────────────
198
+ const SRC = read("pi-extensions/lib/mux-resume-call.ts");
199
+ ok(
200
+ "[QK:MUXRESUME-NO-IDENTITY] the module never mentions a garden id, a record, a lock or a transcript — it opens a place at a directory and knows nothing about whose citizen it is",
201
+ !/gardenId|readAddressableMetaIdentity|acquireLock|transcriptPath/.test(SRC),
202
+ );
203
+ ok(
204
+ "it imports only the mux lane — no entwurf core, no delivery",
205
+ !/from "\.\/entwurf-/.test(SRC) && !/meta-session/.test(SRC),
206
+ );
207
+ ok(
208
+ "it uses the SHARED APPEND_FORMAT rather than adding a start-path field: `-P -F` prints its row BEFORE the child chdirs (measured — pane_current_path there still reports the CALLER's cwd), so a path field would be both free-form in a `|`-joined row and racy",
209
+ SRC.includes("APPEND_FORMAT") && !SRC.includes("pane_start_path"),
210
+ );
211
+
212
+ const PI_SURFACE = read("pi-extensions/entwurf-control.ts");
213
+ const MCP_SURFACE = read("mcp/entwurf-bridge/src/index.ts");
214
+ const V2_RESUME = read("pi-extensions/lib/entwurf-v2-visible-resume.ts");
215
+ // IMPORTS, not mentions: the module header names the mux side deliberately (that is the
216
+ // architecture being documented), so an assertion on prose would forbid the explanation
217
+ // rather than the dependency.
218
+ const v2ImportSpecifiers = [...V2_RESUME.matchAll(/from\s+"([^"]+)"/g)].map((m) => m[1]);
219
+ ok(
220
+ "[QK:MUXRESUME-SURFACE-SEAM] the v2 composition never IMPORTS mux — the launcher is passed IN, which is what keeps docs/mux-launch-rail.md §11 true in both directions",
221
+ v2ImportSpecifiers.length > 0 && !v2ImportSpecifiers.some((s) => /mux-/.test(s)),
222
+ );
223
+ ok(
224
+ "both surfaces are the composition root: each imports the mux launcher AND the v2 composition and joins them itself",
225
+ /mux-resume-call/.test(PI_SURFACE) &&
226
+ /entwurf-v2-visible-resume/.test(PI_SURFACE) &&
227
+ /mux-resume-call/.test(MCP_SURFACE) &&
228
+ /entwurf-v2-visible-resume/.test(MCP_SURFACE),
229
+ );
230
+
231
+ // ── the NATIVE PI registration, judged as a registration ─────────────────────
232
+ // The MCP half is judged on the real runtime tools/list by check-entwurf-bridge-boot.
233
+ // Native pi has no equivalent boot oracle here, so its registration is read from source —
234
+ // but narrowly, on the block itself, because "the module is imported" would stay true with
235
+ // the tool deleted.
236
+ const PI_BLOCK = (PI_SURFACE.split('name: "entwurf_resume_call",')[1] ?? "").split("\n}")[0];
237
+ ok(
238
+ "[QK:MUXRESUME-PI-SURFACE-REGISTERED] the native pi surface REGISTERS entwurf_resume_call and calls that registration during setup — an imported-but-unregistered verb is invisible to the operator while every import assertion stays green",
239
+ PI_SURFACE.includes('name: "entwurf_resume_call"') && /registerResumeCallTool\(pi\);/.test(PI_SURFACE),
240
+ );
241
+ ok(
242
+ "[QK:MUXRESUME-PI-SURFACE-TARGET-ONLY] the native schema is target-only and the handler passes params.target straight through — a model, task or prompt knob would be a second way to decide what a resumed citizen is, when the record already decided",
243
+ /Type\.Object\(\{\s*target: Type\.String\(/.test(PI_BLOCK) &&
244
+ /visibleResume\(params\.target,/.test(PI_BLOCK) &&
245
+ !/model|task|prompt/.test(PI_BLOCK.split("parameters:")[1]?.split("async execute")[0] ?? ""),
246
+ );
247
+
248
+ // ── the description contract, both surfaces ──────────────────────────────────
249
+ // Not prose parity — three claims an operator acts on, plus the host's own cap.
250
+ const PI_DESC = (PI_BLOCK.split("description: `")[1] ?? "").split("`,")[0];
251
+ const MCP_DESC = (MCP_SURFACE.split('"entwurf_resume_call",')[1] ?? "").split("{")[0];
252
+ const statesTheContract = (text: string): boolean =>
253
+ /dormant/i.test(text) && /no turn/i.test(text) && /observation/i.test(text) && /launch/i.test(text);
254
+
255
+ for (const [which, text] of [
256
+ ["native pi", PI_DESC],
257
+ ["MCP bridge", MCP_DESC],
258
+ ] as const) {
259
+ ok(
260
+ `${which}: the resume description fits the host's 2048-char tool-description cap`,
261
+ text.length > 0 && text.length <= 2048,
262
+ );
263
+ }
264
+ // The two claim tokens are written OUT, not interpolated: qualification requires each
265
+ // `[QK:…]` to appear literally exactly once in its gate source, so a token assembled at
266
+ // runtime names a claim no manifest can bind to.
267
+ ok(
268
+ "[QK:MUXRESUME-DESC-CONTRACT-PI] native pi: the description states the three things a caller acts on — DORMANT targets only, no turn is run, and TWO receipts of which the observation is the one that says the citizen is back",
269
+ statesTheContract(PI_DESC),
270
+ );
271
+ ok(
272
+ "[QK:MUXRESUME-DESC-CONTRACT-MCP] MCP bridge: the description states the same three things — DORMANT targets only, no turn is run, and TWO receipts whose observation is the fact that matters",
273
+ statesTheContract(MCP_DESC),
274
+ );
275
+ } finally {
276
+ fs.rmSync(tmp, { recursive: true, force: true });
277
+ }
278
+
279
+ console.log(`\ncheck-mux-resume-call: ${passed} checks passed`);
280
+ }
281
+
282
+ main();
@@ -30,8 +30,11 @@
30
30
  // the native branch: no shell, piped stdio, inherited cwd), because half of what
31
31
  // this gate proves is process semantics that a unit call cannot reach.
32
32
  //
33
- // Kill-proof: scripts/mutants/probe-ordering.json carries one exact-once mutant
34
- // per [QK:...] signature below. THREE properties are SOURCE-pinned rather than
33
+ // Kill-proof, current contract: this gate's claims are DIRECT [CHECK:<claim>]
34
+ // assertions enforced on every run, deliberately no longer replant-qualified
35
+ // (issue #70 verification subtraction; the probe-ordering lane's one retained
36
+ // [QK:] mutant is the product-subject prompt-cutoff claim consumed by
37
+ // check-probe-ordering). THREE properties are SOURCE-pinned rather than
35
38
  // behaviour-pinned, and §12 states the measurement that sent each one there —
36
39
  // this is the same carve-out §11-7-c already records for the fixture's and the
37
40
  // shim's write-callback timing, not a softer bar invented here. The two callback
@@ -378,7 +381,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
378
381
  (statSync(SHIM_LAUNCHER).mode & 0o111) !== 0 &&
379
382
  LAUNCHER_SRC.startsWith("#!/usr/bin/env node\n"),
380
383
  "the shim launcher is extensionless, executable, and shebang-led — a script suffix would move it onto the SDK's " +
381
- "`node|bun <path>` branch, which is NOT the branch the probe's target asserts [QK:SHIM-NATIVE-BRANCH-LAUNCHER]",
384
+ "`node|bun <path>` branch, which is NOT the branch the probe's target asserts [CHECK:SHIM-NATIVE-BRANCH-LAUNCHER]",
382
385
  );
383
386
  // The launcher must stay a launcher: behaviour belongs in the .ts SSOT, which
384
387
  // is the only half tsc/biome/mutants can reach.
@@ -437,7 +440,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
437
440
  sha256(multiByte),
438
441
  "a chunk boundary INSIDE a multi-byte UTF-8 sequence, delivered as two separate reads, still crosses the shim " +
439
442
  "byte-for-byte — the scanner frames on bytes and only complete lines are ever decoded " +
440
- "[QK:SHIM-BYTE-TRANSPARENCY]",
443
+ "[CHECK:SHIM-BYTE-TRANSPARENCY]",
441
444
  );
442
445
 
443
446
  // Bulk framing, on top: CRLF pairs, a 300 KB line, and a final line with no
@@ -470,7 +473,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
470
473
  forwarded.length === 1 && forwarded[0].ordinal === 1,
471
474
  "the control-request frame is NOT a prompt: the SDK writes initialize control traffic on the same stdin, so " +
472
475
  'counting lines would blow the exactly-one binding on every run — only `type:"user"` frames take an ' +
473
- "ordinal [QK:SHIM-PROMPT-ORDINAL-USER-FRAMES-ONLY]",
476
+ "ordinal [CHECK:SHIM-PROMPT-ORDINAL-USER-FRAMES-ONLY]",
474
477
  );
475
478
 
476
479
  // The boot init was RECEIVED before the prompt frame was forwarded, so it is a
@@ -502,7 +505,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
502
505
  0,
503
506
  "an init line with no `tools`, a non-array `tools`, or a `tools` holding a non-string yields NO snapshot: " +
504
507
  "reporting it as an EMPTY name set would FABRICATE the absence reading the B-name-snapshot ladder promotes " +
505
- "[QK:SHIM-INIT-REQUIRES-STRING-TOOL-ARRAY]",
508
+ "[CHECK:SHIM-INIT-REQUIRES-STRING-TOOL-ARRAY]",
506
509
  );
507
510
  }
508
511
 
@@ -516,7 +519,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
516
519
  boots.length === 1 && boots[0].targetPath === STREAM_CLI && boots[0].targetSha256 === hashFileSha256(STREAM_CLI),
517
520
  "the shim boots exactly once and reports the REAL path + content hash of what it exec'd — condition 5 has the " +
518
521
  "classifier verify this against the roster's expected identity, so a fabricated or omitted hash would let a " +
519
- "swapped binary vote in the pair [QK:SHIM-BOOT-TARGET-IDENTITY]",
522
+ "swapped binary vote in the pair [CHECK:SHIM-BOOT-TARGET-IDENTITY]",
520
523
  );
521
524
  assert.ok(
522
525
  boots[0].seq < named(run, PROBE_EVENTS.shimInitSnapshot)[0].seq,
@@ -568,7 +571,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
568
571
  expectedChildEnv,
569
572
  "the child's env is the launch env MINUS exactly the allowlist and nothing else — every remaining key and value " +
570
573
  "byte-identical. A prefix scrub would eat the PROBE_-shaped operator variable this probe has no claim on, and " +
571
- "an incidental deletion anywhere else would move this too [QK:SHIM-SCRUB-EXACT-ALLOWLIST]",
574
+ "an incidental deletion anywhere else would move this too [CHECK:SHIM-SCRUB-EXACT-ALLOWLIST]",
572
575
  );
573
576
 
574
577
  // The `claude auth logout` consumer: not a stream-json turn, so the only line
@@ -576,7 +579,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
576
579
  assert.ok(
577
580
  run.events.length === 1 && run.events[0].event === PROBE_EVENTS.shimBoot,
578
581
  "an invocation that is not a stream-json turn is PURE passthrough — `claudeCliPath()` has a second consumer " +
579
- "(claude auth logout), and the shim must assume nothing about its argv [QK:SHIM-ARGV-AGNOSTIC-PASSTHROUGH]",
582
+ "(claude auth logout), and the shim must assume nothing about its argv [CHECK:SHIM-ARGV-AGNOSTIC-PASSTHROUGH]",
580
583
  );
581
584
  }
582
585
 
@@ -600,7 +603,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
600
603
  !logText.includes("SECRET-PROMPT-TEXT") &&
601
604
  !logText.includes("ENTWURF_SHIM_SECRET"),
602
605
  "the shared log carries no argv, no env name or value, and no prompt body — only the allowlisted init fields, " +
603
- "an ordinal, and timings [QK:SHIM-LOG-PRIVACY]",
606
+ "an ordinal, and timings [CHECK:SHIM-LOG-PRIVACY]",
604
607
  );
605
608
  }
606
609
 
@@ -612,21 +615,21 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
612
615
  assert.ok(
613
616
  nonzero.code === 42 && nonzero.signal === null,
614
617
  "a nonzero CLI exit reaches the parent as the SAME code — a wrapper that reports 0 turns a crash into a " +
615
- "measurement [QK:SHIM-EXIT-CODE-FIDELITY]",
618
+ "measurement [CHECK:SHIM-EXIT-CODE-FIDELITY]",
616
619
  );
617
620
 
618
621
  const signalled = await runShim({ target: SELF_SIGNAL_CLI, argv: ["SIGKILL"] });
619
622
  assert.ok(
620
623
  signalled.signal === "SIGKILL" && signalled.code === null,
621
624
  "a CLI that dies on a signal makes the SHIM die on that same signal: the parent's wait status must carry the " +
622
- "child's real disposition, not a synthesised 128+n exit code [QK:SHIM-SIGNAL-RERAISE]",
625
+ "child's real disposition, not a synthesised 128+n exit code [CHECK:SHIM-SIGNAL-RERAISE]",
623
626
  );
624
627
 
625
628
  const inbound = await runShim({ target: SLEEPER_CLI, signalShim: "SIGTERM" });
626
629
  assert.ok(
627
630
  inbound.signal === "SIGTERM",
628
631
  "a signal sent to the shim is forwarded to the child, whose death then re-raises it here — otherwise the ACP " +
629
- "child's teardown would leave the real CLI orphaned [QK:SHIM-INBOUND-SIGNAL-FORWARDED]",
632
+ "child's teardown would leave the real CLI orphaned [CHECK:SHIM-INBOUND-SIGNAL-FORWARDED]",
630
633
  );
631
634
 
632
635
  const stderrRun = await runShim({ target: STDERR_CLI });
@@ -649,7 +652,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
649
652
  missing.stderr.includes("[probe-cli-shim] cannot read exec target"),
650
653
  "a target that vanished under the pair fails loud with the shell convention for not-found — the runner asserted " +
651
654
  "it was present, so reaching this is a fact the operator needs in words, on ONE errno mapping shared by the " +
652
- "unreadable-at-hash and unspawnable-at-exec paths [QK:SHIM-SPAWN-ERROR-NAMED]",
655
+ "unreadable-at-hash and unspawnable-at-exec paths [CHECK:SHIM-SPAWN-ERROR-NAMED]",
653
656
  );
654
657
  assert.equal(
655
658
  named(missing, PROBE_EVENTS.shimBoot).length,
@@ -695,7 +698,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
695
698
  "a line past the framing bound loses its PARSE, never its BYTES: the oversized line is forwarded verbatim, the " +
696
699
  "scanner recovers at the next newline so the following init still binds, and the skip is recorded in a " +
697
700
  "forensic sidecar rather than as an unknown marker the log door would call MALFORMED " +
698
- "[QK:SHIM-OVERSIZED-LINE-PARSE-SKIP]",
701
+ "[CHECK:SHIM-OVERSIZED-LINE-PARSE-SKIP]",
699
702
  );
700
703
  const diag = JSON.parse(readFileSync(run.diagPath, "utf8").trim()) as Record<string, unknown>;
701
704
  assert.ok(
@@ -733,7 +736,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
733
736
  `framing a ${lineBytes / 1024} KiB line out of ${lineBytes} single-byte reads cost ` +
734
737
  `${counted.retainedAllocations} allocations — the bound is a bound on OBJECTS as well as bytes, or a hostile ` +
735
738
  "stream reaches the byte cap holding millions of buffer headers and the 'bounded in-memory line buffer' " +
736
- "claim is false exactly where it matters [QK:SHIM-FRAMING-BOUNDED-IN-OBJECTS]",
739
+ "claim is false exactly where it matters [CHECK:SHIM-FRAMING-BOUNDED-IN-OBJECTS]",
737
740
  );
738
741
  }
739
742
 
@@ -789,7 +792,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
789
792
  "tearing down on the error ends it in ~0.2 s, while merely IGNORING the error parks the source on a drain " +
790
793
  "that can never arrive and it survives until something kills it — with the real CLI still alive behind it. " +
791
794
  "Promptness is the discriminator; the exit disposition is not, because the ignore path can also die of its " +
792
- "own unhandled error [QK:SHIM-DOWNSTREAM-DEATH-TEARS-DOWN]",
795
+ "own unhandled error [CHECK:SHIM-DOWNSTREAM-DEATH-TEARS-DOWN]",
793
796
  );
794
797
  }
795
798
 
@@ -810,7 +813,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
810
813
  diag !== undefined && diag.stdoutLineParseSkipped === 1,
811
814
  "a line that blew the framing bound and then hit EOF WITHOUT a newline is still counted: the scanner finalises at " +
812
815
  "stream end, so the skip diagnostic cannot silently under-report the one shape that never meets a newline " +
813
- "[QK:SHIM-OVERSIZE-COUNTED-AT-EOF]",
816
+ "[CHECK:SHIM-OVERSIZE-COUNTED-AT-EOF]",
814
817
  );
815
818
  }
816
819
 
@@ -834,7 +837,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
834
837
  ),
835
838
  "under write backpressure on STDOUT the shim stops READING the CLI rather than letting the writable queue grow — " +
836
839
  "peak memory is then set by the pipe buffers, not by the transcript size, which is what keeps an instrument " +
837
- "on a live turn from becoming a memory hazard [QK:SHIM-STDOUT-BACKPRESSURE-PAUSES-SOURCE]",
840
+ "on a live turn from becoming a memory hazard [CHECK:SHIM-STDOUT-BACKPRESSURE-PAUSES-SOURCE]",
838
841
  );
839
842
  assert.ok(
840
843
  SHIM_SRC.includes(
@@ -845,7 +848,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
845
848
  ),
846
849
  "the SAME discipline holds on the stdin side: a CLI slow to read its input must park the SDK's stream, not be " +
847
850
  "absorbed into this process's memory. 11b proves the bytes and the EOF survive it; which side does the " +
848
- "parking is what is pinned here [QK:SHIM-STDIN-BACKPRESSURE-PAUSES-SOURCE]",
851
+ "parking is what is pinned here [CHECK:SHIM-STDIN-BACKPRESSURE-PAUSES-SOURCE]",
849
852
  );
850
853
  assert.ok(
851
854
  SHIM_SRC.includes(
@@ -857,7 +860,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
857
860
  "the snapshot append lives INSIDE the downstream write callback, so the one clock read that stamps the event IS " +
858
861
  "the hand-off moment — the interval's single-SSOT end (§11-7-c condition 6). Timing cannot separate this " +
859
862
  "placement from one just outside the callback, because the shim pauses its source under backpressure and " +
860
- "couples the read to the write, so the shape is pinned here [QK:SHIM-SNAPSHOT-IN-WRITE-CALLBACK]",
863
+ "couples the read to the write, so the shape is pinned here [CHECK:SHIM-SNAPSHOT-IN-WRITE-CALLBACK]",
861
864
  );
862
865
  assert.ok(
863
866
  SHIM_SRC.includes(
@@ -868,7 +871,7 @@ const CONTROL_FRAME = ndjson({ type: "control_request", request_id: "r1", reques
868
871
  ),
869
872
  "the prompt anchor is stamped inside the CHILD-STDIN write callback — 'fully passed to the CLI's stdin', not " +
870
873
  "'we saw a newline'; and an errored write never stamps a hand-off that did not happen " +
871
- "[QK:SHIM-PROMPT-IN-WRITE-CALLBACK]",
874
+ "[CHECK:SHIM-PROMPT-IN-WRITE-CALLBACK]",
872
875
  );
873
876
  assert.ok(
874
877
  SHIM_SRC.includes('stdio: ["pipe", "pipe", "inherit"],') && !/\bshell\s*:/.test(SHIM_SRC),