@junghanacs/entwurf 0.12.5 → 0.12.7

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 (93) hide show
  1. package/AGENTS.md +31 -17
  2. package/BASELINE.md +42 -8
  3. package/CHANGELOG.md +44 -0
  4. package/DELIVERY.md +71 -14
  5. package/README.md +77 -45
  6. package/VERIFY.md +33 -38
  7. package/docs/setup-clean-host.md +80 -22
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
  30. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
  31. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
  33. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
  34. package/mcp/entwurf-bridge/src/index.ts +101 -67
  35. package/mcp/entwurf-bridge/test.sh +1 -1
  36. package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
  37. package/package.json +10 -6
  38. package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
  39. package/pi-extensions/lib/entwurf-self-address.ts +58 -15
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
  41. package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
  42. package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
  43. package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
  44. package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
  45. package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
  46. package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
  47. package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
  48. package/pi-extensions/lib/meta-sender-identity.ts +154 -0
  49. package/pi-extensions/lib/native-push/adapter.ts +255 -0
  50. package/pi-extensions/lib/native-push/register.ts +99 -0
  51. package/run.sh +974 -248
  52. package/scripts/agy-bridge-config.py +446 -0
  53. package/scripts/agy-bridge.sh +359 -0
  54. package/scripts/agy-hooks-bridge.sh +193 -0
  55. package/scripts/agy-hooks-config.py +257 -0
  56. package/scripts/agy-imprint.sh +28 -0
  57. package/scripts/agy-imprint.ts +193 -0
  58. package/scripts/agy-statusline-bridge.sh +176 -0
  59. package/scripts/agy-statusline-config.py +213 -0
  60. package/scripts/agy-statusline.sh +256 -0
  61. package/scripts/build-bridge.sh +20 -0
  62. package/scripts/check-agy-sender-identity.ts +364 -0
  63. package/scripts/check-entwurf-bridge-boot.ts +8 -2
  64. package/scripts/check-entwurf-deliverability.ts +34 -0
  65. package/scripts/check-entwurf-self-address.ts +78 -11
  66. package/scripts/check-entwurf-v2-contract.ts +136 -1
  67. package/scripts/check-entwurf-v2-decider.ts +95 -1
  68. package/scripts/check-entwurf-v2-matrix.ts +14 -3
  69. package/scripts/check-entwurf-v2-native-push.ts +193 -0
  70. package/scripts/check-entwurf-v2-production.ts +68 -1
  71. package/scripts/check-entwurf-v2-runner.ts +58 -0
  72. package/scripts/check-entwurf-v2-surface.ts +35 -0
  73. package/scripts/check-install-surface.ts +357 -0
  74. package/scripts/check-native-push-adapter.ts +319 -0
  75. package/scripts/check-native-push-register.ts +130 -0
  76. package/scripts/dev-bin.sh +195 -0
  77. package/scripts/doctor-pi-provider.ts +140 -0
  78. package/scripts/meta-bridge-doctor.sh +47 -9
  79. package/scripts/meta-bridge-install.sh +20 -17
  80. package/scripts/meta-bridge-state.py +40 -2
  81. package/scripts/meta-bridge-uninstall.sh +23 -12
  82. package/scripts/register-pi-package.py +183 -0
  83. package/scripts/register-pi-provider.py +287 -0
  84. package/scripts/smoke-agy-hooks-state.sh +172 -0
  85. package/scripts/smoke-agy-install-state.sh +660 -0
  86. package/scripts/smoke-agy-native-push-live.ts +243 -0
  87. package/scripts/smoke-agy-statusline-state.sh +300 -0
  88. package/scripts/smoke-meta-async-drift.sh +9 -2
  89. package/scripts/smoke-meta-install-state.sh +178 -8
  90. package/scripts/smoke-pi-provider-state.sh +182 -0
  91. package/scripts/smoke-user-scope-citizen.sh +191 -0
  92. package/scripts/with-dist-lock.sh +81 -0
  93. package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
package/run.sh CHANGED
@@ -38,6 +38,37 @@ PACKAGE_NAME="@junghanacs/entwurf"
38
38
  # saved session anchor.
39
39
  PROVIDER_ID="entwurf"
40
40
 
