@junghanacs/entwurf 0.23.1 → 0.24.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/BASELINE.md +1 -0
- package/CHANGELOG.md +173 -0
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +2 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +2 -2
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -1
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +8 -7
- package/pi-extensions/entwurf-control.ts +56 -0
- package/pi-extensions/lib/acp/backend.ts +16 -6
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +4 -1
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
- package/pi-extensions/lib/herdr-placement.ts +13 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/run.sh +135 -50
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +33 -9
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +44 -5
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +22 -6
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-pi-launch.ts +5 -2
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +11 -0
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
- package/scripts/smoke-herdr-raw-install-live.sh +31 -3
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +2 -2
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
* there is no "session created" field because nothing here creates one.
|
|
87
87
|
*/
|
|
88
88
|
|
|
89
|
+
import { callbackEnvAssignments } from "./callback-env.ts";
|
|
89
90
|
import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
|
|
90
91
|
import {
|
|
91
92
|
CODEX_CALLER_SEAT_HINT,
|
|
@@ -111,6 +112,7 @@ import {
|
|
|
111
112
|
composeFreshCallPrompt,
|
|
112
113
|
FRESH_CALL_BACKENDS,
|
|
113
114
|
FRESH_CALL_CALLBACK_TOOL,
|
|
115
|
+
FRESH_CALL_DELIVERY_TOOL,
|
|
114
116
|
type FreshCallBackend,
|
|
115
117
|
type FreshCallComposition,
|
|
116
118
|
type FreshCallInputRejectReason,
|
|
@@ -158,6 +160,7 @@ export {
|
|
|
158
160
|
buildOmpBootstrapPayload,
|
|
159
161
|
FRESH_CALL_BACKENDS,
|
|
160
162
|
FRESH_CALL_CALLBACK_TOOL,
|
|
163
|
+
FRESH_CALL_DELIVERY_TOOL,
|
|
161
164
|
type FreshCallBackend,
|
|
162
165
|
type FreshCallComposition,
|
|
163
166
|
isSafeFreshCallModel,
|
|
@@ -351,9 +354,11 @@ export type FreshCallResult = { ok: true; receipt: FreshCallReceipt } | { ok: fa
|
|
|
351
354
|
* scrub only on the backend whose measurement surfaced it would encode the claim that the other
|
|
352
355
|
* four are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
|
|
353
356
|
* authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
|
|
354
|
-
* exactly that after this argv has run. This is a fixed
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
+
* exactly that after this argv has run. This is a fixed seam and deliberately NOT a general env
|
|
358
|
+
* carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping power this
|
|
359
|
+
* rail exists to refuse. Two more `-e` assignments ride beside the scrub: the callback pair
|
|
360
|
+
* (`callback-env.ts`), launcher-computed, so the sibling's first action does not retype an
|
|
361
|
+
* address out of prose.
|
|
357
362
|
*/
|
|
358
363
|
const SCRUBBED_INHERITED_ENV = ["PI_SESSION_ID=", "PI_AGENT_ID="] as const;
|
|
359
364
|
|
|
@@ -371,7 +376,8 @@ export function buildFreshCallArgs(
|
|
|
371
376
|
targetSessionId: string,
|
|
372
377
|
runtimePath: string,
|
|
373
378
|
backendArgs: readonly string[],
|
|
374
|
-
cwd
|
|
379
|
+
cwd: string | undefined,
|
|
380
|
+
callback: { target: string; nonce: string },
|
|
375
381
|
): string[] {
|
|
376
382
|
assertSelector("session", targetSessionId);
|
|
377
383
|
assertLaunchTarget(runtimePath);
|
|
@@ -384,6 +390,7 @@ export function buildFreshCallArgs(
|
|
|
384
390
|
"-d",
|
|
385
391
|
"-a",
|
|
386
392
|
...SCRUBBED_INHERITED_ENV.flatMap((assignment) => ["-e", assignment]),
|
|
393
|
+
...callbackEnvAssignments(callback).flatMap((assignment) => ["-e", assignment]),
|
|
387
394
|
"-t",
|
|
388
395
|
`${targetSessionId}:{end}`,
|
|
389
396
|
...(cwd === undefined ? [] : ["-c", cwd]),
|
|
@@ -569,7 +576,13 @@ export function freshCall(
|
|
|
569
576
|
);
|
|
570
577
|
}
|
|
571
578
|
}
|
|
572
|
-
const run = runTmux(
|
|
579
|
+
const run = runTmux(
|
|
580
|
+
buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cwd, {
|
|
581
|
+
target: callerGardenId,
|
|
582
|
+
nonce,
|
|
583
|
+
}),
|
|
584
|
+
env,
|
|
585
|
+
);
|
|
573
586
|
assertTmuxOk("new-window", run);
|
|
574
587
|
|
|
575
588
|
let fields: ReturnType<typeof parseWindowFields>;
|
|
@@ -683,9 +696,11 @@ export function renderFreshCall(result: FreshCallResult): { text: string; isErro
|
|
|
683
696
|
`\n` +
|
|
684
697
|
`This is a LAUNCH receipt: tmux created that window and was asked to start the runtime with the model above. It does ` +
|
|
685
698
|
`NOT mean the sibling is running, that its first turn ran, or that the task was delivered.\n` +
|
|
686
|
-
`The sibling's garden id arrives separately —
|
|
687
|
-
`
|
|
688
|
-
`
|
|
699
|
+
`The sibling's garden id arrives separately — its first action is the zero-argument callback verb ` +
|
|
700
|
+
`(codex is the one arm that still sends the nonce as arguments), and the sender envelope of THAT ` +
|
|
701
|
+
`message is the address. The nonce above is the correlation tag it carries; the sibling reads it ` +
|
|
702
|
+
`from its own launch env, never from this receipt. Nothing is polling for it; if it never comes, ` +
|
|
703
|
+
`the window is visible and can be read directly.`,
|
|
689
704
|
isError: false,
|
|
690
705
|
};
|
|
691
706
|
}
|
|
@@ -83,7 +83,6 @@ import {
|
|
|
83
83
|
type MetaRootBundle,
|
|
84
84
|
ompMetaRootBase,
|
|
85
85
|
ompMetaRoots,
|
|
86
|
-
requireGardenId,
|
|
87
86
|
upsertMetaSession,
|
|
88
87
|
writeMetaSenderMarker,
|
|
89
88
|
} from "./lib/meta-session.ts";
|
|
@@ -162,7 +161,7 @@ export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
|
|
|
162
161
|
/** Payload grammar version. A payload that does not say exactly this is refused rather than
|
|
163
162
|
* best-guessed: the launcher and this unit ship in the same package, so a mismatch means a
|
|
164
163
|
* STALE installed unit — the one condition `doctor-omp-bridge` exists to name out loud. */
|
|
165
|
-
export const OMP_BOOTSTRAP_VERSION =
|
|
164
|
+
export const OMP_BOOTSTRAP_VERSION = 2;
|
|
166
165
|
|
|
167
166
|
/**
|
|
168
167
|
* The model-facing callback tool, spelled for omp. `[측정]` the vendor's minter sanitises to
|
|
@@ -177,7 +176,7 @@ export const OMP_BOOTSTRAP_VERSION = 1;
|
|
|
177
176
|
* spellings are held equal by `test/omp-fresh-bootstrap.contract.test.ts`, the same shape
|
|
178
177
|
* `check-omp-fresh-preflight` already uses for the preflight's reproduced oracles.
|
|
179
178
|
*/
|
|
180
|
-
export const OMP_BOOTSTRAP_CALLBACK_TOOL = "
|
|
179
|
+
export const OMP_BOOTSTRAP_CALLBACK_TOOL = "mcp__entwurf_bridge_entwurf_callback";
|
|
181
180
|
|
|
182
181
|
/** Task ceiling, held equal to the launcher's `TASK_MAX_CHARS` by the same contract test. */
|
|
183
182
|
export const OMP_BOOTSTRAP_TASK_MAX_CHARS = 16000;
|
|
@@ -195,15 +194,9 @@ export const OMP_BOOTSTRAP_READY_TIMEOUT_MS = 60_000;
|
|
|
195
194
|
* primitive; a FIXED DELAY is forbidden because the gap is a race, not a constant. */
|
|
196
195
|
export const OMP_BOOTSTRAP_POLL_MS = 100;
|
|
197
196
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/** What the launcher put on the flag. Exactly three fields — a target to call back to, the
|
|
202
|
-
* correlation nonce, and the operator's task. Nothing here is a command, a path or an env
|
|
203
|
-
* name, and the decoder refuses anything wider. */
|
|
197
|
+
/** What the launcher put on the flag. Address rides process env; this payload is the task
|
|
198
|
+
* released after the no-arg callback succeeds. The decoder refuses anything wider. */
|
|
204
199
|
export interface OmpBootstrapPayload {
|
|
205
|
-
target: string;
|
|
206
|
-
nonce: string;
|
|
207
200
|
task: string;
|
|
208
201
|
}
|
|
209
202
|
|
|
@@ -216,8 +209,6 @@ export type OmpBootstrapRejectReason =
|
|
|
216
209
|
| "payload-not-object"
|
|
217
210
|
| "version-unsupported"
|
|
218
211
|
| "payload-unknown-key"
|
|
219
|
-
| "target-invalid"
|
|
220
|
-
| "nonce-invalid"
|
|
221
212
|
| "task-empty"
|
|
222
213
|
| "task-too-long";
|
|
223
214
|
|
|
@@ -228,7 +219,7 @@ export type OmpBootstrapDecode =
|
|
|
228
219
|
/** The wire keys, closed. An unknown key is a REFUSAL rather than an ignored extra: this
|
|
229
220
|
* flag is the one thing a caller can put arbitrary bytes into, so the decoder's job is to
|
|
230
221
|
* make "what the launcher meant" and "what this unit will act on" the same set. */
|
|
231
|
-
const BOOTSTRAP_KEYS = new Set(["v", "
|
|
222
|
+
const BOOTSTRAP_KEYS = new Set(["v", "task"]);
|
|
232
223
|
|
|
233
224
|
/**
|
|
234
225
|
* Read the flag value into a payload, or name why not.
|
|
@@ -256,18 +247,10 @@ export function decodeOmpBootstrapPayload(raw: unknown): OmpBootstrapDecode {
|
|
|
256
247
|
for (const key of Object.keys(obj)) {
|
|
257
248
|
if (!BOOTSTRAP_KEYS.has(key)) return { ok: false, reason: "payload-unknown-key" };
|
|
258
249
|
}
|
|
259
|
-
let target: string;
|
|
260
|
-
try {
|
|
261
|
-
target = requireGardenId(obj.target);
|
|
262
|
-
} catch {
|
|
263
|
-
return { ok: false, reason: "target-invalid" };
|
|
264
|
-
}
|
|
265
|
-
const nonce = obj.nonce;
|
|
266
|
-
if (typeof nonce !== "string" || !BOOTSTRAP_NONCE_RE.test(nonce)) return { ok: false, reason: "nonce-invalid" };
|
|
267
250
|
const task = obj.task;
|
|
268
251
|
if (typeof task !== "string" || task.trim().length === 0) return { ok: false, reason: "task-empty" };
|
|
269
252
|
if (task.length > OMP_BOOTSTRAP_TASK_MAX_CHARS) return { ok: false, reason: "task-too-long" };
|
|
270
|
-
return { ok: true, value: {
|
|
253
|
+
return { ok: true, value: { task } };
|
|
271
254
|
}
|
|
272
255
|
|
|
273
256
|
/**
|
|
@@ -279,18 +262,13 @@ export function decodeOmpBootstrapPayload(raw: unknown): OmpBootstrapDecode {
|
|
|
279
262
|
* no competing goal at all. Stage two exists so this message never has to compete with the
|
|
280
263
|
* work; adding "then do X" back into it would rebuild exactly the prompt that failed.
|
|
281
264
|
*/
|
|
282
|
-
export function buildOmpCallbackOnlyPrompt(
|
|
265
|
+
export function buildOmpCallbackOnlyPrompt(): string {
|
|
283
266
|
return [
|
|
284
267
|
"You are a fresh visible citizen that entwurf opened in the operator's tmux session.",
|
|
285
268
|
"",
|
|
286
|
-
`FIRST AND ONLY ACTION RIGHT NOW: call ${OMP_BOOTSTRAP_CALLBACK_TOOL} with
|
|
287
|
-
`target=${params.target}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
288
|
-
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
269
|
+
`FIRST AND ONLY ACTION RIGHT NOW: call ${OMP_BOOTSTRAP_CALLBACK_TOOL} with no arguments.`,
|
|
289
270
|
"That call is how the agent that opened you learns your address. Make the call before",
|
|
290
|
-
"reading files, before planning, and before answering in prose.
|
|
291
|
-
"",
|
|
292
|
-
"Do not inspect environment variables, do not call entwurf_self, and do not start an MCP",
|
|
293
|
-
"server yourself. Your own report of your identity is not the address anyone needs.",
|
|
271
|
+
"reading files, before planning, and before answering in prose. There are no parameters.",
|
|
294
272
|
"",
|
|
295
273
|
"Your actual task arrives as the NEXT user message, immediately after that call succeeds.",
|
|
296
274
|
"Do not ask for it and do not guess at it.",
|
|
@@ -756,20 +734,29 @@ export function ompCallbackToolReady(pi: OmpExtensionApi): boolean {
|
|
|
756
734
|
}
|
|
757
735
|
|
|
758
736
|
/**
|
|
759
|
-
* Does this tool event name OUR callback
|
|
737
|
+
* Does this tool event name OUR callback?
|
|
760
738
|
*
|
|
761
739
|
* The event carries no server name, no session id and no original MCP tool name
|
|
762
|
-
* (`extensions/types.ts:916-1017`), so the match is built from what IS there:
|
|
763
|
-
* minted tool name
|
|
764
|
-
*
|
|
765
|
-
*
|
|
740
|
+
* (`extensions/types.ts:916-1017`), so the match is built from what IS there: exact string
|
|
741
|
+
* equality on the canonical minted tool name — a prefix or `includes` here would let a
|
|
742
|
+
* neighbouring tool release this session's task.
|
|
743
|
+
*
|
|
744
|
+
* The target/nonce half of this predicate is GONE, and that is the point of the out-of-band
|
|
745
|
+
* callback: the address rides the sibling's process env, so the verb takes no arguments and
|
|
746
|
+
* there are no argument values left to compare. What replaces the comparison is the ABSENCE
|
|
747
|
+
* of arguments — a call carrying a target or a message is a model supplying an address, which
|
|
748
|
+
* is the second address axis Hard Rule 2 refuses, and it must not release the task. Correlation
|
|
749
|
+
* to THIS bootstrap is not weakened by the removal: it is carried by the stored `toolCallId`
|
|
750
|
+
* (one session, one live tool call) plus the fact that only this session's env named the caller.
|
|
766
751
|
*/
|
|
767
|
-
function matchesCallback(event: OmpToolCallEvent,
|
|
752
|
+
function matchesCallback(event: OmpToolCallEvent, _payload: OmpBootstrapPayload): boolean {
|
|
768
753
|
if (event?.toolName !== OMP_BOOTSTRAP_CALLBACK_TOOL) return false;
|
|
769
754
|
const input = event?.input;
|
|
770
|
-
if (
|
|
755
|
+
if (input === undefined || input === null) return true;
|
|
756
|
+
if (typeof input !== "object" || Array.isArray(input)) return false;
|
|
771
757
|
const args = input as Record<string, unknown>;
|
|
772
|
-
|
|
758
|
+
// No-arg verb: a supplied target/message is a second address axis — not our callback.
|
|
759
|
+
return args.target === undefined && args.message === undefined;
|
|
773
760
|
}
|
|
774
761
|
|
|
775
762
|
/**
|
|
@@ -822,7 +809,7 @@ export function createOmpBootstrap(opts: {
|
|
|
822
809
|
stopTimer();
|
|
823
810
|
if (isFinal()) return;
|
|
824
811
|
phase = "failed";
|
|
825
|
-
log("WARN", `bootstrap-failed
|
|
812
|
+
log("WARN", `bootstrap-failed: ${why}; the task was NOT sent`);
|
|
826
813
|
}
|
|
827
814
|
|
|
828
815
|
/**
|
|
@@ -858,8 +845,8 @@ export function createOmpBootstrap(opts: {
|
|
|
858
845
|
// two callback prompts for one bootstrap.
|
|
859
846
|
phase = "callback-sent";
|
|
860
847
|
stopTimer();
|
|
861
|
-
log("INFO",
|
|
862
|
-
send(buildOmpCallbackOnlyPrompt(
|
|
848
|
+
log("INFO", "bootstrap-ready: callback tool live, sending callback-only prompt");
|
|
849
|
+
send(buildOmpCallbackOnlyPrompt());
|
|
863
850
|
return;
|
|
864
851
|
}
|
|
865
852
|
if (timers.now() >= deadline) {
|
|
@@ -895,14 +882,14 @@ export function createOmpBootstrap(opts: {
|
|
|
895
882
|
const id = event?.toolCallId;
|
|
896
883
|
if (typeof id !== "string" || id.length === 0) return;
|
|
897
884
|
callId = id;
|
|
898
|
-
log("INFO", `bootstrap-callback-observed
|
|
885
|
+
log("INFO", `bootstrap-callback-observed toolCallId=${id}`);
|
|
899
886
|
},
|
|
900
887
|
onToolResult(event: OmpToolResultEvent): void {
|
|
901
888
|
if (phase !== "callback-sent" || callId === null) return;
|
|
902
889
|
if (event?.toolCallId !== callId) return;
|
|
903
890
|
if (!matchesCallback(event, payload)) return;
|
|
904
891
|
if (event?.isError !== false) {
|
|
905
|
-
log("WARN", `bootstrap-callback-errored
|
|
892
|
+
log("WARN", `bootstrap-callback-errored toolCallId=${callId}; the task was NOT sent`);
|
|
906
893
|
return;
|
|
907
894
|
}
|
|
908
895
|
// RELEASED, AND THE TASK IS NOT SENT HERE. This handler runs inside the callback
|
|
@@ -911,7 +898,7 @@ export function createOmpBootstrap(opts: {
|
|
|
911
898
|
// record that the callback provably succeeded; `onTurnEnd` owns the send.
|
|
912
899
|
phase = "released";
|
|
913
900
|
stopTimer();
|
|
914
|
-
log("INFO", `bootstrap-released
|
|
901
|
+
log("INFO", `bootstrap-released toolCallId=${callId}: task armed for the next turn_end`);
|
|
915
902
|
},
|
|
916
903
|
/**
|
|
917
904
|
* The stage-two boundary: the callback turn is over, so the session is at the edge
|
|
@@ -932,7 +919,7 @@ export function createOmpBootstrap(opts: {
|
|
|
932
919
|
onTurnEnd(): void {
|
|
933
920
|
if (phase !== "released") return;
|
|
934
921
|
phase = "task-sent";
|
|
935
|
-
log("INFO",
|
|
922
|
+
log("INFO", "bootstrap-task-sent: delivering the task at the turn_end boundary");
|
|
936
923
|
send(payload.task);
|
|
937
924
|
},
|
|
938
925
|
invalidate(why: string): void {
|
|
@@ -942,7 +929,7 @@ export function createOmpBootstrap(opts: {
|
|
|
942
929
|
// caller's task must die with the session it was addressed to.
|
|
943
930
|
if (isFinal()) return;
|
|
944
931
|
phase = "failed";
|
|
945
|
-
log("INFO", `bootstrap-invalidated
|
|
932
|
+
log("INFO", `bootstrap-invalidated: ${why}; the task was NOT sent`);
|
|
946
933
|
},
|
|
947
934
|
};
|
|
948
935
|
}
|
|
@@ -1118,7 +1105,7 @@ export function startOmpBootstrap(opts: {
|
|
|
1118
1105
|
const handle = createOmpBootstrap({ payload: decoded.value, pi, timers, log });
|
|
1119
1106
|
activeBootstrap = { sessionId: envelope.nativeSessionId, handle };
|
|
1120
1107
|
bootstrapConsumed = true;
|
|
1121
|
-
log("INFO",
|
|
1108
|
+
log("INFO", "bootstrap-armed: payload admitted, waiting for callback tool");
|
|
1122
1109
|
handle.start();
|
|
1123
1110
|
}
|
|
1124
1111
|
|
package/run.sh
CHANGED
|
@@ -122,7 +122,7 @@ Usage:
|
|
|
122
122
|
./run.sh setup [project-dir] # ONE presence-driven composition (#86): per-component PASS/SKIP/FAIL for pi/claude/agy/copilot/omp/codex + stable dev bins + v2 install smoke. Codex birth/MCP/status-line atoms are composed at user scope; setup stays non-green until the operator trusts the birth declaration once in visible Codex. Absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness, starts a daemon, touches credentials, or writes the vendor trust receipt
|
|
123
123
|
./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live) + the Codex Pi -> visible Codex -> visible Pi receipt chain (smoke-codex-fresh-live; initial Pi and the Codex it opens in one session S, explicit operator-owned app-server PID in a DIFFERENT session — that separation is the lane B claim). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
|
|
124
124
|
./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
|
|
125
|
-
./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 and
|
|
125
|
+
./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f/G1h, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2, entwurf_resume_call and entwurf_callback registered/schema + the tools/list surface is EXACTLY the eight shipped garden verbs; tools/list only, no auth/side-effect
|
|
126
126
|
./run.sh check-entwurf-bridge-pi-free # deterministic gate (0.12.1 A, IN pnpm check): static — bridge index eager value-import closure must carry no @earendil-works/pi-* (type-only + dynamic import excluded); proves the meta-bridge boots pi-free
|
|
127
127
|
./run.sh check-model-lock # deterministic unit test for pi-extensions/model-lock.ts (4-quadrant + edge cases, no API)
|
|
128
128
|
./run.sh check-shell-quote # POSIX-safety gate for shellQuote (remote SSH arg quoting in entwurf paths) — source parity + behavior matrix, no SSH
|
|
@@ -276,7 +276,7 @@ Usage:
|
|
|
276
276
|
./run.sh check-dep-versions # local deterministic check that the pi pin agrees across package.json (devDeps + peer range), run.sh (peer-install pins), and the baseline docs (AGENTS/README/ROADMAP/setup-clean-host/demo)
|
|
277
277
|
./run.sh check-node-floor-coherence # binds the Node floor (24+, single axis) across engines.node, run.sh setup preflight, meta-bridge install/doctor judgment logic, clean-host docs, the bridge launcher header, and the CI runner node-version — engines.node is the SSOT, everything else is derived; sweeps tracked contract text for an unregistered declaration
|
|
278
278
|
./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
|
|
279
|
-
./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes, one cell per property: version boundary (@0.85.
|
|
279
|
+
./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes, one cell per property: version boundary (a prefix-extended @0.86.0-beta.1 and an off-pin @0.85.1 must leak, @0.86.0 bare or with any measured peer-hash must pass) and closure prefix (an off-pin @earendil-works/chord must leak — it carries no `pi-` prefix); snapshot-safe qualification oracle, also run first inside check-pack-install
|
|
280
280
|
./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm run check:full): drives real install/setup/fresh-cut in a sandbox; certification refusal is pre-write, quiescence is fail-closed, archives preserve bytes, and the #54 exit matrix distinguishes complete / no-move / usage / incomplete transition / complete-with-cleanup-residue. No model/network/cost
|
|
281
281
|
./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke + the #116 M3-b1 herdr-plugin RUNTIME VERIFIER run against that actual installed tarball (the focused check-herdr-runtime-bootstrap drives a fixture; THIS is where the real package is proven) with the pinned pi peers (pins derived from the package.json devDep; check-dep-versions binds them) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the installed all-absent and copilot-present (four-unit fake-vendor) `entwurf setup` rows + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES) + the INSTALLED-PACKAGE branch of the Copilot and OMP birth installers actually RUN (compiled entry selected, no raw .ts, and a real birth edge mints a citizen — the half a required-artifact list can never stand in for)
|
|
282
282
|
./run.sh check-install-container # 0.12.8 (#51 C): Linux artifact-CONSUMER gate — one candidate .tgz handed read-only to a checkout-invisible node:<engines-major>-bookworm cell. Default packs once to temp; ENTWURF_CANDIDATE_TGZ=/absolute/preserved.tgz consumes those exact bytes with no re-pack and prints canonical path+sha256 for release. Non-root global PATH install, frozen package, MCP tools/list, fake-Claude install-meta-bridge, path+sha256 fence, strict doctor, and the GENERATION host-state matrix (clean / v3-only store bytes unchanged / previous-generation REFUSE→fresh-cut→retry PASS) seeded inline. Docker missing = honest SKIP; ENTWURF_REQUIRE_DOCKER=1 makes that RED (required CI)
|
|
@@ -709,6 +709,31 @@ install_user_scope() {
|
|
|
709
709
|
esac
|
|
710
710
|
shift
|
|
711
711
|
done
|
|
712
|
+
# pi FLOOR — the same closed range `setup` enforces (#119 door symmetry). setup refuses an
|
|
713
|
+
# out-of-range pi by name before it writes any Pi wiring; this verb is the OTHER door onto the
|
|
714
|
+
# same registration (herdr plugin activation reaches it through plugins/herdr/lib/build.mjs →
|
|
715
|
+
# scripts/herdr-plugin-activate.mjs → `run.sh install-user-scope --plugin-runtime …`) and had no
|
|
716
|
+
# version check at all, so a host pi setup would name as FAIL got its citizen written here in
|
|
717
|
+
# silence. Both doors now tell the same truth, off the SAME derived range — `pi_supported_range`
|
|
718
|
+
# reads the package.json devDep pin, so there is one range in the repo and no second parser.
|
|
719
|
+
#
|
|
720
|
+
# ABSENT pi is deliberately NOT changed here. This verb is called by an activation that has
|
|
721
|
+
# already decided pi is one of its backends; inventing a presence verdict at the writer would put
|
|
722
|
+
# a second detection authority next to setup's. Out-of-range is the asymmetry that was real, and
|
|
723
|
+
# it is the only one closed. PI_BIN is the same probe seam setup uses (hermetic gates pin it;
|
|
724
|
+
# production leaves it unset).
|
|
725
|
+
local pi_bin="${PI_BIN:-pi}" pi_range pi_ver
|
|
726
|
+
if command -v "$pi_bin" >/dev/null 2>&1; then
|
|
727
|
+
pi_range=$(pi_supported_range)
|
|
728
|
+
# Bounded capture, same reason as setup's: a resolvable pi whose --version crashes or prints
|
|
729
|
+
# garbage must reach the refusal below, never abort through set -e with nothing said.
|
|
730
|
+
pi_ver="$("$pi_bin" --version 2>/dev/null | head -n 1 | tr -d '[:space:]')" || pi_ver=""
|
|
731
|
+
if ! pi_version_in_range "${pi_ver:-0}" "$pi_range"; then
|
|
732
|
+
echo "[install-user-scope] pi: FAIL — detected pi ${pi_ver:-<unreadable version>} is outside the supported range $pi_range; nothing was written. Align pi, then re-run." >&2
|
|
733
|
+
exit 1
|
|
734
|
+
fi
|
|
735
|
+
fi
|
|
736
|
+
|
|
712
737
|
preflight_v3_store install-user-scope
|
|
713
738
|
section "install-user-scope: the GLOBAL pi citizen only (no project wiring)"
|
|
714
739
|
register_user_scope_citizen "$plugin_runtime"
|
|
@@ -2120,13 +2145,16 @@ assert.equal(peerTui, piAi,
|
|
|
2120
2145
|
// floor tracks the devDep pin so a consumer can't install against a pi lacking
|
|
2121
2146
|
// the public trust exports the bridge imports at the pinned minor, AND an upper
|
|
2122
2147
|
// bound at the next minor stops a fresh install from silently pulling a future
|
|
2123
|
-
// pi (past the declared ceiling — 0.
|
|
2148
|
+
// pi (past the declared ceiling — 0.87+ at the current 0.86.0 pin) whose
|
|
2124
2149
|
// internal export surface has drifted from the one we typecheck against.
|
|
2125
2150
|
// pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
|
|
2126
|
-
// factory churn is exactly this
|
|
2127
|
-
//
|
|
2128
|
-
//
|
|
2129
|
-
//
|
|
2151
|
+
// factory churn is exactly this, and 0.86 is the same shape again: a custom
|
|
2152
|
+
// provider's streamSimple now receives a branded TranscriptContext instead of a
|
|
2153
|
+
// raw Context, with systemPrompt/tools folded into a leading system message), so
|
|
2154
|
+
// an open `>=` floor is exactly how the next installer re-acquires the drift.
|
|
2155
|
+
// The floor is also the HARD MINIMUM a consumer install resolves: at `>=0.86.0`
|
|
2156
|
+
// an existing 0.85.1 host is upgraded, not kept.
|
|
2157
|
+
// Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.86.0 <0.87`).
|
|
2130
2158
|
const [piMaj, piMin] = piAi.split('.').map(Number);
|
|
2131
2159
|
assert.equal(piMaj, 0,
|
|
2132
2160
|
`pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
|
|
@@ -2157,10 +2185,48 @@ assert.equal(peerDepTui, expectedPeer,
|
|
|
2157
2185
|
// read, and sat two bumps stale (`>=0.84.3 <0.85` at a 0.85.1 pin) precisely
|
|
2158
2186
|
// because it was outside this list. Its declaration is a plain range, so the
|
|
2159
2187
|
// range scan below binds it — no new PROSE_DECL is needed for it.
|
|
2160
|
-
|
|
2188
|
+
// plugins/herdr/README.md joined on 2026-09-20, the same repair one bump later:
|
|
2189
|
+
// its step-1 `npm install -g @earendil-works/pi-coding-agent` was UNPINNED, so
|
|
2190
|
+
// from the hour 0.86.0 published it installed a pi outside the range the plugin
|
|
2191
|
+
// ships against — and nothing read that line. It now carries the range and is
|
|
2192
|
+
// scanned here, which is the only reason writing the number down is allowed.
|
|
2193
|
+
// docs/acp-backend-rail.md joined on 2026-09-20 for the same reason the two before it did:
|
|
2194
|
+
// its support table declared `peer >=0.85.1 <0.86` in prose no gate read, and the 0.86.0 bump
|
|
2195
|
+
// would have carried that into a release. Its declaration is a plain closed range and it is the
|
|
2196
|
+
// ONLY one in that file (the other rows declare exact versions of non-pi packages, which neither
|
|
2197
|
+
// pattern below matches), so the range scan binds it with no new PROSE_DECL. The bare `0.86.0`
|
|
2198
|
+
// devDep mentions in the same cell are prose, not an install pin, and stay unscanned — the range
|
|
2199
|
+
// half is what holds the row honest.
|
|
2200
|
+
const BASELINE_DOCS = ['README.md', 'ROADMAP.md', 'docs/setup-clean-host.md', 'demo/README.md', 'VERIFY.md', 'plugins/herdr/README.md', 'docs/acp-backend-rail.md'];
|
|
2201
|
+
|
|
2202
|
+
// ROADMAP.md is a baseline doc AND the home of the dated bump ledger, and those
|
|
2203
|
+
// are opposite kinds of sentence. A ledger entry is a RECEIPT: `2026-09-06 bump
|
|
2204
|
+
// — pi 0.84.4 → 0.85.1` records the range that landed THAT day, and rewriting it
|
|
2205
|
+
// at the next bump would destroy the evidence trail the entry exists to keep.
|
|
2206
|
+
// The comment above always said history keeps its old versions; until 0.86.0
|
|
2207
|
+
// that was only true because the newest entry happened to name the live pin, so
|
|
2208
|
+
// the FIRST bump after an entry was written is what exposed it. A ledger entry
|
|
2209
|
+
// opens at its dated bullet and runs until the enclosing list resumes at column
|
|
2210
|
+
// 0 (measured: every ` - ` bullet inside the Dep-bump block is a bump entry).
|
|
2211
|
+
function stripBumpLedger(file, text) {
|
|
2212
|
+
if (file !== 'ROADMAP.md') return text;
|
|
2213
|
+
let inLedger = false, dropped = 0;
|
|
2214
|
+
const kept = text.split('\n').filter((line) => {
|
|
2215
|
+
if (/^ - \*\*\d{4}-\d{2}-\d{2} bump — /.test(line)) { inLedger = true; }
|
|
2216
|
+
else if (inLedger && /^(- |#|\S)/.test(line)) { inLedger = false; }
|
|
2217
|
+
if (inLedger) dropped++;
|
|
2218
|
+
return !inLedger;
|
|
2219
|
+
});
|
|
2220
|
+
// Guard the exclusion itself: it must find a real ledger and must not eat the
|
|
2221
|
+
// whole file. Either way the range/exact scans below would pass vacuously.
|
|
2222
|
+
assert.ok(dropped > 0, 'ROADMAP.md: no dated bump-ledger entry found — the ledger moved or its bullet shape changed; re-point stripBumpLedger instead of leaving history inside the pin scan');
|
|
2223
|
+
assert.ok(kept.length > 100, `ROADMAP.md: the bump-ledger exclusion swallowed the document (${kept.length} lines left) — the live pin declarations would no longer be scanned`);
|
|
2224
|
+
return kept.join('\n');
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2161
2227
|
let rangeDecls = 0, exactDecls = 0;
|
|
2162
2228
|
for (const file of BASELINE_DOCS) {
|
|
2163
|
-
const text = readFileSync(file, 'utf8');
|
|
2229
|
+
const text = stripBumpLedger(file, readFileSync(file, 'utf8'));
|
|
2164
2230
|
// Closed-range declarations: `>=<floor> <0.<ceiling>` (spaces optional).
|
|
2165
2231
|
for (const [decl, floor, ceilMinor] of text.matchAll(/>=\s?(\d+\.\d+\.\d+)\s?<\s?0\.(\d+)/g)) {
|
|
2166
2232
|
rangeDecls++;
|
|
@@ -3470,8 +3536,10 @@ check_pack() {
|
|
|
3470
3536
|
#
|
|
3471
3537
|
# (1) The version BOUNDARY. A pnpm .pnpm entry is `<name>@<version>` followed by either
|
|
3472
3538
|
# `_<peer-hash>` or end-of-name (measured pnpm 11.20.0 on this tree), so an unbounded
|
|
3473
|
-
# substring match would bless a lookalike
|
|
3474
|
-
#
|
|
3539
|
+
# substring match would bless a lookalike whose version merely STARTS with the pin — a
|
|
3540
|
+
# prerelease such as `@0.86.0-beta.1`, or the `@0.85.10` shape that first exposed this —
|
|
3541
|
+
# while announcing the pinned floor: a false-green oracle (found by independent review,
|
|
3542
|
+
# 2026-08-25).
|
|
3475
3543
|
# (2) The name PREFIX covers the whole @earendil-works closure, not the `pi-` families alone.
|
|
3476
3544
|
# 0.85.0 added `@earendil-works/chord`, declared as a runtime `dependencies` entry by
|
|
3477
3545
|
# pi-coding-agent, pi-agent-core, pi-client and pi-protocol — inside the closure, with a
|
|
@@ -3479,15 +3547,17 @@ check_pack() {
|
|
|
3479
3547
|
# `@earendil-works+chord@0.85.1` next to the seven pi entries, and the old
|
|
3480
3548
|
# `^@earendil-works+pi-` filter did not see it: chord would have floated silently while
|
|
3481
3549
|
# this gate printed a verified pin. The prefix is the closure's org, not one family in it.
|
|
3482
|
-
# (The measured peer-hash suffix
|
|
3483
|
-
# `
|
|
3484
|
-
#
|
|
3550
|
+
# (The measured peer-hash suffix keeps moving — `_@modelcontextprotocol+sdk@…` at 0.84.4,
|
|
3551
|
+
# `_ws@8.21.3` at 0.85.1, and `[측정 2026-09-20]` the combined
|
|
3552
|
+
# `_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6` on a 0.86.0 tree —
|
|
3553
|
+
# which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
|
|
3554
|
+
# below so a future suffix change cannot pass vacuously.)
|
|
3485
3555
|
pack_install_leaked_pi() {
|
|
3486
|
-
grep '^@earendil-works+' | grep -Ev '@0\.
|
|
3556
|
+
grep '^@earendil-works+' | grep -Ev '@0\.86\.0(_|$)' || true
|
|
3487
3557
|
}
|
|
3488
3558
|
|
|
3489
3559
|
# Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
|
|
3490
|
-
# false-green shape (it contains no
|
|
3560
|
+
# false-green shape (it contains no prefix-extended version and no off-pin chord), so the oracle is proven
|
|
3491
3561
|
# against fixture listings. One cell per property of pack_install_leaked_pi, so a regression
|
|
3492
3562
|
# in either is named by its own signature rather than folded into a single verdict. Exposed
|
|
3493
3563
|
# as its own snapshot-safe subcommand because the heavy check-pack-install cannot run inside
|
|
@@ -3497,30 +3567,33 @@ pack_install_leaked_pi() {
|
|
|
3497
3567
|
check_pack_pin_matcher() {
|
|
3498
3568
|
local matcher_probe
|
|
3499
3569
|
|
|
3500
|
-
# Cell 1 — the version boundary.
|
|
3501
|
-
# 0.84.4-era `_@modelcontextprotocol+sdk
|
|
3502
|
-
# 0.85.1 tree
|
|
3570
|
+
# Cell 1 — the version boundary. Every measured peer-hash shape is a fixture: the
|
|
3571
|
+
# 0.84.4-era `_@modelcontextprotocol+sdk@…`, the `_ws@8.21.3` observed on a real
|
|
3572
|
+
# 0.85.1 tree, and `[측정 2026-09-20]` the combined suffix a 0.86.0 install carries.
|
|
3573
|
+
# None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`0.86.0-beta.1`,
|
|
3574
|
+
# the prerelease shape that an unanchored match would bless) and an off-pin version.
|
|
3503
3575
|
matcher_probe=$(printf '%s\n' \
|
|
3504
|
-
'@earendil-works+pi-ai@0.
|
|
3505
|
-
'@earendil-works+pi-ai@0.
|
|
3506
|
-
'@earendil-works+pi-ai@0.
|
|
3507
|
-
'@earendil-works+pi-ai@0.
|
|
3508
|
-
'@earendil-works+pi-
|
|
3509
|
-
|
|
3510
|
-
@earendil-works+pi-
|
|
3511
|
-
|
|
3576
|
+
'@earendil-works+pi-ai@0.86.0' \
|
|
3577
|
+
'@earendil-works+pi-ai@0.86.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
|
|
3578
|
+
'@earendil-works+pi-ai@0.86.0_ws@8.21.3' \
|
|
3579
|
+
'@earendil-works+pi-ai@0.86.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
|
|
3580
|
+
'@earendil-works+pi-ai@0.86.0-beta.1' \
|
|
3581
|
+
'@earendil-works+pi-agent-core@0.85.1' | pack_install_leaked_pi)
|
|
3582
|
+
if [ "$matcher_probe" != '@earendil-works+pi-ai@0.86.0-beta.1
|
|
3583
|
+
@earendil-works+pi-agent-core@0.85.1' ]; then
|
|
3584
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 0.86.0-beta.1 and the off-pin 0.85.1 lookalikes, and pass 0.86.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
|
|
3512
3585
|
return 1
|
|
3513
3586
|
fi
|
|
3514
3587
|
|
|
3515
3588
|
# Cell 2 — the closure prefix. chord is a non-`pi-` member of the same runtime closure
|
|
3516
|
-
# (0.85.0 onward). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
|
|
3589
|
+
# (0.85.0 onward, still true at 0.86.0). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
|
|
3517
3590
|
# back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
|
|
3518
3591
|
matcher_probe=$(printf '%s\n' \
|
|
3592
|
+
'@earendil-works+chord@0.86.0' \
|
|
3519
3593
|
'@earendil-works+chord@0.85.1' \
|
|
3520
|
-
'@earendil-works+
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
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 is a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol at 0.85.x) — got: ${matcher_probe:-<nothing leaked>}"
|
|
3594
|
+
'@earendil-works+pi-ai@0.86.0' | pack_install_leaked_pi)
|
|
3595
|
+
if [ "$matcher_probe" != '@earendil-works+chord@0.85.1' ]; then
|
|
3596
|
+
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 is a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol at 0.86.0 — `[측정 2026-09-20]` `npm view <pkg>@0.86.0 dependencies`) — got: ${matcher_probe:-<nothing leaked>}"
|
|
3524
3597
|
return 1
|
|
3525
3598
|
fi
|
|
3526
3599
|
|
|
@@ -3753,7 +3826,7 @@ _check_pack_install_impl() {
|
|
|
3753
3826
|
printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
|
|
3754
3827
|
|
|
3755
3828
|
# pi-agent-core is pinned even though we never import it: pi-coding-agent depends
|
|
3756
|
-
# on it by CARET (`^0.
|
|
3829
|
+
# on it by CARET (`^0.86.x`), so with no lockfile in this fresh temp project it
|
|
3757
3830
|
# floats to whatever pi published last — and that newer core then drags a NESTED
|
|
3758
3831
|
# pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
|
|
3759
3832
|
# pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
|
|
@@ -3782,19 +3855,31 @@ _check_pack_install_impl() {
|
|
|
3782
3855
|
# It is pinned here for the first time. The leak assertion below covers every
|
|
3783
3856
|
# other @earendil-works package too — but only because the matcher's prefix was
|
|
3784
3857
|
# widened from `pi-` to the org in the same change; before that it was blind to
|
|
3785
|
-
# precisely this class of new member.
|
|
3786
|
-
|
|
3858
|
+
# precisely this class of new member.
|
|
3859
|
+
#
|
|
3860
|
+
# `[측정 2026-09-20]` at 0.86.0 the constellation SHRANK for the first time:
|
|
3861
|
+
# `npm view @earendil-works/pi-coding-agent@0.86.0 dependencies` lists only
|
|
3862
|
+
# {chord, pi-ai, pi-tui, pi-agent-core} — pi-client and pi-protocol are no
|
|
3863
|
+
# longer reachable from the three packages we import (pi-client still carries
|
|
3864
|
+
# {chord, pi-protocol}, pi-protocol still carries {chord}, both published at
|
|
3865
|
+
# 0.86.0). Their explicit pins stay: a pin that is momentarily redundant costs
|
|
3866
|
+
# two frozen rows in a temp tree, while retiring one is how a caret re-enters
|
|
3867
|
+
# the closure unwatched on the next minor — the same shape as the 2026-07-21
|
|
3868
|
+
# pi-agent-core incident. The list moves WITH the floor and is re-measured at
|
|
3869
|
+
# each bump, never pruned on a single quiet minor.
|
|
3870
|
+
# The verified floor is 0.86.0 as of 2026-09-20.
|
|
3871
|
+
echo "[check-pack-install] pnpm add into $tmp (with 0.86.x peers + chord + typebox)"
|
|
3787
3872
|
local install_log
|
|
3788
3873
|
install_log=$(cd "$tmp" && pnpm add \
|
|
3789
3874
|
"$tgz_path" \
|
|
3790
|
-
"@earendil-works/pi-ai@0.
|
|
3791
|
-
"@earendil-works/pi-coding-agent@0.
|
|
3792
|
-
"@earendil-works/pi-tui@0.
|
|
3793
|
-
"@earendil-works/pi-agent-core@0.
|
|
3794
|
-
"@earendil-works/pi-client@0.
|
|
3795
|
-
"@earendil-works/pi-protocol@0.
|
|
3796
|
-
"@earendil-works/pi-telemetry@0.
|
|
3797
|
-
"@earendil-works/chord@0.
|
|
3875
|
+
"@earendil-works/pi-ai@0.86.0" \
|
|
3876
|
+
"@earendil-works/pi-coding-agent@0.86.0" \
|
|
3877
|
+
"@earendil-works/pi-tui@0.86.0" \
|
|
3878
|
+
"@earendil-works/pi-agent-core@0.86.0" \
|
|
3879
|
+
"@earendil-works/pi-client@0.86.0" \
|
|
3880
|
+
"@earendil-works/pi-protocol@0.86.0" \
|
|
3881
|
+
"@earendil-works/pi-telemetry@0.86.0" \
|
|
3882
|
+
"@earendil-works/chord@0.86.0" \
|
|
3798
3883
|
"typebox@latest" \
|
|
3799
3884
|
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
3800
3885
|
fail "[check-pack-install] pnpm add failed:"
|
|
@@ -3804,17 +3889,17 @@ _check_pack_install_impl() {
|
|
|
3804
3889
|
|
|
3805
3890
|
# A pin is a wish until the resolved tree is read back. Assert it: EVERY
|
|
3806
3891
|
# @earendil-works package present — direct or transitive, top level or nested,
|
|
3807
|
-
# chord included — must be the pinned 0.
|
|
3892
|
+
# chord included — must be the pinned 0.86.0. Anything else means an unpinned caret
|
|
3808
3893
|
# floated and the rest of this gate would be exercising a runtime nobody verified,
|
|
3809
|
-
# while still printing "pinned pi 0.
|
|
3894
|
+
# while still printing "pinned pi 0.86.0". Fail loud instead of proving the wrong floor.
|
|
3810
3895
|
local leaked_pi
|
|
3811
3896
|
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
|
|
3812
3897
|
if [ -n "$leaked_pi" ]; then
|
|
3813
|
-
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.
|
|
3898
|
+
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.86.0):"
|
|
3814
3899
|
printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
|
|
3815
3900
|
return 1
|
|
3816
3901
|
fi
|
|
3817
|
-
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.
|
|
3902
|
+
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.86.0 (chord included)"
|
|
3818
3903
|
|
|
3819
3904
|
# Resolve the installed package.json and confirm pi.extensions
|
|
3820
3905
|
# arrived intact. If pi.extensions is empty or missing, the
|
|
@@ -4620,7 +4705,7 @@ function finish(trimmed) {
|
|
|
4620
4705
|
// gate that exists to catch "green clone, dead consumer". Equality also catches the
|
|
4621
4706
|
// other direction the narrow-surface rule cares about: an extra or duplicated verb
|
|
4622
4707
|
// reaching hosts without a decision. Written sorted; `names` is sorted above.
|
|
4623
|
-
const EXPECT_TOOLS = ['entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
4708
|
+
const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
4624
4709
|
if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
|
|
4625
4710
|
console.error('installed MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
|
|
4626
4711
|
process.exit(1);
|
|
@@ -5179,12 +5264,12 @@ function finishOk(trimmed) {
|
|
|
5179
5264
|
}
|
|
5180
5265
|
const names = tools.map((t) => t?.name).sort();
|
|
5181
5266
|
// EXACT set, not a floor. This is the assertion behind the operator-facing claim that
|
|
5182
|
-
// `entwurf check-bridge` proves the installed bytes list the
|
|
5267
|
+
// `entwurf check-bridge` proves the installed bytes list the eight garden tools — and
|
|
5183
5268
|
// under an installed package this launcher IS the dist branch of start.sh, so a subset
|
|
5184
5269
|
// check here let an artifact missing entwurf_fresh_call / entwurf_resume_call read as a
|
|
5185
5270
|
// green bridge. Equality also refuses an undecided extra verb. Written sorted; `names`
|
|
5186
5271
|
// is sorted above.
|
|
5187
|
-
const expected = ['entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
5272
|
+
const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
5188
5273
|
if (names.length !== expected.length || expected.some((n, i) => names[i] !== n)) {
|
|
5189
5274
|
console.error(`MCP tool set MISMATCH — want exactly [${expected.join(',')}] got [${names.join(',')}]`);
|
|
5190
5275
|
process.exit(1);
|