@junghanacs/entwurf 0.23.1 → 0.25.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 (107) hide show
  1. package/BASELINE.md +1 -0
  2. package/CHANGELOG.md +628 -0
  3. package/DELIVERY.md +24 -1
  4. package/FAQ.md +47 -0
  5. package/README.md +5 -3
  6. package/VERIFY.md +20 -2
  7. package/demo/README.md +5 -2
  8. package/docs/acp-backend-rail.md +49 -2
  9. package/docs/setup-clean-host.md +8 -7
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
  23. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
  24. package/mcp/entwurf-bridge/dist/protocol.js +0 -7
  25. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
  26. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
  27. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  28. package/mcp/entwurf-bridge/src/index.ts +46 -5
  29. package/mcp/entwurf-bridge/test.sh +2 -2
  30. package/package.json +11 -9
  31. package/pi-extensions/entwurf-control.ts +83 -154
  32. package/pi-extensions/lib/acp/backend.ts +16 -6
  33. package/pi-extensions/lib/acp/context.ts +30 -10
  34. package/pi-extensions/lib/acp/session-store.ts +2 -2
  35. package/pi-extensions/lib/callback-env.ts +78 -0
  36. package/pi-extensions/lib/compaction-send-guard.ts +6 -1
  37. package/pi-extensions/lib/control-send-receipt.ts +108 -0
  38. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  39. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
  40. package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
  42. package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
  43. package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
  44. package/pi-extensions/lib/fresh-call-composition.ts +56 -22
  45. package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
  46. package/pi-extensions/lib/herdr-placement.ts +24 -4
  47. package/pi-extensions/lib/mux-fresh-call.ts +23 -8
  48. package/pi-extensions/meta-bridge-omp.ts +35 -48
  49. package/protocol.js +0 -8
  50. package/run.sh +306 -53
  51. package/scripts/check-acp-backend-preflight.ts +117 -9
  52. package/scripts/check-acp-carrier-augment.ts +6 -4
  53. package/scripts/check-acp-event-mapper.ts +17 -6
  54. package/scripts/check-acp-prompt-builder.ts +17 -9
  55. package/scripts/check-acp-prompt-lifecycle.ts +23 -6
  56. package/scripts/check-acp-session-reuse.ts +64 -51
  57. package/scripts/check-acp-session-store.ts +31 -8
  58. package/scripts/check-acp-stop-reason.ts +20 -2
  59. package/scripts/check-acp-stream-hooks.ts +22 -5
  60. package/scripts/check-acp-usage-accounting.ts +33 -9
  61. package/scripts/check-entwurf-bridge-boot.ts +16 -1
  62. package/scripts/check-entwurf-v2-surface.ts +289 -10
  63. package/scripts/check-fresh-call-dispatch.ts +4 -5
  64. package/scripts/check-gate-qualification.ts +51 -9
  65. package/scripts/check-herdr-activation.ts +73 -0
  66. package/scripts/check-herdr-fresh-call.ts +30 -6
  67. package/scripts/check-herdr-placement.ts +51 -0
  68. package/scripts/check-herdr-plugin-build.ts +11 -4
  69. package/scripts/check-herdr-plugin.ts +745 -38
  70. package/scripts/check-herdr-sandbox.ts +14 -2
  71. package/scripts/check-install-container.sh +1 -1
  72. package/scripts/check-mux-launcher-fence.ts +37 -0
  73. package/scripts/check-pi-launch.ts +8 -2
  74. package/scripts/check-release-gate-outcomes.ts +165 -0
  75. package/scripts/doctor-pi-provider.ts +44 -4
  76. package/scripts/herdr-rails.mjs +75 -0
  77. package/scripts/lib/mutation-qualify.ts +1 -1
  78. package/scripts/meta-bridge-state.py +18 -1
  79. package/scripts/mutants/acp-backend-preflight.json +20 -0
  80. package/scripts/mutants/bridge-boot-resume.json +26 -0
  81. package/scripts/mutants/bridge-command-boot.json +38 -0
  82. package/scripts/mutants/control-send-receipt.json +235 -0
  83. package/scripts/mutants/herdr-activation.json +16 -0
  84. package/scripts/mutants/herdr-fresh-call.json +15 -4
  85. package/scripts/mutants/herdr-plugin-build.json +7 -11
  86. package/scripts/mutants/herdr-plugin.json +177 -9
  87. package/scripts/mutants/meta-retire.json +22 -0
  88. package/scripts/mutants/mux-fresh-call.json +21 -8
  89. package/scripts/mutants/omp-fresh.json +5 -9
  90. package/scripts/mutants/pack-install.json +4 -4
  91. package/scripts/mutants/pi-floor.json +17 -0
  92. package/scripts/mutants/release-gate.json +48 -0
  93. package/scripts/mutants/retired-sent-surface.json +32 -0
  94. package/scripts/mutants/v2-surface.json +7 -7
  95. package/scripts/probe-bridge-command.ts +1 -0
  96. package/scripts/smoke-acp-session-reuse-live.ts +23 -5
  97. package/scripts/smoke-agy-install-state.sh +2 -2
  98. package/scripts/smoke-codex-fresh-live.ts +94 -13
  99. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  100. package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
  101. package/scripts/smoke-herdr-raw-install-live.sh +90 -5
  102. package/scripts/smoke-meta-install-state.sh +107 -1
  103. package/scripts/smoke-mux-lifecycle-live.ts +9 -2
  104. package/scripts/smoke-omp-fresh-live.ts +4 -2
  105. package/scripts/smoke-omp-mcp-state.sh +1 -1
  106. package/scripts/smoke-pi-provider-state.sh +108 -2
  107. package/scripts/smoke-setup-verdict.sh +18 -2
