@junghanacs/entwurf 0.23.1 → 0.24.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 (67) hide show
  1. package/BASELINE.md +1 -0
  2. package/CHANGELOG.md +173 -0
  3. package/FAQ.md +47 -0
  4. package/README.md +5 -3
  5. package/VERIFY.md +2 -2
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +2 -2
  8. package/docs/setup-clean-host.md +8 -7
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
  17. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  18. package/mcp/entwurf-bridge/src/index.ts +41 -1
  19. package/mcp/entwurf-bridge/test.sh +2 -2
  20. package/package.json +8 -7
  21. package/pi-extensions/entwurf-control.ts +56 -0
  22. package/pi-extensions/lib/acp/backend.ts +16 -6
  23. package/pi-extensions/lib/acp/context.ts +30 -10
  24. package/pi-extensions/lib/acp/session-store.ts +2 -2
  25. package/pi-extensions/lib/callback-env.ts +78 -0
  26. package/pi-extensions/lib/compaction-send-guard.ts +4 -1
  27. package/pi-extensions/lib/fresh-call-composition.ts +56 -22
  28. package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
  29. package/pi-extensions/lib/herdr-placement.ts +13 -4
  30. package/pi-extensions/lib/mux-fresh-call.ts +23 -8
  31. package/pi-extensions/meta-bridge-omp.ts +35 -48
  32. package/run.sh +135 -50
  33. package/scripts/check-acp-backend-preflight.ts +117 -9
  34. package/scripts/check-acp-carrier-augment.ts +6 -4
  35. package/scripts/check-acp-event-mapper.ts +17 -6
  36. package/scripts/check-acp-prompt-builder.ts +17 -9
  37. package/scripts/check-acp-prompt-lifecycle.ts +23 -6
  38. package/scripts/check-acp-session-reuse.ts +64 -51
  39. package/scripts/check-acp-session-store.ts +31 -8
  40. package/scripts/check-acp-stop-reason.ts +20 -2
  41. package/scripts/check-acp-stream-hooks.ts +22 -5
  42. package/scripts/check-acp-usage-accounting.ts +33 -9
  43. package/scripts/check-entwurf-bridge-boot.ts +16 -1
  44. package/scripts/check-fresh-call-dispatch.ts +4 -5
  45. package/scripts/check-gate-qualification.ts +44 -5
  46. package/scripts/check-herdr-activation.ts +73 -0
  47. package/scripts/check-herdr-fresh-call.ts +22 -6
  48. package/scripts/check-herdr-sandbox.ts +14 -2
  49. package/scripts/check-install-container.sh +1 -1
  50. package/scripts/check-pi-launch.ts +5 -2
  51. package/scripts/lib/mutation-qualify.ts +1 -1
  52. package/scripts/mutants/acp-backend-preflight.json +20 -0
  53. package/scripts/mutants/bridge-boot-resume.json +26 -0
  54. package/scripts/mutants/herdr-activation.json +16 -0
  55. package/scripts/mutants/herdr-fresh-call.json +11 -0
  56. package/scripts/mutants/mux-fresh-call.json +21 -8
  57. package/scripts/mutants/omp-fresh.json +5 -9
  58. package/scripts/mutants/pack-install.json +3 -3
  59. package/scripts/probe-bridge-command.ts +1 -0
  60. package/scripts/smoke-acp-session-reuse-live.ts +23 -5
  61. package/scripts/smoke-agy-install-state.sh +2 -2
  62. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  63. package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
  64. package/scripts/smoke-herdr-raw-install-live.sh +31 -3
  65. package/scripts/smoke-omp-fresh-live.ts +4 -2
  66. package/scripts/smoke-omp-mcp-state.sh +1 -1
  67. package/scripts/smoke-pi-provider-state.sh +2 -2
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "acp-backend-preflight",
4
+ "mutants": [
5
+ {
6
+ "claim": "ACP-PREFLIGHT-REPLAYS-TRANSCRIPT-TOOLS",
7
+ "title": "reading a `context.tools` field instead of replaying the transcript makes the exclude-tools preflight unfireable on every pi 0.86 turn — the operator is told a tool is excluded while the backend still runs it",
8
+ "subject": "pi-extensions/lib/acp/backend.ts",
9
+ "find": ["\t\t\tconst activeToolNames = getCurrentTools(context.messages).map((t) => t.name);"],
10
+ "replace": [
11
+ "\t\t\tconst activeToolNames =",
12
+ "\t\t\t\t(context as unknown as { tools?: { name: string }[] }).tools?.map((t) => t.name) ?? [\"read\", \"bash\", \"edit\", \"write\"];"
13
+ ],
14
+ "gate": ["bash", "run.sh", "check-acp-backend-preflight"],
15
+ "timeoutSeconds": 300,
16
+ "signature": "[QK:ACP-PREFLIGHT-REPLAYS-TRANSCRIPT-TOOLS]",
17
+ "signatureSource": "scripts/check-acp-backend-preflight.ts"
18
+ }
19
+ ]
20
+ }
@@ -40,6 +40,32 @@
40
40
  "timeoutSeconds": 120,
41
41
  "signature": "[QK:BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET]",
42
42
  "signatureSource": "scripts/check-entwurf-bridge-boot.ts"
