@junghanacs/entwurf 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/BASELINE.md +60 -5
- package/CHANGELOG.md +557 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +52 -2
- package/FAQ.md +8 -4
- package/README.md +37 -17
- package/VERIFY.md +23 -10
- package/docs/acp-backend-rail.md +5 -1
- package/docs/external-mcp-host.md +8 -6
- package/docs/fresh-cut-policy.md +9 -1
- package/docs/setup-clean-host.md +64 -9
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +100 -24
- 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-mailbox-body.js +10 -4
- 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/remote-pair-receive.js +123 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +259 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js +80 -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/dist/scripts/remote-pair-receive.js +65 -0
- package/mcp/entwurf-bridge/src/index.ts +118 -24
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/mcp/entwurf-bridge/tsconfig.build.json +17 -1
- package/package.json +5 -5
- package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
- package/pi-extensions/entwurf-control.ts +55 -20
- package/pi-extensions/lib/cc-retire-pidfd.py +236 -0
- package/pi-extensions/lib/cc-retire-request.ts +553 -0
- package/pi-extensions/lib/control-retire.ts +195 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +17 -1
- package/pi-extensions/lib/entwurf-v2-retire-production.ts +227 -0
- package/pi-extensions/lib/entwurf-v2-retire.ts +269 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +2 -1
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +390 -36
- package/pi-extensions/lib/fresh-call-composition.ts +15 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +45 -4
- package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +13 -4
- 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/remote-pair-receive.ts +184 -0
- package/pi-extensions/lib/remote-pair-send.ts +325 -0
- package/pi-extensions/lib/remote-pair-transport.ts +81 -0
- package/pi-extensions/lib/resume-launch-identity.ts +104 -18
- package/pi-extensions/lib/retirement-event.ts +253 -0
- package/pi-extensions/meta-bridge-hook.ts +142 -13
- package/pi-extensions/meta-bridge-pi-durable.ts +56 -2
- package/run.sh +122 -7
- package/scripts/agy-bridge-config.py +4 -1
- package/scripts/build-bridge.sh +14 -0
- package/scripts/check-capability-bundle-reach.ts +135 -2
- package/scripts/check-entwurf-bridge-boot.ts +2 -1
- package/scripts/check-entwurf-fact-provider.ts +1 -0
- package/scripts/check-entwurf-v2-production.ts +2 -0
- package/scripts/check-entwurf-v2-surface.ts +7 -0
- package/scripts/check-gate-qualification.ts +20 -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-remote-pair-installed.ts +242 -0
- package/scripts/check-resume-launch-identity.ts +62 -23
- package/scripts/lib/meta-hook-bundle.ts +72 -0
- package/scripts/lib/remote-pair-installed-audit.ts +25 -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 +2 -2
- package/scripts/mutants/copilot-receive.json +2 -6
- package/scripts/mutants/entwurf-v2-retire.json +476 -0
- package/scripts/mutants/fresh-call-dispatch.json +37 -0
- package/scripts/mutants/meta-hook-session-switch.json +88 -0
- package/scripts/mutants/meta-identity.json +57 -0
- package/scripts/mutants/mux-fresh-call.json +1 -1
- package/scripts/mutants/mux-resume-call.json +40 -4
- package/scripts/mutants/pi-durable.json +79 -0
- package/scripts/mutants/pi-retire-request.json +72 -0
- package/scripts/mutants/remote-pair-display.json +53 -0
- package/scripts/mutants/remote-pair-installed-audit.json +28 -0
- package/scripts/mutants/remote-pair-receive.json +126 -0
- package/scripts/mutants/remote-pair-roundtrip.json +83 -0
- package/scripts/mutants/remote-pair-send.json +194 -0
- package/scripts/mutants/remote-pair-transport.json +66 -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/remote-pair-receive.ts +73 -0
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-raw-install-live.sh +5 -4
- package/scripts/smoke-meta-install-state.sh +4 -4
- package/scripts/smoke-mux-lifecycle-live.ts +10 -10
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
- package/scripts/smoke-pi-provider-state.sh +3 -3
|
@@ -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. */
|
|
@@ -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
|
|
@@ -328,6 +328,9 @@ Usage:
|
|
|
328
328
|
./run.sh meta-bridge-prune # 1.0.0 meta-bridge Phase 4: LISTING-ONLY store hygiene — classify orphan/stale/ambiguous/keep, print manual rm commands, delete NOTHING ([dir] [--ttl-days N])
|
|
329
329
|
./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only. EXIT CONTRACT (#54, `--help` prints it): 0 complete / 1 NOTHING MOVED (re-run, do not setup) / 2 usage / 3 cut transition incomplete (inspect) / 4 cut complete but residue cleanup failed (`setup` may run; re-run only before new citizen birth, otherwise remove residue manually)
|
|
330
330
|
./run.sh meta-facts # #65 owner-normalized READ-ONLY store projection: deterministic JSON {schemaVersion:1, storeDir, citizens: full v3 records sorted by gardenId, defects: {filename,message}} — THE listing contract emitted by the owner so consumers stop copying the certification. No liveness/sockets/transcript contents. EXIT: 0 readable (defects in-band; missing store = empty), 2 usage, 3 unreadable ([dir])
|
|
331
|
+
./run.sh check-remote-pair-installed # #107 rail 3, CHECKOUT-ONLY: install a caller-supplied candidate tarball (--tgz <abs>) into a sandboxed consumer prefix, check its compiled remote-pair surface, run the installed `entwurf remote-pair-receive` (usage exit 2 = twin reached), run the T2 roundtrip cells against the installed bridge+entry, and audit that the subject processes resolve every module inside the consumer prefix (with a checkout control). Never repacks; no real ssh, no model
|
|
332
|
+
./run.sh check-remote-pair-installed-audit # #107 rail 3: the deterministic counterpart of check-remote-pair-installed — its checkout-invisibility acceptance primitive proven on synthetic logs (real path, root boundary). In check:vitest
|
|
333
|
+
./run.sh remote-pair-receive # #107 rail 1: the RECEIVING half of one operator-declared remote pair. Reads ONE JSON claim {from,fromAgentId,fromCwd,target,message,wantsReply} on stdin, refuses a from/target outside the declared pair before any mutation (a scope limit under the same-operator SSH account trust model, not authentication), then hands a match to the existing v2 dispatch. Ambient sender carriers are never read. Installed, run_ts runs its compiled twin (dist/scripts/remote-pair-receive.js). EXIT: 0 dispatched / 4 dispatched, the dispatch reported an error (delivery not inferred) / 3 pair refused / 2 invalid claim or declaration (JSON receipt) or wrong argv (usage on stderr, no receipt) / 1 uncaught dispatch or store throw (no receipt; the sender reads it as indeterminate) (--pair <absolute declaration file>)
|
|
331
334
|
./run.sh peer-facts # #116 M2-a owner-normalized READ-ONLY peer projection: the same listEntwurfFacts provider through the same renderer entwurf_peers uses, emitted as deterministic JSON {schemaVersion:1, storeDir, peers, diagnostics} so a consumer never re-implements the placement join (whose pi half is a measured vendor floor). NOT the same bytes as an entwurf_peers call and deliberately so: that surface is a human listing (observationLimit, text over the wire), this is a machine projection with UNBOUNDED observation, because a rationed row says `unobserved` and a machine cannot tell that from "nobody could look". `placement` stays a STRUCTURED tagged union ({kind:"herdr-pane",paneId} / unobserved / none / ambiguous) — never the human `herdr <pane>` string. Publishes NO socket coordinate (ENTWURF_DIR selects what is PROBED, #50 C4) and NO herdr agent_status / interactive_ready / screen text. One placement read per listing, no watcher. EXIT: 0 readable (diagnostics in-band; missing store = empty), 2 usage, 3 unreadable ([dir])
|
|
332
335
|
./run.sh meta-bridge-managed-keys # 0.10.0 meta-bridge: print the SSOT of settings keys entwurf OWNS (consumers read this to stay disjoint — keyset-owner invariant)
|
|
333
336
|
./run.sh check-keyset-overlap <fragment.json...> # 0.10.0 meta-bridge: PREVENTIVE keyset guard — fail if a consumer fragment collides with any pi-owned key (cross-repo; not in pnpm check)
|
|
@@ -1340,6 +1343,44 @@ check_control_send_receipt() {
|
|
|
1340
1343
|
run_vitest pi-extensions/lib/control-send-receipt.test.ts
|
|
1341
1344
|
}
|
|
1342
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
|
+
|
|
1343
1384
|
check_win32_preview() {
|
|
1344
1385
|
# MUTANT EXECUTION COORDINATE, not a discovery path — the Windows preview's (#78) loader and
|
|
1345
1386
|
# platform wiring tests are found by check-tests-beside-behavior like every other beside-behaviour
|
|
@@ -1452,6 +1493,15 @@ check_acp_foreground_only() {
|
|
|
1452
1493
|
run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
|
|
1453
1494
|
}
|
|
1454
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
|
+
|
|
1455
1505
|
check_acp_set_model_readback() {
|
|
1456
1506
|
# NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
|
|
1457
1507
|
# MUTANT EXECUTION COORDINATE for scripts/mutants/acp-set-model-readback.json (#128): the claude
|
|
@@ -1667,7 +1717,8 @@ smoke_mux_lifecycle_live() {
|
|
|
1667
1717
|
# resumed pane_start_path == the RECORD's cwd (separate tmux query, never pane text), and the
|
|
1668
1718
|
# transcript byte-identical across the resume itself -> entwurf_v2 recalling the fact from
|
|
1669
1719
|
# BEFORE the close, which is what proves the same CONVERSATION returned. Claude Code runs the
|
|
1670
|
-
# 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).
|
|
1671
1722
|
# Fixture-fenced writes (XDG, four meta roots, v2 lock dir, control socket via fixture HOME);
|
|
1672
1723
|
# the runtime auth roots stay REAL and sibling transcripts remain in the real pi agent dir as
|
|
1673
1724
|
# evidence. Private tmux server per cell, bounded teardown, operator real-root entry sets
|
|
@@ -2728,7 +2779,7 @@ EOF
|
|
|
2728
2779
|
check_claude_floor_coherence() {
|
|
2729
2780
|
# #51 policy A. The Claude Code floor is a THREE-PART version, unlike the Node
|
|
2730
2781
|
# floor's major lane: the discriminator lives in the patch position (2.1.138 drops
|
|
2731
|
-
# `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
|
|
2732
2783
|
# in a live session). So it cannot borrow check-node-floor-coherence's major-only
|
|
2733
2784
|
# comparison and needs its own binding.
|
|
2734
2785
|
#
|
|
@@ -3387,6 +3438,17 @@ check_acp_sdk_surface() {
|
|
|
3387
3438
|
run_vitest test/acp-sdk-surface.contract.test.ts
|
|
3388
3439
|
}
|
|
3389
3440
|
|
|
3441
|
+
check_remote_pair_installed_audit() {
|
|
3442
|
+
# #107 rail 3. The deterministic counterpart of `check-remote-pair-installed`: that gate needs a
|
|
3443
|
+
# caller-supplied tarball, so it cannot run in the floor or carry mutants, and its
|
|
3444
|
+
# checkout-invisibility verdict rests on one acceptance primitive
|
|
3445
|
+
# (scripts/lib/remote-pair-installed-audit.ts). This proves that primitive on tiny synthetic
|
|
3446
|
+
# logs and files. `test/**` is not discovered by check-tests-beside-behavior, so it needs this
|
|
3447
|
+
# public coordinate. Zero network, zero child process.
|
|
3448
|
+
section "remote-pair installed-closure audit (synthetic logs)"
|
|
3449
|
+
run_vitest test/remote-pair-installed-audit.test.ts
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3390
3452
|
check_pi_queue_oracle() {
|
|
3391
3453
|
# #120 P2. The PUBLIC execution coordinate for `test/pi-queue.oracle.test.ts`, and the reason it
|
|
3392
3454
|
# needs one: the discovery door below selects `pi-extensions/**/*.test.ts` and
|
|
@@ -3829,6 +3891,12 @@ check_pack() {
|
|
|
3829
3891
|
# out: the placement join it emits is the copy a herdr-side consumer would
|
|
3830
3892
|
# otherwise fork, and such a consumer runs against an installed host.
|
|
3831
3893
|
"mcp/entwurf-bridge/dist/scripts/peer-facts.js"
|
|
3894
|
+
# #107 — the receiving host's entry the sender's ssh runs (run_ts twin), plus the three
|
|
3895
|
+
# remote-pair libs the bridge's entwurf_v2 handler and that entry import.
|
|
3896
|
+
"mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js"
|
|
3897
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js"
|
|
3898
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js"
|
|
3899
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js"
|
|
3832
3900
|
# #87 Bundle B — omp-receive-doctor drives this operator projection through
|
|
3833
3901
|
# run.sh, so installed node_modules must have the compiled twin too.
|
|
3834
3902
|
"mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
|
|
@@ -4570,6 +4638,12 @@ _check_pack_install_impl() {
|
|
|
4570
4638
|
"mcp/entwurf-bridge/dist/scripts/meta-facts.js"
|
|
4571
4639
|
# #116 M2-a — the owner-normalized peer projection (see check-pack).
|
|
4572
4640
|
"mcp/entwurf-bridge/dist/scripts/peer-facts.js"
|
|
4641
|
+
# #107 — the receiving host's entry the sender's ssh runs (run_ts twin), plus the three
|
|
4642
|
+
# remote-pair libs the bridge's entwurf_v2 handler and that entry import.
|
|
4643
|
+
"mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js"
|
|
4644
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js"
|
|
4645
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js"
|
|
4646
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js"
|
|
4573
4647
|
# #87 Bundle B — omp-receive-doctor reaches this through the installed dispatcher.
|
|
4574
4648
|
"mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
|
|
4575
4649
|
# 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
|
|
@@ -5355,7 +5429,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
|
|
|
5355
5429
|
#!/usr/bin/env bash
|
|
5356
5430
|
printf '%s\n' "$*" >> "${FAKE_CLAUDE_LOG:?}"
|
|
5357
5431
|
case "$1${2:+ $2}" in
|
|
5358
|
-
"--version") echo "2.1.
|
|
5432
|
+
"--version") echo "2.1.306 (Claude Code)" ;;
|
|
5359
5433
|
"plugin validate") : ;;
|
|
5360
5434
|
"plugin uninstall") : ;;
|
|
5361
5435
|
"plugin marketplace") : ;;
|
|
@@ -5431,6 +5505,8 @@ SH
|
|
|
5431
5505
|
mkdir -p "$nm_probe/lib"
|
|
5432
5506
|
cp "$installed_hook" "$nm_probe/meta-bridge-hook.js"
|
|
5433
5507
|
cp "$stable_asm/entwurf-meta-receive/lib/meta-session.js" "$nm_probe/lib/meta-session.js"
|
|
5508
|
+
# The real installer already supplies this hook import; the node_modules probe must too.
|
|
5509
|
+
cp "$stable_asm/entwurf-meta-receive/lib/entwurf-v2-lock.js" "$nm_probe/lib/entwurf-v2-lock.js"
|
|
5434
5510
|
cp "$stable_asm/entwurf-meta-receive/lib/session-id.js" "$nm_probe/lib/session-id.js"
|
|
5435
5511
|
cp "$stable_asm/entwurf-meta-receive/entwurf-capabilities.json" "$nm_probe/entwurf-capabilities.json"
|
|
5436
5512
|
local probe_env='{"session_id":"pack-probe","transcript_path":"/tmp/x.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"probe"}}'
|
|
@@ -5669,7 +5745,7 @@ function finish(trimmed) {
|
|
|
5669
5745
|
// gate that exists to catch "green clone, dead consumer". Equality also catches the
|
|
5670
5746
|
// other direction the narrow-surface rule cares about: an extra or duplicated verb
|
|
5671
5747
|
// reaching hosts without a decision. Written sorted; `names` is sorted above.
|
|
5672
|
-
const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
5748
|
+
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'];
|
|
5673
5749
|
if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
|
|
5674
5750
|
console.error('installed MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
|
|
5675
5751
|
process.exit(1);
|
|
@@ -6231,12 +6307,12 @@ function finishOk(trimmed) {
|
|
|
6231
6307
|
}
|
|
6232
6308
|
const names = tools.map((t) => t?.name).sort();
|
|
6233
6309
|
// EXACT set, not a floor. This is the assertion behind the operator-facing claim that
|
|
6234
|
-
// `entwurf check-bridge` proves the installed bytes list the
|
|
6310
|
+
// `entwurf check-bridge` proves the installed bytes list the nine garden tools — and
|
|
6235
6311
|
// under an installed package this launcher IS the dist branch of start.sh, so a subset
|
|
6236
6312
|
// check here let an artifact missing entwurf_fresh_call / entwurf_resume_call read as a
|
|
6237
6313
|
// green bridge. Equality also refuses an undecided extra verb. Written sorted; `names`
|
|
6238
6314
|
// is sorted above.
|
|
6239
|
-
const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
6315
|
+
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'];
|
|
6240
6316
|
if (names.length !== expected.length || expected.some((n, i) => names[i] !== n)) {
|
|
6241
6317
|
console.error(`MCP tool set MISMATCH — want exactly [${expected.join(',')}] got [${names.join(',')}]`);
|
|
6242
6318
|
process.exit(1);
|
|
@@ -7476,6 +7552,24 @@ case "$cmd" in
|
|
|
7476
7552
|
check-control-send-receipt)
|
|
7477
7553
|
check_control_send_receipt
|
|
7478
7554
|
;;
|
|
7555
|
+
check-control-retire)
|
|
7556
|
+
check_control_retire
|
|
7557
|
+
;;
|
|
7558
|
+
check-pi-retire-request)
|
|
7559
|
+
check_pi_retire_request
|
|
7560
|
+
;;
|
|
7561
|
+
check-cc-retire-pidfd)
|
|
7562
|
+
check_cc_retire_pidfd
|
|
7563
|
+
;;
|
|
7564
|
+
check-cc-retire-request)
|
|
7565
|
+
check_cc_retire_request
|
|
7566
|
+
;;
|
|
7567
|
+
check-retirement-event)
|
|
7568
|
+
check_retirement_event
|
|
7569
|
+
;;
|
|
7570
|
+
check-entwurf-v2-retire)
|
|
7571
|
+
check_entwurf_v2_retire
|
|
7572
|
+
;;
|
|
7479
7573
|
check-win32-preview)
|
|
7480
7574
|
check_win32_preview
|
|
7481
7575
|
;;
|
|
@@ -7506,6 +7600,9 @@ case "$cmd" in
|
|
|
7506
7600
|
check-acp-foreground-only)
|
|
7507
7601
|
check_acp_foreground_only
|
|
7508
7602
|
;;
|
|
7603
|
+
check-fresh-call-provider)
|
|
7604
|
+
check_fresh_call_provider
|
|
7605
|
+
;;
|
|
7509
7606
|
check-acp-set-model-readback)
|
|
7510
7607
|
check_acp_set_model_readback
|
|
7511
7608
|
;;
|
|
@@ -8412,6 +8509,21 @@ case "$cmd" in
|
|
|
8412
8509
|
shift || true
|
|
8413
8510
|
run_ts scripts/meta-facts.ts "$@"
|
|
8414
8511
|
;;
|
|
8512
|
+
check-remote-pair-installed)
|
|
8513
|
+
# #107 rail 3, CHECKOUT-ONLY install gate: installs a caller-supplied candidate tarball into a
|
|
8514
|
+
# sandboxed consumer prefix and drives that install's bridge and entry through the T2 scene,
|
|
8515
|
+
# auditing that the subject processes resolve nothing outside the prefix. Never repacks.
|
|
8516
|
+
shift || true
|
|
8517
|
+
run_ts scripts/check-remote-pair-installed.ts "$@"
|
|
8518
|
+
;;
|
|
8519
|
+
remote-pair-receive)
|
|
8520
|
+
# #107 rail 1: the receiving host's entry for one declared remote pair. The pair
|
|
8521
|
+
# check runs before the v2 dispatch, so a claim outside the pair mutates nothing;
|
|
8522
|
+
# a match reaches the same record/owner/deliverability checks every local send does.
|
|
8523
|
+
# Installed, run_ts runs its compiled twin; the sending host's ssh reaches it through the bin.
|
|
8524
|
+
shift || true
|
|
8525
|
+
run_ts scripts/remote-pair-receive.ts "$@"
|
|
8526
|
+
;;
|
|
8415
8527
|
peer-facts)
|
|
8416
8528
|
# #116 M2-a READ-ONLY projection of the OBSERVED peer listing: one provider
|
|
8417
8529
|
# (`listEntwurfFacts`) through one renderer (`renderEntwurfPeers().payload`), so
|
|
@@ -8481,6 +8593,9 @@ case "$cmd" in
|
|
|
8481
8593
|
check-pi-queue-oracle)
|
|
8482
8594
|
check_pi_queue_oracle
|
|
8483
8595
|
;;
|
|
8596
|
+
check-remote-pair-installed-audit)
|
|
8597
|
+
check_remote_pair_installed_audit
|
|
8598
|
+
;;
|
|
8484
8599
|
check-demo-reproducibility)
|
|
8485
8600
|
check_demo_reproducibility
|
|
8486
8601
|
;;
|
|
@@ -102,7 +102,7 @@ PERMISSION_STATE_SCHEMA_VERSION = 2
|
|
|
102
102
|
# EVERY call. The bridge was registered and two thirds of it was unusable — the same failure the
|
|
103
103
|
# doctor already names for entwurf_v2, just never checked for the other two.
|
|
104
104
|
#
|
|
105
|
-
# The
|
|
105
|
+
# The five tools deliberately NOT here, because a grant we do not need is a grant we should not take:
|
|
106
106
|
# entwurf_inbox_read — native-push has no inbox to drain. An agy citizen has no meta-mailbox
|
|
107
107
|
# directory at all (measured), so granting it would pre-approve a rail this
|
|
108
108
|
# backend does not have.
|
|
@@ -113,6 +113,9 @@ PERMISSION_STATE_SCHEMA_VERSION = 2
|
|
|
113
113
|
# refuse without a pane anchor. An agy conversation is not a tmux client,
|
|
114
114
|
# so these are not on its normal path either; the count moved from two to
|
|
115
115
|
# four when 0.14 shipped the mux verbs, and the granted set did not.
|
|
116
|
+
# entwurf_retire_call — one retirement attempt on a target a human operator explicitly named.
|
|
117
|
+
# Pre-approving it would replace that request with an installer grant, so
|
|
118
|
+
# it stays at agy's default Ask; the count moved from four to five (#147).
|
|
116
119
|
ALLOW_RULES = tuple(f"mcp({SERVER_KEY}/{tool})" for tool in ("entwurf_v2", "entwurf_peers", "entwurf_self"))
|
|
117
120
|
# Rules that MATCH one of our tools: its exact grant, the server-wide rule, and the action wildcard.
|
|
118
121
|
# Membership here is a statement about agy's matcher, not about which list the rule sits in — the
|
package/scripts/build-bridge.sh
CHANGED
|
@@ -36,3 +36,17 @@ TSC="$REPO_DIR/node_modules/.bin/tsc"
|
|
|
36
36
|
# dependent function called from its own location always resolves).
|
|
37
37
|
cp "$REPO_DIR/pi/entwurf-capabilities.json" \
|
|
38
38
|
"$REPO_DIR/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json"
|
|
39
|
+
|
|
40
|
+
# The Claude Code retire helper must TRAVEL WITH the bundle too (#147 G3).
|
|
41
|
+
#
|
|
42
|
+
# `ccRetireHelperPath()` resolves ONE fixed basename beside its own module, so the emitted
|
|
43
|
+
# dist/pi-extensions/lib/cc-retire-request.js finds the helper only if it sits next to it. The
|
|
44
|
+
# canonical bytes are pi-extensions/lib/cc-retire-pidfd.py; this is a byte copy, never an edit.
|
|
45
|
+
# A missing or non-regular source fails the build — there is no fallback location. Gated by
|
|
46
|
+
# check-capability-bundle-reach (the compiled adapter asks from where it lives, bytes compared).
|
|
47
|
+
CC_RETIRE_HELPER="$REPO_DIR/pi-extensions/lib/cc-retire-pidfd.py"
|
|
48
|
+
if [ ! -f "$CC_RETIRE_HELPER" ] || [ -L "$CC_RETIRE_HELPER" ]; then
|
|
49
|
+
echo "build-bridge: $CC_RETIRE_HELPER is missing or not a regular file" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
cp "$CC_RETIRE_HELPER" "$REPO_DIR/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py"
|