Files
system-dev-template/template/.claude/hooks/role-guard.sh
T
Leo 2f5d9f3bb2 feat(W2 Phase 2-3): JDD 文件範本+八條封路 hook+還清兩件舊債
SDD: docs/3-specs/jdd-dual-profile(active)。編號 task 26/33 完成,Phase 4-5 未開工。

■ Phase 2 JDD 文件範本(orchestrator profile)
範本形狀對齊「實際跑出來的那兩份」(總管已寫的 root.md 15 卡、journeys.md J-1 九站),
不是照規格憑空造:
- 卡片是巢狀 bullet(`- **P1** 🟢 …` + 子項放來源/對帳),非規格畫的平行文字行
- 站點索引**巢狀 bullet 不用表格**(表格會把層級壓平,看不出從屬)
- 兩份都保留「這卷還缺什麼(誠實記)」收尾段——規格沒有,但那是防假綠的地方
新增:root.md / journeys.md / sprint.md / triage-map.md 四範本(add-if-missing,
填了就永不覆蓋)+ plugin-load-order.md(W3 插槽,框架不發明平行外掛格式)

■ Phase 3 封路 hook(八條規則落六支檔)
- role-guard(J1+J2+J3)★命門:考生不能改考卷。六組實測含「考題藏在別的 md 裡」也擋
- jdd-format-guard(J4+J5+J8):紅卡缺對帳日/任務缺站號/PM 文件混技術名詞
- station-done-guard(J6):收工判準是站的考題全綠,不是任務全關
- regression-scope(J7):動實作 → 列出要重考哪幾題(只提醒不擋)
- install-artifact-guard(S1):實例不改機制
- orchestrator-scope-guard(S4):總管不進成員 repo 動實作(從實例上收進框架,
  路徑清單改由實例自填,範本零專名)
掛載鏈依「範圍大的擋在前」:改機制 → 角色 → 位置 → 格式 → 既有三支

■ 還清兩件舊債
- update.sh 檔案清單改讀 manifest(舊硬編降為抓不到來源時的 fallback)
  ——install/update 兩份手抄清單漂移的根因全修
- CLAUDE.md 界標補植:舊實例全文原封包進本地區、框架區重鋪、原檔備份、冪等
  ——解開「沒界標⇒不敢覆蓋⇒框架改的憲法永遠送不到既有實例」這個死結

■ 修掉三個自己造的問題(實測抓出來的,不是想出來的)
- jdd-format-guard 誤擋真實 journeys.md 的「這卷還缺什麼」自述段
  → 排除法改**正面圈定**(只掃卡片本體與站內文),說明區/自述段/索引自然不在範圍
- install-artifact-guard 把 pre-write-guard.sh 也擋了——而它的錯誤訊息正叫人去改那支
  → 使用者自訂插槽列為最優先放行
- check-legacy-paths 用 HEAD 當基準會**自我弱化**:改成清單驅動後保護範圍 35→29 條
  → 基準改指最後一次真正發佈的版本

■ 實測(全部貼過輸出)
- G2 考生改考卷:6/6,含 orchestrator 寫 code/engineer 改考題/考題藏別處
- G4 憲法分流:兩環境重裝,總管版技術軌關鍵字 0、成員版上游指針 8,界標 4/4
- G6 實例改機制:4/4,含框架開發標記放行與自訂插槽放行
- G7 CI 擋實例名:注入違規 → 指出檔案行號 exit 1
- G3 進度以站計量:起牀推「J-1 已點亮 2/9 站」、收工列未亮站並禁用任務數當理由
- 回歸考、界標補植冪等、orchestrator-scope-guard 四組:全通

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

