@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.
- package/AGENTS.md +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- 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:
|
|
34
|
-
//
|
|
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 [
|
|
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
|
-
"[
|
|
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 [
|
|
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
|
-
"[
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
-
"[
|
|
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 [
|
|
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 [
|
|
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
|
-
"[
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
-
"[
|
|
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),
|