@@ -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,8 +6,8 @@
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"],
10
- "replace": [" grep '^@earendil-works+' | grep -v '@0\\.85\\.1' || true"],
9
+ "find": [" grep '^@earendil-works+' | grep -Ev '@0\\.87\\.1(_|$)' || true"],
10
+ "replace": [" grep '^@earendil-works+' | grep -v '@0\\.87\\.0' || true"],
11
11
  "gate": ["bash", "run.sh", "check-pack-pin-matcher"],
12
12
  "timeoutSeconds": 120,
13
13
  "signature": "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED]",
@@ -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\\.87\\.1(_|$)' || true"],
21
+ "replace": [" grep '^@earendil-works+pi-' | grep -Ev '@0\\.87\\.1(_|$)' || 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]",
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "pi-floor",
4
+ "mutants": [
5
+ {
6
+ "claim": "PI-FLOOR-SINGLE-SOURCE",
7
+ "title": "one row of the pnpm workspace closure is left on the previous floor while every other surface moved — the shape a version bump actually fails in. Nothing else reads that file, so the stranded row surfaces only at install time, in a different command, far from the change that stranded it",
8
+ "subject": "pnpm-workspace.yaml",
9
+ "find": [" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1'"],
10
+ "replace": [" - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0'"],
11
+ "gate": ["bash", "run.sh", "check-dep-versions"],
12
+ "timeoutSeconds": 300,
13
+ "signature": "[QK:PI-FLOOR-SINGLE-SOURCE]",
14
+ "signatureSource": "run.sh"
15
+ }
16
+ ]
17
+ }
@@ -227,6 +227,54 @@
227
227
  "timeoutSeconds": 180,
228
228
  "signature": "[QK:CODEX-FIRST-ADMISSION-IS-RELEASE-MUST]",
229
229
  "signatureSource": "scripts/check-release-gate-outcomes.ts"
