@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.
Files changed (77) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +1 -1
  3. package/CHANGELOG.md +73 -0
  4. package/DELIVERY.md +1 -1
  5. package/README.md +178 -674
  6. package/VERIFY.md +16 -15
  7. package/demo/README.md +1 -1
  8. package/docs/acp-backend-rail.md +6 -2
  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 +12 -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 +11 -11
  24. package/pi-extensions/entwurf-control.ts +50 -637
  25. package/pi-extensions/lib/acp/backend-adapter.ts +13 -1
  26. package/pi-extensions/lib/entwurf-fact-provider.ts +2 -2
  27. package/pi-extensions/lib/entwurf-mcp-server.ts +74 -0
  28. package/pi-extensions/lib/entwurf-peers-render.ts +1 -1
  29. package/pi-extensions/lib/entwurf-v2-decider.ts +2 -2
  30. package/pi-extensions/lib/entwurf-v2-mailbox.ts +2 -2
  31. package/pi-extensions/lib/entwurf-v2-production.ts +3 -3
  32. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -5
  33. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +3 -3
  34. package/pi-extensions/lib/fresh-call-composition.ts +13 -6
  35. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  36. package/pi-extensions/lib/mux-fresh-call.ts +3 -3
  37. package/run.sh +163 -50
  38. package/scripts/check-entwurf-peers-surface.ts +8 -15
  39. package/scripts/check-entwurf-self-address.ts +7 -11
  40. package/scripts/check-entwurf-v2-surface.ts +20 -112
  41. package/scripts/check-fresh-call-dispatch.ts +12 -13
  42. package/scripts/check-gate-qualification.ts +248 -4
  43. package/scripts/check-install-surface.ts +3 -2
  44. package/scripts/check-mux-resume-call.ts +24 -35
  45. package/scripts/check-pi-bridge-sender.ts +130 -0
  46. package/scripts/check-pi-mcp-bridge.ts +251 -0
  47. package/scripts/lib/codex-fresh-live-protocol.ts +17 -5
  48. package/scripts/lib/codex-fresh-source-receipts.ts +21 -2
  49. package/scripts/lib/mutation-qualify.ts +269 -44
  50. package/scripts/lib/pi-mcp-bridge-probe.mjs +170 -0
  51. package/scripts/meta-bridge-doctor.sh +16 -5
  52. package/scripts/mutants/acp-foreground-only.json +17 -0
  53. package/scripts/mutants/codex-native.json +11 -11
  54. package/scripts/mutants/control-send-receipt.json +6 -4
  55. package/scripts/mutants/copilot-birth.json +2 -1
  56. package/scripts/mutants/fresh-call-dispatch.json +3 -14
  57. package/scripts/mutants/gate-qualification.json +59 -0
  58. package/scripts/mutants/mux-fresh-call.json +43 -45
  59. package/scripts/mutants/mux-resume-call.json +11 -35
  60. package/scripts/mutants/pack-install.json +15 -4
  61. package/scripts/mutants/pi-bridge-sender.json +54 -0
  62. package/scripts/mutants/pi-floor.json +6 -2
  63. package/scripts/mutants/pi-mcp-bridge.json +36 -0
  64. package/scripts/mutants/pi-mcp-register.json +51 -0
  65. package/scripts/mutants/v2-surface.json +6 -6
  66. package/scripts/resident-rpc-drive.ts +5 -1
  67. package/scripts/smoke-acp-bundled-mcp-live.ts +3 -1
  68. package/scripts/smoke-acp-cortex-live.ts +3 -1
  69. package/scripts/smoke-acp-session-reuse-live.ts +29 -12
  70. package/scripts/smoke-acp-socket-citizen-live.ts +3 -1
  71. package/scripts/smoke-acp-v2-send-live.ts +3 -1
  72. package/scripts/smoke-codex-fresh-live.ts +20 -1
  73. package/scripts/smoke-entwurf-chain-live.ts +6 -2
  74. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -1
  75. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  76. package/scripts/smoke-mux-lifecycle-live.ts +1 -1
  77. 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,24 @@ 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_pi_mcp_register() {
1352
+ # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1353
+ # MUTANT EXECUTION COORDINATE for scripts/mutants/pi-mcp-register.json (same reasoning as
1354
+ # check_entwurf_resume_args above): run_vitest is what emits the structured failed-test titles a
1355
+ # kill is attributed from. It sits in check:hermetic, not check:contracts, because the subject
1356
+ # registers the COMPILED bridge and refuses by name when `build-bridge` has not run.
1357
+ section "pi MCP registration after birth (mutant execution coordinate)"
1358
+ run_vitest pi-extensions/entwurf-control.test.ts
1359
+ }
1360
+
1339
1361
  check_mux_launcher_fence() {
1340
1362
  # Deterministic gate for the shared operator-launcher fence (issue #67):
1341
1363
  # scripts/lib/claude-launcher-fence.ts plus its wiring into BOTH mux LIVE smokes. Replants the
@@ -2185,11 +2207,11 @@ assert.equal(peerTui, piAi,
2185
2207
  // an open `>=` floor is exactly how the next installer re-acquires the drift.
2186
2208
  // The floor is also the HARD MINIMUM a consumer install resolves: at `>=0.86.0`
2187
2209
  // an existing 0.85.1 host is upgraded, not kept.
2188
- // Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.86.0 <0.87`).
2210
+ // Expected shape: `>=<devDep> <<major>.<minor+1>` (e.g. `>=0.86.0 <0.87`, `>=1.0.0 <1.1`).
2211
+ // Pi 1.0 did not change the rule: an unverified minor is still unverified, so the ceiling
2212
+ // stays the next minor of the pin's own major.
2189
2213
  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}`;
2214
+ const expectedPeer = `>=${piAi} <${piMaj}.${piMin + 1}`;
2193
2215
  const peerDepAi = pkg.peerDependencies?.['@earendil-works/pi-ai'];
2194
2216
  const peerDepCoding = pkg.peerDependencies?.['@earendil-works/pi-coding-agent'];
2195
2217
  const peerDepTui = pkg.peerDependencies?.['@earendil-works/pi-tui'];
@@ -2258,13 +2280,13 @@ function stripBumpLedger(file, text) {
2258
2280
  let rangeDecls = 0, exactDecls = 0;
2259
2281
  for (const file of BASELINE_DOCS) {
2260
2282
  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)) {
2283
+ // Closed-range declarations: `>=<floor> <<major>.<ceiling>` (spaces optional).
2284
+ for (const [decl, floor, ceilMajor, ceilMinor] of text.matchAll(/>=\s?(\d+\.\d+\.\d+)\s?<\s?(\d+)\.(\d+)/g)) {
2263
2285
  rangeDecls++;
2264
2286
  assert.equal(floor, piAi,
2265
2287
  `${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})`);
2288
+ assert.equal(`${ceilMajor}.${ceilMinor}`, `${piMaj}.${piMin + 1}`,
2289
+ `${file}: declared pi ceiling in "${decl}" must be the next minor (${piMaj}.${piMin + 1})`);
2268
2290
  }
2269
2291
  // Exact install pins: `@earendil-works/pi-<pkg>@<version>`.
2270
2292
  for (const [decl, ver] of text.matchAll(/@earendil-works\/pi-(?:ai|coding-agent|tui)@(\d+\.\d+\.\d+)/g)) {
@@ -2295,12 +2317,14 @@ for (const [file, re, shape] of PROSE_DECLS) {
2295
2317
  // ONE assertion, because the claim token may appear exactly once in a gate source.
2296
2318
  // Every way the floor can fragment is collected first and named individually in the
2297
2319
  // failure, so "which surface" is never lost to the single-token rule.
2298
- const nextFloorRange = `>=${piAi} <0.${piMin + 1}`;
2320
+ const nextFloorRange = `>=${piAi} <${piMaj}.${piMin + 1}`;
2299
2321
  const floorGaps = [];
2300
2322
  const PI_CONSTELLATION = [
2301
2323
  '@earendil-works/pi-ai', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui',
2302
2324
  '@earendil-works/pi-agent-core', '@earendil-works/pi-client', '@earendil-works/pi-protocol',
2303
2325
  '@earendil-works/pi-telemetry', '@earendil-works/chord',
2326
+ // `[측정 2026-09-30]` 0.99.1: pi-coding-agent's runtime deps gained the built-in extensions' packages.
2327
+ '@earendil-works/pi-codemode', '@earendil-works/pi-mcp',
2304
2328
  ];
2305
2329
  const sameSet = (got, want, where) => {
2306
2330
  const missing = want.filter((n) => !got.includes(n));
@@ -2343,9 +2367,9 @@ if (!matcherPin) {
2343
2367
  // measurement in docs/mux-launch-rail.md, for one) are deliberately NOT scanned:
2344
2368
  // they record what was true then, and rewriting them would falsify evidence.
2345
2369
  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"],
2370
+ ['plugins/herdr/README.md', /npm install -g "@earendil-works\/pi-coding-agent@(>=[\d.]+ <\d+\.\d+)"/, 'the herdr install line'],
2371
+ ['docs/mux-launch-rail.md', /현 supported range 는 `(>=[\d.]+ <\d+\.\d+)`/, "mux-launch-rail's current supported range"],
2372
+ ['docs/acp-backend-rail.md', /devDep exact `(\d+\.\d+\.\d+)`, peer `(>=[\d.]+ <\d+\.\d+)`/, "the ACP support row's exact+range pair"],
2349
2373
  ];
2350
2374
  for (const [file, re, shape] of CURRENT_CONTRACT_DECLS) {
2351
2375
  const m = readFileSync(file, 'utf8').match(re);
@@ -2864,13 +2888,14 @@ if (typeof FLOOR !== 'string' || !/^\d+\.\d+\.\d+$/.test(FLOOR)) {
2864
2888
  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
2889
  process.exit(1);
2866
2890
  }
2867
- // The declared contract is a CLOSED range (`>=<devDep> <0.<minor+1>`, enforced on
2891
+ // The declared contract is a CLOSED range (`>=<devDep> <<major>.<minor+1>`, enforced on
2868
2892
  // package.json by check-dep-versions), so the runtime check must be closed too.
2869
2893
  // A floor-only comparison would bless a resolved pi ABOVE the ceiling — and an
2870
2894
  // out-of-range pi is exactly the drift this cut exists to stop: 0.80.6 landed on
2871
2895
  // the dev box while the repo still declared 0.80.3, and every gate stayed green.
2872
2896
  // 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`;
