Files
system-dev-template/template/.claude/hooks/lib/role-lib.sh
T
Leo 6a49f25aef feat(W2 Phase 0-1): 雙 profile 地基+JDD 兩軸身分+兩支防炸閘
SDD: docs/3-specs/jdd-dual-profile(draft → active,leo 2026-08-05 回「開工」)
範圍:總管指定的「防炸兩件 → Phase 0 → Phase 1」,Phase 2 以後未開工。

■ 防炸(排在所有 task 之前,因為它們炸的是既有的東西)
- check-no-instance-names.sh + instance-names.txt:框架範本不得混入實例專名
  基線實測 22 行命中(非先前誤報的 20)→ 9 行無損泛化改寫、13 行檔級豁免記帳待 W3 搬走
  拒絕假性清理(把專名換成模糊詞=資訊消失、分層問題還在)
- check-legacy-paths.sh:已發佈腳本引用的 35 條遠端路徑只增不移
  舊實例跑的是舊腳本、路徑寫死;搬檔=整排 404 且不會有下一次更新來修它(1.16.0 前科)

■ Phase 0 地基
- template/manifest/{common,repo,orchestrator}.tsv:安裝清單單一真相源
  修好 install/update 兩份硬編清單的既有漂移——install 從不裝 wiki-first-search /
  subagent-wiki-guard / publish-lag-check / decisions-summary,但 update 會
  ⇒ 乾淨安裝反而拿不到 1.16/1.17/1.18 的招牌功能
- .claude/hooks/lib/role-lib.sh:scope×role 兩軸機械判定,零自陳
  身分矩陣六組實測全通過,含「成員 repo × orchestrator」不存在的格子擋下
- .sdt-framework-dev:框架開發標記(官方沒有 --framework-dev 這個參數,實查非記憶)

■ Phase 1 雙 profile
- profiles/{repo,orchestrator}/CLAUDE.md 兩部憲法
- install.sh:--profile + 自動偵測+寫檔前確認、manifest 驅動、
  CLAUDE.md 三段組裝(框架區/本地補充區界標+sha256)、.profile、.template-manifest、
  settings.json 寫入 env.AGENT_ROLE 預設
- update.sh:漂移偵測(不覆蓋手改檔、另存 .new、白話清單)+ 基準快照隨更新前進
- template/CLAUDE.md 原路徑凍結留底(相容)

■ 順手修掉兩個舊 bug(都在本次要動的函式裡)
- add_if_missing 少了 mkdir -p ⇒ 新目錄的檔 curl 失敗但 VERSION 照升(2026-07 記「待回報」至今未修)
- 下載健全性只用 [ -s ]=非空即接受 ⇒ 404 頁面會無聲覆寫好檔
  (SKILL.md 260→1 行的機制;同一支腳本的版本號那條路早就防了,檔案這條沒防)

■ 實測(非推論)
- G4 憲法分流:兩個乾淨環境各裝一次,orchestrator 版含 SDD 三件式關鍵字 0 次、
  repo 版含上游指針 8 次;界標 4/4;sha 宣告與實算相符
- G7 CI 擋實例名:注入違規行 → fail 並指出 sdd-check.md:77,exit 1;還原後 exit 0
- 漂移偵測:手改兩支 hook → 正確報 2 支、手改內容保住、產 .new;
  解掉後歸零;連跑三輪冪等

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:56:48 +08:00

