@junghanacs/entwurf 0.30.1 → 0.31.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 (58) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +112 -0
  3. package/DELIVERY.md +1 -0
  4. package/README.md +47 -7
  5. package/VERIFY.md +17 -1
  6. package/demo/README.md +1 -1
  7. package/docs/acp-backend-rail.md +1 -1
  8. package/docs/setup-clean-host.md +104 -4
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
  10. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
  19. package/mcp/entwurf-bridge/src/index.ts +7 -7
  20. package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
  21. package/package.json +13 -10
  22. package/pi/entwurf-capabilities.json +1 -0
  23. package/pi/pi-durable/bootstrap.mjs +281 -0
  24. package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
  25. package/pi/pi-durable/overlay/upstream-pin.json +16 -0
  26. package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
  27. package/pi-extensions/lib/fresh-call-composition.ts +42 -7
  28. package/pi-extensions/lib/meta-sender-identity.ts +7 -1
  29. package/pi-extensions/lib/meta-session.ts +28 -2
  30. package/pi-extensions/lib/mux-fresh-call.ts +34 -2
  31. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
  32. package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
  33. package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
  34. package/run.sh +200 -33
  35. package/scripts/check-fresh-call-dispatch.ts +18 -0
  36. package/scripts/check-gate-qualification.ts +2 -1
  37. package/scripts/check-pi-durable-contact.ts +834 -0
  38. package/scripts/check-pi-durable-receive.ts +1116 -0
  39. package/scripts/check-pi-durable-send.ts +1661 -0
  40. package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
  41. package/scripts/lib/pi-durable-r1.ts +100 -0
  42. package/scripts/lib/pi-durable-reap.ts +27 -0
  43. package/scripts/lib/pi-durable-receive.ts +137 -0
  44. package/scripts/mutants/control-send-receipt.json +1 -1
  45. package/scripts/mutants/copilot-birth.json +2 -2
  46. package/scripts/mutants/meta-hook-session-switch.json +4 -2
  47. package/scripts/mutants/mux-fresh-call.json +69 -3
  48. package/scripts/mutants/omp-birth.json +2 -2
  49. package/scripts/mutants/omp-fresh.json +6 -4
  50. package/scripts/mutants/pack-install.json +3 -3
  51. package/scripts/mutants/pi-durable.json +390 -0
  52. package/scripts/mutants/pi-floor.json +2 -2
  53. package/scripts/pi-durable-contact-host.mjs +105 -0
  54. package/scripts/pi-durable-receive-host.mjs +127 -0
  55. package/scripts/pi-durable-send-host.mjs +213 -0
  56. package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
  57. package/scripts/smoke-setup-verdict.sh +21 -2
  58. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,390 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "lane": "pi-durable",
