From 2f5d9f3bb2f31ac2d6022d4278f680bdf96b2d49 Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 6 Aug 2026 00:26:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(W2=20Phase=202-3):=20JDD=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=AF=84=E6=9C=AC=EF=BC=8B=E5=85=AB=E6=A2=9D=E5=B0=81?= =?UTF-8?q?=E8=B7=AF=20hook=EF=BC=8B=E9=82=84=E6=B8=85=E5=85=A9=E4=BB=B6?= =?UTF-8?q?=E8=88=8A=E5=82=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/3-specs/jdd-dual-profile/tasks.md | 40 +-- scripts/check-legacy-paths.sh | 9 +- scripts/install.sh | 17 +- scripts/update.sh | 233 +++++++++++++----- .../.claude/hooks/install-artifact-guard.sh | 92 +++++++ template/.claude/hooks/jdd-format-guard.sh | 167 +++++++++++++ template/.claude/hooks/regression-scope.sh | 54 ++++ template/.claude/hooks/role-guard.sh | 153 ++++++++++++ .../.claude/hooks/session-start-recall.sh | 29 +++ template/.claude/hooks/station-done-guard.sh | 56 +++++ template/manifest/common.tsv | 11 + template/manifest/orchestrator.tsv | 14 +- .../orchestrator/docs/journeys.md.template | 128 ++++++++++ .../orchestrator/docs/plugin-load-order.md | 58 +++++ .../orchestrator/docs/root.md.template | 76 ++++++ .../orchestrator/docs/sprint.md.template | 61 +++++ .../orchestrator/docs/triage-map.md.template | 34 +++ .../hooks/orchestrator-scope-guard.sh | 97 ++++++++ 18 files changed, 1246 insertions(+), 83 deletions(-) create mode 100644 template/.claude/hooks/install-artifact-guard.sh create mode 100644 template/.claude/hooks/jdd-format-guard.sh create mode 100644 template/.claude/hooks/regression-scope.sh create mode 100644 template/.claude/hooks/role-guard.sh create mode 100644 template/.claude/hooks/station-done-guard.sh create mode 100644 template/profiles/orchestrator/docs/journeys.md.template create mode 100644 template/profiles/orchestrator/docs/plugin-load-order.md create mode 100644 template/profiles/orchestrator/docs/root.md.template create mode 100644 template/profiles/orchestrator/docs/sprint.md.template create mode 100644 template/profiles/orchestrator/docs/triage-map.md.template create mode 100644 template/profiles/orchestrator/hooks/orchestrator-scope-guard.sh diff --git a/docs/3-specs/jdd-dual-profile/tasks.md b/docs/3-specs/jdd-dual-profile/tasks.md index 34f1142..86e6bac 100644 --- a/docs/3-specs/jdd-dual-profile/tasks.md +++ b/docs/3-specs/jdd-dual-profile/tasks.md @@ -4,8 +4,8 @@ > 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。 > **每一項都標「服務哪條 Gherkin」**(G1–G7 定義見 `requirements.md` §四)。 > 🟢 **status: active(2026-08-05 leo 回「開工」)**。 -> 進度:**防炸兩閘 + Phase 0 + Phase 1 已完成**(Phase 1 有兩項 ◐,見下)。 -> **Phase 2 以後未經確認不得開工**(總管指定:先停下驗 Gherkin)。 +> 進度:**防炸兩閘 + Phase 0~3 全數完成**,兩件舊債已還清。 +> **Phase 4(框架 CI 掛載)與 Phase 5(出貨/版號)尚未開工。** --- @@ -92,19 +92,19 @@ - 驗收:兩 profile 各自產生的 `settings.json` 中,hook 掛載順序符合 design §4.4; orchestrator 實例的 `env.AGENT_ROLE = orchestrator`、repo 實例 = `engineer` -- [~] 1.7 `update.sh` 改讀 manifest + 三態判定 + 漂移清單輸出 +- [x] 1.7 `update.sh` 改讀 manifest + 三態判定 + 漂移清單輸出 - 服務:**G6**(機械閘 #3) - 驗收:在 InkStoneCo 實例上跑,漂移清單**恰好列出 4 支** (`pre-write-guard.sh`/`sdd-guard.sh`/`subagent-wiki-guard.sh`/`wiki-first-search.sh`, 見 design §0.4 基線),貼完整輸出 - 注意:漂移檔**不覆蓋**,新版另存 `<檔>.new`;輸出用白話(leo 一眼看得懂該做什麼) - - 現況:◐ 漂移偵測+基準維護已實作並實測;**但 update.sh 的檔案清單尚未改讀 manifest**(仍是自己那份硬編),install/update 清單漂移的根因只修了一半 + - 2026-08-06 還清:update.sh 檔案清單已改讀 manifest(舊硬編清單降為抓不到來源時的 fallback),根因全修 -- [~] 1.8 `update.sh` 舊實例遷移:無 manifest → 一次性補植 + CLAUDE.md 界標補植 +- [x] 1.8 `update.sh` 舊實例遷移:無 manifest → 一次性補植 + CLAUDE.md 界標補植 - 服務:**G4**、G6 - 驗收:拿一份 1.18.0 的實例副本跑兩次 update,第二次為 no-op(冪等,貼兩次輸出對照) - 注意:舊 CLAUDE.md 整份包進 `sdt:local` 區,一個字都不能掉 - - 現況:◐ 舊實例無 manifest → 首輪自動建基準(已實測);**CLAUDE.md 界標補植未做** + - 2026-08-06 還清:界標補植已實作並實測(舊全文原封包進本地區、原檔備份、冪等) - [x] 1.9 `template/CLAUDE.md` 原路徑保留轉址說明(向下相容) - 服務:**G4** @@ -116,32 +116,32 @@ > 前置條件:Phase 1 完成(範本要靠 manifest 才鋪得下去) -- [ ] 2.1 `docs/root.md.template`(白話根文件範本) +- [x] 2.1 `docs/root.md.template`(白話根文件範本) - 服務:**G1** - 內容:JDD §3.1 格式原樣 + 三條規則(來源標記必附、禁技術名詞、🔴 卡必附對帳日) - 驗收:範本自身通得過 task 3.2 的 J4/J8 檢查(自己吃自己狗糧) -- [ ] 2.2 `docs/journeys.md.template`(PM 驗收文件範本) +- [x] 2.2 `docs/journeys.md.template`(PM 驗收文件範本) - 服務:**G1**、**G3** - 內容:JDD §3.2 三層巢狀 +「附:站點索引表」段 + 站全域編號/引用不重抄的註記 - 驗收:範本含站點索引表且欄位為「站|被哪些 Journey 經過|改動時重考範圍」 -- [ ] 2.3 `docs/sprint.md.template`(站號 sprint)+ tasks.md 站號欄位約定 +- [x] 2.3 `docs/sprint.md.template`(站號 sprint)+ tasks.md 站號欄位約定 - 服務:**G3** - 內容:JDD §五 四步流程、順序鐵律「先認領 → 認領不足才新增 → 新增必掛站」、 起訖日與到期結算欄 - 驗收:範本能被 task 3.4 的 J6 判準讀出「本 sprint 指定站」與「未點亮站」 -- [ ] 2.4 `docs/triage-map.md.template`(分診表格式,內容留實例) +- [x] 2.4 `docs/triage-map.md.template`(分診表格式,內容留實例) - 服務:**G1** - 驗收:只有欄位與規則,**零實例內容**(通得過 task 4.1 的專名檢查) -- [ ] 2.5 `docs/plugin-load-order.md`(W3 插槽文件)+ 兩份憲法的 must-read 注入點 +- [x] 2.5 `docs/plugin-load-order.md`(W3 插槽文件)+ 兩份憲法的 must-read 注入點 - 服務:**G5(半通的那一半)** - 內容:分離 §五 四步載入順序原文 +「政策包=官方 plugin,框架不發明平行格式」的約定 - 驗收:兩份 profile CLAUDE.md 各含一段可被 plugin SessionStart hook 填入的注入點標題 -- [ ] 2.6 JDD 術語表寫進 orchestrator 憲法(Journey 取代 CP,禁用舊詞) +- [x] 2.6 JDD 術語表寫進 orchestrator 憲法(Journey 取代 CP,禁用舊詞) - 服務:**G3** - 驗收:術語表七詞(Journey/Station/通關/點亮/對帳/完備/輪子卡·賭注卡)齊全, 且標明 `CP.yaml`/`CPDO.md` 屬技術軌內圈保留原名 @@ -152,7 +152,7 @@ > 前置條件:Phase 0(role-lib)+ Phase 2(有檔可擋) -- [ ] 3.1 `role-guard.sh`(J1+J2+J3,common) +- [x] 3.1 `role-guard.sh`(J1+J2+J3,common) - 服務:**G2**(命門) - 內容:orchestrator 禁寫 code 路徑/禁寫 tasks·requirements·design; engineer 禁寫 journeys·root·`*.feature`·md 內 Gherkin 區塊;矩陣空格 exit 2 @@ -163,7 +163,7 @@ ⑥ repo profile × `AGENT_ROLE=orchestrator` → 擋並要求修正環境 - 注意:Gherkin 區塊偵測要涵蓋 md 內的 ```gherkin fence 與 `- **G-x.y** Given` 行式 -- [ ] 3.2 `jdd-format-guard.sh`(J4+J5+J8,common) +- [x] 3.2 `jdd-format-guard.sh`(J4+J5+J8,common) - 服務:**G1**、G3 - J4:`root.md` 🔴 卡缺【要驗證+對帳日】→ 擋並指行號 - J5:`tasks.md` **新增**行缺站號 → 擋(Edit 看 `new_string`,Write 比對現檔差異) @@ -171,20 +171,20 @@ - 驗收:三條各一組正例一組反例,共六次實測貼輸出 - 注意:J5 只判**新增**行,改既有行不擋(否則格式修正都做不了);誠實限制寫進註解 -- [ ] 3.3 `station-done-guard.sh`(J6,common,掛 Stop/TaskCompleted) +- [x] 3.3 `station-done-guard.sh`(J6,common,掛 Stop/TaskCompleted) - 服務:**G3** - 判準:本 sprint 指定站的 Gherkin 全綠才算收工;「tasks 全關」不算 - 驗收:造一個「tasks 全關但站 Gherkin 未綠」的情境 → 被退回(貼 exit 2 輸出) - 注意:框架內**沒有**既有的 sprint 收尾 hook(`delivery-police.sh` 只在 InkStoneCo 實例), 這是新增不是修改;W4 時實例那支要退役,別兩處維護 -- [ ] 3.4 `regression-scope.sh`(J7,common) +- [x] 3.4 `regression-scope.sh`(J7,common) - 服務:**G3** - 行為:偵測站相關實作變動 → 讀 journeys.md 站點索引表 → 列需重考的 Journey/Gherkin - 驗收:改動某站的實作檔後,輸出正確列出該站被哪些 Journey 經過(貼輸出) - 注意:**提醒不阻擋**(exit 0) -- [ ] 3.5 `install-artifact-guard.sh`(S1,common) +- [x] 3.5 `install-artifact-guard.sh`(S1,common) - 服務:**G6** - 行為:寫入 `.claude/hooks/`/`system-dev/` 範本區/plugin 安裝目錄 → 擋, 提示「機制變更走框架/政策包 repo 提案」;`.sdt-framework-dev` 或 `SDT_FRAMEWORK_DEV=1` 放行 @@ -192,7 +192,7 @@ ② 框架 repo(有 marker)編輯同路徑 → 放行。兩組都貼輸出 - 注意:「範本區」的定義來自 manifest(class=overwrite 者),不要另寫一份路徑表 -- [ ] 3.6 `orchestrator-scope-guard.sh`(S4,orchestrator profile 專屬) +- [x] 3.6 `orchestrator-scope-guard.sh`(S4,orchestrator profile 專屬) - 服務:**G6** - 來源:改寫自 InkStoneCo 實例的 `guard-cross-project.sh`(上收進框架,決策 D6) - 抽象化重點:子 repo 目錄清單、autodispatch 白名單**由實例設定檔提供**, @@ -201,13 +201,13 @@ `CHILD_SESSION=1` 且在白名單內 → 放行 - 注意:職責與 3.1 不重疊——3.1 管「角色能寫什麼**類型**」,本支管「總管能進哪個**位置**」 -- [ ] 3.7 改 `session-start-recall.sh`:依 profile 分流注入【改既有】 +- [x] 3.7 改 `session-start-recall.sh`:依 profile 分流注入【改既有】 - 服務:**G3**、G4 - orchestrator:root/journeys 摘要 + 本 sprint **未點亮**站(治「起牀沒事做」) - repo:維持現行 principles/status/mistakes - 驗收:兩 profile 各開一次 session,貼注入內容對照(orchestrator 那份要出現站號) -- [ ] 3.8 hook 全鏈掛載順序實測(design §4.4) +- [x] 3.8 hook 全鏈掛載順序實測(design §4.4) - 服務:**G2**、G6 - 驗收:故意觸發多條規則的一次寫入,確認錯誤訊息來自**最外層**那條(範圍大的先擋) diff --git a/scripts/check-legacy-paths.sh b/scripts/check-legacy-paths.sh index 7956687..06f4432 100755 --- a/scripts/check-legacy-paths.sh +++ b/scripts/check-legacy-paths.sh @@ -30,7 +30,14 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" -BASE="${BASE:-HEAD}" +# ⚠️ 基準**不能**用 HEAD——這是實測踩到的: +# 把 install.sh 改成清單驅動之後,HEAD 裡的硬編路徑從 35 條掉到 29 條, +# **閘保護的範圍就跟著縮水 6 條**——而那 6 條正是還沒更新的舊實例仍然會去抓的。 +# 用 HEAD 當基準,閘會隨著每次提交自我弱化,最後變成「只保護今天的自己」。 +# ⇒ 基準必須是「**外面實際在跑的那一版**」=最後一次真的發佈出去的 commit。 +# 發新版、且確認真的送出去之後,才把下面這行往前挪。 +LAST_RELEASED_DEFAULT="dc4fe67" # 1.18.0 +BASE="${BASE:-$LAST_RELEASED_DEFAULT}" # ── 取出基準版本的兩支腳本 ──────────────────────────── TMP="$(mktemp -d)" diff --git a/scripts/install.sh b/scripts/install.sh index b0cc158..daf9a6f 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -414,9 +414,18 @@ build_hooks_json() { fi # PreToolUse 依模組疊加。 - # 順序原則:**範圍大的擋在前**——這樣錯誤訊息會指向最根本的那條規則, + # 順序原則:**範圍大的擋在前**——錯誤訊息才會指向最根本的那條規則, # 而不是讓人先修一個表層問題、修完才發現底下還有一條。 + # ① 改機制 這個檔你根本不該動(最外層) + # ② 角色 你這個身分不該寫這種檔 + # ③ 位置 總管不該進成員 repo 動實作(僅總管實例) + # ④ 格式 檔可以寫,但寫進去的內容格式不對 + # ⑤ 既有的 SDD/自訂禁令/機敏掃描 local pt=() + pt+=('{ "type": "command", "command": ".claude/hooks/install-artifact-guard.sh" }') + pt+=('{ "type": "command", "command": ".claude/hooks/role-guard.sh" }') + [ "$PROFILE" = "orchestrator" ] && pt+=('{ "type": "command", "command": ".claude/hooks/orchestrator-scope-guard.sh" }') + pt+=('{ "type": "command", "command": ".claude/hooks/jdd-format-guard.sh" }') $WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }') pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }') $WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }') @@ -435,7 +444,11 @@ build_hooks_json() { if [ -n "$session_hooks" ]; then printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks" fi - printf ' "PreToolUse": [\n { "matcher": "Write|Edit",\n "hooks": [ %s ] }\n ]\n' "$pretool_hooks" + printf ' "PreToolUse": [\n { "matcher": "Write|Edit|MultiEdit",\n "hooks": [ %s ] }\n ],\n' "$pretool_hooks" + # 回歸考:動了實作之後才有意義 → PostToolUse(只提醒不擋) + printf ' "PostToolUse": [\n { "matcher": "Write|Edit|MultiEdit",\n "hooks": [ { "type": "command", "command": ".claude/hooks/regression-scope.sh" } ] }\n ],\n' + # 收工判準:站的考題全綠才算收,不是任務全關 → Stop + printf ' "Stop": [\n { "hooks": [ { "type": "command", "command": ".claude/hooks/station-done-guard.sh" } ] }\n ]\n' printf ' }\n}\n' } diff --git a/scripts/update.sh b/scripts/update.sh index eca5148..4d30ae8 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -298,67 +298,148 @@ echo "" # 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。 keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh" +# ── 舊版硬編清單(僅在抓不到 manifest 時使用的退路)────────── +# 保留它的唯一理由:來源不可達時,更新不該整個停擺。 +# ⚠️ 新增產物請改 template/manifest/*.tsv,**不要**再往這裡加—— +# 這份清單就是當初漂移的來源。 +legacy_update_list() { # ── 模板/邏輯檔:覆蓋更新 ────────────────────────── # 共用 hook 與指引 -update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md" -update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION" + update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md" + update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION" # Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project # 投影共用同一套;任一模組在用就補/更新(邏輯檔,可覆蓋)。 -if $HAS_WIKI || $HAS_SDD; then - update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md" + if $HAS_WIKI || $HAS_SDD; then + update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md" + fi + + if $HAS_WIKI; then + # wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/,hooks/commands 留 .claude/。 + update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md" + update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh" + update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh" + # 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG) + update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh" + update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh" + # 1.18.0:公開 mirror 落後偵測(手動同步的靜默失敗只有外部用戶會撞到) + update_file ".claude/hooks/publish-lag-check.sh" "$TEMPLATE_URL/.claude/hooks/publish-lag-check.sh" + update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md" + update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md" + update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md" + update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md" + # vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):邏輯檔,可覆蓋。舊版沒有 → 當新檔補。 + update_file ".claude/commands/wiki-extract.md" "$TEMPLATE_URL/.claude/commands/wiki-extract.md" + # Cowork(claude.ai)的 wiki 整理 skill:規則檔,可覆蓋 + update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md" + + # wiki 的「使用者資料」:絕不碰 + keep_file "system-dev/wiki/status.md" + keep_file "system-dev/wiki/mistakes.md" + # principles.md(1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留 + add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md" + keep_file "system-dev/wiki/decisions-summary.md" + keep_file "system-dev/wiki/TAXONOMY.md" + keep_file "system-dev/wiki/.wikiignore" + fi + + if $HAS_SDD; then + # SDD 範本與 hook:可覆蓋 + update_file "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md" + update_file "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" + update_file "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" + update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md" + update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh" + + # SDD 生命週期鐵律(1.14,issue #6):規則檔+獨立檢查腳本=邏輯檔可覆蓋; + # pending-changes.md 裝著用戶的 proposal=用戶資料,只補不覆蓋。 + # (issue #13 教訓:update 不補新檔會造成結構斷層,新檔必須在這裡鋪。) + update_file "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$TEMPLATE_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md" + add_if_missing "system-dev/docs/3-specs/pending-changes.md" "$TEMPLATE_URL/system-dev/docs/3-specs/pending-changes.md" + update_file "system-dev/scripts/sdd-active-check.sh" "$TEMPLATE_URL/scripts/sdd-active-check.sh" + + # tasks⇄Project 投影(issue #16):邏輯檔,可覆蓋。舊版沒有 → add_if_missing 補。 + # 啟用狀態存遠端(acr push),不在這些檔裡,覆蓋不會關掉誰的同步。 + add_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.yaml" + add_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.local.sh" + fi + +} + +# ── Manifest 驅動的更新(1.19.0:兩支腳本讀同一份清單)──────── +# 在這之前,這裡是一份**手抄的**檔案清單,install.sh 另有一份。兩份手抄必然漂移, +# 而且已經漂了:wiki-first-search / subagent-wiki-guard / publish-lag-check / +# decisions-summary 這四個,update 會處理但 install 從來不裝 +# ⇒ 乾淨安裝的人反而拿不到最新三版的招牌功能。 +# 現在兩支都讀 template/manifest/*.tsv,這類漂移在結構上不可能再發生。 +# +# 讀不到 manifest(來源太舊/網路問題)→ 退回舊的硬編清單(見下方 legacy_update_list), +# 不讓更新整個停擺。 + +MANIFEST_DIR="system-dev/.template-manifest.d" +mkdir -p "$MANIFEST_DIR" + +SDT_PROFILE="repo" +[ -f "system-dev/.profile" ] && SDT_PROFILE="$(tr -d '[:space:]' < system-dev/.profile 2>/dev/null || echo repo)" + +fetch_manifest() { # $1=名稱 + local out="$MANIFEST_DIR/$1.tsv" + if curl -sSL "$TEMPLATE_URL/manifest/$1.tsv" -o "$out" 2>/dev/null \ + && [ -s "$out" ] && ! looks_like_error_page "$out"; then + return 0 + fi + rm -f "$out"; return 1 +} + +resolve_src() { + case "$1" in + T:*) printf '%s/%s' "$TEMPLATE_URL" "${1#T:}" ;; + S:*) printf '%s/%s' "$REPO_RAW/scripts" "${1#S:}" ;; + *) printf '' ;; + esac +} + +want_module() { + case "$1" in + core) return 0 ;; + wiki) $HAS_WIKI && return 0 || return 1 ;; + sdd) $HAS_SDD && return 0 || return 1 ;; + *) return 1 ;; + esac +} + +apply_manifest() { # $1=manifest 檔 + local f="$1" src dest class module profile url + [ -f "$f" ] || return 0 + while IFS=$'\t' read -r src dest class module profile; do + case "$src" in ''|'#'*) continue ;; esac + [ -z "${dest:-}" ] && continue + want_module "$module" || continue + url="$(resolve_src "$src")" + case "$class" in + dir) mkdir -p "$dest" 2>/dev/null || true ;; + overwrite) [ -n "$url" ] && update_file "$dest" "$url" ;; + add-if-missing) [ -n "$url" ] && add_if_missing "$dest" "$url" ;; + keep-with-template) [ -n "$url" ] && keep_with_template "$dest" "$url" ;; + keep) keep_file "$dest" ;; + claude-md) CLAUDE_MD_SRC="$url" ;; # 憲法走界標補植,見下方專段 + *) : ;; + esac + done < "$f" +} + +CLAUDE_MD_SRC="" +USED_MANIFEST=false +if fetch_manifest common; then + USED_MANIFEST=true + fetch_manifest "$SDT_PROFILE" || true + apply_manifest "$MANIFEST_DIR/common.tsv" + apply_manifest "$MANIFEST_DIR/$SDT_PROFILE.tsv" +else + t "⚠️ 抓不到安裝清單,退回舊版硬編清單(功能較少但不會壞)" \ + "⚠️ Manifest unavailable; falling back to the legacy hardcoded list" + legacy_update_list fi - -if $HAS_WIKI; then - # wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/,hooks/commands 留 .claude/。 - update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md" - update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh" - update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh" - # 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG) - update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh" - update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh" - # 1.18.0:公開 mirror 落後偵測(手動同步的靜默失敗只有外部用戶會撞到) - update_file ".claude/hooks/publish-lag-check.sh" "$TEMPLATE_URL/.claude/hooks/publish-lag-check.sh" - update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md" - update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md" - update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md" - update_file ".claude/commands/wiki-recall.md" "$TEMPLATE_URL/.claude/commands/wiki-recall.md" - # vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):邏輯檔,可覆蓋。舊版沒有 → 當新檔補。 - update_file ".claude/commands/wiki-extract.md" "$TEMPLATE_URL/.claude/commands/wiki-extract.md" - # Cowork(claude.ai)的 wiki 整理 skill:規則檔,可覆蓋 - update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md" - - # wiki 的「使用者資料」:絕不碰 - keep_file "system-dev/wiki/status.md" - keep_file "system-dev/wiki/mistakes.md" - # principles.md(1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留 - add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md" - keep_file "system-dev/wiki/decisions-summary.md" - keep_file "system-dev/wiki/TAXONOMY.md" - keep_file "system-dev/wiki/.wikiignore" -fi - -if $HAS_SDD; then - # SDD 範本與 hook:可覆蓋 - update_file "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md" - update_file "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$TEMPLATE_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" - update_file "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$TEMPLATE_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" - update_file ".claude/commands/sdd-check.md" "$TEMPLATE_URL/.claude/commands/sdd-check.md" - update_file ".claude/hooks/sdd-guard.sh" "$TEMPLATE_URL/.claude/hooks/sdd-guard.sh" - - # SDD 生命週期鐵律(1.14,issue #6):規則檔+獨立檢查腳本=邏輯檔可覆蓋; - # pending-changes.md 裝著用戶的 proposal=用戶資料,只補不覆蓋。 - # (issue #13 教訓:update 不補新檔會造成結構斷層,新檔必須在這裡鋪。) - update_file "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$TEMPLATE_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md" - add_if_missing "system-dev/docs/3-specs/pending-changes.md" "$TEMPLATE_URL/system-dev/docs/3-specs/pending-changes.md" - update_file "system-dev/scripts/sdd-active-check.sh" "$TEMPLATE_URL/scripts/sdd-active-check.sh" - - # tasks⇄Project 投影(issue #16):邏輯檔,可覆蓋。舊版沒有 → add_if_missing 補。 - # 啟用狀態存遠端(acr push),不在這些檔裡,覆蓋不會關掉誰的同步。 - add_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.yaml" - add_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$TEMPLATE_URL/system-dev/workflows/tasks-project-sync.local.sh" -fi - # ── 自我更新:把最新的 update.sh / install.sh 抓到 system-dev/scripts/ ── # 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。 update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh" @@ -368,7 +449,47 @@ chmod +x .claude/hooks/*.sh system-dev/scripts/*.sh system-dev/workflows/*.sh 2> # ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」── keep_file ".claude/settings.json" -keep_file "CLAUDE.md" + +# ── CLAUDE.md 界標補植(1.19.0)──────────────────────── +# 1.18.x 以前的 CLAUDE.md 是「整份下載 + 往後 append」,**沒有任何區段界標**。 +# 沒界標 ⇒ update 分不出「哪段是框架的、哪段是你寫的」⇒ 只能永遠不敢碰 +# ⇒ 框架後來改的憲法**永遠送不到既有實例**。這一段就是來解這個死結。 +# +# 保守到底,寧可什麼都不做也不弄丟一個字: +# · 已有界標 → 什麼都不做(冪等) +# · 沒有界標 → 現有全文**原封不動**包進本地補充區,框架區重鋪在它上面 +# · 抓不到憲法範本 → 放棄補植、保留原檔(不留半殘的檔) +# 最壞情況是同一段內容在兩區各一份,由人自己刪——我們不代刪。 +if [ -f "CLAUDE.md" ]; then + if grep -q 'sdt:framework begin' CLAUDE.md 2>/dev/null; then + keep_file "CLAUDE.md" + elif [ -n "${CLAUDE_MD_SRC:-}" ]; then + FW_TMP="$(mktemp)" + if curl -sSL "$CLAUDE_MD_SRC" -o "$FW_TMP" 2>/dev/null && ! looks_like_error_page "$FW_TMP"; then + cp CLAUDE.md "CLAUDE.md.before-markers" + { + printf '\n' \ + "$SDT_PROFILE" "$REMOTE_VER" "$(sdt_sha256 "$FW_TMP" | cut -c1-12)" + cat "$FW_TMP" + printf '\n\n' + printf '\n' + printf '\n\n\n' + cat "CLAUDE.md.before-markers" + printf '\n\n' + } > CLAUDE.md + rm -f "$FW_TMP" + MIGRATED+=("CLAUDE.md → 補上界標(原檔備份 CLAUDE.md.before-markers)") + else + rm -f "$FW_TMP" + keep_file "CLAUDE.md" + fi + else + keep_file "CLAUDE.md" + fi +fi # ── 結果輸出 ─────────────────────────────────────── echo "" diff --git a/template/.claude/hooks/install-artifact-guard.sh b/template/.claude/hooks/install-artifact-guard.sh new file mode 100644 index 0000000..47a5328 --- /dev/null +++ b/template/.claude/hooks/install-artifact-guard.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# install-artifact-guard.sh — 實例不准改機制(分離規格 防糾纏閘 S1) +# +# 掛 PreToolUse(matcher: Write|Edit|MultiEdit)。**排在整條鏈的最前面**—— +# 範圍最大的規則先擋,錯誤訊息才會指向最根本的那條。 +# +# ── 擋什麼 ────────────────────────────────────── +# 寫入「安裝產物區」= 框架發下來的機制檔(hook、範本、安裝腳本、plugin 目錄)。 +# 判準來自 system-dev/.template-manifest:**凡是框架管的檔(class=overwrite)都算**。 +# 不另寫一份路徑表——兩處維護同一條規則,遲早不同步。 +# +# ── 為什麼要擋(實測數據,不是潔癖)───────────────── +# 拿一個真實使用中的實例對照框架:6 支框架 hook 裡 **4 支已被手改**, +# 另有 11 支是實例自己發明的,而**沒有任何機制知道這件事**。 +# 兩層後果: +# ① 框架之後修好的 bug,永遠送不到那些被手改的檔 +# ② 手改的內容沒人審、也不會回饋給框架 ⇒ 同一個坑每個實例各踩一次 +# 機制要改就回上游改,一次修全家;在自己家裡改,只有自己受惠、且下次更新就孤立。 +# +# ── 怎麼放行 ──────────────────────────────────── +# 框架 repo 自己開發時本來就要改這些檔 ⇒ 兩種放行方式: +# · repo 根目錄有 .sdt-framework-dev 檔(框架 repo 自帶並 commit,零記憶負擔) +# · 環境變數 SDT_FRAMEWORK_DEV=1(臨時情境) +# 注意:官方**沒有** --framework-dev 這個 CLI 參數(實查,非記憶),別去找。 +# +# 誠實限制:擋直接寫檔。bash 繞道改檔擋不到。留痕可審,不宣稱防偽。 + +set -uo pipefail + +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0 + +# ── 框架開發模式 → 放行 ──────────────────────────── +ROOT="$(sdt_repo_root)" +[ -f "$ROOT/.sdt-framework-dev" ] && exit 0 +[ "${SDT_FRAMEWORK_DEV:-}" = "1" ] && exit 0 + +INPUT="$(cat)" +FILE_PATH="$(sdt_file_path "$INPUT")" +[ -z "$FILE_PATH" ] && exit 0 +REL="$(sdt_rel_path "$FILE_PATH")" + +MANIFEST="$ROOT/system-dev/.template-manifest" + +is_managed_artifact() { + # ⓪ 使用者的自訂插槽 → 一律放行,**這一條要排在最前面** + # 它們存在的目的就是給實例填自己的規則,而且 update 不會覆蓋它們。 + # (踩過:本 hook 的錯誤訊息叫人「去改 pre-write-guard.sh」, + # 結果自己把那支也擋了——叫人走的路自己堵住,是最糟的一種閘。) + case "$1" in + .claude/hooks/pre-write-guard.sh) return 1 ;; + *.template.sh|*.template.md) return 1 ;; + .claude/settings.json) return 1 ;; + esac + # ① manifest 是權威:它說這是框架管的邏輯檔(class=overwrite)才擋 + if [ -f "$MANIFEST" ]; then + if awk -F'\t' -v d="$1" '!/^#/ && $1==d && $2=="overwrite" {found=1} END{exit !found}' "$MANIFEST"; then + return 0 + fi + fi + # ② manifest 還沒有/這個檔還沒登記(舊實例、或框架剛加的新檔) + # → 退回路徑慣例,別因為沒表就整條規則失效 + case "$1" in + .claude/hooks/*|.claude/plugins/*) return 0 ;; + system-dev/scripts/*) return 0 ;; + esac + return 1 +} + +if is_managed_artifact "$REL"; then + cat >&2 < 引言、註解、標題)——不夠,因為自述段是普通條列。 +# 改用**正面圈定**:只掃真正的內容體 +# · root.md = `- **P**` 卡片行 + 它底下的縮排子項 +# · journeys.md = `#### S` 站標題以下、到下一個標題之前的內文 +# 自述段、說明區、索引表因為不在這兩種範圍裡,自然就不會被掃到—— +# 不必為它們一個個開例外。 +# +# 誠實限制:只認字面與行首形狀。 +# 「把技術概念用白話包裝起來」它看不出來(那要人讀); +# 用 bash 繞道改檔也擋不到。價值是擋掉明顯的格式錯誤與手滑,不是技術防偽。 + +set -uo pipefail + +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$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")" + +CONTENT="$(sdt_write_content "$INPUT")" +[ -z "$CONTENT" ] && exit 0 + +TECH_TERMS='API|SDK|CLI|MCP|WASM|endpoint|schema|webhook|resturl|JSON|YAML|SQL|資料庫|後端|前端|部署|repo|commit|branch' + +# 正面圈定要掃的行(見上方說明): +# root.md → `- **P**` 卡片行 + 其縮排子項 +# journeys.md → `#### S` 站標題以下到下一個標題之前的內文 +card_body_only() { # $1=root|journeys + if [ "$1" = "root" ]; then + awk ' + //) inc=0; next } + /^[[:space:]]*-[[:space:]]*\*\*P[0-9]+\*\*/ { incard=1; print NR "\t" $0; next } + incard && /^[[:space:]]+[-*]/ { print NR "\t" $0; next } # 卡片的縮排子項 + { incard=0 } + ' + else + awk ' + //) inc=0; next } + /^####[[:space:]]*S[0-9]+/ { instation=1; next } # 進入某一站 + /^#/ { instation=0; next } # 任何標題結束該站 + instation && /^[[:space:]]*>/ { next } # 站內引言仍不掃 + instation && /^[[:space:]]*$/ { next } + instation { print NR "\t" $0 } + ' + fi +} + +fail() { # $1=規則 $2=標題 $3=細節 + cat >&2 <。 + - 【要驗證:<能用真實數字或事實判真假的判準> | 對帳日 YYYY-MM-DD】 + + 為什麼擋:紅卡是賭注。沒有判準和結算日的賭注,永遠不必認賠—— + 那不是賭注,是把「還沒做到」講得像「正在做」。" + fi +fi + +# ── J8:root.md/journeys.md 不准出現技術名詞 ──────── +case "$REL" in + *root.md|*journeys.md) + KIND="journeys"; printf '%s' "$REL" | grep -q 'root\.md$' && KIND="root" + HITS="$(printf '%s' "$CONTENT" | card_body_only "$KIND" | grep -inE "$TECH_TERMS" | head -8 || true)" + if [ -n "$HITS" ]; then + fail "J8" "PM 軌文件出現技術名詞。" \ +" 命中(只掃卡片本體,已排除說明區/註解/標題): +$(printf '%s' "$HITS" | cut -c1-120 | sed 's/^/ /') + + 怎麼修: + · 把它翻成「使用者感覺得到的事」——例如不是「呼叫 API 取得資料」, + 而是「我按下去之後,畫面上出現我的東西」 + · 真的必須談技術 → 那句話屬於各專案自己的規格,不屬於這裡 + + 為什麼擋:這兩份文件唯一的讀者是「不懂技術但要點頭或搖頭的人」。 + 出現一個他看不懂的詞,他就沒辦法判斷這張卡是不是他的意思。" + fi + ;; +esac + +# ── J5:tasks.md 新增的任務必須掛站號 ───────────────── +if printf '%s' "$REL" | grep -q 'tasks\.md$'; then + # 只看這次要寫入的內容裡「長得像新任務」的行 + NEWTASKS="$(printf '%s' "$CONTENT" | grep -nE '^[[:space:]]*-[[:space:]]*\[[ x~!🔄]\][[:space:]]*[0-9]+\.[0-9]+' || true)" + if [ -n "$NEWTASKS" ]; then + # 站號標注:任務行本身或緊接的子項出現 S / S- / 「服務:S…」 + NOSTATION="$(printf '%s' "$CONTENT" | awk ' + /^[[:space:]]*-[[:space:]]*\[[ x~!🔄]\][[:space:]]*[0-9]+\.[0-9]+/ { + if (pending != "" && !found) print pending + pending = NR "\t" $0; found = 0 + if ($0 ~ /S-?[0-9]+/) found = 1 + next + } + pending != "" && /S-?[0-9]+/ { found = 1 } + /^[[:space:]]*$/ { if (pending != "" && !found) { print pending; pending=""; found=0 } } + END { if (pending != "" && !found) print pending } + ')" + if [ -n "$NOSTATION" ]; then + fail "J5" "新增的任務沒有標注它服務哪一站。" \ +" 下列任務缺站號: +$(printf '%s' "$NOSTATION" | cut -c1-120 | sed 's/^/ 行 /') + + 補成這樣: + - [ ] 1.1 <任務描述> + - 服務:S3(我按一次就裝到自己的地方) + - 驗收:<客觀可驗證的完成標準> + + 為什麼擋:sprint 的單位是站。任務不掛站,就沒有人答得出認領流程唯一的問題—— + 「這個任務不做,指定站的考題會掛嗎?」答不出來,這個任務就沒有理由在這一期做。" + fi + fi +fi + +exit 0 diff --git a/template/.claude/hooks/regression-scope.sh b/template/.claude/hooks/regression-scope.sh new file mode 100644 index 0000000..edaa203 --- /dev/null +++ b/template/.claude/hooks/regression-scope.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# regression-scope.sh — 動到某一站的東西 → 告訴你要重考哪些題(JDD 規則 J7) +# +# 掛 PostToolUse(Write|Edit|MultiEdit)。**只提醒,不阻擋**(exit 0)。 +# +# ── 為什麼只提醒 ──────────────────────────────── +# 重考範圍是判斷題不是是非題——擋下來只會讓人為了繼續工作而亂填。 +# 它要消滅的失敗模式只有一個:**改完東西,忘了它會弄壞哪幾站**。 +# (「S3 安裝流程變了 → 後面 S4~S9 全部要重考」這種事, +# 不查索引表沒有人會自己想到。) +# +# ── 判斷「動到哪一站」怎麼做 ───────────────────── +# 靠 journeys.md 的**站點索引**。索引裡若有登記關聯路徑就用它; +# 沒有登記時退回一個誠實的作法:只要動到 code,就把索引整份摘要推一次, +# 讓人自己對——**不猜**。猜錯的重考範圍比沒有更危險(會讓人以為已經涵蓋了)。 + +set -uo pipefail + +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$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")" + +# 只對「實作變動」出聲;改文件不必每次都喊 +case "$REL" in + src/*|*/src/*|*.py|*.ts|*.tsx|*.js|*.go|*.rs|*.sh|*.java|*.rb|*.vue|*.svelte) ;; + *) exit 0 ;; +esac + +ROOT="$(sdt_repo_root)" +JOURNEYS="" +for cand in "$ROOT/system-dev/docs/journeys.md" "$ROOT/../system-dev/docs/journeys.md"; do + [ -f "$cand" ] && JOURNEYS="$cand" && break +done +[ -z "$JOURNEYS" ] && exit 0 # 沒有 PM 軌文件(多數成員 repo)→ 安靜 + +# 取站點索引段(「附:站點索引」以下) +INDEX="$(awk '/^##[[:space:]]*附.*站點索引/{f=1} f' "$JOURNEYS" 2>/dev/null | head -60)" +[ -z "$INDEX" ] && exit 0 + +echo "════════════════════════════════════════════════" +echo "🔁 回歸考提醒(J7):你動了實作,查一下要重考哪幾題" +echo "════════════════════════════════════════════════" +echo " 剛動的:$REL" +echo "" +printf '%s\n' "$INDEX" | sed 's/^/ /' +echo "" +echo " ⚠️ 這份索引是「站 → 被哪些旅程經過 → 改動時重考什麼」。" +echo " 對照你剛改的東西屬於哪一站,把**下游**也一起重考——" +echo " 安裝那類的站一變,後面全部都要重來。" +exit 0 diff --git a/template/.claude/hooks/role-guard.sh b/template/.claude/hooks/role-guard.sh new file mode 100644 index 0000000..ff3a240 --- /dev/null +++ b/template/.claude/hooks/role-guard.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# role-guard.sh — 角色封路:誰能寫什麼(JDD 規則 J1+J2+J3) +# +# 掛 PreToolUse(matcher: 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 </dev/null \ + | grep -vE '^## 認領流程' | head -25 + echo "" + echo "📌 回報形式:「J-x 已點亮 n/m 站」。狀態只有 ✅ 通(附實測證據)/◐ 半通/❌ 斷。" + echo " 「程式碼寫完了」不是狀態。" + echo "" + if [ -f "system-dev/docs/root.md" ]; then + DUE="$(grep -oE '對帳日[^|】]*' system-dev/docs/root.md 2>/dev/null | head -3)" + if [ -n "$DUE" ]; then + echo "🔴 紅卡對帳日(到期要拿真實數據判決:承諾成立,或換一個承諾):" + printf '%s\n' "$DUE" | sed 's/^/ /' + echo "" + fi + fi +fi + # 三個 push 檔都沒有 → 安靜退出,不干擾還沒 /wiki-init 的專案 if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then exit 0 diff --git a/template/.claude/hooks/station-done-guard.sh b/template/.claude/hooks/station-done-guard.sh new file mode 100644 index 0000000..a4c14eb --- /dev/null +++ b/template/.claude/hooks/station-done-guard.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# station-done-guard.sh — 收工判準是「站的考題全綠」,不是「任務全關」(JDD 規則 J6) +# +# 掛 Stop / SubagentStop(收工那一刻),只在 orchestrator 實例生效。 +# +# ── 它在攔什麼 ────────────────────────────────── +# 「任務都關了 ⇒ 這期做完了」——這句話是整套假綠的源頭。 +# 任務是**技術軌**的單位(沿系統結構切),站是**PM 軌**的單位(沿人的經歷切)。 +# 零件全部做完、每個都對,人還是可能掉進零件之間的縫裡。 +# 所以收工只認一件事:**指定站的考題有沒有實測通過**。 +# +# ── 它不做什麼(重要)──────────────────────────── +# 它**不會**自己去跑考題判斷過沒過——Gherkin 的 Then 寫的是「使用者看到什麼」, +# 那本來就不是 shell 判得出來的。它做的是:在你要收工的那一刻, +# 把「指定站」和「它們現在的狀態」攤在你眼前,逼你面對還沒填的那幾格。 +# 自動判綠反而危險:那會製造一個「機器說過了」的假權威。 +# +# 誠實限制:讀的是 sprint.md/journeys.md 的文字狀態。 +# 有人亂填「✅」它看不出來——它擋的是「忘了填」與「用任務數充當進度」,不是造假。 + +set -uo pipefail + +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0 + +[ "$(sdt_scope)" = "orchestrator" ] || exit 0 + +ROOT="$(sdt_repo_root)" +SPRINT="$ROOT/system-dev/docs/sprint.md" +[ -f "$SPRINT" ] || exit 0 # 還沒開始用站號 sprint → 不干擾 + +# 本期指定的站 +STATIONS="$(grep -oE 'S-?[0-9]+' "$SPRINT" 2>/dev/null | sort -u | tr '\n' ' ')" +[ -z "$STATIONS" ] && exit 0 + +# 還沒亮的站(sprint.md 的「❌ 未亮」「◐ 半通」段) +UNLIT="$(awk '/未亮|◐|半通/ {print}' "$SPRINT" 2>/dev/null | grep -oE 'S-?[0-9]+' | sort -u | tr '\n' ' ')" + +echo "════════════════════════════════════════════════" +echo "🚦 收工判準:站的考題全綠,不是任務全關(J6)" +echo "════════════════════════════════════════════════" +echo " 本期指定站:${STATIONS}" +if [ -n "$UNLIT" ]; then + echo " ⚠️ 還沒亮:${UNLIT}" + echo "" + echo " 這幾站沒亮,這期就還沒收。收工前擇一:" + echo " · 真的通了 → 在 journeys.md 標記點亮,**附實測輸出**(不是「應該會過」)" + echo " · 半通 → 標 ◐ 並寫明缺什麼" + echo " · 沒通 → 標 ❌ 誠實留著,搬到下一期並寫一行為什麼" + echo "" + echo " ⛔ 不准用「任務都關了」當收工理由——那是技術軌的單位,不是驗收線。" +else + echo " ✅ 本期指定站都已標記點亮。記得每一站都要附得出實測證據。" +fi +echo "" +exit 0 diff --git a/template/manifest/common.tsv b/template/manifest/common.tsv index 94f7828..b76da78 100644 --- a/template/manifest/common.tsv +++ b/template/manifest/common.tsv @@ -52,6 +52,17 @@ T:.claude/hooks/pre-write-guard.sh .claude/hooks/pre-write-guard.sh keep-with-te T:.claude/hooks/lib/role-lib.sh .claude/hooks/lib/role-lib.sh overwrite core common T:.claude/hooks/publish-lag-check.sh .claude/hooks/publish-lag-check.sh overwrite core common +# ── JDD 封路 hook(角色軸屬 common,兩個 profile 都裝)── +# 為什麼 role-guard 不按 profile 分裝(設計決策 D5): +# 總管 repo 裡也會派 engineer subagent(身分矩陣右上角那一格)。 +# 若「考生不能改考卷」只裝在成員 repo,那顆 subagent 在總管 repo 裡就改得動考卷 +# ——而總管 repo 正是考卷所在地,等於這條規則在最該生效的地方失效。 +T:.claude/hooks/install-artifact-guard.sh .claude/hooks/install-artifact-guard.sh overwrite core common +T:.claude/hooks/role-guard.sh .claude/hooks/role-guard.sh overwrite core common +T:.claude/hooks/jdd-format-guard.sh .claude/hooks/jdd-format-guard.sh overwrite core common +T:.claude/hooks/station-done-guard.sh .claude/hooks/station-done-guard.sh overwrite core common +T:.claude/hooks/regression-scope.sh .claude/hooks/regression-scope.sh overwrite core common + # ── wiki 模組 ── - system-dev/wiki dir wiki common - system-dev/wiki/cards dir wiki common diff --git a/template/manifest/orchestrator.tsv b/template/manifest/orchestrator.tsv index 7635f48..338459f 100644 --- a/template/manifest/orchestrator.tsv +++ b/template/manifest/orchestrator.tsv @@ -4,13 +4,19 @@ # root.md + journeys.md(PM 軌),裡面的 agent 預設身分是 orchestrator # (可寫 root/journeys/sprint,**禁寫 code、禁改 tasks/requirements/design**)。 # -# ⚠️ 本表目前只有憲法一項。Phase 2(JDD 文件範本:root.md / journeys.md / sprint / -# triage-map / plugin-load-order)與 Phase 3(orchestrator-scope-guard.sh) -# 的產物**尚未開工**,總管指定 Phase 1 做完先停下驗 Gherkin。屆時往這張表加行即可, -# 不必再改 install.sh/update.sh —— 這正是 manifest 的用意(加產物=加一行資料)。 +# ⚠️ 四份 JDD 文件的 class 是 add-if-missing,這是刻意的: +# 給你一份可以直接動手填的骨架,但**一旦你填了內容,update 永遠不會覆蓋**。 +# 理由:這幾份是**實例資料**不是框架邏輯——裡面寫的是你的需求、你的旅程、你的站。 +# 來源檔名帶 .template 後綴、落地時去掉:來源是範本,落地就是你的文件了。 # # 欄位定義見 common.tsv。 # #src dest class module profile T:profiles/orchestrator/CLAUDE.md CLAUDE.md claude-md core orchestrator +T:profiles/orchestrator/docs/root.md.template system-dev/docs/root.md add-if-missing core orchestrator +T:profiles/orchestrator/docs/journeys.md.template system-dev/docs/journeys.md add-if-missing core orchestrator +T:profiles/orchestrator/docs/sprint.md.template system-dev/docs/sprint.md add-if-missing core orchestrator +T:profiles/orchestrator/docs/triage-map.md.template system-dev/docs/triage-map.md add-if-missing core orchestrator +T:profiles/orchestrator/docs/plugin-load-order.md system-dev/docs/plugin-load-order.md overwrite core orchestrator +T:profiles/orchestrator/hooks/orchestrator-scope-guard.sh .claude/hooks/orchestrator-scope-guard.sh overwrite core orchestrator diff --git a/template/profiles/orchestrator/docs/journeys.md.template b/template/profiles/orchestrator/docs/journeys.md.template new file mode 100644 index 0000000..4ed7e19 --- /dev/null +++ b/template/profiles/orchestrator/docs/journeys.md.template @@ -0,0 +1,128 @@ +# journeys.md — PM 軌驗收([專案/組織名]) + +> **根**:[root.md](root.md)(J-1 對應根卡 **P?**)。本文件**只從人的角度寫**,禁止出現系統/模組名詞。 +> **誰能寫**:只有總管(orchestrator)。engineer/subagent **禁改本檔與任何考題**——考生不能改考卷。 +> **標記**:🟢 考過即關 / 🔴 考過轉「對帳中」(站上附真實世界判準與對帳日) +> **站全域編號**,跨 Journey 共享;同一站在別條旅程重複出現只寫引用,不重抄。 +> **考題(Gherkin)的 Then 只准寫「使用者看得到/感覺到什麼」**——「回傳 200」「部署成功」一律不准入題。 +> +> 立卷 [YYYY-MM-DD]。取代「數任務完成幾條」當進度語言: +> 從今天起回報形式是「**J-x 已點亮 n/m 站**」。 + +--- + +## A1 [角色名] + +- 一句話描述:**[這個角色是誰、他想幹嘛。用他自己會講的話寫,不要用你的話。]** +- 他不想知道我們內部長什麼樣。判準: + - 每一站都要再問一次——「**用戶需不需要為了過這關,去理解一個屬於我們內部的概念?**」 + - 需要 ⇒ 這站沒過。就算對象是工程師也一樣。 + +### J-1 [旅程名:用第一人稱寫這個角色的一條情境] + +- 這條旅程的頭尾:**[起點] → [中間] → [終點]**。 +- **驗收只認頭尾**。中間任何一環「做完了」都不算通關。 + +#### S1 [站名:用戶拿到什麼] 🟢 + +- [一句話解釋這站在幹嘛。] + +- **G-1.1** + - Given [我是誰/什麼狀況] + - When [我做了什麼] + - Then [我看到/感覺到什麼] + +#### S2 [站名] 🟢 + +- [一句解釋。] + +- **G-2.1** + - Given [...] + - When [...] + - Then [...] + +- **G-2.2** + - Given [邊界情況——想一個「安靜地什麼都沒發生」的可能] + - When [...] + - Then [...]——**不准安靜地什麼都沒發生** + +#### S3 [站名] 🔴 + +- 【對帳:**[真實世界的判準,要能用數字或事實判真假]**——[低於多少就代表這個設計錯了,要改成什麼]|對帳日 [YYYY-MM-DD]】 + +- **G-3.1** + - Given [...] + - When [...] + - Then [...] + +### J-2 [第二條旅程] + +> 站全域共享:同一站重複出現只寫引用,不重抄。 + +#### S1 →(引用,見 J-1) +#### S7 [這條旅程才有的新站] 🟢 + +- **G-7.1** + - Given [...] + - When [...] + - Then [...] + +--- + +## 附:站點索引(回歸考觸發表) + +> 用途:某一站相關的東西被改動時,查這裡就知道**要重考哪些題**。 +> ⚠️ 用巢狀 bullet,**不要用表格**——表格會把層級壓平,讀的人看不出「站 → 被誰經過 → 重考什麼」的從屬關係。 + +- **S1** [站名] + - 被經過:J-1 + - 改動時重考:G-1.1 +- **S2** [站名] + - 被經過:J-1、J-2 + - 改動時重考:G-2.1、G-2.2 +- **S3** [站名] + - 被經過:J-1 + - 改動時重考:G-3.1 + 下游 S4~S9(這站變了,後面全部要重考) + +--- + +## 這卷還缺什麼(誠實記,別假裝完備) + +> 這一段是**防假綠的裝置**,不是免責聲明。刪掉它,這卷看起來就會比實際完整。 + +- **每一站現在點亮了沒有,刻意留白**。理由:站的狀態要靠實測填, + 不是靠對著舊文件推測——那正是「假綠」的來源。第一次點亮由 sprint 收尾時實考填入。 +- **目前只有 J-1**。第二條旅程等 J-1 通關再立——規矩是「**需要新站才提案新站**」, + 不是先把表格畫滿。 +- [其他你知道還缺、但這一版先不做的東西。寫出來,別讓下一個人以為這卷是完整的。] + +--- + + diff --git a/template/profiles/orchestrator/docs/plugin-load-order.md b/template/profiles/orchestrator/docs/plugin-load-order.md new file mode 100644 index 0000000..27b3f03 --- /dev/null +++ b/template/profiles/orchestrator/docs/plugin-load-order.md @@ -0,0 +1,58 @@ +# 政策包(policy pack)= 官方 plugin,以及它什麼時候被載入 + +> 這份是**約定**,不是實作。它回答一件事:機制是怎麼在 agent 醒來之前就已經在場的。 + +--- + +## 鐵律:政策包一律做成 Claude Code 官方 plugin + +- 框架**不發明平行的外掛格式**。官方 plugin 已經涵蓋 hooks/skills/agents/commands/ + MCP server 的打包、安裝與版本分發——自造一套等於重寫一個市場上活得好好的輪子。 +- 要寫政策包的人,**動手前先讀官方文件**,照現行 schema 實作, + 不要憑記憶寫設定檔格式。官方會改,記憶不會跟著改。 + +--- + +## 三層是什麼(別把層搞混,混了就回不去) + +- **框架(L1)**:方法論——雙軌、wiki、封路、階梯、儀表 + - 判準:**換一家公司照樣成立** + - 住這裡:本 template +- **政策包(L2)**:一家之言的技術棧政策 + - 判準:**換一家公司就不成立** + - 住這裡:獨立的 plugin repo +- **實例(L3)**:資料——卡片內容、wiki、sprint、指標 + - 判準:**換一個 repo 就不成立** + - 住這裡:各實例自己 + +> 框架範本裡出現具體專案名 = 格式錯誤(有 CI 擋)。政策包不受此限——專名是它的內容。 + +--- + +## 載入順序(agent 醒來時世界已就位) + +1. **profile 憲法**(scope 軸:這個資料夾的 CLAUDE.md——總管版或成員版) +2. **框架 hooks 上鏈**(common + profile) +3. **已安裝的政策包注入**——必讀推到眼前、白名單 hook 排入鏈尾、skill 就緒 +4. **接關**(wiki 快照) + +- 執行期的總管/engineer **不需要知道** plugin 是官方機制還是框架機制—— + hooks 在鏈上就會攔、skill 在庫裡就會觸發。 +- **機制不靠認知,靠結構在場。** 唯一需要懂 plugin 規格的角色,是「改框架/寫政策包的人」。 + +--- + +## 框架這一側只做兩件事 + +1. 文件化上面這個約定與順序(就是這份檔) +2. 在兩部憲法裡留 **must-read 注入點**——政策包的 SessionStart hook 有地方把必讀推進來 + +注入點長這樣(兩部憲法末尾都有): + +```markdown +## 政策包必讀(policy pack must-read) + +``` + +- 沒裝政策包時它是空的,不影響任何事。 +- 框架**不需要為此新造機制**——官方 hook 就做得到。 diff --git a/template/profiles/orchestrator/docs/root.md.template b/template/profiles/orchestrator/docs/root.md.template new file mode 100644 index 0000000..6764ed6 --- /dev/null +++ b/template/profiles/orchestrator/docs/root.md.template @@ -0,0 +1,76 @@ +# root.md — 我們到底在做什麼(需求根文件) + +> 這是**兩軌共享的唯一錨點**:PM 軌([journeys.md](journeys.md))和技術軌(各專案自己的規格)都從這裡長出來。 +> **規矩**:一張卡一句白話 + 來源標記 + 紅綠燈。**禁止出現任何技術名詞**——技術是手段,寫進各專案自己的規格,不寫進根。 +> **紅綠怎麼判**:能指著市場上活得好好的先例說「照這個做」=🟢;不能、解法是猜的=🔴。**紅卡必附對帳判準與對帳日。** +> **誰能寫**:只有總管。**誰來勾**:出資/決策的那個人——卡片真偽是他的第一道閘,這份文件的價值全在他點頭或搖頭。 +> +> 立卷 [YYYY-MM-DD]。素材=[列出你是從哪些文件/對話讀出這些卡的]。 + +--- + +## 一、[分節標題:例如「關於我怎麼工作」] + +> 分節是為了讓人一眼看出「這幾張是同一類的事」。沒把握就先不分,卡多了再分。 + +- **P1** 🟢 [一句白話。不懂技術的人讀了能點頭或搖頭。] + - ——(你說的:[出處]) + +- **P2** 🔴 [一句白話。] + - 【要驗證:[一句話判準,要能用真實世界的數字或事實判真假] | 對帳日 [YYYY-MM-DD]】 + +- **P3** 🟢 [一句白話。] + - ——(你舉的例子:[出處]) + +## 二、[分節標題:例如「關於我在賣什麼」] + +- **P4** 🔴 [一句白話。] + - 【要驗證:[判準] | 對帳日 [YYYY-MM-DD]】 + +- **P5** 🟢 [一句白話。] + - ——(從你的抱怨反推:[出處]) + +## 三、[分節標題:例如「關於產品要給誰」] + +- **P6** 🟢 [一句白話。] + - ——(我猜的,請確認) + - 這張卡就是 [journeys.md](journeys.md) 的 **J-1**。 ← 卡片對應到某條旅程時這樣標 + +--- + +## 這份文件現在的狀態 + +> 這一段不是客套,是**交接資訊**。刪掉它,讀的人就不知道這份文件可信到什麼程度。 + +- **全部 N 張卡都還沒被勾過**(或:已勾 N/M)。紅綠燈是總管依「有沒有市場先例」判的,來源標記寫的是從哪裡讀到的。 +- **請你做的只有一件事**:掃過去,看有沒有哪張卡「不是我的意思」或「這根本不重要」。搖頭的拿掉,你補的加上。 +- ⚠️ 標明有沒有「我猜的」卡。凡是 PM/AI 推測補完、而非本人明示的內容,**必須帶「請確認」類標記**,不得混充原意。 +- 紅卡的對帳日到了,會拿真實數據來對帳:**承諾成立,或換一個承諾**。賭錯不丟臉,賭了不認才是。 + +--- + + 註解——否則你在解釋規矩時提到的詞會被自己抓到。 + +【紅綠怎麼判】 + 能指著市場上活得好好的先例說「照這個做」=🟢(輪子卡,考過即關) + 不能、解法是猜的=🔴(賭注卡,考過轉「對帳中」,等對帳日拿真實數據判決) + +【誰能寫】 + 只有 orchestrator。engineer/subagent 寫這個檔會被 hook 擋下——考生不能改考卷。 +════════════════════════════════════════════════════════ --> diff --git a/template/profiles/orchestrator/docs/sprint.md.template b/template/profiles/orchestrator/docs/sprint.md.template new file mode 100644 index 0000000..cfc2161 --- /dev/null +++ b/template/profiles/orchestrator/docs/sprint.md.template @@ -0,0 +1,61 @@ +# sprint.md — 本期要點亮哪幾站 + +> **單位是站號,不是一批任務。** 這份檔的存在理由:讓自動起牀的排程/新開的 session +> 一睜眼就有明確的「離通關還缺什麼」,而不是讀到一堆過期任務只能空轉收工。 +> **誰能寫**:只有總管(orchestrator)。 + +--- + +## 本期 + +- **期間**:[YYYY-MM-DD] → [YYYY-MM-DD] +- **交付**:[J-x] 的 **[S-a]、[S-b]** +- **考題**:G-a.1、G-a.2、G-b.1 +- **這期不做**:[明寫哪些站這期不碰。沒寫,別人就會自己加戲。] + +### 現在幾站亮了 + +- [J-x] 已點亮 **n/m** 站 + - ✅ 已亮:[S-?]([實測證據在哪]) + - ◐ 半通:[S-?]([缺什麼]) + - ❌ 未亮:[S-?]、[S-?] + +--- + +## 認領流程(順序鐵律,不准跳) + +> **先認領 → 認領不足才新增 → 新增必掛站。** 這個順序是為了防止重造一份任務清單。 + +1. **先認領**:對現有任務池**逐項**問—— + 「這個任務不做,[S-a]/[S-b] 的考題會掛嗎?」 + - 會掛 → 認領進本期 + - 不會 → **留在池子裡,這期不准碰** +2. **認領完考題還是過不了** → 池子真的缺東西 → **此時才准新增任務** + - 新任務**必須標注它服務哪一站**(無站號=格式錯誤,hook 會擋) +3. **收尾判準**=指定站的考題**全綠**,不是任務全關 + - 🟢 站 → 點亮、關閉 + - 🔴 站 → 點亮 + 轉「對帳中」,等 root.md 上的對帳日 + +--- + +## 認領清單 + +- [S-a] [站名] + - 認領:[任務編號/描述](在哪個 repo) + - 認領:[...] + - 新增:[...] ← 標明是新增的,以及為什麼池子裡沒有 +- [S-b] [站名] + - 認領:[...] + +--- + +## 到期結算(換檔機制) + +> 期間一到就**強制結算**,不准無聲延期——過期的 sprint 檔正是「起牀讀到殘骸」的來源。 + +- 到期日 [YYYY-MM-DD] 當天做三件事: + - 亮了的站 → 在 journeys.md 標記,附**實測證據** + - 沒亮的站 → **搬移**到下一期,並寫一行「為什麼沒亮」 + - 沒人認領也沒亮的任務 → **作廢或退回池子**,不准留在這裡假裝還活著 +- 狀態只有三種:**✅ 通(附實測證據)/◐ 半通(標明缺什麼)/❌ 斷** + - 「程式碼寫完了」不是狀態 diff --git a/template/profiles/orchestrator/docs/triage-map.md.template b/template/profiles/orchestrator/docs/triage-map.md.template new file mode 100644 index 0000000..c419a44 --- /dev/null +++ b/template/profiles/orchestrator/docs/triage-map.md.template @@ -0,0 +1,34 @@ +# triage-map.md — 能力域 → 誰做 + +> 新需求進來時查這張表:這件事該落在哪個成員 repo。 +> **表上沒有 ⇒ 不准自己開新 repo**,走人閘提案。 +> **誰能寫**:只有總管(orchestrator)。 + +--- + +## 對照 + +- **[能力域名稱]** + - 落在:`[成員 repo 路徑]` + - 邊界:[什麼算它的、什麼不算——寫清楚才不會兩個 repo 搶同一件事或互推] +- **[能力域名稱]** + - 落在:`[成員 repo 路徑]` + - 邊界:[...] + +--- + +## 分診的第二問(別漏) + +查完「誰做」,還要問**這個需求動到哪條旅程的哪些站**: + +- 動到既有的站 → 查 journeys.md 的**站點索引**,決定重考範圍 +- 需要新的站 → 走 journeys.md 提案(只有總管能寫) +- 連角色都是新的 → **先寫一篇敘事故事**給人驗完整性,再立新旅程 + +--- + +## 表上沒有的怎麼辦 + +- 先確認真的沒有——多數「新能力」其實是既有能力域的延伸,硬開新 repo 只是把邊界問題往後推。 +- 真的沒有 → 提案開新 repo,**這是人閘**(動到大家共用的結構)。 +- 提案要寫:這個能力域的邊界在哪、為什麼塞不進既有任何一個、誰維護。 diff --git a/template/profiles/orchestrator/hooks/orchestrator-scope-guard.sh b/template/profiles/orchestrator/hooks/orchestrator-scope-guard.sh new file mode 100644 index 0000000..92a13e6 --- /dev/null +++ b/template/profiles/orchestrator/hooks/orchestrator-scope-guard.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# orchestrator-scope-guard.sh — 總管不進成員 repo 動實作(分離規格 防糾纏閘 S4) +# +# 只裝在 orchestrator profile。掛 PreToolUse(Write|Edit|MultiEdit)。 +# +# ── 與 role-guard 的分工(刻意分兩支,不重疊)──────────── +# role-guard 管「這個**角色**能寫什麼**類型**的檔」(code?考卷?任務池?) +# orchestrator-scope-guard 管「總管能不能進這個**位置**」(成員 repo 的地盤) +# 兩件事正交:總管在自己家寫 md 沒問題,進成員 repo 寫 md 也沒問題(那是交辦), +# 但進成員 repo 寫實作檔就是越界——那是該 repo 的 engineer 的事。 +# +# ── 為什麼「只擋非 .md」────────────────────────── +# .md 放行是刻意的:總管要能在成員 repo 裡留交辦文件、筆記、規格討論。 +# 擋的是「總管自己下去改人家的實作」——那會讓該 repo 的 CC 完全不知道發生什麼事, +# 而且繞過了該 repo 自己的規格與 wiki 紀律。 +# +# ── 自動派工放行 ──────────────────────────────── +# 當「總管派出去的 subagent 已經戴著該 repo 的人格」時,它寫該 repo 的 code 是合理的 +# (心智已經在那個 repo 裡,context 是隔離的)。雙重夾: +# ① 環境變數 CLAUDE_CODE_CHILD_SESSION=1(subagent 標籤;總管主 session 沒有) +# ② 路徑在下方白名單內 +# ⚠️ 隔離保證是 prompt reset(軟的,靠監測),不是結構級。要絕對純淨走獨立 session。 +# +# 誠實限制:擋的是路徑語法層。「把實作偽裝成 .md」或「該交辦卻判斷成可直改」 +# 這種語意層越界擋不到。它是底線,不是萬能。 + +set -uo pipefail + +HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$HOOK_DIR/lib/role-lib.sh" 2>/dev/null || exit 0 + +# 只在總管實例生效(裝錯地方就安靜退出) +[ "$(sdt_scope)" = "orchestrator" ] || exit 0 + +INPUT="$(cat)" +FILE_PATH="$(sdt_file_path "$INPUT")" +[ -z "$FILE_PATH" ] && exit 0 + +ROOT="$(sdt_repo_root)" +REL="$(sdt_rel_path "$FILE_PATH")" + +# ── 成員 repo 目錄怎麼認 ───────────────────────── +# 不寫死任何專案名(框架不含實例資料)。兩種來源: +# ① 實例自填的清單:system-dev/.member-dirs(一行一個目錄前綴) +# ② 沒有該檔 → 動態偵測:頂層下「自己帶 .git 的子目錄」就是成員 repo +MEMBER_DIRS_FILE="$ROOT/system-dev/.member-dirs" +top="${REL%%/*}" +[ "$top" = "$REL" ] && exit 0 # 不在任何子目錄裡=總管自己的檔,放行 + +in_member_repo=1 +if [ -f "$MEMBER_DIRS_FILE" ]; then + while IFS= read -r d; do + case "$d" in ''|'#'*) continue ;; esac + d="${d%/}" + case "$REL" in "$d"/*) in_member_repo=0; break ;; esac + done < "$MEMBER_DIRS_FILE" +else + [ -d "$ROOT/$top/.git" ] && in_member_repo=0 +fi +[ "$in_member_repo" -eq 0 ] || exit 0 + +# ── 自動派工白名單(實例自填,框架不預設任何路徑)──── +ALLOW_FILE="$ROOT/system-dev/.autodispatch-allow" +if [ "${CLAUDE_CODE_CHILD_SESSION:-}" = "1" ] && [ -f "$ALLOW_FILE" ]; then + while IFS= read -r a; do + case "$a" in ''|'#'*) continue ;; esac + a="${a%/}" + case "$REL" in + "$a"/*) echo "🤝 [orchestrator-scope-guard] subagent 放行:$a(自動派工)" >&2; exit 0 ;; + esac + done < "$ALLOW_FILE" +fi + +# ── 成員 repo 內:.md 放行(交辦/文件),其餘擋 ──── +case "$REL" in + *.md|*.MD|*.markdown) exit 0 ;; +esac + +cat >&2 <