@junghanacs/entwurf 0.12.4 → 0.12.6

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/run.sh CHANGED
@@ -91,12 +91,13 @@ Usage:
91
91
  ./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
92
  ./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
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
+ ./run.sh smoke-user-scope-citizen # 0.12.6 install-boundary: pi packages[] registration SSOT (register-pi-package.py) — idempotent + preserves unrelated + normalizes stale + remove symmetry + fails loud. Offline/hermetic (deps: bash+python3)
94
95
  ./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
96
  ./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
97
  ./run.sh check-meta-manifest-schema # 0.12.2 meta-bridge: CLI-version-INDEPENDENT static guard — plugin manifests pinned to the minimal keyset that validates on the lowest supported Claude (closed-schema regression that broke 0.12.1 install on floor) + desired_mcp installed-vs-clone dual-mode. Offline (deps: python3)
97
98
  ./run.sh smoke-claude-native-resume-live # LIVE-only: Claude Code native fresh→--resume continuity + meta-record uniqueness; proves meta-bridge records identity without touching the backend resume path
98
99
 
99
- ./run.sh install-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
100
+ ./run.sh install-meta-bridge # INTERNAL part of `setup` (native-harness plugin) + doctor recovery path — prefer `setup`; stateful GLOBAL install (plugin + USER MCP + settings keyset, honest uninstall state)
100
101
  ./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
101
102
  ./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)
102
103
  ./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])
@@ -106,7 +107,7 @@ Usage:
106
107
  ./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
107
108
  ./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with 0.80.x peers
108
109
  ./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
109
- ./run.sh install [project-dir] # install this local package into project .pi/settings.json
110
+ ./run.sh install [project-dir] # INTERNAL part of `setup` (project .pi/settings.json wiring) + npm-consumer entry — prefer `setup`, don't call directly for dev
110
111
  ./run.sh setup:links [--force] # repair ~/.pi/agent/entwurf-targets.json link (use --force to replace a stale operator file or wrong symlink; a .bak is taken)
111
112
  ./run.sh remove [project-dir] # remove entwurf entries from project .pi/settings.json
112
113
 
@@ -239,11 +240,67 @@ preflight_dep_integrity() {
239
240
  fi
240
241
  }
241
242
 
243
+ preflight_pi_settings_shapes() {
244
+ local project_settings="$1"
245
+ local user_settings="$2"
246
+ python3 - "$project_settings" "$user_settings" <<'PY'
247
+ import json, sys
248
+ from pathlib import Path
249
+
250
+ project = Path(sys.argv[1])
251
+ user = Path(sys.argv[2])
252
+
253
+ def load_object(path: Path, label: str):
254
+ if not path.exists():
255
+ return None
256
+ data = json.loads(path.read_text())
257
+ if not isinstance(data, dict):
258
+ raise SystemExit(f"{label} settings is not a JSON object: {path}")
259
+ return data
260
+
261
+ def check_packages(data, path: Path, label: str):
262
+ if data is None:
263
+ return
264
+ packages = data.get("packages")
265
+ if packages is not None and not isinstance(packages, list):
266
+ raise SystemExit(f"{label} settings packages is not a JSON array: {path}")
267
+
268
+ def check_project_provider(data, path: Path):
269
+ if data is None:
270
+ return
271
+ provider = data.get("entwurfProvider")
272
+ if provider is None:
273
+ return
274
+ if not isinstance(provider, dict):
275
+ raise SystemExit(f"project settings entwurfProvider is not an object: {path}")
276
+ servers = provider.get("mcpServers")
277
+ if servers is not None and not isinstance(servers, dict):
278
+ raise SystemExit(f"project settings entwurfProvider.mcpServers is not an object: {path}")
279
+
280
+ project_data = load_object(project, "project")
281
+ user_data = load_object(user, "user")
282
+ check_packages(project_data, project, "project")
283
+ check_packages(user_data, user, "user")
284
+ check_project_provider(project_data, project)
285
+ PY
286
+ }
287
+
242
288
  install_local_package() {
243
- local project_dir
289
+ local project_dir agent_dir
244
290
  project_dir=$(normalize_project_dir "$1")
291
+ agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
245
292
  preflight_dep_integrity
293
+ # Fail BEFORE any settings write if either target config already has a corrupt
294
+ # shape. The packages[] SSOT and provider writer run in two separate steps, so
295
+ # without this preflight a bad entwurfProvider could leave a half-installed
296
+ # packages[] entry behind (2026-07-03 install-boundary hardening).
297
+ preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$agent_dir/settings.json"
246
298
  mkdir -p "$project_dir/.pi"
299
+ # packages[] registration via the shared SSOT — same is_entwurf_source
300
+ # predicate + idempotency as user-scope and remove (not a substring match).
301
+ 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).
247
304
  python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
248
305
  import json, sys
249
306
  from pathlib import Path
@@ -258,23 +315,6 @@ if settings_path.exists():
258
315
  else:
259
316
  data = {}
260
317
 
