@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.
- package/AGENTS.md +134 -248
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +39 -1
- package/DELIVERY.md +5 -4
- package/README.md +148 -23
- package/VERIFY.md +3 -2
- package/demo/README.md +2 -2
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +24 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +168 -81
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +5 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +16 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +14 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +500 -54
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
- package/mcp/entwurf-bridge/src/index.ts +200 -94
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +81 -39
- package/pi-extensions/lib/acp/models.ts +12 -12
- package/pi-extensions/lib/acp/overlay.ts +5 -3
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +24 -19
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +22 -13
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +11 -3
- package/pi-extensions/lib/meta-sender-identity.ts +15 -5
- package/pi-extensions/lib/meta-session.ts +543 -58
- package/pi-extensions/meta-bridge-hook.ts +8 -2
- package/run.sh +78 -67
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/agy-imprint.ts +15 -1
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-acp-session-reuse.ts +1 -1
- package/scripts/check-acp-session-store.ts +3 -3
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-agy-sender-identity.ts +83 -0
- package/scripts/check-entwurf-control-rpc.ts +2 -2
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +16 -8
- package/scripts/check-entwurf-facts.ts +13 -13
- package/scripts/check-entwurf-resume-args.ts +25 -63
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-session-identity.ts +7 -6
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +9 -7
- package/scripts/check-entwurf-v2-spawn-production.ts +8 -4
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-fresh-cut-gate.sh +305 -4
- package/scripts/check-gate-qualification.ts +785 -0
- package/scripts/check-meta-identity-consumers.ts +501 -1
- package/scripts/check-meta-listing.ts +91 -9
- package/scripts/check-meta-receiver-marker.ts +54 -0
- package/scripts/check-model-lock.ts +1 -1
- package/scripts/check-shell-quote.ts +2 -1
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/meta-bridge-fresh-cut.ts +164 -28
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/pi_settings_io.py +65 -0
- package/scripts/register-pi-package.py +183 -37
- package/scripts/register-pi-provider.py +68 -10
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/scripts/smoke-meta-install-state.sh +1 -1
- package/scripts/smoke-pi-attach.ts +7 -2
- package/scripts/smoke-user-scope-citizen.sh +177 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- package/scripts/check-entwurf-mailbox-guard.ts +0 -262
|
@@ -48,6 +48,9 @@ import {
|
|
|
48
48
|
import {
|
|
49
49
|
FRESH_CUT_COMMAND,
|
|
50
50
|
type MetaIdentity,
|
|
51
|
+
metaRecordExistsByGardenId,
|
|
52
|
+
processStartKey,
|
|
53
|
+
readMetaSenderMarker,
|
|
51
54
|
upsertMetaSession,
|
|
52
55
|
writeMetaSenderMarker,
|
|
53
56
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
@@ -358,6 +361,86 @@ try {
|
|
|
358
361
|
trusted?.identity.gardenId === record.record.gardenId,
|
|
359
362
|
);
|
|
360
363
|
}
|
|
364
|
+
// ── #53 A: an init-owned marker names NOBODY, however live init is ─────────
|
|
365
|
+
// The shape measured on a second Linux host: `meta-senders/claude-code/1.json`,
|
|
366
|
+
// pid 1, and the REAL start-key for init — so the pid-reuse guard PASSES and
|
|
367
|
+
// classifyMarkerOwner answers `live` for as long as the host is up. The defect is
|
|
368
|
+
// one layer up: init owns no Claude session. THIS gate's own writer is one of the
|
|
369
|
+
// two that could produce the shape before #53 A — the agy imprint asked `> 0`
|
|
370
|
+
// while the Claude hook already refused `<= 1` — so "only the shell-form hook
|
|
371
|
+
// could have written it" was never true. After this cut no writer in the tree can.
|
|
372
|
+
{
|
|
373
|
+
clearMarkers();
|
|
374
|
+
const record = upsertMetaSession({
|
|
375
|
+
input: { backend: "claude-code", nativeSessionId: "sess-init", cwd: REPO_DIR },
|
|
376
|
+
});
|
|
377
|
+
const initKey = processStartKey(1);
|
|
378
|
+
ok("this host CAN read a start key for pid 1 (the residue cells are not vacuous)", initKey !== "");
|
|
379
|
+
const initDir = path.join(SENDERS_DIR, "claude-code");
|
|
380
|
+
fs.mkdirSync(initDir, { recursive: true });
|
|
381
|
+
fs.writeFileSync(
|
|
382
|
+
path.join(initDir, "1.json"),
|
|
383
|
+
`${JSON.stringify({
|
|
384
|
+
backend: "claude-code",
|
|
385
|
+
gardenId: record.record.gardenId,
|
|
386
|
+
nativeSessionId: "sess-init",
|
|
387
|
+
cwd: REPO_DIR,
|
|
388
|
+
ownerPid: 1,
|
|
389
|
+
ownerStartKey: initKey,
|
|
390
|
+
updatedAt: "2026-06-10T16:03:10.000Z",
|
|
391
|
+
})}\n`,
|
|
392
|
+
);
|
|
393
|
+
ok(
|
|
394
|
+
"an init-owned sender marker reads as null — it grants no identity",
|
|
395
|
+
readMetaSenderMarker({ backend: "claude-code", ownerPid: 1, sendersDir: SENDERS_DIR }) === null,
|
|
396
|
+
);
|
|
397
|
+
ok(
|
|
398
|
+
"...while its record is present and readable, so the refusal is about the marker's CLAIM",
|
|
399
|
+
metaRecordExistsByGardenId(record.record.gardenId),
|
|
400
|
+
);
|
|
401
|
+
// End to end, with pid 1 supplied as an explicit candidate: the answer is
|
|
402
|
+
// "nobody" — not an identity, and not a throw either. Stated at the size of the
|
|
403
|
+
// evidence: the DEFAULT candidate set is only the bridge's parent and grandparent,
|
|
404
|
+
// so pid 1 enters it just when the native host itself was reparented (a detached
|
|
405
|
+
// Claude). Reachable, not observed — the affected host's own launch shape
|
|
406
|
+
// (bridge → claude → bash → wrapper) never had 1 among its candidates, and no
|
|
407
|
+
// wrong-identity read was measured there. The cut's deadlock is the proven half;
|
|
408
|
+
// this is the code-path half.
|
|
409
|
+
let identityThrew: unknown = null;
|
|
410
|
+
let identityAnswer: unknown = "unset";
|
|
411
|
+
try {
|
|
412
|
+
identityAnswer = resolveTrustedMetaSenderIdentity({ ownerPids: [1], sendersDir: SENDERS_DIR });
|
|
413
|
+
} catch (err) {
|
|
414
|
+
identityThrew = err;
|
|
415
|
+
}
|
|
416
|
+
ok(
|
|
417
|
+
"resolving with pid 1 as a candidate yields NO identity (and no throw)",
|
|
418
|
+
identityAnswer === null && identityThrew === null,
|
|
419
|
+
);
|
|
420
|
+
let initWriteRejected = false;
|
|
421
|
+
try {
|
|
422
|
+
writeMetaSenderMarker({
|
|
423
|
+
backend: "claude-code",
|
|
424
|
+
gardenId: record.record.gardenId,
|
|
425
|
+
nativeSessionId: "sess-init",
|
|
426
|
+
cwd: REPO_DIR,
|
|
427
|
+
ownerPid: 1,
|
|
428
|
+
sendersDir: SENDERS_DIR,
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
initWriteRejected = true;
|
|
432
|
+
}
|
|
433
|
+
ok("minting an init-owned sender marker THROWS at the write boundary", initWriteRejected);
|
|
434
|
+
// WIRING: the agy imprint is the SECOND writer, and the one that still asked
|
|
435
|
+
// `> 0` on its own while the Claude hook already refused `<= 1`. A predicate only
|
|
436
|
+
// one writer knows is how #53 A drifted in; both must reach the shared one.
|
|
437
|
+
ok(
|
|
438
|
+
"the agy imprint routes its owner pid through the shared predicate",
|
|
439
|
+
/isPlausibleOwnerPid\(ownerPid\)/.test(fs.readFileSync(HOOK, "utf8")),
|
|
440
|
+
);
|
|
441
|
+
clearMarkers();
|
|
442
|
+
}
|
|
443
|
+
|
|
361
444
|
ok(
|
|
362
445
|
"antigravity is a native-push backend → its replyable comes from the adapter probe",
|
|
363
446
|
nativePushSupported("antigravity"),
|
|
@@ -196,14 +196,14 @@ async function main(): Promise<void> {
|
|
|
196
196
|
{
|
|
197
197
|
const base = {
|
|
198
198
|
sessionId: "20260613T091000-98363c",
|
|
199
|
-
agentId: "pi/claude-opus-
|
|
199
|
+
agentId: "pi/claude-opus-5",
|
|
200
200
|
cwd: "/w",
|
|
201
201
|
timestamp: "2026-06-13T09:10:00.000Z",
|
|
202
202
|
};
|
|
203
203
|
ok(
|
|
204
204
|
"6: minimal envelope → exact block (leading blank line, required fields only)",
|
|
205
205
|
formatSenderInfoBlock(base) ===
|
|
206
|
-
`\n\n<sender_info>{"sessionId":"20260613T091000-98363c","agentId":"pi/claude-opus-
|
|
206
|
+
`\n\n<sender_info>{"sessionId":"20260613T091000-98363c","agentId":"pi/claude-opus-5","cwd":"/w","timestamp":"2026-06-13T09:10:00.000Z"}</sender_info>`,
|
|
207
207
|
);
|
|
208
208
|
const full = formatSenderInfoBlock({ ...base, origin: "pi-session", replyable: false }, true);
|
|
209
209
|
ok(
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* check-entwurf-deliverability — deterministic gate for the conversational-mailbox
|
|
3
|
-
* deliverability predicate (SE-1/SE-2 slice 2c). This is the predicate
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (SE-
|
|
3
|
+
* deliverability predicate (SE-1/SE-2 slice 2c). This is the predicate an enqueue site
|
|
4
|
+
* must consult in slice 2d before writing a .msg, so a reply never rots in a dead
|
|
5
|
+
* session's mailbox (SE-2) and is never enqueued for a backend that has no mailbox drain
|
|
6
|
+
* (SE-1, pi). The SHIPPED sites are the v2 decider and its send-fallback re-resolve; the
|
|
7
|
+
* v1 fallback / MCP v1 / pi-native v1 sites were removed in the 0.12 cutover.
|
|
7
8
|
*
|
|
8
9
|
* Proves:
|
|
9
10
|
* - computeMetaReceiverActive: the shared atom — active ⟺ recordBacked ∧ ownerAlive ∧
|
|
@@ -124,8 +125,10 @@ ok(
|
|
|
124
125
|
|
|
125
126
|
// ── receiverMarkerMatchesIdentity: the marker ↔ identity SSOT (SE-2 2d-3) ───
|
|
126
127
|
// A present marker only raises a receiver to active when it agrees with the record on
|
|
127
|
-
// garden/backend/native id. The
|
|
128
|
-
// both route through this one helper, so presence-only false-positives are
|
|
128
|
+
// garden/backend/native id. The v2 production deliverability seam and the MCP bridge's
|
|
129
|
+
// entwurf_self both route through this one helper, so presence-only false-positives are
|
|
130
|
+
// closed on every shipped path — and there is no second implementation to drift from since
|
|
131
|
+
// entwurf-mailbox-guard.ts (production importers: 0) was deleted 2026-07-27.
|
|
129
132
|
const ID = { gardenId: "20260612T100000-aaaaaa", backend: "claude-code", nativeSessionId: "n-1" };
|
|
130
133
|
ok(
|
|
131
134
|
"marker matches identity on all three axes → true",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* into ONE `record-less-socket` diagnostic (#50 C4: a diagnostic subject,
|
|
9
9
|
* never a listing section) whose message names the cause + fix (fresh-cut),
|
|
10
10
|
* - corrupt record → meta-record-read-error diagnostic, listing NOT blinded,
|
|
11
|
-
* - gardenId↔socket collision (
|
|
11
|
+
* - gardenId↔socket collision (out-of-socket-domain citizen + same-gid socket) → BOTH sides
|
|
12
12
|
* quarantined (gid in neither peers nor socketOnly) + one
|
|
13
13
|
* garden-id-socket-conflict diagnostic, and listEntwurfFacts does NOT throw
|
|
14
14
|
* (expected external-state corruption → diagnostics, not a crash),
|
|
@@ -63,6 +63,8 @@ interface SocketOpts {
|
|
|
63
63
|
extraNames?: string[];
|
|
64
64
|
/** readdir throws with this code (e.g. "EACCES" / "ENOENT") (P2e②). */
|
|
65
65
|
readdirErrorCode?: string;
|
|
66
|
+
/** `.meta.json` entries that are NOT regular files (symlink/dir/special). */
|
|
67
|
+
irregularMeta?: string[];
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
function deps(
|
|
@@ -72,7 +74,13 @@ function deps(
|
|
|
72
74
|
): EntwurfFactsDeps {
|
|
73
75
|
const symlinkSet = new Set(opts.symlinks ?? []);
|
|
74
76
|
return {
|
|
75
|
-
|
|
77
|
+
// Kind-carrying entries, like the real bindings: the listing must be able to refuse a
|
|
78
|
+
// symlinked record without following it. `irregularMeta` names the ones that are not
|
|
79
|
+
// regular files.
|
|
80
|
+
metaEntries: Object.keys(meta).map((filename) => ({
|
|
81
|
+
filename,
|
|
82
|
+
regularFile: !(opts.irregularMeta ?? []).includes(filename),
|
|
83
|
+
})),
|
|
76
84
|
readRecord: (f: string) => {
|
|
77
85
|
const v = meta[f];
|
|
78
86
|
if (v === undefined) throw new Error(`ENOENT: ${f}`);
|
|
@@ -206,16 +214,16 @@ async function main(): Promise<void> {
|
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
216
|
|
|
209
|
-
// ──
|
|
210
|
-
//
|
|
217
|
+
// ── out-of-socket-domain citizen + symlinked socket (closed gap) ──────────
|
|
218
|
+
// Such a record shares its gid with a *symlinked* `*.sock`. The symlink is
|
|
211
219
|
// never probed, so its gid is absent from socketGids — the old socketGids-only
|
|
212
|
-
// check missed this and the
|
|
220
|
+
// check missed this and the out-of-domain citizen survived as a clean PeerFact while
|
|
213
221
|
// the legacy send path still followed the symlink to a forged receiver. The
|
|
214
|
-
// shared
|
|
222
|
+
// shared isOutOfSocketDomainGardenIdConflict predicate unions socketGids with the
|
|
215
223
|
// symlinked gids, so the citizen is now quarantined (gid in NEITHER peers nor
|
|
216
224
|
// socketOnly) and the conflict diagnostic is raised — alongside the existing
|
|
217
225
|
// socket-symlink-rejected one (both are honest: symlink-rejected = "not probed",
|
|
218
|
-
// conflict = "
|
|
226
|
+
// conflict = "out-of-socket-domain address split").
|
|
219
227
|
{
|
|
220
228
|
const r = await listEntwurfFacts(
|
|
221
229
|
deps(
|
|
@@ -225,7 +233,7 @@ async function main(): Promise<void> {
|
|
|
225
233
|
),
|
|
226
234
|
);
|
|
227
235
|
ok(
|
|
228
|
-
"symlink-collision:
|
|
236
|
+
"symlink-collision: out-of-socket-domain citizen quarantined (NOT in peers)",
|
|
229
237
|
!r.facts.peers.some((p) => p.gardenId === GID_CONFLICT),
|
|
230
238
|
);
|
|
231
239
|
ok(
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import assert from "node:assert/strict";
|
|
24
24
|
import {
|
|
25
25
|
type FactList,
|
|
26
|
-
|
|
26
|
+
isOutOfSocketDomainGardenIdConflict,
|
|
27
27
|
type PeerFact,
|
|
28
28
|
type RecordLessSocketFact,
|
|
29
29
|
resolveFactList,
|
|
@@ -65,7 +65,7 @@ function identity(backend: MetaCitizenBackend, over: Partial<MetaIdentity> = {})
|
|
|
65
65
|
backend,
|
|
66
66
|
nativeSessionId: "native-abc",
|
|
67
67
|
cwd: "/home/junghan/repos/gh/entwurf",
|
|
68
|
-
model: "claude-opus-
|
|
68
|
+
model: "claude-opus-5",
|
|
69
69
|
transcriptPath: "/home/junghan/.claude/projects/x/native-abc.jsonl",
|
|
70
70
|
createdAt: "2026-06-11T00:38:58.000Z",
|
|
71
71
|
recordUpdatedAt: "2026-06-11T02:40:00.000Z",
|
|
@@ -247,7 +247,7 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
|
|
|
247
247
|
} catch {
|
|
248
248
|
threw = true;
|
|
249
249
|
}
|
|
250
|
-
ok("dedup/authority:
|
|
250
|
+
ok("dedup/authority: out-of-socket-domain citizen + same-gid socket → fail-loud", threw);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// ── dedup (동결3): pi citizen + its socket → PeerFact only, never both ────────
|
|
@@ -333,11 +333,11 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
|
|
|
333
333
|
);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
// ──
|
|
336
|
+
// ── isOutOfSocketDomainGardenIdConflict: shared record-side predicate ──────
|
|
337
337
|
// (fact-provider listing + v2 decider dispatch consume this same fn — 4c 재유도
|
|
338
338
|
// 금지 동형). The union over socketGids ∪ symlinkedGardenIds is the fact-provider:125
|
|
339
339
|
// gap closure: a symlinked socket is never probed, so its gid is absent from
|
|
340
|
-
// socketGids; the predicate must STILL flag
|
|
340
|
+
// socketGids; the predicate must STILL flag an out-of-domain citizen owning it.
|
|
341
341
|
{
|
|
342
342
|
const G = "20260611T444444-dddddd";
|
|
343
343
|
const realSockets = new Set([G]);
|
|
@@ -345,21 +345,21 @@ const GID_SOCKET_ONLY = "20260611T135517-5f0d25";
|
|
|
345
345
|
const empty = new Set<string>();
|
|
346
346
|
for (const piBackend of IN_DOMAIN) {
|
|
347
347
|
ok(
|
|
348
|
-
`conflict-predicate:
|
|
349
|
-
!
|
|
348
|
+
`conflict-predicate: socket-domain (${piBackend}) is not a record-side conflict`,
|
|
349
|
+
!isOutOfSocketDomainGardenIdConflict(piBackend, G, realSockets, symlinked),
|
|
350
350
|
);
|
|
351
351
|
}
|
|
352
352
|
ok(
|
|
353
|
-
"conflict-predicate:
|
|
354
|
-
|
|
353
|
+
"conflict-predicate: out-of-socket-domain + real socket → conflict",
|
|
354
|
+
isOutOfSocketDomainGardenIdConflict("claude-code", G, realSockets, empty),
|
|
355
355
|
);
|
|
356
356
|
ok(
|
|
357
|
-
"conflict-predicate:
|
|
358
|
-
|
|
357
|
+
"conflict-predicate: out-of-socket-domain + symlinked socket → conflict",
|
|
358
|
+
isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, symlinked),
|
|
359
359
|
);
|
|
360
360
|
ok(
|
|
361
|
-
"conflict-predicate:
|
|
362
|
-
!
|
|
361
|
+
"conflict-predicate: out-of-socket-domain + no socket → no conflict",
|
|
362
|
+
!isOutOfSocketDomainGardenIdConflict("claude-code", G, empty, empty),
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
365
|
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* check-entwurf-resume-args — deterministic gate for the 5c-3b resume-argv SSOT
|
|
3
|
-
* (`buildResumePiArgs`). It pins the
|
|
4
|
-
*
|
|
3
|
+
* (`buildResumePiArgs`). It pins the RESIDENT citizen launch shape so it cannot drift.
|
|
4
|
+
* This gate once carried a second `legacy` one-shot variant as a contrast; that launcher
|
|
5
|
+
* and its enum member were removed 2026-07-27, and the half of A1 that still ships is
|
|
6
|
+
* asserted directly instead — a resumed citizen stands its control socket up and never
|
|
7
|
+
* emits the one-shot `--no-extensions`:
|
|
5
8
|
*
|
|
6
|
-
* 1. legacy carries `--no-extensions` and NO `--entwurf-control`.
|
|
7
9
|
* 2. v2-control carries `--entwurf-control` and NO `--no-extensions`.
|
|
8
|
-
* 3.
|
|
9
|
-
*
|
|
10
|
-
* 4. `explicitExtensionArgs` is preserved verbatim, exactly once
|
|
10
|
+
* 3. The headless prefix `--mode json -p` stays and the prompt is the final positional
|
|
11
|
+
* (the prompt-as-turn authority is unchanged in v2 — `-p` is NOT dropped).
|
|
12
|
+
* 4. `explicitExtensionArgs` is preserved verbatim, exactly once
|
|
11
13
|
* (load-bearing for a recorded `provider=entwurf` resume; #29 footgun).
|
|
12
|
-
* 5.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 7. no cross-contamination: the legacy-only and v2-only flags never leak into the other.
|
|
14
|
+
* 5. `plan.launchArgs` (`--approve` / empty) ride as flags BEFORE the prompt.
|
|
15
|
+
* 6. a null/undefined provider emits NO `--provider` flag; `--model <m>` and `<prompt>`
|
|
16
|
+
* are the last three tokens.
|
|
17
|
+
* 7. the resident posture is exact: `--entwurf-control` present, `--no-extensions` never.
|
|
17
18
|
*
|
|
18
19
|
* Pure string assembly — no IO, no spawn.
|
|
19
20
|
*/
|
|
@@ -45,34 +46,6 @@ function valueAfter(args: readonly string[], flag: string): string | undefined {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function main(): void {
|
|
48
|
-
// ── legacy variant ──────────────────────────────────────────────────────────────
|
|
49
|
-
{
|
|
50
|
-
const args = buildResumePiArgs({
|
|
51
|
-
variant: "legacy",
|
|
52
|
-
sessionFile: SESSION_FILE,
|
|
53
|
-
explicitExtensionArgs: EXT,
|
|
54
|
-
provider: "entwurf",
|
|
55
|
-
model: "claude-opus-4-8",
|
|
56
|
-
prompt: "continue the task",
|
|
57
|
-
});
|
|
58
|
-
ok("1 legacy has --no-extensions", args.includes("--no-extensions"));
|
|
59
|
-
ok("1 legacy has NO --entwurf-control", !args.includes("--entwurf-control"));
|
|
60
|
-
ok("3 legacy headless prefix --mode json -p", args[0] === "--mode" && args[1] === "json" && args[2] === "-p");
|
|
61
|
-
ok("3 legacy prompt is the final positional", args[args.length - 1] === "continue the task");
|
|
62
|
-
ok("4 legacy keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
|
|
63
|
-
ok("6 legacy provider laid out", valueAfter(args, "--provider") === "entwurf");
|
|
64
|
-
ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-4-8");
|
|
65
|
-
ok("6 legacy resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
|
|
66
|
-
ok("6 legacy carries NO --session-id (the id is pi's own now)", !args.includes("--session-id"));
|
|
67
|
-
// model + prompt are the last three tokens: --model <m> <prompt>
|
|
68
|
-
ok(
|
|
69
|
-
"6 legacy --model <m> <prompt> tail",
|
|
70
|
-
args[args.length - 3] === "--model" &&
|
|
71
|
-
args[args.length - 2] === "claude-opus-4-8" &&
|
|
72
|
-
args[args.length - 1] === "continue the task",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
49
|
// ── v2-control variant ──────────────────────────────────────────────────────────
|
|
77
50
|
{
|
|
78
51
|
const args = buildResumePiArgs({
|
|
@@ -80,7 +53,7 @@ function main(): void {
|
|
|
80
53
|
sessionFile: SESSION_FILE,
|
|
81
54
|
explicitExtensionArgs: EXT,
|
|
82
55
|
provider: "entwurf",
|
|
83
|
-
model: "claude-opus-
|
|
56
|
+
model: "claude-opus-5",
|
|
84
57
|
prompt: "resume now",
|
|
85
58
|
launchArgs: ["--approve"],
|
|
86
59
|
});
|
|
@@ -93,7 +66,7 @@ function main(): void {
|
|
|
93
66
|
ok("5 v2 includes launchArgs --approve", args.includes("--approve"));
|
|
94
67
|
ok("5 v2 --approve is before the prompt", args.indexOf("--approve") < args.length - 1);
|
|
95
68
|
ok("6 v2 provider laid out", valueAfter(args, "--provider") === "entwurf");
|
|
96
|
-
ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-
|
|
69
|
+
ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-5");
|
|
97
70
|
ok("6 v2 resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
|
|
98
71
|
ok(
|
|
99
72
|
"6 v2 carries NO --session-id (a garden id would MINT a session, not resume one)",
|
|
@@ -102,25 +75,11 @@ function main(): void {
|
|
|
102
75
|
ok(
|
|
103
76
|
"6 v2 --model <m> <prompt> tail",
|
|
104
77
|
args[args.length - 3] === "--model" &&
|
|
105
|
-
args[args.length - 2] === "claude-opus-
|
|
78
|
+
args[args.length - 2] === "claude-opus-5" &&
|
|
106
79
|
args[args.length - 1] === "resume now",
|
|
107
80
|
);
|
|
108
81
|
}
|
|
109
82
|
|
|
110
|
-
// ── 5. legacy IGNORES launchArgs (no --approve leaks in) ──────────────────────────
|
|
111
|
-
{
|
|
112
|
-
const args = buildResumePiArgs({
|
|
113
|
-
variant: "legacy",
|
|
114
|
-
sessionFile: SESSION_FILE,
|
|
115
|
-
explicitExtensionArgs: [],
|
|
116
|
-
provider: null,
|
|
117
|
-
model: "m",
|
|
118
|
-
prompt: "p",
|
|
119
|
-
launchArgs: ["--approve"], // present in input but legacy must ignore it
|
|
120
|
-
});
|
|
121
|
-
ok("5 legacy ignores launchArgs (no --approve)", !args.includes("--approve"));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
83
|
// ── 6. null/undefined provider emits NO --provider flag ───────────────────────────
|
|
125
84
|
for (const provider of [null, undefined] as const) {
|
|
126
85
|
const args = buildResumePiArgs({
|
|
@@ -136,20 +95,23 @@ function main(): void {
|
|
|
136
95
|
ok(`6 provider=${provider}: --model <m> <prompt> still tail`, args.slice(-3).join(" ") === "--model m p");
|
|
137
96
|
}
|
|
138
97
|
|
|
139
|
-
// ── 7.
|
|
98
|
+
// ── 7. the resident posture is exact: the one-shot flag never reappears ───────────
|
|
99
|
+
// The `legacy` variant was removed 2026-07-27, so the old cross-contamination pair is
|
|
100
|
+
// gone. What must NOT rot is the A1 half that still ships: a resumed citizen stands its
|
|
101
|
+
// control socket up, which means `--no-extensions` (the one-shot flag that let `pi -p`
|
|
102
|
+
// exit) can never be emitted here again.
|
|
140
103
|
{
|
|
141
|
-
const
|
|
104
|
+
const v2 = buildResumePiArgs({
|
|
142
105
|
sessionFile: SESSION_FILE,
|
|
143
106
|
explicitExtensionArgs: EXT,
|
|
144
107
|
provider: "entwurf",
|
|
145
108
|
model: "m",
|
|
146
109
|
prompt: "p",
|
|
147
110
|
launchArgs: ["--approve"],
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ok("7
|
|
152
|
-
ok("7 v2-only flag absent from legacy", !legacy.includes("--entwurf-control") && !legacy.includes("--approve"));
|
|
111
|
+
variant: "v2-control",
|
|
112
|
+
});
|
|
113
|
+
ok("7 resident posture never emits the one-shot --no-extensions", !v2.includes("--no-extensions"));
|
|
114
|
+
ok("7 resident posture stands the control socket up", v2.includes("--entwurf-control"));
|
|
153
115
|
}
|
|
154
116
|
|
|
155
117
|
console.log(`\ncheck-entwurf-resume-args: ${passed} checks passed`);
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Proves:
|
|
9
9
|
* - PURE truth table (computeSelfAddressability, facts injected): pi replyable ⟺
|
|
10
|
-
* socketAlive;
|
|
11
|
-
*
|
|
10
|
+
* socketAlive; external never replyable; and meta replyability splits by RAIL —
|
|
11
|
+
* self-fetch ⟺ recordBacked ∧ ownerAlive ∧ watchArmed, native-push ⟺ recordBacked ∧
|
|
12
|
+
* probeAlive (a separate axis: an agy citizen never arms a mailbox watch, so the
|
|
13
|
+
* self-fetch atom would make it un-replyable forever), and an unsupplied rail is
|
|
14
|
+
* fail-closed. socketState alive/expected/none is its own assertable field.
|
|
12
15
|
* - The two REGRESSION-PROOF rows the lock requires (record-present, not all-absent):
|
|
13
16
|
* (b) meta record present + owner-dead (start-key mismatch) → false
|
|
14
17
|
* (c) meta record present + watch-unarmed → false
|
|
@@ -147,7 +150,7 @@ function row(facts: SelfAddressabilityFacts): { replyable: boolean; socketState:
|
|
|
147
150
|
|
|
148
151
|
// A native-push citizen must NOT be able to buy replyability with mailbox facts.
|
|
149
152
|
ok(
|
|
150
|
-
"meta/native-push + owner-alive + watch-armed but probe-dead → NOT replyable (mailbox facts cannot rescue it)",
|
|
153
|
+
"meta/native-push + owner-alive + watch-armed but probe-dead → NOT replyable (mailbox facts cannot rescue it) [QK:SELFADDR-RAIL-FACT-LEAK]",
|
|
151
154
|
row(push({ recordBacked: true, ownerAlive: true, watchArmed: true, probeAlive: false })).replyable === false,
|
|
152
155
|
);
|
|
153
156
|
}
|
|
@@ -223,7 +226,10 @@ const piBody = functionBody("buildStrictPiSenderEnvelope");
|
|
|
223
226
|
ok("buildStrictPiSenderEnvelope calls computeSelfAddressability", /computeSelfAddressability\s*\(/.test(piBody));
|
|
224
227
|
// Scoped to THIS function body (not a broad grep): the pi-session envelope must NOT
|
|
225
228
|
// hardcode `replyable: true`; it must derive from the predicate result.
|
|
226
|
-
ok(
|
|
229
|
+
ok(
|
|
230
|
+
"buildStrictPiSenderEnvelope no longer hardcodes `replyable: true` [QK:SELFADDR-NO-HARDCODED-REPLYABLE]",
|
|
231
|
+
!/replyable:\s*true/.test(piBody),
|
|
232
|
+
);
|
|
227
233
|
// existsSync alone is too loose — pin that it probes the CANONICAL socket path,
|
|
228
234
|
// not some other file, so the honesty signal cannot drift to a path that does not
|
|
229
235
|
// represent this session's socket.
|
|
@@ -246,6 +252,183 @@ ok(
|
|
|
246
252
|
/existsSync\s*\(/.test(selfRegion),
|
|
247
253
|
);
|
|
248
254
|
|
|
255
|
+
// ── SLICE INTEGRITY (before any claim is read off the slice) ─────────────────
|
|
256
|
+
// A source-slice assertion is only as good as its boundaries. `toolRegion` counts
|
|
257
|
+
// parens, so an unbalanced paren inside a string literal would silently widen the
|
|
258
|
+
// slice to a LATER tool and let a neighbour's text satisfy an assertion about this
|
|
259
|
+
// one. Pin both ends explicitly instead of trusting the counter: the slice must
|
|
260
|
+
// contain THIS tool's own error label and must NOT reach the next `server.tool(`.
|
|
261
|
+
ok("entwurf_self slice contains its own handler tail (start boundary real)", /entwurf_self error:/.test(selfRegion));
|
|
262
|
+
ok(
|
|
263
|
+
"entwurf_self slice stops before the next tool (end boundary real, no silent widen)",
|
|
264
|
+
!/entwurf_peers/.test(selfRegion) && !/entwurf_inbox_read/.test(selfRegion),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// ── F-1: entwurf_self must render the RAIL, never a universal mailbox ────────
|
|
268
|
+
// The defect: `origin === "meta-session"` unconditionally synthesized
|
|
269
|
+
// `<mailboxDir>/<gardenId>`. `origin` is sender PROVENANCE; the rail is the second
|
|
270
|
+
// axis. A native-push citizen (antigravity) has NO mailbox at all (AGENTS Hard Rule
|
|
271
|
+
// 10, VERIFY "No mailbox/receiver-marker evidence counts on this rail"), so that
|
|
272
|
+
// branch printed a path that will never exist and taught the model mailbox semantics
|
|
273
|
+
// its own rail does not have. Pinned PER RAIL, as one reusable predicate so the same
|
|
274
|
+
// judgement can be driven over negative controls below.
|
|
275
|
+
/** Brace-balanced body of the `if (<guard>) { … }` that follows `guard`, or null. */
|
|
276
|
+
function branchBody(region: string, guard: string): string | null {
|
|
277
|
+
const at = region.indexOf(guard);
|
|
278
|
+
if (at < 0) return null;
|
|
279
|
+
const open = region.indexOf("{", at);
|
|
280
|
+
if (open < 0) return null;
|
|
281
|
+
let depth = 0;
|
|
282
|
+
for (let i = open; i < region.length; i++) {
|
|
283
|
+
const c = region[i];
|
|
284
|
+
if (c === "{") depth++;
|
|
285
|
+
else if (c === "}") {
|
|
286
|
+
depth--;
|
|
287
|
+
if (depth === 0) return region.slice(open, i + 1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function metaRailRenderIsHonest(region: string): { honest: boolean; reason: string } {
|
|
294
|
+
// The mailbox path may be built at most once in the whole render branch…
|
|
295
|
+
const mailboxCalls = region.split("defaultMetaMailboxDir(").length - 1;
|
|
296
|
+
if (mailboxCalls !== 1) {
|
|
297
|
+
return { honest: false, reason: `expected exactly 1 defaultMetaMailboxDir() call, found ${mailboxCalls}` };
|
|
298
|
+
}
|
|
299
|
+
// …and that one call must sit INSIDE the self-fetch branch body. Ordering alone is not
|
|
300
|
+
// containment: an `if (rail === "self-fetch") {}` with an EMPTY body followed by an
|
|
301
|
+
// unconditional mailbox build satisfies "guard appears before mailbox" while still
|
|
302
|
+
// synthesizing a mailbox for every rail — the exact defect this predicate exists to
|
|
303
|
+
// reject. Extract the body by brace-counting and require the call to be in it.
|
|
304
|
+
const selfFetchBody = branchBody(region, 'rail === "self-fetch"');
|
|
305
|
+
if (selfFetchBody === null) {
|
|
306
|
+
return { honest: false, reason: 'no brace-balanced `rail === "self-fetch"` branch body' };
|
|
307
|
+
}
|
|
308
|
+
const inBranch = selfFetchBody.split("defaultMetaMailboxDir(").length - 1;
|
|
309
|
+
if (inBranch !== 1) {
|
|
310
|
+
return {
|
|
311
|
+
honest: false,
|
|
312
|
+
reason: `the self-fetch branch BODY must build the mailbox path exactly once, found ${inBranch} (ordering is not containment)`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const pushBody = branchBody(region, 'rail === "native-push"');
|
|
316
|
+
if (pushBody === null) {
|
|
317
|
+
return { honest: false, reason: 'no brace-balanced `rail === "native-push"` branch body' };
|
|
318
|
+
}
|
|
319
|
+
// The native-push branch must deny an inbox AND keep the direct-inject claim conditional
|
|
320
|
+
// on the probe: a dead probe has no live conversation to inject into, so an unconditional
|
|
321
|
+
// "injects a reply into this live conversation" invents the very rail F-1 is about.
|
|
322
|
+
if (!/no inbox/.test(pushBody)) {
|
|
323
|
+
return { honest: false, reason: "the native-push branch does not state that there is no inbox" };
|
|
324
|
+
}
|
|
325
|
+
if (!/while the adapter probe is alive/.test(pushBody)) {
|
|
326
|
+
return {
|
|
327
|
+
honest: false,
|
|
328
|
+
reason: "the native-push branch states direct injection unconditionally (a dead probe has no live conversation)",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
honest: true,
|
|
333
|
+
reason:
|
|
334
|
+
"the sole mailbox build is INSIDE the self-fetch branch body; native-push denies an inbox and gates injection on the probe",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
{
|
|
339
|
+
const verdict = metaRailRenderIsHonest(selfRegion);
|
|
340
|
+
ok(`entwurf_self renders the meta rail honestly [QK:SELFADDR-RAIL-RENDER] (${verdict.reason})`, verdict.honest);
|
|
341
|
+
ok(
|
|
342
|
+
"entwurf_self reads the rail from the builder (not re-derived at the render site)",
|
|
343
|
+
/self\.metaDeliveryDomain/.test(selfRegion),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// NEGATIVE CONTROLS — two KINDS, because a gate that only rejects the exact old
|
|
347
|
+
// sentence would pass a version where the branch is simply gone (and vice versa).
|
|
348
|
+
// Driving the same predicate over synthetic sources is what proves this assertion
|
|
349
|
+
// BLOCKS, not merely that it exists.
|
|
350
|
+
const retiredUnconditional = [
|
|
351
|
+
'} else if (sender.origin === "meta-session") {',
|
|
352
|
+
"\tconst mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
|
|
353
|
+
"\textra.mailboxPath = mailboxPath;",
|
|
354
|
+
"}",
|
|
355
|
+
].join("\n");
|
|
356
|
+
ok(
|
|
357
|
+
"NEGATIVE 1/3 (exact false sentence): the retired unconditional meta-session mailbox is REJECTED",
|
|
358
|
+
metaRailRenderIsHonest(retiredUnconditional).honest === false,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const selfFetchOnlyNoPushBranch = [
|
|
362
|
+
"const rail = self.metaDeliveryDomain;",
|
|
363
|
+
'if (rail === "self-fetch") {',
|
|
364
|
+
"\tconst mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
|
|
365
|
+
"\textra.mailboxPath = mailboxPath;",
|
|
366
|
+
"}",
|
|
367
|
+
].join("\n");
|
|
368
|
+
ok(
|
|
369
|
+
"NEGATIVE 2/3 (omission): a self-fetch-guarded render with the native-push branch DELETED is REJECTED",
|
|
370
|
+
metaRailRenderIsHonest(selfFetchOnlyNoPushBranch).honest === false,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// NEGATIVE 3/3 — the form the FIRST version of this predicate wrongly accepted (caught in
|
|
374
|
+
// cross-review, 2026-07-27): every marker present, guard textually BEFORE the mailbox, yet
|
|
375
|
+
// the guard's body is empty and the mailbox is built unconditionally. Ordering satisfied,
|
|
376
|
+
// containment violated. This row is why the predicate brace-counts the branch body.
|
|
377
|
+
const guardedInNameOnly = [
|
|
378
|
+
"const rail = self.metaDeliveryDomain;",
|
|
379
|
+
'if (rail === "self-fetch") { }',
|
|
380
|
+
"const mailboxPath = path.join(defaultMetaMailboxDir(), sender.sessionId);",
|
|
381
|
+
"extra.mailboxPath = mailboxPath;",
|
|
382
|
+
'if (rail === "native-push") {',
|
|
383
|
+
'\tlines.push("mailbox: none — no inbox; direct-inject only while the adapter probe is alive");',
|
|
384
|
+
"}",
|
|
385
|
+
].join("\n");
|
|
386
|
+
ok(
|
|
387
|
+
"NEGATIVE 3/3 (ordering-but-not-containment): an EMPTY self-fetch guard followed by an unconditional mailbox build is REJECTED",
|
|
388
|
+
metaRailRenderIsHonest(guardedInNameOnly).honest === false,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── F-1: the identity-wiring errors must not teach one backend's hook as THE hook ──
|
|
393
|
+
// Both native backends mint a garden id from their OWN hook (Claude Code SessionStart,
|
|
394
|
+
// Antigravity PreInvocation). Naming only SessionStart/Claude sent an agy operator
|
|
395
|
+
// looking for a hook its backend never runs.
|
|
396
|
+
{
|
|
397
|
+
/** Extract a `class NAME extends Error { ... }` body by brace-counting. */
|
|
398
|
+
const classBody = (name: string): string => {
|
|
399
|
+
const sig = `class ${name} extends Error {`;
|
|
400
|
+
const at = src.indexOf(sig);
|
|
401
|
+
assert.ok(at >= 0, `${name} present in MCP source`);
|
|
402
|
+
const open = src.indexOf("{", at);
|
|
403
|
+
let depth = 0;
|
|
404
|
+
for (let i = open; i < src.length; i++) {
|
|
405
|
+
const c = src[i];
|
|
406
|
+
if (c === "{") depth++;
|
|
407
|
+
else if (c === "}") {
|
|
408
|
+
depth--;
|
|
409
|
+
if (depth === 0) return src.slice(open, i + 1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
throw new Error(`class ${name} body never closed`);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Anchored per class (not a repo-wide grep) so a mention somewhere else in the file
|
|
416
|
+
// cannot make either row vacuously green.
|
|
417
|
+
for (const cls of ["EntwurfEnvelopeWiringError", "EntwurfSenderIdentityError"]) {
|
|
418
|
+
const body = classBody(cls);
|
|
419
|
+
ok(
|
|
420
|
+
`${cls} names BOTH native hooks (SessionStart AND PreInvocation)`,
|
|
421
|
+
/SessionStart/.test(body) && /PreInvocation/.test(body),
|
|
422
|
+
);
|
|
423
|
+
ok(
|
|
424
|
+
`${cls} no longer presents SessionStart/Claude as THE marker writer`,
|
|
425
|
+
!/The native SessionStart hook writes that marker/.test(body) &&
|
|
426
|
+
!/whose SessionStart hook wrote a live/.test(body) &&
|
|
427
|
+
!/keyed by the\s+Claude Code parent pid/.test(body),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
249
432
|
// ── SE-2 2e-b: meta-session sender replyability from the receiver presence marker ──
|
|
250
433
|
// Identity stays trusted (record-backed), but `replyable` is now derived from whether
|
|
251
434
|
// THIS session's own receiver inbox can actually wake (slice-2 presence marker), not a
|