Compare commits

..

4 Commits

Author SHA1 Message Date
Leo fbc56b9e59 isep-doctor 判準改成「每一個 Claude Code 自己回報有沒有載入」,不再看設定長相(inkstone/ISEP#150 → c6995)
同一台 Mac 有 CLI 與 Desktop 內建兩個 Claude Code,讀同一份 ~/.claude 卻不一定同結論:
settings 多了 path 時 Desktop 2.1.266 忽略 inkstone marketplace、CLI 2.1.220 照常載入(c6991)。
第一版看到 path 就對整台判 ,在 CLI 上是假陽性。

- 逐一找 CLI(PATH)與 Desktop(最新版本,數字排序),各跑 plugin list --json,依 errors 各判各的
- 設定比對只在某一個真的報錯時當解釋+修法,永遠不單獨判 
- 查不了 ⇒ ◐、離開碼 2,不講成 
- test-isep-doctor.sh 離線 11 條(假 claude 可執行檔);舊版跑這份 pass=1 fail=10
- test-isep-doctor-live.sh 實機 3 條:真的 CLI 2.1.220/Desktop 2.1.266,隔離 HOME 重現有/無 path,不動 ~/.claude
- 盤點表與 plugin.json 描述的腳本數 62→65(這棵樹上實數)

版本號待總管定。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:23:40 +08:00
Leo 8eb4c58d2d isep-doctor:講出這台的 ISEP plugin 有沒有真的載入(inkstone/ISEP#150)
根因不是「Desktop 不跑 hook」:Desktop 1.52386.0/CC 2.1.266 內實測專案
PreToolUse hook 照擋。真正斷的是 ~/.claude/settings.json
extraKnownMarketplaces.inkstone.source 多了 path,CC 把它當 git 抓取欄位,
marketplace 被忽略 ⇒ isep@inkstone「Marketplace inkstone not found」,
59 支閘在 CLI 與 Desktop 一起失效,而且沒有任何閘講得出來。

本支從 plugin 外面查(claude plugin list --json + settings 對 known_marketplaces),
點名多出的欄位並印出一行修法。迴歸測試 scripts/test-isep-doctor.sh 5/5。

假設:修 ~/.claude/settings.json 是 leo 的個人設定,由 leo 親手執行,本 commit 不動它。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:55:26 +08:00
Leo c1df3c7a08 定版 v0.30.1(主線成員讀不到就講出來,不再讓雲端工人以為抓盡)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:07:25 +08:00
Leo 544beefc5e Merge '主線成員讀不到任何一條 /dependencies 時不准印出完整名單'(inkstone/ISEP#130)
總管複驗:members-by-dependency 70/71(唯一紅項 ㊽ 與 main 上相同)、gitea-fallback 31/31、repo-mirror 17/17、ticket-pick 58/58

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:07:02 +08:00
6 changed files with 350 additions and 2 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "isep",
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(87 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、62 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
"version": "0.30.0",
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(87 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、65 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
"version": "0.30.1",
"keywords": [
"inkstone",
"guardrails",
+24
View File
@@ -1210,6 +1210,30 @@ claude -p '請執行 git tag -a v9.9.9 -m test'
> 為什麼挑 `release-tag-guard` 當考題:它**只存在於 ISEP**,舊的 `.claude/` 那份沒有。
> 用它才分得出「載到的是 plugin」還是「載到的是舊的」。
### A8b — 這台**每一個** Claude Code 都真的載入了 ISEP:離線 11 條+實機 3 條
```
python3 scripts/isep-doctor # 看這台現況
bash scripts/test-isep-doctor.sh # 離線:假的 claude 可執行檔
bash scripts/test-isep-doctor-live.sh # 實機:真的 CLI 與 Desktop 內建那一份,隔離 HOME,不動 ~/.claude
```
**該看到**`isep-doctor` 逐一列出 CLI 與 Desktop,各一行 ✅;離線 `pass=11 fail=0`;實機 `pass=3 fail=0 skip=0`
**它在守什麼**`inkstone/ISEP#150`):plugin 沒載入時,**會講話的信標本身也沒載入**,
所以只能從 plugin 外面問。而同一台 Mac 有**不只一個** Claude Code,它們讀同一份 `~/.claude` 卻不一定同一個結論:
`extraKnownMarketplaces.inkstone.source` 多了 `path` 時,**Desktop 內建的 2.1.266 整個忽略 marketplace、CLI 2.1.220 照常載入**
c6991 兩份 debug log;本支實機測試在隔離 HOME 重現)。第一版 doctor 看設定長相就對整台判 ❌——在 CLI 上是假陽性。
⇒ 判準是**每一個 Claude Code 自己 `plugin list --json` 回報的 errors**;設定比對只當解釋,永遠不單獨判 ❌。
**失敗**
- ① 紅 ⇒ **誤攔**,又回到看設定長相判 ❌。鑑別力實跑過:拿 8eb4c58 那版跑這份測試是 `pass=1 fail=10`
大多是輸出格式不同;**真正分得出判準對錯的是 ①**——舊版在「CLI 實際載入」時照樣 exit 1 並點名 path
- ② 紅 ⇒ 分不出哪一個 surface 壞了,或又把整台機器講成同一個結論
- ④ 紅 ⇒ 別的原因失敗時編了一個 path 原因出來
- ⑥⑦ 紅 ⇒ 「查不了」被講成 ✅ 或 ❌(讀不到不等於沒載入)
- ⑧⑧b 紅 ⇒ 印出來的修法照著貼走不通
- ⑨ 紅 ⇒ Desktop 版本用字串排序,`2.1.99` 會被當成比 `2.1.266`
- 實機測試印 SKIP ⇒ 這台的版本不是 2.1.220/2.1.266,行為可能不同,那一格**沒驗過**
---
## B. 只有 leo 能跑的(雲端)
+13
View File
@@ -43,6 +43,19 @@
> **62 支、87 條、62 支腳本是在這棵樹上當場數出來的**`ls hooks/*.sh | wc -l`62、
> `grep -c '"command":' hooks/hooks.json`87、`ls -p scripts | grep -v / | wc -l`62)。
> 📌 **`0.30.1``inkstone/ISEP#130`2026-09-13)+0 支、+0 條**:修補 0.30.0 的抓票入口——
> `mainline setrefreshadopt` 讀不到任何一條 `/dependencies` 時,名單標成不完整、`set` 離開碼 3 不印 ✅、
> 保留舊快取;否則雲端工人會拿殘缺名單 `pick` 到「抓盡」而收工(09-13 實撞:hub 票底下三張整批漏收)。
> **62 支、87 條、62 支腳本是在這棵樹上當場數出來的**`ls hooks/*.sh | wc -l`62、
> `grep -c '"command":' hooks/hooks.json`87、`ls -p scripts | grep -v / | wc -l`62)。
> 📌 **`inkstone/ISEP#150`2026-09-13)+0 支、+0 條、腳本 62→65**:沒加閘。`scripts/isep-doctor` 從 plugin **外面**
> 問這台每一個 Claude CodeCLI、Desktop 內建那一份)有沒有真的載入 ISEP——plugin 沒載入時信標自己也講不出話。
> 判準是各自 `plugin list --json` 回報的 errors,不是設定長相(`path` 在 Desktop 2.1.266 會讓 marketplace 被忽略、CLI 2.1.220 不會)。
> 另加兩支測試:`test-isep-doctor.sh`(離線)、`test-isep-doctor-live.sh`(真的 Claude Code、隔離 HOME)。
> **62 支、87 條、65 支腳本是在這棵樹上當場數出來的**`ls hooks/*.sh | wc -l`62、
> `grep -c '"command":' hooks/hooks.json`87、`ls -p scripts | grep -v / | wc -l`65)。
> 📌 **`inkstone/ISEP#147`2026-09-07)+0 支、+1 條**`line-needs-own-worktree.sh` 多掛 PostToolUse `Agent|Task`
> ——教你開分身的那支閘,在線收工那一刻替你收(分支推了就 remove;沒推就點名分支與路徑,不靜默、不 force)。
> 分身的家從「repo 旁邊」改成「repo 裡面的 `.worktrees/`」:leo 在 Finder 看到一排 `ISEP-wt-117``InkStoneCo-wt-112`
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""isep-doctor — 這台機器上每一個 Claude Code,到底有沒有把 ISEP 載進來(inkstone/ISEP#150
為什麼要有這支:
plugin 載入失敗時,ISEP 的閘**一支都不會跑**,而且**沒有任何閘能講出來**——
講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。
為什麼一台機器要分開問(09-13 總管複驗 c6991 推翻了第一版):
同一台 Mac 上有**不只一個** Claude CodeCLI(例:/opt/homebrew/bin/claude 2.1.220
與 Claude Desktop 內建的那一份(~/Library/Application Support/Claude/claude-code/<版本>/…,例:2.1.266)。
它們讀同一份 ~/.claude,**卻不一定得出同一個結論**:
settings.json 的 extraKnownMarketplaces.inkstone.source 多了 path 時——
2.1.266:整個 marketplace 被忽略 ⇒ isep@inkstone 報 "Marketplace inkstone not found"
2.1.220:照常載入
第一版只看「設定長得像會出事」就對整台機器判 ❌,在 2.1.220 上是假陽性。
判準(實際狀態,不是設定長相):
對每一個找得到的 Claude Code 各跑一次 `plugin list --json`,看它**自己**回報的 isep@inkstone
沒有這一筆 ⇒ ❌ 沒安裝
帶 errors ⇒ ❌ 沒載入(照實轉述;若 settings 比已安裝的 marketplace 多了抓取欄位,附上原因與修法)
沒有 errors ⇒ ✅(即使設定裡有 path——那台實際載得進來就是載得進來)
跑不動/看不懂 ⇒ ◐ 查不了(讀不到不等於沒載入,也不等於有)
設定比對**只當解釋**,永遠不單獨判 ❌。
離開碼:0 全部 ✅|1 至少一個 ❌|2 沒有 ❌、但至少一個查不了(或一個 Claude Code 都找不到)
測試用環境變數:
ISEP_DOCTOR_CLAUDE_BINS 用這份清單取代自動尋找;以 os.pathsep 分隔,每項 `名稱=可執行檔路徑`
ISEP_DOCTOR_CLAUDE_HOME 代替 ~/.claude
迴歸測試:scripts/test-isep-doctor.sh(離線)/scripts/test-isep-doctor-live.sh(真的 Claude Code、隔離 HOME
"""
import glob
import json
import os
import re
import shutil
import subprocess
import sys
PLUGIN_ID = "isep@inkstone"
MARKET = "inkstone"
FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths")
DESKTOP_GLOB = "~/Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude"
home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude")
def _ver_key(path):
m = re.search(r"/claude-code/([0-9][0-9.]*)/", path)
return tuple(int(x) for x in m.group(1).split(".") if x) if m else ()
def find_claudes():
"""回傳 [(名稱, 路徑)]。Desktop 只取最新版本那一份(它啟動的就是那一份;舊資料夾可能殘留)。"""
env = os.environ.get("ISEP_DOCTOR_CLAUDE_BINS")
if env is not None:
out = []
for item in env.split(os.pathsep):
if "=" in item:
name, path = item.split("=", 1)
out.append((name, path))
return out
out = []
cli = shutil.which("claude")
if cli:
out.append(("CLI", cli))
desk = sorted(glob.glob(os.path.expanduser(DESKTOP_GLOB)), key=_ver_key) # 數字排序:2.1.99 < 2.1.266
if desk:
out.append(("Desktop", desk[-1]))
return out
def version_of(exe):
try:
r = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=30)
return (r.stdout.strip().split() or ["?"])[0]
except Exception: # noqa: BLE001
return "?"
def ask(exe):
"""回傳 (狀態, 內容)。狀態:'list' 內容=plugin list 陣列;'err' 內容=查不了的原因。"""
try:
r = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=90)
except Exception as e: # noqa: BLE001
return "err", f"執行失敗({e}"
try:
data = json.loads(r.stdout)
if not isinstance(data, list):
raise ValueError("不是陣列")
return "list", data
except Exception: # noqa: BLE001
tail = (r.stdout or r.stderr or "").strip().splitlines()[-1:] or ["(沒有輸出)"]
return "err", f"plugin list --json 看不懂(exit={r.returncode}{tail[0][:200]}"
def load(p):
try:
return json.load(open(p))
except Exception: # noqa: BLE001
return {}
declared = (load(os.path.join(home, "settings.json")).get("extraKnownMarketplaces") or {}).get(MARKET, {}).get("source")
known = (load(os.path.join(home, "plugins", "known_marketplaces.json")).get(MARKET) or {}).get("source")
extra = []
if isinstance(declared, dict):
extra = [k for k in FETCH_FIELDS if k in declared and (not isinstance(known, dict) or declared.get(k) != known.get(k))]
claudes = find_claudes()
if not claudes:
print("◐ 查不了:這台找不到任何 Claude Code 可執行檔(CLI 與 Desktop 都沒有)")
sys.exit(2)
bad, unknown, extra_hit = [], [], False
for name, exe in claudes:
label = f"{name} {version_of(exe)}"
state, data = ask(exe)
if state == "err":
unknown.append(name)
print(f"◐ {label}:查不了——{data}(讀不到不等於沒載入)")
print(f" {exe}")
continue
entry = next((p for p in data if p.get("id") == PLUGIN_ID), None)
if entry is None:
bad.append(name)
print(f"❌ {label}{PLUGIN_ID} 沒有安裝——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
print(" 修法:claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone")
continue
errors = entry.get("errors") or []
if not errors:
print(f"✅ {label}{PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')}")
continue
bad.append(name)
print(f"❌ {label}{PLUGIN_ID} {entry.get('version')} 沒有載入——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
for e in errors:
print(f" Claude Code 回報:{e}")
if extra:
extra_hit = True
print(f" 可能原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)}"
"這一版把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略")
if extra_hit:
fields = ", ".join(repr(k) for k in extra)
print("修法(一行,只拿掉上面點名的欄位;改完完全關掉 Claude Desktop/重開 CLI session 再跑一次本指令):")
print(f" python3 -c \"import json,os;p=os.path.expanduser('~/.claude/settings.json');d=json.load(open(p));"
f"s=d['extraKnownMarketplaces']['{MARKET}']['source'];[s.pop(k,None) for k in ({fields},)];"
"json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)\"")
if bad:
sys.exit(1)
sys.exit(2 if unknown else 0)
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# 實測:scripts/isep-doctor 對**真的** Claude Code(這台的 CLI 與 Desktop 內建那一份)判得對不對(inkstone/ISEP#150
#
# 不動 ~/.claude:把 inkstone marketplace 與 isep 安裝紀錄複製到 mktemp 的隔離 HOME
# 分別放「有 path」「沒有 path」兩份 settings.json,用真的可執行檔跑 doctor。
# 會打網路:只有 Claude Code 自己可能匿名讀 git.uncle6.meGitea),不碰 GitHub、不寫任何遠端。
#
# 期望(09-13 在 CLI 2.1.220Desktop 2.1.266 上實測過的行為):
# 沒有 path ⇒ 兩個都 ✅,離開碼 0 (真陰性)
# 有 path、Desktop 2.1.266 ⇒ ❌ 並點名 path (真陽性)
# 有 path、CLI 2.1.220 ⇒ ✅,不因設定長相被判 ❌ (舊版 doctor 的假陽性)
# 版本不是這兩個時,該版本那一格印 SKIP(行為可能不同,不假裝驗過)。
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
R="$HOME/.claude/plugins"
[ -d "$R/cache/inkstone" ] && [ -d "$R/marketplaces/inkstone" ] || { echo "SKIP:這台沒有安裝 isep@inkstone,無從複製"; exit 0; }
CLI=$(command -v claude || true)
DESK=$(ls -d "$HOME/Library/Application Support/Claude/claude-code/"*/claude.app/Contents/MacOS/claude 2>/dev/null \
| python3 -c 'import sys,re;L=[l.strip() for l in sys.stdin if l.strip()];k=lambda p:tuple(int(x) for x in re.search(r"/claude-code/([0-9.]+)/",p).group(1).split(".") if x);print(sorted(L,key=k)[-1] if L else "")')
BINS=""; [ -n "$CLI" ] && BINS="CLI=$CLI"; [ -n "$DESK" ] && BINS="${BINS:+$BINS:}Desktop=$DESK"
[ -n "$BINS" ] || { echo "SKIP:找不到任何 Claude Code"; exit 0; }
vcli=$([ -n "$CLI" ] && "$CLI" --version | awk '{print $1}'); vdesk=$([ -n "$DESK" ] && "$DESK" --version | awk '{print $1}')
echo "CLI=${CLI:-} ${vcli:-}Desktop=${DESK:-} ${vdesk:-}"
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
C="$T/home/.claude"; mkdir -p "$C/plugins/cache" "$C/plugins/marketplaces"
cp -R "$R/cache/inkstone" "$C/plugins/cache/" && cp -R "$R/marketplaces/inkstone" "$C/plugins/marketplaces/"
python3 - "$R" "$C" <<'EOF'
import json, sys
R, C = sys.argv[1], sys.argv[2]
km = json.load(open(R + "/known_marketplaces.json"))
json.dump({"inkstone": json.loads(json.dumps(km["inkstone"]).replace(R, C + "/plugins"))}, open(C + "/plugins/known_marketplaces.json", "w"))
ip = json.load(open(R + "/installed_plugins.json"))
ip["plugins"] = {"isep@inkstone": ip["plugins"]["isep@inkstone"]}
json.dump(json.loads(json.dumps(ip).replace(R, C + "/plugins")), open(C + "/plugins/installed_plugins.json", "w"))
src = {"source": "git", "url": "https://git.uncle6.me/inkstone/ISEP.git"}
json.dump({"enabledPlugins": {"isep@inkstone": True}, "extraKnownMarketplaces": {"inkstone": {"source": src}}}, open(C + "/settings.nopath.json", "w"))
src["path"] = "/Users/youlinhsieh/Documents/tech_projects/ISEP"
json.dump({"enabledPlugins": {"isep@inkstone": True}, "extraKnownMarketplaces": {"inkstone": {"source": src}}}, open(C + "/settings.path.json", "w"))
EOF
cp -R "$C" "$T/pristine"
pass=0; fail=0; skip=0
doctor() { # settings 變體
rm -rf "$C" && cp -R "$T/pristine" "$C" && cp "$C/settings.$1.json" "$C/settings.json"
OUT=$(cd "$T" && env -u CLAUDE_CODE_CHILD_SESSION HOME="$T/home" ISEP_DOCTOR_CLAUDE_BINS="$BINS" python3 "$HERE/isep-doctor" 2>&1); CODE=$?
echo "── settings=$1exit=$CODE"; printf '%s\n' "$OUT" | sed 's/^/ /'
}
has() { printf '%s' "$OUT" | grep -q -- "$1"; }
judge() { if eval "$2"; then pass=$((pass+1)); echo "ok $1"; else fail=$((fail+1)); echo "FAIL $1"; fi; }
doctor nopath
judge "真陰性:沒有 path → 離開碼 0、沒有 ❌" '[ "$CODE" = 0 ] && ! has "❌"'
doctor path
if [ "$vdesk" = 2.1.266 ]; then
judge "真陽性:Desktop 2.1.266 有 path → ❌ 並點名 path、離開碼 1" '[ "$CODE" = 1 ] && has "❌ Desktop 2.1.266" && has "多了 path"'
else skip=$((skip+1)); echo "SKIP 真陽性:Desktop 是 ${vdesk:-},不是 2.1.266"; fi
if [ "$vcli" = 2.1.220 ]; then
judge "假陽性:CLI 2.1.220 有 path 但實際載入 → ✅" 'has "✅ CLI 2.1.220"'
else skip=$((skip+1)); echo "SKIP 假陽性:CLI 是 ${vcli:-},不是 2.1.220"; fi
echo "pass=$pass fail=$fail skip=$skip"; [ "$fail" = 0 ]
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# 迴歸測試:scripts/isep-doctorinkstone/ISEP#150)——全離線
# 用假的 claude 可執行檔(shell 腳本)代替真的 CLIDesktop,走的是 doctor 真正的 subprocess 那條路。
# 真的 Claude Code 版本的實測在 scripts/test-isep-doctor-live.sh。
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
pass=0; fail=0
URL=https://git.uncle6.me/inkstone/ISEP.git
mkfake() { # 路徑 版本 plugin-list 輸出(原樣印出)
mkdir -p "$(dirname "$1")"
printf '%s' "$3" > "$1.out"
cat > "$1" <<EOF
#!/bin/sh
if [ "\$1" = "--version" ]; then echo "$2 (Claude Code)"; exit 0; fi
cat "$1.out"
EOF
chmod +x "$1"
}
run() { OUT=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" python3 "$HERE/isep-doctor" 2>&1); CODE=$?; }
ok() { pass=$((pass+1)); echo "ok $1"; }
bad() { fail=$((fail+1)); echo "FAIL $1 (exit=$CODE)"; printf '%s\n' "$OUT" | sed 's/^/ /'; }
expect() { # 名稱 離開碼 要有的樣式(| 分隔,可空) 不准有的樣式(可空)
local p miss=""
[ "$CODE" = "$2" ] || miss="exit"
local IFS='|'
for p in $3; do printf '%s' "$OUT" | grep -q -- "$p" || miss="$miss 缺[$p]"; done
if [ -n "${4:-}" ] && printf '%s' "$OUT" | grep -q -- "$4"; then miss="$miss 多了[$4]"; fi
[ -z "$miss" ] && ok "$1" || { bad "$1$miss"; }
}
mkdir -p "$T/home/plugins"
echo "{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\"}}}" > "$T/home/plugins/known_marketplaces.json"
SET_PATH="{\"extraKnownMarketplaces\":{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\",\"path\":\"/Users/x/ISEP\"}}},\"model\":\"keep\"}"
SET_CLEAN="{\"extraKnownMarketplaces\":{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\"}}}}"
LOADED='[{"id":"isep@inkstone","version":"0.30.1","enabled":true}]'
NOTFOUND='[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]'
# ① 假陽性(c6991):設定有 path,但這個 Claude Code 自己回報載入正常 → ✅、不准點名 path
echo "$SET_PATH" > "$T/home/settings.json"
mkfake "$T/b/cli" 2.1.220 "$LOADED"
export ISEP_DOCTOR_CLAUDE_BINS="CLI=$T/b/cli"; run
expect "① 有 path 但實際載入(2.1.220 形狀)→ ✅ 不點名 path" 0 "✅ CLI 2.1.220" "path"
# ② 真陽性(c6990):同一份設定,CLI 載得進、Desktop 載不進 → 1;兩邊各講各的;點名 path;給修法
mkfake "$T/b/desk" 2.1.266 "$NOTFOUND"
export ISEP_DOCTOR_CLAUDE_BINS="CLI=$T/b/cli:Desktop=$T/b/desk"; run
expect "② CLI 載得進、Desktop 載不進 → 1,逐一講、點名 path" 1 "✅ CLI 2.1.220|❌ Desktop 2.1.266|Marketplace inkstone not found|多了 path|^ python3 "
printf '%s' "$OUT" | grep -q "CLI 與 Desktop 都一樣" && bad "② 不准再說 CLI 與 Desktop 都一樣" || ok "② 沒有把整台機器講成同一個結論"
# ③ 真陰性:拿掉 path,兩邊都載入 → 0
echo "$SET_CLEAN" > "$T/home/settings.json"
mkfake "$T/b/desk" 2.1.266 "$LOADED"; run
expect "③ 沒有 path、兩邊都載入 → 0" 0 "✅ CLI 2.1.220|✅ Desktop 2.1.266" "❌"
# ④ 別的原因失敗(設定沒有多餘欄位)→ 1,照實轉述,不編原因、不給 path 修法
mkfake "$T/b/desk" 2.1.266 '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Plugin directory missing"]}]'; run
expect "④ 其他錯誤照實轉述、不編原因" 1 "❌ Desktop|Plugin directory missing" "修法(一行"
# ⑤ 沒安裝 → 1
mkfake "$T/b/desk" 2.1.266 '[]'; run
expect "⑤ 沒安裝 → 1" 1 "❌ Desktop 2.1.266isep@inkstone 沒有安裝"
# ⑥ 查不了(輸出不是 JSON)+另一個正常 → 2,◐,不准講成 ❌ 或 ✅
mkfake "$T/b/desk" 2.1.266 'Error: something broke'; run
expect "⑥ 一個查不了、一個正常 → 2" 2 "◐ Desktop|讀不到不等於沒載入|✅ CLI" "❌"
# ⑦ 一個 Claude Code 都找不到 → 2
export ISEP_DOCTOR_CLAUDE_BINS=""; run
expect "⑦ 找不到任何 Claude Code → 2" 2 "◐ 查不了"
# ⑧ 修法那一行真的修得好(在假 HOME 上原封不動執行印給 leo 的那一行)
echo "$SET_PATH" > "$T/home/settings.json"
mkfake "$T/b/desk" 2.1.266 "$NOTFOUND"
export ISEP_DOCTOR_CLAUDE_BINS="Desktop=$T/b/desk"; run
fixline=$(printf '%s\n' "$OUT" | grep '^ python3 ' | sed 's/^ //')
mkdir -p "$T/fake/.claude" && cp "$T/home/settings.json" "$T/fake/.claude/settings.json"
HOME="$T/fake" bash -c "$fixline"
if [ -n "$fixline" ] && python3 -c "import json,sys;d=json.load(open('$T/fake/.claude/settings.json'));s=d['extraKnownMarketplaces']['inkstone']['source'];sys.exit(0 if 'path' not in s and d['model']=='keep' and s['url'].endswith('ISEP.git') else 1)"; then
ok "⑧ 修法一行拿掉 path、其他設定不動"
else fail=$((fail+1)); echo "FAIL ⑧ 修法一行"; cat "$T/fake/.claude/settings.json"; fi
# ⑧b 修完再問一次(Desktop 改回報正常)→ 0:出路走得通
cp "$T/fake/.claude/settings.json" "$T/home/settings.json"; mkfake "$T/b/desk" 2.1.266 "$LOADED"; run
expect "⑧b 照修法修完再跑 → ✅" 0 "✅ Desktop" "多了 path"
# ⑨ 自動尋找:Desktop 取數字上最新的那一份(2.1.99 < 2.1.266,字串排序會選錯)
unset ISEP_DOCTOR_CLAUDE_BINS
D="$T/h9/Library/Application Support/Claude/claude-code"
mkfake "$D/2.1.99/claude.app/Contents/MacOS/claude" 2.1.99 "$NOTFOUND"
mkfake "$D/2.1.266/claude.app/Contents/MacOS/claude" 2.1.266 "$LOADED"
OUT=$(HOME="$T/h9" PATH="/usr/bin:/bin" ISEP_DOCTOR_CLAUDE_HOME="$T/home" /usr/bin/env python3 "$HERE/isep-doctor" 2>&1); CODE=$?
expect "⑨ Desktop 取最新版本(數字排序)" 0 "✅ Desktop 2.1.266" "2.1.99"
echo "pass=$pass fail=$fail"; [ "$fail" = 0 ]