diff --git a/README.md b/README.md index 0457843..a299e71 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,10 @@ system-dev-template/ │ ├── CLAUDE.md ← 填空版導航牌 │ ├── docs/ ← 文件結構(六層分類) │ └── .claude/ -│ ├── wiki/ ← CC 的記憶空間 -│ └── commands/ ← Slash commands +│ ├── wiki/ ← CC 的記憶空間 +│ ├── 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(底線)都不可省。 + --- ## 設計原則 diff --git a/docs/wishlist.md b/docs/wishlist.md new file mode 100644 index 0000000..e8e8180 --- /dev/null +++ b/docs/wishlist.md @@ -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 失效。沒有 fallback,hook 一失效就靜默回到「全靠 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」的關鍵。 diff --git a/scripts/install.sh b/scripts/install.sh index ef7ba38..496f521 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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" diff --git a/template/.claude/commands/wiki-recall.md b/template/.claude/commands/wiki-recall.md new file mode 100644 index 0000000..e6055c5 --- /dev/null +++ b/template/.claude/commands/wiki-recall.md @@ -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,再動手。 diff --git a/template/.claude/hooks/pre-write-guard.sh b/template/.claude/hooks/pre-write-guard.sh new file mode 100755 index 0000000..50f6d5f --- /dev/null +++ b/template/.claude/hooks/pre-write-guard.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# PreToolUse hook 範本骨架 —— 專案自訂禁令 +# wishlist §2 可選:讓使用者自訂專案禁令(例:「KBDB 禁動表」「某目錄唯讀」)。 +# +# 預設不啟用。要用時: +# 1. 在下面 FORBIDDEN_PATTERNS 填入禁改的路徑/檔名 pattern +# 2. 到 .claude/settings.json 的 PreToolUse 加掛這支 +# +# 掛在 PreToolUse(matcher: 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 </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 <&2 +exit 0 diff --git a/template/.claude/hooks/session-start-recall.sh b/template/.claude/hooks/session-start-recall.sh new file mode 100755 index 0000000..8038758 --- /dev/null +++ b/template/.claude/hooks/session-start-recall.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# SessionStart hook — 開 session 自動注入 status.md 重點 +# wishlist §1 主路徑:不靠 CC 自覺、不用人說,開 session 就把進度推到眼前。 +# +# 掛在 settings.json 的 SessionStart(matcher: 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 diff --git a/template/.claude/settings.json b/template/.claude/settings.json new file mode 100644 index 0000000..1c9297a --- /dev/null +++ b/template/.claude/settings.json @@ -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" + } + ] + } + ] + } +} diff --git a/template/CLAUDE.md b/template/CLAUDE.md index 395c6d8..274495d 100644 --- a/template/CLAUDE.md +++ b/template/CLAUDE.md @@ -39,6 +39,9 @@ | `.claude/wiki/mistakes.md` | 做新功能前 | 已知誤解 + 快速檢查清單 | | `.claude/wiki/decisions-summary.md` | 遇到設計判斷時 | 架構決策快速查 | +> 開 session 由 `SessionStart` hook 自動注入 status 重點。沒自動接關 → 打 `/wiki-recall`。 +> status/wiki 是 **快照非即時狀態**:讀快照 **+ 核實快照**,不盲信。 + --- ## 規範索引