@junghanacs/entwurf 0.12.5 → 0.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/AGENTS.md +31 -17
  2. package/BASELINE.md +42 -8
  3. package/CHANGELOG.md +44 -0
  4. package/DELIVERY.md +71 -14
  5. package/README.md +77 -45
  6. package/VERIFY.md +33 -38
  7. package/docs/setup-clean-host.md +80 -22
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
  30. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
  31. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
  33. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
  34. package/mcp/entwurf-bridge/src/index.ts +101 -67
  35. package/mcp/entwurf-bridge/test.sh +1 -1
  36. package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
  37. package/package.json +10 -6
  38. package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
  39. package/pi-extensions/lib/entwurf-self-address.ts +58 -15
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
  41. package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
  42. package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
  43. package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
  44. package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
  45. package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
  46. package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
  47. package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
  48. package/pi-extensions/lib/meta-sender-identity.ts +154 -0
  49. package/pi-extensions/lib/native-push/adapter.ts +255 -0
  50. package/pi-extensions/lib/native-push/register.ts +99 -0
  51. package/run.sh +974 -248
  52. package/scripts/agy-bridge-config.py +446 -0
  53. package/scripts/agy-bridge.sh +359 -0
  54. package/scripts/agy-hooks-bridge.sh +193 -0
  55. package/scripts/agy-hooks-config.py +257 -0
  56. package/scripts/agy-imprint.sh +28 -0
  57. package/scripts/agy-imprint.ts +193 -0
  58. package/scripts/agy-statusline-bridge.sh +176 -0
  59. package/scripts/agy-statusline-config.py +213 -0
  60. package/scripts/agy-statusline.sh +256 -0
  61. package/scripts/build-bridge.sh +20 -0
  62. package/scripts/check-agy-sender-identity.ts +364 -0
  63. package/scripts/check-entwurf-bridge-boot.ts +8 -2
  64. package/scripts/check-entwurf-deliverability.ts +34 -0
  65. package/scripts/check-entwurf-self-address.ts +78 -11
  66. package/scripts/check-entwurf-v2-contract.ts +136 -1
  67. package/scripts/check-entwurf-v2-decider.ts +95 -1
  68. package/scripts/check-entwurf-v2-matrix.ts +14 -3
  69. package/scripts/check-entwurf-v2-native-push.ts +193 -0
  70. package/scripts/check-entwurf-v2-production.ts +68 -1
  71. package/scripts/check-entwurf-v2-runner.ts +58 -0
  72. package/scripts/check-entwurf-v2-surface.ts +35 -0
  73. package/scripts/check-install-surface.ts +357 -0
  74. package/scripts/check-native-push-adapter.ts +319 -0
  75. package/scripts/check-native-push-register.ts +130 -0
  76. package/scripts/dev-bin.sh +195 -0
  77. package/scripts/doctor-pi-provider.ts +140 -0
  78. package/scripts/meta-bridge-doctor.sh +47 -9
  79. package/scripts/meta-bridge-install.sh +20 -17
  80. package/scripts/meta-bridge-state.py +40 -2
  81. package/scripts/meta-bridge-uninstall.sh +23 -12
  82. package/scripts/register-pi-package.py +183 -0
  83. package/scripts/register-pi-provider.py +287 -0
  84. package/scripts/smoke-agy-hooks-state.sh +172 -0
  85. package/scripts/smoke-agy-install-state.sh +660 -0
  86. package/scripts/smoke-agy-native-push-live.ts +243 -0
  87. package/scripts/smoke-agy-statusline-state.sh +300 -0
  88. package/scripts/smoke-meta-async-drift.sh +9 -2
  89. package/scripts/smoke-meta-install-state.sh +178 -8
  90. package/scripts/smoke-pi-provider-state.sh +182 -0
  91. package/scripts/smoke-user-scope-citizen.sh +191 -0
  92. package/scripts/with-dist-lock.sh +81 -0
  93. package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
@@ -22,10 +22,58 @@ TMP="$(mktemp -d -t psa-meta-install-state.XXXXXX)"
22
22
  cleanup() { rm -rf "$TMP"; }
23
23
  trap cleanup EXIT
24
24
 
