@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.
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|