261
- # --- packages[] registration --------------------------------------------------
262
- packages = data.get("packages")
263
- if not isinstance(packages, list):
264
- packages = []
265
-
266
- filtered = []
267
- for item in packages:
268
- source = item.get("source") if isinstance(item, dict) else item
269
- if isinstance(source, str) and ("entwurf" in source) and source != repo_dir:
270
- continue
271
- filtered.append(item)
272
-
273
- if repo_dir not in filtered:
274
- filtered.append(repo_dir)
275
-
276
- data["packages"] = filtered
277
-
278
318
  # --- entwurfProvider.mcpServers bundled entries ---------------------------
279
319
  # Ship the two in-repo MCP adapters pre-wired so `pi install` produces a
280
320
  # working setup without the consumer hand-editing settings.json. User-authored
@@ -334,9 +374,25 @@ for name, reason in LEGACY_BUNDLED.items():
334
374
 
335
375
  settings_path.write_text(json.dumps(data, indent=2) + "\n")
336
376
  print(f"install: updated {settings_path}")
337
- print(f"install: package source -> {repo_dir}")
338
377
  PY
339
378
  ensure_agent_dir_symlinks
379
+ register_user_scope_citizen
380
+ }
381
+
382
+ # Register entwurf as a pi USER-SCOPE citizen so its extensions
383
+ # (entwurf-control.ts → --entwurf-control / --emacs-agent-socket) load from ANY
384
+ # cwd, not only inside the entwurf checkout. project-scope `.pi/settings.json`
385
+ # only applies when pi runs inside the repo; the `pit`/`pia`/`pihome` global
386
+ # launchers and the npm consumer's "installs → just works" both need the entry
387
+ # in ~/.pi/agent/settings.json's packages[]. This is the wiring that dropped when
388
+ # `pi install` was removed from setup (2026-07-03: `--entwurf-control` unknown in
389
+ # a foreign cwd). Idempotent: absent → append, present → no-op; a stale entwurf
390
+ # entry at a different path is normalized to REPO_DIR. Every other package and key
391
+ # in the operator's user settings is preserved untouched.
392
+ register_user_scope_citizen() {
393
+ local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
394
+ # Shared idempotent implementation (also driven by smoke-user-scope-citizen).
395
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR"
340
396
  }
341
397
 
342
398
  # Ensure agent-level resources that entwurf code reads from
