@junghanacs/entwurf 0.17.1 → 0.18.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.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +248 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +24 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +61 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
- package/mcp/entwurf-bridge/src/index.ts +16 -8
- package/package.json +3 -2
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
- package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
- package/pi-extensions/lib/entwurf-facts.ts +52 -3
- package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
- package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +24 -3
- package/pi-extensions/lib/meta-session.ts +76 -7
- package/pi-extensions/meta-bridge-hook.ts +72 -3
- package/run.sh +21 -0
- package/scripts/check-bridge-delivery.ts +42 -1
- package/scripts/check-entwurf-fact-provider.ts +22 -1
- package/scripts/check-entwurf-facts.ts +17 -2
- package/scripts/check-entwurf-peers-surface.ts +20 -2
- package/scripts/check-entwurf-self-address.ts +20 -2
- package/scripts/check-entwurf-v2-decider.ts +6 -3
- package/scripts/check-entwurf-v2-mailbox.ts +33 -0
- package/scripts/check-entwurf-v2-matrix.ts +10 -5
- package/scripts/check-entwurf-v2-production.ts +72 -1
- package/scripts/check-entwurf-v2-runner.ts +18 -0
- package/scripts/check-entwurf-v2-send.ts +13 -1
- package/scripts/check-entwurf-v2-surface.ts +75 -3
- package/scripts/check-gate-qualification.ts +1 -0
- package/scripts/check-hook-launch-topology.ts +70 -1
- package/scripts/check-mailbox-receipt-state.ts +6 -0
- package/scripts/check-meta-doctor-oracle.sh +73 -0
- package/scripts/check-meta-hook-session-switch.ts +560 -0
- package/scripts/check-meta-mailbox-state-write.ts +9 -2
- package/scripts/check-meta-manifest-schema.py +19 -1
- package/scripts/check-meta-session.ts +10 -2
- package/scripts/meta-bridge-doctor.sh +47 -7
- package/scripts/meta-bridge-state.py +23 -5
- package/scripts/meta-bridge-statusline.sh +65 -2
- package/scripts/mutants/meta-hook-session-switch.json +223 -0
- package/scripts/raw-async-delivery/README.md +280 -9
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
- package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
- package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
- package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
- package/scripts/raw-claude-session-switch/README.md +170 -0
- package/scripts/smoke-acp-cortex-live.ts +22 -2
- package/scripts/smoke-acp-v2-send-live.ts +23 -2
- package/scripts/smoke-entwurf-chain-live.ts +85 -13
- package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
- package/scripts/smoke-meta-async-drift.sh +31 -8
- package/scripts/smoke-meta-install-state.sh +170 -11
- package/scripts/smoke-meta-keyset-guard.sh +4 -1
- package/scripts/smoke-mux-fresh-call-live.ts +13 -3
|
@@ -36,16 +36,23 @@ import { enqueueMetaMessage } from "./meta-session.js";
|
|
|
36
36
|
* legacy mailbox path used). `plan.wantsReply` is threaded into the body — v2 carries the
|
|
37
37
|
* caller's intent, a DELIBERATE divergence from the legacy hard-coded `false`. An enqueue
|
|
38
38
|
* throw PROPAGATES; it is never converted to `{success:false}`.
|
|
39
|
+
*
|
|
40
|
+
* #98 R — the SEND receipt: `enqueueMetaMessage` already returns the exact `.msg` path it
|
|
41
|
+
* wrote ("Returns the paths so a sender can show exactly what was queued"), and this body
|
|
42
|
+
* used to flatten it to `{success:true}`. It now carries that ONE field through. Nothing
|
|
43
|
+
* else from the enqueue result crosses: no timestamps, and specifically no `lastReadAt` —
|
|
44
|
+
* see the `RpcSendResult` note. The path is reported verbatim from the enqueue result, not
|
|
45
|
+
* re-derived from the plan, so it can never disagree with the file that was actually written.
|
|
39
46
|
*/
|
|
40
47
|
export function executeMetaMailboxSend(plan, sender, deps) {
|
|
41
48
|
const body = sender ? formatMetaMailboxBody(sender, plan.message, plan.wantsReply) : plan.message;
|
|
42
|
-
deps.enqueue({
|
|
49
|
+
const enqueued = deps.enqueue({
|
|
43
50
|
gardenId: plan.targetGardenId,
|
|
44
51
|
body,
|
|
45
52
|
sessionsDir: plan.sessionsDir,
|
|
46
53
|
mailboxDir: plan.mailboxDir,
|
|
47
54
|
});
|
|
48
|
-
return { success: true };
|
|
55
|
+
return { success: true, messagePath: enqueued.messagePath };
|
|
49
56
|
}
|
|
50
57
|
/**
|
|
51
58
|
* Build the production `sendViaMailbox(plan, lock)` adapter the 5c-2a send hand consumes.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* claiming "no conflict". The decider's later `inspectSocket` probe is a SEPARATE step.
|
|
30
30
|
*/
|
|
31
31
|
import { sendRpcCommand as realSendRpc, } from "./entwurf-control-rpc.js";
|
|
32
|
-
import { mailboxConversationalDeliverable,
|
|
32
|
+
import { mailboxConversationalDeliverable, resolveMailboxReceiverFacts, } from "./entwurf-deliverability.js";
|
|
33
33
|
import { isOutOfSocketDomainGardenIdConflict } from "./entwurf-facts.js";
|
|
34
34
|
import { isLivenessSupported } from "./entwurf-v2-contract.js";
|
|
35
35
|
import { decideDispatch, } from "./entwurf-v2-decider.js";
|
|
@@ -38,7 +38,7 @@ import { makeProductionSendViaMailbox } from "./entwurf-v2-mailbox.js";
|
|
|
38
38
|
import { makeNativePushSend } from "./entwurf-v2-native-push.js";
|
|
39
39
|
import { executeControlSocketSend, } from "./entwurf-v2-send.js";
|
|
40
40
|
import { resolveDeadControlSendFallback } from "./entwurf-v2-send-fallback.js";
|
|
41
|
-
import { defaultMetaMailboxDir, defaultMetaSessionsDir, enqueueMetaMessage, metaCapabilityFor, metaRecordExistsByGardenId, readAddressableMetaIdentity, readMetaReceiverMarker, } from "./meta-session.js";
|
|
41
|
+
import { defaultMetaMailboxDir, defaultMetaSessionsDir, enqueueMetaMessage, metaCapabilityFor, metaRecordExistsByGardenId, readAddressableMetaIdentity, readMetaReceiverMarker, readMetaSenderMarker, requireBackend, } from "./meta-session.js";
|
|
42
42
|
import { resolveNativePushAdapter as realResolveNativePushAdapter, } from "./native-push/adapter.js";
|
|
43
43
|
import { CONTROL_SOCKET_DIR, controlSocketPath, inspectControlSocketPath, inspectTargetControlSocket, isRecordLessSocketCandidate, } from "./socket-discovery.js";
|
|
44
44
|
import { classifyConnectError, probeSocketLiveness } from "./socket-probe.js";
|
|
@@ -77,6 +77,8 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
77
77
|
metaRecordExists: s.metaRecordExists ?? metaRecordExistsByGardenId,
|
|
78
78
|
readIdentity: s.readIdentity ?? readAddressableMetaIdentity,
|
|
79
79
|
readReceiverMarker: s.readReceiverMarker ?? ((gid) => readMetaReceiverMarker({ gardenId: gid })),
|
|
80
|
+
readSenderMarker: s.readSenderMarker ??
|
|
81
|
+
((backend, ownerPid) => readMetaSenderMarker({ backend: requireBackend(backend), ownerPid })),
|
|
80
82
|
inspectPath: s.inspectPath ?? inspectControlSocketPath,
|
|
81
83
|
acquireLock: s.acquireLock ?? realAcquireLock,
|
|
82
84
|
releaseLock: s.releaseLock ?? realReleaseLock,
|
|
@@ -98,22 +100,32 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
98
100
|
enqueue: io.enqueue,
|
|
99
101
|
});
|
|
100
102
|
// ── ONE deliverability seam (SE-2 2d-3): wake-mode capability AND a live active-
|
|
101
|
-
// receiver
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
103
|
+
// receiver. The SAME closure is injected into the decider AND the dead-fallback, so a
|
|
104
|
+
// direct send and a re-resolved fallback send can never drift to different deliverability
|
|
105
|
+
// verdicts. recordBacked is true by construction — resolveTarget already proved the record
|
|
106
|
+
// exists before any unsupported-backend mailbox route, and the closure is only consulted on
|
|
107
|
+
// that route. A null / dead-owner / identity-mismatched marker is fail-closed to inactive
|
|
108
|
+
// (SE-2): a reply to a terminated self-fetch citizen is rejected, not enqueued as mailbox
|
|
109
|
+
// garbage.
|
|
110
|
+
//
|
|
111
|
+
// The two receiver facts come from the SHARED composition (#101 결함 B), never from one
|
|
112
|
+
// value copied into both slots. `ownerAlive` is the marker↔identity match on a live owner;
|
|
113
|
+
// `watchArmed` is the separate measurement that this owner is STILL serving this garden.
|
|
114
|
+
// They were the same expression until a Claude session switch inside one pid left a retired
|
|
115
|
+
// garden's marker reading as an armed doorbell and a real message rotted unread in its
|
|
116
|
+
// mailbox. `entwurf_self` calls the same composition, so a citizen's own replyability can
|
|
117
|
+
// never disagree with what dispatch decided about it. ──────────
|
|
108
118
|
const mailboxDeliverabilityFor = (identity) => {
|
|
109
119
|
const wakeMode = metaCapabilityFor(identity.backend).wakeMode;
|
|
110
|
-
const
|
|
111
|
-
|
|
120
|
+
const { ownerAlive, watchArmed } = resolveMailboxReceiverFacts(identity, {
|
|
121
|
+
readReceiverMarker: io.readReceiverMarker,
|
|
122
|
+
readSenderMarker: io.readSenderMarker,
|
|
123
|
+
});
|
|
112
124
|
return mailboxConversationalDeliverable({
|
|
113
125
|
wakeMode,
|
|
114
126
|
recordBacked: true,
|
|
115
|
-
ownerAlive
|
|
116
|
-
watchArmed
|
|
127
|
+
ownerAlive,
|
|
128
|
+
watchArmed,
|
|
117
129
|
});
|
|
118
130
|
};
|
|
119
131
|
// ── target resolution (QB1 + QB2) ─────────────────────────────────────────
|
|
@@ -49,7 +49,12 @@ export async function executeDispatch(decision, deps) {
|
|
|
49
49
|
kind: "executed",
|
|
50
50
|
receipt,
|
|
51
51
|
transport,
|
|
52
|
-
outcome: {
|
|
52
|
+
outcome: {
|
|
53
|
+
transport: "control-socket",
|
|
54
|
+
outcome: r.outcome,
|
|
55
|
+
rejectReason: r.rejectReason,
|
|
56
|
+
messagePath: r.messagePath,
|
|
57
|
+
},
|
|
53
58
|
};
|
|
54
59
|
}
|
|
55
60
|
catch (err) {
|
|
@@ -80,7 +85,14 @@ export async function executeDispatch(decision, deps) {
|
|
|
80
85
|
if (r.success !== true) {
|
|
81
86
|
throw new Error("entwurf-v2-runner: meta-mailbox send returned success:false (contract violation; a mailbox has no in-band reject).");
|
|
82
87
|
}
|
|
83
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
kind: "executed",
|
|
90
|
+
receipt,
|
|
91
|
+
transport,
|
|
92
|
+
// #98 R: carry the enqueue receipt verbatim. `undefined` stays `undefined` —
|
|
93
|
+
// the runner never substitutes a guessed path for a missing one.
|
|
94
|
+
outcome: { transport: "meta-mailbox", success: true, messagePath: r.messagePath },
|
|
95
|
+
};
|
|
84
96
|
}
|
|
85
97
|
catch (err) {
|
|
86
98
|
return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
|
|
@@ -58,7 +58,7 @@ export async function executeControlSocketSend(plan, lock, deps) {
|
|
|
58
58
|
drive = { outcome: "failed", error: err };
|
|
59
59
|
}
|
|
60
60
|
finalizeRelease(policy, deps, held, drive);
|
|
61
|
-
return { outcome: drive.outcome, rejectReason: drive.rejectReason };
|
|
61
|
+
return { outcome: drive.outcome, rejectReason: drive.rejectReason, messagePath: drive.messagePath };
|
|
62
62
|
}
|
|
63
63
|
/** Drive the 1차 send and route a connect failure through the F3 split. */
|
|
64
64
|
async function driveSend(plan, lock, deps) {
|
|
@@ -122,7 +122,10 @@ async function driveDeadFallback(plan, lock, deps) {
|
|
|
122
122
|
// hand never reaches for the mailbox on its own; only the resolver routes here.
|
|
123
123
|
try {
|
|
124
124
|
const r = await deps.sendViaMailbox(rePlan, lock);
|
|
125
|
-
|
|
125
|
+
// #98 R: this leg writes a `.msg` exactly like the primary mailbox rail, so it
|
|
126
|
+
// owes the sender the same per-message receipt. Carried only on success — a
|
|
127
|
+
// `rejected` enqueue wrote no file to name.
|
|
128
|
+
return r.success ? { outcome: "fallback-sent", messagePath: r.messagePath } : { outcome: "rejected" };
|
|
126
129
|
}
|
|
127
130
|
catch (err) {
|
|
128
131
|
return { outcome: "failed", error: err };
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* - `runAndRenderEntwurfV2FromSurface` — assemble production deps + run + render, so the root
|
|
19
19
|
* surface never needs to name the `EntwurfV2RunResult` union (it only sees `{text,isError}`).
|
|
20
20
|
*/
|
|
21
|
+
import { basename } from "node:path";
|
|
21
22
|
import { makeProductionEntwurfV2Deps } from "./entwurf-v2-production.js";
|
|
22
23
|
import { runEntwurfV2 } from "./entwurf-v2-runner.js";
|
|
23
24
|
import { FRESH_CUT_PRESCRIPTION } from "./meta-session.js";
|
|
@@ -102,6 +103,12 @@ export function renderEntwurfV2Result(result) {
|
|
|
102
103
|
`\n ${c.detail}` +
|
|
103
104
|
(c.holder ? `\n holder: pid ${c.holder.pid} on ${c.holder.hostname} since ${c.holder.createdAt}` : "");
|
|
104
105
|
}
|
|
106
|
+
// #101 갭 C: name WHICH receiver axis failed. "mailbox-undeliverable" alone sent a
|
|
107
|
+
// caller looking for a dead session when the session was alive and had simply
|
|
108
|
+
// switched to another garden — the predicate knew that and the surface threw it away.
|
|
109
|
+
if (result.diagnostic?.kind === "mailbox-undeliverable") {
|
|
110
|
+
text += `\n mailbox-undeliverable: ${result.diagnostic.reason}`;
|
|
111
|
+
}
|
|
105
112
|
return { text, isError: true };
|
|
106
113
|
}
|
|
107
114
|
case "executed": {
|
|
@@ -109,8 +116,13 @@ export function renderEntwurfV2Result(result) {
|
|
|
109
116
|
if (o.transport === "control-socket") {
|
|
110
117
|
const delivered = o.outcome === "sent" || o.outcome === "fallback-sent";
|
|
111
118
|
const reason = o.rejectReason ? ` (reason: ${o.rejectReason})` : "";
|
|
119
|
+
// #98 R, fallback leg: a dead socket that re-resolved to the mailbox wrote a
|
|
120
|
+
// `.msg` — name it, exactly as the primary mailbox rail does. Absent on a
|
|
121
|
+
// socket-to-socket retry (no file) and on every non-mailbox outcome, so the
|
|
122
|
+
// line degrades to the bare outcome rather than printing "undefined".
|
|
123
|
+
const enqueued = o.messagePath ? ` (enqueued ${basename(o.messagePath)})` : "";
|
|
112
124
|
return {
|
|
113
|
-
text: `entwurf_v2 control-socket → ${o.outcome}${reason}`,
|
|
125
|
+
text: `entwurf_v2 control-socket → ${o.outcome}${reason}${enqueued}`,
|
|
114
126
|
isError: !delivered,
|
|
115
127
|
};
|
|
116
128
|
}
|
|
@@ -121,8 +133,17 @@ export function renderEntwurfV2Result(result) {
|
|
|
121
133
|
isError: false,
|
|
122
134
|
};
|
|
123
135
|
}
|
|
124
|
-
// meta-mailbox
|
|
125
|
-
|
|
136
|
+
// meta-mailbox. #98 R: name the FILE that was enqueued, so the sender's transcript
|
|
137
|
+
// carries a per-message identifier instead of a bare literal. Only the basename —
|
|
138
|
+
// the directory is `<meta-mailbox>/<target garden id>/`, which the caller already
|
|
139
|
+
// typed. Deliberately NOT a read stamp: at enqueue time `lastReadAt` belongs to the
|
|
140
|
+
// PREVIOUS message, so printing it would claim a read that has not happened.
|
|
141
|
+
// A dep that omits the receipt falls back to the old literal rather than printing
|
|
142
|
+
// "undefined" — the delivery still happened.
|
|
143
|
+
return {
|
|
144
|
+
text: `entwurf_v2 meta-mailbox → enqueued${o.messagePath ? ` (${basename(o.messagePath)})` : ""}`,
|
|
145
|
+
isError: false,
|
|
146
|
+
};
|
|
126
147
|
}
|
|
127
148
|
case "execution-failed": {
|
|
128
149
|
if (result.releaseFailed && result.finalizedOutcome) {
|
|
@@ -1661,6 +1661,44 @@ export function readMetaReceiverMarker(opts) {
|
|
|
1661
1661
|
return null;
|
|
1662
1662
|
}
|
|
1663
1663
|
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Retire a receiver presence marker THIS owner pid wrote (#101 결함 A).
|
|
1666
|
+
*
|
|
1667
|
+
* A native process that switches sessions in place — an in-session `/resume` or `/clear`
|
|
1668
|
+
* fires a second SessionStart inside the same pid under a new native session id — leaves
|
|
1669
|
+
* the previous garden's marker behind, naming a live owner whose watch is gone. Measured
|
|
1670
|
+
* on oracle 2026-09-04: one pid (143742) held two gardens' markers four seconds apart, and
|
|
1671
|
+
* mail sent to the retired one sat unread.
|
|
1672
|
+
*
|
|
1673
|
+
* ONLY THE MARKER. The meta-record stays: records are identity, and deleting one
|
|
1674
|
+
* outside `meta-bridge-fresh-cut` would fight store certification (Hard Rule 7/8).
|
|
1675
|
+
* A retired citizen keeps its record, its transcript and its listing — it loses only
|
|
1676
|
+
* the claim that a doorbell is armed for it, which is exactly the claim that stopped
|
|
1677
|
+
* being true.
|
|
1678
|
+
*
|
|
1679
|
+
* Fail-closed the other way too: the marker is read back first and removed ONLY when its
|
|
1680
|
+
* `ownerPid` field equals the one passed here — pid equality, which is what a caller can
|
|
1681
|
+
* actually check; it does not prove the same PROCESS wrote it (a reused pid compares equal),
|
|
1682
|
+
* and the reader-side start-key guard is where that finer distinction lives. It is enough for
|
|
1683
|
+
* the rule this serves: a hook never retires a watch some other pid is holding. Returns whether a marker was actually removed. Never throws — a retirement
|
|
1684
|
+
* that cannot happen is a log line, not a broken session start.
|
|
1685
|
+
*/
|
|
1686
|
+
export function removeMetaReceiverMarker(opts) {
|
|
1687
|
+
try {
|
|
1688
|
+
const receiversDir = opts.receiversDir ?? defaultMetaReceiversDir();
|
|
1689
|
+
const file = metaReceiverMarkerPath(opts.gardenId, receiversDir);
|
|
1690
|
+
// verifyOwner:false — the point is the OWNERSHIP field, not the owner's liveness;
|
|
1691
|
+
// a marker whose owner already died is still this pid's to clean up when it names it.
|
|
1692
|
+
const marker = readMetaReceiverMarker({ markerPath: file, verifyOwner: false });
|
|
1693
|
+
if (!marker || marker.ownerPid !== opts.ownerPid)
|
|
1694
|
+
return false;
|
|
1695
|
+
fs.unlinkSync(file);
|
|
1696
|
+
return true;
|
|
1697
|
+
}
|
|
1698
|
+
catch {
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1664
1702
|
/**
|
|
1665
1703
|
* Idempotent fs upsert (writes v3 identity). CERTIFY the whole active store, then
|
|
1666
1704
|
* decide create-vs-attach on record EXISTENCE and write atomically as v3. On
|
|
@@ -2077,9 +2115,17 @@ export function enqueueMetaMessage(opts) {
|
|
|
2077
2115
|
* Drain a garden citizen's mailbox: read every unread message (a fresh `.msg`
|
|
2078
2116
|
* read before its doorbell, or a doorbell-rung `.msg.delivered`), archive each to
|
|
2079
2117
|
* `*.read` so a re-read never double-returns, and — only if at least one message
|
|
2080
|
-
* was read — stamp `lastReadAt
|
|
2081
|
-
*
|
|
2082
|
-
*
|
|
2118
|
+
* was read — stamp `lastReadAt`. `lastDeliveredAt` stays untouched (see the
|
|
2119
|
+
* stamp-site note below). An empty inbox mutates nothing: reading nothing is not
|
|
2120
|
+
* a receipt.
|
|
2121
|
+
*
|
|
2122
|
+
* WHERE THE PER-MESSAGE TRUTH LIVES (#98 5a, corrected 2026-09-03). The FILE SUFFIX
|
|
2123
|
+
* is the per-message receipt: `.msg` = enqueued, `.msg.delivered` = the doorbell
|
|
2124
|
+
* rang for it, `.msg.delivered.read` = this function handed it to the reader.
|
|
2125
|
+
* `state.json` is NOT that — its three slots are GARDEN-WIDE and overwritten, so
|
|
2126
|
+
* `lastReadAt` says "this citizen last read something at T", never "message X was
|
|
2127
|
+
* read". A sender that quotes `lastReadAt` back as the fate of the letter it just
|
|
2128
|
+
* queued is quoting the PREVIOUS letter's read. That misreading is what opened #98.
|
|
2083
2129
|
*/
|
|
2084
2130
|
export function readMetaInbox(opts) {
|
|
2085
2131
|
const now = opts.now ?? new Date();
|
|
@@ -2101,10 +2147,18 @@ export function readMetaInbox(opts) {
|
|
|
2101
2147
|
if (messages.length === 0) {
|
|
2102
2148
|
return { gardenId: citizen.gardenId, messages, readAt: null, recordPath: recordFile };
|
|
2103
2149
|
}
|
|
2104
|
-
// 3D-4 the cut: the read receipt lives SOLELY in the mailbox state store
|
|
2105
|
-
// Stamp lastReadAt — the one
|
|
2106
|
-
// reached the reader). lastDeliveredAt is
|
|
2107
|
-
//
|
|
2150
|
+
// 3D-4 the cut: the garden-wide read receipt lives SOLELY in the mailbox state store
|
|
2151
|
+
// now. Stamp lastReadAt — the one slot this layer stamps honestly (it KNOWS a body
|
|
2152
|
+
// reached the reader at this instant). lastDeliveredAt is left alone, and #98 5a
|
|
2153
|
+
// corrects WHY: the old comment said "the doorbell owns it", but the shipped
|
|
2154
|
+
// doorbell.sh (54 lines) writes NOTHING to state.json — measured, zero state writes.
|
|
2155
|
+
// So `lastDeliveredAt` is a RESERVED SLOT nobody stamps, permanently null on all
|
|
2156
|
+
// ~180 on-disk states, while 900+ files carry a `.delivered` suffix. Stamping it
|
|
2157
|
+
// here would still be wrong (it would report read-time as delivery-time), so it
|
|
2158
|
+
// stays null — but do not read "the doorbell will fill it in" into that. The
|
|
2159
|
+
// per-message delivery fact is the `.delivered` SUFFIX. The field is left in place
|
|
2160
|
+
// deliberately: removing it is a migration (the parser rejects unknown keys AND a
|
|
2161
|
+
// bumped schemaVersion, and every stamp re-parses first), tracked as #98 5b.
|
|
2108
2162
|
// The state stamp returns the updated state, whose lastReadAt IS the D7 read-receipt.
|
|
2109
2163
|
// Inside the messages.length>0 branch by construction — an empty inbox already
|
|
2110
2164
|
// early-returned (no .read archive, state untouched), so "read nothing" is no
|
|
@@ -28,6 +28,20 @@
|
|
|
28
28
|
* a UserPromptSubmit fire does a degraded RECORD backfill (upsert) but cannot
|
|
29
29
|
* re-arm the idle watch — the record's address is restored, the wake is not.
|
|
30
30
|
*
|
|
31
|
+
* SESSION SWITCH (#101). One Claude process serves one session at a time, but it can
|
|
32
|
+
* change which. Measured on oracle 2026-09-04 (raw lab S1-S6, Claude Code 2.1.260): a bare
|
|
33
|
+
* `claude` mints a NEW session (`source=startup`), and an in-session `/resume` or `/clear`
|
|
34
|
+
* then fires a SECOND SessionStart under the SAME pid for a DIFFERENT native id
|
|
35
|
+
* (`source=resume` / `source=clear`). The first garden is left behind — in the #101 field
|
|
36
|
+
* case its transcript was never written at all — while its receiver marker still names a
|
|
37
|
+
* LIVE owner, so a sender reads an armed doorbell nobody holds. This hook therefore reads
|
|
38
|
+
* the sender marker BEFORE overwriting it and retires the previous garden's receiver marker
|
|
39
|
+
* (marker only — records are identity and are never deleted here; and only a marker this
|
|
40
|
+
* pid owns). Compaction (`source=compact`) re-fires SessionStart for the SAME native id, so
|
|
41
|
+
* the same rule retires nothing there — measured, not assumed. The envelope's `source` is
|
|
42
|
+
* logged beside all of it and decides nothing: the switch is settled by what is on disk,
|
|
43
|
+
* which holds on every host and vendor version.
|
|
44
|
+
*
|
|
31
45
|
* LAUNCH: never invoked directly by Claude. `hooks.json` declares the EXEC form
|
|
32
46
|
* (`command` = `<plugin-root>/scripts/hook-launch.sh`, `args` = [node, this file]),
|
|
33
47
|
* and the launcher `exec`s that argv — so this process inherits the launcher's pid
|
|
@@ -45,7 +59,7 @@
|
|
|
45
59
|
*/
|
|
46
60
|
import * as fs from "node:fs";
|
|
47
61
|
import * as path from "node:path";
|
|
48
|
-
import { defaultMetaMailboxDir, defaultMetaSessionsDir, isPlausibleOwnerPid, upsertMetaSession, writeMetaReceiverMarker, writeMetaSenderMarker, } from "./lib/meta-session.js";
|
|
62
|
+
import { defaultMetaMailboxDir, defaultMetaSessionsDir, isPlausibleOwnerPid, readMetaSenderMarker, removeMetaReceiverMarker, upsertMetaSession, writeMetaReceiverMarker, writeMetaSenderMarker, } from "./lib/meta-session.js";
|
|
49
63
|
function logLine(level, message) {
|
|
50
64
|
try {
|
|
51
65
|
// dirname(meta-sessions) == the pi agent dir — no extra resolver export needed.
|
|
@@ -168,6 +182,13 @@ function main() {
|
|
|
168
182
|
? env.model_id
|
|
169
183
|
: undefined;
|
|
170
184
|
const eventName = typeof env.hook_event_name === "string" ? env.hook_event_name : "SessionStart";
|
|
185
|
+
// `source` (startup | resume | clear | compact) is Claude's own word for WHY this
|
|
186
|
+
// SessionStart fired. It is logged on every line below and decides nothing: a session
|
|
187
|
+
// switch is settled by what is on disk (the sender marker's garden), which is true on
|
|
188
|
+
// every host and every vendor version. Logging it is how this host finally gets a
|
|
189
|
+
// receipt for the envelope order the #101 diagnosis could only read from vendor docs —
|
|
190
|
+
// and the raw lab (scripts/raw-claude-session-switch) reads these lines, not a guess.
|
|
191
|
+
const source = typeof env.source === "string" && env.source.length > 0 ? env.source : "(unset)";
|
|
171
192
|
if (!sessionId || !transcriptPath) {
|
|
172
193
|
// A degraded envelope: cannot mint an honest reference record. Log + no-op
|
|
173
194
|
// rather than write a half-record or guess a transcript path. LEVEL depends
|
|
@@ -185,7 +206,7 @@ function main() {
|
|
|
185
206
|
input: { backend: "claude-code", nativeSessionId: sessionId, transcriptPath, cwd, model },
|
|
186
207
|
});
|
|
187
208
|
gardenId = result.record.gardenId;
|
|
188
|
-
logLine("INFO", `${result.action} record ${path.basename(result.path)} (event=${eventName}, native=${sessionId})`);
|
|
209
|
+
logLine("INFO", `${result.action} record ${path.basename(result.path)} (event=${eventName}, source=${source}, native=${sessionId})`);
|
|
189
210
|
}
|
|
190
211
|
catch (err) {
|
|
191
212
|
// Best-effort: a broken record store must surface via the doctor, not by
|
|
@@ -210,9 +231,48 @@ function main() {
|
|
|
210
231
|
// an env var. Missing launcher provenance or an implausible parent yields no marker.
|
|
211
232
|
const ownerPid = resolveMetaHookOwnerPid();
|
|
212
233
|
if (ownerPid !== null) {
|
|
234
|
+
// SESSION SWITCH RETIREMENT (#101 결함 A). One Claude process serves ONE session at a
|
|
235
|
+
// time, but it can switch which: an in-session `/resume` or `/clear` fires a second
|
|
236
|
+
// SessionStart under the same pid for a different native id, leaving the session the
|
|
237
|
+
// process started with behind (measured on oracle, meta-bridge-hook.log 2026-09-04
|
|
238
|
+
// 13:13:04 `source=startup` → 13:13:37 `source=resume`; the field case at 09:31:35 →
|
|
239
|
+
// 09:31:39 is the same shape). Whatever it was serving before is no longer being
|
|
240
|
+
// drained, so the marker advertising its doorbell has to go.
|
|
241
|
+
//
|
|
242
|
+
// The evidence is the sender marker as it stands RIGHT NOW — pid → the garden this
|
|
243
|
+
// process serves — which is why this reads it BEFORE the write below overwrites it with
|
|
244
|
+
// the new garden. No vendor field is consulted: `source` is logged, not branched on, so
|
|
245
|
+
// a host or version that words it differently changes nothing here. A same-garden
|
|
246
|
+
// re-registration (every UserPromptSubmit, a CwdChanged, a re-fired SessionStart) finds
|
|
247
|
+
// an equal garden id and retires NOTHING — the marker it would remove is the live one.
|
|
248
|
+
//
|
|
249
|
+
// Only the marker, never the record (see removeMetaReceiverMarker), and only a marker
|
|
250
|
+
// this pid owns.
|
|
251
|
+
//
|
|
252
|
+
// AND ONLY ON AN EVENT THAT CAN ARM THE REPLACEMENT (cross-review, 2026-09-04). This
|
|
253
|
+
// block sits before the UserPromptSubmit early-return, and UPS cannot emit watchPaths —
|
|
254
|
+
// so a retirement reached from there would take a doorbell down with nothing in the same
|
|
255
|
+
// run able to put one back. A watch is retired only by a run that arms one; that is the
|
|
256
|
+
// whole rule. `armProvenanceFor` is the same predicate the arm block below uses, so the
|
|
257
|
+
// two can never disagree about which events those are.
|
|
258
|
+
//
|
|
259
|
+
// It is NOT a defence against a keystroke from a session this pid has left: measured on
|
|
260
|
+
// oracle 2026-09-04 (raw lab S1-S6, meta-bridge-hook.log), every UserPromptSubmit named
|
|
261
|
+
// the native id its own pid's preceding SessionStart had established — 8 of 8, none
|
|
262
|
+
// otherwise. The hook runs synchronously inside the session's own process, so a UPS
|
|
263
|
+
// envelope is that session speaking, and the sender pointer it moves is authoritative.
|
|
264
|
+
const previous = armProvenanceFor(eventName) !== null
|
|
265
|
+
? readMetaSenderMarker({ backend: "claude-code", ownerPid, verifyOwner: false })
|
|
266
|
+
: null;
|
|
267
|
+
if (previous && previous.gardenId !== gardenId) {
|
|
268
|
+
const retired = removeMetaReceiverMarker({ gardenId: previous.gardenId, ownerPid });
|
|
269
|
+
logLine("INFO", retired
|
|
270
|
+
? `retired receiver marker ${previous.gardenId} — owner pid ${ownerPid} switched to ${gardenId} (event=${eventName}, source=${source})`
|
|
271
|
+
: `no receiver marker to retire for ${previous.gardenId} — owner pid ${ownerPid} switched to ${gardenId} (event=${eventName}, source=${source})`);
|
|
272
|
+
}
|
|
213
273
|
try {
|
|
214
274
|
writeMetaSenderMarker({ backend: "claude-code", gardenId, nativeSessionId: sessionId, cwd, ownerPid });
|
|
215
|
-
logLine("INFO", `sender marker ${ownerPid} -> ${gardenId} (event=${eventName})`);
|
|
275
|
+
logLine("INFO", `sender marker ${ownerPid} -> ${gardenId} (event=${eventName}, source=${source})`);
|
|
216
276
|
}
|
|
217
277
|
catch (err) {
|
|
218
278
|
logLine("WARN", `sender marker write failed (event=${eventName}, pid=${ownerPid}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -259,7 +319,7 @@ function main() {
|
|
|
259
319
|
ownerPid,
|
|
260
320
|
armProvenance,
|
|
261
321
|
});
|
|
262
|
-
logLine("INFO", `receiver marker ${gardenId} owner=${ownerPid} arm=${eventName}`);
|
|
322
|
+
logLine("INFO", `receiver marker ${gardenId} owner=${ownerPid} arm=${eventName} source=${source}`);
|
|
263
323
|
}
|
|
264
324
|
catch (err) {
|
|
265
325
|
logLine("WARN", `receiver marker write failed (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -66,7 +66,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
66
66
|
import { z } from "zod";
|
|
67
67
|
|
|
68
68
|
import { controlSocketPathIn, defaultControlSocketDir } from "../../../pi-extensions/lib/control-socket-path.js";
|
|
69
|
-
import {
|
|
69
|
+
import { resolveMailboxReceiverFacts } from "../../../pi-extensions/lib/entwurf-deliverability.ts";
|
|
70
70
|
import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provider.ts";
|
|
71
71
|
import { renderEntwurfPeers } from "../../../pi-extensions/lib/entwurf-peers-render.ts";
|
|
72
72
|
import { computeSelfAddressability, type MetaDeliveryDomain } from "../../../pi-extensions/lib/entwurf-self-address.ts";
|
|
@@ -91,6 +91,8 @@ import {
|
|
|
91
91
|
readActiveStoreEntries,
|
|
92
92
|
readMetaInbox,
|
|
93
93
|
readMetaReceiverMarker,
|
|
94
|
+
readMetaSenderMarker,
|
|
95
|
+
requireBackend,
|
|
94
96
|
} from "../../../pi-extensions/lib/meta-session.ts";
|
|
95
97
|
import { freshCall, renderFreshCall } from "../../../pi-extensions/lib/mux-fresh-call.ts";
|
|
96
98
|
import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.ts";
|
|
@@ -272,9 +274,12 @@ async function buildTrustedMetaSenderEnvelope(cwd: string = process.cwd()): Prom
|
|
|
272
274
|
// admits another self-fetch citizen.
|
|
273
275
|
// none ← neither. omp today: no mailbox drain, no native-push adapter. Rendering
|
|
274
276
|
// this as self-fetch printed a mailboxPath nothing drains.
|
|
275
|
-
// self-fetch (claude-code/copilot): can this citizen's own inbox wake? → the
|
|
276
|
-
//
|
|
277
|
-
//
|
|
277
|
+
// self-fetch (claude-code/copilot): can this citizen's own inbox wake? → the SHARED
|
|
278
|
+
// receiver composition `resolveMailboxReceiverFacts`, the same one the v2 dispatch seam
|
|
279
|
+
// uses, so a citizen's self-reported replyability can never disagree with what dispatch
|
|
280
|
+
// decides about it. It reads the presence marker (a dead/reused owner already folds to
|
|
281
|
+
// null) AND, where the watch owner is the sender-marker process, the #101 join that says
|
|
282
|
+
// the owner is still serving THIS garden rather than one it switched away from.
|
|
278
283
|
// native-push (antigravity): there is no inbox and no watch. A reply is injected into a
|
|
279
284
|
// live app-server conversation, so only an adapter probe can answer. Composing the
|
|
280
285
|
// receiver atom here would demand `watchArmed` from a backend that never arms one, and
|
|
@@ -298,14 +303,17 @@ async function buildTrustedMetaSenderEnvelope(cwd: string = process.cwd()): Prom
|
|
|
298
303
|
}
|
|
299
304
|
: metaDeliveryDomain === "self-fetch"
|
|
300
305
|
? (() => {
|
|
301
|
-
const receiver =
|
|
302
|
-
|
|
306
|
+
const receiver = resolveMailboxReceiverFacts(identity, {
|
|
307
|
+
readReceiverMarker: (gardenId: string) => readMetaReceiverMarker({ gardenId }),
|
|
308
|
+
readSenderMarker: (backend: string, ownerPid: number) =>
|
|
309
|
+
readMetaSenderMarker({ backend: requireBackend(backend), ownerPid }),
|
|
310
|
+
});
|
|
303
311
|
return {
|
|
304
312
|
origin: "meta-session" as const,
|
|
305
313
|
metaDeliveryDomain,
|
|
306
314
|
recordBacked: true,
|
|
307
|
-
ownerAlive:
|
|
308
|
-
watchArmed:
|
|
315
|
+
ownerAlive: receiver.ownerAlive,
|
|
316
|
+
watchArmed: receiver.watchArmed,
|
|
309
317
|
};
|
|
310
318
|
})()
|
|
311
319
|
: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junghanacs/entwurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -143,6 +143,7 @@
|
|
|
143
143
|
"check-omp-receive-arm": "./run.sh check-omp-receive-arm",
|
|
144
144
|
"check-omp-fresh-preflight": "./run.sh check-omp-fresh-preflight",
|
|
145
145
|
"check-hook-launch-topology": "./run.sh check-hook-launch-topology",
|
|
146
|
+
"check-meta-hook-session-switch": "./run.sh check-meta-hook-session-switch",
|
|
146
147
|
"check-acp-carrier-augment": "./run.sh check-acp-carrier-augment",
|
|
147
148
|
"check-agy-permission-matrix": "./run.sh check-agy-permission-matrix",
|
|
148
149
|
"check-gate-qualification": "./run.sh check-gate-qualification",
|
|
@@ -165,7 +166,7 @@
|
|
|
165
166
|
"check:toolchain": "pnpm lint && pnpm typecheck",
|
|
166
167
|
"check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface",
|
|
167
168
|
"check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-resume-args && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment",
|
|
168
|
-
"check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
|
|
169
|
+
"check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
|
|
169
170
|
"check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
|
|
170
171
|
},
|
|
171
172
|
"pi": {
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-launch.sh",
|
|
46
46
|
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/doorbell.sh"],
|
|
47
47
|
"asyncRewake": true,
|
|
48
|
-
"timeout": 20
|
|
48
|
+
"timeout": 20,
|
|
49
|
+
"rewakeSummary": "entwurf inbox: sibling mail arrived",
|
|
50
|
+
"rewakeMessage": "entwurf mailbox notice:"
|
|
49
51
|
}
|
|
50
52
|
]
|
|
51
53
|
}
|
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
# with a doorbell notice. Free: a file write + continuation of an already-running
|
|
7
7
|
# subscription session (no `claude -p` spawn).
|
|
8
8
|
#
|
|
9
|
-
# ADDRESSED by GARDEN ID: the changed path arrives on stdin as `file_path
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# signal wakes only that session. No node needed here; the dirname is the mailbox.
|
|
9
|
+
# ADDRESSED by GARDEN ID: the changed path arrives on stdin as `file_path` and this
|
|
10
|
+
# hook takes its DIRECTORY as the mailbox to process. No node needed here; the
|
|
11
|
+
# dirname is the mailbox.
|
|
13
12
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
13
|
+
# Two statements, deliberately kept apart (#98, 2026-09-03 — they used to be one
|
|
14
|
+
# sentence claiming this hook "touches ONLY its own mailbox"):
|
|
15
|
+
# - PREMISE: under a normal install the only watch this session armed is its own
|
|
16
|
+
# <garden-id>/inbox.signal, so that is the only file_path that arrives.
|
|
17
|
+
# - LIMIT: this script does not verify it. It trusts `file_path` and never compares
|
|
18
|
+
# the dirname against its garden id. Measured — a second FileChanged hook in the
|
|
19
|
+
# same session made this doorbell process the OTHER mailbox and race it to exit 2,
|
|
20
|
+
# which is why a second such hook cannot coexist with this one.
|
|
21
|
+
# Receipt: scripts/raw-async-delivery/README.md, "What the probe session actually
|
|
22
|
+
# touches". Not a live defect: nothing pokes a signal outside the garden mailbox.
|
|
18
23
|
#
|
|
19
24
|
# RUNTIME DEPS: bash + python3 (the FileChanged stdin JSON is parsed with python3
|
|
20
25
|
# below — robust against escaping, unlike sed/grep). The meta-bridge doctor must
|