@junghanacs/entwurf 0.12.8 → 0.12.10

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 (96) hide show
  1. package/AGENTS.md +134 -248
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +39 -1
  4. package/DELIVERY.md +5 -4
  5. package/README.md +148 -23
  6. package/VERIFY.md +3 -2
  7. package/demo/README.md +2 -2
  8. package/demo/demo.sh +7 -1
  9. package/docs/setup-clean-host.md +24 -7
  10. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +168 -81
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +16 -18
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +14 -13
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +11 -3
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +500 -54
  26. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
  27. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
  28. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
  29. package/mcp/entwurf-bridge/src/index.ts +200 -94
  30. package/package.json +11 -9
  31. package/pi-extensions/entwurf-control.ts +81 -39
  32. package/pi-extensions/lib/acp/models.ts +12 -12
  33. package/pi-extensions/lib/acp/overlay.ts +5 -3
  34. package/pi-extensions/lib/entwurf-core.ts +26 -9
  35. package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
  36. package/pi-extensions/lib/entwurf-fact-provider.ts +24 -19
  37. package/pi-extensions/lib/entwurf-facts.ts +13 -13
  38. package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
  39. package/pi-extensions/lib/entwurf-self-address.ts +8 -9
  40. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
  41. package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
  42. package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
  43. package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
  44. package/pi-extensions/lib/entwurf-v2-production.ts +22 -13
  45. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +11 -3
  46. package/pi-extensions/lib/meta-sender-identity.ts +15 -5
  47. package/pi-extensions/lib/meta-session.ts +543 -58
  48. package/pi-extensions/meta-bridge-hook.ts +8 -2
  49. package/run.sh +78 -67
  50. package/scripts/agy-bridge-config.py +249 -49
  51. package/scripts/agy-bridge.sh +59 -14
  52. package/scripts/agy-imprint.ts +15 -1
  53. package/scripts/check-acp-carrier-augment.ts +34 -2
  54. package/scripts/check-acp-sdk-surface.ts +22 -11
  55. package/scripts/check-acp-session-reuse.ts +1 -1
  56. package/scripts/check-acp-session-store.ts +3 -3
  57. package/scripts/check-agy-permission-matrix.py +655 -0
  58. package/scripts/check-agy-sender-identity.ts +83 -0
  59. package/scripts/check-entwurf-control-rpc.ts +2 -2
  60. package/scripts/check-entwurf-deliverability.ts +9 -6
  61. package/scripts/check-entwurf-fact-provider.ts +16 -8
  62. package/scripts/check-entwurf-facts.ts +13 -13
  63. package/scripts/check-entwurf-resume-args.ts +25 -63
  64. package/scripts/check-entwurf-self-address.ts +187 -4
  65. package/scripts/check-entwurf-session-identity.ts +7 -6
  66. package/scripts/check-entwurf-v2-contract.ts +2 -2
  67. package/scripts/check-entwurf-v2-production.ts +9 -7
  68. package/scripts/check-entwurf-v2-spawn-production.ts +8 -4
  69. package/scripts/check-entwurf-v2-surface.ts +327 -14
  70. package/scripts/check-fresh-cut-gate.sh +305 -4
  71. package/scripts/check-gate-qualification.ts +785 -0
  72. package/scripts/check-meta-identity-consumers.ts +501 -1
  73. package/scripts/check-meta-listing.ts +91 -9
  74. package/scripts/check-meta-receiver-marker.ts +54 -0
  75. package/scripts/check-model-lock.ts +1 -1
  76. package/scripts/check-shell-quote.ts +2 -1
  77. package/scripts/lib/mutation-qualify.ts +794 -0
  78. package/scripts/meta-bridge-fresh-cut.ts +164 -28
  79. package/scripts/mutants/acp-augment.json +30 -0
  80. package/scripts/mutants/agy-permission.json +144 -0
  81. package/scripts/mutants/meta-identity.json +17 -0
  82. package/scripts/mutants/self-address.json +59 -0
  83. package/scripts/mutants/v2-surface.json +87 -0
  84. package/scripts/pi_settings_io.py +65 -0
  85. package/scripts/register-pi-package.py +183 -37
  86. package/scripts/register-pi-provider.py +68 -10
  87. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  88. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  89. package/scripts/smoke-acp-v2-send-live.ts +33 -18
  90. package/scripts/smoke-agy-install-state.sh +205 -20
  91. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
  92. package/scripts/smoke-meta-install-state.sh +1 -1
  93. package/scripts/smoke-pi-attach.ts +7 -2
  94. package/scripts/smoke-user-scope-citizen.sh +177 -0
  95. package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
  96. package/scripts/check-entwurf-mailbox-guard.ts +0 -262