@@ -424,6 +480,14 @@ ensure_agent_dir_symlinks() {
424
480
  remove_local_package() {
425
481
  local project_dir
426
482
  project_dir=$(normalize_project_dir "$1")
483
+ # Same fail-before-write rule as install: remove has two writers too
484
+ # (packages[] SSOT, then entwurfProvider cleanup), so a malformed provider must
485
+ # not leave a half-removed packages[] entry behind.
486
+ preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$(mktemp -u)"
487
+ # packages[] cleanup via the shared SSOT — same is_entwurf_source predicate as
488
+ # install, so remove never over-deletes a look-alike repo (entwurf-notes, …)
489
+ # that install would never have registered.
490
+ python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR" --remove
427
491
  python3 - "$project_dir/.pi/settings.json" "$REPO_DIR" <<'PY'
428
492
  import json, sys
429
493
  from pathlib import Path
@@ -438,19 +502,6 @@ data = json.loads(settings_path.read_text())
438
502
  if not isinstance(data, dict):
439
503
  raise SystemExit("settings.json is not an object")
440
504
 
441
- # --- packages[] cleanup -------------------------------------------------------
442
- packages = data.get("packages")
443
- pkg_removed = 0
444
- if isinstance(packages, list):
445
- filtered = []
446
- for item in packages:
447
- source = item.get("source") if isinstance(item, dict) else item
448
- if isinstance(source, str) and ("entwurf" in source):
449
- pkg_removed += 1
450
- continue
451
- filtered.append(item)
452
- data["packages"] = filtered
453
-
454
505
  # --- entwurfProvider.mcpServers cleanup ------------------------------------
455
506
  # Only remove entries that look like they came from ./run.sh install: either
456
507
  # the command matches the current $REPO_DIR anchor exactly, or it ends with
@@ -484,7 +535,7 @@ if isinstance(provider, dict):
484
535
  data.pop("entwurfProvider", None)
485
536
 
486
537
  settings_path.write_text(json.dumps(data, indent=2) + "\n")
487
- print(f"remove: removed {pkg_removed} packages[] entries, {mcp_removed} mcpServers entries from {settings_path}")
538
+ print(f"remove: removed {mcp_removed} mcpServers entries from {settings_path}")
488
539
  PY
489
540
  }
490
541
 
@@ -1469,6 +1520,87 @@ check_install_preflight() {
1469
1520
  fi
1470
1521
  rm -rf "$fake" "$proj"
1471
1522
  ok "[check-install-preflight] representative dangling symlink (test -d blind spot) → fails before writing settings, names the dep"
1523
+
1524
+ # negative 3 — corrupt project provider shape. install_local_package now has a
1525
+ # two-step writer (packages[] SSOT, then entwurfProvider.mcpServers), so a
1526
+ # malformed provider MUST fail before the packages[] step writes anything.
1527
+ fake=$(mktemp -d); proj=$(mktemp -d)
1528
+ mkdir -p "$proj/.pi" "$fake/home"
1529
+ printf '{"entwurfProvider": []}\n' > "$proj/.pi/settings.json"
1530
+ local before after
1531
+ before=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1532
+ rc=0; out=$(HOME="$fake/home" PI_CODING_AGENT_DIR="$fake/home/.pi/agent" "$REPO_DIR/run.sh" install "$proj" 2>&1) || rc=$?
1533
+ after=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1534
+ if [ "$rc" -eq 0 ]; then
1535
+ fail "[check-install-preflight] corrupt entwurfProvider did NOT fail install"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1536
+ fi
1537
+ if [ "$before" != "$after" ]; then
1538
+ fail "[check-install-preflight] corrupt entwurfProvider was partially rewritten (packages[] leak)"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$fake" "$proj"; exit 1
1539
+ fi
1540
+ if ! printf '%s' "$out" | grep -q "entwurfProvider is not an object"; then
1541
+ fail "[check-install-preflight] corrupt entwurfProvider failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1542
+ fi
1543
+ if [ -f "$fake/home/.pi/agent/settings.json" ]; then
1544
+ fail "[check-install-preflight] corrupt project provider still wrote user-scope settings"; cat "$fake/home/.pi/agent/settings.json"; rm -rf "$fake" "$proj"; exit 1
1545
+ fi
1546
+ rm -rf "$fake" "$proj"
1547
+ ok "[check-install-preflight] corrupt project entwurfProvider → fails before any project/user settings write"
1548
+
1549
+ # negative 4 — corrupt project mcpServers shape, same no-partial-write contract.
1550
+ fake=$(mktemp -d); proj=$(mktemp -d)
1551
+ mkdir -p "$proj/.pi" "$fake/home"
1552
+ printf '{"entwurfProvider": {"mcpServers": []}}\n' > "$proj/.pi/settings.json"
1553
+ before=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1554
+ rc=0; out=$(HOME="$fake/home" PI_CODING_AGENT_DIR="$fake/home/.pi/agent" "$REPO_DIR/run.sh" install "$proj" 2>&1) || rc=$?
1555
+ after=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1556
+ if [ "$rc" -eq 0 ]; then
1557
+ fail "[check-install-preflight] corrupt entwurfProvider.mcpServers did NOT fail install"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1558
+ fi
1559
+ if [ "$before" != "$after" ]; then
1560
+ fail "[check-install-preflight] corrupt mcpServers was partially rewritten (packages[] leak)"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$fake" "$proj"; exit 1
1561
+ fi
1562
+ if ! printf '%s' "$out" | grep -q "entwurfProvider.mcpServers is not an object"; then
1563
+ fail "[check-install-preflight] corrupt mcpServers failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1564
+ fi
1565
+ rm -rf "$fake" "$proj"
1566
+ ok "[check-install-preflight] corrupt project entwurfProvider.mcpServers → fails before any project settings write"
1567
+
1568
+ # negative 5 — corrupt user-scope packages shape. Since install writes project
1569
+ # settings before user registration, this preflight must catch the user file up
1570
+ # front so a bad ~/.pi/agent/settings.json cannot leave the project half-wired.
1571
+ fake=$(mktemp -d); proj=$(mktemp -d)
1572
+ mkdir -p "$fake/home/.pi/agent"
1573
+ printf '{"packages": {"broken": true}}\n' > "$fake/home/.pi/agent/settings.json"
1574
+ rc=0; out=$(HOME="$fake/home" PI_CODING_AGENT_DIR="$fake/home/.pi/agent" "$REPO_DIR/run.sh" install "$proj" 2>&1) || rc=$?
1575
+ if [ "$rc" -eq 0 ]; then
1576
+ fail "[check-install-preflight] corrupt user packages did NOT fail install"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1577
+ fi
1578
+ if [ -f "$proj/.pi/settings.json" ]; then
1579
+ fail "[check-install-preflight] corrupt user packages still wrote project settings (partial install)"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$fake" "$proj"; exit 1
1580
+ fi
1581
+ if ! printf '%s' "$out" | grep -q "user settings packages is not a JSON array"; then
1582
+ fail "[check-install-preflight] corrupt user packages failed for the WRONG reason:"; echo "$out"; rm -rf "$fake" "$proj"; exit 1
1583
+ fi
1584
+ rm -rf "$fake" "$proj"
1585
+ ok "[check-install-preflight] corrupt user-scope packages → fails before project settings write"
1586
+
1587
+ # negative 6 — remove has the same two-step write risk (packages[] remove,
1588
+ # then provider cleanup). A corrupt provider must fail before packages[] is
1589
+ # altered, otherwise uninstall becomes a partial destructive write.
1590
+ proj=$(mktemp -d)
1591
+ mkdir -p "$proj/.pi"
1592
+ printf '{"entwurfProvider": [], "packages": ["%s"]}\n' "$REPO_DIR" > "$proj/.pi/settings.json"
1593
+ before=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1594
+ rc=0; out=$("$REPO_DIR/run.sh" remove "$proj" 2>&1) || rc=$?
1595
+ after=$(sha256sum "$proj/.pi/settings.json" | cut -d' ' -f1)
1596
+ if [ "$rc" -eq 0 ]; then
1597
+ fail "[check-install-preflight] corrupt provider did NOT fail remove"; echo "$out"; rm -rf "$proj"; exit 1
1598
+ fi
1599
+ if [ "$before" != "$after" ]; then
1600
+ fail "[check-install-preflight] corrupt provider remove partially rewrote packages[]"; echo "$out"; cat "$proj/.pi/settings.json"; rm -rf "$proj"; exit 1
1601
+ fi
1602
+ rm -rf "$proj"
1603
+ ok "[check-install-preflight] remove with corrupt provider → fails before packages[] removal"
1472
1604
  }
1473
1605
 
1474
1606
  check_pi_preflight() {
@@ -1735,6 +1867,12 @@ check_pack() {
1735
1867
  # doctor.sh runs this prebuilt JS when installed under node_modules (strip-types
1736
1868
  # refuses the .ts there), same boundary start.sh crosses. build-bridge emits it.
1737
1869
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js"
1870
+ # 0.12.5 — the node_modules-safe plugin hook + its lib. install-meta-bridge copies
1871
+ # these compiled JS into the assembled plugin when installed (raw .ts can't
1872
+ # strip-types under node_modules). meta-session.js is shared with the store-doctor
1873
+ # above; listed here too so the hook axis fails loud if the emit graph drops it.
1874
+ "mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
1875
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
1738
1876
  "scripts/postinstall-chmod.cjs"
1739
1877
  "pi/entwurf-capabilities.json"
1740
1878
  "pi/entwurf-targets.json"
@@ -1887,6 +2025,10 @@ check_pack_install() {
1887
2025
  # 0.12.4 — prebuilt node_modules-safe store-scan artifact for the doctor
1888
2026
  # (see check-pack). The installed-scan smoke below runs exactly this file.
1889
2027
  "mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js"
2028
+ # 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
2029
+ # hook regression below runs exactly this compiled JS from under node_modules.
2030
+ "mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
2031
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
1890
2032
  "scripts/postinstall-chmod.cjs"
1891
2033
  "pi/entwurf-capabilities.json"
1892
2034
  "pi/entwurf-targets.json"
@@ -1992,8 +2134,14 @@ check_pack_install() {
1992
2134
  return 1
1993
2135
  fi
1994
2136
 
1995
- local loader_out
1996
- loader_out=$(cd "$tmp" && pi -e "$tmp/node_modules/@junghanacs/entwurf" --list-models entwurf 2>&1) || {
2137
+ # HOME/PI_CODING_AGENT_DIR redirected to a throwaway dir: this loader smoke must
2138
+ # depend ONLY on the -e package path, never on the operator's real
2139
+ # ~/.pi/agent/settings.json — otherwise a live-config change could silently
2140
+ # pass/fail the gate (the exact "check must not depend on live wiring" impurity
2141
+ # this whole lane is about).
2142
+ local loader_out loader_home="$tmp/loader-home"
2143
+ 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) || {
1997
2145
  fail "[check-pack-install] pi loader smoke failed (exit non-zero):"
1998
2146
  echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
1999
2147
  return 1
@@ -2043,8 +2191,8 @@ check_pack_install() {
2043
2191
  fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
2044
2192
  return 1
2045
2193
  fi
2046
- if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ]; then
2047
- fail "[check-pack-install] npm-managed neutral install missing package bins (entwurf / entwurf-bridge)"
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)"
2048
2196
  ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
2049
2197
  return 1
2050
2198
  fi
@@ -2065,6 +2213,158 @@ check_pack_install() {
2065
2213
  fi
2066
2214
  echo "[check-pack-install] npm-managed install regression pass (hoisted-dep run.sh install wrote settings)"
2067
2215
 
2216
+ # THE 2026-07-03 regression gate: removing `pi install` from setup dropped
2217
+ # user-scope citizen registration, so `--entwurf-control` was Unknown in a
2218
+ # foreign cwd. Prove the npm consumer path closes it end-to-end: run.sh install
2219
+ # (run above under HOME="$npmhome") must have registered the package in the USER
2220
+ # settings, and pi must then load the entwurf extension + its flags from a cwd
2221
+ # OUTSIDE the project. Fully isolated in the temp HOME — never touches ~/.pi.
2222
+ local npm_user_settings="$npmhome/.pi/agent/settings.json"
2223
+ if [ ! -f "$npm_user_settings" ]; then
2224
+ fail "[check-pack-install] npm-managed install did not register a user-scope citizen at $npm_user_settings"
2225
+ return 1
2226
+ fi
2227
+ if ! python3 -c "
2228
+ import json,sys
2229
+ p=json.load(open('$npm_user_settings')).get('packages',[])
2230
+ srcs=[(x if isinstance(x,str) else x.get('source')) for x in p]
2231
+ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/entwurf') for s in srcs) else 1)
2232
+ "; then
2233
+ fail "[check-pack-install] user-scope settings.json lacks the npm entwurf package in packages[]:"
2234
+ sed 's/^/ /' "$npm_user_settings" >&2
2235
+ return 1
2236
+ fi
2237
+ # Foreign cwd ($tmp — NOT the project, no project .pi): --entwurf-control must be
2238
+ # a KNOWN flag and the entwurf provider must load, sourced only from user scope.
2239
+ # Before the fix this printed "Unknown options: --entwurf-control".
2240
+ 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) || {
2242
+ fail "[check-pack-install] foreign-cwd --entwurf-control smoke failed (user-scope citizen not loading?):"
2243
+ echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
2244
+ return 1
2245
+ }
2246
+ if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-4-8" <<<"$foreign_out"; then
2247
+ fail "[check-pack-install] foreign-cwd --entwurf-control did not load the entwurf extension from user scope:"
2248
+ echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
2249
+ return 1
2250
+ fi
2251
+ echo "[check-pack-install] user-scope citizen regression pass (npm consumer: --entwurf-control loads from a foreign cwd)"
2252
+
2253
+ # Installed meta-bridge ownership regression (0.12.5): package upgrades must not
2254
+ # bake versioned pnpm-store paths into Claude settings. MCP already uses the
2255
+ # stable `entwurf-bridge` bin; statusLine must now use `entwurf-statusline`, and
2256
+ # the plugin marketplace source must be the version-stable operator data dir
2257
+ # rather than <node_modules>/pi/meta-bridge/.assembled. Use a fake claude CLI so
2258
+ # this stays deterministic/offline while running the REAL installed
2259
+ # install-meta-bridge path and meta-bridge-state apply.
2260
+ local fake_claude_dir="$npm_tmp/fake-claude-bin" fake_claude_log="$npm_tmp/fake-claude.log"
2261
+ mkdir -p "$fake_claude_dir"
2262
+ cat > "$fake_claude_dir/claude" <<'SH'
2263
+ #!/usr/bin/env bash
2264
+ printf '%s\n' "$*" >> "${FAKE_CLAUDE_LOG:?}"
2265
+ case "$1${2:+ $2}" in
2266
+ "--version") echo "2.1.197 (Claude Code)" ;;
2267
+ "plugin validate") : ;;
2268
+ "plugin uninstall") : ;;
2269
+ "plugin marketplace") : ;;
2270
+ "plugin install") : ;;
2271
+ "plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
2272
+ "mcp remove") : ;;
2273
+ "mcp add") : ;;
2274
+ "mcp get") printf '%s\n' "Scope: User config" "Status: ✔ Connected" ;;
2275
+ *) : ;;
2276
+ esac
2277
+ exit 0
2278
+ SH
2279
+ chmod +x "$fake_claude_dir/claude"
2280
+ local mb_home="$npm_tmp/meta-home" mb_cfg="$npm_tmp/meta-claude" mb_log
2281
+ 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) || {
2283
+ fail "[check-pack-install] installed install-meta-bridge failed under fake claude:"
2284
+ echo "$mb_log" | tail -20 | sed 's/^/ /' >&2
2285
+ return 1
2286
+ }
2287
+ local stable_asm="$mb_home/.local/share/entwurf/meta-bridge/.assembled"
2288
+ local installed_hook="$stable_asm/entwurf-meta-receive/meta-bridge-hook.js"
2289
+ if [ ! -f "$installed_hook" ]; then
2290
+ fail "[check-pack-install] installed meta-bridge did not assemble the compiled hook JS into the stable operator data dir: $installed_hook"
2291
+ return 1
2292
+ fi
2293
+ if [ -f "$stable_asm/entwurf-meta-receive/meta-bridge-hook.ts" ]; then
2294
+ fail "[check-pack-install] installed meta-bridge shipped a raw .ts hook — installed packages must run compiled JS (strip-types is refused under node_modules)"
2295
+ return 1
2296
+ fi
2297
+ # The artifact existing is not enough (review BLOCKER 1): Claude runs the hooks.json
2298
+ # COMMAND, so assert the baked command actually targets the compiled .js with both
2299
+ # placeholders resolved. A bake that mis-targeted .ts (or left a placeholder) would
2300
+ # still pass the direct-JS smoke below, then fail live.
2301
+ local installed_hooks_json="$stable_asm/entwurf-meta-receive/hooks/hooks.json"
2302
+ if ! grep -q 'meta-bridge-hook\.js' "$installed_hooks_json"; then
2303
+ fail "[check-pack-install] installed hooks.json does not point at the compiled meta-bridge-hook.js: $installed_hooks_json"
2304
+ return 1
2305
+ fi
2306
+ if grep -qE 'meta-bridge-hook\.ts|__HOOK_ENTRY__|__NODE_BIN__' "$installed_hooks_json"; then
2307
+ fail "[check-pack-install] installed hooks.json references a raw .ts entry or an unbaked placeholder (__HOOK_ENTRY__/__NODE_BIN__): $installed_hooks_json"
2308
+ return 1
2309
+ fi
2310
+ if [ -e "$npm_pkg/pi/meta-bridge/.assembled/entwurf-meta-receive" ]; then
2311
+ fail "[check-pack-install] installed meta-bridge still assembled inside the versioned package store: $npm_pkg/pi/meta-bridge/.assembled"
2312
+ return 1
2313
+ fi
2314
+ # 0.12.5 strip-types-fence regression (GPT safety pin). The compiled hook must run
2315
+ # FROM UNDER node_modules with plain node — the exact fence that broke oracle's
2316
+ # 0.12.4 raw-.ts hook (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). L2 relocates
2317
+ # the runtime marketplace source to XDG, but this hardens the ARTIFACT itself so
2318
+ # it is safe even if a future cache/marketplace path lands under node_modules.
2319
+ local nm_probe="$npmroot/node_modules/@junghanacs/entwurf/pi/meta-bridge/.assembled-packprobe/entwurf-meta-receive"
2320
+ mkdir -p "$nm_probe/lib"
2321
+ cp "$installed_hook" "$nm_probe/meta-bridge-hook.js"
2322
+ cp "$stable_asm/entwurf-meta-receive/lib/meta-session.js" "$nm_probe/lib/meta-session.js"
2323
+ cp "$stable_asm/entwurf-meta-receive/lib/session-id.js" "$nm_probe/lib/session-id.js"
2324
+ cp "$stable_asm/entwurf-meta-receive/entwurf-capabilities.json" "$nm_probe/entwurf-capabilities.json"
2325
+ local probe_env='{"session_id":"pack-probe","transcript_path":"/tmp/x.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"probe"}}'
2326
+ local probe_out
2327
+ if probe_out="$(printf '%s' "$probe_env" | env PI_CODING_AGENT_DIR="$(mktemp -d)" CLAUDE_PLUGIN_ROOT="$nm_probe" node "$nm_probe/meta-bridge-hook.js" 2>&1)" && printf '%s' "$probe_out" | grep -q hookSpecificOutput; then
2328
+ : # compiled hook crosses the node_modules strip-types fence safely
2329
+ else
2330
+ fail "[check-pack-install] compiled hook failed to run under node_modules with plain node: $(printf '%s' "$probe_out" | tr '\n' ' ' | cut -c1-200)"
2331
+ rm -rf "$nm_probe"; return 1
2332
+ fi
2333
+ # FAIL-reproduction (review BLOCKER 2): a raw .ts at the SAME node_modules location
2334
+ # must be refused SPECIFICALLY by the strip-types fence — not by some unrelated
2335
+ # failure. A missing lib would also exit nonzero and hollow out the proof, so
2336
+ # capture stderr and require the exact ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING.
2337
+ cp "$npm_pkg/pi-extensions/meta-bridge-hook.ts" "$nm_probe/meta-bridge-hook.ts"
2338
+ local ts_out ts_rc
2339
+ ts_out="$(printf '%s' "$probe_env" | env PI_CODING_AGENT_DIR="$(mktemp -d)" CLAUDE_PLUGIN_ROOT="$nm_probe" node "$nm_probe/meta-bridge-hook.ts" 2>&1)" && ts_rc=0 || ts_rc=$?
2340
+ if [ "${ts_rc:-0}" -eq 0 ]; then
2341
+ fail "[check-pack-install] raw .ts hook UNEXPECTEDLY ran under node_modules — strip-types fence moved; the compiled-hook rationale must be revisited"
2342
+ rm -rf "$nm_probe"; return 1
2343
+ fi
2344
+ if ! printf '%s' "$ts_out" | grep -q 'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING'; then
2345
+ fail "[check-pack-install] raw .ts hook failed under node_modules but NOT via the strip-types fence (expected ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING); got: $(printf '%s' "$ts_out" | tr '\n' ' ' | cut -c1-200) — the regression no longer proves the compiled-hook rationale"
2346
+ rm -rf "$nm_probe"; return 1
2347
+ fi
2348
+ rm -rf "$nm_probe"
2349
+ echo "[check-pack-install] installed hook is node_modules-safe compiled JS (hooks.json points at .js; runs under node_modules with plain node; raw .ts refused there by the strip-types fence)"
2350
+ python3 - "$mb_cfg/settings.json" "$mb_home/.claude.json" "$stable_asm" <<'PY'
2351
+ import json, sys
2352
+ settings = json.load(open(sys.argv[1]))
2353
+ root = json.load(open(sys.argv[2]))
2354
+ stable_asm = sys.argv[3]
2355
+ market = settings.get("extraKnownMarketplaces", {}).get("meta-bridge-local", {})
2356
+ assert market == {"source": {"source": "directory", "path": stable_asm}}, market
2357
+ assert settings.get("statusLine") == {"type": "command", "command": "entwurf-statusline"}, settings.get("statusLine")
2358
+ mcp = root.get("mcpServers", {}).get("entwurf-bridge", {})
2359
+ assert mcp.get("command") == "entwurf-bridge" and mcp.get("args") == [], mcp
2360
+ PY
2361
+ if ! grep -q "$stable_asm" "$fake_claude_log"; then
2362
+ fail "[check-pack-install] fake claude did not receive the stable marketplace path during install-meta-bridge"
2363
+ sed 's/^/ /' "$fake_claude_log" >&2 || true
2364
+ return 1
2365
+ fi
2366
+ echo "[check-pack-install] installed meta-bridge ownership pass (stable statusline bin + stable marketplace dir + stable MCP bin)"
2367
+
2068
2368
  # 0.12.1 C — installed bridge BOOT regression. This is the test whose absence
