@junghanacs/entwurf 0.22.0 → 0.23.1

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 (135) hide show
  1. package/AGENTS.md +4 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +514 -0
  4. package/DELIVERY.md +10 -2
  5. package/README.md +43 -4
  6. package/VERIFY.md +47 -0
  7. package/docs/acp-backend-rail.md +96 -17
  8. package/docs/setup-clean-host.md +11 -1
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +41 -41
  29. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  30. package/package.json +9 -8
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  33. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  34. package/pi-extensions/lib/acp/backend.ts +4 -4
  35. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  36. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  37. package/pi-extensions/lib/codex-declaration.js +612 -0
  38. package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
  39. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  40. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  41. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  42. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  43. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  44. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  45. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  46. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  47. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  48. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  49. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  50. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  51. package/pi-extensions/lib/herdr-placement.ts +276 -0
  52. package/pi-extensions/lib/mux-fresh-call.ts +95 -301
  53. package/run.sh +588 -204
  54. package/scripts/check-acp-usage-accounting.ts +9 -9
  55. package/scripts/check-codex-birth-hook.ts +187 -0
  56. package/scripts/check-entwurf-control-rpc.ts +138 -0
  57. package/scripts/check-entwurf-fact-provider.ts +4 -2
  58. package/scripts/check-entwurf-peers-surface.ts +3 -1
  59. package/scripts/check-entwurf-self-address.ts +68 -0
  60. package/scripts/check-fresh-call-dispatch.ts +301 -0
  61. package/scripts/check-gate-qualification.ts +17 -4
  62. package/scripts/check-herdr-activation.ts +1293 -0
  63. package/scripts/check-herdr-fresh-call.ts +1250 -0
  64. package/scripts/check-herdr-placement.ts +383 -0
  65. package/scripts/check-herdr-plugin-build.ts +842 -0
  66. package/scripts/check-herdr-plugin-profile.ts +371 -0
  67. package/scripts/check-herdr-plugin.ts +523 -0
  68. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  69. package/scripts/check-herdr-sandbox.ts +456 -0
  70. package/scripts/check-herdr-supply.ts +184 -0
  71. package/scripts/check-mux-launcher-fence.ts +138 -0
  72. package/scripts/check-peer-facts.ts +331 -0
  73. package/scripts/check-pi-launch.ts +358 -0
  74. package/scripts/check-release-gate-outcomes.ts +19 -0
  75. package/scripts/check-typing-call-fence.ts +296 -0
  76. package/scripts/codex-birth-doctor.sh +159 -16
  77. package/scripts/codex-birth-install.sh +178 -72
  78. package/scripts/codex-birth-uninstall.sh +162 -7
  79. package/scripts/fixtures/herdr-supply.json +29 -0
  80. package/scripts/herdr-activation.mjs +536 -0
  81. package/scripts/herdr-plugin-activate.mjs +270 -0
  82. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  83. package/scripts/herdr-runtime.mjs +1203 -0
  84. package/scripts/install-herdr-ci.sh +96 -0
  85. package/scripts/inventory-verification-surface.ts +86 -10
  86. package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
  87. package/scripts/lib/tree-digest.ts +97 -0
  88. package/scripts/meta-bridge-install.sh +19 -2
  89. package/scripts/meta-bridge-state.py +167 -11
  90. package/scripts/mutants/codex-caller-seat.json +6 -8
  91. package/scripts/mutants/codex-native.json +195 -29
  92. package/scripts/mutants/control-socket-disconnect.json +57 -0
  93. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  94. package/scripts/mutants/herdr-activation.json +305 -0
  95. package/scripts/mutants/herdr-fresh-call.json +448 -0
  96. package/scripts/mutants/herdr-placement.json +161 -0
  97. package/scripts/mutants/herdr-plugin-build.json +160 -0
  98. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  99. package/scripts/mutants/herdr-plugin.json +133 -0
  100. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  101. package/scripts/mutants/herdr-supply.json +133 -0
  102. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  103. package/scripts/mutants/mux-fresh-call.json +104 -17
  104. package/scripts/mutants/mux-launcher-fence.json +13 -0
  105. package/scripts/mutants/omp-fresh.json +4 -4
  106. package/scripts/mutants/peer-facts.json +98 -0
  107. package/scripts/mutants/pi-launch.json +113 -0
  108. package/scripts/mutants/resume-args.json +6 -6
  109. package/scripts/mutants/self-address.json +33 -0
  110. package/scripts/mutants/typing-call-fence.json +17 -0
  111. package/scripts/mutants/v2-surface.json +2 -2
  112. package/scripts/mutants/v2-visible-resume.json +17 -17
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  117. package/scripts/smoke-codex-birth.sh +281 -12
  118. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  119. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  120. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  121. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  122. package/scripts/tsconfig.json +5 -0
  123. package/scripts/check-entwurf-facts.ts +0 -385
  124. package/scripts/check-entwurf-resume-args.ts +0 -122
  125. package/scripts/check-entwurf-v2-decider.ts +0 -657
  126. package/scripts/check-entwurf-v2-lock.ts +0 -371
  127. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  128. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  129. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  130. package/scripts/check-entwurf-v2-release.ts +0 -151
  131. package/scripts/check-entwurf-v2-runner.ts +0 -448
  132. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  133. package/scripts/check-entwurf-v2-send.ts +0 -458
  134. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  135. package/scripts/check-socket-discovery.ts +0 -401
