@junghanacs/entwurf 0.25.1 → 0.30.1

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 (86) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +190 -0
  4. package/DELIVERY.md +1 -1
  5. package/README.md +183 -674
  6. package/VERIFY.md +21 -17
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +115 -18
  9. package/docs/external-mcp-host.md +1 -1
  10. package/docs/setup-clean-host.md +346 -10
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +27 -4
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +55 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +1 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +2 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +2 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +2 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -5
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +3 -3
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +13 -6
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +3 -3
  22. package/mcp/entwurf-bridge/src/index.ts +30 -3
  23. package/package.json +13 -13
  24. package/pi-extensions/entwurf-control.ts +50 -637
  25. package/pi-extensions/lib/acp/backend-adapter.ts +81 -1
  26. package/pi-extensions/lib/acp/backend.ts +41 -5
  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 +177 -53
  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 +250 -5
  44. package/scripts/check-herdr-plugin-build.ts +159 -57
  45. package/scripts/check-install-surface.ts +3 -2
  46. package/scripts/check-mux-resume-call.ts +24 -35
  47. package/scripts/check-pi-bridge-sender.ts +130 -0
  48. package/scripts/check-pi-mcp-bridge.ts +251 -0
  49. package/scripts/fixtures/herdr-supply.json +8 -8
  50. package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
  51. package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
  52. package/scripts/lib/mutation-qualify.ts +269 -44
  53. package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
  54. package/scripts/meta-bridge-doctor.sh +16 -5
  55. package/scripts/mutants/acp-foreground-only.json +17 -0
  56. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  57. package/scripts/mutants/acp-vendor-rejection.json +62 -0
  58. package/scripts/mutants/codex-native.json +11 -11
  59. package/scripts/mutants/control-send-receipt.json +6 -4
  60. package/scripts/mutants/copilot-birth.json +2 -1
  61. package/scripts/mutants/fresh-call-dispatch.json +3 -14
  62. package/scripts/mutants/gate-qualification.json +59 -0
  63. package/scripts/mutants/herdr-plugin-build.json +11 -0
  64. package/scripts/mutants/herdr-supply.json +3 -3
  65. package/scripts/mutants/mux-fresh-call.json +43 -45
  66. package/scripts/mutants/mux-resume-call.json +11 -35
  67. package/scripts/mutants/pack-install.json +15 -4
  68. package/scripts/mutants/pi-bridge-sender.json +54 -0
  69. package/scripts/mutants/pi-floor.json +6 -2
  70. package/scripts/mutants/pi-mcp-bridge.json +36 -0
  71. package/scripts/mutants/pi-mcp-register.json +51 -0
  72. package/scripts/mutants/v2-surface.json +6 -6
  73. package/scripts/resident-rpc-drive.ts +5 -1
  74. package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
  75. package/scripts/smoke-acp-cortex-live.ts +3 -1
  76. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  77. package/scripts/smoke-acp-session-reuse-live.ts +29 -12
  78. package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
  79. package/scripts/smoke-acp-v2-send-live.ts +3 -1
  80. package/scripts/smoke-codex-fresh-live.ts +20 -1
  81. package/scripts/smoke-entwurf-chain-live.ts +6 -2
  82. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
  83. package/scripts/smoke-herdr-fresh-call-live.ts +21 -7
  84. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  85. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  86. package/scripts/smoke-resident-garden-guard.sh +3 -1
package/run.sh CHANGED
@@ -140,6 +140,9 @@ Usage:
140
140
  ./run.sh check-omp-fresh-preflight # #87 C: the OMP fresh preflight reproduces omp_agent_dir and the tools.xdev read in TS (it runs from two emit depths and cannot call a sibling script). This drives the SHIPPED shell/python leaves over the same inputs — refusals included — and requires the TS half to agree, so the reproduction cannot silently drift from the installer's own oracle
141
141
  ./run.sh check-codex-sender-identity # Codex tools/call _meta triple agreement + addressable record join + cross-carrier conflict refusal
142
142
  ./run.sh check-codex-bridge-identity # real MCP bridge request identity for entwurf_self/v2/fresh; malformed or conflicting _meta fails loud
143
+ ./run.sh check-pi-mcp-register # #125: mutant execution coordinate for pi-extensions/entwurf-control.test.ts — the real extension births through Pi's real loader and registers the compiled bridge after birth, before UI, direct with two hidden. Needs build-bridge
144
+ ./run.sh check-pi-mcp-bridge # #125: a real Pi runtime (builtin MCP, default stdio, faux model only) spawns the COMPILED bridge as the born citizen — six prefixed verbs declared, hidden two absent, child env = record = entwurf_self, cwd = session cwd; newSession vendor conformance observed (old child gone, new record+child, old record kept). Needs build-bridge
145
+ ./run.sh check-pi-bridge-sender # #125: real bridge pi-session carrier is served only when it names a readable pi RECORD; missing / foreign-backend / unreadable each refuse with their own cause; no socket = replyable:false, not refusal
143
146
  ./run.sh check-codex-native-push # app-server WebSocket-over-UDS probe + measured one-shot codex queue delivery; no replay
144
147
  ./run.sh check-codex-birth-hook # sandboxed payload + declaration: first-turn V3 birth, one record per thread, top-level event predicate, no markers
145
148
  ./run.sh check-codex-fresh-preflight # pre-mutation birth/MCP/thread-title/default-app-server readiness
@@ -277,6 +280,7 @@ Usage:
277
280
  ./run.sh check-node-floor-coherence # binds the Node floor (24+, single axis) across engines.node, run.sh setup preflight, meta-bridge install/doctor judgment logic, clean-host docs, the bridge launcher header, and the CI runner node-version — engines.node is the SSOT, everything else is derived; sweeps tracked contract text for an unregistered declaration