2897
+ const [FLOOR_MAJOR, FLOOR_MINOR] = FLOOR.split('.').map(Number);
2898
+ const CEILING = `${FLOOR_MAJOR}.${FLOOR_MINOR + 1}.0`;
2874
2899
  const cmp = (a, b) => {
2875
2900
  const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
2876
2901
  for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
@@ -3710,7 +3735,7 @@ check_pack() {
3710
3735
  # which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
3711
3736
  # below so a future suffix change cannot pass vacuously.)
3712
3737
  pack_install_leaked_pi() {
3713
- grep '^@earendil-works+' | grep -Ev '@0\.87\.1(_|$)' || true
3738
+ grep '^@earendil-works+' | grep -Ev '@1\.0\.0(_|$)' || true
3714
3739
  }
3715
3740
 
3716
3741
  # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
@@ -3730,35 +3755,88 @@ check_pack_pin_matcher() {
3730
3755
  # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`0.86.0-beta.1`,
3731
3756
  # the prerelease shape that an unanchored match would bless) and an off-pin version.
3732
3757
  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>}"
3758
+ '@earendil-works+pi-ai@1.0.0' \
3759
+ '@earendil-works+pi-ai@1.0.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
3760
+ '@earendil-works+pi-ai@1.0.0_ws@8.21.3' \
3761
+ '@earendil-works+pi-ai@1.0.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
3762
+ '@earendil-works+pi-ai@1.0.0-beta.1' \
3763
+ '@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
3764
+ if [ "$matcher_probe" != '@earendil-works+pi-ai@1.0.0-beta.1
3765
+ @earendil-works+pi-agent-core@0.87.1' ]; then
3766
+ 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
3767
  return 1
3743
3768
  fi
3744
3769
 
3745
3770
  # 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
3771
+ # (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
3772
  # back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
3748
3773
  matcher_probe=$(printf '%s\n' \
3774
+ '@earendil-works+chord@1.0.0' \
3749
3775
  '@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>}"
3776
+ '@earendil-works+pi-ai@1.0.0' | pack_install_leaked_pi)
3777
+ if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
3778
+ 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
3779
  return 1
3755
3780
  fi
3756
3781
 
3757
3782
  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
3783
  }
3759
3784
 
3785
+ # The writable roots of ONE consumer sandbox, shared by every consumer subcall of
3786
+ # check-pack-install that drives an installed `run.sh` writer. The writers resolve their agent
3787
+ # dir as `${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}` (`resolve`/`install` below), so swapping HOME
3788
+ # alone let an INHERITED PI_CODING_AGENT_DIR win and the consumer's user-scope registration land
3789
+ # outside the sandbox (#125, measured 2026-10-01: an inherited agent dir received the npm
3790
+ # consumer's settings.json while the gate looked under the temp HOME). Every root is named here,
3791
+ # none is left to the caller's environment. One NAME=value per line; read with mapfile.
3792
+ pack_install_consumer_env() {
3793
+ local h="$1"
3794
+ printf '%s\n' \
3795
+ "HOME=$h" \
3796
+ "XDG_CONFIG_HOME=$h/.config" \
3797
+ "XDG_DATA_HOME=$h/.local/share" \
3798
+ "XDG_STATE_HOME=$h/.local/state" \
3799
+ "XDG_CACHE_HOME=$h/.cache" \
3800
+ "PI_CODING_AGENT_DIR=$h/.pi/agent"
3801
+ }
3802
+
3803
+ # Self-test of the consumer roots, snapshot-safe like the pin matcher: under a POISONED inherited
3804
+ # environment every root a consumer subcall sees must sit inside the consumer home. Runs first
3805
+ # inside check-pack-install too, so the heavy gate cannot proceed on a leaking root set.
3806
+ check_pack_consumer_roots() {
3807
+ local h="/pack-consumer-home" roots=() seen line
3808
+ mapfile -t roots < <(pack_install_consumer_env "$h")
3809
+ seen=$(env HOME=/poisoned/home XDG_CONFIG_HOME=/poisoned/config XDG_DATA_HOME=/poisoned/data \
3810
+ XDG_STATE_HOME=/poisoned/state XDG_CACHE_HOME=/poisoned/cache PI_CODING_AGENT_DIR=/poisoned/agent \
3811
+ "${roots[@]}" sh -c 'printf "%s\n" "$HOME" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME" "$PI_CODING_AGENT_DIR"')
3812
+ while IFS= read -r line; do
3813
+ case "$line" in
3814
+ "$h" | "$h"/*) ;;
3815
+ *)
3816
+ 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' ' ')"
3817
+ return 1
3818
+ ;;
3819
+ esac
3820
+ done <<<"$seen"
3821
+ 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"
3822
+ }
3823
+
3824
+ # Hand-built oracle for the REAL consumer run: check-pack-install exports a sentinel agent dir for
3825
+ # its whole body, standing in for whatever a caller inherits; a subcall that falls back to it
3826
+ # writes there, and this names the stage that did.
3827
+ pack_install_sentinel_untouched() {
3828
+ local sentinel="$1" stage="$2" found
3829
+ found=$(find "$sentinel" -mindepth 1 2>/dev/null | head -5)
3830
+ if [ -n "$found" ]; then
3831
+ fail "[QK:PACK-INSTALL-INHERITED-AGENTDIR-UNTOUCHED] $stage wrote into the inherited PI_CODING_AGENT_DIR instead of its own consumer sandbox:"
3832
+ printf '%s\n' "$found" | sed 's/^/ /' >&2
3833
+ return 1
3834
+ fi
3835
+ }
3836
+
3760
3837
  _check_pack_install_impl() {
3761
3838
  check_pack_pin_matcher || return 1
3839
+ check_pack_consumer_roots || return 1
3762
3840
 
3763
3841
  # Heavy publish gate. Runs the remaining three checks in #13's
3764
3842
  # publish checklist that check_pack (dry-run only) does not cover:
@@ -3797,6 +3875,11 @@ _check_pack_install_impl() {
3797
3875
  local pack_tmp
3798
3876
  pack_tmp=$(mktemp -d -t entwurf-pack.XXXXXX)
3799
3877
  tgz_path="${pack_tmp}/${tgz_name}"
3878
+ # Stand-in for an inherited agent dir, scoped to this function: no consumer subcall may fall
3879
+ # back to it (pack_install_sentinel_untouched). It also keeps the caller's own one out of reach.
3880
+ local agentdir_sentinel="${pack_tmp}/inherited-agent-dir"
3881
+ mkdir -p "$agentdir_sentinel"
3882
+ local -x PI_CODING_AGENT_DIR="$agentdir_sentinel"
3800
3883
 
3801
3884
  # 0.12.1 C — stale-dist guard. `tsc` emit does NOT prune orphaned files from
3802
3885
  # outDir, and `files: ["mcp/"]` would carry any leftover dist file into the
@@ -4027,18 +4110,24 @@ _check_pack_install_impl() {
4027
4110
  # pi-agent-core incident. The list moves WITH the floor and is re-measured at
4028
4111
  # each bump, never pruned on a single quiet minor.
4029
4112
  # 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)"
4113
+ # `[측정 2026-09-30]` at 0.99.1 the constellation GREW: pi-coding-agent now declares
4114
+ # @earendil-works/pi-codemode and @earendil-works/pi-mcp as runtime dependencies (the built-in
4115
+ # codemode and MCP extensions), so both are pinned beside the eight. pi-client and pi-protocol are
4116
+ # still published at 0.99.1 and keep their pins under the rule above.
4117
+ echo "[check-pack-install] pnpm add into $tmp (with 1.0.x peers + chord + codemode + mcp + typebox)"
4031
4118
  local install_log
4032
4119
  install_log=$(cd "$tmp" && pnpm add \
4033
4120
  "$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" \
4121
+ "@earendil-works/pi-ai@1.0.0" \
4122
+ "@earendil-works/pi-coding-agent@1.0.0" \
4123
+ "@earendil-works/pi-tui@1.0.0" \
4124
+ "@earendil-works/pi-agent-core@1.0.0" \
4125
+ "@earendil-works/pi-client@1.0.0" \
4126
+ "@earendil-works/pi-protocol@1.0.0" \
4127
+ "@earendil-works/pi-telemetry@1.0.0" \
4128
+ "@earendil-works/chord@1.0.0" \
4129
+ "@earendil-works/pi-codemode@1.0.0" \
4130
+ "@earendil-works/pi-mcp@1.0.0" \
4042
4131
  "typebox@latest" \
4043
4132
  --ignore-workspace --ignore-scripts 2>&1) || {
4044
4133
  fail "[check-pack-install] pnpm add failed:"
@@ -4054,11 +4143,11 @@ _check_pack_install_impl() {
4054
4143
  local leaked_pi
4055
4144
  leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
4056
4145
  if [ -n "$leaked_pi" ]; then
4057
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.87.1):"
4146
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.0.0):"
4058
4147
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
4059
4148
  return 1
4060
4149
  fi
4061
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.87.1 (chord included)"
4150
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.0.0 (chord included)"
4062
4151
 
4063
4152
  # Resolve the installed package.json and confirm pi.extensions
4064
4153
  # arrived intact. If pi.extensions is empty or missing, the
@@ -4271,11 +4360,14 @@ _check_pack_install_impl() {
4271
4360
  ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
4272
4361
  return 1
4273
4362
  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) || {
4363
+ local consumer_env=()
4364
+ mapfile -t consumer_env < <(pack_install_consumer_env "$npmhome")
4365
+ wire_log=$(env "${consumer_env[@]}" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
4275
4366
  fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
4276
4367
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
4277
4368
  return 1
4278
4369
  }
4370
+ pack_install_sentinel_untouched "$agentdir_sentinel" "npm-managed run.sh install" || return 1
4279
4371
  if [ ! -f "$npmproj/.pi/settings.json" ]; then
4280
4372
  fail "[check-pack-install] npm-managed run.sh install did not write settings.json:"
4281
4373
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
@@ -4313,7 +4405,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
4313
4405
  # a KNOWN flag and the entwurf provider must load, sourced only from user scope.
4314
4406
  # Before the fix this printed "Unknown options: --entwurf-control".
4315
4407
  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) || {
4408
+ foreign_out=$(cd "$tmp" && env "${consumer_env[@]}" "$pi_bin" --entwurf-control --list-models entwurf 2>&1) || {
4317
4409
  fail "[check-pack-install] foreign-cwd --entwurf-control smoke failed (user-scope citizen not loading?):"
4318
4410
  echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
4319
4411
  return 1
@@ -4338,7 +4430,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
4338
4430
  }
4339
4431
  npm2_pkg="$npmroot2/node_modules/@junghanacs/entwurf"
4340
4432
  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)
4433
+ two_root_out=$(env "${consumer_env[@]}" "$npm2_pkg/run.sh" install "$npmproj2" 2>&1)
4342
4434
  two_root_rc=$?
4343
4435
  set -e
4344
4436
  if [ "$two_root_rc" -eq 0 ] || ! grep -q "takeover-user-scope" <<<"$two_root_out"; then
@@ -4359,7 +4451,7 @@ sys.exit(0 if ok_a and not bad_b else 1)
4359
4451
  return 1
4360
4452
  fi
4361
4453
  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)
4454
+ two_root_out=$(env "${consumer_env[@]}" "$npm2_pkg/run.sh" takeover-user-scope 2>&1)
4363
4455
  two_root_rc=$?
4364
4456
  set -e
4365
4457
  if [ "$two_root_rc" -ne 0 ] || ! grep -q "takeover: user-scope entwurf registration moved" <<<"$two_root_out"; then
@@ -4391,7 +4483,7 @@ sys.exit(0 if st.get('installerRoot','').rstrip('/')== '$npm2_pkg' else 1)
4391
4483
  return 1
4392
4484
  fi
4393
4485
  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)
4486
+ two_root_out=$(env "${consumer_env[@]}" "$npm_pkg/run.sh" remove-user-scope 2>&1)
4395
4487
  two_root_rc=$?
4396
4488
  set -e
4397
4489
  if [ "$two_root_rc" -eq 0 ] || ! python3 -c "
@@ -4404,6 +4496,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
4404
4496
  echo "$two_root_out" | tail -8 | sed 's/^/ /' >&2
4405
4497
  return 1
4406
4498
  fi
4499
+ pack_install_sentinel_untouched "$agentdir_sentinel" "two-root ownership row (install / takeover-user-scope / remove-user-scope)" || return 1
4407
4500
  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
4501
 
4409
4502
  # Installed-package aggregate `setup` row (#86 C1, review blocker 2026-08-26):
@@ -5153,7 +5246,9 @@ process.stdout.write(resolveCodexDefaultSocketPath(process.env));
5153
5246
  JSON
5154
5247
  local pc_seed_sha
5155
5248
  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")
5249
+ # `-u`: the default resolution is only proven if no inherited agent dir can pre-empt it
5250
+ # (check-pack-install exports a sentinel one for its whole body).
5251
+ 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
5252
  local pc_before pc_after pc_out pc_rc
5158
5253
  pc_before=$(cd "$pc_home" && find . -type f -exec sha256sum {} + 2>/dev/null | sort)
5159
5254
 
@@ -5338,6 +5433,7 @@ JSON
5338
5433
  fi
5339
5434
  echo "[check-pack-install] installed doctor dispatch lock pass (store-scan → dist JS, v2-surface deferred)"
5340
5435
 
5436
+ pack_install_sentinel_untouched "$agentdir_sentinel" "a later installed-consumer cell" || return 1
5341
5437
  ok "[check-pack-install] publish install smoke pass"
5342
5438
  return 0
5343
5439
  }
@@ -5563,15 +5659,15 @@ setup_mode() {
5563
5659
  }
5564
5660
 
5565
5661
  # 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>`).
5662
+ # pin (the SSOT check-dep-versions binds: peer range == `>=<pin> <<major>.<minor+1>`).
5567
5663
  # Never retyped here as a second literal.
5568
5664
  pi_supported_range() {
5569
5665
  node -e '
5570
5666
  const pkg = require(process.argv[1]);
5571
5667
  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}`);
5668
+ if (!/^\d+\.\d+\.\d+$/.test(pin ?? "")) { console.error(`unparseable pi pin: ${pin}`); process.exit(1); }
5669
+ const [maj, min] = pin.split(".").map(Number);
5670
+ console.log(`>=${pin} <${maj}.${min + 1}`);
5575
5671
  ' "$REPO_DIR/package.json"
5576
5672
  }
5577
5673
 
@@ -6655,6 +6751,9 @@ case "$cmd" in
6655
6751
  check-entwurf-resume-args)
6656
6752
  check_entwurf_resume_args
6657
6753
  ;;
6754
+ check-acp-foreground-only)
6755
+ check_acp_foreground_only
6756
+ ;;
6658
6757
  check-resume-launch-identity)
6659
6758
  check_resume_launch_identity
6660
6759
  ;;
@@ -7662,6 +7761,17 @@ case "$cmd" in
7662
7761
  shift || true
7663
7762
  run_ts scripts/check-codex-bridge-identity.ts "$@"
7664
7763
  ;;
7764
+ check-pi-bridge-sender)
7765
+ shift || true
7766
+ run_ts scripts/check-pi-bridge-sender.ts "$@"
7767
+ ;;
7768
+ check-pi-mcp-register)
7769
+ check_pi_mcp_register
7770
+ ;;
7771
+ check-pi-mcp-bridge)
7772
+ shift || true
7773
+ run_ts scripts/check-pi-mcp-bridge.ts "$@"
7774
+ ;;
7665
7775
  check-codex-native-push)
7666
7776
  shift || true
7667
7777
  run_ts scripts/check-codex-native-push.ts "$@"
@@ -7688,6 +7798,9 @@ case "$cmd" in
7688
7798
  check-pack-pin-matcher)
7689
7799
  check_pack_pin_matcher
7690
7800
  ;;
7801
+ check-pack-consumer-roots)
7802
+ check_pack_consumer_roots
7803
+ ;;
7691
7804
  check-pack-install)
7692
7805
  check_pack_install
7693
7806
  ;;
@@ -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 " +