@junghanacs/entwurf 0.35.1 → 0.36.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 +2 -2
- package/CHANGELOG.md +209 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +15 -0
- package/FAQ.md +3 -3
- package/README.md +30 -16
- package/VERIFY.md +9 -7
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +30 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
- package/package.json +19 -14
- package/pi/pi-durable/bootstrap.mjs +12 -8
- package/pi/pi-durable/carrier/runtime.js +52 -2
- package/pi/pi-durable/carrier/subagent.js +6 -1
- package/pi/pi-durable/carrier/tui.js +31 -4
- package/pi/pi-durable/carrier-relocation.mjs +2 -2
- package/pi/pi-durable/overlay/upstream-pin.json +79 -36
- package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
- package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
- package/run.sh +112 -38
- package/scripts/check-codex-app-server-launch.ts +55 -12
- package/scripts/check-codex-native-push.ts +231 -12
- package/scripts/check-gate-qualification.ts +3 -3
- package/scripts/codex-app-server-launch.sh +54 -29
- package/scripts/codex-socket-classify.ts +46 -0
- package/scripts/emit-pi-durable-carrier.ts +159 -168
- package/scripts/mutants/codex-app-server-launch.json +39 -12
- package/scripts/mutants/codex-native.json +15 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +165 -16
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/pi-durable-contact-host.mjs +1 -1
- package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* WHAT THIS IS. The long-running durable app (`@earendil-works/pi-coding-agent`
|
|
5
5
|
* `experimental/durable`, which upstream omits from its npm distribution — Entwurf's package
|
|
6
|
-
* supplies it as a
|
|
7
|
-
* models/auth, storage, prompt and recovery. This module is
|
|
6
|
+
* supplies it as a carrier emitted from its owned source, #130/#141, not as an operator
|
|
7
|
+
* prerequisite) owns its runtime, TUI, models/auth, storage, prompt and recovery. This module is
|
|
8
|
+
* the whole Entwurf side of its contact:
|
|
8
9
|
*
|
|
9
10
|
* createPiDurableContact({ bridgeEntry })
|
|
10
11
|
* .extension tools-only durable Extension, handed to the overlay's
|
|
@@ -49,14 +50,16 @@
|
|
|
49
50
|
* bridge whose list differs: the callable bridge schema is the truth, the copy only has to agree.
|
|
50
51
|
*
|
|
51
52
|
* RECEIVE (self-fetch, announce-only). When the bootstrap hands `attach` the overlay's
|
|
52
|
-
* `submitToRoot`, the contact arms a doorbell after the bridge is ready: mailbox, then signal,
|
|
53
|
+
* `submitToRoot` and `onFailed`, the contact arms a doorbell after the bridge is ready: mailbox, then signal,
|
|
53
54
|
* then the watch, then a check that the record and this process's sender marker still name the
|
|
54
55
|
* session, and only then the receiver marker. A fresh `.msg` is stamped `.msg.delivered` and
|
|
55
56
|
* announced to the ROOT conversation as input; the body stays in the mailbox until the model
|
|
56
57
|
* calls `entwurf_inbox_read`, which is the read receipt. The watch and the sender marker share
|
|
57
58
|
* this process's pid, so the #101 owner join applies (`pi-durable-host`). A watch error, a
|
|
58
|
-
* vanished signal, a failed ring, a rejected admission
|
|
59
|
-
*
|
|
59
|
+
* vanished signal, a failed ring, a rejected admission, the session failing (at once, from the
|
|
60
|
+
* overlay's `onFailed`, not at the next ring — and before `attach` returns when it had already
|
|
61
|
+
* failed, with no receiver marker left behind) or close retires the doorbell — watch closed, our own
|
|
62
|
+
* marker removed, the reason kept by name — and nothing retries it.
|
|
60
63
|
*
|
|
61
64
|
* WITHDRAW (#140). Each notice is a native `followUp`, placed one per run; a read drains EVERY
|
|
62
65
|
* unread body. So after a read the notices still queued for that mail would each cost a turn and an
|
|
@@ -295,9 +298,14 @@ export interface PiDurableRoots {
|
|
|
295
298
|
// ---------------------------------------------------------------------------
|
|
296
299
|
// Durable surface, typed NARROWLY and locally — read at pi-durable 1.0.2 @cd32f77 (#129,
|
|
297
300
|
// `harness/types.ts`; historical coordinates, not re-anchored here). The current supply is the
|
|
298
|
-
// carrier
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
+
// carrier emitted from Entwurf's owned source (pi/pi-durable/src, #141) against the 1.2.0 SDK set
|
|
302
|
+
// `pi/pi-durable/overlay/upstream-pin.json` names (origin `abe508e`). `[read 2026-10-13]` at
|
|
303
|
+
// v1.2.0 one shape typed below moved: `ToolExecutionResult.content` is now `output`, beside the new
|
|
304
|
+
// `structuredOutput` (`harness/types.ts`, its CHANGELOG's breaking changes). An omitted `output`
|
|
305
|
+
// gives the model the text a tool retained through `api.output()`; `content` is not read, so a
|
|
306
|
+
// result naming only `content` that retained no output reaches the model as nothing. The contact's
|
|
307
|
+
// tools retain none, so they answer in `output`.
|
|
308
|
+
// The submission draft, its `abort()` answers and the identity `define*` helpers did not move.
|
|
301
309
|
// `defineExtension`/`defineTool` are identity functions (`harness/define.ts:6-17`), so a plain
|
|
302
310
|
// object is the extension.
|
|
303
311
|
// ---------------------------------------------------------------------------
|
|
@@ -314,8 +322,9 @@ export interface DurableCallContext {
|
|
|
314
322
|
readonly abortSignal?: AbortSignal;
|
|
315
323
|
}
|
|
316
324
|
|
|
325
|
+
/** `ToolExecutionResult`, narrowed: `output` is what the model reads. */
|
|
317
326
|
export interface DurableToolResult {
|
|
318
|
-
|
|
327
|
+
output: { type: "text"; text: string }[];
|
|
319
328
|
}
|
|
320
329
|
|
|
321
330
|
export interface DurableTool {
|
|
@@ -562,14 +571,19 @@ export interface DurableAdmittedSubmission {
|
|
|
562
571
|
/** The overlay's `OpenDurableResult.submitToRoot`. */
|
|
563
572
|
export type DurableSubmitToRoot = (draft: DurableDoorbellDraft) => Promise<DurableAdmittedSubmission>;
|
|
564
573
|
|
|
574
|
+
/** The overlay's `OpenDurableResult.onFailed`: one synchronous call when the session fails and closes itself. */
|
|
575
|
+
export type DurableOnFailed = (listener: () => void) => () => void;
|
|
576
|
+
|
|
565
577
|
/** What `attach` needs to arm the doorbell. Without it the citizen only sends. */
|
|
566
578
|
export interface DurableRootAdmission {
|
|
567
579
|
submitToRoot: DurableSubmitToRoot;
|
|
580
|
+
onFailed: DurableOnFailed;
|
|
568
581
|
}
|
|
569
582
|
|
|
570
583
|
/** Why an armed doorbell was retired. Nothing re-arms it. */
|
|
571
584
|
export type PiDurableReceiverRetirement =
|
|
572
585
|
| "closed"
|
|
586
|
+
| "session-failed"
|
|
573
587
|
| "watch-error"
|
|
574
588
|
| "signal-vanished"
|
|
575
589
|
| "doorbell-failed"
|
|
@@ -630,7 +644,7 @@ export interface PiDurableAttachment {
|
|
|
630
644
|
/** The roots written — and the roots the bridge child was bound to. */
|
|
631
645
|
roots: PiDurableRoots;
|
|
632
646
|
bridgePid: number | null;
|
|
633
|
-
/** The receiver marker this attach published
|
|
647
|
+
/** The receiver marker this attach published; null when no root admission was handed in, or the session had already failed. */
|
|
634
648
|
receiverMarkerPath: string | null;
|
|
635
649
|
}
|
|
636
650
|
|
|
@@ -682,6 +696,7 @@ interface ArmedDoorbell {
|
|
|
682
696
|
markerPath: string;
|
|
683
697
|
watcher: fs.FSWatcher;
|
|
684
698
|
submitToRoot: DurableSubmitToRoot;
|
|
699
|
+
unsubscribeFailed: () => void;
|
|
685
700
|
}
|
|
686
701
|
|
|
687
702
|
/** One admitted notice: its native handle and the `.msg` names it announced. */
|
|
@@ -783,7 +798,7 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
783
798
|
const call = () => client.callTool(name, (args ?? {}) as Record<string, unknown>, context?.abortSignal);
|
|
784
799
|
const result = name === "entwurf_inbox_read" ? await readInbox(call, context) : await call();
|
|
785
800
|
if (result.isError) throw new Error(result.text || `${name} failed`);
|
|
786
|
-
return {
|
|
801
|
+
return { output: [{ type: "text", text: result.text }] };
|
|
787
802
|
},
|
|
788
803
|
});
|
|
789
804
|
|
|
@@ -836,6 +851,7 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
836
851
|
const armed = doorbell;
|
|
837
852
|
if (armed === null) return [];
|
|
838
853
|
doorbell = null;
|
|
854
|
+
armed.unsubscribeFailed();
|
|
839
855
|
const problems: string[] = [];
|
|
840
856
|
try {
|
|
841
857
|
armed.watcher.close();
|
|
@@ -985,15 +1001,16 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
985
1001
|
* Mailbox, signal, watch — then the check that the record and this process's sender marker
|
|
986
1002
|
* still name the session — and only then the receiver marker, the one fact a sender reads.
|
|
987
1003
|
* Synchronous, so nothing can close the contact between the check and the publish. Throws,
|
|
988
|
-
* after closing whatever it opened, when any step fails.
|
|
1004
|
+
* after closing whatever it opened, when any step fails. Returns null when the session had
|
|
1005
|
+
* already failed: the doorbell was retired as `session-failed` before this returned.
|
|
989
1006
|
*/
|
|
990
1007
|
const arm = (
|
|
991
1008
|
gardenId: string,
|
|
992
1009
|
nativeSessionId: string,
|
|
993
1010
|
ownerPid: number,
|
|
994
1011
|
roots: PiDurableRoots,
|
|
995
|
-
|
|
996
|
-
): string => {
|
|
1012
|
+
root: DurableRootAdmission,
|
|
1013
|
+
): string | null => {
|
|
997
1014
|
if (ownerPid !== process.pid) {
|
|
998
1015
|
throw new Error(
|
|
999
1016
|
`the doorbell's watch lives in this process (${process.pid}); a receiver marker naming owner ${ownerPid} would advertise a watch that pid does not hold`,
|
|
@@ -1004,6 +1021,7 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
1004
1021
|
fs.mkdirSync(mailbox, { recursive: true });
|
|
1005
1022
|
if (!fs.existsSync(signal)) fs.writeFileSync(signal, "", { mode: 0o600 });
|
|
1006
1023
|
const watcher = watch(signal, () => void ring("signal"));
|
|
1024
|
+
let unsubscribeFailed = (): void => {};
|
|
1007
1025
|
try {
|
|
1008
1026
|
watcher.on("error", (error) => retire("watch-error", describeError(error)));
|
|
1009
1027
|
const identity = readAddressableMetaIdentity(gardenId, roots.sessionsDir);
|
|
@@ -1020,6 +1038,21 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
1020
1038
|
: `the sender marker for owner ${ownerPid} names ${sender.gardenId}/${sender.nativeSessionId}, not ${gardenId}/${nativeSessionId}`,
|
|
1021
1039
|
);
|
|
1022
1040
|
}
|
|
1041
|
+
// A failed session admits nothing: retire at once, so no sender reads a marker for a host that cannot wake.
|
|
1042
|
+
// Subscribed before the marker is written, so a registration that throws leaves no marker. A session that
|
|
1043
|
+
// failed before this arm says so during the subscription, before the doorbell exists: that is noted, and
|
|
1044
|
+
// the retirement runs once the doorbell is published below. It is final; nothing re-arms.
|
|
1045
|
+
let published = false;
|
|
1046
|
+
let failedBeforePublished = false;
|
|
1047
|
+
const sessionFailed = (): void => {
|
|
1048
|
+
if (!published) {
|
|
1049
|
+
failedBeforePublished = true;
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const problems = retire("session-failed", "the durable session failed and closed itself");
|
|
1053
|
+
if (problems.length > 0) throw new Error(`the doorbell did not retire cleanly: ${problems.join("; ")}`);
|
|
1054
|
+
};
|
|
1055
|
+
unsubscribeFailed = root.onFailed(sessionFailed);
|
|
1023
1056
|
const markerPath = writeMetaReceiverMarker({
|
|
1024
1057
|
gardenId,
|
|
1025
1058
|
backend: PI_DURABLE_BACKEND,
|
|
@@ -1029,17 +1062,37 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
1029
1062
|
armProvenance: "session-start",
|
|
1030
1063
|
receiversDir: roots.receiversDir,
|
|
1031
1064
|
});
|
|
1032
|
-
doorbell = {
|
|
1065
|
+
doorbell = {
|
|
1066
|
+
gardenId,
|
|
1067
|
+
ownerPid,
|
|
1068
|
+
mailbox,
|
|
1069
|
+
signal,
|
|
1070
|
+
markerPath,
|
|
1071
|
+
watcher,
|
|
1072
|
+
submitToRoot: root.submitToRoot,
|
|
1073
|
+
unsubscribeFailed,
|
|
1074
|
+
};
|
|
1033
1075
|
inbox = mailbox;
|
|
1034
1076
|
report = { status: "armed", gardenId, markerPath, admissions };
|
|
1077
|
+
published = true;
|
|
1078
|
+
if (failedBeforePublished) {
|
|
1079
|
+
sessionFailed();
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1035
1082
|
return markerPath;
|
|
1036
1083
|
} catch (error) {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1084
|
+
const cleanup: unknown[] = [];
|
|
1085
|
+
for (const step of [unsubscribeFailed, () => watcher.close()]) {
|
|
1086
|
+
try {
|
|
1087
|
+
step();
|
|
1088
|
+
} catch (cleanupError) {
|
|
1089
|
+
cleanup.push(cleanupError);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (cleanup.length > 0) {
|
|
1040
1093
|
throw new AggregateError(
|
|
1041
|
-
[error,
|
|
1042
|
-
`${describeError(error)} (cleanup also failed: ${describeError(
|
|
1094
|
+
[error, ...cleanup],
|
|
1095
|
+
`${describeError(error)} (cleanup also failed: ${cleanup.map(describeError).join("; ")})`,
|
|
1043
1096
|
);
|
|
1044
1097
|
}
|
|
1045
1098
|
throw error;
|
|
@@ -1068,6 +1121,9 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
1068
1121
|
if (root !== undefined && typeof root.submitToRoot !== "function") {
|
|
1069
1122
|
throw new Error("the root admission handed to attach has no submitToRoot function");
|
|
1070
1123
|
}
|
|
1124
|
+
if (root !== undefined && typeof root.onFailed !== "function") {
|
|
1125
|
+
throw new Error("the root admission handed to attach has no onFailed function");
|
|
1126
|
+
}
|
|
1071
1127
|
const ownerPid = options.ownerPid ?? process.pid;
|
|
1072
1128
|
const sendersDir = path.resolve(options.sendersDir ?? defaultMetaSendersDir());
|
|
1073
1129
|
const upsert = upsertMetaSession({
|
|
@@ -1119,7 +1175,7 @@ export function createPiDurableContact(options: PiDurableContactOptions): PiDura
|
|
|
1119
1175
|
let receiverMarkerPath: string | null = null;
|
|
1120
1176
|
if (root !== undefined) {
|
|
1121
1177
|
try {
|
|
1122
|
-
receiverMarkerPath = arm(gardenId, id, ownerPid, roots, root
|
|
1178
|
+
receiverMarkerPath = arm(gardenId, id, ownerPid, roots, root);
|
|
1123
1179
|
} catch (error) {
|
|
1124
1180
|
return await failAfterCleanup(new Error(`the doorbell did not arm: ${describeError(error)}`), () =>
|
|
1125
1181
|
client.close(),
|
package/run.sh
CHANGED
|
@@ -181,7 +181,7 @@ Usage:
|
|
|
181
181
|
./run.sh check-pi-durable-contact # #129 L-identity gate, CHECKOUT-ONLY (an installed package refuses it in run_ts). Opens the REAL durable app from THIS checkout's verified carrier (pi/pi-durable/carrier/ under its resolver, against the checkout's pinned pi SDK set — refused by name first) with the pi-durable contact, births the citizen and runs its tools adapter-direct through a privately emitted branch bridge (ENTWURF_PI_DURABLE_BRIDGE_ENTRY; its registry must match this checkout). Matrix roots {default, explicit} x {fresh, --continue}: one record keyed by the durable session dir, sender marker on the spawned host pid, exactly one bridge child whose /proc PPID is that host, poisoned identity carriers kept from it, its four root carriers bound to the roots written, the receiver ARMED (raw marker owned by the host pid + start key as pi-durable-host, joined to the sender marker; removed on close), self = the record and replyable, peers from an outside bridge, same garden id on reopen; the bundle's compiled owner join must admit pi-durable-host. Reaps only the host/bridges it owns on any failure. Harness-bypassed: no model turn, no ToolTask. Missing inputs → SKIP (97), never a pass
|
|
182
182
|
./run.sh check-pi-durable-send # #129 L-send FIRST CELL, CHECKOUT-ONLY. Same input as check-pi-durable-contact. One native durable turn: the app's own controller input → GenerationTask → ToolTask → the contact's entwurf_v2 → the private bridge → a seeded self-fetch target (C2 recipe, owned by a separate idle child). S = a 127.0.0.1 scripted OpenAI-compatible endpoint configured through the app's documented models.json (dummy key, PI_OFFLINE); H = the native ToolTask path; V (a vendor turn) is NOT run. Oracles: the wire (tools offered, v2 schema = the bridge's tools/list, exactly two requests, the tool result + receipt in the second), the raw mailbox (.msg 1, signal, sender = the host record), the native view (one call, its settled result, root current). Not exactly-once, not recovery, not a recipient turn. Missing inputs → SKIP (97)
|
|
183
183
|
./run.sh check-demo-reproducibility # source-checkout-only: discover offline demo Node tests; no LIVE, model, render or publication
|
|
184
|
-
./run.sh check-pi-durable-beside # #129 mutant execution coordinate (VERIFY rule 3): run_vitest over the six pi-durable beside tests (adapter, carrier/SDK-set verifier, preflight, owner join, carrier relocation/resolver
|
|
184
|
+
./run.sh check-pi-durable-beside # #129 mutant execution coordinate (VERIFY rule 3): run_vitest over the six pi-durable beside tests (adapter, carrier/SDK-set verifier, preflight, owner join, carrier relocation/resolver, owned-source emission and typecheck fence, task footer, native-module file and directory ingress) — the pi-durable qualification lane's gate argv. Same files check-tests-beside-behavior already runs.
|
|
185
185
|
./run.sh check-pi-durable-receive # #129 L-receive FIRST CELL, CHECKOUT-ONLY. Same input (the bundle must carry the pi-durable-host owner join). The host driver gives the app NO input; a seeded claude-code sender (marker owned by a separate idle child, handed to a gate-owned sending bridge through ENTWURF_META_SENDER_MARKER) enqueues one message, the host's doorbell admits its notice to the ROOT conversation, and the scripted model drains it with entwurf_inbox_read. S withholds response #1 until the ENQUEUE receipt, the .delivered bytes, the signal, the host-owned receiver marker and the notice are captured. Oracles: receiver() admission, native view, raw archive (.delivered.read + lastReadAt), wire (exactly two requests, drain result = view), and after a green reap a forensic read-only SQLite copy (submission row by request id = reported id, root, input, done, placed entry = the notice). V NOT run. Missing inputs → SKIP (97)
|
|
186
186
|
./run.sh check-copilot-receive-arm # #82 RAIL 5 gate: the REAL receiver installer + the REAL extension.mjs forked with a stubbed SDK. Arms only after birth, marker owned by the WATCHER pid, self-fetch dispatch answer, doorbell carries the garden id and NOT the body, id-drift/foreign-parent refusals. Hermetic; no Copilot, no model turn
|
|
187
187
|
./run.sh check-copilot-launch # #82 RAIL 7 gate: the MANAGED launch `entwurf copilot`, driven through its public address against a FAKE VENDOR on a sandbox PATH. Receiver precondition refusals, EXTENSIONS token + operator token preservation, injected defaults before the `--` terminator, byte-identical argv, the 11 explicit permission/surface policy overrides that suppress `--yolo`, exec (not fork) pid identity, exit passthrough, recursion refusal. Hermetic; no Copilot, no model turn
|
|
@@ -273,7 +273,7 @@ Usage:
|
|
|
273
273
|
./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
|
|
274
274
|
./run.sh doctor-meta-bridge # THE RELEASE ORACLE (#51, Linux-certified repair axis). exit 0 = every required layer was MEASURED on this Linux host: toolchain + state + plugin/MCP + resolved-artifact launch-form classification (all 3 owner hooks + doorbell static contract) + synthetic owner join + store scan + hook errors + SessionStart evidence + REQUIRED live MCP↔marker join + writer-version parity. Missing live evidence is NOT CERTIFIED (open a Claude session and re-run), never a pass; Darwin is NOT CERTIFIED and stays nonzero, on ONE narrowed reason — start-key and ppid are portable there (`ps` fallback, same string the TS core mints), so what holds the lane closed is per-process environment DISCOVERY alone, pending physical-host evidence. Detection power is held by check-meta-doctor-oracle
|
|
275
275
|
./run.sh copilot [args...] # #82 RAIL 7: the MANAGED Copilot launch. exec()s the vendor CLI in THIS terminal (cwd/pid/exit preserved) with COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS set for that one invocation — entwurf owns no part of your shell and writes nothing to it, but it owns the process it is about to become. Without that flag Copilot skips the extension scan SILENTLY. Refuses to launch unless the receiver unit is actually installed. Injects `--model auto` when no --model was given and `--yolo` when no explicit permission/surface policy flag was given, both BEFORE any `--`; every argument you pass is forwarded byte-identical. RUNNING THIS IS YOUR CONSENT to that profile — use plain `copilot` for stock vendor behaviour. Not tmux, not fresh-call, mints no citizen (birth is still the first prompt)
|
|
276
|
-
./run.sh pi-durable [args...] # #129/#130: the MANAGED durable launch. Verifies this package's pi-durable CARRIER (pi/pi-durable/carrier/,
|
|
276
|
+
./run.sh pi-durable [args...] # #129/#130: the MANAGED durable launch. Verifies this package's pi-durable CARRIER (pi/pi-durable/carrier/, emitted deterministically from Entwurf's owned source pi/pi-durable/src) and the pi SDK set it resolves — every declared member/direct edge at exactly the pinned version — refusing by name (package-incomplete / carrier-drift / sdk-absent / sdk-mismatch, exit 3), then exec()s node under the carrier resolver with the packaged bootstrap (pi/pi-durable/bootstrap.mjs) in this terminal. A new session needs --provider <p> --model <exact catalog id> --width task-wide [--entwurf-bootstrap <{v:2,task}>]; --continue takes none of them. Nothing outside the package is located, installed or repaired
|
|
277
277
|
./run.sh install-copilot-bridge # #82: GLOBAL install of the Copilot BIRTH plugin (own marketplace root; node+entry baked into the no-argv exec string). MCP wiring and the RECEIVER extension are separate install surfaces (install-copilot-mcp, install-copilot-receive). Also retires the stale Claude unit (--keep-stale-claude-unit opts out)
|
|
278
278
|
./run.sh uninstall-copilot-bridge # #86 C3a: honest inverse of install-copilot-bridge from the package-owned install-state (exact qualified id + exact marketplace name/path + recorded assembly only; state deleted LAST; a failing vendor list is UNKNOWN and refuses; never --force, never the stale Claude unit)
|
|
279
279
|
./run.sh doctor-copilot-bridge # #82/#86: fail-loud surface for that unit — runtime axis (red = a hook that RAN and failed, or a broken/unbaked artifact; "installed with zero records" is NOT red and is reported as NOT-YET: a Copilot session is born on its FIRST PROMPT, not when the window opens, measured) + ownership axis (install-state shape/binding, marketplace path drift, UNKNOWN vendor lists; legacy no-state install = named non-green, repair = install-copilot-bridge adoption). Either axis red = red
|
|
@@ -1606,7 +1606,7 @@ check_pi_durable_beside() {
|
|
|
1606
1606
|
# from re-running every beside test. The six files are one contact: the adapter and its packaged
|
|
1607
1607
|
# bootstrap, the installed carrier/SDK-set verifier (which also reads the three checkout gates as
|
|
1608
1608
|
# SOURCE), the fresh preflight, the owner join that admits the durable host, the carrier's
|
|
1609
|
-
# relocation grammar, resolver
|
|
1609
|
+
# relocation grammar, resolver, owned-source emitter (#130, #141) and source fence with the carrier's task footer (#134),
|
|
1610
1610
|
# and the bootstrap's explicit --native-module ingress (#130 P2) and its --native-module-dir (#134).
|
|
1611
1611
|
run_vitest pi-extensions/meta-bridge-pi-durable.test.ts pi-extensions/lib/pi-durable-runtime.test.ts pi-extensions/lib/pi-durable-fresh-preflight.test.ts pi-extensions/lib/entwurf-deliverability.owner-join.test.ts pi-extensions/lib/pi-durable-carrier.test.ts pi-extensions/lib/pi-durable-native-module.test.ts
|
|
1612
1612
|
}
|
|
@@ -2435,8 +2435,8 @@ const nextFloorRange = `>=${piAi} <${piMaj}.${piMin + 1}`;
|
|
|
2435
2435
|
const floorGaps = [];
|
|
2436
2436
|
const PI_CONSTELLATION = [
|
|
2437
2437
|
'@earendil-works/pi-ai', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui',
|
|
2438
|
-
'@earendil-works/pi-agent-core', '@earendil-works/pi-
|
|
2439
|
-
|
|
2438
|
+
'@earendil-works/pi-agent-core', '@earendil-works/pi-telemetry', '@earendil-works/chord',
|
|
2439
|
+
// `[측정 2026-10-13]` 1.2.0: pi-client and pi-protocol left — upstream removed both, E404 at 1.2.0.
|
|
2440
2440
|
// `[측정 2026-09-30]` 0.99.1: pi-coding-agent's runtime deps gained the built-in extensions' packages.
|
|
2441
2441
|
'@earendil-works/pi-codemode', '@earendil-works/pi-mcp',
|
|
2442
2442
|
// #130: the durable library the pi-durable carrier imports — a production dependency since 0.32.
|
|
@@ -2577,6 +2577,65 @@ for (const [file, re, min, label] of SITES) {
|
|
|
2577
2577
|
bound += hits.length;
|
|
2578
2578
|
}
|
|
2579
2579
|
|
|
2580
|
+
// #141: the pi-durable carrier's emitter identity, the one place CI runs an EXACT Node. The check job
|
|
2581
|
+
// runs the Node the pin records as the carrier's emitter (CI-BOUND), and its check:full floor step runs
|
|
2582
|
+
// with ENTWURF_REQUIRE_EMITTER_NODE=1 (CI-REQUIRED), so that floor proves the emission under that
|
|
2583
|
+
// identity; every other job floats the major so the newest release of the lane stays covered. Both are
|
|
2584
|
+
// read from ACTIVE workflow lines only — the step's own `with:` / `env:` mapping, comments stripped —
|
|
2585
|
+
// so commented-out text or the same key on another step can never stand in for them.
|
|
2586
|
+
const EMITTER_BOUND_QK = '[QK:PI-DURABLE-EMITTER-NODE-CI-BOUND]';
|
|
2587
|
+
const EMITTER_REQUIRED_QK = '[QK:PI-DURABLE-EMITTER-NODE-CI-REQUIRED]';
|
|
2588
|
+
const emitterNode = JSON.parse(readFileSync('pi/pi-durable/overlay/upstream-pin.json', 'utf8')).carrier?.emitter?.node;
|
|
2589
|
+
assert.match(String(emitterNode), /^v\d+\.\d+\.\d+$/,
|
|
2590
|
+
`${EMITTER_BOUND_QK} pi/pi-durable/overlay/upstream-pin.json carrier.emitter.node must be an exact vX.Y.Z (got ${emitterNode})`);
|
|
2591
|
+
// jobs → steps → active lines. A full-line comment is dropped and a trailing ` # …` is cut; job heads are
|
|
2592
|
+
// the two-space keys under `jobs:`, a step starts at its six-space `- ` item.
|
|
2593
|
+
const ciJobs = new Map();
|
|
2594
|
+
{
|
|
2595
|
+
let inJobs = false;
|
|
2596
|
+
let steps;
|
|
2597
|
+
for (const raw of readFileSync('.github/workflows/ci.yml', 'utf8').split('\n')) {
|
|
2598
|
+
const line = raw.replace(/\s+#.*$/, '');
|
|
2599
|
+
if (/^\s*#/.test(raw) || line.trim() === '') continue;
|
|
2600
|
+
if (/^\S/.test(line)) { inJobs = line === 'jobs:'; steps = undefined; continue; }
|
|
2601
|
+
if (!inJobs) continue;
|
|
2602
|
+
const head = /^ ([a-z][a-z0-9-]*):$/.exec(line);
|
|
2603
|
+
if (head) { steps = []; ciJobs.set(head[1], steps); continue; }
|
|
2604
|
+
if (steps === undefined) continue;
|
|
2605
|
+
if (/^ - /.test(line)) steps.push([` ${line.slice(8)}`]);
|
|
2606
|
+
else if (steps.length > 0 && /^ /.test(line)) steps.at(-1).push(line);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
// A step's own value for `key` (`with:` / `env:` child at ten spaces), or a top-level step key at eight.
|
|
2610
|
+
const stepKey = (step, key) => step.find((l) => l.startsWith(` ${key}: `))?.slice(10 + key.length);
|
|
2611
|
+
const stepChild = (step, block, key) => {
|
|
2612
|
+
const at = step.indexOf(` ${block}:`);
|
|
2613
|
+
if (at === -1) return undefined;
|
|
2614
|
+
const children = [];
|
|
2615
|
+
for (const l of step.slice(at + 1)) { if (!l.startsWith(' ')) break; children.push(l); }
|
|
2616
|
+
return children.find((l) => l.startsWith(` ${key}: `))?.slice(12 + key.length);
|
|
2617
|
+
};
|
|
2618
|
+
const setupNodes = (job) => (ciJobs.get(job) ?? []).filter((s) => /^actions\/setup-node@/.test(stepKey(s, 'uses') ?? ''));
|
|
2619
|
+
const checkSetup = setupNodes('check');
|
|
2620
|
+
assert.equal(checkSetup.length, 1,
|
|
2621
|
+
`${EMITTER_BOUND_QK} .github/workflows/ci.yml: the check job must have exactly one actions/setup-node step (found ${checkSetup.length})`);
|
|
2622
|
+
const checkNode = stepChild(checkSetup[0], 'with', 'node-version');
|
|
2623
|
+
assert.equal(`v${checkNode}`, emitterNode,
|
|
2624
|
+
`${EMITTER_BOUND_QK} .github/workflows/ci.yml: the check job's setup-node runs Node ${checkNode}, but the carrier's pinned emitter is ${emitterNode} — its floor would prove the emission under another identity`);
|
|
2625
|
+
const floors = (ciJobs.get('check') ?? []).filter((s) => stepKey(s, 'run') === 'pnpm run check:full');
|
|
2626
|
+
assert.equal(floors.length, 1,
|
|
2627
|
+
`${EMITTER_REQUIRED_QK} .github/workflows/ci.yml: the check job must run \`pnpm run check:full\` in exactly one step (found ${floors.length})`);
|
|
2628
|
+
assert.equal(stepChild(floors[0], 'env', 'ENTWURF_REQUIRE_EMITTER_NODE'), '"1"',
|
|
2629
|
+
`${EMITTER_REQUIRED_QK} .github/workflows/ci.yml: the check job's check:full step does not set ENTWURF_REQUIRE_EMITTER_NODE: "1" in its own env, so an emitter-identity mismatch on that floor would be a skip, not red`);
|
|
2630
|
+
for (const [job] of ciJobs) {
|
|
2631
|
+
if (job === 'check') continue;
|
|
2632
|
+
for (const s of setupNodes(job)) {
|
|
2633
|
+
const value = stepChild(s, 'with', 'node-version');
|
|
2634
|
+
assert.match(String(value), /^\d+$/,
|
|
2635
|
+
`${EMITTER_BOUND_QK} .github/workflows/ci.yml: job ${job} pins node-version ${value}; only the check job is exact, the others float the major`);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2580
2639
|
// Sweep the tracked first-party contract TEXT surfaces, NOT just the files
|
|
2581
2640
|
// already registered above. The first cut of this gate swept only SITES' own
|
|
2582
2641
|
// files and therefore could not see a declaration in an UNREGISTERED file —
|
|
@@ -2627,7 +2686,7 @@ assert.ok(swept >= 6,
|
|
|
2627
2686
|
// `bound` is the real declaration count; SITES.length is the number of RULES
|
|
2628
2687
|
// (the CI rule alone matches two node-version lines). Naming the rule count a
|
|
2629
2688
|
// declaration count would be this gate telling a small lie about its own reach.
|
|
2630
|
-
console.log(`[check-node-floor-coherence] ok — Node >=${MAJOR} is coherent across engines.node, ${bound} bound declarations from ${SITES.length} site rules, and ${swept} declarations swept over ${tracked.length} tracked first-party contract text surfaces (single supported axis, no legacy lane). Non-text carriers (e.g. a Dockerfile \`FROM node:*\`) are OUTSIDE this pattern and must be bound by their own gate.`);
|
|
2689
|
+
console.log(`[check-node-floor-coherence] ok — Node >=${MAJOR} is coherent across engines.node, ${bound} bound declarations from ${SITES.length} site rules, and ${swept} declarations swept over ${tracked.length} tracked first-party contract text surfaces (single supported axis, no legacy lane); the pi-durable emitter ${emitterNode} is the CI check job's exact Node. Non-text carriers (e.g. a Dockerfile \`FROM node:*\`) are OUTSIDE this pattern and must be bound by their own gate.`);
|
|
2631
2690
|
EOF
|
|
2632
2691
|
)
|
|
2633
2692
|
}
|
|
@@ -2916,7 +2975,7 @@ check_pi_import_surface() {
|
|
|
2916
2975
|
# resolve under the extension loader.
|
|
2917
2976
|
#
|
|
2918
2977
|
# #130 CARRIER FENCE (replaces the #129 bootstrap exception, retired with the source runtime):
|
|
2919
|
-
# the pi-durable carrier (pi/pi-durable/carrier/*.js,
|
|
2978
|
+
# the pi-durable carrier (pi/pi-durable/carrier/*.js, emitted from Entwurf's owned source by
|
|
2920
2979
|
# scripts/emit-pi-durable-carrier.ts) is the ONE place pi-coding-agent internals are reached,
|
|
2921
2980
|
# and only through the reserved `entwurf-pi-dist:` scheme its resolver answers. The fence is
|
|
2922
2981
|
# declared and asserted here, not inferred from the root-only scan staying quiet:
|
|
@@ -3758,7 +3817,6 @@ check_pack() {
|
|
|
3758
3817
|
"mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js"
|
|
3759
3818
|
"pi/pi-durable/bootstrap.mjs"
|
|
3760
3819
|
"pi/pi-durable/overlay/upstream-pin.json"
|
|
3761
|
-
"pi/pi-durable/overlay/runtime-contacts.patch"
|
|
3762
3820
|
"pi/pi-durable/carrier-resolver.mjs"
|
|
3763
3821
|
"pi/pi-durable/carrier-relocation.mjs"
|
|
3764
3822
|
"pi/pi-durable/carrier/runtime.js"
|
|
@@ -3845,6 +3903,11 @@ check_pack() {
|
|
|
3845
3903
|
# Maintainer take/renderer/helper TS is source-only, including non-test implementations.
|
|
3846
3904
|
'^demo/.*\.ts$'
|
|
3847
3905
|
)
|
|
3906
|
+
# #141: the owned pi-durable source, its typecheck fence and the retired overlay patch are maintainer
|
|
3907
|
+
# input. An installed consumer runs the emitted carrier JS (and its LICENSE) only — Rule 11: compiled
|
|
3908
|
+
# JS for installed entrypoints. The files array lists pi/pi-durable per FILE, so nothing admits these
|
|
3909
|
+
# today; this tripwire is what fails loud the day an allowlist entry would.
|
|
3910
|
+
local durable_source_pattern='^pi/pi-durable/(src/|tsconfig\.json$|overlay/runtime-contacts\.patch$)'
|
|
3848
3911
|
|
|
3849
3912
|
local pass=1 f pat hit
|
|
3850
3913
|
|
|
@@ -3864,6 +3927,13 @@ check_pack() {
|
|
|
3864
3927
|
fi
|
|
3865
3928
|
done
|
|
3866
3929
|
|
|
3930
|
+
hit=$(grep -E "$durable_source_pattern" <<<"$file_list" || true)
|
|
3931
|
+
if [ -n "$hit" ]; then
|
|
3932
|
+
fail "[check-pack] [QK:PI-DURABLE-SOURCE-NOT-SHIPPED] the owned pi-durable source rides the tarball — an installed consumer runs the carrier JS only:"
|
|
3933
|
+
echo "$hit" | sed 's/^/ /' >&2
|
|
3934
|
+
pass=0
|
|
3935
|
+
fi
|
|
3936
|
+
|
|
3867
3937
|
local total
|
|
3868
3938
|
total=$(printf '%s\n' "$file_list" | wc -l | tr -d ' ')
|
|
3869
3939
|
echo "[check-pack] $total files in tarball"
|
|
@@ -3898,7 +3968,7 @@ check_pack() {
|
|
|
3898
3968
|
# which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
|
|
3899
3969
|
# below so a future suffix change cannot pass vacuously.)
|
|
3900
3970
|
pack_install_leaked_pi() {
|
|
3901
|
-
grep '^@earendil-works+' | grep -Ev '@1\.
|
|
3971
|
+
grep '^@earendil-works+' | grep -Ev '@1\.2\.0(_|$)' || true
|
|
3902
3972
|
}
|
|
3903
3973
|
|
|
3904
3974
|
# Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
|
|
@@ -3918,15 +3988,15 @@ check_pack_pin_matcher() {
|
|
|
3918
3988
|
# None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`0.86.0-beta.1`,
|
|
3919
3989
|
# the prerelease shape that an unanchored match would bless) and an off-pin version.
|
|
3920
3990
|
matcher_probe=$(printf '%s\n' \
|
|
3921
|
-
'@earendil-works+pi-ai@1.
|
|
3922
|
-
'@earendil-works+pi-ai@1.
|
|
3923
|
-
'@earendil-works+pi-ai@1.
|
|
3924
|
-
'@earendil-works+pi-ai@1.
|
|
3925
|
-
'@earendil-works+pi-ai@1.
|
|
3991
|
+
'@earendil-works+pi-ai@1.2.0' \
|
|
3992
|
+
'@earendil-works+pi-ai@1.2.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
|
|
3993
|
+
'@earendil-works+pi-ai@1.2.0_ws@8.21.3' \
|
|
3994
|
+
'@earendil-works+pi-ai@1.2.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
|
|
3995
|
+
'@earendil-works+pi-ai@1.2.0-beta.1' \
|
|
3926
3996
|
'@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
|
|
3927
|
-
if [ "$matcher_probe" != '@earendil-works+pi-ai@1.
|
|
3997
|
+
if [ "$matcher_probe" != '@earendil-works+pi-ai@1.2.0-beta.1
|
|
3928
3998
|
@earendil-works+pi-agent-core@0.87.1' ]; then
|
|
3929
|
-
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.
|
|
3999
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.2.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.2.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
|
|
3930
4000
|
return 1
|
|
3931
4001
|
fi
|
|
3932
4002
|
|
|
@@ -3934,11 +4004,11 @@ check_pack_pin_matcher() {
|
|
|
3934
4004
|
# (0.85.0 onward, still true at 0.99.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
|
|
3935
4005
|
# back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
|
|
3936
4006
|
matcher_probe=$(printf '%s\n' \
|
|
3937
|
-
'@earendil-works+chord@1.
|
|
4007
|
+
'@earendil-works+chord@1.2.0' \
|
|
3938
4008
|
'@earendil-works+chord@0.87.1' \
|
|
3939
|
-
'@earendil-works+pi-ai@1.
|
|
4009
|
+
'@earendil-works+pi-ai@1.2.0' | pack_install_leaked_pi)
|
|
3940
4010
|
if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
|
|
3941
|
-
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
|
|
4011
|
+
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>}"
|
|
3942
4012
|
return 1
|
|
3943
4013
|
fi
|
|
3944
4014
|
|
|
@@ -4113,8 +4183,8 @@ ORACLE
|
|
|
4113
4183
|
# copied from this checkout into the sandbox — it never ships (test/ is outside the package files).
|
|
4114
4184
|
# Its initialization appends `init` to the receipt this cell names; the same public launch must then
|
|
4115
4185
|
# reach the same carrier model refusal, so the installed bootstrap loaded the module before the
|
|
4116
|
-
# runtime and the native registry installed its extension (carrier/runtime.js
|
|
4117
|
-
# refusal (runtime.js
|
|
4186
|
+
# runtime and the native registry installed its extension (carrier/runtime.js 159) before that
|
|
4187
|
+
# refusal (runtime.js 175). A module claiming an extension name the app installs itself is refused by
|
|
4118
4188
|
# name before anything opens: no durable session directory, no citizen, in a fresh sandbox home.
|
|
4119
4189
|
# Native SDK file opens here are ordinary (the module runs in the native process); none is judged.
|
|
4120
4190
|
local mod_dir="$box/pd-module" mod_home="$box/pd-module-home" mod_env=()
|
|
@@ -4169,7 +4239,7 @@ ORACLE
|
|
|
4169
4239
|
# aligned host binary (the pinned version, installed above for L1) must be inside the range.
|
|
4170
4240
|
# The previous host version is a variable, not a quoted `@pkg@x.y.z` spec: check-dep-versions reads
|
|
4171
4241
|
# those specs as this repo's install pins, and this one is deliberately off the floor.
|
|
4172
|
-
local host_home host_proj host_old="$box/pd-host-old" host_env=() host_old_ver="1.0
|
|
4242
|
+
local host_home host_proj host_old="$box/pd-host-old" host_env=() host_old_ver="1.1.0"
|
|
4173
4243
|
mkdir -p "$host_old"
|
|
4174
4244
|
for spec in "${host_specs[@]}"; do host_old_specs+=("${spec%@*}@$host_old_ver"); done
|
|
4175
4245
|
out=$(cd "$host_old" && npm install "${host_old_specs[@]}" --no-audit --no-fund 2>&1) || {
|
|
@@ -4187,7 +4257,7 @@ ORACLE
|
|
|
4187
4257
|
fail "[check-pack-install] host $cell: the native pi-durable row must stay PASS whatever the PATH host is:"; echo "$out" | tail -20 | sed 's/^/ /' >&2; return 1
|
|
4188
4258
|
fi
|
|
4189
4259
|
if [ "$cell" = old ]; then
|
|
4190
|
-
if [ "$rc" -eq 0 ] || ! grep -q "pi: FAIL — detected pi $host_old_ver is outside the supported range >=$pin_ver <1.
|
|
4260
|
+
if [ "$rc" -eq 0 ] || ! grep -q "pi: FAIL — detected pi $host_old_ver is outside the supported range >=$pin_ver <1.3" <<<"$out" || [ -e "$host_proj/.pi" ] || [ -e "$host_home/.pi/agent/settings.json" ]; then
|
|
4191
4261
|
fail "[QK:PACK-INSTALL-HOST-RANGE-SEPARATE] [check-pack-install] a real pi $host_old_ver host beside the $pin_ver native set must be a named pi FAIL with zero Pi wiring and a non-green setup (rc=$rc):"
|
|
4192
4262
|
echo "$out" | tail -20 | sed 's/^/ /' >&2
|
|
4193
4263
|
return 1
|
|
@@ -4395,7 +4465,6 @@ _check_pack_install_impl() {
|
|
|
4395
4465
|
"mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js"
|
|
4396
4466
|
"pi/pi-durable/bootstrap.mjs"
|
|
4397
4467
|
"pi/pi-durable/overlay/upstream-pin.json"
|
|
4398
|
-
"pi/pi-durable/overlay/runtime-contacts.patch"
|
|
4399
4468
|
"pi/pi-durable/carrier-resolver.mjs"
|
|
4400
4469
|
"pi/pi-durable/carrier-relocation.mjs"
|
|
4401
4470
|
"pi/pi-durable/carrier/runtime.js"
|
|
@@ -4558,21 +4627,26 @@ _check_pack_install_impl() {
|
|
|
4558
4627
|
# pi-ai, pi-codemode, pi-mcp, pi-tui}, pi-ai {pi-telemetry}, pi-agent-core {pi-ai}, pi-durable
|
|
4559
4628
|
# {chord, pi-ai}, and `npm view` gives pi-client {chord, pi-protocol}, pi-protocol {chord}. All
|
|
4560
4629
|
# eleven are published at 1.1.0.
|
|
4561
|
-
|
|
4630
|
+
# `[측정 2026-10-13]` at 1.2.0 the constellation SHRINKS to the nine-member SDK set: upstream
|
|
4631
|
+
# removed pi-client and pi-protocol (7472cda0d), and `npm view <pkg>@1.2.0` answers E404 for both,
|
|
4632
|
+
# so their pins cannot move with the floor and leave instead of staying redundant. The 1.2.0
|
|
4633
|
+
# registry manifests give pi-coding-agent {pi-agent-core, pi-ai, pi-codemode, pi-mcp, pi-tui}
|
|
4634
|
+
# (chord dropped), pi-ai {pi-telemetry}, pi-agent-core {pi-ai}, pi-durable {chord, pi-ai,
|
|
4635
|
+
# pi-codemode}; all nine are published at 1.2.0
|
|
4636
|
+
# (.tmp-verify/141-kickoff/s1a/map-receipts/01-tarballs.txt).
|
|
4637
|
+
echo "[check-pack-install] pnpm add into $tmp (with the aligned 1.2.x SDK + chord + codemode + mcp + durable + typebox)"
|
|
4562
4638
|
local install_log
|
|
4563
4639
|
install_log=$(cd "$tmp" && pnpm add \
|
|
4564
4640
|
"$tgz_path" \
|
|
4565
|
-
"@earendil-works/pi-ai@1.
|
|
4566
|
-
"@earendil-works/pi-coding-agent@1.
|
|
4567
|
-
"@earendil-works/pi-tui@1.
|
|
4568
|
-
"@earendil-works/pi-agent-core@1.
|
|
4569
|
-
"@earendil-works/pi-
|
|
4570
|
-
"@earendil-works/
|
|
4571
|
-
"@earendil-works/pi-
|
|
4572
|
-
"@earendil-works/
|
|
4573
|
-
"@earendil-works/pi-
|
|
4574
|
-
"@earendil-works/pi-mcp@1.1.0" \
|
|
4575
|
-
"@earendil-works/pi-durable@1.1.0" \
|
|
4641
|
+
"@earendil-works/pi-ai@1.2.0" \
|
|
4642
|
+
"@earendil-works/pi-coding-agent@1.2.0" \
|
|
4643
|
+
"@earendil-works/pi-tui@1.2.0" \
|
|
4644
|
+
"@earendil-works/pi-agent-core@1.2.0" \
|
|
4645
|
+
"@earendil-works/pi-telemetry@1.2.0" \
|
|
4646
|
+
"@earendil-works/chord@1.2.0" \
|
|
4647
|
+
"@earendil-works/pi-codemode@1.2.0" \
|
|
4648
|
+
"@earendil-works/pi-mcp@1.2.0" \
|
|
4649
|
+
"@earendil-works/pi-durable@1.2.0" \
|
|
4576
4650
|
"typebox@latest" \
|
|
4577
4651
|
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
4578
4652
|
fail "[check-pack-install] pnpm add failed:"
|
|
@@ -4588,11 +4662,11 @@ _check_pack_install_impl() {
|
|
|
4588
4662
|
local leaked_pi
|
|
4589
4663
|
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
|
|
4590
4664
|
if [ -n "$leaked_pi" ]; then
|
|
4591
|
-
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.
|
|
4665
|
+
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.2.0):"
|
|
4592
4666
|
printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
|
|
4593
4667
|
return 1
|
|
4594
4668
|
fi
|
|
4595
|
-
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.
|
|
4669
|
+
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.2.0 (chord and pi-durable included)"
|
|
4596
4670
|
|
|
4597
4671
|
# Resolve the installed package.json and confirm pi.extensions
|
|
4598
4672
|
# arrived intact. If pi.extensions is empty or missing, the
|
|
@@ -322,25 +322,35 @@ exit "\${FAKE_CODEX_EXIT:-0}"
|
|
|
322
322
|
);
|
|
323
323
|
}
|
|
324
324
|
{
|
|
325
|
+
// The classifier's INDETERMINATE domain, as one claim: two unidentifiable paths, both
|
|
326
|
+
// reaching the launcher through the one `indeterminate)` arm. A path that exists and is
|
|
327
|
+
// not a socket must read indeterminate, not stale, so the launcher refuses instead of
|
|
328
|
+
// clobbering it. A symlink that is NOT Codex's exact rendezvous alias must be refused with
|
|
329
|
+
// the product classifier's own reason, which is what pins the WIRING: a launcher that grew
|
|
330
|
+
// its own symlink rule again would refuse there too, but not in those words. Both runs are
|
|
331
|
+
// made before the one assertion, so a regression on that shared arm fails HERE, at this
|
|
332
|
+
// claim, and not at an earlier unnamed cell. The exact alias's accept path is certified on
|
|
333
|
+
// `checkCodexSocketFile` (check-codex-native-push), not here: this gate has no seam to put
|
|
334
|
+
// Codex's socket directory in a sandbox.
|
|
325
335
|
const fileHome = path.join(root, "file-home");
|
|
326
336
|
const fileSock = resolveCodexDefaultSocketPath({ CODEX_HOME: fileHome });
|
|
327
337
|
mkdirSync(path.dirname(fileSock), { recursive: true });
|
|
328
338
|
writeFileSync(fileSock, "");
|
|
329
|
-
const
|
|
330
|
-
ok(
|
|
331
|
-
"a path that exists and is not a socket is indeterminate, not stale — the launcher refuses instead of clobbering something it cannot identify",
|
|
332
|
-
r.status !== 0 && r.out.includes("codex-app-server-socket-indeterminate") && r.args.length === 0,
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
{
|
|
339
|
+
const regular = launch([], { CODEX_HOME: fileHome });
|
|
336
340
|
const linkHome = path.join(root, "link-home");
|
|
337
341
|
const linkSock = resolveCodexDefaultSocketPath({ CODEX_HOME: linkHome });
|
|
338
342
|
mkdirSync(path.dirname(linkSock), { recursive: true });
|
|
339
343
|
symlinkSync(path.join(root, "nowhere.sock"), linkSock);
|
|
340
|
-
const
|
|
344
|
+
const link = launch([], { CODEX_HOME: linkHome });
|
|
341
345
|
ok(
|
|
342
|
-
"[QK:CODEX-APP-SERVER-REFUSES-INDETERMINATE-SOCKET] a
|
|
343
|
-
|
|
346
|
+
"[QK:CODEX-APP-SERVER-REFUSES-INDETERMINATE-SOCKET] an unidentifiable path is refused, never launched over: a path that exists and is not a socket, and a symlink that is not Codex's rendezvous alias (refused with the classifier's own reason) — the same classification the delivery rail's socket check uses",
|
|
347
|
+
regular.status !== 0 &&
|
|
348
|
+
regular.out.includes("codex-app-server-socket-indeterminate") &&
|
|
349
|
+
regular.args.length === 0 &&
|
|
350
|
+
link.status !== 0 &&
|
|
351
|
+
link.out.includes("codex-app-server-socket-indeterminate") &&
|
|
352
|
+
link.out.includes("is not Codex's rendezvous alias") &&
|
|
353
|
+
link.args.length === 0,
|
|
344
354
|
);
|
|
345
355
|
}
|
|
346
356
|
{
|
|
@@ -358,17 +368,50 @@ exit "\${FAKE_CODEX_EXIT:-0}"
|
|
|
358
368
|
// Hard Rule 15: an unrecognised reading is the one case where proceeding is unsafe,
|
|
359
369
|
// because every branch above is a decision about whether this launch would clobber a
|
|
360
370
|
// running server. The stimulus is a sandbox `python3` that exits 0 while printing
|
|
361
|
-
// something nobody wrote — the exact shape a silent fall-through needs.
|
|
371
|
+
// something nobody wrote — the exact shape a silent fall-through needs. The launcher runs
|
|
372
|
+
// that connect probe only on an endpoint the classifier verified as a socket, so the cell
|
|
373
|
+
// puts a (dead) socket file there; without one the probe would never be asked.
|
|
374
|
+
const oddHome = path.join(root, "odd-home");
|
|
375
|
+
const oddSock = resolveCodexDefaultSocketPath({ CODEX_HOME: oddHome });
|
|
376
|
+
mkdirSync(path.dirname(oddSock), { recursive: true });
|
|
377
|
+
spawnSync("python3", ["-c", "import socket,sys;s=socket.socket(socket.AF_UNIX);s.bind(sys.argv[1])", oddSock]);
|
|
362
378
|
const oddBin = path.join(root, "odd-probe-bin");
|
|
363
379
|
mkdirSync(oddBin, { recursive: true });
|
|
364
380
|
const oddPython = path.join(oddBin, "python3");
|
|
365
381
|
writeFileSync(oddPython, "#!/usr/bin/env bash\ncat >/dev/null\necho 'unexpected-probe-status'\nexit 0\n");
|
|
366
382
|
chmodSync(oddPython, 0o755);
|
|
367
|
-
const r = launch([], { PATH: `${oddBin}:${bin}:${process.env.PATH ?? ""}` });
|
|
383
|
+
const r = launch([], { CODEX_HOME: oddHome, PATH: `${oddBin}:${bin}:${process.env.PATH ?? ""}` });
|
|
384
|
+
// The second stimulus is the CLASSIFIER answering a word it never gives. `stale` is the
|
|
385
|
+
// discriminating one: it is a valid verdict of the connect probe, so a launcher that passed
|
|
386
|
+
// unknown classifier words through would read it as a dead socket and LAUNCH. A sandbox
|
|
387
|
+
// `node` answers for the classifier's basename only and hands every other invocation to
|
|
388
|
+
// the real Node — a test fixture on this gate's PATH, not a seam the product reads.
|
|
389
|
+
const oddClassifierHome = path.join(root, "odd-classifier-home");
|
|
390
|
+
const oddClassifierBin = path.join(root, "odd-classifier-bin");
|
|
391
|
+
mkdirSync(oddClassifierBin, { recursive: true });
|
|
392
|
+
const oddNode = path.join(oddClassifierBin, "node");
|
|
393
|
+
writeFileSync(
|
|
394
|
+
oddNode,
|
|
395
|
+
"#!/usr/bin/env bash\n" +
|
|
396
|
+
'for a in "$@"; do case "$a" in */codex-socket-classify.js) printf \'stale\'; exit 0 ;; esac; done\n' +
|
|
397
|
+
`exec ${JSON.stringify(process.execPath)} "$@"\n`,
|
|
398
|
+
);
|
|
399
|
+
chmodSync(oddNode, 0o755);
|
|
400
|
+
const c = launch([], {
|
|
401
|
+
CODEX_HOME: oddClassifierHome,
|
|
402
|
+
PATH: `${oddClassifierBin}:${bin}:${process.env.PATH ?? ""}`,
|
|
403
|
+
});
|
|
368
404
|
ok(
|
|
369
405
|
"[QK:CODEX-APP-SERVER-REFUSES-UNRECOGNISED-PROBE] a socket classifier that exits 0 with a reading nobody wrote REFUSES instead of falling through to the exec",
|
|
370
406
|
r.status !== 0 && r.out.includes("codex-app-server-socket-probe-unrecognised") && r.args.length === 0,
|
|
371
407
|
);
|
|
408
|
+
ok(
|
|
409
|
+
`[QK:CODEX-APP-SERVER-REFUSES-UNRECOGNISED-CLASSIFIER] a classifier word the classifier never gives is refused before it can be read as one of the probe's own verdicts — 'stale' would otherwise launch (status ${c.status}, ${c.args.length} vendor args)`,
|
|
410
|
+
c.status !== 0 &&
|
|
411
|
+
c.out.includes("codex-app-server-socket-probe-unrecognised") &&
|
|
412
|
+
c.out.includes("the socket classifier answered\n 'stale'") &&
|
|
413
|
+
c.args.length === 0,
|
|
414
|
+
);
|
|
372
415
|
}
|
|
373
416
|
|
|
374
417
|
// ── 5. refusals that keep this from becoming something it is not ────────────
|