@junghanacs/entwurf 0.12.6 → 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 (92) hide show
  1. package/AGENTS.md +31 -17
  2. package/BASELINE.md +42 -8
  3. package/CHANGELOG.md +22 -0
  4. package/DELIVERY.md +71 -14
  5. package/README.md +77 -45
  6. package/VERIFY.md +18 -13
  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 -5
  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 +756 -214
  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 +36 -2
  79. package/scripts/register-pi-package.py +37 -3
  80. package/scripts/register-pi-provider.py +287 -0
  81. package/scripts/smoke-agy-hooks-state.sh +172 -0
  82. package/scripts/smoke-agy-install-state.sh +660 -0
  83. package/scripts/smoke-agy-native-push-live.ts +243 -0
  84. package/scripts/smoke-agy-statusline-state.sh +300 -0
  85. package/scripts/smoke-meta-async-drift.sh +9 -2
  86. package/scripts/smoke-meta-install-state.sh +20 -0
  87. package/scripts/smoke-pi-provider-state.sh +182 -0
  88. package/scripts/smoke-user-scope-citizen.sh +62 -0
  89. package/scripts/with-dist-lock.sh +81 -0
  90. package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
  91. package/scripts/__pycache__/meta-bridge-state.cpython-313.pyc +0 -0
  92. package/scripts/__pycache__/register-pi-package.cpython-313.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,6 +126,9 @@ 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
94
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)
95
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)
96
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)
@@ -100,6 +138,15 @@ Usage:
100
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)
101
139
  ./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
102
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
103
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])
104
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)
105
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)
@@ -109,7 +156,8 @@ Usage:
109
156
  ./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
110
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
111
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)
112
- ./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)
113
161
 
114
162
  Notes:
115
163
  - project-dir defaults to current directory
@@ -299,82 +347,11 @@ install_local_package() {
299
347
  # packages[] registration via the shared SSOT — same is_entwurf_source
300
348
  # predicate + idempotency as user-scope and remove (not a substring match).
301
349
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR"
302
- # entwurfProvider.mcpServers + legacy prune (project-scope only; packages[] was
303
- # already written above by register-pi-package.py).
304
- python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
305
- import json, sys
306
- from pathlib import Path
307
-
308
- settings_path = Path(sys.argv[1])
309
- repo_dir = str(Path(sys.argv[2]).resolve())
310
- settings_path.parent.mkdir(parents=True, exist_ok=True)
311
- if settings_path.exists():
312
- data = json.loads(settings_path.read_text())
313
- if not isinstance(data, dict):
314
- raise SystemExit("settings.json is not an object")
315
- else:
316
- data = {}
317
-
318
- # --- entwurfProvider.mcpServers bundled entries ---------------------------
319
- # Ship the two in-repo MCP adapters pre-wired so `pi install` produces a
320
- # working setup without the consumer hand-editing settings.json. User-authored
321
- # overrides (different command/args) are preserved untouched.
322
- provider = data.setdefault("entwurfProvider", {})
323
- if not isinstance(provider, dict):
324
- raise SystemExit("entwurfProvider is not an object")
325
- servers = provider.setdefault("mcpServers", {})
326
- if not isinstance(servers, dict):
327
- raise SystemExit("entwurfProvider.mcpServers is not an object")
328
-
329
- BUNDLED = ("entwurf-bridge",)
330
- # 0.4.14: session-bridge MCP was retracted (issue #7 — unified entwurf surface).
331
- # Install path now only wires entwurf-bridge. Existing operator settings that
332
- # carry the old bundled session-bridge entry are pruned below during install;
333
- # `./run.sh remove` also keeps session-bridge in its cleanup tuple so legacy
334
- # entries are removed on uninstall too.
335
- for name in BUNDLED:
336
- desired_cmd = f"{repo_dir}/mcp/{name}/start.sh"
337
- desired = {"command": desired_cmd, "args": []}
338
- existing = servers.get(name)
339
- if existing is None:
340
- servers[name] = desired
341
- print(f"install: added entwurfProvider.mcpServers.{name}")
342
- elif isinstance(existing, dict) and existing.get("command") == desired_cmd:
343
- # Already managed by us at the current repo path. Add the default args
344
- # field when missing, but never overwrite user-customized args.
345
- if "args" not in existing:
346
- existing["args"] = []
347
- print(f"install: normalized entwurfProvider.mcpServers.{name}.args -> []")
348
- elif existing.get("args") != []:
349
- print(f"install: preserved entwurfProvider.mcpServers.{name}.args (custom args)")
350
- else:
351
- cmd_repr = existing.get("command") if isinstance(existing, dict) else existing
352
- print(f"install: preserved entwurfProvider.mcpServers.{name} (user override: {cmd_repr})")
353
-
354
- # Legacy-bundled MCP names that prior versions of this installer wrote and that
355
- # the current cutover supersedes. One-shot install-time prune (NOT a runtime
356
- # alias): only remove entries whose command matches the bundled start.sh path,
357
- # so user-customized commands are left alone.
358
- # - session-bridge: retracted in 0.4.14 (issue #7, unified entwurf surface).
359
- # - pi-tools-bridge: renamed to entwurf-bridge in 0.11 S2 cutover.
360
- LEGACY_BUNDLED = {
361
- "session-bridge": "retracted in 0.4.14, issue #7",
362
- "pi-tools-bridge": "renamed to entwurf-bridge in 0.11 S2 cutover",
363
- }
364
- for name, reason in LEGACY_BUNDLED.items():
365
- existing = servers.get(name)
366
- if not isinstance(existing, dict):
367
- continue
368
- cmd = existing.get("command")
369
- if not isinstance(cmd, str):
370
- continue
371
- if cmd == f"{repo_dir}/mcp/{name}/start.sh" or cmd.endswith(f"/entwurf/mcp/{name}/start.sh"):
372
- del servers[name]
373
- print(f"install: pruned legacy entwurfProvider.mcpServers.{name} ({reason})")
374
-
375
- settings_path.write_text(json.dumps(data, indent=2) + "\n")
376
- print(f"install: updated {settings_path}")
377
- PY
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
378
355
  ensure_agent_dir_symlinks
379
356
  register_user_scope_citizen
380
357
  }
@@ -393,6 +370,31 @@ register_user_scope_citizen() {
393
370
  local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
394
371
  # Shared idempotent implementation (also driven by smoke-user-scope-citizen).
395
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"
396
398
  }
397
399
 
398
400
  # Ensure agent-level resources that entwurf code reads from
