@junghanacs/entwurf 0.12.1 → 0.12.2

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.12.2 — 2026-06-29
8
+
9
+ ### Fixed
10
+
11
+ - **Claude Code meta-bridge installs on the supported 2.1.x floor.** Claude's plugin validator is a closed schema whose accepted marketplace keys differ by patch level; 0.12.1's root-level `marketplace.json` `description` passed on Claude 2.1.195 but failed on the floor host's 2.1.97 with `Unrecognized key: "description"`. The marketplace manifest now keeps only the minimal root keyset confirmed on the floor (`name`, `owner`, `plugins`), leaving explanatory text in installer/docs comments rather than in closed-schema JSON.
12
+ - **Installed meta-bridge MCP wiring no longer bakes pnpm store paths.** On npm/pnpm-installed hosts, the user-scope Claude MCP entry now points at the stable `entwurf-bridge` bin instead of `$REPO/mcp/entwurf-bridge/start.sh`, whose `$REPO` can be a `.pnpm/@junghanacs+entwurf@.../node_modules/...` hash path that goes stale on version or peer changes. Development clones still pin to that clone's `start.sh`; both branches preserve the external Claude sender env (`ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code`, `ENTWURF_BRIDGE_REQUIRE_META_SENDER=1`).
13
+
14
+ ### Added
15
+
16
+ - **`check-meta-manifest-schema` — a CLI-version-independent meta-bridge manifest guard.** The new deterministic gate pins the Claude marketplace/plugin/hooks manifests to the minimal keysets used by the meta-bridge, including the load-bearing hook event names (`SessionStart`, `CwdChanged`, `UserPromptSubmit`, `FileChanged`) and hook command keys. It also asserts `meta-bridge-state.py::desired_mcp()` chooses the installed-vs-clone MCP wiring without invoking a real Claude CLI. The gate is wired into `pnpm check` so future decorative closed-schema keys fail before release.
17
+
18
+ ### Verification
19
+
20
+ - `pnpm check` passes with the new `check-meta-manifest-schema` static guard included.
21
+ - `./run.sh smoke-meta-install-state` passes, proving the state manager/doctor consumers still follow `desired_mcp()` without drift.
22
+ - Installed-location regression probe passes: running `scripts/check-meta-manifest-schema.py` from a synthetic `node_modules/@junghanacs/entwurf` package no longer self-fails.
23
+ - Claude Code 2.1.97 floor validation passed on `hejdev6` after removing the root marketplace `description`; current Claude 2.1.195 validation also passes with warnings only.
24
+ - `LIVE=1 ./run.sh release-gate /tmp/psa-release-gate-0.12.2.NLGhet` passed on 2026-06-29 with `MUST: PASS=17 FAIL=0 SKIP=0` and `BEHAVIOR: PASS=1 FAIL=0`; log `/tmp/entwurf-release-gate-0.12.2-20260629T205339.log`.
25
+
7
26
  ## 0.12.1 — 2026-06-29
8
27
 
9
28
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -118,8 +118,9 @@
118
118
  "typecheck": "tsc --noEmit && tsc -p mcp/tsconfig.json && tsc -p scripts/tsconfig.json",
119
119
  "lint": "biome check .",
120
120
  "format": "biome check --write .",
121
+ "check-meta-manifest-schema": "./run.sh check-meta-manifest-schema",
121
122
  "check-package-source-routing": "./run.sh check-package-source-routing",
122
- "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-record-v2 && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-meta-dual-read && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-migration && ./run.sh check-meta-dual-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-entwurf-mailbox-guard && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-pack"
123
+ "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-record-v2 && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-meta-dual-read && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-migration && ./run.sh check-meta-dual-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-entwurf-mailbox-guard && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-pack"
123
124
  },