@@ -16,11 +16,19 @@
16
16
  * - mode "strict" throws on any error; "collect" (default) returns partial,
17
17
  * - errors sorted by filename.
18
18
  *
19
+ * #52 adds the store-WIDE half this listing can afford (it already holds every
20
+ * record): two records claiming one `nativeSessionId` are not two citizens. NEITHER
21
+ * is listed — a facts surface may not mint the ownership the certification refuses
22
+ * to mint — both become errors naming each other, and every unrelated citizen keeps
23
+ * listing. Before this, `entwurf_peers` reported a store the doctor calls
24
+ * uncertifiable as clean, which is a wrong FACT, not merely a missing guard.
25
+ *
19
26
  * Pure; entries/readRecord injected, no IO.
20
27
  */
21
28
 
22
29
  import assert from "node:assert/strict";
23
30
  import {
31
+ type ActiveStoreEntry,
24
32
  listAllMetaIdentities,
25
33
  type MetaCitizenBackend,
26
34
  type MetaIdentity,
@@ -61,10 +69,17 @@ function reader(map: Record<string, string>): (filename: string) => string {
61
69
  };
62
70
  }
63
71
 
72
+ /** Store entries from a fixture map — every name a REGULAR file unless `irregular` says
73
+ * otherwise. The listing takes kind, not bare names, so rule 1 (a symlinked record is
74
+ * refused and never followed) is enforceable on this surface too. */
75
+ function entries(map: Record<string, string>, irregular: readonly string[] = []): ActiveStoreEntry[] {
76
+ return Object.keys(map).map((filename) => ({ filename, regularFile: !irregular.includes(filename) }));
77
+ }
78
+
64
79
  // ── valid records → identities, sorted ──────────────────────────────────────
65
80
  {
66
81
  const map = { [`${GID_B}.meta.json`]: rec(GID_B), [`${GID_A}.meta.json`]: rec(GID_A) };
67
- const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
82
+ const { identities, errors } = listAllMetaIdentities(entries(map), reader(map));
68
83
  ok("2 valid → 2 identities, 0 errors", identities.length === 2 && errors.length === 0);
69
84
  ok("identities sorted by gardenId", identities[0]?.gardenId === GID_A);
70
85
  }
