@junghanacs/entwurf 0.25.1 → 0.30.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 (77) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +73 -0
  4. package/DELIVERY.md +1 -1
  5. package/README.md +178 -674
  6. package/VERIFY.md +16 -15
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +6 -2
  9. package/docs/external-mcp-host.md +1 -1
  10. package/docs/setup-clean-host.md +346 -10
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +12 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
  22. package/mcp/entwurf-bridge/src/index.ts +30 -3
  23. package/package.json +11 -11
  24. package/pi-extensions/entwurf-control.ts +50 -637
  25. package/pi-extensions/lib/acp/backend-adapter.ts +13 -1
  26. package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
  27. package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
  28. package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
  29. package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
  30. package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
  31. package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
  32. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
  33. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
  34. package/pi-extensions/lib/fresh-call-composition.ts +13 -6
  35. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  36. package/pi-extensions/lib/mux-fresh-call.ts +3 -3
  37. package/run.sh +163 -50
  38. package/scripts/check-entwurf-peers-surface.ts +8 -15
  39. package/scripts/check-entwurf-self-address.ts +7 -11
  40. package/scripts/check-entwurf-v2-surface.ts +20 -112
  41. package/scripts/check-fresh-call-dispatch.ts +12 -13
  42. package/scripts/check-gate-qualification.ts +248 -4
  43. package/scripts/check-install-surface.ts +3 -2
  44. package/scripts/check-mux-resume-call.ts +24 -35
  45. package/scripts/check-pi-bridge-sender.ts +130 -0
  46. package/scripts/check-pi-mcp-bridge.ts +251 -0
  47. package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
  48. package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
  49. package/scripts/lib/mutation-qualify.ts +269 -44
  50. package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
  51. package/scripts/meta-bridge-doctor.sh +16 -5
  52. package/scripts/mutants/acp-foreground-only.json +17 -0
  53. package/scripts/mutants/codex-native.json +11 -11
  54. package/scripts/mutants/control-send-receipt.json +6 -4
  55. package/scripts/mutants/copilot-birth.json +2 -1
  56. package/scripts/mutants/fresh-call-dispatch.json +3 -14
  57. package/scripts/mutants/gate-qualification.json +59 -0
  58. package/scripts/mutants/mux-fresh-call.json +43 -45
  59. package/scripts/mutants/mux-resume-call.json +11 -35
  60. package/scripts/mutants/pack-install.json +15 -4
  61. package/scripts/mutants/pi-bridge-sender.json +54 -0
  62. package/scripts/mutants/pi-floor.json +6 -2
  63. package/scripts/mutants/pi-mcp-bridge.json +36 -0
  64. package/scripts/mutants/pi-mcp-register.json +51 -0
  65. package/scripts/mutants/v2-surface.json +6 -6
  66. package/scripts/resident-rpc-drive.ts +5 -1
  67. package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
  68. package/scripts/smoke-acp-cortex-live.ts +3 -1
  69. package/scripts/smoke-acp-session-reuse-live.ts +29 -12
  70. package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
  71. package/scripts/smoke-acp-v2-send-live.ts +3 -1
  72. package/scripts/smoke-codex-fresh-live.ts +20 -1
  73. package/scripts/smoke-entwurf-chain-live.ts +6 -2
  74. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
  75. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  76. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  77. package/scripts/smoke-resident-garden-guard.sh +3 -1
@@ -169,7 +169,11 @@ const result: DriveResult = {
169
169
  timedOut: false,
170
170
  };
171
171
 
172
- const piArgs = ["--no-extensions", "-e", REPO_ROOT, "--entwurf-control"];
172
+ // `--no-extensions` keeps ambient discovery out, and since Pi 0.99 it also turns the BUILT-IN
173
+ // extensions off: a citizen born here had no MCP, so its entwurf-bridge registration threw
174
+ // `register_mcp_server` (`[측정 2026-10-02]` pi 0.99.2 and 1.0.0 alike). `-e builtin:mcp` restores
175
+ // that one built-in — the MCP a resident has — and nothing else.
176
+ const piArgs = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT, "--entwurf-control"];
173
177
  // NO --session-id. That injection is what the #50 C2 cut removed: pi mints its own
174
178
  // id and the record mints the address. A resume names the FILE, never an id.
175
179
  if (sessionFile) piArgs.push("--session", sessionFile);
@@ -62,7 +62,9 @@ const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
62
62
  const SOCKET_SUFFIX = ".sock";
63
63
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
64
64
  // Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