124
125
  "pi": {
125
126
  "extensions": [
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "meta-bridge-local",
3
3
  "owner": { "name": "junghan0611" },
4
- "description": "Local marketplace for the entwurf garden-native meta-bridge. Shipped in-repo (repo-stable, NOT /tmp — an ephemeral source breaks `claude plugin marketplace update`). The installer (./run.sh install-meta-bridge) assembles a self-contained copy under .assembled/ (entry + lib + node-path-baked hooks) and runs `marketplace add` + `install --scope user`.",
5
4
  "plugins": [
6
5
  {
7
6
  "name": "entwurf-meta-receive",
package/run.sh CHANGED
@@ -93,6 +93,7 @@ Usage:
93
93
  ./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)
94
94
  ./run.sh smoke-meta-prune # 1.0.0 meta-bridge Phase 4: listing-only store janitor regression gate — classify keep/orphan/stale/ambiguous, delete nothing. Offline/deterministic (deps: bash+node)
95
95
  ./run.sh smoke-meta-keyset-guard # 0.10.0 meta-bridge: keyset-owner guard regression — check-keyset-overlap + managed-keys SSOT (disjoint passes, collisions fail). Offline/hermetic (deps: bash+python3)
96
+ ./run.sh check-meta-manifest-schema # 0.12.2 meta-bridge: CLI-version-INDEPENDENT static guard — plugin manifests pinned to the minimal keyset that validates on the lowest supported Claude (closed-schema regression that broke 0.12.1 install on floor) + desired_mcp installed-vs-clone dual-mode. Offline (deps: python3)
96
97
  ./run.sh smoke-claude-native-resume-live # LIVE-only: Claude Code native fresh→--resume continuity + meta-record uniqueness; proves meta-bridge records identity without touching the backend resume path
97
98
 
98
99
  ./run.sh install-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
@@ -2803,6 +2804,15 @@ case "$cmd" in
2803
2804
  # Offline/hermetic (deps: bash+python3).
2804
2805
  (cd "$REPO_DIR" && bash scripts/smoke-meta-keyset-guard.sh)
2805
2806
  ;;
2807
+ check-meta-manifest-schema)
2808
+ # 0.12.2 meta-bridge: deterministic, CLI-version-INDEPENDENT guard. `claude plugin
2809
+ # validate` is a CLOSED schema whose allowed keyset differs by version, so a
2810
+ # decorative key (0.12.1's root `description`) passed on the dev box but broke
2811
+ # install on the floor Claude. This pins the committed manifests to the minimal
2812
+ # validated keyset and asserts desired_mcp()'s installed-vs-clone dual-mode.
2813
+ # Offline/hermetic (deps: python3).
2814
+ (cd "$REPO_DIR" && python3 scripts/check-meta-manifest-schema.py)
2815
+ ;;
2806
2816
  smoke-meta-install-state)
2807
2817
  # 1.0.0 meta-bridge Phase 2 regression gate: state file captures pre-install
2808
2818
  # values, install/uninstall touches only the managed keyset, uninstall refuses
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ # check-meta-manifest-schema — deterministic, CLI-version-INDEPENDENT guard for the
3
+ # meta-bridge plugin manifests + the installed-vs-clone MCP wiring decision.
4
+ #
5
+ # Why this exists (0.12.2): `claude plugin validate` is a CLOSED schema — it REJECTS
6
+ # unrecognized keys, and the allowed keyset differs by Claude Code version. 0.12.1
7
+ # shipped a marketplace.json carrying a root `description`; Claude 2.1.195 (the dev
8
+ # box) accepted it, Claude 2.1.97 (the install floor) rejected it with
9
+ # `Unrecognized key: "description"`, so `entwurf install-meta-bridge` died on the
10
+ # floor host while the release looked green. The lesson: a "nice to have" decorative
11
+ # key in a closed-schema manifest is a future regression surface. This guard pins the
12
+ # committed manifests to the MINIMAL keyset confirmed to validate on the lowest
13
+ # supported Claude, independent of whatever CLI version happens to run here.
14
+ #
15
+ # It also asserts meta-bridge-state.py::desired_mcp() picks the stable `entwurf-bridge`
16
+ # bin for an installed package and the clone's start.sh for a dev clone (the other
17
+ # 0.12.1 install-surface fragility: baking the pnpm store path goes stale on peer bumps).
18
+ #
19
+ # Offline / hermetic. Deps: python3 only.
20
+ import json
21
+ import subprocess
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ HERE = Path(__file__).resolve().parent
26
+ REPO = HERE.parent
27
+ MB = REPO / "pi" / "meta-bridge"
28
+ STATE = HERE / "meta-bridge-state.py"
29
+
30
+ fail = 0
31
+
32
+
33
+ def ok(msg: str) -> None:
34
+ print(f" ok {msg}")
35
+
36
+
37
+ def bad(msg: str) -> None:
38
+ global fail
39
+ print(f" FAIL {msg}")
40
+ fail = 1
41
+
42
+
43
+ def load(path: Path):
44
+ try:
45
+ return json.loads(path.read_text(encoding="utf-8"))
46
+ except Exception as exc: # noqa: BLE001
47
+ bad(f"cannot parse {path}: {exc}")
48
+ return None
49
+
50
+
51
+ def subset(label: str, got, allowed: set[str]) -> None:
52
+ if not isinstance(got, dict):
53
+ bad(f"{label}: expected an object, got {type(got).__name__}")
54
+ return
55
+ extra = set(got) - allowed
56
+ if extra:
57
+ bad(f"{label}: keys {sorted(extra)} outside minimal allowed set {sorted(allowed)} "
58
+ f"(closed-schema risk: an older Claude may reject them)")
59
+ else:
60
+ ok(f"{label}: keys ⊆ {sorted(allowed)}")
61
+
62
+
63
+ # --- marketplace.json --------------------------------------------------------
64
+ mkt = load(MB / ".claude-plugin" / "marketplace.json")
65
+ if mkt is not None:
66
+ subset("marketplace root", mkt, {"name", "owner", "plugins"})
67
+ subset("marketplace.owner", mkt.get("owner", {}), {"name"})
68
+ plugins = mkt.get("plugins")
69
+ if not isinstance(plugins, list) or not plugins:
70
+ bad("marketplace.plugins must be a non-empty array")
71
+ else:
72
+ for i, p in enumerate(plugins):
73
+ subset(f"marketplace.plugins[{i}]", p, {"name", "source", "description"})
74
+
75
+ # --- plugin.json -------------------------------------------------------------
76
+ plug = load(MB / "entwurf-meta-receive" / ".claude-plugin" / "plugin.json")
77
+ if plug is not None:
78
+ subset("plugin.json", plug, {"name", "version", "description"})
79
+
80
+ # --- hooks.json --------------------------------------------------------------
81
+ hooks = load(MB / "entwurf-meta-receive" / "hooks" / "hooks.json")
82
+ if hooks is not None:
83
+ subset("hooks.json root", hooks, {"hooks"})
84
+ # Pin the hook EVENT names too — an unrecognized event key is the same closed-schema
85
+ # risk as an unrecognized field. These four are the meta-bridge's load-bearing events.
86
+ subset("hooks.json events", hooks.get("hooks") or {},
87
+ {"SessionStart", "CwdChanged", "UserPromptSubmit", "FileChanged"})
88
+ for event, entries in (hooks.get("hooks") or {}).items():
89
+ if not isinstance(entries, list):
90
+ bad(f"hooks.{event} must be an array")
91
+ continue
92
+ for j, entry in enumerate(entries):
93
+ subset(f"hooks.{event}[{j}]", entry, {"matcher", "hooks"})
94
+ for k, h in enumerate(entry.get("hooks", []) if isinstance(entry, dict) else []):
95
+ # asyncRewake/timeout are load-bearing on the FileChanged doorbell —
96
+ # allowed, NOT decorative. Anything beyond this set must be reviewed.
97
+ subset(f"hooks.{event}[{j}].hooks[{k}]", h, {"type", "command", "asyncRewake", "timeout"})
98
+
99
+ # --- desired_mcp() installed-vs-clone dual-mode ------------------------------
100
+ def desired_mcp(repo: str):
101
+ out = subprocess.run(
102
+ [sys.executable, str(STATE), "desired-mcp", "--repo", repo],
103
+ capture_output=True, text=True,
104
+ )
105
+ if out.returncode != 0:
106
+ bad(f"desired-mcp --repo {repo} exited {out.returncode}: {out.stderr.strip()}")
107
+ return None
108
+ return json.loads(out.stdout)
109
+
110
+
111
+ # Both probes use SYNTHETIC paths that need not exist — never REPO. If this guard ran
112
+ # from an installed package (e.g. `entwurf check-meta-manifest-schema`), REPO would
113
+ # itself end in node_modules/@junghanacs/entwurf and a REPO-based clone probe would
114
+ # wrongly resolve to the installed shape and self-fail. Fixed, installed-vs-clone-shaped
115
+ # literals make the assertion location-independent.
116
+ installed = desired_mcp("/opt/x/node_modules/@junghanacs/entwurf")
117
+ if installed is not None:
118
+ if installed.get("command") == "entwurf-bridge" and installed.get("args") == []:
119
+ ok("desired_mcp(installed-shaped) → stable `entwurf-bridge` bin (no store path baked)")
120
+ else:
121
+ bad(f"desired_mcp(installed-shaped) should wire the `entwurf-bridge` bin, got {installed.get('command')} {installed.get('args')}")
122
+
123
+ clone = desired_mcp("/opt/entwurf-dev-clone")
124
+ if clone is not None:
125
+ args = clone.get("args") or [""]
126
+ if clone.get("command") == "bash" and str(args[0]).endswith("mcp/entwurf-bridge/start.sh"):
127
+ ok("desired_mcp(clone-shaped) → that clone's start.sh (not the global bin)")
128
+ else:
129
+ bad(f"desired_mcp(clone-shaped) should wire bash start.sh, got {clone.get('command')} {args}")
130
+
131
+ # both modes must keep the canonical sender env
132
+ for label, entry in (("installed", installed), ("clone", clone)):
133
+ if entry is None:
134
+ continue
135
+ env = entry.get("env", {})
136
+ if env.get("ENTWURF_BRIDGE_REQUIRE_META_SENDER") == "1" and \
137
+ env.get("ENTWURF_BRIDGE_EXTERNAL_AGENT_ID") == "external-mcp/claude-code":
138
+ ok(f"desired_mcp({label}) carries canonical sender env")
139
+ else:
140
+ bad(f"desired_mcp({label}) dropped canonical sender env: {env}")
141
+
142
+ if fail:
143
+ print("check-meta-manifest-schema FAIL")
144
+ sys.exit(1)
145
+ print("check-meta-manifest-schema PASS")
@@ -115,9 +115,23 @@ claude mcp remove entwurf-bridge -s user >/dev/null 2>&1 || true
115
115
  # 0.11 S2 cutover: drop any stale USER-scope pi-tools-bridge entry written by a
116
116
  # prior version (one-shot rename cleanup, not a runtime alias).
117
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
118
+ # 0.12.2 installed-vs-clone dual-mode MUST mirror meta-bridge-state.py::desired_mcp(),
119
+ # which the trailing `apply` re-asserts as the SSOT. An installed package ($REPO ends
120
+ # in node_modules/@junghanacs/entwurf) wires the STABLE `entwurf-bridge` bin shim; baking
121
+ # the pnpm store path here would go stale on any peer/version bump. A dev clone pins to
122
+ # this clone's start.sh. Both branches carry the same two env vars desired_mcp() writes.
123
+ case "$REPO" in
124
+ */node_modules/@junghanacs/entwurf)
125
+ claude mcp add -s user entwurf-bridge \
126
+ -e ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code \
127
+ -e ENTWURF_BRIDGE_REQUIRE_META_SENDER=1 \
128
+ -- entwurf-bridge >/dev/null ;;
129
+ *)
130
+ claude mcp add -s user entwurf-bridge \
131
+ -e ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code \
132
+ -e ENTWURF_BRIDGE_REQUIRE_META_SENDER=1 \
133
+ -- bash "$REPO/mcp/entwurf-bridge/start.sh" >/dev/null ;;
134
+ esac
121
135
  (cd /tmp && claude mcp get entwurf-bridge 2>/dev/null | grep -q "Scope: User config") || \
