@junghanacs/entwurf 0.25.1 → 0.30.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 +1 -1
- package/BASELINE.md +1 -1
- package/CHANGELOG.md +73 -0
- package/DELIVERY.md +1 -1
- package/README.md +178 -674
- package/VERIFY.md +16 -15
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +6 -2
- package/docs/external-mcp-host.md +1 -1
- package/docs/setup-clean-host.md +346 -10
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +12 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
- package/mcp/entwurf-bridge/src/index.ts +30 -3
- package/package.json +11 -11
- package/pi-extensions/entwurf-control.ts +50 -637
- package/pi-extensions/lib/acp/backend-adapter.ts +13 -1
- package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
- package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
- package/pi-extensions/lib/fresh-call-composition.ts +13 -6
- package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -3
- package/run.sh +163 -50
- package/scripts/check-entwurf-peers-surface.ts +8 -15
- package/scripts/check-entwurf-self-address.ts +7 -11
- package/scripts/check-entwurf-v2-surface.ts +20 -112
- package/scripts/check-fresh-call-dispatch.ts +12 -13
- package/scripts/check-gate-qualification.ts +248 -4
- package/scripts/check-install-surface.ts +3 -2
- package/scripts/check-mux-resume-call.ts +24 -35
- package/scripts/check-pi-bridge-sender.ts +130 -0
- package/scripts/check-pi-mcp-bridge.ts +251 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
- package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
- package/scripts/lib/mutation-qualify.ts +269 -44
- package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
- package/scripts/meta-bridge-doctor.sh +16 -5
- package/scripts/mutants/acp-foreground-only.json +17 -0
- package/scripts/mutants/codex-native.json +11 -11
- package/scripts/mutants/control-send-receipt.json +6 -4
- package/scripts/mutants/copilot-birth.json +2 -1
- package/scripts/mutants/fresh-call-dispatch.json +3 -14
- package/scripts/mutants/gate-qualification.json +59 -0
- package/scripts/mutants/mux-fresh-call.json +43 -45
- package/scripts/mutants/mux-resume-call.json +11 -35
- package/scripts/mutants/pack-install.json +15 -4
- package/scripts/mutants/pi-bridge-sender.json +54 -0
- package/scripts/mutants/pi-floor.json +6 -2
- package/scripts/mutants/pi-mcp-bridge.json +36 -0
- package/scripts/mutants/pi-mcp-register.json +51 -0
- package/scripts/mutants/v2-surface.json +6 -6
- package/scripts/resident-rpc-drive.ts +5 -1
- package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
- package/scripts/smoke-acp-cortex-live.ts +3 -1
- package/scripts/smoke-acp-session-reuse-live.ts +29 -12
- package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
- package/scripts/smoke-acp-v2-send-live.ts +3 -1
- package/scripts/smoke-codex-fresh-live.ts +20 -1
- package/scripts/smoke-entwurf-chain-live.ts +6 -2
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- package/scripts/smoke-resident-garden-guard.sh +3 -1
|
@@ -142,7 +142,7 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
|
|
|
142
142
|
}
|
|
143
143
|
ok(`${tag} — mode param is isolated (non-vacuous)`, modeDesc.length > 60);
|
|
144
144
|
ok(
|
|
145
|
-
`${tag} — mode param scopes itself to a CONTROL-SOCKET send and denies the other rails [QK:V2SURF-
|
|
145
|
+
`${tag} — mode param scopes itself to a CONTROL-SOCKET send and denies the other rails [QK:V2SURF-MODE-SCOPE]`,
|
|
146
146
|
/CONTROL-SOCKET send/.test(modeDesc) && /no mode/.test(modeDesc),
|
|
147
147
|
);
|
|
148
148
|
ok(
|
|
@@ -692,39 +692,21 @@ async function main(): Promise<void> {
|
|
|
692
692
|
ok("3: surface is ctx-free — no ExtensionAPI", !code.includes("ExtensionAPI"));
|
|
693
693
|
}
|
|
694
694
|
|
|
695
|
-
// ── 4: pi-native control
|
|
695
|
+
// ── 4: pi-native control surface — the RECEIVER half, and no second caller route ──
|
|
696
|
+
// #125: the Entwurf verbs are no longer native pi tools. A pi session reaches them through Pi's
|
|
697
|
+
// built-in MCP as `mcp__entwurf_bridge__<verb>` — the SAME bridge §5 certifies — so the pi-native
|
|
698
|
+
// copies of §5's model-facing checks (rail semantics, dormant honesty, host cap, peers dead row)
|
|
699
|
+
// and the fence/senderProvider wiring they needed are retired with them, not re-proved against a
|
|
700
|
+
// file that no longer has them. What stays here is what this file still owns: the control
|
|
701
|
+
// socket's RECEIVER answer, and the absence of a second caller route.
|
|
696
702
|
{
|
|
697
703
|
const src = await fs.readFile(CONTROL_SRC, "utf8");
|
|
698
704
|
const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
699
|
-
|
|
705
|
+
// Hard Rule 1: one surface name, no dual route. A native registration of any Entwurf verb
|
|
706
|
+
// beside the MCP one would put the bare and the prefixed name in the same session.
|
|
700
707
|
ok(
|
|
701
|
-
"4: pi-native —
|
|
702
|
-
|
|
703
|
-
);
|
|
704
|
-
// The whole point of the dynamic import: NO static import of the fence v2 chain into the
|
|
705
|
-
// emit-capable root program (those literal `.ts` imports would be TS5097).
|
|
706
|
-
ok(
|
|
707
|
-
"4: pi-native — NO static import of the v2 fence (runner/production/surface) — TS5097 stays closed",
|
|
708
|
-
!/import[^;]*from\s*"\.\/lib\/entwurf-v2-(runner|production|surface)\.(js|ts)"/.test(code),
|
|
709
|
-
);
|
|
710
|
-
// SE-1 2e-a: senderProvider decorates origin:'pi-session' but `replyable` is now an
|
|
711
|
-
// HONEST fact (canonical socket existsSync via computeSelfAddressability), NOT a
|
|
712
|
-
// hardcoded true. The self-address fence lib is reached through the same non-literal
|
|
713
|
-
// dynamic import pattern as the v2 surface.
|
|
714
|
-
ok(
|
|
715
|
-
"4: pi-native — senderProvider decorates origin:'pi-session' (no hardcoded replyable:true)",
|
|
716
|
-
/origin:\s*"pi-session"/.test(code) && !/replyable:\s*true/.test(code),
|
|
717
|
-
);
|
|
718
|
-
ok(
|
|
719
|
-
"4: pi-native — replyability via computeSelfAddressability + existsSync, dynamic-imported",
|
|
720
|
-
/const ENTWURF_SELF_ADDRESS_MODULE\s*=/.test(code) &&
|
|
721
|
-
/await import\(ENTWURF_SELF_ADDRESS_MODULE\)/.test(code) &&
|
|
722
|
-
/computeSelfAddressability/.test(code) &&
|
|
723
|
-
/existsSync\(/.test(code),
|
|
724
|
-
);
|
|
725
|
-
ok(
|
|
726
|
-
"4: pi-native — NO static import of the self-address fence (TS5097 stays closed)",
|
|
727
|
-
!/import[^;]*from\s*"\.\/lib\/entwurf-self-address\.(js|ts)"/.test(code),
|
|
708
|
+
"4: pi-native — entwurf-control registers NO native tool (the verbs live on the one MCP bridge, #125)",
|
|
709
|
+
!/\bregisterTool\s*\(/.test(code) && !/name:\s*"entwurf_/.test(code),
|
|
728
710
|
);
|
|
729
711
|
// #120 P2: the RECEIVER's own answer. The sender can only render a boundary the receiver
|
|
730
712
|
// actually names, so this is the far end of the same contract — and the bare `delivered`
|
|
@@ -745,82 +727,6 @@ async function main(): Promise<void> {
|
|
|
745
727
|
);
|
|
746
728
|
})(),
|
|
747
729
|
);
|
|
748
|
-
// Every SHIPPED rail must appear in the model-facing text. Measured 2026-07-27: both
|
|
749
|
-
// surfaces described only control-socket/mailbox and told the model that an
|
|
750
|
-
// `unsupported` citizen is reached "→ mailbox" — false for Antigravity, whose native-push
|
|
751
|
-
// rail is intercepted BEFORE the mailbox mini-table and has no mailbox at all. A model
|
|
752
|
-
// reading that would pick mailbox semantics for a citizen that has none.
|
|
753
|
-
//
|
|
754
|
-
// Scope this to the registration block, NOT the whole file: the first version of this
|
|
755
|
-
// assertion swept `src`, so deleting the sentence from the description still passed on an
|
|
756
|
-
// unrelated `native-push` string elsewhere in the module. A rail-completeness claim that
|
|
757
|
-
// any import line can satisfy is not a claim.
|
|
758
|
-
const piV2Start = src.indexOf("function registerEntwurfV2Tool");
|
|
759
|
-
const piV2End = src.indexOf("\nfunction ", piV2Start + 1);
|
|
760
|
-
// Require a REAL end marker. `piV2End === -1 ? undefined : …` would silently widen the
|
|
761
|
-
// block to end-of-file and keep passing, which is the opposite of the fail-loud this
|
|
762
|
-
// scope exists for.
|
|
763
|
-
ok(
|
|
764
|
-
"4: pi-native — registration block is isolated (non-vacuous scope for the description checks)",
|
|
765
|
-
piV2Start !== -1 && piV2End > piV2Start,
|
|
766
|
-
);
|
|
767
|
-
const piV2Block = src.slice(piV2Start, piV2End);
|
|
768
|
-
ok("4: pi-native — registration block is substantial", piV2Block.length > 500);
|
|
769
|
-
// BLOCK SCOPE IS NOT ENOUGH — the two model-facing strings mask each other. Measured
|
|
770
|
-
// 2026-07-27 (round 4): the `intent` param description was corrected to split the two
|
|
771
|
-
// reject reasons while the LONG tool description still advertised the merged, false one,
|
|
772
|
-
// and this gate stayed green at 51 checks because both live in the same block and only
|
|
773
|
-
// ONE had to carry each literal. So slice them apart and require EACH to be true.
|
|
774
|
-
// The start marker MUST be unique to the long description. Measured 2026-07-27 (round 5):
|
|
775
|
-
// a bare `description:` matched the `target` PARAM first, so this "long description"
|
|
776
|
-
// slice was 4,468 chars spanning target + intent + the long text — the very masking the
|
|
777
|
-
// split was added to remove. An over-wide slice is a silent pass, so pin the exact
|
|
778
|
-
// backtick opening instead, and assert the two slices are actually disjoint.
|
|
779
|
-
const piLongDesc = sliceDescription(piV2Block, "description: `CANONICAL", "\n\t\tparameters:");
|
|
780
|
-
const piIntentDesc = sliceDescription(piV2Block, "intent: StringEnum(", "message:");
|
|
781
|
-
ok(
|
|
782
|
-
"4: pi-native — long/intent slices are disjoint (neither can satisfy the other's claim)",
|
|
783
|
-
piLongDesc.length > 0 &&
|
|
784
|
-
piIntentDesc.length > 0 &&
|
|
785
|
-
!piLongDesc.includes(piIntentDesc) &&
|
|
786
|
-
!piIntentDesc.includes(piLongDesc),
|
|
787
|
-
);
|
|
788
|
-
for (const [what, text] of [
|
|
789
|
-
["long tool description", piLongDesc],
|
|
790
|
-
["intent param description", piIntentDesc],
|
|
791
|
-
] as const) {
|
|
792
|
-
ok(`4: pi-native — ${what} is isolated (non-vacuous)`, text.length > 120);
|
|
793
|
-
ok(`4: pi-native — ${what} names the native-push rail`, /native-push/.test(text));
|
|
794
|
-
assertDormantHonesty(`4: pi-native — ${what}`, text);
|
|
795
|
-
}
|
|
796
|
-
ok("4: pi-native — long description denies native-push a mailbox", /NO mailbox/.test(piLongDesc));
|
|
797
|
-
ok(
|
|
798
|
-
"4: pi-native — long description steers live/alive peer → fire-and-forget",
|
|
799
|
-
/liveness=alive/.test(piLongDesc) && /fire-and-forget/.test(piLongDesc),
|
|
800
|
-
);
|
|
801
|
-
assertLongDescExcludesParams("4: pi-native", piLongDesc, [
|
|
802
|
-
"target: Type.String(",
|
|
803
|
-
"intent: StringEnum(",
|
|
804
|
-
"mode: Type.Optional(",
|
|
805
|
-
"wants_reply: Type.Optional(",
|
|
806
|
-
]);
|
|
807
|
-
const piModeDesc = sliceDescription(piV2Block, "mode: Type.Optional(", "wants_reply:");
|
|
808
|
-
assertRailSemantics("4: pi-native", piLongDesc, piIntentDesc, piModeDesc);
|
|
809
|
-
assertDescriptionFitsHostCap("4: pi-native", piLongDesc);
|
|
810
|
-
|
|
811
|
-
// 4b: pi-native entwurf_peers. Asserted on THIS surface too, not only the MCP one: the two
|
|
812
|
-
// descriptions are read by different callers (a resident pi model vs a sibling reaching in
|
|
813
|
-
// over MCP), and a correction that lands on one of them is a correction half the garden
|
|
814
|
-
// never sees. Same real-boundary rule as every other slice here.
|
|
815
|
-
const piPeersStart = src.indexOf('name: "entwurf_peers"');
|
|
816
|
-
const piPeersEnd = src.indexOf("parameters:", piPeersStart + 1);
|
|
817
|
-
ok(
|
|
818
|
-
"4b: pi-native — entwurf_peers description block has a REAL end boundary",
|
|
819
|
-
piPeersStart !== -1 && piPeersEnd > piPeersStart,
|
|
820
|
-
);
|
|
821
|
-
const piPeersBlock = src.slice(piPeersStart, piPeersEnd);
|
|
822
|
-
ok("4b: pi-native — entwurf_peers description is isolated (non-vacuous)", piPeersBlock.length > 200);
|
|
823
|
-
assertPeersDeadRowHonesty("4b: pi-native", piPeersBlock);
|
|
824
730
|
}
|
|
825
731
|
|
|
826
732
|
// ── 5: MCP bridge wiring guard ────────────────────────────────────────────
|
|
@@ -1052,15 +958,16 @@ async function main(): Promise<void> {
|
|
|
1052
958
|
);
|
|
1053
959
|
}
|
|
1054
960
|
|
|
1055
|
-
// ── #120 P2 (D8): the OUTCOME list in
|
|
1056
|
-
// A caller reads
|
|
961
|
+
// ── #120 P2 (D8): the OUTCOME list in the tool description names every rail ──
|
|
962
|
+
// A caller reads this blurb and nothing else before dispatching. (#125: the pi-native copy is
|
|
963
|
+
// retired with the native verbs; a pi session reads the bridge's own blurb.) The control
|
|
1057
964
|
// rail's four boundaries are the ONLY receiver-acceptance values; a mailbox answers with an
|
|
1058
965
|
// ENQUEUE receipt and native-push with an INJECTION, and folding those into the boundary union
|
|
1059
966
|
// would tell a caller that `mailbox-enqueued` is something a receiver observed about its own
|
|
1060
967
|
// queues. Missing either rail is red — the earlier draft listed `mailbox-enqueued` beside the
|
|
1061
968
|
// four and left native-push out entirely, and nothing noticed.
|
|
1062
969
|
{
|
|
1063
|
-
const outcomeSites = ["mcp/entwurf-bridge/src/index.ts"
|
|
970
|
+
const outcomeSites = ["mcp/entwurf-bridge/src/index.ts"];
|
|
1064
971
|
const missing: string[] = [];
|
|
1065
972
|
for (const rel of outcomeSites) {
|
|
1066
973
|
const text = await fs.readFile(path.join(REPO, rel), "utf8");
|
|
@@ -1080,12 +987,14 @@ async function main(): Promise<void> {
|
|
|
1080
987
|
}
|
|
1081
988
|
}
|
|
1082
989
|
ok(
|
|
1083
|
-
`[QK:SEND-OUTCOME-LIST-NAMES-EVERY-RAIL]
|
|
990
|
+
`[QK:SEND-OUTCOME-LIST-NAMES-EVERY-RAIL] the tool description names 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
991
|
missing.length === 0,
|
|
1085
992
|
);
|
|
1086
993
|
}
|
|
1087
994
|
|
|
1088
|
-
// ── #120 P2: the
|
|
995
|
+
// ── #120 P2: the caller-facing prose sites, checked for what they SAY ──
|
|
996
|
+
// (#125: three sites now — the pi-native copy left with the native verbs; a pi session reads
|
|
997
|
+
// the bridge's own `mode` text, which stays the first site below.)
|
|
1089
998
|
// `[측정 2026-09-22, pi-agent-core 0.87.0; 재측정 2026-09-23, 0.87.1 — 좌표 동일]` neither mode interrupts anything: `agent-loop.js:186`
|
|
1090
999
|
// drains steering after a turn ENDS and `:191-197` drains follow-ups only once the inner loop
|
|
1091
1000
|
// has ended. A caller who reads "interrupt the current turn" picks `steer` for urgency and gets
|
|
@@ -1099,7 +1008,6 @@ async function main(): Promise<void> {
|
|
|
1099
1008
|
{
|
|
1100
1009
|
const sites = [
|
|
1101
1010
|
"mcp/entwurf-bridge/src/index.ts",
|
|
1102
|
-
"pi-extensions/entwurf-control.ts",
|
|
1103
1011
|
"pi-extensions/lib/entwurf-v2-contract.ts",
|
|
1104
1012
|
"pi-extensions/lib/entwurf-v2-contract-schema.ts",
|
|
1105
1013
|
];
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* FCD-HERDR-BACKEND-FIRST a non-pilot backend refuses before any preflight and any mutation
|
|
21
21
|
* FCD-CODEX-PREFLIGHT-TMUX the Codex preflight still gates the tmux rail, and only that rail
|
|
22
22
|
* FCD-ONE-NONCE one nonce is minted per call and reaches the selected rail
|
|
23
|
-
* FCD-SURFACE-
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* FCD-SURFACE-ROOT-ONLY the one public surface (the bridge; pi reaches it through Pi's
|
|
24
|
+
* built-in MCP since #125) calls the dispatcher and renderer, with
|
|
25
|
+
* no copy of the rail choice or preflight, and pi keeps no second path
|
|
26
26
|
* FCD-RENDER-PER-RAIL each rail renders its own receipt; no universal tmux prose
|
|
27
27
|
* FCD-HERDR-VIEW-NOT-ADDRESS the herdr receipt says its coordinates are a view
|
|
28
28
|
* FCD-RECOVERY-VISIBLE a post-split failure shows closed vs orphan-unreclaimed:reason
|
|
@@ -190,11 +190,16 @@ async function main(): Promise<void> {
|
|
|
190
190
|
/result: await herdrFreshCall\(/.test(dispatchCode),
|
|
191
191
|
);
|
|
192
192
|
|
|
193
|
-
// ── one composition root,
|
|
193
|
+
// ── one composition root, one public surface ─────────────────────────────────────────
|
|
194
|
+
// #125: the pi-native fresh-call copy is retired — a pi session reaches the bridge's through Pi's
|
|
195
|
+
// built-in MCP — so the claim is no longer parity between two copies. It is that the ONE
|
|
196
|
+
// surface reaches the root without carrying rail choice, preflight or render of its own, and
|
|
197
|
+
// that the pi extension did not keep a second path to the root beside it.
|
|
194
198
|
ok(
|
|
195
|
-
"[QK:FCD-SURFACE-
|
|
196
|
-
PI_SRC.includes("dispatchFreshCall(") &&
|
|
197
|
-
PI_SRC.includes("renderDispatchedFreshCall(") &&
|
|
199
|
+
"[QK:FCD-SURFACE-ROOT-ONLY] the bridge reaches the dispatcher and its renderer and carries no rail choice, Codex preflight or rail-specific render of its own; the pi extension carries no fresh-call path at all",
|
|
200
|
+
!PI_SRC.includes("dispatchFreshCall(") &&
|
|
201
|
+
!PI_SRC.includes("renderDispatchedFreshCall(") &&
|
|
202
|
+
!PI_SRC.includes("fresh-call-dispatch") &&
|
|
198
203
|
MCP_SRC.includes("dispatchFreshCall({") &&
|
|
199
204
|
MCP_SRC.includes("renderDispatchedFreshCall(") &&
|
|
200
205
|
// Both DESCRIBE the rule to callers ("inside herdr (HERDR_ENV=1)…") and must keep
|
|
@@ -206,12 +211,6 @@ async function main(): Promise<void> {
|
|
|
206
211
|
!MCP_SRC.includes("renderHerdrFreshCall") &&
|
|
207
212
|
!PI_SRC.includes("renderHerdrFreshCall"),
|
|
208
213
|
);
|
|
209
|
-
ok(
|
|
210
|
-
"[QK:FCD-PI-DYNAMIC-IMPORT] pi still reaches the composition root through its LAZY dynamic import — a static import would pull the rails into pi's startup path, which is the fence that keeps extension load cheap",
|
|
211
|
-
/const FRESH_CALL_DISPATCH_MODULE = "\.\/lib\/fresh-call-dispatch\.ts";/.test(PI_SRC) &&
|
|
212
|
-
PI_SRC.includes("await import(FRESH_CALL_DISPATCH_MODULE)") &&
|
|
213
|
-
!/^import .*fresh-call-dispatch/m.test(PI_SRC),
|
|
214
|
-
);
|
|
215
214
|
|
|
216
215
|
// ── rendering stays rail-honest ──────────────────────────────────────────────────────
|
|
217
216
|
const herdrRefusal = renderDispatchedFreshCall({
|
|
@@ -505,6 +505,45 @@ if (ARGS.mode === "attribution-self-test") {
|
|
|
505
505
|
validateManifestSet([man], { ...permissiveOrigin, regularContainedFile: (f: string) => f !== "gates/kill.sh" }),
|
|
506
506
|
"signatureSource gates/kill.sh is not a regular",
|
|
507
507
|
);
|
|
508
|
+
|
|
509
|
+
// A declared build output (#125): optional, exact keys, a normalized repo-relative
|
|
510
|
+
// directory outside node_modules/.git, and one declaration per gate group.
|
|
511
|
+
const build = { argv: ["bash", "build.sh"], output: "out" };
|
|
512
|
+
ok(
|
|
513
|
+
"manifest: a declared build validates and rides the spec",
|
|
514
|
+
JSON.stringify(validateManifest(mutate({ build }), "m").mutants[0].build) === JSON.stringify(build),
|
|
515
|
+
);
|
|
516
|
+
ok("manifest: a spec without a build carries no build key", !("build" in man.mutants[0]));
|
|
517
|
+
throws(
|
|
518
|
+
"manifest: an unknown build key is refused",
|
|
519
|
+
() => validateManifest(mutate({ build: { ...build, env: {} } }), "m"),
|
|
520
|
+
"unknown key",
|
|
521
|
+
);
|
|
522
|
+
throws(
|
|
523
|
+
"manifest: a shell-string build is refused (argv array only)",
|
|
524
|
+
() => validateManifest(mutate({ build: { ...build, argv: "bash build.sh" } }), "m"),
|
|
525
|
+
"non-empty array",
|
|
526
|
+
);
|
|
527
|
+
for (const output of ["../out", "/tmp/out", "out/../x", "./out", "out/", "node_modules/x", ".git/x"]) {
|
|
528
|
+
throws(
|
|
529
|
+
`manifest: a build output ${JSON.stringify(output)} is refused`,
|
|
530
|
+
() => validateManifest(mutate({ build: { ...build, output } }), "m"),
|
|
531
|
+
"output",
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const withBuild = validateManifest(
|
|
535
|
+
{
|
|
536
|
+
...good,
|
|
537
|
+
lane: "built",
|
|
538
|
+
mutants: [{ ...good.mutants[0], claim: "SELFTEST-BUILT", signature: "[QK:SELFTEST-BUILT]", build }],
|
|
539
|
+
},
|
|
540
|
+
"c",
|
|
541
|
+
);
|
|
542
|
+
throws(
|
|
543
|
+
"[QK:QUALIFY-BUILD-GROUP-CONSISTENT] manifest set: one gate whose mutants declare different builds (here: one none) is refused — the same gate would be handed different inputs by run order",
|
|
544
|
+
() => validateManifestSet([man, withBuild], permissiveOrigin),
|
|
545
|
+
"mixes build declarations",
|
|
546
|
+
);
|
|
508
547
|
}
|
|
509
548
|
|
|
510
549
|
// ═══ Phase 1c — the pipeline over a synthetic fixture repo ══════════════════
|
|
@@ -879,6 +918,207 @@ const quiet = (): void => {};
|
|
|
879
918
|
}
|
|
880
919
|
}
|
|
881
920
|
|
|
921
|
+
// ═══ Phase 1c-b — a declared build output over a source-only fixture (#125) ═══
|
|
922
|
+
//
|
|
923
|
+
// The snapshot replicates the work surface only, so an IGNORED build output a gate consumes
|
|
924
|
+
// (the compiled entwurf-bridge) is absent, and such a gate is CONTROL-RED in every run. A
|
|
925
|
+
// mutant may declare that build instead. Each cell is a discriminating negative: a runner
|
|
926
|
+
// that skipped the rebuild after a mutation, read a build red as a kill, let one group's
|
|
927
|
+
// output reach another, or ignored a gate writing into the output flips exactly one of them.
|
|
928
|
+
|
|
929
|
+
function buildDeclaredOutputFixture(): string {
|
|
930
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-qualify-build-")));
|
|
931
|
+
const write = (rel: string, body: string): void => {
|
|
932
|
+
const p = path.join(dir, rel);
|
|
933
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
934
|
+
fs.writeFileSync(p, body);
|
|
935
|
+
};
|
|
936
|
+
write(".gitignore", "out/\n");
|
|
937
|
+
write("src.txt", "alpha\noriginal-line\nomega\n");
|
|
938
|
+
write("tracked-out/keep.txt", "a tracked directory is source, never a build output\n");
|
|
939
|
+
// The build prints the build-red mutant's own token on its FAILURE line on purpose: a
|
|
940
|
+
// runner that read build output as gate output would call the broken build a kill.
|
|
941
|
+
write(
|
|
942
|
+
"build.sh",
|
|
943
|
+
'set -e\nrm -rf out\nmkdir -p out\nif grep -q break-build src.txt; then echo "FAIL: build broke [QK:SELFTEST-BUILD-RED]" >&2; exit 1; fi\ncp src.txt out/built.txt\n',
|
|
944
|
+
);
|
|
945
|
+
// Reads ONLY the build output: it can see the planted defect only if the runner rebuilt.
|
|
946
|
+
write(
|
|
947
|
+
"gates/built.sh",
|
|
948
|
+
'if [ ! -f out/built.txt ]; then echo "FAIL: build output missing" >&2; exit 1; fi\nif grep -q defect-line out/built.txt; then echo "FAIL: built defect [QK:SELFTEST-BUILD-KILL]" >&2; exit 1; fi\nexit 0\n',
|
|
949
|
+
);
|
|
950
|
+
write(
|
|
951
|
+
"gates/writer.sh",
|
|
952
|
+
'if [ ! -f out/built.txt ]; then exit 1; fi\nif grep -q defect-line out/built.txt; then echo stray > out/stray.txt; echo "FAIL: built defect [QK:SELFTEST-BUILD-WRITER]" >&2; exit 1; fi\nexit 0\n',
|
|
953
|
+
);
|
|
954
|
+
write(
|
|
955
|
+
"gates/no-output.sh",
|
|
956
|
+
'if [ -e out ]; then echo "FAIL: a build output leaked out of its group" >&2; exit 1; fi\nexit 0\n',
|
|
957
|
+
);
|
|
958
|
+
const git = (...args: string[]): void => {
|
|
959
|
+
const r = spawnSync("git", ["-C", dir, ...args], {
|
|
960
|
+
stdio: "ignore",
|
|
961
|
+
env: {
|
|
962
|
+
...process.env,
|
|
963
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
964
|
+
GIT_CONFIG_SYSTEM: "/dev/null",
|
|
965
|
+
GIT_AUTHOR_NAME: "fx",
|
|
966
|
+
GIT_AUTHOR_EMAIL: "fx@localhost",
|
|
967
|
+
GIT_COMMITTER_NAME: "fx",
|
|
968
|
+
GIT_COMMITTER_EMAIL: "fx@localhost",
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
assert.equal(r.status, 0, `fixture git ${args.join(" ")} failed`);
|
|
972
|
+
};
|
|
973
|
+
git("init", "-q");
|
|
974
|
+
git("add", "-A");
|
|
975
|
+
git("-c", "commit.gpgsign=false", "commit", "-q", "-m", "fixture");
|
|
976
|
+
return dir;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
{
|
|
980
|
+
const origin = buildDeclaredOutputFixture();
|
|
981
|
+
const build = { argv: ["bash", "build.sh"], output: "out" };
|
|
982
|
+
const spec = (patch: Partial<MutantSpec>): MutantSpec => ({
|
|
983
|
+
claim: "SELFTEST-BUILD-KILL",
|
|
984
|
+
title: "fixture",
|
|
985
|
+
subject: "src.txt",
|
|
986
|
+
find: ["original-line"],
|
|
987
|
+
replace: ["defect-line"],
|
|
988
|
+
gate: ["bash", "gates/built.sh"],
|
|
989
|
+
timeoutSeconds: 30,
|
|
990
|
+
signature: "[QK:SELFTEST-BUILD-KILL]",
|
|
991
|
+
signatureSource: "gates/built.sh",
|
|
992
|
+
build,
|
|
993
|
+
...patch,
|
|
994
|
+
});
|
|
995
|
+
const snap = createRepoSnapshot(origin);
|
|
996
|
+
try {
|
|
997
|
+
ok(
|
|
998
|
+
"build fixture: the snapshot is source-only — the ignored output is not replicated",
|
|
999
|
+
!fs.existsSync(path.join(snap.repoDir, "out")),
|
|
1000
|
+
);
|
|
1001
|
+
const report = await qualifyMutants(
|
|
1002
|
+
snap,
|
|
1003
|
+
[
|
|
1004
|
+
// No declaration, same script: the #125 shape — the gate needs an output nobody built.
|
|
1005
|
+
// It runs FIRST, so its verdict never depends on whether an earlier group cleaned up.
|
|
1006
|
+
spec({
|
|
1007
|
+
claim: "SELFTEST-BUILD-UNDECLARED",
|
|
1008
|
+
signature: "[QK:SELFTEST-BUILD-UNDECLARED]",
|
|
1009
|
+
gate: ["bash", "gates/built.sh", "undeclared"],
|
|
1010
|
+
build: undefined,
|
|
1011
|
+
}),
|
|
1012
|
+
spec({}),
|
|
1013
|
+
spec({
|
|
1014
|
+
claim: "SELFTEST-BUILD-RED",
|
|
1015
|
+
signature: "[QK:SELFTEST-BUILD-RED]",
|
|
1016
|
+
replace: ["break-build"],
|
|
1017
|
+
}),
|
|
1018
|
+
spec({
|
|
1019
|
+
claim: "SELFTEST-NO-LEAK",
|
|
1020
|
+
signature: "[QK:SELFTEST-NO-LEAK]",
|
|
1021
|
+
gate: ["bash", "gates/no-output.sh"],
|
|
1022
|
+
build: undefined,
|
|
1023
|
+
}),
|
|
1024
|
+
],
|
|
1025
|
+
quiet,
|
|
1026
|
+
);
|
|
1027
|
+
const byClaim = new Map(report.groups.flatMap((g) => g.mutants.map((m) => [m.claim, { g, m }] as const)));
|
|
1028
|
+
ok(
|
|
1029
|
+
"[QK:QUALIFY-BUILD-TRACKS-MUTATION] a gate that reads ONLY the declared build output kills a mutant planted in the build's SOURCE — the runner rebuilt from the mutated bytes, and the restored bytes rebuilt the baseline (control-post green)",
|
|
1030
|
+
byClaim.get("SELFTEST-BUILD-KILL")?.m.verdict === "KILLED" &&
|
|
1031
|
+
byClaim.get("SELFTEST-BUILD-KILL")?.g.control === "ok",
|
|
1032
|
+
);
|
|
1033
|
+
ok(
|
|
1034
|
+
"[QK:QUALIFY-BUILD-RED-NOT-A-KILL] a mutation that breaks the build is never a kill, even when the build prints the claim token on its failure line — the gate did not run",
|
|
1035
|
+
byClaim.get("SELFTEST-BUILD-RED")?.m.verdict === "WRONG-REASON" &&
|
|
1036
|
+
(byClaim.get("SELFTEST-BUILD-RED")?.m.detail ?? "").includes("declared build red"),
|
|
1037
|
+
);
|
|
1038
|
+
ok(
|
|
1039
|
+
"[QK:QUALIFY-BUILD-SCOPED-TO-GROUP] the declared output belongs to its group alone — a later group never sees it, it is gone when the run ends, and the snapshot tree and porcelain are exactly as before",
|
|
1040
|
+
byClaim.get("SELFTEST-NO-LEAK")?.g.control === "ok" &&
|
|
1041
|
+
!fs.existsSync(path.join(snap.repoDir, "out")) &&
|
|
1042
|
+
report.treeClean &&
|
|
1043
|
+
report.porcelainClean,
|
|
1044
|
+
);
|
|
1045
|
+
ok(
|
|
1046
|
+
"build fixture: a gate that needs an output its group did not declare is CONTROL-RED (the #125 qualification shape)",
|
|
1047
|
+
byClaim.get("SELFTEST-BUILD-UNDECLARED")?.m.verdict === "CONTROL-RED",
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
const writer = await qualifyMutants(
|
|
1051
|
+
snap,
|
|
1052
|
+
[
|
|
1053
|
+
spec({
|
|
1054
|
+
claim: "SELFTEST-BUILD-WRITER",
|
|
1055
|
+
signature: "[QK:SELFTEST-BUILD-WRITER]",
|
|
1056
|
+
gate: ["bash", "gates/writer.sh"],
|
|
1057
|
+
signatureSource: "gates/writer.sh",
|
|
1058
|
+
}),
|
|
1059
|
+
],
|
|
1060
|
+
quiet,
|
|
1061
|
+
);
|
|
1062
|
+
ok(
|
|
1063
|
+
"[QK:QUALIFY-BUILD-OUTPUT-GUARDED] a gate that writes into its declared build output voids its own run — IMPURE, never KILLED, and the group is post-red",
|
|
1064
|
+
writer.groups[0].mutants[0].verdict === "IMPURE" && writer.groups[0].control === "post-red",
|
|
1065
|
+
);
|
|
1066
|
+
ok("build fixture: an impure-output report does not pass", reportPassed(writer) === false);
|
|
1067
|
+
|
|
1068
|
+
const baselineRed = await qualifyMutants(
|
|
1069
|
+
snap,
|
|
1070
|
+
[spec({ build: { argv: ["bash", "-c", "exit 3"], output: "out" } })],
|
|
1071
|
+
quiet,
|
|
1072
|
+
);
|
|
1073
|
+
ok(
|
|
1074
|
+
"build fixture: a build red at baseline voids the group — pre-red, every mutant CONTROL-RED",
|
|
1075
|
+
baselineRed.groups[0].control === "pre-red" && baselineRed.groups[0].mutants[0].verdict === "CONTROL-RED",
|
|
1076
|
+
);
|
|
1077
|
+
await assert.rejects(
|
|
1078
|
+
qualifyMutants(snap, [spec({ build: { argv: ["bash", "build.sh"], output: "tracked-out" } })], quiet),
|
|
1079
|
+
/already exists in the snapshot/,
|
|
1080
|
+
"a declared output that is on the work surface must be refused before anything runs",
|
|
1081
|
+
);
|
|
1082
|
+
ok(
|
|
1083
|
+
"build fixture: a declared output that is on the work surface is refused before anything runs, and its source bytes are untouched",
|
|
1084
|
+
fs.existsSync(path.join(snap.repoDir, "tracked-out", "keep.txt")),
|
|
1085
|
+
);
|
|
1086
|
+
await assert.rejects(
|
|
1087
|
+
qualifyMutants(snap, [spec({ build: { argv: ["bash", "build.sh"], output: "absent-parent/out" } })], quiet),
|
|
1088
|
+
/no parent directory in the snapshot/,
|
|
1089
|
+
"a declared output whose parent the snapshot lacks must be refused — the build would leave that directory behind",
|
|
1090
|
+
);
|
|
1091
|
+
ok(
|
|
1092
|
+
"build fixture: a declared output whose parent is absent is refused before anything runs, and nothing was created",
|
|
1093
|
+
!fs.existsSync(path.join(snap.repoDir, "absent-parent")),
|
|
1094
|
+
);
|
|
1095
|
+
// A build that never cleans its output (append-only) still reproduces its baseline: the
|
|
1096
|
+
// runner removes the output before every build, so the contract does not rest on the script.
|
|
1097
|
+
const appending = await qualifyMutants(
|
|
1098
|
+
snap,
|
|
1099
|
+
[
|
|
1100
|
+
spec({
|
|
1101
|
+
gate: ["bash", "gates/built.sh", "appending"],
|
|
1102
|
+
build: {
|
|
1103
|
+
argv: ["bash", "-c", "mkdir -p out && cp src.txt out/built.txt && echo run >> out/append.log"],
|
|
1104
|
+
output: "out",
|
|
1105
|
+
},
|
|
1106
|
+
}),
|
|
1107
|
+
],
|
|
1108
|
+
quiet,
|
|
1109
|
+
);
|
|
1110
|
+
ok(
|
|
1111
|
+
"build fixture: a build that does not clean its own output still kills and reproduces its baseline (control ok, tree clean)",
|
|
1112
|
+
appending.groups[0].mutants[0].verdict === "KILLED" &&
|
|
1113
|
+
appending.groups[0].control === "ok" &&
|
|
1114
|
+
appending.treeClean,
|
|
1115
|
+
);
|
|
1116
|
+
} finally {
|
|
1117
|
+
fs.rmSync(snap.baseDir, { recursive: true, force: true });
|
|
1118
|
+
fs.rmSync(origin, { recursive: true, force: true });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
882
1122
|
// ═══ Phase 1c-g — the PARTIAL group receipt over the fixture (#124 P1) ═════
|
|
883
1123
|
//
|
|
884
1124
|
// `--group` is a mixed CLI/process surface (argv, snapshot, gate children, a file
|
|
@@ -3081,6 +3321,7 @@ let manifestCount: number;
|
|
|
3081
3321
|
"acp-backend-preflight": 1,
|
|
3082
3322
|
"acp-augment": 10,
|
|
3083
3323
|
"acp-cortex": 14,
|
|
3324
|
+
"acp-foreground-only": 1,
|
|
3084
3325
|
"acp-launch-namespace": 2,
|
|
3085
3326
|
"acp-overlay": 1,
|
|
3086
3327
|
"acp-prompt-lifecycle": 15,
|
|
@@ -3101,11 +3342,14 @@ let manifestCount: number;
|
|
|
3101
3342
|
"copilot-launch": 14,
|
|
3102
3343
|
"pi-launch": 9,
|
|
3103
3344
|
"pi-floor": 1,
|
|
3345
|
+
"pi-bridge-sender": 4,
|
|
3346
|
+
"pi-mcp-bridge": 2,
|
|
3347
|
+
"pi-mcp-register": 3,
|
|
3104
3348
|
"copilot-receive": 20,
|
|
3105
3349
|
"entwurf-peers": 1,
|
|
3106
|
-
"fresh-call-dispatch":
|
|
3350
|
+
"fresh-call-dispatch": 11,
|
|
3107
3351
|
"fresh-cut": 3,
|
|
3108
|
-
"gate-qualification":
|
|
3352
|
+
"gate-qualification": 7,
|
|
3109
3353
|
"herdr-placement": 13,
|
|
3110
3354
|
"herdr-plugin": 25,
|
|
3111
3355
|
"herdr-plugin-profile": 14,
|
|
@@ -3123,10 +3367,10 @@ let manifestCount: number;
|
|
|
3123
3367
|
"mux-fresh-call": 59,
|
|
3124
3368
|
"mux-launcher-fence": 8,
|
|
3125
3369
|
"mux-parent-artifact": 3,
|
|
3126
|
-
"pack-install":
|
|
3370
|
+
"pack-install": 3,
|
|
3127
3371
|
"pi-package-ownership": 8,
|
|
3128
3372
|
"qualification-ledger": 42,
|
|
3129
|
-
"mux-resume-call":
|
|
3373
|
+
"mux-resume-call": 10,
|
|
3130
3374
|
"omp-birth": 13,
|
|
3131
3375
|
"omp-fresh": 24,
|
|
3132
3376
|
"omp-receive": 11,
|
|
@@ -260,8 +260,9 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
|
|
|
260
260
|
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
261
261
|
const RUNSH_DRIVE = /(?:\$\{?RUN\}?|run\.sh)\s+(install|setup|remove-user-scope|remove)\b/;
|
|
262
262
|
// install/setup write the operator's REAL user-scope surfaces under "$HOME"
|
|
263
|
-
// (
|
|
264
|
-
// the
|
|
263
|
+
// (settings.json registration follows PI_CODING_AGENT_DIR when set, but provider
|
|
264
|
+
// state and the rest stay HOME/XDG-rooted), so sandboxing the agent dir alone is not
|
|
265
|
+
// isolation for them; HOME is required. (setup:links died with
|
|
265
266
|
// the target registry — #50 C3.)
|
|
266
267
|
const HOME_ROOTED = new Set(["install", "setup"]);
|
|
267
268
|
|
|
@@ -212,6 +212,7 @@ function main(): void {
|
|
|
212
212
|
|
|
213
213
|
const PI_SURFACE = read("pi-extensions/entwurf-control.ts");
|
|
214
214
|
const MCP_SURFACE = read("mcp/entwurf-bridge/src/index.ts");
|
|
215
|
+
const PI_CODE = PI_SURFACE.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
215
216
|
const V2_RESUME = read("pi-extensions/lib/entwurf-v2-visible-resume.ts");
|
|
216
217
|
// IMPORTS, not mentions: the module header names the mux side deliberately (that is the
|
|
217
218
|
// architecture being documented), so an assertion on prose would forbid the explanation
|
|
@@ -222,55 +223,43 @@ function main(): void {
|
|
|
222
223
|
v2ImportSpecifiers.length > 0 && !v2ImportSpecifiers.some((s) => /mux-/.test(s)),
|
|
223
224
|
);
|
|
224
225
|
ok(
|
|
225
|
-
"
|
|
226
|
-
/mux-resume-call/.test(
|
|
227
|
-
/entwurf-v2-visible-resume/.test(PI_SURFACE) &&
|
|
228
|
-
/mux-resume-call/.test(MCP_SURFACE) &&
|
|
229
|
-
/entwurf-v2-visible-resume/.test(MCP_SURFACE),
|
|
226
|
+
"the bridge is the composition root: it imports the mux launcher AND the v2 composition and joins them itself",
|
|
227
|
+
/mux-resume-call/.test(MCP_SURFACE) && /entwurf-v2-visible-resume/.test(MCP_SURFACE),
|
|
230
228
|
);
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// the tool deleted.
|
|
237
|
-
const PI_BLOCK = (PI_SURFACE.split('name: "entwurf_resume_call",')[1] ?? "").split("\n}")[0];
|
|
229
|
+
// #125: pi reaches entwurf_resume_call through Pi's built-in MCP — the bridge above IS its
|
|
230
|
+
// surface. The native pi registration, its target-only TypeBox schema and its description are
|
|
231
|
+
// retired with it (registration and target-only are held on the runtime tools/list by
|
|
232
|
+
// BRIDGEBOOT-RESUME-CALL-REGISTERED / -TARGET-ONLY, the description by DESC-CONTRACT-MCP below).
|
|
233
|
+
// What is left to say about the pi file is that it does not come back as a second root.
|
|
238
234
|
ok(
|
|
239
|
-
"
|
|
240
|
-
|
|
235
|
+
"the pi extension no longer joins the resume halves itself — no second composition root beside the bridge",
|
|
236
|
+
!/mux-resume-call/.test(PI_CODE) && !/entwurf-v2-visible-resume/.test(PI_CODE),
|
|
241
237
|
);
|
|
238
|
+
// The pass-through half the native claim used to carry, now on the one surface that has it:
|
|
239
|
+
// the handler hands the validated target to the v2 composition UNCHANGED. The schema proves the
|
|
240
|
+
// input is target-only; this proves the value that reaches the record lookup is that input.
|
|
241
|
+
const MCP_RESUME_BLOCK = (MCP_SURFACE.split('"entwurf_resume_call",')[1] ?? "").split("\n);")[0];
|
|
242
242
|
ok(
|
|
243
|
-
"[QK:MUXRESUME-
|
|
244
|
-
/
|
|
245
|
-
/visibleResume\(
|
|
246
|
-
!/model|task|prompt/.test(PI_BLOCK.split("parameters:")[1]?.split("async execute")[0] ?? ""),
|
|
243
|
+
"[QK:MUXRESUME-MCP-TARGET-PASSTHROUGH] the bridge handler passes its target straight to visibleResume — a rewritten, defaulted or substituted id would reopen a DIFFERENT citizen than the one the caller named",
|
|
244
|
+
/async \(\{ target \}\) =>/.test(MCP_RESUME_BLOCK) &&
|
|
245
|
+
/await visibleResume\(target, makeVisibleResumeDeps\(visibleResumeLaunch\)\)/.test(MCP_RESUME_BLOCK),
|
|
247
246
|
);
|
|
248
247
|
|
|
249
|
-
// ── the description contract
|
|
248
|
+
// ── the description contract ────────────────────────────────────────────────
|
|
250
249
|
// Not prose parity — three claims an operator acts on, plus the host's own cap.
|
|
251
|
-
const PI_DESC = (PI_BLOCK.split("description: `")[1] ?? "").split("`,")[0];
|
|
252
250
|
const MCP_DESC = (MCP_SURFACE.split('"entwurf_resume_call",')[1] ?? "").split("{")[0];
|
|
253
251
|
const statesTheContract = (text: string): boolean =>
|
|
254
252
|
/dormant/i.test(text) && /no turn/i.test(text) && /observation/i.test(text) && /launch/i.test(text);
|
|
255
253
|
|
|
256
|
-
for (const [which, text] of [
|
|
257
|
-
["native pi", PI_DESC],
|
|
258
|
-
["MCP bridge", MCP_DESC],
|
|
259
|
-
] as const) {
|
|
260
|
-
ok(
|
|
261
|
-
`${which}: the resume description fits the host's 2048-char tool-description cap`,
|
|
262
|
-
text.length > 0 && text.length <= 2048,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
// The two claim tokens are written OUT, not interpolated: qualification requires each
|
|
266
|
-
// `[QK:…]` to appear literally exactly once in its gate source, so a token assembled at
|
|
267
|
-
// runtime names a claim no manifest can bind to.
|
|
268
254
|
ok(
|
|
269
|
-
"
|
|
270
|
-
|
|
255
|
+
"MCP bridge: the resume description fits the host's 2048-char tool-description cap",
|
|
256
|
+
MCP_DESC.length > 0 && MCP_DESC.length <= 2048,
|
|
271
257
|
);
|
|
258
|
+
// The claim token is written OUT, not interpolated: qualification requires each `[QK:…]` to
|
|
259
|
+
// appear literally exactly once in its gate source, so a token assembled at runtime names a
|
|
260
|
+
// claim no manifest can bind to.
|
|
272
261
|
ok(
|
|
273
|
-
"[QK:MUXRESUME-DESC-CONTRACT-MCP] MCP bridge: the description states the
|
|
262
|
+
"[QK:MUXRESUME-DESC-CONTRACT-MCP] MCP bridge: the description states the three things a caller acts on — DORMANT targets only, no turn is run, and TWO receipts whose observation is the fact that matters",
|
|
274
263
|
statesTheContract(MCP_DESC),
|
|
275
264
|
);
|
|
276
265
|
} finally {
|