25
+ # ⓪ 설치 경계 봉쇄 (2026-07-03 `?` 사건의 구조적 소멸): source origin(repo/npm)과
26
+ # live artifact(XDG)를 분리한다. dev clone과 installed 소비자는 같은 user-scope
27
+ # install이고, 둘 다 marketplace source를 XDG data dir에 조립한다 — 절대 checkout
28
+ # 내부가 아니다. 이 smoke의 어떤 단계도 repo 안에 live source를 만들지 않음을
29
+ # top/bottom fingerprint로 감싸 증명하고, 아래 fake-install로 "install → XDG"를 직접 건다.
30
+ # present/absent만 보면 present→present 덮어쓰기를 놓치므로 내용 해시까지 비교한다.
31
+ asm_fingerprint() {
32
+ if [ -e "$REPO/pi/meta-bridge/.assembled" ]; then (cd "$REPO/pi/meta-bridge/.assembled" && find . -type f -exec sha256sum {} + 2>/dev/null | sort); else echo ABSENT; fi
33
+ }
34
+ REPO_ASM_FP_START="$(asm_fingerprint)"
35
+
36
+ # 실제 meta-bridge-install.sh를 격리된 HOME + XDG_DATA_HOME + fake claude로 돌려
37
+ # live marketplace source가 XDG 아래에 조립되고 `plugin marketplace add`가 그 XDG
38
+ # 경로를 받는지 직접 증명한다(오프라인: 실제 claude 없음).
39
+ INS_HOME="$TMP/ins-home"; INS_XDG="$TMP/ins-xdg"; INS_BIN="$TMP/ins-bin"
40
+ mkdir -p "$INS_HOME/.claude" "$INS_BIN"
41
+ echo '{}' > "$INS_HOME/.claude/settings.json"
42
+ echo '{}' > "$INS_HOME/.claude.json"
43
+ INS_LOG="$TMP/ins-claude.log"
44
+ cat > "$INS_BIN/claude" <<'SH'
45
+ #!/usr/bin/env bash
46
+ printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
47
+ case "$1${2:+ $2}" in
48
+ "plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
49
+ "mcp get") printf '%s\n' "Scope: User config" "Status: Connected" ;;
50
+ *) : ;;
51
+ esac
52
+ exit 0
53
+ SH
54
+ chmod +x "$INS_BIN/claude"
55
+ if env HOME="$INS_HOME" CLAUDE_CONFIG_DIR="$INS_HOME/.claude" XDG_DATA_HOME="$INS_XDG" \
56
+ FAKE_CLAUDE_LOG="$INS_LOG" PATH="$INS_BIN:$PATH" \
57
+ bash "$REPO/scripts/meta-bridge-install.sh" >/dev/null 2>&1; then
58
+ if [ -e "$INS_XDG/entwurf/meta-bridge/.assembled/entwurf-meta-receive" ]; then ok "dev install assembles the live marketplace source under XDG (not the checkout)"; else bad "dev install did not assemble under XDG_DATA_HOME"; fi
59
+ MKT_ADD="$(grep '^plugin marketplace add ' "$INS_LOG" | head -1 || true)"
60
+ if printf '%s' "$MKT_ADD" | grep -Fq "$INS_XDG/entwurf/meta-bridge/.assembled"; then ok "claude plugin marketplace add received the XDG artifact path"; else bad "marketplace add did not point at XDG: $MKT_ADD"; fi
61
+ else
62
+ bad "real install-meta-bridge failed under isolated HOME/XDG/fake-claude:"$'\n'"$(cat "$INS_LOG" 2>/dev/null)"
63
+ fi
64
+
25
65
  export HOME="$TMP/home"
66
+ # The wrapper-uninstall below removes ${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/
67
+ # meta-bridge. Pin XDG_DATA_HOME into the sandbox too, so a developer who runs this
68
+ # smoke with XDG_DATA_HOME set in their shell can never have their real live
69
+ # artifact removed — the boundary that keeps check/smoke off global user wiring.
70
+ export XDG_DATA_HOME="$TMP/xdg"
26
71
  export CLAUDE_CONFIG_DIR="$HOME/.claude"
27
72
  mkdir -p "$CLAUDE_CONFIG_DIR"
28
- ASM="$TMP/assembled-marketplace"
73
+ # Canonical-suffix asm: production install ALWAYS records a path ending in
74
+ # …/entwurf/meta-bridge/.assembled, and state.py check now shape-validates that
75
+ # suffix — so the survival harness must use a realistic path, not a bare stub.
76
+ ASM="$TMP/asm-xdg/entwurf/meta-bridge/.assembled"
29
77
  export ASM
30
78
  mkdir -p "$ASM"
31
79
 
@@ -213,8 +261,10 @@ then ok "uninstall restores scalars/maps and removes only managed array addition
213
261
 
214
262
  if py uninstall >/dev/null 2>&1; then bad "state manager uninstall without state should not guess"; else ok "state manager uninstall without state fails instead of guessing"; fi
215
263
 
216
- # The wrapper must ALSO refuse before side effects. A fake claude records whether
217
- # plugin/MCP removals were attempted; no state means the log must stay empty.
264
+ # 봉쇄: wrapper-uninstall을 실제 $REPO 스크립트로 직접 돌린다. 이제 uninstall이
265
+ # 지우는 live artifact는 ${XDG_DATA_HOME}/entwurf/meta-bridge 하나뿐이고, 위에서
266
+ # HOME + XDG_DATA_HOME를 샌드박스로 고정했으므로 실제 개발자 배선/체크아웃에는
267
+ # 닿을 수 없다(2026-07-03 `?` 사건의 구조적 소멸 — 사본 repo 우회가 더는 불필요).
218
268
  FAKE_BIN="$TMP/fake-bin"; mkdir -p "$FAKE_BIN"
219
269
  cat > "$FAKE_BIN/claude" <<'SH'
220
270
  #!/usr/bin/env bash
@@ -222,6 +272,9 @@ printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
222
272
  exit 0
223
273
  SH
