From 5342917f2bc09300c01cfbdcb8ec6d5c097c6ffd Mon Sep 17 00:00:00 2001 From: claude-code Date: Fri, 28 Aug 2026 12:36:13 +0000 Subject: [PATCH] =?UTF-8?q?roster-guard=20=E8=AA=8D=E4=B8=8D=E5=87=BA?= =?UTF-8?q?=E7=9C=9F=E5=AF=A6=E7=9A=84=20subagent=5Ftype=EF=BC=8C=E6=95=B4?= =?UTF-8?q?=E5=80=8B=E7=92=B0=E5=A2=83=E6=B4=BE=E4=B8=8D=E5=87=BA=E5=B7=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code 給 plugin agent 的 subagent_type 帶前綴(isep:isep-hand), 而 agents/*.md 的 frontmatter name 沒有(isep-hand)。roster.find() 用完全相等比對 ⇒ 真實派工一律被擋,而名單上的裸名字工具又不收 ⇒ 兩種寫法都派不出去。 測試沒抓到是因為 24 條全部餵手寫 payload,沒有一條用真實的 subagent_type 形狀。 補第 19 條專測這個:舊 roster.py + 新測試 → 紅;修好 → 25/25。 順手補 gate-ok 漏掉的 roster——v0.15.0 新增那支閘時沒同步逃生口清單, 所以撞上死鎖時連留痕的出口都沒有。 --- .claude-plugin/plugin.json | 2 +- hooks/lib/roster.py | 9 ++++++++- hooks/tests/roster-guard.test.sh | 24 ++++++++++++++++++++++++ scripts/gate-ok | 4 +++- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e079a56..65092ed 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "isep", "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:60 支機械閘(82 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、48 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。", - "version": "0.16.0", + "version": "0.16.1", "keywords": [ "inkstone", "guardrails", diff --git a/hooks/lib/roster.py b/hooks/lib/roster.py index 2df1ac9..5cb5d71 100644 --- a/hooks/lib/roster.py +++ b/hooks/lib/roster.py @@ -97,11 +97,18 @@ def names(directory=None): def find(name, directory=None): + # 🔴 subagent_type 在 plugin 裡帶前綴:Claude Code 給的是 "isep:isep-hand", + # 而 agents/*.md 的 frontmatter name 是 "isep-hand"。原本用完全相等比對 + # ⇒ 真實的派工一律認不出來,而名單裡的名字工具又不收 ⇒ 兩種寫法都派不出去。 + # 2026-08-28 v0.16.0 上線後實撞:整個環境派不出任何工,只能蓋 solo 戳記自己修。 + # 測試沒抓到是因為 24 條全部餵手寫 payload,沒有一條用真實的 subagent_type 形狀。 n = (name or "").strip() if not n: return None + # 允許 ":";冒號後面那半才是名單上的名字 + bare = n.rsplit(":", 1)[-1].strip() if ":" in n else n for w in load(directory): - if w["name"] == n: + if w["name"] == n or w["name"] == bare: return w return None diff --git a/hooks/tests/roster-guard.test.sh b/hooks/tests/roster-guard.test.sh index 0bd2f2f..596844c 100755 --- a/hooks/tests/roster-guard.test.sh +++ b/hooks/tests/roster-guard.test.sh @@ -176,6 +176,30 @@ else printf ' ❌ ⑱ 身份欄沒有吃下工人名字(或把原本的三個角色弄壞了)\n'; FAIL=$((FAIL+1)) fi +echo "── 驗收 4:真實的 subagent_type 形狀(2026-08-28 實撞的死鎖)──────" + +N=$((N+1)) +if python3 - "$ROOT" <<'PY' 2>/dev/null +import sys, os +sys.path.insert(0, os.path.join(sys.argv[1], "hooks", "lib")) +import roster +d = os.path.join(sys.argv[1], "agents") +# Claude Code 給 plugin agent 的真實值帶前綴,agents/*.md 的 name 沒有 +assert roster.find("isep:isep-hand", d), "帶 plugin 前綴的真實 subagent_type 認不出來" +assert roster.find("isep:scout", d), "帶前綴的 scout 認不出來" +# 名單上的裸名字仍然要收(人會照 roster list 直接貼) +assert roster.find("isep-hand", d), "裸名字被弄壞了" +# 剝前綴不能變成放行任何東西 +assert roster.find("isep:nobody", d) is None, "不存在的名字被放行了" +assert roster.find("nobody", d) is None, "不存在的裸名字被放行了" +assert roster.find("", d) is None, "空字串被放行了" +PY +then + printf ' ✅ ⑲ 帶 plugin 前綴的真實 subagent_type 認得出來,不存在的仍然擋\n'; PASS=$((PASS+1)) +else + printf ' ❌ ⑲ 真實的 subagent_type(isep:isep-hand)認不出來——就是這個造成 8/28 的死鎖\n'; FAIL=$((FAIL+1)) +fi + echo printf '結果:%s 通過 / %s 失敗(共 %s 條)\n' "$PASS" "$FAIL" "$N" [ "$FAIL" -eq 0 ] || exit 1 diff --git a/scripts/gate-ok b/scripts/gate-ok index 098ed1a..9e38598 100755 --- a/scripts/gate-ok +++ b/scripts/gate-ok @@ -41,6 +41,7 @@ usage() { solo 這件事真的該自己做(subagent-first-guard)——回覆裡要寫理由 dispatch 派工單真的有例外(dispatch-format-guard) ask 這題真的是人閘(ask-user-question-guard) + roster 派工真的是名單的例外(roster-guard) kbdb-down KBDB 真的連不上(history-first-guard)——回覆裡要說明 --list 列出目前存在的戳記 @@ -93,7 +94,7 @@ case "$GATE" in kbdb-down) stamp /tmp/.kbdb-down "KBDB 連不上而放行——**回覆裡要說明**,這是留痕不是豁免" ;; - solo|dispatch|ask) + solo|dispatch|ask|roster) SID="${1:-${CLAUDE_SESSION_ID:-}}" if [ -z "$SID" ]; then printf '❌ %s 要綁 session id(閘用它認「是不是同一個 session」)。\n' "$GATE" >&2 @@ -104,6 +105,7 @@ case "$GATE" in solo) stamp "/tmp/.solo-ok-$SID" "這件事自己做——**回覆裡要寫理由**" ;; dispatch) stamp "/tmp/.dispatch-ok-$SID" "這張派工單的例外,放行一次" ;; ask) stamp "/tmp/.ask-ok-$SID" "這題確認是人閘,放行一次" ;; + roster) stamp "/tmp/.roster-ok-$SID" "這次派工確認是名單的例外,放行一次" ;; esac ;; *) -- 2.52.0