@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
|
@@ -53,20 +53,38 @@
|
|
|
53
53
|
* closure (build-bridge → dist), because Node REFUSES strip-types on a `.ts` below
|
|
54
54
|
* node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). The installer picks
|
|
55
55
|
* the artifact by layout (0.12.5), mirroring start.sh / store-doctor.
|
|
56
|
-
* Imports `./lib/meta-session.ts` (+ `./lib/session-id.js`);
|
|
57
|
-
* rewrites
|
|
58
|
-
* copies
|
|
56
|
+
* Imports `./lib/meta-session.ts` and `./lib/entwurf-v2-lock.ts` (+ `./lib/session-id.js`);
|
|
57
|
+
* `rewriteRelativeImportExtensions` rewrites those to `.js` in the emitted closure. The
|
|
58
|
+
* installer copies exactly those lib files alongside this file so `${CLAUDE_PLUGIN_ROOT}`
|
|
59
|
+
* self-locates them.
|
|
60
|
+
*
|
|
61
|
+
* RECORD-ARM LOCK (#147 L2 B1). Every write this hook makes to an EXISTING record happens
|
|
62
|
+
* under a per-gid lock (`defaultMetaRecordArmLocksDir`, the shared `acquireLock` primitive in
|
|
63
|
+
* its own directory — never the v2 dispatch lock dir, which visible resume holds across its
|
|
64
|
+
* observation). The garden id is learned by a READ-ONLY certified lookup; a record found
|
|
65
|
+
* absent is published create-only. On an arm-capable event the lock is held from the upsert
|
|
66
|
+
* that commits a fresh `attachIncarnation` through the receiver marker that copies it, so two
|
|
67
|
+
* normal arms (or an arm and a UserPromptSubmit) always finish as a matching record/marker
|
|
68
|
+
* pair. An arm that cannot take the lock within its bound writes NOTHING and logs ERROR: that
|
|
69
|
+
* attach is not committed, and a live native process may then exist that no record names — a
|
|
70
|
+
* boundary, not a refusal. A UserPromptSubmit that finds the lock busy skips its backfill.
|
|
59
71
|
*/
|
|
60
72
|
|
|
61
73
|
import * as fs from "node:fs";
|
|
62
74
|
import * as path from "node:path";
|
|
75
|
+
import { acquireLock, type LockClaim, releaseLock } from "./lib/entwurf-v2-lock.ts";
|
|
63
76
|
import {
|
|
64
77
|
defaultMetaMailboxDir,
|
|
78
|
+
defaultMetaRecordArmLocksDir,
|
|
65
79
|
defaultMetaSessionsDir,
|
|
80
|
+
findCertifiedIdentityByNativeSessionId,
|
|
66
81
|
isPlausibleOwnerPid,
|
|
82
|
+
type MetaIdentityMintInput,
|
|
67
83
|
type MetaReceiverArmProvenance,
|
|
84
|
+
mintAttachIncarnation,
|
|
68
85
|
readMetaSenderMarker,
|
|
69
86
|
removeMetaReceiverMarker,
|
|
87
|
+
UpsertActionMismatchError,
|
|
70
88
|
upsertMetaSession,
|
|
71
89
|
writeMetaReceiverMarker,
|
|
72
90
|
writeMetaSenderMarker,
|
|
@@ -97,8 +115,62 @@ function logLine(level: LogLevel, message: string): void {
|
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
|
|
118
|
+
/** The record-arm lock this run holds, released on every exit path (see {@link emit}). */
|
|
119
|
+
let heldArmLock: LockClaim | null = null;
|
|
120
|
+
|
|
121
|
+
/** Arm-capable events wait this long for the record-arm lock; the critical section is a few file writes. */
|
|
122
|
+
const ARM_LOCK_WAIT_MS = 2_000;
|
|
123
|
+
const ARM_LOCK_POLL_MS = 25;
|
|
124
|
+
|
|
125
|
+
function sleepSync(ms: number): void {
|
|
126
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Take the record-arm lock for `gardenId`: ONE attempt, or (arm-capable events) polled up to
|
|
131
|
+
* {@link ARM_LOCK_WAIT_MS}. A throw from the primitive (its claim could not be written) is
|
|
132
|
+
* the same answer as busy — the caller writes nothing.
|
|
133
|
+
*/
|
|
134
|
+
function takeArmLock(gardenId: string, wait: boolean): { ok: true } | { ok: false; detail: string } {
|
|
135
|
+
const dir = defaultMetaRecordArmLocksDir();
|
|
136
|
+
const deadline = Date.now() + (wait ? ARM_LOCK_WAIT_MS : 0);
|
|
137
|
+
for (;;) {
|
|
138
|
+
let detail: string;
|
|
139
|
+
try {
|
|
140
|
+
const acquired = acquireLock(gardenId, { dir });
|
|
141
|
+
if (acquired.ok) {
|
|
142
|
+
heldArmLock = acquired.claim;
|
|
143
|
+
return { ok: true };
|
|
144
|
+
}
|
|
145
|
+
detail = acquired.conflict.detail;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
detail = err instanceof Error ? err.message : String(err);
|
|
148
|
+
}
|
|
149
|
+
if (Date.now() >= deadline) return { ok: false, detail };
|
|
150
|
+
sleepSync(ARM_LOCK_POLL_MS);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function releaseArmLock(): void {
|
|
155
|
+
const claim = heldArmLock;
|
|
156
|
+
if (claim === null) return;
|
|
157
|
+
heldArmLock = null;
|
|
158
|
+
try {
|
|
159
|
+
const outcome = releaseLock(claim, { dir: defaultMetaRecordArmLocksDir() });
|
|
160
|
+
if (outcome !== "released") logLine("WARN", `record-arm lock release for ${claim.gardenId}: ${outcome}`);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
// The holder pid is this short-lived hook, so a lock it failed to unlink is reclaimed
|
|
163
|
+
// by the next acquirer once this process is gone (same host + ESRCH).
|
|
164
|
+
logLine(
|
|
165
|
+
"WARN",
|
|
166
|
+
`record-arm lock release failed for ${claim.gardenId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
100
171
|
/** Emit a hook response on stdout and exit 0. `{}` means "did nothing, do not block startup". */
|
|
101
172
|
function emit(payload: Record<string, unknown>): never {
|
|
173
|
+
releaseArmLock();
|
|
102
174
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
103
175
|
process.exit(0);
|
|
104
176
|
}
|
|
@@ -174,6 +246,59 @@ function resolveMetaHookOwnerPid(): number | null {
|
|
|
174
246
|
return ownerPid;
|
|
175
247
|
}
|
|
176
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Commit this event's record write and return the garden id (#147 L2 B1). The garden id to
|
|
251
|
+
* lock is learned READ ONLY; a record found absent is published create-only (a create needs no
|
|
252
|
+
* lock: its exclusive publish cannot replace anything, and a concurrent publisher turns the
|
|
253
|
+
* create into a refusal here, never into an unlocked attach). Every write to an EXISTING
|
|
254
|
+
* record then happens under the record-arm lock, which the caller keeps through the receiver
|
|
255
|
+
* marker write (released by {@link emit}). An arm-capable event mints its binding in that same
|
|
256
|
+
* atomic upsert. Lock busy: an arm writes nothing and logs ERROR (admission failure — not a
|
|
257
|
+
* committed attach, not an armed watch); any other event skips its backfill with a WARN.
|
|
258
|
+
* Throws on a store defect, exactly as the upsert does.
|
|
259
|
+
*/
|
|
260
|
+
function commitRecord(
|
|
261
|
+
input: MetaIdentityMintInput,
|
|
262
|
+
armsWatch: boolean,
|
|
263
|
+
logContext: string,
|
|
264
|
+
): { gardenId: string; attachIncarnation: string | undefined } {
|
|
265
|
+
let existing = findCertifiedIdentityByNativeSessionId(input.nativeSessionId);
|
|
266
|
+
if (existing === null) {
|
|
267
|
+
try {
|
|
268
|
+
const created = upsertMetaSession({ input, expectAction: "create" });
|
|
269
|
+
logLine("INFO", `create record ${path.basename(created.path)} (${logContext})`);
|
|
270
|
+
// Nothing to bind and nothing older to preserve: the create is the whole write.
|
|
271
|
+
if (!armsWatch) return { gardenId: created.record.gardenId, attachIncarnation: undefined };
|
|
272
|
+
existing = created.record;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (!(err instanceof UpsertActionMismatchError)) throw err;
|
|
275
|
+
existing = findCertifiedIdentityByNativeSessionId(input.nativeSessionId);
|
|
276
|
+
if (existing === null) throw err;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const locked = takeArmLock(existing.gardenId, armsWatch);
|
|
280
|
+
if (!locked.ok) {
|
|
281
|
+
if (armsWatch) {
|
|
282
|
+
// Admission failure: NOTHING is written, so the store keeps whatever pair it had. This
|
|
283
|
+
// attach is not committed, and a live process may now hold this session unseen.
|
|
284
|
+
logLine(
|
|
285
|
+
"ERROR",
|
|
286
|
+
`record-arm lock busy — attach NOT committed, watch not armed (garden=${existing.gardenId}, ${logContext}): ${locked.detail}`,
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
logLine(
|
|
290
|
+
"WARN",
|
|
291
|
+
`record-arm lock busy — backfill skipped (garden=${existing.gardenId}, ${logContext}): ${locked.detail}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
emit({});
|
|
295
|
+
}
|
|
296
|
+
const attachIncarnation = armsWatch ? mintAttachIncarnation() : undefined;
|
|
297
|
+
const result = upsertMetaSession({ input: { ...input, attachIncarnation }, expectAction: "attach" });
|
|
298
|
+
logLine("INFO", `${result.action} record ${path.basename(result.path)} (${logContext})`);
|
|
299
|
+
return { gardenId: result.record.gardenId, attachIncarnation };
|
|
300
|
+
}
|
|
301
|
+
|
|
177
302
|
function main(): void {
|
|
178
303
|
let raw: string;
|
|
179
304
|
try {
|
|
@@ -230,16 +355,16 @@ function main(): void {
|
|
|
230
355
|
emit({});
|
|
231
356
|
}
|
|
232
357
|
|
|
358
|
+
// Arm-capable events commit a fresh binding; every other event preserves it.
|
|
359
|
+
const armsWatch = armProvenanceFor(eventName) !== null;
|
|
233
360
|
let gardenId: string;
|
|
361
|
+
let attachIncarnation: string | undefined;
|
|
234
362
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"INFO",
|
|
241
|
-
`${result.action} record ${path.basename(result.path)} (event=${eventName}, source=${source}, native=${sessionId})`,
|
|
242
|
-
);
|
|
363
|
+
({ gardenId, attachIncarnation } = commitRecord(
|
|
364
|
+
{ backend: "claude-code", nativeSessionId: sessionId, transcriptPath, cwd, model },
|
|
365
|
+
armsWatch,
|
|
366
|
+
`event=${eventName}, source=${source}, native=${sessionId}`,
|
|
367
|
+
));
|
|
243
368
|
} catch (err) {
|
|
244
369
|
// Best-effort: a broken record store must surface via the doctor, not by
|
|
245
370
|
// breaking the user's session open. Log and continue with no arm. This is
|
|
@@ -362,12 +487,16 @@ function main(): void {
|
|
|
362
487
|
nativeSessionId: sessionId,
|
|
363
488
|
ownerPid,
|
|
364
489
|
armProvenance,
|
|
490
|
+
attachIncarnation,
|
|
365
491
|
});
|
|
366
492
|
logLine("INFO", `receiver marker ${gardenId} owner=${ownerPid} arm=${eventName} source=${source}`);
|
|
367
493
|
} catch (err) {
|
|
494
|
+
// ERROR, after `INFO armed watch` so the hook-log doctor does not read a later arm as
|
|
495
|
+
// recovery: the record already carries this arm's binding, the marker does not, so the
|
|
496
|
+
// garden is undeliverable AND a later resume refuses it as unbound.
|
|
368
497
|
logLine(
|
|
369
|
-
"
|
|
370
|
-
`receiver marker write failed (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`,
|
|
498
|
+
"ERROR",
|
|
499
|
+
`receiver marker write failed — record binding committed, marker not (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`,
|
|
371
500
|
);
|
|
372
501
|
}
|
|
373
502
|
} else {
|
|
@@ -98,6 +98,13 @@
|
|
|
98
98
|
* operator host started outside tmux gets the bridge's named `no-tmux-context` refusal. Exposing the
|
|
99
99
|
* tool is not a caller LIVE proof.
|
|
100
100
|
*
|
|
101
|
+
* CALLER RESUME (the seventh tool, #147 L1). `entwurf_resume_call` lets this durable citizen reopen a
|
|
102
|
+
* DORMANT pi or Claude Code citizen under its own garden id (#147 L2), under the bridge's own
|
|
103
|
+
* single-`target` schema (pinned below). The bridge decides everything else from the target's record,
|
|
104
|
+
* exactly as for any other caller: other backends have no same-id resume, a live target is refused,
|
|
105
|
+
* and the window opens in the tmux session this host was launched in. Exposing the tool names no new authority and is not a caller LIVE
|
|
106
|
+
* proof; it never resumes this durable host itself.
|
|
107
|
+
*
|
|
101
108
|
* NOT HERE: crash-safe receive. `entwurf_inbox_read` archives a body before the durable tool
|
|
102
109
|
* result commits, so a crash between the two loses that body to the model; recovery settles the
|
|
103
110
|
* call as interrupted and no doorbell rings again. A drain that fails part-way has archived the
|
|
@@ -156,6 +163,8 @@ export const PI_DURABLE_TOOL_NAMES = [
|
|
|
156
163
|
"entwurf_inbox_read",
|
|
157
164
|
"entwurf_callback",
|
|
158
165
|
"entwurf_fresh_call",
|
|
166
|
+
"entwurf_resume_call",
|
|
167
|
+
"entwurf_retire_call",
|
|
159
168
|
] as const;
|
|
160
169
|
export type PiDurableToolName = (typeof PI_DURABLE_TOOL_NAMES)[number];
|
|
161
170
|
|
|
@@ -164,7 +173,7 @@ const DRAFT_07 = "http://json-schema.org/draft-07/schema#";
|
|
|
164
173
|
/**
|
|
165
174
|
* The bridge's `tools/list` inputSchemas for the exposed tools (`mcp/entwurf-bridge/src/index.ts`
|
|
166
175
|
* `entwurf_v2` 456-507, `entwurf_self`, `entwurf_peers`, `entwurf_inbox_read`, `entwurf_callback`,
|
|
167
|
-
* `entwurf_fresh_call`),
|
|
176
|
+
* `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_retire_call`),
|
|
168
177
|
* as the bridge serialises them.
|
|
169
178
|
* `attach` compares the live list against these and refuses on any difference.
|
|
170
179
|
*/
|
|
@@ -235,7 +244,7 @@ export const PI_DURABLE_TOOL_SCHEMAS: Readonly<Record<PiDurableToolName, Readonl
|
|
|
235
244
|
maxLength: 200,
|
|
236
245
|
pattern: "^[A-Za-z0-9][A-Za-z0-9._/:\\[\\]-]*$",
|
|
237
246
|
description:
|
|
238
|
-
"Required runtime model: canonical provider/model for pi, provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.",
|
|
247
|
+
"Required runtime model: canonical provider/model for pi (a bare alias is refused), provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name. Neither pi nor pi-durable may name the openrouter provider.",
|
|
239
248
|
},
|
|
240
249
|
task: {
|
|
241
250
|
type: "string",
|
|
@@ -266,6 +275,33 @@ export const PI_DURABLE_TOOL_SCHEMAS: Readonly<Record<PiDurableToolName, Readonl
|
|
|
266
275
|
required: ["backend", "model", "task"],
|
|
267
276
|
$schema: DRAFT_07,
|
|
268
277
|
},
|
|
278
|
+
entwurf_resume_call: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
target: {
|
|
282
|
+
type: "string",
|
|
283
|
+
minLength: 1,
|
|
284
|
+
pattern: "^\\d{8}T\\d{6}-[0-9a-f]{6}$",
|
|
285
|
+
description: "Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers).",
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
required: ["target"],
|
|
289
|
+
$schema: DRAFT_07,
|
|
290
|
+
},
|
|
291
|
+
entwurf_retire_call: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {
|
|
294
|
+
target: {
|
|
295
|
+
type: "string",
|
|
296
|
+
minLength: 1,
|
|
297
|
+
pattern: "^\\d{8}T\\d{6}-[0-9a-f]{6}$",
|
|
298
|
+
description:
|
|
299
|
+
"Exact garden id of the Pi or Claude Code target the human operator explicitly requested to retire.",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
required: ["target"],
|
|
303
|
+
$schema: DRAFT_07,
|
|
304
|
+
},
|
|
269
305
|
};
|
|
270
306
|
|
|
271
307
|
/** Key-order-insensitive JSON, for comparing a schema copy against the bridge's own. */
|
|
@@ -318,7 +354,7 @@ export interface PiDurableRoots {
|
|
|
318
354
|
// ---------------------------------------------------------------------------
|
|
319
355
|
// Durable surface, typed NARROWLY and locally — read at pi-durable 1.0.2 @cd32f77 (#129,
|
|
320
356
|
// `harness/types.ts`; historical coordinates, not re-anchored here). The current supply is the
|
|
321
|
-
// carrier emitted from Entwurf's owned source (pi/pi-durable/src, #141) against the 1.
|
|
357
|
+
// carrier emitted from Entwurf's owned source (pi/pi-durable/src, #141) against the 1.4.0 SDK set
|
|
322
358
|
// `pi/pi-durable/overlay/upstream-pin.json` names (origin `abe508e`). `[read 2026-10-13]` at
|
|
323
359
|
// v1.2.0 one shape typed below moved: `ToolExecutionResult.content` is now `output`, beside the new
|
|
324
360
|
// `structuredOutput` (`harness/types.ts`, its CHANGELOG's breaking changes). An omitted `output`
|
|
@@ -748,6 +784,24 @@ const TOOL_DESCRIPTIONS: Record<PiDurableToolName, string> = {
|
|
|
748
784
|
"(backend, model and task required; optional cwd and placement). Returns the bridge's LAUNCH receipt or its " +
|
|
749
785
|
"named refusal — not a started runtime, a delivered task or a reply; the sibling's callback names it. Opening " +
|
|
750
786
|
"beside this host needs the tmux pane this host itself was launched in.",
|
|
787
|
+
entwurf_resume_call:
|
|
788
|
+
"Reopen ONE dormant pi or Claude Code citizen under its own garden id in a visible tmux window through the " +
|
|
789
|
+
"entwurf-bridge (target required; the record supplies the session and cwd — no task, no prompt, no turn). " +
|
|
790
|
+
"Returns the bridge's LAUNCH and OBSERVATION receipts or its named refusal: a live target, another backend, " +
|
|
791
|
+
"or a Claude Code citizen not proven dormant is refused by name, and opening the window needs the tmux " +
|
|
792
|
+
"session this host itself was launched in.",
|
|
793
|
+
entwurf_retire_call:
|
|
794
|
+
"Only when the human operator explicitly requested this particular target/work: make ONE retirement attempt for that request " +
|
|
795
|
+
"through the bridge (target only, Pi or Claude Code; self retirement refuses). Development work, a role or " +
|
|
796
|
+
"a record-backed actor is NOT a grant. Use host approval where provided/configured; " +
|
|
797
|
+
"Pi and pi-durable have no built-in per-call approval prompt by default. An explicit operator request and " +
|
|
798
|
+
"agent operating policy are required; attribution and MCP annotations are hints, not grants or proof of approval. " +
|
|
799
|
+
"No autonomous cleanup, timers or automatic " +
|
|
800
|
+
"retry of unknown, cancelled or lost results. Do not manually /resume or /clear the target during the request. " +
|
|
801
|
+
"Pi shutdown-requested is only an ACK, not process exit, flush or model silence. Claude Code exited is that " +
|
|
802
|
+
"fd-pinned process only, not conversation end, children or flush. The JSON receipt keeps outcome, record and " +
|
|
803
|
+
"release separate; receipt return does not mean exit, and cancellation does not undo an action. Invoke once " +
|
|
804
|
+
"per explicitly named target, never infer a roster. This contact does not turn hints into authorization.",
|
|
751
805
|
};
|
|
752
806
|
|
|
753
807
|
/** Announce, never push: the notice names the garden and the tool, never the body. */
|
package/run.sh
CHANGED
|
@@ -224,7 +224,7 @@ Usage:
|
|
|
224
224
|
./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). COST FENCE: open that agy conversation on gemini-3.6-flash (free account) — never a Pro tier; entwurf never selects the agy model and no assertion reads it. LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
|
|
225
225
|
./run.sh smoke-codex-native-push-live # on-demand LIVE acceptance for Codex native-push — needs LIVE=1 + CODEX_LIVE_THREAD_ID loaded by a visible TUI attached to the default app-server; probes the real UDS, routes through production runEntwurfV2, queues one exact-token turn without retry, re-probes, and leaves the visible TUI to show the model result
|
|
226
226
|
./run.sh smoke-codex-fresh-live # #95 RELEASE MUST: record-backed receipt fixture -> PUBLIC initial Pi fresh/callback in a session S that is NOT the app-server's -> Pi PUBLIC omitted-placement Codex fresh/exact callback, also in S -> Pi-to-Codex addressed native-push v2 -> Codex PUBLIC omitted-placement AND omitted-cwd fresh Pi/exact callback IN S beside its own TUI (lane B claim: the app-server env names another session, so S can only come from the caller's pane title; lane C claim: no cwd was asked for, so the directory can only come from the Codex caller's own record — and the Codex thread's own rollout session_meta.cwd, its pane and its record must all be the requested scratch, not the app-server's repo) -> Codex final v2 evidence. Needs LIVE=1 + ENTWURF_CODEX_APP_SERVER_PID + ENTWURF_CODEX_FRESH_MODEL + ENTWURF_CODEX_FRESH_PI_MODEL, and its ONE stable launch directory answered 'Trust' once in a plain codex -C there: codex records its direct consent per exact directory, so a directory with no answer at all opens a consent screen instead of a first turn. Only THIS gate treats that as a precondition and fails up front as codex-launch-cwd-undecided rather than timing out on a callback — an ordinary entwurf_fresh_call only prints the note and opens the window, because a human can answer the screen. The smoke PRINTS that directory as 'launch-cwd <dir>' and repeats it in the failure as a ready-to-run repair command — do not retype it from memory, and do not write it as \$TMPDIR/... (os.tmpdir() falls back to /tmp when TMPDIR is unset, so that spelling names a different directory in the filesystem root). The PID names the operator-owned app-server, which must sit in a DIFFERENT session from S; the smoke checks/prints all four coordinates, never starts/stops/guesses the app-server/session, never reads screen text, interrupts only its exact still-running Codex turn, and cleans only receipt-named window ids
|
|
227
|
-
./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume refused target-
|
|
227
|
+
./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume of the still-live sibling refused target-live, no window opened and no lock residue (its dormant same-id resume is deterministic-only, #147 L2). LIVE=1 ./run.sh smoke-mux-lifecycle-live
|
|
228
228
|
./run.sh check-meta-facts # deterministic gate for the meta-facts projection (#65): drives the REAL CLI — full-record join, parse-before-uniqueness, no-winner duplicates, drift/symlink/invalid-UTF-8 defects in-band, deterministic bytes, exit contract 0/2/3, dispatch+emit reachability
|
|
229
229
|
./run.sh check-herdr-plugin # deterministic gate for the #116 M2-b herdr plugin (`plugins/herdr/`): STATIC manifest shape for herdr 0.9.0 + the forbidden sections ([[startup]]/[[events]]/[[actions]]/[[link_handlers]]) asserted absent ([[build]] is check-herdr-plugin-build's subject since #116 M3-b3); BEHAVIOURAL drive of the REAL pane entry against a stub `entwurf` and stub HERDR_BIN_PATH that LOG every call — exactly one `herdr integration status` + one `entwurf peer-facts` + one `herdr agent list` per open, in that order (counted, not claimed), through ONE spawn call site; the four blocks each naming their own owner/observation time with no aggregate verdict; the D3=C fallback reading the activation ledger from the path its WRITER owns (XDG state, never the runtime's data root) and only when the phase is `active`, with ENTWURF_BIN and PATH still winning outright; the citizen axis alone skipping by name when no binary is reachable; every broken-evidence state a named red rather than a crash or an empty table, and a selected herdr atom that is `outdated`/`needs repair` as block [1]'s OWN red (`not installed` stays a reported skip); ambiguous never first-wins, diagnostics shown, activity in its own column, and zero writes to the plugin state/config dirs. No herdr binary, no Entwurf install
|
|
230
230
|
./run.sh check-herdr-runtime-bootstrap # deterministic gate for the #116 M3-b1 runtime leaf (`plugins/herdr/lib/runtime-bootstrap.mjs`): the Entwurf-owned stable active root as a REAL directory (nothing is put on PATH — the scoped wiring above names ABSOLUTE commands under it), ownership decided BEFORE the first mkdir, disk facts from lstat (a dangling symlink is not 'absent' and a link into another tree is not a directory), exact name@version + compiled entry + all three required bins present AND executable + a real `check-bridge`, the plugin-owned artifact lock coherent with the checkout, the owned npm cache and --ignore-scripts, a CERTIFIED journal (non-object/array/scalar/blank-identity/phase-contradicting-digest all refused) as the only ownership proof, all EIGHT active/staging/previous combinations named, the last good runtime never lost (torn swap AND corrupt-active beside a good backup), prior provenance carried across an install that may not finish, a failed candidate leaving the running runtime byte-identical, idempotent same-spec reinstall, a journal believed only while the disk backs it, a preflight-then-mutate inverse taking runtime last, and the refusal to record Herdr's commit as ours. NETWORK ZERO, npm ZERO — it drives a FIXTURE package, so the ACTUAL package proof lives in check-pack-install
|
|
@@ -1343,6 +1343,44 @@ check_control_send_receipt() {
|
|
|
1343
1343
|
run_vitest pi-extensions/lib/control-send-receipt.test.ts
|
|
1344
1344
|
}
|
|
1345
1345
|
|
|
1346
|
+
check_control_retire() {
|
|
1347
|
+
# Mutant execution coordinate only; the existing beside glob owns test discovery.
|
|
1348
|
+
section "Pi idle-only shutdown boundary (mutant execution coordinate)"
|
|
1349
|
+
run_vitest pi-extensions/lib/control-retire.test.ts
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
check_pi_retire_request() {
|
|
1353
|
+
# Mutant execution coordinate only; the existing beside glob owns test discovery.
|
|
1354
|
+
section "Pi retire request adapter (mutant execution coordinate)"
|
|
1355
|
+
run_vitest pi-extensions/lib/pi-retire-request.test.ts
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
check_cc_retire_pidfd() {
|
|
1359
|
+
# MUTANT EXECUTION COORDINATE, not a discovery path — check-tests-beside-behavior finds the test
|
|
1360
|
+
# like every other beside lane. This case exists because the #147 G3 CCRETIRE-* mutants name it as
|
|
1361
|
+
# gate argv; it stays in check:contracts for the same reason as check-control-send-receipt.
|
|
1362
|
+
section "Linux pidfd retire helper (mutant execution coordinate)"
|
|
1363
|
+
run_vitest pi-extensions/lib/cc-retire-pidfd.test.ts
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
check_cc_retire_request() {
|
|
1367
|
+
# Mutant execution coordinate only; the existing beside glob owns test discovery.
|
|
1368
|
+
section "Claude Code retire request adapter (mutant execution coordinate)"
|
|
1369
|
+
run_vitest pi-extensions/lib/cc-retire-request.test.ts
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
check_retirement_event() {
|
|
1373
|
+
# Mutant execution coordinate only; the existing beside glob owns test discovery.
|
|
1374
|
+
section "Retirement append leaf (mutant execution coordinate)"
|
|
1375
|
+
run_vitest pi-extensions/lib/retirement-event.test.ts
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
check_entwurf_v2_retire() {
|
|
1379
|
+
# Mutant execution coordinate only; the existing beside glob owns test discovery.
|
|
1380
|
+
section "Requested retirement dispatcher and production binding (mutant execution coordinate)"
|
|
1381
|
+
run_vitest pi-extensions/lib/entwurf-v2-retire.test.ts pi-extensions/lib/entwurf-v2-retire-production.test.ts
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1346
1384
|
check_win32_preview() {
|
|
1347
1385
|
# MUTANT EXECUTION COORDINATE, not a discovery path — the Windows preview's (#78) loader and
|
|
1348
1386
|
# platform wiring tests are found by check-tests-beside-behavior like every other beside-behaviour
|
|
@@ -1455,6 +1493,15 @@ check_acp_foreground_only() {
|
|
|
1455
1493
|
run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
|
|
1456
1494
|
}
|
|
1457
1495
|
|
|
1496
|
+
check_fresh_call_provider() {
|
|
1497
|
+
# NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
|
|
1498
|
+
# MUTANT EXECUTION COORDINATE for the #76 provider claims in scripts/mutants/fresh-call-dispatch.json
|
|
1499
|
+
# (same reasoning as check_entwurf_resume_args above): pi and pi-durable refuse a model that names
|
|
1500
|
+
# the openrouter provider, and pi refuses one that names no provider, at the dispatch root.
|
|
1501
|
+
section "fresh-call provider contract (mutant execution coordinate)"
|
|
1502
|
+
run_vitest pi-extensions/lib/fresh-call-dispatch.test.ts
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1458
1505
|
check_acp_set_model_readback() {
|
|
1459
1506
|
# NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
|
|
1460
1507
|
# MUTANT EXECUTION COORDINATE for scripts/mutants/acp-set-model-readback.json (#128): the claude
|
|
@@ -1670,7 +1717,8 @@ smoke_mux_lifecycle_live() {
|
|
|
1670
1717
|
# resumed pane_start_path == the RECORD's cwd (separate tmux query, never pane text), and the
|
|
1671
1718
|
# transcript byte-identical across the resume itself -> entwurf_v2 recalling the fact from
|
|
1672
1719
|
# BEFORE the close, which is what proves the same CONVERSATION returned. Claude Code runs the
|
|
1673
|
-
# callback+mailbox half and its resume is refused as target-
|
|
1720
|
+
# callback+mailbox half and its resume, while that sibling is still live, is refused as target-live with no
|
|
1721
|
+
# window opened and no lock residue (#147 L2: Claude Code's dormant resume is certified deterministically).
|
|
1674
1722
|
# Fixture-fenced writes (XDG, four meta roots, v2 lock dir, control socket via fixture HOME);
|
|
1675
1723
|
# the runtime auth roots stay REAL and sibling transcripts remain in the real pi agent dir as
|
|
1676
1724
|
# evidence. Private tmux server per cell, bounded teardown, operator real-root entry sets
|
|
@@ -2731,7 +2779,7 @@ EOF
|
|
|
2731
2779
|
check_claude_floor_coherence() {
|
|
2732
2780
|
# #51 policy A. The Claude Code floor is a THREE-PART version, unlike the Node
|
|
2733
2781
|
# floor's major lane: the discriminator lives in the patch position (2.1.138 drops
|
|
2734
|
-
# `args`, 2.1.139 introduces the exec form, 2.1.217 is the version
|
|
2782
|
+
# `args`, 2.1.139 introduces the exec form, 2.1.217 is the version the exec form was proven on
|
|
2735
2783
|
# in a live session). So it cannot borrow check-node-floor-coherence's major-only
|
|
2736
2784
|
# comparison and needs its own binding.
|
|
2737
2785
|
#
|
|
@@ -3973,6 +4021,10 @@ check_pack() {
|
|
|
3973
4021
|
# JS for installed entrypoints. The files array lists pi/pi-durable per FILE, so nothing admits these
|
|
3974
4022
|
# today; this tripwire is what fails loud the day an allowlist entry would.
|
|
3975
4023
|
local durable_source_pattern='^pi/pi-durable/(src/|tsconfig\.json$|overlay/runtime-contacts\.patch$)'
|
|
4024
|
+
# #148 S1b: the goldens and generator a beside test reads (pi-extensions/lib/fixtures/) are test input,
|
|
4025
|
+
# not product. `pi-extensions/` ships whole, so the `!pi-extensions/**/fixtures/**` negation is the only
|
|
4026
|
+
# thing keeping them out, and this is its tripwire (same 결합 규칙 as the `.test.ts` pattern above).
|
|
4027
|
+
local test_fixture_pattern='^pi-extensions/(.*/)?fixtures/'
|
|
3976
4028
|
|
|
3977
4029
|
local pass=1 f pat hit
|
|
3978
4030
|
|
|
@@ -3999,6 +4051,13 @@ check_pack() {
|
|
|
3999
4051
|
pass=0
|
|
4000
4052
|
fi
|
|
4001
4053
|
|
|
4054
|
+
hit=$(grep -E "$test_fixture_pattern" <<<"$file_list" || true)
|
|
4055
|
+
if [ -n "$hit" ]; then
|
|
4056
|
+
fail "[check-pack] a beside test's fixtures (pi-extensions/**/fixtures/) ride the tarball — test input is not product; restore the files-array negation:"
|
|
4057
|
+
echo "$hit" | sed 's/^/ /' >&2
|
|
4058
|
+
pass=0
|
|
4059
|
+
fi
|
|
4060
|
+
|
|
4002
4061
|
# #78 — a pack carries the Windows addon not at all, or as exactly a verified addon + manifest
|
|
4003
4062
|
# pair (pack_win32_carrier_verdict, proven on owned fixtures by check-pack-win32-carrier).
|
|
4004
4063
|
# "present" means the carrier verified, never that the build was approved for publication.
|
|
@@ -4109,7 +4168,7 @@ check_pack_win32_carrier() {
|
|
|
4109
4168
|
# which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
|
|
4110
4169
|
# below so a future suffix change cannot pass vacuously.)
|
|
4111
4170
|
pack_install_leaked_pi() {
|
|
4112
|
-
grep '^@earendil-works+' | grep -Ev '@1\.
|
|
4171
|
+
grep '^@earendil-works+' | grep -Ev '@1\.4\.0(_|$)' || true
|
|
4113
4172
|
}
|
|
4114
4173
|
|
|
4115
4174
|
# Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
|
|
@@ -4126,18 +4185,18 @@ check_pack_pin_matcher() {
|
|
|
4126
4185
|
# Cell 1 — the version boundary. Every measured peer-hash shape is a fixture: the
|
|
4127
4186
|
# 0.84.4-era `_@modelcontextprotocol+sdk@…`, the `_ws@8.21.3` observed on a real
|
|
4128
4187
|
# 0.85.1 tree, and `[측정 2026-09-20]` the combined suffix a 0.86.0 install carries.
|
|
4129
|
-
# None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`
|
|
4188
|
+
# None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`1.4.0-beta.1`,
|
|
4130
4189
|
# the prerelease shape that an unanchored match would bless) and an off-pin version.
|
|
4131
4190
|
matcher_probe=$(printf '%s\n' \
|
|
4132
|
-
'@earendil-works+pi-ai@1.
|
|
4133
|
-
'@earendil-works+pi-ai@1.
|
|
4134
|
-
'@earendil-works+pi-ai@1.
|
|
4135
|
-
'@earendil-works+pi-ai@1.
|
|
4136
|
-
'@earendil-works+pi-ai@1.
|
|
4191
|
+
'@earendil-works+pi-ai@1.4.0' \
|
|
4192
|
+
'@earendil-works+pi-ai@1.4.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
|
|
4193
|
+
'@earendil-works+pi-ai@1.4.0_ws@8.21.3' \
|
|
4194
|
+
'@earendil-works+pi-ai@1.4.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
|
|
4195
|
+
'@earendil-works+pi-ai@1.4.0-beta.1' \
|
|
4137
4196
|
'@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
|
|
4138
|
-
if [ "$matcher_probe" != '@earendil-works+pi-ai@1.
|
|
4197
|
+
if [ "$matcher_probe" != '@earendil-works+pi-ai@1.4.0-beta.1
|
|
4139
4198
|
@earendil-works+pi-agent-core@0.87.1' ]; then
|
|
4140
|
-
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.
|
|
4199
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.4.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.4.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
|
|
4141
4200
|
return 1
|
|
4142
4201
|
fi
|
|
4143
4202
|
|
|
@@ -4145,9 +4204,9 @@ check_pack_pin_matcher() {
|
|
|
4145
4204
|
# (0.85.0 onward, still true at 0.99.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
|
|
4146
4205
|
# back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
|
|
4147
4206
|
matcher_probe=$(printf '%s\n' \
|
|
4148
|
-
'@earendil-works+chord@1.
|
|
4207
|
+
'@earendil-works+chord@1.4.0' \
|
|
4149
4208
|
'@earendil-works+chord@0.87.1' \
|
|
4150
|
-
'@earendil-works+pi-ai@1.
|
|
4209
|
+
'@earendil-works+pi-ai@1.4.0' | pack_install_leaked_pi)
|
|
4151
4210
|
if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
|
|
4152
4211
|
fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it was a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0, `[측정 2026-09-23]` at 0.87.1 and `[측정 2026-09-30]` at 0.99.1, where pi-coding-agent adds pi-codemode and pi-mcp beside them; `[측정 2026-10-13]` at 1.2.0 pi-durable alone declares it) — got: ${matcher_probe:-<nothing leaked>}"
|
|
4153
4212
|
return 1
|
|
@@ -4161,7 +4220,7 @@ check_pack_pin_matcher() {
|
|
|
4161
4220
|
# Cells: L1s (Entwurf alone, $3), L1 (project-local beside an aligned host pi-coding-agent at the
|
|
4162
4221
|
# pinned SDK version), L2 (an isolated `npm -g --prefix`), each through (a) the INSTALLED verifier
|
|
4163
4222
|
# (exit 0, prints its own carrier-resolver) and (b) an independent consumer oracle; then the L3
|
|
4164
|
-
# NEGATIVE (an override binds pi-durable's pi-
|
|
4223
|
+
# NEGATIVE (an override binds pi-durable's pi-codemode edge to an older published version: the installed verifier must
|
|
4165
4224
|
# refuse it by name, while a bare import still succeeds); then (c) the public launch on L1s.
|
|
4166
4225
|
# The oracle observes RESOLUTIONS (a resolve hook), not every filesystem read; it asserts each
|
|
4167
4226
|
# resolved product file lies inside THIS consumer's node_modules — a sandbox ancestor is not the
|
|
@@ -4256,13 +4315,18 @@ ORACLE
|
|
|
4256
4315
|
echo "[check-pack-install] pi-durable $cell: installed verifier PASS; consumer oracle $out"
|
|
4257
4316
|
done
|
|
4258
4317
|
|
|
4259
|
-
|
|
4318
|
+
# The wrong edge must still IMPORT, or the control proves a crash rather than a refusal. `[측정 2026-10-22]`
|
|
4319
|
+
# SDK 1.4 pi-durable imports pi-ai's new `utils/identity`, absent from the older pi-ai control versions
|
|
4320
|
+
# checked here (1.0.3 and 1.3.0 both fail to load); pi-telemetry 1.0.3/1.3.0 and chord 1.3.0 lack exports
|
|
4321
|
+
# it imports. pi-codemode 1.3.0 loads, and depends on no SDK member, so this tree carries exactly one
|
|
4322
|
+
# depth-2 mismatch (L4 keeps depth 3).
|
|
4323
|
+
printf '%s\n' "{\"name\":\"pd-mismatch\",\"version\":\"1.0.0\",\"private\":true,\"overrides\":{\"@earendil-works/pi-durable\":{\"@earendil-works/pi-codemode\":\"1.3.0\"}}}" > "$box/pd-mismatch/package.json"
|
|
4260
4324
|
out=$(cd "$box/pd-mismatch" && npm install "$tgz" --no-audit --no-fund 2>&1) || {
|
|
4261
4325
|
fail "[check-pack-install] pi-durable L3 (mismatch control) install failed:"; echo "$out" | tail -10 | sed 's/^/ /' >&2; return 1; }
|
|
4262
4326
|
pkg="$box/pd-mismatch/node_modules/@junghanacs/entwurf"
|
|
4263
4327
|
rc=0; out=$(cd "$box" && node "$pkg/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js" resolve 2>&1) || rc=$?
|
|
4264
|
-
if [ "$rc" -ne 3 ] || ! grep -q "pi-durable-sdk-mismatch: @earendil-works/pi-durable → @earendil-works/pi-
|
|
4265
|
-
fail "[QK:PACK-INSTALL-DURABLE-MISMATCH-REFUSED] [check-pack-install] pi-durable L3: an installed tree whose pi-durable binds pi-
|
|
4328
|
+
if [ "$rc" -ne 3 ] || ! grep -q "pi-durable-sdk-mismatch: @earendil-works/pi-durable → @earendil-works/pi-codemode wants $pin_ver, binds 1.3.0" <<<"$out"; then
|
|
4329
|
+
fail "[QK:PACK-INSTALL-DURABLE-MISMATCH-REFUSED] [check-pack-install] pi-durable L3: an installed tree whose pi-durable binds pi-codemode 1.3.0 must be refused by name as pi-durable-sdk-mismatch (rc=$rc): $out"
|
|
4266
4330
|
return 1
|
|
4267
4331
|
fi
|
|
4268
4332
|
rc=0; out=$(cd "$box" && node --input-type=module -e "import { pathToFileURL } from 'node:url'; await import('$pkg/pi/pi-durable/carrier-resolver.mjs'); const m = await import(pathToFileURL('$pkg/pi/pi-durable/carrier/runtime.js').href); console.log(typeof m.openDurable);" 2>&1) || rc=$?
|
|
@@ -4787,19 +4851,21 @@ _check_pack_install_impl() {
|
|
|
4787
4851
|
# `[측정 2026-10-16]` at 1.3.0 the constellation is unchanged: the installed manifests give the
|
|
4788
4852
|
# same member edges as at 1.2.0, now every one exact 1.3.0 (upstream #10865), and the consumer
|
|
4789
4853
|
# oracle walked 10 member edges with 0 mismatches (.tmp-verify/145-stage1/08-pack-install.log).
|
|
4790
|
-
|
|
4854
|
+
# `[측정 2026-10-22]` at 1.4.0 the nine members are again exact 1.4.0 edges (registry manifests; #148 S0),
|
|
4855
|
+
# and pi-tui 1.4.0 adds one third-party dependency (marked-cjk-friendly 0.1.2) outside the closure.
|
|
4856
|
+
echo "[check-pack-install] pnpm add into $tmp (with the aligned 1.4.x SDK + chord + codemode + mcp + durable + typebox)"
|
|
4791
4857
|
local install_log
|
|
4792
4858
|
install_log=$(cd "$tmp" && pnpm add \
|
|
4793
4859
|
"$tgz_path" \
|
|
4794
|
-
"@earendil-works/pi-ai@1.
|
|
4795
|
-
"@earendil-works/pi-coding-agent@1.
|
|
4796
|
-
"@earendil-works/pi-tui@1.
|
|
4797
|
-
"@earendil-works/pi-agent-core@1.
|
|
4798
|
-
"@earendil-works/pi-telemetry@1.
|
|
4799
|
-
"@earendil-works/chord@1.
|
|
4800
|
-
"@earendil-works/pi-codemode@1.
|
|
4801
|
-
"@earendil-works/pi-mcp@1.
|
|
4802
|
-
"@earendil-works/pi-durable@1.
|
|
4860
|
+
"@earendil-works/pi-ai@1.4.0" \
|
|
4861
|
+
"@earendil-works/pi-coding-agent@1.4.0" \
|
|
4862
|
+
"@earendil-works/pi-tui@1.4.0" \
|
|
4863
|
+
"@earendil-works/pi-agent-core@1.4.0" \
|
|
4864
|
+
"@earendil-works/pi-telemetry@1.4.0" \
|
|
4865
|
+
"@earendil-works/chord@1.4.0" \
|
|
4866
|
+
"@earendil-works/pi-codemode@1.4.0" \
|
|
4867
|
+
"@earendil-works/pi-mcp@1.4.0" \
|
|
4868
|
+
"@earendil-works/pi-durable@1.4.0" \
|
|
4803
4869
|
"typebox@latest" \
|
|
4804
4870
|
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
4805
4871
|
fail "[check-pack-install] pnpm add failed:"
|
|
@@ -4815,11 +4881,11 @@ _check_pack_install_impl() {
|
|
|
4815
4881
|
local leaked_pi
|
|
4816
4882
|
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
|
|
4817
4883
|
if [ -n "$leaked_pi" ]; then
|
|
4818
|
-
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.
|
|
4884
|
+
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.4.0):"
|
|
4819
4885
|
printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
|
|
4820
4886
|
return 1
|
|
4821
4887
|
fi
|
|
4822
|
-
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.
|
|
4888
|
+
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.4.0 (chord and pi-durable included)"
|
|
4823
4889
|
|
|
4824
4890
|
# Resolve the installed package.json and confirm pi.extensions
|
|
4825
4891
|
# arrived intact. If pi.extensions is empty or missing, the
|
|
@@ -5381,7 +5447,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
|
|
|
5381
5447
|
#!/usr/bin/env bash
|
|
5382
5448
|
printf '%s\n' "$*" >> "${FAKE_CLAUDE_LOG:?}"
|
|
5383
5449
|
case "$1${2:+ $2}" in
|
|
5384
|
-
"--version") echo "2.1.
|
|
5450
|
+
"--version") echo "2.1.306 (Claude Code)" ;;
|
|
5385
5451
|
"plugin validate") : ;;
|
|
5386
5452
|
"plugin uninstall") : ;;
|
|
5387
5453
|
"plugin marketplace") : ;;
|
|
@@ -5457,6 +5523,8 @@ SH
|
|
|
5457
5523
|
mkdir -p "$nm_probe/lib"
|
|
5458
5524
|
cp "$installed_hook" "$nm_probe/meta-bridge-hook.js"
|
|
5459
5525
|
cp "$stable_asm/entwurf-meta-receive/lib/meta-session.js" "$nm_probe/lib/meta-session.js"
|
|
5526
|
+
# The real installer already supplies this hook import; the node_modules probe must too.
|
|
5527
|
+
cp "$stable_asm/entwurf-meta-receive/lib/entwurf-v2-lock.js" "$nm_probe/lib/entwurf-v2-lock.js"
|
|
5460
5528
|
cp "$stable_asm/entwurf-meta-receive/lib/session-id.js" "$nm_probe/lib/session-id.js"
|
|
5461
5529
|
cp "$stable_asm/entwurf-meta-receive/entwurf-capabilities.json" "$nm_probe/entwurf-capabilities.json"
|
|
5462
5530
|
local probe_env='{"session_id":"pack-probe","transcript_path":"/tmp/x.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"probe"}}'
|
|
@@ -5695,7 +5763,7 @@ function finish(trimmed) {
|
|
|
5695
5763
|
// gate that exists to catch "green clone, dead consumer". Equality also catches the
|
|
5696
5764
|
// other direction the narrow-surface rule cares about: an extra or duplicated verb
|
|
5697
5765
|
// reaching hosts without a decision. Written sorted; `names` is sorted above.
|
|
5698
|
-
const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
5766
|
+
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'];
|
|
5699
5767
|
if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
|
|
5700
5768
|
console.error('installed MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
|
|
5701
5769
|
process.exit(1);
|
|
@@ -6257,12 +6325,12 @@ function finishOk(trimmed) {
|
|
|
6257
6325
|
}
|
|
6258
6326
|
const names = tools.map((t) => t?.name).sort();
|
|
6259
6327
|
// EXACT set, not a floor. This is the assertion behind the operator-facing claim that
|
|
6260
|
-
// `entwurf check-bridge` proves the installed bytes list the
|
|
6328
|
+
// `entwurf check-bridge` proves the installed bytes list the nine garden tools — and
|
|
6261
6329
|
// under an installed package this launcher IS the dist branch of start.sh, so a subset
|
|
6262
6330
|
// check here let an artifact missing entwurf_fresh_call / entwurf_resume_call read as a
|
|
6263
6331
|
// green bridge. Equality also refuses an undecided extra verb. Written sorted; `names`
|
|
6264
6332
|
// is sorted above.
|
|
6265
|
-
const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
6333
|
+
const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_retire_call', 'entwurf_self', 'entwurf_v2'];
|
|
6266
6334
|
if (names.length !== expected.length || expected.some((n, i) => names[i] !== n)) {
|
|
6267
6335
|
console.error(`MCP tool set MISMATCH — want exactly [${expected.join(',')}] got [${names.join(',')}]`);
|
|
6268
6336
|
process.exit(1);
|
|
@@ -7502,6 +7570,24 @@ case "$cmd" in
|
|
|
7502
7570
|
check-control-send-receipt)
|
|
7503
7571
|
check_control_send_receipt
|
|
7504
7572
|
;;
|
|
7573
|
+
check-control-retire)
|
|
7574
|
+
check_control_retire
|
|
7575
|
+
;;
|
|
7576
|
+
check-pi-retire-request)
|
|
7577
|
+
check_pi_retire_request
|
|
7578
|
+
;;
|
|
7579
|
+
check-cc-retire-pidfd)
|
|
7580
|
+
check_cc_retire_pidfd
|
|
7581
|
+
;;
|
|
7582
|
+
check-cc-retire-request)
|
|
7583
|
+
check_cc_retire_request
|
|
7584
|
+
;;
|
|
7585
|
+
check-retirement-event)
|
|
7586
|
+
check_retirement_event
|
|
7587
|
+
;;
|
|
7588
|
+
check-entwurf-v2-retire)
|
|
7589
|
+
check_entwurf_v2_retire
|
|
7590
|
+
;;
|
|
7505
7591
|
check-win32-preview)
|
|
7506
7592
|
check_win32_preview
|
|
7507
7593
|
;;
|
|
@@ -7532,6 +7618,9 @@ case "$cmd" in
|
|
|
7532
7618
|
check-acp-foreground-only)
|
|
7533
7619
|
check_acp_foreground_only
|
|
7534
7620
|
;;
|
|
7621
|
+
check-fresh-call-provider)
|
|
7622
|
+
check_fresh_call_provider
|
|
7623
|
+
;;
|
|
7535
7624
|
check-acp-set-model-readback)
|
|
7536
7625
|
check_acp_set_model_readback
|
|
7537
7626
|
;;
|