4
+ "mutants": [
5
+ {
6
+ "claim": "PI-DURABLE-RUNTIME-UNTRACKED-DRIFT",
7
+ "title": "a non-ignored untracked file in the operator runtime no longer counts as pin drift",
8
+ "subject": "pi-extensions/lib/pi-durable-runtime.ts",
9
+ "find": [" || facts.untracked !== \"\" || "],
10
+ "replace": [" || "],
11
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
12
+ "timeoutSeconds": 300,
13
+ "signature": "[QK:PI-DURABLE-RUNTIME-UNTRACKED-DRIFT]",
14
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
15
+ "build": {
16
+ "argv": ["bash", "scripts/build-bridge.sh"],
17
+ "output": "mcp/entwurf-bridge/dist"
18
+ }
19
+ },
20
+ {
21
+ "claim": "PI-DURABLE-RUNTIME-MODELDATA-DRIFT",
22
+ "title": "model data that is not the pinned bytes is no longer its own refusal",
23
+ "subject": "pi-extensions/lib/pi-durable-runtime.ts",
24
+ "find": ["\t\treturn \"pi-durable-runtime-modeldata-drift\";\n"],
25
+ "replace": ["\t\treturn null;\n"],
26
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
27
+ "timeoutSeconds": 300,
28
+ "signature": "[QK:PI-DURABLE-RUNTIME-MODELDATA-DRIFT]",
29
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
30
+ "build": {
31
+ "argv": ["bash", "scripts/build-bridge.sh"],
32
+ "output": "mcp/entwurf-bridge/dist"
33
+ }
34
+ },
35
+ {
36
+ "claim": "PI-DURABLE-RUNTIME-READ-ONLY",
37
+ "title": "verification runs git against the operator checkout's own index instead of a private copy, so a stale stat cache is rewritten",
38
+ "subject": "pi-extensions/lib/pi-durable-runtime.ts",
39
+ "find": ["const onCopy = { ...base, GIT_INDEX_FILE: copy };"],
40
+ "replace": ["const onCopy = { ...base };"],
41
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
42
+ "timeoutSeconds": 300,
43
+ "signature": "[QK:PI-DURABLE-RUNTIME-READ-ONLY]",
44
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
45
+ "build": {
46
+ "argv": ["bash", "scripts/build-bridge.sh"],
47
+ "output": "mcp/entwurf-bridge/dist"
48
+ }
49
+ },
50
+ {
51
+ "claim": "PI-DURABLE-RUNTIME-LOCATOR-FIXED",
52
+ "title": "a relative XDG_DATA_HOME is honoured, so the runtime's one fixed place follows the caller's cwd",
53
+ "subject": "pi-extensions/lib/pi-durable-runtime.ts",
54
+ "find": [" && xdg.length > 0 && path.isAbsolute(xdg)) {"],
55
+ "replace": [" && xdg.length > 0) {"],
56
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
57
+ "timeoutSeconds": 300,
58
+ "signature": "[QK:PI-DURABLE-RUNTIME-LOCATOR-FIXED]",
59
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
60
+ "build": {
61
+ "argv": ["bash", "scripts/build-bridge.sh"],
62
+ "output": "mcp/entwurf-bridge/dist"
63
+ }
64
+ },
65
+ {
66
+ "claim": "PI-DURABLE-RUNTIME-STAGED-DRIFT",
67
+ "title": "the index == HEAD term leaves the verdict, so a staged change behind an equal worktree diff verifies as the pin",
68
+ "subject": "pi-extensions/lib/pi-durable-runtime.ts",
69
+ "find": ["\tif (!facts.indexMatchesHead) return \"pi-durable-runtime-pin-drift\";\n"],
70
+ "replace": [""],
71
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
72
+ "timeoutSeconds": 300,
73
+ "signature": "[QK:PI-DURABLE-RUNTIME-STAGED-DRIFT]",
74
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
75
+ "build": {
76
+ "argv": ["bash", "scripts/build-bridge.sh"],
77
+ "output": "mcp/entwurf-bridge/dist"
78
+ }
79
+ },
80
+ {
81
+ "claim": "PI-DURABLE-ENTRY-GUARD-FLOOR-SAFE",
82
+ "title": "the verifier's entry guard goes back to import.meta.main, which the floor's runner can misread as the process entry",
83
+ "subject": "pi-extensions/lib/pi-durable-runtime.ts",
84
+ "find": ["if (invokedDirectly) {\n"],
85
+ "replace": ["if (import.meta.main) {\n"],
86
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
87
+ "timeoutSeconds": 300,
88
+ "signature": "[QK:PI-DURABLE-ENTRY-GUARD-FLOOR-SAFE]",
89
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
90
+ "build": {
91
+ "argv": ["bash", "scripts/build-bridge.sh"],
92
+ "output": "mcp/entwurf-bridge/dist"
93
+ }
94
+ },
95
+ {
96
+ "claim": "PI-DURABLE-RUNTIME-CONFIG-NEUTRAL",
97
+ "title": "the diff's blank-context spacing is left to the runtime's git configuration again, so a config can move the verdict",
98
+ "subject": "pi-extensions/lib/pi-durable-runtime.ts",
99
+ "find": [
100
+ "[\"-c\", \"core.abbrev=8\", \"-c\", \"diff.suppressBlankEmpty=false\", \"diff\", ...PI_DURABLE_DIFF_SHAPE]"
101
+ ],
102
+ "replace": ["[\"-c\", \"core.abbrev=8\", \"diff\", ...PI_DURABLE_DIFF_SHAPE]"],
103
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
104
+ "timeoutSeconds": 300,
105
+ "signature": "[QK:PI-DURABLE-RUNTIME-CONFIG-NEUTRAL]",
106
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
107
+ "build": {
108
+ "argv": ["bash", "scripts/build-bridge.sh"],
109
+ "output": "mcp/entwurf-bridge/dist"
110
+ }
111
+ },
112
+ {
113
+ "claim": "PI-DURABLE-RUNTIME-GATES-CONSUME-INDEX-TERM",
114
+ "title": "the contact gate's tracked-overlay assertion loses the index term, so it certifies a runtime tree it never saw",
115
+ "subject": "scripts/check-pi-durable-contact.ts",
116
+ "find": ["facts.gitAnswered && facts.indexMatchesHead && facts.diffEqualsPatch && facts.untracked"],
117
+ "replace": ["facts.gitAnswered && facts.diffEqualsPatch && facts.untracked"],
118
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
119
+ "timeoutSeconds": 300,
120
+ "signature": "[QK:PI-DURABLE-RUNTIME-GATES-CONSUME-INDEX-TERM]",
121
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
122
+ "build": {
123
+ "argv": ["bash", "scripts/build-bridge.sh"],
124
+ "output": "mcp/entwurf-bridge/dist"
125
+ }
126
+ },
127
+ {
128
+ "claim": "PI-DURABLE-PREFLIGHT-FOUR-UNITS",
129
+ "title": "the receive unit stops asking whether the compiled owner join admits the durable host, so a launch proceeds to a citizen nobody can deliver to",
130
+ "subject": "pi-extensions/lib/pi-durable-fresh-preflight.ts",
131
+ "find": [
132
+ "\tif (!ownerJoinAdmitsDurableHost(layout.deliverability)) return \"pi-durable-receive-unit-missing\";\n"
133
+ ],
134
+ "replace": [""],
135
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
136
+ "timeoutSeconds": 300,
137
+ "signature": "[QK:PI-DURABLE-PREFLIGHT-FOUR-UNITS]",
138
+ "signatureSource": "pi-extensions/lib/pi-durable-fresh-preflight.test.ts",
139
+ "build": {
140
+ "argv": ["bash", "scripts/build-bridge.sh"],
141
+ "output": "mcp/entwurf-bridge/dist"
142
+ }
143
+ },
144
+ {
145
+ "claim": "PI-DURABLE-PREFLIGHT-MODEL-QUALIFIED",
146
+ "title": "a model without a provider part passes the preflight and reaches the durable launch",
147
+ "subject": "pi-extensions/lib/pi-durable-fresh-preflight.ts",
148
+ "find": ["\tif (at <= 0 || at === model.length - 1) return null;"],
149
+ "replace": ["\tif (at < 0 || at === model.length - 1) return null;"],
150
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
151
+ "timeoutSeconds": 300,
152
+ "signature": "[QK:PI-DURABLE-PREFLIGHT-MODEL-QUALIFIED]",
153
+ "signatureSource": "pi-extensions/lib/pi-durable-fresh-preflight.test.ts",
154
+ "build": {
155
+ "argv": ["bash", "scripts/build-bridge.sh"],
156
+ "output": "mcp/entwurf-bridge/dist"
157
+ }
158
+ },
159
+ {
160
+ "claim": "PI-DURABLE-BOOTSTRAP-ARGV-NO-BRIDGE-ENTRY",
161
+ "title": "the launch argv accepts --bridge-entry, so a caller can point the contact at a bridge outside this package",
162
+ "subject": "pi/pi-durable/bootstrap.mjs",
163
+ "find": ["const VALUE_FLAGS = [\"--provider\", \"--model\", \"--width\", \"--entwurf-bootstrap\"];"],
164
+ "replace": [
165
+ "const VALUE_FLAGS = [\"--bridge-entry\", \"--provider\", \"--model\", \"--width\", \"--entwurf-bootstrap\"];"
166
+ ],
167
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
168
+ "timeoutSeconds": 300,
169
+ "signature": "[QK:PI-DURABLE-BOOTSTRAP-ARGV-NO-BRIDGE-ENTRY]",
170
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
171
+ "build": {
172
+ "argv": ["bash", "scripts/build-bridge.sh"],
173
+ "output": "mcp/entwurf-bridge/dist"
174
+ }
175
+ },
176
+ {
177
+ "claim": "PI-DURABLE-FRESH-CALL-EXPOSED",
178
+ "title": "entwurf_fresh_call drops out of the durable contact's tools, so the citizen cannot open a sibling",
179
+ "subject": "pi-extensions/meta-bridge-pi-durable.ts",
180
+ "find": ["\t\ttools: PI_DURABLE_TOOL_NAMES.map(tool),"],
181
+ "replace": ["\t\ttools: PI_DURABLE_TOOL_NAMES.filter((name) => name !== \"entwurf_fresh_call\").map(tool),"],
182
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
183
+ "timeoutSeconds": 300,
184
+ "signature": "[QK:PI-DURABLE-FRESH-CALL-EXPOSED]",
185
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
186
+ "build": {
187
+ "argv": ["bash", "scripts/build-bridge.sh"],
188
+ "output": "mcp/entwurf-bridge/dist"
189
+ }
190
+ },
191
+ {
192
+ "claim": "PI-DURABLE-BRIDGE-DIRECT-SCRUBBED",
193
+ "title": "the identity scrub is omitted, so inherited identity carriers reach the bridge child",
194
+ "subject": "pi-extensions/meta-bridge-pi-durable.ts",
195
+ "find": ["\t\tif (value !== undefined && !scrubbed.has(key)) env[key] = value;\n"],
196
+ "replace": ["\t\tif (value !== undefined) env[key] = value;\n"],
197
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
198
+ "timeoutSeconds": 300,
199
+ "signature": "[QK:PI-DURABLE-BRIDGE-DIRECT-SCRUBBED]",
200
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
201
+ "build": {
202
+ "argv": ["bash", "scripts/build-bridge.sh"],
203
+ "output": "mcp/entwurf-bridge/dist"
204
+ }
205
+ },
206
+ {
207
+ "claim": "PI-DURABLE-CHILD-ROOTS-ARE-WRITTEN-ROOTS",
208
+ "title": "the root binding is omitted, so the bridge child reads whatever garden roots it inherited instead of the roots the contact wrote",
209
+ "subject": "pi-extensions/meta-bridge-pi-durable.ts",
210
+ "find": [
211
+ "\tfor (const key of Object.keys(PI_DURABLE_BRIDGE_ROOT_ENV) as (keyof PiDurableRoots)[]) {\n\t\tenv[PI_DURABLE_BRIDGE_ROOT_ENV[key]] = roots[key];\n\t}\n"
212
+ ],
213
+ "replace": [""],
214
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
215
+ "timeoutSeconds": 300,
216
+ "signature": "[QK:PI-DURABLE-CHILD-ROOTS-ARE-WRITTEN-ROOTS]",
217
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
218
+ "build": {
219
+ "argv": ["bash", "scripts/build-bridge.sh"],
220
+ "output": "mcp/entwurf-bridge/dist"
221
+ }
222
+ },
223
+ {
224
+ "claim": "PDH-JOIN-FOREIGN-SENDER-REFUSED",
225
+ "title": "the durable host leaves the sender join, so a live durable receiver whose owner sender marker names another garden is no longer refused (ABSENT-SENDER and SCOPE also go red)",
226
+ "subject": "pi-extensions/lib/entwurf-deliverability.ts",
227
+ "find": [
228
+ "export const SENDER_JOINED_RECEIVER_OWNER_KINDS: readonly string[] = [\"claude-code-cli\", \"pi-durable-host\"];"
229
+ ],
230
+ "replace": ["export const SENDER_JOINED_RECEIVER_OWNER_KINDS: readonly string[] = [\"claude-code-cli\"];"],
231
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
232
+ "timeoutSeconds": 300,
233
+ "signature": "[QK:PDH-JOIN-FOREIGN-SENDER-REFUSED]",
234
+ "signatureSource": "pi-extensions/lib/entwurf-deliverability.owner-join.test.ts",
235
+ "build": {
236
+ "argv": ["bash", "scripts/build-bridge.sh"],
237
+ "output": "mcp/entwurf-bridge/dist"
238
+ }
239
+ },
240
+ {
241
+ "claim": "PI-DURABLE-SCHEMA-DRIFT-REFUSED",
242
+ "title": "schema drift is never reported, so a bridge whose tool schemas differ from the exposed copies is accepted at birth",
243
+ "subject": "pi-extensions/meta-bridge-pi-durable.ts",
244
+ "find": ["\treturn problems.length === 0 ? undefined : problems.join(\"; \");"],
245
+ "replace": ["\treturn undefined;"],
246
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
247
+ "timeoutSeconds": 300,
248
+ "signature": "[QK:PI-DURABLE-SCHEMA-DRIFT-REFUSED]",
249
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
250
+ "build": {
251
+ "argv": ["bash", "scripts/build-bridge.sh"],
252
+ "output": "mcp/entwurf-bridge/dist"
253
+ }
254
+ },
255
+ {
256
+ "claim": "PI-DURABLE-RECEIVER-REFUSES-UNVERIFIED-IDENTITY",
257
+ "title": "the receiver arms against any live sender marker, even one naming another garden or session",
258
+ "subject": "pi-extensions/meta-bridge-pi-durable.ts",
259
+ "find": [
260
+ "\t\t\tif (sender === null || sender.gardenId !== gardenId || sender.nativeSessionId !== nativeSessionId) {"
261
+ ],
262
+ "replace": [
263
+ "\t\t\tif (sender === null || (sender.gardenId !== gardenId && sender.nativeSessionId !== nativeSessionId)) {"
264
+ ],
265
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
266
+ "timeoutSeconds": 300,
267
+ "signature": "[QK:PI-DURABLE-RECEIVER-REFUSES-UNVERIFIED-IDENTITY]",
268
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
269
+ "build": {
270
+ "argv": ["bash", "scripts/build-bridge.sh"],
271
+ "output": "mcp/entwurf-bridge/dist"
272
+ }
273
+ },
274
+ {
275
+ "claim": "PI-DURABLE-ONE-BIRTH",
276
+ "title": "a born contact can attach again, minting a second birth on the same contact",
277
+ "subject": "pi-extensions/meta-bridge-pi-durable.ts",
278
+ "find": [
279
+ "\t\t\tif (state.kind !== \"pending\") {\n\t\t\t\tthrow new Error(`pi-durable contact: attach is a one-time birth"
280
+ ],
281
+ "replace": [
282
+ "\t\t\tif (state.kind === \"attaching\") {\n\t\t\t\tthrow new Error(`pi-durable contact: attach is a one-time birth"
283
+ ],
284
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
285
+ "timeoutSeconds": 300,
286
+ "signature": "[QK:PI-DURABLE-ONE-BIRTH]",
287
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
288
+ "build": {
289
+ "argv": ["bash", "scripts/build-bridge.sh"],
290
+ "output": "mcp/entwurf-bridge/dist"
291
+ }
292
+ },
293
+ {
294
+ "claim": "PI-DURABLE-FIRST-INPUT-ONCE",
295
+ "title": "the launcher's first input loses its fixed request id, so a reopen can admit the same task again",
296
+ "subject": "pi/pi-durable/bootstrap.mjs",
297
+ "find": ["\t\t\t\trequestId: `entwurf-first-input:${attachment.gardenId}`,\n"],
298
+ "replace": [""],
299
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
300
+ "timeoutSeconds": 300,
301
+ "signature": "[QK:PI-DURABLE-FIRST-INPUT-ONCE]",
302
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
303
+ "build": {
304
+ "argv": ["bash", "scripts/build-bridge.sh"],
305
+ "output": "mcp/entwurf-bridge/dist"
306
+ }
307
+ },
308
+ {
309
+ "claim": "PI-DURABLE-GATE-SEND-OWNS-QUALIFIED-START",
310
+ "title": "the send gate registers a bridge under a fresh start read instead of the start that qualified it, so a pid recycled after qualification is owned and signalled",
311
+ "subject": "scripts/check-pi-durable-send.ts",
312
+ "find": ["own(child.pid, \"bridge\", child.start)"],
313
+ "replace": ["own(child.pid, \"bridge\")"],
314
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
315
+ "timeoutSeconds": 300,
316
+ "signature": "[QK:PI-DURABLE-GATE-SEND-OWNS-QUALIFIED-START]",
317
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
318
+ "build": {
319
+ "argv": ["bash", "scripts/build-bridge.sh"],
320
+ "output": "mcp/entwurf-bridge/dist"
321
+ }
322
+ },
323
+ {
324
+ "claim": "PI-DURABLE-GATE-SEND-OBSERVED-BY-IDENTITY",
325
+ "title": "the send gate looks complete observation up by pid, so a host whose pid was reused inherits its predecessor's observed children",
326
+ "subject": "scripts/check-pi-durable-send.ts",
327
+ "find": ["childrenObserved.has(host)"],
328
+ "replace": ["[...childrenObserved].some((h) => h.pid === host.pid)"],
329
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
330
+ "timeoutSeconds": 300,
331
+ "signature": "[QK:PI-DURABLE-GATE-SEND-OBSERVED-BY-IDENTITY]",
332
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
333
+ "build": {
334
+ "argv": ["bash", "scripts/build-bridge.sh"],
335
+ "output": "mcp/entwurf-bridge/dist"
336
+ }
337
+ },
338
+ {
339
+ "claim": "PI-DURABLE-GATE-SEND-S3-START-RECHECKED",
340
+ "title": "the send gate's SIGKILL of the paused bridge loses its start-key re-check, so a process that replaced it after the IO and state reads is killed",
341
+ "subject": "scripts/check-pi-durable-send.ts",
342
+ "find": [
343
+ "\tif (procStat(bridge1.pid)?.start !== bridge1.start)\n\t\tthrow new Error(`GAP: bridge1 ${bridge1.pid} is not the observed process immediately before its SIGKILL`);\n"
344
+ ],
345
+ "replace": [""],
346
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
347
+ "timeoutSeconds": 300,
348
+ "signature": "[QK:PI-DURABLE-GATE-SEND-S3-START-RECHECKED]",
349
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
350
+ "build": {
351
+ "argv": ["bash", "scripts/build-bridge.sh"],
352
+ "output": "mcp/entwurf-bridge/dist"
353
+ }
354
+ },
355
+ {
356
+ "claim": "PI-DURABLE-GATE-RECEIVE-CONNECT-CLOSES",
357
+ "title": "the receive gate's failed connect no longer closes the transport, so a bridge spawned inside connect is left neither owned nor closed",
358
+ "subject": "scripts/check-pi-durable-receive.ts",
359
+ "find": [
360
+ "\ttry {\n\t\tawait client.connect(transport);\n\t} catch (error) {\n\t\t// A failed connect may already have spawned the bridge, which is not owned yet: close the\n\t\t// transport (it ends the child it spawned) before rethrowing, and keep the cause first.\n\t\ttry {\n\t\t\tawait transport.close();\n\t\t} catch (closeError) {\n\t\t\tthrow new AggregateError(\n\t\t\t\t[error, closeError],\n\t\t\t\t`the sending bridge did not connect (${(error as Error).message}); closing it also failed`,\n\t\t\t);\n\t\t}\n\t\tthrow error;\n\t}\n"
361
+ ],
362
+ "replace": ["\tawait client.connect(transport);\n"],
363
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
364
+ "timeoutSeconds": 300,
365
+ "signature": "[QK:PI-DURABLE-GATE-RECEIVE-CONNECT-CLOSES]",
366
+ "signatureSource": "pi-extensions/lib/pi-durable-runtime.test.ts",
367
+ "build": {
368
+ "argv": ["bash", "scripts/build-bridge.sh"],
369
+ "output": "mcp/entwurf-bridge/dist"
370
+ }
371
+ },
372
+ {
373
+ "claim": "PI-DURABLE-RECEIVER-DIAGNOSTIC-LATEST-ONLY",
374
+ "title": "the receiver diagnostic appends every successful admission instead of keeping the latest one, so it grows with the host's lifetime (RING-COALESCES, whose two-pass expectation is latest-only, also goes red)",
375
+ "subject": "pi-extensions/meta-bridge-pi-durable.ts",
376
+ "find": [
377
+ "\t\t\t\tadmissions.splice(0, admissions.length, { why, requestId, submissionId: submission.id, fresh, unread });"
378
+ ],
379
+ "replace": ["\t\t\t\tadmissions.push({ why, requestId, submissionId: submission.id, fresh, unread });"],
380
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
381
+ "timeoutSeconds": 300,
382
+ "signature": "[QK:PI-DURABLE-RECEIVER-DIAGNOSTIC-LATEST-ONLY]",
383
+ "signatureSource": "pi-extensions/meta-bridge-pi-durable.test.ts",
384
+ "build": {
385
+ "argv": ["bash", "scripts/build-bridge.sh"],
386
+ "output": "mcp/entwurf-bridge/dist"
387
+ }
388
+ }
389
+ ]
390
+ }
@@ -7,10 +7,10 @@
7
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
8
  "subject": "pnpm-workspace.yaml",
