@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.
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +173 -0
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +2 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +2 -2
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -1
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +8 -7
- package/pi-extensions/entwurf-control.ts +56 -0
- package/pi-extensions/lib/acp/backend.ts +16 -6
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +4 -1
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
- package/pi-extensions/lib/herdr-placement.ts +13 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/run.sh +135 -50
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +33 -9
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +44 -5
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +22 -6
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-pi-launch.ts +5 -2
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +11 -0
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
- package/scripts/smoke-herdr-raw-install-live.sh +31 -3
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- 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 [
|
|
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 [
|
|
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 ${
|
|
71
|
-
"replace": ["\t\t`FINAL ACTION, after completing the task: call ${
|
|
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-
|
|
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 ${
|
|
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: \"
|
|
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(
|
|
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
|
|
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\\.
|
|
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\\.
|
|
21
|
-
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@0\\.
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
|
190
|
-
*
|
|
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
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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 !==
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
868
|
-
//
|
|
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(
|
|
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
|
|
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 ?? "") ===
|
|
904
|
+
const callAt = indexOfEntry(childActivity, (e) => (e.debug ?? "") === `Calling MCP tool: ${CALLBACK_VERB}`);
|
|
882
905
|
const doneAt = indexOfEntry(childActivity, (e) =>
|
|
883
|
-
(e.debug ?? "").startsWith(
|
|
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
|
|
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(
|
|
930
|
+
order.includes(CALLBACK_VERB) &&
|
|
908
931
|
beforeCallback.every((name) => READ_ONLY_FIRST.has(name)) &&
|
|
909
932
|
within &&
|
|
910
933
|
callbackArrived &&
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|