122
136
  die "post-install: entwurf-bridge is not reachable as USER-scope MCP from /tmp"
123
137
  echo "[meta-bridge-install] installed entwurf-bridge MCP (scope: user = global receiver tools)"
@@ -264,20 +264,42 @@ def desired_marketplace(asm: Path) -> dict[str, Any]:
264
264
  return {"source": {"source": "directory", "path": str(asm.resolve())}}
265
265
 
266
266
 
267
+ def is_installed_package(repo: Path) -> bool:
268
+ # An installed (npm / pnpm) layout always ends in node_modules/@junghanacs/entwurf
269
+ # (pnpm global nests it under .pnpm/<hash>/node_modules/@junghanacs/entwurf). A dev
270
+ # clone never lives under node_modules/@junghanacs/entwurf, so the trailing three
271
+ # path components cleanly distinguish the two WITHOUT a PATH lookup (`command -v
272
+ # entwurf-bridge` could resolve a stale GLOBAL bin from a clone host and is wrong here).
273
+ parts = repo.resolve().parts
274
+ return parts[-3:] == ("node_modules", "@junghanacs", "entwurf")
275
+
276
+
267
277
  def desired_mcp(repo: Path) -> dict[str, Any]:
278
+ env = {
279
+ "ENTWURF_BRIDGE_EXTERNAL_AGENT_ID": "external-mcp/claude-code",
280
+ # Anonymous sends are forbidden on the Claude Code install path: a send
281
+ # with no pi-session identity AND no meta-sender marker is refused, not
282
+ # delivered as an unidentified external. The SessionStart hook writes
283
+ # the marker (parent-pid keyed), so a normally-opened session always has
284
+ # an authoritative garden-id sender.
285
+ "ENTWURF_BRIDGE_REQUIRE_META_SENDER": "1",
286
+ }
287
+ if is_installed_package(repo):
288
+ # Installed package: wire the STABLE `entwurf-bridge` bin shim that npm/pnpm
289
+ # place on PATH. Baking repo/mcp/entwurf-bridge/start.sh would embed the pnpm
290
+ # store path (.pnpm/@junghanacs+entwurf@<ver>_<peer-hash>/...), which moves
291
+ # on any peer/version change and leaves a dead MCP command. The bare bin name
292
+ # tracks whatever version is currently installed — the right behaviour for a
293
+ # managed install host. start.sh's own position-based dual-mode then runs the
294
+ # dist boot under node_modules. (0.12.2 fix; 0.12.1 shipped only the clone path.)
295
+ return {"type": "stdio", "command": "entwurf-bridge", "args": [], "env": env}
296
+ # Dev clone: pin to THIS clone's launcher (an absolute resolved path), never the
297
+ # global bin, so a clone host stays bound to its own source tree.
268
298
  return {
269
299
  "type": "stdio",
270
300
  "command": "bash",
271
301
  "args": [str((repo / "mcp" / "entwurf-bridge" / "start.sh").resolve())],
272
- "env": {
273
- "ENTWURF_BRIDGE_EXTERNAL_AGENT_ID": "external-mcp/claude-code",
274
- # Anonymous sends are forbidden on the Claude Code install path: a send
275
- # with no pi-session identity AND no meta-sender marker is refused, not
276
- # delivered as an unidentified external. The SessionStart hook writes
277
- # the marker (parent-pid keyed), so a normally-opened session always has
278
- # an authoritative garden-id sender.
279
- "ENTWURF_BRIDGE_REQUIRE_META_SENDER": "1",
280
- },
302
+ "env": env,
281
303
  }
