@junghanacs/entwurf 0.25.1 → 0.30.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.
Files changed (86) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +190 -0
  4. package/DELIVERY.md +1 -1
  5. package/README.md +183 -674
  6. package/VERIFY.md +21 -17
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +115 -18
  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 +55 -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 +13 -13
  24. package/pi-extensions/entwurf-control.ts +50 -637
  25. package/pi-extensions/lib/acp/backend-adapter.ts +81 -1
  26. package/pi-extensions/lib/acp/backend.ts +41 -5
  27. package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
  28. package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
  29. package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
  30. package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
  31. package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
  32. package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
  33. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
  34. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
  35. package/pi-extensions/lib/fresh-call-composition.ts +13 -6
  36. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  37. package/pi-extensions/lib/mux-fresh-call.ts +3 -3
  38. package/run.sh +177 -53
  39. package/scripts/check-entwurf-peers-surface.ts +8 -15
  40. package/scripts/check-entwurf-self-address.ts +7 -11
  41. package/scripts/check-entwurf-v2-surface.ts +20 -112
  42. package/scripts/check-fresh-call-dispatch.ts +12 -13
  43. package/scripts/check-gate-qualification.ts +250 -5
  44. package/scripts/check-herdr-plugin-build.ts +159 -57
  45. package/scripts/check-install-surface.ts +3 -2
  46. package/scripts/check-mux-resume-call.ts +24 -35
  47. package/scripts/check-pi-bridge-sender.ts +130 -0
  48. package/scripts/check-pi-mcp-bridge.ts +251 -0
  49. package/scripts/fixtures/herdr-supply.json +8 -8
  50. package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
  51. package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
  52. package/scripts/lib/mutation-qualify.ts +269 -44
  53. package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
  54. package/scripts/meta-bridge-doctor.sh +16 -5
  55. package/scripts/mutants/acp-foreground-only.json +17 -0
  56. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  57. package/scripts/mutants/acp-vendor-rejection.json +62 -0
  58. package/scripts/mutants/codex-native.json +11 -11
  59. package/scripts/mutants/control-send-receipt.json +6 -4
  60. package/scripts/mutants/copilot-birth.json +2 -1
  61. package/scripts/mutants/fresh-call-dispatch.json +3 -14
  62. package/scripts/mutants/gate-qualification.json +59 -0
  63. package/scripts/mutants/herdr-plugin-build.json +11 -0
  64. package/scripts/mutants/herdr-supply.json +3 -3
  65. package/scripts/mutants/mux-fresh-call.json +43 -45
  66. package/scripts/mutants/mux-resume-call.json +11 -35
  67. package/scripts/mutants/pack-install.json +15 -4
  68. package/scripts/mutants/pi-bridge-sender.json +54 -0
  69. package/scripts/mutants/pi-floor.json +6 -2
  70. package/scripts/mutants/pi-mcp-bridge.json +36 -0
  71. package/scripts/mutants/pi-mcp-register.json +51 -0
  72. package/scripts/mutants/v2-surface.json +6 -6
  73. package/scripts/resident-rpc-drive.ts +5 -1
  74. package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
  75. package/scripts/smoke-acp-cortex-live.ts +3 -1
  76. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  77. package/scripts/smoke-acp-session-reuse-live.ts +29 -12
  78. package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
  79. package/scripts/smoke-acp-v2-send-live.ts +3 -1
  80. package/scripts/smoke-codex-fresh-live.ts +20 -1
  81. package/scripts/smoke-entwurf-chain-live.ts +6 -2
  82. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
  83. package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
  84. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  85. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  86. package/scripts/smoke-resident-garden-guard.sh +3 -1
