@junghanacs/entwurf 0.12.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 +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-bridge-boot — deterministic gate (0.11 step 5d-5-pre, G1a/G1b):
|
|
3
|
+
* boots the entwurf-bridge MCP server EXACTLY as it ships (start.sh →
|
|
4
|
+
* `node --experimental-strip-types src/index.ts`, no build step) and asserts the
|
|
5
|
+
* runtime contract that the source-shape gate `check-entwurf-v2-surface` cannot:
|
|
6
|
+
*
|
|
7
|
+
* - G1a: the server actually BOOTS. start.sh static-imports the whole v2 fence
|
|
8
|
+
* graph (entwurf-v2-surface → runner → production → decider/lock/send/spawn/
|
|
9
|
+
* mailbox/control-rpc) at top level under strip-types; a strip-types-hostile
|
|
10
|
+
* construct (enum / namespace / parameter property / `import =`) anywhere in
|
|
11
|
+
* that graph would crash the server at boot. A parseable tools/list (id:2)
|
|
12
|
+
* response IS the proof the graph loaded and `server.tool` registration ran.
|
|
13
|
+
* - G1b: `entwurf_v2` is registered on the runtime tools/list surface with the
|
|
14
|
+
* expected input schema (props ⊇ target/intent/message/mode/wants_reply;
|
|
15
|
+
* required ⊇ target/intent/message; intent/mode enums).
|
|
16
|
+
*
|
|
17
|
+
* Scope boundary (D1=A안): this gate owns boot + entwurf_v2 registration/schema.
|
|
18
|
+
* The broad protocol/negative suite stays in check-bridge/test.sh. Only a
|
|
19
|
+
* `tools/list` is sent — no `tools/call` — so there is NO lock/filesystem side
|
|
20
|
+
* effect and no auth/model is needed; it lives in `pnpm check`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawn } from "node:child_process";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
|
+
const START_SH = path.join(REPO_DIR, "mcp", "entwurf-bridge", "start.sh");
|
|
29
|
+
|
|
30
|
+
let passed = 0;
|
|
31
|
+
function ok(label: string, cond: boolean, detail?: string): void {
|
|
32
|
+
if (!cond) {
|
|
33
|
+
console.error(` ✗ ${label}`);
|
|
34
|
+
if (detail) console.error(detail);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
passed++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fatal(msg: string): never {
|
|
41
|
+
console.error(`check-entwurf-bridge-boot: ${msg}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface BridgeTool {
|
|
46
|
+
name?: string;
|
|
47
|
+
inputSchema?: {
|
|
48
|
+
properties?: Record<string, { enum?: unknown; maxLength?: unknown }>;
|
|
49
|
+
required?: unknown;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Boot start.sh, run the MCP handshake + a single tools/list (id:2), and resolve
|
|
55
|
+
* with the registered tools. The failure paths are split (timeout / early-exit /
|
|
56
|
+
* parse / missing result.tools) so a strip-types boot crash surfaces with its
|
|
57
|
+
* rc/signal/stderr — not as an opaque "no response".
|
|
58
|
+
*/
|
|
59
|
+
function bootAndListTools(): Promise<BridgeTool[]> {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const child = spawn(START_SH, { stdio: ["pipe", "pipe", "pipe"] });
|
|
62
|
+
let stdout = "";
|
|
63
|
+
let stderr = "";
|
|
64
|
+
let settled = false;
|
|
65
|
+
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
if (settled) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
try {
|
|
70
|
+
child.kill("SIGTERM");
|
|
71
|
+
} catch {}
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
try {
|
|
74
|
+
child.kill("SIGKILL");
|
|
75
|
+
} catch {}
|
|
76
|
+
}, 300);
|
|
77
|
+
fatal(`no tools/list response before timeout${stderr.trim() ? `\n--- stderr ---\n${stderr.trim()}` : ""}`);
|
|
78
|
+
}, 10_000);
|
|
79
|
+
|
|
80
|
+
function tryResolve(): void {
|
|
81
|
+
if (settled) return;
|
|
82
|
+
// MCP frames are newline-delimited JSON-RPC; find the id:2 (tools/list) reply.
|
|
83
|
+
for (const line of stdout
|
|
84
|
+
.split("\n")
|
|
85
|
+
.map((l) => l.trim())
|
|
86
|
+
.filter(Boolean)) {
|
|
87
|
+
let msg: { id?: unknown; result?: { tools?: unknown } };
|
|
88
|
+
try {
|
|
89
|
+
msg = JSON.parse(line);
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (msg?.id !== 2) continue;
|
|
94
|
+
settled = true;
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
try {
|
|
97
|
+
child.kill("SIGTERM");
|
|
98
|
+
} catch {}
|
|
99
|
+
const tools = msg?.result?.tools;
|
|
100
|
+
if (!Array.isArray(tools)) {
|
|
101
|
+
fatal(`id:2 response missing result.tools\n--- raw ---\n${line}`);
|
|
102
|
+
}
|
|
103
|
+
resolve(tools as BridgeTool[]);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
child.stdout.on("data", (d) => {
|
|
109
|
+
stdout += d.toString();
|
|
110
|
+
tryResolve();
|
|
111
|
+
});
|
|
112
|
+
child.stderr.on("data", (d) => {
|
|
113
|
+
stderr += d.toString();
|
|
114
|
+
});
|
|
115
|
+
child.on("error", (err) => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
settled = true;
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
fatal(`failed to spawn start.sh: ${String(err)}`);
|
|
120
|
+
});
|
|
121
|
+
child.on("close", (code, signal) => {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
settled = true;
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
// Closed before id:2 — this is where a strip-types boot crash / SyntaxError
|
|
126
|
+
// in the static-import fence graph surfaces.
|
|
127
|
+
fatal(
|
|
128
|
+
`server exited before tools/list response (code=${code} signal=${String(signal)})` +
|
|
129
|
+
(stderr.trim() ? `\n--- stderr ---\n${stderr.trim()}` : "") +
|
|
130
|
+
(stdout.trim() ? `\n--- stdout ---\n${stdout.trim()}` : ""),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const send = (obj: unknown): void => {
|
|
135
|
+
child.stdin.write(`${JSON.stringify(obj)}\n`);
|
|
136
|
+
};
|
|
137
|
+
send({
|
|
138
|
+
jsonrpc: "2.0",
|
|
139
|
+
id: 1,
|
|
140
|
+
method: "initialize",
|
|
141
|
+
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "check-boot", version: "0" } },
|
|
142
|
+
});
|
|
143
|
+
send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
144
|
+
send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Order-independent set equality — the enum is a contract on membership, not order. */
|
|
149
|
+
function setEq(actual: unknown, expected: string[]): boolean {
|
|
150
|
+
if (!Array.isArray(actual)) return false;
|
|
151
|
+
const got = new Set(actual.map(String));
|
|
152
|
+
return got.size === expected.length && expected.every((e) => got.has(e));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function main(): Promise<void> {
|
|
156
|
+
const tools = await bootAndListTools();
|
|
157
|
+
|
|
158
|
+
// G1a — a parseable tools/list arrived → the strip-types fence graph loaded + server.tool ran.
|
|
159
|
+
ok("G1a: MCP server boots under strip-types and answers tools/list", tools.length > 0);
|
|
160
|
+
|
|
161
|
+
// G1b — entwurf_v2 registered + schema contract.
|
|
162
|
+
const v2 = tools.find((t) => t?.name === "entwurf_v2");
|
|
163
|
+
ok("G1b: entwurf_v2 registered on the runtime tools/list surface", !!v2);
|
|
164
|
+
|
|
165
|
+
const schema = v2?.inputSchema ?? {};
|
|
166
|
+
const rawSchema = JSON.stringify(schema);
|
|
167
|
+
const props = schema.properties ?? {};
|
|
168
|
+
for (const p of ["target", "intent", "message", "mode", "wants_reply"]) {
|
|
169
|
+
ok(`G1b: entwurf_v2 schema has property '${p}'`, p in props, `--- inputSchema ---\n${rawSchema}`);
|
|
170
|
+
}
|
|
171
|
+
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
172
|
+
for (const r of ["target", "intent", "message"]) {
|
|
173
|
+
ok(`G1b: entwurf_v2 schema requires '${r}'`, required.includes(r), `--- inputSchema ---\n${rawSchema}`);
|
|
174
|
+
}
|
|
175
|
+
ok(
|
|
176
|
+
"G1b: entwurf_v2 intent enum == {fire-and-forget, owned-outcome}",
|
|
177
|
+
setEq(props.intent?.enum, ["fire-and-forget", "owned-outcome"]),
|
|
178
|
+
`--- inputSchema ---\n${rawSchema}`,
|
|
179
|
+
);
|
|
180
|
+
ok(
|
|
181
|
+
"G1b: entwurf_v2 mode enum == {steer, follow_up}",
|
|
182
|
+
setEq(props.mode?.enum, ["steer", "follow_up"]),
|
|
183
|
+
`--- inputSchema ---\n${rawSchema}`,
|
|
184
|
+
);
|
|
185
|
+
ok(
|
|
186
|
+
"G1b: entwurf_v2 message maxLength == 16000",
|
|
187
|
+
props.message?.maxLength === 16000,
|
|
188
|
+
`--- inputSchema ---\n${rawSchema}`,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
ok(
|
|
192
|
+
"G1c: legacy v1 MCP tools are absent",
|
|
193
|
+
!tools.some((t) => ["entwurf", "entwurf_resume", "entwurf_send"].includes(String(t?.name))),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
console.log(`\ncheck-entwurf-bridge-boot: ${passed} checks passed`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await main();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-capabilities — deterministic gate for 0.11 Stage 0 step 3C:
|
|
3
|
+
* the backend capability source. Pure parse over the packaged registry data
|
|
4
|
+
* file; no backend, no network, no hook. Safe in the `pnpm check` static floor.
|
|
5
|
+
*
|
|
6
|
+
* Proves the capability registry (`pi/entwurf-capabilities.json`) that gives the
|
|
7
|
+
* v2 record a place to send wakeMode/deliveryLevel/nativeIdLabel:
|
|
8
|
+
* - the shipped file parses + COVERS exactly META_BACKENDS_V2 (pi included),
|
|
9
|
+
* - it AGREES with the live `META_BACKEND_DESCRIPTORS` for the three existing
|
|
10
|
+
* backends (a drift guard for the 3C→3D transition while the live const is
|
|
11
|
+
* still the authority — frozen "META_BACKEND_DESCRIPTORS 소비처 유지"),
|
|
12
|
+
* - strict keyset / coverage / field validation all crash, not warn.
|
|
13
|
+
*
|
|
14
|
+
* Scope is 3C ONLY: schema + parser + gate. No live routing — the record writer/
|
|
15
|
+
* upsert/readMetaInbox/enqueueMetaMessage and the META_BACKEND_DESCRIPTORS live
|
|
16
|
+
* consumers are untouched; cutting them over (and dropping wakeMode from the
|
|
17
|
+
* record) lands in step 3D.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import {
|
|
23
|
+
META_BACKEND_DESCRIPTORS,
|
|
24
|
+
META_BACKENDS_V2,
|
|
25
|
+
MetaRecordError,
|
|
26
|
+
metaCapabilitiesFilePath,
|
|
27
|
+
parseMetaCapabilityRegistry,
|
|
28
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
29
|
+
|
|
30
|
+
let passed = 0;
|
|
31
|
+
function ok(label: string, cond: boolean): void {
|
|
32
|
+
assert.ok(cond, label);
|
|
33
|
+
console.log(` ok ${label}`);
|
|
34
|
+
passed++;
|
|
35
|
+
}
|
|
36
|
+
function throws(label: string, fn: () => unknown): void {
|
|
37
|
+
assert.throws(fn, MetaRecordError, label);
|
|
38
|
+
console.log(` ok ${label}`);
|
|
39
|
+
passed++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The shipped registry, read from disk and parsed.
|
|
43
|
+
const SHIPPED = fs.readFileSync(metaCapabilitiesFilePath(), "utf8");
|
|
44
|
+
const reg = parseMetaCapabilityRegistry(SHIPPED);
|
|
45
|
+
|
|
46
|
+
// 1. coverage — the shipped file covers exactly META_BACKENDS_V2, pi included.
|
|
47
|
+
ok(
|
|
48
|
+
"shipped registry covers exactly META_BACKENDS_V2 (pi included)",
|
|
49
|
+
[...META_BACKENDS_V2].sort().join(",") === Object.keys(reg.backends).sort().join(","),
|
|
50
|
+
);
|
|
51
|
+
ok("shipped registry includes the pi backend", reg.backends.pi !== undefined);
|
|
52
|
+
|
|
53
|
+
// 2. drift guard — for the three backends the live const still owns, the JSON
|
|
54
|
+
// must agree field-for-field (so the new source cannot silently diverge from
|
|
55
|
+
// META_BACKEND_DESCRIPTORS before 3D cuts the live consumer over).
|
|
56
|
+
for (const backend of ["claude-code", "antigravity", "codex"] as const) {
|
|
57
|
+
const live = META_BACKEND_DESCRIPTORS[backend];
|
|
58
|
+
const cap = reg.backends[backend];
|
|
59
|
+
ok(
|
|
60
|
+
`capability for ${backend} agrees with live META_BACKEND_DESCRIPTORS (wakeMode/deliveryLevel/nativeIdLabel)`,
|
|
61
|
+
cap.wakeMode === live.wakeMode &&
|
|
62
|
+
cap.deliveryLevel === live.deliveryLevel &&
|
|
63
|
+
cap.nativeIdLabel === live.nativeIdLabel,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. pi capability is well-formed (its values are NEW — no live const to compare).
|
|
68
|
+
// pi = direct-inject: pi's live wake path is the control socket
|
|
69
|
+
// (pi.sendMessage triggerTurn) which injects the body straight into the
|
|
70
|
+
// model-visible turn — that is direct-inject, not Claude's mailbox self-fetch.
|
|
71
|
+
ok(
|
|
72
|
+
"pi capability is well-formed (direct-inject / D6 / sessionId)",
|
|
73
|
+
reg.backends.pi.wakeMode === "direct-inject" &&
|
|
74
|
+
reg.backends.pi.deliveryLevel === "D6" &&
|
|
75
|
+
reg.backends.pi.nativeIdLabel === "sessionId",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// 4. crash, don't warn — schema/coverage/keyset/field validation.
|
|
79
|
+
const base = JSON.parse(SHIPPED) as Record<string, unknown>;
|
|
80
|
+
throws("parse: invalid JSON throws", () => parseMetaCapabilityRegistry("{nope"));
|
|
81
|
+
throws("parse: array (non-object) throws", () => parseMetaCapabilityRegistry("[]"));
|
|
82
|
+
throws("parse: wrong schemaVersion throws", () =>
|
|
83
|
+
parseMetaCapabilityRegistry(JSON.stringify({ ...base, schemaVersion: 2 })),
|
|
84
|
+
);
|
|
85
|
+
throws("parse: unexpected top-level key throws", () =>
|
|
86
|
+
parseMetaCapabilityRegistry(JSON.stringify({ ...base, extra: 1 })),
|
|
87
|
+
);
|
|
88
|
+
throws("parse: missing a backend (coverage short) throws", () => {
|
|
89
|
+
const backends = { ...(base.backends as Record<string, unknown>) };
|
|
90
|
+
delete backends.pi;
|
|
91
|
+
return parseMetaCapabilityRegistry(JSON.stringify({ ...base, backends }));
|
|
92
|
+
});
|
|
93
|
+
throws("parse: unknown extra backend (coverage over) throws", () =>
|
|
94
|
+
parseMetaCapabilityRegistry(
|
|
95
|
+
JSON.stringify({ ...base, backends: { ...(base.backends as Record<string, unknown>), gemini: reg.backends.pi } }),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
throws("parse: bad wakeMode value throws", () =>
|
|
99
|
+
parseMetaCapabilityRegistry(
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
...base,
|
|
102
|
+
backends: { ...(base.backends as Record<string, unknown>), pi: { ...reg.backends.pi, wakeMode: "telepathy" } },
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
throws("parse: unexpected key inside a capability entry throws", () =>
|
|
107
|
+
parseMetaCapabilityRegistry(
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
...base,
|
|
110
|
+
backends: { ...(base.backends as Record<string, unknown>), pi: { ...reg.backends.pi, tmuxTarget: "psa:3.1" } },
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
throws("parse: empty deliveryLevel throws", () =>
|
|
115
|
+
parseMetaCapabilityRegistry(
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
...base,
|
|
118
|
+
backends: { ...(base.backends as Record<string, unknown>), pi: { ...reg.backends.pi, deliveryLevel: "" } },
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
console.log(`[check-entwurf-capabilities] ${passed} assertions ok`);
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-control-rpc — gate for the 5d-2 RPC-helper EXTRACTION micro-slice. It
|
|
3
|
+
* proves the `--entwurf-control` socket protocol moved to the ctx-free SSOT
|
|
4
|
+
* `lib/entwurf-control-rpc.ts` WITHOUT a behaviour change, and that the surface file now
|
|
5
|
+
* consumes it instead of defining its own:
|
|
6
|
+
*
|
|
7
|
+
* 1. ctx-free source guard — the new lib imports NO surface-only world
|
|
8
|
+
* (ExtensionContext / ExtensionAPI / pi. / @earendil-works/pi-ai). It is a pure
|
|
9
|
+
* transport so the 5d-2b production `sendOverSocket` can share it from lib/.
|
|
10
|
+
* 2. extraction guard — `entwurf-control.ts` IMPORTS `sendRpcCommand` from the shared
|
|
11
|
+
* lib and no longer DEFINES its own `async function sendRpcCommand` (no protocol fork).
|
|
12
|
+
* 3. round-trip — a real (short) unix-socket server echoes `{type:"response",
|
|
13
|
+
* command:<same>, success:true}` and `sendRpcCommand` resolves `{response.success:true}`
|
|
14
|
+
* with the matching command tag (the newline-JSON write + matched-response parse wire).
|
|
15
|
+
* 4. close-before-response — a server that accepts then closes WITHOUT a response makes
|
|
16
|
+
* `sendRpcCommand` reject `connection closed before response` (the 2026-05-18
|
|
17
|
+
* receiver-stuck backstop the settled-guard preserves).
|
|
18
|
+
* 5. get_info runtime helper parses/formats cwd/model/idle once for every caller.
|
|
19
|
+
*
|
|
20
|
+
* No model / auth / pi process — only `net.Server` on a tmp socket, so it rides `pnpm check`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import * as fs from "node:fs/promises";
|
|
25
|
+
import * as net from "node:net";
|
|
26
|
+
import * as os from "node:os";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import {
|
|
30
|
+
fetchControlSocketRuntimeInfo,
|
|
31
|
+
formatRuntimeModel,
|
|
32
|
+
parseGetInfoResponseData,
|
|
33
|
+
type RpcSendCommand,
|
|
34
|
+
sendRpcCommand,
|
|
35
|
+
} from "../pi-extensions/lib/entwurf-control-rpc.ts";
|
|
36
|
+
|
|
37
|
+
let passed = 0;
|
|
38
|
+
function ok(label: string, cond: boolean): void {
|
|
39
|
+
assert.ok(cond, label);
|
|
40
|
+
console.log(` ok ${label}`);
|
|
41
|
+
passed++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
const REPO = path.join(HERE, "..");
|
|
46
|
+
const LIB_SRC = path.join(REPO, "pi-extensions/lib/entwurf-control-rpc.ts");
|
|
47
|
+
const CONTROL_SRC = path.join(REPO, "pi-extensions/entwurf-control.ts");
|
|
48
|
+
|
|
49
|
+
/** Spin up a one-shot unix-socket server; `onLine` decides the reply (or null = no reply). */
|
|
50
|
+
async function withServer(
|
|
51
|
+
onLine: (line: string, socket: net.Socket) => void,
|
|
52
|
+
body: (socketPath: string) => Promise<void>,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "rpc-"));
|
|
55
|
+
const socketPath = path.join(dir, "s.sock");
|
|
56
|
+
const server = net.createServer((socket) => {
|
|
57
|
+
socket.setEncoding("utf8");
|
|
58
|
+
let buf = "";
|
|
59
|
+
socket.on("data", (chunk) => {
|
|
60
|
+
buf += chunk;
|
|
61
|
+
let nl = buf.indexOf("\n");
|
|
62
|
+
while (nl !== -1) {
|
|
63
|
+
const line = buf.slice(0, nl).trim();
|
|
64
|
+
buf = buf.slice(nl + 1);
|
|
65
|
+
nl = buf.indexOf("\n");
|
|
66
|
+
if (line) onLine(line, socket);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
await new Promise<void>((resolve) => server.listen(socketPath, resolve));
|
|
71
|
+
try {
|
|
72
|
+
await body(socketPath);
|
|
73
|
+
} finally {
|
|
74
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
75
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function main(): Promise<void> {
|
|
80
|
+
// ── 1: ctx-free source guard ──────────────────────────────────────────────
|
|
81
|
+
{
|
|
82
|
+
const src = await fs.readFile(LIB_SRC, "utf8");
|
|
83
|
+
// Strip line/block comments so the module-header prose ("must NOT import …") never
|
|
84
|
+
// trips the guard — only real import statements count.
|
|
85
|
+
const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
86
|
+
const forbidden = ["ExtensionContext", "ExtensionAPI", "@earendil-works/pi-ai", "pi."];
|
|
87
|
+
for (const term of forbidden) {
|
|
88
|
+
ok(`1: lib is ctx-free — no '${term}'`, !code.includes(term));
|
|
89
|
+
}
|
|
90
|
+
ok("1: lib imports only node:net (value)", /import \* as net from "node:net";/.test(code));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── 2: extraction guard — surface imports shared runtime helper, no fork ────
|
|
94
|
+
{
|
|
95
|
+
const src = await fs.readFile(CONTROL_SRC, "utf8");
|
|
96
|
+
ok(
|
|
97
|
+
"2: entwurf-control imports fetchControlSocketRuntimeInfo from the shared lib",
|
|
98
|
+
/from "\.\/lib\/entwurf-control-rpc\.js"/.test(src) && /\bfetchControlSocketRuntimeInfo\b/.test(src),
|
|
99
|
+
);
|
|
100
|
+
ok("2: entwurf-control no longer DEFINES sendRpcCommand", !/(async\s+)?function\s+sendRpcCommand\s*\(/.test(src));
|
|
101
|
+
ok("2: entwurf-control does not call sendRpcCommand directly", !/[^.]\bsendRpcCommand\s*\(/.test(src));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── 3: round-trip — write command, matched response, success:true ─────────
|
|
105
|
+
{
|
|
106
|
+
let seenLine: string | null = null;
|
|
107
|
+
await withServer(
|
|
108
|
+
(line, socket) => {
|
|
109
|
+
seenLine = line;
|
|
110
|
+
const cmd = JSON.parse(line);
|
|
111
|
+
socket.write(`${JSON.stringify({ type: "response", command: cmd.type, success: true, data: { ok: 1 } })}\n`);
|
|
112
|
+
},
|
|
113
|
+
async (socketPath) => {
|
|
114
|
+
const cmd: RpcSendCommand = { type: "send", message: "hi", mode: "follow_up", wants_reply: false };
|
|
115
|
+
const { response } = await sendRpcCommand(socketPath, cmd, { timeout: 2000 });
|
|
116
|
+
ok("3: round-trip → response.success true", response.success === true);
|
|
117
|
+
ok("3: response tagged with the SAME command", response.command === "send");
|
|
118
|
+
ok(
|
|
119
|
+
"3: server saw the newline-JSON command",
|
|
120
|
+
seenLine !== null && JSON.parse(seenLine as string).type === "send",
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── 4: close-before-response → reject 'connection closed before response' ──
|
|
127
|
+
{
|
|
128
|
+
let rejected: unknown;
|
|
129
|
+
await withServer(
|
|
130
|
+
(_line, socket) => {
|
|
131
|
+
// Accept the command, then close WITHOUT replying.
|
|
132
|
+
socket.end();
|
|
133
|
+
},
|
|
134
|
+
async (socketPath) => {
|
|
135
|
+
try {
|
|
136
|
+
await sendRpcCommand(socketPath, { type: "get_info" }, { timeout: 2000 });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
rejected = err;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
ok(
|
|
143
|
+
"4: close-before-response → rejects 'connection closed before response'",
|
|
144
|
+
rejected instanceof Error && rejected.message === "connection closed before response",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── 5: get_info runtime parse/format/fetch SSOT ───────────────────────────
|
|
149
|
+
{
|
|
150
|
+
const parsed = parseGetInfoResponseData({
|
|
151
|
+
cwd: "/work/cos",
|
|
152
|
+
model: { provider: "entwurf", id: "gpt-5.5" },
|
|
153
|
+
idle: false,
|
|
154
|
+
});
|
|
155
|
+
ok("5: parse cwd", parsed.cwd === "/work/cos");
|
|
156
|
+
ok("5: parse model id", parsed.modelId === "gpt-5.5");
|
|
157
|
+
ok("5: parse model provider", parsed.modelProvider === "entwurf");
|
|
158
|
+
ok("5: parse idle false", parsed.idle === false);
|
|
159
|
+
ok("5: format provider/model", formatRuntimeModel(parsed) === "entwurf/gpt-5.5");
|
|
160
|
+
ok("5: format model-only fallback", formatRuntimeModel({ modelId: "gpt-5.5" }) === "gpt-5.5");
|
|
161
|
+
const malformed = parseGetInfoResponseData({ model: null });
|
|
162
|
+
ok(
|
|
163
|
+
"5: parse malformed data yields undefined fields",
|
|
164
|
+
malformed.cwd === undefined && malformed.modelId === undefined,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
await withServer(
|
|
168
|
+
(line, socket) => {
|
|
169
|
+
const cmd = JSON.parse(line);
|
|
170
|
+
socket.write(
|
|
171
|
+
`${JSON.stringify({ type: "response", command: cmd.type, success: true, data: { cwd: "/w", model: { provider: "p", id: "m" }, idle: true } })}\n`,
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
async (socketPath) => {
|
|
175
|
+
const info = await fetchControlSocketRuntimeInfo(socketPath, { timeout: 2000 });
|
|
176
|
+
ok(
|
|
177
|
+
"5: fetch get_info parses response",
|
|
178
|
+
info.cwd === "/w" && formatRuntimeModel(info) === "p/m" && info.idle === true,
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`\ncheck-entwurf-control-rpc: ${passed} checks passed`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await main();
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-deliverability — deterministic gate for the conversational-mailbox
|
|
3
|
+
* deliverability predicate (SE-1/SE-2 slice 2c). This is the predicate the enqueue
|
|
4
|
+
* sites (v1 fallback, MCP v1, pi-native v1, v2 decider/send-fallback) must consult in
|
|
5
|
+
* slice 2d before writing a .msg, so a reply never rots in a dead session's mailbox
|
|
6
|
+
* (SE-2) and is never enqueued for a backend that has no mailbox drain (SE-1, pi).
|
|
7
|
+
*
|
|
8
|
+
* Proves:
|
|
9
|
+
* - computeMetaReceiverActive: the shared atom — active ⟺ recordBacked ∧ ownerAlive ∧
|
|
10
|
+
* watchArmed; every axis required, undefined treated as false (fail-closed), each
|
|
11
|
+
* failure names its own cause.
|
|
12
|
+
* - mailboxConversationalDeliverable: deliverable ⟺ wakeMode "self-fetch" AND active.
|
|
13
|
+
* KEY ROWS — direct-inject (pi) is refused even when the receiver looks active
|
|
14
|
+
* (SE-1: no mailbox drain), and a self-fetch receiver with a dead owner / unarmed
|
|
15
|
+
* watch is refused (SE-2: would rot as garbage).
|
|
16
|
+
* - WIRING: the self-addressability predicate shares this exact atom (one source of
|
|
17
|
+
* truth for "active receiver"), so self-reply and target-enqueue cannot drift.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import {
|
|
25
|
+
computeMetaReceiverActive,
|
|
26
|
+
mailboxConversationalDeliverable,
|
|
27
|
+
receiverMarkerMatchesIdentity,
|
|
28
|
+
} from "../pi-extensions/lib/entwurf-deliverability.ts";
|
|
29
|
+
|
|
30
|
+
let passed = 0;
|
|
31
|
+
function ok(label: string, cond: boolean): void {
|
|
32
|
+
assert.ok(cond, label);
|
|
33
|
+
console.log(` ok ${label}`);
|
|
34
|
+
passed++;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
38
|
+
|
|
39
|
+
// ── computeMetaReceiverActive: the shared atom ──────────────────────────────
|
|
40
|
+
ok(
|
|
41
|
+
"active ⟺ recordBacked ∧ ownerAlive ∧ watchArmed",
|
|
42
|
+
computeMetaReceiverActive({ recordBacked: true, ownerAlive: true, watchArmed: true }).active === true,
|
|
43
|
+
);
|
|
44
|
+
ok(
|
|
45
|
+
"no backing record → inactive",
|
|
46
|
+
computeMetaReceiverActive({ recordBacked: false, ownerAlive: true, watchArmed: true }).active === false,
|
|
47
|
+
);
|
|
48
|
+
ok(
|
|
49
|
+
"owner dead → inactive",
|
|
50
|
+
computeMetaReceiverActive({ recordBacked: true, ownerAlive: false, watchArmed: true }).active === false,
|
|
51
|
+
);
|
|
52
|
+
ok(
|
|
53
|
+
"watch unarmed → inactive",
|
|
54
|
+
computeMetaReceiverActive({ recordBacked: true, ownerAlive: true, watchArmed: false }).active === false,
|
|
55
|
+
);
|
|
56
|
+
ok("undefined axes → fail-closed inactive", computeMetaReceiverActive({ recordBacked: true }).active === false);
|
|
57
|
+
// each failure names its own cause (no conflation)
|
|
58
|
+
ok(
|
|
59
|
+
"owner-dead reason mentions start-key",
|
|
60
|
+
/start-key/.test(computeMetaReceiverActive({ recordBacked: true, ownerAlive: false, watchArmed: true }).reason),
|
|
61
|
+
);
|
|
62
|
+
ok(
|
|
63
|
+
"watch-unarmed reason mentions doorbell",
|
|
64
|
+
/doorbell/.test(computeMetaReceiverActive({ recordBacked: true, ownerAlive: true, watchArmed: false }).reason),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// ── mailboxConversationalDeliverable: the enqueue gate ──────────────────────
|
|
68
|
+
const D = (f: Parameters<typeof mailboxConversationalDeliverable>[0]) =>
|
|
69
|
+
mailboxConversationalDeliverable(f).deliverable;
|
|
70
|
+
|
|
71
|
+
ok(
|
|
72
|
+
"self-fetch + active → deliverable",
|
|
73
|
+
D({ wakeMode: "self-fetch", recordBacked: true, ownerAlive: true, watchArmed: true }) === true,
|
|
74
|
+
);
|
|
75
|
+
// SE-1 KEY ROW: a direct-inject backend (pi) has no mailbox drain — refuse even when
|
|
76
|
+
// the receiver otherwise looks fully active.
|
|
77
|
+
ok(
|
|
78
|
+
"direct-inject (pi) + active → NOT deliverable (no mailbox drain — SE-1)",
|
|
79
|
+
D({ wakeMode: "direct-inject", recordBacked: true, ownerAlive: true, watchArmed: true }) === false,
|
|
80
|
+
);
|
|
81
|
+
// SE-2 KEY ROWS: self-fetch but the receiver is gone / never armed.
|
|
82
|
+
ok(
|
|
83
|
+
"self-fetch + owner-dead → NOT deliverable (terminated session — SE-2)",
|
|
84
|
+
D({ wakeMode: "self-fetch", recordBacked: true, ownerAlive: false, watchArmed: true }) === false,
|
|
85
|
+
);
|
|
86
|
+
ok(
|
|
87
|
+
"self-fetch + watch-unarmed → NOT deliverable (no doorbell wake — SE-2)",
|
|
88
|
+
D({ wakeMode: "self-fetch", recordBacked: true, ownerAlive: true, watchArmed: false }) === false,
|
|
89
|
+
);
|
|
90
|
+
ok(
|
|
91
|
+
"self-fetch + no record → NOT deliverable",
|
|
92
|
+
D({ wakeMode: "self-fetch", recordBacked: false, ownerAlive: true, watchArmed: true }) === false,
|
|
93
|
+
);
|
|
94
|
+
ok("unset wakeMode → NOT deliverable", D({ recordBacked: true, ownerAlive: true, watchArmed: true }) === false);
|
|
95
|
+
ok(
|
|
96
|
+
"unknown wakeMode string → fail-closed NOT deliverable",
|
|
97
|
+
D({ wakeMode: "direct-inject-future", recordBacked: true, ownerAlive: true, watchArmed: true }) === false,
|
|
98
|
+
);
|
|
99
|
+
// reasons distinguish the wake-mode refusal from the receiver-inactive refusal
|
|
100
|
+
ok(
|
|
101
|
+
"direct-inject refusal reason names wake mode",
|
|
102
|
+
/wake mode/.test(
|
|
103
|
+
mailboxConversationalDeliverable({
|
|
104
|
+
wakeMode: "direct-inject",
|
|
105
|
+
recordBacked: true,
|
|
106
|
+
ownerAlive: true,
|
|
107
|
+
watchArmed: true,
|
|
108
|
+
}).reason,
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
ok(
|
|
112
|
+
"self-fetch inactive refusal reason names the receiver",
|
|
113
|
+
/receiver inactive/.test(
|
|
114
|
+
mailboxConversationalDeliverable({
|
|
115
|
+
wakeMode: "self-fetch",
|
|
116
|
+
recordBacked: true,
|
|
117
|
+
ownerAlive: false,
|
|
118
|
+
watchArmed: true,
|
|
119
|
+
}).reason,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ── receiverMarkerMatchesIdentity: the marker ↔ identity SSOT (SE-2 2d-3) ───
|
|
124
|
+
// A present marker only raises a receiver to active when it agrees with the record on
|
|
125
|
+
// garden/backend/native id. The v1 mailbox guard AND the v2 production deliverability seam
|
|
126
|
+
// both route through this one helper, so presence-only false-positives are closed in both.
|
|
127
|
+
const ID = { gardenId: "20260612T100000-aaaaaa", backend: "claude-code", nativeSessionId: "n-1" };
|
|
128
|
+
ok(
|
|
129
|
+
"marker matches identity on all three axes → true",
|
|
130
|
+
receiverMarkerMatchesIdentity(
|
|
131
|
+
{ gardenId: ID.gardenId, backend: ID.backend, nativeSessionId: ID.nativeSessionId },
|
|
132
|
+
ID,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
ok(
|
|
136
|
+
"garden id drift → false (foreign marker)",
|
|
137
|
+
!receiverMarkerMatchesIdentity({ ...ID, gardenId: "20260612T999999-bbbbbb" }, ID),
|
|
138
|
+
);
|
|
139
|
+
ok("backend drift → false", !receiverMarkerMatchesIdentity({ ...ID, backend: "pi" }, ID));
|
|
140
|
+
ok(
|
|
141
|
+
"native session id drift → false (stale/recycled marker)",
|
|
142
|
+
!receiverMarkerMatchesIdentity({ ...ID, nativeSessionId: "n-2" }, ID),
|
|
143
|
+
);
|
|
144
|
+
ok("null marker → false (fail-closed)", !receiverMarkerMatchesIdentity(null, ID));
|
|
145
|
+
ok("undefined marker → false (fail-closed)", !receiverMarkerMatchesIdentity(undefined, ID));
|
|
146
|
+
|
|
147
|
+
// ── WIRING: self-addressability shares the SAME atom ────────────────────────
|
|
148
|
+
const selfSrc = readFileSync(path.join(REPO_DIR, "pi-extensions", "lib", "entwurf-self-address.ts"), "utf8");
|
|
149
|
+
ok(
|
|
150
|
+
"entwurf-self-address imports computeMetaReceiverActive from the deliverability lib",
|
|
151
|
+
/computeMetaReceiverActive/.test(selfSrc) && /entwurf-deliverability\.ts/.test(selfSrc),
|
|
152
|
+
);
|
|
153
|
+
ok(
|
|
154
|
+
"entwurf-self-address calls computeMetaReceiverActive (no duplicated conjunction)",
|
|
155
|
+
/computeMetaReceiverActive\s*\(/.test(selfSrc),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
console.log(`\ncheck-entwurf-deliverability: ${passed} checks passed`);
|