154 lines
6.1 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-guard.sh — 角色封路:誰能寫什麼(JDD 規則 J1+J2+J3)
#
# 掛 PreToolUsematcher: Write|Edit|MultiEdit)。
#
# ── 為什麼三條規則合成一支 hook ─────────────────────────
# J1 orchestrator 不准寫 code
# J2 orchestrator 不准寫技術軌文件(任務池/規格)
# J3 engineer 不准改考卷(旅程/根文件/任何考題)
# 三條都是「同一個 hook 事件 × 同一份身分判定 × 同一張路徑表」。
# 拆三支=解析三次 JSON、三處維護同一張表,改一條規則要記得改三個檔。
# 規則編號保留在程式碼與錯誤訊息裡,追溯不會少。
#
# ── J3 是命門 ───────────────────────────────────────
# 考生不能改考卷。這條沒守住,整套 PM 軌就是裝飾品:
# 考題沒過的人,只要改一下考題就「過了」,而且沒有人會發現。
# 所以 J3 攔的不只是檔案,還包括 md 檔裡的 Gherkin 區塊——
# 考題常常就住在別的文件裡。
#
# 誠實限制:擋的是「直接寫檔」這個語法層動作。
# 用 bash 繞道(sed -i / cat > / python 改檔)擋不到。
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱不可能繞過。
set -uo pipefail
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/role-lib.sh
. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0 # 函式庫不在就放行(容錯)
INPUT="$(cat)"
FILE_PATH="$(sdt_file_path "$INPUT")"
[ -z "$FILE_PATH" ] && exit 0 # 拿不到路徑 → 放行,寧可漏擋不誤殺
REL="$(sdt_rel_path "$FILE_PATH")"
ROLE="$(sdt_role)"
# 身分矩陣先驗(成員 repo × orchestrator 這格不存在)
sdt_assert_identity || exit 2
# ── 路徑分類 ───────────────────────────────────────
is_code_path() {
case "$1" in
src/*|*/src/*) return 0 ;;
*.py|*.ts|*.tsx|*.js|*.jsx|*.go|*.rs|*.java|*.rb|*.c|*.h|*.cpp|*.sh|*.bash|*.zsh) return 0 ;;
*.sql|*.vue|*.svelte|*.swift|*.kt|*.php) return 0 ;;
esac
return 1
}
is_tech_track_doc() { # 技術軌文件:任務池與規格
case "$1" in
*tasks.md|*requirements.md|*design.md) return 0 ;;
esac
return 1
}
is_exam_paper() { # 考卷:旅程/根文件/獨立考題檔
case "$1" in
*journeys.md|*root.md|*.feature) return 0 ;;
esac
return 1
}
# md 檔內含 Gherkin 區塊?(考題常寄住在別的文件裡)
content_has_gherkin() {
local c
c="$(sdt_write_content "$INPUT")"
[ -z "$c" ] && return 1
# ```gherkin 圍籬,或 journeys.md 的考題行式(- **G-1.1**),或 Given/When/Then 三件成組
printf '%s' "$c" | grep -qiE '```[[:space:]]*gherkin' && return 0
printf '%s' "$c" | grep -qE '^\s*-?\s*\*\*G-[0-9]+\.[0-9]+\*\*' && return 0
if printf '%s' "$c" | grep -qE '^\s*-?\s*Given ' \
&& printf '%s' "$c" | grep -qE '^\s*-?\s*When ' \
&& printf '%s' "$c" | grep -qE '^\s*-?\s*Then '; then
return 0
fi
return 1
}
block() { # $1=規則編號 $2=標題 $3=正確做法(多行)
cat >&2 <<EOF
❌ BLOCKED by role-guard(規則 $1
$2
身分:role=$ROLE(來源:AGENT_ROLE 環境變數/依安裝位置推定)
路徑:$REL
$3
EOF
exit 2
}
# ── J1J2orchestrator 的禁區 ─────────────────────
if [ "$ROLE" = "orchestrator" ]; then
if is_code_path "$REL"; then
block "J1" "總管(PM)不寫 code。" \
" 你的工作是決定「要點亮哪幾站、考題是什麼」,不是自己下去寫。
正確做法:
· 把它變成一個掛站號的任務,交給該 repo 的 engineer
· 你要的結果寫成考題(Gherkin),不是寫成實作
為什麼擋:PM 自己下去寫 code,就沒有人在看「零件之間有沒有人掉進縫裡」了
——那正是這個角色唯一不能被取代的價值。"
fi
if is_tech_track_doc "$REL"; then
block "J2" "總管(PM)不寫技術軌文件(任務池/規格)。" \
" 任務池與規格屬於 engineer。你對它們**只讀**。
正確做法:
· 要調整優先順序 → 改 sprint.md 的指定站,讓認領流程自己去挑任務
· 覺得少了任務 → 說出「哪一站的考題會掛」,由 engineer 認領或新增
· 要改驗收標準 → 改 journeys.md 的考題(那是你的檔)
為什麼擋:你一旦動手改任務清單,就等於繞過「先認領→不足才新增」的順序,
很快會長出第二份平行的任務池,兩邊不同步。"
fi
fi
# ── J3:engineer 不准改考卷(命門)─────────────────
if [ "$ROLE" = "engineer" ]; then
if is_exam_paper "$REL"; then
block "J3" "考生不能改考卷。" \
" journeys.mdroot.md/考題檔是 PM 的檔案,engineer 只能讀當期指定站的段落。
正確做法:
· 考題沒過 → 去把東西做對,不是把題目改掉
· 真的認為題目出錯了(判準不合理、站定義有誤)→ **回報 PM**,由他決定改不改
· 你需要的是「這站到底要什麼」→ 讀,不要寫
為什麼這條是命門:考題沒過的人只要改一下考題就「過了」,
而且沒有任何人會發現。這樣整套驗收就是裝飾品。"
fi
# md 檔本身不是考卷,但內容含考題 → 一樣擋(考題常寄住在別的文件裡)
case "$REL" in
*.md)
if content_has_gherkin; then
block "J3" "考生不能改考卷(這次寫入的內容含考題)。" \
" 這個檔名不是考卷,但你要寫進去的內容裡有 Gherkin 考題(GivenWhenThen 或 G-x.y)。
正確做法:
· 要記錄「我做到哪了」→ 寫 wiki/status,不要在文件裡複寫考題
· 要提出新考題 → 回報 PM,由他寫進 journeys.md
為什麼擋:考題散落成兩份就會不同步,
而不同步的那一刻起,「全綠」代表什麼就沒人說得準了。"
fi
;;
esac
fi
exit 0