43
+ },
44
+ {
45
+ "claim": "BRIDGEBOOT-CALLBACK-REGISTERED",
46
+ "title": "the callback verb is imported but never registered — invisible to every host, while every import-level assertion stays green",
47
+ "subject": "mcp/entwurf-bridge/src/index.ts",
48
+ "find": ["server.tool(\n\t\"entwurf_callback\","],
49
+ "replace": [
50
+ "const _unregisteredCallbackTool = (...a: unknown[]) => void a;\n_unregisteredCallbackTool(\n\t\"entwurf_callback\","
51
+ ],
52
+ "gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
53
+ "timeoutSeconds": 120,
54
+ "signature": "[QK:BRIDGEBOOT-CALLBACK-REGISTERED]",
55
+ "signatureSource": "scripts/check-entwurf-bridge-boot.ts"
56
+ },
57
+ {
58
+ "claim": "BRIDGEBOOT-CALLBACK-ZERO-ARG",
59
+ "title": "a target parameter is added to the callback schema, giving the model a way to supply an address the env pair was meant to own",
60
+ "subject": "mcp/entwurf-bridge/src/index.ts",
61
+ "find": ["\t\"MCP child). No arguments, no fallback to a model-supplied target.\",\n\t{},"],
62
+ "replace": [
63
+ "\t\"MCP child). No arguments, no fallback to a model-supplied target.\",\n\t{ target: z.string().optional() },"
64
+ ],
65
+ "gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
66
+ "timeoutSeconds": 120,
67
+ "signature": "[QK:BRIDGEBOOT-CALLBACK-ZERO-ARG]",
68
+ "signatureSource": "scripts/check-entwurf-bridge-boot.ts"
43
69
  }
44
70
  ]
45
71
  }
@@ -13,6 +13,22 @@
13
13
  "signature": "[QK:HAC-PI-USER-SCOPE-ONLY]",
14
14
  "signatureSource": "scripts/check-herdr-activation.ts"
15
15
  },
16
+ {
17
+ "claim": "HAC-PI-FLOOR-BOTH-DOORS",
18
+ "title": "dropping the pi range check from install-user-scope re-opens the door asymmetry: setup refuses a below-floor pi by name while the herdr activation door writes the same registration in silence, so one host reports two verdicts for one citizen",
19
+ "subject": "run.sh",
20
+ "find": [
21
+ " if ! pi_version_in_range \"${pi_ver:-0}\" \"$pi_range\"; then",
22
+ " echo \"[install-user-scope] pi: FAIL — detected pi ${pi_ver:-<unreadable version>} is outside the supported range $pi_range; nothing was written. Align pi, then re-run.\" >&2",
23
+ " exit 1",
24
+ " fi"
25
+ ],
26
+ "replace": [" : \"$pi_range\""],
27
+ "gate": ["bash", "run.sh", "check-herdr-activation"],
28
+ "timeoutSeconds": 300,
29
+ "signature": "[QK:HAC-PI-FLOOR-BOTH-DOORS]",
30
+ "signatureSource": "scripts/check-herdr-activation.ts"
31
+ },
16
32
  {
17
33
  "claim": "HAC-COMMAND-IS-DERIVED",
18
34
  "title": "accepting any absolute directory as 'the plugin runtime' is a way to point every pi session at an executable of the caller's choosing — 'absolute' was never the contract, 'this host's stable runtime' was",
@@ -443,6 +443,17 @@
443
443
  "timeoutSeconds": 120,
444
444
  "signature": "[QK:HFC-WITNESS-BINDING-COMPLETE]",
445
445
  "signatureSource": "scripts/check-herdr-fresh-call.ts"
446
+ },
447
+ {
448
+ "claim": "HFC-CALLBACK-ENV",
449
+ "title": "the launcher-computed callback pair is dropped from herdr --env, so a herdr sibling's first action has to retype a garden id out of prose",
450
+ "subject": "pi-extensions/lib/herdr-fresh-call.ts",
451
+ "find": ["\t\t...callbackEnvAssignments(params.callback).flatMap((assignment) => [\"--env\", assignment]),"],
452
+ "replace": [""],
453
+ "gate": ["bash", "run.sh", "check-herdr-fresh-call"],
454
+ "timeoutSeconds": 120,
455
+ "signature": "[QK:HFC-CALLBACK-ENV]",
456
+ "signatureSource": "scripts/check-herdr-fresh-call.ts"
446
457
  }
447
458
  ]
448
459
  }
@@ -18,7 +18,7 @@
18
18
  "title": "claude-code goes back to the variadic space form, which swallows the prompt as an option value",
19
19
  "subject": "pi-extensions/lib/fresh-call-composition.ts",
20
20
  "find": [
21
- "\t\t\treturn [composition.prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL[\"claude-code\"]}`, `--model=${model}`];"
21
+ "\t\t\treturn [\n\t\t\t\tcomposition.prompt,\n\t\t\t\t`--allowedTools=${FRESH_CALL_CALLBACK_TOOL[\"claude-code\"]},${FRESH_CALL_DELIVERY_TOOL[\"claude-code\"]}`,\n\t\t\t\t`--model=${model}`,\n\t\t\t];"
22
22
  ],