224
274
  chmod +x "$FAKE_BIN/claude"
275
+
276
+ # The wrapper must refuse before side effects. A fake claude records whether
277
+ # plugin/MCP removals were attempted; no state means the log must stay empty.
225
278
  FAKE_CLAUDE_LOG="$TMP/fake-claude-nostate.log"
226
279
  if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
227
280
  bad "wrapper uninstall without state should fail"
@@ -229,10 +282,14 @@ else
229
282
  if [ ! -s "$FAKE_CLAUDE_LOG" ]; then ok "wrapper uninstall without state has zero Claude side effects"; else bad "wrapper uninstall without state touched Claude: $(cat "$FAKE_CLAUDE_LOG")"; fi
230
283
  fi
231
284
 
232
- # With valid state, the wrapper may remove Claude registrations and then restore
233
- # JSON state. This proves the preflight gate is ordered before side effects.
234
- py prepare >/dev/null
235
- py apply >/dev/null
285
+ # With valid state, the wrapper removes Claude registrations, restores JSON state,
286
+ # AND removes the XDG live artifact RECORDED in state never the checkout, never a
287
+ # path recomputed from the env. Record an XDG-structured assembled path and seed the
288
+ # sentinel artifact so we can prove uninstall drops exactly the recorded tree.
289
+ WASM="$XDG_DATA_HOME/entwurf/meta-bridge/.assembled"
290
+ python3 "$STATE" prepare --repo "$REPO" --asm "$WASM" >/dev/null
291
+ python3 "$STATE" apply --repo "$REPO" --asm "$WASM" >/dev/null
292
+ mkdir -p "$WASM"; : > "$WASM/.sentinel"
236
293
  FAKE_CLAUDE_LOG="$TMP/fake-claude-state.log"
237
294
  if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
238
295
  if grep -q 'plugin uninstall entwurf-meta-receive@meta-bridge-local' "$FAKE_CLAUDE_LOG" && grep -q 'mcp remove entwurf-bridge -s user' "$FAKE_CLAUDE_LOG" && [ ! -f "$STATE_FILE" ]; then
@@ -240,10 +297,97 @@ if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts
240
297
  else
241
298
  bad "wrapper uninstall with state missed expected side effects/state removal"
242
299
  fi
300
+ # 봉쇄가 회피가 아니라 구조임을 증명: uninstall이 지우는 건 recorded XDG artifact뿐.
301
+ if [ ! -e "$XDG_DATA_HOME/entwurf/meta-bridge" ]; then ok "wrapper removes the recorded XDG live artifact tree, not the checkout"; else bad "wrapper did not remove the recorded XDG live artifact"; fi
243
302
  else
244
303
  bad "wrapper uninstall with valid state failed"
245
304
  fi
246
305
 