173 lines
7.7 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# role-lib.sh — 兩軸身分判定函式庫(被 source,不獨立掛 hook
#
# 出處:《分離導入規格》第二節「兩軸身分(scope × role,全部機械判定,agent 不自我判斷)」
# +《JDD 導入規格》第四節「角色與權限」。
#
# ── 為什麼是「兩軸」而不是「一個角色設定」──────────────────────
# scope(我在誰的地盤)=**安裝位置**決定:agent 在哪個資料夾醒來,
# 就只讀得到那部憲法。來源= system-dev/.profileinstall 時寫死)。
# role (我是什麼工種)=**環境變數 AGENT_ROLE** 決定:
# orchestrator 效忠 root+journeys、禁寫 code 禁改 tasks
# engineer 效忠 requirements、禁改考卷(journeys/root/Gherkin)。
#
# 兩軸都有機械來源 ⇒ **沒有任何一格是靠 agent 自陳的**。
# 這是整套封路的地基:agent 說自己是誰不算數,檔案和環境說了才算。
#
# ── 身分矩陣(注意右下角那個空格)──────────────────────────
# AGENT_ROLE=orchestrator AGENT_ROLE=engineer
# orchestrator profile PM 本尊 ✅ 總管 repo 裡的技術 subagent ✅
# repo profile ❌ 不存在(擋) 寫 code 的 subagent ✅
#
# 左下角為什麼要擋而不是靜默降級:成員 repo 裡沒有 PM。
# 靜默降級=把一個設定錯誤變成「看起來正常但權限不對」,那比報錯危險。
# 空格也是封路(分離導入規格 §2)。
#
# ── 為什麼是函式庫不是六支各自為政的 hook ────────────────────
# 六支封路 hook 都要做同樣四件事:解析 JSON 取 file_path、算 repo 相對路徑、
# 判 scope、判 role。各寫一份=四處維護同一條規則,正是分離導入規格 §6.4
# 要避免的「兩處維護同一條規則」。
#
# 用法(在 hook 裡):
# source "$(dirname "${BASH_SOURCE[0]}")/lib/role-lib.sh"
# INPUT="$(cat)"
# FILE_PATH="$(sdt_file_path "$INPUT")"
# sdt_assert_identity || exit 2
#
# 誠實限制:這裡判的是「宣告出來的身分」。有人硬改 .profile 或亂設 AGENT_ROLE
# 一樣會通過——它擋的是**意外與疏忽**,不是刻意繞道。留痕可審,不宣稱防偽。
# 注意:本檔被 source**不要**在這裡 set -euo pipefail
#(會把呼叫端的 shell 選項一起改掉,hook 的容錯行為會跟著變)。
# ── repo 根目錄 ────────────────────────────────────────
# 從本檔位置往上推三層:<root>/.claude/hooks/lib/role-lib.sh
# 為什麼不用 $CLAUDE_PROJECT_DIR:雲端/CI/被 source 進別的腳本時不保證存在
#(分離導入規格 §3:封路 hook 工具箱要「環境偵測、repo 相對路徑,雲端可跑」)。
sdt_repo_root() {
local libdir
libdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$libdir/../../.." && pwd)
}
# ── 路徑正規化:絕對或相對 → repo 相對 ──────────────────
sdt_rel_path() {
local file_path="$1" root
root="$(sdt_repo_root)"
case "$file_path" in
"$root"/*) printf '%s' "${file_path#"$root"/}" ;;
/*) printf '%s' "$file_path" ;; # 別的地方的絕對路徑,原樣回(呼叫端自己判要不要管)
*) printf '%s' "$file_path" ;; # 已是相對路徑
esac
}
# ── scope 軸:安裝位置決定 ──────────────────────────────
# 讀不到 .profile 時回 repo:多數實例是成員 repo,而且此時 orchestrator 專屬的
# 攔截本來就不該觸發(總管禁入成員 repo 那條只裝在 orchestrator profile)。
# 這是「最小驚訝」而非「最小權限」——選擇的代價寫在這裡,別假裝沒有。
sdt_scope() {
local f v
f="$(sdt_repo_root)/system-dev/.profile"
if [ -f "$f" ]; then
v="$(tr -d '[:space:]' < "$f" 2>/dev/null || true)"
case "$v" in
repo|orchestrator) printf '%s' "$v"; return 0 ;;
esac
fi
printf 'repo'
}
# ── role 軸:AGENT_ROLE 環境變數決定;沒設就依 scope 推定 ──
sdt_role() {
case "${AGENT_ROLE:-}" in
orchestrator|engineer) printf '%s' "$AGENT_ROLE"; return 0 ;;
esac
# 未設定 → 依 scope 推定(不問 agent、不猜)
case "$(sdt_scope)" in
orchestrator) printf 'orchestrator' ;;
*) printf 'engineer' ;;
esac
}
# ── 矩陣檢查:擋掉不存在的那一格 ────────────────────────
# 回傳 0 = 身分合法;回傳 2 = 非法(呼叫端應 exit 2)
sdt_assert_identity() {
local scope role
scope="$(sdt_scope)"
role="$(sdt_role)"
if [ "$scope" = "repo" ] && [ "$role" = "orchestrator" ]; then
cat >&2 <<EOF
❌ 身分組合不存在:成員 repo × orchestrator
scope(安裝位置):repo ← system-dev/.profile
role AGENT_ROLE):orchestrator ← 環境變數
成員 repo 裡沒有 PM。PMorchestrator)的動作——改 root.mdjourneys.md
排 sprint 站號——只能在總管 repo 做。
怎麼修(擇一):
· 你其實是來寫 code 的 → unset AGENT_ROLE,或設成 engineer
· 你真的要做 PM 的事 → 回總管 repo 做,不要在成員 repo 裡做
· 這個 repo 其實是總管 repo → system-dev/.profile 內容應為 orchestrator
(裝錯 profile 了,重跑 install.sh --profile=orchestrator
EOF
return 2
fi
return 0
}
# ── 從 hook 的 JSON 取 file_path ────────────────────────
# 三段 fallbackjq → python3 → grep。沿本 template 既有 hook 的容錯慣例
#(拿不到就回空字串,讓呼叫端放行——寧可漏擋也不誤殺)。
# 用法:FILE_PATH="$(sdt_file_path "$INPUT")" ← 傳字串,不是讀 stdin
#(stdin 只能讀一次,統一由呼叫端 `INPUT="$(cat)"` 讀走再傳進來)
sdt_file_path() {
local input="$1"
[ -z "$input" ] && return 0
if command -v jq >/dev/null 2>&1; then
printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null && return 0
fi
if command -v python3 >/dev/null 2>&1; then
printf '%s' "$input" | python3 -c '
import json,sys
try:
d=json.load(sys.stdin)
print(d.get("tool_input",{}).get("file_path","") or "")
except Exception:
print("")
' 2>/dev/null && return 0
fi
printf '%s' "$input" \
| grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' \
| head -1 \
| sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//'
}
# ── 從 hook 的 JSON 取要寫入的內容(Write 用 content、Edit 用 new_string)──
sdt_write_content() {
local input="$1"
[ -z "$input" ] && return 0
if command -v jq >/dev/null 2>&1; then
printf '%s' "$input" \
| jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")' 2>/dev/null \
&& return 0
fi
if command -v python3 >/dev/null 2>&1; then
printf '%s' "$input" | python3 -c '
import json,sys
try:
ti=json.load(sys.stdin).get("tool_input",{})
print("\n".join(x for x in (ti.get("content"), ti.get("new_string")) if x))
except Exception:
print("")
' 2>/dev/null && return 0
fi
# 無 jq/python3:退回整包(寧可多掃不漏掃,沿 wiki-secret-scan.sh 的慣例)
printf '%s' "$input"
}
# ── 除錯用:一行印出目前身分 ────────────────────────────
sdt_identity_line() {
printf 'scope=%s role=%s root=%s' "$(sdt_scope)" "$(sdt_role)" "$(sdt_repo_root)"
}