@junghanacs/entwurf 0.40.0 → 0.41.1
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 +562 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +18 -2
- package/FAQ.md +8 -4
- package/README.md +60 -45
- package/VERIFY.md +14 -11
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +6 -2
- package/docs/external-mcp-host.md +8 -6
- package/docs/fresh-cut-policy.md +9 -1
- package/docs/setup-clean-host.md +76 -20
- 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 +15 -14
- package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
- package/pi/pi-durable/carrier/tui.js +18 -3
- package/pi/pi-durable/overlay/upstream-pin.json +12 -12
- package/pi-extensions/entwurf-control.ts +55 -20
- package/pi-extensions/lib/acp/backend-adapter.ts +5 -3
- package/pi-extensions/lib/acp/backend.ts +12 -9
- 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 +57 -3
- package/run.sh +123 -34
- 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/codex-fresh-live-protocol.ts +3 -2
- 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/control-send-receipt.json +1 -1
- 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/pack-install.json +4 -4
- package/scripts/mutants/pi-durable.json +139 -0
- package/scripts/mutants/pi-floor.json +5 -5
- 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-codex-fresh-live.ts +2 -1
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-raw-install-live.sh +15 -7
- 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
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* check-resume-launch-identity — deterministic gate for `resume-launch-identity.ts`, the
|
|
3
|
-
* record-authoritative leaf
|
|
4
|
-
* caller it had.
|
|
3
|
+
* record-authoritative leaf of a visible same-id resume (its consumer is `entwurf-v2-visible-resume`).
|
|
5
4
|
*
|
|
6
|
-
* WHY A GATE FOR
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* would be re-adopted, months later, by a visible-resume composition that trusts prose. The cells
|
|
5
|
+
* WHY A GATE FOR AN IDENTITY LEAF. Identity authority is a fail-closed risk class: this leaf's whole
|
|
6
|
+
* job is deciding WHICH being a resume lands in. A leaf whose only guarantee is its own header
|
|
7
|
+
* comment would be trusted as prose by every composition that calls it. The cells
|
|
10
8
|
* below are transplanted from the deleted `check-entwurf-v2-spawn-production` §9 (the plan
|
|
11
9
|
* parameter became a bare garden id; nothing else about the identity contract changed), plus one
|
|
12
10
|
* new cell for the #52 read the leaf's header claims and the deleted gate never isolated.
|
|
@@ -25,14 +23,18 @@
|
|
|
25
23
|
* backend, no recorded transcript, recorded-but-missing file, no model, headerless file. The
|
|
26
24
|
* missing-file cell is a regression pin: before the existence check that case fell through
|
|
27
25
|
* `readSessionIdentity`'s ENOENT swallow and lied "no recorded model".
|
|
28
|
-
* 4b. THE ONE EXPECTED REFUSAL — a backend with no same-id resume
|
|
29
|
-
* record is fine and the operator reached past a
|
|
30
|
-
* TYPED reason (`target-
|
|
31
|
-
* of parsing a sentence or re-reading the
|
|
26
|
+
* 4b. THE ONE EXPECTED REFUSAL — a backend with no same-id resume (since #147 L2: anything but pi
|
|
27
|
+
* and claude-code) is not a defect at all: the record is fine and the operator reached past a
|
|
28
|
+
* capability boundary. It alone carries a TYPED reason (`target-backend-unsupported`) so the
|
|
29
|
+
* visible-resume composition can match a FIELD instead of parsing a sentence or re-reading the
|
|
30
|
+
* record. Everything else here stays a bare throw.
|
|
31
|
+
* 4e. CLAUDE CODE (#147 L2) — a claude-code record resolves to its own session id, the record's
|
|
32
|
+
* cwd and createdAt, reading no transcript; a session id outside the lowercase-UUID grammar and
|
|
33
|
+
* a recorded transcript that is not a file on disk are each refused with their own cause.
|
|
32
34
|
* 4c. ABSOLUTE TRANSCRIPT — a relative recorded path would resolve against the WINDOW's own cwd
|
|
33
35
|
* at launch, not against this process's, so it names a different file than the one checked
|
|
34
36
|
* here. Refused before anything opens.
|
|
35
|
-
* 5. SSOT — the module header names THIS gate, so the
|
|
37
|
+
* 5. SSOT — the module header names THIS gate and its consumer, so the leaf cannot advertise a
|
|
36
38
|
* certification that does not exist.
|
|
37
39
|
*
|
|
38
40
|
* No spawn, no socket, no timer: the launch this identity feeds is deliberately not in scope.
|
|
@@ -91,12 +93,49 @@ function main(): void {
|
|
|
91
93
|
ok(label, msg.includes(needle));
|
|
92
94
|
};
|
|
93
95
|
|
|
96
|
+
// ── 4e. CLAUDE CODE (#147 L2) ─────────────────────────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
const native = "0b5e2a4c-1f3d-4e8a-9c7b-2d6f8e1a3c5d";
|
|
99
|
+
const transcript = writeTranscript("cc-session.jsonl", "vendor-owned bytes this leaf never parses\n");
|
|
100
|
+
const gidCc = mintRecord(native, transcript, "claude-code");
|
|
101
|
+
const raw = JSON.parse(fs.readFileSync(path.join(storeDir, `${gidCc}.meta.json`), "utf8")) as {
|
|
102
|
+
createdAt: string;
|
|
103
|
+
};
|
|
104
|
+
let cc: ReturnType<typeof resolveResumeLaunchIdentity> | null = null;
|
|
105
|
+
try {
|
|
106
|
+
cc = resolveResumeLaunchIdentity(gidCc);
|
|
107
|
+
} catch {
|
|
108
|
+
cc = null;
|
|
109
|
+
}
|
|
110
|
+
ok(
|
|
111
|
+
"4e: a claude-code record resolves to its own session id, the record's cwd and createdAt [QK:RESUME-ID-CC-RESOLVES]",
|
|
112
|
+
cc?.backend === "claude-code" &&
|
|
113
|
+
cc.nativeSessionId === native &&
|
|
114
|
+
cc.cwd === cwd &&
|
|
115
|
+
cc.createdAt === raw.createdAt,
|
|
116
|
+
);
|
|
117
|
+
rejects(
|
|
118
|
+
mintRecord("--dangerously-skip-permissions", transcript, "claude-code"),
|
|
119
|
+
"is not a Claude Code session id",
|
|
120
|
+
"4e: a recorded session id outside the UUID grammar is refused before it can become a claude flag [QK:RESUME-ID-CC-SESSION-GRAMMAR]",
|
|
121
|
+
);
|
|
122
|
+
rejects(
|
|
123
|
+
mintRecord("0b5e2a4c-1f3d-4e8a-9c7b-2d6f8e1a3c5e", path.join(world, "gone.jsonl"), "claude-code"),
|
|
124
|
+
"is not a file on disk",
|
|
125
|
+
"4e: a recorded transcript that is gone is refused — claude --resume would open nothing [QK:RESUME-ID-CC-TRANSCRIPT-PRESENT]",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
// ── 1. HAPPY ──────────────────────────────────────────────────────────────
|
|
95
130
|
const nativeOk = "0199aaaa-1111-4222-8333-444455556666";
|
|
96
131
|
const fileOk = writeTranscript("own.jsonl", sessionLine(nativeOk) + modelLine);
|
|
97
132
|
const gidOk = mintRecord(nativeOk, fileOk);
|
|
98
133
|
const launch = resolveResumeLaunchIdentity(gidOk);
|
|
99
|
-
ok("1: record
|
|
134
|
+
assert.ok(launch.backend === "pi", "1: a pi record resolves as pi");
|
|
135
|
+
ok(
|
|
136
|
+
"1: record-backed resume resolves the recorded transcript",
|
|
137
|
+
launch.backend === "pi" && launch.sessionFile === fileOk,
|
|
138
|
+
);
|
|
100
139
|
ok("1: resume cwd = transcript header authority (#9 cold-resume)", launch.cwd === cwd);
|
|
101
140
|
ok(
|
|
102
141
|
"1: resume model/provider = first model_change",
|
|
@@ -122,29 +161,29 @@ function main(): void {
|
|
|
122
161
|
// ── 4. CAUSE FIDELITY ─────────────────────────────────────────────────────
|
|
123
162
|
rejects("20260101T000000-facade", "not a garden citizen", "4: recordless gid → not a citizen");
|
|
124
163
|
|
|
125
|
-
const
|
|
164
|
+
const gidCodex = mintRecord("codex-thread-1", null, "codex");
|
|
126
165
|
rejects(
|
|
127
|
-
|
|
128
|
-
"is a
|
|
166
|
+
gidCodex,
|
|
167
|
+
"is a codex citizen",
|
|
129
168
|
"4: out-of-domain citizen → refused as a capability-domain miss, not as citizen rank",
|
|
130
169
|
);
|
|
131
170
|
// This one refusal is EXPECTED, not a defect: the record is perfectly good and the operator
|
|
132
171
|
// has reached past a capability boundary. So it is the only failure here that carries a
|
|
133
172
|
// typed `reason` a caller can match on a FIELD — the visible-resume composition turns it
|
|
134
|
-
// into `target-
|
|
173
|
+
// into `target-backend-unsupported`, and if it degraded to a bare Error that caller would either parse
|
|
135
174
|
// this sentence or read the record a second time to ask which backend it was.
|
|
136
175
|
{
|
|
137
176
|
let caught: Error | null = null;
|
|
138
177
|
try {
|
|
139
|
-
resolveResumeLaunchIdentity(
|
|
178
|
+
resolveResumeLaunchIdentity(gidCodex);
|
|
140
179
|
} catch (err) {
|
|
141
180
|
caught = err as Error;
|
|
142
181
|
}
|
|
143
182
|
ok(
|
|
144
183
|
"4: the backend miss is a TYPED refusal carrying reason + backend, not a bare Error [QK:RESUME-ID-BACKEND-TYPED-REFUSAL]",
|
|
145
184
|
caught instanceof ResumeBackendUnsupportedError &&
|
|
146
|
-
caught.reason === "target-
|
|
147
|
-
caught.backend === "
|
|
185
|
+
caught.reason === "target-backend-unsupported" &&
|
|
186
|
+
caught.backend === "codex",
|
|
148
187
|
);
|
|
149
188
|
}
|
|
150
189
|
|
|
@@ -195,7 +234,8 @@ function main(): void {
|
|
|
195
234
|
retired.err.model === "claude-sonnet-5" &&
|
|
196
235
|
routed.err instanceof AcpModelNotCuratedError &&
|
|
197
236
|
routed.err.model === "claude-opus-5" &&
|
|
198
|
-
current.identity?.
|
|
237
|
+
current.identity?.backend === "pi" &&
|
|
238
|
+
current.identity.provider === "entwurf" &&
|
|
199
239
|
current.identity.model === "claude-sonnet-5-5",
|
|
200
240
|
);
|
|
201
241
|
}
|
|
@@ -243,6 +283,7 @@ function main(): void {
|
|
|
243
283
|
transcriptPath: fileOk,
|
|
244
284
|
createdAt: "2026-03-01T12:00:00.000Z",
|
|
245
285
|
recordUpdatedAt: "2026-03-01T12:30:00.000Z",
|
|
286
|
+
attachIncarnation: null,
|
|
246
287
|
}),
|
|
247
288
|
);
|
|
248
289
|
// The happy-path id resolved a moment ago; the ONLY thing that changed is a second record
|
|
@@ -271,9 +312,7 @@ function main(): void {
|
|
|
271
312
|
"5: the leaf's header names check-resume-launch-identity (no certification it does not have) [QK:RESUME-ID-GATE-SSOT]",
|
|
272
313
|
src.includes("`check-resume-launch-identity`"),
|
|
273
314
|
);
|
|
274
|
-
|
|
275
|
-
// be able to tell "kept on purpose, for the visible lane" from "forgotten".
|
|
276
|
-
ok("5: the leaf states it is preserved with no consumer, on purpose", /no consumer in the shipped tree/.test(src));
|
|
315
|
+
ok("5: the leaf names its consumer", /`entwurf-v2-visible-resume` is its consumer/.test(src));
|
|
277
316
|
}
|
|
278
317
|
|
|
279
318
|
console.log(`\ncheck-resume-launch-identity: ${passed} checks passed`);
|
|
@@ -52,8 +52,9 @@ export function buildCodexInstruction(input: CodexInstructionInput): string {
|
|
|
52
52
|
// DIRECTORY must come from this Codex's own record (#95 lane C), which is a fact only
|
|
53
53
|
// because its thread now opens where its pane is rather than where the app-server sits.
|
|
54
54
|
// Omitting the parameter is the whole point — passing one would prove neither.
|
|
55
|
-
'NO cwd parameter and NO placement parameter, and task "
|
|
56
|
-
|
|
55
|
+
'NO cwd parameter and NO placement parameter, and task "Your automatic callback is the whole task and there is ' +
|
|
56
|
+
"no result to send. After it succeeds, end the turn: no message, no report, no further tool call. Do not open " +
|
|
57
|
+
'another sibling."\n' +
|
|
57
58
|
"2. Stop and wait for the Pi callback delivered into this Codex thread. Accept only a callback whose body " +
|
|
58
59
|
"repeats the exact nonce from your LAUNCH receipt and whose sender envelope carries a garden id on its `session:` line.\n" +
|
|
59
60
|
`3. Only after that match, call public mcp__entwurf_bridge__entwurf_v2 once with target ${input.callerGid}, ` +
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-hook-bundle — the Claude Code hook bundle a gate stages, derived from the hook's OWN
|
|
3
|
+
* import graph rather than from a hand-kept file list (#147 L2 B1).
|
|
4
|
+
*
|
|
5
|
+
* The installed bundle is the entry plus exactly the `lib/` files `meta-bridge-install.sh`
|
|
6
|
+
* copies. A gate that hand-copies its own list can pass while the installer misses a file the
|
|
7
|
+
* hook now imports — the false green this module exists to remove. So a staged bundle carries
|
|
8
|
+
* the entry's import closure, and `installerCopiedLibFiles` reads what the installer actually
|
|
9
|
+
* copies, for the gate to compare the two.
|
|
10
|
+
*
|
|
11
|
+
* SCOPE, deliberately narrow: only static `from "./…"` / `from "../…"` specifiers in DOUBLE
|
|
12
|
+
* quotes — the one form the hook's closure (entry, meta-session, entwurf-v2-lock, session-id)
|
|
13
|
+
* uses today. A side-effect `import "./x"`, a dynamic `import("./x")`, a `require`, or a
|
|
14
|
+
* single-quoted specifier is NOT followed, so the comparison says nothing about them; this is
|
|
15
|
+
* not a general module resolver and must not grow into one. `meta-session`'s win32 addon is
|
|
16
|
+
* one such unfollowed load (`createRequire`, win32 only, outside the Linux/macOS bundle). The
|
|
17
|
+
* independent proof that the closure is complete is the gate RUNNING the staged compiled hook.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { chmodSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
|
|
23
|
+
const IMPORT_RE = /\bfrom\s+"(\.{1,2}\/[^"]+)"/g;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The entry's relative-import closure, as paths relative to the entry's directory
|
|
27
|
+
* (`lib/meta-session.ts`, …). The entry itself is not included.
|
|
28
|
+
*/
|
|
29
|
+
export function hookImportClosure(entryFile: string): string[] {
|
|
30
|
+
const root = path.dirname(entryFile);
|
|
31
|
+
const seen = new Set<string>();
|
|
32
|
+
const visit = (file: string): void => {
|
|
33
|
+
const source = readFileSync(file, "utf8");
|
|
34
|
+
for (const match of source.matchAll(IMPORT_RE)) {
|
|
35
|
+
const target = path.resolve(path.dirname(file), match[1] as string);
|
|
36
|
+
const rel = path.relative(root, target);
|
|
37
|
+
if (seen.has(rel)) continue;
|
|
38
|
+
seen.add(rel);
|
|
39
|
+
visit(target);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
visit(entryFile);
|
|
43
|
+
return [...seen].sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The `lib/<name>` destinations `meta-bridge-install.sh` copies into the assembled plugin. */
|
|
47
|
+
export function installerCopiedLibFiles(installerText: string): string[] {
|
|
48
|
+
const out = new Set<string>();
|
|
49
|
+
for (const match of installerText.matchAll(/^cp\s.*"\$ASM\/\$PLUGIN\/lib\/([^"]+)"\s*$/gm)) {
|
|
50
|
+
out.add(match[1] as string);
|
|
51
|
+
}
|
|
52
|
+
return [...out].sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Stage entry + its import closure + the launcher under `root`, exactly as installed. `entryFile`
|
|
57
|
+
* is the source (`pi-extensions/meta-bridge-hook.ts`) or the compiled entry
|
|
58
|
+
* (`mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js`); the closure follows whichever.
|
|
59
|
+
*/
|
|
60
|
+
export function stageMetaHookBundle(root: string, entryFile: string, launcher: string): string[] {
|
|
61
|
+
const closure = hookImportClosure(entryFile);
|
|
62
|
+
mkdirSync(path.join(root, "scripts"), { recursive: true });
|
|
63
|
+
copyFileSync(entryFile, path.join(root, path.basename(entryFile)));
|
|
64
|
+
for (const rel of closure) {
|
|
65
|
+
mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
66
|
+
copyFileSync(path.join(path.dirname(entryFile), rel), path.join(root, rel));
|
|
67
|
+
}
|
|
68
|
+
const staged = path.join(root, "scripts", "hook-launch.sh");
|
|
69
|
+
copyFileSync(launcher, staged);
|
|
70
|
+
chmodSync(staged, 0o755);
|
|
71
|
+
return closure;
|
|
72
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
#
|
|
7
7
|
# The floor is a full three-part version, not a major lane: the discriminator sits
|
|
8
8
|
# in the PATCH position (2.1.138 drops `args`, 2.1.139 introduced the exec form,
|
|
9
|
-
# 2.1.217 is the version this repo
|
|
9
|
+
# 2.1.217 is the version this repo proved the exec form on in a live session, 2.1.223
|
|
10
|
+
# is where `--resume <id>` started finding a session outside the current project). A major-only
|
|
10
11
|
# comparison like the Node floor's `-lt 24` cannot express it, so a real semver
|
|
11
12
|
# compare has to exist — and it must exist exactly ONCE. Two hand-rolled compares in
|
|
12
13
|
# two shell scripts is how "the installer blessed a version the doctor refuses"
|
|
@@ -16,15 +17,18 @@
|
|
|
16
17
|
# `entwurf.claudeCodeFloor` is the SSOT and everything here reads it. There is one
|
|
17
18
|
# number to move. `check-claude-floor-coherence` binds every other spelling of it.
|
|
18
19
|
#
|
|
19
|
-
# WHY THE FLOOR IS 2.1.
|
|
20
|
+
# WHY THE FLOOR IS 2.1.306
|
|
20
21
|
#
|
|
21
|
-
# 2.1.
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
22
|
+
# 2.1.217 is the version whose exec-form runtime behavior was observed end to end
|
|
23
|
+
# (#51 B2: `args` honored per element, no shell on the launch path, `asyncRewake` exit 2
|
|
24
|
+
# waking an idle session); those observations stand. #147 added same-id visible resume
|
|
25
|
+
# of a Claude Code citizen, which runs `claude --resume <nativeSessionId>` from the
|
|
26
|
+
# record's LAST OBSERVED cwd. The vendor CLI reference (code.claude.com cli-reference,
|
|
27
|
+
# read 2026-10-21, unversioned page) says the session-id search covers every project on
|
|
28
|
+
# the machine only from v2.1.223; below it a session whose cwd moved may not be found.
|
|
29
|
+
# GLG's decision (2026-10-21): the floor follows what the direction needs, with no
|
|
30
|
+
# back-compat — 2.1.306, the version GLG relayed as running here. That number is a
|
|
31
|
+
# declared floor: no LIVE receipt has been taken at it yet.
|
|
28
32
|
#
|
|
29
33
|
# Requires: python3 (already a hard dependency of both callers).
|
|
30
34
|
|
|
@@ -53,8 +53,10 @@ esac
|
|
|
53
53
|
# a second derivation is a second thing to forget when the layout moves.
|
|
54
54
|
if [ "$LIB_EXT" = "js" ]; then
|
|
55
55
|
SRC_MS="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
56
|
+
SRC_LOCK="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js"
|
|
56
57
|
else
|
|
57
58
|
SRC_MS="$REPO/pi-extensions/lib/meta-session.ts"
|
|
59
|
+
SRC_LOCK="$REPO/pi-extensions/lib/entwurf-v2-lock.ts"
|
|
58
60
|
fi
|
|
59
61
|
# shellcheck source=scripts/meta-bridge-hook-log.sh
|
|
60
62
|
source "$REPO/scripts/meta-bridge-hook-log.sh"
|
|
@@ -1276,6 +1278,21 @@ fi
|
|
|
1276
1278
|
if [ -f "$ASM_MS" ] && [ "$asm_h" != "$src_h" ]; then
|
|
1277
1279
|
warn "assembled bundle differs from source ($asm_v vs $src_v) — stale .assembled; install re-assembles it"
|
|
1278
1280
|
fi
|
|
1281
|
+
# The hook's record-arm lock (#147 L2 B1) is part of the same writer closure: a writer that
|
|
1282
|
+
# commits an arm binding without the lock module it imports cannot load at all, and a stale one
|
|
1283
|
+
# serializes against a different contract. Same authority as the writer: the INSTALLED bundle.
|
|
1284
|
+
src_lock_h="$(hash12 "$SRC_LOCK")"
|
|
1285
|
+
if [ -n "${INST_MS:-}" ]; then
|
|
1286
|
+
INST_LOCK="$(dirname "$INST_MS")/entwurf-v2-lock.$LIB_EXT"
|
|
1287
|
+
inst_lock_h="$(hash12 "$INST_LOCK")"
|
|
1288
|
+
if [ ! -f "$INST_LOCK" ]; then
|
|
1289
|
+
bad "deployed hook lock module MISSING ($INST_LOCK) — the hook imports it, so the installed writer cannot load. Run ./run.sh install-meta-bridge to redeploy."
|
|
1290
|
+
elif [ "$inst_lock_h" != "$src_lock_h" ]; then
|
|
1291
|
+
bad "deployed hook lock module STALE: installed=$inst_lock_h vs source=$src_lock_h ($INST_LOCK). Run ./run.sh install-meta-bridge to redeploy."
|
|
1292
|
+
else
|
|
1293
|
+
ok "deployed hook lock module matches source ($inst_lock_h)"
|
|
1294
|
+
fi
|
|
1295
|
+
fi
|
|
1279
1296
|
|
|
1280
1297
|
# Identity-writer dependency: loadMetaCapabilityRegistry reads
|
|
1281
1298
|
# entwurf-capabilities.json at runtime. In the bundle layout it must sit at the
|
|
@@ -58,11 +58,13 @@ case "$REPO" in
|
|
|
58
58
|
HOOK_ENTRY="meta-bridge-hook.js"
|
|
59
59
|
HOOK_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
|
|
60
60
|
LIB_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
61
|
+
LOCK_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js"
|
|
61
62
|
LIB_EXT="js" ;;
|
|
62
63
|
*)
|
|
63
64
|
HOOK_ENTRY="meta-bridge-hook.ts"
|
|
64
65
|
HOOK_SRC="$REPO/pi-extensions/meta-bridge-hook.ts"
|
|
65
66
|
LIB_SRC="$REPO/pi-extensions/lib/meta-session.ts"
|
|
67
|
+
LOCK_SRC="$REPO/pi-extensions/lib/entwurf-v2-lock.ts"
|
|
66
68
|
LIB_EXT="ts" ;;
|
|
67
69
|
esac
|
|
68
70
|
|
|
@@ -187,9 +189,13 @@ cp -r "$SRC/$PLUGIN" "$ASM/$PLUGIN"
|
|
|
187
189
|
# Installed → tsc-emitted JS (dist, node_modules-safe); dev clone → strip-types .ts.
|
|
188
190
|
[ -f "$HOOK_SRC" ] || die "hook artifact missing: $HOOK_SRC (installed package ships it via prepack build-bridge → dist; reinstall @junghanacs/entwurf, or run 'pnpm run build-bridge' in a dev clone)."
|
|
189
191
|
[ -f "$LIB_SRC" ] || die "hook lib artifact missing: $LIB_SRC (same build-bridge dist closure)."
|
|
192
|
+
[ -f "$LOCK_SRC" ] || die "hook lock artifact missing: $LOCK_SRC (same build-bridge dist closure)."
|
|
190
193
|
cp "$HOOK_SRC" "$ASM/$PLUGIN/$HOOK_ENTRY"
|
|
191
194
|
mkdir -p "$ASM/$PLUGIN/lib"
|
|
192
195
|
cp "$LIB_SRC" "$ASM/$PLUGIN/lib/meta-session.$LIB_EXT"
|
|
196
|
+
# The record-arm lock (#147 L2 B1): the hook imports the shared per-gid lock primitive, so it
|
|
197
|
+
# travels in the same closure. The doctor's writer-version parity hashes it beside the writer.
|
|
198
|
+
cp "$LOCK_SRC" "$ASM/$PLUGIN/lib/entwurf-v2-lock.$LIB_EXT"
|
|
193
199
|
cp "$REPO/pi-extensions/lib/session-id.js" "$ASM/$PLUGIN/lib/session-id.js"
|
|
194
200
|
# v2 writer (3D-3+) reads the capability registry at runtime
|
|
195
201
|
# (loadMetaCapabilityRegistry). It MUST travel at the plugin ROOT — meta-session's
|
package/scripts/meta-facts.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* {
|
|
16
16
|
* "schemaVersion": 1, // of THIS projection, not of the records
|
|
17
17
|
* "storeDir": "/abs/path",
|
|
18
|
-
* "citizens": [ <full verbatim strict
|
|
18
|
+
* "citizens": [ <full verbatim strict v3 records, sorted by gardenId> ],
|
|
19
19
|
* "defects": [ { "filename", "message" }, … sorted by filename ]
|
|
20
20
|
* }
|
|
21
21
|
*
|
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
"claim": "BRIDGEBOOT-RESUME-CALL-TARGET-ONLY",
|
|
20
20
|
"title": "a model knob is added to the resume schema, giving the caller a second way to decide what a resumed citizen is after the record already decided",
|
|
21
21
|
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
22
|
-
"find": [
|
|
22
|
+
"find": [
|
|
23
|
+
"\t\t\t.describe(\"Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers).\"),"
|
|
24
|
+
],
|
|
23
25
|
"replace": [
|
|
24
|
-
"\t\t\t.describe(\"Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).\")
|
|
26
|
+
"\t\t\t.describe(\"Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers).\"),",
|
|
27
|
+
"\t\tmodel: z.string().optional().describe(\"Override the recorded model.\"),"
|
|
25
28
|
],
|
|
26
29
|
"gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
|
|
27
30
|
"timeoutSeconds": 120,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "cc-retire-pidfd",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "CCRETIRE-WRONG-KEY-NO-SIGNAL",
|
|
7
|
+
"title": "the start-key re-check after pidfd_open is gone, so a reused pid number pins and offers for termination a process the caller never checked",
|
|
8
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
9
|
+
"find": [" if observed != key:"],
|
|
10
|
+
"replace": [" if False:"],
|
|
11
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
12
|
+
"timeoutSeconds": 600,
|
|
13
|
+
"signature": "[QK:CCRETIRE-WRONG-KEY-NO-SIGNAL]",
|
|
14
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"claim": "CCRETIRE-CANCEL-NO-SIGNAL",
|
|
18
|
+
"title": "an explicit `cancel` frame is read as `go`, so the caller's own refusal after re-checking its binding terminates the process anyway",
|
|
19
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
20
|
+
"find": [" finish(3, \"cancelled\")"],
|
|
21
|
+
"replace": [" line = \"go\""],
|
|
22
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
23
|
+
"timeoutSeconds": 600,
|
|
24
|
+
"signature": "[QK:CCRETIRE-CANCEL-NO-SIGNAL]",
|
|
25
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"claim": "CCRETIRE-EOF-NO-SIGNAL",
|
|
29
|
+
"title": "a caller that died or closed the pipe without a word is read as `go`, so losing the caller authorises the signal",
|
|
30
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
31
|
+
"find": [" finish(3, \"cancelled-eof\")"],
|
|
32
|
+
"replace": [" kind, line = \"line\", \"go\""],
|
|
33
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
34
|
+
"timeoutSeconds": 600,
|
|
35
|
+
"signature": "[QK:CCRETIRE-EOF-NO-SIGNAL]",
|
|
36
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"claim": "CCRETIRE-TRUNCATED-GO-NO-SIGNAL",
|
|
40
|
+
"title": "the bytes `go` followed by EOF with no newline are accepted as a complete frame, so a half-written control authorises the signal",
|
|
41
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
42
|
+
"find": ["return (\"eof\", None) if not buf else (\"truncated\", None)"],
|
|
43
|
+
"replace": ["return (\"eof\", None) if not buf else (\"line\", buf.decode(\"ascii\"))"],
|
|
44
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
45
|
+
"timeoutSeconds": 600,
|
|
46
|
+
"signature": "[QK:CCRETIRE-TRUNCATED-GO-NO-SIGNAL]",
|
|
47
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"claim": "CCRETIRE-OVERSIZE-NO-SIGNAL",
|
|
51
|
+
"title": "the control frame bound is lifted, so an over-long frame is parsed as a line instead of refused as oversize-control",
|
|
52
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
53
|
+
"find": ["CONTROL_MAX = 16"],
|
|
54
|
+
"replace": ["CONTROL_MAX = 4096"],
|
|
55
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
56
|
+
"timeoutSeconds": 600,
|
|
57
|
+
"signature": "[QK:CCRETIRE-OVERSIZE-NO-SIGNAL]",
|
|
58
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"claim": "CCRETIRE-PARTIAL-DEADLINE-NO-SIGNAL",
|
|
62
|
+
"title": "a partial frame still open at the go deadline is taken as the frame, so a caller that never finished its line authorises the signal",
|
|
63
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
64
|
+
"find": [" return (\"deadline\", None)"],
|
|
65
|
+
"replace": [" return (\"deadline\", None) if not buf else (\"line\", buf.decode(\"ascii\"))"],
|
|
66
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
67
|
+
"timeoutSeconds": 600,
|
|
68
|
+
"signature": "[QK:CCRETIRE-PARTIAL-DEADLINE-NO-SIGNAL]",
|
|
69
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"claim": "CCRETIRE-GO-TERMINATES-SAME-PROCESS",
|
|
73
|
+
"title": "SIGKILL replaces SIGTERM, so the target gets no chance to run its own shutdown and the receiver sees a different death",
|
|
74
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
75
|
+
"find": ["signal.pidfd_send_signal(fd, signal.SIGTERM, None, 0)"],
|
|
76
|
+
"replace": ["signal.pidfd_send_signal(fd, signal.SIGKILL, None, 0)"],
|
|
77
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
78
|
+
"timeoutSeconds": 600,
|
|
79
|
+
"signature": "[QK:CCRETIRE-GO-TERMINATES-SAME-PROCESS]",
|
|
80
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"claim": "CCRETIRE-TERM-IGNORED-NO-ESCALATION",
|
|
84
|
+
"title": "a target that outlives the exit bound is signalled a second time, the escalation the helper promises never to make",
|
|
85
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
86
|
+
"find": ["finish(4, \"exit-unobserved\", waitedS=EXIT_WAIT_S)"],
|
|
87
|
+
"replace": [
|
|
88
|
+
"signal.pidfd_send_signal(fd, signal.SIGTERM, None, 0) or finish(4, \"exit-unobserved\", waitedS=EXIT_WAIT_S)"
|
|
89
|
+
],
|
|
90
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
91
|
+
"timeoutSeconds": 600,
|
|
92
|
+
"signature": "[QK:CCRETIRE-TERM-IGNORED-NO-ESCALATION]",
|
|
93
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"claim": "CCRETIRE-GONE-PID-REFUSED",
|
|
97
|
+
"title": "a pid with no process behind it is reported as `exited`, so a target that was already gone reads as a termination this helper performed",
|
|
98
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
99
|
+
"find": ["finish(2, \"refused\", reason=\"pid-not-found\")"],
|
|
100
|
+
"replace": ["finish(0, \"exited\")"],
|
|
101
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
102
|
+
"timeoutSeconds": 600,
|
|
103
|
+
"signature": "[QK:CCRETIRE-GONE-PID-REFUSED]",
|
|
104
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"claim": "CCRETIRE-MALFORMED-REFUSED",
|
|
108
|
+
"title": "the Linux PID_MAX_LIMIT bound is dropped, so an out-of-range number reaches pidfd_open instead of being refused as malformed input",
|
|
109
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
110
|
+
"find": ["1 < int(raw_pid) <= PID_MAX"],
|
|
111
|
+
"replace": ["1 < int(raw_pid)"],
|
|
112
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
113
|
+
"timeoutSeconds": 600,
|
|
114
|
+
"signature": "[QK:CCRETIRE-MALFORMED-REFUSED]",
|
|
115
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"claim": "CCRETIRE-NO-NUMERIC-KILL",
|
|
119
|
+
"title": "the pidfd signal is replaced by a numeric os.kill, reopening the check -> signal race the helper exists to close; no live cell can see it, only the static cell",
|
|
120
|
+
"subject": "pi-extensions/lib/cc-retire-pidfd.py",
|
|
121
|
+
"find": ["signal.pidfd_send_signal(fd, signal.SIGTERM, None, 0)"],
|
|
122
|
+
"replace": ["os.kill(pid, signal.SIGTERM)"],
|
|
123
|
+
"gate": ["bash", "run.sh", "check-cc-retire-pidfd"],
|
|
124
|
+
"timeoutSeconds": 600,
|
|
125
|
+
"signature": "[QK:CCRETIRE-NO-NUMERIC-KILL]",
|
|
126
|
+
"signatureSource": "pi-extensions/lib/cc-retire-pidfd.test.ts"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|