2069
2369
  # let the 0.12.0 install bug ship: the README's bridge launcher was
2070
2370
  # `node --experimental-strip-types src/index.ts`, which Node REFUSES under
@@ -2873,6 +3173,14 @@ case "$cmd" in
2873
3173
  # duplicate/drift records. Offline + deterministic (deps bash+node+python3).
2874
3174
  (cd "$REPO_DIR" && bash scripts/smoke-meta-install-state.sh)
2875
3175
  ;;
3176
+ smoke-user-scope-citizen)
3177
+ # 0.12.6 install-boundary gate: register-pi-package.py is the shared
3178
+ # packages[] SSOT for project/user install and remove; user scope makes
3179
+ # --entwurf-control load from any cwd. Idempotent, preserves unrelated
3180
+ # packages/keys, normalizes stale entries, remove is symmetric, and corrupt
3181
+ # settings fail loud. The tripwire the 2026-07-03 `pi install` removal lacked.
3182
+ (cd "$REPO_DIR" && bash scripts/smoke-user-scope-citizen.sh)
3183
+ ;;
2876
3184
  smoke-claude-native-resume-live)
2877
3185
  # LIVE-only Detour A probe: two real Claude Code native turns (fresh +
2878
3186
  # --resume) in a scratch cwd. Verifies native resume works while the