65
- const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
65
+ // `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
66
+ // registration needs (scripts/resident-rpc-drive.ts has the measurement).
67
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT] as const;
66
68
 
67
69
  const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
68
70
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
@@ -68,7 +68,9 @@ const CORTEX_OVERLAYS_ROOT = path.join(os.homedir(), ".pi", "agent", "cortex-ove
68
68
  const SOCKET_SUFFIX = ".sock";
69
69
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
70
70
  // Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
71
- const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
71
+ // `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
72
+ // registration needs (scripts/resident-rpc-drive.ts has the measurement).
73
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT] as const;
72
74
 
73
75
  const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
74
76
  // The cortex CLI self-extracts on first launch and its newSession alone is
@@ -3,13 +3,20 @@
3
3
  // LIVE=1 ./run.sh smoke-acp-session-reuse-live
4
4
  //
5
5
  // The deterministic gate (check-acp-session-reuse) proves the prompt SCOPE
6
- // (delta-only) and the wiring with a fake seam. This proves the REAL thing: a
7
- // process-scoped pi process drives TWO real ACP turns over ONE reused
8
- // claude-agent-acp child, and turn 2 — which sends ONLY the latest user delta —
9
- // gets an answer that depends on a codeword introduced in turn 1. That is only
10
- // possible if (a) the child/connection were actually reused (not respawned) and
11
- // (b) the live ACP session kept turn 1 in its own history (so the delta was
12
- // enough). A respawn-per-turn backend (S2c) would forget the codeword.
6
+ // (delta-only) and the wiring with a fake seam. This drives the REAL backend
7
+ // (real overlay + spawn + vendor) through TWO ACP turns in one process-scoped
8
+ // process and asserts that turn 2 took the REUSE path.
9
+ //
10
+ // The reuse verdict is the production lifecycle notice, NOT the recall. A
11
+ // respawned (`new`) turn re-sends the FULL transcript (context.ts
12
+ // buildAcpPrompt "new"), and turn 2's context below carries turn 1's codeword, so
13
+ // a respawn-per-turn backend would recall it just as well — recall alone cannot
14
+ // tell reuse from rebuild. Turn 2 must therefore announce `[acp: reusing live
15
+ // session]` and must not announce `[acp: preparing …]`; check-acp-session-reuse
16
+ // pins that the reuse branch emits exactly that and the new branch the opposite.
17
+ // The recall is kept as a supporting check that the reused session still answers
18
+ // from its own history. Delta-only on the wire stays the deterministic gate's
19
+ // claim: the vendor stdio is not observed here.
13
20
  //
14
21
  // It forces process-scoped by pushing the real `--entwurf-control` resident flag
15
22
  // into argv (resolveLifecyclePolicy reads process.argv), then calls the REAL
@@ -175,18 +182,28 @@ async function main(): Promise<void> {
175
182
  if (!r2.done) fail("turn 2 did not complete");
176
183
  console.error(`[smoke-acp-session-reuse-live] turn 2 reply: ${JSON.stringify(r2.text.slice(-120))}`);
177
184
 
178
- // The codeword only lives in turn 1. Turn 2 sent only the delta question, so a
179
- // correct answer proves the reused live ACP session remembered turn 1.
185
+ // The reuse verdict: the backend's own lifecycle notices ride the text stream, so
186
+ // turn 2 names the path it took. A rebuild would announce `preparing` instead.
187
+ assert.ok(
188
+ r2.text.includes("[acp: reusing live session]"),
189
+ `turn 2 did not take the reuse path — no "[acp: reusing live session]" notice (reply: ${JSON.stringify(r2.text.slice(-200))})`,
190
+ );
191
+ assert.ok(
192
+ !r2.text.includes("[acp: preparing"),
193
+ `turn 2 re-bootstrapped a session ("[acp: preparing …]") instead of reusing turn 1's (reply: ${JSON.stringify(r2.text.slice(-200))})`,
194
+ );
195
+ // Supporting only: the reused session still answers from its own history. A rebuild
196
+ // would pass this too (it re-sends the codeword), which is why it is not the verdict.
180
197
  assert.ok(
181
198
  r2.text.includes(codeword),
182
- `turn 2 reply did not contain the turn-1 codeword ${codeword} — reuse/memory failed (reply: ${JSON.stringify(r2.text.slice(-200))})`,
199
+ `turn 2 reply did not contain the turn-1 codeword ${codeword} — the reused session lost its history (reply: ${JSON.stringify(r2.text.slice(-200))})`,
183
200
  );
184
201
 
185
202
  console.log(
186
- "[smoke-acp-session-reuse-live] PASS — 2-turn reuse over one ACP child; turn-2 delta recalled the turn-1 codeword",
203
+ "[smoke-acp-session-reuse-live] PASS — turn 2 took the reuse path ([acp: reusing live session], no re-bootstrap) and recalled the turn-1 codeword",
187
204
  );
188
205
  console.log(` model: entwurf/${MODEL}`);
189
- console.log(` codeword: ${codeword} recalled in turn 2 (delta-only)`);
206
+ console.log(` codeword: ${codeword} recalled in turn 2 (supporting; delta-only is check-acp-session-reuse's claim)`);
190
207
  }
