@junghanacs/entwurf 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/BASELINE.md +57 -203
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +117 -261
- package/README.md +68 -408
- package/VERIFY.md +58 -87
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +231 -0
- package/docs/external-mcp-host.md +132 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +123 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +21 -48
- 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 +3 -3
- package/mcp/entwurf-bridge/src/index.ts +21 -48
- package/package.json +12 -9
- package/pi-extensions/entwurf-control.ts +20 -49
- 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 +305 -36
- 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 +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +244 -108
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-sdk-surface.ts +60 -21
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-entwurf-v2-surface.ts +41 -0
- package/scripts/check-gate-qualification.ts +6 -3
- package/scripts/check-probe-ordering.ts +39 -7
- package/scripts/check-release-gate-outcomes.ts +332 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/probe-ordering.json +27 -5
- package/scripts/mutants/release-gate.json +105 -0
- package/scripts/mutants/v2-surface.json +26 -15
- 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 +4 -3
- 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 +2 -2
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +2 -4
- package/scripts/smoke-meta-install-state.sh +4 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// P3 — the cross-harness delivery CHAIN, on real authenticated rails.
|
|
2
|
+
//
|
|
3
|
+
// LIVE=1 ./run.sh smoke-entwurf-chain-live
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS. Every v2 delivery proof we had covered ONE hop between TWO
|
|
6
|
+
// citizens on ONE rail: matrix-live dispatches programmatically, v2-send-live has
|
|
7
|
+
// an ACP model send into a mailbox, bundled-mcp-live only reads identity. The
|
|
8
|
+
// claim the project actually makes — that independent harnesses address each
|
|
9
|
+
// other by garden id — is a CHAIN claim, and nothing exercised a chain. A gate
|
|
10
|
+
// that never links three harnesses cannot catch the failure where hop 2 loses the
|
|
11
|
+
// sender identity hop 1 carried, or where a rail change breaks only the seam
|
|
12
|
+
// between two backends.
|
|
13
|
+
//
|
|
14
|
+
// THE CHAIN (each arrow is a real model turn on a real authenticated backend):
|
|
15
|
+
//
|
|
16
|
+
// A native Claude Code --entwurf_v2--> B pi / openai-codex (GPT)
|
|
17
|
+
// B --entwurf_v2--> C pi / entwurf claude-sonnet-5 (ACP)
|
|
18
|
+
// C --entwurf_v2--> D mailbox-backed self-fetch citizen
|
|
19
|
+
//
|
|
20
|
+
// Three harnesses, three rails: A is a native self-fetch citizen born by its own
|
|
21
|
+
// SessionStart hook, B and C are live control-socket citizens, and D is the
|
|
22
|
+
// mailbox terminus. The terminus is what makes the READ-RECEIPT axis observable
|
|
23
|
+
// at all — a control-socket delivery has no receipt to stamp, so a chain that
|
|
24
|
+
// ended at C could not produce one. Draining D through the production inbox
|
|
25
|
+
// reader stamps `lastReadAt`, and that is the receipt.
|
|
26
|
+
//
|
|
27
|
+
// WHAT IS REAL AND WHAT IS SMOKE-OWNED (state this honestly or the evidence is
|
|
28
|
+
// worth nothing):
|
|
29
|
+
// - REAL: A's meta-record and its first arming are minted by the operator's own
|
|
30
|
+
// Claude Code SessionStart hook; all three deliveries are production
|
|
31
|
+
// `entwurf_v2` dispatches chosen by the decider; all three model turns are
|
|
32
|
+
// real authenticated calls; every sender envelope is the real one.
|
|
33
|
+
// - SMOKE-OWNED: the isolated world (store / mailbox / receivers under one temp
|
|
34
|
+
// root) so the operator's live garden is never paged, and D's arming marker.
|
|
35
|
+
// D is a seeded citizen exactly as smoke-acp-v2-send-live seeds its peer.
|
|
36
|
+
// - NOT CLAIMED: this proves the chain LINKS, not that any model chooses the
|
|
37
|
+
// entwurf surface on its own. Each hop is TOLD which tool to call, which is
|
|
38
|
+
// why this is a MUST-tier delivery gate and not a BEHAVIOR-lane signal (the
|
|
39
|
+
// lane note in run.sh draws exactly that line).
|
|
40
|
+
//
|
|
41
|
+
// The nonce is the thread: it is minted here, embedded in A's instruction, and
|
|
42
|
+
// must arrive at D having been re-sent by B and again by C. A nonce at D proves
|
|
43
|
+
// the payload traversed every hop — no hop can fabricate it.
|
|
44
|
+
//
|
|
45
|
+
// Sender identity is proven the same way v2-send-live proves it: no hop is ever
|
|
46
|
+
// told its OWN garden id, so a gid can only appear downstream because the real
|
|
47
|
+
// envelope carried it.
|
|
48
|
+
|
|
49
|
+
import { strict as assert } from "node:assert";
|
|
50
|
+
import { type ChildProcess, spawn, spawnSync } from "node:child_process";
|
|
51
|
+
import crypto from "node:crypto";
|
|
52
|
+
import { existsSync } from "node:fs";
|
|
53
|
+
import fsp from "node:fs/promises";
|
|
54
|
+
import os from "node:os";
|
|
55
|
+
import path from "node:path";
|
|
56
|
+
import { fileURLToPath } from "node:url";
|
|
57
|
+
import { readMetaInbox, upsertMetaSession, writeMetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
|
|
58
|
+
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
59
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
60
|
+
|
|
61
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
62
|
+
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
63
|
+
const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
|
|
64
|
+
const SOCKET_SUFFIX = ".sock";
|
|
65
|
+
|
|
66
|
+
const GPT_TARGET = process.env.ENTWURF_CHAIN_GPT_TARGET?.trim() || "openai-codex/gpt-5.4";
|
|
67
|
+
const ACP_TARGET = process.env.ENTWURF_CHAIN_ACP_TARGET?.trim() || "entwurf/claude-sonnet-5";
|
|
68
|
+
const BOOT_TIMEOUT_MS = 45_000;
|
|
69
|
+
const CLAUDE_TURN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_CLAUDE_TIMEOUT_MS) || 240_000;
|
|
70
|
+
const CHAIN_TIMEOUT_MS = Number(process.env.ENTWURF_CHAIN_TIMEOUT_MS) || 420_000;
|
|
71
|
+
const POLL_MS = 250;
|
|
72
|
+
|
|
73
|
+
let passed = 0;
|
|
74
|
+
function ok(label: string, cond: boolean): void {
|
|
75
|
+
assert.ok(cond, `smoke-entwurf-chain-live: ${label}`);
|
|
76
|
+
console.log(` ok ${label}`);
|
|
77
|
+
passed++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function splitTarget(t: string): [provider: string, model: string] {
|
|
81
|
+
const i = t.indexOf("/");
|
|
82
|
+
assert.ok(i > 0, `target must be <provider>/<model>, got ${t}`);
|
|
83
|
+
return [t.slice(0, i), t.slice(i + 1)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
87
|
+
|
|
88
|
+
async function readRecords(dir: string): Promise<Array<{ gardenId: string; backend: string }>> {
|
|
89
|
+
const out: Array<{ gardenId: string; backend: string }> = [];
|
|
90
|
+
for (const f of await fsp.readdir(dir).catch(() => [] as string[])) {
|
|
91
|
+
if (!f.endsWith(".meta.json")) continue;
|
|
92
|
+
try {
|
|
93
|
+
const r = JSON.parse(await fsp.readFile(path.join(dir, f), "utf8")) as Record<string, unknown>;
|
|
94
|
+
if (typeof r.gardenId === "string" && typeof r.backend === "string") {
|
|
95
|
+
out.push({ gardenId: r.gardenId, backend: r.backend });
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
/* a half-written record on the next poll is not an error */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Wait for ONE new record of `backend` that is not already in `known`. */
|
|
105
|
+
async function waitForNewRecord(dir: string, backend: string, known: Set<string>, ms: number): Promise<string | null> {
|
|
106
|
+
const until = Date.now() + ms;
|
|
107
|
+
while (Date.now() < until) {
|
|
108
|
+
for (const r of await readRecords(dir)) {
|
|
109
|
+
if (r.backend === backend && !known.has(r.gardenId)) return r.gardenId;
|
|
110
|
+
}
|
|
111
|
+
await sleep(POLL_MS);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function waitForSocket(p: string, ms: number): Promise<boolean> {
|
|
117
|
+
const until = Date.now() + ms;
|
|
118
|
+
while (Date.now() < until) {
|
|
119
|
+
if (existsSync(p)) return true;
|
|
120
|
+
await sleep(POLL_MS);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function main(): Promise<void> {
|
|
126
|
+
if (process.env.LIVE !== "1") {
|
|
127
|
+
skipLive(
|
|
128
|
+
"smoke-entwurf-chain-live",
|
|
129
|
+
"set LIVE=1 to run (drives three real authenticated model turns across Claude Code, pi GPT and pi ACP).",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (spawnSync("bash", ["-lc", "command -v claude"], { encoding: "utf8" }).status !== 0) {
|
|
133
|
+
skipLive("smoke-entwurf-chain-live", "claude not on PATH — hop 1 is a real native Claude Code turn.");
|
|
134
|
+
}
|
|
135
|
+
const [gptProvider, gptModel] = splitTarget(GPT_TARGET);
|
|
136
|
+
const [acpProvider, acpModel] = splitTarget(ACP_TARGET);
|
|
137
|
+
// pi holds each backend's credentials in its own auth store; a missing one is a
|
|
138
|
+
// PREREQUISITE (the operator has not logged that backend in), never a defect.
|
|
139
|
+
const authPath = path.join(os.homedir(), ".pi", "agent", "auth.json");
|
|
140
|
+
let auth: Record<string, unknown> = {};
|
|
141
|
+
try {
|
|
142
|
+
auth = JSON.parse(await fsp.readFile(authPath, "utf8")) as Record<string, unknown>;
|
|
143
|
+
} catch {
|
|
144
|
+
skipLive("smoke-entwurf-chain-live", `no pi auth store at ${authPath} — log in the chain's backends first.`);
|
|
145
|
+
}
|
|
146
|
+
if (!(gptProvider in auth)) {
|
|
147
|
+
skipLive(
|
|
148
|
+
"smoke-entwurf-chain-live",
|
|
149
|
+
`pi has no '${gptProvider}' credentials (hop 2 target ${GPT_TARGET}); log that backend in, or point ` +
|
|
150
|
+
"ENTWURF_CHAIN_GPT_TARGET at one you have.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const world = await fsp.mkdtemp(path.join(os.tmpdir(), "entwurf-chain-"));
|
|
155
|
+
const sessionsDir = path.join(world, "sessions");
|
|
156
|
+
const mailboxDir = path.join(world, "mailbox");
|
|
157
|
+
const receiversDir = path.join(world, "receivers");
|
|
158
|
+
for (const d of [sessionsDir, mailboxDir, receiversDir]) await fsp.mkdir(d, { recursive: true });
|
|
159
|
+
const worldEnv = {
|
|
160
|
+
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
161
|
+
ENTWURF_META_MAILBOX_DIR: mailboxDir,
|
|
162
|
+
ENTWURF_META_RECEIVERS_DIR: receiversDir,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const nonce = `ENTWURF-CHAIN-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
|
|
166
|
+
console.error(`[smoke-entwurf-chain-live] repo: ${REPO_ROOT}`);
|
|
167
|
+
console.error(`[smoke-entwurf-chain-live] world: ${world}`);
|
|
168
|
+
console.error(`[smoke-entwurf-chain-live] hop2: ${GPT_TARGET}`);
|
|
169
|
+
console.error(`[smoke-entwurf-chain-live] hop3: ${ACP_TARGET}`);
|
|
170
|
+
console.error(`[smoke-entwurf-chain-live] nonce: ${nonce}`);
|
|
171
|
+
|
|
172
|
+
// ── D: the mailbox terminus (seeded + armed by this process, like v2-send) ──
|
|
173
|
+
const terminus = upsertMetaSession({
|
|
174
|
+
input: { backend: "claude-code", nativeSessionId: `chain-terminus-${process.pid}`, cwd: world },
|
|
175
|
+
dir: sessionsDir,
|
|
176
|
+
});
|
|
177
|
+
const gidD = terminus.record.gardenId;
|
|
178
|
+
writeMetaReceiverMarker({
|
|
179
|
+
gardenId: gidD,
|
|
180
|
+
backend: "claude-code",
|
|
181
|
+
nativeSessionId: terminus.record.nativeSessionId,
|
|
182
|
+
ownerPid: process.pid,
|
|
183
|
+
armProvenance: "session-start",
|
|
184
|
+
receiversDir,
|
|
185
|
+
});
|
|
186
|
+
console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
|
|
187
|
+
|
|
188
|
+
const children: ChildProcess[] = [];
|
|
189
|
+
let streamB = "";
|
|
190
|
+
let streamC = "";
|
|
191
|
+
try {
|
|
192
|
+
// ── C: the ACP Claude Sonnet citizen (spawned first so B can be told its id) ──
|
|
193
|
+
const known = new Set((await readRecords(sessionsDir)).map((r) => r.gardenId));
|
|
194
|
+
const c = spawn(
|
|
195
|
+
"pi",
|
|
196
|
+
[...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", acpProvider, "--model", acpModel, "--mode", "rpc"],
|
|
197
|
+
{ cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
|
|
198
|
+
);
|
|
199
|
+
children.push(c);
|
|
200
|
+
c.stdout?.on("data", (b: Buffer) => {
|
|
201
|
+
streamC = (streamC + b.toString()).slice(-200_000);
|
|
202
|
+
});
|
|
203
|
+
c.stderr?.on("data", (b: Buffer) => {
|
|
204
|
+
streamC = (streamC + b.toString()).slice(-200_000);
|
|
205
|
+
});
|
|
206
|
+
const gidC = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
|
|
207
|
+
ok(`C birthed its own pi record (${ACP_TARGET})`, gidC !== null);
|
|
208
|
+
known.add(gidC as string);
|
|
209
|
+
ok(
|
|
210
|
+
"C stood up a control socket",
|
|
211
|
+
await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidC}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// ── B: the pi GPT citizen ────────────────────────────────────────────────
|
|
215
|
+
const b = spawn(
|
|
216
|
+
"pi",
|
|
217
|
+
[...REPO_EXTENSION_ARGS, "--entwurf-control", "--provider", gptProvider, "--model", gptModel, "--mode", "rpc"],
|
|
218
|
+
{ cwd: world, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...worldEnv } },
|
|
219
|
+
);
|
|
220
|
+
children.push(b);
|
|
221
|
+
b.stdout?.on("data", (x: Buffer) => {
|
|
222
|
+
streamB = (streamB + x.toString()).slice(-200_000);
|
|
223
|
+
});
|
|
224
|
+
b.stderr?.on("data", (x: Buffer) => {
|
|
225
|
+
streamB = (streamB + x.toString()).slice(-200_000);
|
|
226
|
+
});
|
|
227
|
+
const gidB = await waitForNewRecord(sessionsDir, "pi", known, BOOT_TIMEOUT_MS);
|
|
228
|
+
ok(`B birthed its own pi record (${GPT_TARGET})`, gidB !== null);
|
|
229
|
+
known.add(gidB as string);
|
|
230
|
+
ok(
|
|
231
|
+
"B stood up a control socket",
|
|
232
|
+
await waitForSocket(path.join(REAL_CONTROL_DIR, `${gidB}${SOCKET_SUFFIX}`), BOOT_TIMEOUT_MS),
|
|
233
|
+
);
|
|
234
|
+
ok("B, C and D are three distinct citizens", new Set([gidB, gidC, gidD]).size === 3);
|
|
235
|
+
|
|
236
|
+
// ── A: one real native Claude Code turn that opens the chain ─────────────
|
|
237
|
+
// The instruction each hop receives is the message the previous hop sends.
|
|
238
|
+
// No hop is told its own garden id — that is what makes the downstream
|
|
239
|
+
// sender assertions meaningful.
|
|
240
|
+
const hop3 = [
|
|
241
|
+
`${nonce} hop3. You are the third hop of a delivery chain.`,
|
|
242
|
+
`Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidD}, intent=fire-and-forget,`,
|
|
243
|
+
`and message=${nonce} terminus. Then reply with only the tool's outcome line.`,
|
|
244
|
+
].join(" ");
|
|
245
|
+
const hop2 = [
|
|
246
|
+
`${nonce} hop2. You are the second hop of a delivery chain.`,
|
|
247
|
+
`Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidC}, intent=fire-and-forget,`,
|
|
248
|
+
"and message set to exactly the text between INNER-BEGIN and INNER-END.",
|
|
249
|
+
"Then reply with only the tool's outcome line.",
|
|
250
|
+
"INNER-BEGIN",
|
|
251
|
+
hop3,
|
|
252
|
+
"INNER-END",
|
|
253
|
+
].join("\n");
|
|
254
|
+
const hop1Prompt = [
|
|
255
|
+
"You are the first hop of a delivery chain. Do exactly this and nothing else.",
|
|
256
|
+
`Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidB}, intent=fire-and-forget,`,
|
|
257
|
+
"and message set to exactly the text between BEGIN and END.",
|
|
258
|
+
"Then reply with only the tool's outcome line.",
|
|
259
|
+
"BEGIN",
|
|
260
|
+
hop2,
|
|
261
|
+
"END",
|
|
262
|
+
].join("\n");
|
|
263
|
+
|
|
264
|
+
const claudeRun = spawnSync("claude", ["-p", "--output-format=json", hop1Prompt], {
|
|
265
|
+
cwd: world,
|
|
266
|
+
encoding: "utf8",
|
|
267
|
+
timeout: CLAUDE_TURN_TIMEOUT_MS,
|
|
268
|
+
env: { ...process.env, ...worldEnv },
|
|
269
|
+
});
|
|
270
|
+
const claudeOut = `${claudeRun.stdout ?? ""}\n${claudeRun.stderr ?? ""}`;
|
|
271
|
+
ok("A's native Claude Code turn ran", claudeRun.status === 0);
|
|
272
|
+
|
|
273
|
+
const gidA = await waitForNewRecord(sessionsDir, "claude-code", new Set([gidD]), BOOT_TIMEOUT_MS);
|
|
274
|
+
ok("A was born a garden citizen by its OWN SessionStart hook (not seeded here)", gidA !== null);
|
|
275
|
+
ok("A is distinct from every other citizen", !new Set([gidB, gidC, gidD]).has(gidA as string));
|
|
276
|
+
console.error(`[smoke-entwurf-chain-live] A: ${gidA} (native Claude Code)`);
|
|
277
|
+
|
|
278
|
+
// ── the chain runs on its own from here; wait for the terminus ───────────
|
|
279
|
+
const boxDir = path.join(mailboxDir, gidD);
|
|
280
|
+
const until = Date.now() + CHAIN_TIMEOUT_MS;
|
|
281
|
+
let msgs: string[] = [];
|
|
282
|
+
while (Date.now() < until) {
|
|
283
|
+
msgs = (await fsp.readdir(boxDir).catch(() => [] as string[])).filter(
|
|
284
|
+
(f) => f.endsWith(".msg") || f.endsWith(".msg.delivered"),
|
|
285
|
+
);
|
|
286
|
+
if (msgs.length > 0) break;
|
|
287
|
+
await sleep(POLL_MS);
|
|
288
|
+
}
|
|
289
|
+
if (msgs.length === 0) {
|
|
290
|
+
console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut.slice(0, 1500)}`);
|
|
291
|
+
console.error(`[smoke-entwurf-chain-live] B stream tail:\n${streamB.slice(-2000)}`);
|
|
292
|
+
console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
|
|
293
|
+
}
|
|
294
|
+
ok(`the chain reached the mailbox terminus (${msgs.length} .msg)`, msgs.length === 1);
|
|
295
|
+
ok("the terminus doorbell was poked", existsSync(path.join(boxDir, "inbox.signal")));
|
|
296
|
+
|
|
297
|
+
const body = await fsp.readFile(path.join(boxDir, msgs[0] as string), "utf8");
|
|
298
|
+
|
|
299
|
+
// ── hop-by-hop evidence ─────────────────────────────────────────────────
|
|
300
|
+
// hop 1: B saw the payload AND the real sender identity of a native
|
|
301
|
+
// Claude Code citizen. A never learned its own gid, so its presence in B's
|
|
302
|
+
// stream can only come from the envelope entwurf_v2 rendered.
|
|
303
|
+
ok("hop 1 — B received the chain payload", streamB.includes(nonce));
|
|
304
|
+
ok("hop 1 — B saw A's real garden id as the sender", streamB.includes(gidA as string));
|
|
305
|
+
ok("hop 1 — the sender was rendered replyable to B", /replyable/i.test(streamB));
|
|
306
|
+
|
|
307
|
+
// hop 2: C saw the payload and B's identity, so the chain did not collapse
|
|
308
|
+
// into A talking to C directly.
|
|
309
|
+
ok("hop 2 — C received the chain payload", streamC.includes(nonce));
|
|
310
|
+
ok("hop 2 — C saw B's real garden id as the sender", streamC.includes(gidB as string));
|
|
311
|
+
|
|
312
|
+
// hop 3: the terminus body carries C's identity, its model, and replyable.
|
|
313
|
+
ok("hop 3 — the terminus payload is the original nonce", body.includes(nonce));
|
|
314
|
+
ok("hop 3 — the terminus names C as the sender", body.includes(gidC as string));
|
|
315
|
+
ok(`hop 3 — the terminus names C's backend model (${acpModel})`, body.includes(acpModel));
|
|
316
|
+
ok("hop 3 — the terminus sender is replyable", /replyable/i.test(body));
|
|
317
|
+
ok(
|
|
318
|
+
"the terminus was not reached by a shortcut — A and B are not its sender",
|
|
319
|
+
!body.includes(gidA as string) && !body.includes(gidB as string),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// ── the read receipt: the one axis only the mailbox rail can produce ─────
|
|
323
|
+
// The PRODUCTION reader, not a re-implementation: reading is what stamps the
|
|
324
|
+
// receipt, so a hand-rolled drain would prove nothing about the real path.
|
|
325
|
+
const statePath = path.join(mailboxDir, gidD, "state.json");
|
|
326
|
+
const beforeRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
|
|
327
|
+
const drained = readMetaInbox({ gardenId: gidD, sessionsDir, mailboxDir });
|
|
328
|
+
ok(
|
|
329
|
+
"draining the terminus returned the chain payload",
|
|
330
|
+
drained.messages.some((m) => m.body.includes(nonce)),
|
|
331
|
+
);
|
|
332
|
+
ok(
|
|
333
|
+
"the drain stamped a read receipt (lastReadAt)",
|
|
334
|
+
typeof drained.readAt === "string" && drained.readAt.length > 0,
|
|
335
|
+
);
|
|
336
|
+
const afterRaw = await fsp.readFile(statePath, "utf8").catch(() => "");
|
|
337
|
+
ok(
|
|
338
|
+
"the receipt is durable on disk, not just in the return value",
|
|
339
|
+
/lastReadAt/.test(afterRaw) && afterRaw !== beforeRaw,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
console.error(`[smoke-entwurf-chain-live] chain: ${gidA} → ${gidB} → ${gidC} → ${gidD}`);
|
|
343
|
+
} finally {
|
|
344
|
+
for (const ch of children) await terminateChild(ch).catch(() => undefined);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(
|
|
348
|
+
`[smoke-entwurf-chain-live] ok — ${passed} assertions; three harnesses linked on real authenticated rails`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await main();
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
writeMetaReceiverMarker,
|
|
65
65
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
66
66
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
67
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
67
68
|
import { waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
68
69
|
|
|
69
70
|
// pi's control socket lives at the canonical dir keyed by the RECORD's garden id (#50 C4: the
|
|
@@ -130,8 +131,7 @@ function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
|
|
|
130
131
|
|
|
131
132
|
async function main(): Promise<void> {
|
|
132
133
|
if (process.env.LIVE !== "1") {
|
|
133
|
-
|
|
134
|
-
return;
|
|
134
|
+
skipLive("smoke-entwurf-v2-matrix-live", "set LIVE=1 to run (spawns a real pi + opens a real socket).");
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const { provider, model } = resolveTarget();
|
|
@@ -59,6 +59,7 @@ import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
|
59
59
|
import { resolveResumeLaunchIdentity } from "../pi-extensions/lib/entwurf-v2-spawn-production.ts";
|
|
60
60
|
import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
61
61
|
import { controlSocketPath } from "../pi-extensions/lib/socket-discovery.ts";
|
|
62
|
+
import { skipLive } from "./lib/live-skip.ts";
|
|
62
63
|
|
|
63
64
|
// pi owns the canonical control-socket dir; the resume child opens its socket HERE and the
|
|
64
65
|
// decider's expectedSocketPath is built from the SAME dir — they must coincide (a fresh gid
|
|
@@ -208,10 +209,7 @@ async function pollForNonce(
|
|
|
208
209
|
|
|
209
210
|
async function main(): Promise<void> {
|
|
210
211
|
if (process.env.LIVE !== "1") {
|
|
211
|
-
|
|
212
|
-
"[smoke-entwurf-v2-spawn-resume-live] skipped — set LIVE=1 to run (spawns a real pi child + opens a real socket).",
|
|
213
|
-
);
|
|
214
|
-
return;
|
|
212
|
+
skipLive("smoke-entwurf-v2-spawn-resume-live", "set LIVE=1 to run (spawns a real pi child + opens a real socket).");
|
|
215
213
|
}
|
|
216
214
|
|
|
217
215
|
const { provider, model } = resolveTarget();
|
|
@@ -87,6 +87,10 @@ case "$1${2:+ $2}" in
|
|
|
87
87
|
"mcp get")
|
|
88
88
|
printf '%s\n' "Scope: User config" "Status: ✔ Connected"
|
|
89
89
|
[ "${FAKE_MCP_RC:-0}" != 0 ] && exit "$FAKE_MCP_RC"
|
|
90
|
+
# Give grep -q a scheduling window to consume the match and close the pipe
|
|
91
|
+
# before the long tail starts. Without this, a fast host can buffer the whole
|
|
92
|
+
# tail before grep exits, making the old broken probe spuriously green.
|
|
93
|
+
sleep 0.05
|
|
90
94
|
# Keep writing well past the pipe buffer: a reader that leaves early SIGPIPEs us.
|
|
91
95
|
cat "$FAKE_MCP_TAIL"
|
|
92
96
|
exit $? ;;
|