@junghanacs/entwurf 0.13.0 → 0.14.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 (193) hide show
  1. package/AGENTS.md +52 -17
  2. package/BASELINE.md +60 -206
  3. package/CHANGELOG.md +64 -0
  4. package/CONTRIBUTING.md +14 -10
  5. package/DELIVERY.md +118 -261
  6. package/README.md +91 -431
  7. package/VERIFY.md +74 -95
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +236 -0
  12. package/docs/external-mcp-host.md +131 -0
  13. package/docs/fresh-cut-policy.md +99 -0
  14. package/docs/setup-clean-host.md +124 -328
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  38. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  39. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  40. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  41. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  42. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  43. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  44. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  45. package/mcp/entwurf-bridge/src/index.ts +176 -67
  46. package/mcp/entwurf-bridge/start.sh +2 -2
  47. package/mcp/entwurf-bridge/test.sh +23 -9
  48. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  49. package/package.json +25 -11
  50. package/pi-extensions/entwurf-control.ts +220 -71
  51. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  52. package/pi-extensions/lib/acp/augment.ts +42 -3
  53. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  54. package/pi-extensions/lib/acp/backend.ts +366 -38
  55. package/pi-extensions/lib/acp/config.ts +1 -1
  56. package/pi-extensions/lib/acp/engraving.ts +43 -1
  57. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  58. package/pi-extensions/lib/acp/models.ts +4 -4
  59. package/pi-extensions/lib/acp/overlay.ts +11 -9
  60. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  61. package/pi-extensions/lib/entwurf-core.ts +15 -15
  62. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  63. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  64. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  65. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  66. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  67. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  68. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  69. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  70. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  71. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  72. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  73. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  74. package/pi-extensions/lib/meta-session.ts +93 -5
  75. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  76. package/pi-extensions/lib/mux-launch.ts +267 -0
  77. package/pi-extensions/lib/mux-placement.ts +387 -0
  78. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  79. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  80. package/pi-extensions/lib/session-id.js +8 -5
  81. package/pi-extensions/lib/socket-discovery.ts +3 -3
  82. package/prompts/engraving.md +13 -5
  83. package/run.sh +688 -282
  84. package/scripts/agy-bridge-config.py +5 -1
  85. package/scripts/check-acp-backend-preflight.ts +1 -1
  86. package/scripts/check-acp-carrier-augment.ts +246 -10
  87. package/scripts/check-acp-cortex.ts +5 -5
  88. package/scripts/check-acp-overlay.ts +13 -3
  89. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  90. package/scripts/check-acp-stop-reason.ts +342 -0
  91. package/scripts/check-acp-stream-hooks.ts +504 -0
  92. package/scripts/check-elapsed.sh +25 -0
  93. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  94. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  95. package/scripts/check-entwurf-control-rpc.ts +4 -3
  96. package/scripts/check-entwurf-resume-args.ts +72 -70
  97. package/scripts/check-entwurf-session-identity.ts +14 -10
  98. package/scripts/check-entwurf-v2-contract.ts +34 -59
  99. package/scripts/check-entwurf-v2-decider.ts +17 -177
  100. package/scripts/check-entwurf-v2-lock.ts +5 -2
  101. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  102. package/scripts/check-entwurf-v2-production.ts +2 -91
  103. package/scripts/check-entwurf-v2-release.ts +10 -105
  104. package/scripts/check-entwurf-v2-runner.ts +4 -85
  105. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  106. package/scripts/check-entwurf-v2-send.ts +0 -28
  107. package/scripts/check-entwurf-v2-surface.ts +198 -128
  108. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  109. package/scripts/check-fresh-cut-gate.sh +1 -1
  110. package/scripts/check-gate-qualification.ts +101 -7
  111. package/scripts/check-install-container.sh +10 -2
  112. package/scripts/check-install-surface.ts +1 -1
  113. package/scripts/check-keyset-overlap.py +1 -1
  114. package/scripts/check-meta-facts.ts +249 -0
  115. package/scripts/check-meta-identity-consumers.ts +1 -1
  116. package/scripts/check-meta-session.ts +169 -0
  117. package/scripts/check-mux-launch-tmux.ts +316 -0
  118. package/scripts/check-mux-launch.ts +288 -0
  119. package/scripts/check-mux-launcher-fence.ts +264 -0
  120. package/scripts/check-mux-parent-artifact.ts +195 -0
  121. package/scripts/check-mux-placement-tmux.ts +322 -0
  122. package/scripts/check-mux-placement.ts +323 -0
  123. package/scripts/check-mux-resume-call.ts +282 -0
  124. package/scripts/check-probe-cli-shim.ts +25 -22
  125. package/scripts/check-probe-ordering.ts +121 -81
  126. package/scripts/check-release-gate-outcomes.ts +452 -0
  127. package/scripts/check-resume-launch-identity.ts +244 -0
  128. package/scripts/check-socket-discovery.ts +1 -1
  129. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  130. package/scripts/inventory-verification-surface.ts +349 -0
  131. package/scripts/lib/claude-launcher-fence.ts +322 -0
  132. package/scripts/lib/live-skip.ts +33 -0
  133. package/scripts/lib/mutation-qualify.ts +109 -3
  134. package/scripts/lib/probe-acp-turn.ts +33 -12
  135. package/scripts/lib/step-outcome.sh +88 -0
  136. package/scripts/meta-bridge-doctor.sh +6 -8
  137. package/scripts/meta-facts.ts +60 -0
  138. package/scripts/mutants/acp-augment.json +106 -0
  139. package/scripts/mutants/acp-cortex.json +2 -2
  140. package/scripts/mutants/acp-overlay.json +17 -0
  141. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  142. package/scripts/mutants/acp-stop-reason.json +80 -0
  143. package/scripts/mutants/acp-stream-hooks.json +158 -0
  144. package/scripts/mutants/bridge-boot-resume.json +45 -0
  145. package/scripts/mutants/meta-facts.json +50 -0
  146. package/scripts/mutants/meta-identity.json +36 -0
  147. package/scripts/mutants/mux-boundary.json +196 -0
  148. package/scripts/mutants/mux-fresh-call.json +185 -0
  149. package/scripts/mutants/mux-launcher-fence.json +123 -0
  150. package/scripts/mutants/mux-parent-artifact.json +39 -0
  151. package/scripts/mutants/mux-resume-call.json +148 -0
  152. package/scripts/mutants/probe-ordering.json +6 -1021
  153. package/scripts/mutants/release-gate.json +140 -0
  154. package/scripts/mutants/resume-args.json +76 -0
  155. package/scripts/mutants/resume-launch-identity.json +96 -0
  156. package/scripts/mutants/v2-surface.json +74 -23
  157. package/scripts/mutants/v2-visible-resume.json +215 -0
  158. package/scripts/new-session-id.ts +9 -4
  159. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  160. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  161. package/scripts/smoke-acp-cortex-live.ts +8 -14
  162. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  163. package/scripts/smoke-acp-mcp-live.ts +2 -2
  164. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  165. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  166. package/scripts/smoke-acp-overlay-live.ts +2 -2
  167. package/scripts/smoke-acp-provider-live.ts +2 -2
  168. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  169. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  170. package/scripts/smoke-acp-skill-live.ts +2 -2
  171. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  172. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  173. package/scripts/smoke-agy-native-push-live.ts +10 -20
  174. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  175. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  176. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
  177. package/scripts/smoke-meta-honesty.sh +1 -1
  178. package/scripts/smoke-meta-install-state.sh +4 -0
  179. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  180. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  181. package/scripts/smoke-pi-attach.ts +1 -1
  182. package/scripts/smoke-user-scope-citizen.sh +1 -1
  183. package/scripts/tsconfig.json +1 -0
  184. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  185. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  186. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  187. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  188. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  189. package/scripts/check-acp-sdk-surface.ts +0 -236
  190. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  191. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  192. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  193. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