@@ -2882,9 +3190,10 @@ case "$cmd" in
2882
3190
  ;;
2883
3191
  install-meta-bridge)
2884
3192
  # 1.0.0 meta-bridge step 5: operator-grade GLOBAL install of the garden-native
2885
- # receive plugin. Assembles a self-contained, node-path-baked copy under
2886
- # pi/meta-bridge/.assembled and runs marketplace add + install --scope user, so
2887
- # every native Claude Code session auto-loads it (no manual --plugin-dir).
3193
+ # receive plugin. Assembles a self-contained, node-path-baked copy under the
3194
+ # XDG data dir ($XDG_DATA_HOME/entwurf/meta-bridge/.assembled dev clone and
3195
+ # installed package alike, never the checkout) and runs marketplace add +
3196
+ # install --scope user, so every native Claude Code session auto-loads it.
2888
3197
  # Idempotent; Linux/macOS only (Windows fail-fast).
2889
3198
  (cd "$REPO_DIR" && bash scripts/meta-bridge-install.sh "$@")
2890
3199
  ;;
@@ -17,6 +17,23 @@ MKT_NAME="meta-bridge-local"
17
17
  PLUGIN="entwurf-meta-receive"
18
18
  CLAUDE_CFG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
19
19
  REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ # The live artifact lives under the XDG data dir (dev clone and installed package
