@junghanacs/entwurf 0.40.0 → 0.41.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 +4 -2
- package/BASELINE.md +55 -2
- package/CHANGELOG.md +321 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +18 -2
- package/FAQ.md +8 -4
- package/README.md +26 -8
- package/VERIFY.md +13 -10
- package/docs/acp-backend-rail.md +5 -1
- package/docs/external-mcp-host.md +8 -6
- package/docs/fresh-cut-policy.md +9 -1
- package/docs/setup-clean-host.md +24 -8
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +81 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py +236 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-request.js +473 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-retire.js +125 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire-production.js +149 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire.js +158 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +283 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +7 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +96 -16
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +5 -29
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +28 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +26 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-retire-request.js +76 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +67 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/retirement-event.js +211 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +124 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -2
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +93 -19
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -1
- package/package.json +4 -4
- package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
- package/pi-extensions/entwurf-control.ts +55 -20
- package/pi-extensions/lib/cc-retire-pidfd.py +236 -0
- package/pi-extensions/lib/cc-retire-request.ts +553 -0
- package/pi-extensions/lib/control-retire.ts +195 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +17 -1
- package/pi-extensions/lib/entwurf-v2-retire-production.ts +227 -0
- package/pi-extensions/lib/entwurf-v2-retire.ts +269 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +2 -1
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +390 -36
- package/pi-extensions/lib/fresh-call-composition.ts +15 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +45 -4
- package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
- package/pi-extensions/lib/meta-session.ts +140 -16
- package/pi-extensions/lib/mux-fresh-call.ts +7 -27
- package/pi-extensions/lib/mux-launch.ts +29 -0
- package/pi-extensions/lib/mux-resume-call.ts +33 -12
- package/pi-extensions/lib/pi-retire-request.ts +111 -0
- package/pi-extensions/lib/resume-launch-identity.ts +104 -18
- package/pi-extensions/lib/retirement-event.ts +253 -0
- package/pi-extensions/meta-bridge-hook.ts +142 -13
- package/pi-extensions/meta-bridge-pi-durable.ts +56 -2
- package/run.sh +78 -7
- package/scripts/agy-bridge-config.py +4 -1
- package/scripts/build-bridge.sh +14 -0
- package/scripts/check-capability-bundle-reach.ts +135 -2
- package/scripts/check-entwurf-bridge-boot.ts +2 -1
- package/scripts/check-entwurf-fact-provider.ts +1 -0
- package/scripts/check-entwurf-v2-production.ts +2 -0
- package/scripts/check-entwurf-v2-surface.ts +7 -0
- package/scripts/check-gate-qualification.ts +14 -8
- package/scripts/check-herdr-activation.ts +2 -2
- package/scripts/check-herdr-placement.ts +1 -0
- package/scripts/check-hook-launch-topology.ts +6 -15
- package/scripts/check-install-container.sh +2 -2
- package/scripts/check-meta-doctor-oracle.sh +27 -1
- package/scripts/check-meta-facts.ts +8 -3
- package/scripts/check-meta-hook-session-switch.ts +230 -13
- package/scripts/check-meta-identity-consumers.ts +1 -0
- package/scripts/check-meta-listing.ts +1 -0
- package/scripts/check-meta-manifest-schema.py +1 -1
- package/scripts/check-meta-session.ts +31 -0
- package/scripts/check-meta-v3-record.ts +55 -1
- package/scripts/check-mux-resume-call.ts +35 -9
- package/scripts/check-peer-facts.ts +1 -0
- package/scripts/check-pi-durable-contact.ts +3 -1
- package/scripts/check-pi-durable-send.ts +4 -2
- package/scripts/check-pi-mcp-bridge.ts +1 -0
- package/scripts/check-resume-launch-identity.ts +62 -23
- package/scripts/lib/meta-hook-bundle.ts +72 -0
- package/scripts/meta-bridge-claude-floor.sh +13 -9
- package/scripts/meta-bridge-doctor.sh +17 -0
- package/scripts/meta-bridge-install.sh +6 -0
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/bridge-boot-resume.json +5 -2
- package/scripts/mutants/cc-retire-pidfd.json +129 -0
- package/scripts/mutants/cc-retire-request.json +171 -0
- package/scripts/mutants/control-retire.json +105 -0
- package/scripts/mutants/copilot-receive.json +2 -6
- package/scripts/mutants/entwurf-v2-retire.json +476 -0
- package/scripts/mutants/fresh-call-dispatch.json +37 -0
- package/scripts/mutants/meta-hook-session-switch.json +88 -0
- package/scripts/mutants/meta-identity.json +57 -0
- package/scripts/mutants/mux-fresh-call.json +1 -1
- package/scripts/mutants/mux-resume-call.json +40 -4
- package/scripts/mutants/pi-durable.json +79 -0
- package/scripts/mutants/pi-retire-request.json +72 -0
- package/scripts/mutants/resume-launch-identity.json +35 -8
- package/scripts/mutants/retirement-event.json +195 -0
- package/scripts/mutants/v2-surface.json +16 -0
- package/scripts/mutants/v2-visible-resume.json +283 -13
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-raw-install-live.sh +5 -4
- package/scripts/smoke-meta-install-state.sh +4 -4
- package/scripts/smoke-mux-lifecycle-live.ts +10 -10
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
- package/scripts/smoke-pi-provider-state.sh +3 -3
|
@@ -28,8 +28,14 @@
|
|
|
28
28
|
* - B4: the live load seam `metaCapabilityFor()` returns for every
|
|
29
29
|
* META_CITIZEN_BACKENDS backend without throwing — the real `entwurf_v2` path.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* - C (#147 L2 B1): the compiled Claude Code hook's static `from` closure equals what
|
|
32
|
+
* the installer copies, and the hook staged with only that closure runs.
|
|
33
|
+
* - D (#147 G3): the compiled Claude Code retire adapter resolves its helper beside itself,
|
|
34
|
+
* byte-identical to pi-extensions/lib/cc-retire-pidfd.py.
|
|
35
|
+
*
|
|
36
|
+
* Deterministic: filesystem, import, and ONE shipped-hook child process (C2) that writes a
|
|
37
|
+
* record into its own private store under a private HOME and XDG roots. No backend, vendor
|
|
38
|
+
* CLI, model, network or API. Safe in the `pnpm check` static floor.
|
|
33
39
|
*
|
|
34
40
|
* REQUIRES a built bridge (`pnpm run build-bridge`, which `prepare`/`prepack` run).
|
|
35
41
|
* A missing dist is a FAILURE, never a skip — a gate that skips when the artifact
|
|
@@ -37,9 +43,13 @@
|
|
|
37
43
|
*/
|
|
38
44
|
|
|
39
45
|
import assert from "node:assert/strict";
|
|
46
|
+
import { spawnSync } from "node:child_process";
|
|
40
47
|
import fs from "node:fs";
|
|
48
|
+
import os from "node:os";
|
|
41
49
|
import path from "node:path";
|
|
42
50
|
import { pathToFileURL } from "node:url";
|
|
51
|
+
import { hookImportClosure, installerCopiedLibFiles, stageMetaHookBundle } from "./lib/meta-hook-bundle.ts";
|
|
52
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
43
53
|
|
|
44
54
|
const REPO = path.join(import.meta.dirname, "..");
|
|
45
55
|
|
|
@@ -127,4 +137,127 @@ for (const copy of copies) {
|
|
|
127
137
|
}
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
// C — the COMPILED Claude Code hook bundle reaches its whole closure (#147 L2 B1). Moved here from
|
|
141
|
+
// check-meta-hook-session-switch, whose source-only qualification snapshot has no dist: this gate
|
|
142
|
+
// already requires the built bundle, so the emission proof sits with the dist consumer. Scope is
|
|
143
|
+
// the static double-quoted `from` imports scripts/lib/meta-hook-bundle.ts follows; the staged RUN
|
|
144
|
+
// is the independent proof that nothing outside them is needed.
|
|
145
|
+
{
|
|
146
|
+
const DIST_HOOK = path.join(BRIDGE_DIST, "pi-extensions", "meta-bridge-hook.js");
|
|
147
|
+
ok(
|
|
148
|
+
"C0: the compiled hook entry exists (a missing dist is a failure, not a skip)",
|
|
149
|
+
fs.existsSync(DIST_HOOK),
|
|
150
|
+
`--- expected ---\n${DIST_HOOK}\nrun: pnpm run build-bridge`,
|
|
151
|
+
);
|
|
152
|
+
const installerCopies = installerCopiedLibFiles(
|
|
153
|
+
fs.readFileSync(path.join(REPO, "scripts", "meta-bridge-install.sh"), "utf8"),
|
|
154
|
+
);
|
|
155
|
+
const distClosure = hookImportClosure(DIST_HOOK)
|
|
156
|
+
.map((rel) => path.basename(rel))
|
|
157
|
+
.map((base) => (base === "session-id.js" ? base : base.replace(/\.(ts|js)$/, ".$LIB_EXT")))
|
|
158
|
+
.sort();
|
|
159
|
+
ok(
|
|
160
|
+
"C1: the installer copies exactly the compiled hook's static `from` import closure [QK:BUNDLE-REACH-COMPILED-HOOK-CLOSURE]",
|
|
161
|
+
distClosure.includes("entwurf-v2-lock.$LIB_EXT") && JSON.stringify(distClosure) === JSON.stringify(installerCopies),
|
|
162
|
+
`--- compiled closure ---\n${distClosure.join("\n")}\n--- installer copies ---\n${installerCopies.join("\n")}`,
|
|
163
|
+
);
|
|
164
|
+
// Stage the compiled entry with ONLY that closure and run it the way Claude does: the shipped
|
|
165
|
+
// manifest's exec argv through the shipped launcher, every meta root sandboxed, provenance never
|
|
166
|
+
// inherited. A record on disk means the compiled closure loaded end to end.
|
|
167
|
+
const PLUGIN_SRC = path.join(REPO, "pi", "meta-bridge", "entwurf-meta-receive");
|
|
168
|
+
const leaf =
|
|
169
|
+
(
|
|
170
|
+
JSON.parse(fs.readFileSync(path.join(PLUGIN_SRC, "hooks", "hooks.json"), "utf8")) as {
|
|
171
|
+
hooks: Record<string, Array<{ hooks: Array<{ command?: string; args?: string[] }> }>>;
|
|
172
|
+
}
|
|
173
|
+
).hooks.SessionStart?.[0]?.hooks[0] ?? {};
|
|
174
|
+
const root = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "psa-bundle-reach-jshook-")));
|
|
175
|
+
const agent = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "psa-bundle-reach-jsagent-")));
|
|
176
|
+
stageMetaHookBundle(root, DIST_HOOK, path.join(PLUGIN_SRC, "scripts", "hook-launch.sh"));
|
|
177
|
+
const el = (v: string) =>
|
|
178
|
+
v
|
|
179
|
+
.replace("__NODE_BIN__", process.execPath)
|
|
180
|
+
.replaceAll("${CLAUDE_PLUGIN_ROOT}", root)
|
|
181
|
+
.replace("__HOOK_ENTRY__", "meta-bridge-hook.js");
|
|
182
|
+
// Hard Rule 12: the child's HOME and every writable XDG root are private too, not only the
|
|
183
|
+
// meta roots — a hook that resolved anything through HOME/XDG must land inside `agent`.
|
|
184
|
+
const priv = (name: string) => {
|
|
185
|
+
const dir = path.join(agent, name);
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
187
|
+
return dir;
|
|
188
|
+
};
|
|
189
|
+
const env: NodeJS.ProcessEnv = {
|
|
190
|
+
...process.env,
|
|
191
|
+
HOME: priv("home"),
|
|
192
|
+
XDG_CONFIG_HOME: priv("xdg-config"),
|
|
193
|
+
XDG_CACHE_HOME: priv("xdg-cache"),
|
|
194
|
+
XDG_DATA_HOME: priv("xdg-data"),
|
|
195
|
+
XDG_STATE_HOME: priv("xdg-state"),
|
|
196
|
+
XDG_RUNTIME_DIR: priv("xdg-runtime"),
|
|
197
|
+
CLAUDE_PLUGIN_ROOT: root,
|
|
198
|
+
PI_CODING_AGENT_DIR: agent,
|
|
199
|
+
ENTWURF_META_SESSIONS_DIR: path.join(agent, "meta-sessions"),
|
|
200
|
+
ENTWURF_META_SENDERS_DIR: path.join(agent, "meta-senders"),
|
|
201
|
+
ENTWURF_META_RECEIVERS_DIR: path.join(agent, "meta-receivers"),
|
|
202
|
+
ENTWURF_META_MAILBOX_DIR: path.join(agent, "meta-mailbox"),
|
|
203
|
+
};
|
|
204
|
+
delete env.ENTWURF_META_HOOK_LAUNCH;
|
|
205
|
+
const run = spawnSync(el(leaf.command ?? ""), (leaf.args ?? []).map(el), {
|
|
206
|
+
encoding: "utf8",
|
|
207
|
+
input: JSON.stringify({
|
|
208
|
+
hook_event_name: "SessionStart",
|
|
209
|
+
session_id: "c0c0c0c0-compiled-closure",
|
|
210
|
+
transcript_path: path.join(agent, "c0c0c0c0.jsonl"),
|
|
211
|
+
cwd: agent,
|
|
212
|
+
source: "startup",
|
|
213
|
+
}),
|
|
214
|
+
env,
|
|
215
|
+
});
|
|
216
|
+
const sessions = path.join(agent, "meta-sessions");
|
|
217
|
+
ok(
|
|
218
|
+
"C2: the compiled hook, staged with only that closure, runs to a written record [QK:BUNDLE-REACH-COMPILED-HOOK-RUNS]",
|
|
219
|
+
run.status === 0 && fs.existsSync(sessions) && fs.readdirSync(sessions).some((n) => n.endsWith(".meta.json")),
|
|
220
|
+
`--- exit ${run.status} ---\n${run.stderr}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// D — the COMPILED Claude Code retire adapter reaches its helper from where it lives (#147 G3).
|
|
225
|
+
// `ccRetireHelperPath()` resolves a fixed basename beside its own module, which is a tautology from
|
|
226
|
+
// source; only the emitted copy can say whether build-bridge.sh put the bytes next to it. Import and
|
|
227
|
+
// path only — nothing is spawned or signalled.
|
|
228
|
+
{
|
|
229
|
+
const DIST_ADAPTER = path.join(BRIDGE_DIST, "pi-extensions", "lib", "cc-retire-request.js");
|
|
230
|
+
const HELPER_SOURCE = path.join(REPO, "pi-extensions", "lib", "cc-retire-pidfd.py");
|
|
231
|
+
ok(
|
|
232
|
+
"D0: the compiled retire adapter exists (a missing dist is a failure, not a skip)",
|
|
233
|
+
fs.existsSync(DIST_ADAPTER),
|
|
234
|
+
`--- expected ---\n${DIST_ADAPTER}\nrun: pnpm run build-bridge`,
|
|
235
|
+
);
|
|
236
|
+
const adapter = await import(pathToFileURL(DIST_ADAPTER).href);
|
|
237
|
+
const resolved: string | null = adapter.ccRetireHelperPath();
|
|
238
|
+
ok(
|
|
239
|
+
"D1: the compiled adapter resolves the helper beside itself, byte-identical to the canonical source [QK:BUNDLE-REACH-CC-RETIRE-HELPER]",
|
|
240
|
+
resolved === path.join(path.dirname(DIST_ADAPTER), "cc-retire-pidfd.py") &&
|
|
241
|
+
fs.readFileSync(resolved).equals(fs.readFileSync(HELPER_SOURCE)),
|
|
242
|
+
`--- resolved ---\n${resolved}\n--- canonical ---\n${HELPER_SOURCE}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// E — ask the actual emitted Pi request adapter, not its source-path test import.
|
|
247
|
+
// Pure parsing only: no socket, SDK lifecycle, model, or operator state is touched.
|
|
248
|
+
{
|
|
249
|
+
const entry = path.join(BRIDGE_DIST, "pi-extensions", "lib", "pi-retire-request.js");
|
|
250
|
+
ok("E0: the compiled Pi retire adapter exists (missing emission is not a skip)", fs.existsSync(entry));
|
|
251
|
+
const adapter = await import(pathToFileURL(entry).href);
|
|
252
|
+
const response = { type: "response", command: "retire", success: true, data: { boundary: "shutdown-requested" } };
|
|
253
|
+
const accepted = adapter.parsePiRetireResponse(response);
|
|
254
|
+
const rejected = adapter.parsePiRetireResponse({ ...response, command: "send" });
|
|
255
|
+
ok(
|
|
256
|
+
"E1: the shipped Pi adapter loads, accepts a requested reply, and rejects a foreign command as unknown [QK:BUNDLE-REACH-PI-RETIRE-ADAPTER]",
|
|
257
|
+
accepted.boundary === "shutdown-requested" &&
|
|
258
|
+
rejected.boundary === "unknown" &&
|
|
259
|
+
rejected.reason === "malformed-response",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
130
263
|
console.log(`\ncheck-capability-bundle-reach: PASS (${passed} assertions)`);
|
|
@@ -253,11 +253,12 @@ async function main(): Promise<void> {
|
|
|
253
253
|
"entwurf_peers",
|
|
254
254
|
"entwurf_register_native",
|
|
255
255
|
"entwurf_resume_call",
|
|
256
|
+
"entwurf_retire_call",
|
|
256
257
|
"entwurf_self",
|
|
257
258
|
"entwurf_v2",
|
|
258
259
|
];
|
|
259
260
|
ok(
|
|
260
|
-
"[QK:BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET] G1f: the runtime tools/list surface is EXACTLY the
|
|
261
|
+
"[QK:BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET] G1f: the runtime tools/list surface is EXACTLY the nine garden verbs — no missing verb, no undecided extra, no duplicate",
|
|
261
262
|
publicSurface.length === expectedSurface.length && expectedSurface.every((n, i) => publicSurface[i] === n),
|
|
262
263
|
`--- want ---\n${expectedSurface.join(",")}\n--- got ---\n${publicSurface.join(",")}`,
|
|
263
264
|
);
|
|
@@ -80,6 +80,7 @@ function identity(backend: MetaIdentity["backend"], gardenId = GID): MetaIdentit
|
|
|
80
80
|
transcriptPath: null,
|
|
81
81
|
createdAt: "2026-06-13T01:00:00.000Z",
|
|
82
82
|
recordUpdatedAt: "2026-06-13T01:00:00.000Z",
|
|
83
|
+
attachIncarnation: null,
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -97,6 +98,7 @@ function receiverMarker(gid: string, backend: string, nativeSessionId = "n"): Me
|
|
|
97
98
|
ownerKind: "claude-code-cli",
|
|
98
99
|
armProvenance: "session-start",
|
|
99
100
|
updatedAt: "t",
|
|
101
|
+
attachIncarnation: null,
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -630,6 +630,13 @@ async function main(): Promise<void> {
|
|
|
630
630
|
/visible-first/.test(dormantHint) &&
|
|
631
631
|
/entwurf_resume_call/.test(dormantHint),
|
|
632
632
|
);
|
|
633
|
+
ok(
|
|
634
|
+
"[QK:V2SURF-RESUME-HINT-SCOPE] dormant hint names Pi/Claude Code promptless resume only on explicit operator request, never automatic permission",
|
|
635
|
+
dormantHint.includes("Only on an explicit operator request") &&
|
|
636
|
+
dormantHint.includes("entwurf_resume_call {target} for Pi or Claude Code") &&
|
|
637
|
+
dormantHint.includes("it submits no prompt") &&
|
|
638
|
+
dormantHint.includes("not permission to resume automatically"),
|
|
639
|
+
);
|
|
633
640
|
// The frozen wire id still spells "-no-spawn" although nothing spawns anymore. The id is
|
|
634
641
|
// deliberately NOT renamed (renaming a public reject is its own contract cut), so the
|
|
635
642
|
// hint carries the whole correction: probe inconclusive, and nothing ran.
|
|
@@ -3449,9 +3449,15 @@ let manifestCount: number;
|
|
|
3449
3449
|
"agy-permission": 6,
|
|
3450
3450
|
"bridge-boot-resume": 5,
|
|
3451
3451
|
"bridge-command-boot": 12,
|
|
3452
|
+
"cc-retire-pidfd": 11,
|
|
3453
|
+
"cc-retire-request": 15,
|
|
3454
|
+
"entwurf-v2-retire": 32,
|
|
3452
3455
|
"capability-cache": 3,
|
|
3453
3456
|
"codex-app-server-launch": 10,
|
|
3454
3457
|
"codex-caller-seat": 28,
|
|
3458
|
+
"pi-retire-request": 6,
|
|
3459
|
+
"retirement-event": 16,
|
|
3460
|
+
"control-retire": 9,
|
|
3455
3461
|
"control-send-receipt": 19,
|
|
3456
3462
|
"codex-native": 87,
|
|
3457
3463
|
"compaction-send-guard": 7,
|
|
@@ -3464,10 +3470,10 @@ let manifestCount: number;
|
|
|
3464
3470
|
"pi-bridge-sender": 4,
|
|
3465
3471
|
"pi-mcp-bridge": 2,
|
|
3466
3472
|
"pi-mcp-register": 3,
|
|
3467
|
-
"pi-durable":
|
|
3473
|
+
"pi-durable": 89,
|
|
3468
3474
|
"copilot-receive": 20,
|
|
3469
3475
|
"entwurf-peers": 1,
|
|
3470
|
-
"fresh-call-dispatch":
|
|
3476
|
+
"fresh-call-dispatch": 15,
|
|
3471
3477
|
"fresh-cut": 3,
|
|
3472
3478
|
"gate-qualification": 10,
|
|
3473
3479
|
"herdr-placement": 13,
|
|
@@ -3481,8 +3487,8 @@ let manifestCount: number;
|
|
|
3481
3487
|
"herdr-supply": 11,
|
|
3482
3488
|
"meta-facts": 4,
|
|
3483
3489
|
"peer-facts": 8,
|
|
3484
|
-
"meta-hook-session-switch":
|
|
3485
|
-
"meta-identity":
|
|
3490
|
+
"meta-hook-session-switch": 25,
|
|
3491
|
+
"meta-identity": 9,
|
|
3486
3492
|
"meta-retire": 4,
|
|
3487
3493
|
"mux-boundary": 16,
|
|
3488
3494
|
"mux-fresh-call": 65,
|
|
@@ -3491,7 +3497,7 @@ let manifestCount: number;
|
|
|
3491
3497
|
"pack-install": 5,
|
|
3492
3498
|
"pi-package-ownership": 8,
|
|
3493
3499
|
"qualification-ledger": 42,
|
|
3494
|
-
"mux-resume-call":
|
|
3500
|
+
"mux-resume-call": 13,
|
|
3495
3501
|
"omp-birth": 13,
|
|
3496
3502
|
"omp-fresh": 24,
|
|
3497
3503
|
"omp-receive": 11,
|
|
@@ -3505,13 +3511,13 @@ let manifestCount: number;
|
|
|
3505
3511
|
"remote-pair-transport": 4,
|
|
3506
3512
|
"retired-sent-surface": 2,
|
|
3507
3513
|
"resume-args": 6,
|
|
3508
|
-
"resume-launch-identity":
|
|
3514
|
+
"resume-launch-identity": 10,
|
|
3509
3515
|
"self-address": 8,
|
|
3510
3516
|
"setup-verdict": 14,
|
|
3511
3517
|
"source-install": 2,
|
|
3512
3518
|
"typing-call-fence": 1,
|
|
3513
|
-
"v2-surface":
|
|
3514
|
-
"v2-visible-resume":
|
|
3519
|
+
"v2-surface": 14,
|
|
3520
|
+
"v2-visible-resume": 40,
|
|
3515
3521
|
"win32-preview": 19,
|
|
3516
3522
|
};
|
|
3517
3523
|
const laneTally: Record<string, number> = {};
|
|
@@ -113,7 +113,7 @@ function fakeClaude(env: NodeJS.ProcessEnv): string {
|
|
|
113
113
|
"#!/usr/bin/env bash",
|
|
114
114
|
'printf \'%s\\n\' "$*" >> "${FAKE_CLAUDE_LOG:?}"',
|
|
115
115
|
'case "$1${2:+ $2}" in',
|
|
116
|
-
' "--version") echo "2.1.
|
|
116
|
+
' "--version") echo "2.1.306 (Claude Code)" ;;',
|
|
117
117
|
' "plugin validate") : ;;',
|
|
118
118
|
' "plugin uninstall") : ;;',
|
|
119
119
|
' "plugin marketplace") : ;;',
|
|
@@ -1317,7 +1317,7 @@ function ledgerWith(
|
|
|
1317
1317
|
[
|
|
1318
1318
|
"#!/usr/bin/env bash",
|
|
1319
1319
|
'case "$1 $2" in',
|
|
1320
|
-
' "--version "*|"--version") echo "2.1.
|
|
1320
|
+
' "--version "*|"--version") echo "2.1.306 (Claude Code)" ;;',
|
|
1321
1321
|
' *) echo "broken fixture" >&2; exit 1 ;;',
|
|
1322
1322
|
"esac",
|
|
1323
1323
|
].join("\n"),
|
|
@@ -41,10 +41,11 @@
|
|
|
41
41
|
|
|
42
42
|
import assert from "node:assert/strict";
|
|
43
43
|
import { spawnSync } from "node:child_process";
|
|
44
|
-
import {
|
|
44
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
45
45
|
import { tmpdir } from "node:os";
|
|
46
46
|
import * as path from "node:path";
|
|
47
47
|
import { fileURLToPath } from "node:url";
|
|
48
|
+
import { stageMetaHookBundle } from "./lib/meta-hook-bundle.ts";
|
|
48
49
|
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
49
50
|
|
|
50
51
|
let passed = 0;
|
|
@@ -191,7 +192,7 @@ for (const event of OWNER_EVENTS) {
|
|
|
191
192
|
const bare = spawnSync(LAUNCHER, [], { encoding: "utf8", input: "" });
|
|
192
193
|
ok("launcher with EMPTY argv exits non-zero", bare.status !== null && bare.status !== 0);
|
|
193
194
|
ok("launcher with empty argv names the args drop on stderr", /discarded the hook's `args`/.test(bare.stderr));
|
|
194
|
-
ok("launcher with empty argv names the required floor", /2\.1\.
|
|
195
|
+
ok("launcher with empty argv names the required floor", /2\.1\.306/.test(bare.stderr));
|
|
195
196
|
ok("launcher with empty argv refuses a fallback", /no shell-form fallback/i.test(bare.stderr));
|
|
196
197
|
ok("launcher writes nothing to stdout on refusal (never a hook response)", bare.stdout === "");
|
|
197
198
|
}
|
|
@@ -232,19 +233,9 @@ for (const event of OWNER_EVENTS) {
|
|
|
232
233
|
// exactly what the metacharacter cell below silently failed to prove in its first
|
|
233
234
|
// version (cross-review, 2026-07-22).
|
|
234
235
|
function makeBundle(root: string): void {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
copyFileSync(
|
|
239
|
-
path.join(REPO_DIR, "pi-extensions", "lib", "meta-session.ts"),
|
|
240
|
-
path.join(root, "lib", "meta-session.ts"),
|
|
241
|
-
);
|
|
242
|
-
copyFileSync(path.join(REPO_DIR, "pi-extensions", "lib", "session-id.js"), path.join(root, "lib", "session-id.js"));
|
|
243
|
-
const launcher = path.join(root, "scripts", "hook-launch.sh");
|
|
244
|
-
copyFileSync(LAUNCHER, launcher);
|
|
245
|
-
// The exec form names the launcher as the executable, so the +x bit is load-bearing:
|
|
246
|
-
// without it Claude gets ENOEXEC, not a degraded path. The installer chmods it too.
|
|
247
|
-
chmodSync(launcher, 0o755);
|
|
236
|
+
// Entry + its OWN import closure + the launcher (scripts/lib/meta-hook-bundle.ts): a hand-kept
|
|
237
|
+
// list here could stay green while the installer missed a file the hook now imports.
|
|
238
|
+
stageMetaHookBundle(root, path.join(REPO_DIR, "pi-extensions", "meta-bridge-hook.ts"), LAUNCHER);
|
|
248
239
|
}
|
|
249
240
|
|
|
250
241
|
/** Resolve one manifest element the way Claude does: plain-string substitution, no shell. */
|
|
@@ -321,7 +321,7 @@ function finish(t) {
|
|
|
321
321
|
// artifact ship" is precisely the question it owns. The subset form named only the five
|
|
322
322
|
// pre-0.14 verbs and would have accepted a candidate with no entwurf_fresh_call /
|
|
323
323
|
// entwurf_resume_call; equality also refuses an extra or duplicated verb.
|
|
324
|
-
const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
324
|
+
const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_retire_call', 'entwurf_self', 'entwurf_v2'];
|
|
325
325
|
if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
|
|
326
326
|
console.error('MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
|
|
327
327
|
process.exit(1);
|
|
@@ -357,7 +357,7 @@ cat > "$FAKEBIN/claude" <<'SH'
|
|
|
357
357
|
#!/usr/bin/env bash
|
|
358
358
|
printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
|
|
359
359
|
case "$1${2:+ $2}${3:+ $3}" in
|
|
360
|
-
"--version") echo "2.1.
|
|
360
|
+
"--version") echo "2.1.306 (Claude Code)" ;;
|
|
361
361
|
"plugin list --json")
|
|
362
362
|
printf '[{"id":"entwurf-meta-receive@meta-bridge-local","version":"0.1.0","enabled":true,"installPath":"%s"}]\n' "$FAKE_INSTALL_PATH" ;;
|
|
363
363
|
"plugin list"*) printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
|
|
@@ -91,7 +91,7 @@ cat > "$BIN/claude" <<'SH'
|
|
|
91
91
|
printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
|
|
92
92
|
case "$1${2:+ $2}${3:+ $3}" in
|
|
93
93
|
"--version")
|
|
94
|
-
echo "2.1.
|
|
94
|
+
echo "2.1.306 (Claude Code)" ;;
|
|
95
95
|
"plugin list --json")
|
|
96
96
|
# FAKE_NO_JSON stands in for a Claude floor with no --json surface at all.
|
|
97
97
|
[ "${FAKE_NO_JSON:-0}" = 1 ] && exit 0
|
|
@@ -214,6 +214,23 @@ if env CLAUDE_PLUGIN_ROOT="$PLANTED" "${HOOK_ARGV[@]}" < "$TMP/hook-input.json"
|
|
|
214
214
|
else
|
|
215
215
|
bad "owner fixture drive failed: $(tr '\n' ' ' < "$TMP/hook-run.txt" | cut -c1-300)"
|
|
216
216
|
fi
|
|
217
|
+
# The INSTALLED closure, not the checkout, committed the arm binding (#147 L2 B1): the record
|
|
218
|
+
# and the receiver marker the planted bundle wrote carry the same fresh value. A bundle missing
|
|
219
|
+
# the lock module its hook imports could not have loaded far enough to write either.
|
|
220
|
+
if python3 - "$AGENT" <<'PY'
|
|
221
|
+
import glob, json, os, re, sys
|
|
222
|
+
agent = sys.argv[1]
|
|
223
|
+
recs = [json.load(open(f)) for f in glob.glob(os.path.join(agent, "meta-sessions", "*.meta.json"))]
|
|
224
|
+
rec = next(r for r in recs if r.get("nativeSessionId") == "oracle-native-1")
|
|
225
|
+
marker = json.load(open(os.path.join(agent, "meta-receivers", f"{rec['gardenId']}.json")))
|
|
226
|
+
value = rec.get("attachIncarnation")
|
|
227
|
+
sys.exit(0 if isinstance(value, str) and re.fullmatch(r"[0-9a-f]{32}", value) and marker.get("attachIncarnation") == value else 1)
|
|
228
|
+
PY
|
|
229
|
+
then
|
|
230
|
+
ok "the installed bundle committed a paired arm binding (record == receiver marker)"
|
|
231
|
+
else
|
|
232
|
+
bad "[QK:DOCTOR-ORACLE-INSTALLED-ARM-BINDING] the installed bundle's arm did not leave a record and receiver marker carrying the same attachIncarnation"
|
|
233
|
+
fi
|
|
217
234
|
|
|
218
235
|
# Fake live entwurf MCP child of this same stand-in Claude pid. The doctor discovers it
|
|
219
236
|
# by env + agent dir, so a UNIQUE sandbox agent dir keeps it from colliding with the
|
|
@@ -553,6 +570,15 @@ mv "$PLANTED/lib/meta-session.ts" "$TMP/meta-session.bak"
|
|
|
553
570
|
expect_red "installed writer bundle missing" "deployed writer version is UNKNOWN"
|
|
554
571
|
mv "$TMP/meta-session.bak" "$PLANTED/lib/meta-session.ts"
|
|
555
572
|
|
|
573
|
+
# M10b/M10c — the hook's record-arm lock module (#147 L2 B1) is part of the deployed writer
|
|
574
|
+
# closure: missing and drifted bytes are each a named red, not a runtime-only surprise.
|
|
575
|
+
mv "$PLANTED/lib/entwurf-v2-lock.ts" "$TMP/entwurf-v2-lock.bak"
|
|
576
|
+
expect_red "installed hook lock module missing" "deployed hook lock module MISSING"
|
|
577
|
+
cp "$TMP/entwurf-v2-lock.bak" "$PLANTED/lib/entwurf-v2-lock.ts"
|
|
578
|
+
printf '\n// drift\n' >> "$PLANTED/lib/entwurf-v2-lock.ts"
|
|
579
|
+
expect_red "installed hook lock module drifted" "deployed hook lock module STALE"
|
|
580
|
+
mv "$TMP/entwurf-v2-lock.bak" "$PLANTED/lib/entwurf-v2-lock.ts"
|
|
581
|
+
|
|
556
582
|
# M11 — a FAILING `claude mcp get` whose output still matches. `|| true` would have
|
|
557
583
|
# read this as a healthy USER-scope reach; rc must survive the capture.
|
|
558
584
|
export FAKE_MCP_RC=3
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Drives the REAL CLI as a subprocess against sandboxed fixture stores (never the
|
|
6
6
|
* live store; every path is an explicit mkdtemp dir). What it proves:
|
|
7
7
|
*
|
|
8
|
-
* - the join is the FULL verbatim strict
|
|
8
|
+
* - the join is the FULL verbatim strict v3 record — gardenId,
|
|
9
9
|
* nativeSessionId AND transcriptPath cross together, sorted by gardenId;
|
|
10
10
|
* - drift / symlink / invalid-UTF-8 entries become in-band defects with exit 0,
|
|
11
11
|
* and a symlinked record's target bytes are never followed into the join;
|
|
@@ -58,6 +58,7 @@ function record(gardenId: string, over: Partial<MetaIdentity> = {}): string {
|
|
|
58
58
|
transcriptPath: `/t/${gardenId}.jsonl`,
|
|
59
59
|
createdAt: "2026-06-11T00:00:00.000Z",
|
|
60
60
|
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
61
|
+
attachIncarnation: null,
|
|
61
62
|
...over,
|
|
62
63
|
});
|
|
63
64
|
}
|
|
@@ -119,6 +120,7 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
119
120
|
transcriptPath: null,
|
|
120
121
|
createdAt: "2026-06-11T00:00:00.000Z",
|
|
121
122
|
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
123
|
+
attachIncarnation: null,
|
|
122
124
|
},
|
|
123
125
|
{
|
|
124
126
|
schemaVersion: 3,
|
|
@@ -130,11 +132,14 @@ function parseProjection(run: CliRun, label: string): Projection {
|
|
|
130
132
|
transcriptPath: `/t/${GID_B}.jsonl`,
|
|
131
133
|
createdAt: "2026-06-11T00:00:00.000Z",
|
|
132
134
|
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
135
|
+
attachIncarnation: null,
|
|
133
136
|
},
|
|
134
137
|
],
|
|
135
|
-
"[QK:MF-FULL-V3-JOIN] citizens are the full verbatim
|
|
138
|
+
"[QK:MF-FULL-V3-JOIN] citizens are the full verbatim v3 records (10 fields, attachIncarnation included), sorted by gardenId",
|
|
139
|
+
);
|
|
140
|
+
console.log(
|
|
141
|
+
" ok citizens are the full verbatim v3 records (10 fields, attachIncarnation included), sorted by gardenId",
|
|
136
142
|
);
|
|
137
|
-
console.log(" ok citizens are the full verbatim 9-field v3 records, sorted by gardenId");
|
|
138
143
|
passed++;
|
|
139
144
|
ok("valid store has no defects", facts.defects.length === 0);
|
|
140
145
|
const again = cli([dir]);
|