306
+ # Honest inverse under a CHANGED XDG_DATA_HOME (GPT hardening): uninstall must remove
307
+ # the RECORDED artifact path, never one recomputed from the current env. Record A,
308
+ # run uninstall with the env pointing at B, and prove A is gone while B is untouched.
309
+ XDGA="$TMP/xdgA"; XDGB="$TMP/xdgB"
310
+ ASM_A="$XDGA/entwurf/meta-bridge/.assembled"
311
+ python3 "$STATE" prepare --repo "$REPO" --asm "$ASM_A" >/dev/null
312
+ python3 "$STATE" apply --repo "$REPO" --asm "$ASM_A" >/dev/null
313
+ mkdir -p "$ASM_A"; : > "$ASM_A/.sentinel"
314
+ mkdir -p "$XDGB/entwurf/meta-bridge/.assembled"; : > "$XDGB/entwurf/meta-bridge/.assembled/.sentinel"
315
+ if XDG_DATA_HOME="$XDGB" PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$TMP/fake-claude-mismatch.log" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
316
+ if [ ! -e "$XDGA/entwurf/meta-bridge" ]; then ok "uninstall removes the RECORDED artifact (XDG A) even when env XDG_DATA_HOME differs"; else bad "uninstall did not remove the recorded artifact A — recomputed from env instead"; fi
317
+ if [ -e "$XDGB/entwurf/meta-bridge/.assembled/.sentinel" ]; then ok "uninstall leaves the current-env XDG (B) untouched (no env recompute)"; else bad "uninstall wrongly removed the current-env XDG B"; fi
318
+ else
319
+ bad "wrapper uninstall (recorded-path mismatch case) failed"
320
+ fi
321
+
322
+ # GPT hardening: a corrupt recorded assembledMarketplacePath must fail the wrapper
323
+ # uninstall LOUD and BEFORE any side effect — no Claude removal, state file intact
324
+ # (honest inverse never guesses / never partially uninstalls / never WARN-then-DONE).
325
+ # Two corrupt shapes: a trivially-bad "/" AND the basename-corrupt case that a
326
+ # PARENT-ONLY guard would silently pass (…/entwurf/meta-bridge/not-assembled) — the
327
+ # rm targets the parent meta-bridge dir, so that shape must be refused by the FULL
328
+ # suffix guard or it nukes the real artifact + Claude registrations (2026-07-03).
329
+ for CORRUPT in "/" "$XDG_DATA_HOME/entwurf/meta-bridge/not-assembled"; do
330
+ python3 "$STATE" prepare --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
331
+ python3 "$STATE" apply --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
332
+ # seed the real artifact so an over-broad rm would be observable as its removal
333
+ mkdir -p "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled"; : > "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled/.sentinel"
334
+ CORRUPT="$CORRUPT" python3 - <<'PY'
335
+ import json, os
336
+ p = os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
337
+ s = json.load(open(p)); s['assembledMarketplacePath'] = os.environ['CORRUPT']
338
+ json.dump(s, open(p, 'w'), indent=2)
339
+ PY
340
+ FAKE_CLAUDE_LOG="$TMP/fake-claude-corrupt.log"; : > "$FAKE_CLAUDE_LOG"
341
+ if PATH="$FAKE_BIN:$PATH" FAKE_CLAUDE_LOG="$FAKE_CLAUDE_LOG" bash "$REPO/scripts/meta-bridge-uninstall.sh" >/dev/null 2>&1; then
342
+ bad "wrapper uninstall with corrupt recorded path '$CORRUPT' should fail loud"
343
+ elif [ -s "$FAKE_CLAUDE_LOG" ] || [ ! -f "$STATE_FILE" ] || [ ! -e "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled/.sentinel" ]; then
344
+ bad "corrupt-path uninstall '$CORRUPT' leaked side effects/removed artifact: claude_log=[$(cat "$FAKE_CLAUDE_LOG")] state_exists=$([ -f "$STATE_FILE" ] && echo yes || echo no) artifact=$([ -e "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled/.sentinel" ] && echo intact || echo REMOVED)"
345
+ else
346
+ ok "corrupt recorded path '$CORRUPT' → uninstall fails BEFORE side effects (no Claude removal, state + artifact intact)"
347
+ fi
348
+ rm -f "$STATE_FILE"; rm -rf "$XDG_DATA_HOME/entwurf/meta-bridge"
349
+ done
350
+
351
+ # GPT hardening: state.py check must compare the RECORDED marketplace path, not the
352
+ # --asm handed in. Install/apply with A, then check with a DIFFERENT --asm B: it must
353
+ # still PASS (Claude settings + state both hold A; only the caller's XDG differs).
354
+ CKA="$TMP/ck-xdgA/entwurf/meta-bridge/.assembled"
355
+ CKB="$TMP/ck-xdgB/entwurf/meta-bridge/.assembled"
356
+ python3 "$STATE" prepare --repo "$REPO" --asm "$CKA" >/dev/null
357
+ python3 "$STATE" apply --repo "$REPO" --asm "$CKA" >/dev/null
358
+ if python3 "$STATE" check --repo "$REPO" --asm "$CKB" >/dev/null 2>&1; then ok "state.py check passes when --asm differs from the recorded path (compares recorded, no XDG false-fail)"; else bad "state.py check false-FAILed on a recorded≠--asm mismatch"; fi
359
+ rm -f "$STATE_FILE"
360
+
361
+ # GPT hardening: check must shape-validate the recorded path, not just compare it to
362
+ # settings. Corrupt BOTH state + settings to the same basename-bad value: a pure
363
+ # consistency compare would PASS (both agree) and greenlight a bogus marketplace
364
+ # source. The suffix guard must FAIL it instead.
365
+ python3 "$STATE" prepare --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
366
+ python3 "$STATE" apply --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
367
+ python3 - <<'PY'
368
+ import json, os
369
+ xdg = os.environ['XDG_DATA_HOME']; corrupt = xdg + '/entwurf/meta-bridge/not-assembled'
370
+ sp = os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
371
+ s = json.load(open(sp)); s['assembledMarketplacePath'] = corrupt; json.dump(s, open(sp, 'w'), indent=2)
372
+ cp = os.environ['CLAUDE_CONFIG_DIR'] + '/settings.json'
373
+ c = json.load(open(cp)); c['extraKnownMarketplaces']['meta-bridge-local'] = {'source': {'source': 'directory', 'path': corrupt}}; json.dump(c, open(cp, 'w'), indent=2)
374
+ PY
375
+ if python3 "$STATE" check --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null 2>&1; then bad "state.py check greenlit a both-corrupt (state+settings) malformed marketplace path"; else ok "state.py check fails a malformed recorded path even when settings agree (shape guard, not just consistency)"; fi
376
+ rm -f "$STATE_FILE"
377
+
378
+ # GPT hardening: a MISSING/empty recorded path is corruption too — every state our
379
+ # code writes carries the field, so check must NOT fall back to --asm and PASS.
380
+ # Delete the field with settings still valid; the shape guard must FAIL, not greenlight.
381
+ python3 "$STATE" prepare --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
382
+ python3 "$STATE" apply --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null
383
+ python3 - <<'PY'
384
+ import json, os
385
+ sp = os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
386
+ s = json.load(open(sp)); s.pop('assembledMarketplacePath', None); json.dump(s, open(sp, 'w'), indent=2)
387
+ PY
388
+ if python3 "$STATE" check --repo "$REPO" --asm "$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" >/dev/null 2>&1; then bad "state.py check PASSed with a MISSING recorded assembledMarketplacePath (fell back to --asm)"; else ok "state.py check fails a missing/empty recorded path (no --asm fallback greenlight)"; fi
389
+ rm -f "$STATE_FILE"
390
+
247
391
  # Doctor hook-log recovery predicate: only a later `INFO armed watch` clears an