278
281
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
279
282
  ./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes, one cell per property: version boundary (a prefix-extended @0.86.0-beta.1 and an off-pin @0.85.1 must leak, @0.86.0 bare or with any measured peer-hash must pass) and closure prefix (an off-pin @earendil-works/chord must leak — it carries no `pi-` prefix); snapshot-safe qualification oracle, also run first inside check-pack-install
283
+ ./run.sh check-pack-consumer-roots # pure self-test of check-pack-install's consumer sandbox: under a poisoned inherited HOME/XDG/PI_CODING_AGENT_DIR every writable root a consumer subcall sees resolves inside its own consumer home; snapshot-safe qualification oracle, also run first inside check-pack-install
280
284
  ./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm run check:full): drives real install/setup/fresh-cut in a sandbox; certification refusal is pre-write, quiescence is fail-closed, archives preserve bytes, and the #54 exit matrix distinguishes complete / no-move / usage / incomplete transition / complete-with-cleanup-residue. No model/network/cost
281
285
  ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke + the #116 M3-b1 herdr-plugin RUNTIME VERIFIER run against that actual installed tarball (the focused check-herdr-runtime-bootstrap drives a fixture; THIS is where the real package is proven) with the pinned pi peers (pins derived from the package.json devDep; check-dep-versions binds them) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the installed all-absent and copilot-present (four-unit fake-vendor) `entwurf setup` rows + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES) + the INSTALLED-PACKAGE branch of the Copilot and OMP birth installers actually RUN (compiled entry selected, no raw .ts, and a real birth edge mints a citizen — the half a required-artifact list can never stand in for)
282
286
  ./run.sh check-install-container # 0.12.8 (#51 C): Linux artifact-CONSUMER gate — one candidate .tgz handed read-only to a checkout-invisible node:<engines-major>-bookworm cell. Default packs once to temp; ENTWURF_CANDIDATE_TGZ=/absolute/preserved.tgz consumes those exact bytes with no re-pack and prints canonical path+sha256 for release. Non-root global PATH install, frozen package, MCP tools/list, fake-Claude install-meta-bridge, path+sha256 fence, strict doctor, and the GENERATION host-state matrix (clean / v3-only store bytes unchanged / previous-generation REFUSE→fresh-cut→retry PASS) seeded inline. Docker missing = honest SKIP; ENTWURF_REQUIRE_DOCKER=1 makes that RED (required CI)
@@ -1336,6 +1340,32 @@ check_entwurf_resume_args() {
1336
1340
  run_vitest pi-extensions/lib/entwurf-resume-args.test.ts
1337
1341
  }
1338
1342
 