@@ -0,0 +1,249 @@
1
+ /**
2
+ * check-meta-facts — deterministic gate for the #65 owner-normalized store
3
+ * projection (`entwurf meta-facts` / scripts/meta-facts.ts).
4
+ *
5
+ * Drives the REAL CLI as a subprocess against sandboxed fixture stores (never the
6
+ * live store; every path is an explicit mkdtemp dir). What it proves:
7
+ *
8
+ * - the join is the FULL verbatim strict 9-field v3 record — gardenId,
9
+ * nativeSessionId AND transcriptPath cross together, sorted by gardenId;
10
+ * - drift / symlink / invalid-UTF-8 entries become in-band defects with exit 0,
11
+ * and a symlinked record's target bytes are never followed into the join;
12
+ * - the projection carries the owner's listing semantics, not a copy: a
13
+ * schema-invalid rival is dropped by parse BEFORE the uniqueness pass and can
14
+ * never quarantine a healthy citizen; a true duplicate pair loses BOTH sides;
15
+ * - output is byte-deterministic across runs;
16
+ * - a missing store is a readable EMPTY store (exit 0), an unreadable store is
17
+ * exit 3 with NO JSON (never mistakable for empty), bad argv is exit 2;
18
+ * - the surface is reachable: run.sh dispatches it and the bridge build emits
19
+ * its compiled twin (the installed-host fence).
20
+ *
21
+ * The gate spawns the CLI rather than importing its pieces because the exit code
22
+ * and stdout bytes ARE the contract a consumer holds. Every [QK:MF-*] token
23
+ * appears exactly once, on the assertion that fails for that claim.
24
+ */
25
+
26
+ import assert from "node:assert/strict";
27
+ import { spawnSync } from "node:child_process";
28
+ import fs from "node:fs";
29
+ import os from "node:os";
30
+ import path from "node:path";
31
+ import {
32
+ type MetaCitizenBackend,
33
+ type MetaIdentity,
34
+ serializeMetaIdentity,
35
+ } from "../pi-extensions/lib/meta-session.ts";
36
+
37
+ let passed = 0;
38
+ function ok(label: string, cond: boolean): void {
39
+ assert.ok(cond, label);
40
+ console.log(` ok ${label}`);
41
+ passed++;
42
+ }
43
+
44
+ const GID_A = "20260611T111111-aaaaaa";
45
+ const GID_B = "20260611T222222-bbbbbb";
46
+ const GID_C = "20260611T333333-cccccc";
47
+
48
+ function record(gardenId: string, over: Partial<MetaIdentity> = {}): string {
49
+ const backend: MetaCitizenBackend = "pi";
50
+ return serializeMetaIdentity({
51
+ schemaVersion: 3,
52
+ gardenId,
53
+ backend,
54
+ nativeSessionId: `n-${gardenId}`,
55
+ cwd: "/x",
56
+ model: "provider/model",
57
+ transcriptPath: `/t/${gardenId}.jsonl`,
58
+ createdAt: "2026-06-11T00:00:00.000Z",
59
+ recordUpdatedAt: "2026-06-11T00:00:00.000Z",
60
+ ...over,
61
+ });
62
+ }
63
+
64
+ function store(files: Record<string, string | Buffer>): string {
65
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
66
+ for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), body);
67
+ return dir;
68
+ }
69
+
70
+ interface CliRun {
71
+ status: number | null;
72
+ stdout: string;
73
+ stderr: string;
74
+ }
75
+
76
+ function cli(args: string[], env: Record<string, string> = {}): CliRun {
77
+ const res = spawnSync(process.execPath, ["--experimental-strip-types", "scripts/meta-facts.ts", ...args], {
78
+ encoding: "utf8",
79
+ env: { ...process.env, ...env },
80
+ });
81
+ return { status: res.status, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
82
+ }
83
+
84
+ interface Projection {
85
+ schemaVersion: number;
86
+ storeDir: string;
87
+ citizens: MetaIdentity[];
88
+ defects: { filename: string; message: string }[];
89
+ }
90
+
91
+ function parseProjection(run: CliRun, label: string): Projection {
92
+ assert.equal(run.status, 0, `${label}: exit 0 (stderr: ${run.stderr})`);
93
+ return JSON.parse(run.stdout) as Projection;
94
+ }
95
+
96
+ // ── full verbatim join, sorted, deterministic ───────────────────────────────
97
+ {
98
+ const dir = store({
99
+ [`${GID_B}.meta.json`]: record(GID_B),
100
+ [`${GID_A}.meta.json`]: record(GID_A, { model: null, transcriptPath: null }),
101
+ });
102
+ const run = cli([dir]);
103
+ const facts = parseProjection(run, "valid store");
104
+ ok(
105
+ "projection declares schemaVersion 1 and the resolved storeDir",
106
+ facts.schemaVersion === 1 && facts.storeDir === path.resolve(dir),
107
+ );
108
+ assert.deepEqual(
109
+ facts.citizens,
110
+ [
111
+ {
112
+ schemaVersion: 3,
113
+ gardenId: GID_A,
114
+ backend: "pi",
115
+ nativeSessionId: `n-${GID_A}`,
116
+ cwd: "/x",
117
+ model: null,
118
+ transcriptPath: null,
119
+ createdAt: "2026-06-11T00:00:00.000Z",
120
+ recordUpdatedAt: "2026-06-11T00:00:00.000Z",
121
+ },
122
+ {
123
+ schemaVersion: 3,
124
+ gardenId: GID_B,
125
+ backend: "pi",
126
+ nativeSessionId: `n-${GID_B}`,
127
+ cwd: "/x",
128
+ model: "provider/model",
129
+ transcriptPath: `/t/${GID_B}.jsonl`,
130
+ createdAt: "2026-06-11T00:00:00.000Z",
131
+ recordUpdatedAt: "2026-06-11T00:00:00.000Z",
132
+ },
133
+ ],
134
+ "[QK:MF-FULL-V3-JOIN] citizens are the full verbatim 9-field v3 records, sorted by gardenId",
135
+ );
136
+ console.log(" ok citizens are the full verbatim 9-field v3 records, sorted by gardenId");
137
+ passed++;
138
+ ok("valid store has no defects", facts.defects.length === 0);
139
+ const again = cli([dir]);
140
+ ok("two runs over one store are byte-identical", again.status === 0 && again.stdout === run.stdout);
141
+ // default-dir resolution rides the same env override every runtime surface uses
142
+ const viaEnv = cli([], { ENTWURF_META_SESSIONS_DIR: dir });
143
+ ok(
144
+ "no argv resolves the store through ENTWURF_META_SESSIONS_DIR",
145
+ viaEnv.status === 0 && viaEnv.stdout === run.stdout,
146
+ );
147
+ }
148
+
149
+ // ── drift / symlink / invalid UTF-8 are in-band defects, exit 0 ─────────────
150
+ {
151
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-outside-"));
152
+ fs.writeFileSync(path.join(outside, "foreign.meta.json"), record(GID_C));
153
+ const dir = store({
154
+ [`${GID_A}.meta.json`]: record(GID_A),
155
+ [`${GID_B}.meta.json`]: record(GID_C), // body gid ≠ filename → drift
156
+ "20260611T444444-dddddd.meta.json": Buffer.from([0xff, 0xfe, 0x00, 0x7b]), // invalid UTF-8
157
+ });
158
+ fs.symlinkSync(path.join(outside, "foreign.meta.json"), path.join(dir, "20260611T555555-eeeeee.meta.json"));
159
+ const facts = parseProjection(cli([dir]), "defective store");
160
+ ok(
161
+ "[QK:MF-DEFECTS-IN-BAND] drift + symlink + invalid UTF-8 are in-band defects and the store still answers exit 0",
162
+ facts.defects.length === 3 && facts.citizens.length === 1 && facts.citizens[0]?.gardenId === GID_A,
163
+ );
164
+ ok(
165
+ "a symlinked record's target bytes are never followed into the join",
166
+ !facts.citizens.some((c) => c.gardenId === GID_C) &&
167
+ facts.defects.some((d) => d.message.includes("not a regular file")),
168
+ );
169
+ ok(
170
+ "drift and invalid-UTF-8 defects carry their own causes",
171
+ facts.defects.some((d) => d.message.includes("drift")) &&
172
+ facts.defects.some((d) => d.message.includes("not valid JSON")),
173
+ );
174
+ }
175
+
176
+ // ── parse-before-uniqueness: a schema-invalid rival never quarantines ───────
177
+ {
178
+ const rival = record(GID_B, { nativeSessionId: `n-${GID_A}` }).replace('"schemaVersion": 3', '"schemaVersion": 2');
179
+ const dir = store({
180
+ [`${GID_A}.meta.json`]: record(GID_A),
181
+ [`${GID_B}.meta.json`]: rival,
182
+ });
183
+ const facts = parseProjection(cli([dir]), "invalid rival");
184
+ ok(
185
+ "a schema-invalid rival is a defect and the healthy citizen stays listed",
186
+ facts.citizens.length === 1 &&
187
+ facts.citizens[0]?.gardenId === GID_A &&
188
+ facts.defects.length === 1 &&
189
+ facts.defects[0]?.filename === `${GID_B}.meta.json`,
190
+ );
191
+ }
192
+
193
+ // ── no-winner duplicates: both rivals leave the join ────────────────────────
194
+ {
195
+ const dir = store({
196
+ [`${GID_A}.meta.json`]: record(GID_A, { nativeSessionId: "n-shared" }),
197
+ [`${GID_B}.meta.json`]: record(GID_B, { nativeSessionId: "n-shared" }),
198
+ [`${GID_C}.meta.json`]: record(GID_C),
199
+ });
200
+ const facts = parseProjection(cli([dir]), "duplicate pair");
201
+ ok(
202
+ "a duplicate nativeSessionId pair loses BOTH sides; the unrelated citizen keeps listing",
203
+ facts.citizens.length === 1 && facts.citizens[0]?.gardenId === GID_C && facts.defects.length === 2,
204
+ );
205
+ }
206
+
207
+ // ── missing store = readable EMPTY store ────────────────────────────────────
208
+ {
209
+ const gone = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-")), "never-created");
210
+ const facts = parseProjection(cli([gone]), "missing store");
211
+ ok(
212
+ "a store that does not exist is a readable empty store (ENOENT only)",
213
+ facts.citizens.length === 0 && facts.defects.length === 0,
214
+ );
215
+ }
216
+
217
+ // ── unreadable store = exit 3, NO JSON ──────────────────────────────────────
218
+ {
219
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-meta-facts-"));
220
+ const file = path.join(base, "not-a-dir");
221
+ fs.writeFileSync(file, "plain file");
222
+ const run = cli([path.join(file, "store")]); // ENOTDIR: the path runs THROUGH a regular file
223
+ ok(
224
+ "[QK:MF-EXIT-UNREADABLE] an unreadable store is exit 3 with no JSON — never mistakable for an empty one",
225
+ run.status === 3 && run.stdout === "" && run.stderr.includes("FAIL"),
226
+ );
227
+ }
228
+
229
+ // ── usage ───────────────────────────────────────────────────────────────────
230
+ {
231
+ ok("[QK:MF-USAGE] two positional args are a usage error (exit 2)", cli(["/a", "/b"]).status === 2);
232
+ ok("a dash argv is a usage error, not a store directory", cli(["--help"]).status === 2);
233
+ }
234
+
235
+ // ── reachability: dispatch + compiled twin ──────────────────────────────────
236
+ {
237
+ const runSh = fs.readFileSync("run.sh", "utf8");
238
+ ok(
239
+ "run.sh dispatches the meta-facts operator verb through run_ts",
240
+ /^ {2}meta-facts\)$/m.test(runSh) && runSh.includes("run_ts scripts/meta-facts.ts"),
241
+ );
242
+ const build = fs.readFileSync("mcp/entwurf-bridge/tsconfig.build.json", "utf8");
243
+ ok(
244
+ "the bridge build emits the compiled twin an installed host needs",
245
+ build.includes('"../../scripts/meta-facts.ts"'),
246
+ );
247
+ }
248
+
249
+ console.log(`check-meta-facts: ${passed} checks passed`);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * check-meta-identity-consumers — deterministic gate for the V3-only identity
3
3
  * consumer seam. A real temp dir; no backend, no network, no hook. Safe in the