230
+ },
231
+ {
232
+ "claim": "CODEX-OPERATOR-PREREQ-ABSENT-SKIPS",
233
+ "title": "a constant-false guard leaves the decline arm as dead code, so every absent operator input falls through to the assertions and exits FAIL again — the shape that made this MUST step red inside every cut and would require a per-cut exception",
234
+ "subject": "scripts/smoke-codex-fresh-live.ts",
235
+ "find": [
236
+ "\tconst absentInputs = OPERATOR_INPUTS.filter(([name]) => (process.env[name]?.trim() ?? \"\") === \"\");",
237
+ "\tif (absentInputs.length > 0) {"
238
+ ],
239
+ "replace": [
240
+ "\tconst absentInputs = OPERATOR_INPUTS.filter(([name]) => (process.env[name]?.trim() ?? \"\") === \"\");",
241
+ "\tif (absentInputs.length > 0 && process.env.ENTWURF_NEVER_SET === \"1\") {"
242
+ ],
243
+ "gate": ["bash", "run.sh", "check-release-gate-outcomes"],
244
+ "timeoutSeconds": 300,
245
+ "signature": "[QK:CODEX-OPERATOR-PREREQ-ABSENT-SKIPS]",
246
+ "signatureSource": "scripts/check-release-gate-outcomes.ts"
247
+ },
248
+ {
249
+ "claim": "CODEX-OPERATOR-PREREQ-WRONG-FAILS",
250
+ "title": "turning the pid grammar into a decline means a malformed, dead or non-Codex app-server pid the operator DID supply reports as a missing prerequisite, so a broken answer buys silence where a cut reads silence as an unmet precondition rather than a defect",
251
+ "subject": "scripts/smoke-codex-fresh-live.ts",
252
+ "find": [
253
+ "\tok(",
254
+ "\t\t\"ENTWURF_CODEX_APP_SERVER_PID explicitly names the existing operator-owned app-server\",",
255
+ "\t\t/^[1-9]\\d*$/.test(pidText),"
256
+ ],
257
+ "replace": [
258
+ "\tif (!/^[1-9]\\d*$/.test(pidText)) skipLive(LABEL, \"no usable app-server pid was supplied\");",
259
+ "\tok(",
260
+ "\t\t\"an app-server pid was supplied\",",
261
+ "\t\ttrue,"
262
+ ],
263
+ "gate": ["bash", "run.sh", "check-release-gate-outcomes"],
264
+ "timeoutSeconds": 300,
265
+ "signature": "[QK:CODEX-OPERATOR-PREREQ-WRONG-FAILS]",
266
+ "signatureSource": "scripts/check-release-gate-outcomes.ts"
267
+ },
268
+ {
269
+ "claim": "CODEX-OPERATOR-PREREQ-STATED-NOT-SEARCHED",
270
+ "title": "deriving the app-server pid by process search in the release instructions teaches the exact inference the MUST step refuses, and on a host running several app-servers it hands the gate an arbitrary process to verify as the operator's answer",
271
+ "subject": ".claude/skills/entwurf-release/SKILL.md",
272
+ "find": [": \"${CODEX_APP_SERVER_PID:?state the app-server pid you started; never search for it}\""],
273
+ "replace": ["CODEX_APP_SERVER_PID=$(pgrep -f 'codex.*app-server' | head -1)"],
274
+ "gate": ["bash", "run.sh", "check-release-gate-outcomes"],
275
+ "timeoutSeconds": 300,
276
+ "signature": "[QK:CODEX-OPERATOR-PREREQ-STATED-NOT-SEARCHED]",
277
+ "signatureSource": "scripts/check-release-gate-outcomes.ts"
230
278
  }
231
279
  ]
232
280
  }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "retired-sent-surface",
4
+ "mutants": [
5
+ {
6
+ "claim": "NO-RENDERER-WITHOUT-PRODUCER",
7
+ "title": "a renderer is registered for a customType nothing in this extension emits — a producer-less orphan box no operator can ever reach, the same shape that shipped for two releases behind comments naming producers that did not exist",
8
+ "subject": "pi-extensions/entwurf-control.ts",
9
+ "find": ["\tpi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);"],
10
+ "replace": [
11
+ "\tpi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);\n\tpi.registerMessageRenderer(\"entwurf-orphan\", renderSessionMessage);"
12
+ ],
13
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
14
+ "timeoutSeconds": 300,
15
+ "signature": "[QK:NO-RENDERER-WITHOUT-PRODUCER]",
16
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
17
+ },
18
+ {
19
+ "claim": "RETIRED-SENT-SURFACE-ABSENT",
20
+ "title": "the retired sender-side echo constant comes back as a public export of a SHIPPED file — a name an npm consumer can import and nothing in the repo can ever produce, which is the promise #120 P3 removed rather than renamed",
21
+ "subject": "protocol.js",
22
+ "find": ["export const ENTWURF_PROJECT_CONTEXT_OPEN_TAG = \"<project-context\";"],
23
+ "replace": [
24
+ "export const ENTWURF_PROJECT_CONTEXT_OPEN_TAG = \"<project-context\";\nexport const ENTWURF_SENT_MESSAGE_TYPE = \"entwurf-sent\";"
25
+ ],
26
+ "gate": ["bash", "run.sh", "check-entwurf-v2-surface"],
27
+ "timeoutSeconds": 300,
28
+ "signature": "[QK:RETIRED-SENT-SURFACE-ABSENT]",
29
+ "signatureSource": "scripts/check-entwurf-v2-surface.ts"
30
+ }
31
+ ]
32
+ }
@@ -29,10 +29,10 @@
29
29
  },