23
23
  "replace": [
24
24
  "\t\t\treturn [composition.prompt, \"--allowedTools\", FRESH_CALL_CALLBACK_TOOL[\"claude-code\"], `--model=${model}`];"
@@ -55,9 +55,11 @@
55
55
  "title": "Claude Code drops the explicit model token and silently resolves its ambient Opus default again",
56
56
  "subject": "pi-extensions/lib/fresh-call-composition.ts",
57
57
  "find": [
58
- "\t\t\treturn [composition.prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL[\"claude-code\"]}`, `--model=${model}`];"
58
+ "\t\t\treturn [\n\t\t\t\tcomposition.prompt,\n\t\t\t\t`--allowedTools=${FRESH_CALL_CALLBACK_TOOL[\"claude-code\"]},${FRESH_CALL_DELIVERY_TOOL[\"claude-code\"]}`,\n\t\t\t\t`--model=${model}`,\n\t\t\t];"
59
+ ],
60
+ "replace": [
61
+ "\t\t\treturn [\n\t\t\t\tcomposition.prompt,\n\t\t\t\t`--allowedTools=${FRESH_CALL_CALLBACK_TOOL[\"claude-code\"]},${FRESH_CALL_DELIVERY_TOOL[\"claude-code\"]}`,\n\t\t\t];"
59
62
  ],
60
- "replace": ["\t\t\treturn [composition.prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL[\"claude-code\"]}`];"],
61
63
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
62
64
  "timeoutSeconds": 120,
63
65
  "signature": "[QK:FRESHCALL-CLAUDE-MODEL-ARGV]",
@@ -67,8 +69,8 @@
67
69
  "claim": "FRESHCALL-CALLBACK-PRECEDES-TASK",
68
70
  "title": "the task is framed before the callback instruction, so a sibling that stalls in the task never names itself",
69
71
  "subject": "pi-extensions/lib/fresh-call-composition.ts",
70
- "find": ["\t\t`FIRST ACTION, before reading files or anything else: call ${tool} with ` +"],
71
- "replace": ["\t\t`FINAL ACTION, after completing the task: call ${tool} with ` +"],
72
+ "find": ["\t\t\t\t\t`FIRST ACTION, before reading files or anything else: call ${birthTool} with no arguments.`"],
73
+ "replace": ["\t\t\t\t\t`FINAL ACTION, after completing the task: call ${birthTool} with no arguments.`"],
72
74
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
73
75
  "timeoutSeconds": 120,
74
76
  "signature": "[QK:FRESHCALL-CALLBACK-PRECEDES-TASK]",
@@ -275,7 +277,7 @@
275
277
  "claim": "FRESHCALL-COPILOT-CALLBACK-TOOL",
276
278
  "title": "the Copilot callback names Claude Code's `mcp__server__tool` spelling — a dialect copied instead of measured, and the whole first turn is spent looking for a tool that is not there",
277
279
  "subject": "pi-extensions/lib/fresh-call-composition.ts",
278
- "find": ["\tcopilot: \"entwurf-bridge-entwurf_v2\","],
280
+ "find": ["\tcopilot: \"entwurf-bridge-entwurf_callback\","],
279
281
  "replace": ["\tcopilot: \"mcp__entwurf-bridge__entwurf_v2\","],
280
282
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
281
283
  "timeoutSeconds": 180,
@@ -669,7 +671,7 @@
669
671
  "title": "the first turn stops naming where the result goes, so a sibling finishes its task, prints the answer in its own window, and the caller never learns it is done",
670
672
  "subject": "pi-extensions/lib/fresh-call-composition.ts",
671
673
  "find": [
672
- "\t\t`When the task reaches its requested final result, send that result to the same target with ${tool}.`,"
674
+ "\t\t`When the task reaches its requested final result, send that result to the same target with ${deliveryTool}.`,"
673
675
  ],
674
676
  "replace": [""],
675
677
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
@@ -682,13 +684,24 @@
682
684
  "title": "the Claude child loses the one sentence that tells it how to load a deferred callback tool — the framing goes back to naming a tool the child cannot call on its first turn, which is the measured silence: 0/5 directly callable, 5 of 14 production launches dead",
683
685
  "subject": "pi-extensions/lib/fresh-call-composition.ts",
684
686
  "find": [
685
- "\t\"claude-code\": [\n\t\t\"If ${callbackTool} is not callable yet, the entwurf-bridge server is still connecting or its\",\n\t\t'tools are deferred: load them first with ToolSearch(\"select:${callbackTool},${peersTool}\") —',\n\t\t\"it waits for the server — then call ${callbackTool}.\",\n\t],"
687
+ "\t\"claude-code\": [\n\t\t\"If ${callbackTool} is not callable yet, the entwurf-bridge server is still connecting or its\",\n\t\t'tools are deferred: load them first with ToolSearch(\"select:${callbackTool},${deliveryTool},${peersTool}\") —',\n\t\t\"it waits for the server — then call ${callbackTool}.\",\n\t],"
686
688
  ],
687
689
  "replace": ["\t\"claude-code\": [],"],
688
690
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
689
691
  "timeoutSeconds": 300,
690
692
  "signature": "[QK:FRESHCOMP-TOOL-LOAD-HINT-CLAUDE-ONLY]",
691
693
  "signatureSource": "test/fresh-call-composition.contract.test.ts"
694
+ },
695
+ {
696
+ "claim": "FRESHCALL-CALLBACK-ENV",
697
+ "title": "the launcher-computed callback pair is dropped from tmux -e, so the sibling's first action has to retype a garden id out of prose — Hard Rule 2",
698
+ "subject": "pi-extensions/lib/mux-fresh-call.ts",
699
+ "find": ["\t\t...callbackEnvAssignments(callback).flatMap((assignment) => [\"-e\", assignment]),"],
700
+ "replace": [""],
701
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
702
+ "timeoutSeconds": 120,
703
+ "signature": "[QK:FRESHCALL-CALLBACK-ENV]",
704
+ "signatureSource": "test/mux-fresh-call.test.ts"
692
705
  }
693
706
  ]
694
707
  }
@@ -45,7 +45,7 @@
45
45
  "claim": "FRESHCALL-OMP-CALLBACK-TOOL-DIALECT",