@@ -72,7 +87,7 @@ function reader(map: Record<string, string>): (filename: string) => string {
72
87
  // ── corrupt record does NOT blind the listing ───────────────────────────────
73
88
  {
74
89
  const map = { [`${GID_A}.meta.json`]: rec(GID_A), [`${GID_B}.meta.json`]: "{ broken json" };
75
- const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
90
+ const { identities, errors } = listAllMetaIdentities(entries(map), reader(map));
76
91
  ok("corrupt 1 → valid still listed (not blinded)", identities.length === 1 && identities[0]?.gardenId === GID_A);
77
92
  ok("corrupt → exactly 1 error", errors.length === 1);
78
93
  ok("error names the corrupt file", errors[0]?.filename === `${GID_B}.meta.json`);
@@ -81,7 +96,7 @@ function reader(map: Record<string, string>): (filename: string) => string {
81
96
  // ── body/filename drift → error, excluded ───────────────────────────────────
82
97
  {
83
98
  const map = { [`${GID_A}.meta.json`]: rec(GID_B) }; // filename A, body claims B
84
- const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
99
+ const { identities, errors } = listAllMetaIdentities(entries(map), reader(map));
85
100
  ok("drift → excluded from identities", identities.length === 0);
86
101
  ok("drift → 1 error mentioning drift", errors.length === 1 && /drift/.test(errors[0]?.message ?? ""));
87
102
  }
@@ -92,21 +107,27 @@ function reader(map: Record<string, string>): (filename: string) => string {
92
107
  if (f === `${GID_B}.meta.json`) throw new Error("ENOENT mid-scan");
93
108
  return rec(GID_A);
94
109
  };
95
- const { identities, errors } = listAllMetaIdentities([`${GID_A}.meta.json`, `${GID_B}.meta.json`], read);
110
+ const { identities, errors } = listAllMetaIdentities(
111
+ [
112
+ { filename: `${GID_A}.meta.json`, regularFile: true },
113
+ { filename: `${GID_B}.meta.json`, regularFile: true },
114
+ ],
115
+ read,
116
+ );
96
117
  ok("reader throw → error, valid still listed", identities.length === 1 && errors.length === 1);
97
118
  }
98
119
 
99
120
  // ── non-.meta.json ignored ──────────────────────────────────────────────────
100
121
  {
101
122
  const map = { [`${GID_A}.meta.json`]: rec(GID_A), "README.txt": "x", "inbox.signal": "y" };
102
- const { identities, errors } = listAllMetaIdentities(Object.keys(map), reader(map));
123
+ const { identities, errors } = listAllMetaIdentities(entries(map), reader(map));
103
124
  ok("non-.meta.json entries ignored", identities.length === 1 && errors.length === 0);
104
125
  }
105
126
 
106
127
  // ── verbatim-or-nothing: error keyset = {filename, message} ─────────────────
107
128
  {
108
129
  const map = { [`${GID_A}.meta.json`]: "{ broken" };
109
- const { errors } = listAllMetaIdentities(Object.keys(map), reader(map));
130
+ const { errors } = listAllMetaIdentities(entries(map), reader(map));
110
131
  const keys = Object.keys(errors[0] ?? {}).sort();
111
132
  assert.deepStrictEqual(keys, ["filename", "message"], `error keyset drift: ${keys.join(",")}`);
112
133
  ok("error verbatim-or-nothing (filename + message only, no identity field)", true);
@@ -117,20 +138,81 @@ function reader(map: Record<string, string>): (filename: string) => string {
117
138
  const map = { [`${GID_A}.meta.json`]: rec(GID_A), [`${GID_B}.meta.json`]: "{ broken" };
118
139
  let threw = false;
119
140
  try {
120
- listAllMetaIdentities(Object.keys(map), reader(map), { mode: "strict" });
141
+ listAllMetaIdentities(entries(map), reader(map), { mode: "strict" });
121
142
  } catch {
122
143
  threw = true;
123
144
  }
124
145
  ok("mode strict → throw if any record unreadable", threw);
125
- const { identities } = listAllMetaIdentities(Object.keys(map), reader(map), { mode: "collect" });
146
+ const { identities } = listAllMetaIdentities(entries(map), reader(map), { mode: "collect" });
126
147
  ok("mode collect (default) → partial, no throw", identities.length === 1);
127
148
  }
128
149
 
129
150
  // ── errors sorted by filename ───────────────────────────────────────────────
130
151
  {
131
152
  const map = { [`${GID_B}.meta.json`]: "{ x", [`${GID_A}.meta.json`]: "{ y" };
132
- const { errors } = listAllMetaIdentities(Object.keys(map), reader(map));
153
+ const { errors } = listAllMetaIdentities(entries(map), reader(map));
133
154
  ok("errors sorted by filename", errors[0]?.filename === `${GID_A}.meta.json`);
134
155
  }
135
156
 
157
+ // ── #52: duplicate nativeSessionId is not two citizens ──────────────────────
158
+ {
159
+ const shared = "n-shared-conversation";
160
+ const map = {
161
+ [`${GID_A}.meta.json`]: rec(GID_A, { nativeSessionId: shared }),
162
+ [`${GID_B}.meta.json`]: rec(GID_B, { nativeSessionId: shared }),
163
+ };
164
+ const { identities, errors } = listAllMetaIdentities(entries(map), reader(map));
165
+ ok("duplicate nativeSessionId → NEITHER record is listed as a citizen", identities.length === 0);
166
+ ok("duplicate → one error per rival (both are told)", errors.length === 2);
167
+ ok(
168
+ "each error names the OTHER holder, so the pair is debuggable from either side",
169
+ (errors[0]?.message ?? "").includes(`${GID_B}.meta.json`) &&
170
+ (errors[1]?.message ?? "").includes(`${GID_A}.meta.json`),
171
+ );
172
+ ok(
173
+ "the duplicate error names the fresh-cut verb (one prescription, as everywhere else)",
174
+ errors.every((e) => e.message.includes("meta-bridge-fresh-cut")),
175
+ );
176
+ }
177
+
178
+ // ── #52: a duplicate must not blind the citizens around it ──────────────────
179
+ {
180
+ const shared = "n-shared-conversation";
181
+ const GID_C = "20260611T333333-cccccc";
182
+ const map = {
183
+ [`${GID_A}.meta.json`]: rec(GID_A, { nativeSessionId: shared }),
184
+ [`${GID_B}.meta.json`]: rec(GID_B, { nativeSessionId: shared }),
185
+ [`${GID_C}.meta.json`]: rec(GID_C),
186
+ };
187
+ const { identities, errors } = listAllMetaIdentities(entries(map), reader(map));
188
+ ok(
189
+ "an unrelated citizen still lists beside a duplicate pair (0.10 lesson holds)",
190
+ identities.length === 1 && identities[0]?.gardenId === GID_C,
191
+ );
192
+ ok("only the two rivals become errors", errors.length === 2);
193
+ }
194
+
195
+ // ── #52: strict mode refuses a store with a duplicate ───────────────────────
196
+ {
197
+ const shared = "n-shared-conversation";
198
+ const map = {
199
+ [`${GID_A}.meta.json`]: rec(GID_A, { nativeSessionId: shared }),
200
+ [`${GID_B}.meta.json`]: rec(GID_B, { nativeSessionId: shared }),
201
+ };
202
+ let threw = false;
203
+ try {
204
+ listAllMetaIdentities(entries(map), reader(map), { mode: "strict" });
205
+ } catch {
206
+ threw = true;
207
+ }
208
+ ok("mode strict → a duplicate throws like any other unreadable store", threw);
209
+ }
210
+
211
+ // ── #52: one holder is not a duplicate (no false positive) ──────────────────
212
+ {
213
+ const map = { [`${GID_A}.meta.json`]: rec(GID_A), [`${GID_B}.meta.json`]: rec(GID_B) };
214
+ const { identities, errors } = listAllMetaIdentities(entries(map), reader(map));
215
+ ok("distinct nativeSessionIds are untouched by the uniqueness pass", identities.length === 2 && errors.length === 0);
216
+ }
217
+
136
218
  console.log(`\n[check-meta-listing] ${passed} assertions ok`);
@@ -107,6 +107,60 @@ ok(
107
107
  readMetaReceiverMarker({ gardenId: ghostGarden, receiversDir: DIR, verifyOwner: false }) !== null,
108
108
  );
109
109
 
110
+ // ── the owner CLAIM, one layer above liveness (#53 A) ───────────────────────
111
+ // A marker naming pid 1 passes the pid-reuse guard on a running Linux host: init is
112
+ // still the very process the marker named, so its start-key matches. The claim is
113
+ // what is false — init owns no Claude session. Such a marker is legacy or corrupt
114
+ // residue (any pre-fix writer that was reparented to init; a foreign/damaged file);
115
+ // the one observed was a shell-form hook. Honoring it made a dead garden id read as
116
+ // an ACTIVE RECEIVER and, one surface over, left meta-bridge-fresh-cut unable to be
117
+ // unblocked by the action it prescribes (#53 A).
118
+ const initGarden = "20260614T130000-cccccc";
119
+ const initKey = processStartKey(1);
120
+ ok("this host CAN read a start key for pid 1 (the residue cells are not vacuous)", initKey !== "");
121
+ writeFileSync(
122
+ metaReceiverMarkerPath(initGarden, DIR),
123
+ `${JSON.stringify({
124
+ gardenId: initGarden,
125
+ backend: "claude-code",
126
+ nativeSessionId: "n-init",
127
+ ownerPid: 1,
128
+ ownerStartKey: initKey, // the REAL key: the pid-reuse guard would pass
129
+ ownerKind: "claude-code-cli",
130
+ armProvenance: "session-start",
131
+ updatedAt: "2026-06-10T16:03:10.000Z",
132
+ })}\n`,
133
+ );
134
+ ok(
135
+ "an init-owned marker reads as null — pid 1 cannot own a session, however live init is",
136
+ readMetaReceiverMarker({ gardenId: initGarden, receiversDir: DIR }) === null,
137
+ );
138
+ ok(
139
+ "the classifier itself still says LIVE for it — the refusal is a CLAIM rule, not a liveness one",
140
+ classifyMarkerOwner(initKey, { currentStartKey: processStartKey(1), pidExists: probePidExistence(1) }) === "live",
141
+ );
142
+ ok(
143
+ "verifyOwner:false does NOT reach past plausibility (it opts out of liveness, not of validity)",
144
+ readMetaReceiverMarker({ gardenId: initGarden, receiversDir: DIR, verifyOwner: false }) === null,
145
+ );
146
+ let initWriteRejected = false;
147
+ try {
148
+ writeMetaReceiverMarker({
149
+ gardenId: initGarden,
150
+ backend: "claude-code",
151
+ nativeSessionId: "n-init",
152
+ ownerPid: 1,
153
+ armProvenance: "session-start",
154
+ receiversDir: DIR,
155
+ });
156
+ } catch {
157
+ initWriteRejected = true;
158
+ }
159
+ ok(
160
+ "minting an init-owned receiver marker THROWS at the write boundary (no writer can reintroduce it)",
161
+ initWriteRejected,
162
+ );
163
+
110
164
  // ── armProvenance constraint ────────────────────────────────────────────────
111
165
  ok(
112
166
  "arm provenances are exactly the arm-capable events",
@@ -211,7 +211,7 @@ async function makeHarness(opts: MakeHarnessOpts = {}): Promise<Harness> {
211
211
  const NATIVE_A: ModelLike = { provider: "openai-codex", id: "gpt-5.4" };
212
212
  const NATIVE_B: ModelLike = { provider: "openai-codex", id: "gpt-5.5" };
213
213
  const PSA_SONNET: ModelLike = { provider: "entwurf", id: "claude-sonnet-5" };
214
- const PSA_OPUS: ModelLike = { provider: "entwurf", id: "claude-opus-4-8" };
214
+ const PSA_OPUS: ModelLike = { provider: "entwurf", id: "claude-opus-5" };
215
215
 
216
216
  let passed = 0;
217
217
  let failed = 0;
@@ -6,7 +6,8 @@
6
6
  * Backticks and `$(...)` inside a user prompt got executed by the remote shell
7
7
  * before pi ever saw them. The fix introduced `shellQuote()` (POSIX `'...'`
8
8
  * with `'\''` escape) in two places:
9
- * - pi-extensions/lib/entwurf-core.ts (legacy sync spawn/resume helper while v2 extraction is in progress)
9
+ * - pi-extensions/lib/entwurf-core.ts (the one remaining site; the entwurf.ts /
10
+ * entwurf-async.ts siblings this once cross-checked were removed in 0.12)
10
11
  *
11
12
  * This script enforces two invariants:
12
13
  * 1. `shellQuote()` source matches the reference implementation byte-for-byte.