30
30
  {
31
31
  "claim": "V2PROD-INBAND-ERROR-WIRED",
32
- "title": "the production sendOverSocket closure stops copying response.error onto the RpcSendResult, so a receiver's named in-band refusal is stranded one hop before driveSend and the caller sees a reasonless rejected",
32
+ "title": "the production sendOverSocket closure stops copying response.error onto the RpcSendResult, so a receiver's named in-band refusal is stranded one hop before driveSend and the caller sees a reasonless rejected (#120 P2: the wire seam now also classifies the acceptance boundary, so the same line carries one more field — the mutation still drops the receiver's own error)",
33
33
  "subject": "pi-extensions/lib/entwurf-v2-production.ts",
34
- "find": ["\t\t\treturn { success: response.success, error: response.error };"],
35
- "replace": ["\t\t\treturn { success: response.success };"],
34
+ "find": ["\t\t\treturn { success: response.success, error: response.error, boundary };"],
35
+ "replace": ["\t\t\treturn { success: response.success, boundary };"],
36
36
  "gate": ["bash", "run.sh", "check-entwurf-v2-production"],
37
37
  "timeoutSeconds": 60,
38
38
  "signature": "[QK:V2PROD-INBAND-ERROR-WIRED]",
@@ -40,15 +40,15 @@
40
40
  },
41
41
  {
42
42
  "claim": "V2SURF-PI-MODE-SCOPE",
43
- "title": "pi-native mode param reverts to \"Delivery mode for a live send\" (false for native-push)",
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
44
  "subject": "pi-extensions/entwurf-control.ts",
45
45
  "find": [
46
- "\t\t\t\t\t\"Injection style for a CONTROL-SOCKET send only: steer (immediate) or follow_up (after task). \" +",
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. \" +",
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 (immediate, interrupt the current turn) or follow_up \" +",
51
- "\t\t\t\t\t\"(queued after the current task finishes running to completion).\","
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). \" +",
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,
@@ -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
@@ -669,12 +669,75 @@ async function run(): Promise<void> {
669
669
  "set LIVE=1 plus ENTWURF_CODEX_APP_SERVER_PID, ENTWURF_CODEX_FRESH_MODEL, and ENTWURF_CODEX_FRESH_PI_MODEL to run; this opens real Codex and Pi windows and spends model turns.",
670
670
  );
671
671
  }