248
392
  # ERROR. A UserPromptSubmit `INFO attach record` is degraded backfill, not wake
249
393
  # recovery.
@@ -365,7 +509,7 @@ if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null 2>&1; the
365
509
  # chain to its final summary line — i.e. no early set -e death anywhere.
366
510
  DOC_HOME="$TMP/doctor-home"; DOC_CFG="$DOC_HOME/.claude"
367
511
  DOC_AGENT="$TMP/doctor-agent"; DOC_STORE="$DOC_AGENT/meta-sessions"
368
- DOC_BIN="$TMP/doctor-bin"; DOC_ASM="$REPO/pi/meta-bridge/.assembled" # SAME asm the doctor's check uses, so only the MCP drifts
512
+ DOC_BIN="$TMP/doctor-bin"; DOC_ASM="$XDG_DATA_HOME/entwurf/meta-bridge/.assembled" # SAME asm the doctor computes (XDG), so only the MCP drifts
369
513
  mkdir -p "$DOC_CFG" "$DOC_STORE" "$DOC_BIN"
370
514
  echo '{}' > "$DOC_CFG/settings.json"
371
515
  echo '{}' > "$DOC_HOME/.claude.json"
@@ -375,6 +519,14 @@ valid_record "20260606T120000-eeeeee" "doctor-native" > "$DOC_STORE/20260606T120
375
519
  # Fake claude: a healthy toolchain so the doctor sails past every other section.
376
520
  cat > "$DOC_BIN/claude" <<'SH'
377
521
  #!/usr/bin/env bash
522
+ if [ "${1:-} ${2:-} ${3:-}" = "plugin list --json" ]; then
523
+ if [ "${FAKE_PLUGIN_CACHE_MISS:-0}" = 1 ]; then
524
+ printf '%s\n' '[{"id":"entwurf-meta-receive@meta-bridge-local","enabled":true,"errors":["Marketplace meta-bridge-local failed to load: cache-miss"]}]'
525
+ else
526
+ printf '%s\n' '[{"id":"entwurf-meta-receive@meta-bridge-local","enabled":true}]'
527
+ fi
528
+ exit 0
529
+ fi
378
530
  case "$1${2:+ $2}" in
379
531
  "--version") echo "2.1.167 (Claude Code)" ;;
380
532
  "plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
@@ -405,5 +557,23 @@ if printf '%s\n' "$DOC_OUT" | grep -q 'user MCP entwurf-bridge'; then ok "doctor
405
557
  if printf '%s\n' "$DOC_OUT" | grep -q '\[plugin install'; then ok "doctor continues to a later section after the drift (no early set -e death)"; else bad "doctor did not reach a later section header after the drift:"$'\n'"$DOC_OUT"; fi
406
558
  if printf '%s\n' "$DOC_OUT" | grep -q 'meta-bridge doctor: FAIL'; then ok "doctor runs the whole chain to its final summary line"; else bad "doctor did not reach its final summary line (mid-run death):"$'\n'"$DOC_OUT"; fi
407
559
 
560
+ # A plugin can be configured enabled while Claude refuses to load its missing marketplace
561
+ # source. Text-only `grep enabled` called that healthy and let `🪛 ? cc` look mysterious.
562
+ set +e
563
+ DOC_CACHE_OUT="$(env HOME="$DOC_HOME" CLAUDE_CONFIG_DIR="$DOC_CFG" PI_CODING_AGENT_DIR="$DOC_AGENT" ENTWURF_META_SESSIONS_DIR="$DOC_STORE" FAKE_PLUGIN_CACHE_MISS=1 PATH="$DOC_BIN:$PATH" bash "$REPO/scripts/meta-bridge-doctor.sh" 2>&1)"
564
+ DOC_CACHE_CODE=$?
565
+ set -e
566
+ if [ "$DOC_CACHE_CODE" -eq 1 ] && printf '%s\n' "$DOC_CACHE_OUT" | grep -q 'enabled=true but FAILED TO LOAD:.*cache-miss'; then
567
+ ok "doctor distinguishes configured-enabled from loadable (cache-miss is a hard failure)"
568
+ else
569
+ bad "doctor did not surface enabled-but-cache-miss as a hard load failure:"$'\n'"$DOC_CACHE_OUT"
570
+ fi
571
+
572
+ # ⓪ 경계 종료 단언: 이 smoke 전체(install 조립 + state + wrapper-uninstall + doctor)가
573
+ # 끝난 뒤에도 checkout 안에는 live marketplace source가 생기지 않았다 — source
574
+ # origin(repo)과 live artifact(XDG)가 구조적으로 분리됐다는 최종 증명. 미래 회귀가
575
+ # repo 내부 ASM을 되살리거나 덮어쓰면(내용 해시 변화 포함) 여기서 잡힌다.
576
+ if [ "$(asm_fingerprint)" = "$REPO_ASM_FP_START" ]; then ok "checkout-internal marketplace source is byte-identical before/after — no op created or mutated \$REPO/.assembled (source origin ≠ live artifact)"; else bad "a meta-bridge operation created/mutated \$REPO/pi/meta-bridge/.assembled — repo boundary breached"; fi
577
+
408
578
  echo