21
+ # alike — never the checkout). Prefer the path RECORDED in install-state so a doctor
22
+ # run under a different XDG_DATA_HOME than install still hashes the REAL assembled
23
+ # bundle (and state.py check compares the same recorded marketplace path); fall back
24
+ # to the current-XDG computation only when no state exists yet (fresh host).
25
+ # LIB_EXT still tracks the hook artifact mode (0.12.5): installed packages ship the
26
+ # tsc-emitted `.js` closure (node_modules-safe), dev clones run the `.ts` source.
27
+ # The writer-version parity below hashes meta-session.<LIB_EXT> so an installed
28
+ # `.js` bundle is compared against the SAME-pipeline dist `.js`, never against the
29
+ # `.ts` source (which would hash-mismatch and false-STALE).
30
+ ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge/.assembled"
31
+ RECORDED_ASM="$(python3 "$REPO/scripts/meta-bridge-state.py" assembled-path --repo "$REPO" 2>/dev/null || true)"
32
+ [ -n "$RECORDED_ASM" ] && ASM="$RECORDED_ASM"
33
+ case "$REPO" in
34
+ */node_modules/@junghanacs/entwurf) LIB_EXT="js" ;;
35
+ *) LIB_EXT="ts" ;;
36
+ esac
20
37
  # shellcheck source=scripts/meta-bridge-hook-log.sh