672
+
673
+ // ── THE OPERATOR-SUPPLIED PREREQUISITE TRIPLE, AND WHY ITS ABSENCE IS A SKIP ──
674
+ //
675
+ // Entwurf does not own the Codex app-server (VERIFY: "The A cell takes no inferred
676
+ // server or models"), so these three values can only come from the operator. That
677
+ // stays true. What was wrong is the CLASSIFICATION of their absence.
678
+ //
679
+ // `[측정 2026-09-23]` the skip line directly above promised four prerequisites in one
680
+ // breath — LIVE=1 *plus* the three env names — but only the first could ever produce a
681
+ // SKIP. The other three were `ok()` assertions, and `ok()` throws, so a missing
682
+ // operator input exited as FAIL. `release_gate()` supplies none of them, so this MUST
683
+ // step declined as a FAILURE inside every aggregate on every host: 0.25.0's own
684
+ // candidate receipt reads `MUST PASS=23 FAIL=1`, and the release record had to carry a
685
+ // prose classification explaining that the one red was "the shape of that step", and the
686
+ // cut stayed BLOCKED. A contract that manufactures a red every cut and then needs a human
687
+ // to excuse it is not a floor.
688
+ //
689
+ // `scripts/lib/step-outcome.sh` already wrote the rule this violated: "a step that RAN
690
+ // AND BROKE and a step that NEVER RAN are different facts, and a release record that
691
+ // blurs them is worthless: the first is a defect to fix, the second is a prerequisite to
692
+ // supply." An absent operator input is the second.
693
+ //
694
+ // So the axis is split, and the split is the whole repair:
695
+ // ABSENT (unset/empty) -> skipLive: nothing ran, the missing names are printed, and
696
+ // `--cut` still refuses it as BLOCKED (MUST SKIP). Nothing is
697
+ // excused — a cut without this receipt remains impossible.
698
+ // PRESENT BUT WRONG -> FAIL, unchanged: a malformed or dead PID, a process whose
699
+ // argv is not a Codex app-server, a foreign uid, A == S. The
700
+ // operator answered, and the answer is a defect.
701
+ // Nothing is inferred either way, and no default is invented for a value the operator
702
+ // did not give. The reproducible green is the invocation VERIFY and the release skill's
703
+ // P5 both now spell out, and `release_gate` needs no plumbing for it: measured on this
704
+ // source, that function exports no scrub and unsets nothing, so an exported triple
705
+ // already reaches this step.
706
+ //
707
+ // DELIBERATELY NOT MOVED: the installed-config preflights below (`codexFreshPreflight`,
708
+ // `codexCallerFreshPreflight`, `codexLaunchCwdFreshPreflight`). Those are host state
709
+ // `setup` establishes and the Codex doctors cover, and the file header's contract
710
+ // already decided them — "missing system birth, user MCP/status/terminal-title config,
711
+ // app-server, models, runtime, bridge, or tmux seat is a FAIL". A one-time answered
712
+ // launch directory is host setup, not a per-invocation input, and reclassifying it would
713
+ // be a second contract change riding this one.
714
+ const OPERATOR_INPUTS = [
715
+ ["ENTWURF_CODEX_APP_SERVER_PID", "the pid of the app-server YOU started (never inferred)"],
716
+ ["ENTWURF_CODEX_FRESH_MODEL", "the Codex-side model this cell may spend a turn on"],
717
+ ["ENTWURF_CODEX_FRESH_PI_MODEL", "the pi-side model of the callback sibling"],
718
+ ] as const;
719
+ const absentInputs = OPERATOR_INPUTS.filter(([name]) => (process.env[name]?.trim() ?? "") === "");
720
+ if (absentInputs.length > 0) {
721
+ skipLive(
722
+ LABEL,
723
+ `this cell takes no inferred server or models, and ${absentInputs.length} operator-supplied ` +
724
+ `prerequisite(s) are absent: ${absentInputs.map(([name, why]) => `${name} (${why})`).join("; ")}. ` +
725
+ "Supply them on the invocation — LIVE=1 ENTWURF_CODEX_APP_SERVER_PID=<pid> " +
726
+ "ENTWURF_CODEX_FRESH_MODEL=<codex-model> ENTWURF_CODEX_FRESH_PI_MODEL=<pi-model> — either for the " +
727
+ "standalone run or for `./run.sh release-gate <scratch> --cut`, which inherits them. " +
728
+ "A cut is still blocked without this receipt: --cut reads a MUST SKIP as red.",
729
+ );
730
+ }
731
+
672
732
  ok("the measured/certified host axis is Linux", process.platform === "linux", `host=${process.platform}`);
673
733
 
734
+ // Present, so a malformed value is the operator's answer being wrong — a FAIL, not a
735
+ // declined prerequisite. The absence arm above already left.
674
736
  const pidText = process.env.ENTWURF_CODEX_APP_SERVER_PID?.trim() ?? "";
675
737
  ok(
676
738
  "ENTWURF_CODEX_APP_SERVER_PID explicitly names the existing operator-owned app-server",
677
739
  /^[1-9]\d*$/.test(pidText),
740
+ `ENTWURF_CODEX_APP_SERVER_PID=${JSON.stringify(pidText)} is not a positive decimal pid`,
678
741
  );
679
742
  const appServerPid = Number(pidText);
680
743
  const procDir = `/proc/${appServerPid}`;
