@junghanacs/entwurf 0.18.1 → 0.19.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 +2 -2
- package/CHANGELOG.md +248 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +8 -6
- package/VERIFY.md +3 -3
- package/docs/external-mcp-host.md +2 -2
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +18 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +76 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +54 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resolve-tmux-session.js +114 -0
- package/mcp/entwurf-bridge/src/index.ts +22 -7
- package/package.json +1 -1
- package/pi-extensions/entwurf-control.ts +27 -3
- package/pi-extensions/lib/mux-fresh-call.ts +98 -11
- package/pi-extensions/lib/mux-placement.ts +65 -10
- package/pi-extensions/lib/resolve-tmux-session.ts +137 -0
- package/scripts/check-fresh-cut-gate.sh +54 -2
- package/scripts/check-gate-qualification.ts +5 -5
- package/scripts/check-mux-launch-tmux.ts +77 -2
- package/scripts/check-mux-launch.ts +17 -0
- package/scripts/check-mux-placement-tmux.ts +61 -1
- package/scripts/check-mux-placement.ts +33 -0
- package/scripts/check-release-gate-outcomes.ts +224 -9
- package/scripts/ci-qualify-decide.sh +159 -0
- package/scripts/fixtures/qualify-replay.json +149 -0
- package/scripts/mutants/fresh-cut.json +26 -0
- package/scripts/mutants/mux-boundary.json +24 -0
- package/scripts/mutants/mux-fresh-call.json +81 -0
- package/scripts/mutants/omp-birth.json +16 -3
- package/scripts/mutants/release-gate.json +45 -1
- package/scripts/omp-bridge-doctor.sh +11 -1
- package/scripts/smoke-mux-fresh-call-live.ts +175 -3
- package/scripts/smoke-omp-bridge-state.sh +5 -2
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* smoke-mux-fresh-call-live — the ONE axis a deterministic gate cannot reach: a real window, a
|
|
3
3
|
* real runtime, a real first turn, and a real callback arriving on a real inbound surface.
|
|
4
4
|
*
|
|
5
|
-
* OUT of `pnpm check`. Needs `LIVE=1`. Costs
|
|
5
|
+
* OUT of `pnpm check`. Needs `LIVE=1`. Costs FOUR model turns (three pi, one Claude Code): two
|
|
6
|
+
* open in the caller's own session, and #105 added two more in a second SEAT session on the same
|
|
7
|
+
* private server so the seat x cwd matrix is read on real panes rather than argued about.
|
|
6
8
|
*
|
|
7
9
|
* ── Isolate the WRITES, keep the runtimes real ──
|
|
8
10
|
*
|
|
@@ -59,6 +61,17 @@ const REAL_CONTROL_DIR = path.join(REAL_HOME, ".pi", "entwurf-control");
|
|
|
59
61
|
// so the real-HOME Claude cell can be given exact operator-env parity (issue #67).
|
|
60
62
|
const ORIGINAL_XDG = snapshotOriginalXdg(process.env);
|
|
61
63
|
|
|
64
|
+
// The #105 seat fixture. A second session on the SAME private server, its own session_path, and
|
|
65
|
+
// two distinct requested cwds — three directories that must never be confusable with each other.
|
|
66
|
+
const SEAT_SESSION = "seat";
|
|
67
|
+
const SEAT_HOME = (root: string): string => path.join(root, "seat-home");
|
|
68
|
+
const CROSS_REPO_A = (root: string): string => path.join(root, "cross-repo-a");
|
|
69
|
+
const CROSS_REPO_B = (root: string): string => path.join(root, "cross-repo-b");
|
|
70
|
+
/** tmux fills `#{pane_current_path}` with the SERVER's cwd at creation time and only becomes
|
|
71
|
+
* true 0.2–0.6s later (measured ×2, #105 R1) — so acceptance reads it after a delay, never from
|
|
72
|
+
* the launch receipt. */
|
|
73
|
+
const PANE_CWD_SETTLE_SECONDS = "3";
|
|
74
|
+
|
|
62
75
|
let passed = 0;
|
|
63
76
|
function ok(label: string, cond: boolean): void {
|
|
64
77
|
if (!cond) throw new Error(`${LABEL}: FAILED — ${label}`);
|
|
@@ -89,6 +102,13 @@ function tmux(socket: string, args: string[], env: NodeJS.ProcessEnv): { status:
|
|
|
89
102
|
return { status: r.status, stdout: (r.stdout ?? "").trim() };
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
/** The session the caller's own anchor pane sits in — tmux's answer, never a name we assembled. */
|
|
106
|
+
function inspectedSessionId(socket: string, env: NodeJS.ProcessEnv): string {
|
|
107
|
+
return tmux(socket, ["display-message", "-p", "-t", String(env.TMUX_PANE), "#{session_id}"], env).stdout.split(
|
|
108
|
+
"\n",
|
|
109
|
+
)[0];
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
function pidIsAlive(pid: number): boolean {
|
|
93
113
|
try {
|
|
94
114
|
process.kill(pid, 0);
|
|
@@ -150,6 +170,10 @@ async function main(): Promise<void> {
|
|
|
150
170
|
};
|
|
151
171
|
for (const dir of Object.values(fenced)) fs.mkdirSync(dir, { recursive: true });
|
|
152
172
|
fs.mkdirSync(scratch, { recursive: true });
|
|
173
|
+
// The #105 seat cells need three directories that are all DIFFERENT from each other and from
|
|
174
|
+
// the scratch cwd, so "the pane landed here" can only be true for one reason. `seatHome` is
|
|
175
|
+
// the second session's own `session_path` — the candidate the cwd must NOT come from.
|
|
176
|
+
for (const dir of [SEAT_HOME(root), CROSS_REPO_A(root), CROSS_REPO_B(root)]) fs.mkdirSync(dir, { recursive: true });
|
|
153
177
|
// A runtime dir is a private surface by convention and tmux checks it.
|
|
154
178
|
fs.chmodSync(fenced.XDG_RUNTIME_DIR, 0o700);
|
|
155
179
|
for (const [k, v] of Object.entries(fenced)) process.env[k] = v;
|
|
@@ -160,7 +184,7 @@ async function main(): Promise<void> {
|
|
|
160
184
|
// Import the meta layer only AFTER the redirects — a module that resolved its roots at import
|
|
161
185
|
// time would have captured the operator's.
|
|
162
186
|
const meta = await import("../pi-extensions/lib/meta-session.ts");
|
|
163
|
-
const { freshCall } = await import("../pi-extensions/lib/mux-fresh-call.ts");
|
|
187
|
+
const { freshCall, renderFreshCall } = await import("../pi-extensions/lib/mux-fresh-call.ts");
|
|
164
188
|
|
|
165
189
|
const siblingGids = new Set<string>();
|
|
166
190
|
const siblingPids = new Set<number>();
|
|
@@ -223,6 +247,11 @@ async function main(): Promise<void> {
|
|
|
223
247
|
// onboarding instead of exercising the configured runtime. Separate servers preserve
|
|
224
248
|
// those backend-native environments without adding an env carrier to the product.
|
|
225
249
|
const nonces = new Map<string, string>();
|
|
250
|
+
/** Every launched cell's pane, with the directory the pane is REQUIRED to have settled in
|
|
251
|
+
* — read after a delay, which is the only honest way to observe it (see the constant). */
|
|
252
|
+
const cwdCells: Array<{ label: string; socket: string; env: NodeJS.ProcessEnv; paneId: string; expect: string }> =
|
|
253
|
+
[];
|
|
254
|
+
const servers = new Map<string, { socket: string; inherited: NodeJS.ProcessEnv }>();
|
|
226
255
|
// Intentional: pi + claude-code only. Copilot clause-7 LIVE is operator-metered
|
|
227
256
|
// and is not a release MUST — do not add `copilot` to this loop (VERIFY.md).
|
|
228
257
|
for (const backend of ["pi", "claude-code"] as const) {
|
|
@@ -248,12 +277,17 @@ async function main(): Promise<void> {
|
|
|
248
277
|
const anchorPane = tmux(socket, ["display-message", "-p", "-t", "fixture:anchor", "#{pane_id}"], env).stdout;
|
|
249
278
|
const inherited: NodeJS.ProcessEnv = { ...env, TMUX: `${socket},0,0`, TMUX_PANE: anchorPane };
|
|
250
279
|
ok(`${backend}: tmux server is a private socket, never the operator's`, String(inherited.TMUX).startsWith(root));
|
|
280
|
+
servers.set(backend, { socket, inherited });
|
|
251
281
|
|
|
282
|
+
// The caller-session half of the #105 cwd matrix: pi omits the cwd, claude-code names
|
|
283
|
+
// one. Both open in the CALLER's own session, which is the pre-#105 behaviour.
|
|
284
|
+
const requestedCwd = backend === "claude-code" ? CROSS_REPO_A(root) : undefined;
|
|
252
285
|
const result = freshCall(
|
|
253
286
|
{
|
|
254
287
|
backend,
|
|
255
288
|
model: LIVE_MODEL[backend],
|
|
256
289
|
task: "Reply with the single word ACK and then stop. Do not read files.",
|
|
290
|
+
cwd: requestedCwd,
|
|
257
291
|
callerGardenId: callerGid,
|
|
258
292
|
},
|
|
259
293
|
inherited,
|
|
@@ -262,6 +296,13 @@ async function main(): Promise<void> {
|
|
|
262
296
|
if (!result.ok) return;
|
|
263
297
|
nonces.set(backend, result.receipt.nonce);
|
|
264
298
|
siblingPids.add(Number(result.receipt.panePid));
|
|
299
|
+
cwdCells.push({
|
|
300
|
+
label: `${backend} @caller-session cwd=${requestedCwd === undefined ? "(omitted)" : "requested"}`,
|
|
301
|
+
socket,
|
|
302
|
+
env: inherited,
|
|
303
|
+
paneId: result.receipt.paneId,
|
|
304
|
+
expect: requestedCwd ?? scratch,
|
|
305
|
+
});
|
|
265
306
|
ok(
|
|
266
307
|
`${backend}: the receipt carries tmux coordinates, requested model and nonce, and nothing about delivery`,
|
|
267
308
|
Boolean(
|
|
@@ -271,6 +312,113 @@ async function main(): Promise<void> {
|
|
|
271
312
|
result.receipt.nonce,
|
|
272
313
|
) && !("gardenId" in result.receipt),
|
|
273
314
|
);
|
|
315
|
+
ok(
|
|
316
|
+
`${backend}: with no seat requested the window is in the CALLER's own session and the receipt names no seat`,
|
|
317
|
+
result.receipt.sessionId === inspectedSessionId(socket, inherited) && result.receipt.tmuxSession === undefined,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── #105 acceptance: the optional project seat ───────────────────────────
|
|
322
|
+
// One extra session on the SAME private pi server (R6: a second session is one line and
|
|
323
|
+
// costs no extra server). Its own `-c` gives it a session_path that is neither the
|
|
324
|
+
// caller's cwd nor either requested cwd, so the cwd matrix below can only pass for one
|
|
325
|
+
// reason.
|
|
326
|
+
const pi = servers.get("pi");
|
|
327
|
+
if (!pi) throw new Error(`${LABEL}: the pi fixture server was not captured`);
|
|
328
|
+
const callerSessionId = inspectedSessionId(pi.socket, pi.inherited);
|
|
329
|
+
if (
|
|
330
|
+
tmux(pi.socket, ["new-session", "-d", "-s", SEAT_SESSION, "-c", SEAT_HOME(root), "-n", "anchor"], pi.inherited)
|
|
331
|
+
.status !== 0
|
|
332
|
+
) {
|
|
333
|
+
throw new Error(`${LABEL}: could not create the second fixture session ${SEAT_SESSION}`);
|
|
334
|
+
}
|
|
335
|
+
const seatSessionId = tmux(
|
|
336
|
+
pi.socket,
|
|
337
|
+
["list-windows", "-t", `=${SEAT_SESSION}`, "-F", "#{session_id}"],
|
|
338
|
+
pi.inherited,
|
|
339
|
+
).stdout.split("\n")[0];
|
|
340
|
+
ok(
|
|
341
|
+
"seat: the fixture now holds two sessions on ONE server, and the seat is not the caller's",
|
|
342
|
+
/^\$[0-9]+$/.test(seatSessionId) && seatSessionId !== callerSessionId,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Acceptance 2 FIRST, because it is the one that must leave nothing behind. Both refusals
|
|
346
|
+
// are read against a byte-identical session/window inventory.
|
|
347
|
+
const inventory = (): string =>
|
|
348
|
+
tmux(pi.socket, ["list-windows", "-a", "-F", "#{session_id}|#{window_id}"], pi.inherited).stdout;
|
|
349
|
+
const beforeRefusals = inventory();
|
|
350
|
+
for (const [label, seat, reason] of [
|
|
351
|
+
["absent", "nosuchseat", "tmux-session-missing"],
|
|
352
|
+
["unresolvable", "no.such.seat", "tmux-session-name-invalid"],
|
|
353
|
+
] as const) {
|
|
354
|
+
const refused = freshCall(
|
|
355
|
+
{
|
|
356
|
+
backend: "pi",
|
|
357
|
+
model: LIVE_MODEL.pi,
|
|
358
|
+
task: "This task must never run.",
|
|
359
|
+
placement: { tmuxSession: seat },
|
|
360
|
+
callerGardenId: callerGid,
|
|
361
|
+
},
|
|
362
|
+
pi.inherited,
|
|
363
|
+
);
|
|
364
|
+
ok(
|
|
365
|
+
`seat: an ${label} seat is refused as ${reason}, and NOTHING was created`,
|
|
366
|
+
!refused.ok && refused.reason === reason && inventory() === beforeRefusals,
|
|
367
|
+
);
|
|
368
|
+
const rendered = renderFreshCall(refused);
|
|
369
|
+
ok(
|
|
370
|
+
`seat: the ${label} refusal says so in the operator's words`,
|
|
371
|
+
rendered.isError && rendered.text.includes("No window was opened"),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Acceptance 1 + the other half of the cwd matrix: two seated pi siblings, one with the
|
|
376
|
+
// cwd omitted and one with a requested cwd.
|
|
377
|
+
for (const [label, requestedCwd] of [
|
|
378
|
+
["seat cwd=(omitted)", undefined],
|
|
379
|
+
["seat cwd=requested", CROSS_REPO_B(root)],
|
|
380
|
+
] as const) {
|
|
381
|
+
const callerWindowsBefore = tmux(
|
|
382
|
+
pi.socket,
|
|
383
|
+
["list-windows", "-t", callerSessionId, "-F", "#{window_id}"],
|
|
384
|
+
pi.inherited,
|
|
385
|
+
).stdout;
|
|
386
|
+
const seated = freshCall(
|
|
387
|
+
{
|
|
388
|
+
backend: "pi",
|
|
389
|
+
model: LIVE_MODEL.pi,
|
|
390
|
+
task: "Reply with the single word ACK and then stop. Do not read files.",
|
|
391
|
+
cwd: requestedCwd,
|
|
392
|
+
placement: { tmuxSession: SEAT_SESSION },
|
|
393
|
+
callerGardenId: callerGid,
|
|
394
|
+
},
|
|
395
|
+
pi.inherited,
|
|
396
|
+
);
|
|
397
|
+
ok(`${label}: launch receipt is ok`, seated.ok);
|
|
398
|
+
if (!seated.ok) return;
|
|
399
|
+
nonces.set(label, seated.receipt.nonce);
|
|
400
|
+
siblingPids.add(Number(seated.receipt.panePid));
|
|
401
|
+
cwdCells.push({
|
|
402
|
+
label: `pi @${label}`,
|
|
403
|
+
socket: pi.socket,
|
|
404
|
+
env: pi.inherited,
|
|
405
|
+
paneId: seated.receipt.paneId,
|
|
406
|
+
expect: requestedCwd ?? scratch,
|
|
407
|
+
});
|
|
408
|
+
ok(
|
|
409
|
+
`${label}: the receipt echoes the REQUESTED seat name and reports the RESOLVED target session, not the caller's`,
|
|
410
|
+
seated.receipt.tmuxSession === SEAT_SESSION &&
|
|
411
|
+
seated.receipt.sessionId === seatSessionId &&
|
|
412
|
+
seated.receipt.sessionId !== callerSessionId,
|
|
413
|
+
);
|
|
414
|
+
ok(
|
|
415
|
+
`${label}: the window really landed in the seat, and the caller's session is byte-identical`,
|
|
416
|
+
tmux(pi.socket, ["list-windows", "-t", seatSessionId, "-F", "#{window_id}"], pi.inherited).stdout.includes(
|
|
417
|
+
seated.receipt.windowId,
|
|
418
|
+
) &&
|
|
419
|
+
tmux(pi.socket, ["list-windows", "-t", callerSessionId, "-F", "#{window_id}"], pi.inherited).stdout ===
|
|
420
|
+
callerWindowsBefore,
|
|
421
|
+
);
|
|
274
422
|
}
|
|
275
423
|
|
|
276
424
|
// The sender envelope rides INSIDE the mailbox body (` session: <gid> (…)`) — that line
|
|
@@ -295,9 +443,33 @@ async function main(): Promise<void> {
|
|
|
295
443
|
if (sender) siblingGids.add(sender);
|
|
296
444
|
}
|
|
297
445
|
ok(
|
|
298
|
-
"correlation:
|
|
446
|
+
"correlation: every sibling reported a DIFFERENT garden id — the envelope identifies each one, not the launcher",
|
|
299
447
|
siblingGids.size === arrived.size && !siblingGids.has(callerGid),
|
|
300
448
|
);
|
|
449
|
+
|
|
450
|
+
// ── #105 acceptance 3: the seat and the cwd are ORTHOGONAL ───────────────
|
|
451
|
+
// Read AFTER a delay and from the stable %pane, never from the launch receipt: at
|
|
452
|
+
// creation time `#{pane_current_path}` reports the tmux SERVER's cwd in every cell
|
|
453
|
+
// (measured ×2), which is why the receipt reports the REQUESTED cwd and this is the
|
|
454
|
+
// acceptance that reports the observed one. The 2×2 is {cwd omitted, cwd requested} ×
|
|
455
|
+
// {caller session, seat}, and the seat's own `session_path` is a fourth directory that
|
|
456
|
+
// no cell may ever land in.
|
|
457
|
+
spawnSync("sleep", [PANE_CWD_SETTLE_SECONDS]);
|
|
458
|
+
for (const cell of cwdCells) {
|
|
459
|
+
const observed = tmux(
|
|
460
|
+
cell.socket,
|
|
461
|
+
["display-message", "-p", "-t", cell.paneId, "#{pane_current_path}"],
|
|
462
|
+
cell.env,
|
|
463
|
+
).stdout;
|
|
464
|
+
ok(
|
|
465
|
+
`cwd x seat: ${cell.label} settled in ${cell.expect} — never the target session's path, never the other cell's`,
|
|
466
|
+
observed === cell.expect && observed !== SEAT_HOME(root),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
ok(
|
|
470
|
+
"cwd x seat: all four cells ran — {cwd omitted, cwd requested} x {caller session, seat}",
|
|
471
|
+
cwdCells.length === 4 && new Set(cwdCells.map((c) => c.expect)).size === 3,
|
|
472
|
+
);
|
|
301
473
|
ok(
|
|
302
474
|
"fence: those sibling records live in the FIXTURE store",
|
|
303
475
|
[...siblingGids].every(
|
|
@@ -22,7 +22,10 @@ wait_carrier() {
|
|
|
22
22
|
local pid="$1" key="$2" i=0
|
|
23
23
|
while [ "$i" -lt 50 ]; do
|
|
24
24
|
if kill -0 "$pid" 2>/dev/null && [ -r "/proc/$pid/environ" ]; then
|
|
25
|
-
|
|
25
|
+
# `-c` not `-q`, for the reason in omp-bridge-doctor.sh's carrier scan: under
|
|
26
|
+
# pipefail an early-exiting grep SIGPIPEs `tr` and the pipeline reports failure even
|
|
27
|
+
# on a match. Here that only costs a retry, but the hazard is the same one.
|
|
28
|
+
if [ "$(tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -cE "^${key}" || true)" -gt 0 ]; then
|
|
26
29
|
return 0
|
|
27
30
|
fi
|
|
28
31
|
fi
|
|
@@ -103,7 +106,7 @@ CARRIER_PID=$!
|
|
|
103
106
|
wait_carrier "$CARRIER_PID" "PI_SESSION_ID=foreign-garden"
|
|
104
107
|
if OUT="$(ENTWURF_OMP_CARRIER_PIDS="$CARRIER_PID" "$RUN" doctor-omp-bridge 2>&1)"; then
|
|
105
108
|
printf '%s\n' "$OUT" >&2
|
|
106
|
-
die "doctor accepted the live omp fixture with nonblank inherited PI carriers"
|
|
109
|
+
die "[QK:OMP-DOCTOR-CARRIER-NONBLANK-IS-RED] doctor accepted the live omp fixture with nonblank inherited PI carriers"
|
|
107
110
|
fi
|
|
108
111
|
printf '%s\n' "$OUT" | grep -q "live omp process(es).* $CARRIER_PID" || die "doctor red without naming the nonblank-carrier fixture"
|
|
109
112
|
ok "a nonblank inherited PI carrier on a live omp process remains red"
|