9
9
  "find": [
10
- " - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0'"
10
+ " - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0 || 1.0.2'"
11
11
  ],
12
12
  "replace": [
13
- " - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2'"
13
+ " - '@earendil-works/pi-telemetry@0.85.0 || 0.85.1 || 0.86.0 || 0.87.0 || 0.87.1 || 0.99.1 || 0.99.2 || 1.0.0'"
14
14
  ],
15
15
  "gate": ["bash", "run.sh", "check-dep-versions"],
16
16
  "timeoutSeconds": 300,
@@ -0,0 +1,105 @@
1
+ // G-contact host half (#129 L-identity). Runs AS the durable host, under the pinned overlay
2
+ // checkout's source resolver, and is spawned only by scripts/check-pi-durable-contact.ts:
3
+ //
4
+ // node --import <checkout>/packages/coding-agent/src/experimental/source-resolver.ts \
5
+ // scripts/pi-durable-contact-host.mjs <bridge entry> new|continue \
6
+ // [<sessions dir> <senders dir> <mailbox dir> <receivers dir>]
7
+ //
8
+ // The four optional directories are explicit garden roots handed to the contact, all or none;
9
+ // without them the contact uses the shared roots its environment selects.
10
+ //
11
+ // With ENTWURF_PI_DURABLE_CONTACT_FAULT=malformed-report (set only by the gate's control) the report
12
+ // line is deliberately not JSON.
13
+ //
14
+ // It opens the durable app through the production bootstrap composition (`openDurableCitizen`),
15
+ // then executes the contact's registered tools DIRECTLY — adapter-direct, harness-bypassed: no
16
+ // model turn and no ToolTask, so this proves the adapter's bridge contact, not the harness
17
+ // selecting or validating the tool. The report carries the contact's own `receiver()` state, never
18
+ // the path the attachment returned. It prints one JSON report line, holds the host and its
19
+ // bridge child alive until the orchestrator writes `close` (so /proc can be read from outside),
20
+ // closes, prints `{"closed":true}` and exits.
21
+ //
22
+ // The bridge's environment is observed at the SDK connector boundary only: the contact's existing
23
+ // `connect` injection wraps the COMPILED production `connectStdioBridge`, records the spawn spec's
24
+ // command/args/cwd, whether each of the four poisoned identity carrier NAMES is present, and the
25
+ // four root carrier VALUES — nothing else of the env — and forwards the spec unchanged to that real
26
+ // connector. It is the spec the adapter hands the SDK, not the kernel child environment; no
27
+ // /proc/<pid>/environ is read anywhere.
28
+ import { createInterface } from "node:readline";
29
+ import { connectStdioBridge } from "../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js";
30
+ import { openDurableCitizen } from "../plugins/pi-durable/bootstrap.mjs";
31
+
32
+ const IDENTITY_CARRIERS = ["PI_SESSION_ID", "PI_AGENT_ID", "ENTWURF_META_SENDER_MARKER", "ENTWURF_BRIDGE_NATIVE_HOST"];
33
+ const ROOT_CARRIERS = [
34
+ "ENTWURF_META_SESSIONS_DIR",
35
+ "ENTWURF_META_SENDERS_DIR",
36
+ "ENTWURF_META_MAILBOX_DIR",
37
+ "ENTWURF_META_RECEIVERS_DIR",
38
+ ];
39
+ const bridgeSpecs = [];
40
+ const connect = async (spec) => {
41
+ bridgeSpecs.push({
42
+ command: spec.command,
43
+ args: [...spec.args],
44
+ cwd: spec.cwd,
45
+ identityCarrierPresent: Object.fromEntries(IDENTITY_CARRIERS.map((name) => [name, Object.hasOwn(spec.env, name)])),
46
+ rootCarriers: Object.fromEntries(ROOT_CARRIERS.map((name) => [name, spec.env[name] ?? null])),
47
+ });
48
+ return connectStdioBridge(spec);
49
+ };
50
+
51
+ const [bridgeEntry, mode, ...rootArgs] = process.argv.slice(2);
52
+ if (bridgeEntry === undefined || (mode !== "new" && mode !== "continue") || ![0, 4].includes(rootArgs.length)) {
53
+ throw new Error(
54
+ "usage: pi-durable-contact-host.mjs <bridge entry> new|continue [<sessions dir> <senders dir> <mailbox dir> <receivers dir>]",
55
+ );
56
+ }
57
+ const [sessionsDir, sendersDir, mailboxDir, receiversDir] = rootArgs;
58
+
59
+ const citizen = await openDurableCitizen({
60
+ bridgeEntry,
61
+ continueSession: mode === "continue",
62
+ cwd: process.cwd(),
63
+ contact: rootArgs.length === 0 ? { connect } : { sessionsDir, sendersDir, mailboxDir, receiversDir, connect },
64
+ });
65
+ let code = 0;
66
+ try {
67
+ const run = async (name) => {
68
+ const tool = citizen.contact.extension.tools.find((t) => t.name === name);
69
+ if (tool === undefined) throw new Error(`the contact registers no ${name}`);
70
+ const result = await tool.execute({}, undefined, {});
71
+ return result.content.map((block) => block.text).join("\n");
72
+ };
73
+ const view = citizen.durable.view.current();
74
+ const report = {
75
+ hostPid: process.pid,
76
+ session: view.session,
77
+ attachment: citizen.attachment,
78
+ receiver: citizen.contact.receiver(),
79
+ toolNames: citizen.contact.extension.tools.map((t) => t.name),
80
+ bridgeSpecs,
81
+ self: await run("entwurf_self"),
82
+ peers: await run("entwurf_peers"),
83
+ notices: view.notices.map((n) => `${n.level}: ${n.message}`),
84
+ };
85
+ // The gate's malformed-report control: the report line is deliberately not JSON.
86
+ if (process.env.ENTWURF_PI_DURABLE_CONTACT_FAULT === "malformed-report") {
87
+ process.stdout.write(`{"malformed report line\n`);
88
+ } else {
89
+ process.stdout.write(`${JSON.stringify(report)}\n`);
90
+ }
91
+ const lines = createInterface({ input: process.stdin });
92
+ for await (const line of lines) if (line.trim() === "close") break;
93
+ } catch (error) {
94
+ code = 1;
95
+ // The failure report keeps its cause first and carries, beside it, only the host pid and the same
96
+ // connector-boundary record the success report has — so the gate can judge the scrub/root QKs before
97
+ // it throws this error. It is never a success: no attachment, self, peers or session is reported.
98
+ process.stdout.write(
99
+ `${JSON.stringify({ error: error instanceof Error ? error.message : String(error), hostPid: process.pid, bridgeSpecs })}\n`,
100
+ );
101
+ } finally {
102
+ await citizen.close();
103
+ }
104
+ process.stdout.write(`${JSON.stringify({ closed: true })}\n`);
105
+ process.exit(code);
@@ -0,0 +1,127 @@
1
+ // G-receive host half (#129 L-receive). Runs AS the durable host, under the pinned overlay checkout's
2
+ // source resolver, and is spawned only by scripts/check-pi-durable-receive.ts:
3
+ //
4
+ // node --import <checkout>/packages/coding-agent/src/experimental/source-resolver.ts \
5
+ // scripts/pi-durable-receive-host.mjs <bridge entry>
6
+ //
7
+ // It opens the durable app through the production bootstrap composition — which arms the contact's
8
+ // doorbell on the root conversation — reports `born`, and then gives the app NO input: no controller
9
+ // call, no tool call, no bridge call, no native state of its own. Whatever runs afterwards was
10
+ // started by the doorbell's root admission. The driver only watches the native view and the
11
+ // contact's own `receiver()` state (never the path the attachment returned, which a later retire
12
+ // can make stale), and reports once:
13
+ //
14
+ // - the receiver has made an admission, the view holds an assistant entry after the newest user
15
+ // entry that calls no tool, and the conversation is no longer busy; or
16
+ // - the receiver is no longer armed; or
17
+ // - the app raised an error notice.
18
+ //
19
+ // Then it holds the host alive until the gate writes `close`, closes, prints `{"closed":true}` and
20
+ // exits.
21
+ import { createInterface } from "node:readline";
22
+ import { openDurableCitizen } from "../plugins/pi-durable/bootstrap.mjs";
23
+
24
+ const USAGE = "usage: pi-durable-receive-host.mjs <bridge entry>";
25
+ const [bridgeEntry, ...extra] = process.argv.slice(2);
26
+ if (bridgeEntry === undefined || extra.length > 0) throw new Error(USAGE);
27
+
28
+ const stdinLines = createInterface({ input: process.stdin })[Symbol.asyncIterator]();
29
+ /** Wait for one exact line from the gate; the gate closing stdin first is a failure. */
30
+ async function waitFor(word) {
31
+ for (;;) {
32
+ const next = await stdinLines.next();
33
+ if (next.done) throw new Error(`stdin closed before "${word}"`);
34
+ if (next.value.trim() === word) return;
35
+ }
36
+ }
37
+
38
+ /** One entry of the native view, reduced to what the gate compares (the send driver's reduction). */
39
+ function summarize(entry) {
40
+ const message = entry.model?.[0];
41
+ const blocks = Array.isArray(message?.content) ? message.content : [];
42
+ const text = (typeof message?.content === "string" ? [message.content] : [])
43
+ .concat(blocks.flatMap((b) => (b.type === "text" ? [b.text] : [])))
44
+ .join("\n");
45
+ return {
46
+ id: entry.id,
47
+ kind: entry.kind,
48
+ role: message?.role,
49
+ toolCalls: blocks
50
+ .filter((b) => b.type === "toolCall")
51
+ .map((b) => ({ id: b.id, name: b.name, arguments: b.arguments })),
52
+ ...(message?.role === "toolResult"
53
+ ? {
54
+ toolCallId: message.toolCallId,
55
+ toolName: message.toolName,
56
+ isError: message.isError === true,
57
+ diagnostics: Array.isArray(entry.data?.diagnostics) ? entry.data.diagnostics : [],
58
+ }
59
+ : {}),
60
+ text,
61
+ };
62
+ }
63
+
64
+ const citizen = await openDurableCitizen({ bridgeEntry, cwd: process.cwd() });
65
+
66
+ function reportOf(view, why) {
67
+ return {
68
+ settled: why,
69
+ hostPid: process.pid,
70
+ session: view.session,
71
+ conversationId: view.conversation.conversation.id,
72
+ receiver: citizen.contact.receiver(),
73
+ entries: view.conversation.entries.filter((e) => e.kind.startsWith("pi.")).map(summarize),
74
+ busy: view.conversation.docs["pi.live"]?.run !== undefined,
75
+ notices: view.notices.map((n) => `${n.level}: ${n.message}`),
76
+ };
77
+ }
78
+
79
+ let code = 0;
80
+ try {
81
+ const { durable } = citizen;
82
+ const born = durable.view.current();
83
+ const noticesBefore = born.notices.length;
84
+ process.stdout.write(
85
+ `${JSON.stringify({
86
+ born: true,
87
+ hostPid: process.pid,
88
+ session: born.session,
89
+ conversationId: born.conversation.conversation.id,
90
+ attachment: citizen.attachment,
91
+ receiver: citizen.contact.receiver(),
92
+ })}\n`,
93
+ );
94
+ const why = await new Promise((resolve) => {
95
+ let done = false;
96
+ const check = () => {
97
+ if (done) return;
98
+ const view = durable.view.current();
99
+ const receiver = citizen.contact.receiver();
100
+ const entries = view.conversation.entries;
101
+ const lastUser = entries.findLastIndex((e) => e.kind === "pi.user");
102
+ const lastAnswer =
103
+ lastUser < 0 ? undefined : entries.slice(lastUser + 1).findLast((e) => e.kind === "pi.assistant");
104
+ const answered = lastAnswer !== undefined && summarize(lastAnswer).toolCalls.length === 0;
105
+ const busy = view.conversation.docs["pi.live"]?.run !== undefined;
106
+ let outcome;
107
+ if (receiver.status !== "armed") outcome = "receiver-not-armed";
108
+ else if (view.notices.slice(noticesBefore).some((n) => n.level === "error")) outcome = "error-notice";
109
+ else if (receiver.admissions.length > 0 && answered && !busy) outcome = "answered";
110
+ if (outcome === undefined) return;
111
+ done = true;
112
+ unsubscribe();
113
+ resolve(outcome);
114
+ };
115
+ const unsubscribe = durable.view.subscribe(check);
116
+ check();
117
+ });
118
+ process.stdout.write(`${JSON.stringify(reportOf(durable.view.current(), why))}\n`);
119
+ await waitFor("close");
120
+ } catch (error) {
121
+ code = 1;
122
+ process.stdout.write(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`);
123
+ } finally {
124
+ await citizen.close();
125
+ }
126
+ process.stdout.write(`${JSON.stringify({ closed: true })}\n`);
127
+ process.exit(code);