1343
+ check_acp_foreground_only() {
1344
+ # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1345
+ # MUTANT EXECUTION COORDINATE for scripts/mutants/acp-foreground-only.json (same reasoning as
1346
+ # check_entwurf_resume_args above), with a file filter narrow on purpose.
1347
+ section "claude ACP foreground-only launch env (mutant execution coordinate)"
1348
+ run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
1349
+ }
1350
+
1351
+ check_acp_vendor_rejection() {
1352
+ # Behaviour-adjacent tests are discovered by the glob lane. This narrow case is the
1353
+ # mutant execution coordinate: include the adapter admission and real turn pipeline,
1354
+ # and keep run_vitest's failed-title attribution for qualification.
1355
+ section "ACP vendor rejection admission (mutant execution coordinate)"
1356
+ run_vitest pi-extensions/lib/acp/backend-adapter.test.ts pi-extensions/lib/acp/backend.vendor-rejection.test.ts
1357
+ }
1358
+
1359
+ check_pi_mcp_register() {
1360
+ # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1361
+ # MUTANT EXECUTION COORDINATE for scripts/mutants/pi-mcp-register.json (same reasoning as
1362
+ # check_entwurf_resume_args above): run_vitest is what emits the structured failed-test titles a
1363
+ # kill is attributed from. It sits in check:hermetic, not check:contracts, because the subject
1364
+ # registers the COMPILED bridge and refuses by name when `build-bridge` has not run.
1365
+ section "pi MCP registration after birth (mutant execution coordinate)"
1366
+ run_vitest pi-extensions/entwurf-control.test.ts
1367
+ }
1368
+
1339
1369
  check_mux_launcher_fence() {
1340
1370
  # Deterministic gate for the shared operator-launcher fence (issue #67):
1341
1371
  # scripts/lib/claude-launcher-fence.ts plus its wiring into BOTH mux LIVE smokes. Replants the
@@ -1600,9 +1630,9 @@ smoke_acp_session_reuse_live() {
1600
1630
  # needs LIVE=1. Forces process-scoped (pushes --entwurf-control into argv) and
1601
1631
  # drives TWO real ACP turns over ONE reused claude-agent-acp child via the real
1602
1632
  # streamShellAcp: turn 1 introduces a codeword (full transcript), turn 2 sends
1603
- # ONLY the latest user delta and must recall the codeword — proving the child
1604
- # was reused and the live ACP session kept turn-1 history (a respawn-per-turn
1605
- # backend would forget it). The one-shot exit0 half is owned by
1633
+ # ONLY the latest user delta. The REUSE lifecycle notice, with no new bootstrap,
1634
+ # is the verdict; codeword recall is supporting only (a rebuilt full transcript
1635
+ # could recall it too). The one-shot exit0 half is owned by
1606
1636
  # smoke-acp-provider-live.
1607
1637
  # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
1608
1638
  # LIVE=1 ./run.sh smoke-acp-session-reuse-live
@@ -2185,11 +2215,11 @@ assert.equal(peerTui, piAi,
2185
2215
  // an open `>=` floor is exactly how the next installer re-acquires the drift.
2186
2216
  // The floor is also the HARD MINIMUM a consumer install resolves: at `>=0.86.0`
2187
2217
  // an existing 0.85.1 host is upgraded, not kept.
2188
- // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.86.0 <0.87`).
2218
+ // Expected shape: `>=<devDep> <<major>.<minor+1>` (e.g. `>=0.86.0 <0.87`, `>=1.0.0 <1.1`).
2219
+ // Pi 1.0 did not change the rule: an unverified minor is still unverified, so the ceiling
2220
+ // stays the next minor of the pin's own major.
2189
2221
  const [piMaj, piMin] = piAi.split('.').map(Number);
2190
- assert.equal(piMaj, 0,
2191
- `pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
2192
- const expectedPeer = `>=${piAi} <0.${piMin + 1}`;
2222
+ const expectedPeer = `>=${piAi} <${piMaj}.${piMin + 1}`;
2193
2223
  const peerDepAi = pkg.peerDependencies?.['@earendil-works/pi-ai'];
2194
2224
  const peerDepCoding = pkg.peerDependencies?.['@earendil-works/pi-coding-agent'];
2195
2225
  const peerDepTui = pkg.peerDependencies?.['@earendil-works/pi-tui'];
@@ -2258,13 +2288,13 @@ function stripBumpLedger(file, text) {
2258
2288
  let rangeDecls = 0, exactDecls = 0;
2259
2289
  for (const file of BASELINE_DOCS) {
2260
2290
  const text = stripBumpLedger(file, readFileSync(file, 'utf8'));
2261
- // Closed-range declarations: `>=<floor> <0.<ceiling>` (spaces optional).
2262
- for (const [decl, floor, ceilMinor] of text.matchAll(/>=\s?(\d+\.\d+\.\d+)\s?<\s?0\.(\d+)/g)) {
2291
+ // Closed-range declarations: `>=<floor> <<major>.<ceiling>` (spaces optional).
2292
+ for (const [decl, floor, ceilMajor, ceilMinor] of text.matchAll(/>=\s?(\d+\.\d+\.\d+)\s?<\s?(\d+)\.(\d+)/g)) {
2263
2293
  rangeDecls++;
2264
2294
  assert.equal(floor, piAi,
2265
2295
  `${file}: declared pi floor in "${decl}" is ${floor}, but the devDep pin is ${piAi} — a baseline doc may not advertise a version no gate drives`);
2266
- assert.equal(Number(ceilMinor), piMin + 1,
2267
- `${file}: declared pi ceiling in "${decl}" must be the next minor (0.${piMin + 1})`);
2296
+ assert.equal(`${ceilMajor}.${ceilMinor}`, `${piMaj}.${piMin + 1}`,
2297
+ `${file}: declared pi ceiling in "${decl}" must be the next minor (${piMaj}.${piMin + 1})`);
2268
2298
  }
2269
2299
  // Exact install pins: `@earendil-works/pi-<pkg>@<version>`.
2270
2300
  for (const [decl, ver] of text.matchAll(/@earendil-works\/pi-(?:ai|coding-agent|tui)@(\d+\.\d+\.\d+)/g)) {
@@ -2295,12 +2325,14 @@ for (const [file, re, shape] of PROSE_DECLS) {
2295
2325
  // ONE assertion, because the claim token may appear exactly once in a gate source.
2296
2326
  // Every way the floor can fragment is collected first and named individually in the
2297
2327
  // failure, so "which surface" is never lost to the single-token rule.
2298
- const nextFloorRange = `>=${piAi} <0.${piMin + 1}`;
2328
+ const nextFloorRange = `>=${piAi} <${piMaj}.${piMin + 1}`;
2299
2329
  const floorGaps = [];
2300
2330
  const PI_CONSTELLATION = [
2301
2331
  '@earendil-works/pi-ai', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui',
2302
2332
  '@earendil-works/pi-agent-core', '@earendil-works/pi-client', '@earendil-works/pi-protocol',
2303
2333
  '@earendil-works/pi-telemetry', '@earendil-works/chord',
2334
+ // `[측정 2026-09-30]` 0.99.1: pi-coding-agent's runtime deps gained the built-in extensions' packages.
2335
+ '@earendil-works/pi-codemode', '@earendil-works/pi-mcp',
2304
2336
  ];
2305
2337
  const sameSet = (got, want, where) => {
2306
2338
  const missing = want.filter((n) => !got.includes(n));
@@ -2343,9 +2375,9 @@ if (!matcherPin) {
2343
2375
  // measurement in docs/mux-launch-rail.md, for one) are deliberately NOT scanned:
2344
2376
  // they record what was true then, and rewriting them would falsify evidence.
2345
2377
  const CURRENT_CONTRACT_DECLS = [
2346
- ['plugins/herdr/README.md', /npm install -g "@earendil-works\/pi-coding-agent@(>=[\d.]+ <0\.\d+)"/, 'the herdr install line'],
2347
- ['docs/mux-launch-rail.md', /현 supported range 는 `(>=[\d.]+ <0\.\d+)`/, "mux-launch-rail's current supported range"],
2348
- ['docs/acp-backend-rail.md', /devDep exact `(\d+\.\d+\.\d+)`, peer `(>=[\d.]+ <0\.\d+)`/, "the ACP support row's exact+range pair"],
2378
+ ['plugins/herdr/README.md', /npm install -g "@earendil-works\/pi-coding-agent@(>=[\d.]+ <\d+\.\d+)"/, 'the herdr install line'],
2379
+ ['docs/mux-launch-rail.md', /현 supported range 는 `(>=[\d.]+ <\d+\.\d+)`/, "mux-launch-rail's current supported range"],
2380
+ ['docs/acp-backend-rail.md', /devDep exact `(\d+\.\d+\.\d+)`, peer `(>=[\d.]+ <\d+\.\d+)`/, "the ACP support row's exact+range pair"],
2349
2381
  ];
2350
2382
  for (const [file, re, shape] of CURRENT_CONTRACT_DECLS) {
2351
2383
  const m = readFileSync(file, 'utf8').match(re);
@@ -2864,13 +2896,14 @@ if (typeof FLOOR !== 'string' || !/^\d+\.\d+\.\d+$/.test(FLOOR)) {
2864
2896
  console.error(`[check-pi-runtime-version] FAIL: package.json devDependencies['@earendil-works/pi-coding-agent'] must be an EXACT x.y.z pin to serve as the runtime floor (got ${FLOOR ?? 'nothing'})`);
2865
2897
  process.exit(1);
2866
2898
  }
2867
- // The declared contract is a CLOSED range (`>=<devDep> <0.<minor+1>`, enforced on
2899
+ // The declared contract is a CLOSED range (`>=<devDep> <<major>.<minor+1>`, enforced on
2868
2900
  // package.json by check-dep-versions), so the runtime check must be closed too.
2869
2901
  // A floor-only comparison would bless a resolved pi ABOVE the ceiling — and an
2870
2902
  // out-of-range pi is exactly the drift this cut exists to stop: 0.80.6 landed on
2871
2903
  // the dev box while the repo still declared 0.80.3, and every gate stayed green.
2872
2904
  // Verifying only half of a declared range is the same lie in the other direction.
2873
- const CEILING = `0.${Number(FLOOR.split('.')[1]) + 1}.0`;
2905
+ const [FLOOR_MAJOR, FLOOR_MINOR] = FLOOR.split('.').map(Number);
2906
+ const CEILING = `${FLOOR_MAJOR}.${FLOOR_MINOR + 1}.0`;
2874
2907
  const cmp = (a, b) => {
2875
2908
  const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
2876
2909
  for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
@@ -3710,7 +3743,7 @@ check_pack() {
3710
3743
  # which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
3711
3744
  # below so a future suffix change cannot pass vacuously.)
3712
3745
  pack_install_leaked_pi() {
3713
- grep '^@earendil-works+' | grep -Ev '@0\.87\.1(_|$)' || true
3746
+ grep '^@earendil-works+' | grep -Ev '@1\.0\.0(_|$)' || true
3714
3747
  }
3715
3748
 
3716
3749
  # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
@@ -3730,35 +3763,88 @@ check_pack_pin_matcher() {
3730
3763
  # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`0.86.0-beta.1`,
3731
3764
  # the prerelease shape that an unanchored match would bless) and an off-pin version.
3732
3765
  matcher_probe=$(printf '%s\n' \
3733
- '@earendil-works+pi-ai@0.87.1' \
3734
- '@earendil-works+pi-ai@0.87.1_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
3735
- '@earendil-works+pi-ai@0.87.1_ws@8.21.3' \
3736
- '@earendil-works+pi-ai@0.87.1_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
3737
- '@earendil-works+pi-ai@0.87.1-beta.1' \
3738
- '@earendil-works+pi-agent-core@0.87.0' | pack_install_leaked_pi)
3739
- if [ "$matcher_probe" != '@earendil-works+pi-ai@0.87.1-beta.1
3740
- @earendil-works+pi-agent-core@0.87.0' ]; then
3741
- fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 0.87.1-beta.1 and the off-pin 0.87.0 lookalikes, and pass 0.87.1 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
3766
+ '@earendil-works+pi-ai@1.0.0' \
3767
+ '@earendil-works+pi-ai@1.0.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
3768
+ '@earendil-works+pi-ai@1.0.0_ws@8.21.3' \
3769
+ '@earendil-works+pi-ai@1.0.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
3770
+ '@earendil-works+pi-ai@1.0.0-beta.1' \
3771
+ '@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
3772
+ if [ "$matcher_probe" != '@earendil-works+pi-ai@1.0.0-beta.1
3773
+ @earendil-works+pi-agent-core@0.87.1' ]; then
3774
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.0.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.0.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
3742
3775
  return 1
3743
3776
  fi
3744
3777
 
3745
3778
  # Cell 2 — the closure prefix. chord is a non-`pi-` member of the same runtime closure
3746
- # (0.85.0 onward, still true at 0.87.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
3779
+ # (0.85.0 onward, still true at 0.99.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
3747
3780
  # back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
3748
3781
  matcher_probe=$(printf '%s\n' \
3782
+ '@earendil-works+chord@1.0.0' \
3749
3783
  '@earendil-works+chord@0.87.1' \
3750
- '@earendil-works+chord@0.87.0' \
3751
- '@earendil-works+pi-ai@0.87.1' | pack_install_leaked_pi)
3752
- if [ "$matcher_probe" != '@earendil-works+chord@0.87.0' ]; then
3753
- fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it is a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0 and `[측정 2026-09-23]` at 0.87.1: the same four @earendil-works direct deps) — got: ${matcher_probe:-<nothing leaked>}"
3784
+ '@earendil-works+pi-ai@1.0.0' | pack_install_leaked_pi)
3785
+ if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
3786
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it is a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0, `[측정 2026-09-23]` at 0.87.1 and `[측정 2026-09-30]` at 0.99.1, where pi-coding-agent adds pi-codemode and pi-mcp beside them) — got: ${matcher_probe:-<nothing leaked>}"
3754
3787
  return 1
3755
3788
  fi
3756
3789
 
3757
3790
  echo "[check-pack-pin-matcher] ok — the pin-leak matcher is version-bounded (lookalikes leak, pinned version passes bare and with either measured peer-hash) and covers the whole @earendil-works closure (chord included)"
3758
3791
  }
3759
3792
 
3793
+ # The writable roots of ONE consumer sandbox, shared by every consumer subcall of
3794
+ # check-pack-install that drives an installed `run.sh` writer. The writers resolve their agent
3795
+ # dir as `${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}` (`resolve`/`install` below), so swapping HOME
3796
+ # alone let an INHERITED PI_CODING_AGENT_DIR win and the consumer's user-scope registration land
3797
+ # outside the sandbox (#125, measured 2026-10-01: an inherited agent dir received the npm
3798
+ # consumer's settings.json while the gate looked under the temp HOME). Every root is named here,
3799
+ # none is left to the caller's environment. One NAME=value per line; read with mapfile.
3800
+ pack_install_consumer_env() {
3801
+ local h="$1"
3802
+ printf '%s\n' \
3803
+ "HOME=$h" \
3804
+ "XDG_CONFIG_HOME=$h/.config" \
3805
+ "XDG_DATA_HOME=$h/.local/share" \
3806
+ "XDG_STATE_HOME=$h/.local/state" \
3807
+ "XDG_CACHE_HOME=$h/.cache" \
3808
+ "PI_CODING_AGENT_DIR=$h/.pi/agent"
3809
+ }
3810
+
3811
+ # Self-test of the consumer roots, snapshot-safe like the pin matcher: under a POISONED inherited
3812
+ # environment every root a consumer subcall sees must sit inside the consumer home. Runs first
3813
+ # inside check-pack-install too, so the heavy gate cannot proceed on a leaking root set.
3814
+ check_pack_consumer_roots() {
3815
+ local h="/pack-consumer-home" roots=() seen line
3816
+ mapfile -t roots < <(pack_install_consumer_env "$h")
3817
+ seen=$(env HOME=/poisoned/home XDG_CONFIG_HOME=/poisoned/config XDG_DATA_HOME=/poisoned/data \
3818
+ XDG_STATE_HOME=/poisoned/state XDG_CACHE_HOME=/poisoned/cache PI_CODING_AGENT_DIR=/poisoned/agent \
3819
+ "${roots[@]}" sh -c 'printf "%s\n" "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" "$PI_CODING_AGENT_DIR"')
3820
+ while IFS= read -r line; do
3821
+ case "$line" in
3822
+ "$h" | "$h"/*) ;;
3823
+ *)
3824
+ fail "[QK:PACK-INSTALL-CONSUMER-ROOTS-CONTAINED] every writable root of a consumer subcall must be inside its own consumer home ($h) whatever the caller inherited — got: $(printf '%s' "$seen" | tr '\n' ' ')"
3825
+ return 1
3826
+ ;;
3827
+ esac
3828
+ done <<<"$seen"
3829
+ echo "[check-pack-consumer-roots] ok — HOME, the four XDG roots and PI_CODING_AGENT_DIR of a consumer subcall all resolve inside the consumer home under a poisoned inherited environment"
3830
+ }
3831
+
3832
+ # Hand-built oracle for the REAL consumer run: check-pack-install exports a sentinel agent dir for
3833
+ # its whole body, standing in for whatever a caller inherits; a subcall that falls back to it
3834
+ # writes there, and this names the stage that did.
3835
+ pack_install_sentinel_untouched() {
3836
+ local sentinel="$1" stage="$2" found
3837
+ found=$(find "$sentinel" -mindepth 1 2>/dev/null | head -5)
3838
+ if [ -n "$found" ]; then
3839
+ fail "[QK:PACK-INSTALL-INHERITED-AGENTDIR-UNTOUCHED] $stage wrote into the inherited PI_CODING_AGENT_DIR instead of its own consumer sandbox:"
3840
+ printf '%s\n' "$found" | sed 's/^/ /' >&2
3841
+ return 1
3842
+ fi
3843
+ }
3844
+
3760
3845
  _check_pack_install_impl() {
3761
3846
  check_pack_pin_matcher || return 1
3847
+ check_pack_consumer_roots || return 1
3762
3848
 
3763
3849
  # Heavy publish gate. Runs the remaining three checks in #13's
3764
3850
  # publish checklist that check_pack (dry-run only) does not cover:
@@ -3797,6 +3883,11 @@ _check_pack_install_impl() {
3797
3883
  local pack_tmp
3798
3884
  pack_tmp=$(mktemp -d -t entwurf-pack.XXXXXX)
3799
3885
  tgz_path="${pack_tmp}/${tgz_name}"
3886
+ # Stand-in for an inherited agent dir, scoped to this function: no consumer subcall may fall
3887
+ # back to it (pack_install_sentinel_untouched). It also keeps the caller's own one out of reach.
3888
+ local agentdir_sentinel="${pack_tmp}/inherited-agent-dir"
3889
+ mkdir -p "$agentdir_sentinel"
3890
+ local -x PI_CODING_AGENT_DIR="$agentdir_sentinel"
3800
3891
 
3801
3892
  # 0.12.1 C — stale-dist guard. `tsc` emit does NOT prune orphaned files from
3802
3893
  # outDir, and `files: ["mcp/"]` would carry any leftover dist file into the
@@ -4027,18 +4118,24 @@ _check_pack_install_impl() {
4027
4118
  # pi-agent-core incident. The list moves WITH the floor and is re-measured at
4028
4119
  # each bump, never pruned on a single quiet minor.
4029
4120
  # The verified floor is 0.86.0 as of 2026-09-20.
4030
- echo "[check-pack-install] pnpm add into $tmp (with 0.87.x peers + chord + typebox)"
4121
+ # `[측정 2026-09-30]` at 0.99.1 the constellation GREW: pi-coding-agent now declares
4122
+ # @earendil-works/pi-codemode and @earendil-works/pi-mcp as runtime dependencies (the built-in
4123
+ # codemode and MCP extensions), so both are pinned beside the eight. pi-client and pi-protocol are
4124
+ # still published at 0.99.1 and keep their pins under the rule above.
4125
+ echo "[check-pack-install] pnpm add into $tmp (with 1.0.x peers + chord + codemode + mcp + typebox)"
4031
4126
  local install_log
4032
4127
  install_log=$(cd "$tmp" && pnpm add \
4033
4128
  "$tgz_path" \
4034
- "@earendil-works/pi-ai@0.87.1" \
4035
- "@earendil-works/pi-coding-agent@0.87.1" \
4036
- "@earendil-works/pi-tui@0.87.1" \
4037
- "@earendil-works/pi-agent-core@0.87.1" \
4038
- "@earendil-works/pi-client@0.87.1" \
4039
- "@earendil-works/pi-protocol@0.87.1" \
4040
- "@earendil-works/pi-telemetry@0.87.1" \
4041
- "@earendil-works/chord@0.87.1" \
4129
+ "@earendil-works/pi-ai@1.0.0" \
4130
+ "@earendil-works/pi-coding-agent@1.0.0" \
4131
+ "@earendil-works/pi-tui@1.0.0" \
4132
+ "@earendil-works/pi-agent-core@1.0.0" \
4133
+ "@earendil-works/pi-client@1.0.0" \
4134
+ "@earendil-works/pi-protocol@1.0.0" \
4135
+ "@earendil-works/pi-telemetry@1.0.0" \
4136
+ "@earendil-works/chord@1.0.0" \
4137
+ "@earendil-works/pi-codemode@1.0.0" \
4138
+ "@earendil-works/pi-mcp@1.0.0" \
4042
4139
  "typebox@latest" \
4043
4140
  --ignore-workspace --ignore-scripts 2>&1) || {
4044
4141
  fail "[check-pack-install] pnpm add failed:"
@@ -4054,11 +4151,11 @@ _check_pack_install_impl() {
4054
4151
  local leaked_pi
4055
4152
  leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
4056
4153
  if [ -n "$leaked_pi" ]; then
4057
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.87.1):"
4154
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.0.0):"
4058
4155
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
4059
4156
  return 1
4060
4157
  fi
4061
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.87.1 (chord included)"
4158
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.0.0 (chord included)"
4062
4159
 
4063
4160
  # Resolve the installed package.json and confirm pi.extensions
4064
4161
  # arrived intact. If pi.extensions is empty or missing, the
@@ -4271,11 +4368,14 @@ _check_pack_install_impl() {
4271
4368
  ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
4272
4369
  return 1
4273
4370
  fi
4274
- wire_log=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
4371
+ local consumer_env=()
4372
+ mapfile -t consumer_env < <(pack_install_consumer_env "$npmhome")
4373
+ wire_log=$(env "${consumer_env[@]}" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
4275
4374
  fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
4276
4375
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
4277
4376
  return 1
4278
4377
  }
4378
+ pack_install_sentinel_untouched "$agentdir_sentinel" "npm-managed run.sh install" || return 1
4279
4379
  if [ ! -f "$npmproj/.pi/settings.json" ]; then
4280
4380
  fail "[check-pack-install] npm-managed run.sh install did not write settings.json:"
4281
4381
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
@@ -4313,7 +4413,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
4313
4413
  # a KNOWN flag and the entwurf provider must load, sourced only from user scope.
4314
4414
  # Before the fix this printed "Unknown options: --entwurf-control".
4315
4415
  local foreign_out
4316
- foreign_out=$(cd "$tmp" && HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" PI_CODING_AGENT_DIR="$npmhome/.pi/agent" "$pi_bin" --entwurf-control --list-models entwurf 2>&1) || {
4416
+ foreign_out=$(cd "$tmp" && env "${consumer_env[@]}" "$pi_bin" --entwurf-control --list-models entwurf 2>&1) || {
4317
4417
  fail "[check-pack-install] foreign-cwd --entwurf-control smoke failed (user-scope citizen not loading?):"
4318
4418
  echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
4319
4419
  return 1
@@ -4338,7 +4438,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
4338
4438
  }
4339
4439
  npm2_pkg="$npmroot2/node_modules/@junghanacs/entwurf"
4340
4440
  set +e
4341
- two_root_out=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm2_pkg/run.sh" install "$npmproj2" 2>&1)
4441
+ two_root_out=$(env "${consumer_env[@]}" "$npm2_pkg/run.sh" install "$npmproj2" 2>&1)
4342
4442
  two_root_rc=$?
4343
4443
  set -e
4344
4444
  if [ "$two_root_rc" -eq 0 ] || ! grep -q "takeover-user-scope" <<<"$two_root_out"; then
@@ -4359,7 +4459,7 @@ sys.exit(0 if ok_a and not bad_b else 1)
4359
4459
  return 1
4360
4460
  fi
4361
4461
  set +e
4362
- two_root_out=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm2_pkg/run.sh" takeover-user-scope 2>&1)
4462
+ two_root_out=$(env "${consumer_env[@]}" "$npm2_pkg/run.sh" takeover-user-scope 2>&1)
4363
4463
  two_root_rc=$?
4364
4464
  set -e
4365
4465
  if [ "$two_root_rc" -ne 0 ] || ! grep -q "takeover: user-scope entwurf registration moved" <<<"$two_root_out"; then
@@ -4391,7 +4491,7 @@ sys.exit(0 if st.get('installerRoot','').rstrip('/')== '$npm2_pkg' else 1)
4391
4491
  return 1
4392
4492
  fi
4393
4493
  set +e
4394
- two_root_out=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm_pkg/run.sh" remove-user-scope 2>&1)
4494
+ two_root_out=$(env "${consumer_env[@]}" "$npm_pkg/run.sh" remove-user-scope 2>&1)
4395
4495
  two_root_rc=$?
4396
4496
  set -e
4397
4497
  if [ "$two_root_rc" -eq 0 ] || ! python3 -c "
@@ -4404,6 +4504,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
4404
4504
  echo "$two_root_out" | tail -8 | sed 's/^/ /' >&2
4405
4505
  return 1
4406
4506
  fi
4507
+ pack_install_sentinel_untouched "$agentdir_sentinel" "two-root ownership row (install / takeover-user-scope / remove-user-scope)" || return 1
4407
4508
  echo "[check-pack-install] two-root ownership row pass (second root: normal install refused naming takeover-user-scope; explicit takeover moved entry + provider installerRoot old->new; old root's inverse refused)"
4408
4509
 
4409
4510
  # Installed-package aggregate `setup` row (#86 C1, review blocker 2026-08-26):
@@ -5153,7 +5254,9 @@ process.stdout.write(resolveCodexDefaultSocketPath(process.env));
5153
5254
  JSON
5154
5255
  local pc_seed_sha
5155
5256
  pc_seed_sha=$(sha256sum "$pc_store/$pc_record" | cut -d' ' -f1)
5156
- local pc_env=(HOME="$pc_home" XDG_DATA_HOME="$pc_home/.local/share" XDG_STATE_HOME="$pc_home/.local/state" XDG_CACHE_HOME="$pc_home/.cache" CLAUDE_CONFIG_DIR="$pc_cfg" FAKE_CLAUDE_LOG="$pc_claude_log" PATH="$fake_claude_dir:$npmroot/node_modules/.bin:$PATH")
5257
+ # `-u`: the default resolution is only proven if no inherited agent dir can pre-empt it
5258
+ # (check-pack-install exports a sentinel one for its whole body).
5259
+ local pc_env=(-u PI_CODING_AGENT_DIR HOME="$pc_home" XDG_DATA_HOME="$pc_home/.local/share" XDG_STATE_HOME="$pc_home/.local/state" XDG_CACHE_HOME="$pc_home/.cache" CLAUDE_CONFIG_DIR="$pc_cfg" FAKE_CLAUDE_LOG="$pc_claude_log" PATH="$fake_claude_dir:$npmroot/node_modules/.bin:$PATH")
5157
5260
  local pc_before pc_after pc_out pc_rc
5158
5261
  pc_before=$(cd "$pc_home" && find . -type f -exec sha256sum {} + 2>/dev/null | sort)
5159
5262
 
@@ -5338,6 +5441,7 @@ JSON
5338
5441
  fi
5339
5442
  echo "[check-pack-install] installed doctor dispatch lock pass (store-scan → dist JS, v2-surface deferred)"
5340
5443
 
5444
+ pack_install_sentinel_untouched "$agentdir_sentinel" "a later installed-consumer cell" || return 1
5341
5445
  ok "[check-pack-install] publish install smoke pass"
5342
5446
  return 0
5343
5447
  }
@@ -5563,15 +5667,15 @@ setup_mode() {
5563
5667
  }
5564
5668
 
5565
5669
  # Supported pi range, derived at runtime from the package.json devDependencies
5566
- # pin (the SSOT check-dep-versions binds: peer range == `>=<pin> <0.<minor+1>`).
5670
+ # pin (the SSOT check-dep-versions binds: peer range == `>=<pin> <<major>.<minor+1>`).
5567
5671
  # Never retyped here as a second literal.
5568
5672
  pi_supported_range() {
5569
5673
  node -e '
5570
5674
  const pkg = require(process.argv[1]);
5571
5675
  const pin = pkg.devDependencies?.["@earendil-works/pi-coding-agent"];
5572
- if (!/^0\.\d+\.\d+$/.test(pin ?? "")) { console.error(`unparseable pi pin: ${pin}`); process.exit(1); }
5573
- const [, min] = pin.split(".").map(Number);
5574
- console.log(`>=${pin} <0.${min + 1}`);
5676
+ if (!/^\d+\.\d+\.\d+$/.test(pin ?? "")) { console.error(`unparseable pi pin: ${pin}`); process.exit(1); }
5677
+ const [maj, min] = pin.split(".").map(Number);
5678
+ console.log(`>=${pin} <${maj}.${min + 1}`);
5575
5679
  ' "$REPO_DIR/package.json"
5576
5680
  }
5577
5681
 
@@ -6655,6 +6759,9 @@ case "$cmd" in
6655
6759
  check-entwurf-resume-args)
6656
6760
  check_entwurf_resume_args
6657
6761
  ;;
6762
+ check-acp-foreground-only)
6763
+ check_acp_foreground_only
6764
+ ;;
6658
6765
  check-resume-launch-identity)
6659
6766
  check_resume_launch_identity
6660
6767
  ;;
@@ -7617,6 +7724,9 @@ case "$cmd" in
7617
7724
  check-acp-prompt-lifecycle)
7618
7725
  check_acp_prompt_lifecycle
7619
7726
  ;;
7727
+ check-acp-vendor-rejection)
7728
+ check_acp_vendor_rejection
7729
+ ;;
7620
7730
  check-acp-launch-namespace)
7621
7731
  check_acp_launch_namespace
7622
7732
  ;;
@@ -7662,6 +7772,17 @@ case "$cmd" in
7662
7772
  shift || true
7663
7773
  run_ts scripts/check-codex-bridge-identity.ts "$@"
7664
7774
  ;;
7775
+ check-pi-bridge-sender)
7776
+ shift || true
7777
+ run_ts scripts/check-pi-bridge-sender.ts "$@"
7778
+ ;;
7779
+ check-pi-mcp-register)
7780
+ check_pi_mcp_register
7781
+ ;;
7782
+ check-pi-mcp-bridge)
7783
+ shift || true
7784
+ run_ts scripts/check-pi-mcp-bridge.ts "$@"
7785
+ ;;
7665
7786
  check-codex-native-push)
7666
7787
  shift || true
7667
7788
  run_ts scripts/check-codex-native-push.ts "$@"
@@ -7688,6 +7809,9 @@ case "$cmd" in
7688
7809
  check-pack-pin-matcher)
7689
7810
  check_pack_pin_matcher
7690
7811
  ;;
7812
+ check-pack-consumer-roots)
7813
+ check_pack_consumer_roots
7814
+ ;;
7691
7815
  check-pack-install)
7692
7816
  check_pack_install
7693
7817
  ;;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * check-entwurf-peers-surface — deterministic gate for the MCP + pi-native `entwurf_peers`
2
+ * check-entwurf-peers-surface — deterministic gate for the `entwurf_peers` (one MCP surface since #125)
3
3
  * RENDER/PAYLOAD layer (0.11 Stage 0 step 4, slice 4c; #50 C4 re-author). Drives the PURE
4
4
  * `renderEntwurfPeers` with a fabricated `EntwurfFactsResult` (no IO) and proves
5
5
  * the surface contract (GPi + Fable 수렴 + the C4 demotion):
@@ -17,9 +17,9 @@
17
17
  * dropped),
18
18
  * - diagnostics sharing one (kind + message) AGGREGATE in text (F8) — including
19
19
  * record-less-socket groups, which share a message per liveness,
20
- * - WIRING guard (Fable e②/6): the MCP handler and pi-native tool call
21
- * listEntwurfFacts + renderEntwurfPeers; getLiveSessions and the
22
- * `/entwurf-sessions` socket-scan command stay gone from both surfaces.
20
+ * - WIRING guard (Fable e②/6): the MCP handler calls listEntwurfFacts +
21
+ * renderEntwurfPeers; getLiveSessions and the `/entwurf-sessions` socket-scan
22
+ * command stay gone from the bridge AND the pi extension.
23
23
  *
24
24
  * No IO — the facts are fabricated; only the wiring guard reads the bridge source
25
25
  * as text (a static assertion, not an execution).
@@ -252,31 +252,24 @@ function main(): void {
252
252
  ok("the aggregated record-less line samples gids", agg.text.includes(gids[0] as string));
253
253
  }
254
254
 
255
- // ── WIRING guard: both surfaces call the provider+render; the socket-scan lane is gone ──
255
+ // ── WIRING guard: the bridge calls the provider+render; the socket-scan lane is gone ──
256
+ // #125: the pi-native entwurf_peers copy is retired — a pi session reads the bridge's own
257
+ // surface through Pi's built-in MCP — so its wiring halves left with it. The pi file is still
258
+ // held to the lanes it must NOT grow back.
256
259
  {
257
260
  const here = path.dirname(fileURLToPath(import.meta.url));
258
261
  const bridgeSrc = readFileSync(path.join(here, "..", "mcp", "entwurf-bridge", "src", "index.ts"), "utf8");
259
262
  const nativeSrc = readFileSync(path.join(here, "..", "pi-extensions", "entwurf-control.ts"), "utf8");
260
263
  ok("wiring: bridge calls listEntwurfFacts(", bridgeSrc.includes("listEntwurfFacts("));
261
264
  ok("wiring: bridge calls renderEntwurfPeers(", bridgeSrc.includes("renderEntwurfPeers("));
262
- ok("wiring: native pi tool calls listEntwurfFacts(", nativeSrc.includes("listEntwurfFacts("));
263
- ok("wiring: native pi tool calls renderEntwurfPeers(", nativeSrc.includes("renderEntwurfPeers("));
264
265
  ok(
265
266
  "#112 wiring: MCP observation uses the render limit",
266
267
  bridgeSrc.includes("observationLimit: ENTWURF_PEERS_RENDER_LIMIT"),
267
268
  );
268
- ok(
269
- "#112 wiring: native pi observation uses the render limit",
270
- nativeSrc.includes("observationLimit: render.ENTWURF_PEERS_RENDER_LIMIT"),
271
- );
272
269
  ok(
273
270
  "wiring: bridge does not paste full JSON payload into human text",
274
271
  !bridgeSrc.includes("JSON.stringify(payload)"),
275
272
  );
276
- ok(
277
- "wiring: native pi tool does not paste full JSON payload into human text",
278
- !nativeSrc.includes("JSON.stringify(payload)"),
279
- );
280
273
  // `\bname\s*\(` catches a definition OR a call (tolerating a space before the
281
274
  // paren, GPi Q4); a bare prose mention in a removal-note comment (no paren) is
282
275
  // allowed — the guard targets the second scan, not the word.
@@ -495,17 +495,13 @@ ok(
495
495
  /origin:\s*"meta-session"/.test(metaBody) && /replyable:\s*self\.replyable/.test(metaBody),
496
496
  );
497
497
 
498
- // ── SE-1 2e-a: pi-native surface derives pi-session replyability the SAME way ──
499
- // entwurf-control.ts is a root-tsc emit surface, so it reaches the self-address fence via a
500
- // non-literal dynamic import (never a static `.ts` import — TS5097), then decorates the
501
- // sender with computeSelfAddressability + canonical socket existsSync (no hardcoded true).
498
+ // ── pi-session replyability has ONE computation now (#125) ──
499
+ // The pi-native sender decoration (`decoratePiSenderAddressability`) left with the native verbs: a
500
+ // pi session reaches entwurf_v2/entwurf_self through the bridge, whose `buildStrictPiSenderEnvelope`
501
+ // derives replyability from the same computeSelfAddressability + canonical-socket existsSync and is
502
+ // pinned above. What the pi file still takes from this fence is the not-a-citizen notice below,
503
+ // reached through the non-literal dynamic import that keeps the `.ts` fence out of root tsc.
502
504
  const nativeSrc = readFileSync(path.join(REPO_DIR, "pi-extensions", "entwurf-control.ts"), "utf8");
503
- ok(
504
- "pi-native: decoratePiSenderAddressability derives replyable from computeSelfAddressability + canonical existsSync",
505
- /function\s+decoratePiSenderAddressability/.test(nativeSrc) &&
506
- /computeSelfAddressability/.test(nativeSrc) &&
507
- /existsSync\s*\(\s*getSocketPath/.test(nativeSrc),
508
- );
509
505
  ok(
510
506
  "pi-native: reaches the self-address fence via non-literal dynamic import (no static import; TS5097)",
511
507
  /const ENTWURF_SELF_ADDRESS_MODULE\s*=/.test(nativeSrc) &&
@@ -555,7 +551,7 @@ ok(
555
551
  // a mutant to move it into, and passing a constant would make the decision above unreachable.
556
552
  const noticeBody = nativeSrc.slice(
557
553
  nativeSrc.indexOf("async function noticeUncitizenedSession"),
558
- nativeSrc.indexOf("function shouldRegisterControlTools"),
554
+ nativeSrc.indexOf("// Extension Export", nativeSrc.indexOf("async function noticeUncitizenedSession")),
559
555
  );
560
556
  ok(
561
557
  "pi-native: the not-a-citizen notice is decided from the REAL flag, on the UI only, never on stderr " +