@junghanacs/entwurf 0.25.0 → 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.
Files changed (88) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +182 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +1 -1
  6. package/README.md +178 -674
  7. package/VERIFY.md +17 -16
  8. package/demo/README.md +1 -1
  9. package/docs/acp-backend-rail.md +6 -2
  10. package/docs/external-mcp-host.md +1 -1
  11. package/docs/setup-clean-host.md +346 -10
  12. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +12 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
  23. package/mcp/entwurf-bridge/src/index.ts +30 -3
  24. package/package.json +11 -11
  25. package/pi-extensions/entwurf-control.ts +50 -637
  26. package/pi-extensions/lib/acp/backend-adapter.ts +13 -1
  27. package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
  28. package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
  29. package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
  30. package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
  31. package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
  32. package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
  33. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
  34. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
  35. package/pi-extensions/lib/fresh-call-composition.ts +13 -6
  36. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  37. package/pi-extensions/lib/mux-fresh-call.ts +3 -3
  38. package/run.sh +168 -54
  39. package/scripts/check-entwurf-peers-surface.ts +8 -15
  40. package/scripts/check-entwurf-self-address.ts +7 -11
  41. package/scripts/check-entwurf-v2-surface.ts +20 -112
  42. package/scripts/check-fresh-call-dispatch.ts +12 -13
  43. package/scripts/check-gate-qualification.ts +2741 -157
  44. package/scripts/check-install-surface.ts +3 -2
  45. package/scripts/check-mux-resume-call.ts +24 -35
  46. package/scripts/check-pi-bridge-sender.ts +130 -0
  47. package/scripts/check-pi-mcp-bridge.ts +251 -0
  48. package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
  49. package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
  50. package/scripts/lib/mutation-qualify.ts +269 -44
  51. package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
  52. package/scripts/lib/qualification-acceptance.ts +146 -0
  53. package/scripts/lib/qualification-artifact.ts +359 -0
  54. package/scripts/lib/qualification-collection.ts +384 -0
  55. package/scripts/lib/qualification-gh.ts +231 -0
  56. package/scripts/lib/qualification-ledger.ts +669 -0
  57. package/scripts/lib/qualification-oracle-core.ts +279 -0
  58. package/scripts/lib/qualification-receipt.ts +561 -0
  59. package/scripts/lib/qualification-release.ts +271 -0
  60. package/scripts/meta-bridge-doctor.sh +16 -5
  61. package/scripts/mutants/acp-foreground-only.json +17 -0
  62. package/scripts/mutants/codex-native.json +11 -11
  63. package/scripts/mutants/control-send-receipt.json +6 -4
  64. package/scripts/mutants/copilot-birth.json +2 -1
  65. package/scripts/mutants/fresh-call-dispatch.json +3 -14
  66. package/scripts/mutants/gate-qualification.json +59 -0
  67. package/scripts/mutants/mux-fresh-call.json +43 -45
  68. package/scripts/mutants/mux-resume-call.json +11 -35
  69. package/scripts/mutants/pack-install.json +15 -4
  70. package/scripts/mutants/pi-bridge-sender.json +54 -0
  71. package/scripts/mutants/pi-floor.json +6 -2
  72. package/scripts/mutants/pi-mcp-bridge.json +36 -0
  73. package/scripts/mutants/pi-mcp-register.json +51 -0
  74. package/scripts/mutants/qualification-ledger.json +489 -0
  75. package/scripts/mutants/v2-surface.json +6 -6
  76. package/scripts/qualification-oracle.ts +124 -0
  77. package/scripts/resident-rpc-drive.ts +5 -1
  78. package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
  79. package/scripts/smoke-acp-cortex-live.ts +3 -1
  80. package/scripts/smoke-acp-session-reuse-live.ts +29 -12
  81. package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
  82. package/scripts/smoke-acp-v2-send-live.ts +3 -1
  83. package/scripts/smoke-codex-fresh-live.ts +20 -1
  84. package/scripts/smoke-entwurf-chain-live.ts +6 -2
  85. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
  86. package/scripts/smoke-herdr-raw-install-live.sh +8 -3
  87. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  88. 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-PI-MODE-SCOPE]`,
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 wiring guard ─────────────────────────────────────
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
- ok("4: pi-native — entwurf-control registers entwurf_v2", /name:\s*"entwurf_v2"/.test(src));
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 — reaches the fence via a NON-LITERAL dynamic import (string-const specifier)",
702
- /const ENTWURF_V2_SURFACE_MODULE\s*=/.test(code) && /await import\(ENTWURF_V2_SURFACE_MODULE\)/.test(code),
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 both tool descriptions names every rail ──
1056
- // A caller reads one of these two blurbs and nothing else before dispatching. The control
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", "pi-extensions/entwurf-control.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] 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)})`,
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 four caller-facing prose sites, checked for what they SAY ──
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-PARITY both surfaces call the same dispatcher and renderer, with no
24
- * second copy of the rail choice or the preflight ordering
25
- * FCD-PI-DYNAMIC-IMPORT pi still reaches it through its lazy dynamic import
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, two surfaces ───────────────────────────────────────────────
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-PARITY] BOTH surfaces reach the dispatcher and its renderer, and NEITHER carries its own rail choice, its own Codex preflight or its own rail-specific render — a defect planted in one copy would survive on the other",
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({