roster-guard 認不出真實的 subagent_type,整個環境派不出工(inkstone/ISEP#86 迴歸) #108

Merged
claude-code merged 1 commits from fix/roster-plugin-prefix into main 2026-08-28 12:37:37 +00:00
4 changed files with 36 additions and 3 deletions
+1 -1
View File
@@ -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",
+8 -1
View File
@@ -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
# 允許 "<plugin>:<name>";冒號後面那半才是名單上的名字
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
+24
View File
@@ -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_typeisep:isep-hand)認不出來——就是這個造成 8/28 的死鎖\n'; FAIL=$((FAIL+1))
fi
echo
printf '結果:%s 通過 / %s 失敗(共 %s 條)\n' "$PASS" "$FAIL" "$N"
[ "$FAIL" -eq 0 ] || exit 1
+3 -1
View File
@@ -41,6 +41,7 @@ usage() {
solo <session_id> 這件事真的該自己做(subagent-first-guard)——回覆裡要寫理由
dispatch <session_id> 派工單真的有例外(dispatch-format-guard
ask <session_id> 這題真的是人閘(ask-user-question-guard
roster <session_id> 派工真的是名單的例外(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 ;;
*)