@junghanacs/entwurf 0.24.0 → 0.25.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/CHANGELOG.md +455 -0
- package/DELIVERY.md +24 -1
- package/README.md +2 -2
- package/VERIFY.md +19 -1
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +4 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +13 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +11 -0
- package/mcp/entwurf-bridge/dist/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/src/index.ts +5 -4
- package/package.json +10 -9
- package/pi-extensions/entwurf-control.ts +27 -154
- package/pi-extensions/lib/compaction-send-guard.ts +5 -3
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +13 -3
- package/pi-extensions/lib/herdr-placement.ts +11 -0
- package/protocol.js +0 -8
- package/run.sh +197 -29
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-gate-qualification.ts +7 -4
- package/scripts/check-herdr-fresh-call.ts +10 -2
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +7 -4
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-fresh-call.json +4 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-herdr-fresh-call-live.ts +15 -8
- package/scripts/smoke-herdr-raw-install-live.sh +59 -2
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- package/scripts/smoke-pi-provider-state.sh +106 -0
- package/scripts/smoke-setup-verdict.sh +18 -2
|
@@ -282,6 +282,22 @@ const SUCCESS_RECEIPT = {
|
|
|
282
282
|
observedLiveness: "alive" as const,
|
|
283
283
|
};
|
|
284
284
|
|
|
285
|
+
/** Collect `const X = "literal"` bindings out of a parsed source, so a customType that travels
|
|
286
|
+
* through a named constant resolves to the same string a literal would. */
|
|
287
|
+
function visitConstsInto(
|
|
288
|
+
src: import("typescript").SourceFile,
|
|
289
|
+
into: Map<string, string>,
|
|
290
|
+
ts: typeof import("typescript"),
|
|
291
|
+
): void {
|
|
292
|
+
const walk = (node: import("typescript").Node): void => {
|
|
293
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer !== undefined) {
|
|
294
|
+
if (ts.isStringLiteral(node.initializer)) into.set(node.name.text, node.initializer.text);
|
|
295
|
+
}
|
|
296
|
+
ts.forEachChild(node, walk);
|
|
297
|
+
};
|
|
298
|
+
walk(src);
|
|
299
|
+
}
|
|
300
|
+
|
|
285
301
|
async function main(): Promise<void> {
|
|
286
302
|
// ── 1: toDispatchInput mapping ────────────────────────────────────────────
|
|
287
303
|
{
|
|
@@ -336,16 +352,50 @@ async function main(): Promise<void> {
|
|
|
336
352
|
rr.isError && rr.text.includes("target-locked") && rr.text.includes("pid 999"),
|
|
337
353
|
);
|
|
338
354
|
|
|
339
|
-
//
|
|
340
|
-
|
|
355
|
+
// #120 P2 — THE BOUNDARY IS WHAT THIS LINE SAYS, and the route is an annotation beside it.
|
|
356
|
+
// The delivered branch renders what the RECEIVER observed, never which leg carried the
|
|
357
|
+
// bytes: for 78 minutes `control-socket → sent` was printed about messages lying in a
|
|
358
|
+
// queue, and the word `sent` is the whole reason nobody could tell.
|
|
359
|
+
const ctl = (
|
|
360
|
+
outcome: "sent" | "fallback-sent",
|
|
361
|
+
boundary?: "sent" | "queued-steer" | "queued-follow-up" | "accepted-unknown-boundary",
|
|
362
|
+
messagePath?: string,
|
|
363
|
+
): EntwurfV2RunResult => ({
|
|
341
364
|
kind: "executed",
|
|
342
365
|
receipt: SUCCESS_RECEIPT,
|
|
343
366
|
transport: "control-socket",
|
|
344
|
-
outcome: { transport: "control-socket", outcome
|
|
345
|
-
};
|
|
367
|
+
outcome: { transport: "control-socket", outcome, boundary, messagePath },
|
|
368
|
+
});
|
|
369
|
+
const idle = renderEntwurfV2Result(ctl("sent", "sent"));
|
|
370
|
+
ok("2: an idle direct trigger renders `sent`, and is not an error", !idle.isError && idle.text.endsWith("→ sent"));
|
|
371
|
+
const steered = renderEntwurfV2Result(ctl("sent", "queued-steer"));
|
|
372
|
+
const followed = renderEntwurfV2Result(ctl("sent", "queued-follow-up"));
|
|
373
|
+
ok(
|
|
374
|
+
"[QK:SEND-SURFACE-RENDERS-BOUNDARY] 2: a BUSY receiver renders its queue by name, as a success — and never the word `sent`. The route word answers WHICH LEG carried the bytes; this line has to answer what the receiver did with them, because for 78 minutes it said `sent` about messages lying in a queue",
|
|
375
|
+
!steered.isError &&
|
|
376
|
+
steered.text.endsWith("→ queued-steer") &&
|
|
377
|
+
!followed.isError &&
|
|
378
|
+
followed.text.endsWith("→ queued-follow-up") &&
|
|
379
|
+
!/→ sent/.test(steered.text) &&
|
|
380
|
+
!/→ sent/.test(followed.text),
|
|
381
|
+
);
|
|
382
|
+
const unknown = renderEntwurfV2Result(ctl("sent", "accepted-unknown-boundary"));
|
|
383
|
+
ok(
|
|
384
|
+
"[QK:SEND-SURFACE-UNKNOWN-NOT-SENT] 2: an answer we cannot classify renders `accepted-unknown-boundary` — a success (the receiver accepted; a retry would double-deliver) and NOT spelled with `sent`, which would put the removed false claim straight back",
|
|
385
|
+
!unknown.isError && unknown.text.endsWith("→ accepted-unknown-boundary") && !/sent/.test(unknown.text),
|
|
386
|
+
);
|
|
387
|
+
const fbQueued = renderEntwurfV2Result(ctl("fallback-sent", "queued-steer"));
|
|
346
388
|
ok(
|
|
347
|
-
"2:
|
|
348
|
-
!
|
|
389
|
+
"[QK:SEND-SURFACE-FALLBACK-KEEPS-BOUNDARY] 2: a re-resolved SOCKET keeps the boundary and annotates the route — a dead first socket re-resolving to a BUSY one must not come out reading as a plain delivery",
|
|
390
|
+
!fbQueued.isError && fbQueued.text.includes("queued-steer") && fbQueued.text.includes("re-resolved"),
|
|
391
|
+
);
|
|
392
|
+
const fbMail = renderEntwurfV2Result(ctl("fallback-sent", undefined, "/m/20260903T134455-e55e87/x.msg"));
|
|
393
|
+
ok(
|
|
394
|
+
"2: a re-resolved MAILBOX renders the durable receipt, not a bare route word",
|
|
395
|
+
!fbMail.isError &&
|
|
396
|
+
fbMail.text.includes("mailbox-enqueued") &&
|
|
397
|
+
fbMail.text.includes("enqueued x.msg") &&
|
|
398
|
+
!/fallback-sent/.test(fbMail.text),
|
|
349
399
|
);
|
|
350
400
|
|
|
351
401
|
// control in-band rejected with N3 rejectReason → non-delivery
|
|
@@ -375,7 +425,12 @@ async function main(): Promise<void> {
|
|
|
375
425
|
},
|
|
376
426
|
};
|
|
377
427
|
const fb = renderEntwurfV2Result(fallback);
|
|
378
|
-
|
|
428
|
+
// #120 P2: it is still a delivery, and it is now named by what it actually was — a durable
|
|
429
|
+
// mailbox enqueue reached through a re-resolved route — rather than by the internal leg word.
|
|
430
|
+
ok(
|
|
431
|
+
"2: a re-resolved mailbox delivery is not an error, and says mailbox-enqueued rather than the leg word",
|
|
432
|
+
!fb.isError && fb.text.includes("mailbox-enqueued") && fb.text.includes("re-resolved"),
|
|
433
|
+
);
|
|
379
434
|
ok(
|
|
380
435
|
"2: fallback-sent names the enqueued FILE (#98 R, fallback leg)",
|
|
381
436
|
fb.text.includes("2026-09-03T09-58-02-114Z-ab12cd.msg"),
|
|
@@ -393,12 +448,12 @@ async function main(): Promise<void> {
|
|
|
393
448
|
kind: "executed",
|
|
394
449
|
receipt: { ...SUCCESS_RECEIPT, transport: "control-socket" },
|
|
395
450
|
transport: "control-socket",
|
|
396
|
-
outcome: { transport: "control-socket", outcome: "fallback-sent" },
|
|
451
|
+
outcome: { transport: "control-socket", outcome: "fallback-sent", boundary: "sent" },
|
|
397
452
|
};
|
|
398
453
|
const fbs = renderEntwurfV2Result(fallbackSocket);
|
|
399
454
|
ok(
|
|
400
|
-
"2: socket-retry
|
|
401
|
-
fbs.text === "entwurf_v2 control-socket →
|
|
455
|
+
"2: a socket-to-socket retry names no file (none was written) and still carries its boundary",
|
|
456
|
+
fbs.text === "entwurf_v2 control-socket → sent (via a re-resolved route)" && !fbs.isError,
|
|
402
457
|
);
|
|
403
458
|
|
|
404
459
|
// The two spawn-bg render cells (lock-retained / socket-alive) were deleted with the
|
|
@@ -489,6 +544,53 @@ async function main(): Promise<void> {
|
|
|
489
544
|
n1r.isError && n1r.text.includes("DIRTY") && n1r.text.includes("do NOT retry"),
|
|
490
545
|
);
|
|
491
546
|
|
|
547
|
+
// #120 P2 — A DIRTY LOCK IS A REASON TO BE MORE PRECISE, NOT LESS. This line used to read
|
|
548
|
+
// `DELIVERED (<route>)` for all three shapes that reach it, and it was wrong about two: a
|
|
549
|
+
// `rejected` finalization delivered nothing at all, and a `queued-*` one is sitting in
|
|
550
|
+
// volatile receiver memory. The operator reading it is about to decide BY HAND whether
|
|
551
|
+
// anything still needs sending, which is the worst possible moment for the one word on
|
|
552
|
+
// screen to be the internal route.
|
|
553
|
+
const dirty = (
|
|
554
|
+
finalizedOutcome: "sent" | "fallback-sent" | "rejected",
|
|
555
|
+
finalizedBoundary?: "sent" | "queued-steer" | "queued-follow-up" | "accepted-unknown-boundary",
|
|
556
|
+
finalizedMessagePath?: string,
|
|
557
|
+
finalizedRejectReason?: string,
|
|
558
|
+
): { text: string; isError: boolean } =>
|
|
559
|
+
renderEntwurfV2Result({
|
|
560
|
+
kind: "execution-failed",
|
|
561
|
+
receipt: SUCCESS_RECEIPT,
|
|
562
|
+
transport: "control-socket",
|
|
563
|
+
error: "release boom",
|
|
564
|
+
finalizedOutcome,
|
|
565
|
+
finalizedBoundary,
|
|
566
|
+
finalizedMessagePath,
|
|
567
|
+
finalizedRejectReason,
|
|
568
|
+
releaseFailed: true,
|
|
569
|
+
retrySafe: false,
|
|
570
|
+
});
|
|
571
|
+
const dirtyBusy = dirty("sent", "queued-steer");
|
|
572
|
+
const dirtyMailbox = dirty("fallback-sent", undefined, "/m/20260903T134455-e55e87/x.msg");
|
|
573
|
+
const dirtyRefused = dirty("rejected", undefined, undefined, "compacting");
|
|
574
|
+
ok(
|
|
575
|
+
"[QK:SEND-DIRTY-LOCK-NAMES-ACCEPTANCE] a release failure after a terminal send names the ACCEPTANCE it reached — the receiver's queue, the enqueued file, or the refusal — and never a bare `DELIVERED (<route>)`, while the retry-unsafe dirty-lock meaning stays intact on all three",
|
|
576
|
+
// the queue, by name, and not the word delivered
|
|
577
|
+
dirtyBusy.text.includes("queued-steer") &&
|
|
578
|
+
!/DELIVERED|\bdelivered\b/.test(dirtyBusy.text) &&
|
|
579
|
+
// the mailbox leg names its durable file rather than a route word
|
|
580
|
+
dirtyMailbox.text.includes("mailbox-enqueued") &&
|
|
581
|
+
dirtyMailbox.text.includes("enqueued x.msg") &&
|
|
582
|
+
!/DELIVERED|\bdelivered\b/.test(dirtyMailbox.text) &&
|
|
583
|
+
// a REFUSAL is never dressed as a delivery
|
|
584
|
+
dirtyRefused.text.includes("rejected") &&
|
|
585
|
+
// the receiver's OWN reason survives to the line the operator acts on
|
|
586
|
+
dirtyRefused.text.includes("compacting") &&
|
|
587
|
+
!/DELIVERED|\bdelivered\b/.test(dirtyRefused.text) &&
|
|
588
|
+
// and every one of them keeps the thing the operator must act on
|
|
589
|
+
[dirtyBusy, dirtyMailbox, dirtyRefused].every(
|
|
590
|
+
(r) => r.isError && r.text.includes("DIRTY") && r.text.includes("do NOT retry"),
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
|
|
492
594
|
// plain execution-failed → error
|
|
493
595
|
const failed: EntwurfV2RunResult = {
|
|
494
596
|
kind: "execution-failed",
|
|
@@ -624,6 +726,25 @@ async function main(): Promise<void> {
|
|
|
624
726
|
"4: pi-native — NO static import of the self-address fence (TS5097 stays closed)",
|
|
625
727
|
!/import[^;]*from\s*"\.\/lib\/entwurf-self-address\.(js|ts)"/.test(code),
|
|
626
728
|
);
|
|
729
|
+
// #120 P2: the RECEIVER's own answer. The sender can only render a boundary the receiver
|
|
730
|
+
// actually names, so this is the far end of the same contract — and the bare `delivered`
|
|
731
|
+
// it replaced is the one DELIVERY.md refuses by name, because it was equally true of a
|
|
732
|
+
// direct trigger and of a message rotting in a queue nobody drains.
|
|
733
|
+
ok(
|
|
734
|
+
"[QK:SEND-RECEIVER-EMITS-BOUNDARY] the control-socket `send` answers with the typed boundary its own state produces, and with no bare `delivered` / unconsumed `deliveredAs` beside it — a field that is true of every outcome tells a sender nothing, and there is no sender left that reads those two",
|
|
735
|
+
(() => {
|
|
736
|
+
// Scoped to the RPC ANSWER, not the whole file: `deliveredAs` also appears in the
|
|
737
|
+
// (separately owned, #120 Part 2) sent-message renderer, and a whole-file ban here
|
|
738
|
+
// would make this claim fail for a reason it is not about.
|
|
739
|
+
const at = code.indexOf('respond(true, "send"');
|
|
740
|
+
const answer = at < 0 ? "" : code.slice(at, code.indexOf(");", at));
|
|
741
|
+
return (
|
|
742
|
+
/boundary:\s*sendReceiptBoundary\(\{\s*idle:\s*isIdle,\s*mode\s*\}\)/.test(answer) &&
|
|
743
|
+
!/\bdelivered:\s*true/.test(answer) &&
|
|
744
|
+
!/\bdeliveredAs\b/.test(answer)
|
|
745
|
+
);
|
|
746
|
+
})(),
|
|
747
|
+
);
|
|
627
748
|
// Every SHIPPED rail must appear in the model-facing text. Measured 2026-07-27: both
|
|
628
749
|
// surfaces described only control-socket/mailbox and told the model that an
|
|
629
750
|
// `unsupported` citizen is reached "→ mailbox" — false for Antigravity, whose native-push
|
|
@@ -847,6 +968,164 @@ async function main(): Promise<void> {
|
|
|
847
968
|
);
|
|
848
969
|
}
|
|
849
970
|
|
|
971
|
+
// ── #120 P3: a renderer nobody can produce for, and the retired surface it drew ──
|
|
972
|
+
// PARSED, NOT GREPPED. The defect being closed here is a renderer registered for a customType
|
|
973
|
+
// that no code emits — and the thing that hid it for two releases was PROSE: `protocol.js` said
|
|
974
|
+
// "the bridge emits it", `entwurf-control.ts` named a producer in `streamShellAcp`, and neither
|
|
975
|
+
// was true. A regex oracle would have been satisfied by exactly those sentences. So the
|
|
976
|
+
// registration and the producer are both read out of the TypeScript AST: a `registerMessageRenderer`
|
|
977
|
+
// CALL, and an object literal property `customType` whose value resolves to the same string.
|
|
978
|
+
//
|
|
979
|
+
// SCOPE, stated: this is the pi-native file's own contract — registrations here must have a
|
|
980
|
+
// producer here. A future cross-file producer is a reason to widen this gate WITH the evidence,
|
|
981
|
+
// never a reason to let a comment satisfy it.
|
|
982
|
+
{
|
|
983
|
+
const ts = (await import("typescript")).default;
|
|
984
|
+
const file = path.join(REPO, "pi-extensions", "entwurf-control.ts");
|
|
985
|
+
const src = ts.createSourceFile(file, await fs.readFile(file, "utf8"), ts.ScriptTarget.Latest, true);
|
|
986
|
+
|
|
987
|
+
// `const X = "literal"` — the only indirection a customType is allowed to take here.
|
|
988
|
+
const consts = new Map<string, string>();
|
|
989
|
+
const visitConsts = (node: import("typescript").Node): void => {
|
|
990
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer !== undefined) {
|
|
991
|
+
if (ts.isStringLiteral(node.initializer)) consts.set(node.name.text, node.initializer.text);
|
|
992
|
+
}
|
|
993
|
+
ts.forEachChild(node, visitConsts);
|
|
994
|
+
};
|
|
995
|
+
visitConsts(src);
|
|
996
|
+
// Imported constants resolve through `protocol.js`, which is a string-literal module too.
|
|
997
|
+
const protocolSrc = ts.createSourceFile(
|
|
998
|
+
"protocol.js",
|
|
999
|
+
await fs.readFile(path.join(REPO, "protocol.js"), "utf8"),
|
|
1000
|
+
ts.ScriptTarget.Latest,
|
|
1001
|
+
true,
|
|
1002
|
+
);
|
|
1003
|
+
visitConstsInto(protocolSrc, consts, ts);
|
|
1004
|
+
|
|
1005
|
+
const resolve = (expr: import("typescript").Expression): string | null => {
|
|
1006
|
+
if (ts.isStringLiteral(expr)) return expr.text;
|
|
1007
|
+
if (ts.isIdentifier(expr)) return consts.get(expr.text) ?? null;
|
|
1008
|
+
return null;
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const registered: { type: string | null; text: string }[] = [];
|
|
1012
|
+
const produced = new Set<string>();
|
|
1013
|
+
const visit = (node: import("typescript").Node): void => {
|
|
1014
|
+
if (
|
|
1015
|
+
ts.isCallExpression(node) &&
|
|
1016
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
1017
|
+
node.expression.name.text === "registerMessageRenderer" &&
|
|
1018
|
+
node.arguments.length > 0
|
|
1019
|
+
) {
|
|
1020
|
+
registered.push({ type: resolve(node.arguments[0]), text: node.arguments[0].getText(src) });
|
|
1021
|
+
}
|
|
1022
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "customType") {
|
|
1023
|
+
const value = resolve(node.initializer);
|
|
1024
|
+
if (value !== null) produced.add(value);
|
|
1025
|
+
}
|
|
1026
|
+
ts.forEachChild(node, visit);
|
|
1027
|
+
};
|
|
1028
|
+
visit(src);
|
|
1029
|
+
|
|
1030
|
+
const orphans = registered.filter((r) => r.type === null || !produced.has(r.type));
|
|
1031
|
+
ok(
|
|
1032
|
+
`[QK:NO-RENDERER-WITHOUT-PRODUCER] every message renderer this extension registers is registered for a customType this extension actually EMITS — read from the AST, because the producer-less one that prompted this claim was covered by two comments naming producers that do not exist. A renderer nobody can reach is a box the operator will never see and a maintenance surface that answers no question (registered ${JSON.stringify(registered.map((r) => r.text))}, produced ${JSON.stringify([...produced])}, orphans ${JSON.stringify(orphans.map((o) => o.text))})`,
|
|
1033
|
+
registered.length > 0 && orphans.length === 0,
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
// The retired surface itself, sealed in the two LIVE files it lived in. History keeps its
|
|
1037
|
+
// own record — CHANGELOG, ROADMAP and the demo note are deliberately NOT scanned.
|
|
1038
|
+
// The PHRASES too, not just the identifiers: what kept this surface alive through two
|
|
1039
|
+
// releases was prose — a comment promising "the [entwurf sent →] box exists for every
|
|
1040
|
+
// session that DOES send" outlived the code by one removal and would have been read as a
|
|
1041
|
+
// live guarantee. Sealing the identifiers alone leaves the lie behind.
|
|
1042
|
+
const retired =
|
|
1043
|
+
/ENTWURF_SENT_MESSAGE_TYPE|entwurf-sent|renderSentMessage|buildSentMessageBox|SentBoxData|entwurf sent|sender-side UI box/i;
|
|
1044
|
+
const live = [
|
|
1045
|
+
{ rel: "protocol.js", text: await fs.readFile(path.join(REPO, "protocol.js"), "utf8") },
|
|
1046
|
+
{ rel: "pi-extensions/entwurf-control.ts", text: await fs.readFile(file, "utf8") },
|
|
1047
|
+
];
|
|
1048
|
+
const survivors = live.filter((f) => retired.test(f.text)).map((f) => f.rel);
|
|
1049
|
+
ok(
|
|
1050
|
+
`[QK:RETIRED-SENT-SURFACE-ABSENT] the sender-side echo surface is gone from the live bytes — no constant, no renderer, no box builder, no payload type — rather than left as a public export nothing produces. It was shipped in \`files\`, so an unreachable name there is a promise to a consumer that no code could ever keep (survivors: ${JSON.stringify(survivors)})`,
|
|
1051
|
+
survivors.length === 0,
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ── #120 P2 (D8): the OUTCOME list in both tool descriptions names every rail ──
|
|
1056
|
+
// A caller reads one of these two blurbs and nothing else before dispatching. The control
|
|
1057
|
+
// rail's four boundaries are the ONLY receiver-acceptance values; a mailbox answers with an
|
|
1058
|
+
// ENQUEUE receipt and native-push with an INJECTION, and folding those into the boundary union
|
|
1059
|
+
// would tell a caller that `mailbox-enqueued` is something a receiver observed about its own
|
|
1060
|
+
// queues. Missing either rail is red — the earlier draft listed `mailbox-enqueued` beside the
|
|
1061
|
+
// four and left native-push out entirely, and nothing noticed.
|
|
1062
|
+
{
|
|
1063
|
+
const outcomeSites = ["mcp/entwurf-bridge/src/index.ts", "pi-extensions/entwurf-control.ts"];
|
|
1064
|
+
const missing: string[] = [];
|
|
1065
|
+
for (const rel of outcomeSites) {
|
|
1066
|
+
const text = await fs.readFile(path.join(REPO, rel), "utf8");
|
|
1067
|
+
const at = text.indexOf("reports ONE");
|
|
1068
|
+
const blurb = at < 0 ? "" : text.slice(at, at + 900);
|
|
1069
|
+
if (!/acceptance boundary/i.test(blurb)) missing.push(`${rel}: control receiver acceptance boundary`);
|
|
1070
|
+
for (const b of ["sent", "queued-steer", "queued-follow-up", "accepted-unknown-boundary"]) {
|
|
1071
|
+
if (!blurb.includes(b)) missing.push(`${rel}: boundary ${b}`);
|
|
1072
|
+
}
|
|
1073
|
+
if (!/those four only/i.test(blurb)) missing.push(`${rel}: the four-only scope of the boundary union`);
|
|
1074
|
+
if (!/mailbox ENQUEUE/i.test(blurb)) missing.push(`${rel}: mailbox enqueue receipt`);
|
|
1075
|
+
if (!/native-push INJECTION/i.test(blurb)) missing.push(`${rel}: native-push injection`);
|
|
1076
|
+
if (!/rejection|rejected/i.test(blurb)) missing.push(`${rel}: rejection`);
|
|
1077
|
+
if (!/lock-dirty/i.test(blurb)) missing.push(`${rel}: accepted-but-lock-dirty`);
|
|
1078
|
+
if (/mailbox-enqueued`? \/|\/ `?mailbox-enqueued/i.test(blurb)) {
|
|
1079
|
+
missing.push(`${rel}: mailbox-enqueued is listed INSIDE the receiver-boundary union`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
ok(
|
|
1083
|
+
`[QK:SEND-OUTCOME-LIST-NAMES-EVERY-RAIL] both tool descriptions name each rail's own result — the control receiver's four acceptance boundaries (and that those four are the whole union), the mailbox ENQUEUE receipt, the native-push INJECTION, a rejection, and accepted-but-lock-dirty — so a caller is never told that one rail's vocabulary describes another's (missing: ${JSON.stringify(missing)})`,
|
|
1084
|
+
missing.length === 0,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ── #120 P2: the four caller-facing prose sites, checked for what they SAY ──
|
|
1089
|
+
// `[측정 2026-09-22, pi-agent-core 0.87.0; 재측정 2026-09-23, 0.87.1 — 좌표 동일]` neither mode interrupts anything: `agent-loop.js:186`
|
|
1090
|
+
// drains steering after a turn ENDS and `:191-197` drains follow-ups only once the inner loop
|
|
1091
|
+
// has ended. A caller who reads "interrupt the current turn" picks `steer` for urgency and gets
|
|
1092
|
+
// a queue with no order against the other one — the decision #120 was opened about.
|
|
1093
|
+
//
|
|
1094
|
+
// POSITIVE, not merely a banned-word sweep. The first cut of this claim only looked for the old
|
|
1095
|
+
// wording, which meant a site could pass by deleting its description entirely — the assertion
|
|
1096
|
+
// would have said four sites explain the queues while one of them explained nothing. Each site
|
|
1097
|
+
// now has to carry all four facts a caller needs before choosing a mode, and the old wording is
|
|
1098
|
+
// still banned beside them.
|
|
1099
|
+
{
|
|
1100
|
+
const sites = [
|
|
1101
|
+
"mcp/entwurf-bridge/src/index.ts",
|
|
1102
|
+
"pi-extensions/entwurf-control.ts",
|
|
1103
|
+
"pi-extensions/lib/entwurf-v2-contract.ts",
|
|
1104
|
+
"pi-extensions/lib/entwurf-v2-contract-schema.ts",
|
|
1105
|
+
];
|
|
1106
|
+
const axes: { name: string; holds: (t: string) => boolean }[] = [
|
|
1107
|
+
{ name: "names both queues", holds: (t) => /steering/i.test(t) && /follow-?ups?\b/i.test(t) },
|
|
1108
|
+
{ name: "says neither is an interrupt", holds: (t) => /(neither|not)\s+(is\s+)?an?\s+interrupt/i.test(t) },
|
|
1109
|
+
{ name: "says a later steer overtakes an earlier follow_up", holds: (t) => /overtakes/i.test(t) },
|
|
1110
|
+
{
|
|
1111
|
+
name: "says the queues are volatile and an abort drops them",
|
|
1112
|
+
holds: (t) => /volatile/i.test(t) && /abort/i.test(t),
|
|
1113
|
+
},
|
|
1114
|
+
];
|
|
1115
|
+
const gaps: string[] = [];
|
|
1116
|
+
for (const rel of sites) {
|
|
1117
|
+
const text = await fs.readFile(path.join(REPO, rel), "utf8");
|
|
1118
|
+
for (const axis of axes) if (!axis.holds(text)) gaps.push(`${rel}: ${axis.name}`);
|
|
1119
|
+
if (/steer \((?:interrupt|immediate)|interrupt the current turn|steer = interrupt/i.test(text)) {
|
|
1120
|
+
gaps.push(`${rel}: still claims steer interrupts`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
ok(
|
|
1124
|
+
`[QK:SEND-PROSE-NO-INTERRUPT] every caller-facing description of \`mode\` names BOTH queues, says neither is an interrupt, says a later steer overtakes an earlier follow_up, and says both are volatile so an abort drops them — and none of them still claims an interrupt. A caller chooses a mode from this text alone (gaps: ${JSON.stringify(gaps)})`,
|
|
1125
|
+
gaps.length === 0,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
850
1129
|
console.log(`\ncheck-entwurf-v2-surface: ${passed} checks passed`);
|
|
851
1130
|
}
|
|
852
1131
|
|
|
@@ -832,23 +832,25 @@ let manifestCount: number;
|
|
|
832
832
|
"acp-usage-accounting": 12,
|
|
833
833
|
"agy-permission": 6,
|
|
834
834
|
"bridge-boot-resume": 5,
|
|
835
|
-
"bridge-command-boot":
|
|
835
|
+
"bridge-command-boot": 12,
|
|
836
836
|
"capability-cache": 3,
|
|
837
837
|
"codex-app-server-launch": 9,
|
|
838
838
|
"codex-caller-seat": 28,
|
|
839
|
+
"control-send-receipt": 19,
|
|
839
840
|
"codex-native": 82,
|
|
840
841
|
"compaction-send-guard": 7,
|
|
841
842
|
"control-socket-disconnect": 4,
|
|
842
843
|
"copilot-birth": 19,
|
|
843
844
|
"copilot-launch": 14,
|
|
844
845
|
"pi-launch": 9,
|
|
846
|
+
"pi-floor": 1,
|
|
845
847
|
"copilot-receive": 20,
|
|
846
848
|
"entwurf-peers": 1,
|
|
847
849
|
"fresh-call-dispatch": 12,
|
|
848
850
|
"fresh-cut": 3,
|
|
849
851
|
"gate-qualification": 2,
|
|
850
852
|
"herdr-placement": 13,
|
|
851
|
-
"herdr-plugin":
|
|
853
|
+
"herdr-plugin": 25,
|
|
852
854
|
"herdr-plugin-profile": 14,
|
|
853
855
|
"herdr-activation": 25,
|
|
854
856
|
"herdr-plugin-build": 13,
|
|
@@ -859,7 +861,7 @@ let manifestCount: number;
|
|
|
859
861
|
"peer-facts": 8,
|
|
860
862
|
"meta-hook-session-switch": 17,
|
|
861
863
|
"meta-identity": 4,
|
|
862
|
-
"meta-retire":
|
|
864
|
+
"meta-retire": 4,
|
|
863
865
|
"mux-boundary": 16,
|
|
864
866
|
"mux-fresh-call": 59,
|
|
865
867
|
"mux-launcher-fence": 8,
|
|
@@ -871,7 +873,8 @@ let manifestCount: number;
|
|
|
871
873
|
"omp-fresh": 24,
|
|
872
874
|
"omp-receive": 11,
|
|
873
875
|
"probe-ordering": 1,
|
|
874
|
-
"release-gate":
|
|
876
|
+
"release-gate": 22,
|
|
877
|
+
"retired-sent-surface": 2,
|
|
875
878
|
"resume-args": 6,
|
|
876
879
|
"resume-launch-identity": 6,
|
|
877
880
|
"self-address": 8,
|
|
@@ -344,10 +344,18 @@ async function main(): Promise<void> {
|
|
|
344
344
|
const code = MODULE_SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
345
345
|
const specifiers = [...MODULE_SRC.matchAll(/^import\s[^;]*?from\s+"([^"]+)";$/gm)].map((m) => m[1]);
|
|
346
346
|
ok(
|
|
347
|
-
"[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins, the neutral composition leaf,
|
|
347
|
+
"[QK:HFC-RAIL-IMPORT-FENCE] the herdr rail imports only node builtins, the neutral composition leaf, the callback-env pair, and the dependency-free rails DECLARATION leaf — one mux or entwurf import here would re-couple the two rails and make either one undeletable",
|
|
348
348
|
specifiers.length > 0 &&
|
|
349
349
|
specifiers.every(
|
|
350
|
-
(s) =>
|
|
350
|
+
(s) =>
|
|
351
|
+
s.startsWith("node:") ||
|
|
352
|
+
s === "./fresh-call-composition.ts" ||
|
|
353
|
+
s === "./callback-env.ts" ||
|
|
354
|
+
// #116 A7 — the ONE place the backend tuple and the per-backend model syntax
|
|
355
|
+
// example are written, shared with the status pane and the install narration.
|
|
356
|
+
// It imports nothing itself, so admitting it couples this rail to a constant
|
|
357
|
+
// rather than to another rail; the mux/entwurf ban below is untouched.
|
|
358
|
+
s === "../../scripts/herdr-rails.mjs",
|
|
351
359
|
) &&
|
|
352
360
|
!/from\s+"\.\/(mux-|entwurf-)/.test(code),
|
|
353
361
|
);
|
|
@@ -165,6 +165,57 @@ async function main(): Promise<void> {
|
|
|
165
165
|
piNativeSessionIdFromPath("/s/2026-09-14T05-17-03-979Z_not-a-uuid.jsonl") === null &&
|
|
166
166
|
piNativeSessionIdFromPath(`/s/${PI_NATIVE}.jsonl`) === null,
|
|
167
167
|
);
|
|
168
|
+
{
|
|
169
|
+
// THE VENDOR HALF OF THAT CLAIM, FROM THE INSTALLED VENDOR — not a literal we typed.
|
|
170
|
+
// The key this parser recovers lives inside a filename pi builds, so the rule is only as
|
|
171
|
+
// true as pi's naming, and a source read cannot settle whether that naming is a contract
|
|
172
|
+
// or a convention. `[측정 2026-09-22, @earendil-works/pi-coding-agent 0.87.0]` the installed
|
|
173
|
+
// `SessionManager` writes a REAL session file here, in a temp dir, with zero model, zero
|
|
174
|
+
// network and zero child process: `create()` then one `appendMessage` flushes
|
|
175
|
+
// `<stamp>_<id>.jsonl` whose own `{"type":"session"}` header carries the same id.
|
|
176
|
+
//
|
|
177
|
+
// The 0.86.0 receipt this replaces needed a provider call and was therefore taken by hand
|
|
178
|
+
// once; it stays in the bump ledger as a 0.86.0 fact. This one re-takes itself on every
|
|
179
|
+
// run, so the vendor floor cannot drift under us silently again.
|
|
180
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hp-vendor-"));
|
|
181
|
+
try {
|
|
182
|
+
const { SessionManager } = await import("@earendil-works/pi-coding-agent");
|
|
183
|
+
const manager = await SessionManager.create(process.cwd(), dir);
|
|
184
|
+
await manager.appendMessage({
|
|
185
|
+
role: "assistant",
|
|
186
|
+
content: [{ type: "text", text: "placement vendor-floor probe" }],
|
|
187
|
+
api: "test",
|
|
188
|
+
provider: "test",
|
|
189
|
+
model: "test",
|
|
190
|
+
// Zero everything: this message exists to make the manager flush a file, and a
|
|
191
|
+
// fixture that invented usage numbers would be inventing vendor accounting too.
|
|
192
|
+
usage: {
|
|
193
|
+
input: 0,
|
|
194
|
+
output: 0,
|
|
195
|
+
cacheRead: 0,
|
|
196
|
+
cacheWrite: 0,
|
|
197
|
+
totalTokens: 0,
|
|
198
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
199
|
+
},
|
|
200
|
+
stopReason: "stop",
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
const written = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
204
|
+
const header = JSON.parse(readFileSync(path.join(dir, written[0]), "utf8").split("\n")[0]);
|
|
205
|
+
const recovered = piNativeSessionIdFromPath(path.join(dir, written[0]));
|
|
206
|
+
ok(
|
|
207
|
+
"the vendor floor is re-measured, not remembered: the installed pi writes a real session file whose " +
|
|
208
|
+
"basename this parser reads back to the same id its own header declares",
|
|
209
|
+
written.length === 1 &&
|
|
210
|
+
header.type === "session" &&
|
|
211
|
+
typeof header.id === "string" &&
|
|
212
|
+
recovered === header.id &&
|
|
213
|
+
recovered === manager.getSessionId(),
|
|
214
|
+
);
|
|
215
|
+
} finally {
|
|
216
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
168
219
|
ok(
|
|
169
220
|
"[QK:HP-JOIN-NEVER-GUESSES] an unmeasured agent_session kind and an empty value both yield NO key — an " +
|
|
170
221
|
"unknown herdr shape is declined, never coerced into one of the two rules we actually measured",
|
|
@@ -33,6 +33,7 @@ import fs from "node:fs";
|
|
|
33
33
|
import os from "node:os";
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { pathToFileURL } from "node:url";
|
|
36
|
+
import * as HERDR_RAILS from "./herdr-rails.mjs";
|
|
36
37
|
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
37
38
|
import { treeDigest } from "./lib/tree-digest.ts";
|
|
38
39
|
|
|
@@ -769,17 +770,23 @@ function drive(
|
|
|
769
770
|
// contains `claude-sonnet-5`, so a containment test cannot tell the two grammars apart —
|
|
770
771
|
// which is the whole claim.
|
|
771
772
|
const idIn = (lines: string[], backend: string): string | null => {
|
|
772
|
-
const line = lines.find((l) => l.startsWith(`note: ${backend}: `) && l.includes("model
|
|
773
|
+
const line = lines.find((l) => l.startsWith(`note: ${backend}: `) && l.includes("model string looks like"));
|
|
773
774
|
return line === undefined ? null : (/`([^`]+)`/.exec(line)?.[1] ?? null);
|
|
774
775
|
};
|
|
775
776
|
const piId = idIn(notesOf(full), "pi");
|
|
776
777
|
const claudeId = idIn(notesOf(full), "claude-code");
|
|
777
778
|
return (
|
|
778
|
-
|
|
779
|
+
// Compared against the SHARED leaf, not a literal copied into this gate: the install
|
|
780
|
+
// narration and the status pane's RAILS block now read one declaration
|
|
781
|
+
// (`scripts/herdr-rails.mjs`, #116 A7), and a gate holding its own third copy would
|
|
782
|
+
// be the very drift that leaf exists to stop. The GRAMMAR assertions stay literal —
|
|
783
|
+
// provider-qualified vs bare vendor id is the fact, and it must not become whatever
|
|
784
|
+
// the leaf happens to say.
|
|
785
|
+
piId === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE.pi &&
|
|
779
786
|
piId.includes("/") &&
|
|
780
|
-
claudeId === "claude-
|
|
787
|
+
claudeId === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE["claude-code"] &&
|
|
781
788
|
!claudeId.includes("/") &&
|
|
782
|
-
idIn(notesOf(piOnly), "pi") ===
|
|
789
|
+
idIn(notesOf(piOnly), "pi") === HERDR_RAILS.MODEL_SYNTAX_EXAMPLE.pi &&
|
|
783
790
|
idIn(notesOf(piOnly), "claude-code") === null
|
|
784
791
|
);
|
|
785
792
|
})(),
|