@@ -28,6 +28,11 @@
28
28
  "./**/*.ts",
29
29
  "../test/**/*.ts",
30
30
  "../pi-extensions/lib/**/*.ts",
31
+ // The behavior-adjacent tests the root config just excluded (#119 V2). Listed at
32
+ // pi-extensions/** rather than pi-extensions/lib/** so a test beside an extension
33
+ // OUTSIDE lib/ lands in a fence too — the root exclude is glob-wide, and a file in
34
+ // no fence is worse than a file in two.
35
+ "../pi-extensions/**/*.test.ts",
31
36
  "../pi-extensions/model-lock.ts",
32
37
  "../pi-extensions/meta-bridge-hook.ts",
33
38
  "../pi-extensions/meta-bridge-hook-copilot.ts",
@@ -1,385 +0,0 @@
1
- /**
2
- * check-entwurf-facts — deterministic gate for the fact-provider's pure core
3
- * (0.11 Stage 0 step 4, slice 1). Locks the FACT shape and the R1/R3b liveness
4
- * invariant in code BEFORE any IO wiring (gate-first).
5
- *
6
- * Proves:
7
- * - R1 (out-of-domain → unsupported, NEVER coerced): claude-code / codex /
8
- * antigravity resolve to `unsupported` for EVERY socket input (alive, dead,
9
- * indeterminate, null) — the socket value is never folded into the fact for a
10
- * backend whose liveness predicate is undefined.
11
- * - R3b (in-domain 4-value): pi maps alive→alive, dead→dead,
12
- * indeterminate→indeterminate, and null→indeterminate (no proof is
13
- * `indeterminate`, never `dead`).
14
- * - liveness is always a member of FACT_LIVENESSES.
15
- * - identity / cwd-history fields pass through verbatim from the meta-record.
16
- * - facts-only keyset: PeerFact carries EXACTLY the identity facts + `liveness`
17
- * — no verb-routing field (resumable/sendable/transport/dispatch/action) and
18
- * no transcriptPath. (동결결정 10: the fact layer must not bake routing.)
19
- *
20
- * Pure; no IO, no backend, no API.
21
- */
22
-
23
- import assert from "node:assert/strict";
24
- import {
25
- type FactList,
26
- isOutOfSocketDomainGardenIdConflict,
27
- type PeerFact,
28
- type RecordLessSocketFact,
29
- resolveFactList,
30
- resolvePeerFact,
31
- type SocketProbe,
32
- } from "../pi-extensions/lib/entwurf-facts.ts";
33
- import {
34
- FACT_LIVENESSES,
35
- type FactLiveness,
36
- isLivenessSupported,
37
- LIVENESS_DOMAIN_BACKENDS,
38
- } from "../pi-extensions/lib/entwurf-v2-contract.ts";
39
- import {
40
- META_CITIZEN_BACKENDS,
41
- type MetaCitizenBackend,
42
- type MetaIdentity,
43
- } from "../pi-extensions/lib/meta-session.ts";
44
- import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
45
-
46
- // Derive the backend coverage from the SSOT registry so a NEW backend in
47
- // META_CITIZEN_BACKENDS auto-extends this gate (Fable/GPT non-blocking suggestion):
48
- // out-of-domain backends must all resolve to `unsupported`, the in-domain set is
49
- // the only one with a defined liveness predicate. A drift assertion below pins
50
- // the in-domain set to ["pi"] so widening the domain forces new R3b coverage.
51
- const OUT_OF_DOMAIN = META_CITIZEN_BACKENDS.filter((b) => !isLivenessSupported(b));
52
- const IN_DOMAIN = META_CITIZEN_BACKENDS.filter((b) => isLivenessSupported(b));
53
-
54
- let passed = 0;
55
- function ok(label: string, cond: boolean): void {
56
- assert.ok(cond, label);
57
- console.log(` ok ${label}`);
58
- passed++;
59
- }
60
-
61
- function identity(backend: MetaCitizenBackend, over: Partial<MetaIdentity> = {}): MetaIdentity {
62
- return {
63
- schemaVersion: 3,
64
- gardenId: "20260611T093858-14984d",
65
- backend,
66
- nativeSessionId: "native-abc",
67
- cwd: "/home/junghan/repos/gh/entwurf",
68
- model: "claude-opus-5",
69
- transcriptPath: "/home/junghan/.claude/projects/x/native-abc.jsonl",
70
- createdAt: "2026-06-11T00:38:58.000Z",
71
- recordUpdatedAt: "2026-06-11T02:40:00.000Z",
72
- ...over,
73
- };
74
- }
75
-
76
- const SOCKET_INPUTS: (SocketLiveness | null)[] = ["alive", "dead", "indeterminate", null];
77
-
78
- // ── R1: out-of-domain backend → unsupported for EVERY socket input ──────────
79
- ok("R1 out-of-domain set is non-empty (gate is actually exercising R1)", OUT_OF_DOMAIN.length > 0);
80
- for (const backend of OUT_OF_DOMAIN) {
81
- for (const socket of SOCKET_INPUTS) {
82
- const fact = resolvePeerFact(identity(backend), socket);
83
- ok(`R1 ${backend} + socket=${socket ?? "null"} → unsupported (not coerced)`, fact.liveness === "unsupported");
84
- }
85
- }
86
-
87
- // Drift guard: the in-domain set is exactly the contract's domain and is ["pi"].
88
- // If the domain widens, this fails → forces adding R3b value coverage for the
89
- // new backend instead of silently leaving it untested.
90
- assert.deepStrictEqual(IN_DOMAIN, [...LIVENESS_DOMAIN_BACKENDS], "IN_DOMAIN ≠ LIVENESS_DOMAIN_BACKENDS");
91
- assert.deepStrictEqual(IN_DOMAIN, ["pi"], "liveness domain widened — add R3b coverage for the new backend");
92
- ok("drift guard: in-domain set == LIVENESS_DOMAIN_BACKENDS == ['pi']", true);
93
-
94
- // ── R3b: in-domain (pi) 4-value mapping, null = indeterminate (never dead) ──
95
- const PI_CASES: [SocketLiveness | null, FactLiveness][] = [
96
- ["alive", "alive"],
97
- ["dead", "dead"],
98
- ["indeterminate", "indeterminate"],
99
- [null, "indeterminate"],
100
- ];
101
- for (const [socket, expected] of PI_CASES) {
102
- const fact = resolvePeerFact(identity("pi"), socket);
103
- ok(`R3b pi + socket=${socket ?? "null"} → ${expected}`, fact.liveness === expected);
104
- }
105
- ok(
106
- "R3b pi null is indeterminate, NOT dead (no proof ≠ dead)",
107
- resolvePeerFact(identity("pi"), null).liveness === "indeterminate",
108
- );
109
-
110
- // ── liveness always ∈ FACT_LIVENESSES ──────────────────────────────────────
111
- for (const backend of META_CITIZEN_BACKENDS) {
112
- for (const socket of SOCKET_INPUTS) {
113
- const fact = resolvePeerFact(identity(backend), socket);
114
- ok(
115
- `liveness ∈ FACT_LIVENESSES (${backend}/${socket ?? "null"} → ${fact.liveness})`,
116
- (FACT_LIVENESSES as readonly string[]).includes(fact.liveness),
117
- );
118
- }
119
- }
120
-
121
- // ── identity / cwd-history pass through verbatim ────────────────────────────
122
- {
123
- const id = identity("pi", {
124
- gardenId: "20260611T112732-0f42b6",
125
- nativeSessionId: "uuid-xyz",
126
- cwd: "/tmp/work",
127
- model: null,
128
- createdAt: "2026-06-11T01:00:00.000Z",
129
- recordUpdatedAt: "2026-06-11T03:00:00.000Z",
130
- });
131
- const fact = resolvePeerFact(id, "alive");
132
- ok("passthrough gardenId", fact.gardenId === id.gardenId);
133
- ok("passthrough backend", fact.backend === id.backend);
134
- ok("passthrough nativeSessionId", fact.nativeSessionId === id.nativeSessionId);
135
- ok("passthrough cwd", fact.cwd === id.cwd);
136
- ok("passthrough model (null preserved)", fact.model === null);
137
- ok("passthrough createdAt", fact.createdAt === id.createdAt);
138
- ok("passthrough recordUpdatedAt", fact.recordUpdatedAt === id.recordUpdatedAt);
139
- }
140
-
141
- // ── facts-only keyset: identity facts + liveness + the two observed axes, and
142
- // still NO verb-routing / no transcript PATH ────────────────────────────────
143
- {
144
- const fact = resolvePeerFact(identity("pi"), "alive");
145
- const keys = Object.keys(fact).sort();
146
- const expected = [
147
- "backend",
148
- "createdAt",
149
- "cwd",
150
- "gardenId",
151
- "liveness",
152
- "model",
153
- "nativeSessionId",
154
- "recordUpdatedAt",
155
- "receiver",
156
- "transcript",
157
- ].sort();
158
- assert.deepStrictEqual(keys, expected, `PeerFact keyset drift: got ${keys.join(",")}`);
159
- ok("facts-only keyset exact (identity facts + liveness + observed receiver/transcript)", true);
160
-
161
- // #101: an unmeasured row says so. `unobserved` is the default for a caller that
162
- // injected no observer — never `none`/`absent`, which would be a fabricated fact.
163
- ok(
164
- "an observer-less composition reports both axes as unobserved (never a default fact)",
165
- fact.receiver === "unobserved" && fact.transcript === "unobserved",
166
- );
167
- const observed = resolvePeerFact(identity("claude-code"), null, { receiver: "inactive", transcript: "absent" });
168
- ok(
169
- "an injected observation rides onto the fact verbatim",
170
- observed.receiver === "inactive" && observed.transcript === "absent",
171
- );
172
-
173
- const FORBIDDEN = [
174
- "resumable",
175
- "sendable",
176
- "transport",
177
- "dispatch",
178
- "action",
179
- "transcriptPath",
180
- "parentGardenId",
181
- "isEntwurf",
182
- ];
183
- for (const k of FORBIDDEN) {
184
- ok(`no '${k}' on PeerFact (facts-only, no verb-routing/transcript)`, !(k in fact));
185
- }
186
- }
187
-
188
- // Type-level guard: the test references PeerFact so a field rename breaks tsc.
189
- const _typecheck: (f: PeerFact) => FactLiveness = (f) => f.liveness;
190
- void _typecheck;
191
-
192
- // ════════════════════════════════════════════════════════════════════════════
193
- // slice 2 — resolveFactList: meta-store ⨯ socket union (설계 동결 2026-06-11,
194
- // GPT힣 + Fable; #50 C4 demotion). PeerFact for record citizens,
195
- // RecordLessSocketFact for sockets no record claims — a DIAGNOSTIC subject the
196
- // provider folds into `record-less-socket`, never a citizen; gardenId is the
197
- // correlation key.
198
- // ════════════════════════════════════════════════════════════════════════════
199
-
200
- function socketProbe(gardenId: string, liveness: SocketLiveness): SocketProbe {
201
- return { gardenId, liveness };
202
- }
203
-
204
- const GID_PI_LIVE = "20260611T115213-3aa371";
205
- const GID_PI_DORMANT = "20260611T093858-14984d";
206
- const GID_CLAUDE = "20260611T112732-0f42b6";
207
- const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
208
-
209
- // ── basic union: pi-live citizen + claude citizen + record-less live socket ──
210
- {
211
- const ids = [identity("pi", { gardenId: GID_PI_LIVE }), identity("claude-code", { gardenId: GID_CLAUDE })];
212
- const probes = [socketProbe(GID_PI_LIVE, "alive"), socketProbe(GID_SOCKET_ONLY, "alive")];
213
- const out = resolveFactList(ids, probes);
214
- ok("union: 2 citizens → 2 PeerFacts", out.peers.length === 2);
215
- ok("union: 1 record-less socket → 1 RecordLessSocketFact", out.recordLessSockets.length === 1);
216
- ok(
217
- "union: pi-live citizen liveness = alive (from its probe)",
218
- out.peers.find((p) => p.gardenId === GID_PI_LIVE)?.liveness === "alive",
219
- );
220
- ok(
221
- "union: claude citizen liveness = unsupported (out-of-domain, socket ignored)",
222
- out.peers.find((p) => p.gardenId === GID_CLAUDE)?.liveness === "unsupported",
223
- );
224
- ok("union: record-less gardenId surfaced", out.recordLessSockets[0]?.gardenId === GID_SOCKET_ONLY);
225
- }
226
-
227
- // ── dormant trap (Fable): pi citizen probed dead → dead (NOT indeterminate) ──
228
- {
229
- const out = resolveFactList([identity("pi", { gardenId: GID_PI_DORMANT })], [socketProbe(GID_PI_DORMANT, "dead")]);
230
- ok(
231
- "dormant trap: pi citizen + probe=dead → liveness=dead (resumable, not stranded)",
232
- out.peers[0]?.liveness === "dead",
233
- );
234
- ok("dormant trap: dead pi citizen is a PeerFact, never record-less", out.recordLessSockets.length === 0);
235
- }
236
-
237
- // ── F3 preserve: pi citizen probed indeterminate → not folded to dead ────────
238
- {
239
- const out = resolveFactList([identity("pi", { gardenId: GID_PI_LIVE })], [socketProbe(GID_PI_LIVE, "indeterminate")]);
240
- ok(
241
- "F3 preserve: pi citizen + probe=indeterminate → indeterminate (never folded)",
242
- out.peers[0]?.liveness === "indeterminate",
243
- );
244
- }
245
-
246
- // ── wiring invariant: unprobed in-domain citizen → throw (null is unprobed-only) ─
247
- {
248
- let threw = false;
249
- try {
250
- resolveFactList([identity("pi", { gardenId: GID_PI_LIVE })], []);
251
- } catch {
252
- threw = true;
253
- }
254
- ok("wiring invariant: in-domain citizen absent from probes → throw (never silent null/indeterminate)", threw);
255
- }
256
-
257
- // ── fail-loud (동결3): out-of-domain citizen owning a control socket = ambiguity ─
258
- {
259
- let threw = false;
260
- try {
261
- resolveFactList([identity("claude-code", { gardenId: GID_CLAUDE })], [socketProbe(GID_CLAUDE, "alive")]);
262
- } catch {
263
- threw = true;
264
- }
265
- ok("dedup/authority: out-of-socket-domain citizen + same-gid socket → fail-loud", threw);
266
- }
267
-
268
- // ── dedup (동결3): pi citizen + its socket → PeerFact only, never both ────────
269
- {
270
- const out = resolveFactList([identity("pi", { gardenId: GID_PI_LIVE })], [socketProbe(GID_PI_LIVE, "alive")]);
271
- ok("dedup: pi citizen consumes its socket (1 PeerFact)", out.peers.length === 1);
272
- ok("dedup: consumed gid not also emitted as RecordLessSocketFact", out.recordLessSockets.length === 0);
273
- const allGids = [...out.peers.map((p) => p.gardenId), ...out.recordLessSockets.map((s) => s.gardenId)];
274
- ok("dedup: no gardenId appears in both sections", new Set(allGids).size === allGids.length);
275
- }
276
-
277
- // ── duplicate inputs → throw ─────────────────────────────────────────────────
278
- {
279
- let dupProbe = false;
280
- try {
281
- resolveFactList([], [socketProbe(GID_PI_LIVE, "alive"), socketProbe(GID_PI_LIVE, "dead")]);
282
- } catch {
283
- dupProbe = true;
284
- }
285
- ok("duplicate socket probe for one gid → throw", dupProbe);
286
-
287
- let dupId = false;
288
- try {
289
- resolveFactList(
290
- [identity("pi", { gardenId: GID_PI_LIVE }), identity("pi", { gardenId: GID_PI_LIVE })],
291
- [socketProbe(GID_PI_LIVE, "alive")],
292
- );
293
- } catch {
294
- dupId = true;
295
- }
296
- ok("duplicate meta-record for one gid → throw", dupId);
297
- }
298
-
299
- // ── RecordLessSocketFact (#50 C4): gid + 3-value liveness, nothing else ──────
300
- // The demotion is structural: no enrich (cwd/model/idle came from get_info-ing
301
- // the socket — treating it as a citizen), no kind tag, no synthetic identity.
302
- {
303
- const out = resolveFactList([], [socketProbe(GID_SOCKET_ONLY, "alive")]);
304
- const s = out.recordLessSockets[0] as RecordLessSocketFact;
305
- ok("record-less: liveness ∈ SocketLiveness 3-value (never unsupported)", s.liveness === "alive");
306
- const keys = Object.keys(s).sort();
307
- assert.deepStrictEqual(keys, ["gardenId", "liveness"], `RecordLessSocketFact keyset drift: ${keys.join(",")}`);
308
- ok("record-less: keyset exact (gardenId + liveness ONLY — enrich is gone with the listing)", true);
309
- const FORBIDDEN = [
310
- "resumable",
311
- "sendable",
312
- "transport",
313
- "dispatch",
314
- "action",
315
- "backend",
316
- "nativeSessionId",
317
- "isEntwurf",
318
- "cwd",
319
- "model",
320
- "idle",
321
- "infoError",
322
- "kind",
323
- ];
324
- for (const k of FORBIDDEN) {
325
- ok(`record-less: no '${k}' (no verb-routing, no synthetic identity, no enrich)`, !(k in s));
326
- }
327
- // dead/indeterminate stay surfaced too — hiding a dead socket is GC's job.
328
- const dead = resolveFactList([], [socketProbe(GID_SOCKET_ONLY, "dead")]);
329
- ok("record-less: dead socket stays surfaced (GC hides, the listing never does)", dead.recordLessSockets.length === 1);
330
- }
331
-
332
- // ── determinism: output sorted by gardenId in each section ───────────────────
333
- {
334
- const ids = [
335
- identity("pi", { gardenId: "20260611T222222-bbbbbb" }),
336
- identity("pi", { gardenId: "20260611T111111-aaaaaa" }),
337
- ];
338
- const probes = [
339
- socketProbe("20260611T222222-bbbbbb", "alive"),
340
- socketProbe("20260611T111111-aaaaaa", "alive"),
341
- socketProbe("20260611T333333-cccccc", "alive"),
342
- ];
343
- const out = resolveFactList(ids, probes);
344
- ok("determinism: peers sorted by gardenId", out.peers[0]?.gardenId === "20260611T111111-aaaaaa");
345
- ok(
346
- "determinism: recordLessSockets sorted by gardenId",
347
- out.recordLessSockets[0]?.gardenId === "20260611T333333-cccccc",
348
- );
349
- }
350
-
351
- // ── isOutOfSocketDomainGardenIdConflict: shared record-side predicate ──────
352
- // (fact-provider listing + v2 decider dispatch consume this same fn — 4c 재유도
353
- // 금지 동형). The union over socketGids ∪ symlinkedGardenIds is the fact-provider:125
354
- // gap closure: a symlinked socket is never probed, so its gid is absent from
355
- // socketGids; the predicate must STILL flag an out-of-domain citizen owning it.
356
- {
357
- const G = "20260611T444444-dddddd";
358
- const realSockets = new Set([G]);
359
- const symlinked = new Set([G]);
360
- const empty = new Set<string>();
361
- for (const piBackend of IN_DOMAIN) {
362
- ok(
363
- `conflict-predicate: socket-domain (${piBackend}) is not a record-side conflict`,
364
- !isOutOfSocketDomainGardenIdConflict(piBackend, G, realSockets, symlinked),
365
- );
366
- }
367
- ok(
368
- "conflict-predicate: out-of-socket-domain + real socket → conflict",
369
- isOutOfSocketDomainGardenIdConflict("claude-code", G, realSockets, empty),
370
- );
371
- ok(
372
- "conflict-predicate: out-of-socket-domain + symlinked socket → conflict",
373
- isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, symlinked),
374
- );
375
- ok(
376
- "conflict-predicate: out-of-socket-domain + no socket → no conflict",
377
- !isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, empty),
378
- );
379
- }
380
-
381
- // Type-level guard: reference FactList so a shape rename breaks tsc.
382
- const _factlist: (f: FactList) => number = (f) => f.peers.length + f.recordLessSockets.length;
383
- void _factlist;
384
-
385
- console.log(`\n[check-entwurf-facts] ${passed} assertions ok`);
@@ -1,122 +0,0 @@
1
- /**
2
- * check-entwurf-resume-args — deterministic gate for the resume-argv SSOT (`buildResumePiArgs`).
3
- *
4
- * The contract this gate holds changed with S1, and the change is the point: the builder used to
5
- * emit a HEADLESS child (`--mode json -p … <prompt>`) because its only caller was the detached
6
- * `spawn-bg` watcher. That transport was withdrawn, and the argv was MEASURED against the runtime
7
- * before the new consumer was written (2026-08-06, private tmux + fixture): dropped into a tmux
8
- * window, `-p` is pi's own "non-interactive mode" and the operator gets a JSON stream instead of a
9
- * session they can type into. The shape that actually reopens a visible citizen carries neither.
10
- *
11
- * So this gate pins the ONE shipped posture, and pins the absences as hard as the presences —
12
- * a re-introduced `-p` would still open a window, still stand a socket up, and still look green
13
- * to anything that only asserted `--entwurf-control`:
14
- *
15
- * RESUMEARGS-CONTROL-FIRST `--entwurf-control` leads, so the resumed session is addressable
16
- * RESUMEARGS-NO-HEADLESS no `--mode`, no `-p` — the window is interactive
17
- * RESUMEARGS-NO-PROMPT-TAIL no positional prompt — a resume runs no turn
18
- * RESUMEARGS-EXT-ONCE-BEFORE-SESSION bridge args exactly once, between control and --session
19
- * RESUMEARGS-PROVIDER-OPTIONAL a recorded provider is emitted, an absent one emits no flag
20
- * RESUMEARGS-SESSION-IS-FILE `--session <abs path>`, never `--session-id` (which MINTS)
21
- *
22
- * Pure string assembly — no IO, no spawn.
23
- */
24
-
25
- import assert from "node:assert/strict";
26
- import { buildResumePiArgs } from "../pi-extensions/lib/entwurf-resume-args.ts";
27
-
28
- let passed = 0;
29
- function ok(label: string, cond: boolean): void {
30
- assert.ok(cond, label);
31
- console.log(` ok ${label}`);
32
- passed++;
33
- }
34
-
35
- // The resume target is a session FILE (#50 C2), not a garden id: the record owns the address, so
36
- // argv only has to name WHICH transcript to reopen.
37
- const SESSION_FILE =
38
- "/home/op/.pi/agent/sessions/-home-op-repo/2026-06-13T09-10-00-000Z_019e8faa-04ea-7b73-bf2c-1465d525c2e8.jsonl";
39
- // Production shape, measured: getEntwurfExplicitExtensions emits `-e <path>`, not `--extension`.
40
- const EXT = ["-e", "/path/to/entwurf/index.ts"] as const;
41
-
42
- function valueAfter(args: readonly string[], flag: string): string | undefined {
43
- const i = args.indexOf(flag);
44
- return i === -1 ? undefined : args[i + 1];
45
- }
46
-
47
- function main(): void {
48
- // ── The shipped posture, with a recorded ACP provider (the only shape whose ext args are
49
- // non-empty — measured as `["-e", "<bridge>"]` on a real recorded provider=entwurf record).
50
- {
51
- const args = buildResumePiArgs({
52
- sessionFile: SESSION_FILE,
53
- explicitExtensionArgs: EXT,
54
- provider: "entwurf",
55
- model: "claude-sonnet-5",
56
- });
57
-
58
- // Assertion order is a contract, not a reading preference: each claim must be the FIRST
59
- // one a defect against it reaches, or a red would be reported under someone else's name.
60
- // The absences come first because a re-introduced headless prefix also displaces the
61
- // leading flag, and "the argv is headless again" is the truer diagnosis of that.
62
- ok(
63
- "[QK:RESUMEARGS-NO-HEADLESS] no --mode and no -p — `-p` is pi's non-interactive mode, so the headless prefix would open a window streaming JSON instead of a session the operator can use",
64
- !args.includes("--mode") && !args.includes("-p") && !args.includes("--print"),
65
- );
66
- ok(
67
- "[QK:RESUMEARGS-CONTROL-FIRST] --entwurf-control is the FIRST token — the resumed session stands its control socket up, which is the whole difference between a restored transcript and an addressable citizen",
68
- args[0] === "--entwurf-control",
69
- );
70
- ok(
71
- "[QK:RESUMEARGS-NO-PROMPT-TAIL] the argv ends at --model <m> with NO positional prompt — a resume reopens a conversation and runs no turn (measured: the transcript stayed byte-identical)",
72
- args[args.length - 2] === "--model" && args[args.length - 1] === "claude-sonnet-5",
73
- );
74
- ok(
75
- "no --no-extensions either: the one-shot posture that let `pi -p` exit is exactly what a resident resume must not emit",
76
- !args.includes("--no-extensions"),
77
- );
78
-
79
- // SESSION-IS-FILE precedes the ext-args claim because the ext claim is stated RELATIVE to
80
- // `--session`: if that flag were renamed, the positional assertion below would go red about
81
- // the extensions while the actual defect is the flag that MINTS a session.
82
- ok(
83
- "[QK:RESUMEARGS-SESSION-IS-FILE] the builder targets an exact FILE via --session and never --session-id, which would CREATE the id when missing and silently mint an empty session instead of resuming one",
84
- valueAfter(args, "--session") === SESSION_FILE && !args.includes("--session-id"),
85
- );
86
- ok(
87
- "[QK:RESUMEARGS-EXT-ONCE-BEFORE-SESSION] the bridge args appear EXACTLY once and sit between --entwurf-control and --session — dropping them re-opens the #29 'Unknown provider' footgun, and doubling them is a different broken argv",
88
- args.filter((a) => a === "-e").length === 1 &&
89
- args.indexOf("--entwurf-control") < args.indexOf("-e") &&
90
- args.indexOf("-e") < args.indexOf("--session"),
91
- );
92
- ok("a recorded provider is laid out as two tokens", valueAfter(args, "--provider") === "entwurf");
93
- ok("model is laid out as two tokens", valueAfter(args, "--model") === "claude-sonnet-5");
94
- ok(
95
- "the full measured argv is reproduced exactly",
96
- args.join(" ") ===
97
- `--entwurf-control -e /path/to/entwurf/index.ts --session ${SESSION_FILE} --provider entwurf --model claude-sonnet-5`,
98
- );
99
- }
100
-
101
- // ── The native shape: no recorded provider, no bridge. Measured as explicitExtensionArgs=[].
102
- for (const provider of [null, undefined] as const) {
103
- const args = buildResumePiArgs({
104
- sessionFile: SESSION_FILE,
105
- explicitExtensionArgs: [],
106
- provider,
107
- model: "gpt-5.6-terra",
108
- });
109
- ok(
110
- `[QK:RESUMEARGS-PROVIDER-OPTIONAL] provider=${provider}: NO --provider flag is emitted — an empty flag value would be a different citizen's launch, not a missing one`,
111
- !args.includes("--provider"),
112
- );
113
- ok(
114
- `provider=${provider}: the argv is exactly the four tokens the native shape needs`,
115
- args.join(" ") === `--entwurf-control --session ${SESSION_FILE} --model gpt-5.6-terra`,
116
- );
117
- }
118
-
119
- console.log(`\ncheck-entwurf-resume-args: ${passed} checks passed`);
120
- }
121
-
122
- main();