21
38
  source "$REPO/scripts/meta-bridge-hook-log.sh"
22
39
 
@@ -65,7 +82,7 @@ if command -v python3 >/dev/null; then
65
82
  # which key drifted — silent instead of fail-loud. As an `if` condition the
66
83
  # substitution's nonzero status is consumed (set -e suspended), so we always
67
84
  # 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
85
+ if CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$ASM" 2>&1 >/dev/null)"; then
69
86
  ok "state file present and managed settings/MCP keyset survives intact"
70
87
  else
71
88
  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:"
@@ -93,8 +110,23 @@ if [ -n "$CACHE_HOOKS" ]; then
93
110
  # line 158) so BAKED="" routes to that bad(...): an installed hooks.json exists but
94
111
  # the doctor cannot read its load-bearing hook command, so the store-churn guard is
95
112
  # 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"
113
+ # Match either hook artifact (0.12.5): installed ships meta-bridge-hook.js, dev
114
+ # clones meta-bridge-hook.ts. Capture the whole command so we run the SAME entry
115
+ # file the live hook runs (never a hardcoded extension that drifts from install).
116
+ HOOK_CMD="$({ grep -oE '"command": "[^ ]+ \$\{CLAUDE_PLUGIN_ROOT\}/meta-bridge-hook\.(ts|js)"' "$CACHE_HOOKS" || true; } | head -1)"
117
+ BAKED="$(printf '%s' "$HOOK_CMD" | sed -E 's/.*"command": "([^ ]+) .*/\1/')"
118
+ HOOK_FILE="$(printf '%s' "$HOOK_CMD" | sed -E 's#.*/([^/"]+)"$#\1#')"
119
+ if [ -n "$BAKED" ] && [ -x "$BAKED" ]; then
120
+ ok "baked node exists + executable: $BAKED"
121
+ CACHE_ROOT="$(cd "$(dirname "$CACHE_HOOKS")/.." && pwd)"
122
+ TMP_AGENT="$(mktemp -d 2>/dev/null || mktemp -d -t entwurf-doctor-hook)"
123
+ HOOK_ENV='{"session_id":"doctor-synthetic-native","transcript_path":"/tmp/entwurf-doctor-synthetic-transcript.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"doctor-model"}}'
124
+ if HOOK_OUT="$(printf '%s' "$HOOK_ENV" | env PI_CODING_AGENT_DIR="$TMP_AGENT" CLAUDE_PLUGIN_ROOT="$CACHE_ROOT" "$BAKED" "$CACHE_ROOT/$HOOK_FILE" 2>&1)" && printf '%s' "$HOOK_OUT" | grep -q 'hookSpecificOutput'; then
125
+ ok "cached SessionStart hook executes cleanly in an isolated temp agent dir"
126
+ else
127
+ bad "cached SessionStart hook failed to execute — stale plugin cache / unsupported TS syntax / broken bundle. Re-run install-meta-bridge from the intended surface. Detail: $(printf '%s' "$HOOK_OUT" | tr '\n' ' ' | cut -c1-300)"
128
+ fi
129
+ rm -rf "$TMP_AGENT" 2>/dev/null || true
98
130
  elif [ -n "$BAKED" ]; then bad "baked node path is DEAD (nix GC / version bump?): $BAKED — re-run ./run.sh install-meta-bridge"