@@ -488,55 +490,21 @@ remove_local_package() {
488
490
  # install, so remove never over-deletes a look-alike repo (entwurf-notes, …)
489
491
  # that install would never have registered.
490
492
  python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR" --remove
491
- python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
492
- import json, sys
493
- from pathlib import Path
494
-
495
- settings_path = Path(sys.argv[1])
496
- repo_dir = str(Path(sys.argv[2]).resolve())
497
- if not settings_path.exists():
498
- print(f"remove: nothing to do ({settings_path} missing)")
499
- raise SystemExit(0)
500
-
501
- data = json.loads(settings_path.read_text())
502
- if not isinstance(data, dict):
503
- raise SystemExit("settings.json is not an object")
504
-
505
- # --- entwurfProvider.mcpServers cleanup ------------------------------------
506
- # Only remove entries that look like they came from ./run.sh install: either
507
- # the command matches the current $REPO_DIR anchor exactly, or it ends with
508
- # the bundled "/entwurf/mcp/<name>/start.sh" pattern (covers a rebuilt
509
- # checkout under a different directory). Anything else is treated as a user
510
- # override and left in place.
511
- BUNDLED = ("entwurf-bridge", "session-bridge", "pi-tools-bridge")
512
- provider = data.get("entwurfProvider")
513
- mcp_removed = 0
514
- if isinstance(provider, dict):
515
- servers = provider.get("mcpServers")
516
- if isinstance(servers, dict):
517
- for name in BUNDLED:
518
- existing = servers.get(name)
519
- if not isinstance(existing, dict):
520
- continue
521
- cmd = existing.get("command")
522
- if not isinstance(cmd, str):
523
- continue
524
- exact = cmd == f"{repo_dir}/mcp/{name}/start.sh"
525
- pattern = cmd.endswith(f"/entwurf/mcp/{name}/start.sh")
526
- if exact or pattern:
527
- del servers[name]
528
- mcp_removed += 1
529
- print(f"remove: removed entwurfProvider.mcpServers.{name}")
530
- else:
531
- print(f"remove: preserved entwurfProvider.mcpServers.{name} (user override: {cmd})")
532
- if not servers:
533
- provider.pop("mcpServers", None)
534
- if not provider:
535
- data.pop("entwurfProvider", None)
536
-
537
- settings_path.write_text(json.dumps(data, indent=2) + "\n")
538
- print(f"remove: removed {mcp_removed} mcpServers entries from {settings_path}")
539
- PY
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
540
508
  }
541
509
 
542
510
  check_model_lock() {
@@ -544,7 +512,7 @@ check_model_lock() {
544
512
  # No pi process, no network, no API cost. Mocks ExtensionAPI/Context and
545
513
  # drives the model_select handler through every quadrant + edge case
546
514
  # (see scripts/check-model-lock.ts header for the full matrix).
547
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-model-lock.ts)
515
+ run_ts scripts/check-model-lock.ts
548
516
  }
549
517
 
550
518
  check_shell_quote() {
@@ -553,7 +521,7 @@ check_shell_quote() {
553
521
  # across the two duplication sites AND behavioral correctness on the
554
522
  # payload classes that caused the 2026-05-18 remote entwurf incident
555
523
  # (backtick / $(...) / $VAR / korean tokens). No process spawn, no SSH.
556
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-shell-quote.ts)
524
+ run_ts scripts/check-shell-quote.ts
557
525
  }
558
526
 
559
527
  check_entwurf_session_identity() {
@@ -563,7 +531,7 @@ check_entwurf_session_identity() {
563
531
  # models, titleSlug canonicalization, registry exact-tuple membership, name=
564
532
  # info-only invariants, and header-scan collision pre-check. Isolates registry
565
533
  # + sessions base to a temp dir. No backend, no API, no spawn.
566
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-session-identity.ts)
534
+ run_ts scripts/check-entwurf-session-identity.ts
567
535
  }
568
536
 
569
537
  check_meta_session() {
@@ -573,7 +541,7 @@ check_meta_session() {
573
541
  # real temp dir), idempotent existence-keyed decideUpsert + identity-drift
574
542
  # refusal, and the pre-drilled read-receipt mutators. Pure functions; no
575
543
  # backend, no hook, no API.
576
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-session.ts)
544
+ run_ts scripts/check-meta-session.ts
577
545
  }
578
546
 
579
547
  check_meta_record_v2() {
@@ -583,7 +551,7 @@ check_meta_record_v2() {
583
551
  # and v2 field-contract crashes. Reader/normalizer only — no v2 writer yet.
584
552
  # Kept separate from check-meta-session so 3D's v1-gate rewrites leave this
585
553
  # back-compat golden untouched. Pure functions; no backend, no hook, no API.
586
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-record-v2.ts)
554
+ run_ts scripts/check-meta-record-v2.ts
587
555
  }
588
556
 
589
557
  check_mailbox_receipt_state() {
@@ -593,7 +561,7 @@ check_mailbox_receipt_state() {
593
561
  # keyset, then the fs store (stamp → persist → read-back) in a temp mailbox
594
562
  # dir. Schema/store only — no live enqueue/read dual-write (that is 3D). No
595
563
  # backend, no hook, no API.
596
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-mailbox-receipt-state.ts)
564
+ run_ts scripts/check-mailbox-receipt-state.ts
597
565
  }
598
566
 
599
567
  check_entwurf_capabilities() {
@@ -604,7 +572,7 @@ check_entwurf_capabilities() {
604
572
  # META_BACKEND_DESCRIPTORS for the three existing backends (drift guard), and
605
573
  # strict keyset/coverage/field crashes. Parser/gate only — no live routing,
606
574
  # no record/descriptor consumer change (that is 3D). No backend, no API.
607
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-capabilities.ts)
575
+ run_ts scripts/check-entwurf-capabilities.ts
608
576
  }
609
577
 
610
578
  check_meta_dual_read() {
@@ -614,7 +582,7 @@ check_meta_dual_read() {
614
582
  # unknown-version crash. Pure functions only — no fs upsert wiring, no
615
583
  # readMetaInbox/enqueueMetaMessage change, no record.delivery removal (3D-2/3/4).
616
584
  # No backend, no hook, no API.
617
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-read.ts)
585
+ run_ts scripts/check-meta-dual-read.ts
618
586
  }
619
587
 
620
588
  check_meta_mailbox_state_write() {
@@ -626,7 +594,7 @@ check_meta_mailbox_state_write() {
626
594
  # carries lastEnqueuedAt/lastReadAt with field isolation, lastDeliveredAt is never
627
595
  # invented, an empty inbox is a no-op on BOTH record and state (⑥), and a state
628
596
  # drift surfaces fail-loud. v2 citizen seeded via upsertMetaSession. No API.
629
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-mailbox-state-write.ts)
597
+ run_ts scripts/check-meta-mailbox-state-write.ts
630
598
  }
631
599
 
632
600
  check_meta_receiver_marker() {
@@ -639,7 +607,7 @@ check_meta_receiver_marker() {
639
607
  # from "no marker"); armProvenance constrained to the arm-capable events so
640
608
  # UserPromptSubmit cannot mint presence; reader does NOT gate on record existence
641
609
  # (recordBacked is the deliverability predicate's fact). Real tmpdir, no API.
642
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-receiver-marker.ts)
610
+ run_ts scripts/check-meta-receiver-marker.ts
643
611
  }
644
612
 
645
613
  check_meta_migration() {
@@ -649,7 +617,7 @@ check_meta_migration() {
649
617
  # state.json). Crash-order: a v1 record's receipts migrate to state BEFORE the v2
650
618
  # rewrite (proven via upsert attach), and a drift'd state makes migrate throw with
651
619
  # the record STILL v1 (recoverable: next attach re-migrates). Temp dir, no API.
652
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-migration.ts)
620
+ run_ts scripts/check-meta-migration.ts
653
621
  }
654
622
 
655
623
  check_meta_dual_consumers() {
@@ -661,7 +629,7 @@ check_meta_dual_consumers() {
661
629
  # authority ambiguity → throw, so the v2 upsert never duplicate-mints) + v1 normalize +
662
630
  # body/filename drift fail-fast. The v1-only raw readers remain for v1-fixture gates.
663
631
  # Temp dir, no API.
664
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-dual-consumers.ts)
632
+ run_ts scripts/check-meta-dual-consumers.ts
665
633
  }