409
579
  if [ "$fail" -eq 0 ]; then echo "smoke-meta-install-state: PASS"; else echo "smoke-meta-install-state: FAIL (see above)"; exit 1; fi
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-pi-provider-state — regression gate for the pi provider install adapter (#46 Task 2):
3
+ # register-pi-provider.py (ownership-classified install/remove, user+project scopes) + the
4
+ # read-only doctor-pi-provider.ts (effective shadow view). Runs in an ISOLATED HOME + XDG with a
5
+ # fake stable bin and isolated settings files (no real ~/.pi). Asserts:
6
+ # - user install ownership matrix: absent→create+state; managed-legacy(repo start.sh)→bare bin +
7
+ # state(preimage audit, NOT restored) + siblings preserved; managed-current→idempotent;
8
+ # user-override→NOT overwritten, NO state (unowned).
9
+ # - user remove: state-based honest inverse (managed-* → remove OUR key, a legacy repo path is
10
+ # NOT restored; siblings kept; parent tidied); user-override → no state → nothing to undo.
11
+ # - legacy bundle prune (session-bridge/pi-tools-bridge repo path) alongside, user my-own kept.
12
+ # - project scope: NO state; install normalizes the bare bin, remove strips our-managed shapes
13
+ # (bare bin AND legacy repo path), a user override left in place.
14
+ # - doctor: effective = project shadows user; user-only/both-bare → ok green; project-STALE
15
+ # (user bare, project legacy) → effective legacy note (the "doctor green runtime red" guard);
16
+ # state-owned DRIFT → FAIL; bare-but-dangling → FAIL; malformed settings → FAIL; symlink refuse.
17
+ # - checkout stays byte-identical (nothing under $REPO).
18
+ # Offline + deterministic (deps: bash + python3 + node --experimental-strip-types).
19
+ set -euo pipefail
20
+
21
+ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
22
+ REG="$REPO_DIR/scripts/register-pi-provider.py"
23
+ DOCTOR="$REPO_DIR/scripts/doctor-pi-provider.ts"
24
+
25
+ pass=0
26
+ ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
27
+ die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
28
+ want() { eval "$2" && ok "$1" || die "$1"; }
29
+
30
+ REPO_BEFORE="$(cd "$REPO_DIR" && git status --porcelain)"
31
+
32
+ SB="$(mktemp -d)"
33
+ trap 'rm -rf "$SB"' EXIT
34
+ export HOME="$SB/home"
35
+ export XDG_DATA_HOME="$SB/xdg"
36
+ GLOBAL="$SB/global.json"
37
+ PROJECT="$SB/proj/.pi/settings.json"
38
+ STATE="$XDG_DATA_HOME/entwurf/pi-provider/install-state.json"
39
+ mkdir -p "$SB/proj/.pi" "$SB/bin" "$SB/home"
40
+
41
+ # A fake repo anchor for the managed-legacy predicate (endswith /entwurf/mcp/entwurf-bridge/start.sh).
42
+ FAKE_REPO="$SB/checkout/entwurf"
43
+ LEGACY_CMD="$FAKE_REPO/mcp/entwurf-bridge/start.sh"
44
+
45
+ # fake stable bin on PATH so the bare command RESOLVES in the doctor. Keep THIS fake
46
+ # authoritative: drop the dir that holds a REAL entwurf-bridge (this dev host exposes one at
47
+ # ~/.local/bin) so the dangling test is not masked by it.
48
+ printf '#!/usr/bin/env bash\necho fake-bridge\n' > "$SB/bin/entwurf-bridge"
49
+ chmod +x "$SB/bin/entwurf-bridge"
50
+ _real_bridge="$(command -v entwurf-bridge 2>/dev/null || true)"
51
+ _real_dir="${_real_bridge%/*}"
52
+ if [ -n "$_real_dir" ]; then
53
+ PATH="$(printf '%s' "$PATH" | tr ':' '\n' | grep -vFx "$_real_dir" | paste -sd: -)"
54
+ fi
55
+ export PATH="$SB/bin:$PATH"
56
+
57
+ reg() { python3 "$REG" "$@"; }
58
+ doctor() { PI_PROVIDER_GLOBAL_SETTINGS="$GLOBAL" PI_PROVIDER_PROJECT_SETTINGS="$PROJECT" PI_PROVIDER_STATE="$STATE" \
59
+ node --experimental-strip-types "$DOCTOR"; }
60
+ cmd_of() { python3 -c 'import json,sys
61
+ try:
62
+ d=json.load(open(sys.argv[1])); print(d["entwurfProvider"]["mcpServers"]["entwurf-bridge"]["command"])
63
+ except Exception: print("<none>")' "$1"; }
64
+
65
+ # ── A: user install — absent → create + state(ownership=absent) ───────────────
66
+ printf '{"defaultProvider":"x"}\n' > "$GLOBAL"
67
+ reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
68
+ want "user/absent: command is bare stable bin" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
69
+ want "user/absent: state ownership=absent" \
70
+ "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"ownership\"])' '$STATE' | grep -qx absent"
71
+
72
+ # ── B: user install — managed-legacy(repo start.sh) → bare bin + preimage + siblings kept ──
73
+ rm -f "$STATE"
74
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"appendSystemPrompt":False,"skillPlugins":["/p"],"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}},"packages":["y"]}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD"
75
+ OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
76
+ want "user/managed-legacy: reported as managed-legacy" "printf '%s' \"\$OUT\" | grep -q 'managed-legacy'"
77
+ want "user/managed-legacy: normalized to bare bin" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
78
+ want "user/managed-legacy: siblings preserved (skillPlugins/appendSystemPrompt)" \
79
+ "python3 -c 'import json,sys; ep=json.load(open(sys.argv[1]))[\"entwurfProvider\"]; sys.exit(0 if ep.get(\"skillPlugins\")==[\"/p\"] and ep.get(\"appendSystemPrompt\") is False else 1)' '$GLOBAL'"
80
+ want "user/managed-legacy: state ownership=managed-legacy" \
81
+ "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"ownership\"])' '$STATE' | grep -qx managed-legacy"
82
+ want "user/managed-legacy: preimage records the repo path (audit, not restored)" \
83
+ "python3 -c 'import json,sys; p=json.load(open(sys.argv[1]))[\"preimage\"]; sys.exit(0 if p and p.get(\"command\")==sys.argv[2] else 1)' '$STATE' '$LEGACY_CMD'"
84
+ want "user/managed-legacy: timing honesty label emitted" \
85
+ "printf '%s' \"\$OUT\" | grep -q 'existing pi sessions unaffected until restart'"
86
+
87
+ # ── C: user install — managed-current(bare) → idempotent ──────────────────────
88
+ OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
89
+ want "user/managed-current: idempotent (stays bare)" "[ \"\$(cmd_of '$GLOBAL')\" = entwurf-bridge ]"
90
+ want "user/managed-current: reported managed-current" "printf '%s' \"\$OUT\" | grep -q 'managed-current'"
91
+
92
+ # ── D: user install — user-override → NOT overwritten, NO state ───────────────
93
+ rm -f "$STATE"
94
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"/my/own/thing"}}}}\n' > "$GLOBAL"
95
+ OUT="$(reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE")"
96
+ want "user/override: command left untouched" "[ \"\$(cmd_of '$GLOBAL')\" = /my/own/thing ]"
97
+ want "user/override: NO state written (unowned)" "[ ! -f '$STATE' ]"
98
+ want "user/override: reported preserved user override" "printf '%s' \"\$OUT\" | grep -qi 'user override'"
99
+
100
+ # ── E: user remove — state-based honest inverse (key removed, siblings kept) ──
101
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"skillPlugins":["/p"],"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD"
102
+ reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
103
+ reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
104
+ want "user/remove: our key removed (NOT restored to repo path)" "[ \"\$(cmd_of '$GLOBAL')\" = '<none>' ]"
105
+ want "user/remove: sibling skillPlugins preserved" \
106
+ "python3 -c 'import json,sys; sys.exit(0 if json.load(open(sys.argv[1]))[\"entwurfProvider\"].get(\"skillPlugins\")==[\"/p\"] else 1)' '$GLOBAL'"
107
+ want "user/remove: state removed" "[ ! -f '$STATE' ]"
108
+ reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null 2>&1
109
+ ok "user/remove: no-state is idempotent (nothing to undo)"
110
+
111
+ # ── F: legacy bundle prune alongside, user-authored server kept ───────────────
112
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]},"session-bridge":{"command":sys.argv[3]},"my-own":{"command":"/custom"}}}}, open(sys.argv[1],"w"))' "$GLOBAL" "$LEGACY_CMD" "$FAKE_REPO/mcp/session-bridge/start.sh"
113
+ reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null
114
+ want "legacy-prune: session-bridge removed" \
115
+ "python3 -c 'import json,sys; sys.exit(0 if \"session-bridge\" not in json.load(open(sys.argv[1]))[\"entwurfProvider\"][\"mcpServers\"] else 1)' '$GLOBAL'"
116
+ want "legacy-prune: user-authored my-own kept" \
117
+ "python3 -c 'import json,sys; sys.exit(0 if \"my-own\" in json.load(open(sys.argv[1]))[\"entwurfProvider\"][\"mcpServers\"] else 1)' '$GLOBAL'"
118
+ reg remove "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null; rm -f "$STATE"
119
+
120
+ # ── G: project scope — NO state; install normalizes, remove strips managed shapes ──
121
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$LEGACY_CMD"
122
+ reg install "$PROJECT" "$FAKE_REPO" --scope project >/dev/null
123
+ want "project/install: normalized to bare bin" "[ \"\$(cmd_of '$PROJECT')\" = entwurf-bridge ]"
124
+ want "project/install: NO user-scope state created" "[ ! -f '$STATE' ]"
125
+ reg remove "$PROJECT" "$FAKE_REPO" --scope project >/dev/null
126
+ want "project/remove: strips the bare bin (our-managed)" "[ \"\$(cmd_of '$PROJECT')\" = '<none>' ]"
127
+
128
+ # ── H: doctor — effective shadow view + verdicts ──────────────────────────────
129
+ # H-1: user-only bare → effective user, ok green
130
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
131
+ printf '{}\n' > "$PROJECT"; rm -f "$STATE"
132
+ OUT="$(doctor)"; RC=$?
133
+ want "doctor(user-only bare): exits 0" "[ '$RC' -eq 0 ]"
134
+ want "doctor(user-only bare): ok — bare bin resolves" "printf '%s' \"\$OUT\" | grep -q 'bare stable bin'"
135
+
136
+ # H-2: project STALE (user bare, project legacy) → effective=project=legacy note (green-runtime-red guard)
137
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
138
+ python3 -c 'import json,sys; json.dump({"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":sys.argv[2]}}}}, open(sys.argv[1],"w"))' "$PROJECT" "$LEGACY_CMD"
139
+ OUT="$(doctor)"; RC=$?
140
+ want "doctor(project-stale): exits 0 (honest note, not a crash)" "[ '$RC' -eq 0 ]"
141
+ want "doctor(project-stale): EFFECTIVE is project (shadows user)" "printf '%s' \"\$OUT\" | grep -q 'EFFECTIVE (project)'"
142
+ want "doctor(project-stale): flags legacy managed path (not adopted)" "printf '%s' \"\$OUT\" | grep -q 'LEGACY managed repo path'"
143
+
144
+ # H-3: state-owned DRIFT → FAIL (state says managed but effective is foreign)
145
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"/foreign/thing"}}}}\n' > "$GLOBAL"
146
+ printf '{}\n' > "$PROJECT"
147
+ python3 -c 'import json,sys; json.dump({"schemaVersion":1,"managedSettingsPath":sys.argv[1],"scope":"user","ownership":"managed-legacy","command":"entwurf-bridge","preimage":None}, open(sys.argv[2],"w"))' "$GLOBAL" "$STATE"
148
+ if doctor >/dev/null 2>&1; then die "doctor(drift): should FAIL (state owns but effective drifted)"; fi
149
+ ok "doctor(drift): FAILS on state-owned + effective-drifted"
150
+ rm -f "$STATE"
151
+
152
+ # H-4: bare-but-dangling (bin not on PATH) → FAIL
153
+ printf '{"entwurfProvider":{"mcpServers":{"entwurf-bridge":{"command":"entwurf-bridge"}}}}\n' > "$GLOBAL"
154
+ printf '{}\n' > "$PROJECT"
155
+ mv "$SB/bin/entwurf-bridge" "$SB/bin/entwurf-bridge.hidden"
156
+ if doctor >/dev/null 2>&1; then die "doctor(dangling): should FAIL (bare bin not resolvable)"; fi
157
+ ok "doctor(dangling): FAILS when the bare bin does not resolve"
158
+ mv "$SB/bin/entwurf-bridge.hidden" "$SB/bin/entwurf-bridge"
159
+
160
+ # H-5: malformed settings → FAIL (SSOT read throws)
161
+ printf 'not json{{{' > "$GLOBAL"; printf '{}\n' > "$PROJECT"
162
+ if doctor >/dev/null 2>&1; then die "doctor(malformed): should FAIL"; fi
163
+ ok "doctor(malformed): FAILS on malformed settings (SSOT read throws)"
164
+
165
+ # H-6: none configured → honest '?' note, exits 0
166
+ printf '{}\n' > "$GLOBAL"; printf '{}\n' > "$PROJECT"
167
+ OUT="$(doctor)"; RC=$?
168
+ want "doctor(none): exits 0" "[ '$RC' -eq 0 ]"
169
+ want "doctor(none): honest never-installed '?' note" "printf '%s' \"\$OUT\" | grep -q 'never installed'"
170
+
171
+ # ── I: symlink target → install REFUSES ───────────────────────────────────────
172
+ printf '{}\n' > "$SB/real.json"; rm -f "$GLOBAL"; ln -s "$SB/real.json" "$GLOBAL"
173
+ if reg install "$GLOBAL" "$FAKE_REPO" --scope user --state "$STATE" >/dev/null 2>&1; then die "symlink: install should REFUSE"; fi
174
+ ok "symlink: install refused (someone else's SSOT)"
175
+ rm -f "$GLOBAL"
176
+
177
+ # ── checkout purity ───────────────────────────────────────────────────────────
178
+ REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
179
+ want "purity: checkout unchanged (all writes stayed in the sandbox HOME+XDG)" \
180
+ "[ \"\$REPO_BEFORE\" = \"\$REPO_AFTER\" ]"
181
+
182
+ printf '\nsmoke-pi-provider-state: %d checks passed\n' "$pass"