282
304
 
283
305
 
@@ -519,7 +541,7 @@ def main() -> int:
519
541
  parser = argparse.ArgumentParser(description="entwurf meta-bridge state manager")
520
542
  parser.add_argument(
521
543
  "command",
522
- choices=["prepare", "apply", "preflight-uninstall", "uninstall", "check", "managed-keys"],
544
+ choices=["prepare", "apply", "preflight-uninstall", "uninstall", "check", "managed-keys", "desired-mcp"],
523
545
  )
524
546
  parser.add_argument("--repo", default=Path(__file__).resolve().parents[1], type=Path)
525
547
  parser.add_argument("--asm", default=None, type=Path)
@@ -539,6 +561,11 @@ def main() -> int:
539
561
  check(repo, asm)
540
562
  elif args.command == "managed-keys":
541
563
  print(json.dumps(managed_keys(), indent=2))
564
+ elif args.command == "desired-mcp":
565
+ # Debug/guard surface: print the user-scope MCP entry desired_mcp() would
566
+ # write for --repo. Lets a deterministic guard assert the installed-vs-clone
567
+ # dual-mode without spinning up a real `claude` CLI. --repo need not exist.
568
+ print(json.dumps(desired_mcp(repo), indent=2))
542
569
  except StateError as exc:
543
570
  print(f"meta-bridge-state: {exc}", file=sys.stderr)
544
571
  return 1