4
- * `pnpm check` static floor.
4
+ * `pnpm run check:full` static floor.
5
5
  *
6
6
  * Proves the consumers every live path stands on (MCP sender-marker check, prune,
7
7
  * store-doctor, the identity writers):
@@ -14,6 +14,7 @@
14
14
 
15
15
  import assert from "node:assert/strict";
16
16
  import fs from "node:fs";
17
+ import { syncBuiltinESMExports } from "node:module";
17
18
  import os from "node:os";
18
19
  import path from "node:path";
19
20
  import {
@@ -27,6 +28,7 @@ import {
27
28
  metaRecordFilename,
28
29
  mintMetaIdentity,
29
30
  parseMetaIdentity,
31
+ publishExclusiveIdentity,
30
32
  readMailboxReceiptState,
31
33
  readMetaInbox,
32
34
  serializeMetaIdentity,
@@ -166,6 +168,173 @@ check("upsertMetaSession: creates the store dir if absent; leaves no .tmp residu
166
168
  }
167
169
  });
168
170
 
171
+ // ---------------------------------------------------------------- exclusive CREATE publish (#66 fail-closed)
172
+ check("publishExclusiveIdentity: absent final publishes, round-trips, no tmp residue", () => {
173
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
174
+ try {
175
+ const record = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-fresh" }), T0);
176
+ const file = path.join(dir, metaRecordFilename(record));
177
+ publishExclusiveIdentity(file, record);
178
+ assert.deepEqual(parseMetaIdentity(fs.readFileSync(file, "utf8")), record, "published bytes round-trip");
179
+ assert.deepEqual(
180
+ fs.readdirSync(dir).filter((f) => f.includes(".tmp-")),
181
+ [],
182
+ "normal exclusive publish leaves no tmp residue",
183
+ );
184
+ } finally {
185
+ fs.rmSync(dir, { recursive: true, force: true });
186
+ }
187
+ });
188
+
189
+ check("publishExclusiveIdentity: pre-planted final REFUSES, victim byte-identical, tmp gone", () => {
190
+ // Deterministic collision fixture: the minted suffix is random, so the public
191
+ // upsert seam cannot be forced onto an occupied final path without a
192
+ // retry/sample trick — the primitive is the production subject here, and the
193
+ // wiring claim below pins upsert's create branch to it.
194
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
195
+ try {
196
+ const victim = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-victim" }), T0);
197
+ const rival = {
198
+ ...mintMetaIdentity(claudeInput({ nativeSessionId: "excl-rival" }), T1),
199
+ gardenId: victim.gardenId,
200
+ };
201
+ const file = path.join(dir, metaRecordFilename(victim));
202
+ fs.writeFileSync(file, serializeMetaIdentity(victim));
203
+ const victimBytes = fs.readFileSync(file);
204
+ assert.throws(
205
+ () => publishExclusiveIdentity(file, rival),
206
+ (err: unknown) => {
207
+ assert.ok(err instanceof MetaRecordError, "collision throws MetaRecordError");
208
+ assert.match(err.message, /CREATE collision/, "named collision cause");
209
+ assert.ok(err.message.includes(victim.gardenId), "error names the colliding garden id");
210
+ assert.ok(err.message.includes(file), "error names the occupied record path");
211
+ return true;
212
+ },
213
+ "occupied final path must refuse loud, never replace [QK:META-CREATE-EXCLUSIVE]",
214
+ );
215
+ assert.ok(fs.readFileSync(file).equals(victimBytes), "victim bytes byte-identical");
216
+ assert.deepEqual(
217
+ fs.readdirSync(dir).filter((f) => f.includes(".tmp-")),
218
+ [],
219
+ "collision refusal leaves no tmp residue",
220
+ );
221
+ } finally {
222
+ fs.rmSync(dir, { recursive: true, force: true });
223
+ }
224
+ });
225
+
226
+ /**
227
+ * Measured official patch seam for the two error-contract cells below: patch the
228
+ * default CJS fs object, then `syncBuiltinESMExports()` so the production
229
+ * `import * as fs` namespace binding sees it (measured on this host: the
230
+ * namespace does NOT see the patch before sync, does after, and restores after
231
+ * the finally's sync). A non-EEXIST link errno and a post-publish cleanup
232
+ * failure cannot be forced on the real filesystem synchronously.
233
+ */
234
+ function withPatchedFs(method: "linkSync" | "unlinkSync", impl: () => never, fn: () => void): void {
235
+ const real = fs[method];
236
+ (fs as unknown as Record<string, unknown>)[method] = impl;
237
+ syncBuiltinESMExports();
238
+ try {
239
+ fn();
240
+ } finally {
241
+ (fs as unknown as Record<string, unknown>)[method] = real;
242
+ syncBuiltinESMExports();
243
+ }
244
+ }
245
+
246
+ check("publishExclusiveIdentity: non-EEXIST link failure rethrows the ORIGINAL error, tmp cleaned", () => {
247
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
248
+ try {
249
+ const record = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-eacces" }), T0);
250
+ const file = path.join(dir, metaRecordFilename(record));
251
+ const primary = Object.assign(new Error("EACCES: permission denied, link"), { code: "EACCES" });
252
+ withPatchedFs(
253
+ "linkSync",
254
+ () => {
255
+ throw primary;
256
+ },
257
+ () => {
258
+ assert.throws(
259
+ () => publishExclusiveIdentity(file, record),
260
+ (err: unknown) => {
261
+ assert.equal(err, primary, "the identical original error object is rethrown (errno/cause intact)");
262
+ return true;
263
+ },
264
+ "non-EEXIST link failure must surface the original error, not a translation",
265
+ );
266
+ },
267
+ );
268
+ assert.ok(!fs.existsSync(file), "no final record was published");
269
+ assert.deepEqual(
270
+ fs.readdirSync(dir).filter((f) => f.includes(".tmp-")),
271
+ [],
272
+ "tmp cleaned even when the link failed for a non-EEXIST reason",
273
+ );
274
+ } finally {
275
+ fs.rmSync(dir, { recursive: true, force: true });
276
+ }
277
+ });
278
+
279
+ check("publishExclusiveIdentity: post-link cleanup failure is LOUD, final stays published, no rollback", () => {
280
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-excl-"));
281
+ try {
282
+ const record = mintMetaIdentity(claudeInput({ nativeSessionId: "excl-cleanup" }), T0);
283
+ const file = path.join(dir, metaRecordFilename(record));
284
+ withPatchedFs(
285
+ "unlinkSync",
286
+ () => {
287
+ throw Object.assign(new Error("EPERM: operation not permitted, unlink"), { code: "EPERM" });
288
+ },
289
+ () => {
290
+ assert.throws(
291
+ () => publishExclusiveIdentity(file, record),
292
+ (err: unknown) => {
293
+ assert.ok(err instanceof MetaRecordError, "cleanup failure throws MetaRecordError");
294
+ assert.match(err.message, /already published/, "states the record IS published");
295
+ assert.match(err.message, /temp cleanup failed/, "names the cleanup failure");
296
+ assert.ok(err.message.includes(".tmp-"), "names the leftover tmp path");
297
+ assert.match(
298
+ err.message,
299
+ /no successful birth\/registration receipt/,
300
+ "states the transport-neutral abort consequence",
301
+ );
302
+ assert.match(err.message, /will ATTACH/, "states the retry semantics");
303
+ return true;
304
+ },
305
+ "a real cleanup failure after publish must be loud, never a false success [QK:META-CREATE-PUBLISHED-CLEANUP]",
306
+ );
307
+ },
308
+ );
309
+ assert.deepEqual(
310
+ parseMetaIdentity(fs.readFileSync(file, "utf8")),
311
+ record,
312
+ "the published final record remains and round-trips (no rollback)",
313
+ );
314
+ assert.equal(
315
+ fs.readdirSync(dir).filter((f) => f.includes(".tmp-")).length,
316
+ 1,
317
+ "the tmp the primitive could not clean is still there (honest state)",
318
+ );
319
+ } finally {
320
+ fs.rmSync(dir, { recursive: true, force: true });
321
+ }
322
+ });
323
+
324
+ check("upsertMetaSession: CREATE dispatches to the exclusive publish, ATTACH to the replace write", () => {
325
+ // Structural action-dispatch claim, deliberately minimal: runtime forcing of a
326
+ // create collision through upsertMetaSession is genuinely unavailable without
327
+ // a seam (the gid suffix is drawn from randomBytes inside mint), so the
328
+ // runtime oracles above prove the primitive and this claim proves the wiring.
329
+ const source = fs.readFileSync(new URL("../pi-extensions/lib/meta-session.ts", import.meta.url), "utf8");
330
+ assert.ok(
331
+ source.includes(
332
+ 'if (decision.action === "create") publishExclusiveIdentity(file, decision.record);\n\telse atomicWriteIdentity(file, decision.record);',
333
+ ),
334
+ "upsertMetaSession must split the write by action: create→exclusive, attach→replace [QK:META-CREATE-WIRING]",
335
+ );
336
+ });
337
+
169
338
  expectThrows("upsertMetaSession: duplicate nativeSessionId in the store throws (fail-fast)", () => {
170
339
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meta-session-store-"));
171
340
  try {