feat: 接關 hook + SDD 強制 hook(實作 wishlist 兩項)

§1 接關機制(雙保險):
- session-start-recall.sh:SessionStart 自動注入 status 重點 + 快照核實提醒
- /wiki-recall:fallback 命令,hook 失效時手動接關

§2 軟規範 → 硬攔截:
- sdd-guard.sh:動 code 檔但無 SDD → exit 2 擋(/sdd-check 自動版)
- pre-write-guard.sh:專案自訂禁令骨架(預設停用)
- settings.json:掛 SessionStart + PreToolUse

配套:install.sh 下載 hooks/settings(settings 比照 CLAUDE.md 不覆蓋);
README/CLAUDE.md 補文件 + 誠實限制聲明。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:04:58 +08:00
parent 04e2cc3071
commit 39783cccc8
9 changed files with 338 additions and 2 deletions
+21 -1
View File
@@ -63,7 +63,9 @@ system-dev-template/
│ ├── docs/ ← 文件結構(六層分類)
│ └── .claude/
│ ├── wiki/ ← CC 的記憶空間
── commands/ ← Slash commands
── commands/ ← Slash commands
│ ├── hooks/ ← 硬攔截(SessionStart 接關 + SDD 協議)
│ └── settings.json ← 掛 hooks
├── skills/
│ └── llm-wiki/ ← 複製到 Legacy-Workspace/.claude/skills/
@@ -86,10 +88,28 @@ system-dev-template/
| Command | 做什麼 |
|---------|-------|
| `/wiki-init` | 初始化 wiki(新專案或接入已有專案)|
| `/wiki-recall` | Session 開始,手動接關(hook 沒啟動時的 fallback|
| `/wiki-capture` | 把這次對話的結論存進 wiki |
| `/wiki-update` | Session 結束,更新 status.md |
| `/sdd-check` | 確認當前任務有沒有對應 SDD |
命名閉環:init(建) → update(存,session 末) ↔ recall(接,session 初) → capture(隨時存結論)。
---
## Hooks(軟規範 → 硬攔截)
規範不再只是 CLAUDE.md 的軟提醒,加了底線機制:
| Hook | 角色 |
|------|------|
| `session-start-recall.sh` | 開 session 自動注入 status 重點,不靠 CC 自覺 |
| `sdd-guard.sh` | 動 code 檔但沒有任何 SDD → 攔(exit 2),對應 `/sdd-check` 的自動版 |
| `pre-write-guard.sh` | 專案自訂禁令範本骨架(預設停用,填 pattern 才生效)|
> 誠實限制:hook 擋語法層明顯違規(直接寫檔),擋不了藏在 helper / bash 裡的繞道。
> 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽——文檔(mindset)+ hook(底線)都不可省。
---
## 設計原則
+48
View File
@@ -0,0 +1,48 @@
# Wishlist
system-dev-template 自身要補的功能。
---
## 1. 接關機制(開新對話自動恢復進度)
**問題**template 現在「接關」只靠 `CLAUDE.md` 的軟提醒「Wiki 讀取順序:status.md = session 開始第一件事」,期待 CC 開新對話自己去讀。但軟提醒擋不住「CC 讀了 CLAUDE.md 卻沒真讀 status」,使用者也無法確定它讀了沒。
**設計:雙保險(hook 自動 + 命令兜底)**
| 機制 | 角色 | 何時 |
|---|---|---|
| **SessionStart hook** | 主路徑:開 session 自動注入 status 重點,不靠 CC 自覺、不用人說 | 每次 startup/resume/clear 自動 |
| **`/wiki-recall` 命令** | Fallback:hook 沒啟動時手動接關;要完整脈絡時也用 | 使用者見沒自動接關 → 打一句 |
主路徑不靠人;命令應對 hook 失效。沒有 fallbackhook 一失效就靜默回到「全靠 CC 自覺」(正是要解決的問題)。
**要做**
- `template/.claude/hooks/session-start-recall.sh`:注入 status.md 的「正在做 / 下次第一件事」。
- `template/.claude/settings.json`:掛 `SessionStart`matcher `startup|resume|clear`)。
- `template/.claude/commands/wiki-recall.md`:手動接關命令(讀 status → decisions → wishlist → HANDOFF)。
- `install.sh`:偵測已有 settings.json 則不覆蓋,提示手動加 SessionStart(比照 CLAUDE.md 處理)。
**命名閉環**init(建) → update(存,session 末) ↔ recall(接,session 初) → capture(隨時存結論)。
**配套鐵律(寫進 wiki-recall + 文件)**status/wiki 是 **point-in-time 快照非即時狀態**。接關=讀快照 **+ 核實快照**,不盲信。實例:某專案 status 曾寫「待 A 收尾 X」,實際 X 早完成,照舊資訊會催已完成的事。
---
## 2. Hook 強制機制(軟規範 → 硬攔截)
**問題**template 現在**完全沒有 hook**,所有規範(SDD 協議、wiki 維護)都是 CLAUDE.md 的軟提醒。CC 想跳過就跳過,沒有東西抓得到。
**對照 arcrun(值得 template 借鑑)**arcrun 有一套 hook 把鐵律變強制——
- `pre-write-guard.sh` / `pre-bash-guard.sh`:違反禁令(沒讀 SDD 就動 code、寫違規檔)→ **exit 2 直接 block**CC 跳不過。
- `session-start-load-sdd.sh`:開 session 強制注入進度 + 禁令。
- 效果:「想跳過 SDD 協議就會被抓到」,不靠自覺。
**要做(template 標配一個最小 hook 集)**
- **SDD 協議 hook**:動 code.ts/.go/...)前若沒對應 SDD / 沒在回覆宣告已讀 → 擋或警告。對應 template 既有的 `/sdd-check`,但從「命令要人打」升級成「hook 自動攔」。
- **SessionStart hook**:見上 §1(接關 + 注入規範)。
- **可選 pre-write guard**:讓使用者自訂專案禁令(如本 InkStoneCo 的「KBDB 禁動表」hook),template 給範本骨架。
**設計原則(抄 arcrun 的誠實限制)**:hook 擋語法層明顯違規,擋不了藏在 helper 裡的繞道 → **文檔(mindset+ hook(底線)都不可省**,絕不在文件聲稱「不可能繞過」。hook 的價值是「想跳過會被抓到」+ 留痕可審,不是技術防偽。
**為何重要**template 的兩大賣點(SDD 強制先設計 + wiki 持久記憶)現在都只是「請你照做」。加 hook 才從「建議」變「機制」——這正是 template 區別於「就是寫幾個 markdown」的關鍵。
+30
View File
@@ -34,6 +34,7 @@ create_dir "docs/5-records/test-reports"
create_dir "docs/6-user"
create_dir ".claude/wiki"
create_dir ".claude/commands"
create_dir ".claude/hooks"
# ── 檔案(從 repo 下載,只在不存在時)──────────────
@@ -58,8 +59,15 @@ download_if_missing ".claude/wiki/decisions-summary.md" "$REPO_URL/.claude/wiki/
download_if_missing ".claude/commands/wiki-init.md" "$REPO_URL/.claude/commands/wiki-init.md"
download_if_missing ".claude/commands/wiki-capture.md" "$REPO_URL/.claude/commands/wiki-capture.md"
download_if_missing ".claude/commands/wiki-update.md" "$REPO_URL/.claude/commands/wiki-update.md"
download_if_missing ".claude/commands/wiki-recall.md" "$REPO_URL/.claude/commands/wiki-recall.md"
download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md"
# hooks(軟規範 → 硬攔截。下載後補執行權限)
download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh"
download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh"
download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh"
chmod +x .claude/hooks/*.sh 2>/dev/null || true
# docs/README.md(分類地圖)
download_if_missing "docs/README.md" "$REPO_URL/docs/README.md"
@@ -70,6 +78,13 @@ else
SKIPPED+=("CLAUDE.md (已存在,請手動加入 wiki 讀取順序區塊)")
fi
# .claude/settings.json:只在完全不存在時建立(比照 CLAUDE.md,不覆蓋既有設定)
if [ ! -f ".claude/settings.json" ]; then
download_if_missing ".claude/settings.json" "$REPO_URL/.claude/settings.json"
else
SKIPPED+=(".claude/settings.json (已存在,請手動加入 hooks 區塊)")
fi
# ── 輸出結果 ──────────────────────────────────────
echo ""
@@ -105,6 +120,21 @@ if [ -f "CLAUDE.md" ]; then
fi
fi
# 如果 settings.json 已存在,提醒手動加入 hooks 區塊
if [ -f ".claude/settings.json" ]; then
if ! grep -q "session-start-recall.sh" .claude/settings.json; then
echo ""
echo "📌 .claude/settings.json 已存在但缺少 hooks。"
echo " 請手動把以下 hooks 合併進去(已有設定請保留):"
echo ""
echo ' "SessionStart": [{ "matcher": "startup|resume|clear",'
echo ' "hooks": [{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }] }],'
echo ' "PreToolUse": [{ "matcher": "Write|Edit",'
echo ' "hooks": [{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" },'
echo ' { "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }] }]'
fi
fi
echo ""
echo "🚀 下一步:在 Claude Code 對話裡執行:"
echo " /wiki-init"
+58
View File
@@ -0,0 +1,58 @@
# /wiki-recall — Session 開始,手動接關
開新對話時接上次進度。**Fallback 命令**SessionStart hook 沒啟動時手動接關;要完整脈絡時也用。
> 主路徑是 SessionStart hook 自動注入 status 重點,不靠你打命令。
> 這支命令應對 hook 失效,以及需要比「status 重點」更完整脈絡的時候。
---
## 命名閉環
init(建) → update(存,session 末) ↔ **recall(接,session 初)** → capture(隨時存結論)
---
## 執行流程
### 第一步:讀 status.md(當前進度)
`.claude/wiki/status.md`,掌握:
- 正在做什麼、阻擋點
- 下次 session 第一件事
- 待負責人確認、已知問題
### 第二步:讀 decisions-summary.md(為什麼這樣做)
`.claude/wiki/decisions-summary.md`,掌握相關的架構決策——避免重新討論已定案的事。
### 第三步:讀 mistakes.md(別重犯)
`.claude/wiki/mistakes.md`,掌握已知誤解 + 快速檢查清單。
### 第四步:掃 wishlist / HANDOFF(如果有)
- `docs/wishlist.md`:待補功能
- 任何 `HANDOFF.md` / 交接note:上一棒留下的脈絡
### 第五步:回報接關結果
```
📍 接關完成
🔄 上次正在做:[status 的「正在做」]
🎯 下次第一件事:[status 的「下次 session 第一件事」]
⚠️ 待確認:[如有]
```
---
## 鐵律:快照非即時狀態
status / wiki 是 **point-in-time 快照,不是即時狀態**
接關 讀快照 ** 核實快照****不盲信**。
> 實例:某專案 status 曾寫「待 A 收尾 X」,實際 X 早已完成。
> 照舊資訊行動會去催一件已完成的事。
動手前,先用當前 code / git / 檔案核實快照寫的事項是否仍成立。發現落差 → 先更新 status,再動手。
+52
View File
@@ -0,0 +1,52 @@
#!/bin/bash
# PreToolUse hook 範本骨架 —— 專案自訂禁令
# wishlist §2 可選:讓使用者自訂專案禁令(例:「KBDB 禁動表」「某目錄唯讀」)。
#
# 預設不啟用。要用時:
# 1. 在下面 FORBIDDEN_PATTERNS 填入禁改的路徑/檔名 pattern
# 2. 到 .claude/settings.json 的 PreToolUse 加掛這支
#
# 掛在 PreToolUsematcher: Write|Edit)。stdin 收到 JSON{ tool_name, tool_input: { file_path } }
# 命中禁令 → exit 2 擋。
#
# 誠實限制:只擋直接寫檔。bash 繞道、helper 間接改動擋不到。留痕可審 ≠ 技術防偽。
set -euo pipefail
# ── 專案自訂:禁改的 pattern(一行一個,case glob 語法)──────
# 範例(已註解,啟用前請改成自己的):
# "*/db/schema.sql" # 禁手改 schema
# "*/migrations/*" # migration 一旦建立不可改
FORBIDDEN_PATTERNS=(
# "*/your/protected/path/*"
)
# 沒設任何禁令 → 直接放行(骨架預設狀態)
[ ${#FORBIDDEN_PATTERNS[@]} -eq 0 ] && exit 0
INPUT=$(cat)
if command -v jq >/dev/null 2>&1; then
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
else
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
fi
[ -z "$FILE_PATH" ] && exit 0
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
# shellcheck disable=SC2254
case "$FILE_PATH" in
$pattern)
cat >&2 <<EOF
🚫 專案禁令攔截:$FILE_PATH 命中禁改規則($pattern)。
這是本專案 .claude/hooks/pre-write-guard.sh 設定的硬底線。
要動 → 先和負責人確認,並更新禁令設定。
EOF
exit 2
;;
esac
done
exit 0
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# PreToolUse hook — 動 code 前檢查有沒有對應 SDD
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
#
# 掛在 settings.json 的 PreToolUsematcher: Write|Edit)。
# stdin 收到 JSON{ tool_name, tool_input: { file_path, ... } }
# 行為:動到 code 檔(.ts/.go/...)但 docs/3-specs/ 下沒有任何 SDD → 警告(exit 2 擋)。
#
# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
set -euo pipefail
INPUT=$(cat)
# 解析 file_path。優先用 jq,沒有 jq 退回 grep(容錯)。
if command -v jq >/dev/null 2>&1; then
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
else
FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//')
fi
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
[ -z "$FILE_PATH" ] && exit 0
# 只管 code 檔。docs/markdown/設定檔等放行。
case "$FILE_PATH" in
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;;
*) exit 0 ;;
esac
# 改 SDD 自己 / 測試檔 → 放行
case "$FILE_PATH" in
*docs/3-specs/*) exit 0 ;;
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
esac
# docs/3-specs/ 下完全沒有 design.md → 攔
SDD_COUNT=0
if [ -d "docs/3-specs" ]; then
SDD_COUNT=$(find docs/3-specs -name 'design.md' -not -path '*TEMPLATE*' 2>/dev/null | wc -l | tr -d ' ')
fi
if [ "$SDD_COUNT" -eq 0 ]; then
cat >&2 <<EOF
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 docs/3-specs/ 下找不到任何 SDD。
絕對鐵律:任何 code 變動前必須有對應 SDDdesign.md)。
請先:
1. 確認這個改動屬於哪個子系統
2. 在 docs/3-specs/[子系統]/ 建立 design.md(可用 /sdd-check 協助)
3. 在回覆開頭宣告已讀 SDD + 對應 task
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
EOF
exit 2
fi
# 有 SDD:放行,但留痕提醒要宣告(stderr 警告,不擋)
echo "📋 提醒:docs/3-specs/ 下有 SDD。動手前請確認已讀對應 design.md 並在回覆宣告。" >&2
exit 0
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# SessionStart hook — 開 session 自動注入 status.md 重點
# wishlist §1 主路徑:不靠 CC 自覺、不用人說,開 session 就把進度推到眼前。
#
# 掛在 settings.json 的 SessionStartmatcher: startup|resume|clear)。
# stdout 會被當成 context 注入給 CC。
#
# 鐵律:status 是 point-in-time 快照,非即時狀態。
# 這個 hook 只負責「把快照推到眼前」,核實快照是 CC 的責任——下面的提醒就是要它別盲信。
set -euo pipefail
STATUS_FILE=".claude/wiki/status.md"
# 沒有 wiki 就安靜退出(exit 0),不干擾還沒 /wiki-init 的專案
if [ ! -f "$STATUS_FILE" ]; then
exit 0
fi
echo "════════════════════════════════════════════════"
echo "📍 接關:上次進度(來自 $STATUS_FILE 快照)"
echo "════════════════════════════════════════════════"
echo ""
cat "$STATUS_FILE"
echo ""
echo "────────────────────────────────────────────────"
echo "⚠️ 以上是 point-in-time 快照,非即時狀態。"
echo " 動手前先核實:快照寫的事項是否真的還沒做完?"
echo " 需要完整脈絡(decisions / mistakes / SDD)→ 執行 /wiki-recall"
echo "════════════════════════════════════════════════"
exit 0
+30
View File
@@ -0,0 +1,30 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/session-start-recall.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/sdd-guard.sh"
},
{
"type": "command",
"command": ".claude/hooks/pre-write-guard.sh"
}
]
}
]
}
}
+3
View File
@@ -39,6 +39,9 @@
| `.claude/wiki/mistakes.md` | 做新功能前 | 已知誤解 + 快速檢查清單 |
| `.claude/wiki/decisions-summary.md` | 遇到設計判斷時 | 架構決策快速查 |
> 開 session 由 `SessionStart` hook 自動注入 status 重點。沒自動接關 → 打 `/wiki-recall`。
> status/wiki 是 **快照非即時狀態**:讀快照 **+ 核實快照**,不盲信。
---
## 規範索引