@junghanacs/entwurf 0.12.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 (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
@@ -0,0 +1,673 @@
1
+ /**
2
+ * check-meta-session — deterministic gate for the 1.0.0 meta-bridge record
3
+ * authority (step 2). Pure functions + a real temp-dir scan; no backend, no
4
+ * network, no hook. Safe in the `pnpm check` static floor.
5
+ *
6
+ * Proves the contract the later hook/CLI/mailbox steps build on:
7
+ * - mint stamps a valid garden id + seeded read-receipt slot;
8
+ * - serialize is deterministic and round-trips through parse;
9
+ * - parse crashes (not warns) on every malformed shape;
10
+ * - scanByNativeId is the lookup AUTHORITY by record BODY, never by filename;
11
+ * - decideUpsert keys on record existence (idempotent), refuses identity drift;
12
+ * - the pre-drilled read-receipt mutators touch only their own field.
13
+ */
14
+
15
+ import assert from "node:assert/strict";
16
+ import fs from "node:fs";
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import {
20
+ decideUpsert,
21
+ defaultMetaMailboxDir,
22
+ defaultMetaSessionsDir,
23
+ enqueueMetaMessage,
24
+ META_BACKEND_DESCRIPTORS,
25
+ META_SCHEMA_VERSION,
26
+ META_SCHEMA_VERSION_V2,
27
+ type MetaMintInput,
28
+ MetaRecordError,
29
+ markDelivered,
30
+ markEnqueued,
31
+ markRead,
32
+ metaRecordFilename,
33
+ mintMetaRecord,
34
+ parseMetaIdentity,
35
+ parseMetaRecord,
36
+ readMailboxReceiptState,
37
+ readMetaInbox,
38
+ readMetaRecordV1ByGardenId,
39
+ scanByNativeId,
40
+ serializeMetaRecord,
41
+ upsertMetaSession,
42
+ } from "../pi-extensions/lib/meta-session.ts";
43
+
44
+ const SESSION_ID_RE = /^\d{8}T\d{6}-[0-9a-f]{6}$/;
45
+ const T0 = new Date("2026-06-05T05:00:00.000Z");
46
+ const T1 = new Date("2026-06-05T06:30:00.000Z");
47
+
48
+ let assertions = 0;
49
+ function check(label: string, fn: () => void): void {
50
+ fn();
51
+ assertions += 1;
52
+ process.stdout.write(`[check-meta-session] ${label}: ok\n`);
53
+ }
54
+
55
+ function claudeInput(overrides: Partial<MetaMintInput> = {}): MetaMintInput {
56
+ return {
57
+ backend: "claude-code",
58
+ nativeSessionId: "11111111-1111-4111-8111-111111111111",
59
+ transcriptPath: "/home/u/.claude/projects/-home-u-proj/11111111.jsonl",
60
+ cwd: "/home/u/proj",
61
+ ...overrides,
62
+ };
63
+ }
64
+
65
+ function expectThrows(label: string, fn: () => void): void {
66
+ check(label, () => {
67
+ assert.throws(fn, MetaRecordError, `${label}: expected MetaRecordError`);
68
+ });
69
+ }
70
+
71
+ // ---------------------------------------------------------------- mint
72
+ check("mint: garden id matches the SSOT grammar", () => {
73
+ const r = mintMetaRecord(claudeInput(), T0);
74
+ assert.match(r.gardenId, SESSION_ID_RE);
75
+ });
76
+
77
+ check("mint: createdAt == lastSeen at birth, ISO from `now`", () => {
78
+ const r = mintMetaRecord(claudeInput(), T0);
79
+ assert.equal(r.createdAt, T0.toISOString());
80
+ assert.equal(r.lastSeen, T0.toISOString());
81
+ });
82
+
83
+ check("mint: delivery slot seeded from backend descriptor, receipts null", () => {
84
+ const r = mintMetaRecord(claudeInput(), T0);
85
+ const d = META_BACKEND_DESCRIPTORS["claude-code"];
86
+ assert.equal(r.delivery.wakeMode, d.wakeMode);
87
+ assert.equal(r.delivery.wakeMode, "self-fetch");
88
+ assert.equal(r.delivery.deliveryLevel, d.deliveryLevel);
89
+ assert.equal(r.delivery.lastEnqueuedAt, null);
90
+ assert.equal(r.delivery.lastDeliveredAt, null);
91
+ assert.equal(r.delivery.lastReadAt, null);
92
+ assert.equal(r.schemaVersion, META_SCHEMA_VERSION);
93
+ });
94
+
95
+ check("mint: agy/codex direct-inject descriptors differ from claude self-fetch", () => {
96
+ const agy = mintMetaRecord(claudeInput({ backend: "antigravity", nativeSessionId: "agy-conv-1" }), T0);
97
+ const codex = mintMetaRecord(claudeInput({ backend: "codex", nativeSessionId: "codex-thread-1" }), T0);
98
+ assert.equal(agy.delivery.wakeMode, "direct-inject");
99
+ assert.equal(codex.delivery.wakeMode, "direct-inject");
100
+ });
101
+
102
+ expectThrows("mint: empty nativeSessionId throws", () => mintMetaRecord(claudeInput({ nativeSessionId: "" }), T0));
103
+ expectThrows("mint: empty transcriptPath throws", () => mintMetaRecord(claudeInput({ transcriptPath: "" }), T0));
104
+ expectThrows("mint: empty cwd throws", () => mintMetaRecord(claudeInput({ cwd: "" }), T0));
105
+ expectThrows("mint: bad backend throws", () =>
106
+ mintMetaRecord(claudeInput({ backend: "gemini" as unknown as MetaMintInput["backend"] }), T0),
107
+ );
108
+
109
+ // ---------------------------------------------------------------- serialize / parse
110
+ check("serialize: deterministic (same record → byte-identical)", () => {
111
+ const r = mintMetaRecord(claudeInput(), T0);
112
+ assert.equal(serializeMetaRecord(r), serializeMetaRecord(r));
113
+ });
114
+
115
+ check("serialize → parse round-trips to a deep-equal record", () => {
116
+ const r = mintMetaRecord(claudeInput(), T0);
117
+ assert.deepEqual(parseMetaRecord(serializeMetaRecord(r)), r);
118
+ });
119
+
120
+ check("serialize: trailing newline, 2-space indent, stable key order", () => {
121
+ const r = mintMetaRecord(claudeInput(), T0);
122
+ const text = serializeMetaRecord(r);
123
+ assert.ok(text.endsWith("}\n"));
124
+ const keys = Object.keys(JSON.parse(text));
125
+ assert.deepEqual(keys, [
126
+ "schemaVersion",
127
+ "gardenId",
128
+ "backend",
129
+ "nativeSessionId",
130
+ "transcriptPath",
131
+ "cwd",
132
+ "createdAt",
133
+ "lastSeen",
134
+ "delivery",
135
+ ]);
136
+ });
137
+
138
+ expectThrows("parse: invalid JSON throws", () => parseMetaRecord("{not json"));
139
+ expectThrows("parse: array (non-object) throws", () => parseMetaRecord("[]"));
140
+ expectThrows("parse: wrong schemaVersion throws", () => {
141
+ const r = mintMetaRecord(claudeInput(), T0);
142
+ const bad = JSON.parse(serializeMetaRecord(r));
143
+ bad.schemaVersion = 2;
144
+ parseMetaRecord(JSON.stringify(bad));
145
+ });
146
+ expectThrows("parse: malformed gardenId throws", () => {
147
+ const r = mintMetaRecord(claudeInput(), T0);
148
+ const bad = JSON.parse(serializeMetaRecord(r));
149
+ bad.gardenId = "not-a-garden-id";
150
+ parseMetaRecord(JSON.stringify(bad));
151
+ });
152
+ expectThrows("parse: unknown backend throws", () => {
153
+ const r = mintMetaRecord(claudeInput(), T0);
154
+ const bad = JSON.parse(serializeMetaRecord(r));
155
+ bad.backend = "openai";
156
+ parseMetaRecord(JSON.stringify(bad));
157
+ });
158
+ expectThrows("parse: missing nativeSessionId throws", () => {
159
+ const r = mintMetaRecord(claudeInput(), T0);
160
+ const bad = JSON.parse(serializeMetaRecord(r));
161
+ delete bad.nativeSessionId;
162
+ parseMetaRecord(JSON.stringify(bad));
163
+ });
164
+ expectThrows("parse: bad delivery.wakeMode throws", () => {
165
+ const r = mintMetaRecord(claudeInput(), T0);
166
+ const bad = JSON.parse(serializeMetaRecord(r));
167
+ bad.delivery.wakeMode = "magic";
168
+ parseMetaRecord(JSON.stringify(bad));
169
+ });
170
+ expectThrows("parse: delivery not an object throws", () => {
171
+ const r = mintMetaRecord(claudeInput(), T0);
172
+ const bad = JSON.parse(serializeMetaRecord(r));
173
+ bad.delivery = "nope";
174
+ parseMetaRecord(JSON.stringify(bad));
175
+ });
176
+ expectThrows("parse: backend↔wakeMode contradiction throws (claude record claiming direct-inject)", () => {
177
+ const r = mintMetaRecord(claudeInput(), T0); // claude-code → self-fetch
178
+ const bad = JSON.parse(serializeMetaRecord(r));
179
+ bad.delivery.wakeMode = "direct-inject"; // valid mode, wrong for this backend
180
+ parseMetaRecord(JSON.stringify(bad));
181
+ });
182
+ check("parse: matching backend↔wakeMode round-trips (codex direct-inject)", () => {
183
+ const r = mintMetaRecord(claudeInput({ backend: "codex", nativeSessionId: "codex-thread-X" }), T0);
184
+ assert.deepEqual(parseMetaRecord(serializeMetaRecord(r)), r);
185
+ });
186
+
187
+ // ---------------------------------------------------------------- filename
188
+ check("metaRecordFilename: <gardenId>.meta.json", () => {
189
+ const r = mintMetaRecord(claudeInput(), T0);
190
+ assert.equal(metaRecordFilename(r), `${r.gardenId}.meta.json`);
191
+ });
192
+
193
+ // ---------------------------------------------------------------- scanByNativeId (in-memory)
194
+ check("scanByNativeId: ignores non-.meta.json entries", () => {
195
+ const r = mintMetaRecord(claudeInput(), T0);
196
+ const reader = (f: string) => {
197
+ if (f === "x.meta.json") return serializeMetaRecord(r);
198
+ throw new Error(`unexpected read ${f}`);
199
+ };
200
+ const found = scanByNativeId(["README.md", "notes.txt", "x.meta.json"], r.nativeSessionId, reader);
201
+ assert.deepEqual(found, r);
202
+ });
203
+
204
+ check("scanByNativeId: returns null when no body matches", () => {
205
+ const r = mintMetaRecord(claudeInput(), T0);
206
+ const found = scanByNativeId(["a.meta.json"], "no-such-native-id", () => serializeMetaRecord(r));
207
+ assert.equal(found, null);
208
+ });
209
+
210
+ check("scanByNativeId: malformed entry is skipped via onSkip, scan continues", () => {
211
+ const good = mintMetaRecord(claudeInput(), T0);
212
+ const skipped: string[] = [];
213
+ const reader = (f: string) => (f === "bad.meta.json" ? "{broken" : serializeMetaRecord(good));
214
+ const found = scanByNativeId(["bad.meta.json", "good.meta.json"], good.nativeSessionId, reader, (f) =>
215
+ skipped.push(f),
216
+ );
217
+ assert.deepEqual(found, good);
218
+ assert.deepEqual(skipped, ["bad.meta.json"]);
219
+ });
220
+
221
+ expectThrows("scanByNativeId: duplicate nativeSessionId is authority ambiguity → throws", () => {
222
+ // Two records (different garden ids) claiming the SAME nativeSessionId. The
223
+ // native→garden mapping must be unique; the scan must fail-fast, not pick one.
224
+ const a = mintMetaRecord(claudeInput({ nativeSessionId: "dup-native" }), T0);
225
+ const b = mintMetaRecord(claudeInput({ nativeSessionId: "dup-native" }), T1);
226
+ assert.notEqual(a.gardenId, b.gardenId);
227
+ const reader = (f: string) => (f === "a.meta.json" ? serializeMetaRecord(a) : serializeMetaRecord(b));
228
+ scanByNativeId(["a.meta.json", "b.meta.json"], "dup-native", reader);
229
+ });
230
+
231
+ // ---------------------------------------------------------------- decideUpsert
232
+ check("decideUpsert: absent → create, fresh garden id", () => {
233
+ const dec = decideUpsert(null, claudeInput(), T0);
234
+ assert.equal(dec.action, "create");
235
+ assert.match(dec.record.gardenId, SESSION_ID_RE);
236
+ });
237
+
238
+ check("decideUpsert: present → attach, identity preserved, recordUpdatedAt refreshed (v2)", () => {
239
+ const created = decideUpsert(null, claudeInput(), T0).record;
240
+ const moved = claudeInput({ transcriptPath: "/new/path.jsonl", cwd: "/new/cwd" });
241
+ const dec = decideUpsert(created, moved, T1);
242
+ assert.equal(dec.action, "attach");
243
+ assert.equal(dec.record.schemaVersion, META_SCHEMA_VERSION_V2); // v2 identity
244
+ assert.equal(dec.record.gardenId, created.gardenId); // same id
245
+ assert.equal(dec.record.createdAt, created.createdAt); // birth preserved
246
+ assert.equal(dec.record.recordUpdatedAt, T1.toISOString()); // refreshed (NOT lastSeen)
247
+ assert.equal(dec.record.transcriptPath, "/new/path.jsonl"); // string set
248
+ assert.equal(dec.record.cwd, "/new/cwd");
249
+ });
250
+
251
+ check("decideUpsert: 3-value attach merge — undefined keeps, null clears, string sets (G5)", () => {
252
+ const created = decideUpsert(null, claudeInput({ transcriptPath: "/orig.jsonl" }), T0).record;
253
+ assert.equal(created.transcriptPath, "/orig.jsonl");
254
+ // undefined transcriptPath → KEEP existing (a pi-birth caller must not wipe it)
255
+ const kept = decideUpsert(
256
+ created,
257
+ { backend: "claude-code", nativeSessionId: created.nativeSessionId, cwd: "/c" },
258
+ T1,
259
+ );
260
+ assert.equal(kept.record.transcriptPath, "/orig.jsonl", "undefined keeps existing");
261
+ // explicit null → CLEAR
262
+ const cleared = decideUpsert(
263
+ created,
264
+ { backend: "claude-code", nativeSessionId: created.nativeSessionId, cwd: "/c", transcriptPath: null },
265
+ T1,
266
+ );
267
+ assert.equal(cleared.record.transcriptPath, null, "explicit null clears");
268
+ });
269
+
270
+ expectThrows("decideUpsert: non-boolean isEntwurf throws (runtime guard, not coerced)", () => {
271
+ decideUpsert(null, { ...claudeInput(), isEntwurf: "yes" as unknown as boolean }, T0);
272
+ });
273
+
274
+ check("decideUpsert: idempotent — create then attach never mints a 2nd id", () => {
275
+ const first = decideUpsert(null, claudeInput(), T0).record;
276
+ const second = decideUpsert(first, claudeInput(), T1);
277
+ assert.equal(second.action, "attach");
278
+ assert.equal(second.record.gardenId, first.gardenId);
279
+ });
280
+
281
+ expectThrows("decideUpsert: nativeSessionId mismatch throws (wrong scan key)", () => {
282
+ const created = decideUpsert(null, claudeInput(), T0).record;
283
+ decideUpsert(created, claudeInput({ nativeSessionId: "different-id" }), T1);
284
+ });
285
+
286
+ expectThrows("decideUpsert: backend drift for same native id throws", () => {
287
+ const created = decideUpsert(null, claudeInput(), T0).record;
288
+ // same nativeSessionId, different backend → corruption
289
+ decideUpsert(created, claudeInput({ backend: "codex" }), T1);
290
+ });
291
+
292
+ // ---------------------------------------------------------------- read-receipt mutators
293
+ check("read-receipt mutators touch only their own field", () => {
294
+ const base = mintMetaRecord(claudeInput(), T0);
295
+ const enq = markEnqueued(base, T1);
296
+ assert.equal(enq.delivery.lastEnqueuedAt, T1.toISOString());
297
+ assert.equal(enq.delivery.lastDeliveredAt, null);
298
+ assert.equal(enq.delivery.lastReadAt, null);
299
+
300
+ const del = markDelivered(enq, T1);
301
+ assert.equal(del.delivery.lastDeliveredAt, T1.toISOString());
302
+ assert.equal(del.delivery.lastReadAt, null);
303
+
304
+ const read = markRead(del, T1);
305
+ assert.equal(read.delivery.lastReadAt, T1.toISOString());
306
+ // original untouched (pure)
307
+ assert.equal(base.delivery.lastEnqueuedAt, null);
308
+ });
309
+
310
+ // ---------------------------------------------------------------- temp-dir: authority = body, not filename
311
+ check("temp-dir scan: authority is record BODY, not filename", () => {
312
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-scan-"));
313
+ try {
314
+ const a = mintMetaRecord(claudeInput({ nativeSessionId: "native-A" }), T0);
315
+ const b = mintMetaRecord(claudeInput({ backend: "codex", nativeSessionId: "native-B" }), T1);
316
+ // Write A under its honest filename, but B under a DECOY filename whose
317
+ // garden id does not match B's body — a filename-parser would be fooled.
318
+ fs.writeFileSync(path.join(dir, metaRecordFilename(a)), serializeMetaRecord(a));
319
+ const decoyName = "19990101T000000-deadbe.meta.json";
320
+ assert.notEqual(decoyName, metaRecordFilename(b));
321
+ fs.writeFileSync(path.join(dir, decoyName), serializeMetaRecord(b));
322
+ fs.writeFileSync(path.join(dir, "unrelated.txt"), "noise");
323
+
324
+ const entries = fs.readdirSync(dir);
325
+ const reader = (f: string) => fs.readFileSync(path.join(dir, f), "utf8");
326
+
327
+ const foundA = scanByNativeId(entries, "native-A", reader);
328
+ assert.ok(foundA && foundA.gardenId === a.gardenId);
329
+ // B is found by BODY nativeSessionId despite the decoy filename.
330
+ const foundB = scanByNativeId(entries, "native-B", reader);
331
+ assert.ok(foundB && foundB.gardenId === b.gardenId && foundB.backend === "codex");
332
+ assert.equal(scanByNativeId(entries, "native-Z", reader), null);
333
+ } finally {
334
+ fs.rmSync(dir, { recursive: true, force: true });
335
+ }
336
+ });
337
+
338
+ // ---------------------------------------------------------------- upsertMetaSession (real fs, step 3)
339
+ check("upsertMetaSession: first call creates a record on disk", () => {
340
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-store-"));
341
+ try {
342
+ const res = upsertMetaSession({ dir, input: claudeInput(), now: T0 });
343
+ assert.equal(res.action, "create");
344
+ assert.equal(res.record.schemaVersion, META_SCHEMA_VERSION_V2); // writes v2
345
+ assert.equal(res.path, path.join(dir, `${res.record.gardenId}.meta.json`));
346
+ assert.ok(fs.existsSync(res.path));
347
+ // on-disk bytes parse back (dual-read) to the same v2 identity
348
+ assert.deepEqual(parseMetaIdentity(fs.readFileSync(res.path, "utf8")), res.record);
349
+ } finally {
350
+ fs.rmSync(dir, { recursive: true, force: true });
351
+ }
352
+ });
353
+
354
+ check("upsertMetaSession: second call attaches — same file/id, recordUpdatedAt refreshed, no 2nd file", () => {
355
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-store-"));
356
+ try {
357
+ const first = upsertMetaSession({ dir, input: claudeInput(), now: T0 });
358
+ const moved = claudeInput({ transcriptPath: "/moved/path.jsonl", cwd: "/moved/cwd" });
359
+ const second = upsertMetaSession({ dir, input: moved, now: T1 });
360
+ assert.equal(second.action, "attach");
361
+ assert.equal(second.record.gardenId, first.record.gardenId); // same id
362
+ assert.equal(second.path, first.path); // same file, rewritten in place
363
+ assert.equal(second.record.createdAt, T0.toISOString()); // birth preserved
364
+ assert.equal(second.record.recordUpdatedAt, T1.toISOString()); // refreshed (NOT lastSeen)
365
+ assert.equal(second.record.transcriptPath, "/moved/path.jsonl");
366
+ // exactly ONE .meta.json on disk (idempotent — no shadow record)
367
+ const metas = fs.readdirSync(dir).filter((f) => f.endsWith(".meta.json"));
368
+ assert.deepEqual(metas, [`${first.record.gardenId}.meta.json`]);
369
+ } finally {
370
+ fs.rmSync(dir, { recursive: true, force: true });
371
+ }
372
+ });
373
+
374
+ check("upsertMetaSession: creates the store dir if absent; leaves no .tmp residue", () => {
375
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-store-"));
376
+ const dir = path.join(base, "nested", "meta-sessions"); // does not exist yet
377
+ try {
378
+ const res = upsertMetaSession({ dir, input: claudeInput(), now: T0 });
379
+ assert.ok(fs.existsSync(res.path));
380
+ const residue = fs.readdirSync(dir).filter((f) => f.includes(".tmp-"));
381
+ assert.deepEqual(residue, []); // atomic write cleaned up
382
+ } finally {
383
+ fs.rmSync(base, { recursive: true, force: true });
384
+ }
385
+ });
386
+
387
+ expectThrows("upsertMetaSession: duplicate nativeSessionId in the store throws (fail-fast)", () => {
388
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-store-"));
389
+ try {
390
+ // Two records sharing a nativeSessionId already on disk → ambiguous authority.
391
+ const a = mintMetaRecord(claudeInput({ nativeSessionId: "dup" }), T0);
392
+ const b = mintMetaRecord(claudeInput({ nativeSessionId: "dup" }), T1);
393
+ fs.writeFileSync(path.join(dir, metaRecordFilename(a)), serializeMetaRecord(a));
394
+ fs.writeFileSync(path.join(dir, metaRecordFilename(b)), serializeMetaRecord(b));
395
+ upsertMetaSession({ dir, input: claudeInput({ nativeSessionId: "dup" }), now: T1 });
396
+ } finally {
397
+ fs.rmSync(dir, { recursive: true, force: true });
398
+ }
399
+ });
400
+
401
+ check("defaultMetaSessionsDir: honors ENTWURF_META_SESSIONS_DIR then PI_CODING_AGENT_DIR", () => {
402
+ const saved = { m: process.env.ENTWURF_META_SESSIONS_DIR, a: process.env.PI_CODING_AGENT_DIR };
403
+ try {
404
+ process.env.ENTWURF_META_SESSIONS_DIR = "/explicit/meta";
405
+ assert.equal(defaultMetaSessionsDir(), "/explicit/meta");
406
+ process.env.ENTWURF_META_SESSIONS_DIR = "";
407
+ delete process.env.ENTWURF_META_SESSIONS_DIR;
408
+ process.env.PI_CODING_AGENT_DIR = "/iso/agent";
409
+ assert.equal(defaultMetaSessionsDir(), path.join("/iso/agent", "meta-sessions"));
410
+ } finally {
411
+ if (saved.m === undefined) delete process.env.ENTWURF_META_SESSIONS_DIR;
412
+ else process.env.ENTWURF_META_SESSIONS_DIR = saved.m;
413
+ if (saved.a === undefined) delete process.env.PI_CODING_AGENT_DIR;
414
+ else process.env.PI_CODING_AGENT_DIR = saved.a;
415
+ }
416
+ });
417
+
418
+ // ---------------------------------------------------------------- mailbox delivery (step 6)
419
+ // A self-contained store+mailbox fixture: mint one citizen, return its dirs + garden id.
420
+ function mailboxFixture(): { sessionsDir: string; mailboxDir: string; gardenId: string; cleanup: () => void } {
421
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "meta-mailbox-"));
422
+ const sessionsDir = path.join(root, "meta-sessions");
423
+ const mailboxDir = path.join(root, "meta-mailbox");
424
+ const up = upsertMetaSession({ dir: sessionsDir, input: claudeInput({ nativeSessionId: "mbx-native" }), now: T0 });
425
+ return {
426
+ sessionsDir,
427
+ mailboxDir,
428
+ gardenId: up.record.gardenId,
429
+ cleanup: () => fs.rmSync(root, { recursive: true, force: true }),
430
+ };
431
+ }
432
+
433
+ check("enqueueMetaMessage: writes a .msg, pokes inbox.signal, stamps lastEnqueuedAt", () => {
434
+ const fx = mailboxFixture();
435
+ try {
436
+ const r = enqueueMetaMessage({
437
+ gardenId: fx.gardenId,
438
+ body: "hello",
439
+ sessionsDir: fx.sessionsDir,
440
+ mailboxDir: fx.mailboxDir,
441
+ now: T1,
442
+ });
443
+ assert.ok(fs.existsSync(r.messagePath) && r.messagePath.endsWith(".msg"), "a .msg body is written");
444
+ assert.ok(fs.existsSync(r.signalPath) && r.signalPath.endsWith("inbox.signal"), "inbox.signal is poked");
445
+ const st = readMailboxReceiptState({ gardenId: fx.gardenId, mailboxDir: fx.mailboxDir });
446
+ assert.equal(st.lastEnqueuedAt, T1.toISOString(), "lastEnqueuedAt stamped in mailbox state at enqueue time");
447
+ assert.equal(st.lastReadAt, null, "not read yet");
448
+ } finally {
449
+ fx.cleanup();
450
+ }
451
+ });
452
+
453
+ check("readMetaInbox: drains a fresh .msg, returns the body, stamps lastReadAt (D7 receipt)", () => {
454
+ const fx = mailboxFixture();
455
+ try {
456
+ enqueueMetaMessage({
457
+ gardenId: fx.gardenId,
458
+ body: "drain me",
459
+ sessionsDir: fx.sessionsDir,
460
+ mailboxDir: fx.mailboxDir,
461
+ now: T1,
462
+ });
463
+ const read = readMetaInbox({
464
+ gardenId: fx.gardenId,
465
+ sessionsDir: fx.sessionsDir,
466
+ mailboxDir: fx.mailboxDir,
467
+ now: T1,
468
+ });
469
+ assert.equal(read.messages.length, 1, "one message read");
470
+ assert.equal(read.messages[0]?.body, "drain me", "body intact");
471
+ assert.equal(read.readAt, T1.toISOString(), "readAt returned");
472
+ const st = readMailboxReceiptState({ gardenId: fx.gardenId, mailboxDir: fx.mailboxDir });
473
+ assert.equal(st.lastReadAt, T1.toISOString(), "lastReadAt stamped in mailbox state = the honest read receipt");
474
+ // #5 honesty: lastDeliveredAt is the doorbell's to stamp; readMetaInbox must NOT invent it.
475
+ assert.equal(st.lastDeliveredAt, null, "lastDeliveredAt left null (read does not record a delivery time)");
476
+ } finally {
477
+ fx.cleanup();
478
+ }
479
+ });
480
+
481
+ check("readMetaInbox: drains a doorbell-rung .msg.delivered too", () => {
482
+ const fx = mailboxFixture();
483
+ try {
484
+ const dir = path.join(fx.mailboxDir, fx.gardenId);
485
+ fs.mkdirSync(dir, { recursive: true });
486
+ fs.writeFileSync(path.join(dir, "x.msg.delivered"), "via doorbell");
487
+ const read = readMetaInbox({
488
+ gardenId: fx.gardenId,
489
+ sessionsDir: fx.sessionsDir,
490
+ mailboxDir: fx.mailboxDir,
491
+ now: T1,
492
+ });
493
+ assert.equal(read.messages.length, 1, "the .msg.delivered is read");
494
+ assert.equal(read.messages[0]?.body, "via doorbell");
495
+ } finally {
496
+ fx.cleanup();
497
+ }
498
+ });
499
+
500
+ check("readMetaInbox: ONE drain returns ALL queued bodies in order (level-triggered body state; #34 D8 basis)", () => {
501
+ const fx = mailboxFixture();
502
+ try {
503
+ // Three messages land while the session is idle. The wake SIGNAL
504
+ // (inbox.signal) is edge-triggered and can coalesce N rapid pokes into a
505
+ // single FileChanged. Each BODY is a separate level-state file on disk, so
506
+ // a later wake/read drains the whole directory instead of trusting the number
507
+ // of signal edges. A fully lost doorbell can still leave an idle-forever
508
+ // unread backlog; #34 tracks that heartbeat/re-poke backstop separately.
509
+ // This gate asserts the deterministic robustness basis: once the receiver
510
+ // self-fetches, it gets every queued body, including both fresh .msg and
511
+ // doorbell-rung .msg.delivered files.
512
+ const t1 = new Date("2026-06-05T06:30:00.000Z");
513
+ const t2 = new Date("2026-06-05T06:31:00.000Z");
514
+ const t3 = new Date("2026-06-05T06:32:00.000Z");
515
+ const t4 = new Date("2026-06-05T06:33:00.000Z");
516
+ enqueueMetaMessage({
517
+ gardenId: fx.gardenId,
518
+ body: "first",
519
+ sessionsDir: fx.sessionsDir,
520
+ mailboxDir: fx.mailboxDir,
521
+ now: t1,
522
+ });
523
+ enqueueMetaMessage({
524
+ gardenId: fx.gardenId,
525
+ body: "second",
526
+ sessionsDir: fx.sessionsDir,
527
+ mailboxDir: fx.mailboxDir,
528
+ now: t2,
529
+ });
530
+ // The third one's doorbell DID ring: FileChanged moved .msg -> .msg.delivered.
531
+ const third = enqueueMetaMessage({
532
+ gardenId: fx.gardenId,
533
+ body: "third",
534
+ sessionsDir: fx.sessionsDir,
535
+ mailboxDir: fx.mailboxDir,
536
+ now: t3,
537
+ });
538
+ fs.renameSync(third.messagePath, `${third.messagePath}.delivered`);
539
+
540
+ const read = readMetaInbox({
541
+ gardenId: fx.gardenId,
542
+ sessionsDir: fx.sessionsDir,
543
+ mailboxDir: fx.mailboxDir,
544
+ now: t4,
545
+ });
546
+ assert.equal(read.messages.length, 3, "a single read drains all three queued bodies");
547
+ assert.deepEqual(
548
+ read.messages.map((m) => m.body),
549
+ ["first", "second", "third"],
550
+ "timestamp-distinct bodies drain in deterministic filename order",
551
+ );
552
+ // One batch, one honest receipt — NOT one-per-message.
553
+ const st = readMailboxReceiptState({ gardenId: fx.gardenId, mailboxDir: fx.mailboxDir });
554
+ assert.equal(st.lastReadAt, t4.toISOString(), "a single lastReadAt receipt for the whole batch");
555
+ // Every body archived to .read; nothing unread remains; re-read is empty.
556
+ const dir = path.join(fx.mailboxDir, fx.gardenId);
557
+ const stillUnread = fs.readdirSync(dir).filter((f) => f.endsWith(".msg") || f.endsWith(".msg.delivered"));
558
+ assert.equal(stillUnread.length, 0, "no unread body remains after the batch drain (all archived to .read)");
559
+ assert.equal(
560
+ readMetaInbox({ gardenId: fx.gardenId, sessionsDir: fx.sessionsDir, mailboxDir: fx.mailboxDir, now: t4 }).messages
561
+ .length,
562
+ 0,
563
+ "re-read after the batch drain is empty (idempotent consume)",
564
+ );
565
+ } finally {
566
+ fx.cleanup();
567
+ }
568
+ });
569
+
570
+ check("readMetaInbox: empty inbox returns nothing AND mutates no receipt; re-read after drain is empty", () => {
571
+ const fx = mailboxFixture();
572
+ try {
573
+ // empty read: no message, no receipt
574
+ const empty = readMetaInbox({
575
+ gardenId: fx.gardenId,
576
+ sessionsDir: fx.sessionsDir,
577
+ mailboxDir: fx.mailboxDir,
578
+ now: T1,
579
+ });
580
+ assert.equal(empty.messages.length, 0, "empty inbox: no messages");
581
+ assert.equal(empty.readAt, null, "empty inbox: no receipt stamped");
582
+ assert.equal(
583
+ readMailboxReceiptState({ gardenId: fx.gardenId, mailboxDir: fx.mailboxDir }).lastReadAt,
584
+ null,
585
+ "empty read leaves lastReadAt null (state.json not created)",
586
+ );
587
+ // enqueue + drain, then re-read must be empty (archived to .read, not double-returned)
588
+ enqueueMetaMessage({
589
+ gardenId: fx.gardenId,
590
+ body: "once",
591
+ sessionsDir: fx.sessionsDir,
592
+ mailboxDir: fx.mailboxDir,
593
+ now: T1,
594
+ });
595
+ assert.equal(
596
+ readMetaInbox({ gardenId: fx.gardenId, sessionsDir: fx.sessionsDir, mailboxDir: fx.mailboxDir, now: T1 }).messages
597
+ .length,
598
+ 1,
599
+ "first drain returns it",
600
+ );
601
+ assert.equal(
602
+ readMetaInbox({ gardenId: fx.gardenId, sessionsDir: fx.sessionsDir, mailboxDir: fx.mailboxDir, now: T1 }).messages
603
+ .length,
604
+ 0,
605
+ "re-read is empty",
606
+ );
607
+ } finally {
608
+ fx.cleanup();
609
+ }
610
+ });
611
+
612
+ expectThrows("enqueueMetaMessage: unknown garden id fails loud (not a citizen)", () => {
613
+ const fx = mailboxFixture();
614
+ try {
615
+ enqueueMetaMessage({
616
+ gardenId: "20200101T000000-aaaaaa",
617
+ body: "x",
618
+ sessionsDir: fx.sessionsDir,
619
+ mailboxDir: fx.mailboxDir,
620
+ now: T1,
621
+ });
622
+ } finally {
623
+ fx.cleanup();
624
+ }
625
+ });
626
+
627
+ expectThrows("enqueueMetaMessage: empty body fails loud", () => {
628
+ const fx = mailboxFixture();
629
+ try {
630
+ enqueueMetaMessage({
631
+ gardenId: fx.gardenId,
632
+ body: "",
633
+ sessionsDir: fx.sessionsDir,
634
+ mailboxDir: fx.mailboxDir,
635
+ now: T1,
636
+ });
637
+ } finally {
638
+ fx.cleanup();
639
+ }
640
+ });
641
+
642
+ expectThrows("readMetaRecordV1ByGardenId: body/filename gardenId drift is corruption (body is SSOT)", () => {
643
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "meta-drift-"));
644
+ try {
645
+ const sessionsDir = path.join(root, "meta-sessions");
646
+ fs.mkdirSync(sessionsDir, { recursive: true });
647
+ // A record whose BODY gardenId differs from the <id>.meta.json filename it lives in.
648
+ const rec = mintMetaRecord(claudeInput({ nativeSessionId: "drift" }), T0);
649
+ const wrongName = "20200101T000000-bbbbbb.meta.json"; // filename id != rec.gardenId
650
+ fs.writeFileSync(path.join(sessionsDir, wrongName), serializeMetaRecord(rec));
651
+ readMetaRecordV1ByGardenId("20200101T000000-bbbbbb", sessionsDir);
652
+ } finally {
653
+ fs.rmSync(root, { recursive: true, force: true });
654
+ }
655
+ });
656
+
657
+ check("defaultMetaMailboxDir: sibling of meta-sessions under the pi agent dir", () => {
658
+ const saved = { m: process.env.ENTWURF_META_MAILBOX_DIR, a: process.env.PI_CODING_AGENT_DIR };
659
+ try {
660
+ process.env.ENTWURF_META_MAILBOX_DIR = "/explicit/mbx";
661
+ assert.equal(defaultMetaMailboxDir(), "/explicit/mbx");
662
+ delete process.env.ENTWURF_META_MAILBOX_DIR;
663
+ process.env.PI_CODING_AGENT_DIR = "/iso/agent";
664
+ assert.equal(defaultMetaMailboxDir(), path.join("/iso/agent", "meta-mailbox"));
665
+ } finally {
666
+ if (saved.m === undefined) delete process.env.ENTWURF_META_MAILBOX_DIR;
667
+ else process.env.ENTWURF_META_MAILBOX_DIR = saved.m;
668
+ if (saved.a === undefined) delete process.env.PI_CODING_AGENT_DIR;
669
+ else process.env.PI_CODING_AGENT_DIR = saved.a;
670
+ }
671
+ });
672
+
673
+ process.stdout.write(`[check-meta-session] ${assertions} assertions ok\n`);