41
+ # THE strip-types fence, in one place. Node REFUSES `--experimental-strip-types`
42
+ # for any .ts below node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so
43
+ # an installed package MUST run the prepack-emitted JS twin — the same boundary
44
+ # start.sh (0.12.1), the store-doctor (0.12.4), the plugin hook (0.12.5), and the
45
+ # agy imprint (0.12.7) each cross. Every .ts entrypoint routes through here so a
46
+ # NEW one cannot silently reintroduce the class: it was hand-written per surface
47
+ # before, and three operator commands (doctor-pi-provider / new-session-id /
48
+ # meta-bridge-prune) shipped dead under node_modules because of exactly that.
49
+ #
50
+ # A dev-only gate has no emitted twin by design (check-*/smoke-* are not shipped
51
+ # surfaces). Under an installed package it REFUSES rather than falling back to raw
52
+ # .ts — a fallback would just re-raise the fence error with a worse message.
53
+ # check-install-surface pins both halves statically.
54
+ run_ts() {
55
+ local rel="$1"; shift
56
+ case "$REPO_DIR" in
57
+ */node_modules/*)
58
+ local dist="$REPO_DIR/mcp/entwurf-bridge/dist/${rel%.ts}.js"
59
+ if [ ! -f "$dist" ]; then
60
+ echo "entwurf: '$rel' is a dev-clone-only surface — the installed package ships no compiled twin." >&2
61
+ echo " (Node cannot strip types below node_modules; run this from a checkout.)" >&2
62
+ return 1
63
+ fi
64
+ (cd "$REPO_DIR" && node "$dist" "$@")
65
+ ;;
66
+ *)
67
+ (cd "$REPO_DIR" && node --experimental-strip-types "$rel" "$@")
68
+ ;;
69
+ esac
70
+ }
71
+
41
72
  usage() {
42
73
  cat <<'EOF'
43
74
  Usage:
@@ -76,6 +107,7 @@ Usage:
76
107
  ./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
77
108
  ./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
78
109
  ./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
110
+ ./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), owned-outcome→native-push-no-resume-authority, bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
79
111
  ./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
80
112
  ./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
81
113
  ./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
@@ -84,6 +116,9 @@ Usage:
84
116
  ./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)
85
117
  ./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
86
118
  ./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
119
+ ./run.sh check-native-push-adapter # deterministic gate (봉인 3/8): native-push adapter leaf (antigravity) via a FAKE runner — FULL pid scan (not head -1), dead vs indeterminate, VOLATILE route re-discovery (no cache), send argv+ANTIGRAVITY_LS_ADDRESS env, non-zero exit throws, NO adapter-level retry (executor-owned), resolveNativePushAdapter fail-fast
120
+ ./run.sh check-native-push-register # deterministic gate (봉인 5): registerNativeConversation (entwurf_register_native core) via fake adapter + isolated mkdtemp store — live probe→CREATE, re-register→ATTACH (same gid, cwd refreshed, no dup), not-live probe→REFUSE (throws, no record), receiver-marker abstinence (보정① source guard)
121
+ ./run.sh check-agy-sender-identity # deterministic gate (#46 sender lane): WHO is calling the bridge — real agy hook as a child process writes an antigravity sender marker keyed by its PARENT pid (never on upsert failure), and resolveTrustedMetaSenderIdentity over isolated stores yields 0→null / 1→identity on EITHER backend / two distinct live identities on one owner pid→THROW (never guess, never downgrade to anonymous). This is what turns an agy send from external-mcp/unknown-host into a replyable garden citizen
87
122
  ./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
88
123
  ./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
89
124
  ./run.sh new-session-id # print one fresh garden-native session id for operator launchers (--session-id)
@@ -91,14 +126,27 @@ Usage:
91
126
  ./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)
92
127
  ./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)
93
128
  ./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)
129
+ ./run.sh smoke-agy-install-state # agy MCP + exact permission ownership regression (120): isolated HOME+XDG, adopt/state/inverse, symlink refuse, setup degrade. Offline/deterministic
130
+ ./run.sh smoke-agy-statusline-state # agy ambient garden-id statusLine install/doctor/inverse regression (62). Offline/deterministic
131
+ ./run.sh smoke-agy-hooks-state # agy PreInvocation birth/sender hook install/doctor/inverse + direct stdin→meta-record regression (37). Offline/deterministic
132
+ ./run.sh smoke-user-scope-citizen # 0.12.6 install-boundary: pi packages[] registration SSOT (register-pi-package.py) — idempotent + preserves unrelated + normalizes stale + remove symmetry + fails loud. Offline/hermetic (deps: bash+python3)
94
133
  ./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)
95
134
  ./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)
96
135
  ./run.sh check-meta-manifest-schema # 0.12.2 meta-bridge: CLI-version-INDEPENDENT static guard — plugin manifests pinned to the minimal keyset that validates on the lowest supported Claude (closed-schema regression that broke 0.12.1 install on floor) + desired_mcp installed-vs-clone dual-mode. Offline (deps: python3)
97
136
  ./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
98
137
 
99
- ./run.sh install-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
138
+ ./run.sh install-meta-bridge # INTERNAL part of `setup` (native-harness plugin) + doctor recovery path — prefer `setup`; stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
100
139
  ./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
101
140
  ./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)
141
+ ./run.sh install-agy-bridge # 봉인 7: agy MCP install adapter — register ONE entwurf-bridge server in the agy mcp_config (adopt file / create / REFUSE symlink), stable bin command, install-state under $XDG_DATA_HOME/entwurf/agy-bridge/
142
+ ./run.sh uninstall-agy-bridge # 봉인 7: honest inverse of install-agy-bridge from install-state (restore preimage / remove key; refuse if config became a symlink)
143
+ ./run.sh doctor-agy-bridge # fail-loud doctor: MCP config + exact permission rule + state + live probe label
144
+ ./run.sh install-agy-statusline # own the agy statusLine subtree with bare entwurf-agy-statusline; preserve unrelated settings
145
+ ./run.sh uninstall-agy-statusline # honest inverse from statusline install-state
146
+ ./run.sh doctor-agy-statusline # fail-loud statusLine config/bin/state doctor + honest live SKIP
147
+ ./run.sh install-agy-hooks # #46 agy birth imprint hook — named PreInvocation hook running bare entwurf-agy-imprint, preserving other hooks
148
+ ./run.sh uninstall-agy-hooks # honest inverse of install-agy-hooks from install-state
149
+ ./run.sh doctor-agy-hooks # fail-loud doctor for agy hooks.json imprint wiring
102
150
  ./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])
103
151
  ./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)
104
152
  ./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)
@@ -106,9 +154,10 @@ Usage:
106
154
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
107
155
  ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with 0.80.x peers
108
156
  ./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
109
- ./run.sh install [project-dir] # install this local package into project .pi/settings.json
157
+ ./run.sh install [project-dir] # INTERNAL part of `setup` (project .pi/settings.json wiring) + npm-consumer entry — prefer `setup`, don't call directly for dev
110
158
  ./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)
111
- ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json
159
+ ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json (project scope only; global user-scope citizen left intact)
160
+ ./run.sh remove-user-scope # explicit GLOBAL inverse of install's user-scope citizen: drop entwurf from ~/.pi/agent/settings.json packages[] (affects ALL cwds — shared entry, not per-project)
112
161
 
113
162
  Notes:
114
163
  - project-dir defaults to current directory
@@ -239,104 +288,113 @@ preflight_dep_integrity() {
239
288
  fi
240
289
  }
241
290
 
242
- install_local_package() {
243
- local project_dir
244
- project_dir=$(normalize_project_dir "$1")
245
- preflight_dep_integrity
246
- mkdir -p "$project_dir/.pi"
247
- python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
291
+ preflight_pi_settings_shapes() {
292
+ local project_settings="$1"
293
+ local user_settings="$2"
294
+ python3 - "$project_settings" "$user_settings" <<'PY'
248
295
  import json, sys
249
296
  from pathlib import Path
250
297
 
251
- settings_path = Path(sys.argv[1])
252
- repo_dir = str(Path(sys.argv[2]).resolve())
253
- settings_path.parent.mkdir(parents=True, exist_ok=True)
254
- if settings_path.exists():
255
- data = json.loads(settings_path.read_text())
256
- if not isinstance(data, dict):
257
- raise SystemExit("settings.json is not an object")
258
- else:
259
- data = {}
298
+ project = Path(sys.argv[1])
299
+ user = Path(sys.argv[2])
260
300
 
261
- # --- packages[] registration --------------------------------------------------
262
- packages = data.get("packages")
263
- if not isinstance(packages, list):
264
- packages = []
265
-
266
- filtered = []
267
- for item in packages:
268
- source = item.get("source") if isinstance(item, dict) else item
269
- if isinstance(source, str) and ("entwurf" in source) and source != repo_dir:
270
- continue
271
- filtered.append(item)
272
-
273
- if repo_dir not in filtered:
274
- filtered.append(repo_dir)
275
-
276
- data["packages"] = filtered
277
-
278
- # --- entwurfProvider.mcpServers bundled entries ---------------------------
279
- # Ship the two in-repo MCP adapters pre-wired so `pi install` produces a
280
- # working setup without the consumer hand-editing settings.json. User-authored
281
- # overrides (different command/args) are preserved untouched.
282
- provider = data.setdefault("entwurfProvider", {})
283
- if not isinstance(provider, dict):
284
- raise SystemExit("entwurfProvider is not an object")
285
- servers = provider.setdefault("mcpServers", {})
286
- if not isinstance(servers, dict):
287
- raise SystemExit("entwurfProvider.mcpServers is not an object")
288
-
289
- BUNDLED = ("entwurf-bridge",)
290
- # 0.4.14: session-bridge MCP was retracted (issue #7 — unified entwurf surface).
291
- # Install path now only wires entwurf-bridge. Existing operator settings that
292
- # carry the old bundled session-bridge entry are pruned below during install;
293
- # `./run.sh remove` also keeps session-bridge in its cleanup tuple so legacy
294
- # entries are removed on uninstall too.
295
- for name in BUNDLED:
296
- desired_cmd = f"{repo_dir}/mcp/{name}/start.sh"
297
- desired = {"command": desired_cmd, "args": []}
298
- existing = servers.get(name)
299
- if existing is None:
300
- servers[name] = desired
301
- print(f"install: added entwurfProvider.mcpServers.{name}")
302
- elif isinstance(existing, dict) and existing.get("command") == desired_cmd:
303
- # Already managed by us at the current repo path. Add the default args
304
- # field when missing, but never overwrite user-customized args.
305
- if "args" not in existing:
306
- existing["args"] = []
307
- print(f"install: normalized entwurfProvider.mcpServers.{name}.args -> []")
308
- elif existing.get("args") != []:
309
- print(f"install: preserved entwurfProvider.mcpServers.{name}.args (custom args)")
310
- else:
311
- cmd_repr = existing.get("command") if isinstance(existing, dict) else existing
312
- print(f"install: preserved entwurfProvider.mcpServers.{name} (user override: {cmd_repr})")
313
-
314
- # Legacy-bundled MCP names that prior versions of this installer wrote and that
315
- # the current cutover supersedes. One-shot install-time prune (NOT a runtime
316
- # alias): only remove entries whose command matches the bundled start.sh path,
317
- # so user-customized commands are left alone.
318
- # - session-bridge: retracted in 0.4.14 (issue #7, unified entwurf surface).
319
- # - pi-tools-bridge: renamed to entwurf-bridge in 0.11 S2 cutover.
320
- LEGACY_BUNDLED = {
321
- "session-bridge": "retracted in 0.4.14, issue #7",
322
- "pi-tools-bridge": "renamed to entwurf-bridge in 0.11 S2 cutover",
323
- }
324
- for name, reason in LEGACY_BUNDLED.items():
325
- existing = servers.get(name)
326
- if not isinstance(existing, dict):
327
- continue
328
- cmd = existing.get("command")
329
- if not isinstance(cmd, str):
330
- continue
331
- if cmd == f"{repo_dir}/mcp/{name}/start.sh" or cmd.endswith(f"/entwurf/mcp/{name}/start.sh"):
332
- del servers[name]
333
- print(f"install: pruned legacy entwurfProvider.mcpServers.{name} ({reason})")
334
-
335
- settings_path.write_text(json.dumps(data, indent=2) + "\n")
336
- print(f"install: updated {settings_path}")
337
- print(f"install: package source -> {repo_dir}")
301
+ def load_object(path: Path, label: str):
302
+ if not path.exists():
303
+ return None
304
+ data = json.loads(path.read_text())
305
+ if not isinstance(data, dict):
306
+ raise SystemExit(f"{label} settings is not a JSON object: {path}")
307
+ return data
308
+
309
+ def check_packages(data, path: Path, label: str):
310
+ if data is None:
311
+ return
312
+ packages = data.get("packages")
313
+ if packages is not None and not isinstance(packages, list):
314
+ raise SystemExit(f"{label} settings packages is not a JSON array: {path}")
315
+
316
+ def check_project_provider(data, path: Path):
317
+ if data is None:
318
+ return
319
+ provider = data.get("entwurfProvider")
320
+ if provider is None:
321
+ return
322
+ if not isinstance(provider, dict):
323
+ raise SystemExit(f"project settings entwurfProvider is not an object: {path}")
324
+ servers = provider.get("mcpServers")
325
+ if servers is not None and not isinstance(servers, dict):
326
+ raise SystemExit(f"project settings entwurfProvider.mcpServers is not an object: {path}")
327
+
328
+ project_data = load_object(project, "project")
329
+ user_data = load_object(user, "user")
330
+ check_packages(project_data, project, "project")
331
+ check_packages(user_data, user, "user")
332
+ check_project_provider(project_data, project)
338
333
  PY
334
+ }
335
+
336
+ install_local_package() {
337
+ local project_dir agent_dir
338
+ project_dir=$(normalize_project_dir "$1")
339
+ agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
340
+ preflight_dep_integrity
341
+ # Fail BEFORE any settings write if either target config already has a corrupt
342
+ # shape. The packages[] SSOT and provider writer run in two separate steps, so
343
+ # without this preflight a bad entwurfProvider could leave a half-installed
344
+ # packages[] entry behind (2026-07-03 install-boundary hardening).
345
+ preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$agent_dir/settings.json"
346
+ mkdir -p "$project_dir/.pi"
347
+ # packages[] registration via the shared SSOT — same is_entwurf_source
348
+ # predicate + idempotency as user-scope and remove (not a substring match).
349
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR"
350
+ # entwurfProvider.mcpServers.entwurf-bridge (project scope — checkout-local, NO state; #46
351
+ # Task 2) via the shared register-pi-provider SSOT: normalize the command to the bare stable
352
+ # bin `entwurf-bridge` (ownership-classified: absent/managed-current/managed-legacy adopt, a
353
+ # true user-override is left untouched) + prune legacy bundles. project remove is the inverse.
354
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$project_dir/.pi/settings.json" "$REPO_DIR" --scope project
339
355
  ensure_agent_dir_symlinks
356
+ register_user_scope_citizen
357
+ }
358
+
359
+ # Register entwurf as a pi USER-SCOPE citizen so its extensions
360
+ # (entwurf-control.ts → --entwurf-control / --emacs-agent-socket) load from ANY
361
+ # cwd, not only inside the entwurf checkout. project-scope `.pi/settings.json`
362
+ # only applies when pi runs inside the repo; the `pit`/`pia`/`pihome` global
363
+ # launchers and the npm consumer's "installs → just works" both need the entry
364
+ # in ~/.pi/agent/settings.json's packages[]. This is the wiring that dropped when
365
+ # `pi install` was removed from setup (2026-07-03: `--entwurf-control` unknown in
366
+ # a foreign cwd). Idempotent: absent → append, present → no-op; a stale entwurf
367
+ # entry at a different path is normalized to REPO_DIR. Every other package and key
368
+ # in the operator's user settings is preserved untouched.
369
+ register_user_scope_citizen() {
370
+ local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
371
+ # Shared idempotent implementation (also driven by smoke-user-scope-citizen).
372
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR"
373
+ # #46 Task 2: own entwurfProvider.mcpServers.entwurf-bridge as the bare stable bin at USER scope
374
+ # (GLOBAL/durable →파급s to every cwd), so its inverse needs an install-state honest inverse
375
+ # under $XDG_DATA_HOME/entwurf/pi-provider/ (Task 0/1 discipline). project scope is checkout-
376
+ # local and covered by `run.sh remove` (no state) — deliberate, reasoned asymmetry.
377
+ local pp_state="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-provider/install-state.json"
378
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" install "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$pp_state"
379
+ }
380
+
381
+ # The honest inverse of register_user_scope_citizen: drop entwurf from the GLOBAL
382
+ # ~/.pi/agent/settings.json packages[]. Deliberately NOT folded into `run.sh remove`
383
+ # (project scope): the user-scope citizen is a single GLOBAL entry keyed on this
384
+ # checkout and SHARED by every project + every foreign cwd, so tearing it down as a
385
+ # side effect of one project's remove would break `--entwurf-control` everywhere else
386
+ # (the exact "install → just works from any cwd" invariant register_user_scope_citizen
387
+ # exists to hold). Same explicit-global-lifecycle shape as install/uninstall-meta-bridge.
388
+ # Uses the same is_entwurf_source SSOT + --remove, so it never over-deletes a look-alike
389
+ # (entwurf-notes, openclaw-entwurf) and preserves every other package/key. Idempotent:
390
+ # no entwurf entry → no-op.
391
+ remove_user_scope_citizen() {
392
+ local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
393
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" --remove
394
+ # #46 Task 2: honest inverse of the user-scope entwurfProvider ownership — the install-state
395
+ # drives it (absent/managed-* → remove OUR key; a user-override we never owned is untouched).
396
+ local pp_state="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/pi-provider/install-state.json"
397
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" remove "$agent_dir/settings.json" "$REPO_DIR" --scope user --state "$pp_state"
340
398
  }
341
399
 
342
400
  # Ensure agent-level resources that entwurf code reads from
@@ -424,68 +482,29 @@ ensure_agent_dir_symlinks() {
424
482
  remove_local_package() {
425
483
  local project_dir
426
484
  project_dir=$(normalize_project_dir "$1")
427
- python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
428
- import json, sys
429
- from pathlib import Path
430
-
431
- settings_path = Path(sys.argv[1])
432
- repo_dir = str(Path(sys.argv[2]).resolve())
433
- if not settings_path.exists():
434
- print(f"remove: nothing to do ({settings_path} missing)")
435
- raise SystemExit(0)
436
-
437
- data = json.loads(settings_path.read_text())
438
- if not isinstance(data, dict):
439
- raise SystemExit("settings.json is not an object")
440
-
441
- # --- packages[] cleanup -------------------------------------------------------
442
- packages = data.get("packages")
443
- pkg_removed = 0
444
- if isinstance(packages, list):
445
- filtered = []
446
- for item in packages:
447
- source = item.get("source") if isinstance(item, dict) else item
448
- if isinstance(source, str) and ("entwurf" in source):
449
- pkg_removed += 1
450
- continue
451
- filtered.append(item)
452
- data["packages"] = filtered
453
-
454
- # --- entwurfProvider.mcpServers cleanup ------------------------------------
455
- # Only remove entries that look like they came from ./run.sh install: either
456
- # the command matches the current $REPO_DIR anchor exactly, or it ends with
457
- # the bundled "/entwurf/mcp/<name>/start.sh" pattern (covers a rebuilt
458
- # checkout under a different directory). Anything else is treated as a user
459
- # override and left in place.
460
- BUNDLED = ("entwurf-bridge", "session-bridge", "pi-tools-bridge")
461
- provider = data.get("entwurfProvider")
462
- mcp_removed = 0
463
- if isinstance(provider, dict):
464
- servers = provider.get("mcpServers")
465
- if isinstance(servers, dict):
466
- for name in BUNDLED:
467
- existing = servers.get(name)
468
- if not isinstance(existing, dict):
469
- continue
470
- cmd = existing.get("command")
471
- if not isinstance(cmd, str):
472
- continue
473
- exact = cmd == f"{repo_dir}/mcp/{name}/start.sh"
474
- pattern = cmd.endswith(f"/entwurf/mcp/{name}/start.sh")
475
- if exact or pattern:
476
- del servers[name]
477
- mcp_removed += 1
478
- print(f"remove: removed entwurfProvider.mcpServers.{name}")
479
- else:
480
- print(f"remove: preserved entwurfProvider.mcpServers.{name} (user override: {cmd})")
481
- if not servers:
482
- provider.pop("mcpServers", None)
483
- if not provider:
484
- data.pop("entwurfProvider", None)
485
-
486
- settings_path.write_text(json.dumps(data, indent=2) + "\n")
487
- print(f"remove: removed {pkg_removed} packages[] entries, {mcp_removed} mcpServers entries from {settings_path}")
488
- PY
485
+ # Same fail-before-write rule as install: remove has two writers too
486
+ # (packages[] SSOT, then entwurfProvider cleanup), so a malformed provider must
487
+ # not leave a half-removed packages[] entry behind.
488
+ preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$(mktemp -u)"
489
+ # packages[] cleanup via the shared SSOT — same is_entwurf_source predicate as
490
+ # install, so remove never over-deletes a look-alike repo (entwurf-notes, …)
491
+ # that install would never have registered.
492
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR" --remove
493
+ # entwurfProvider.mcpServers.entwurf-bridge cleanup (project scope) via the shared SSOT: strip
494
+ # our-managed shapes (the bare stable bin AND the legacy repo start.sh path — a true user
495
+ # override is left in place) + prune legacy bundles. Mirrors install's ownership predicate so it
496
+ # never over-deletes a look-alike, and now also catches the bare bin the Task-2 install writes.
497
+ python3 "$REPO_DIR/scripts/register-pi-provider.py" remove "$project_dir/.pi/settings.json" "$REPO_DIR" --scope project
498
+ # `remove` is project-scope only. The GLOBAL user-scope citizen in
499
+ # ~/.pi/agent/settings.json (written by install's register_user_scope_citizen)
500
+ # is shared across every project + foreign cwd, so it is left intact here to
501
+ # avoid breaking `--entwurf-control` elsewhere. Point the operator at the
502
+ # explicit global inverse so the install↔remove asymmetry is never silent.
503
+ local agent_settings="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/settings.json"
504
+ if python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_settings" "$REPO_DIR" --remove --dry-run 2>/dev/null | grep -q 'would remove'; then
505
+ log "note: global user-scope citizen still registered in $agent_settings"
506
+ log " run './run.sh remove-user-scope' to remove it (affects ALL cwds)"
507
+ fi
489
508
  }
490
509
 
491
510
  check_model_lock() {
@@ -493,7 +512,7 @@ check_model_lock() {
493
512
  # No pi process, no network, no API cost. Mocks ExtensionAPI/Context and
494
513
  # drives the model_select handler through every quadrant + edge case
495
514
  # (see scripts/check-model-lock.ts header for the full matrix).
496
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-model-lock.ts)
515
+ run_ts scripts/check-model-lock.ts
497
516
  }
498
517
 
499
518
  check_shell_quote() {
@@ -502,7 +521,7 @@ check_shell_quote() {
502
521
  # across the two duplication sites AND behavioral correctness on the
503
522
  # payload classes that caused the 2026-05-18 remote entwurf incident
504
523
  # (backtick / $(...) / $VAR / korean tokens). No process spawn, no SSH.
505
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-shell-quote.ts)
524
+ run_ts scripts/check-shell-quote.ts
506
525
  }
507
526
 
508
527
  check_entwurf_session_identity() {
@@ -512,7 +531,7 @@ check_entwurf_session_identity() {
512
531
  # models, titleSlug canonicalization, registry exact-tuple membership, name=
513
532
  # info-only invariants, and header-scan collision pre-check. Isolates registry
514
533
  # + sessions base to a temp dir. No backend, no API, no spawn.
515
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-session-identity.ts)
534
+ run_ts scripts/check-entwurf-session-identity.ts
516
535
  }
517
536
 
518
537
  check_meta_session() {
@@ -522,7 +541,7 @@ check_meta_session() {
522
541
  # real temp dir), idempotent existence-keyed decideUpsert + identity-drift
523
542
  # refusal, and the pre-drilled read-receipt mutators. Pure functions; no
524
543
  # backend, no hook, no API.
525
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-session.ts)
544
+ run_ts scripts/check-meta-session.ts
526
545
  }
527
546
 
528
547
  check_meta_record_v2() {
@@ -532,7 +551,7 @@ check_meta_record_v2() {
532
551
  # and v2 field-contract crashes. Reader/normalizer only — no v2 writer yet.
533
552
  # Kept separate from check-meta-session so 3D's v1-gate rewrites leave this
534
553
  # back-compat golden untouched. Pure functions; no backend, no hook, no API.
535
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-record-v2.ts)
554
+ run_ts scripts/check-meta-record-v2.ts
536
555
  }
537
556
 
538
557
  check_mailbox_receipt_state() {
@@ -542,7 +561,7 @@ check_mailbox_receipt_state() {
542
561
  # keyset, then the fs store (stamp → persist → read-back) in a temp mailbox
543
562
  # dir. Schema/store only — no live enqueue/read dual-write (that is 3D). No
544
563
  # backend, no hook, no API.
545
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-mailbox-receipt-state.ts)
564
+ run_ts scripts/check-mailbox-receipt-state.ts
546
565
  }
547
566
 
548
567
  check_entwurf_capabilities() {
@@ -553,7 +572,7 @@ check_entwurf_capabilities() {
553
572
  # META_BACKEND_DESCRIPTORS for the three existing backends (drift guard), and
554
573
  # strict keyset/coverage/field crashes. Parser/gate only — no live routing,
555
574
  # no record/descriptor consumer change (that is 3D). No backend, no API.
556
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-capabilities.ts)
575
+ run_ts scripts/check-entwurf-capabilities.ts
557
576
  }
558
577
 
559
578
  check_meta_dual_read() {
@@ -563,7 +582,7 @@ check_meta_dual_read() {
563
582
  # unknown-version crash. Pure functions only — no fs upsert wiring, no
564
583
  # readMetaInbox/enqueueMetaMessage change, no record.delivery removal (3D-2/3/4).
565
584
  # No backend, no hook, no API.
566
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-read.ts)
585
+ run_ts scripts/check-meta-dual-read.ts
567
586
  }
568
587
 
569
588
  check_meta_mailbox_state_write() {
@@ -575,7 +594,7 @@ check_meta_mailbox_state_write() {
575
594
  # carries lastEnqueuedAt/lastReadAt with field isolation, lastDeliveredAt is never
576
595
  # invented, an empty inbox is a no-op on BOTH record and state (⑥), and a state
577
596
  # drift surfaces fail-loud. v2 citizen seeded via upsertMetaSession. No API.
578
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-mailbox-state-write.ts)
597
+ run_ts scripts/check-meta-mailbox-state-write.ts
579
598
  }
580
599
 
581
600
  check_meta_receiver_marker() {
@@ -588,7 +607,7 @@ check_meta_receiver_marker() {
588
607
  # from "no marker"); armProvenance constrained to the arm-capable events so
589
608
  # UserPromptSubmit cannot mint presence; reader does NOT gate on record existence
590
609
  # (recordBacked is the deliverability predicate's fact). Real tmpdir, no API.
591
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-receiver-marker.ts)
610
+ run_ts scripts/check-meta-receiver-marker.ts
592
611
  }
593
612
 
594
613
  check_meta_migration() {
@@ -598,7 +617,7 @@ check_meta_migration() {
598
617
  # state.json). Crash-order: a v1 record's receipts migrate to state BEFORE the v2
599
618
  # rewrite (proven via upsert attach), and a drift'd state makes migrate throw with
600
619
  # the record STILL v1 (recoverable: next attach re-migrates). Temp dir, no API.
601
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-migration.ts)
620
+ run_ts scripts/check-meta-migration.ts
602
621
  }
603
622
 
604
623
  check_meta_dual_consumers() {
@@ -610,7 +629,7 @@ check_meta_dual_consumers() {
610
629
  # authority ambiguity → throw, so the v2 upsert never duplicate-mints) + v1 normalize +
611
630
  # body/filename drift fail-fast. The v1-only raw readers remain for v1-fixture gates.
612
631
  # Temp dir, no API.
613
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-consumers.ts)
632
+ run_ts scripts/check-meta-dual-consumers.ts
614
633
  }
615
634
 
616
635
  check_meta_capability_source() {
@@ -623,7 +642,7 @@ check_meta_capability_source() {
623
642
  # 3 backends). The record.delivery.wakeMode SLOT stays (removal is 3D-4); only the
624
643
  # SOURCE moves. check-entwurf-capabilities still owns the registry ≡ const drift
625
644
  # guard. Pure — no fs writes, no backend, no hook, no API.
626
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-capability-source.ts)
645
+ run_ts scripts/check-meta-capability-source.ts
627
646
  }
628
647
 
629
648
  check_socket_probe() {
@@ -634,7 +653,7 @@ check_socket_probe() {
634
653
  # invariant. Listing lists alive only. Pure classify + GC/listing policy +
635
654
  # a two-socket integration (live listener → alive survives; nonexistent →
636
655
  # dead GC-eligible). No wire timeout fixture, no backend, no API.
637
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-probe.ts)
656
+ run_ts scripts/check-socket-probe.ts
638
657
  }
639
658
 
640
659
  check_project_trust_handler() {
@@ -646,7 +665,7 @@ check_project_trust_handler() {
646
665
  # never undefined. Plus the thin adapter: fake ctx.ui.select, single-writer
647
666
  # (handler never calls store.set — pi persists on remember:true), F5a evidence
648
667
  # in the prompt title. No real UI, no backend, no API.
649
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-project-trust-handler.ts)
668
+ run_ts scripts/check-project-trust-handler.ts
650
669
  }
651
670
 
652
671
  check_entwurf_v2_contract() {
@@ -660,7 +679,7 @@ check_entwurf_v2_contract() {
660
679
  # Q2 owned-outcome+live never auto-sends. R5 taxonomy covers table reasons +
661
680
  # pre-claims bad-target/untrusted-fail-fast/target-locked (bucket B F2). Plus
662
681
  # a schema↔types drift guard on the TypeBox input/receipt. Pure, no API.
663
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-contract.ts)
682
+ run_ts scripts/check-entwurf-v2-contract.ts
664
683
  }
665
684
 
666
685
  check_entwurf_v2_lock() {
@@ -675,7 +694,7 @@ check_entwurf_v2_lock() {
675
694
  # fail-closed to conflict. Empty/corrupt lockfile = conflict, never
676
695
  # auto-deleted. F2-P1: a malformed gid throws before any path is built. Real
677
696
  # temp dir (wx atomicity under test); clock/nonce/pid/host/kill injected.
678
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-lock.ts)
697
+ run_ts scripts/check-entwurf-v2-lock.ts
679
698
  }
680
699
 
681
700
  check_entwurf_v2_decider() {
@@ -689,7 +708,7 @@ check_entwurf_v2_decider() {
689
708
  # takes NO lock (?7); resume plan has no mode/wantsReply/provider/model but has
690
709
  # expectedSocketPath/observeTimeoutMs/releaseWhen; an invalid gid throws before
691
710
  # any lookup (F2-P1). Pure, no IO, no API.
692
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-decider.ts)
711
+ run_ts scripts/check-entwurf-v2-decider.ts
693
712
  }
694
713
 
695
714
  check_entwurf_v2_matrix() {
@@ -703,7 +722,7 @@ check_entwurf_v2_matrix() {
703
722
  # FAILS if any transport / lock class / pre-probe reject is missing — a dropped
704
723
  # decider cell cannot pass silently. Thin coverage, NOT a decider re-impl; surface
705
724
  # parity stays in check-entwurf-v2-surface. Pure, no IO, no API.
706
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-matrix.ts)
725
+ run_ts scripts/check-entwurf-v2-matrix.ts
707
726
  }
708
727
 
709
728
  check_entwurf_v2_release() {
@@ -715,7 +734,7 @@ check_entwurf_v2_release() {
715
734
  # release on the FIRST observed transition (socket-alive ∨ child-exited any code)
716
735
  # or a failed start; socket↔exit race idempotent (single release either order);
717
736
  # decideReleasePolicy enforces the lock-nullness invariant (?7). Pure, no IO.
718
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-release.ts)
737
+ run_ts scripts/check-entwurf-v2-release.ts
719
738
  }
720
739
 
721
740
  check_entwurf_v2_send() {
@@ -727,7 +746,7 @@ check_entwurf_v2_send() {
727
746
  # indeterminate->failed+rethrow with deadFallback+mailbox NEVER called (no
728
747
  # double-delivery on an alive-but-stalled socket). Release fires exactly once per
729
748
  # send-final; a releaseLock throw never masks the send failure (5b). IO-via-dep.
730
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send.ts)
749
+ run_ts scripts/check-entwurf-v2-send.ts
731
750
  }
732
751
 
733
752
  check_entwurf_v2_send_fallback() {
@@ -740,7 +759,7 @@ check_entwurf_v2_send_fallback() {
740
759
  # / bad-target + address-conflict->reject pre-probe. Mis-wire (plan/lock gid) fails loud
741
760
  # before IO; inspect/probe throws PROPAGATE (the hand owns failed+release); the resolver
742
761
  # has NO release seam; every execute plan keeps the held gid and is never spawn-bg.
743
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send-fallback.ts)
762
+ run_ts scripts/check-entwurf-v2-send-fallback.ts
744
763
  }
745
764
 
746
765
  check_entwurf_v2_mailbox() {
@@ -757,7 +776,19 @@ check_entwurf_v2_mailbox() {
757
776
  # every access throws still resolves). Source guard: the lib code has NO release seam
758
777
  # and NO routing seam (no releaseLock / inspect / probe / resolve) — a lock leak or
759
778
  # re-route is structurally impossible.
760
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-mailbox.ts)
779
+ run_ts scripts/check-entwurf-v2-mailbox.ts
780
+ }
781
+
782
+ check_entwurf_v2_native_push() {
783
+ # Deterministic gate for 봉인 3/4: the native-push SEND hand (deliverViaNativePush +
784
+ # makeNativePushSend), the executor half of the native-push rail — where the 1-shot retry
785
+ # lives (moved out of the adapter leaf). Proves over a fake adapter (no agy/socket): success
786
+ # first try -> {retried:false}, ONE send over the planted route, ZERO re-probe; fail ->
787
+ # re-probe alive -> re-send success -> {retried:true}, TWO sends, the 2nd over the RE-
788
+ # DISCOVERED route; re-send FAIL -> throws (no 3rd attempt); re-probe dead/indeterminate ->
789
+ # throws (not retried), NO second send. makeNativePushSend resolves the adapter from
790
+ # plan.backend and IGNORES the lock (lock-free rail).
791
+ run_ts scripts/check-entwurf-v2-native-push.ts
761
792
  }
762
793
 
763
794
  check_entwurf_v2_runner() {
@@ -771,7 +802,7 @@ check_entwurf_v2_runner() {
771
802
  # SendDeliveredReleaseFailedError -> execution-failed{finalizedOutcome, releaseFailed,
772
803
  # retrySafe:false}; a plain hand throw -> execution-failed{retrySafe:false} with no
773
804
  # finalizedOutcome. Exactly one hand runs per execute.
774
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-runner.ts)
805
+ run_ts scripts/check-entwurf-v2-runner.ts
775
806
  }
776
807
 
777
808
  check_entwurf_v2_surface() {
@@ -782,7 +813,7 @@ check_entwurf_v2_surface() {
782
813
  # lock-retained, N1 delivered-but-dirty) / surface ctx-free source guard / entwurf-control
783
814
  # registers entwurf_v2 + reaches the fence via a NON-LITERAL dynamic import (no static fence
784
815
  # import → TS5097 stays closed) + decorates sender origin:pi-session/replyable:true.
785
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-surface.ts)
816
+ run_ts scripts/check-entwurf-v2-surface.ts
786
817
  }
787
818
 
788
819
  check_entwurf_bridge_boot() {
@@ -793,7 +824,7 @@ check_entwurf_bridge_boot() {
793
824
  # registered on the runtime surface with its schema (G1b). tools/list only → no tools/call,
794
825
  # no lock/fs side effect, no auth → safe in pnpm check. Broad protocol/negative suite stays
795
826
  # in check-bridge/test.sh (D1=A안).
796
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-boot.ts)
827
+ run_ts scripts/check-entwurf-bridge-boot.ts
797
828
  }
798
829
 
799
830
  check_entwurf_bridge_pi_free() {
@@ -804,7 +835,7 @@ check_entwurf_bridge_pi_free() {
804
835
  # value-imports @earendil-works/pi-*. Type-only imports and dynamic `await import()`
805
836
  # (the intended lazy preflight boundary) are excluded — the runtime boot smoke is the
806
837
  # final authority that peers/self/list/mailbox-deliver come up pi-free.
807
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-pi-free.ts)
838
+ run_ts scripts/check-entwurf-bridge-pi-free.ts
808
839
  }
809
840
 
810
841
  check_entwurf_v2_production() {
@@ -815,7 +846,7 @@ check_entwurf_v2_production() {
815
846
  # lockDir / QB3 the spawn watcher releases via the SHARED lockDir release (not the spawn
816
847
  # factory default) / the mailbox hand enqueues onto the wired dirs / a dead control send
817
848
  # re-resolves to the SAME sendViaMailbox instance on the SAME dirs (Q3+Q5 no drift).
818
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-production.ts)
849
+ run_ts scripts/check-entwurf-v2-production.ts
819
850
  }
820
851
 
821
852
  check_entwurf_control_rpc() {
@@ -826,7 +857,7 @@ check_entwurf_control_rpc() {
826
857
  # sendRpcCommand from the lib and no longer defines its own / real short unix-socket round-trip
827
858
  # (write command -> matched {type:response,command,success:true} -> resolve) / close-before-
828
859
  # response rejects 'connection closed before response'. net.Server only, no model/pi process.
829
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-control-rpc.ts)
860
+ run_ts scripts/check-entwurf-control-rpc.ts
830
861
  }
831
862
 
832
863
  check_entwurf_v2_spawn() {
@@ -840,7 +871,7 @@ check_entwurf_v2_spawn() {
840
871
  # obtainable (grace elapses, or a post-spawn watch dep throws and the exit can't be
841
872
  # observed) -> lock-retained fail-closed (released:false, pid/socket/lockPath surfaced) —
842
873
  # there is NO direct-release hatch; deps.releaseLock is reached ONLY via reduceRelease.
843
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn.ts)
874
+ run_ts scripts/check-entwurf-v2-spawn.ts
844
875
  }
845
876
 
846
877
  check_entwurf_resume_args() {
@@ -853,7 +884,7 @@ check_entwurf_resume_args() {
853
884
  # and the prompt-as-turn positional (`-p` NOT dropped in v2); explicitExtensionArgs
854
885
  # preserved exactly once (provider-resolution footgun #29); v2 includes plan.launchArgs
855
886
  # (--approve) before the prompt; null provider emits no --provider; no cross-contamination.
856
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-resume-args.ts)
887
+ run_ts scripts/check-entwurf-resume-args.ts
857
888
  }
858
889
 
859
890
  check_entwurf_v2_spawn_production() {
@@ -867,7 +898,7 @@ check_entwurf_v2_spawn_production() {
867
898
  # (symlink) rejects without connecting, dead→wait→alive, abort clears the sleep. awaitChildExit
868
899
  # resolves the code + removes the listener on abort. awaitTimeout schedules + abort-clears.
869
900
  # killChild=SIGTERM; releaseLock delegates; a proc-less child fails loud (mis-wire).
870
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn-production.ts)
901
+ run_ts scripts/check-entwurf-v2-spawn-production.ts
871
902
  }
872
903
 
873
904
 
@@ -883,7 +914,7 @@ smoke_entwurf_v2_spawn_live() {
883
914
  echo "[smoke-entwurf-v2-spawn-live] skipped — set LIVE=1 to run (spawns real children + opens a real unix socket)."
884
915
  return 0
885
916
  fi
886
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-spawn-live.ts)
917
+ run_ts scripts/smoke-entwurf-v2-spawn-live.ts
887
918
  }
888
919
 
889
920
  smoke_acp_socket_citizen_live() {
@@ -896,7 +927,7 @@ smoke_acp_socket_citizen_live() {
896
927
  # citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
897
928
  # Model override: ENTWURF_S1_MODEL (default claude-opus-4-8).
898
929
  # LIVE=1 ./run.sh smoke-acp-socket-citizen-live
899
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-socket-citizen-live.ts)
930
+ run_ts scripts/smoke-acp-socket-citizen-live.ts
900
931
  }
901
932
 
902
933
  smoke_acp_raw_turn_live() {
@@ -910,7 +941,7 @@ smoke_acp_raw_turn_live() {
910
941
  # fails acceptance unless ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK=1, debug).
911
942
  # Model override: ENTWURF_ACP_RAW_TURN_MODEL (default claude-sonnet-5).
912
943
  # LIVE=1 ./run.sh smoke-acp-raw-turn-live
913
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-raw-turn-live.ts)
944
+ run_ts scripts/smoke-acp-raw-turn-live.ts
914
945
  }
915
946
 
916
947
  smoke_acp_overlay_live() {
@@ -928,7 +959,7 @@ smoke_acp_overlay_live() {
928
959
  # fallback fails acceptance unless ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK=1).
929
960
  # Model override: ENTWURF_ACP_OVERLAY_MODEL (default claude-sonnet-5).
930
961
  # LIVE=1 ./run.sh smoke-acp-overlay-live
931
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-overlay-live.ts)
962
+ run_ts scripts/smoke-acp-overlay-live.ts
932
963
  }
933
964
 
934
965
  smoke_acp_memory_containment_live() {
@@ -943,7 +974,7 @@ smoke_acp_memory_containment_live() {
943
974
  # fallback fails acceptance unless ENTWURF_ACP_MEMORY_ALLOW_PATH_FALLBACK=1).
944
975
  # Model override: ENTWURF_ACP_MEMORY_MODEL (default claude-sonnet-5).
945
976
  # LIVE=1 ./run.sh smoke-acp-memory-containment-live
946
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-memory-containment-live.ts)
977
+ run_ts scripts/smoke-acp-memory-containment-live.ts
947
978
  }
948
979
 
949
980
  smoke_acp_provider_live() {
@@ -958,7 +989,7 @@ smoke_acp_provider_live() {
958
989
  # event-mapper gate owns the tool→notice contract.
959
990
  # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
960
991
  # LIVE=1 ./run.sh smoke-acp-provider-live
961
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-provider-live.ts)
992
+ run_ts scripts/smoke-acp-provider-live.ts
962
993
  }
963
994
 
964
995
  smoke_acp_session_reuse_live() {
@@ -972,7 +1003,7 @@ smoke_acp_session_reuse_live() {
972
1003
  # smoke-acp-provider-live.
973
1004
  # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
974
1005
  # LIVE=1 ./run.sh smoke-acp-session-reuse-live
975
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-session-reuse-live.ts)
1006
+ run_ts scripts/smoke-acp-session-reuse-live.ts
976
1007
  }
977
1008
 
978
1009
  smoke_acp_carrier_augment_live() {
@@ -984,7 +1015,7 @@ smoke_acp_carrier_augment_live() {
984
1015
  # check via SMOKE_ACP_CARRIER_PRESENT=1 (non-blocking).
985
1016
  # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
986
1017
  # LIVE=1 ./run.sh smoke-acp-carrier-augment-live
987
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-carrier-augment-live.ts)
1018
+ run_ts scripts/smoke-acp-carrier-augment-live.ts
988
1019
  }
989
1020
 
990
1021
  smoke_acp_mcp_live() {
@@ -997,7 +1028,7 @@ smoke_acp_mcp_live() {
997
1028
  # fix). Isolated probe (not entwurf-bridge) so a failure does not blur into
998
1029
  # identity/env wiring. Model override: ENTWURF_ACP_PROVIDER_MODEL.
999
1030
  # LIVE=1 ./run.sh smoke-acp-mcp-live
1000
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-mcp-live.ts)
1031
+ run_ts scripts/smoke-acp-mcp-live.ts
1001
1032
  }
1002
1033
 
1003
1034
  smoke_acp_skill_live() {
@@ -1009,7 +1040,7 @@ smoke_acp_skill_live() {
1009
1040
  # the Skill/Skill(*) auto-add reach the live session (the other half of the GLG
1010
1041
  # baseline). Model override: ENTWURF_ACP_PROVIDER_MODEL.
1011
1042
  # LIVE=1 ./run.sh smoke-acp-skill-live
1012
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-skill-live.ts)
1043
+ run_ts scripts/smoke-acp-skill-live.ts
1013
1044
  }
1014
1045
 
1015
1046
  smoke_acp_bundled_mcp_live() {
@@ -1025,7 +1056,7 @@ smoke_acp_bundled_mcp_live() {
1025
1056
  # diagnostic backlog, not the 0.11.0 release circuit). Model override:
1026
1057
  # ENTWURF_ACP_PROVIDER_MODEL.
1027
1058
  # LIVE=1 ./run.sh smoke-acp-bundled-mcp-live
1028
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-bundled-mcp-live.ts)
1059
+ run_ts scripts/smoke-acp-bundled-mcp-live.ts
1029
1060
  }
1030
1061
 
1031
1062
  smoke_acp_rgg_live() {
@@ -1062,7 +1093,7 @@ smoke_entwurf_v2_matrix_live() {
1062
1093
  echo "[smoke-entwurf-v2-matrix-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket)."
1063
1094
  return 0
1064
1095
  fi
1065
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-matrix-live.ts)
1096
+ run_ts scripts/smoke-entwurf-v2-matrix-live.ts
1066
1097
  }
1067
1098
 
1068
1099
  smoke_entwurf_v2_spawn_resume_live() {
@@ -1084,7 +1115,7 @@ smoke_entwurf_v2_spawn_resume_live() {
1084
1115
  echo "[smoke-entwurf-v2-spawn-resume-live] skipped — set LIVE=1 to run (spawns a real pi resume child + opens a real socket)."
1085
1116
  return 0
1086
1117
  fi
1087
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-spawn-resume-live.ts)
1118
+ run_ts scripts/smoke-entwurf-v2-spawn-resume-live.ts
1088
1119
  }
1089
1120
 
1090
1121
  check_entwurf_facts() {
@@ -1096,7 +1127,7 @@ check_entwurf_facts() {
1096
1127
  # (no proof ≠ dead). facts-only keyset: identity facts + liveness, NO
1097
1128
  # verb-routing (resumable/sendable/transport/dispatch/action) and NO
1098
1129
  # transcriptPath (동결결정 10). Pure, no IO, no API.
1099
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-facts.ts)
1130
+ run_ts scripts/check-entwurf-facts.ts
1100
1131
  }
1101
1132
 
1102
1133
  check_socket_discovery() {
@@ -1108,7 +1139,7 @@ check_socket_discovery() {
1108
1139
  # indeterminate (F3), never folded to dead by an alive-only listing. Dir
1109
1140
  # hygiene (non-.sock / malformed names ignored), dedup, missing-dir, sort, and
1110
1141
  # an end-to-end scanSocketProbes→resolveFactList. readdir/probe injected, no IO.
1111
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-discovery.ts)
1142
+ run_ts scripts/check-socket-discovery.ts
1112
1143
  }
1113
1144
 
1114
1145
  check_meta_listing() {
@@ -1119,7 +1150,7 @@ check_meta_listing() {
1119
1150
  # becomes an explicit error carrying ONLY {filename, message}, verbatim (a
1120
1151
  # salvaged gid string as a fact = synthetic backdoor). mode strict throws on
1121
1152
  # any error, collect returns partial. entries/readRecord injected, no IO.
1122
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-listing.ts)
1153
+ run_ts scripts/check-meta-listing.ts
1123
1154
  }
1124
1155
 
1125
1156
  check_entwurf_fact_provider() {
@@ -1131,7 +1162,7 @@ check_entwurf_fact_provider() {
1131
1162
  # survives; impossible wiring invariant (resolveFactList duplicate/unprobed) →
1132
1163
  # throw, never swallowed. A collision quarantines BOTH the PeerFact and the
1133
1164
  # socket (gid is the universal address). meta + socket deps injected, no IO.
1134
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-fact-provider.ts)
1165
+ run_ts scripts/check-entwurf-fact-provider.ts
1135
1166
  }
1136
1167
 
1137
1168
  check_entwurf_peers_surface() {
@@ -1145,7 +1176,7 @@ check_entwurf_peers_surface() {
1145
1176
  # diagnostics in both surfaces; empty → "(none)"; unsupported shown; enrich null
1146
1177
  # → "(not enriched)". WIRING guard: bridge calls listEntwurfFacts+renderEntwurfPeers,
1147
1178
  # getLiveSessions gone. Facts fabricated, no IO (only static source read).
1148
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-peers-surface.ts)
1179
+ run_ts scripts/check-entwurf-peers-surface.ts
1149
1180
  }
1150
1181
 
1151
1182
 
@@ -1162,7 +1193,7 @@ check_entwurf_self_address() {
1162
1193
  # probes the socket, and entwurf_self renders alive vs expected (no path lie).
1163
1194
  # Slice boundary: meta watchArmed is wired from the slice-2 presence marker; do NOT
1164
1195
  # claim slice 1 green standalone (1+2 close in the same release block).
1165
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-self-address.ts)
1196
+ run_ts scripts/check-entwurf-self-address.ts
1166
1197
  }
1167
1198
 
1168
1199
  check_entwurf_deliverability() {
@@ -1174,7 +1205,7 @@ check_entwurf_deliverability() {
1174
1205
  # (pi) refused even when active (SE-1, no mailbox drain), self-fetch + dead-owner/unarmed
1175
1206
  # refused (SE-2, would rot); WIRING that the self-addressability predicate shares the
1176
1207
  # SAME active-receiver atom (one source of truth). Pure, no IO.
1177
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-deliverability.ts)
1208
+ run_ts scripts/check-entwurf-deliverability.ts
1178
1209
  }
1179
1210
 
1180
1211
  check_entwurf_mailbox_guard() {
@@ -1185,7 +1216,47 @@ check_entwurf_mailbox_guard() {
1185
1216
  # one calls it exactly once; TMPDIR SNAPSHOT with the real enqueueMetaMessage — a refused
1186
1217
  # send leaves the mailbox tree byte-identical (file list + content hash, not just mtime),
1187
1218
  # an accepted send writes exactly one .msg; plus fact gathering from record/capability/marker.
1188
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-mailbox-guard.ts)
1219
+ run_ts scripts/check-entwurf-mailbox-guard.ts
1220
+ }
1221
+
1222
+ check_native_push_adapter() {
1223
+ # Deterministic gate for the native-push adapter LEAF (봉인 3/8). Drives
1224
+ # createAntigravityAdapter with a FAKE runner (no real agy/ss/pgrep). Asserts: FULL pid
1225
+ # scan (only the 2nd host pid serves the conv → probe still finds the route; raw-agy-send
1226
+ # head -1 corrected); dead (no host) vs indeterminate (host alive, no LS port served the
1227
+ # conv, never coerced to dead); VOLATILE route / no cache (a repeated probe re-discovers a
1228
+ # CHANGED route); send argv === [binary,agentapi,send-message,conv,body] with
1229
+ # ANTIGRAVITY_LS_ADDRESS env, non-zero exit THROWS; NO retry in the adapter (single send,
1230
+ # no re-probe — retry is the executor hand's job, step ⑥); resolveNativePushAdapter fail-fast.
1231
+ run_ts scripts/check-native-push-adapter.ts
1232
+ }
1233
+
1234
+ check_native_push_register() {
1235
+ # Deterministic gate for 봉인 5: registerNativeConversation (the core of the
1236
+ # entwurf_register_native MCP tool). Drives it with a FAKE adapter + an ISOLATED mkdtemp
1237
+ # store (never the real ~/.pi). Asserts: live probe -> CREATE (record carries backend/
1238
+ # nativeSessionId/caller-cwd); re-register -> ATTACH (SAME garden id, cwd refreshed, ONE
1239
+ # record, no duplicate mint); dead/indeterminate probe -> REFUSE (throws, NO record written);
1240
+ # RECEIVER-MARKER ABSTINENCE (보정①) — the register source references no receiver-marker
1241
+ # writer (writeMetaReceiverMarker / armProvenance / META_RECEIVER_ARM_PROVENANCES).
1242
+ run_ts scripts/check-native-push-register.ts
1243
+ }
1244
+
1245
+ check_agy_sender_identity() {
1246
+ # Deterministic gate for the #46 sender-identity lane — WHO is calling the bridge.
1247
+ # A birthed agy conversation could already CALL entwurf_v2 for real, yet its message
1248
+ # landed as external-mcp/unknown-host (non-replyable): the hook wrote only the
1249
+ # meta-record, and the bridge's resolver looked markers up under `claude-code` alone.
1250
+ # Behavioral, not source-regex: the real hook runs as a child process (so the marker's
1251
+ # ownerPid is the gate's own pid — the same parent-pid join production performs), and
1252
+ # the resolver runs against isolated marker/record stores.
1253
+ # Rows: hook writes an antigravity marker keyed by its PARENT pid; an upsert failure
1254
+ # writes NO marker (record authority first); resolver 0→null, 1→identity on EITHER
1255
+ # backend, no-record/drifted marker→null, two distinct live identities on one owner pid
1256
+ # →THROW (never guess, never downgrade to anonymous), two markers naming the SAME
1257
+ # identity→not a conflict; antigravity is native-push so its replyable comes from the
1258
+ # adapter probe, never from a mailbox watch it can never arm (보정①).
1259
+ run_ts scripts/check-agy-sender-identity.ts
1189
1260
  }
1190
1261
 
1191
1262
 
@@ -1200,7 +1271,7 @@ check_package_source_routing() {
1200
1271
  # plus self-root fallback and the resume unresolvedAcpIntent signal. Isolated
1201
1272
  # via a temp PI_CODING_AGENT_DIR — the real ~/.pi/agent is never touched. No
1202
1273
  # backend, no spawn, no API cost.
1203
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-package-source-routing.ts)
1274
+ run_ts scripts/check-package-source-routing.ts
1204
1275
  }
1205
1276
 
1206
1277
 
@@ -1216,7 +1287,7 @@ smoke_session_id_name() {
1216
1287
  fail "[smoke-session-id-name] pi binary not on PATH — cannot run live substrate proof"
1217
1288
  return 1
1218
1289
  fi
1219
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-session-id-name.ts) || {
1290
+ run_ts scripts/smoke-session-id-name.ts || {
1220
1291
  fail "[smoke-session-id-name] live substrate smoke failed"
1221
1292
  return 1
1222
1293
  }
@@ -1469,6 +1540,87 @@ check_install_preflight() {
1469
1540
  fi
1470
1541
  rm -rf "$fake" "$proj"
1471
1542
  ok "[check-install-preflight] representative dangling symlink (test -d blind spot) → fails before writing settings, names the dep"
1543
+
1544
+ # negative 3 — corrupt project provider shape. install_local_package now has a
1545
+ # two-step writer (packages[] SSOT, then entwurfProvider.mcpServers), so a
1546
+ # malformed provider MUST fail before the packages[] step writes anything.
1547
+ fake=$(mktemp -d); proj=$(mktemp -d)
1548
+ mkdir -p "$proj/.pi" "$fake/home"
1549
+ printf '{"entwurfProvider": []}\n' > "$proj/.pi/settings.json"
1550
+ local before after
1551
+ before=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1552
+ rc=0; out=$(HOME="$fake/home" PI_CODING_AGENT_DIR="$fake/home/.pi/agent" "$REPO_DIR/run.sh" install "$proj" 2>&1) || rc=$?
1553
+ after=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1554
+ if [ "$rc" -eq 0 ]; then
1555
+ fail "[check-install-preflight] corrupt entwurfProvider did NOT fail install"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1556
+ fi
1557
+ if [ "$before" != "$after" ]; then
1558
+ fail "[check-install-preflight] corrupt entwurfProvider was partially rewritten (packages[] leak)"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$fake" "$proj"; exit 1
1559
+ fi
1560
+ if ! printf '%s' "$out" | grep -q "entwurfProvider is not an object"; then
1561
+ fail "[check-install-preflight] corrupt entwurfProvider failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1562
+ fi
1563
+ if [ -f "$fake/home/.pi/agent/settings.json" ]; then
1564
+ fail "[check-install-preflight] corrupt project provider still wrote user-scope settings"; cat "$fake/home/.pi/agent/settings.json"; rm -rf "$fake" "$proj"; exit 1
1565
+ fi
1566
+ rm -rf "$fake" "$proj"
1567
+ ok "[check-install-preflight] corrupt project entwurfProvider → fails before any project/user settings write"
1568
+
1569
+ # negative 4 — corrupt project mcpServers shape, same no-partial-write contract.
1570
+ fake=$(mktemp -d); proj=$(mktemp -d)
1571
+ mkdir -p "$proj/.pi" "$fake/home"
1572
+ printf '{"entwurfProvider": {"mcpServers": []}}\n' > "$proj/.pi/settings.json"
1573
+ before=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1574
+ rc=0; out=$(HOME="$fake/home" PI_CODING_AGENT_DIR="$fake/home/.pi/agent" "$REPO_DIR/run.sh" install "$proj" 2>&1) || rc=$?
1575
+ after=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1576
+ if [ "$rc" -eq 0 ]; then
1577
+ fail "[check-install-preflight] corrupt entwurfProvider.mcpServers did NOT fail install"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1578
+ fi
1579
+ if [ "$before" != "$after" ]; then
1580
+ fail "[check-install-preflight] corrupt mcpServers was partially rewritten (packages[] leak)"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$fake" "$proj"; exit 1
1581
+ fi
1582
+ if ! printf '%s' "$out" | grep -q "entwurfProvider.mcpServers is not an object"; then
1583
+ fail "[check-install-preflight] corrupt mcpServers failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1584
+ fi
1585
+ rm -rf "$fake" "$proj"
1586
+ ok "[check-install-preflight] corrupt project entwurfProvider.mcpServers → fails before any project settings write"
1587
+
1588
+ # negative 5 — corrupt user-scope packages shape. Since install writes project
1589
+ # settings before user registration, this preflight must catch the user file up
1590
+ # front so a bad ~/.pi/agent/settings.json cannot leave the project half-wired.
1591
+ fake=$(mktemp -d); proj=$(mktemp -d)
1592
+ mkdir -p "$fake/home/.pi/agent"
1593
+ printf '{"packages": {"broken": true}}\n' > "$fake/home/.pi/agent/settings.json"
1594
+ rc=0; out=$(HOME="$fake/home" PI_CODING_AGENT_DIR="$fake/home/.pi/agent" "$REPO_DIR/run.sh" install "$proj" 2>&1) || rc=$?
1595
+ if [ "$rc" -eq 0 ]; then
1596
+ fail "[check-install-preflight] corrupt user packages did NOT fail install"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1597
+ fi
1598
+ if [ -f "$proj/.pi/settings.json" ]; then
1599
+ fail "[check-install-preflight] corrupt user packages still wrote project settings (partial install)"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$fake" "$proj"; exit 1
1600
+ fi
1601
+ if ! printf '%s' "$out" | grep -q "user settings packages is not a JSON array"; then
1602
+ fail "[check-install-preflight] corrupt user packages failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1603
+ fi
1604
+ rm -rf "$fake" "$proj"
1605
+ ok "[check-install-preflight] corrupt user-scope packages → fails before project settings write"
1606
+
1607
+ # negative 6 — remove has the same two-step write risk (packages[] remove,
1608
+ # then provider cleanup). A corrupt provider must fail before packages[] is
1609
+ # altered, otherwise uninstall becomes a partial destructive write.
1610
+ proj=$(mktemp -d)
1611
+ mkdir -p "$proj/.pi"
1612
+ printf '{"entwurfProvider": [], "packages": ["%s"]}\n' "$REPO_DIR" > "$proj/.pi/settings.json"
1613
+ before=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1614
+ rc=0; out=$("$REPO_DIR/run.sh" remove "$proj" 2>&1) || rc=$?
1615
+ after=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1616
+ if [ "$rc" -eq 0 ]; then
1617
+ fail "[check-install-preflight] corrupt provider did NOT fail remove"; echo "$out"; rm -rf "$proj"; exit 1
1618
+ fi
1619
+ if [ "$before" != "$after" ]; then
1620
+ fail "[check-install-preflight] corrupt provider remove partially rewrote packages[]"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$proj"; exit 1
1621
+ fi
1622
+ rm -rf "$proj"
1623
+ ok "[check-install-preflight] remove with corrupt provider → fails before packages[] removal"
1472
1624
  }
1473
1625
 
1474
1626
  check_pi_preflight() {
@@ -1476,7 +1628,7 @@ check_pi_preflight() {
1476
1628
  # decision 8 precedence (saved false > saved true > prefix > no-inputs >
1477
1629
  # fail-fast) and decision 7's separator-boundary prefix against pi's own
1478
1630
  # ProjectTrustStore in a temp agentDir. Deterministic, no network/backend.
1479
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-pi-preflight.ts)
1631
+ run_ts scripts/check-pi-preflight.ts
1480
1632
  }
1481
1633
 
1482
1634
 
@@ -1531,7 +1683,7 @@ check_acp_provider_surface() {
1531
1683
  # sentinel shape, and a FAIL-LOUD streamSimple (calling it throws — no native
1532
1684
  # fallback, no empty-but-successful stream). Pure, no pi runtime, no API.
1533
1685
  section "ACP provider surface (S0 loader/fence)"
1534
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-provider-surface.ts)
1686
+ run_ts scripts/check-acp-provider-surface.ts
1535
1687
  }
1536
1688
 
1537
1689
  check_acp_sdk_surface() {
@@ -1543,7 +1695,7 @@ check_acp_sdk_surface() {
1543
1695
  # turn needs (silent-rename gate), and forbids any source-level anthropic SDK
1544
1696
  # import / API-client use (the anthropic dep is a peer-pin ONLY).
1545
1697
  section "ACP SDK surface (S2a dep pin + peer-resolution + no-client-use)"
1546
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-sdk-surface.ts)
1698
+ run_ts scripts/check-acp-sdk-surface.ts
1547
1699
  }
1548
1700
 
1549
1701
  check_acp_overlay() {
@@ -1556,7 +1708,7 @@ check_acp_overlay() {
1556
1708
  # stale symlinks cleaned; binary-owned files preserved; CLAUDE_CONFIG_DIR
1557
1709
  # launch-env planted; idempotent. Pure, no live model.
1558
1710
  section "ACP overlay (S2b claude-config-overlay)"
1559
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-overlay.ts)
1711
+ run_ts scripts/check-acp-overlay.ts
1560
1712
  }
1561
1713
 
1562
1714
  check_acp_tool_surface() {
@@ -1567,7 +1719,7 @@ check_acp_tool_surface() {
1567
1719
  # extraArgs/plugins) + the S2b billing-carrier guard (no _meta.systemPrompt
1568
1720
  # unless a caller supplies one). Pure preflight — NOT a backend wire read.
1569
1721
  section "ACP tool surface (S2b exclude-tools preflight + session meta)"
1570
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-tool-surface.ts)
1722
+ run_ts scripts/check-acp-tool-surface.ts
1571
1723
  }
1572
1724
 
1573
1725
  check_acp_event_mapper() {
@@ -1579,7 +1731,7 @@ check_acp_event_mapper() {
1579
1731
  # context→ACP-prompt transcript passthrough (excludes systemPrompt/thinking,
1580
1732
  # single text block). Pure, no live backend.
1581
1733
  section "ACP event mapper (S2c notification→stream + context)"
1582
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-event-mapper.ts)
1734
+ run_ts scripts/check-acp-event-mapper.ts
1583
1735
  }
1584
1736
 
1585
1737
  check_acp_prompt_builder() {
@@ -1590,7 +1742,7 @@ check_acp_prompt_builder() {
1590
1742
  # excluded so a reuse session is not re-injected its own history). Pure, no
1591
1743
  # session store yet — locks the builder before S2d wires the reuse paths.
1592
1744
  section "ACP prompt builder (S2d bootstrapPath prompt scope)"
1593
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-prompt-builder.ts)
1745
+ run_ts scripts/check-acp-prompt-builder.ts
1594
1746
  }
1595
1747
 
1596
1748
  check_acp_config() {
@@ -1603,7 +1755,7 @@ check_acp_config() {
1603
1755
  # headers, and envelope enrich (PI_SESSION_ID/PI_AGENT_ID into entwurf-bridge
1604
1756
  # only, stale filtered, post-hash). Pure + temp-dir settings I/O, no child/spawn.
1605
1757
  section "ACP provider config (S2g operator mcpServers/skillPlugins/tools passthrough)"
1606
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-config.ts)
1758
+ run_ts scripts/check-acp-config.ts
1607
1759
  }
1608
1760
 
1609
1761
  check_acp_session_store() {
@@ -1614,7 +1766,7 @@ check_acp_session_store() {
1614
1766
  # (turn-scoped/-p one-shot is ALWAYS new — no in-memory reuse, no persisted
1615
1767
  # resume/load in the first cut). Pure + temp-dir record I/O, no child/spawn.
1616
1768
  section "ACP session store (S2d-1b-1 signature/compat/bootstrap decision)"
1617
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-store.ts)
1769
+ run_ts scripts/check-acp-session-store.ts
1618
1770
  }
1619
1771
 
1620
1772
  check_acp_backend_preflight() {
@@ -1625,7 +1777,7 @@ check_acp_backend_preflight() {
1625
1777
  # assertExcludeToolsHonored is wired into the live provider path, not just the
1626
1778
  # pure gate. No backend launched (preflight throws first). Pure.
1627
1779
  section "ACP backend preflight (S2c runtime exclude-tools wiring)"
1628
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-backend-preflight.ts)
1780
+ run_ts scripts/check-acp-backend-preflight.ts
1629
1781
  }
1630
1782
 
1631
1783
  check_acp_session_reuse() {
@@ -1638,7 +1790,7 @@ check_acp_session_reuse() {
1638
1790
  # never torn down between turns, and source-locks buildAcpPrompt wiring +
1639
1791
  # single-site applyAcpSessionUpdate via the router. No real child launched.
1640
1792
  section "ACP session reuse (S2d-1b-2b delta-only capture + mutable routing)"
1641
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-reuse.ts)
1793
+ run_ts scripts/check-acp-session-reuse.ts
1642
1794
  }
1643
1795
 
1644
1796
  check_acp_carrier_augment() {
@@ -1650,7 +1802,7 @@ check_acp_carrier_augment() {
1650
1802
  # so it never enters contextMessageSignatures — with entwurf cwd/AGENTS.md
1651
1803
  # de-dup. Pure + temp-dir fs, no spawn.
1652
1804
  section "ACP carrier + augment (S2d-1c engraving + first-user augment)"
1653
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-carrier-augment.ts)
1805
+ run_ts scripts/check-acp-carrier-augment.ts
1654
1806
  }
1655
1807
 
1656
1808
  check_pack() {
@@ -1676,7 +1828,11 @@ check_pack() {
1676
1828
  # npm's own lifecycle banner stay off stdout; otherwise they pollute the --json
1677
1829
  # payload this parses. prepack runs on dry-run too, which is how dist lands in
1678
1830
  # this gate's file list.
1679
- json=$(cd "$REPO_DIR" && npm pack --dry-run --json --silent 2>/dev/null) || {
1831
+ # with-dist-lock wraps the WHOLE pack (prepack build-bridge emit + npm's
1832
+ # post-build dist read) so a concurrent pack/build can't `rm -rf dist` mid-read
1833
+ # (the 2026-07-03 phantom "dist missing" race). The nested prepack build-bridge
1834
+ # is reentrant via ENTWURF_BUILD_LOCK_HELD.
1835
+ json=$(cd "$REPO_DIR" && bash scripts/with-dist-lock.sh npm pack --dry-run --json --silent 2>/dev/null) || {
1680
1836
  fail "[check-pack] npm pack --dry-run failed"
1681
1837
  return 1
1682
1838
  }
@@ -1735,6 +1891,14 @@ check_pack() {
1735
1891
  # doctor.sh runs this prebuilt JS when installed under node_modules (strip-types
1736
1892
  # refuses the .ts there), same boundary start.sh crosses. build-bridge emits it.
1737
1893
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js"
1894
+ # 0.12.7 — node_modules-safe agy PreInvocation hook. The stable npm bin
1895
+ # dispatches here because Node refuses raw .ts below node_modules.
1896
+ "mcp/entwurf-bridge/dist/scripts/agy-imprint.js"
1897
+ # 0.12.7 — the three OPERATOR commands run.sh dispatches. Installed, REPO_DIR is
1898
+ # under node_modules, so run_ts must find a compiled twin or the command is dead.
1899
+ "mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js"
1900
+ "mcp/entwurf-bridge/dist/scripts/new-session-id.js"
1901
+ "mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js"
1738
1902
  # 0.12.5 — the node_modules-safe plugin hook + its lib. install-meta-bridge copies
1739
1903
  # these compiled JS into the assembled plugin when installed (raw .ts can't
1740
1904
  # strip-types under node_modules). meta-session.js is shared with the store-doctor
@@ -1772,6 +1936,13 @@ check_pack() {
1772
1936
  '\.tmp-verify/'
1773
1937
  '\.agent-(reports|shell)/'
1774
1938
  'pi/meta-bridge/\.assembled/'
1939
+ # Python bytecode residue — `scripts/` ships whole via the files allowlist,
1940
+ # which BYPASSES .gitignore/.npmignore for its contents, so a `pnpm check`
1941
+ # run's generated scripts/__pycache__/*.pyc rode into the 0.12.6 tarball. The
1942
+ # files-array `!**/__pycache__` / `!**/*.pyc` negations exclude it; this is the
1943
+ # tripwire that fails loud if that negation is ever dropped (결합 규칙).
1944
+ '__pycache__'
1945
+ '\.pyc$'
1775
1946
  )
1776
1947
 
1777
1948
  local pass=1 f pat hit
@@ -1804,7 +1975,7 @@ check_pack() {
1804
1975
  return 1
1805
1976
  }
1806
1977
 
1807
- check_pack_install() {
1978
+ _check_pack_install_impl() {
1808
1979
  # Heavy publish gate. Runs the remaining three checks in #13's
1809
1980
  # publish checklist that check_pack (dry-run only) does not cover:
1810
1981
  #
@@ -1833,8 +2004,15 @@ check_pack_install() {
1833
2004
  # that lands as `junghanacs-entwurf-<version>.tgz`. Hardcoded against
1834
2005
  # the scope above so a name change cannot silently slide past this gate.
1835
2006
  tgz_name="junghanacs-entwurf-${version}.tgz"
1836
- tgz_path="${REPO_DIR}/${tgz_name}"
1837
- rm -f "$tgz_path"
2007
+ # Pack into a UNIQUE per-run dir, never the repo root. Writing/removing a fixed
2008
+ # ${REPO_DIR}/<tgz> pollutes the working tree (a crash leaves a stray tarball) AND
2009
+ # lets two concurrent packs corrupt/delete each other's artifact ("tarball data
2010
+ # seems corrupted" / ENOENT; a half-installed package then fails the later
2011
+ # install-meta-bridge check downstream). A per-run pack dir makes the tarball
2012
+ # instance-private end-to-end — every consumer below + the trap follow $pack_tmp.
2013
+ local pack_tmp
2014
+ pack_tmp=$(mktemp -d -t entwurf-pack.XXXXXX)
2015
+ tgz_path="${pack_tmp}/${tgz_name}"
1838
2016
 
1839
2017
  # 0.12.1 C — stale-dist guard. `tsc` emit does NOT prune orphaned files from
1840
2018
  # outDir, and `files: ["mcp/"]` would carry any leftover dist file into the
@@ -1846,7 +2024,11 @@ check_pack_install() {
1846
2024
  printf 'module.exports = "stale";\n' > "$stale_probe"
1847
2025
 
1848
2026
  echo "[check-pack-install] npm pack -> ${tgz_name}"
1849
- (cd "$REPO_DIR" && npm pack --dry-run=false 2>&1 | tail -1) || {
2027
+ # with-dist-lock: same whole-pack serialization as check-pack this heavy gate
2028
+ # and a background check-pack (via `pnpm check`) both pack, and unserialized they
2029
+ # race the shared dist dir. The stale-dist sentinel planted just above still
2030
+ # proves build-bridge's own `rm -rf dist` clean step under the lock.
2031
+ (cd "$REPO_DIR" && bash scripts/with-dist-lock.sh npm pack --dry-run=false --pack-destination "$pack_tmp" 2>&1 | tail -1) || {
1850
2032
  fail "[check-pack-install] npm pack failed"
1851
2033
  return 1
1852
2034
  }
@@ -1867,7 +2049,7 @@ check_pack_install() {
1867
2049
  # If it did, build-bridge's `rm -rf dist` clean step regressed and stale/orphan
1868
2050
  # emit can ship. (See the plant just before npm pack above.)
1869
2051
  if grep -qxF "mcp/entwurf-bridge/dist/__stale_probe__.js" <<<"$tar_files"; then
1870
- rm -f "$tgz_path"
2052
+ rm -rf "$pack_tmp"
1871
2053
  fail "[check-pack-install] stale dist file shipped — build-bridge did not clean dist before emit (orphan-emit publish risk)"
1872
2054
  return 1
1873
2055
  fi
@@ -1893,6 +2075,14 @@ check_pack_install() {
1893
2075
  # 0.12.4 — prebuilt node_modules-safe store-scan artifact for the doctor
1894
2076
  # (see check-pack). The installed-scan smoke below runs exactly this file.
1895
2077
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js"
2078
+ # 0.12.7 — node_modules-safe agy PreInvocation hook. The installed bin smoke
2079
+ # below executes this exact compiled leaf from under node_modules.
2080
+ "mcp/entwurf-bridge/dist/scripts/agy-imprint.js"
2081
+ # 0.12.7 — the three operator commands (see check-pack). The installed-command
2082
+ # regression below drives each one through the real `entwurf` bin.
2083
+ "mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js"
2084
+ "mcp/entwurf-bridge/dist/scripts/new-session-id.js"
2085
+ "mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js"
1896
2086
  # 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
1897
2087
  # hook regression below runs exactly this compiled JS from under node_modules.
1898
2088
  "mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
@@ -1924,6 +2114,10 @@ check_pack_install() {
1924
2114
  '^plugins/' '^node_modules/'
1925
2115
  '\.tmp-verify/' '\.agent-(reports|shell)/'
1926
2116
  'pi/meta-bridge/\.assembled/'
2117
+ # Python bytecode residue (see check-pack forbidden note): scripts/ ships
2118
+ # whole, so generated pyc bypasses ignore files — this cross-checks the actual
2119
+ # tarball, not just the dry-run resolver.
2120
+ '__pycache__' '\.pyc$'
1927
2121
  )
1928
2122
  for pat in "${tar_forbidden[@]}"; do
1929
2123
  local hit
@@ -1936,7 +2130,7 @@ check_pack_install() {
1936
2130
  done
1937
2131
 
1938
2132
  if [ "$pass" != "1" ]; then
1939
- rm -f "$tgz_path"
2133
+ rm -rf "$pack_tmp"
1940
2134
  fail "[check-pack-install] tar -tf invariants violated"
1941
2135
  return 1
1942
2136
  fi
@@ -1955,7 +2149,7 @@ check_pack_install() {
1955
2149
  # nested under $tmp, whose pnpm-add node_modules/package.json would make npm
1956
2150
  # climb the parent and choke ("Cannot read properties of null").
1957
2151
  npm_tmp=$(mktemp -d -t entwurf-npm-managed.XXXXXX)
1958
- trap 'rm -rf "$tmp" "$npm_tmp" "$tgz_path"' RETURN
2152
+ trap 'rm -rf "$tmp" "$npm_tmp" "$pack_tmp"' RETURN
1959
2153
 
1960
2154
  printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
1961
2155
 
@@ -2002,8 +2196,14 @@ check_pack_install() {
2002
2196
  return 1
2003
2197
  fi
2004
2198
 
2005
- local loader_out
2006
- loader_out=$(cd "$tmp" && pi -e "$tmp/node_modules/@junghanacs/entwurf" --list-models entwurf 2>&1) || {
2199
+ # HOME/PI_CODING_AGENT_DIR redirected to a throwaway dir: this loader smoke must
2200
+ # depend ONLY on the -e package path, never on the operator's real
2201
+ # ~/.pi/agent/settings.json — otherwise a live-config change could silently
2202
+ # pass/fail the gate (the exact "check must not depend on live wiring" impurity
2203
+ # this whole lane is about).
2204
+ local loader_out loader_home="$tmp/loader-home"
2205
+ mkdir -p "$loader_home/.pi/agent"
2206
+ loader_out=$(cd "$tmp" && HOME="$loader_home" XDG_DATA_HOME="$loader_home/.local/share" XDG_STATE_HOME="$loader_home/.local/state" XDG_CACHE_HOME="$loader_home/.cache" PI_CODING_AGENT_DIR="$loader_home/.pi/agent" pi -e "$tmp/node_modules/@junghanacs/entwurf" --list-models entwurf 2>&1) || {
2007
2207
  fail "[check-pack-install] pi loader smoke failed (exit non-zero):"
2008
2208
  echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
2009
2209
  return 1
@@ -2053,12 +2253,12 @@ check_pack_install() {
2053
2253
  fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
2054
2254
  return 1
2055
2255
  fi
2056
- if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-statusline" ]; then
2057
- fail "[check-pack-install] npm-managed neutral install missing package bins (entwurf / entwurf-bridge / entwurf-statusline)"
2256
+ if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-statusline" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-agy-statusline" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-agy-imprint" ]; then
2257
+ fail "[check-pack-install] npm-managed neutral install missing package bins (entwurf / entwurf-bridge / entwurf-statusline / entwurf-agy-statusline / entwurf-agy-imprint)"
2058
2258
  ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
2059
2259
  return 1
2060
2260
  fi
2061
- wire_log=$(HOME="$npmhome" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
2261
+ wire_log=$(HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" "$npm_pkg/run.sh" install "$npmproj" 2>&1) || {
2062
2262
  fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
2063
2263
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
2064
2264
  return 1
@@ -2075,6 +2275,43 @@ check_pack_install() {
2075
2275
  fi
2076
2276
  echo "[check-pack-install] npm-managed install regression pass (hoisted-dep run.sh install wrote settings)"
2077
2277
 
2278
+ # THE 2026-07-03 regression gate: removing `pi install` from setup dropped
2279
+ # user-scope citizen registration, so `--entwurf-control` was Unknown in a
2280
+ # foreign cwd. Prove the npm consumer path closes it end-to-end: run.sh install
2281
+ # (run above under HOME="$npmhome") must have registered the package in the USER
2282
+ # settings, and pi must then load the entwurf extension + its flags from a cwd
2283
+ # OUTSIDE the project. Fully isolated in the temp HOME — never touches ~/.pi.
2284
+ local npm_user_settings="$npmhome/.pi/agent/settings.json"
2285
+ if [ ! -f "$npm_user_settings" ]; then
2286
+ fail "[check-pack-install] npm-managed install did not register a user-scope citizen at $npm_user_settings"
2287
+ return 1
2288
+ fi
2289
+ if ! python3 -c "
2290
+ import json,sys
2291
+ p=json.load(open('$npm_user_settings')).get('packages',[])
2292
+ srcs=[(x if isinstance(x,str) else x.get('source')) for x in p]
2293
+ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/entwurf') for s in srcs) else 1)
2294
+ "; then
2295
+ fail "[check-pack-install] user-scope settings.json lacks the npm entwurf package in packages[]:"
2296
+ sed 's/^/ /' "$npm_user_settings" >&2
2297
+ return 1
2298
+ fi
2299
+ # Foreign cwd ($tmp — NOT the project, no project .pi): --entwurf-control must be
2300
+ # a KNOWN flag and the entwurf provider must load, sourced only from user scope.
2301
+ # Before the fix this printed "Unknown options: --entwurf-control".
2302
+ local foreign_out
2303
+ foreign_out=$(cd "$tmp" && HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" PI_CODING_AGENT_DIR="$npmhome/.pi/agent" pi --entwurf-control --list-models entwurf 2>&1) || {
2304
+ fail "[check-pack-install] foreign-cwd --entwurf-control smoke failed (user-scope citizen not loading?):"
2305
+ echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
2306
+ return 1
2307
+ }
2308
+ if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-4-8" <<<"$foreign_out"; then
2309
+ fail "[check-pack-install] foreign-cwd --entwurf-control did not load the entwurf extension from user scope:"
2310
+ echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
2311
+ return 1
2312
+ fi
2313
+ echo "[check-pack-install] user-scope citizen regression pass (npm consumer: --entwurf-control loads from a foreign cwd)"
2314
+
2078
2315
  # Installed meta-bridge ownership regression (0.12.5): package upgrades must not
2079
2316
  # bake versioned pnpm-store paths into Claude settings. MCP already uses the
2080
2317
  # stable `entwurf-bridge` bin; statusLine must now use `entwurf-statusline`, and
@@ -2104,7 +2341,7 @@ SH
2104
2341
  chmod +x "$fake_claude_dir/claude"
2105
2342
  local mb_home="$npm_tmp/meta-home" mb_cfg="$npm_tmp/meta-claude" mb_log
2106
2343
  mkdir -p "$mb_home" "$mb_cfg"
2107
- mb_log=$(HOME="$mb_home" XDG_DATA_HOME="$mb_home/.local/share" CLAUDE_CONFIG_DIR="$mb_cfg" FAKE_CLAUDE_LOG="$fake_claude_log" PATH="$fake_claude_dir:$npmroot/node_modules/.bin:$PATH" "$npm_pkg/run.sh" install-meta-bridge 2>&1) || {
2344
+ mb_log=$(HOME="$mb_home" XDG_DATA_HOME="$mb_home/.local/share" XDG_STATE_HOME="$mb_home/.local/state" XDG_CACHE_HOME="$mb_home/.cache" CLAUDE_CONFIG_DIR="$mb_cfg" FAKE_CLAUDE_LOG="$fake_claude_log" PATH="$fake_claude_dir:$npmroot/node_modules/.bin:$PATH" "$npm_pkg/run.sh" install-meta-bridge 2>&1) || {
2108
2345
  fail "[check-pack-install] installed install-meta-bridge failed under fake claude:"
2109
2346
  echo "$mb_log" | tail -20 | sed 's/^/ /' >&2
2110
2347
  return 1
@@ -2239,7 +2476,7 @@ function finish(trimmed) {
2239
2476
  try { msg = JSON.parse(trimmed); }
2240
2477
  catch { console.error('unparseable tools/list:', trimmed.slice(0, 300)); if (stderr.trim()) console.error(stderr.trim()); process.exit(1); }
2241
2478
  const names = (msg?.result?.tools ?? []).map((t) => t?.name).sort();
2242
- for (const need of ['entwurf_v2', 'entwurf_peers', 'entwurf_self', 'entwurf_inbox_read']) {
2479
+ for (const need of ['entwurf_v2', 'entwurf_peers', 'entwurf_self', 'entwurf_inbox_read', 'entwurf_register_native']) {
2243
2480
  if (!names.includes(need)) { console.error('missing MCP tool from installed boot:', need, '— got', names.join(',')); process.exit(1); }
2244
2481
  }
2245
2482
  console.log(names.join(','));
@@ -2259,6 +2496,109 @@ JS
2259
2496
  fi
2260
2497
  echo "[check-pack-install] installed bridge boot pass (dist boots under node_modules, pi-free: $boot_out)"
2261
2498
 
2499
+ # 0.12.7 — installed AGY IMPRINT regression. The npm bin resolves through a
2500
+ # node_modules symlink; raw scripts/agy-imprint.ts is therefore forbidden by
2501
+ # Node's strip-types fence. Execute the real installed bin with an isolated
2502
+ # agent dir and prove the neutral hook response + one record write. This is the
2503
+ # exact package bug that a dev-only hook smoke cannot see.
2504
+ local installed_agy_imprint="$npmroot/node_modules/.bin/entwurf-agy-imprint"
2505
+ local agy_imprint_agent="$npm_tmp/agy-imprint-agent" agy_imprint_out agy_record_count
2506
+ mkdir -p "$agy_imprint_agent"
2507
+ if ! agy_imprint_out=$(printf '%s\n' '{"conversationId":"pack-install-agy-conversation","workspacePaths":["/tmp/entwurf-pack-install"],"modelName":"probe-model"}' | HOME="$npmhome" XDG_DATA_HOME="$npmhome/.local/share" XDG_STATE_HOME="$npmhome/.local/state" XDG_CACHE_HOME="$npmhome/.cache" PI_CODING_AGENT_DIR="$agy_imprint_agent" "$installed_agy_imprint" 2>&1); then
2508
+ fail "[check-pack-install] installed entwurf-agy-imprint FAILED under node_modules (raw-.ts strip-types regression or emitted hook missing):"
2509
+ echo "$agy_imprint_out" | tail -15 | sed 's/^/ /' >&2
2510
+ return 1
2511
+ fi
2512
+ if [ "$agy_imprint_out" != '{"injectSteps":[]}' ]; then
2513
+ fail "[check-pack-install] installed entwurf-agy-imprint returned a non-neutral hook response: $agy_imprint_out"
2514
+ return 1
2515
+ fi
2516
+ agy_record_count=$(find "$agy_imprint_agent/meta-sessions" -maxdepth 1 -name '*.meta.json' -type f 2>/dev/null | wc -l | tr -d ' ')
2517
+ if [ "$agy_record_count" != "1" ]; then
2518
+ fail "[check-pack-install] installed entwurf-agy-imprint did not write exactly one isolated meta-record (got $agy_record_count)"
2519
+ return 1
2520
+ fi
2521
+ echo "[check-pack-install] installed agy imprint pass (compiled JS runs under node_modules; neutral response + record write)"
2522
+
2523
+ # 0.12.7 — installed OPERATOR COMMAND regression. `entwurf <cmd>` dispatches through
2524
+ # run.sh, whose REPO_DIR is under node_modules once installed, so a raw-.ts entrypoint
2525
+ # dies on the strip-types fence. These three are operator surfaces, not dev gates:
2526
+ # doctor-pi-provider is the pi-ownership verdict, new-session-id is the alias that mints
2527
+ # a garden citizen (docs/setup-clean-host.md tells operators to run the installed bin),
2528
+ # and meta-bridge-prune is the store maintenance verb. All three shipped DEAD through
2529
+ # 0.12.6 because the only installed smokes drove bins, never subcommands.
2530
+ #
2531
+ # Assert MEANING, not just the absence of the fence string: a command that regressed to
2532
+ # a stub or an empty exit would still "not crash". So: the id must be well-formed, the
2533
+ # doctor must reach its own verdict body, and prune must actually walk a 0-record store.
2534
+ local installed_entwurf="$npmroot/node_modules/.bin/entwurf"
2535
+ local op_agent="$npm_tmp/op-agent" op_out
2536
+ mkdir -p "$op_agent/meta-sessions"
2537
+
2538
+ # Same sandbox root set on every operator-command drive: doctor-pi-provider READS install-state
2539
+ # below XDG_DATA_HOME, so an inherited real root would make this gate's verdict depend on the
2540
+ # operator's host instead of the sandbox (non-hermetic even when nothing is written).
2541
+ local op_xdg_data="$npmhome/.local/share" op_xdg_state="$npmhome/.local/state" op_xdg_cache="$npmhome/.cache"
2542
+ if ! op_out=$(HOME="$npmhome" XDG_DATA_HOME="$op_xdg_data" XDG_STATE_HOME="$op_xdg_state" XDG_CACHE_HOME="$op_xdg_cache" PI_CODING_AGENT_DIR="$op_agent" "$installed_entwurf" new-session-id 2>&1); then
2543
+ fail "[check-pack-install] installed 'entwurf new-session-id' FAILED under node_modules (strip-types fence or missing compiled twin):"
2544
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
2545
+ return 1
2546
+ fi
2547
+ # Same shape as SESSION_ID_RE (the garden id SSOT): <denote-stamp>-<6 hex>. A stub or a
2548
+ # truncated id would still "not crash", so the gate reads the id, not just the exit code.
2549
+ if ! printf '%s' "$op_out" | grep -qE '^[0-9]{8}T[0-9]{6}-[0-9a-f]{6}$'; then
2550
+ fail "[check-pack-install] installed 'entwurf new-session-id' did not print a well-formed garden session id: $op_out"
2551
+ return 1
2552
+ fi
2553
+
2554
+ if ! op_out=$(HOME="$npmhome" XDG_DATA_HOME="$op_xdg_data" XDG_STATE_HOME="$op_xdg_state" XDG_CACHE_HOME="$op_xdg_cache" PI_CODING_AGENT_DIR="$op_agent" "$installed_entwurf" doctor-pi-provider 2>&1); then
2555
+ # A doctor may exit non-zero on an unadopted host — that is a VERDICT, not a crash.
2556
+ # The fence, by contrast, kills it before any verdict body is printed.
2557
+ if printf '%s' "$op_out" | grep -q ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING; then
2558
+ fail "[check-pack-install] installed 'entwurf doctor-pi-provider' hit the node_modules strip-types fence:"
2559
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
2560
+ return 1
2561
+ fi
2562
+ fi
2563
+ if ! printf '%s' "$op_out" | grep -q '\[pi-provider doctor\]' || ! printf '%s' "$op_out" | grep -q 'EFFECTIVE'; then
2564
+ fail "[check-pack-install] installed 'entwurf doctor-pi-provider' never reached its verdict body (no scopes/EFFECTIVE report):"
2565
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
2566
+ return 1
2567
+ fi
2568
+
2569
+ if ! op_out=$(HOME="$npmhome" XDG_DATA_HOME="$op_xdg_data" XDG_STATE_HOME="$op_xdg_state" XDG_CACHE_HOME="$op_xdg_cache" PI_CODING_AGENT_DIR="$op_agent" "$installed_entwurf" meta-bridge-prune "$op_agent/meta-sessions" 2>&1); then
2570
+ fail "[check-pack-install] installed 'entwurf meta-bridge-prune' FAILED on a 0-record store:"
2571
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
2572
+ return 1
2573
+ fi
2574
+ if ! printf '%s' "$op_out" | grep -q 'prune candidates' || ! printf '%s' "$op_out" | grep -q "store: $op_agent/meta-sessions"; then
2575
+ fail "[check-pack-install] installed 'entwurf meta-bridge-prune' did not scan the store it was given:"
2576
+ echo "$op_out" | tail -8 | sed 's/^/ /' >&2
2577
+ return 1
2578
+ fi
2579
+ echo "[check-pack-install] installed operator commands pass (new-session-id id-shaped, doctor-pi-provider reaches its verdict, meta-bridge-prune walks a 0-record store)"
2580
+
2581
+ # A dev-only gate has NO compiled twin by design. Under an installed package run_ts must
2582
+ # REFUSE it with a legible message — never fall back to raw .ts (that just re-raises the
2583
+ # fence error) and never exit 0 (a silent no-op would let CI "pass" a gate it never ran).
2584
+ local devgate_out devgate_rc=0
2585
+ devgate_out=$(HOME="$npmhome" XDG_DATA_HOME="$op_xdg_data" XDG_STATE_HOME="$op_xdg_state" XDG_CACHE_HOME="$op_xdg_cache" PI_CODING_AGENT_DIR="$op_agent" "$installed_entwurf" check-meta-session 2>&1) || devgate_rc=$?
2586
+ if [ "$devgate_rc" -eq 0 ]; then
2587
+ fail "[check-pack-install] a dev-only gate (check-meta-session) exited 0 from an installed package — it cannot have run; run_ts must refuse it"
2588
+ return 1
2589
+ fi
2590
+ if printf '%s' "$devgate_out" | grep -q ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING; then
2591
+ fail "[check-pack-install] a dev-only gate leaked the raw strip-types fence error instead of run_ts's refusal:"
2592
+ echo "$devgate_out" | tail -6 | sed 's/^/ /' >&2
2593
+ return 1
2594
+ fi
2595
+ if ! printf '%s' "$devgate_out" | grep -q 'dev-clone-only surface'; then
2596
+ fail "[check-pack-install] a dev-only gate under an installed package did not produce run_ts's refusal message:"
2597
+ echo "$devgate_out" | tail -6 | sed 's/^/ /' >&2
2598
+ return 1
2599
+ fi
2600
+ echo "[check-pack-install] dev-only gate refusal pass (installed package refuses check-* legibly, no raw-.ts fallback, no silent exit 0)"
2601
+
2262
2602
  # 0.12.4 — installed STORE-DOCTOR regression. meta-bridge-doctor.sh's full store
2263
2603
  # scan runs the prebuilt dist JS when it lives under node_modules (strip-types
2264
2604
  # refuses the .ts there — the same class as the bridge boot above). Before the dist
@@ -2309,6 +2649,41 @@ JS
2309
2649
  return 0
2310
2650
  }
2311
2651
 
2652
+ check_pack_install() {
2653
+ # SELF-FENCE wrapper (rule 11). Keep this OUTSIDE the implementation so every exit path —
2654
+ # including an early failure before the implementation's cleanup trap is installed — returns
2655
+ # through the comparison. The DATA tree is byte-fenced. STATE cannot be byte-fenced because a
2656
+ # live native session may append legitimate lines concurrently, so fence the gate's unique fake
2657
+ # conversation marker instead; any increase proves that agy-imprint escaped its sandbox.
2658
+ trap - RETURN
2659
+ local real_data_root="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf"
2660
+ local real_imprint_log="${XDG_STATE_HOME:-$HOME/.local/state}/entwurf/agy-imprint.log"
2661
+ local data_before data_after fake_before fake_after rc=0
2662
+ data_before="$( (find "$real_data_root" -type f -print0 2>/dev/null | sort -z | xargs -0r sha256sum) 2>/dev/null || true)"
2663
+ fake_before="$(grep -Fc 'conversationId=pack-install-agy-conversation' "$real_imprint_log" 2>/dev/null || true)"
2664
+
2665
+ _check_pack_install_impl "$@" || rc=$?
2666
+ # _check_pack_install_impl installs a RETURN cleanup trap after allocating its temp roots.
2667
+ # It has fired now; clear it before this wrapper returns so it cannot leak into its caller.
2668
+ trap - RETURN
2669
+
2670
+ data_after="$( (find "$real_data_root" -type f -print0 2>/dev/null | sort -z | xargs -0r sha256sum) 2>/dev/null || true)"
2671
+ fake_after="$(grep -Fc 'conversationId=pack-install-agy-conversation' "$real_imprint_log" 2>/dev/null || true)"
2672
+ if [ "$data_before" != "$data_after" ]; then
2673
+ fail "[check-pack-install] SELF-FENCE: this gate changed the operator's REAL install-state tree ($real_data_root) — a sandbox drive is leaking through an inherited XDG root:"
2674
+ diff <(printf '%s\n' "$data_before") <(printf '%s\n' "$data_after") | sed 's/^/ /' >&2 || true
2675
+ rc=1
2676
+ fi
2677
+ if [ "$fake_before" != "$fake_after" ]; then
2678
+ fail "[check-pack-install] SELF-FENCE: this gate appended its fake agy birth marker to the operator's REAL state log ($real_imprint_log): before=$fake_before after=$fake_after"
2679
+ rc=1
2680
+ fi
2681
+ if [ "$rc" -eq 0 ]; then
2682
+ echo "[check-pack-install] self-fence pass (real DATA tree byte-identical; fake STATE marker count unchanged)"
2683
+ fi
2684
+ return "$rc"
2685
+ }
2686
+
2312
2687
 
2313
2688
  # --- v2 install/runtime verification gates ---
2314
2689
  #
@@ -2350,7 +2725,7 @@ function finishOk(trimmed) {
2350
2725
  process.exit(1);
2351
2726
  }
2352
2727
  const names = tools.map((t) => t?.name).sort();
2353
- const expected = ['entwurf_peers', 'entwurf_self', 'entwurf_inbox_read', 'entwurf_v2'];
2728
+ const expected = ['entwurf_peers', 'entwurf_self', 'entwurf_inbox_read', 'entwurf_register_native', 'entwurf_v2'];
2354
2729
  for (const name of expected) {
2355
2730
  if (!names.includes(name)) {
2356
2731
  console.error(`missing MCP tool: ${name}`);
@@ -2452,7 +2827,7 @@ setup_all() {
2452
2827
 
2453
2828
  echo "[setup] repo: $REPO_DIR"
2454
2829
  echo "[setup] project: $project_dir"
2455
- echo "[setup] scope: entwurf v2 orchestration install (pi-native; ACP backends dropped)"
2830
+ echo "[setup] scope: entwurf v2 package + detected native-harness bridges + pi adapter"
2456
2831
  echo "[setup] verification: v2 install smoke (entwurf-bridge; LIVE substrate = release-gate)"
2457
2832
 
2458
2833
  (cd "$REPO_DIR" && pnpm install --frozen-lockfile)
@@ -2472,6 +2847,28 @@ setup_all() {
2472
2847
  echo "[setup] no native harness (claude) on PATH — skipping meta-bridge wiring (pi-only host)"
2473
2848
  fi
2474
2849
 
2850
+ # Expose entwurf's STABLE bins on PATH for this DEV checkout (막힘 ②) BEFORE wiring agy: the
2851
+ # agy mcp_config, settings.statusLine, and hooks.json record the bare names `entwurf-bridge` /
2852
+ # `entwurf-agy-statusline` / `entwurf-agy-imprint`, so they must resolve for the agy doctors
2853
+ # to pass. NON-FATAL — a foreign bin already on PATH is left as-is.
2854
+ expose_dev_bin
2855
+
2856
+ # Fold agy (Antigravity) MCP bridge wiring into setup (막힘 ①, GLG 2026-07-04: install
2857
+ # ownership moves to entwurf). Same detection-gated, idempotent, NON-FATAL posture as the
2858
+ # meta-bridge block above — agy is an OPTIONAL harness, so a refused/corrupt agy config warns
2859
+ # but never bricks a pi/Claude setup. The hard gate stays doctor-agy-bridge.
2860
+ wire_agy_bridge
2861
+
2862
+ # Fold agy statusLine wiring into setup (#46 Task 1: own the ambient status area so it shows
2863
+ # driver + garden id, the claude meta-bridge symmetry). Same detection-gated NON-FATAL posture
2864
+ # — an explicit install-agy-statusline is fail-loud, but the setup wrapper WARNs + continues.
2865
+ wire_agy_statusline
2866
+
2867
+ # Fold agy PreInvocation birth imprint wiring into setup (#46 close criterion: manual register 0).
2868
+ # The hook runs the thin `entwurf-agy-imprint` bin and returns {"injectSteps":[]} so agy's loop
2869
+ # survives while the meta-record is created/attached by conversationId.
2870
+ wire_agy_hooks
2871
+
2475
2872
  # Deterministic preflight lives in `pnpm check`; live substrate acceptance lives
2476
2873
  # in `LIVE=1 ./run.sh release-gate <scratch>`. Setup is the install path, so it
2477
2874
  # verifies the installed MCP bridge boundary only and does NOT run the legacy
@@ -2480,13 +2877,175 @@ setup_all() {
2480
2877
  validate_entwurf_bridge
2481
2878
 
2482
2879
  echo ""
2483
- echo "DONE: entwurf setup (pi package + meta-bridge + v2 install smoke) green."
2880
+ echo "DONE: entwurf setup (pi adapter + detected native bridges + v2 install smoke) green."
2484
2881
  if command -v claude >/dev/null 2>&1; then
2485
- echo "Verify native-harness wiring with: ./run.sh doctor-meta-bridge"
2882
+ echo "Verify Claude wiring with: ./run.sh doctor-meta-bridge"
2883
+ fi
2884
+ if command -v agy >/dev/null 2>&1; then
2885
+ echo "Verify agy wiring with: ./run.sh doctor-agy-bridge"
2886
+ echo " ./run.sh doctor-agy-statusline"
2887
+ echo " ./run.sh doctor-agy-hooks"
2486
2888
  fi
2487
2889
  echo "Run 'LIVE=1 ./run.sh release-gate <scratch>' for live substrate acceptance."
2488
2890
  }
2489
2891
 
2892
+ # wire_agy_bridge — detection-gated, NON-FATAL agy (Antigravity) MCP bridge wiring, folded into
2893
+ # setup so a relocate/clone needs ONE idempotent command (막힘 ①). Mirrors the meta-bridge
2894
+ # block: agy on PATH → idempotent install-agy-bridge; no agy → honest skip, NO state ("있으면
2895
+ # 설정, 없으면 담아준다"). NON-FATAL by contract: agy is an OPTIONAL harness, so a refused/corrupt
2896
+ # agy config must NOT brick a pi/Claude setup — the hard gate is doctor-agy-bridge (issue #45:
2897
+ # the '?' surfaces in doctor, not by killing setup). WARNs are reason-specific so a transitional
2898
+ # symlink (someone else's SSOT) and a corrupt config (invalid JSON) are never conflated.
2899
+ wire_agy_bridge() {
2900
+ # Detection = the agy binary on PATH. AGY_BIN pins the target (default `agy`) so the
2901
+ # hermetic smoke can point at a fake agy or a definitely-absent path without depending on
2902
+ # whatever agy the CI/dev host happens to have — production leaves it unset (= `command -v
2903
+ # agy`, no regression). Same override spirit as agy-bridge.sh's AGY_MCP_CONFIG/AGY_BRIDGE_COMMAND.
2904
+ if ! command -v "${AGY_BIN:-agy}" >/dev/null 2>&1; then
2905
+ echo "[setup] no agy on PATH — skipping agy bridge wiring (no state; pi/Claude-only host)"
2906
+ return 0
2907
+ fi
2908
+ section "agy bridge install (native harness detected: Antigravity)"
2909
+ local out rc
2910
+ set +e
2911
+ out="$(bash "$REPO_DIR/scripts/agy-bridge.sh" install 2>&1)"
2912
+ rc=$?
2913
+ set -e
2914
+ printf '%s\n' "$out"
2915
+ if [ "$rc" -eq 0 ]; then
2916
+ echo "[setup] agy bridge wired (idempotent). Verify with: ./run.sh doctor-agy-bridge"
2917
+ return 0
2918
+ fi
2919
+ # NON-FATAL: keep setup alive, surface the reason honestly (never a silent pass).
2920
+ # ORDER MATTERS: the permission failures carry the same words as the mcp_config ones ("refused
2921
+ # (symlink)", "invalid JSON") and MUST be matched first — otherwise a settings.json problem gets
2922
+ # reported as an mcp_config problem and sends the operator to repair the wrong file.
2923
+ case "$out" in
2924
+ *"permission refused (symlink)"*|*"permission invalid JSON"*|*"permission could not be granted"*)
2925
+ # The MCP server IS registered — only the allow rule failed. agy defaults every mcp action to
2926
+ # Ask, so the bridge works but stops for a y/n on EVERY entwurf_v2 call. Half-wired, and said
2927
+ # so: the explicit installer fails loud on this; setup only degrades it, never hides it.
2928
+ echo "[setup] WARN: agy settings.json could not take our permission rule — bridge REGISTERED but NOT GRANTED." >&2
2929
+ echo "[setup] agy will prompt on every entwurf_v2 call until 'mcp(entwurf-bridge/entwurf_v2)'" >&2
2930
+ echo "[setup] is in its permissions.allow (see the line above for why). setup continues." >&2
2931
+ ;;
2932
+ *"refused (symlink)"*)
2933
+ echo "[setup] WARN: agy mcp_config is a symlink — someone else's SSOT (transitional)." >&2
2934
+ echo "[setup] Bridge NOT wired; expected until install ownership moves to entwurf." >&2
2935
+ echo "[setup] Re-run setup once the symlink is dropped. setup continues." >&2
2936
+ ;;
2937
+ *"invalid JSON"*)
2938
+ echo "[setup] WARN: your agy mcp_config is CORRUPT (invalid JSON) — bridge NOT wired." >&2
2939
+ echo "[setup] doctor-agy-bridge will KEEP FAILING until you repair that file." >&2
2940
+ echo "[setup] (Not a silent skip — fix the config, then re-run setup.) setup continues." >&2
2941
+ ;;
2942
+ *)
2943
+ echo "[setup] WARN: agy bridge install did not complete (rc=$rc; see the line above)." >&2
2944
+ echo "[setup] Bridge NOT wired; verify with ./run.sh doctor-agy-bridge. setup continues." >&2
2945
+ ;;
2946
+ esac
2947
+ return 0
2948
+ }
2949
+
2950
+ # wire_agy_statusline — detection-gated, NON-FATAL agy statusLine wiring, folded into setup (#46
2951
+ # Task 1). Mirrors wire_agy_bridge: agy on PATH → idempotent install-agy-statusline; no agy →
2952
+ # honest skip, NO state. NON-FATAL by contract (agy is OPTIONAL — a refused/corrupt settings must
2953
+ # not brick a pi/Claude setup); the hard gate is doctor-agy-statusline. The renderer is the
2954
+ # stable bin entwurf-agy-statusline, exposed by expose_dev_bin above.
2955
+ wire_agy_statusline() {
2956
+ if ! command -v "${AGY_BIN:-agy}" >/dev/null 2>&1; then
2957
+ echo "[setup] no agy on PATH — skipping agy statusLine wiring (no state; pi/Claude-only host)"
2958
+ return 0
2959
+ fi
2960
+ section "agy statusLine install (native harness detected: Antigravity)"
2961
+ local out rc
2962
+ set +e
2963
+ out="$(bash "$REPO_DIR/scripts/agy-statusline-bridge.sh" install 2>&1)"
2964
+ rc=$?
2965
+ set -e
2966
+ printf '%s\n' "$out"
2967
+ if [ "$rc" -eq 0 ]; then
2968
+ echo "[setup] agy statusLine wired (idempotent). Verify with: ./run.sh doctor-agy-statusline"
2969
+ return 0
2970
+ fi
2971
+ case "$out" in
2972
+ *"refused (symlink)"*)
2973
+ echo "[setup] WARN: agy settings.json is a symlink — someone else's SSOT (transitional)." >&2
2974
+ echo "[setup] statusLine NOT wired; expected until install ownership moves to entwurf." >&2
2975
+ echo "[setup] Re-run setup once the symlink is dropped. setup continues." >&2
2976
+ ;;
2977
+ *"invalid JSON"*)
2978
+ echo "[setup] WARN: your agy settings.json is CORRUPT (invalid JSON) — statusLine NOT wired." >&2
2979
+ echo "[setup] doctor-agy-statusline will KEEP FAILING until you repair that file." >&2
2980
+ echo "[setup] (Not a silent skip — fix the config, then re-run setup.) setup continues." >&2
2981
+ ;;
2982
+ *)
2983
+ echo "[setup] WARN: agy statusLine install did not complete (rc=$rc; see the line above)." >&2
2984
+ echo "[setup] statusLine NOT wired; verify with ./run.sh doctor-agy-statusline. setup continues." >&2
2985
+ ;;
2986
+ esac
2987
+ return 0
2988
+ }
2989
+
2990
+ # wire_agy_hooks — detection-gated, NON-FATAL agy PreInvocation imprint wiring. Same setup posture
2991
+ # as the MCP/statusLine adapters, but this one is the birth writer that turns statusLine '?' into a
2992
+ # garden id after the first invocation.
2993
+ wire_agy_hooks() {
2994
+ if ! command -v "${AGY_BIN:-agy}" >/dev/null 2>&1; then
2995
+ echo "[setup] no agy on PATH — skipping agy hooks wiring (no state; pi/Claude-only host)"
2996
+ return 0
2997
+ fi
2998
+ section "agy hooks install (native harness detected: Antigravity)"
2999
+ local out rc
3000
+ set +e
3001
+ out="$(bash "$REPO_DIR/scripts/agy-hooks-bridge.sh" install 2>&1)"
3002
+ rc=$?
3003
+ set -e
3004
+ printf '%s\n' "$out"
3005
+ if [ "$rc" -eq 0 ]; then
3006
+ echo "[setup] agy hooks wired (idempotent). Verify with: ./run.sh doctor-agy-hooks"
3007
+ return 0
3008
+ fi
3009
+ case "$out" in
3010
+ *"refused (symlink)"*)
3011
+ echo "[setup] WARN: agy hooks.json is a symlink — someone else's SSOT (transitional)." >&2
3012
+ echo "[setup] birth imprint NOT wired; re-run setup once the symlink is dropped. setup continues." >&2
3013
+ ;;
3014
+ *"invalid JSON"*)
3015
+ echo "[setup] WARN: your agy hooks.json is CORRUPT (invalid JSON) — birth imprint NOT wired." >&2
3016
+ echo "[setup] doctor-agy-hooks will KEEP FAILING until you repair that file. setup continues." >&2
3017
+ ;;
3018
+ *)
3019
+ echo "[setup] WARN: agy hooks install did not complete (rc=$rc; see the line above)." >&2
3020
+ echo "[setup] Birth imprint NOT wired; verify with ./run.sh doctor-agy-hooks. setup continues." >&2
3021
+ ;;
3022
+ esac
3023
+ return 0
3024
+ }
3025
+
3026
+ # expose_dev_bin — make the `entwurf-bridge` STABLE bin resolve for a DEV checkout (막힘 ②).
3027
+ # setup IS the dev install command (consumers get the bin from npm bin-linking), so setup owns a
3028
+ # managed ~/.local/bin/entwurf-bridge symlink into this checkout via scripts/dev-bin.sh, recorded
3029
+ # for an honest inverse (remove-dev-bin). NON-FATAL like wire_agy_bridge: a foreign bin already on
3030
+ # PATH (a real npm install) is REFUSED not clobbered, and a BIN_DIR off PATH only WARNs — neither
3031
+ # bricks setup. The hard gate stays doctor-agy-bridge (a missing/dangling bin FAILs there).
3032
+ expose_dev_bin() {
3033
+ section "dev bin: expose entwurf-bridge on PATH (dev checkout)"
3034
+ local rc
3035
+ set +e
3036
+ bash "$REPO_DIR/scripts/dev-bin.sh" expose
3037
+ rc=$?
3038
+ set -e
3039
+ [ "$rc" -eq 0 ] && return 0
3040
+ if [ "$rc" -eq 3 ]; then
3041
+ echo "[setup] WARN: entwurf-bridge is already on PATH as someone else's bin (not ours) — left as-is." >&2
3042
+ echo "[setup] If that is a stale/foreign bin, remove it and re-run setup. setup continues." >&2
3043
+ else
3044
+ echo "[setup] WARN: dev bin exposure did not complete (rc=$rc; see above). setup continues." >&2
3045
+ fi
3046
+ return 0
3047
+ }
3048
+
2490
3049
  # release-gate — the single command that, when GREEN, is sufficient to cut
2491
3050
  # release cuts. Runs the full static floor (`pnpm check`) followed by the
2492
3051
  # v2-native live gates, then emits one PASS/FAIL/SKIP summary. Everything is
@@ -2783,6 +3342,13 @@ case "$cmd" in
2783
3342
  check-shell-quote)
2784
3343
  check_shell_quote
2785
3344
  ;;
3345
+ check-install-surface)
3346
+ # 0.12.7 — structural half of the node_modules strip-types fence: run_ts is the only
3347
+ # crossing, every operator subcommand has a compiled twin, bin wrappers branch, dev
3348
+ # gates stay out of the tarball, and offline smokes never touch the real $HOME.
3349
+ # check-pack-install owns the dynamic half (it drives the installed commands).
3350
+ run_ts scripts/check-install-surface.ts
3351
+ ;;
2786
3352
  check-entwurf-session-identity)
2787
3353
  check_entwurf_session_identity
2788
3354
  ;;
@@ -2846,6 +3412,9 @@ case "$cmd" in
2846
3412
  check-entwurf-v2-mailbox)
2847
3413
  check_entwurf_v2_mailbox
2848
3414
  ;;
3415
+ check-entwurf-v2-native-push)
3416
+ check_entwurf_v2_native_push
3417
+ ;;
2849
3418
  check-entwurf-v2-runner)
2850
3419
  check_entwurf_v2_runner
2851
3420
  ;;
@@ -2939,11 +3508,20 @@ case "$cmd" in
2939
3508
  check-entwurf-mailbox-guard)
2940
3509
  check_entwurf_mailbox_guard
2941
3510
  ;;
3511
+ check-native-push-adapter)
3512
+ check_native_push_adapter
3513
+ ;;
3514
+ check-native-push-register)
3515
+ check_native_push_register
3516
+ ;;
3517
+ check-agy-sender-identity)
3518
+ check_agy_sender_identity
3519
+ ;;
2942
3520
  new-session-id)
2943
3521
  # Garden launcher helper: print one fresh garden sessionId (SSOT:
2944
3522
  # generateSessionId). Used by the operator alias to make every
2945
3523
  # --entwurf-control session a garden citizen. Stdout = the id only.
2946
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/new-session-id.ts)
3524
+ run_ts scripts/new-session-id.ts
2947
3525
  ;;
2948
3526
  smoke-resident-garden-guard)
2949
3527
  # LIVE negative (0 tokens) + opt-in positive gate for the resident
@@ -2998,6 +3576,55 @@ case "$cmd" in
2998
3576
  # duplicate/drift records. Offline + deterministic (deps bash+node+python3).
2999
3577
  (cd "$REPO_DIR" && bash scripts/smoke-meta-install-state.sh)
3000
3578
  ;;
3579
+ smoke-agy-install-state)
3580
+ # 봉인 8 regression gate for the agy MCP install adapter: install→doctor→uninstall in an
3581
+ # ISOLATED HOME+XDG with a fake stable bin + fake pgrep/ss — adopt (preserve unrelated) +
3582
+ # state, doctor static-clean/live-SKIP (+live-PASS with a fake agy), honest-inverse
3583
+ # uninstall, symlink refuse, dangling FAIL, create-new inverse, and ⓪ checkout impurity 0.
3584
+ # Offline + deterministic (deps: bash+python3).
3585
+ (cd "$REPO_DIR" && bash scripts/smoke-agy-install-state.sh)
3586
+ ;;
3587
+ smoke-agy-statusline-state)
3588
+ # #46 Task 1 regression gate for the agy statusLine install adapter: install→doctor→uninstall
3589
+ # in an ISOLATED HOME+XDG with a fake stable bin (entwurf-agy-statusline) + fake pgrep —
3590
+ # own the statusLine subtree WHOLE (preserve unrelated keys) + state (stable command, prior
3591
+ # subtree as preimage), doctor static-clean/live-SKIP (+live-consistent with a fake agy) /
3592
+ # drift-FAIL / dangling-command-FAIL / not-ours note, honest-inverse uninstall, symlink +
3593
+ # dangling-symlink refuse, create-new inverse, the NON-FATAL wire wrapper, and checkout
3594
+ # impurity 0. Offline + deterministic (deps: bash+python3).
3595
+ (cd "$REPO_DIR" && bash scripts/smoke-agy-statusline-state.sh)
3596
+ ;;
3597
+ smoke-agy-hooks-state)
3598
+ # #46 birth imprint regression gate: hooks.json named hook ownership + direct
3599
+ # PreInvocation stdin → upsertMetaSession antigravity record, isolated HOME/XDG/PI agent.
3600
+ (cd "$REPO_DIR" && bash scripts/smoke-agy-hooks-state.sh)
3601
+ ;;
3602
+ smoke-pi-provider-state)
3603
+ # #46 Task 2 regression gate for the pi provider install adapter: register-pi-provider.py
3604
+ # (ownership-classified install/remove, user+project scopes) + read-only doctor-pi-provider.ts
3605
+ # (effective shadow view). ISOLATED HOME+XDG + fake stable bin — user ownership matrix
3606
+ # (absent/managed-legacy/managed-current/user-override), state honest-inverse (legacy NOT
3607
+ # restored), sibling + legacy prune, project no-state strip, doctor effective/drift/dangling/
3608
+ # malformed/project-stale/'?', symlink refuse, and checkout impurity 0. Offline (bash+python3+node).
3609
+ (cd "$REPO_DIR" && bash scripts/smoke-pi-provider-state.sh)
3610
+ ;;
3611
+ smoke-agy-native-push-live)
3612
+ # 봉인 8 LIVE acceptance gate for the native-push (agy) delivery rail. Drives the REAL
3613
+ # antigravity adapter + register core + runEntwurfV2 (production deps) against a live agy
3614
+ # conversation (AGY_CONVERSATION_ID): probe route, register create/attach idempotency,
3615
+ # fire→native-push delivered, owned-outcome reject, bogus-conv probe-indeterminate reject.
3616
+ # Meta-store is isolated to a temp dir (only the agy round-trip is real); honest SKIP when
3617
+ # LIVE!=1. doctor-static preflight FAILs before the agy bridge is wired (③).
3618
+ run_ts scripts/smoke-agy-native-push-live.ts
3619
+ ;;
3620
+ smoke-user-scope-citizen)
3621
+ # 0.12.6 install-boundary gate: register-pi-package.py is the shared
3622
+ # packages[] SSOT for project/user install and remove; user scope makes
3623
+ # --entwurf-control load from any cwd. Idempotent, preserves unrelated
3624
+ # packages/keys, normalizes stale entries, remove is symmetric, and corrupt
3625
+ # settings fail loud. The tripwire the 2026-07-03 `pi install` removal lacked.
3626
+ (cd "$REPO_DIR" && bash scripts/smoke-user-scope-citizen.sh)
3627
+ ;;
3001
3628
  smoke-claude-native-resume-live)
3002
3629
  # LIVE-only Detour A probe: two real Claude Code native turns (fresh +
3003
3630
  # --resume) in a scratch cwd. Verifies native resume works while the
@@ -3007,9 +3634,10 @@ case "$cmd" in
3007
3634
  ;;
3008
3635
  install-meta-bridge)
3009
3636
  # 1.0.0 meta-bridge step 5: operator-grade GLOBAL install of the garden-native
3010
- # receive plugin. Assembles a self-contained, node-path-baked copy under
3011
- # pi/meta-bridge/.assembled and runs marketplace add + install --scope user, so
3012
- # every native Claude Code session auto-loads it (no manual --plugin-dir).
3637
+ # receive plugin. Assembles a self-contained, node-path-baked copy under the
3638
+ # XDG data dir ($XDG_DATA_HOME/entwurf/meta-bridge/.assembled dev clone and
3639
+ # installed package alike, never the checkout) and runs marketplace add +
3640
+ # install --scope user, so every native Claude Code session auto-loads it.
3013
3641
  # Idempotent; Linux/macOS only (Windows fail-fast).
3014
3642
  (cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh "$@")
3015
3643
  ;;
@@ -3027,6 +3655,101 @@ case "$cmd" in
3027
3655
  # zero claude-code meta-records is a SILENT MISS -> non-zero exit.
3028
3656
  (cd "$REPO_DIR" && bash scripts/meta-bridge-doctor.sh "$@")
3029
3657
  ;;
3658
+ install-agy-bridge)
3659
+ # 봉인 7: the agy (Antigravity) MCP install ADAPTER (SEPARATE from the Claude
3660
+ # marketplace install — only runner/reporting is shared). Registers ONE entwurf-bridge
3661
+ # server entry in the agy mcp_config: adopt a regular file / create a new one / REFUSE a
3662
+ # symlink (someone else's SSOT). Records an install-state under $XDG_DATA_HOME/entwurf/
3663
+ # agy-bridge/ for an honest inverse. The command written is a STABLE bin (entwurf-bridge),
3664
+ # never a repo/git-hash path (the oracle dangling lesson).
3665
+ (cd "$REPO_DIR" && bash scripts/agy-bridge.sh install "$@")
3666
+ ;;
3667
+ uninstall-agy-bridge)
3668
+ # 봉인 7: honest inverse of install-agy-bridge from the install-state (restore the
3669
+ # captured preimage / remove our key; remove the file if we created it empty). Refuses if
3670
+ # the managed config became a symlink since install; no state → nothing to undo.
3671
+ (cd "$REPO_DIR" && bash scripts/agy-bridge.sh uninstall "$@")
3672
+ ;;
3673
+ doctor-agy-bridge)
3674
+ # 봉인 7: 2-tier fail-loud doctor. STATIC proves both candidate configs (global
3675
+ # ~/.gemini/config/mcp_config.json — the file live agy actually reads — + legacy
3676
+ # ~/.gemini/antigravity-cli which install now cleans) resolve, parse, and carry a
3677
+ # RESOLVABLE command (a dangling command FAILS). LIVE proves runtime-effectiveness only
3678
+ # when an agy process exists; with no agy it is an honest SKIP (never a PASS in disguise).
3679
+ (cd "$REPO_DIR" && bash scripts/agy-bridge.sh doctor "$@")
3680
+ ;;
3681
+ wire-agy-bridge)
3682
+ # 막힘 ①: the detection-gated, NON-FATAL setup wrapper around install-agy-bridge (agy on
3683
+ # PATH → idempotent install; no agy → honest skip, no state). HIDDEN/internal — setup calls
3684
+ # this; it is exposed as a subcommand only so smoke-agy-install-state can drive it
3685
+ # deterministically. The hard gate stays doctor-agy-bridge (issue #45: the '?' is a doctor
3686
+ # signal, not a setup-killer).
3687
+ wire_agy_bridge
3688
+ ;;
3689
+ install-agy-statusline)
3690
+ # #46 Task 1: own the WHOLE statusLine subtree of agy settings.json → the stable-bin renderer
3691
+ # entwurf-agy-statusline (driver + garden id), the claude meta-bridge statusLine symmetry.
3692
+ # Adopt a regular file / create / REFUSE a symlink. install-state under $XDG_DATA_HOME/
3693
+ # entwurf/agy-statusline/ for an honest inverse. The command is a BARE stable bin — dev AND
3694
+ # installed (the checkout path lives only in the dev-bin symlink state, never in settings).
3695
+ (cd "$REPO_DIR" && bash scripts/agy-statusline-bridge.sh install "$@")
3696
+ ;;
3697
+ uninstall-agy-statusline)
3698
+ # #46 Task 1: honest inverse of install-agy-statusline — restore the captured statusLine
3699
+ # subtree preimage (remove the key if absent, else set it back; remove the file if we created
3700
+ # it empty). Refuses if settings.json became a symlink since install; no state → nothing.
3701
+ (cd "$REPO_DIR" && bash scripts/agy-statusline-bridge.sh uninstall "$@")
3702
+ ;;
3703
+ doctor-agy-statusline)
3704
+ # #46 Task 1: fail-loud doctor. STATIC proves the SINGLE settings root agy reads statusLine
3705
+ # from (~/.gemini/antigravity-cli/settings.json — Task-1 capture, not the 2-candidate mcp
3706
+ # root) parses and carries OUR RESOLVABLE command (dangling FAILs, state drift FAILs). LIVE
3707
+ # proves runtime-effectiveness only with an agy process; else an honest SKIP.
3708
+ (cd "$REPO_DIR" && bash scripts/agy-statusline-bridge.sh doctor "$@")
3709
+ ;;
3710
+ install-agy-hooks)
3711
+ # #46 birth writer: install the Antigravity PreInvocation named hook that runs
3712
+ # entwurf-agy-imprint and returns {"injectSteps":[]}.
3713
+ (cd "$REPO_DIR" && bash scripts/agy-hooks-bridge.sh install "$@")
3714
+ ;;
3715
+ uninstall-agy-hooks)
3716
+ # Honest inverse of install-agy-hooks: restore/remove only our named hook.
3717
+ (cd "$REPO_DIR" && bash scripts/agy-hooks-bridge.sh uninstall "$@")
3718
+ ;;
3719
+ doctor-agy-hooks)
3720
+ # Fail-loud doctor for agy hooks.json imprint wiring.
3721
+ (cd "$REPO_DIR" && bash scripts/agy-hooks-bridge.sh doctor "$@")
3722
+ ;;
3723
+ doctor-pi-provider)
3724
+ # #46 Task 2: read-only fail-loud doctor for the pi provider ownership (entwurfProvider.
3725
+ # mcpServers.entwurf-bridge). Uses config.ts readProviderSettingsFile SSOT for the EFFECTIVE
3726
+ # (project-shadows-user) command — never a re-implemented merge. Reports user/project/effective,
3727
+ # gates on stable-bin resolvability, and distinguishes state-owned drift (FAIL) from an
3728
+ # unowned user override (honest note). No agy/pi process needed — pure settings inspection.
3729
+ run_ts scripts/doctor-pi-provider.ts "$@"
3730
+ ;;
3731
+ wire-agy-statusline)
3732
+ # #46 Task 1: the detection-gated, NON-FATAL setup wrapper around install-agy-statusline.
3733
+ # HIDDEN/internal — setup calls this; exposed so smoke-agy-statusline-state can drive it
3734
+ # deterministically. The hard gate stays doctor-agy-statusline.
3735
+ wire_agy_statusline
3736
+ ;;
3737
+ wire-agy-hooks)
3738
+ # #46 birth writer setup wrapper around install-agy-hooks.
3739
+ wire_agy_hooks
3740
+ ;;
3741
+ expose-dev-bin)
3742
+ # 막힘 ②: expose the entwurf-bridge STABLE bin on PATH for a DEV checkout (a managed symlink
3743
+ # into this checkout, recorded for an honest inverse). HIDDEN/internal — setup calls the
3744
+ # NON-FATAL expose_dev_bin wrapper; exposed here so smoke-agy-install-state can drive the
3745
+ # foreign-refuse WARN path. The exposure logic lives in scripts/dev-bin.sh.
3746
+ expose_dev_bin
3747
+ ;;
3748
+ remove-dev-bin)
3749
+ # 막힘 ②: honest inverse of expose-dev-bin — remove ONLY our managed link + state (REFUSE if
3750
+ # it became foreign). The raw script (no wrapper) so an operator sees a loud failure.
3751
+ (cd "$REPO_DIR" && bash scripts/dev-bin.sh remove "$@")
3752
+ ;;
3030
3753
  meta-bridge-prune)
3031
3754
  # 1.0.0 meta-bridge Phase 4: LISTING-ONLY janitor for the meta-session store.
3032
3755
  # doctor reds on corrupt/duplicate/drift but intentionally does NOT fail on
@@ -3037,7 +3760,7 @@ case "$cmd" in
3037
3760
  # surviving authority). Default store = defaultMetaSessionsDir(); pass [dir]
3038
3761
  # + [--ttl-days N] to override.
3039
3762
  shift || true
3040
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/meta-bridge-prune.ts "$@")
3763
+ run_ts scripts/meta-bridge-prune.ts "$@"
3041
3764
  ;;
3042
3765
  meta-bridge-managed-keys)
3043
3766
  # 0.10.0 meta-bridge: emit the SSOT of settings.json/~/.claude.json keys that
@@ -3134,6 +3857,9 @@ case "$cmd" in
3134
3857
  remove)
3135
3858
  remove_local_package "$TARGET_PROJECT_DIR"
3136
3859
  ;;
3860
+ remove-user-scope)
3861
+ remove_user_scope_citizen
3862
+ ;;
3137
3863
  -h|--help|help|"")
3138
3864
  usage
3139
3865
  ;;