191
208
 
192
209
  main()
@@ -41,7 +41,9 @@ const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
41
41
  const SOCKET_SUFFIX = ".sock";
42
42
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
43
43
  // Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
44
- const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
44
+ // `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
45
+ // registration needs (scripts/resident-rpc-drive.ts has the measurement).
46
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT] as const;
45
47
 
46
48
  const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
47
49
  const POLL_MS = 100;
@@ -95,7 +95,9 @@ const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
95
95
  const SOCKET_SUFFIX = ".sock";
96
96
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
97
97
  // Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
98
- const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
98
+ // `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
99
+ // registration needs (scripts/resident-rpc-drive.ts has the measurement).
100
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT] as const;
99
101
 
100
102
  const BOOT_TIMEOUT_MS = PI_BOOT_TIMEOUT_MS; // shared: pi lock-stale window + boot (see pi-record-discovery)
101
103
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
@@ -75,6 +75,7 @@ import {
75
75
  import {
76
76
  buildCodexInstruction,
77
77
  buildInitialPiPhaseOne,
78
+ buildInitialPiRelay,
78
79
  runCleanupStages,
79
80
  selectReport,
80
81
  } from "./lib/codex-fresh-live-protocol.ts";
@@ -954,6 +955,8 @@ async function run(): Promise<void> {
954
955
  model: piModel,
955
956
  cwd: scratch,
956
957
  task:
958
+ "Session-info already supplies device and time. Do not recheck time, cwd, or identity with shell or other tools. " +
959
+ "Your FIRST tool call must be mcp__entwurf_bridge__entwurf_callback with no arguments, and nothing before it. " +
957
960
  `After your required callback receipt, end the turn and wait passively for one addressed message containing ${initialPiWaitToken}. ` +
958
961
  "That message is the only authority to open the Codex sibling. Do not call shell, sleep, terminal, or any tool to wait.",
959
962
  });
@@ -1070,7 +1073,6 @@ async function run(): Promise<void> {
1070
1073
  codexWaitToken,
1071
1074
  codexModel,
1072
1075
  scratch,
1073
- codexInstruction,
1074
1076
  });
1075
1077
 
1076
1078
  // The model is asked to operate public tools, but every launch and delivery assertion below
