@junghanacs/entwurf 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env bash
2
+ # meta-bridge-doctor.sh — the FAIL-LOUD surface for the meta-bridge (1.0.0 step 5).
3
+ #
4
+ # The runtime hook is best-effort + log (it must never scream into a user's
5
+ # terminal or block startup). THIS is where a silent miss becomes loud. It proves
6
+ # the whole chain a real user depends on:
7
+ # platform -> claude/node toolchain -> the BAKED node path still resolves
8
+ # (catches NixOS /nix/store churn after a rebuild) -> plugin installed+enabled
9
+ # globally -> meta-record dir writable -> SessionStart actually landed a record
10
+ # (hook log + >=1 claude-code meta-record). Plugin present but zero record
11
+ # evidence = the dangerous silent miss -> non-zero exit.
12
+ #
13
+ # Exit 0 = the meta-bridge is wired AND proven to have created a garden citizen.
14
+ set -euo pipefail
15
+
16
+ MKT_NAME="meta-bridge-local"
17
+ PLUGIN="entwurf-meta-receive"
18
+ CLAUDE_CFG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
19
+ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ # shellcheck source=scripts/meta-bridge-hook-log.sh
21
+ source "$REPO/scripts/meta-bridge-hook-log.sh"
22
+
23
+ fail=0
24
+ ok() { echo " ok $*"; }
25
+ warn() { echo " WARN $*"; }
26
+ bad() { echo " FAIL $*"; fail=1; }
27
+
28
+ # pi agent dir resolution — mirror meta-session.ts (PI_CODING_AGENT_DIR or ~/.pi/agent).
29
+ agent_dir() {
30
+ if [ -n "${PI_CODING_AGENT_DIR:-}" ]; then
31
+ case "$PI_CODING_AGENT_DIR" in "~") echo "$HOME";; "~/"*) echo "$HOME/${PI_CODING_AGENT_DIR#\~/}";; *) echo "$PI_CODING_AGENT_DIR";; esac
32
+ else echo "$HOME/.pi/agent"; fi
33
+ }
34
+ AGENT="$(agent_dir)"
35
+ META_SESSIONS="${ENTWURF_META_SESSIONS_DIR:-$AGENT/meta-sessions}"
36
+ HOOK_LOG="$AGENT/meta-bridge-hook.log"
37
+
38
+ echo "meta-bridge doctor"
39
+ echo "config=$CLAUDE_CFG agent-dir=$AGENT"
40
+
41
+ echo "[platform]"
42
+ case "$(uname -s)" in
43
+ Linux | Darwin) ok "$(uname -s) supported" ;;
44
+ *) bad "$(uname -s) unsupported (Linux/macOS only)" ;;
45
+ esac
46
+
47
+ echo "[toolchain]"
48
+ if command -v claude >/dev/null; then ok "claude: $(claude --version 2>/dev/null | head -1)"; else bad "claude not on PATH"; fi
49
+ if command -v python3 >/dev/null; then ok "python3: $(python3 --version 2>/dev/null | head -1) (doorbell JSON parser)"; else bad "python3 not on PATH — FileChanged wake runtime would silently die"; fi
50
+ if command -v node >/dev/null; then
51
+ NV="$(node -p 'process.versions.node' 2>/dev/null || echo 0)"; MJ="${NV%%.*}"; MN="$(printf '%s' "$NV" | cut -d. -f2)"
52
+ if [ "${MJ:-0}" -gt 22 ] || { [ "${MJ:-0}" -eq 22 ] && [ "${MN:-0}" -ge 6 ]; }; then ok "node $NV (>= 22.6 strip-types)"; else bad "node $NV too old (need >= 22.6)"; fi
53
+ else bad "node not on PATH"; fi
54
+
55
+ echo "[managed config state]"
56
+ if command -v python3 >/dev/null; then
57
+ # state.py check is the effect-based keyset-survival guard: it asserts every
58
+ # pi-owned key (scalar policy, permissions items, plugin/marketplace,
59
+ # statusLine, user MCP) still holds its expected value. If another consumer
60
+ # (agent-config merge, hand edit) overwrote one, this fails. Surface WHICH key
61
+ # drifted — a blanket "keyset drifted" sends the operator hunting.
62
+ # NB: branch on the exit code, NOT on empty stderr. A bare CHECK_ERR="$(...)"
63
+ # assignment dies under `set -e` the moment state.py exits nonzero (drift), so
64
+ # the doctor would print "[managed config state]" and exit 1 BEFORE showing
65
+ # which key drifted — silent instead of fail-loud. As an `if` condition the
66
+ # substitution's nonzero status is consumed (set -e suspended), so we always
67
+ # reach the detail. stderr (drift detail) is captured via `2>&1 >/dev/null`.
68
+ if CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$REPO/pi/meta-bridge/.assembled" 2>&1 >/dev/null)"; then
69
+ ok "state file present and managed settings/MCP keyset survives intact"
70
+ else
71
+ bad "managed settings/MCP keyset missing or overwritten — another consumer may have clobbered a pi-owned key. Re-run ./run.sh install-meta-bridge. Drift detail:"
72
+ printf '%s\n' "$CHECK_ERR" | sed 's/^/ /'
73
+ fi
74
+ else
75
+ bad "cannot validate stateful install without python3"
76
+ fi
77
+
78
+ echo "[plugin install (global / --scope user)]"
79
+ if claude plugin list 2>/dev/null | grep -q "$PLUGIN@$MKT_NAME"; then
80
+ ok "$PLUGIN@$MKT_NAME present"
81
+ claude plugin list 2>/dev/null | grep -A3 "$PLUGIN@$MKT_NAME" | grep -qi "enabled" && ok "enabled" || bad "installed but NOT enabled"
82
+ else
83
+ bad "$PLUGIN@$MKT_NAME not installed — run ./run.sh install-meta-bridge"
84
+ fi
85
+
86
+ echo "[baked node path still resolves (NixOS store-churn guard)]"
87
+ CACHE_HOOKS="$(ls "$CLAUDE_CFG/plugins/cache/$MKT_NAME/$PLUGIN/"*/hooks/hooks.json 2>/dev/null | head -1 || true)"
88
+ if [ -n "$CACHE_HOOKS" ]; then
89
+ # `[ -n "$CACHE_HOOKS" ]` proves the file EXISTS, not that the pattern matches.
90
+ # If a future hooks.json drifts the command format, grep finds nothing, exits 1,
91
+ # and under `set -euo pipefail` the doctor would abort here instead of reaching
92
+ # the parse-drift diagnostic below. Swallow grep's nonzero in-pipe (same idiom as
93
+ # line 158) so BAKED="" routes to that bad(...): an installed hooks.json exists but
94
+ # the doctor cannot read its load-bearing hook command, so the store-churn guard is
95
+ # blind — a verify-impossible state must fail loud, not pass as a soft warn (A3b).
96
+ BAKED="$({ grep -oE '"command": "[^ ]+ \$\{CLAUDE_PLUGIN_ROOT\}/meta-bridge-hook.ts"' "$CACHE_HOOKS" || true; } | head -1 | sed -E 's/.*"command": "([^ ]+) .*/\1/')"
97
+ if [ -n "$BAKED" ] && [ -x "$BAKED" ]; then ok "baked node exists + executable: $BAKED"
98
+ elif [ -n "$BAKED" ]; then bad "baked node path is DEAD (nix GC / version bump?): $BAKED — re-run ./run.sh install-meta-bridge"
99
+ else bad "could not parse baked node path from $CACHE_HOOKS — hook command format drift; doctor cannot verify the NixOS store-churn guard. Re-run ./run.sh install-meta-bridge or update the doctor parser."; fi
100
+ else
101
+ warn "no installed hooks.json in cache (plugin not installed?)"
102
+ fi
103
+
104
+ echo "[statusline — garden identity visible in native Claude]"
105
+ if command -v python3 >/dev/null; then
106
+ STATUSLINE_CMD="$(python3 - <<'PY'
107
+ import json, os
108
+ p=os.path.expanduser('~/.claude/settings.json') if not os.environ.get('CLAUDE_CONFIG_DIR') else os.path.join(os.environ['CLAUDE_CONFIG_DIR'], 'settings.json')
109
+ try:
110
+ d=json.load(open(p))
111
+ sl=d.get('statusLine') if isinstance(d, dict) else None
112
+ print(sl.get('command','') if isinstance(sl, dict) else '')
113
+ except Exception:
114
+ print('')
115
+ PY
116
+ )"
117
+ EXPECTED_STATUSLINE="$REPO/scripts/meta-bridge-statusline.sh"
118
+ if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command is repo-owned: $STATUSLINE_CMD"; else bad "statusLine.command drifted (got '$STATUSLINE_CMD', expected '$EXPECTED_STATUSLINE')"; fi
119
+ if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
120
+ SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-4-6"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
121
+ if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q ' cc | s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
122
+ else
123
+ bad "cannot validate statusline without python3"
124
+ fi
125
+
126
+ echo "[receiver MCP reach — entwurf_inbox_read in EVERY native session, NOT plugin-owned]"
127
+ # The plugin owns ONLY the wake/record hooks. The receiver self-fetch tool
128
+ # (entwurf_inbox_read) comes from the user's entwurf-bridge MCP wiring — a Claude
129
+ # Code / agent-config responsibility, never injected here (injecting from the
130
+ # plugin duplicates the server and drops its identity env). The honest test is
131
+ # GLOBAL REACH: a native session in an arbitrary cwd must see the tool. A
132
+ # PROJECT-scoped ~/.mcp.json is NOT enough (it only reaches its own project; a
133
+ # /tmp session would wake with no way to record its receipt). USER scope is.
134
+ # So probe from a neutral non-project cwd, exactly like a real native session.
135
+ if command -v claude >/dev/null; then
136
+ MCP_GET="$(cd /tmp && claude mcp get entwurf-bridge 2>/dev/null || true)"
137
+ if printf '%s\n' "$MCP_GET" | grep -q "Scope: User config" && printf '%s\n' "$MCP_GET" | grep -q "Status: .*Connected"; then
138
+ ok "entwurf-bridge reachable from a neutral cwd (/tmp) as USER-scope MCP — every native session can entwurf_inbox_read"
139
+ else
140
+ bad "entwurf-bridge is not USER-scope+Connected from /tmp — a native session outside the wired project cannot entwurf_inbox_read, so a woken receipt is never recorded. A PROJECT-scoped ~/.mcp.json is not enough; wire it USER scope: claude mcp add -s user entwurf-bridge -e ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code -- bash \"$REPO/mcp/entwurf-bridge/start.sh\""
141
+ fi
142
+ else
143
+ warn "claude not on PATH — cannot probe MCP reach"
144
+ fi
145
+
146
+ echo "[meta-record store]"
147
+ mkdir -p "$META_SESSIONS" 2>/dev/null || true
148
+ if [ -d "$META_SESSIONS" ] && [ -w "$META_SESSIONS" ]; then ok "writable: $META_SESSIONS"; else bad "meta-sessions dir not writable: $META_SESSIONS"; fi
149
+ if command -v node >/dev/null; then
150
+ if node --experimental-strip-types "$REPO/scripts/meta-bridge-store-doctor.ts" "$META_SESSIONS" >/dev/null 2>&1; then
151
+ ok "full store scan: no corrupt records, duplicate nativeSessionId, body/filename drift, or backend↔wakeMode contradiction"
152
+ else
153
+ bad "full store scan failed — corrupt/duplicate/drift meta-record(s) present. Inspect with: node --experimental-strip-types $REPO/scripts/meta-bridge-store-doctor.ts $META_SESSIONS"
154
+ fi
155
+ else
156
+ bad "cannot scan meta-record store without node"
157
+ fi
158
+
159
+ echo "[SessionStart creation evidence (silent-miss guard)]"
160
+ # `set -euo pipefail` + empty store = doctor abort: with no *.meta.json the glob
161
+ # stays literal, grep exits 2 (no such file) / 1 (no match), pipefail propagates,
162
+ # and the doctor dies right after the section header — hiding the very "open a
163
+ # session" hint a fresh-but-correct install needs. Swallow grep's nonzero IN the
164
+ # pipe (same idiom as the `[ -f ] || continue` guard below) so CC_COUNT=0 cleanly.
165
+ CC_COUNT="$({ grep -l '"backend": "claude-code"' "$META_SESSIONS"/*.meta.json 2>/dev/null || true; } | wc -l | tr -d ' ')"
166
+ if [ -f "$HOOK_LOG" ]; then
167
+ ok "hook log present: $HOOK_LOG ($(wc -l < "$HOOK_LOG" | tr -d ' ') lines)"
168
+ # ERROR is sticky in an append-only log: a one-time miss that was later recovered
169
+ # must NOT keep the doctor red forever. Recovery is deliberately narrow: only a
170
+ # later `INFO armed watch` proves a real SessionStart/CwdChanged wake path is
171
+ # back. A later UserPromptSubmit `INFO attach record` is degraded record
172
+ # backfill and must NOT clear an arm/upsert failure.
173
+ if hook_status="$(meta_bridge_hook_log_status "$HOOK_LOG")"; then
174
+ if [ "$hook_status" = "no-error" ]; then
175
+ ok "hook log contains no ERROR line(s)"
176
+ else
177
+ ok "hook log has past ERROR(s) but a later INFO armed watch recovered the wake path"
178
+ fi
179
+ else
180
+ bad "unrecovered hook ERROR (no later INFO armed watch) — last ERROR: $hook_status"
181
+ fi
182
+ if grep -q ' INFO sender marker ' "$HOOK_LOG"; then
183
+ ok "sender marker evidence present (native meta-session can send as replyable garden-id)"
184
+ else
185
+ bad "no sender marker evidence in hook log — native sessions may receive mail but send as anonymous external-mcp. Re-run ./run.sh install-meta-bridge, then trigger a Claude prompt/SessionStart so the updated hook writes the marker."
186
+ fi
187
+ else warn "no hook log yet ($HOOK_LOG) — open a Claude Code session first"; fi
188
+ if [ "${CC_COUNT:-0}" -ge 1 ]; then
189
+ ok "$CC_COUNT claude-code meta-record(s) landed (garden citizen proven)"
190
+ else
191
+ if claude plugin list 2>/dev/null | grep -q "$PLUGIN@$MKT_NAME"; then
192
+ bad "plugin installed but ZERO claude-code meta-records — SILENT MISS. Open a Claude Code session; if still zero, inspect $HOOK_LOG."
193
+ else
194
+ warn "no meta-records yet (plugin not installed)"
195
+ fi
196
+ fi
197
+
198
+ # ── writer-version parity (source ↔ assembled ↔ installed) ──────────────────
199
+ # The dangerous lag: a meta-record schema cut updates the repo SOURCE but the
200
+ # DEPLOYED bundle (what the live SessionStart hook runs) is not re-assembled, so
201
+ # new sessions are still written by the old writer — invisibly. "source complete"
202
+ # ≠ "deployed complete". This section makes the running writer version legible:
203
+ # live-write schema = does the bundle carry serializeMetaIdentity (v2 identity
204
+ # write) or only mintMetaRecord (v1). Hash = drift catch. The authority for "what
205
+ # records me" is the INSTALLED bundle; a mismatch vs source is a loud FAIL.
206
+ echo
207
+ echo "writer-version parity"
208
+ livewrite_schema() { # $1=meta-session.ts → v2|v1|absent
209
+ [ -f "$1" ] || { echo "absent"; return; }
210
+ if grep -q "serializeMetaIdentity" "$1"; then echo "v2"; else echo "v1"; fi
211
+ }
212
+ hash12() { [ -f "$1" ] && sha256sum "$1" | cut -c1-12 || echo "------------"; }
213
+ registry_for_ms() { # $1=bundle meta-session.ts → sibling plugin-root registry path
214
+ [ -f "$1" ] || { echo ""; return; }
215
+ dirname "$(dirname "$1")"
216
+ }
217
+
218
+ SRC_MS="$REPO/pi-extensions/lib/meta-session.ts"
219
+ SRC_REG="$REPO/pi/entwurf-capabilities.json"
220
+ ASM_MS="$REPO/pi/meta-bridge/.assembled/$PLUGIN/lib/meta-session.ts"
221
+ INST_MS="$(ls "$CLAUDE_CFG"/plugins/cache/"$MKT_NAME"/"$PLUGIN"/*/lib/meta-session.ts 2>/dev/null | head -1 || true)"
222
+
223
+ ASM_ROOT="$(registry_for_ms "$ASM_MS")"
224
+ INST_ROOT="$(registry_for_ms "${INST_MS:-}")"
225
+ ASM_REG="${ASM_ROOT:+$ASM_ROOT/entwurf-capabilities.json}"
226
+ INST_REG="${INST_ROOT:+$INST_ROOT/entwurf-capabilities.json}"
227
+
228
+ src_v="$(livewrite_schema "$SRC_MS")"; src_h="$(hash12 "$SRC_MS")"; src_reg_h="$(hash12 "$SRC_REG")"
229
+ asm_v="$(livewrite_schema "$ASM_MS")"; asm_h="$(hash12 "$ASM_MS")"; asm_reg_h="$(hash12 "${ASM_REG:-/none}")"
230
+ inst_v="$(livewrite_schema "${INST_MS:-/none}")"; inst_h="$(hash12 "${INST_MS:-/none}")"; inst_reg_h="$(hash12 "${INST_REG:-/none}")"
231
+
232
+ echo " source : $src_v ($src_h) registry=$src_reg_h"
233
+ echo " assembled : $asm_v ($asm_h) registry=$asm_reg_h"
234
+ echo " installed : $inst_v ($inst_h) registry=$inst_reg_h ${INST_MS:-<none>}"
235
+
236
+ # store reality — distribution of schemaVersion across landed records.
237
+ sv1=0; sv2=0
238
+ for mf in "$META_SESSIONS"/*.meta.json; do
239
+ [ -f "$mf" ] || continue
240
+ case "$(grep -o '"schemaVersion"[[:space:]]*:[[:space:]]*[0-9]*' "$mf" | grep -o '[0-9]*$' | head -1)" in
241
+ 1) sv1=$((sv1 + 1)) ;; 2) sv2=$((sv2 + 1)) ;;
242
+ esac
243
+ done
244
+ echo " store : v1=$sv1 v2=$sv2 (dual-read reads both)"
245
+
246
+ if [ -z "${INST_MS:-}" ]; then
247
+ warn "no installed bundle to compare — run ./run.sh install-meta-bridge"
248
+ elif [ "$inst_h" = "$src_h" ]; then
249
+ ok "deployed writer matches source ($inst_v) — no version lag"
250
+ else
251
+ bad "deployed writer STALE: installed=$inst_v($inst_h) vs source=$src_v($src_h). The live hook records sessions with the OLD writer. Run ./run.sh install-meta-bridge to redeploy, then open a Claude session and re-run this doctor."
252
+ fi
253
+ if [ -f "$ASM_MS" ] && [ "$asm_h" != "$src_h" ]; then
254
+ warn "assembled bundle differs from source ($asm_v vs $src_v) — stale .assembled; install re-assembles it"
255
+ fi
256
+
257
+ # v2 writer dependency: loadMetaCapabilityRegistry reads entwurf-capabilities.json
258
+ # at runtime. In the bundle layout it must sit at the plugin ROOT (resolved via
259
+ # `../` from lib/). A v2 bundle WITHOUT it throws on every mint/parse — a silent
260
+ # hook break that hash parity alone cannot see (it only hashes meta-session.ts).
261
+ check_registry_dep() { # $1=bundle meta-session.ts $2=label $3=registry-path $4=registry-hash
262
+ local ms="$1" label="$2" reg="$3" reg_h="$4"
263
+ [ -f "$ms" ] && grep -q "serializeMetaIdentity" "$ms" || return 0 # v1/absent: no registry dep
264
+ if [ ! -f "$reg" ]; then
265
+ bad "$label is v2 but MISSING entwurf-capabilities.json at plugin root ($reg) — the v2 writer throws on mint/parse (silent hook break). Re-run ./run.sh install-meta-bridge (now bundles the registry)."
266
+ elif [ "$reg_h" != "$src_reg_h" ]; then
267
+ bad "$label v2 carries a STALE capability registry: $reg_h vs source=$src_reg_h ($reg). Re-run ./run.sh install-meta-bridge so the live writer and its load-bearing registry move together."
268
+ else
269
+ ok "$label v2 carries capability registry ($reg, $reg_h)"
270
+ fi
271
+ }
272
+ check_registry_dep "$ASM_MS" "assembled" "${ASM_REG:-}" "$asm_reg_h"
273
+ check_registry_dep "$INST_MS" "installed" "${INST_REG:-}" "$inst_reg_h"
274
+
275
+ echo
276
+ echo "[entwurf-control / v2 dispatch surface]"
277
+ # The entwurf_v2 verb (0.11 step 5d-3) rides two surfaces: the pi-native tool (which
278
+ # depends on the --entwurf-control flag) and the MCP verb. These are SOURCE/static +
279
+ # single-gate checks — a live `pi --entwurf-control` model turn is 5d-5 matrix / live-smoke
280
+ # territory (it takes auth/model state), NOT the doctor's job. `pi --help | grep` is also
281
+ # avoided: flag exposure depends on extension-load conditions, so source+gate is the honest signal.
282
+ V2_CONTROL_SRC="$REPO/pi-extensions/entwurf-control.ts"
283
+ V2_SURFACE_SRC="$REPO/pi-extensions/lib/entwurf-v2-surface.ts"
284
+ V2_MCP_SRC="$REPO/mcp/entwurf-bridge/src/index.ts"
285
+ if [ -f "$V2_CONTROL_SRC" ] && grep -q 'ENTWURF_FLAG = "entwurf-control"' "$V2_CONTROL_SRC" && grep -q 'registerFlag(ENTWURF_FLAG' "$V2_CONTROL_SRC"; then
286
+ ok "pi-native --entwurf-control flag registered (ENTWURF_FLAG)"
287
+ else
288
+ bad "pi-native --entwurf-control flag not found in entwurf-control.ts — the v2 pi tool depends on it"
289
+ fi
290
+ if [ -f "$V2_CONTROL_SRC" ] && grep -q 'registerEntwurfV2Tool' "$V2_CONTROL_SRC" && grep -q 'name: "entwurf_v2"' "$V2_CONTROL_SRC"; then
291
+ ok "pi-native entwurf_v2 tool registered"
292
+ else
293
+ bad "pi-native entwurf_v2 tool not found in entwurf-control.ts"
294
+ fi
295
+ if [ -f "$V2_MCP_SRC" ] && grep -q 'server.tool(' "$V2_MCP_SRC" && grep -q '"entwurf_v2"' "$V2_MCP_SRC"; then
296
+ ok "MCP entwurf_v2 verb registered"
297
+ else
298
+ bad "MCP entwurf_v2 verb not found in entwurf-bridge"
299
+ fi
300
+ if [ -f "$V2_SURFACE_SRC" ] && (cd "$REPO" && node --experimental-strip-types scripts/check-entwurf-v2-surface.ts >/dev/null 2>&1); then
301
+ ok "check-entwurf-v2-surface gate passes (surface adapter + pi-native/MCP wiring)"
302
+ else
303
+ bad "check-entwurf-v2-surface gate FAILED or surface adapter missing — run: ./run.sh check-entwurf-v2-surface"
304
+ fi
305
+ # prefixRoots operator policy (5d-4b): the shared env SSOT both v2 surfaces read. Display
306
+ # ONLY — the parser SSOT lives in entwurf-v2-surface.ts (proven by check-entwurf-v2-surface);
307
+ # the doctor does NOT re-implement parsing. Unset ⇒ no prefix promotion (the safe default).
308
+ if [ -z "${ENTWURF_PREFIX_ROOTS:-}" ]; then
309
+ ok "ENTWURF_PREFIX_ROOTS unset → no prefix auto-approve (preflight default trust)"
310
+ else
311
+ ok "ENTWURF_PREFIX_ROOTS set → operator prefix-approve roots: $ENTWURF_PREFIX_ROOTS"
312
+ fi
313
+
314
+ echo
315
+ if [ "$fail" -eq 0 ]; then echo "meta-bridge doctor: PASS"; else echo "meta-bridge doctor: FAIL (see above)"; exit 1; fi
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Helper for doctor/smoke: decide whether an append-only meta-bridge hook log has
3
+ # an unrecovered ERROR. Recovery is intentionally narrow: only `INFO armed watch`
4
+ # proves the Claude SessionStart/CwdChanged hook re-armed the mailbox watch. A
5
+ # later `INFO attach record (event=UserPromptSubmit, ...)` is merely degraded
6
+ # backfill and must NOT clear an arm/upsert failure.
7
+
8
+ meta_bridge_hook_log_status() {
9
+ local log="$1"
10
+ if ! grep -q ' ERROR ' "$log" 2>/dev/null; then
11
+ echo "no-error"
12
+ return 0
13
+ fi
14
+
15
+ local last_err last_recovery
16
+ last_err="$(grep -n ' ERROR ' "$log" | tail -1 | cut -d: -f1)"
17
+ last_recovery="$(grep -n ' INFO armed watch ' "$log" | tail -1 | cut -d: -f1)"
18
+
19
+ if [ -z "$last_recovery" ] || [ "$last_err" -gt "$last_recovery" ]; then
20
+ grep ' ERROR ' "$log" | tail -1
21
+ return 1
22
+ fi
23
+
24
+ echo "recovered-after-armed-watch"
25
+ return 0
26
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ # meta-bridge-install.sh — operator-grade global installer for the garden-native
3
+ # meta-bridge plugin (1.0.0 step 5). Makes the exact Claude Code wiring the repo
4
+ # needs, so a user never hand-edits hook/plugin settings or passes --plugin-dir.
5
+ #
6
+ # Mechanism (all proven on 2026-06-05):
7
+ # 1. ASSEMBLE a self-contained plugin under pi/meta-bridge/.assembled/ (gitignored):
8
+ # copy the committed skeleton, copy the entry shell + its lib (so
9
+ # ${CLAUDE_PLUGIN_ROOT} self-locates them), and BAKE the node abspath into
10
+ # hooks.json. The node path is the ONLY templated surface — the mailbox /
11
+ # meta-record dirs resolve at runtime inside entry.ts (<pi-agent-dir>, a
12
+ # fixed ~/ path). The plugin is wake/record hooks ONLY; the receiver-side
13
+ # entwurf_inbox_read tool comes from USER-scope entwurf-bridge MCP wiring
14
+ # (`claude mcp add -s user ...`). Project-scoped .mcp.json is deliberately
15
+ # not enough: a /tmp native session would wake without a receipt tool.
16
+ # 2. marketplace add <repo-stable .assembled> (NOT /tmp — ephemeral source
17
+ # would break `claude plugin marketplace update`).
18
+ # 3. install entwurf-meta-receive@meta-bridge-local --scope user (= global:
19
+ # every native session auto-loads it; no manual --plugin-dir).
20
+ # 4. install/update USER-scope entwurf-bridge MCP, so every native session has
21
+ # entwurf_inbox_read without duplicating MCP inside the plugin.
22
+ # Idempotent: re-running removes the prior marketplace/plugin first, so a
23
+ # `nix rebuild` that moved node just re-bakes and re-installs cleanly.
24
+ #
25
+ # Platform: Linux + macOS only. Windows fails fast (no "untested but maybe works").
26
+ set -euo pipefail
27
+
28
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ REPO="$(cd "$HERE/.." && pwd)"
30
+ MKT_NAME="meta-bridge-local"
31
+ PLUGIN="entwurf-meta-receive"
32
+ SRC="$REPO/pi/meta-bridge"
33
+ ASM="$SRC/.assembled"
34
+
35
+ die() { echo "meta-bridge-install: $*" >&2; exit 1; }
36
+
37
+ # --- platform gate (Linux/macOS only) ---------------------------------------
38
+ case "$(uname -s)" in
39
+ Linux | Darwin) ;;
40
+ *) die "unsupported platform '$(uname -s)'. meta-bridge supports Linux + macOS only (Windows is fail-fast, not silently attempted)." ;;
41
+ esac
42
+
43
+ # --- toolchain gate ---------------------------------------------------------
44
+ command -v claude >/dev/null || die "'claude' CLI not on PATH. Install Claude Code first."
45
+ command -v python3 >/dev/null || die "'python3' not on PATH. The FileChanged doorbell parses hook JSON with python3; install refuses a silently-dead wake runtime."
46
+ NODE_BIN="$(command -v node)" || die "'node' not on PATH (the hook needs it to run the entry shell)."
47
+ # Need TypeScript type-stripping: native default >= 23.6, --experimental-strip-types >= 22.6.
48
+ NODE_VER="$(node -p 'process.versions.node')"
49
+ NODE_MAJOR="${NODE_VER%%.*}"
50
+ NODE_MINOR="$(printf '%s' "$NODE_VER" | cut -d. -f2)"
51
+ if [ "$NODE_MAJOR" -lt 22 ] || { [ "$NODE_MAJOR" -eq 22 ] && [ "$NODE_MINOR" -lt 6 ]; }; then
52
+ die "node $NODE_VER too old; meta-bridge entry needs >= 22.6.0 (TypeScript strip-types)."
53
+ fi
54
+ echo "[meta-bridge-install] platform=$(uname -s) node=$NODE_VER ($NODE_BIN) python3=$(command -v python3)"
55
+
56
+ # Capture the user's pre-install values BEFORE any Claude CLI helper can mutate
57
+ # settings.json / ~/.claude.json. Re-runs preserve the first snapshot, so
58
+ # uninstall restores the true pre-entwurf state rather than the last install
59
+ # run's already-managed values.
60
+ python3 "$REPO/scripts/meta-bridge-state.py" prepare --repo "$REPO" --asm "$ASM"
61
+
62
+ # --- 1. assemble a self-contained, node-baked plugin ------------------------
63
+ rm -rf "$ASM"
64
+ mkdir -p "$ASM"
65
+ cp -r "$SRC/.claude-plugin" "$ASM/.claude-plugin"
66
+ cp -r "$SRC/$PLUGIN" "$ASM/$PLUGIN"
67
+ # entry shell + its lib travel WITH the plugin so the install copy is self-contained.
68
+ cp "$REPO/pi-extensions/meta-bridge-hook.ts" "$ASM/$PLUGIN/meta-bridge-hook.ts"
69
+ mkdir -p "$ASM/$PLUGIN/lib"
70
+ cp "$REPO/pi-extensions/lib/meta-session.ts" "$ASM/$PLUGIN/lib/meta-session.ts"
71
+ cp "$REPO/pi-extensions/lib/session-id.js" "$ASM/$PLUGIN/lib/session-id.js"
72
+ # v2 writer (3D-3+) reads the capability registry at runtime
73
+ # (loadMetaCapabilityRegistry). It MUST travel at the plugin ROOT — meta-session's
74
+ # metaCapabilitiesFilePath() resolves it via `../` from lib/ in the bundle layout
75
+ # (the repo `../../pi` path escapes the plugin dir under the cache version dir).
76
+ # Without this, a v2 writer throws on every mint/parse. doctor-meta-bridge asserts it.
77
+ cp "$REPO/pi/entwurf-capabilities.json" "$ASM/$PLUGIN/entwurf-capabilities.json"
78
+ chmod +x "$ASM/$PLUGIN/scripts/doorbell.sh"
79
+ # Bake the node abspath into hooks.json — the ONLY templated surface. mailbox /
80
+ # meta-record dirs resolve at runtime inside entry.ts (<pi-agent-dir>, fixed ~/).
81
+ # The plugin owns ONLY the wake/record hooks; the receiver-side entwurf_inbox_read
82
+ # tool is NOT the plugin's job. It comes from USER-scope entwurf-bridge MCP
83
+ # wiring (`claude mcp add -s user ...`), never a plugin .mcp.json duplicate.
84
+ HOOKS="$ASM/$PLUGIN/hooks/hooks.json"
85
+ HOOKS_PATH="$HOOKS" NODE_PATH_TO_BAKE="$NODE_BIN" python3 - <<'PY'
86
+ from pathlib import Path
87
+ import os
88
+ hooks = Path(os.environ["HOOKS_PATH"])
89
+ node = os.environ["NODE_PATH_TO_BAKE"]
90
+ text = hooks.read_text(encoding="utf-8")
91
+ if "__NODE_BIN__" not in text:
92
+ raise SystemExit(f"node-path bake failed before replacement (placeholder absent in {hooks})")
93
+ hooks.write_text(text.replace("__NODE_BIN__", node), encoding="utf-8")
94
+ PY
95
+ grep -q "__NODE_BIN__" "$HOOKS" && die "node-path bake failed (placeholder still present in $HOOKS)."
96
+ echo "[meta-bridge-install] assembled $ASM (node baked, entry+lib bundled; MCP wiring is NOT plugin-owned)"
97
+
98
+ # --- validate the manifests before touching user config ---------------------
99
+ claude plugin validate "$ASM" >/dev/null || die "marketplace manifest validation failed for $ASM"
100
+ echo "[meta-bridge-install] manifest validate: ok"
101
+
102
+ # --- 2 + 3. (re)register the marketplace and (re)install globally -----------
103
+ claude plugin uninstall "$PLUGIN@$MKT_NAME" >/dev/null 2>&1 || true
104
+ claude plugin marketplace remove "$MKT_NAME" >/dev/null 2>&1 || true
105
+ claude plugin marketplace add "$ASM" >/dev/null
106
+ claude plugin install "$PLUGIN@$MKT_NAME" --scope user >/dev/null
107
+ echo "[meta-bridge-install] installed $PLUGIN@$MKT_NAME (scope: user = global)"
108
+
109
+ # --- 4. ensure USER-scope receiver MCP wiring -------------------------------
110
+ # One canonical MCP entry only: user-scope entwurf-bridge via the repo-managed
111
+ # start.sh. This reaches /tmp and every other native Claude Code cwd. Do not put
112
+ # entwurf-bridge in the plugin (.mcp.json): that duplicates the server and drops
113
+ # the canonical external identity env.
114
+ claude mcp remove entwurf-bridge -s user >/dev/null 2>&1 || true
115
+ # 0.11 S2 cutover: drop any stale USER-scope pi-tools-bridge entry written by a
116
+ # prior version (one-shot rename cleanup, not a runtime alias).
117
+ claude mcp remove pi-tools-bridge -s user >/dev/null 2>&1 || true
118
+ claude mcp add -s user entwurf-bridge \
119
+ -e ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code \
120
+ -- bash "$REPO/mcp/entwurf-bridge/start.sh" >/dev/null
121
+ (cd /tmp && claude mcp get entwurf-bridge 2>/dev/null | grep -q "Scope: User config") || \
122
+ die "post-install: entwurf-bridge is not reachable as USER-scope MCP from /tmp"
123
+ echo "[meta-bridge-install] installed entwurf-bridge MCP (scope: user = global receiver tools)"
124
+
125
+ # Re-assert the repo-owned keyset through our stateful manager. The Claude CLI
126
+ # calls above are allowed to maintain their cache/registry files, but the
127
+ # operator-facing JSON keys are owned here so uninstall can be honest.
128
+ python3 "$REPO/scripts/meta-bridge-state.py" apply --repo "$REPO" --asm "$ASM"
129
+
130
+ # --- evidence ---------------------------------------------------------------
131
+ echo "--- claude plugin list ---"
132
+ claude plugin list 2>/dev/null | grep -A3 "$PLUGIN" || die "post-install: plugin not in list (install did not take)."
133
+ echo
134
+ echo "[meta-bridge-install] DONE. Open a Claude Code session, then verify with:"
135
+ echo " ./run.sh doctor-meta-bridge"