@junghanacs/entwurf 0.12.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 (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
package/run.sh ADDED
@@ -0,0 +1,2832 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Model id convention (see AGENTS.md Hard Rule #1):
4
+ # - User-facing examples use the qualified form `entwurf/<backend-model>`
5
+ # (e.g. `entwurf/claude-sonnet-4-6`); the prefix routes to this provider
6
+ # so `--provider` is redundant and is dropped in docs.
7
+ # - Smoke helpers that feed `ensureBridgeSession({modelId})` directly (cancel,
8
+ # model-switch) pass BARE backend ids (`claude-sonnet-4-6`, `gpt-5.4`)
9
+ # because the bridge library contract is bare. Smoke helpers that invoke pi
10
+ # via the CLI still pin `--provider entwurf` and can accept either
11
+ # bare or qualified model, but we keep bare here to match the bridge-level
12
+ # dispatch tables.
13
+ #
14
+ set -euo pipefail
15
+
16
+ REPO_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
17
+ PROJECT_DIR_DEFAULT=$(pwd)
18
+ TARGET_PROJECT_DIR=${2:-$PROJECT_DIR_DEFAULT}
19
+ # npm publish identity. Scoped 2026-05-18 — bare `entwurf` was not on npm
20
+ # and we adopted the same `@junghanacs` scope as the OpenClaw plugin sibling
21
+ # (`@junghanacs/openclaw-entwurf`) for source-of-origin parity. This
22
+ # variable documents intent; check-pack-install hardcodes the tarball name
23
+ # and install path against the same scope for traceability.
24
+ PACKAGE_NAME="@junghanacs/entwurf"
25
+ # Runtime provider id — DO NOT change. Embedded in model strings
26
+ # (`entwurf/claude-sonnet-4-6`), settings keys (`entwurfProvider`),
27
+ # log prefixes (`[entwurf:bootstrap]`), and the `--provider entwurf`
28
+ # CLI surface. Renaming this would break every consumer transcript and every
29
+ # saved session anchor.
30
+ PROVIDER_ID="entwurf"
31
+
32
+ usage() {
33
+ cat <<'EOF'
34
+ Usage:
35
+ ./run.sh setup [project-dir] # ONE confident install: pnpm install + install + meta-bridge (if native harness) + v2 install smoke (LIVE substrate = release-gate)
36
+ ./run.sh release-gate [project-dir] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm check) + the v2-native live gates (v2 matrix/spawn-resume-live, check-bridge, retargeted smoke-session-id-name, RGG) + the ACP plugin acceptance floor (11 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). LIVE-gated MUST steps HONEST-SKIP when LIVE!=1 (a CUT needs LIVE=1, SKIP=0). --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
37
+ ./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
38
+ ./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b, IN pnpm check): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 registered/schema; tools/list only, no auth/side-effect
39
+ ./run.sh check-model-lock # deterministic unit test for pi-extensions/model-lock.ts (4-quadrant + edge cases, no API)
40
+ ./run.sh check-shell-quote # POSIX-safety gate for shellQuote (remote SSH arg quoting in entwurf paths) — source parity + behavior matrix, no SSH
41
+ ./run.sh check-entwurf-session-identity # deterministic gate for locked garden session identity & name grammar (sessionId/buildSessionName/parse/collision), no API
42
+ ./run.sh check-meta-session # deterministic gate (#30 step 2): meta-record mint/serialize/parse + scanByNativeId body-authority + idempotent decideUpsert, no API
43
+ ./run.sh check-meta-record-v2 # deterministic golden gate (0.11 Stage 0 step 3A): synthetic v1 fixture → normalizeMetaIdentity v2 identity golden + dual-read version fences, no API
44
+ ./run.sh check-mailbox-receipt-state # deterministic gate (0.11 Stage 0 step 3B): mailbox receipt state schema + store (stamp→persist→read-back) in a temp mailbox, strict keyset, no API
45
+ ./run.sh check-entwurf-capabilities # deterministic gate (0.11 Stage 0 step 3C): backend capability registry (pi/entwurf-capabilities.json) — coverage==META_BACKENDS_V2 + agrees with live META_BACKEND_DESCRIPTORS + strict keyset, no API
46
+ ./run.sh check-meta-dual-read # deterministic gate (0.11 Stage 0 step 3D-1): v2 write shape (serializeMetaIdentity) + dual-read dispatcher (parseMetaRecordAny/parseMetaIdentity) + write→read round-trip, pure, no API
47
+ ./run.sh check-meta-mailbox-state-write # deterministic gate (0.11 Stage 0 step 3D-4 commit2): post-cut receipt is state-only — meta-record file byte-identical across enqueue/read, state carries lastEnqueuedAt/lastReadAt (field isolation), empty inbox no-op on record+state, drift surfaces; no API
48
+ ./run.sh check-meta-receiver-marker # deterministic gate (SE-2 slice 2b): meta-receiver presence marker — write/read round-trip garden-id keyed + atomic 0600, dead-owner start-key guard reads null, armProvenance limited to arm-capable events (UserPromptSubmit can't mint presence), reader doesn't gate on record existence
49
+ ./run.sh check-meta-migration # deterministic gate (0.11 Stage 0 step 3D-4 commit2): v1→v2 delivery-receipt migration (per-field state-wins, 3 timestamps, no-op when nothing to fill) + crash-order inside upsert (migrate before v2 rewrite; drift throws with record still v1), no API
50
+ ./run.sh check-meta-dual-consumers # deterministic gate (0.11 Stage 0 step 3D-4): delivery-agnostic dual-read seam — readMetaIdentityByGardenId + scanIdentityByNativeId read v1 AND v2, cross-schema duplicate = ambiguity throw (G1); v1-only raw readers remain for v1-fixture gates, no API
51
+ ./run.sh check-meta-capability-source # deterministic gate (0.11 Stage 0 step 3D-3): capability-source cut-over — mint/parse read wakeMode/deliveryLevel from the registry (metaCapabilityFor, registry-driven via injection), not META_BACKEND_DESCRIPTORS; behaviour-preserving (registry ≡ const), slot stays (3D-4), no API
52
+ ./run.sh check-socket-probe # deterministic gate (0.11 Stage 0, F3): three-valued control-socket liveness (alive|dead|indeterminate) — GC reclaims dead only, indeterminate survives; pure classify + 2-socket integration, no API
53
+ ./run.sh check-project-trust-handler # deterministic gate (0.11 Stage 0, Trust 2층): project_trust handler — decideProjectTrust matrix (escape=inherited-false+interactive+trust-here→{yes,remember:true}; non-interactive→undecided; never undefined) + adapter single-writer, fake prompt, no UI
54
+ ./run.sh check-entwurf-v2-contract # deterministic gate (0.11 Stage 0 step 4-pre, 동결결정 10 + Fable R1-R5): FROZEN entwurf_v2 contract — R1 backend liveness domain (pi only; claude/codex/agy=unsupported, not folded), 6-cell intent×liveness table (single verdict, 2 allow/4 reject), N1 indeterminate-no-spawn, Q2 owned-live-no-autosend, R3 table↔receipt round-trip, R5 taxonomy, schema↔types drift; pure, no API
55
+ ./run.sh check-entwurf-v2-lock # deterministic gate (0.11 Stage 0 step 5a, 버킷 B F2): per-gid dispatch LOCK primitive — openSync wx atomic acquire, second-acquire=target-locked conflict (holder JSON for human cleanup), nonce-owned release (successor survives late release), stale reclaim same-host+ESRCH-only (EPERM/remote/alive/unknown fail-closed), empty/corrupt=conflict not auto-deleted, F2-P1 malformed gid throws; real temp dir, deps injected
56
+ ./run.sh check-entwurf-v2-decider # deterministic gate (0.11 Stage 0 step 5b): PURE dispatch decider decideDispatch — frozen 7-step order over injected fakes, lock acquire+release tracked so reject⇒no-plan-no-lock proven; pre-probe rejects observedLiveness=null, send/resume execute keep lock + mailbox no-lock (?7), resume plan no mode/provider/model, invalid gid throws (F2-P1); pure, no IO
57
+ ./run.sh check-entwurf-v2-matrix # deterministic gate (0.11 Stage 0 step 5d-5 a): REACHABILITY + LOCK SSOT table — drives REAL decideDispatch over fakes, fixes every (target kind → transport → lock class) cell as one table (control-socket/meta-mailbox/spawn-bg + bad-target/conflict/locked/undeliverable/owned-live/dormant/indeterminate rejects), coverage pass fails on a dropped cell; thin coverage not a decider re-impl; pure, no IO
58
+ ./run.sh check-entwurf-v2-release # deterministic gate (0.11 Stage 0 step 5c-1): PURE release-policy reducer (decideReleasePolicy + reduceRelease) — Fable-3 release-after-observation as a state machine; spawn-started is NOT a release event, release on first socket-alive ∨ child-exited (any code) or failed start, socket↔exit race idempotent (single release), lock-nullness invariant enforced; pure, no IO
59
+ ./run.sh check-entwurf-v2-send # deterministic gate (0.11 Stage 0 step 5c-2a): control-socket SEND hand (executeControlSocketSend) wiring transport IO onto the 5c-1 reducer — ack→sent, in-band reject→rejected (no fallback), dead→same-lock one-shot re-resolve (control retry / mailbox enqueue), indeterminate→failed+rethrow with NO fallback (no double-delivery); release exactly once, releaseLock throw never masks the send error; IO-via-dep
60
+ ./run.sh check-entwurf-v2-send-fallback # deterministic gate (0.11 Stage 0 step 5c-2b): same-lock re-resolve RESOLVER (resolveDeadControlSendFallback) — fire-and-forget re-resolve: alive→control retry, dead→reject (NEVER spawn-bg), indeterminate→reject, unsupported+deliverable→mailbox plan, undeliverable/bad-target/conflict→reject; resolver never releases, mis-wire fails loud, inspect/probe throws propagate; no IO (fakes)
61
+ ./run.sh check-entwurf-v2-runner # deterministic gate (0.11 Stage 0 step 5d-1): execute-router (executeDispatch) routing an already-decided DispatchDecision to its 5c transport hand → one outcome-rich EntwurfV2RunResult. reject→rejected (no hand) / control/spawn/mailbox→matching hand with decision.lock verbatim / spawn lock-retained rides executed (fail-closed) / N3 rejectReason carried / N1 SendDeliveredReleaseFailedError→execution-failed{finalizedOutcome,releaseFailed,retrySafe:false}; fake hands, no IO
62
+ ./run.sh check-entwurf-v2-mailbox # deterministic gate (0.11 Stage 0 step 5c-4, LAST 5c transport slice): ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + production sendViaMailbox adapter — sender→formatMetaMailboxBody with plan.wantsReply threaded (divergence from legacy hard false), sender absent→raw plan.message, enqueue opts EXACTLY {gardenId,body,sessionsDir,mailboxDir}, enqueue throw PROPAGATES (no success:false fold — mailbox has no in-band refuse); adapter NEVER touches lock (release is the hand's job); source guard: no release/routing seam
63
+ ./run.sh check-entwurf-v2-spawn # deterministic gate (0.11 Stage 0 step 5c-3a): spawn-bg RESUME watcher hand (executeSpawnBgResume) wiring spawn + socket-observe IO onto the 5c-1 reducer — Fable-3: TIMEOUT IS NOT A RELEASE (bare observeTimeout→killChild, release 0; bounded killGrace then real socket-alive ∨ child-exited releases ×1); spawnChild throw→spawn-start-failed; no observation obtainable (grace elapses / post-spawn watch dep throws)→lock-retained fail-closed (released:false, evidence surfaced), NO direct-release hatch; IO-via-dep, controlled promises
64
+ ./run.sh check-entwurf-resume-args # deterministic gate (0.11 Stage 0 step 5c-3b): resume-argv SSOT (buildResumePiArgs) shared by the legacy async worker and the v2 spawn-bg resident citizen — A1: legacy=--no-extensions + no --entwurf-control (one-shot pi -p exits), v2-control=--entwurf-control + no --no-extensions (keep-alive is the goal, resumed session stays addressable); BOTH keep --mode json -p + prompt-as-turn (-p NOT dropped in v2); explicitExtensionArgs preserved once (#29); v2 includes plan.launchArgs (--approve); null provider→no --provider; no cross-contamination
65
+ ./run.sh check-entwurf-v2-spawn-production # deterministic gate (0.11 Stage 0 step 5c-3c): production SpawnBgResumeDeps factory (makeProductionSpawnBgResumeDeps) wiring the 5c-3a watcher's 6 IO seams — no real pi/socket/timer (that=opt-in smoke-entwurf-v2-spawn-live, OUT of pnpm check). socketWatchVerdict: address-conflict→forged (reject, never wait)/alive→alive/dead·indeterminate→wait; spawnChild builds v2-control argv (--entwurf-control, no --no-extensions, -p+prompt, --approve, cwd authority); awaitSocketAlive connectable→resolve / symlink→reject without connect / dead→wait→alive / abort-clears; awaitChildExit code + listener cleanup; awaitTimeout schedule + abort-clear; killChild=SIGTERM; proc-less child fails loud
66
+ ./run.sh smoke-entwurf-v2-spawn-live # LIVE phase gate (0.11 Stage 0 step 5c-3c, D5) — OUT of pnpm check, needs LIVE=1. Exercises the production SpawnBgResumeDeps against REAL OS objects: S1 real unix socket → awaitSocketAlive resolves (real lstat+probe), symlink→forged, absent→abort settles; S2 real child → spawn-event resolve + SIGTERM kill + exit-code capture; S3 watcher integration → real timeout→kill→child-exited→release ×1. Does NOT spawn a real pi resume (that=5d matrix). Run before 5d: LIVE=1 ./run.sh smoke-entwurf-v2-spawn-live
67
+ ./run.sh smoke-entwurf-v2-spawn-resume-live # 0.11.0 (A) ACCEPTANCE gate — OUT of pnpm check, needs LIVE=1. The FULL spawn-bg resident lifecycle: mint backend=pi identity → seed a REAL dormant pi session (one-shot into ~/.pi/agent/sessions) → runEntwurfV2(owned-outcome) routes dormant→spawn-bg resume → a REAL detached pi --entwurf-control child stands its socket up, resumes, DOES a model turn. Asserts executed/spawn-bg/socket-alive/released + lock released ×1 + no lock file + pid alive + socket connectable + resume USER & assistant OK nonces in the session JSONL. Model-in-loop IN. The gate v1 deprecation (0.12) is predicated on. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4). LIVE=1 ./run.sh smoke-entwurf-v2-spawn-resume-live
68
+ ./run.sh smoke-entwurf-v2-matrix-live # LIVE sentinel (0.11 Stage 0 step 5d-5, D4-b) — OUT of pnpm check, needs LIVE=1. Drives REAL production runEntwurfV2 deps over REAL OS objects, 4 cells: C1 control-socket (real pi --entwurf-control resident → RPC send → lock acquire→release ×1), C1b socket-only (record-less live pi → control-socket sent / owned→bad-target, A1 narrow), C2 meta-mailbox deliverable (armed self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver → reject, no garbage). Model-in-loop OUT (transport/lock/enqueue gate, GPT Q2); negative/timeout stay deterministic. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4). LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
69
+ ./run.sh check-entwurf-facts # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 1+2): PURE PeerFact core + resolveFactList union — R1 out-of-domain→unsupported, R3b pi 4-value, facts-only keyset; union: PeerFact+SocketOnlyFact by gardenId, dormant→dead, F3 indeterminate preserved, non-pi+socket fail-loud; pure, no IO
70
+ ./run.sh check-socket-discovery # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 3): SOCKET-axis scanSocketProbes — probes (dir sockets) ∪ (in-domain citizen canonical paths) 3-valued; dormant citizen no-file → dead (resumable, not unprobed), stall → indeterminate (F3), dir hygiene/dedup/missing-dir + e2e → resolveFactList; readdir/probe injected, no IO
71
+ ./run.sh check-meta-listing # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4a): META-STORE axis listAllMetaIdentities — explicit-partial: parse failure / body-filename drift → explicit {filename,message} error (verbatim, no synthetic fields), valid records still listed (corrupt doesn't blind); mode strict throws / collect partial; entries/readRecord injected, no IO
72
+ ./run.sh check-entwurf-fact-provider # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4b): ASSEMBLY listEntwurfFacts — listAllMetaIdentities→scanSocketProbes→pre-quarantine non-pi/socket conflicts→resolveFactList(clean)→{facts,diagnostics}; C-원칙: expected corruption (parse/collision)→diagnostics (listing survives), impossible invariant (dup/unprobed)→throw; collision quarantines BOTH PeerFact+socket; deps injected, no IO
73
+ ./run.sh check-entwurf-peers-surface # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4c): MCP entwurf_peers RENDER renderEntwurfPeers — legacy `sessions` = projection of facts (alive only, no 2nd scan), socketPath via controlSocketPath (SSOT), count=projection length, three distinct arrays, NO verb-routing key (JSON deep scan) NOR word (text), diagnostics both surfaces, empty→(none), unsupported shown, enrich→(not enriched); WIRING guard: bridge calls provider+render, getLiveSessions gone; facts fabricated, no IO
74
+ ./run.sh check-entwurf-self-address # deterministic gate (SE-1/SE-2 slice 1): self-addressability honesty predicate computeSelfAddressability — pi replyable ⟺ live socket; meta ⟺ recordBacked ∧ ownerAlive ∧ watchArmed (regression-proof record-present rows); SOURCE GUARD buildStrictPiSenderEnvelope drops hardcoded replyable:true + existsSync-probes socket, entwurf_self renders alive vs expected. meta watchArmed wired in slice 2 (same release block)
75
+ ./run.sh check-entwurf-deliverability # deterministic gate (SE-1/SE-2 slice 2c): conversational-mailbox deliverability predicate — computeMetaReceiverActive (recordBacked ∧ ownerAlive ∧ watchArmed) + mailboxConversationalDeliverable (self-fetch AND active); direct-inject pi refused (SE-1), self-fetch dead/unarmed refused (SE-2); self-address shares the same atom
76
+ ./run.sh check-entwurf-mailbox-guard # deterministic gate (SE-1/SE-2 slice 2d): guarded mailbox enqueue — PURE 0-call (undeliverable target leaves injected enqueue uncalled) + TMPDIR snapshot (refused send leaves mailbox byte-identical, accepted writes one .msg) + fact gathering from record/capability/receiver-marker
77
+ ./run.sh check-package-source-routing # deterministic gate (#29): package-source -> install-root mapping + fail-fast routing (local/git/npm/missing/project/no-source × local+remote, self-root, resume), no backend
78
+ ./run.sh smoke-session-id-name # live 3-turn substrate smoke (Phase 3a): Pi 0.78 --session-id/--name through the bridge — header id/cwd, session_info name, append-not-recreate, spawn-only name, wrong-cwd footgun evidence
79
+ ./run.sh new-session-id # print one fresh garden-native session id for operator launchers (--session-id)
80
+ ./run.sh smoke-resident-garden-guard # live resident --entwurf-control garden guard (negative 0-token; SMOKE_RGG_POSITIVE=1 for positive)
81
+ ./run.sh smoke-meta-async-drift # 1.0.0 meta-bridge step 1: drift sentinel — version pins + Claude binary undocumented-behavior markers (LIVE=1 adds plugin watch-arm probe)
82
+ ./run.sh smoke-meta-honesty # 1.0.0 meta-bridge: honesty regression gate (#30 blockers) — doorbell counts ALL msgs honestly + hook logs failures as ERROR (best-effort, no scream). Offline/deterministic (deps: bash+node+python3)
83
+ ./run.sh smoke-meta-install-state # 1.0.0 meta-bridge Phase 2: stateful install/uninstall + store-doctor regression gate. Offline/deterministic (deps: bash+node+python3)
84
+ ./run.sh smoke-meta-prune # 1.0.0 meta-bridge Phase 4: listing-only store janitor regression gate — classify keep/orphan/stale/ambiguous, delete nothing. Offline/deterministic (deps: bash+node)
85
+ ./run.sh smoke-meta-keyset-guard # 0.10.0 meta-bridge: keyset-owner guard regression — check-keyset-overlap + managed-keys SSOT (disjoint passes, collisions fail). Offline/hermetic (deps: bash+python3)
86
+ ./run.sh smoke-claude-native-resume-live # LIVE-only: Claude Code native fresh→--resume continuity + meta-record uniqueness; proves meta-bridge records identity without touching the backend resume path
87
+
88
+ ./run.sh install-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
89
+ ./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
90
+ ./run.sh doctor-meta-bridge # 1.0.0 meta-bridge Phase 2: fail-loud doctor — toolchain + state + plugin/MCP + store scan + hook errors + SessionStart evidence + writer-version parity (source↔assembled↔installed: FAIL on a stale deployed meta-record writer)
91
+ ./run.sh meta-bridge-prune # 1.0.0 meta-bridge Phase 4: LISTING-ONLY store hygiene — classify orphan/stale/ambiguous/keep, print manual rm commands, delete NOTHING ([dir] [--ttl-days N])
92
+ ./run.sh meta-bridge-managed-keys # 0.10.0 meta-bridge: print the SSOT of settings keys entwurf OWNS (consumers read this to stay disjoint — keyset-owner invariant)
93
+ ./run.sh check-keyset-overlap <fragment.json...> # 0.10.0 meta-bridge: PREVENTIVE keyset guard — fail if a consumer fragment collides with any pi-owned key (cross-repo; not in pnpm check)
94
+ ./run.sh check-dep-versions # local deterministic check that version pins (package.json/run.sh/README.md + pi devDeps/peer pins) agree
95
+ ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
96
+ ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with 0.80.x peers
97
+ ./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
98
+ ./run.sh install [project-dir] # install this local package into project .pi/settings.json
99
+ ./run.sh setup:links [--force] # repair ~/.pi/agent/entwurf-targets.json link (use --force to replace a stale operator file or wrong symlink; a .bak is taken)
100
+ ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json
101
+
102
+ Notes:
103
+ - project-dir defaults to current directory
104
+ - Claude Code login should already exist (e.g. ~/.claude.json)
105
+ - setup's runtime verification is the v2 install smoke (entwurf-bridge); the v2 dispatch substrate is proven live by release-gate
106
+ - API key is optional; this bridge is intended to work with Claude Code auth
107
+ EOF
108
+ }
109
+
110
+ require_cmd() {
111
+ command -v "$1" >/dev/null 2>&1 || {
112
+ echo "Missing command: $1" >&2
113
+ exit 1
114
+ }
115
+ }
116
+
117
+ log() { echo " $*"; }
118
+ ok() { echo " ✅ $*"; }
119
+ warn() { echo " ⚠ $*"; }
120
+ fail() { echo " ❌ $*"; }
121
+ section() { echo ""; echo "=== $* ==="; }
122
+
123
+ normalize_project_dir() {
124
+ python3 - "$1" <<'PY'
125
+ import os, sys
126
+ print(os.path.abspath(os.path.expanduser(sys.argv[1])))
127
+ PY
128
+ }
129
+
130
+ sync_auth() {
131
+ local auth_path="$HOME/.pi/agent/auth.json"
132
+ python3 - "$auth_path" "$PROVIDER_ID" <<'PY'
133
+ import json, os, sys
134
+ from pathlib import Path
135
+
136
+ auth_path = Path(sys.argv[1]).expanduser()
137
+ provider_id = sys.argv[2]
138
+ auth_path.parent.mkdir(parents=True, exist_ok=True)
139
+
140
+ if auth_path.exists():
141
+ data = json.loads(auth_path.read_text())
142
+ if not isinstance(data, dict):
143
+ raise SystemExit("auth.json is not an object")
144
+ else:
145
+ data = {}
146
+
147
+ anthropic = data.get("anthropic")
148
+ if not isinstance(anthropic, dict):
149
+ print("sync-auth: skipped (no anthropic OAuth credentials in ~/.pi/agent/auth.json)")
150
+ raise SystemExit(0)
151
+
152
+ before = json.dumps(data.get(provider_id), sort_keys=True)
153
+ after = json.dumps(anthropic, sort_keys=True)
154
+ if before == after:
155
+ print(f"sync-auth: already synced ({provider_id})")
156
+ raise SystemExit(0)
157
+
158
+ data[provider_id] = anthropic
159
+ backup = auth_path.with_suffix(auth_path.suffix + ".bak")
160
+ if auth_path.exists():
161
+ backup.write_text(auth_path.read_text())
162
+ auth_path.write_text(json.dumps(data, indent=2) + "\n")
163
+ print(f"sync-auth: wrote {provider_id} alias to {auth_path}")
164
+ if backup.exists():
165
+ print(f"sync-auth: backup -> {backup}")
166
+ PY
167
+ }
168
+
169
+ # Repo dependency integrity is a HARD requirement for install — NOT gated on
170
+ # backend (Claude/ACP) auth. A cloned-but-not-installed or a moved/renamed repo
171
+ # has missing or path-stale node_modules; `install` would otherwise happily wire
172
+ # settings.json and the failure only surfaces minutes later as a dead MCP bridge
173
+ # / pi-extension resolve error (the 2026-06-23 relocation failure: entwurf-bridge
174
+ # ✘ Failed to connect + check-entwurf-v2-surface ERR_MODULE_NOT_FOUND). Catch it
175
+ # HERE, before any settings file is written (no silent-red), with a package
176
+ # manifest access that follows the pnpm symlink to each dep — NOT a real module
177
+ # import (per-package "exports" maps forbid that uniformly) and NOT a bare
178
+ # `test -d node_modules`, which a dir-move breaks at the symlink-store level
179
+ # while the top-level dir still looks present.
180
+ preflight_dep_integrity() {
181
+ command -v node >/dev/null 2>&1 || { fail "node not on PATH — cannot verify repo dependency integrity"; exit 1; }
182
+ # Each wired pi-extension / MCP bridge / ACP backend root-imports its bundled
183
+ # runtime deps at load/spawn time. Assert they actually resolve from the
184
+ # package's location BEFORE writing settings, so a broken install fails loud
185
+ # here instead of surfacing later as a dead MCP bridge (entwurf-bridge ✘) or an
186
+ # ERR_MODULE_NOT_FOUND at runtime.
187
+ #
188
+ # Resolution must follow Node's OWN algorithm, because the package lives in two
189
+ # layouts: a pnpm clone (deps in $REPO_DIR/node_modules) OR a pi-managed
190
+ # `pi install npm:@junghanacs/entwurf` (deps HOISTED to an ancestor node_modules,
191
+ # e.g. ~/.pi/agent/npm/node_modules, with NO package-local node_modules). A
192
+ # cwd-relative `node_modules/<dep>` probe only sees the clone layout and wrongly
193
+ # rejects every pi-managed npm install. So walk Node's real module-resolution
194
+ # paths (Module._nodeModulePaths) and accessSync each candidate package.json:
195
+ # exports-immune (no bare-root / ./package.json import) and hoist-aware, while
196
+ # still catching a pnpm dir-move that left the symlink store dangling
197
+ # (accessSync follows the link).
198
+ #
199
+ # Probe set = the BUNDLED runtime `dependencies` only. The `@earendil-works/pi-*`
200
+ # peer trio is intentionally EXCLUDED: pi-managed installs omit peers
201
+ # (--legacy-peer-deps) and the pi loader provides that runtime itself, so the
202
+ # trio is legitimately absent from node_modules on the npm path. pi runtime
203
+ # presence/version is covered by check-pi-runtime-version / check-pi-import-surface.
204
+ local probe=(
205
+ "@modelcontextprotocol/sdk" "@agentclientprotocol/sdk" "@agentclientprotocol/claude-agent-acp"
206
+ "@anthropic-ai/sdk" "zod"
207
+ )
208
+ local missing=() dep
209
+ for dep in "${probe[@]}"; do
210
+ if ! (cd "$REPO_DIR" && node -e '
211
+ const M = require("module"), fs = require("fs"), path = require("path");
212
+ const dep = process.argv[1];
213
+ for (const nm of M._nodeModulePaths(process.cwd())) {
214
+ try { fs.accessSync(path.join(nm, dep, "package.json")); process.exit(0); } catch {}
215
+ }
216
+ process.exit(1);
217
+ ' "$dep") >/dev/null 2>&1; then
218
+ missing+=("$dep")
219
+ fi
220
+ done
221
+ if [ ${#missing[@]} -gt 0 ]; then
222
+ fail "repo dependency integrity check failed — cannot resolve: ${missing[*]}"
223
+ echo " node_modules is missing or path-stale (common right after a clone," >&2
224
+ echo " or a repo move/rename — pnpm's symlink store points at the old path)." >&2
225
+ echo " Fix: (cd \"$REPO_DIR\" && pnpm install)" >&2
226
+ echo " Then re-run ./run.sh install . — settings.json was NOT written." >&2
227
+ exit 1
228
+ fi
229
+ }
230
+
231
+ install_local_package() {
232
+ local project_dir
233
+ project_dir=$(normalize_project_dir "$1")
234
+ preflight_dep_integrity
235
+ mkdir -p "$project_dir/.pi"
236
+ python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
237
+ import json, sys
238
+ from pathlib import Path
239
+
240
+ settings_path = Path(sys.argv[1])
241
+ repo_dir = str(Path(sys.argv[2]).resolve())
242
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
243
+ if settings_path.exists():
244
+ data = json.loads(settings_path.read_text())
245
+ if not isinstance(data, dict):
246
+ raise SystemExit("settings.json is not an object")
247
+ else:
248
+ data = {}
249
+
250
+ # --- packages[] registration --------------------------------------------------
251
+ packages = data.get("packages")
252
+ if not isinstance(packages, list):
253
+ packages = []
254
+
255
+ filtered = []
256
+ for item in packages:
257
+ source = item.get("source") if isinstance(item, dict) else item
258
+ if isinstance(source, str) and ("entwurf" in source) and source != repo_dir:
259
+ continue
260
+ filtered.append(item)
261
+
262
+ if repo_dir not in filtered:
263
+ filtered.append(repo_dir)
264
+
265
+ data["packages"] = filtered
266
+
267
+ # --- entwurfProvider.mcpServers bundled entries ---------------------------
268
+ # Ship the two in-repo MCP adapters pre-wired so `pi install` produces a
269
+ # working setup without the consumer hand-editing settings.json. User-authored
270
+ # overrides (different command/args) are preserved untouched.
271
+ provider = data.setdefault("entwurfProvider", {})
272
+ if not isinstance(provider, dict):
273
+ raise SystemExit("entwurfProvider is not an object")
274
+ servers = provider.setdefault("mcpServers", {})
275
+ if not isinstance(servers, dict):
276
+ raise SystemExit("entwurfProvider.mcpServers is not an object")
277
+
278
+ BUNDLED = ("entwurf-bridge",)
279
+ # 0.4.14: session-bridge MCP was retracted (issue #7 — unified entwurf surface).
280
+ # Install path now only wires entwurf-bridge. Existing operator settings that
281
+ # carry the old bundled session-bridge entry are pruned below during install;
282
+ # `./run.sh remove` also keeps session-bridge in its cleanup tuple so legacy
283
+ # entries are removed on uninstall too.
284
+ for name in BUNDLED:
285
+ desired_cmd = f"{repo_dir}/mcp/{name}/start.sh"
286
+ desired = {"command": desired_cmd, "args": []}
287
+ existing = servers.get(name)
288
+ if existing is None:
289
+ servers[name] = desired
290
+ print(f"install: added entwurfProvider.mcpServers.{name}")
291
+ elif isinstance(existing, dict) and existing.get("command") == desired_cmd:
292
+ # Already managed by us at the current repo path. Add the default args
293
+ # field when missing, but never overwrite user-customized args.
294
+ if "args" not in existing:
295
+ existing["args"] = []
296
+ print(f"install: normalized entwurfProvider.mcpServers.{name}.args -> []")
297
+ elif existing.get("args") != []:
298
+ print(f"install: preserved entwurfProvider.mcpServers.{name}.args (custom args)")
299
+ else:
300
+ cmd_repr = existing.get("command") if isinstance(existing, dict) else existing
301
+ print(f"install: preserved entwurfProvider.mcpServers.{name} (user override: {cmd_repr})")
302
+
303
+ # Legacy-bundled MCP names that prior versions of this installer wrote and that
304
+ # the current cutover supersedes. One-shot install-time prune (NOT a runtime
305
+ # alias): only remove entries whose command matches the bundled start.sh path,
306
+ # so user-customized commands are left alone.
307
+ # - session-bridge: retracted in 0.4.14 (issue #7, unified entwurf surface).
308
+ # - pi-tools-bridge: renamed to entwurf-bridge in 0.11 S2 cutover.
309
+ LEGACY_BUNDLED = {
310
+ "session-bridge": "retracted in 0.4.14, issue #7",
311
+ "pi-tools-bridge": "renamed to entwurf-bridge in 0.11 S2 cutover",
312
+ }
313
+ for name, reason in LEGACY_BUNDLED.items():
314
+ existing = servers.get(name)
315
+ if not isinstance(existing, dict):
316
+ continue
317
+ cmd = existing.get("command")
318
+ if not isinstance(cmd, str):
319
+ continue
320
+ if cmd == f"{repo_dir}/mcp/{name}/start.sh" or cmd.endswith(f"/entwurf/mcp/{name}/start.sh"):
321
+ del servers[name]
322
+ print(f"install: pruned legacy entwurfProvider.mcpServers.{name} ({reason})")
323
+
324
+ settings_path.write_text(json.dumps(data, indent=2) + "\n")
325
+ print(f"install: updated {settings_path}")
326
+ print(f"install: package source -> {repo_dir}")
327
+ PY
328
+ ensure_agent_dir_symlinks
329
+ }
330
+
331
+ # Ensure agent-level resources that entwurf code reads from
332
+ # ~/.pi/agent/ are wired up at install time. Currently:
333
+ # - entwurf-targets.json — pi-extensions/lib/entwurf-core.ts reads
334
+ # ~/.pi/agent/entwurf-targets.json. The package ships the canonical
335
+ # version at $REPO_DIR/pi/entwurf-targets.json. Without this symlink
336
+ # any entwurf tool call throws EntwurfRegistryError (lazy load — no
337
+ # surface during plain `pi --model ...` runs but blocks delegation
338
+ # immediately when the operator first calls entwurf).
339
+ #
340
+ # Fail-fast policy (added v0.5.0): drift between canonical and the
341
+ # operator's link/file is treated as a bug to fix, not noise to preserve.
342
+ # The v0.4.x oracle install regression came from a stale operator-copied
343
+ # entwurf-targets.json that previous installs silently kept "as operator
344
+ # file"; the only signal was a sentinel failure several minutes later.
345
+ #
346
+ # Two explicit exits are honored:
347
+ # 1. `./run.sh setup:links --force` — back up + overwrite with canonical
348
+ # 2. `ENTWURF_TARGETS_PATH=/path/to/custom.json` — tells entwurf-core
349
+ # to read elsewhere, freeing this slot from any policy obligation
350
+ #
351
+ # Idempotent for the happy path. Lazy registry load means a corrected
352
+ # file/symlink is picked up on the next entwurf call without restarting
353
+ # pi or the MCP bridge process.
354
+ ensure_agent_dir_symlinks() {
355
+ local agent_dir="$HOME/.pi/agent"
356
+ mkdir -p "$agent_dir"
357
+
358
+ local target="$REPO_DIR/pi/entwurf-targets.json"
359
+ local link="$agent_dir/entwurf-targets.json"
360
+ local force="${1:-}"
361
+
362
+ if [ ! -f "$target" ]; then
363
+ fail "canonical registry missing at $target — repo install is broken"
364
+ exit 1
365
+ fi
366
+
367
+ if [ -L "$link" ]; then
368
+ local current
369
+ current=$(readlink "$link")
370
+ if [ "$current" = "$target" ]; then
371
+ return 0 # already correct, silent
372
+ fi
373
+ if [ "$force" = "--force" ]; then
374
+ rm -f "$link"
375
+ ln -s "$target" "$link"
376
+ echo "install: relinked $link -> $target (was -> $current)"
377
+ return 0
378
+ fi
379
+ fail "stale entwurf-targets symlink at $link"
380
+ echo " points to: $current" >&2
381
+ echo " expected: $target" >&2
382
+ echo " Fix with one of:" >&2
383
+ echo " ./run.sh setup:links --force # relink to canonical" >&2
384
+ echo " export ENTWURF_TARGETS_PATH=$current # honor your override explicitly" >&2
385
+ exit 1
386
+ fi
387
+
388
+ if [ -e "$link" ]; then
389
+ if cmp -s "$link" "$target"; then
390
+ return 0 # operator copy is byte-identical to canonical, silent
391
+ fi
392
+ if [ "$force" = "--force" ]; then
393
+ local backup="${link}.bak.$(date +%Y%m%d-%H%M%S)"
394
+ mv "$link" "$backup"
395
+ ln -s "$target" "$link"
396
+ echo "install: replaced stale $link with symlink -> $target (backup: $backup)"
397
+ return 0
398
+ fi
399
+ fail "stale entwurf-targets file at $link (drifts from canonical)"
400
+ echo " canonical: $target" >&2
401
+ echo " diff (link vs canonical):" >&2
402
+ diff -u "$link" "$target" | sed 's/^/ /' >&2 || true
403
+ echo " Fix with one of:" >&2
404
+ echo " ./run.sh setup:links --force # back up + replace with symlink to canonical" >&2
405
+ echo " export ENTWURF_TARGETS_PATH=$link # honor your file as an explicit override" >&2
406
+ exit 1
407
+ fi
408
+
409
+ ln -s "$target" "$link"
410
+ echo "install: linked $link -> $target"
411
+ }
412
+
413
+ remove_local_package() {
414
+ local project_dir
415
+ project_dir=$(normalize_project_dir "$1")
416
+ python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
417
+ import json, sys
418
+ from pathlib import Path
419
+
420
+ settings_path = Path(sys.argv[1])
421
+ repo_dir = str(Path(sys.argv[2]).resolve())
422
+ if not settings_path.exists():
423
+ print(f"remove: nothing to do ({settings_path} missing)")
424
+ raise SystemExit(0)
425
+
426
+ data = json.loads(settings_path.read_text())
427
+ if not isinstance(data, dict):
428
+ raise SystemExit("settings.json is not an object")
429
+
430
+ # --- packages[] cleanup -------------------------------------------------------
431
+ packages = data.get("packages")
432
+ pkg_removed = 0
433
+ if isinstance(packages, list):
434
+ filtered = []
435
+ for item in packages:
436
+ source = item.get("source") if isinstance(item, dict) else item
437
+ if isinstance(source, str) and ("entwurf" in source):
438
+ pkg_removed += 1
439
+ continue
440
+ filtered.append(item)
441
+ data["packages"] = filtered
442
+
443
+ # --- entwurfProvider.mcpServers cleanup ------------------------------------
444
+ # Only remove entries that look like they came from ./run.sh install: either
445
+ # the command matches the current $REPO_DIR anchor exactly, or it ends with
446
+ # the bundled "/entwurf/mcp/<name>/start.sh" pattern (covers a rebuilt
447
+ # checkout under a different directory). Anything else is treated as a user
448
+ # override and left in place.
449
+ BUNDLED = ("entwurf-bridge", "session-bridge", "pi-tools-bridge")
450
+ provider = data.get("entwurfProvider")
451
+ mcp_removed = 0
452
+ if isinstance(provider, dict):
453
+ servers = provider.get("mcpServers")
454
+ if isinstance(servers, dict):
455
+ for name in BUNDLED:
456
+ existing = servers.get(name)
457
+ if not isinstance(existing, dict):
458
+ continue
459
+ cmd = existing.get("command")
460
+ if not isinstance(cmd, str):
461
+ continue
462
+ exact = cmd == f"{repo_dir}/mcp/{name}/start.sh"
463
+ pattern = cmd.endswith(f"/entwurf/mcp/{name}/start.sh")
464
+ if exact or pattern:
465
+ del servers[name]
466
+ mcp_removed += 1
467
+ print(f"remove: removed entwurfProvider.mcpServers.{name}")
468
+ else:
469
+ print(f"remove: preserved entwurfProvider.mcpServers.{name} (user override: {cmd})")
470
+ if not servers:
471
+ provider.pop("mcpServers", None)
472
+ if not provider:
473
+ data.pop("entwurfProvider", None)
474
+
475
+ settings_path.write_text(json.dumps(data, indent=2) + "\n")
476
+ print(f"remove: removed {pkg_removed} packages[] entries, {mcp_removed} mcpServers entries from {settings_path}")
477
+ PY
478
+ }
479
+
480
+ check_model_lock() {
481
+ # Deterministic policy unit test for pi-extensions/model-lock.ts.
482
+ # No pi process, no network, no API cost. Mocks ExtensionAPI/Context and
483
+ # drives the model_select handler through every quadrant + edge case
484
+ # (see scripts/check-model-lock.ts header for the full matrix).
485
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-model-lock.ts)
486
+ }
487
+
488
+ check_shell_quote() {
489
+ # POSIX-safety gate for the shellQuote helper used in remote SSH command
490
+ # builders (entwurf.ts + entwurf-core.ts). Verifies source-string parity
491
+ # across the two duplication sites AND behavioral correctness on the
492
+ # payload classes that caused the 2026-05-18 remote entwurf incident
493
+ # (backtick / $(...) / $VAR / korean tokens). No process spawn, no SSH.
494
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-shell-quote.ts)
495
+ }
496
+
497
+ check_entwurf_session_identity() {
498
+ # Deterministic gate for the locked garden session identity & name grammar
499
+ # (NEXT.md "Locked — session identity & name grammar"): sessionId validator,
500
+ # buildSessionName/parseSessionName round-trip incl. `.`-bearing registry
501
+ # models, titleSlug canonicalization, registry exact-tuple membership, name=
502
+ # info-only invariants, and header-scan collision pre-check. Isolates registry
503
+ # + sessions base to a temp dir. No backend, no API, no spawn.
504
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-session-identity.ts)
505
+ }
506
+
507
+ check_meta_session() {
508
+ # Deterministic gate for the 1.0.0 meta-bridge record authority (#30 step 2):
509
+ # mint/serialize/parse round-trip + crash-on-malformed, scanByNativeId lookup
510
+ # authority BY RECORD BODY (not filename, proven with a decoy filename in a
511
+ # real temp dir), idempotent existence-keyed decideUpsert + identity-drift
512
+ # refusal, and the pre-drilled read-receipt mutators. Pure functions; no
513
+ # backend, no hook, no API.
514
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-session.ts)
515
+ }
516
+
517
+ check_meta_record_v2() {
518
+ # Deterministic golden gate for 0.11 Stage 0 step 3A: the v1→v2 identity
519
+ # normalize seam. A synthetic, sanitized v1 fixture normalizes to a
520
+ # hand-written v2 identity literal (golden), plus dual-read version fences
521
+ # and v2 field-contract crashes. Reader/normalizer only — no v2 writer yet.
522
+ # Kept separate from check-meta-session so 3D's v1-gate rewrites leave this
523
+ # back-compat golden untouched. Pure functions; no backend, no hook, no API.
524
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-record-v2.ts)
525
+ }
526
+
527
+ check_mailbox_receipt_state() {
528
+ # Deterministic gate for 0.11 Stage 0 step 3B: the mailbox receipt state
529
+ # schema + store — the new home for the read-receipt before v2 drops
530
+ # record.delivery (NEXT.md 고정순서 4). Pure schema round-trip + strict
531
+ # keyset, then the fs store (stamp → persist → read-back) in a temp mailbox
532
+ # dir. Schema/store only — no live enqueue/read dual-write (that is 3D). No
533
+ # backend, no hook, no API.
534
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-mailbox-receipt-state.ts)
535
+ }
536
+
537
+ check_entwurf_capabilities() {
538
+ # Deterministic gate for 0.11 Stage 0 step 3C: the backend capability source
539
+ # (pi/entwurf-capabilities.json) — the new home for wakeMode/deliveryLevel/
540
+ # nativeIdLabel before v2 drops them from the record (frozen decision 1).
541
+ # Asserts coverage == META_BACKENDS_V2 (pi included), agreement with the live
542
+ # META_BACKEND_DESCRIPTORS for the three existing backends (drift guard), and
543
+ # strict keyset/coverage/field crashes. Parser/gate only — no live routing,
544
+ # no record/descriptor consumer change (that is 3D). No backend, no API.
545
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-capabilities.ts)
546
+ }
547
+
548
+ check_meta_dual_read() {
549
+ # Deterministic gate for 0.11 Stage 0 step 3D-1: the v2 write shape
550
+ # (serializeMetaIdentity) + the dual-read dispatcher (parseMetaRecordAny /
551
+ # parseMetaIdentity). Canonical serialize + round-trip + version dispatch +
552
+ # unknown-version crash. Pure functions only — no fs upsert wiring, no
553
+ # readMetaInbox/enqueueMetaMessage change, no record.delivery removal (3D-2/3/4).
554
+ # No backend, no hook, no API.
555
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-read.ts)
556
+ }
557
+
558
+ check_meta_mailbox_state_write() {
559
+ # Deterministic gate for 0.11 Stage 0 step 3D-4 commit2 (the cut). Renamed from
560
+ # check-meta-mailbox-dualwrite: after the cut the receipt is no longer dual-written
561
+ # (record.delivery is gone from the v2 record) — it lives SOLELY in the mailbox
562
+ # state store. Asserts the meta-record FILE is byte-identical before/after enqueue
563
+ # AND read (enqueue/read no longer touch the record — invariant ⑤), the state
564
+ # carries lastEnqueuedAt/lastReadAt with field isolation, lastDeliveredAt is never
565
+ # invented, an empty inbox is a no-op on BOTH record and state (⑥), and a state
566
+ # drift surfaces fail-loud. v2 citizen seeded via upsertMetaSession. No API.
567
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-mailbox-state-write.ts)
568
+ }
569
+
570
+ check_meta_receiver_marker() {
571
+ # Deterministic gate for the meta-receiver presence marker (SE-2 slice 2b). The
572
+ # active-receiver signal a self-fetch backend (Claude Code) needs: a meta-record
573
+ # proves a session once existed; this marker proves a live watch owner is still
574
+ # there to be woken, so a terminated session's lingering record does not read as a
575
+ # ghost active receiver (mailbox garbage). Asserts: write→read round-trip keyed by
576
+ # GARDEN id, atomic 0600; dead-owner/pid-reuse start-key guard reads null (distinct
577
+ # from "no marker"); armProvenance constrained to the arm-capable events so
578
+ # UserPromptSubmit cannot mint presence; reader does NOT gate on record existence
579
+ # (recordBacked is the deliverability predicate's fact). Real tmpdir, no API.
580
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-receiver-marker.ts)
581
+ }
582
+
583
+ check_meta_migration() {
584
+ # Deterministic gate for 0.11 Stage 0 step 3D-4 commit2: the v1→v2 delivery-receipt
585
+ # migration (migrateV1DeliveryReceipts) + its crash-order inside upsert. Per-field
586
+ # STATE WINS, 3 timestamps only; v1-all-null / state-already-wins are no-ops (no
587
+ # state.json). Crash-order: a v1 record's receipts migrate to state BEFORE the v2
588
+ # rewrite (proven via upsert attach), and a drift'd state makes migrate throw with
589
+ # the record STILL v1 (recoverable: next attach re-migrates). Temp dir, no API.
590
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-migration.ts)
591
+ }
592
+
593
+ check_meta_dual_consumers() {
594
+ # Deterministic gate for 0.11 Stage 0 step 3D-4: the delivery-agnostic dual-read
595
+ # seam. readMetaIdentityByGardenId + scanIdentityByNativeId read v1 AND v2 records and
596
+ # return normalized identity, so the live consumers (enqueue/read, MCP marker, prune,
597
+ # store-doctor, the v2 upsert's existence scan) survive the v2 cut. Proves cross-schema
598
+ # match + THE G1 invariant (a nativeSessionId duplicated across a v1 AND v2 file is
599
+ # authority ambiguity → throw, so the v2 upsert never duplicate-mints) + v1 normalize +
600
+ # body/filename drift fail-fast. The v1-only raw readers remain for v1-fixture gates.
601
+ # Temp dir, no API.
602
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-consumers.ts)
603
+ }
604
+
605
+ check_meta_capability_source() {
606
+ # Deterministic gate for 0.11 Stage 0 step 3D-3: the capability-source cut-over.
607
+ # mint/parse now read backend honesty metadata (wakeMode/deliveryLevel) from the
608
+ # capability registry (3C) via metaCapabilityFor, NOT META_BACKEND_DESCRIPTORS.
609
+ # Proves the seam is registry-DRIVEN (a doctored registry injection is followed),
610
+ # mint sources delivery metadata through it, the parse drift guard is now
611
+ # registry-sourced, and the cut-over preserves behaviour (registry ≡ const for the
612
+ # 3 backends). The record.delivery.wakeMode SLOT stays (removal is 3D-4); only the
613
+ # SOURCE moves. check-entwurf-capabilities still owns the registry ≡ const drift
614
+ # guard. Pure — no fs writes, no backend, no hook, no API.
615
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-capability-source.ts)
616
+ }
617
+
618
+ check_socket_probe() {
619
+ # Deterministic gate for 0.11 Stage 0 (F3 fix): three-valued control-socket
620
+ # liveness. classifyConnectError is a pure boundary (ECONNREFUSED/ENOENT →
621
+ # dead; timeout/EACCES/unknown → indeterminate). GC reclaims dead only;
622
+ # indeterminate (a load-stalled live socket) survives the sweep — the F3
623
+ # invariant. Listing lists alive only. Pure classify + GC/listing policy +
624
+ # a two-socket integration (live listener → alive survives; nonexistent →
625
+ # dead GC-eligible). No wire timeout fixture, no backend, no API.
626
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-probe.ts)
627
+ }
628
+
629
+ check_project_trust_handler() {
630
+ # Deterministic gate for 0.11 Stage 0 (Trust 2층 active-prompt escape): the
631
+ # project_trust handler. Pure decideProjectTrust over real preflight outcomes —
632
+ # approve/trusted-no-arg→{yes,remember:false}; direct distrust→{no}; inherited
633
+ # distrust + interactive + "trust-here"→{yes,remember:true} (THE escape, beats
634
+ # the ancestor false); non-interactive (pi -p / rpc)→{undecided}, never prompts;
635
+ # never undefined. Plus the thin adapter: fake ctx.ui.select, single-writer
636
+ # (handler never calls store.set — pi persists on remember:true), F5a evidence
637
+ # in the prompt title. No real UI, no backend, no API.
638
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-project-trust-handler.ts)
639
+ }
640
+
641
+ check_entwurf_v2_contract() {
642
+ # Deterministic gate for 0.11 Stage 0 step 4-pre: the FROZEN entwurf_v2
643
+ # contract (동결결정 10 + 버킷 B F1/F4/F6 + Fable R1-R5). The intent×liveness
644
+ # decision table is a constant; the "table cell ↔ dispatch receipt" round-trip
645
+ # is asserted exhaustively — THE executable proof F6 demands ("산문 금지").
646
+ # R1 backend liveness domain (pi only; claude-code/codex/antigravity =
647
+ # unsupported, never folded into dead/indeterminate). 6-cell table, single
648
+ # verdict per cell (Q2), 2 allow / 4 reject. N1 indeterminate never spawns;
649
+ # Q2 owned-outcome+live never auto-sends. R5 taxonomy covers table reasons +
650
+ # pre-claims bad-target/untrusted-fail-fast/target-locked (bucket B F2). Plus
651
+ # a schema↔types drift guard on the TypeBox input/receipt. Pure, no API.
652
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-contract.ts)
653
+ }
654
+
655
+ check_entwurf_v2_lock() {
656
+ # Deterministic gate for 0.11 Stage 0 step 5a (버킷 B F2): the per-gid dispatch
657
+ # LOCK primitive — the only guard against a double-spawn of the same dormant
658
+ # target (pi self-guards CREATE but not RESUME, 검증원장 F2). acquire =
659
+ # openSync(lockPath,"wx") atomic; a second acquire without release =
660
+ # target-locked conflict carrying the holder JSON (F2-P2 human cleanup). release
661
+ # = unlink ONLY when the on-disk nonce is still ours (a successor's re-acquire
662
+ # survives a late release). Stale reclaim ONLY for same host + ESRCH; EPERM
663
+ # (other user's live pid) / different host / alive pid / unknown error all
664
+ # fail-closed to conflict. Empty/corrupt lockfile = conflict, never
665
+ # auto-deleted. F2-P1: a malformed gid throws before any path is built. Real
666
+ # temp dir (wx atomicity under test); clock/nonce/pid/host/kill injected.
667
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-lock.ts)
668
+ }
669
+
670
+ check_entwurf_v2_decider() {
671
+ # Deterministic gate for 0.11 Stage 0 step 5b: the PURE dispatch decider
672
+ # decideDispatch. Drives the frozen 7-step order over INJECTED fakes (target
673
+ # lookup / lock / socket inspect+probe / preflight / capability), tracking lock
674
+ # acquire+release so "reject ⇒ no plan AND no lock retained" is PROVEN. Covers:
675
+ # bad-target/target-locked/target-address-conflict carry observedLiveness=null
676
+ # (pre-probe), every other reject + untrusted-fail-fast carry a measured value;
677
+ # control-socket send + spawn-bg resume execute KEEP the lock, meta-mailbox send
678
+ # takes NO lock (?7); resume plan has no mode/wantsReply/provider/model but has
679
+ # expectedSocketPath/observeTimeoutMs/releaseWhen; an invalid gid throws before
680
+ # any lookup (F2-P1). Pure, no IO, no API.
681
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-decider.ts)
682
+ }
683
+
684
+ check_entwurf_v2_matrix() {
685
+ # Deterministic gate for 0.11 Stage 0 step 5d-5 (a): the REACHABILITY + LOCK SSOT
686
+ # TABLE. Drives the REAL decideDispatch over minimal injected fakes and fixes, as
687
+ # one readable table, every (target kind → transport → lock class) cell the 5d-5
688
+ # claim covers: bad-target/address-conflict/target-locked rejects, unsupported
689
+ # meta-mailbox (deliverable) vs mailbox-undeliverable (inactive) vs owned reject,
690
+ # in-domain control-socket (live) / spawn-bg (dormant owned) / released rejects
691
+ # (owned-live, ff-dormant, indeterminate, under-lock conflict). A coverage pass
692
+ # FAILS if any transport / lock class / pre-probe reject is missing — a dropped
693
+ # decider cell cannot pass silently. Thin coverage, NOT a decider re-impl; surface
694
+ # parity stays in check-entwurf-v2-surface. Pure, no IO, no API.
695
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-matrix.ts)
696
+ }
697
+
698
+ check_entwurf_v2_release() {
699
+ # Deterministic gate for 0.11 Stage 0 step 5c-1: the PURE release-policy reducer
700
+ # (decideReleasePolicy + reduceRelease) for the 5c transport hand. Proves the
701
+ # Fable-3 "release-after-observation" timing as a pure state machine BEFORE any
702
+ # spawn/send IO: meta-mailbox=never release (no lock), control-socket=release once
703
+ # on send-final, spawn-bg=spawn-started is NOT a release event (load-bearing) →
704
+ # release on the FIRST observed transition (socket-alive ∨ child-exited any code)
705
+ # or a failed start; socket↔exit race idempotent (single release either order);
706
+ # decideReleasePolicy enforces the lock-nullness invariant (?7). Pure, no IO.
707
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-release.ts)
708
+ }
709
+
710
+ check_entwurf_v2_send() {
711
+ # Deterministic gate for 0.11 Stage 0 step 5c-2a: the control-socket SEND hand
712
+ # (executeControlSocketSend) that WIRES real transport IO onto the 5c-1 release
713
+ # reducer. Proves the send->outcome->release ordering over injected fakes (no socket):
714
+ # ack->sent / in-band reject->rejected (no fallback) / dead->same-lock one-shot
715
+ # re-resolve (control retry or mailbox enqueue)->fallback-sent|rejected|failed /
716
+ # indeterminate->failed+rethrow with deadFallback+mailbox NEVER called (no
717
+ # double-delivery on an alive-but-stalled socket). Release fires exactly once per
718
+ # send-final; a releaseLock throw never masks the send failure (5b). IO-via-dep.
719
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send.ts)
720
+ }
721
+
722
+ check_entwurf_v2_send_fallback() {
723
+ # Deterministic gate for 0.11 Stage 0 step 5c-2b: the same-lock re-resolve RESOLVER
724
+ # (resolveDeadControlSendFallback) the 5c-2a hand calls on a dead connect. Proves the
725
+ # fire-and-forget re-resolve routing over injected fakes (no filesystem): alive->
726
+ # control-socket retry (inspected socketPath) / dead(absent)->reject (dormant-fire-
727
+ # forget-unsupported, NEVER spawn-bg) / indeterminate->reject / unsupported+deliverable
728
+ # ->meta-mailbox plan (mini-table, no inspect/probe) / unsupported+undeliverable->reject
729
+ # / bad-target + address-conflict->reject pre-probe. Mis-wire (plan/lock gid) fails loud
730
+ # before IO; inspect/probe throws PROPAGATE (the hand owns failed+release); the resolver
731
+ # has NO release seam; every execute plan keeps the held gid and is never spawn-bg.
732
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send-fallback.ts)
733
+ }
734
+
735
+ check_entwurf_v2_mailbox() {
736
+ # Deterministic gate for 0.11 Stage 0 step 5c-4 (the LAST 5c transport slice): the
737
+ # ENQUEUE-ONLY meta-mailbox SEND body (executeMetaMailboxSend) + its production
738
+ # sendViaMailbox adapter (makeProductionSendViaMailbox). Proves the wiring over an
739
+ # injected fake enqueue (no filesystem): sender present -> formatMetaMailboxBody with
740
+ # plan.wantsReply threaded (yes/no in body, the deliberate divergence from legacy's
741
+ # hard-coded false) / sender absent -> raw plan.message / enqueue opts EXACTLY
742
+ # {gardenId: plan.targetGardenId, body, sessionsDir, mailboxDir} (no re-derivation) /
743
+ # enqueue throw PROPAGATES (never folded into success:false — a mailbox has no in-band
744
+ # refuse) / success -> {success:true}. Production adapter resolves {success:true},
745
+ # consults senderProvider once, and NEVER touches the lock (a poison LockClaim whose
746
+ # every access throws still resolves). Source guard: the lib code has NO release seam
747
+ # and NO routing seam (no releaseLock / inspect / probe / resolve) — a lock leak or
748
+ # re-route is structurally impossible.
749
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-mailbox.ts)
750
+ }
751
+
752
+ check_entwurf_v2_runner() {
753
+ # Deterministic gate for 0.11 Stage 0 step 5d-1: the execute-router (executeDispatch) that
754
+ # routes an already-decided DispatchDecision to its 5c transport hand and maps the outcome
755
+ # to one outcome-rich EntwurfV2RunResult. Proves over injected fake hands (no socket/spawn/
756
+ # timer): reject -> rejected (receipt+diagnostic carried, NO hand called) / control-socket ->
757
+ # sendControl(plan, lock) / spawn-bg -> resumeSpawnBg (socket-alive AND lock-retained both
758
+ # ride `executed`, fail-closed is not a failure) / meta-mailbox -> sendMailbox(plan, NULL
759
+ # lock, ?7). Carry-overs: N3 control `rejected` carries rejectReason verbatim; N1
760
+ # SendDeliveredReleaseFailedError -> execution-failed{finalizedOutcome, releaseFailed,
761
+ # retrySafe:false}; a plain hand throw -> execution-failed{retrySafe:false} with no
762
+ # finalizedOutcome. Exactly one hand runs per execute.
763
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-runner.ts)
764
+ }
765
+
766
+ check_entwurf_v2_surface() {
767
+ # Deterministic gate for 0.11 Stage 0 step 5d-3a: the ctx-free surface adapter
768
+ # (entwurf-v2-surface.ts) + the entwurf-control.ts wiring contract. Proves the pure parts:
769
+ # toDispatchInput (wants_reply→wantsReply, absent mode/wants_reply undefined) / renderEntwurfV2Result
770
+ # per result kind ({text,isError} surfacing reject diagnostic, control N3 rejectReason, spawn
771
+ # lock-retained, N1 delivered-but-dirty) / surface ctx-free source guard / entwurf-control
772
+ # registers entwurf_v2 + reaches the fence via a NON-LITERAL dynamic import (no static fence
773
+ # import → TS5097 stays closed) + decorates sender origin:pi-session/replyable:true.
774
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-surface.ts)
775
+ }
776
+
777
+ check_entwurf_bridge_boot() {
778
+ # Deterministic gate for 0.11 step 5d-5-pre (G1a/G1b): boots the entwurf-bridge MCP server
779
+ # as it ships (start.sh → node --experimental-strip-types, no build) and asserts what the
780
+ # source-shape gate check-entwurf-v2-surface cannot — that the whole v2 fence graph LOADS at
781
+ # boot under strip-types (G1a: a parseable tools/list proves it) and that entwurf_v2 is
782
+ # registered on the runtime surface with its schema (G1b). tools/list only → no tools/call,
783
+ # no lock/fs side effect, no auth → safe in pnpm check. Broad protocol/negative suite stays
784
+ # in check-bridge/test.sh (D1=A안).
785
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-boot.ts)
786
+ }
787
+
788
+ check_entwurf_v2_production() {
789
+ # Deterministic gate for 0.11 Stage 0 step 5d-2b: makeProductionEntwurfV2Deps — the ctx-free
790
+ # PRODUCTION assembly of runEntwurfV2's deps. Proves the wiring over fake leaf-IO spies (no
791
+ # real socket/lock/spawn/meta-record): decide wraps decideDispatch and acquires under the
792
+ # wired lockDir / control sendOverSocket builds the RpcSendCommand + maps + releases under
793
+ # lockDir / QB3 the spawn watcher releases via the SHARED lockDir release (not the spawn
794
+ # factory default) / the mailbox hand enqueues onto the wired dirs / a dead control send
795
+ # re-resolves to the SAME sendViaMailbox instance on the SAME dirs (Q3+Q5 no drift).
796
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-production.ts)
797
+ }
798
+
799
+ check_entwurf_control_rpc() {
800
+ # Gate for 0.11 Stage 0 step 5d-2 (RPC-helper extraction micro-slice): the --entwurf-control
801
+ # socket protocol (wire types + the newline-JSON client sendRpcCommand) moved to the ctx-free
802
+ # SSOT lib/entwurf-control-rpc.ts behaviour-preservingly. Proves: lib is ctx-free (no
803
+ # ExtensionContext/ExtensionAPI/@earendil-works/pi-ai) / entwurf-control.ts imports
804
+ # sendRpcCommand from the lib and no longer defines its own / real short unix-socket round-trip
805
+ # (write command -> matched {type:response,command,success:true} -> resolve) / close-before-
806
+ # response rejects 'connection closed before response'. net.Server only, no model/pi process.
807
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-control-rpc.ts)
808
+ }
809
+
810
+ check_entwurf_v2_spawn() {
811
+ # Deterministic gate for 0.11 Stage 0 step 5c-3a: the spawn-bg RESUME watcher hand
812
+ # (executeSpawnBgResume) wiring spawn + socket-observe IO onto the 5c-1 reducer. Proves
813
+ # Fable-3 over injected deferred promises (no real child/socket/timer): TIMEOUT IS NOT A
814
+ # RELEASE — a bare observeTimeout escalates to killChild (release 0), then a BOUNDED
815
+ # killGrace waits for a real socket-alive / child-exited to release. socket-alive ∨
816
+ # child-exited(any code incl. null) -> release exactly once; the loser settling later is a
817
+ # no-op. spawnChild throw -> spawn-start-failed (release, nothing to watch). No observation
818
+ # obtainable (grace elapses, or a post-spawn watch dep throws and the exit can't be
819
+ # observed) -> lock-retained fail-closed (released:false, pid/socket/lockPath surfaced) —
820
+ # there is NO direct-release hatch; deps.releaseLock is reached ONLY via reduceRelease.
821
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn.ts)
822
+ }
823
+
824
+ check_entwurf_resume_args() {
825
+ # Deterministic gate for 0.11 Stage 0 step 5c-3b: the resume-argv SSOT
826
+ # (buildResumePiArgs) that the legacy async worker AND the v2 spawn-bg resident citizen
827
+ # share so their launch shapes never drift. Pins the load-bearing A1 difference: legacy =
828
+ # `--no-extensions` + NO `--entwurf-control` (one-shot `pi -p` can exit); v2-control =
829
+ # `--entwurf-control` + NO `--no-extensions` (the keep-alive legacy avoided is the goal —
830
+ # resumed session stands its socket up and stays addressable). BOTH keep `--mode json -p`
831
+ # and the prompt-as-turn positional (`-p` NOT dropped in v2); explicitExtensionArgs
832
+ # preserved exactly once (provider-resolution footgun #29); v2 includes plan.launchArgs
833
+ # (--approve) before the prompt; null provider emits no --provider; no cross-contamination.
834
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-resume-args.ts)
835
+ }
836
+
837
+ check_entwurf_v2_spawn_production() {
838
+ # Deterministic gate for 0.11 Stage 0 step 5c-3c: the production SpawnBgResumeDeps factory
839
+ # (makeProductionSpawnBgResumeDeps) wiring the 5c-3a watcher's six IO seams onto the real
840
+ # world — proven WITHOUT a real pi spawn/socket/timer (that is the opt-in
841
+ # smoke-entwurf-v2-spawn-live, kept OUT of pnpm check). socketWatchVerdict (R2 policy):
842
+ # address-conflict→forged (reject, never wait), alive→alive, dead/indeterminate→wait.
843
+ # spawnChild builds the v2-control argv (--entwurf-control, no --no-extensions, -p+prompt,
844
+ # --approve, ext/provider/model, header cwd). awaitSocketAlive: connectable resolves, forged
845
+ # (symlink) rejects without connecting, dead→wait→alive, abort clears the sleep. awaitChildExit
846
+ # resolves the code + removes the listener on abort. awaitTimeout schedules + abort-clears.
847
+ # killChild=SIGTERM; releaseLock delegates; a proc-less child fails loud (mis-wire).
848
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn-production.ts)
849
+ }
850
+
851
+
852
+ smoke_entwurf_v2_spawn_live() {
853
+ # LIVE phase gate for 0.11 Stage 0 step 5c-3c (D5) — kept OUT of `pnpm check`. Exercises the
854
+ # production SpawnBgResumeDeps against REAL OS objects (a real unix socket, real child
855
+ # processes, real timers, real abort teardown) to catch what the deterministic gate's fakes
856
+ # cannot: actual spawn/exit/error event semantics, real lstat+connect liveness, and the
857
+ # 5c-3a watcher's timeout→kill→child-exited→release integration on a live process. It does
858
+ # NOT spawn a real `pi --entwurf-control` resume (that is the 5d surface matrix). Run once
859
+ # before 5d and record the result: LIVE=1 ./run.sh smoke-entwurf-v2-spawn-live
860
+ if [ "${LIVE:-}" != "1" ]; then
861
+ echo "[smoke-entwurf-v2-spawn-live] skipped — set LIVE=1 to run (spawns real children + opens a real unix socket)."
862
+ return 0
863
+ fi
864
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-spawn-live.ts)
865
+ }
866
+
867
+ smoke_acp_socket_citizen_live() {
868
+ # S1 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
869
+ # Spawns a REAL `pi --entwurf-control` resident on an ACP model
870
+ # (entwurf/claude-opus-4-8) and proves it is a first-class socket-citizen:
871
+ # the control socket stands up, get_info answers with the ACP model (model-lock
872
+ # did NOT revert — QM1), idle/cwd are reported, and the fail-loud streamSimple
873
+ # stub never fires (turn-free launch — QM2). No prompt is sent: S1 proves
874
+ # citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
875
+ # Model override: ENTWURF_S1_MODEL (default claude-opus-4-8).
876
+ # LIVE=1 ./run.sh smoke-acp-socket-citizen-live
877
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-socket-citizen-live.ts)
878
+ }
879
+
880
+ smoke_acp_raw_turn_live() {
881
+ # S2a-2 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
882
+ # Drives ONE real ACP turn through the pinned Claude adapter: spawns
883
+ # claude-agent-acp from its resolved package bin, speaks ACP over stdio NDJSON
884
+ # (ndJsonStream + the connectAcpClient adapter), runs initialize -> newSession ->
885
+ # (sonnet) setSessionConfigOption(model) -> prompt("say OK"), and asserts a live "OK" reply
886
+ # plus captured raw NDJSON bytes. NO provider/overlay/streamSimple/_meta — the
887
+ # raw backend pipe only. Launch source must be the package bin (PATH fallback
888
+ # fails acceptance unless ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK=1, debug).
889
+ # Model override: ENTWURF_ACP_RAW_TURN_MODEL (default claude-sonnet-4-6).
890
+ # LIVE=1 ./run.sh smoke-acp-raw-turn-live
891
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-raw-turn-live.ts)
892
+ }
893
+
894
+ smoke_acp_overlay_live() {
895
+ # S2b acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
896
+ # One layer above the S2a raw turn: materializes the Claude config overlay
897
+ # (realDir = operator ~/.claude for live creds; overlay settings.json is ours,
898
+ # hooks:{}), spawns claude-agent-acp with CLAUDE_CONFIG_DIR=<overlay> (verified
899
+ # in the child's /proc/<pid>/environ), opens a session with a tool-narrowed
900
+ # _meta.claudeCode.options (tools + disallowedTools) and NO _meta.systemPrompt
901
+ # (billing carrier stays absent), then drives one live "OK" turn. NO
902
+ # provider/streamSimple (backend-stub stays fail-loud — that is S2c); no
903
+ # event-mapping/session-reuse/engraving (S2d). Does NOT diff the live
904
+ # meta-store for mailbox absence (flaky — concurrent sessions); the honest
905
+ # claim is overlay-supplies-hooks:{}. Launch must be the package bin (PATH
906
+ # fallback fails acceptance unless ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK=1).
907
+ # Model override: ENTWURF_ACP_OVERLAY_MODEL (default claude-sonnet-4-6).
908
+ # LIVE=1 ./run.sh smoke-acp-overlay-live
909
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-overlay-live.ts)
910
+ }
911
+
912
+ smoke_acp_memory_containment_live() {
913
+ # Gate D — ACP Claude memory containment, end-to-end. OUT of pnpm check, LIVE=1.
914
+ # THE regression guard that was missing: drives the SHIPPED config (overlay +
915
+ # PRESENT engraving carrier = the v1 preset-replacement lever) with a turn that
916
+ # EXPLICITLY asks the model to persist a nonce to its memory, then asserts NO
917
+ # file appears under <overlay>/projects/**/memory/**. Permission is GRANTED (not
918
+ # cancelled) and writeTextFile delegation is PERFORMED, so the only thing that
919
+ # can stop a memory write is the lever — not us. Fails loud if engraving.md is
920
+ # empty (carrier OFF = no containment). Launch must be the package bin (PATH
921
+ # fallback fails acceptance unless ENTWURF_ACP_MEMORY_ALLOW_PATH_FALLBACK=1).
922
+ # Model override: ENTWURF_ACP_MEMORY_MODEL (default claude-sonnet-4-6).
923
+ # LIVE=1 ./run.sh smoke-acp-memory-containment-live
924
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-memory-containment-live.ts)
925
+ }
926
+
927
+ smoke_acp_provider_live() {
928
+ # S2c acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
929
+ # Drives the REAL pi PROVIDER path end to end: a real `pi` loads this
930
+ # checkout's extension (--no-extensions -e REPO_ROOT), selects
931
+ # entwurf/<model>, and pi's runner calls our streamSimple (backend.ts),
932
+ # which spawns claude-agent-acp under the overlay, runs one turn, and maps the
933
+ # result back through the S2c event mapper. Asserts a unique nonce in the
934
+ # assistant reply (live model proof) + the removed S0 stub error never appears
935
+ # (provider path actually opened) + pi exits 0. Tool-free prompt; the
936
+ # event-mapper gate owns the tool→notice contract.
937
+ # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-4-6).
938
+ # LIVE=1 ./run.sh smoke-acp-provider-live
939
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-provider-live.ts)
940
+ }
941
+
942
+ smoke_acp_session_reuse_live() {
943
+ # S2d-1b-2b acceptance smoke (in-memory session reuse) — OUT of pnpm check,
944
+ # needs LIVE=1. Forces process-scoped (pushes --entwurf-control into argv) and
945
+ # drives TWO real ACP turns over ONE reused claude-agent-acp child via the real
946
+ # streamShellAcp: turn 1 introduces a codeword (full transcript), turn 2 sends
947
+ # ONLY the latest user delta and must recall the codeword — proving the child
948
+ # was reused and the live ACP session kept turn-1 history (a respawn-per-turn
949
+ # backend would forget it). The one-shot exit0 half is owned by
950
+ # smoke-acp-provider-live.
951
+ # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-4-6).
952
+ # LIVE=1 ./run.sh smoke-acp-session-reuse-live
953
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-session-reuse-live.ts)
954
+ }
955
+
956
+ smoke_acp_carrier_augment_live() {
957
+ # S2e-1 acceptance smoke (billing carrier + first-user augment) — OUT of pnpm
958
+ # check, needs LIVE=1. Writes a unique secret into the scratch cwd's AGENTS.md
959
+ # (never the prompt) and drives one real provider turn: the reply must carry the
960
+ # secret (the augment rode the wire to the model) and the EMPTY default carrier
961
+ # must bill clean (exit 0, no HTTP-400 canary — 핀1 live). Optional tiny carrier
962
+ # check via SMOKE_ACP_CARRIER_PRESENT=1 (non-blocking).
963
+ # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-4-6).
964
+ # LIVE=1 ./run.sh smoke-acp-carrier-augment-live
965
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-carrier-augment-live.ts)
966
+ }
967
+
968
+ smoke_acp_mcp_live() {
969
+ # S2g LIVE 1 — operator MCP passthrough acceptance. OUT of pnpm check, needs
970
+ # LIVE=1. Registers a TINY isolated probe MCP server (scripts/fixtures/
971
+ # probe-mcp-server.ts, one tool probe_nonce) in a scratch .pi/settings.json and
972
+ # drives one real provider turn: the model must CALL the tool and echo the nonce
973
+ # that lives only inside the MCP server env. Proves the operator's
974
+ # entwurfProvider.mcpServers reaches the live ACP session (the GLG-baseline
975
+ # fix). Isolated probe (not entwurf-bridge) so a failure does not blur into
976
+ # identity/env wiring. Model override: ENTWURF_ACP_PROVIDER_MODEL.
977
+ # LIVE=1 ./run.sh smoke-acp-mcp-live
978
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-mcp-live.ts)
979
+ }
980
+
981
+ smoke_acp_skill_live() {
982
+ # S2g LIVE 2 — operator skillPlugins passthrough acceptance. OUT of pnpm check,
983
+ # needs LIVE=1. Builds a temp skill plugin (.claude-plugin/plugin.json +
984
+ # skills/<name>/SKILL.md carrying a unique nonce instruction), points
985
+ # entwurfProvider.skillPlugins at it, and drives one real provider turn: the
986
+ # model must surface/use the skill and echo the nonce. Proves skillPlugins +
987
+ # the Skill/Skill(*) auto-add reach the live session (the other half of the GLG
988
+ # baseline). Model override: ENTWURF_ACP_PROVIDER_MODEL.
989
+ # LIVE=1 ./run.sh smoke-acp-skill-live
990
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-skill-live.ts)
991
+ }
992
+
993
+ smoke_acp_bundled_mcp_live() {
994
+ # S2g LIVE 3 (axis 3) — the BUNDLED entwurf-bridge reaches the live ACP session
995
+ # via the 0.11.0 resident/RPC circuit. OUT of pnpm check, needs LIVE=1. Launches a
996
+ # real `pi --entwurf-control --mode rpc` resident on an ACP model and drives ONE
997
+ # model turn over the stdin RPC asking it to call mcp__entwurf-bridge__entwurf_self;
998
+ # captures the identity envelope (the resident's own fresh gid — never told to the
999
+ # model, only in the bridge env — + agentId + socketState alive) and agent_end
1000
+ # DIRECTLY from the stdout RPC event stream (gnew-rpc-drive shape). Complements
1001
+ # smoke-acp-mcp-live (tiny isolated probe): this proves the REAL bundled bridge with
1002
+ # envelope injection. NOT `pi -p` one-shot (that bundled-MCP teardown hang is
1003
+ # diagnostic backlog, not the 0.11.0 release circuit). Model override:
1004
+ # ENTWURF_ACP_PROVIDER_MODEL.
1005
+ # LIVE=1 ./run.sh smoke-acp-bundled-mcp-live
1006
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-bundled-mcp-live.ts)
1007
+ }
1008
+
1009
+ smoke_acp_rgg_live() {
1010
+ # S2e-2 — ACP-provider resident garden guard (RGG). Thin wrapper (GPT c32a6c8):
1011
+ # runs the SHARED resident-garden-guard runner against the entwurf provider
1012
+ # target with the DETERMINISTIC half only (SMOKE_RGG_POSITIVE=0). What this lane
1013
+ # treats as release-blocking is that garden-native resident discipline (uuid
1014
+ # refuse / new·clone cancel / legacy-resume pre-cancel / gnew clean birth) holds
1015
+ # under the ACP provider too — the guard logic is provider-agnostic. The positive
1016
+ # GNEW T3 (model autonomously calling entwurf_self) is N/A here BY ACP BOUNDARY:
1017
+ # the ACP child is spawned with mcpServers:[] so it has no entwurf_self call
1018
+ # surface (plugin stays lightweight, no ambient MCP — S2b/S2d boundary). To
1019
+ # observe that boundary directly, run the shared runner with SMOKE_RGG_POSITIVE=1
1020
+ # and ENTWURF_LIVE_TARGET set (T3 will report N/A, not a real failure).
1021
+ # Target override: ENTWURF_RGG_TARGET (default entwurf/claude-sonnet-4-6).
1022
+ # ./run.sh smoke-acp-rgg-live
1023
+ local target="${ENTWURF_RGG_TARGET:-entwurf/claude-sonnet-4-6}"
1024
+ (cd "$REPO_DIR" && ENTWURF_LIVE_TARGET="$target" SMOKE_RGG_POSITIVE=0 bash scripts/smoke-resident-garden-guard.sh)
1025
+ }
1026
+
1027
+ smoke_entwurf_v2_matrix_live() {
1028
+ # LIVE sentinel for 0.11 Stage 0 step 5d-5 (D4-b) — kept OUT of `pnpm check`. The deterministic
1029
+ # sibling (check-entwurf-v2-matrix) fixes every (target kind → transport → lock) cell over fakes
1030
+ # with ZERO IO; this drives the REAL production runEntwurfV2 deps against REAL OS objects on the
1031
+ # substrate happy path across 3 cells: C1 control-socket (a real `pi --entwurf-control` resident
1032
+ # → control-socket RPC send → lock acquire→release ×1), C2 meta-mailbox deliverable (armed
1033
+ # self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver →
1034
+ # reject, no garbage). Model-in-loop is OUT (GPT Q2): "does the sender model call entwurf_v2"
1035
+ # is a separate behavior test — this is a transport/lock/enqueue gate. Negative/timeout/contention
1036
+ # stay deterministic. Honest skip when LIVE!=1 so the release-gate is runnable unattended.
1037
+ # Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4).
1038
+ # LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
1039
+ if [ "${LIVE:-}" != "1" ]; then
1040
+ echo "[smoke-entwurf-v2-matrix-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket)."
1041
+ return 0
1042
+ fi
1043
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-matrix-live.ts)
1044
+ }
1045
+
1046
+ smoke_entwurf_v2_spawn_resume_live() {
1047
+ # The 0.11.0 (A) acceptance gate — kept OUT of `pnpm check`. Unlike matrix-live (a
1048
+ # transport/lock sentinel, model-in-loop OUT) and spawn-live (OS-substrate watcher, no real
1049
+ # pi), this drives the FULL production loop: mint a backend=pi meta identity → seed a REAL
1050
+ # dormant pi session (one-shot `pi --mode json -p --no-extensions` into the REAL
1051
+ # ~/.pi/agent/sessions) → runEntwurfV2(intent=owned-outcome) routes the dormant in-domain pi
1052
+ # citizen to spawn-bg resume → a REAL detached `pi --entwurf-control` child stands its socket
1053
+ # up, resumes, and DOES a model turn. Asserts: executed/spawn-bg/socket-alive/released, lock
1054
+ # released exactly once + no lock file, resident pid alive + socket connectable, and the
1055
+ # resume USER + assistant OK nonces appended to the session JSONL (real work, not just
1056
+ # "process up"). This is the evidence v1 deprecation (0.12) is predicated on. Model-in-loop is
1057
+ # IN. Honest skip when LIVE!=1 (skip = CI safety, NOT an acceptance PASS).
1058
+ # Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.4);
1059
+ # ENTWURF_SPAWN_RESUME_ASSISTANT_TIMEOUT_MS (default 180000).
1060
+ # LIVE=1 ./run.sh smoke-entwurf-v2-spawn-resume-live
1061
+ if [ "${LIVE:-}" != "1" ]; then
1062
+ echo "[smoke-entwurf-v2-spawn-resume-live] skipped — set LIVE=1 to run (spawns a real pi resume child + opens a real socket)."
1063
+ return 0
1064
+ fi
1065
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-spawn-resume-live.ts)
1066
+ }
1067
+
1068
+ check_entwurf_facts() {
1069
+ # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 1): the PURE
1070
+ # fact core. Locks the PeerFact shape + R1/R3b liveness invariant before any IO
1071
+ # wiring (gate-first). R1: out-of-domain backend (claude-code/codex/antigravity)
1072
+ # → unsupported for EVERY socket input (never coerced to the socket value or
1073
+ # dead). R3b: in-domain pi → alive/dead/indeterminate, null → indeterminate
1074
+ # (no proof ≠ dead). facts-only keyset: identity facts + liveness, NO
1075
+ # verb-routing (resumable/sendable/transport/dispatch/action) and NO
1076
+ # transcriptPath (동결결정 10). Pure, no IO, no API.
1077
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-facts.ts)
1078
+ }
1079
+
1080
+ check_socket_discovery() {
1081
+ # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 3): the
1082
+ # SOCKET-axis wiring scanSocketProbes. Probes the union of (dir sockets) ∪
1083
+ # (every in-domain pi citizen's canonical path) so a dormant citizen with no
1084
+ # socket file reads dead (ENOENT) → resumable, never an unprobed gap (slice 2
1085
+ # throws on that). Three-valued throughout — a stalled socket stays
1086
+ # indeterminate (F3), never folded to dead by an alive-only listing. Dir
1087
+ # hygiene (non-.sock / malformed names ignored), dedup, missing-dir, sort, and
1088
+ # an end-to-end scanSocketProbes→resolveFactList. readdir/probe injected, no IO.
1089
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-discovery.ts)
1090
+ }
1091
+
1092
+ check_meta_listing() {
1093
+ # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4a): the
1094
+ # meta-store axis listAllMetaIdentities. Explicit-partial: a parse failure or
1095
+ # body/filename drift does NOT blind the listing (valid records still surface)
1096
+ # and does NOT throw (0.10 "corrupt blocks registration forever" lesson) — it
1097
+ # becomes an explicit error carrying ONLY {filename, message}, verbatim (a
1098
+ # salvaged gid string as a fact = synthetic backdoor). mode strict throws on
1099
+ # any error, collect returns partial. entries/readRecord injected, no IO.
1100
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-listing.ts)
1101
+ }
1102
+
1103
+ check_entwurf_fact_provider() {
1104
+ # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4b): the
1105
+ # ASSEMBLY layer listEntwurfFacts. listAllMetaIdentities → scanSocketProbes →
1106
+ # pre-quarantine non-pi/socket conflicts → resolveFactList(clean) →
1107
+ # {facts, diagnostics}. Throw-vs-diagnostics policy (GPT힣 C-원칙): expected
1108
+ # corruption (parse failure / gardenId↔socket collision) → diagnostics, listing
1109
+ # survives; impossible wiring invariant (resolveFactList duplicate/unprobed) →
1110
+ # throw, never swallowed. A collision quarantines BOTH the PeerFact and the
1111
+ # socket (gid is the universal address). meta + socket deps injected, no IO.
1112
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-fact-provider.ts)
1113
+ }
1114
+
1115
+ check_entwurf_peers_surface() {
1116
+ # Deterministic gate for 0.11 Stage 0 step 4 (fact-provider slice 4c): the MCP
1117
+ # entwurf_peers RENDER/PAYLOAD layer renderEntwurfPeers. Legacy `sessions` is a
1118
+ # PROJECTION of facts (alive pi citizens + alive socket-only), NOT a second scan
1119
+ # (a re-run getLiveSessions would bypass the provider quarantine); socketPath via
1120
+ # controlSocketPath (SSOT, no correlation-authority drift); count = projection
1121
+ # length not peers.length; three distinct arrays (peers/socketOnly/diagnostics);
1122
+ # NO verb-routing field in JSON (deep key scan) NOR word in text (title leak);
1123
+ # diagnostics in both surfaces; empty → "(none)"; unsupported shown; enrich null
1124
+ # → "(not enriched)". WIRING guard: bridge calls listEntwurfFacts+renderEntwurfPeers,
1125
+ # getLiveSessions gone. Facts fabricated, no IO (only static source read).
1126
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-peers-surface.ts)
1127
+ }
1128
+
1129
+
1130
+ check_entwurf_self_address() {
1131
+ # Deterministic gate for the self-addressability honesty predicate (SE-1/SE-2
1132
+ # slice 1). Guards the bug where the MCP bridge / pi-native claim replyable:true
1133
+ # from env presence alone: a socketless pi session, or a meta citizen whose owner
1134
+ # exited / whose idle-watch was never armed, all advertised replyable while
1135
+ # delivery silently failed (SE-1). Asserts: PURE truth table (pi replyable ⟺
1136
+ # socketAlive; meta ⟺ recordBacked ∧ ownerAlive ∧ watchArmed; external never),
1137
+ # incl. the two regression-proof rows (record-present + owner-dead / watch-unarmed)
1138
+ # that stay meaningful after slice 3 mints records; SOURCE GUARD that
1139
+ # buildStrictPiSenderEnvelope drops the hardcoded `replyable: true` and existsSync-
1140
+ # probes the socket, and entwurf_self renders alive vs expected (no path lie).
1141
+ # Slice boundary: meta watchArmed is wired from the slice-2 presence marker; do NOT
1142
+ # claim slice 1 green standalone (1+2 close in the same release block).
1143
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-self-address.ts)
1144
+ }
1145
+
1146
+ check_entwurf_deliverability() {
1147
+ # Deterministic gate for the conversational-mailbox deliverability predicate
1148
+ # (SE-1/SE-2 slice 2c). The predicate the enqueue sites must consult (slice 2d)
1149
+ # before writing a .msg. Asserts: computeMetaReceiverActive (active iff recordBacked
1150
+ # AND ownerAlive AND watchArmed, fail-closed, per-cause reasons); mailboxConversational-
1151
+ # Deliverable (deliverable iff wakeMode self-fetch AND active) — KEY rows: direct-inject
1152
+ # (pi) refused even when active (SE-1, no mailbox drain), self-fetch + dead-owner/unarmed
1153
+ # refused (SE-2, would rot); WIRING that the self-addressability predicate shares the
1154
+ # SAME active-receiver atom (one source of truth). Pure, no IO.
1155
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-deliverability.ts)
1156
+ }
1157
+
1158
+ check_entwurf_mailbox_guard() {
1159
+ # Deterministic gate for the guarded mailbox enqueue (SE-1/SE-2 slice 2d) — the IO
1160
+ # orchestration conversational-reply sites use instead of enqueueMetaMessage directly.
1161
+ # Asserts (GPT Q5, both axes): PURE 0-call — an undeliverable target (dead receiver /
1162
+ # direct-inject pi / absent record) leaves the injected enqueue UNCALLED, a deliverable
1163
+ # one calls it exactly once; TMPDIR SNAPSHOT with the real enqueueMetaMessage — a refused
1164
+ # send leaves the mailbox tree byte-identical (file list + content hash, not just mtime),
1165
+ # an accepted send writes exactly one .msg; plus fact gathering from record/capability/marker.
1166
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-mailbox-guard.ts)
1167
+ }
1168
+
1169
+
1170
+
1171
+ check_package_source_routing() {
1172
+ # Deterministic gate for #29 (package-installed Entwurf ACP routing). Pins
1173
+ # resolveExplicitExtensionSpec()'s package-source -> install-root mapping and
1174
+ # the fail-fast routing contract through the two public routing surfaces
1175
+ # (getRegistryRouting spawn path, getEntwurfExplicitExtensions resume path).
1176
+ # Covers the install matrix: local path / git user / npm user (+version) /
1177
+ # install-missing / project-scope-unseen / no-source, across local + remote,
1178
+ # plus self-root fallback and the resume unresolvedAcpIntent signal. Isolated
1179
+ # via a temp PI_CODING_AGENT_DIR — the real ~/.pi/agent is never touched. No
1180
+ # backend, no spawn, no API cost.
1181
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-package-source-routing.ts)
1182
+ }
1183
+
1184
+
1185
+ smoke_session_id_name() {
1186
+ # LIVE 3-turn substrate smoke (Phase 3a) for Pi 0.78 --session-id/--name,
1187
+ # exercised through the bridge but NOT through the Entwurf tool surface, so it
1188
+ # lands independently of the taskId->sessionId migration. Spawns real cheap
1189
+ # sonnet turns (auth + tokens) and asserts: header id/cwd, session_info name as
1190
+ # info layer, append-not-recreate, spawn-only name, and the wrong-cwd footgun
1191
+ # as documented evidence. Isolated via a temp PI_CODING_AGENT_DIR.
1192
+ section "smoke: --session-id / --name substrate (direct pi, no Entwurf API)"
1193
+ if ! command -v pi >/dev/null 2>&1; then
1194
+ fail "[smoke-session-id-name] pi binary not on PATH — cannot run live substrate proof"
1195
+ return 1
1196
+ fi
1197
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-session-id-name.ts) || {
1198
+ fail "[smoke-session-id-name] live substrate smoke failed"
1199
+ return 1
1200
+ }
1201
+ ok "[smoke-session-id-name] --session-id/--name substrate proven (append + spawn-only name + wrong-cwd footgun)"
1202
+ return 0
1203
+ }
1204
+
1205
+
1206
+
1207
+ check_dep_versions() {
1208
+ # Catches version-pin drift across package.json, run.sh, and README.md.
1209
+ # Concretely the kind of skew that produced commit 21de0f9's "0.11.1
1210
+ # leftover" review comment: package.json bumped to 0.12.0 while README
1211
+ # and run.sh's setup gate still claimed 0.11.1. Static check, no
1212
+ # subprocess — fast enough to run inside `pnpm check` and pre-commit.
1213
+ (cd "$REPO_DIR" && node --input-type=module <<'EOF'
1214
+ import { strict as assert } from 'node:assert';
1215
+ import { readFileSync } from 'node:fs';
1216
+
1217
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
1218
+ const geminiBundled = pkg.dependencies['@google/gemini-cli'] ?? pkg.optionalDependencies?.['@google/gemini-cli'];
1219
+ assert.equal(geminiBundled, undefined, 'package.json must not bundle @google/gemini-cli — gemini is an external PATH runtime');
1220
+
1221
+ // `^` + `m` flag anchors to the start-of-line shell assignment so we don't
1222
+ // accidentally pick up the regex literal inside this very check function's
1223
+ // heredoc (which is indented, so won't match `^...`).
1224
+ const runSh = readFileSync('run.sh', 'utf8');
1225
+
1226
+ // pi peer/dev alignment (#26 / 0.8.0 dep-alignment gate). The three
1227
+ // @earendil-works/pi-* devDeps must pin one identical version, and that
1228
+ // version must match the check-pack-install peer-install pins below
1229
+ // (`pnpm add @earendil-works/pi-ai@X ...`). Without this, a pi bump could
1230
+ // drift package.json devDeps away from the fresh-temp install smoke and
1231
+ // the "dependency alignment gate" would not actually verify pi.
1232
+ const piAi = pkg.devDependencies?.['@earendil-works/pi-ai'];
1233
+ const piCoding = pkg.devDependencies?.['@earendil-works/pi-coding-agent'];
1234
+ const piTui = pkg.devDependencies?.['@earendil-works/pi-tui'];
1235
+ assert.ok(piAi, 'package.json devDependencies must pin @earendil-works/pi-ai');
1236
+ assert.equal(piCoding, piAi,
1237
+ `@earendil-works/pi-coding-agent (${piCoding}) must match @earendil-works/pi-ai (${piAi})`);
1238
+ assert.equal(piTui, piAi,
1239
+ `@earendil-works/pi-tui (${piTui}) must match @earendil-works/pi-ai (${piAi})`);
1240
+
1241
+ // check-pack-install peer-install pins (quoted `@earendil-works/pi-*@<ver>`
1242
+ // args; the `\d`-anchored version avoids matching this regex literal itself).
1243
+ const peerAi = runSh.match(/"@earendil-works\/pi-ai@(\d[\d.]*)"/)?.[1];
1244
+ const peerCoding = runSh.match(/"@earendil-works\/pi-coding-agent@(\d[\d.]*)"/)?.[1];
1245
+ const peerTui = runSh.match(/"@earendil-works\/pi-tui@(\d[\d.]*)"/)?.[1];
1246
+ assert.equal(peerAi, piAi,
1247
+ `run.sh check-pack-install pi-ai peer pin (${peerAi}) must match package.json devDep (${piAi})`);
1248
+ assert.equal(peerCoding, piAi,
1249
+ `run.sh check-pack-install pi-coding-agent peer pin (${peerCoding}) must match (${piAi})`);
1250
+ assert.equal(peerTui, piAi,
1251
+ `run.sh check-pack-install pi-tui peer pin (${peerTui}) must match (${piAi})`);
1252
+
1253
+ // peerDependencies must be a CLOSED range (0.11 Stage 0, drift-proofing): the
1254
+ // floor tracks the devDep pin so a consumer can't install against a pi lacking
1255
+ // the 0.80 public trust exports the bridge imports, AND an upper bound at the
1256
+ // next minor stops a fresh install from silently pulling a future pi (0.81+)
1257
+ // whose internal export surface has drifted from the one we typecheck against.
1258
+ // pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
1259
+ // factory churn is exactly this), so an open `>=` floor is exactly how the next
1260
+ // installer re-acquires the drift. Expected
1261
+ // shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.80.2 <0.81`).
1262
+ const [piMaj, piMin] = piAi.split('.').map(Number);
1263
+ assert.equal(piMaj, 0,
1264
+ `pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
1265
+ const expectedPeer = `>=${piAi} <0.${piMin + 1}`;
1266
+ const peerDepAi = pkg.peerDependencies?.['@earendil-works/pi-ai'];
1267
+ const peerDepCoding = pkg.peerDependencies?.['@earendil-works/pi-coding-agent'];
1268
+ const peerDepTui = pkg.peerDependencies?.['@earendil-works/pi-tui'];
1269
+ assert.equal(peerDepAi, expectedPeer,
1270
+ `package.json peerDependencies @earendil-works/pi-ai (${peerDepAi}) must be "${expectedPeer}" (devDep floor + next-minor ceiling)`);
1271
+ assert.equal(peerDepCoding, expectedPeer,
1272
+ `package.json peerDependencies @earendil-works/pi-coding-agent (${peerDepCoding}) must be "${expectedPeer}"`);
1273
+ assert.equal(peerDepTui, expectedPeer,
1274
+ `package.json peerDependencies @earendil-works/pi-tui (${peerDepTui}) must be "${expectedPeer}"`);
1275
+
1276
+ console.log('[check-dep-versions] 11 assertions ok');
1277
+ EOF
1278
+ )
1279
+ }
1280
+
1281
+ check_pi_import_surface() {
1282
+ # 0.11 Stage 0 (동결결정 9): the bridge may reference @earendil-works/pi-*
1283
+ # ONLY by the package root. ANY subpath (`/dist`, `/core`, `/src`, `/foo`, …)
1284
+ # reaches pi's private surface and silently breaks on pi internal reshuffles.
1285
+ # The check is intentionally SPECIFIER-shaped, not import-keyword-shaped: it
1286
+ # matches a quoted/backtick module specifier `@earendil-works/pi-*/…`, so one
1287
+ # pattern catches static `from`, dynamic `import()`, `require()`,
1288
+ # `export … from`, side-effect `import "…"`, and whitespace variants alike.
1289
+ # Root import `@earendil-works/pi-coding-agent` (no trailing slash) is allowed.
1290
+ # Scans EVERY tracked .ts/.js/.mjs/.cjs source (git ls-files), not a hardcoded
1291
+ # file list — a new root file (acp-bridge.ts, event-mapper.ts, engraving.ts,
1292
+ # pi-context-augment.ts, protocol.js, …) can never silently escape the gate.
1293
+ #
1294
+ # pi 0.80 EXCEPTION — exactly ONE allowlisted subpath:
1295
+ # @earendil-works/pi-ai/compat
1296
+ # 0.80 moved the standalone root `getModels()` to the deprecated `/compat`
1297
+ # entrypoint. This repo's pi-extensions/** are loaded by pi's EXTENSION loader
1298
+ # (pi-coding-agent `core/extensions/loader.ts`), whose jiti alias map resolves
1299
+ # ONLY three pi-ai specifiers for extensions — the bare root, `/compat`, and
1300
+ # `/oauth` — all to `ai/dist/compat.js`. A `providers/*` subpath is NOT in that
1301
+ # map: jiti prefix-matches the bare `@earendil-works/pi-ai` alias and appends
1302
+ # the remainder, producing the unresolvable `…/dist/compat.js/providers/
1303
+ # anthropic` (verified live: extension load crash — invisible to static
1304
+ # typecheck, which resolves against node_modules `exports`). So `/compat` is the
1305
+ # sanctioned extension entrypoint for the old global model-catalog API
1306
+ # (lib/acp/models.ts: `getModels`), and the ONLY allowlisted exception. The
1307
+ # allow-pattern is closing-quote-anchored (`@earendil-works/pi-ai/compat["'\`]`)
1308
+ # so it permits ONLY that exact specifier: `/compat-foo`, `/oauth`, every
1309
+ # `/providers/*`, and any deeper path stay FORBIDDEN (we use only `/compat`).
1310
+ # Do NOT widen this to a `providers/*` subpath — it typechecks but CANNOT
1311
+ # resolve under the extension loader.
1312
+ local hits
1313
+ hits=$(cd "$REPO_DIR" && git ls-files '*.ts' '*.js' '*.mjs' '*.cjs' \
1314
+ | grep -vE '^(node_modules|dist)/' \
1315
+ | xargs -r grep -HnE "[\"'\`]@earendil-works/pi-(ai|coding-agent|tui)/" 2>/dev/null \
1316
+ | grep -vE "[\"'\`]@earendil-works/pi-ai/compat[\"'\`]" 2>/dev/null || true)
1317
+ if [ -n "$hits" ]; then
1318
+ echo "[check-pi-import-surface] FAIL: pi private subpath reference(s) — import @earendil-works/pi-* by the package ROOT only:"
1319
+ echo "$hits"
1320
+ exit 1
1321
+ fi
1322
+ ok "[check-pi-import-surface] pi references are root-only (no private subpath; all tracked ts/js scanned)"
1323
+ }
1324
+
1325
+ check_env_namespace() {
1326
+ # 0.11 S3 cutover lock: after the env-namespace rename, NO tracked source may
1327
+ # carry the old pi-centric env/const prefixes (PI_SHELL_ACP*, PI_META*,
1328
+ # PI_TOOLS_BRIDGE*, PI_ENTWURF*). This deterministic guard keeps the cutover
1329
+ # from silently regressing — a single old prefix slipping back in fails loud.
1330
+ # KEEP pi-adapter env (PI_SESSION_ID, PI_AGENT_ID, PI_CODING_AGENT_DIR,
1331
+ # PI_SETTINGS_PATH, PI_EMACS_AGENT_SOCKET) is NOT in the forbidden set, so it
1332
+ # passes untouched. The forbidden pattern uses a [_] char-class for the
1333
+ # trailing underscore so THIS gate's own definition never self-matches; for
1334
+ # the same reason every prose mention above uses a `*`, not a trailing `_`.
1335
+ # Docs/CHANGELOG/NEXT keep historical mentions and are excluded.
1336
+ local hits
1337
+ hits=$(cd "$REPO_DIR" && git ls-files \
1338
+ | grep -vE '\.(md|org)$|(^|/)NEXT|(^|/)CHANGELOG|^docs/' \
1339
+ | xargs -r grep -HnE 'PI_SHELL_ACP[_]|PI_META[_]|PI_TOOLS_BRIDGE[_]|PI_ENTWURF[_]' 2>/dev/null || true)
1340
+ if [ -n "$hits" ]; then
1341
+ echo "[check-env-namespace] FAIL: old pi env/const prefix survived the S3 cutover — rename to ENTWURF_*/ENTWURF_ACP_*/ENTWURF_META_*/ENTWURF_BRIDGE_*:"
1342
+ echo "$hits"
1343
+ exit 1
1344
+ fi
1345
+ ok "[check-env-namespace] env namespace is entwurf-only (no old pi env/const prefix in tracked source)"
1346
+ }
1347
+
1348
+ check_pi_runtime_version() {
1349
+ # 0.11 Stage 0 (동결결정 9, runtime half): tsc catches a missing 0.80 export
1350
+ # at dev time, but an installed environment can still resolve an older pi at
1351
+ # runtime where the named trust exports / 0.80 provider-factory surface do not
1352
+ # exist. Verify VERSION >= floor via a DYNAMIC import of the package root only —
1353
+ # never statically import a floor-only symbol here, or this guard would crash
1354
+ # before it can fail loud.
1355
+ (cd "$REPO_DIR" && node --input-type=module <<'EOF'
1356
+ const FLOOR = '0.80.2';
1357
+ const cmp = (a, b) => {
1358
+ const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
1359
+ for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
1360
+ return 0;
1361
+ };
1362
+ let VERSION;
1363
+ try {
1364
+ ({ VERSION } = await import('@earendil-works/pi-coding-agent'));
1365
+ } catch (e) {
1366
+ console.error(`[check-pi-runtime-version] FAIL: cannot import @earendil-works/pi-coding-agent root — ${e?.message ?? e}`);
1367
+ process.exit(1);
1368
+ }
1369
+ if (typeof VERSION !== 'string') {
1370
+ console.error('[check-pi-runtime-version] FAIL: pi root export VERSION is not a string');
1371
+ process.exit(1);
1372
+ }
1373
+ if (cmp(VERSION, FLOOR) < 0) {
1374
+ console.error(`[check-pi-runtime-version] FAIL: pi VERSION ${VERSION} < ${FLOOR} — the bridge is built and tested against the 0.80.2 public/runtime surface (trust exports hasTrustRequiringProjectResources + ProjectTrustStore nearest-ancestor get, the 0.80 model-catalog API getModels reached via the deprecated /compat entrypoint — 0.80 moved the standalone root getModels there, and the extension loader resolves only /compat, NOT the providers/* factory subpath — provider registration surface, compaction semantics) that older pi lacks or behaves differently on. Bump @earendil-works/pi-*.`);
1375
+ process.exit(1);
1376
+ }
1377
+ console.log(`[check-pi-runtime-version] ok — pi VERSION ${VERSION} >= ${FLOOR}`);
1378
+ EOF
1379
+ )
1380
+ }
1381
+
1382
+ check_install_preflight() {
1383
+ # 0.12 relocation guard (2026-06-23): `install` MUST fail loud on a repo whose
1384
+ # node_modules is missing (fresh clone, no pnpm install) or path-stale (a dir
1385
+ # move/rename broke the pnpm symlink store) — and it must fail BEFORE writing
1386
+ # settings.json, so the breakage is not a silent-red that only surfaces minutes
1387
+ # later as a dead MCP bridge (entwurf-bridge ✘ Failed to connect) + a
1388
+ # check-entwurf-v2-surface ERR_MODULE_NOT_FOUND. The preflight follows each
1389
+ # dep's symlink to its real package.json (immune to per-package "exports" maps
1390
+ # that forbid a bare-root or ./package.json import), which a bare
1391
+ # `test -d node_modules` cannot do for a dir-move. REPO_DIR is the dir holding
1392
+ # run.sh (line 16), so the negative cases copy run.sh into a temp dir to point
1393
+ # REPO_DIR at a deliberately-broken tree.
1394
+ local rc out proj fake dep
1395
+
1396
+ # positive — the live repo passes the REAL preflight. Calling it directly keeps
1397
+ # preflight_dep_integrity the single source of truth for the probe set (no
1398
+ # second hardcoded list to drift). Subshell so its exit-on-fail can't kill us.
1399
+ if ! ( preflight_dep_integrity ) >/dev/null 2>&1; then
1400
+ fail "[check-install-preflight] live repo fails preflight_dep_integrity — run 'pnpm install' (gate cannot validate against a broken repo)"
1401
+ exit 1
1402
+ fi
1403
+ ok "[check-install-preflight] live repo passes preflight (all runtime hard deps resolve)"
1404
+
1405
+ # negative 1 — missing node_modules (fresh clone)
1406
+ fake=$(mktemp -d); proj=$(mktemp -d)
1407
+ cp "$REPO_DIR/run.sh" "$fake/run.sh"
1408
+ rc=0; out=$("$fake/run.sh" install "$proj" 2>&1) || rc=$?
1409
+ if [ "$rc" -eq 0 ]; then
1410
+ fail "[check-install-preflight] missing node_modules did NOT fail install"; rm -rf "$fake" "$proj"; exit 1
1411
+ fi
1412
+ if [ -f "$proj/.pi/settings.json" ]; then
1413
+ fail "[check-install-preflight] install wrote settings.json with missing deps (SILENT-RED)"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1414
+ fi
1415
+ if ! printf '%s' "$out" | grep -q "repo dependency integrity check failed"; then
1416
+ fail "[check-install-preflight] missing node_modules failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1417
+ fi
1418
+ rm -rf "$fake" "$proj"
1419
+ ok "[check-install-preflight] missing node_modules → fails before writing settings"
1420
+
1421
+ # negative 2 — representative dangling symlink (dir move): node_modules/ EXISTS
1422
+ # (a bare `test -d` would pass) with the other runtime deps symlinked live, but a
1423
+ # representative dep (@anthropic-ai/sdk) dangles. Asserts the dir-move blind spot
1424
+ # is closed AND that the failure names the broken dep. Uses a BUNDLED runtime dep
1425
+ # because the @earendil-works/pi-* peer trio is loader-provided and no longer part
1426
+ # of the install preflight probe set.
1427
+ fake=$(mktemp -d); proj=$(mktemp -d)
1428
+ cp "$REPO_DIR/run.sh" "$fake/run.sh"
1429
+ mkdir -p "$fake/node_modules/@modelcontextprotocol" "$fake/node_modules/@agentclientprotocol" "$fake/node_modules/@anthropic-ai"
1430
+ ln -s "$REPO_DIR/node_modules/@modelcontextprotocol/sdk" "$fake/node_modules/@modelcontextprotocol/sdk"
1431
+ ln -s "$REPO_DIR/node_modules/@agentclientprotocol/sdk" "$fake/node_modules/@agentclientprotocol/sdk"
1432
+ ln -s "$REPO_DIR/node_modules/@agentclientprotocol/claude-agent-acp" "$fake/node_modules/@agentclientprotocol/claude-agent-acp"
1433
+ ln -s "$REPO_DIR/node_modules/zod" "$fake/node_modules/zod"
1434
+ ln -s /nonexistent/pnpm-store/anthropic-sdk "$fake/node_modules/@anthropic-ai/sdk"
1435
+ rc=0; out=$("$fake/run.sh" install "$proj" 2>&1) || rc=$?
1436
+ if [ "$rc" -eq 0 ]; then
1437
+ fail "[check-install-preflight] dangling dep symlink did NOT fail install (test -d blind spot)"; rm -rf "$fake" "$proj"; exit 1
1438
+ fi
1439
+ if [ -f "$proj/.pi/settings.json" ]; then
1440
+ fail "[check-install-preflight] install wrote settings.json with a dangling dep (SILENT-RED)"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1441
+ fi
1442
+ if ! printf '%s' "$out" | grep -q "repo dependency integrity check failed"; then
1443
+ fail "[check-install-preflight] dangling symlink failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1444
+ fi
1445
+ if ! printf '%s' "$out" | grep -q "@anthropic-ai/sdk"; then
1446
+ fail "[check-install-preflight] dangling case did not name the broken dep (@anthropic-ai/sdk):"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1447
+ fi
1448
+ rm -rf "$fake" "$proj"
1449
+ ok "[check-install-preflight] representative dangling symlink (test -d blind spot) → fails before writing settings, names the dep"
1450
+ }
1451
+
1452
+ check_pi_preflight() {
1453
+ # 0.11 Stage 0 (2): the controlled-launch trust decision. Proves frozen
1454
+ # decision 8 precedence (saved false > saved true > prefix > no-inputs >
1455
+ # fail-fast) and decision 7's separator-boundary prefix against pi's own
1456
+ # ProjectTrustStore in a temp agentDir. Deterministic, no network/backend.
1457
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-pi-preflight.ts)
1458
+ }
1459
+
1460
+
1461
+
1462
+ check_auth_boundary() {
1463
+ # Auth-boundary guard (re-introduced for the ACP plugin on v2, retargeted off
1464
+ # the deleted 0.11.0 index.ts/acp-bridge.ts onto the new provider entry). This
1465
+ # is the code-level pair of AGENTS §Operating boundaries (trust invariants):
1466
+ # entwurf is a no-auth ACP plugin at the pi provider layer — it does NOT
1467
+ # provide, resell, or bypass any backend credentials.
1468
+ #
1469
+ # pi.registerProvider requires an apiKey when defining custom models, but the
1470
+ # plugin consumes none: backend auth belongs to the operator's own Claude CLI
1471
+ # child process. The registration MUST therefore use the lowercase+hyphen
1472
+ # no-auth sentinel, NOT a bare ALL-CAPS legacy-ENV reference (e.g.
1473
+ # "ANTHROPIC_API_KEY") which trips pi's legacy-env deprecation AND falsely
1474
+ # presents the plugin as API-key dependent.
1475
+ #
1476
+ # Scope: the new provider entry + its lib/acp/* modules. The regex matches only
1477
+ # an `apiKey:` field assigned a quoted ALL-CAPS env name, so explanatory
1478
+ # comments and the no-auth sentinel identifier pass.
1479
+ section "auth boundary (ACP plugin no-auth sentinel)"
1480
+ (cd "$REPO_DIR" && node --input-type=module <<'EOF'
1481
+ import { strict as assert } from 'node:assert';
1482
+ import { readFileSync, readdirSync } from 'node:fs';
1483
+ const files = ['pi-extensions/acp-provider.ts'];
1484
+ for (const f of readdirSync('pi-extensions/lib/acp')) {
1485
+ if (f.endsWith('.ts')) files.push(`pi-extensions/lib/acp/${f}`);
1486
+ }
1487
+ const offenders = [];
1488
+ let sentinelSeen = false;
1489
+ for (const f of files) {
1490
+ const src = readFileSync(f, 'utf8');
1491
+ const re = /apiKey:\s*"([A-Z][A-Z0-9_]*)"/g;
1492
+ let m;
1493
+ while ((m = re.exec(src)) !== null) offenders.push(`${f}: apiKey: "${m[1]}"`);
1494
+ if (src.includes('entwurf-no-auth')) sentinelSeen = true;
1495
+ }
1496
+ assert.equal(offenders.length, 0,
1497
+ `ACP provider apiKey must be a no-auth sentinel, not a legacy-ENV reference. Offenders:\n ${offenders.join('\n ')}`);
1498
+ assert.ok(sentinelSeen,
1499
+ 'no-auth sentinel literal "entwurf-no-auth" not found in the ACP provider surface — auth boundary unverified');
1500
+ console.log(`[check-auth-boundary] ok — no legacy-ENV apiKey literal across ${files.length} ACP provider file(s); no-auth sentinel present`);
1501
+ EOF
1502
+ )
1503
+ }
1504
+
1505
+ check_acp_provider_surface() {
1506
+ # Deterministic gate for the S0 ACP provider loader/fence slice. Loads the REAL
1507
+ # provider lib modules and asserts the registration surface: one surface name,
1508
+ # curated Claude anchor present with full ProviderModelConfig rows, no-auth
1509
+ # sentinel shape, and a FAIL-LOUD streamSimple (calling it throws — no native
1510
+ # fallback, no empty-but-successful stream). Pure, no pi runtime, no API.
1511
+ section "ACP provider surface (S0 loader/fence)"
1512
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-provider-surface.ts)
1513
+ }
1514
+
1515
+ check_acp_sdk_surface() {
1516
+ # Deterministic gate for the S2a ACP SDK dependency surface. Pins the three
1517
+ # ACP runtime deps to the 0.11.0 oracle versions (@agentclientprotocol/sdk
1518
+ # 0.22.1 + claude-agent-acp 0.39.0 + @anthropic-ai/sdk 0.100.1), locks the
1519
+ # peer-resolution that keeps claude-agent-sdk satisfiable (0.100.1, not the
1520
+ # stale 0.91.1), asserts the wire SDK still value-exports the symbols the raw
1521
+ # turn needs (silent-rename gate), and forbids any source-level anthropic SDK
1522
+ # import / API-client use (the anthropic dep is a peer-pin ONLY).
1523
+ section "ACP SDK surface (S2a dep pin + peer-resolution + no-client-use)"
1524
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-sdk-surface.ts)
1525
+ }
1526
+
1527
+ check_acp_overlay() {
1528
+ # Deterministic gate for the S2b Claude config overlay materializer. Drives
1529
+ # ensureClaudeConfigOverlay against injected temp realDir/overlayDir (no
1530
+ # operator ~/.claude touched) and asserts: settings.json hooks:{} +
1531
+ # defaultMode default + autoMemory off; whitelisted entries symlinked;
1532
+ # projects/sessions overlay-private real dirs (NOT symlinks); operator
1533
+ # personal config (CLAUDE.md/settings.local.json/plugins/agents) never leaks;
1534
+ # stale symlinks cleaned; binary-owned files preserved; CLAUDE_CONFIG_DIR
1535
+ # launch-env planted; idempotent. Pure, no live model.
1536
+ section "ACP overlay (S2b claude-config-overlay)"
1537
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-overlay.ts)
1538
+ }
1539
+
1540
+ check_acp_tool_surface() {
1541
+ # Deterministic gate for the S2b Claude tool surface + exclude-tools
1542
+ # truthfulness preflight. Matrix over assertExcludeToolsHonored (claude
1543
+ # narrows via tools / native always-exposes / extension-tool exclusion is
1544
+ # honest) + buildClaudeSessionMeta shape lock (tools/allow/disallowed/
1545
+ # extraArgs/plugins) + the S2b billing-carrier guard (no _meta.systemPrompt
1546
+ # unless a caller supplies one). Pure preflight — NOT a backend wire read.
1547
+ section "ACP tool surface (S2b exclude-tools preflight + session meta)"
1548
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-tool-surface.ts)
1549
+ }
1550
+
1551
+ check_acp_event_mapper() {
1552
+ # Deterministic gate for the S2c ACP→pi event mapper + context conversion.
1553
+ # Feeds synthetic ACP session_notification updates through the mapper and
1554
+ # asserts the pi AssistantMessageEvent sequence, including the hard boundary:
1555
+ # tool_call / tool_call_update render as TEXT NOTICES, never structured
1556
+ # toolcall_* (the ACP child already executed the tool). Also locks the
1557
+ # context→ACP-prompt transcript passthrough (excludes systemPrompt/thinking,
1558
+ # single text block). Pure, no live backend.
1559
+ section "ACP event mapper (S2c notification→stream + context)"
1560
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-event-mapper.ts)
1561
+ }
1562
+
1563
+ check_acp_prompt_builder() {
1564
+ # Deterministic gate for the S2d bootstrapPath-scoped ACP prompt builder (핀4).
1565
+ # Proves prompt SCOPE follows bootstrapPath: new=full transcript (history
1566
+ # carrier), reuse/resume/load=latest user delta (first user after last
1567
+ # assistant, SessionStart hook skipped, image marker kept, prior history
1568
+ # excluded so a reuse session is not re-injected its own history). Pure, no
1569
+ # session store yet — locks the builder before S2d wires the reuse paths.
1570
+ section "ACP prompt builder (S2d bootstrapPath prompt scope)"
1571
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-prompt-builder.ts)
1572
+ }
1573
+
1574
+ check_acp_config() {
1575
+ # Deterministic gate for the S2g operator provider-config loader. Locks:
1576
+ # global+project merge (project overrides defined keys only; mcpServers merge
1577
+ # per-name with project win), defaults (strict-mcp-config on, [] sources,
1578
+ # baseline tools), fail-loud on invalid mcpServers/skillPlugins/
1579
+ # appendSystemPrompt:true/strictMcpConfig:false, nonempty skillPlugins auto-add
1580
+ # Skill+Skill(*), deterministic sorted mcp hash sensitive to command/env/url/
1581
+ # headers, and envelope enrich (PI_SESSION_ID/PI_AGENT_ID into entwurf-bridge
1582
+ # only, stale filtered, post-hash). Pure + temp-dir settings I/O, no child/spawn.
1583
+ section "ACP provider config (S2g operator mcpServers/skillPlugins/tools passthrough)"
1584
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-config.ts)
1585
+ }
1586
+
1587
+ check_acp_session_store() {
1588
+ # Deterministic gate for the S2d-1b-1 session store / signature / bootstrap
1589
+ # decision. Locks: model-lock fail-loud throw in the pure decision, prefix-
1590
+ # compat (only a prefix history reuses; edited/compaction → new), carrier
1591
+ # drift → signature change → incompatible, and bootstrapPath ⟂ lifecyclePolicy
1592
+ # (turn-scoped/-p one-shot is ALWAYS new — no in-memory reuse, no persisted
1593
+ # resume/load in the first cut). Pure + temp-dir record I/O, no child/spawn.
1594
+ section "ACP session store (S2d-1b-1 signature/compat/bootstrap decision)"
1595
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-store.ts)
1596
+ }
1597
+
1598
+ check_acp_backend_preflight() {
1599
+ # Deterministic gate for the S2c runtime tool-surface preflight. Calls
1600
+ # streamShellAcp with a context whose declared tools exclude a built-in the
1601
+ # Claude child still exposes (read) and asserts the turn fails fast into the
1602
+ # returned stream as an error event BEFORE any spawn — proving
1603
+ # assertExcludeToolsHonored is wired into the live provider path, not just the
1604
+ # pure gate. No backend launched (preflight throws first). Pure.
1605
+ section "ACP backend preflight (S2c runtime exclude-tools wiring)"
1606
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-backend-preflight.ts)
1607
+ }
1608
+
1609
+ check_acp_session_reuse() {
1610
+ # Deterministic gate for S2d-1b-2b in-memory session reuse (backend.ts). Injects
1611
+ # a fake spawn/connection seam and CAPTURES each turn's prompt payload to prove
1612
+ # reuse is DELTA-ONLY: turn 2 carries the new nonce, never the turn-1 history,
1613
+ # with no second spawn/newSession. Also proves the mutable activePromptHandler
1614
+ # routes each turn's notices to its own stream, a persisted record is NOT
1615
+ # resumed in 1b-2b, a concurrent prompt fails loud (busy), the reused child is
1616
+ # never torn down between turns, and source-locks buildAcpPrompt wiring +
1617
+ # single-site applyAcpSessionUpdate via the router. No real child launched.
1618
+ section "ACP session reuse (S2d-1b-2b delta-only capture + mutable routing)"
1619
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-reuse.ts)
1620
+ }
1621
+
1622
+ check_acp_carrier_augment() {
1623
+ # Deterministic gate for S2d-1c billing carrier (engraving) + first-user augment.
1624
+ # Separate axis from the reuse gate (GPT c32a6c8): locks that the carrier is
1625
+ # SHORT/empty-by-default/pure and folds into bridgeConfigSignature (so a carrier
1626
+ # change invalidates reuse but a stable carrier never rebuilds), and that the
1627
+ # rich augment rides the `new` prompt on the WIRE only — never the pi Context,
1628
+ # so it never enters contextMessageSignatures — with entwurf cwd/AGENTS.md
1629
+ # de-dup. Pure + temp-dir fs, no spawn.
1630
+ section "ACP carrier + augment (S2d-1c engraving + first-user augment)"
1631
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-carrier-augment.ts)
1632
+ }
1633
+
1634
+ check_pack() {
1635
+ # Dry-run tarball invariant gate for the public npm surface.
1636
+ #
1637
+ # Runs `npm pack --dry-run --json`, then asserts:
1638
+ # - runtime-critical files and the public verification/docs
1639
+ # surface (run.sh, scripts/, curated docs/assets/*.gif,
1640
+ # demo/) are present;
1641
+ # - private/dev residue is absent (session dumps, debug logs,
1642
+ # dev configs, workspace metadata, the OpenClaw plugin
1643
+ # monorepo sibling that ships as its own npm package).
1644
+ #
1645
+ # Scope: this is the first of four checks in #13's publish gate.
1646
+ # The remaining three — actual `npm pack`, `tar -tf`, and local
1647
+ # install smoke from the packed tarball — are covered by
1648
+ # check_pack_install() below (commit 9e2a2ca, Phase 2.3 closeout).
1649
+ # Intent + policy live in NEXT.md Phase 2.3.
1650
+ section "pack invariants (dry-run)"
1651
+
1652
+ local json
1653
+ json=$(cd "$REPO_DIR" && npm pack --dry-run --json 2>/dev/null) || {
1654
+ fail "[check-pack] npm pack --dry-run failed"
1655
+ return 1
1656
+ }
1657
+
1658
+ local file_list
1659
+ file_list=$(node -e '
1660
+ const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
1661
+ if (!Array.isArray(data) || data.length !== 1) {
1662
+ console.error("[check-pack] expected single tarball entry, got " +
1663
+ (Array.isArray(data) ? data.length : "non-array"));
1664
+ process.exit(2);
1665
+ }
1666
+ for (const f of data[0].files) console.log(f.path);
1667
+ ' <<<"$json") || {
1668
+ fail "[check-pack] failed to parse npm pack output"
1669
+ return 1
1670
+ }
1671
+
1672
+ # .sh mode regression gate. The repo tracks 100755 in git, but if a
1673
+ # contributor's umask or a stray `git update-index --chmod=-x` drops
1674
+ # the bit the tarball will ship 0644 — and pi install hands the
1675
+ # tarball straight to `npm install`, so the bit needs to survive the
1676
+ # whole publish pipeline. Catch it here at dry-run time.
1677
+ local sh_mode_violations
1678
+ sh_mode_violations=$(node -e '
1679
+ const data = JSON.parse(require("fs").readFileSync(0, "utf8"));
1680
+ const bad = data[0].files
1681
+ .filter(f => f.path.endsWith(".sh"))
1682
+ .filter(f => (f.mode & 0o111) === 0);
1683
+ for (const f of bad) console.log(f.path + " mode=0" + (f.mode || 0).toString(8));
1684
+ ' <<<"$json") || {
1685
+ fail "[check-pack] failed to inspect tarball modes"
1686
+ return 1
1687
+ }
1688
+ if [ -n "$sh_mode_violations" ]; then
1689
+ fail "[check-pack] .sh files missing executable bit in tarball:"
1690
+ echo "$sh_mode_violations" | sed 's/^/ /' >&2
1691
+ return 1
1692
+ fi
1693
+
1694
+ local required=(
1695
+ "package.json" "README.md" "LICENSE" "CHANGELOG.md"
1696
+ "protocol.js" "run.sh"
1697
+ "pi-extensions/acp-provider.ts"
1698
+ "pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/backend.ts"
1699
+ "pi-extensions/lib/acp/overlay.ts" "pi-extensions/lib/acp/tool-surface.ts"
1700
+ "pi-extensions/lib/acp/event-mapper.ts" "pi-extensions/lib/acp/context.ts"
1701
+ "pi-extensions/entwurf-control.ts"
1702
+ "pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
1703
+ "mcp/entwurf-bridge/src/index.ts"
1704
+ "scripts/postinstall-chmod.cjs"
1705
+ "pi/entwurf-capabilities.json"
1706
+ "pi/entwurf-targets.json"
1707
+ "pi/meta-bridge/.claude-plugin/marketplace.json"
1708
+ "pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json"
1709
+ "pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json"
1710
+ "pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh"
1711
+ "pi-extensions/meta-bridge-hook.ts"
1712
+ "pi-extensions/lib/meta-session.ts"
1713
+ "pi-extensions/lib/session-id.js"
1714
+ "scripts/meta-bridge-install.sh"
1715
+ "scripts/meta-bridge-state.py"
1716
+ )
1717
+
1718
+ # Patterns that must NOT appear in the tarball. Anchored where the
1719
+ # match should be exact (e.g. ^bench\.sh$); loose where the residue
1720
+ # may appear under any path (e.g. \.log$).
1721
+ local forbidden_patterns=(
1722
+ 'pi-session-.*\.html$'
1723
+ '\.log$'
1724
+ '\.cast$'
1725
+ '^bench\.sh$'
1726
+ '^biome\.json$'
1727
+ '^tsconfig\.json$'
1728
+ '^pnpm-(lock\.yaml|workspace\.yaml)$'
1729
+ '^NEXT\.md$'
1730
+ '^plugins/'
1731
+ '^node_modules/'
1732
+ '\.tmp-verify/'
1733
+ '\.agent-(reports|shell)/'
1734
+ 'pi/meta-bridge/\.assembled/'
1735
+ )
1736
+
1737
+ local pass=1 f pat hit
1738
+
1739
+ for f in "${required[@]}"; do
1740
+ if ! grep -qxF "$f" <<<"$file_list"; then
1741
+ fail "[check-pack] MISSING required: $f"
1742
+ pass=0
1743
+ fi
1744
+ done
1745
+
1746
+ for pat in "${forbidden_patterns[@]}"; do
1747
+ hit=$(grep -E "$pat" <<<"$file_list" || true)
1748
+ if [ -n "$hit" ]; then
1749
+ fail "[check-pack] FORBIDDEN matches pattern $pat:"
1750
+ echo "$hit" | sed 's/^/ /' >&2
1751
+ pass=0
1752
+ fi
1753
+ done
1754
+
1755
+ local total
1756
+ total=$(printf '%s\n' "$file_list" | wc -l | tr -d ' ')
1757
+ echo "[check-pack] $total files in tarball"
1758
+
1759
+ if [ "$pass" = "1" ]; then
1760
+ ok "[check-pack] invariants pass"
1761
+ return 0
1762
+ fi
1763
+ fail "[check-pack] invariants violated"
1764
+ return 1
1765
+ }
1766
+
1767
+ check_pack_install() {
1768
+ # Heavy publish gate. Runs the remaining three checks in #13's
1769
+ # publish checklist that check_pack (dry-run only) does not cover:
1770
+ #
1771
+ # 2. actual `npm pack` — produces the real tarball
1772
+ # 3. `tar -tf` — cross-checks contents against dry-run invariants
1773
+ # 4. fresh-temp project local install smoke — pnpm add the tarball
1774
+ # with required peers, then import('@junghanacs/entwurf/package.json')
1775
+ # to confirm the installed shape resolves end-to-end.
1776
+ #
1777
+ # Excluded from the default `pnpm check` because the install smoke
1778
+ # spends 5-15s on dependency resolution. Wired into prepublishOnly
1779
+ # so `npm publish` cannot succeed if the actual install path is
1780
+ # broken even when dry-run invariants look fine.
1781
+ #
1782
+ # Force --dry-run=false because `npm publish --dry-run` exports
1783
+ # npm_config_dry_run=true into lifecycle scripts. Without the explicit
1784
+ # override, this nested actual-pack smoke prints the tarball name but
1785
+ # does not write the .tgz file, causing prepublishOnly to fail before
1786
+ # a real publish can be exercised.
1787
+ section "publish install smoke (actual pack + tar + fresh install)"
1788
+
1789
+ local version tgz_name tgz_path
1790
+ version=$(node -p "require('${REPO_DIR}/package.json').version")
1791
+ # Scoped npm packages produce a tarball named "<scope>-<name>-<version>.tgz"
1792
+ # where the `@` is stripped and `/` becomes `-`. For `@junghanacs/entwurf`
1793
+ # that lands as `junghanacs-entwurf-<version>.tgz`. Hardcoded against
1794
+ # the scope above so a name change cannot silently slide past this gate.
1795
+ tgz_name="junghanacs-entwurf-${version}.tgz"
1796
+ tgz_path="${REPO_DIR}/${tgz_name}"
1797
+ rm -f "$tgz_path"
1798
+
1799
+ echo "[check-pack-install] npm pack -> ${tgz_name}"
1800
+ (cd "$REPO_DIR" && npm pack --dry-run=false 2>&1 | tail -1) || {
1801
+ fail "[check-pack-install] npm pack failed"
1802
+ return 1
1803
+ }
1804
+
1805
+ if [ ! -f "$tgz_path" ]; then
1806
+ fail "[check-pack-install] tarball not produced: $tgz_path"
1807
+ return 1
1808
+ fi
1809
+
1810
+ # tar -tf invariants — cross-check against dry-run shape. Same
1811
+ # required/forbidden axes as check_pack; if they disagree, the
1812
+ # dry-run resolver and the actual tarball diverged (npm bug or
1813
+ # files allowlist drift) and publish must not proceed.
1814
+ local tar_files pass=1 f pat
1815
+ tar_files=$(tar -tf "$tgz_path" | sed 's|^package/||' | grep -v '/$' || true)
1816
+
1817
+ # Required tarball contents. The old 0.11.0 ACP root files (index.ts,
1818
+ # acp-bridge.ts, event-mapper.ts, engraving.ts, pi-context-augment.ts,
1819
+ # pi-extensions/entwurf.ts) were removed on v2-only and are GONE — keeping them
1820
+ # here made this heavy gate silently RED (publish-blocking) while `pnpm check`
1821
+ # (which runs only check-pack, not check-pack-install) stayed green. The ACP
1822
+ # plugin re-enters on v2 as the provider entry + lib/acp/* modules below.
1823
+ local tar_required=(
1824
+ "package.json" "README.md" "LICENSE" "CHANGELOG.md"
1825
+ "protocol.js" "run.sh"
1826
+ "pi-extensions/acp-provider.ts"
1827
+ "pi-extensions/lib/acp/models.ts" "pi-extensions/lib/acp/backend.ts"
1828
+ "pi-extensions/lib/acp/overlay.ts" "pi-extensions/lib/acp/tool-surface.ts"
1829
+ "pi-extensions/lib/acp/event-mapper.ts" "pi-extensions/lib/acp/context.ts"
1830
+ "pi-extensions/entwurf-control.ts"
1831
+ "pi-extensions/model-lock.ts" "pi-extensions/lib/entwurf-core.ts"
1832
+ "mcp/entwurf-bridge/src/index.ts"
1833
+ "scripts/postinstall-chmod.cjs"
1834
+ "pi/entwurf-capabilities.json"
1835
+ "pi/entwurf-targets.json"
1836
+ "pi/meta-bridge/.claude-plugin/marketplace.json"
1837
+ "pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json"
1838
+ "pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json"
1839
+ "pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh"
1840
+ "pi-extensions/meta-bridge-hook.ts"
1841
+ "pi-extensions/lib/meta-session.ts"
1842
+ "pi-extensions/lib/session-id.js"
1843
+ "scripts/meta-bridge-install.sh"
1844
+ "scripts/meta-bridge-state.py"
1845
+ )
1846
+ for f in "${tar_required[@]}"; do
1847
+ if ! grep -qxF "$f" <<<"$tar_files"; then
1848
+ fail "[check-pack-install] tar missing required: $f"
1849
+ pass=0
1850
+ fi
1851
+ done
1852
+
1853
+ local tar_forbidden=(
1854
+ 'pi-session-.*\.html$' '\.log$' '\.cast$'
1855
+ '^bench\.sh$' '^biome\.json$' '^tsconfig\.json$'
1856
+ '^pnpm-(lock\.yaml|workspace\.yaml)$' '^NEXT\.md$'
1857
+ '^plugins/' '^node_modules/'
1858
+ '\.tmp-verify/' '\.agent-(reports|shell)/'
1859
+ 'pi/meta-bridge/\.assembled/'
1860
+ )
1861
+ for pat in "${tar_forbidden[@]}"; do
1862
+ local hit
1863
+ hit=$(grep -E "$pat" <<<"$tar_files" || true)
1864
+ if [ -n "$hit" ]; then
1865
+ fail "[check-pack-install] tar contains forbidden pattern $pat:"
1866
+ echo "$hit" | sed 's/^/ /' >&2
1867
+ pass=0
1868
+ fi
1869
+ done
1870
+
1871
+ if [ "$pass" != "1" ]; then
1872
+ rm -f "$tgz_path"
1873
+ fail "[check-pack-install] tar -tf invariants violated"
1874
+ return 1
1875
+ fi
1876
+ echo "[check-pack-install] tar -tf invariants pass ($(printf '%s\n' "$tar_files" | wc -l | tr -d ' ') files)"
1877
+
1878
+ # Fresh-temp install smoke. Uses pnpm because that is what this
1879
+ # repo packages with; --ignore-workspace stops it from re-attaching
1880
+ # to our pnpm-workspace.yaml; --ignore-scripts blocks the husky
1881
+ # prepare hook (and any future install scripts) from running inside
1882
+ # the consumer project. Peer deps are pinned to the 0.80.x release
1883
+ # baseline so the smoke matches the same shape an external pi user
1884
+ # would have after `pi install`.
1885
+ local tmp npm_tmp
1886
+ tmp=$(mktemp -d -t entwurf-install-smoke.XXXXXX)
1887
+ # Separate tree for the npm-managed regression below: npm install must NOT be
1888
+ # nested under $tmp, whose pnpm-add node_modules/package.json would make npm
1889
+ # climb the parent and choke ("Cannot read properties of null").
1890
+ npm_tmp=$(mktemp -d -t entwurf-npm-managed.XXXXXX)
1891
+ trap 'rm -rf "$tmp" "$npm_tmp" "$tgz_path"' RETURN
1892
+
1893
+ printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
1894
+
1895
+ echo "[check-pack-install] pnpm add into $tmp (with 0.80.x peers + typebox)"
1896
+ local install_log
1897
+ install_log=$(cd "$tmp" && pnpm add \
1898
+ "$tgz_path" \
1899
+ "@earendil-works/pi-ai@0.80.2" \
1900
+ "@earendil-works/pi-coding-agent@0.80.2" \
1901
+ "@earendil-works/pi-tui@0.80.2" \
1902
+ "typebox@latest" \
1903
+ --ignore-workspace --ignore-scripts 2>&1) || {
1904
+ fail "[check-pack-install] pnpm add failed:"
1905
+ echo "$install_log" | tail -10 | sed 's/^/ /' >&2
1906
+ return 1
1907
+ }
1908
+
1909
+ # Resolve the installed package.json and confirm pi.extensions
1910
+ # arrived intact. If pi.extensions is empty or missing, the
1911
+ # consumer pi runtime would fail to register any extension.
1912
+ local probe
1913
+ probe=$(cd "$tmp" && node --input-type=module -e "
1914
+ const m = await import('@junghanacs/entwurf/package.json', { with: { type: 'json' } });
1915
+ const pkg = m.default;
1916
+ const exts = Array.isArray(pkg.pi?.extensions) ? pkg.pi.extensions.length : 0;
1917
+ if (exts === 0) { console.error('pi.extensions missing or empty'); process.exit(1); }
1918
+ console.log(pkg.version + ' (' + exts + ' extensions)');
1919
+ " 2>&1) || {
1920
+ fail "[check-pack-install] installed package probe failed:"
1921
+ echo "$probe" | sed 's/^/ /' >&2
1922
+ return 1
1923
+ }
1924
+ echo "[check-pack-install] installed: $probe"
1925
+
1926
+ # Pi package loader smoke — actual `pi` reads the manifest and
1927
+ # registers the provider. rc=0 + the curated model list in the
1928
+ # output means pi accepted the package as a real extension, not
1929
+ # just a well-shaped npm tarball. `--list-models` does not spawn
1930
+ # the Claude/Codex/Gemini backends, so this stays credential-free
1931
+ # and safe to run in CI. Output goes to stderr; capture both
1932
+ # streams with 2>&1.
1933
+ if ! command -v pi >/dev/null 2>&1; then
1934
+ fail "[check-pack-install] pi binary not on PATH — cannot run loader smoke"
1935
+ return 1
1936
+ fi
1937
+
1938
+ local loader_out
1939
+ loader_out=$(cd "$tmp" && pi -e "$tmp/node_modules/@junghanacs/entwurf" --list-models entwurf 2>&1) || {
1940
+ fail "[check-pack-install] pi loader smoke failed (exit non-zero):"
1941
+ echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
1942
+ return 1
1943
+ }
1944
+ if ! grep -q "entwurf" <<<"$loader_out"; then
1945
+ fail "[check-pack-install] pi loader output missing entwurf model surface:"
1946
+ echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
1947
+ return 1
1948
+ fi
1949
+ # Verify the full curated Claude surface is visible: the Sonnet row AND the
1950
+ # opus anchor (CURATED_ANCHOR_MODEL_ID in lib/acp/models.ts — the model whose
1951
+ # absence is a hard registry regression). Checking only one would let half the
1952
+ # surface drop silently.
1953
+ for anchor in "claude-sonnet-4-6" "claude-opus-4-8"; do
1954
+ if ! grep -q "$anchor" <<<"$loader_out"; then
1955
+ fail "[check-pack-install] pi loader output missing curated Claude model $anchor:"
1956
+ echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
1957
+ return 1
1958
+ fi
1959
+ done
1960
+ echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-4-6 + claude-opus-4-8 anchor)"
1961
+
1962
+ # npm-managed `run.sh install` regression — the README's PRIMARY install path,
1963
+ # `pi install npm:@junghanacs/entwurf` then `run.sh install .`. This layout
1964
+ # differs from the pnpm-add smoke above in the exact way that broke the install:
1965
+ # pi installs via npm with --legacy-peer-deps, so the package lands under a
1966
+ # managed prefix (~/.pi/agent/npm/node_modules/@junghanacs/entwurf), runtime deps
1967
+ # HOIST to the sibling node_modules, there is NO package-local node_modules, and
1968
+ # the pi peer trio is absent (loader-provided). The old cwd-relative
1969
+ # preflight_dep_integrity rejected every dep in this layout, so the documented
1970
+ # primary path failed on first use while every dry-run/pnpm-add gate stayed green.
1971
+ # Prove `run.sh install` actually writes settings from the hoisted layout. HOME is
1972
+ # redirected to a throwaway dir so ensure_agent_dir_symlinks operates on a temp
1973
+ # ~/.pi/agent and never touches (or fails against) the operator's real targets link.
1974
+ if ! command -v npm >/dev/null 2>&1; then
1975
+ fail "[check-pack-install] npm not on PATH — cannot run npm-managed install regression"
1976
+ return 1
1977
+ fi
1978
+ local npmroot="$npm_tmp/npmroot" npmhome="$npm_tmp/npmhome" npmproj="$npm_tmp/npmproj" npm_log npm_pkg wire_log
1979
+ mkdir -p "$npmroot" "$npmhome" "$npmproj"
1980
+ npm_log=$(cd "$npmroot" && npm install "$tgz_path" --legacy-peer-deps --no-audit --no-fund 2>&1) || {
1981
+ fail "[check-pack-install] npm-managed install failed:"
1982
+ echo "$npm_log" | tail -10 | sed 's/^/ /' >&2
1983
+ return 1
1984
+ }
1985
+ npm_pkg="$npmroot/node_modules/@junghanacs/entwurf"
1986
+ if [ ! -x "$npm_pkg/run.sh" ]; then
1987
+ fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
1988
+ return 1
1989
+ fi
1990
+ wire_log=$(HOME="$npmhome" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
1991
+ fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
1992
+ echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
1993
+ return 1
1994
+ }
1995
+ if [ ! -f "$npmproj/.pi/settings.json" ]; then
1996
+ fail "[check-pack-install] npm-managed run.sh install did not write settings.json:"
1997
+ echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
1998
+ return 1
1999
+ fi
2000
+ if ! grep -q "node_modules/@junghanacs/entwurf" <<<"$wire_log"; then
2001
+ fail "[check-pack-install] npm-managed install did not report the npm package source:"
2002
+ echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
2003
+ return 1
2004
+ fi
2005
+ echo "[check-pack-install] npm-managed install regression pass (hoisted-dep run.sh install wrote settings)"
2006
+
2007
+ ok "[check-pack-install] publish install smoke pass"
2008
+ return 0
2009
+ }
2010
+
2011
+
2012
+ # --- v2 install/runtime verification gates ---
2013
+ #
2014
+ # These validators complement the local deterministic check_* gates by exercising
2015
+ # the runtime surfaces an installed v2 package depends on. On v2-only, setup must
2016
+ # NOT run the legacy ACP/v1 Axis 1 interview gates (session-messaging/sentinel):
2017
+ # those call removed surfaces and survive only as fail-loud reference subcommands
2018
+ # until rewritten onto entwurf_v2. The release-gate owns the heavier live v2
2019
+ # substrate proof (matrix-live + spawn-resume-live).
2020
+
2021
+ validate_entwurf_bridge() {
2022
+ local bridge_dir="$REPO_DIR/mcp/entwurf-bridge"
2023
+ local raw
2024
+
2025
+ if [ ! -x "$bridge_dir/start.sh" ]; then
2026
+ fail "entwurf-bridge: launcher missing at $bridge_dir/start.sh"
2027
+ return 1
2028
+ fi
2029
+
2030
+ log "entwurf-bridge: direct MCP smoke (strip-types launcher, no build step)"
2031
+
2032
+ if ! raw=$(cd "$bridge_dir" && node --input-type=module <<'JS'
2033
+ import { spawn } from 'node:child_process';
2034
+
2035
+ const child = spawn('./start.sh');
2036
+ let stdout = '';
2037
+ let stderr = '';
2038
+ let done = false;
2039
+
2040
+ function finishOk(trimmed) {
2041
+ if (done) return;
2042
+ done = true;
2043
+ clearTimeout(timer);
2044
+ if (stderr.trim()) console.error(stderr.trim());
2045
+ const msg = JSON.parse(trimmed);
2046
+ const tools = msg?.result?.tools;
2047
+ if (!Array.isArray(tools)) {
2048
+ console.error('tools/list response missing result.tools');
2049
+ process.exit(1);
2050
+ }
2051
+ const names = tools.map((t) => t?.name).sort();
2052
+ const expected = ['entwurf_peers', 'entwurf_self', 'entwurf_inbox_read', 'entwurf_v2'];
2053
+ for (const name of expected) {
2054
+ if (!names.includes(name)) {
2055
+ console.error(`missing MCP tool: ${name}`);
2056
+ process.exit(1);
2057
+ }
2058
+ }
2059
+ console.log(names.join(','));
2060
+ child.kill('SIGTERM');
2061
+ process.exit(0);
2062
+ }
2063
+
2064
+ child.stdout.on('data', (d) => {
2065
+ stdout += d.toString();
2066
+ const trimmed = stdout.trim();
2067
+ if (trimmed) finishOk(trimmed);
2068
+ });
2069
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
2070
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) + '\n');
2071
+
2072
+ const timer = setTimeout(() => {
2073
+ child.kill('SIGKILL');
2074
+ console.error('entwurf-bridge direct smoke timeout');
2075
+ process.exit(1);
2076
+ }, 3000);
2077
+
2078
+ child.on('error', (err) => {
2079
+ if (done) return;
2080
+ clearTimeout(timer);
2081
+ console.error(String(err));
2082
+ process.exit(1);
2083
+ });
2084
+
2085
+ child.on('close', () => {
2086
+ if (done) return;
2087
+ clearTimeout(timer);
2088
+ const trimmed = stdout.trim();
2089
+ if (!trimmed) {
2090
+ if (stderr.trim()) console.error(stderr.trim());
2091
+ console.error('empty tools/list response');
2092
+ process.exit(1);
2093
+ }
2094
+ finishOk(trimmed);
2095
+ });
2096
+ JS
2097
+ ); then
2098
+ fail "entwurf-bridge: direct MCP smoke failed"
2099
+ return 1
2100
+ fi
2101
+ ok "entwurf-bridge direct MCP smoke ($raw)"
2102
+
2103
+ if ! (cd "$bridge_dir" && ./test.sh >/dev/null); then
2104
+ fail "entwurf-bridge: protocol/negative-path tests failed"
2105
+ return 1
2106
+ fi
2107
+ ok "entwurf-bridge test.sh"
2108
+
2109
+ # check-bridge deliberately stops at the objective MCP boundary. Live v2
2110
+ # substrate/orchestration is covered by the v2 live smokes, whose assertions
2111
+ # parse operational artifacts instead of asking a model to self-report which
2112
+ # tool schema it sees. This split keeps check-bridge credential-free and avoids
2113
+ # model self-recognition variance blocking setup.
2114
+ }
2115
+
2116
+ check_bridge() {
2117
+ section "entwurf-bridge (direct MCP protocol)"
2118
+ validate_entwurf_bridge
2119
+ }
2120
+
2121
+
2122
+ # setup_all — full entwurf v2 install.
2123
+ #
2124
+ # Installs the v2 dispatch substrate + MCP entwurf-bridge into a target project
2125
+ # and verifies the installed bridge boundary. ACP/v1 backend interview gates are
2126
+ # deliberately not part of setup on v2-only; the heavier live v2 substrate proof
2127
+ # is release-gate's job.
2128
+ #
2129
+ # An external harness that consumes entwurf (e.g. agent-config as a
2130
+ # pi package + skills set) may still have its own install/setup for its
2131
+ # own concerns; those are outside the scope of this script.
2132
+ setup_all() {
2133
+ local project_dir
2134
+ project_dir=$(normalize_project_dir "$1")
2135
+
2136
+ require_cmd pnpm
2137
+ require_cmd python3
2138
+ require_cmd pi
2139
+ require_cmd node
2140
+
2141
+ # MCP bridge launchers run via `node --experimental-strip-types` (stable in
2142
+ # Node 23.6, experimental from 22.6). Anything older lacks the flag, and an
2143
+ # ACP session would hit a cryptic "unknown argument" rather than a clear
2144
+ # setup-time error. Fail early with an actionable message. package.json
2145
+ # engines.node mirrors this floor.
2146
+ if ! node -e 'const [M,m]=process.versions.node.split(".").map(Number); process.exit((M>22||(M===22&&m>=6))?0:1)'; then
2147
+ echo "[setup] entwurf requires Node >= 22.6.0 (got $(node -v))" >&2
2148
+ echo "[setup] MCP bridge launchers depend on --experimental-strip-types." >&2
2149
+ exit 1
2150
+ fi
2151
+
2152
+ echo "[setup] repo: $REPO_DIR"
2153
+ echo "[setup] project: $project_dir"
2154
+ echo "[setup] scope: entwurf v2 orchestration install (pi-native; ACP backends dropped)"
2155
+ echo "[setup] verification: v2 install smoke (entwurf-bridge; LIVE substrate = release-gate)"
2156
+
2157
+ (cd "$REPO_DIR" && pnpm install --frozen-lockfile)
2158
+ sync_auth
2159
+ install_local_package "$project_dir"
2160
+
2161
+ # Single confident install command (GLG 2026-06-23): setup ALSO wires the
2162
+ # native-harness meta-bridge, so a relocate/clone needs ONE command — not the
2163
+ # install + install-meta-bridge two-pronged split that froze the Claude
2164
+ # statusLine on the old path (the 2026-06-23 relocation gap). Detection-gated:
2165
+ # a pi-only host with no native harness skips it cleanly (§10 "있으면 설정,
2166
+ # 없으면 담아준다"); meta-bridge-install.sh is itself idempotent.
2167
+ if command -v claude >/dev/null 2>&1; then
2168
+ section "meta-bridge install (native harness detected: Claude Code)"
2169
+ (cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh)
2170
+ else
2171
+ echo "[setup] no native harness (claude) on PATH — skipping meta-bridge wiring (pi-only host)"
2172
+ fi
2173
+
2174
+ # Deterministic preflight lives in `pnpm check`; live substrate acceptance lives
2175
+ # in `LIVE=1 ./run.sh release-gate <scratch>`. Setup is the install path, so it
2176
+ # verifies the installed MCP bridge boundary only and does NOT run the legacy
2177
+ # ACP/v1 session-messaging/sentinel gates.
2178
+ section "v2 install smoke: entwurf-bridge (direct MCP protocol)"
2179
+ validate_entwurf_bridge
2180
+
2181
+ echo ""
2182
+ echo "DONE: entwurf setup (pi package + meta-bridge + v2 install smoke) green."
2183
+ if command -v claude >/dev/null 2>&1; then
2184
+ echo "Verify native-harness wiring with: ./run.sh doctor-meta-bridge"
2185
+ fi
2186
+ echo "Run 'LIVE=1 ./run.sh release-gate <scratch>' for live substrate acceptance."
2187
+ }
2188
+
2189
+ # release-gate — the single command that, when GREEN, is sufficient to cut
2190
+ # release cuts. Runs the full static floor (`pnpm check`) followed by the
2191
+ # v2-native live gates, then emits one PASS/FAIL/SKIP summary. Everything is
2192
+ # invoked through run.sh subcommands — never a script in scripts/ directly.
2193
+ #
2194
+ # Design invariants (NEXT Step 1e + GPT-5.5 reviews):
2195
+ # - v2-native live floor: the MUST tier is the v2 dispatch substrate
2196
+ # (smoke-entwurf-v2-matrix-live + smoke-entwurf-v2-spawn-resume-live, opt-in
2197
+ # LIVE), the MCP bridge (check-bridge), and the garden-native substrate/guard
2198
+ # (smoke-session-id-name on a pi-native target via ENTWURF_LIVE_TARGET,
2199
+ # and smoke-resident-garden-guard).
2200
+ # - ACP plugin acceptance floor (S0~S2g): the 11 ACP LIVE smokes
2201
+ # (socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/
2202
+ # memory-containment/rgg + S2g mcp/skill config passthrough + S2g axis-3 bundled-mcp resident/RPC)
2203
+ # are MUST, not BEHAVIOR — they prove programmatic transport/provider/backend
2204
+ # invariants of the ACP plugin on the v2 core, so a failure is a release
2205
+ # defect, not an advisory model-in-loop signal. Each is LIVE-gated honest-SKIP.
2206
+ # - v1 entwurf verbs are gone (v2 core): the old xt-tool-surface / session-messaging
2207
+ # / sentinel floor gates do not exist on this tree. --allow-skip-gemini is
2208
+ # accepted-but-ignored (back-compat).
2209
+ # - Final release authorization is GLG's, not this script's: a green
2210
+ # run is necessary, and the operator closes the decision.
2211
+ release_gate() {
2212
+ local -a positional=()
2213
+ local a
2214
+ for a in "$@"; do
2215
+ case "$a" in
2216
+ --allow-skip-gemini) ;; # accepted-but-ignored: gemini removed from the claude-only floor (back-compat for existing scripts)
2217
+ *) positional+=("$a") ;;
2218
+ esac
2219
+ done
2220
+ local project_dir
2221
+ project_dir=$(normalize_project_dir "${positional[0]:-$PROJECT_DIR_DEFAULT}")
2222
+
2223
+ # Absolute path to this script — survives the `cd "$project_dir"` below. The
2224
+ # live gates derive their pi session dir from $PWD (tmux `-c "$PWD"`,
2225
+ # PROJECT_DIR_DEFAULT, and the bare `pi -p` invocations that don't `cd`
2226
+ # themselves). Some gates (e.g. check-bridge and the garden guard) take no
2227
+ # project arg, so if release-gate runs from the repo their sessions could land
2228
+ # in the repo's own session dir — polluting the very
2229
+ # tree we ship and breaking the "scratch full gate" evidence claim. Running
2230
+ # EVERY live gate with PWD=project_dir makes a single
2231
+ # `./run.sh release-gate <scratch>` invocation route all sessions to scratch
2232
+ # regardless of the operator's cwd. `-e "$REPO_DIR/..."` (extension load) and
2233
+ # every other path the gates touch are absolute, so the cd is safe.
2234
+ #
2235
+ # The two garden-native identity gates (smoke-session-id-name,
2236
+ # smoke-resident-garden-guard) also take no project arg but are exempt from
2237
+ # the repo-pollution concern by construction: the substrate smoke runs every
2238
+ # pi turn under its own os.tmpdir() agent dir + cwds (mkdtemp, cleaned up),
2239
+ # and the guard is wired here as the NEGATIVE path only — a 0-token fail-fast
2240
+ # that writes no session file at all.
2241
+ #
2242
+ # smoke-acp-bundled-mcp-live is a DELIBERATE exception to the PWD=project_dir
2243
+ # routing: it runs its resident with cwd=os.tmpdir() and relies on the
2244
+ # operator's INSTALLED bundled bridge (global ~/.pi/agent/settings.json
2245
+ # entwurfProvider.mcpServers.entwurf-bridge) — that IS the operator circuit
2246
+ # this axis restores, not a scratch-isolated probe (that is smoke-acp-mcp-live's
2247
+ # job). It writes only a tmpdir-cwd session (no repo pollution) and fails loud if
2248
+ # the operator has not wired the bundled bridge.
2249
+ local self="$REPO_DIR/run.sh"
2250
+ gate() { ( cd "$project_dir" && "$@" ); }
2251
+
2252
+ local pass=0 failc=0 skip=0
2253
+ local -a results=()
2254
+
2255
+ run_step() {
2256
+ local name="$1"; shift
2257
+ section "release-gate step: $name"
2258
+ if "$@"; then
2259
+ ok "$name: PASS"
2260
+ results+=("PASS $name"); pass=$((pass + 1))
2261
+ else
2262
+ fail "$name: FAIL"
2263
+ results+=("FAIL $name"); failc=$((failc + 1))
2264
+ fi
2265
+ }
2266
+
2267
+ # LIVE-gated MUST step: a release-blocking gate that needs a real backend turn
2268
+ # (auth/model/credit). When LIVE!=1 it is an HONEST SKIP — counted as SKIP, NOT
2269
+ # PASS — so an unattended `./run.sh release-gate` stays runnable without faking
2270
+ # coverage. A release CUT therefore requires `LIVE=1 ./run.sh release-gate
2271
+ # <scratch>` to land MUST PASS with SKIP=0; a green run that still shows SKIP is
2272
+ # CI safety, never live acceptance. (Same rule the v2 substrate sentinels below
2273
+ # spell out inline; this helper applies it to the ACP plugin acceptance floor.)
2274
+ run_live_step() {
2275
+ local name="$1"; shift
2276
+ section "release-gate step: $name"
2277
+ if [ "${LIVE:-}" = "1" ]; then
2278
+ if "$@"; then
2279
+ ok "$name: PASS"
2280
+ results+=("PASS $name"); pass=$((pass + 1))
2281
+ else
2282
+ fail "$name: FAIL"
2283
+ results+=("FAIL $name"); failc=$((failc + 1))
2284
+ fi
2285
+ else
2286
+ warn "$name: LIVE!=1 — skipped (opt-in: needs auth/model — NOT a live acceptance run)"
2287
+ results+=("SKIP $name (LIVE!=1)"); skip=$((skip + 1))
2288
+ fi
2289
+ }
2290
+
2291
+ # BEHAVIOR lane (0.11.0, GLG+GPT+Opus): a SEPARATE advisory counter for
2292
+ # model-in-loop gates that probe whether the *model* autonomously selects the
2293
+ # MCP entwurf surface (vs. bypassing via Bash/Terminal/pi-CLI). These gates are
2294
+ # flaky by the model's nature (Claude Sonnet's MCP-vs-Bash choice is
2295
+ # non-deterministic on 0.79.4), so a single flake must NOT block the cut. They
2296
+ # are NEVER folded into `failc`/`pass` — exit authority below is `failc` only.
2297
+ # Honesty rails: (1) "non-blocking" is NOT "pass" — a BEHAVIOR-FAIL is surfaced
2298
+ # loudly in the summary with its artifact path, never buried; (2) a Bash-bypass
2299
+ # of the entwurf surface stays a hard FAIL *inside* this lane — a bypass is
2300
+ # never relabelled a pass; (3) the entwurf_v2 surface itself is proven by the
2301
+ # deterministic/programmatic must-pass gates above (check-entwurf-v2-*,
2302
+ # check-bridge) — this lane is autonomous-tool-selection *behavior*, not
2303
+ # *function*. Residual bypass → 0.11.x usability lane.
2304
+ local behavior_pass=0 behavior_failc=0
2305
+ local -a behavior_results=()
2306
+
2307
+ run_behavior_step() {
2308
+ local name="$1"; shift
2309
+ section "release-gate BEHAVIOR step (advisory, non-blocking): $name"
2310
+ if "$@"; then
2311
+ ok "$name: BEHAVIOR-PASS"
2312
+ behavior_results+=("BEHAVIOR-PASS $name"); behavior_pass=$((behavior_pass + 1))
2313
+ else
2314
+ warn "$name: BEHAVIOR-FAIL (advisory — model-in-loop signal; S7=Bash-bypass stays hard-fail here; NOT a cut blocker)"
2315
+ behavior_results+=("BEHAVIOR-FAIL $name"); behavior_failc=$((behavior_failc + 1))
2316
+ fi
2317
+ }
2318
+
2319
+ # 1. Static floor (deterministic; includes the two folded gates).
2320
+ section "release-gate step: static (pnpm check)"
2321
+ if (cd "$REPO_DIR" && pnpm check); then
2322
+ ok "static (pnpm check): PASS"
2323
+ results+=("PASS static (pnpm check)"); pass=$((pass + 1))
2324
+ else
2325
+ fail "static (pnpm check): FAIL"
2326
+ results+=("FAIL static (pnpm check)"); failc=$((failc + 1))
2327
+ fi
2328
+
2329
+ # 2. (gemini-availability step removed — claude-only floor; gemini CLI is
2330
+ # deprecated, so the gate no longer asserts a three-backend claim.)
2331
+
2332
+ # 3. Live per-invariant gates (each is a run.sh subcommand). Every one runs
2333
+ # with PWD=project_dir (via gate()) so cwd-derived pi session dirs land in
2334
+ # the scratch project, never the repo — see the note above.
2335
+ #
2336
+ # Foundational garden-native identity gates run first (0.9.0, #28): the
2337
+ # substrate proof (Pi --session-id/--name through the bridge) and the
2338
+ # resident --entwurf-control guard (non-garden id → 0-token fail-fast,
2339
+ # PLUS the positive path: a garden id resident actually boots, gets a
2340
+ # control-tagged name, and is never entwurf-resumable). If the identity
2341
+ # foundation is broken, every Entwurf live gate below is meaningless, so
2342
+ # fail fast here. Negative path is 0-token; positive path is ~1 cheap turn;
2343
+ # the substrate smoke is a few cheap turns.
2344
+ run_step "smoke-session-id-name (3a substrate)" gate bash "$self" smoke-session-id-name
2345
+ # RGG split (0.11.0): the deterministic half (negative/id-safety + /gnew
2346
+ # zero-token live path) is release-blocking and stays here as a must-pass with
2347
+ # SMOKE_RGG_POSITIVE=0. The model-in-loop half (post-/gnew backend entwurf_self
2348
+ # identity turn [T3] + positive garden --session-id model turn) is gated behind
2349
+ # SMOKE_RGG_POSITIVE=1 and runs in the BEHAVIOR lane below — advisory, because
2350
+ # it depends on the backend child autonomously calling entwurf_self.
2351
+ run_step "smoke-resident-garden-guard (3c guard: negative/id-safety + /gnew 0-token, deterministic)" gate env SMOKE_RGG_POSITIVE=0 bash "$self" smoke-resident-garden-guard
2352
+ run_step "check-bridge" gate bash "$self" check-bridge
2353
+ # D4-c: the v2 dispatch substrate sentinel (5d-5). A SINGLE run (NOT backend-looped — it proves
2354
+ # production runEntwurfV2 deps + real pi control-socket RPC + real mailbox enqueue + v2 lock, not
2355
+ # per-backend model behavior). Placed right after check-bridge: the MCP/protocol substrate must be
2356
+ # green first so a matrix-live failure reads as "v2 transport/lock/enqueue", not bridge basics.
2357
+ # Opt-in LIVE: it spawns a real `pi --entwurf-control` (needs auth/model), so LIVE!=1 is an HONEST
2358
+ # SKIP (not a PASS) — an unattended release-gate stays runnable without faking coverage. Independent
2359
+ # of --allow-skip-gemini (now a no-op back-compat flag on the claude-only floor; this gates substrate auth).
2360
+ section "release-gate step: smoke-entwurf-v2-matrix-live"
2361
+ if [ "${LIVE:-}" = "1" ]; then
2362
+ if gate env LIVE=1 bash "$self" smoke-entwurf-v2-matrix-live; then
2363
+ ok "smoke-entwurf-v2-matrix-live: PASS"
2364
+ results+=("PASS smoke-entwurf-v2-matrix-live"); pass=$((pass + 1))
2365
+ else
2366
+ fail "smoke-entwurf-v2-matrix-live: FAIL"
2367
+ results+=("FAIL smoke-entwurf-v2-matrix-live"); failc=$((failc + 1))
2368
+ fi
2369
+ else
2370
+ warn "smoke-entwurf-v2-matrix-live: LIVE!=1 — skipped (v2 substrate sentinel, opt-in: needs auth/model)"
2371
+ results+=("SKIP smoke-entwurf-v2-matrix-live (LIVE!=1)"); skip=$((skip + 1))
2372
+ fi
2373
+ # 0.11.0 (A) acceptance: the FULL spawn-bg resident lifecycle — a real `pi` resume child stands
2374
+ # its control socket up, does a model turn, lock released ×1. Placed right after matrix-live (the
2375
+ # transport sentinel): a spawn-resume failure then reads as "resume/resident lifecycle", not
2376
+ # transport basics. Same opt-in LIVE rule — LIVE!=1 is an HONEST SKIP (model-in-loop, needs
2377
+ # auth/model). NOTE: a 0.11.0 tag REQUIRES `LIVE=1 ./run.sh release-gate` (or this step direct)
2378
+ # to PASS — the SKIP is CI safety, never acceptance.
2379
+ section "release-gate step: smoke-entwurf-v2-spawn-resume-live"
2380
+ if [ "${LIVE:-}" = "1" ]; then
2381
+ if gate env LIVE=1 bash "$self" smoke-entwurf-v2-spawn-resume-live; then
2382
+ ok "smoke-entwurf-v2-spawn-resume-live: PASS"
2383
+ results+=("PASS smoke-entwurf-v2-spawn-resume-live"); pass=$((pass + 1))
2384
+ else
2385
+ fail "smoke-entwurf-v2-spawn-resume-live: FAIL"
2386
+ results+=("FAIL smoke-entwurf-v2-spawn-resume-live"); failc=$((failc + 1))
2387
+ fi
2388
+ else
2389
+ warn "smoke-entwurf-v2-spawn-resume-live: LIVE!=1 — skipped (0.11.0 A acceptance, opt-in: needs auth/model)"
2390
+ results+=("SKIP smoke-entwurf-v2-spawn-resume-live (LIVE!=1)"); skip=$((skip + 1))
2391
+ fi
2392
+
2393
+ # 3b. ACP plugin acceptance floor (S0~S2f live). These prove the ACP plugin's
2394
+ # programmatic transport/provider/backend invariants on the v2 core — NOT
2395
+ # model-in-loop autonomous tool-selection — so they belong in the MUST tier,
2396
+ # not BEHAVIOR. Their deterministic counterparts (check-acp-*, check-auth-
2397
+ # boundary, check-acp-overlay/tool-surface/event-mapper/prompt-builder/
2398
+ # session-store/session-reuse/carrier-augment) already run inside `pnpm
2399
+ # check` above; these are the LIVE acceptance halves. Ordered from the
2400
+ # cheapest, most foundational invariant outward: turn-free citizenship →
2401
+ # pinned ACP pipe/auth → overlay/tool meta → real pi provider path (+
2402
+ # progress visibility / L3 marker) → process-scoped reuse + semantic recall
2403
+ # → first-user augment delivery + empty-carrier billing-clean (핀1) →
2404
+ # ACP-target garden guard (deterministic half). Opt-in LIVE: LIVE!=1 is an
2405
+ # HONEST SKIP via run_live_step (see its note) — a CUT needs LIVE=1, SKIP=0.
2406
+ run_live_step "smoke-acp-socket-citizen-live (S1: turn-free socket citizenship)" gate env LIVE=1 bash "$self" smoke-acp-socket-citizen-live
2407
+ run_live_step "smoke-acp-raw-turn-live (S2a: pinned ACP pipe + local auth)" gate env LIVE=1 bash "$self" smoke-acp-raw-turn-live
2408
+ run_live_step "smoke-acp-overlay-live (S2b: config overlay + hooks:{} + tool meta)" gate env LIVE=1 bash "$self" smoke-acp-overlay-live
2409
+ run_live_step "smoke-acp-provider-live (S2c/S2f: real pi provider path + progress/L3)" gate env LIVE=1 bash "$self" smoke-acp-provider-live
2410
+ run_live_step "smoke-acp-session-reuse-live (S2d: process-scoped reuse + recall)" gate env LIVE=1 bash "$self" smoke-acp-session-reuse-live
2411
+ run_live_step "smoke-acp-carrier-augment-live (S2e-1: augment delivery + 핀1 billing)" gate env LIVE=1 bash "$self" smoke-acp-carrier-augment-live
2412
+ run_live_step "smoke-acp-memory-containment-live (Gate D: no overlay memory leak)" gate env LIVE=1 bash "$self" smoke-acp-memory-containment-live
2413
+ run_live_step "smoke-acp-rgg-live (S2e-2: ACP-target garden guard, deterministic half)" gate env LIVE=1 bash "$self" smoke-acp-rgg-live
2414
+ run_live_step "smoke-acp-mcp-live (S2g: operator mcpServers reach the live ACP session)" gate env LIVE=1 bash "$self" smoke-acp-mcp-live
2415
+ run_live_step "smoke-acp-skill-live (S2g: operator skillPlugins reach the live ACP session)" gate env LIVE=1 bash "$self" smoke-acp-skill-live
2416
+ run_live_step "smoke-acp-bundled-mcp-live (S2g axis 3: bundled entwurf-bridge via 0.11.0 resident/RPC circuit)" gate env LIVE=1 bash "$self" smoke-acp-bundled-mcp-live
2417
+
2418
+ # 4. BEHAVIOR lane (advisory, non-blocking). Model-in-loop gates that probe
2419
+ # whether the model AUTONOMOUSLY drives the MCP entwurf surface. These never
2420
+ # touch `failc`; the cut is decided by the MUST tier above.
2421
+ #
2422
+ # Only genuinely flaky model-in-loop signals live here. Programmatic ACP
2423
+ # plugin invariants are MUST (section 3b above), not BEHAVIOR — a failed
2424
+ # transport/provider/backend smoke is a release defect, not advisory. The old
2425
+ # v1 floor gates (session-messaging / xt-tool-surface / sentinel) do not exist
2426
+ # on the v2 core — the v1 entwurf verbs they exercised are gone.
2427
+ # SMOKE_RGG_POSITIVE=1 re-runs the FULL guard with its positives enabled (not a
2428
+ # positive-only mode) — the deterministic paths run again here too, but only the
2429
+ # two model-in-loop turns (post-/gnew entwurf_self identity [T3] + positive
2430
+ # garden model turn) are the reason this run is advisory; the deterministic half
2431
+ # is already release-blocking via the POSITIVE=0 must-pass step above.
2432
+ run_behavior_step "smoke-resident-garden-guard (positives enabled: post-/gnew entwurf_self identity turn [T3] + positive garden model turn)" gate env SMOKE_RGG_POSITIVE=1 bash "$self" smoke-resident-garden-guard
2433
+
2434
+ # 5. Summary — two tiers. MUST is release-blocking and owns the exit code; the
2435
+ # word "green" is reserved for the MUST tier. BEHAVIOR is advisory and is
2436
+ # surfaced (with per-step artifact paths above) but never blocks the cut.
2437
+ section "release-gate summary"
2438
+ echo " MUST (release-blocking):"
2439
+ printf ' %s\n' "${results[@]}"
2440
+ echo " MUST: PASS=$pass FAIL=$failc SKIP=$skip"
2441
+ echo ""
2442
+ echo " BEHAVIOR (advisory, non-blocking — model-in-loop autonomous MCP tool-selection):"
2443
+ if [ "${#behavior_results[@]}" -gt 0 ]; then
2444
+ printf ' %s\n' "${behavior_results[@]}"
2445
+ fi
2446
+ echo " BEHAVIOR: PASS=$behavior_pass FAIL=$behavior_failc"
2447
+ echo " (per-step artifact paths are printed in each step's output above)"
2448
+ if [ "$behavior_failc" -gt 0 ]; then
2449
+ echo ""
2450
+ warn "BEHAVIOR FAIL present ($behavior_failc) — advisory model-in-loop signal (e.g. S7 Bash-bypass / entwurf_self not autonomously called). Tracked, NOT a cut blocker; see the 0.11.x usability lane."
2451
+ fi
2452
+ # Exit authority = MUST `failc` ONLY. A BEHAVIOR fail never blocks the cut.
2453
+ if [ "$failc" -gt 0 ]; then
2454
+ echo ""
2455
+ fail "release-gate MUST NOT green — $failc release-blocking step(s) failed. Current release is NOT releasable."
2456
+ echo " A green MUST gate is necessary but not sufficient; GLG closes the call."
2457
+ return 1
2458
+ fi
2459
+ if [ "$behavior_failc" -gt 0 ]; then
2460
+ ok "release-gate MUST PASS (all release-blocking steps green); BEHAVIOR FAIL present (advisory). Necessary condition met — GLG authorizes the cut."
2461
+ else
2462
+ ok "release-gate MUST PASS + BEHAVIOR PASS — all green. Necessary condition met — GLG authorizes the cut."
2463
+ fi
2464
+ return 0
2465
+ }
2466
+
2467
+ cmd=${1:-}
2468
+ case "$cmd" in
2469
+ setup)
2470
+ setup_all "$TARGET_PROJECT_DIR"
2471
+ ;;
2472
+ release-gate)
2473
+ shift || true
2474
+ release_gate "$@"
2475
+ ;;
2476
+ check-bridge)
2477
+ check_bridge
2478
+ ;;
2479
+ check-model-lock)
2480
+ check_model_lock
2481
+ ;;
2482
+ check-shell-quote)
2483
+ check_shell_quote
2484
+ ;;
2485
+ check-entwurf-session-identity)
2486
+ check_entwurf_session_identity
2487
+ ;;
2488
+ check-meta-session)
2489
+ check_meta_session
2490
+ ;;
2491
+ check-meta-record-v2)
2492
+ check_meta_record_v2
2493
+ ;;
2494
+ check-mailbox-receipt-state)
2495
+ check_mailbox_receipt_state
2496
+ ;;
2497
+ check-entwurf-capabilities)
2498
+ check_entwurf_capabilities
2499
+ ;;
2500
+ check-meta-dual-read)
2501
+ check_meta_dual_read
2502
+ ;;
2503
+ check-meta-mailbox-state-write)
2504
+ check_meta_mailbox_state_write
2505
+ ;;
2506
+ check-meta-receiver-marker)
2507
+ check_meta_receiver_marker
2508
+ ;;
2509
+ check-meta-migration)
2510
+ check_meta_migration
2511
+ ;;
2512
+ check-meta-dual-consumers)
2513
+ check_meta_dual_consumers
2514
+ ;;
2515
+ check-meta-capability-source)
2516
+ check_meta_capability_source
2517
+ ;;
2518
+ check-socket-probe)
2519
+ check_socket_probe
2520
+ ;;
2521
+ check-project-trust-handler)
2522
+ check_project_trust_handler
2523
+ ;;
2524
+ check-entwurf-v2-contract)
2525
+ check_entwurf_v2_contract
2526
+ ;;
2527
+ check-entwurf-v2-lock)
2528
+ check_entwurf_v2_lock
2529
+ ;;
2530
+ check-entwurf-v2-decider)
2531
+ check_entwurf_v2_decider
2532
+ ;;
2533
+ check-entwurf-v2-matrix)
2534
+ check_entwurf_v2_matrix
2535
+ ;;
2536
+ check-entwurf-v2-release)
2537
+ check_entwurf_v2_release
2538
+ ;;
2539
+ check-entwurf-v2-send)
2540
+ check_entwurf_v2_send
2541
+ ;;
2542
+ check-entwurf-v2-send-fallback)
2543
+ check_entwurf_v2_send_fallback
2544
+ ;;
2545
+ check-entwurf-v2-mailbox)
2546
+ check_entwurf_v2_mailbox
2547
+ ;;
2548
+ check-entwurf-v2-runner)
2549
+ check_entwurf_v2_runner
2550
+ ;;
2551
+ check-entwurf-control-rpc)
2552
+ check_entwurf_control_rpc
2553
+ ;;
2554
+ check-entwurf-v2-production)
2555
+ check_entwurf_v2_production
2556
+ ;;
2557
+ check-entwurf-v2-surface)
2558
+ check_entwurf_v2_surface
2559
+ ;;
2560
+ check-entwurf-bridge-boot)
2561
+ check_entwurf_bridge_boot
2562
+ ;;
2563
+ check-entwurf-v2-spawn)
2564
+ check_entwurf_v2_spawn
2565
+ ;;
2566
+ check-entwurf-resume-args)
2567
+ check_entwurf_resume_args
2568
+ ;;
2569
+ check-entwurf-v2-spawn-production)
2570
+ check_entwurf_v2_spawn_production
2571
+ ;;
2572
+ smoke-entwurf-v2-spawn-live)
2573
+ smoke_entwurf_v2_spawn_live
2574
+ ;;
2575
+ smoke-entwurf-v2-spawn-resume-live)
2576
+ smoke_entwurf_v2_spawn_resume_live
2577
+ ;;
2578
+ smoke-entwurf-v2-matrix-live)
2579
+ smoke_entwurf_v2_matrix_live
2580
+ ;;
2581
+ smoke-acp-raw-turn-live)
2582
+ smoke_acp_raw_turn_live
2583
+ ;;
2584
+ smoke-acp-overlay-live)
2585
+ smoke_acp_overlay_live
2586
+ ;;
2587
+ smoke-acp-memory-containment-live)
2588
+ smoke_acp_memory_containment_live
2589
+ ;;
2590
+ smoke-acp-provider-live)
2591
+ smoke_acp_provider_live
2592
+ ;;
2593
+ smoke-acp-session-reuse-live)
2594
+ smoke_acp_session_reuse_live
2595
+ ;;
2596
+ smoke-acp-mcp-live)
2597
+ smoke_acp_mcp_live
2598
+ ;;
2599
+ smoke-acp-skill-live)
2600
+ smoke_acp_skill_live
2601
+ ;;
2602
+ smoke-acp-bundled-mcp-live)
2603
+ smoke_acp_bundled_mcp_live
2604
+ ;;
2605
+ smoke-acp-carrier-augment-live)
2606
+ smoke_acp_carrier_augment_live
2607
+ ;;
2608
+ smoke-acp-rgg-live)
2609
+ smoke_acp_rgg_live
2610
+ ;;
2611
+ smoke-acp-socket-citizen-live)
2612
+ smoke_acp_socket_citizen_live
2613
+ ;;
2614
+ check-entwurf-facts)
2615
+ check_entwurf_facts
2616
+ ;;
2617
+ check-socket-discovery)
2618
+ check_socket_discovery
2619
+ ;;
2620
+ check-meta-listing)
2621
+ check_meta_listing
2622
+ ;;
2623
+ check-entwurf-fact-provider)
2624
+ check_entwurf_fact_provider
2625
+ ;;
2626
+ check-entwurf-peers-surface)
2627
+ check_entwurf_peers_surface
2628
+ ;;
2629
+ check-entwurf-self-address)
2630
+ check_entwurf_self_address
2631
+ ;;
2632
+ check-entwurf-deliverability)
2633
+ check_entwurf_deliverability
2634
+ ;;
2635
+ check-entwurf-mailbox-guard)
2636
+ check_entwurf_mailbox_guard
2637
+ ;;
2638
+ new-session-id)
2639
+ # Garden launcher helper: print one fresh garden sessionId (SSOT:
2640
+ # generateSessionId). Used by the operator alias to make every
2641
+ # --entwurf-control session a garden citizen. Stdout = the id only.
2642
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/new-session-id.ts)
2643
+ ;;
2644
+ smoke-resident-garden-guard)
2645
+ # LIVE negative (0 tokens) + opt-in positive gate for the resident
2646
+ # --entwurf-control garden-native enforcement. NEGATIVE: raw uuid session
2647
+ # must blow up before any turn. POSITIVE (SMOKE_RGG_POSITIVE=1): garden id
2648
+ # passes + control-tagged name.
2649
+ (cd "$REPO_DIR" && bash scripts/smoke-resident-garden-guard.sh)
2650
+ ;;
2651
+ smoke-meta-async-drift)
2652
+ # 1.0.0 meta-bridge step 1 (#30): drift sentinel + capability gate. DEFAULT is
2653
+ # deterministic/offline — version pins (Claude/codex/agy) + Claude-binary
2654
+ # undocumented-behavior marker cross-validation; SCREAMS on drift. LIVE=1 adds
2655
+ # the plugin SessionStart watch-arm probe (spawns one metered claude -p).
2656
+ (cd "$REPO_DIR" && bash scripts/smoke-meta-async-drift.sh)
2657
+ ;;
2658
+ smoke-meta-honesty)
2659
+ # 1.0.0 meta-bridge HONESTY regression gate (#30 bbot release blockers): the
2660
+ # doorbell must count EVERY queued message honestly (blocker #1), and the
2661
+ # runtime hook must log a silent registration miss as ` ERROR ` for the doctor
2662
+ # to catch while staying best-effort (blocker #2). Offline + deterministic (no
2663
+ # claude binary; deps bash+node+python3), so unlike the drift sentinel it is
2664
+ # CI/pnpm-check safe.
2665
+ (cd "$REPO_DIR" && bash scripts/smoke-meta-honesty.sh)
2666
+ ;;
2667
+ smoke-meta-prune)
2668
+ # 1.0.0 meta-bridge Phase 4 regression gate: synthetic store covering every
2669
+ # class (keep/orphan/stale/duplicate/corrupt/drift) proves meta-bridge-prune
2670
+ # classifies correctly, exits 0, and deletes NOTHING (listing-only invariant).
2671
+ # Offline/deterministic (deps: bash+node).
2672
+ (cd "$REPO_DIR" && bash scripts/smoke-meta-prune.sh)
2673
+ ;;
2674
+ smoke-meta-keyset-guard)
2675
+ # 0.10.0 meta-bridge regression gate: the PREVENTIVE keyset guard
2676
+ # (check-keyset-overlap) + managed-keys SSOT. Synthetic fragments prove a
2677
+ # disjoint consumer passes and exact/array/parent-child collisions fail loud.
2678
+ # Offline/hermetic (deps: bash+python3).
2679
+ (cd "$REPO_DIR" && bash scripts/smoke-meta-keyset-guard.sh)
2680
+ ;;
2681
+ smoke-meta-install-state)
2682
+ # 1.0.0 meta-bridge Phase 2 regression gate: state file captures pre-install
2683
+ # values, install/uninstall touches only the managed keyset, uninstall refuses
2684
+ # to guess without state, and the doctor store scan fails on corrupt/
2685
+ # duplicate/drift records. Offline + deterministic (deps bash+node+python3).
2686
+ (cd "$REPO_DIR" && bash scripts/smoke-meta-install-state.sh)
2687
+ ;;
2688
+ smoke-claude-native-resume-live)
2689
+ # LIVE-only Detour A probe: two real Claude Code native turns (fresh +
2690
+ # --resume) in a scratch cwd. Verifies native resume works while the
2691
+ # meta-bridge only records backend=claude-code/nativeSessionId/transcriptPath
2692
+ # once. Not in pnpm check; does not use the ACP provider.
2693
+ (cd "$REPO_DIR" && bash scripts/smoke-claude-native-resume-live.sh)
2694
+ ;;
2695
+ install-meta-bridge)
2696
+ # 1.0.0 meta-bridge step 5: operator-grade GLOBAL install of the garden-native
2697
+ # receive plugin. Assembles a self-contained, node-path-baked copy under
2698
+ # pi/meta-bridge/.assembled and runs marketplace add + install --scope user, so
2699
+ # every native Claude Code session auto-loads it (no manual --plugin-dir).
2700
+ # Idempotent; Linux/macOS only (Windows fail-fast).
2701
+ (cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh "$@")
2702
+ ;;
2703
+ uninstall-meta-bridge)
2704
+ # 1.0.0 meta-bridge Phase 2: honest inverse of install-meta-bridge. Uses the
2705
+ # install-state file to restore original scalar/map values and remove only the
2706
+ # permission-array entries entwurf added; without state it refuses to guess.
2707
+ (cd "$REPO_DIR" && bash scripts/meta-bridge-uninstall.sh "$@")
2708
+ ;;
2709
+ doctor-meta-bridge)
2710
+ # 1.0.0 meta-bridge Phase 2: the FAIL-LOUD surface. Proves toolchain (incl.
2711
+ # python3), stateful managed config, baked node path (NixOS store-churn guard),
2712
+ # global plugin install, USER MCP reach, meta-record store integrity, hook log
2713
+ # no-ERROR, and actual SessionStart creation evidence. A plugin present with
2714
+ # zero claude-code meta-records is a SILENT MISS -> non-zero exit.
2715
+ (cd "$REPO_DIR" && bash scripts/meta-bridge-doctor.sh "$@")
2716
+ ;;
2717
+ meta-bridge-prune)
2718
+ # 1.0.0 meta-bridge Phase 4: LISTING-ONLY janitor for the meta-session store.
2719
+ # doctor reds on corrupt/duplicate/drift but intentionally does NOT fail on
2720
+ # transcript-gone records, so a green store can silently bloat with abandoned
2721
+ # records. This surface CLASSIFIES (orphan/stale/ambiguous/keep) and prints
2722
+ # the exact manual rm commands. It deletes NOTHING — no --apply in 1.0.0;
2723
+ # ambiguous (corrupt/duplicate/drift) stays manual-only (operator picks the
2724
+ # surviving authority). Default store = defaultMetaSessionsDir(); pass [dir]
2725
+ # + [--ttl-days N] to override.
2726
+ shift || true
2727
+ (cd "$REPO_DIR" && node --experimental-strip-types scripts/meta-bridge-prune.ts "$@")
2728
+ ;;
2729
+ meta-bridge-managed-keys)
2730
+ # 0.10.0 meta-bridge: emit the SSOT of settings.json/~/.claude.json keys that
2731
+ # entwurf's install OWNS. Consumers (agent-config fragment, future
2732
+ # harnesses) read this to set only their OWN keys — the keyset-owner invariant.
2733
+ (cd "$REPO_DIR" && python3 scripts/meta-bridge-state.py managed-keys)
2734
+ ;;
2735
+ check-keyset-overlap)
2736
+ # 0.10.0 meta-bridge: PREVENTIVE half of the keyset guard. Fails loud if a
2737
+ # consumer fragment sets a key entwurf owns (exact or ancestor/descendant).
2738
+ # Cross-repo + non-hermetic (fragment path is an arg) → NOT in pnpm check;
2739
+ # its own logic is regression-tested hermetically by smoke-meta-keyset-guard.
2740
+ shift || true
2741
+ (cd "$REPO_DIR" && python3 scripts/check-keyset-overlap.py "$@")
2742
+ ;;
2743
+ check-package-source-routing)
2744
+ check_package_source_routing
2745
+ ;;
2746
+ smoke-session-id-name)
2747
+ smoke_session_id_name
2748
+ ;;
2749
+ check-dep-versions)
2750
+ check_dep_versions
2751
+ ;;
2752
+ check-install-preflight)
2753
+ check_install_preflight
2754
+ ;;
2755
+ check-pi-import-surface)
2756
+ check_pi_import_surface
2757
+ ;;
2758
+ check-env-namespace)
2759
+ check_env_namespace
2760
+ ;;
2761
+ check-pi-runtime-version)
2762
+ check_pi_runtime_version
2763
+ ;;
2764
+ check-pi-preflight)
2765
+ check_pi_preflight
2766
+ ;;
2767
+ check-auth-boundary)
2768
+ check_auth_boundary
2769
+ ;;
2770
+ check-acp-provider-surface)
2771
+ check_acp_provider_surface
2772
+ ;;
2773
+ check-acp-sdk-surface)
2774
+ check_acp_sdk_surface
2775
+ ;;
2776
+ check-acp-overlay)
2777
+ check_acp_overlay
2778
+ ;;
2779
+ check-acp-tool-surface)
2780
+ check_acp_tool_surface
2781
+ ;;
2782
+ check-acp-event-mapper)
2783
+ check_acp_event_mapper
2784
+ ;;
2785
+ check-acp-prompt-builder)
2786
+ check_acp_prompt_builder
2787
+ ;;
2788
+ check-acp-config)
2789
+ check_acp_config
2790
+ ;;
2791
+ check-acp-session-store)
2792
+ check_acp_session_store
2793
+ ;;
2794
+ check-acp-backend-preflight)
2795
+ check_acp_backend_preflight
2796
+ ;;
2797
+ check-acp-session-reuse)
2798
+ check_acp_session_reuse
2799
+ ;;
2800
+ check-acp-carrier-augment)
2801
+ check_acp_carrier_augment
2802
+ ;;
2803
+ check-pack)
2804
+ check_pack
2805
+ ;;
2806
+ check-pack-install)
2807
+ check_pack_install
2808
+ ;;
2809
+ sync-auth)
2810
+ sync_auth
2811
+ ;;
2812
+ install)
2813
+ install_local_package "$TARGET_PROJECT_DIR"
2814
+ ;;
2815
+ setup:links)
2816
+ # Repair / refresh ~/.pi/agent/entwurf-targets.json without re-running
2817
+ # the full setup flow. Pass --force to overwrite a stale operator file
2818
+ # or a wrong symlink (a backup is taken for regular files).
2819
+ ensure_agent_dir_symlinks "${2:-}"
2820
+ ;;
2821
+ remove)
2822
+ remove_local_package "$TARGET_PROJECT_DIR"
2823
+ ;;
2824
+ -h|--help|help|"")
2825
+ usage
2826
+ ;;
2827
+ *)
2828
+ echo "Unknown command: $cmd" >&2
2829
+ usage
2830
+ exit 1
2831
+ ;;
2832
+ esac