46
46
  "title": "the callback tool name spelled the way a reader expects instead of the way omp mints it — `entwurf_v2` keeps its digit, which omp's sanitizer charset [a-z_] eats. The sibling is told to call a tool that does not exist in its session, so the whole first turn is spent and no address ever comes back. A launch receipt still prints",
47
47
  "subject": "pi-extensions/lib/fresh-call-composition.ts",
48
- "find": ["\tomp: \"mcp__entwurf_bridge_entwurf_v\","],
48
+ "find": ["\tomp: \"mcp__entwurf_bridge_entwurf_callback\","],
49
49
  "replace": ["\tomp: \"mcp__entwurf_bridge_entwurf_v2\","],
50
50
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
51
51
  "timeoutSeconds": 300,
@@ -167,10 +167,8 @@
167
167
  "claim": "OMP-BOOTSTRAP-CALLBACK-ONLY",
168
168
  "title": "the task folded back into the stage-one prompt, so the sibling is asked to call back AND to work in one message. This is the prompt the failed LIVE actually ran, minus the missing tool: a competing goal beside the callback is what the two-stage split exists to remove, and nothing about argv or readiness would notice it coming back",
169
169
  "subject": "pi-extensions/meta-bridge-omp.ts",
170
- "find": ["\t\t\tsend(buildOmpCallbackOnlyPrompt({ target: payload.target, nonce: payload.nonce }));"],
171
- "replace": [
172
- "\t\t\tsend(`${buildOmpCallbackOnlyPrompt({ target: payload.target, nonce: payload.nonce })}\\n\\n${payload.task}`);"
173
- ],
170
+ "find": ["\t\t\tsend(buildOmpCallbackOnlyPrompt());"],
171
+ "replace": ["\t\t\tsend(`${buildOmpCallbackOnlyPrompt()}\\n\\n${payload.task}`);"],
174
172
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
175
173
  "timeoutSeconds": 300,
176
174
  "signature": "[QK:OMP-BOOTSTRAP-CALLBACK-ONLY]",
@@ -204,11 +202,9 @@
204
202
  "claim": "OMP-BOOTSTRAP-RESULT-ARMS-ONLY",
205
203
  "title": "the task sent from inside the tool_result handler again, instead of at the turn_end boundary. `[LIVE 2026-08-30]` this is the exact shape that failed: the hook log showed armed/ready/callback-observed/released, the release predicate was provably correct, and the task still never appeared in the session — because that handler runs while the callback turn is streaming. Every static contract about correlation and readiness stays green while the product delivers nothing",
206
204
  "subject": "pi-extensions/meta-bridge-omp.ts",
207
- "find": [
208
- "\t\t\tlog(\"INFO\", `bootstrap-released nonce=${payload.nonce} toolCallId=${callId}: task armed for the next turn_end`);"
209
- ],
205
+ "find": ["\t\t\tlog(\"INFO\", `bootstrap-released toolCallId=${callId}: task armed for the next turn_end`);"],
210
206
  "replace": [
211
- "\t\t\tlog(\"INFO\", `bootstrap-released nonce=${payload.nonce} toolCallId=${callId}: task armed for the next turn_end`);\n\t\t\tphase = \"task-sent\";\n\t\t\tsend(payload.task);"
207
+ "\t\t\tlog(\"INFO\", `bootstrap-released toolCallId=${callId}: task armed for the next turn_end`);\n\t\t\tphase = \"task-sent\";\n\t\t\tsend(payload.task);"
212
208
  ],
213
209
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
214
210
  "timeoutSeconds": 300,
@@ -6,7 +6,7 @@
6
6
  "claim": "PACK-INSTALL-PIN-MATCHER-BOUNDED",
7
7
  "title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes — @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
8
8
  "subject": "run.sh",
9
- "find": [" grep '^@earendil-works+' | grep -Ev '@0\\.85\\.1(_|$)' || true"],
9
+ "find": [" grep '^@earendil-works+' | grep -Ev '@0\\.86\\.0(_|$)' || true"],
10
10
  "replace": [" grep '^@earendil-works+' | grep -v '@0\\.85\\.1' || true"],
11
11
  "gate": ["bash", "run.sh", "check-pack-pin-matcher"],
12
12
  "timeoutSeconds": 120,
@@ -17,8 +17,8 @@
17
17
  "claim": "PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE",
18
18
  "title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure — @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
19
19
  "subject": "run.sh",
20
- "find": [" grep '^@earendil-works+' | grep -Ev '@0\\.85\\.1(_|$)' || true"],
21
- "replace": [" grep '^@earendil-works+pi-' | grep -Ev '@0\\.85\\.1(_|$)' || true"],
20
+ "find": [" grep '^@earendil-works+' | grep -Ev '@0\\.86\\.0(_|$)' || true"],
21
+ "replace": [" grep '^@earendil-works+pi-' | grep -Ev '@0\\.86\\.0(_|$)' || true"],
22
22
  "gate": ["bash", "run.sh", "check-pack-pin-matcher"],
23
23
  "timeoutSeconds": 120,
24
24
  "signature": "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE]",
@@ -67,6 +67,7 @@ export const EXPECTED_TOOLS = [
67
67
  "entwurf_register_native",
68
68
  "entwurf_fresh_call",
69
69
  "entwurf_resume_call",
70
+ "entwurf_callback",
70
71
  ] as const;
71
72
  const DEFAULT_TIMEOUT_MS = 10_000;
72
73
  /** How long a probed child gets to die politely before it is killed outright. */