666
634
 
667
635
  check_meta_capability_source() {
@@ -674,7 +642,7 @@ check_meta_capability_source() {
674
642
  # 3 backends). The record.delivery.wakeMode SLOT stays (removal is 3D-4); only the
675
643
  # SOURCE moves. check-entwurf-capabilities still owns the registry ≡ const drift
676
644
  # guard. Pure — no fs writes, no backend, no hook, no API.
677
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-capability-source.ts)
645
+ run_ts scripts/check-meta-capability-source.ts
678
646
  }
679
647
 
680
648
  check_socket_probe() {
@@ -685,7 +653,7 @@ check_socket_probe() {
685
653
  # invariant. Listing lists alive only. Pure classify + GC/listing policy +
686
654
  # a two-socket integration (live listener → alive survives; nonexistent →
687
655
  # dead GC-eligible). No wire timeout fixture, no backend, no API.
688
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-probe.ts)
656
+ run_ts scripts/check-socket-probe.ts
689
657
  }
690
658
 
691
659
  check_project_trust_handler() {
@@ -697,7 +665,7 @@ check_project_trust_handler() {
697
665
  # never undefined. Plus the thin adapter: fake ctx.ui.select, single-writer
698
666
  # (handler never calls store.set — pi persists on remember:true), F5a evidence
699
667
  # in the prompt title. No real UI, no backend, no API.
700
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-project-trust-handler.ts)
668
+ run_ts scripts/check-project-trust-handler.ts
701
669
  }
702
670
 
703
671
  check_entwurf_v2_contract() {
@@ -711,7 +679,7 @@ check_entwurf_v2_contract() {
711
679
  # Q2 owned-outcome+live never auto-sends. R5 taxonomy covers table reasons +
712
680
  # pre-claims bad-target/untrusted-fail-fast/target-locked (bucket B F2). Plus
713
681
  # a schema↔types drift guard on the TypeBox input/receipt. Pure, no API.
714
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-contract.ts)
682
+ run_ts scripts/check-entwurf-v2-contract.ts
715
683
  }
716
684
 
717
685
  check_entwurf_v2_lock() {
@@ -726,7 +694,7 @@ check_entwurf_v2_lock() {
726
694
  # fail-closed to conflict. Empty/corrupt lockfile = conflict, never
727
695
  # auto-deleted. F2-P1: a malformed gid throws before any path is built. Real
728
696
  # temp dir (wx atomicity under test); clock/nonce/pid/host/kill injected.
729
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-lock.ts)
697
+ run_ts scripts/check-entwurf-v2-lock.ts
730
698
  }
731
699
 
732
700
  check_entwurf_v2_decider() {
@@ -740,7 +708,7 @@ check_entwurf_v2_decider() {
740
708
  # takes NO lock (?7); resume plan has no mode/wantsReply/provider/model but has
741
709
  # expectedSocketPath/observeTimeoutMs/releaseWhen; an invalid gid throws before
742
710
  # any lookup (F2-P1). Pure, no IO, no API.
743
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-decider.ts)
711
+ run_ts scripts/check-entwurf-v2-decider.ts
744
712
  }
745
713
 
746
714
  check_entwurf_v2_matrix() {
@@ -754,7 +722,7 @@ check_entwurf_v2_matrix() {
754
722
  # FAILS if any transport / lock class / pre-probe reject is missing — a dropped
755
723
  # decider cell cannot pass silently. Thin coverage, NOT a decider re-impl; surface
756
724
  # parity stays in check-entwurf-v2-surface. Pure, no IO, no API.
757
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-matrix.ts)
725
+ run_ts scripts/check-entwurf-v2-matrix.ts
758
726
  }
759
727
 
760
728
  check_entwurf_v2_release() {
@@ -766,7 +734,7 @@ check_entwurf_v2_release() {
766
734
  # release on the FIRST observed transition (socket-alive ∨ child-exited any code)
767
735
  # or a failed start; socket↔exit race idempotent (single release either order);
768
736
  # decideReleasePolicy enforces the lock-nullness invariant (?7). Pure, no IO.
769
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-release.ts)
737
+ run_ts scripts/check-entwurf-v2-release.ts
770
738
  }
771
739
 
772
740
  check_entwurf_v2_send() {
@@ -778,7 +746,7 @@ check_entwurf_v2_send() {
778
746
  # indeterminate->failed+rethrow with deadFallback+mailbox NEVER called (no
779
747
  # double-delivery on an alive-but-stalled socket). Release fires exactly once per
780
748
  # send-final; a releaseLock throw never masks the send failure (5b). IO-via-dep.
781
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send.ts)
749
+ run_ts scripts/check-entwurf-v2-send.ts
782
750
  }
783
751
 
784
752
  check_entwurf_v2_send_fallback() {
@@ -791,7 +759,7 @@ check_entwurf_v2_send_fallback() {
791
759
  # / bad-target + address-conflict->reject pre-probe. Mis-wire (plan/lock gid) fails loud
792
760
  # before IO; inspect/probe throws PROPAGATE (the hand owns failed+release); the resolver
793
761
  # has NO release seam; every execute plan keeps the held gid and is never spawn-bg.
794
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-send-fallback.ts)
762
+ run_ts scripts/check-entwurf-v2-send-fallback.ts
795
763
  }
796
764
 