@@ -749,10 +812,13 @@ async function run(): Promise<void> {
749
812
  `fixture-session=${fixtureCoordinate.sessionId}\nfixture-name=${fixtureSessionName}\n` +
750
813
  `fixture-window=${fixtureCoordinate.windowId}`;
751
814
 
815
+ // Both are non-empty: the prerequisite arm at the top of `run` left otherwise. Asserting
816
+ // `.length > 0` again here would be a tautology dressed as a check, so the pair is
817
+ // RECORDED instead — the tier a live receipt is read on is a fact the evidence needs, and
818
+ // 0.25.0 already had to explain in prose that its pi leg ran on terra rather than luna.
752
819
  const codexModel = process.env.ENTWURF_CODEX_FRESH_MODEL?.trim() ?? "";
753
820
  const piModel = process.env.ENTWURF_CODEX_FRESH_PI_MODEL?.trim() ?? "";
754
- ok("ENTWURF_CODEX_FRESH_MODEL explicitly names the Codex runtime model", codexModel.length > 0);
755
- ok("ENTWURF_CODEX_FRESH_PI_MODEL explicitly names the Pi callback sibling model", piModel.length > 0);
821
+ receipts["1b-operator-supplied-models"] = `codex=${codexModel}\npi=${piModel}`;
756
822
  const codexRuntime = resolveExecutable("codex", appServerEnv);
757
823
  ok("the Codex runtime resolves on the app-server PATH", codexRuntime !== null);
758
824
  const bridgeRuntime = resolveExecutable("entwurf-bridge", appServerEnv);
@@ -1016,11 +1082,19 @@ async function run(): Promise<void> {
1016
1082
  message: initialPiInstruction,
1017
1083
  });
1018
1084
  receipts["5-fixture-to-initial-pi-v2"] = instructInitialPi.text;
1085
+ // #120 P2: the control-socket rail answers with the ACCEPTANCE BOUNDARY the receiver observed,
1086
+ // and every member of that closed set is a success. The initial Pi is mid-turn while the fixture
1087
+ // addresses it, so `queued-follow-up` is an ORDINARY answer here — measured 2026-09-23, this
1088
+ // exact step. The old `sent|delivered` pin passed only when the caller happened to be idle, and
1089
+ // before P2 it passed on the busy path too because the rail said `sent` either way; `delivered`
1090
+ // was never a member of the set at all. The sibling LIVE smokes took this widening in 6d90d73
1091
+ // (`smoke-herdr-fresh-call-live.ts`, `smoke-mux-lifecycle-live.ts`); this step was missed.
1019
1092
  ok(
1020
1093
  "the fixture addressed the real initial Pi caller through an actual control-socket entwurf_v2 receipt",
1021
1094
  !instructInitialPi.isError &&
1022
- /control-socket/i.test(instructInitialPi.text) &&
1023
- /(?:sent|delivered)/i.test(instructInitialPi.text),
1095
+ /entwurf_v2 control-socket → (?:sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/.test(
1096
+ instructInitialPi.text,
1097
+ ),
1024
1098
  instructInitialPi.text,
1025
1099
  );
1026
1100
 
@@ -1325,15 +1399,22 @@ async function run(): Promise<void> {
1325
1399
  assertExactSourceToolCalls(
1326
1400
  piSourceToolReceipts(readInitialPiEntries()),
1327
1401
  [
1328
- {
1329
- toolName: "entwurf_v2",
1330
- arguments: {
1331
- target: callerGid,
1332
- intent: "fire-and-forget",
1333
- message: initialPiNonce,
1334
- wants_reply: false,
1335
- },
1336
- },
1402
+ // 0.24.0 moved a pi sibling's first action to the ZERO-ARGUMENT eighth verb, and this
1403
+ // smoke's own launch receipt says so in the same run: "its first action is the
1404
+ // zero-argument callback verb (codex is the one arm that still sends the nonce as
1405
+ // arguments)". The Codex audit below therefore keeps its `entwurf_v2` shape, and only
1406
+ // this one moves. `{}` is matched by deep-strict equality, so it pins the verb's actual
1407
+ // contract — a target or message typed here would be a second address axis and the verb
1408
+ // refuses it by name.
1409
+ //
1410
+ // Nothing is weakened by dropping the nonce from THIS expectation: the correlation is
1411
+ // proven earlier and from the delivered body, which is where a nonce can actually be
1412
+ // corrupted — `the initial Pi callback carries the exact launch nonce` (:970-973) reads
1413
+ // the fixture's own mailbox, requires exactly one exact match and refuses duplicates,
1414
+ // and the two cells beside it bind that callback's sender envelope to a real pi V3
1415
+ // citizen. This audit's job is the different one its name says: exactly these calls,
1416
+ // once each, and nothing else.
1417
+ { toolName: "entwurf_callback", arguments: {} },
1337
1418
  {
1338
1419
  toolName: "entwurf_fresh_call",
1339
1420
  arguments: {
@@ -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