@@ -1190,6 +1192,23 @@ async function run(): Promise<void> {
1190
1192
  `requested=${scratch}\npane=${codexPaneCwd}\nrollout-session_meta=${codexRolloutCwd}\n` +
1191
1193
  `record=${codexIdentity.cwd}\napp-server=${appServerCwd}`;
1192
1194
 
1195
+ // Only after the exact callback and record/cwd joins: instruct the initial Pi to relay,
1196
+ // never deliver the Codex payload on its behalf.
1197
+ const relayInitialPi = await bridge.call("entwurf_v2", {
1198
+ target: initialPiGid,
1199
+ intent: "fire-and-forget",
1200
+ message: buildInitialPiRelay({ codexGardenId, codexInstruction }),
1201
+ });
1202
+ receipts["8c-fixture-to-initial-pi-relay"] = relayInitialPi.text;
1203
+ ok(
1204
+ "the fixture addressed the initial Pi relay phase through the control socket",
1205
+ !relayInitialPi.isError &&
1206
+ /entwurf_v2 control-socket → (?:sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/.test(
1207
+ relayInitialPi.text,
1208
+ ),
1209
+ relayInitialPi.text,
1210
+ );
1211
+
1193
1212
  const initialPiToCodex = await awaitOrRecover("the initial Pi joined native-push receipt", SOURCE_WAIT_MS, () => {
1194
1213
  const piSources = piSourceToolReceipts(readInitialPiEntries());
1195
1214
  assertSourceCallScopes(piSources, "entwurf_v2", "target", [callerGid, codexGardenId], "initial Pi v2 roles");
@@ -67,7 +67,9 @@ import { terminateChild } from "./lib/acp-child-cleanup.ts";
67
67
  import { skipLive } from "./lib/live-skip.ts";
68
68
 
69
69
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
70
- const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
70
+ // `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
71
+ // registration needs (scripts/resident-rpc-drive.ts has the measurement).
72
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT] as const;
71
73
  const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
72
74
  const SOCKET_SUFFIX = ".sock";
73
75
 
@@ -329,9 +331,11 @@ async function main(): Promise<void> {
329
331
  `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidD}, intent=fire-and-forget,`,
330
332
  `and message=${nonce} terminus. Then reply with only the tool's outcome line.`,
331
333
  ].join(" ");
334
+ // hop2 is read by B, a NATIVE pi citizen: Pi folds the server key's hyphen, so B is declared
335
+ // `mcp__entwurf_bridge__entwurf_v2`. hop1 (Claude Code) and hop3 (ACP Claude) keep the hyphen.
332
336
  const hop2 = [
333
337
  `${nonce} hop2. You are the second hop of a delivery chain.`,
334
- `Call the tool mcp__entwurf-bridge__entwurf_v2 exactly once with target=${gidC}, intent=fire-and-forget,`,
338
+ `Call the tool mcp__entwurf_bridge__entwurf_v2 exactly once with target=${gidC}, intent=fire-and-forget,`,
335
339
  "and message set to exactly the text between INNER-BEGIN and INNER-END.",
336
340
  "Then reply with only the tool's outcome line.",
337
341
  "INNER-BEGIN",
@@ -76,7 +76,9 @@ const SOCKET_SUFFIX = ".sock";
76
76
  // Release-gate topology: repo-under-test, not deployment smoke. Load only this
77
77
  // checkout's extension so resident behavior is independent of global pi packages.
78
78
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
79
- const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
79
+ // `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
80
+ // registration needs (scripts/resident-rpc-drive.ts has the measurement).
81
+ const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", "builtin:mcp", "-e", REPO_ROOT] as const;
80
82
 
81
83
  // Staged timeouts (automation): short and per-stage so a stall is attributable.
82
84
  //
@@ -491,7 +491,7 @@ for (const name of ["entwurf", "entwurf-bridge", "entwurf-statusline"]) {
491
491
 
492
492
  // The REAL subcommand, from the installed bin, under node_modules — which is the branch of
493
493
  // start.sh that runs the prebuilt dist. Its own oracle is an EXACT set (`EXPECT_TOOLS` in run.sh check-bridge), so a
494
- // zero exit here is the artifact listing all seven garden verbs and no eighth.
494
+ // zero exit here is the artifact listing all eight garden verbs and no ninth.
495
495
  const run = spawnSync(path.join(binDir, "entwurf"), ["check-bridge"], { encoding: "utf8" });
496
496
  const said = `${run.stdout ?? ""}${run.stderr ?? ""}`;
497
497
  // [a-z0-9_] and not [a-z_]: the first draft of this line printed `entwurf_v` for entwurf_v2
@@ -108,7 +108,7 @@ const BRIDGE_LAUNCHER = path.join(REPO, "mcp", "entwurf-bridge", "start.sh");
108
108
 
109
109
  /** The two pi shapes. They differ in exactly one thing that matters here — whether the record
110
110
  * carries `provider=entwurf`, which is what makes the resume argv re-inject the bridge (#29). */
111
- const PI_NATIVE_MODEL = "openai-codex/gpt-5.6-luna";
111
+ const PI_NATIVE_MODEL = "openai-codex/gpt-6-luna";
112
112
  const PI_ACP_MODEL = "entwurf/claude-sonnet-5";
113
113
  const CLAUDE_MODEL = "claude-sonnet-5";
114
114
 
@@ -91,7 +91,9 @@ TIMEOUT="${SMOKE_RGG_TIMEOUT:-90}"
91
91
  # Release-gate topology: this is a repo-under-test smoke, not a deployment smoke.
92
92
  # Load ONLY this checkout's extension so results do not depend on device-local global
93
93
  # packages / current branch wiring.
94
- REPO_EXTENSION_ARGS=(--no-extensions -e "$REPO")
94
+ # `builtin:mcp`: `--no-extensions` also turns off Pi's built-in MCP, which a citizen's entwurf-bridge
95
+ # registration needs (scripts/resident-rpc-drive.ts has the measurement).
96
+ REPO_EXTENSION_ARGS=(--no-extensions -e builtin:mcp -e "$REPO")
95
97
 
96
98
  pass=0
97
99
  fail=0