@@ -0,0 +1,36 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "pi-mcp-bridge",
4
+ "mutants": [
5
+ {
6
+ "claim": "PI-MCP-BRIDGE-DECLARED-SURFACE",
7
+ "title": "entwurf_self joins the hidden list, so a pi model is no longer declared the verb that tells it who it is",
8
+ "subject": "pi-extensions/lib/entwurf-mcp-server.ts",
9
+ "find": [
10
+ "export const ENTWURF_MCP_HIDDEN_TOOLS = [\"entwurf_inbox_read\", \"entwurf_register_native\"] as const;"
11
+ ],
12
+ "replace": [
13
+ "export const ENTWURF_MCP_HIDDEN_TOOLS = [\"entwurf_inbox_read\", \"entwurf_register_native\", \"entwurf_self\"] as const;"
14
+ ],
15
+ "gate": ["bash", "run.sh", "check-pi-mcp-bridge"],
16
+ "timeoutSeconds": 120,
17
+ "signature": "[QK:PI-MCP-BRIDGE-DECLARED-SURFACE]",
18
+ "signatureSource": "scripts/check-pi-mcp-bridge.ts",
19
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
20
+ },
21
+ {
22
+ "claim": "PI-MCP-BRIDGE-SAME-CHILD-IDENTITY",
23
+ "title": "the explicit carrier is built from the model label when one is known, so the spawned child claims an identity no record holds",
24
+ "subject": "pi-extensions/lib/entwurf-mcp-server.ts",
25
+ "find": ["\t\tenv: { PI_SESSION_ID: input.gardenId, ...(input.agentId ? { PI_AGENT_ID: input.agentId } : {}) },"],
26
+ "replace": [
27
+ "\t\tenv: { PI_SESSION_ID: input.agentId ?? input.gardenId, ...(input.agentId ? { PI_AGENT_ID: input.agentId } : {}) },"
28
+ ],
29
+ "gate": ["bash", "run.sh", "check-pi-mcp-bridge"],
30
+ "timeoutSeconds": 120,
31
+ "signature": "[QK:PI-MCP-BRIDGE-SAME-CHILD-IDENTITY]",
32
+ "signatureSource": "scripts/check-pi-mcp-bridge.ts",
33
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "pi-mcp-register",
4
+ "mutants": [
5
+ {
6
+ "claim": "PI-MCP-REGISTER-AFTER-BIRTH",
7
+ "title": "the extension factory registers the bridge at load, before any record exists — the server is on Pi's registry with no born identity to carry",
8
+ "subject": "pi-extensions/entwurf-control.ts",
9
+ "find": ["\tpi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);"],
10
+ "replace": [
11
+ "\tpi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);",
12
+ "\tpi.registerMcpServer(ENTWURF_MCP_SERVER_NAME, { command: process.execPath, args: [] });"
13
+ ],
14
+ "gate": ["bash", "run.sh", "check-pi-mcp-register"],
15
+ "timeoutSeconds": 60,
16
+ "signature": "[QK:PI-MCP-REGISTER-AFTER-BIRTH]",
17
+ "signatureSource": "pi-extensions/entwurf-control.test.ts",
18
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
19
+ },
20
+ {
21
+ "claim": "PI-MCP-IDENTITY-BEFORE-UI",
22
+ "title": "the status line is rendered before the registration, so a UI failure leaves a born citizen without its verbs",
23
+ "subject": "pi-extensions/entwurf-control.ts",
24
+ "find": [
25
+ "\t\tregisterEntwurfMcpServer(pi, ctx, birth.gardenId);",
26
+ "\t\tupdateStatus(ctx, true, birth.gardenId);"
27
+ ],
28
+ "replace": [
29
+ "\t\tupdateStatus(ctx, true, birth.gardenId);",
30
+ "\t\tregisterEntwurfMcpServer(pi, ctx, birth.gardenId);"
31
+ ],
32
+ "gate": ["bash", "run.sh", "check-pi-mcp-register"],
33
+ "timeoutSeconds": 60,
34
+ "signature": "[QK:PI-MCP-IDENTITY-BEFORE-UI]",
35
+ "signatureSource": "pi-extensions/entwurf-control.test.ts",
36
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
37
+ },
38
+ {
39
+ "claim": "PI-MCP-EXPOSURE",
40
+ "title": "the verbs are registered behind codemode instead of declared directly, so the model is handed a different surface than every other host",
41
+ "subject": "pi-extensions/lib/entwurf-mcp-server.ts",
42
+ "find": ["\t\texposure: \"direct\","],
43
+ "replace": ["\t\texposure: \"codemode\","],
44
+ "gate": ["bash", "run.sh", "check-pi-mcp-register"],
45
+ "timeoutSeconds": 60,
46
+ "signature": "[QK:PI-MCP-EXPOSURE]",
47
+ "signatureSource": "pi-extensions/entwurf-control.test.ts",
48
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
49
+ }
50
+ ]
51
+ }
@@ -39,20 +39,20 @@
39
39
  "signatureSource": "scripts/check-entwurf-v2-production.ts"
40
40
  },