@@ -24,11 +24,26 @@ import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
24
24
  import { tmpdir } from "node:os";
25
25
  import { dirname, resolve } from "node:path";
26
26
  import { fileURLToPath, pathToFileURL } from "node:url";
27
- import type { Api, AssistantMessageEvent, Context, Model } from "@earendil-works/pi-ai";
27
+ import type { Api, AssistantMessageEvent, Model, TranscriptContext } from "@earendil-works/pi-ai";
28
+ import { normalizeContext } from "@earendil-works/pi-ai";
28
29
  import { skipLive } from "./lib/live-skip.ts";
29
30
 
30
31
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
31
32
  const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-5";
33
+ // The pi built-ins the Claude child also exposes natively. pi declares its tool surface on
34
+ // every turn, and since 0.86 that declaration reaches a provider as the leading system
35
+ // message's `toolsAdded` (`normalizeContext`), which backend.ts replays for the exclude-tools
36
+ // truthfulness preflight. A fixture that declares NO tools is therefore not "a turn with the
37
+ // defaults" — it is a turn where the operator excluded everything, and the preflight rejects it
38
+ // before the prompt is ever sent. So every provider-path context here is built through
39
+ // `normalizeContext`, never typed as a raw `Context`: that annotation is exactly what hid this
40
+ // shape from typecheck when the 0.86 pin landed.
41
+ const PI_DECLARED_TOOLS = ["read", "bash", "edit", "write"].map((name) => ({
42
+ name,
43
+ description: "",
44
+ parameters: {} as never,
45
+ }));
46
+
32
47
  const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
33
48
 