99
131
  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
132
  else
@@ -114,9 +146,15 @@ except Exception:
114
146
  print('')
115
147
  PY
116
148
  )"
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
149
+ EXPECTED_STATUSLINE="$(python3 "$REPO/scripts/meta-bridge-state.py" desired-statusline --repo "$REPO" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("command", ""))')"
150
+ if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command matches expected (dev: checkout path; installed: entwurf-statusline bin shim): $STATUSLINE_CMD"; else bad "statusLine.command drifted (got '$STATUSLINE_CMD', expected '$EXPECTED_STATUSLINE')"; fi
151
+ if [[ "$EXPECTED_STATUSLINE" == */* ]]; then
152
+ if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
153
+ elif command -v "$EXPECTED_STATUSLINE" >/dev/null 2>&1; then
154
+ ok "statusline bin resolves on PATH: $(command -v "$EXPECTED_STATUSLINE")"
155
+ else
156
+ bad "statusline bin not on PATH: $EXPECTED_STATUSLINE"
157
+ fi
120
158
  SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
121
159
  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
160
  else
@@ -241,10 +279,17 @@ registry_for_ms() { # $1=bundle meta-session.ts → sibling plugin-root registry
241
279
  dirname "$(dirname "$1")"
242
280
  }
243
281
 
244
- SRC_MS="$REPO/pi-extensions/lib/meta-session.ts"
282
+ # Source authority for parity depends on the mode: an installed bundle carries the
283
+ # dist `.js`, so compare it against the dist `.js` (same tsc pipeline → hash-equal);
284
+ # a dev clone carries the `.ts`, so compare against the `.ts` source.
285
+ if [ "$LIB_EXT" = "js" ]; then
286
+ SRC_MS="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
287
+ else
288
+ SRC_MS="$REPO/pi-extensions/lib/meta-session.ts"
289
+ fi
245
290
  SRC_REG="$REPO/pi/entwurf-capabilities.json"
246
- ASM_MS="$REPO/pi/meta-bridge/.assembled/$PLUGIN/lib/meta-session.ts"
247
- INST_MS="$(ls "$CLAUDE_CFG"/plugins/cache/"$MKT_NAME"/"$PLUGIN"/*/lib/meta-session.ts 2>/dev/null | head -1 || true)"
291
+ ASM_MS="$ASM/$PLUGIN/lib/meta-session.$LIB_EXT"
292
+ INST_MS="$(ls "$CLAUDE_CFG"/plugins/cache/"$MKT_NAME"/"$PLUGIN"/*/lib/meta-session."$LIB_EXT" 2>/dev/null | head -1 || true)"
248
293
 
249
294
  ASM_ROOT="$(registry_for_ms "$ASM_MS")"
250
295
  INST_ROOT="$(registry_for_ms "${INST_MS:-}")"