41
41
  {
42
- "claim": "V2SURF-PI-MODE-SCOPE",
43
- "title": "pi-native mode param reverts to \"Delivery mode for a live send\" (false for native-push) (#120 P2: the description was rewritten to stop claiming an interrupt, so the mutation now strips the rail scope from the corrected sentence)",
44
- "subject": "pi-extensions/entwurf-control.ts",
42
+ "claim": "V2SURF-MODE-SCOPE",
43
+ "title": "the bridge mode param (the one every caller reads, pi included since #125) reverts to \"Delivery mode for a live send\" (false for native-push) (#120 P2: the description was rewritten to stop claiming an interrupt, so the mutation now strips the rail scope from the corrected sentence)",
44
+ "subject": "mcp/entwurf-bridge/src/index.ts",
45
45
  "find": [
46
- "\t\t\t\t\t\"Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. \" +",
46
+ "\t\t\t\t\"Injection style for a CONTROL-SOCKET send only: steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. \" +",
47
47
  "\t\t\t\t\t\"The mailbox and native-push plans carry no mode, so it has no effect on those rails.\","
48
48
  ],
49
49
  "replace": [
50
- "\t\t\t\t\t\"Delivery mode for a live send: steer (ask the steering queue) or follow_up (ask the follow-up queue). \" +",
50
+ "\t\t\t\t\"Delivery mode for a live send: steer (ask the steering queue) or follow_up (ask the follow-up queue). \" +",
51
51
  "\t\t\t\t\t\"Both queues are volatile process memory.\","
52
52
  ],
53
53
  "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
54
54
  "timeoutSeconds": 60,
55
- "signature": "[QK:V2SURF-PI-MODE-SCOPE]",
55
+ "signature": "[QK:V2SURF-MODE-SCOPE]",
56
56
  "signatureSource": "scripts/check-entwurf-v2-surface.ts"
57
57
  },