34
49
  function fail(msg: string): never {
@@ -101,7 +116,8 @@ async function main(): Promise<void> {
101
116
  console.error(`[smoke-acp-session-reuse-live] codeword: ${codeword}`);
102
117
 
103
118
  // --- turn 1 (new): introduce the codeword, full transcript ---
104
- const turn1: Context = {
119
+ const turn1: TranscriptContext = normalizeContext({
120
+ tools: PI_DECLARED_TOOLS,
105
121
  messages: [
106
122
  {
107
123
  role: "user",
@@ -109,7 +125,7 @@ async function main(): Promise<void> {
109
125
  timestamp: 0,
110
126
  },
111
127
  ],
112
- };
128
+ });
113
129
  const r1 = await withTimeout(
114
130
  "turn 1",
115
131
  consume(backend.streamShellAcp(model, turn1, options) as Stream),
@@ -121,7 +137,9 @@ async function main(): Promise<void> {
121
137
  console.error("[smoke-acp-session-reuse-live] starting turn 2 reuse prompt");
122
138
 
123
139
  // --- turn 2 (reuse): ask for the codeword, DELTA ONLY ---
124
- const turn2: Context = {
140
+ // turn1.messages already carries the folded system message, so spreading it keeps the
141
+ // declared surface on the reuse turn too; normalizeContext does not double it.
142
+ const turn2: TranscriptContext = normalizeContext({
125
143
  messages: [
126
144
  ...turn1.messages,
127
145
  {
@@ -147,7 +165,7 @@ async function main(): Promise<void> {
147
165
  timestamp: 0,
148
166
  },
149
167
  ],
150
- };
168
+ });
151
169
  const r2 = await withTimeout(
152
170
  "turn 2",
153
171
  consume(backend.streamShellAcp(model, turn2, options) as Stream),
@@ -67,7 +67,7 @@ write_mcp_fake() { # $1 = path
67
67
  while IFS= read -r line; do
68
68
  case "$line" in
69
69
  *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
70
- *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
70
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"},{"name":"entwurf_callback"}]}}' ;;
71
71
  esac
72
72
  done
73
73
  FAKE
@@ -89,7 +89,7 @@ if [ "${REVIEW_ENV:-}" = "fail" ]; then echo configured-env-failure >&2; exit 24
89
89
  while IFS= read -r line; do
90
90
  case "$line" in
91
91
  *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
92
- *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
92
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"},{"name":"entwurf_callback"}]}}' ;;
93
93
  esac
94
94
  done
95
95
  FAKE
@@ -26,7 +26,7 @@ write_mcp_fake() {
26
26
  while IFS= read -r line; do
27
27
  case "$line" in
28
28
  *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
29
- *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
29
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"},{"name":"entwurf_callback"}]}}' ;;
30
30
  esac
31
31
  done
32
32
  FAKE
@@ -186,17 +186,38 @@ function transcriptRecords(file: string): string[] {
186
186
  }
187
187
 
188
188
  /**
189
- * The id of the `entwurf_v2` toolCall in this record that IS the callback, or null. Pi writes a call
190
- * and its result as two records; this is one half of the join that replaces a same-record read.
189
+ * The BARE verb names, as the two surfaces this smoke reads actually spell them NOT the
190
+ * model-facing dialect. `[측정 2026-09-20, this smoke's own red fixture]` the claude MCP activity
191
+ * log carries `Calling MCP tool: entwurf_callback` / `Tool 'entwurf_callback' completed`, and pi
192
+ * writes `"type":"toolCall","name":"entwurf_callback"`; neither records
193
+ * `mcp__entwurf-bridge__entwurf_callback`. The per-host dialect is owned by
194
+ * `FRESH_CALL_CALLBACK_TOOL` / `FRESH_CALL_DELIVERY_TOOL` in `fresh-call-composition.ts` and is
195
+ * what the FRAMING says; these two are what an OBSERVER sees, and conflating them is what this
196
+ * pair of constants exists to stop.
197
+ */
198
+ const CALLBACK_VERB = "entwurf_callback";
199
+ const DELIVERY_VERB = "entwurf_v2";
200
+
201
+ /**
202
+ * The id of the CALLBACK toolCall in this record, or null. Pi writes a call and its result as two
203
+ * records; this is one half of the join that replaces a same-record read.
191
204
  *
192
- * WHAT MAKES IT THE CALLBACK, READ STRUCTURALLY `[sol 재검 2026-09-18]`. An earlier version only
193
- * asked whether the serialised arguments CONTAINED the nonce, which a call carrying `prefix+nonce`,
194
- * or the right nonce to the wrong target, satisfies just as well and then the later, correct call
195
- * is the one that produces the artifact, so the predicate could join a wrong call to a right
196
- * delivery. The arguments are an object, so they are read as one: the message must BE the nonce,
197
- * the target must be the caller that minted it, and the intent must be the one the framing names.
205
+ * WHAT MAKES IT THE CALLBACK `[ff09522]`. It used to be the ARGUMENTS: message === nonce, target
206
+ * === the caller, intent === fire-and-forget. That predicate is gone because the thing it read is
207
+ * gone the birth verb takes ZERO arguments and reads the caller and the nonce out of its own
208
+ * process env, so there is nothing in the call for a model to get wrong and nothing here to
209
+ * compare. What identifies the callback now is the verb plus the ABSENCE of an address: a call
210
+ * carrying a target or a message is a model supplying an address, which is the second address axis
211
+ * Hard Rule 2 refuses, and it is not this act.
212
+ *
213
+ * The nonce↔sender correlation the old predicate carried did not disappear with it — it moved to
214
+ * the DELIVERED ARTIFACT, which this smoke checks independently and earlier ("the child's callback
215
+ * ARRIVED … its body is a production nonce and its sender is the child the direct witness resolved
216
+ * to"). Two records, one event, same as before; only the half that names the call has changed.
198
217
  */
199
218
  function entwurfCallIdFor(record: string, nonce: string, callerGid: string): string | null {
219
+ void nonce;
220
+ void callerGid;
200
221
  let parsed: unknown;
201
222
  try {
202
223
  parsed = JSON.parse(record);
@@ -207,16 +228,14 @@ function entwurfCallIdFor(record: string, nonce: string, callerGid: string): str
207
228
  if (!Array.isArray(content)) return null;
208
229
  for (const part of content) {
209
230
  const call = part as { type?: unknown; name?: unknown; id?: unknown; arguments?: unknown };
210
- if (call.type !== "toolCall" || call.name !== "entwurf_v2") continue;
211
- const args = call.arguments as
212
- | { message?: unknown; target?: unknown; intent?: unknown; wants_reply?: unknown }
213
- | undefined;
214
- if (typeof args !== "object" || args === null) continue;
215
- if (args.message !== nonce) continue;
216
- if (args.target !== callerGid) continue;
217
- if (args.intent !== "fire-and-forget") continue;
218
- // The framing asks for no reply; a call that asked for one is a different act.
219
- if (args.wants_reply === true) continue;
231
+ if (call.type !== "toolCall" || call.name !== CALLBACK_VERB) continue;
232
+ // Zero-argument verb: absent, null and `{}` are all the shape it is called with. A supplied
233
+ // target or message is a model naming an address the env already owns — not this act.
234
+ const args = call.arguments as { message?: unknown; target?: unknown } | undefined | null;
235
+ if (args !== undefined && args !== null) {
236
+ if (typeof args !== "object" || Array.isArray(args)) continue;
237
+ if (args.target !== undefined || args.message !== undefined) continue;
238
+ }
220
239
  return typeof call.id === "string" ? call.id : null;
221
240
  }
222
241
  return null;
@@ -864,23 +883,27 @@ async function main(): Promise<void> {
864
883
  // OFFERS exactly that corroboration ("you can corroborate the caller first if you
865
884
  // want to"), so the old oracle contradicted our own prompt and would have forbidden
866
885
  // the behaviour we asked for. What still must hold is everything the claim was
867
- // actually about — the callback lands before any work, it completes, and no
868
- // entwurf_v2 in the log failed or timed out.
886
+ // actually about — the callback lands before any work, it completes, and neither the
887
+ // callback verb nor the delivery verb failed or timed out anywhere in the log.
888
+ // `[ff09522]` the verb this joins on MOVED: the birth callback is `entwurf_callback`
889
+ // and `entwurf_v2` is now only the DELIVERY the closing line asks for. Joining on the
890
+ // old name does not merely miss — it mis-attributes, finding the result-delivery call
891
+ // and then reporting the real callback as forbidden work before it.
869
892
  // ONLY what the framing actually offers. `entwurf_self` used to sit in this set and
870
893
  // nothing ever proposed it to the child — an allowance for a tool we do not mention
871
894
  // widens the oracle without widening the contract (sol D1, 2026-09-18).
872
895
  const READ_ONLY_FIRST = new Set(["entwurf_peers"]);
873
- const beforeCallback = order.slice(0, Math.max(order.indexOf("entwurf_v2"), 0));
896
+ const beforeCallback = order.slice(0, Math.max(order.indexOf(CALLBACK_VERB), 0));
874
897
  // THE JOIN THIS AXIS CAN ACTUALLY MAKE. The claude MCP log records WHICH tool was
875
898
  // called and whether it completed — never its arguments or its result body — so
876
- // "the first entwurf_v2 completed" alone would also be true of a call that delivered
899
+ // "the first callback completed" alone would also be true of a call that delivered
877
900
  // somebody else's nonce or came back as a semantic reject over a successful
878
901
  // transport. The second artifact closes it: the caller's own delivered message
879
902
  // carries the EXACT nonce and the child as its sender, and its enqueue timestamp has
880
903
  // to fall inside the window of that first call. Two independent records, one event.
881
- const callAt = indexOfEntry(childActivity, (e) => (e.debug ?? "") === "Calling MCP tool: entwurf_v2");
904
+ const callAt = indexOfEntry(childActivity, (e) => (e.debug ?? "") === `Calling MCP tool: ${CALLBACK_VERB}`);
882
905
  const doneAt = indexOfEntry(childActivity, (e) =>
883
- (e.debug ?? "").startsWith("Tool 'entwurf_v2' completed successfully"),
906
+ (e.debug ?? "").startsWith(`Tool '${CALLBACK_VERB}' completed successfully`),
884
907
  );
885
908
  const callbackStamp = deliveredMessages(String(fenced.ENTWURF_META_MAILBOX_DIR), callerGid)[0]?.file ?? "";
886
909
  const stampedAt = Date.parse(
@@ -902,14 +925,19 @@ async function main(): Promise<void> {
902
925
  // here, and a claim about "any work" is wider than the oracle. What IS observed, and
903
926
  // is the thing the framing actually asks for, is the ORDER OF TOOLS: the first
904
927
  // non-read-only tool this child called was the callback.
905
- `${cell.label}: the child's FIRST non-read-only tool call was the callbackthe FIRST entwurf_v2 completed successfully and the delivered callback carrying this exact nonce was enqueued inside that call's own window, preceded only by the read-only corroboration the framing offers, with no failed or timed-out entwurf_v2 anywhere in its log (this cell reads tool activity only; assistant text is not observable on this rail)`,
928
+ `${cell.label}: the child's FIRST non-read-only tool call was the zero-argument ${CALLBACK_VERB} it completed successfully and the delivered callback carrying this exact nonce was enqueued inside that call's own window, preceded only by the read-only corroboration the framing offers, with neither ${CALLBACK_VERB} nor ${DELIVERY_VERB} failing or timing out anywhere in its log (this cell reads tool activity only; assistant text is not observable on this rail)`,
906
929
  childActivity.length > 0 &&
907
- order.includes("entwurf_v2") &&
930
+ order.includes(CALLBACK_VERB) &&
908
931
  beforeCallback.every((name) => READ_ONLY_FIRST.has(name)) &&
909
932
  within &&
910
933
  callbackArrived &&
911
- !childActivity.some(
912
- (e) => (e.debug ?? "").startsWith("Tool 'entwurf_v2' failed") || (e.error ?? "").includes("entwurf_v2"),
934
+ // BOTH verbs, because the child uses both: the birth callback and, after the
935
+ // work, the delivery verb the framing's closing line names. A failed delivery
936
+ // is as fatal to this rail as a failed callback.
937
+ ![CALLBACK_VERB, DELIVERY_VERB].some((verb) =>
938
+ childActivity.some(
939
+ (e) => (e.debug ?? "").startsWith(`Tool '${verb}' failed`) || (e.error ?? "").includes(verb),
940
+ ),
913
941
  ),
914
942
  );
915
943
  }
@@ -553,13 +553,19 @@ const HOME = process.env.HOME;
553
553
  const SOCKET_DIR = path.join(HOME, ".pi", "entwurf-control");
554
554
  const META_DIR = path.join(HOME, ".pi", "agent", "meta-sessions");
555
555
  const ENTWURF_BIN = process.argv[2];
556
+ // The ENTRY POINT is an argument, because there are two of them and they are different
557
+ // claims. `pi --entwurf-control` proves the WIRING; `entwurf pi` proves the LAUNCHER a user
558
+ // received from npm. One program, so neither claim drifts away from the other.
559
+ const LABEL = process.argv[3];
560
+ const [COMMAND, ...COMMAND_ARGS] = process.argv.slice(4);
556
561
 
557
562
  let bad = 0;
558
563
  const ok = (m) => console.log(" ok " + m);
559
564
  const no = (m) => { console.log(" FAIL " + m); bad = 1; };
560
565
  const ls = (dir) => { try { return fs.readdirSync(dir); } catch { return []; } };
561
566
 
562
- const child = spawn("pi", ["--entwurf-control", "--mode", "rpc"], { stdio: ["pipe", "pipe", "pipe"] });
567
+ console.log(` ENTRY ${LABEL}: ${COMMAND} ${COMMAND_ARGS.join(" ")}`);
568
+ const child = spawn(COMMAND, COMMAND_ARGS, { stdio: ["pipe", "pipe", "pipe"] });
563
569
  let stderr = "";
564
570
  child.stderr.on("data", (d) => { stderr += d.toString(); });
565
571
 
@@ -655,14 +661,14 @@ child.stdout.on("data", (d) => {
655
661
  child.on("error", (err) => { no(`pi failed to spawn: ${err.message}`); done(1); });
656
662
  child.on("exit", (code) => {
657
663
  if (settled) return;
658
- no(`pi exited ${code} before answering get_state — stderr: ${stderr.trim().slice(0, 600)}`);
664
+ no(`${LABEL}: the session exited ${code} before answering get_state — stderr: ${stderr.trim().slice(0, 600)}`);
659
665
  done(1);
660
666
  });
661
667
 
662
668
  setTimeout(() => { child.stdin.write(`${JSON.stringify({ type: "get_state", id: "g1" })}\n`); }, 500);
663
669
  setTimeout(() => { if (!settled) { no("pi did not answer get_state within 60s"); done(1); } }, 60_000);
664
670
  DRIVER_EOF
665
- node /tmp/use-path-drive.mjs "$ACTIVE/node_modules/.bin/entwurf" || fail=1
671
+ node /tmp/use-path-drive.mjs "$ACTIVE/node_modules/.bin/entwurf" "the plugin wiring" pi --entwurf-control --mode rpc || fail=1
666
672
 
667
673
  # ── 8. the status fan, driven by the REAL binaries this install produced ─────
668
674
  # `check-herdr-plugin` already drives `lib/status.mjs` against stub `entwurf` and `herdr`
@@ -736,6 +742,28 @@ else
736
742
  fi
737
743
  herdr server stop >/dev/null 2>&1 || true
738
744
 
745
+ # ── 8a. the SHIPPED LAUNCHER, from the bin the user actually received ───────
746
+ # [7] proves the wiring by running `pi` off PATH. That is not the same claim as the one #118
747
+ # hop 2 makes, and astra named the gap: a user who installed this plugin does not have this
748
+ # checkout, so the only launcher that means anything to them is the one inside the npm
749
+ # artifact the lock names. Measured on the published tarball before this cell existed, the
750
+ # `pi)` case IS in `run.sh` at 0.23.1 — so the claim is now testable, and this cell is what
751
+ # tests it: the same 0-token birth, driven through `<active>/node_modules/.bin/entwurf pi`.
752
+ #
753
+ # The proof that it EXEC'd pi is the birth itself. A launcher that silently did nothing, or
754
+ # resolved to something else, cannot produce a V3 record and a gardenId-keyed socket.
755
+ #
756
+ # IT SITS AFTER [8] ON PURPOSE: the fan cell asserts the unobserved count as an exact
757
+ # literal, and a second citizen born before it would change that number. Order here is the
758
+ # cheaper of the two ways to keep that oracle exact.
759
+ echo; echo "[8a] entwurf pi — the launcher inside the npm artifact, same 0-token birth"
760
+ ENTWURF_INSTALLED_BIN="$ACTIVE/node_modules/.bin/entwurf"
761
+ if [ -x "$ENTWURF_INSTALLED_BIN" ]; then
762
+ node /tmp/use-path-drive.mjs "$ENTWURF_INSTALLED_BIN" "the shipped launcher" "$ENTWURF_INSTALLED_BIN" pi --mode rpc || fail=1
763
+ else
764
+ bad "no installed entwurf bin at $ENTWURF_INSTALLED_BIN — the launcher claim cannot be measured"
765
+ fi
766
+
739
767
  # ── 9. deactivate: the roundtrip, on the npm source (#118 H1-5) ──────────────
740
768
  # `check-herdr-activation` cell 16 proves this against fixtures: [QK:HAC-DEACTIVATE-ROUNDTRIP]
741
769
  # asserts the pi settings return to their ORIGINAL MEANING (JSON equality, not bytes — the
@@ -434,7 +434,9 @@ ok(
434
434
  // callback-only prompt is delivered first, and the task is released only by the exact
435
435
  // successful callback result. The transcript is where both are visible, in order, in one
436
436
  // native session.
437
- const bootstrapLine = buildOmpCallbackOnlyPrompt({ target: callerGid, nonce: nonce as string })
437
+ // Since the callback moved out of band (ENTWURF_CALLBACK_TARGET/NONCE in the sibling env), the
438
+ // callback-only prompt carries no address; the receipt is the no-arg first action itself.
439
+ const bootstrapLine = buildOmpCallbackOnlyPrompt()
438
440
  .split("\n")
439
441
  .find((l) => l.includes("FIRST AND ONLY ACTION")) as string;
440
442
  const transcript = fs.readFileSync(transcriptPath as string, "utf8");
@@ -442,7 +444,7 @@ const callbackAt = transcript.indexOf(bootstrapLine);
442
444
  const taskAt = transcript.indexOf(sceneFact);
443
445
  receipts["5-two-stage-order"] = `callback-only prompt @${callbackAt}, task scene fact @${taskAt}`;
444
446
  ok(
445
- "stage one is in the sibling's own transcript: the extension delivered the callback-ONLY prompt, carrying this call's target and nonce",
447
+ "stage one is in the sibling's own transcript: the extension delivered the callback-ONLY prompt (no-arg first action; the address rides the sibling env, never this prompt)",
446
448
  callbackAt >= 0,
447
449
  ` expected line: ${bootstrapLine}`,
448
450
  );
@@ -53,7 +53,7 @@ cat > "$SB/bin/entwurf-bridge" <<'FAKE'
53
53
  while IFS= read -r line; do
54
54
  case "$line" in
55
55
  *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
56
- *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
56
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"},{"name":"entwurf_callback"}]}}' ;;
57
57
  esac
58
58
  done
59
59
  FAKE
@@ -67,7 +67,7 @@ write_mcp_fake() { # $1 = path
67
67
  while IFS= read -r line; do
68
68
  case "$line" in
69
69
  *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
70
- *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
70
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"},{"name":"entwurf_callback"}]}}' ;;
71
71
  esac
72
72
  done
73
73
  FAKE
@@ -93,7 +93,7 @@ write_stale_fake() { # $1 = path
93
93
  while IFS= read -r line; do
94
94
  case "$line" in
95
95
  *'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"stale-bridge","version":"0"}}}' ;;
96
- *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
96
+ *'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"},{"name":"entwurf_callback"}]}}' ;;
97
97
  esac
98
98
  done
99
99
  FAKE