797
765
  check_entwurf_v2_mailbox() {
@@ -808,7 +776,19 @@ check_entwurf_v2_mailbox() {
808
776
  # every access throws still resolves). Source guard: the lib code has NO release seam
809
777
  # and NO routing seam (no releaseLock / inspect / probe / resolve) — a lock leak or
810
778
  # re-route is structurally impossible.
811
- (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
812
792
  }
813
793
 
814
794
  check_entwurf_v2_runner() {
@@ -822,7 +802,7 @@ check_entwurf_v2_runner() {
822
802
  # SendDeliveredReleaseFailedError -> execution-failed{finalizedOutcome, releaseFailed,
823
803
  # retrySafe:false}; a plain hand throw -> execution-failed{retrySafe:false} with no
824
804
  # finalizedOutcome. Exactly one hand runs per execute.
825
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-runner.ts)
805
+ run_ts scripts/check-entwurf-v2-runner.ts
826
806
  }
827
807
 
828
808
  check_entwurf_v2_surface() {
@@ -833,7 +813,7 @@ check_entwurf_v2_surface() {
833
813
  # lock-retained, N1 delivered-but-dirty) / surface ctx-free source guard / entwurf-control
834
814
  # registers entwurf_v2 + reaches the fence via a NON-LITERAL dynamic import (no static fence
835
815
  # import → TS5097 stays closed) + decorates sender origin:pi-session/replyable:true.
836
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-surface.ts)
816
+ run_ts scripts/check-entwurf-v2-surface.ts
837
817
  }
838
818
 
839
819
  check_entwurf_bridge_boot() {
@@ -844,7 +824,7 @@ check_entwurf_bridge_boot() {
844
824
  # registered on the runtime surface with its schema (G1b). tools/list only → no tools/call,
845
825
  # no lock/fs side effect, no auth → safe in pnpm check. Broad protocol/negative suite stays
846
826
  # in check-bridge/test.sh (D1=A안).
847
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-boot.ts)
827
+ run_ts scripts/check-entwurf-bridge-boot.ts
848
828
  }
849
829
 
850
830
  check_entwurf_bridge_pi_free() {
@@ -855,7 +835,7 @@ check_entwurf_bridge_pi_free() {
855
835
  # value-imports @earendil-works/pi-*. Type-only imports and dynamic `await import()`
856
836
  # (the intended lazy preflight boundary) are excluded — the runtime boot smoke is the
857
837
  # final authority that peers/self/list/mailbox-deliver come up pi-free.
858
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-bridge-pi-free.ts)
838
+ run_ts scripts/check-entwurf-bridge-pi-free.ts
859
839
  }
860
840
 
861
841
  check_entwurf_v2_production() {
@@ -866,7 +846,7 @@ check_entwurf_v2_production() {
866
846
  # lockDir / QB3 the spawn watcher releases via the SHARED lockDir release (not the spawn
867
847
  # factory default) / the mailbox hand enqueues onto the wired dirs / a dead control send
868
848
  # re-resolves to the SAME sendViaMailbox instance on the SAME dirs (Q3+Q5 no drift).
869
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-production.ts)
849
+ run_ts scripts/check-entwurf-v2-production.ts
870
850
  }
871
851
 
872
852
  check_entwurf_control_rpc() {
@@ -877,7 +857,7 @@ check_entwurf_control_rpc() {
877
857
  # sendRpcCommand from the lib and no longer defines its own / real short unix-socket round-trip
878
858
  # (write command -> matched {type:response,command,success:true} -> resolve) / close-before-
879
859
  # response rejects 'connection closed before response'. net.Server only, no model/pi process.
880
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-control-rpc.ts)
860
+ run_ts scripts/check-entwurf-control-rpc.ts
881
861
  }
882
862
 
883
863
  check_entwurf_v2_spawn() {
@@ -891,7 +871,7 @@ check_entwurf_v2_spawn() {
891
871
  # obtainable (grace elapses, or a post-spawn watch dep throws and the exit can't be
892
872
  # observed) -> lock-retained fail-closed (released:false, pid/socket/lockPath surfaced) —
893
873
  # there is NO direct-release hatch; deps.releaseLock is reached ONLY via reduceRelease.
894
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn.ts)
874
+ run_ts scripts/check-entwurf-v2-spawn.ts
895
875
  }
896
876
 
897
877
  check_entwurf_resume_args() {
@@ -904,7 +884,7 @@ check_entwurf_resume_args() {
904
884
  # and the prompt-as-turn positional (`-p` NOT dropped in v2); explicitExtensionArgs
905
885
  # preserved exactly once (provider-resolution footgun #29); v2 includes plan.launchArgs
906
886
  # (--approve) before the prompt; null provider emits no --provider; no cross-contamination.
907
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-resume-args.ts)
887
+ run_ts scripts/check-entwurf-resume-args.ts
908
888
  }
909
889
 
910
890
  check_entwurf_v2_spawn_production() {
@@ -918,7 +898,7 @@ check_entwurf_v2_spawn_production() {
918
898
  # (symlink) rejects without connecting, dead→wait→alive, abort clears the sleep. awaitChildExit
919
899
  # resolves the code + removes the listener on abort. awaitTimeout schedules + abort-clears.
920
900
  # killChild=SIGTERM; releaseLock delegates; a proc-less child fails loud (mis-wire).
921
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-v2-spawn-production.ts)
901
+ run_ts scripts/check-entwurf-v2-spawn-production.ts
922
902
  }
923
903
 
924
904
 
@@ -934,7 +914,7 @@ smoke_entwurf_v2_spawn_live() {
934
914
  echo "[smoke-entwurf-v2-spawn-live] skipped — set LIVE=1 to run (spawns real children + opens a real unix socket)."
935
915
  return 0
936
916
  fi
937
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-spawn-live.ts)
917
+ run_ts scripts/smoke-entwurf-v2-spawn-live.ts
938
918
  }
939
919
 
940
920
  smoke_acp_socket_citizen_live() {
@@ -947,7 +927,7 @@ smoke_acp_socket_citizen_live() {
947
927
  # citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
948
928
  # Model override: ENTWURF_S1_MODEL (default claude-opus-4-8).
949
929
  # LIVE=1 ./run.sh smoke-acp-socket-citizen-live
950
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-socket-citizen-live.ts)
930
+ run_ts scripts/smoke-acp-socket-citizen-live.ts
951
931
  }
952
932
 
953
933
  smoke_acp_raw_turn_live() {
@@ -961,7 +941,7 @@ smoke_acp_raw_turn_live() {
961
941
  # fails acceptance unless ENTWURF_ACP_RAW_TURN_ALLOW_PATH_FALLBACK=1, debug).
962
942
  # Model override: ENTWURF_ACP_RAW_TURN_MODEL (default claude-sonnet-5).
963
943
  # LIVE=1 ./run.sh smoke-acp-raw-turn-live
964
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-raw-turn-live.ts)
944
+ run_ts scripts/smoke-acp-raw-turn-live.ts
965
945
  }
966
946
 
967
947
  smoke_acp_overlay_live() {
@@ -979,7 +959,7 @@ smoke_acp_overlay_live() {
979
959
  # fallback fails acceptance unless ENTWURF_ACP_OVERLAY_ALLOW_PATH_FALLBACK=1).
980
960
  # Model override: ENTWURF_ACP_OVERLAY_MODEL (default claude-sonnet-5).
981
961
  # LIVE=1 ./run.sh smoke-acp-overlay-live
982
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-overlay-live.ts)
962
+ run_ts scripts/smoke-acp-overlay-live.ts
983
963
  }
984
964
 
985
965
  smoke_acp_memory_containment_live() {
@@ -994,7 +974,7 @@ smoke_acp_memory_containment_live() {
994
974
  # fallback fails acceptance unless ENTWURF_ACP_MEMORY_ALLOW_PATH_FALLBACK=1).
995
975
  # Model override: ENTWURF_ACP_MEMORY_MODEL (default claude-sonnet-5).
996
976
  # LIVE=1 ./run.sh smoke-acp-memory-containment-live
997
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-memory-containment-live.ts)
977
+ run_ts scripts/smoke-acp-memory-containment-live.ts
998
978
  }
999
979
 
1000
980
  smoke_acp_provider_live() {
@@ -1009,7 +989,7 @@ smoke_acp_provider_live() {
1009
989
  # event-mapper gate owns the tool→notice contract.
1010
990
  # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
1011
991
  # LIVE=1 ./run.sh smoke-acp-provider-live
1012
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-provider-live.ts)
992
+ run_ts scripts/smoke-acp-provider-live.ts
1013
993
  }
1014
994
 
1015
995
  smoke_acp_session_reuse_live() {
@@ -1023,7 +1003,7 @@ smoke_acp_session_reuse_live() {
1023
1003
  # smoke-acp-provider-live.
1024
1004
  # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
1025
1005
  # LIVE=1 ./run.sh smoke-acp-session-reuse-live
1026
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-session-reuse-live.ts)
1006
+ run_ts scripts/smoke-acp-session-reuse-live.ts
1027
1007
  }
1028
1008
 
1029
1009
  smoke_acp_carrier_augment_live() {
@@ -1035,7 +1015,7 @@ smoke_acp_carrier_augment_live() {
1035
1015
  # check via SMOKE_ACP_CARRIER_PRESENT=1 (non-blocking).
1036
1016
  # Model override: ENTWURF_ACP_PROVIDER_MODEL (default claude-sonnet-5).
1037
1017
  # LIVE=1 ./run.sh smoke-acp-carrier-augment-live
1038
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-carrier-augment-live.ts)
1018
+ run_ts scripts/smoke-acp-carrier-augment-live.ts
1039
1019
  }
1040
1020
 
1041
1021
  smoke_acp_mcp_live() {
@@ -1048,7 +1028,7 @@ smoke_acp_mcp_live() {
1048
1028
  # fix). Isolated probe (not entwurf-bridge) so a failure does not blur into
1049
1029
  # identity/env wiring. Model override: ENTWURF_ACP_PROVIDER_MODEL.
1050
1030
  # LIVE=1 ./run.sh smoke-acp-mcp-live
1051
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-mcp-live.ts)
1031
+ run_ts scripts/smoke-acp-mcp-live.ts
1052
1032
  }
1053
1033
 
1054
1034
  smoke_acp_skill_live() {
@@ -1060,7 +1040,7 @@ smoke_acp_skill_live() {
1060
1040
  # the Skill/Skill(*) auto-add reach the live session (the other half of the GLG
1061
1041
  # baseline). Model override: ENTWURF_ACP_PROVIDER_MODEL.
1062
1042
  # LIVE=1 ./run.sh smoke-acp-skill-live
1063
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-skill-live.ts)
1043
+ run_ts scripts/smoke-acp-skill-live.ts
1064
1044
  }
1065
1045
 
1066
1046
  smoke_acp_bundled_mcp_live() {
@@ -1076,7 +1056,7 @@ smoke_acp_bundled_mcp_live() {
1076
1056
  # diagnostic backlog, not the 0.11.0 release circuit). Model override:
1077
1057
  # ENTWURF_ACP_PROVIDER_MODEL.
1078
1058
  # LIVE=1 ./run.sh smoke-acp-bundled-mcp-live
1079
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-acp-bundled-mcp-live.ts)
1059
+ run_ts scripts/smoke-acp-bundled-mcp-live.ts
1080
1060
  }
1081
1061
 
1082
1062
  smoke_acp_rgg_live() {
@@ -1113,7 +1093,7 @@ smoke_entwurf_v2_matrix_live() {
1113
1093
  echo "[smoke-entwurf-v2-matrix-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control + opens a real socket)."
1114
1094
  return 0
1115
1095
  fi
1116
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-entwurf-v2-matrix-live.ts)
1096
+ run_ts scripts/smoke-entwurf-v2-matrix-live.ts
1117
1097
  }
1118
1098
 
1119
1099
  smoke_entwurf_v2_spawn_resume_live() {
@@ -1135,7 +1115,7 @@ smoke_entwurf_v2_spawn_resume_live() {
1135
1115
  echo "[smoke-entwurf-v2-spawn-resume-live] skipped — set LIVE=1 to run (spawns a real pi resume child + opens a real socket)."
1136
1116
  return 0
1137
1117
  fi
1138
- (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
1139
1119
  }
1140
1120
 
1141
1121
  check_entwurf_facts() {
@@ -1147,7 +1127,7 @@ check_entwurf_facts() {
1147
1127
  # (no proof ≠ dead). facts-only keyset: identity facts + liveness, NO
1148
1128
  # verb-routing (resumable/sendable/transport/dispatch/action) and NO
1149
1129
  # transcriptPath (동결결정 10). Pure, no IO, no API.
1150
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-facts.ts)
1130
+ run_ts scripts/check-entwurf-facts.ts
1151
1131
  }
1152
1132
 
1153
1133
  check_socket_discovery() {
@@ -1159,7 +1139,7 @@ check_socket_discovery() {
1159
1139
  # indeterminate (F3), never folded to dead by an alive-only listing. Dir
1160
1140
  # hygiene (non-.sock / malformed names ignored), dedup, missing-dir, sort, and
1161
1141
  # an end-to-end scanSocketProbes→resolveFactList. readdir/probe injected, no IO.
1162
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-socket-discovery.ts)
1142
+ run_ts scripts/check-socket-discovery.ts
1163
1143
  }
1164
1144
 
1165
1145
  check_meta_listing() {
@@ -1170,7 +1150,7 @@ check_meta_listing() {
1170
1150
  # becomes an explicit error carrying ONLY {filename, message}, verbatim (a
1171
1151
  # salvaged gid string as a fact = synthetic backdoor). mode strict throws on
1172
1152
  # any error, collect returns partial. entries/readRecord injected, no IO.
1173
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-meta-listing.ts)
1153
+ run_ts scripts/check-meta-listing.ts
1174
1154
  }
1175
1155
 
1176
1156
  check_entwurf_fact_provider() {
@@ -1182,7 +1162,7 @@ check_entwurf_fact_provider() {
1182
1162
  # survives; impossible wiring invariant (resolveFactList duplicate/unprobed) →
1183
1163
  # throw, never swallowed. A collision quarantines BOTH the PeerFact and the
1184
1164
  # socket (gid is the universal address). meta + socket deps injected, no IO.
1185
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-fact-provider.ts)
1165
+ run_ts scripts/check-entwurf-fact-provider.ts
1186
1166
  }
1187
1167
 
1188
1168
  check_entwurf_peers_surface() {
@@ -1196,7 +1176,7 @@ check_entwurf_peers_surface() {
1196
1176
  # diagnostics in both surfaces; empty → "(none)"; unsupported shown; enrich null
1197
1177
  # → "(not enriched)". WIRING guard: bridge calls listEntwurfFacts+renderEntwurfPeers,
1198
1178
  # getLiveSessions gone. Facts fabricated, no IO (only static source read).
1199
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-peers-surface.ts)
1179
+ run_ts scripts/check-entwurf-peers-surface.ts
1200
1180
  }
1201
1181
 
1202
1182
 
@@ -1213,7 +1193,7 @@ check_entwurf_self_address() {
1213
1193
  # probes the socket, and entwurf_self renders alive vs expected (no path lie).
1214
1194
  # Slice boundary: meta watchArmed is wired from the slice-2 presence marker; do NOT
1215
1195
  # claim slice 1 green standalone (1+2 close in the same release block).
1216
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-self-address.ts)
1196
+ run_ts scripts/check-entwurf-self-address.ts
1217
1197
  }
1218
1198
 
1219
1199
  check_entwurf_deliverability() {
@@ -1225,7 +1205,7 @@ check_entwurf_deliverability() {
1225
1205
  # (pi) refused even when active (SE-1, no mailbox drain), self-fetch + dead-owner/unarmed
1226
1206
  # refused (SE-2, would rot); WIRING that the self-addressability predicate shares the
1227
1207
  # SAME active-receiver atom (one source of truth). Pure, no IO.
1228
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-entwurf-deliverability.ts)
1208
+ run_ts scripts/check-entwurf-deliverability.ts
1229
1209
  }
1230
1210
 
1231
1211
  check_entwurf_mailbox_guard() {
@@ -1236,7 +1216,47 @@ check_entwurf_mailbox_guard() {
1236
1216
  # one calls it exactly once; TMPDIR SNAPSHOT with the real enqueueMetaMessage — a refused
1237
1217
  # send leaves the mailbox tree byte-identical (file list + content hash, not just mtime),
1238
1218
  # an accepted send writes exactly one .msg; plus fact gathering from record/capability/marker.
1239
- (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
1240
1260
  }
1241
1261
 
1242
1262
 
@@ -1251,7 +1271,7 @@ check_package_source_routing() {
1251
1271
  # plus self-root fallback and the resume unresolvedAcpIntent signal. Isolated
1252
1272
  # via a temp PI_CODING_AGENT_DIR — the real ~/.pi/agent is never touched. No
1253
1273
  # backend, no spawn, no API cost.
1254
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-package-source-routing.ts)
1274
+ run_ts scripts/check-package-source-routing.ts
1255
1275
  }
1256
1276
 
1257
1277
 
@@ -1267,7 +1287,7 @@ smoke_session_id_name() {
1267
1287
  fail "[smoke-session-id-name] pi binary not on PATH — cannot run live substrate proof"
1268
1288
  return 1
1269
1289
  fi
1270
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/smoke-session-id-name.ts) || {
1290
+ run_ts scripts/smoke-session-id-name.ts || {
1271
1291
  fail "[smoke-session-id-name] live substrate smoke failed"
1272
1292
  return 1
1273
1293
  }
@@ -1608,7 +1628,7 @@ check_pi_preflight() {
1608
1628
  # decision 8 precedence (saved false > saved true > prefix > no-inputs >
1609
1629
  # fail-fast) and decision 7's separator-boundary prefix against pi's own
1610
1630
  # ProjectTrustStore in a temp agentDir. Deterministic, no network/backend.
1611
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-pi-preflight.ts)
1631
+ run_ts scripts/check-pi-preflight.ts
1612
1632
  }
1613
1633
 
1614
1634
 
@@ -1663,7 +1683,7 @@ check_acp_provider_surface() {
1663
1683
  # sentinel shape, and a FAIL-LOUD streamSimple (calling it throws — no native
1664
1684
  # fallback, no empty-but-successful stream). Pure, no pi runtime, no API.
1665
1685
  section "ACP provider surface (S0 loader/fence)"
1666
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-provider-surface.ts)
1686
+ run_ts scripts/check-acp-provider-surface.ts
1667
1687
  }
1668
1688
 
1669
1689
  check_acp_sdk_surface() {
@@ -1675,7 +1695,7 @@ check_acp_sdk_surface() {
1675
1695
  # turn needs (silent-rename gate), and forbids any source-level anthropic SDK
1676
1696
  # import / API-client use (the anthropic dep is a peer-pin ONLY).
1677
1697
  section "ACP SDK surface (S2a dep pin + peer-resolution + no-client-use)"
1678
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-sdk-surface.ts)
1698
+ run_ts scripts/check-acp-sdk-surface.ts
1679
1699
  }
1680
1700
 
1681
1701
  check_acp_overlay() {
@@ -1688,7 +1708,7 @@ check_acp_overlay() {
1688
1708
  # stale symlinks cleaned; binary-owned files preserved; CLAUDE_CONFIG_DIR
1689
1709
  # launch-env planted; idempotent. Pure, no live model.
1690
1710
  section "ACP overlay (S2b claude-config-overlay)"
1691
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-overlay.ts)
1711
+ run_ts scripts/check-acp-overlay.ts
1692
1712
  }
1693
1713
 
1694
1714
  check_acp_tool_surface() {
@@ -1699,7 +1719,7 @@ check_acp_tool_surface() {
1699
1719
  # extraArgs/plugins) + the S2b billing-carrier guard (no _meta.systemPrompt
1700
1720
  # unless a caller supplies one). Pure preflight — NOT a backend wire read.
1701
1721
  section "ACP tool surface (S2b exclude-tools preflight + session meta)"
1702
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-tool-surface.ts)
1722
+ run_ts scripts/check-acp-tool-surface.ts
1703
1723
  }
1704
1724
 
1705
1725
  check_acp_event_mapper() {
@@ -1711,7 +1731,7 @@ check_acp_event_mapper() {
1711
1731
  # context→ACP-prompt transcript passthrough (excludes systemPrompt/thinking,
1712
1732
  # single text block). Pure, no live backend.
1713
1733
  section "ACP event mapper (S2c notification→stream + context)"
1714
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-event-mapper.ts)
1734
+ run_ts scripts/check-acp-event-mapper.ts
1715
1735
  }
1716
1736
 
1717
1737
  check_acp_prompt_builder() {
@@ -1722,7 +1742,7 @@ check_acp_prompt_builder() {
1722
1742
  # excluded so a reuse session is not re-injected its own history). Pure, no
1723
1743
  # session store yet — locks the builder before S2d wires the reuse paths.
1724
1744
  section "ACP prompt builder (S2d bootstrapPath prompt scope)"
1725
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-prompt-builder.ts)
1745
+ run_ts scripts/check-acp-prompt-builder.ts
1726
1746
  }
1727
1747
 
1728
1748
  check_acp_config() {
@@ -1735,7 +1755,7 @@ check_acp_config() {
1735
1755
  # headers, and envelope enrich (PI_SESSION_ID/PI_AGENT_ID into entwurf-bridge
1736
1756
  # only, stale filtered, post-hash). Pure + temp-dir settings I/O, no child/spawn.
1737
1757
  section "ACP provider config (S2g operator mcpServers/skillPlugins/tools passthrough)"
1738
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-config.ts)
1758
+ run_ts scripts/check-acp-config.ts
1739
1759
  }
1740
1760
 
1741
1761
  check_acp_session_store() {
@@ -1746,7 +1766,7 @@ check_acp_session_store() {
1746
1766
  # (turn-scoped/-p one-shot is ALWAYS new — no in-memory reuse, no persisted
1747
1767
  # resume/load in the first cut). Pure + temp-dir record I/O, no child/spawn.
1748
1768
  section "ACP session store (S2d-1b-1 signature/compat/bootstrap decision)"
1749
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-store.ts)
1769
+ run_ts scripts/check-acp-session-store.ts
1750
1770
  }
1751
1771
 
1752
1772
  check_acp_backend_preflight() {
@@ -1757,7 +1777,7 @@ check_acp_backend_preflight() {
1757
1777
  # assertExcludeToolsHonored is wired into the live provider path, not just the
1758
1778
  # pure gate. No backend launched (preflight throws first). Pure.
1759
1779
  section "ACP backend preflight (S2c runtime exclude-tools wiring)"
1760
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-backend-preflight.ts)
1780
+ run_ts scripts/check-acp-backend-preflight.ts
1761
1781
  }
1762
1782
 
1763
1783
  check_acp_session_reuse() {
@@ -1770,7 +1790,7 @@ check_acp_session_reuse() {
1770
1790
  # never torn down between turns, and source-locks buildAcpPrompt wiring +
1771
1791
  # single-site applyAcpSessionUpdate via the router. No real child launched.
1772
1792
  section "ACP session reuse (S2d-1b-2b delta-only capture + mutable routing)"
1773
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-session-reuse.ts)
1793
+ run_ts scripts/check-acp-session-reuse.ts
1774
1794
  }
1775
1795
 
1776
1796
  check_acp_carrier_augment() {
@@ -1782,7 +1802,7 @@ check_acp_carrier_augment() {
1782
1802
  # so it never enters contextMessageSignatures — with entwurf cwd/AGENTS.md
1783
1803
  # de-dup. Pure + temp-dir fs, no spawn.
1784
1804
  section "ACP carrier + augment (S2d-1c engraving + first-user augment)"
1785
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/check-acp-carrier-augment.ts)
1805
+ run_ts scripts/check-acp-carrier-augment.ts
1786
1806
  }
1787
1807
 
1788
1808
  check_pack() {
@@ -1808,7 +1828,11 @@ check_pack() {
1808
1828
  # npm's own lifecycle banner stay off stdout; otherwise they pollute the --json
1809
1829
  # payload this parses. prepack runs on dry-run too, which is how dist lands in
1810
1830
  # this gate's file list.
1811
- 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) || {
1812
1836
  fail "[check-pack] npm pack --dry-run failed"
1813
1837
  return 1
1814
1838
  }
@@ -1867,6 +1891,14 @@ check_pack() {
1867
1891
  # doctor.sh runs this prebuilt JS when installed under node_modules (strip-types
1868
1892
  # refuses the .ts there), same boundary start.sh crosses. build-bridge emits it.
1869
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"
1870
1902
  # 0.12.5 — the node_modules-safe plugin hook + its lib. install-meta-bridge copies
1871
1903
  # these compiled JS into the assembled plugin when installed (raw .ts can't
1872
1904
  # strip-types under node_modules). meta-session.js is shared with the store-doctor
@@ -1904,6 +1936,13 @@ check_pack() {
1904
1936
  '\.tmp-verify/'
1905
1937
  '\.agent-(reports|shell)/'
1906
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$'
1907
1946
  )
1908
1947
 
1909
1948
  local pass=1 f pat hit
@@ -1936,7 +1975,7 @@ check_pack() {
1936
1975
  return 1
1937
1976
  }
1938
1977
 
1939
- check_pack_install() {
1978
+ _check_pack_install_impl() {
1940
1979
  # Heavy publish gate. Runs the remaining three checks in #13's
1941
1980
  # publish checklist that check_pack (dry-run only) does not cover:
1942
1981
  #
@@ -1965,8 +2004,15 @@ check_pack_install() {
1965
2004
  # that lands as `junghanacs-entwurf-<version>.tgz`. Hardcoded against
1966
2005
  # the scope above so a name change cannot silently slide past this gate.
1967
2006
  tgz_name="junghanacs-entwurf-${version}.tgz"
1968
- tgz_path="${REPO_DIR}/${tgz_name}"
1969
- 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}"
1970
2016
 
1971
2017
  # 0.12.1 C — stale-dist guard. `tsc` emit does NOT prune orphaned files from
1972
2018
  # outDir, and `files: ["mcp/"]` would carry any leftover dist file into the
@@ -1978,7 +2024,11 @@ check_pack_install() {
1978
2024
  printf 'module.exports = "stale";\n' > "$stale_probe"
1979
2025
 
1980
2026
  echo "[check-pack-install] npm pack -> ${tgz_name}"
1981
- (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) || {
1982
2032
  fail "[check-pack-install] npm pack failed"
1983
2033
  return 1
1984
2034
  }
@@ -1999,7 +2049,7 @@ check_pack_install() {
1999
2049
  # If it did, build-bridge's `rm -rf dist` clean step regressed and stale/orphan
2000
2050
  # emit can ship. (See the plant just before npm pack above.)
2001
2051
  if grep -qxF "mcp/entwurf-bridge/dist/__stale_probe__.js" <<<"$tar_files"; then
2002
- rm -f "$tgz_path"
2052
+ rm -rf "$pack_tmp"
2003
2053
  fail "[check-pack-install] stale dist file shipped — build-bridge did not clean dist before emit (orphan-emit publish risk)"
2004
2054
  return 1
2005
2055
  fi
@@ -2025,6 +2075,14 @@ check_pack_install() {
2025
2075
  # 0.12.4 — prebuilt node_modules-safe store-scan artifact for the doctor
2026
2076
  # (see check-pack). The installed-scan smoke below runs exactly this file.
2027
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"
2028
2086
  # 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
2029
2087
  # hook regression below runs exactly this compiled JS from under node_modules.
2030
2088
  "mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
@@ -2056,6 +2114,10 @@ check_pack_install() {
2056
2114
  '^plugins/' '^node_modules/'
2057
2115
  '\.tmp-verify/' '\.agent-(reports|shell)/'
2058
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$'
2059
2121
  )
2060
2122
  for pat in "${tar_forbidden[@]}"; do
2061
2123
  local hit
@@ -2068,7 +2130,7 @@ check_pack_install() {
2068
2130
  done
2069
2131
 
2070
2132
  if [ "$pass" != "1" ]; then
2071
- rm -f "$tgz_path"
2133
+ rm -rf "$pack_tmp"
2072
2134
  fail "[check-pack-install] tar -tf invariants violated"
2073
2135
  return 1
2074
2136
  fi
@@ -2087,7 +2149,7 @@ check_pack_install() {
2087
2149
  # nested under $tmp, whose pnpm-add node_modules/package.json would make npm
2088
2150
  # climb the parent and choke ("Cannot read properties of null").
2089
2151
  npm_tmp=$(mktemp -d -t entwurf-npm-managed.XXXXXX)
2090
- trap 'rm -rf "$tmp" "$npm_tmp" "$tgz_path"' RETURN
2152
+ trap 'rm -rf "$tmp" "$npm_tmp" "$pack_tmp"' RETURN
2091
2153
 
2092
2154
  printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
2093
2155
 
@@ -2141,7 +2203,7 @@ check_pack_install() {
2141
2203
  # this whole lane is about).
2142
2204
  local loader_out loader_home="$tmp/loader-home"
2143
2205
  mkdir -p "$loader_home/.pi/agent"
2144
- loader_out=$(cd "$tmp" && HOME="$loader_home" PI_CODING_AGENT_DIR="$loader_home/.pi/agent" pi -e "$tmp/node_modules/@junghanacs/entwurf" --list-models entwurf 2>&1) || {
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) || {
2145
2207
  fail "[check-pack-install] pi loader smoke failed (exit non-zero):"
2146
2208
  echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
2147
2209
  return 1
@@ -2191,12 +2253,12 @@ check_pack_install() {
2191
2253
  fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
2192
2254
  return 1
2193
2255
  fi
2194
- if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-statusline" ]; then
2195
- 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)"
2196
2258
  ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
2197
2259
  return 1
2198
2260
  fi
2199
- 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) || {
2200
2262
  fail "[check-pack-install] npm-managed run.sh install failed (preflight rejected hoisted deps?):"
2201
2263
  echo "$wire_log" | tail -15 | sed 's/^/ /' >&2
2202
2264
  return 1
@@ -2238,7 +2300,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
2238
2300
  # a KNOWN flag and the entwurf provider must load, sourced only from user scope.
2239
2301
  # Before the fix this printed "Unknown options: --entwurf-control".
2240
2302
  local foreign_out
2241
- foreign_out=$(cd "$tmp" && HOME="$npmhome" PI_CODING_AGENT_DIR="$npmhome/.pi/agent" pi --entwurf-control --list-models entwurf 2>&1) || {
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) || {
2242
2304
  fail "[check-pack-install] foreign-cwd --entwurf-control smoke failed (user-scope citizen not loading?):"
2243
2305
  echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
2244
2306
  return 1
@@ -2279,7 +2341,7 @@ SH
2279
2341
  chmod +x "$fake_claude_dir/claude"
2280
2342
  local mb_home="$npm_tmp/meta-home" mb_cfg="$npm_tmp/meta-claude" mb_log
2281
2343
  mkdir -p "$mb_home" "$mb_cfg"
2282
- 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) || {
2283
2345
  fail "[check-pack-install] installed install-meta-bridge failed under fake claude:"
2284
2346
  echo "$mb_log" | tail -20 | sed 's/^/ /' >&2
2285
2347
  return 1
@@ -2414,7 +2476,7 @@ function finish(trimmed) {
2414
2476
  try { msg = JSON.parse(trimmed); }
2415
2477
  catch { console.error('unparseable tools/list:', trimmed.slice(0, 300)); if (stderr.trim()) console.error(stderr.trim()); process.exit(1); }
2416
2478
  const names = (msg?.result?.tools ?? []).map((t) => t?.name).sort();
2417
- 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']) {
2418
2480
  if (!names.includes(need)) { console.error('missing MCP tool from installed boot:', need, '— got', names.join(',')); process.exit(1); }
2419
2481
  }
2420
2482
  console.log(names.join(','));
@@ -2434,6 +2496,109 @@ JS
2434
2496
  fi
2435
2497
  echo "[check-pack-install] installed bridge boot pass (dist boots under node_modules, pi-free: $boot_out)"
2436
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
+
2437
2602
  # 0.12.4 — installed STORE-DOCTOR regression. meta-bridge-doctor.sh's full store
2438
2603
  # scan runs the prebuilt dist JS when it lives under node_modules (strip-types
2439
2604
  # refuses the .ts there — the same class as the bridge boot above). Before the dist
@@ -2484,6 +2649,41 @@ JS
2484
2649
  return 0
2485
2650
  }
2486
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
+
2487
2687
 
2488
2688
  # --- v2 install/runtime verification gates ---
2489
2689
  #
@@ -2525,7 +2725,7 @@ function finishOk(trimmed) {
2525
2725
  process.exit(1);
2526
2726
  }
2527
2727
  const names = tools.map((t) => t?.name).sort();
2528
- 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'];
2529
2729
  for (const name of expected) {
2530
2730
  if (!names.includes(name)) {
2531
2731
  console.error(`missing MCP tool: ${name}`);
@@ -2627,7 +2827,7 @@ setup_all() {
2627
2827
 
2628
2828
  echo "[setup] repo: $REPO_DIR"
2629
2829
  echo "[setup] project: $project_dir"
2630
- 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"
2631
2831
  echo "[setup] verification: v2 install smoke (entwurf-bridge; LIVE substrate = release-gate)"
2632
2832
 
2633
2833
  (cd "$REPO_DIR" && pnpm install --frozen-lockfile)
@@ -2647,6 +2847,28 @@ setup_all() {
2647
2847
  echo "[setup] no native harness (claude) on PATH — skipping meta-bridge wiring (pi-only host)"
2648
2848
  fi
2649
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
+
2650
2872
  # Deterministic preflight lives in `pnpm check`; live substrate acceptance lives
2651
2873
  # in `LIVE=1 ./run.sh release-gate <scratch>`. Setup is the install path, so it
2652
2874
  # verifies the installed MCP bridge boundary only and does NOT run the legacy
@@ -2655,13 +2877,175 @@ setup_all() {
2655
2877
  validate_entwurf_bridge
2656
2878
 
2657
2879
  echo ""
2658
- 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."
2659
2881
  if command -v claude >/dev/null 2>&1; then
2660
- 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"
2661
2888
  fi
2662
2889
  echo "Run 'LIVE=1 ./run.sh release-gate <scratch>' for live substrate acceptance."
2663
2890
  }
2664
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
+
2665
3049
  # release-gate — the single command that, when GREEN, is sufficient to cut
2666
3050
  # release cuts. Runs the full static floor (`pnpm check`) followed by the
2667
3051
  # v2-native live gates, then emits one PASS/FAIL/SKIP summary. Everything is
@@ -2958,6 +3342,13 @@ case "$cmd" in
2958
3342
  check-shell-quote)
2959
3343
  check_shell_quote
2960
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
+ ;;
2961
3352
  check-entwurf-session-identity)
2962
3353
  check_entwurf_session_identity
2963
3354
  ;;
@@ -3021,6 +3412,9 @@ case "$cmd" in
3021
3412
  check-entwurf-v2-mailbox)
3022
3413
  check_entwurf_v2_mailbox
3023
3414
  ;;
3415
+ check-entwurf-v2-native-push)
3416
+ check_entwurf_v2_native_push
3417
+ ;;
3024
3418
  check-entwurf-v2-runner)
3025
3419
  check_entwurf_v2_runner
3026
3420
  ;;
@@ -3114,11 +3508,20 @@ case "$cmd" in
3114
3508
  check-entwurf-mailbox-guard)
3115
3509
  check_entwurf_mailbox_guard
3116
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
+ ;;
3117
3520
  new-session-id)
3118
3521
  # Garden launcher helper: print one fresh garden sessionId (SSOT:
3119
3522
  # generateSessionId). Used by the operator alias to make every
3120
3523
  # --entwurf-control session a garden citizen. Stdout = the id only.
3121
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/new-session-id.ts)
3524
+ run_ts scripts/new-session-id.ts
3122
3525
  ;;
3123
3526
  smoke-resident-garden-guard)
3124
3527
  # LIVE negative (0 tokens) + opt-in positive gate for the resident
@@ -3173,6 +3576,47 @@ case "$cmd" in
3173
3576
  # duplicate/drift records. Offline + deterministic (deps bash+node+python3).
3174
3577
  (cd "$REPO_DIR" && bash scripts/smoke-meta-install-state.sh)
3175
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
+ ;;
3176
3620
  smoke-user-scope-citizen)
3177
3621
  # 0.12.6 install-boundary gate: register-pi-package.py is the shared
3178
3622
  # packages[] SSOT for project/user install and remove; user scope makes
@@ -3211,6 +3655,101 @@ case "$cmd" in
3211
3655
  # zero claude-code meta-records is a SILENT MISS -> non-zero exit.
3212
3656
  (cd "$REPO_DIR" && bash scripts/meta-bridge-doctor.sh "$@")
3213
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
+ ;;
3214
3753
  meta-bridge-prune)
3215
3754
  # 1.0.0 meta-bridge Phase 4: LISTING-ONLY janitor for the meta-session store.
3216
3755
  # doctor reds on corrupt/duplicate/drift but intentionally does NOT fail on
@@ -3221,7 +3760,7 @@ case "$cmd" in
3221
3760
  # surviving authority). Default store = defaultMetaSessionsDir(); pass [dir]
3222
3761
  # + [--ttl-days N] to override.
3223
3762
  shift || true
3224
- (cd "$REPO_DIR" && node --experimental-strip-types scripts/meta-bridge-prune.ts "$@")
3763
+ run_ts scripts/meta-bridge-prune.ts "$@"
3225
3764
  ;;
3226
3765
  meta-bridge-managed-keys)
3227
3766
  # 0.10.0 meta-bridge: emit the SSOT of settings.json/~/.claude.json keys that
@@ -3318,6 +3857,9 @@ case "$cmd" in
3318
3857
  remove)
3319
3858
  remove_local_package "$TARGET_PROJECT_DIR"
3320
3859
  ;;
3860
+ remove-user-scope)
3861
+ remove_user_scope_citizen
3862
+ ;;
3321
3863
  -h|--help|help|"")
3322
3864
  usage
3323
3865
  ;;