58
58
  {
@@ -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,7 +3,7 @@
3
3
  // LIVE=1 ./run.sh smoke-acp-raw-turn-live
4
4
  //
5
5
  // What this proves (and ONLY this): the pinned Claude ACP adapter
6
- // (@agentclientprotocol/claude-agent-acp@0.79.0) spawns, speaks the ACP wire
6
+ // (@agentclientprotocol/claude-agent-acp, exact pin in package.json) spawns, speaks the ACP wire
7
7
  // protocol over stdio NDJSON, and returns one real model turn. It is the
8
8
  // bytes-flow proof that the S2a dep surface is not just installable but
9
9
  // actually drivable — before any provider/overlay/streamSimple code (S2b+).
@@ -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
  //
@@ -196,10 +196,9 @@ function transcriptRecords(file: string): string[] {
196
196
  }
197
197
 
198
198
  /**
199
- * The BARE verb names, as the two surfaces this smoke reads actually spell them — NOT the
200
- * model-facing dialect. `[측정 2026-09-20, this smoke's own red fixture]` the claude MCP activity
201
- * log carries `Calling MCP tool: entwurf_callback` / `Tool 'entwurf_callback' completed`, and pi
202
- * writes `"type":"toolCall","name":"entwurf_callback"`; neither records
199
+ * The verb names as the CLAUDE MCP activity log spells them — NOT the model-facing dialect.
200
+ * `[측정 2026-09-20, this smoke's own red fixture]` that log carries `Calling MCP tool:
201
+ * entwurf_callback` / `Tool 'entwurf_callback' completed`, never
203
202
  * `mcp__entwurf-bridge__entwurf_callback`. The per-host dialect is owned by
204
203
  * `FRESH_CALL_CALLBACK_TOOL` / `FRESH_CALL_DELIVERY_TOOL` in `fresh-call-composition.ts` and is
205
204
  * what the FRAMING says; these two are what an OBSERVER sees, and conflating them is what this
@@ -208,6 +207,17 @@ function transcriptRecords(file: string): string[] {
208
207
  const CALLBACK_VERB = "entwurf_callback";
209
208
  const DELIVERY_VERB = "entwurf_v2";
210
209
 
210
+ /**
211
+ * The callback as a Pi 1.0 TRANSCRIPT spells it. Pi's built-in MCP is the tool ingress since
212
+ * 0.30.0, and Pi records the qualified name the model called. The bare `entwurf_callback` this
213
+ * smoke read on 2026-09-20 belonged to the retired native-tool registration; a pi→pi child that
214
+ * called back correctly would then never match, which is a red about the oracle, not the rail.
215
+ * Declared here as the OBSERVED literal, not imported from the compositor, so a change to the
216
+ * production framing cannot quietly move this expectation with it — the same rule as
217
+ * `PI_SOURCE_VERBS` in `lib/codex-fresh-source-receipts.ts`. No prefix strip, no legacy OR.
218
+ */
219
+ const PI_CALLBACK_TOOL = "mcp__entwurf_bridge__entwurf_callback";
220
+
211
221
  /**
212
222
  * The id of the CALLBACK toolCall in this record, or null. Pi writes a call and its result as two
213
223
  * records; this is one half of the join that replaces a same-record read.
@@ -238,7 +248,9 @@ function entwurfCallIdFor(record: string, nonce: string, callerGid: string): str
238
248
  if (!Array.isArray(content)) return null;
239
249
  for (const part of content) {
240
250
  const call = part as { type?: unknown; name?: unknown; id?: unknown; arguments?: unknown };
241
- if (call.type !== "toolCall" || call.name !== CALLBACK_VERB) continue;
251
+ if (call.type !== "toolCall") continue;
252
+ if (call.name === CALLBACK_VERB) throw new Error(`${LABEL}: retired raw Pi tool name ${CALLBACK_VERB}`);
253
+ if (call.name !== PI_CALLBACK_TOOL) continue;
242
254
  // Zero-argument verb: absent, null and `{}` are all the shape it is called with. A supplied
243
255
  // target or message is a model naming an address the env already owns — not this act.
244
256
  const args = call.arguments as { message?: unknown; target?: unknown } | undefined | null;
@@ -444,8 +456,10 @@ async function main(): Promise<void> {
444
456
  childBackend: "pi",
445
457
  callerKind: "pi",
446
458
  childKind: "pi",
447
- callerModel: "openai-codex/gpt-5.6-sol",
448
- childModel: "openai-codex/gpt-5.6-sol",
459
+ // The approved roster's lighter Pi GPT tier (agent-config MODELS.md, 2026-09-30); gpt-5.6-sol
460
+ // is still in the catalog but no longer on the roster. A LIVE cost choice, not a product set.
461
+ callerModel: "openai-codex/gpt-6-luna",
462
+ childModel: "openai-codex/gpt-6-luna",
449
463
  },
450
464
  {
451
465
  label: "claude→claude",
@@ -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