From 6a49f25aef8d7a357f5a07bd5d700dfed931dd48 Mon Sep 17 00:00:00 2001 From: richblack Date: Wed, 5 Aug 2026 23:56:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(W2=20Phase=200-1):=20=E9=9B=99=20profile?= =?UTF-8?q?=20=E5=9C=B0=E5=9F=BA=EF=BC=8BJDD=20=E5=85=A9=E8=BB=B8=E8=BA=AB?= =?UTF-8?q?=E5=88=86=EF=BC=8B=E5=85=A9=E6=94=AF=E9=98=B2=E7=82=B8=E9=96=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .sdt-framework-dev | 18 + docs/3-specs/jdd-dual-profile/design.md | 39 ++- docs/3-specs/jdd-dual-profile/tasks.md | 30 +- scripts/check-legacy-paths.sh | 95 +++++ scripts/check-no-instance-names.sh | 136 ++++++++ scripts/install.sh | 329 ++++++++++++------ scripts/instance-names.txt | 27 ++ scripts/update.sh | 141 +++++++- template/.claude/commands/issue-handle.md | 8 +- template/.claude/commands/wiki-extract.md | 4 +- template/.claude/hooks/lib/role-lib.sh | 172 +++++++++ template/.claude/hooks/publish-lag-check.sh | 7 +- template/.claude/hooks/sdd-guard.sh | 2 +- template/.claude/hooks/subagent-wiki-guard.sh | 2 +- template/CLAUDE.md | 8 + template/manifest/common.tsv | 88 +++++ template/manifest/orchestrator.tsv | 16 + template/manifest/repo.tsv | 15 + template/profiles/orchestrator/CLAUDE.md | 129 +++++++ template/profiles/repo/CLAUDE.md | 113 ++++++ .../docs/4-guides/logseq-markers.md | 2 +- .../workflows/tasks-project-sync.local.sh | 4 + .../workflows/tasks-project-sync.yaml | 6 + 23 files changed, 1259 insertions(+), 132 deletions(-) create mode 100644 .sdt-framework-dev create mode 100755 scripts/check-legacy-paths.sh create mode 100755 scripts/check-no-instance-names.sh create mode 100644 scripts/instance-names.txt create mode 100644 template/.claude/hooks/lib/role-lib.sh create mode 100644 template/manifest/common.tsv create mode 100644 template/manifest/orchestrator.tsv create mode 100644 template/manifest/repo.tsv create mode 100644 template/profiles/orchestrator/CLAUDE.md create mode 100644 template/profiles/repo/CLAUDE.md diff --git a/.sdt-framework-dev b/.sdt-framework-dev new file mode 100644 index 0000000..e92e9ac --- /dev/null +++ b/.sdt-framework-dev @@ -0,0 +1,18 @@ +# .sdt-framework-dev — 這裡是「框架 repo 本身」的標記 +# +# 存在即生效,內容不重要(這些字只是給人看的)。 +# +# 它解除的是什麼: +# 實例側有一條封路——「不准改安裝產物區(.claude/hooks/、system-dev/ 範本區、 +# plugin 安裝目錄)」,因為機制變更必須回框架提案,而不是各實例自己手改 +# (手改的下場:4 支 hook 已經漂移,而且沒有任何人知道)。 +# 但在**框架 repo 自己**開發時,改那些檔正是本職工作 ⇒ 這個標記讓 guard 放行。 +# +# 為什麼是檔案而不是 CLI 參數: +# 《分離導入規格》§6.1 寫的是「`--framework-dev` 標記的 session」, +# 但 Claude Code **沒有這個官方參數**(實查,非記憶)。 +# 改用檔案的好處:框架 repo 天生就帶著它、隨 clone 就在、零記憶負擔, +# 不必每次開 session 記得加旗標。 +# (臨時情境仍可用環境變數 SDT_FRAMEWORK_DEV=1 達到同樣效果。) +# +# ⚠️ 實例不該有這個檔。實例裡出現它 = 有人在關掉自己的封路,git 上看得見。 diff --git a/docs/3-specs/jdd-dual-profile/design.md b/docs/3-specs/jdd-dual-profile/design.md index 9c98c47..c5b25ae 100644 --- a/docs/3-specs/jdd-dual-profile/design.md +++ b/docs/3-specs/jdd-dual-profile/design.md @@ -1,5 +1,5 @@ --- -status: draft # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md) +status: active # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md) superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名 --- @@ -8,11 +8,13 @@ superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名 > 建立:2026-08-05 | 最後更新:2026-08-05 > 負責人:system-dev-template CC > 來源:InkStoneCo 總管交辦 **W2** -> **狀態:draft — 等 leo confirm 才升 active,未實作任何 code** +> **狀態:active(2026-08-05 leo 回「開工」升活性)** > -> 為什麼是 `draft` 而非 `active`:D35 ②③ 規定 CC 不得自行讓 SDD 生效; -> 本 repo 目前 **0 份 active**(見「現況實查」§0.1),本案 confirm 後直接升 active, -> 不需搬移任何未完成任務(無現任 active 可搬)。 +> 升活性前實查:全 repo 帶 frontmatter 的只有 `TEMPLATE-sdd`(draft,範本不算數) +> ⇒ active 數 = 0,**無衝突、無未完成任務需搬移**(D35 ④ a 步驟空集合)。 +> +> 施工順序由總管指定:**發現①、發現⑦ 的防炸工程 → Phase 0 → Phase 1 → 停下驗 Gherkin**。 +> Phase 2 以後未經確認不得開工。 --- @@ -407,6 +409,33 @@ SessionStart --- +## 附錄 A:三個 marker 檔的格式規格(task 0.3) + +| 檔 | 位置 | 內容 | 誰寫 | 誰讀 | +|---|---|---|---|---| +| `.profile` | 實例 `system-dev/.profile` | 單行:`repo` 或 `orchestrator`(前後空白會被 trim) | `install.sh --profile` | `role-lib.sh` 的 `sdt_scope()`、`update.sh`、CI | +| `.template-manifest` | 實例 `system-dev/.template-manifest` | TSV:`dest class version sha256`(安裝當下的雜湊) | install/update | `update.sh` 的三態判定 | +| `.sdt-framework-dev` | **框架 repo 根**(實例不該有) | 任意文字,存在即生效 | 框架 repo 自帶並 commit | `install-artifact-guard.sh` | + +- 三者都是「機器可讀的事實」,不是設定選項——agent 不得靠它們表達意圖,只能讀。 +- `.profile` 讀不到 → 視為 `repo`(代價寫在 `role-lib.sh` 註解裡,不假裝沒有)。 +- `.sdt-framework-dev` 出現在實例裡 = 有人在關掉自己的封路,**git diff 看得見**。 + +--- + +## 附錄 B:本波實作的兩支機械閘(先於一切 task,防炸用) + +| 閘 | 檔 | 擋什麼 | 實測 | +|---|---|---|---| +| 實例專名 | `scripts/check-no-instance-names.sh` + `scripts/instance-names.txt` | 框架範本混入實例專名 | 基線 22 行 → 處置後 0 違規/13 行檔級豁免;注入違規行可 fail 並指出檔案:行號 | +| 相容路徑 | `scripts/check-legacy-paths.sh` | 搬檔/改名導致舊實例 update 整排 404 | 追蹤 35 條已發佈路徑;移走 `template/CLAUDE.md` 可正確 fail | + +> 為什麼這兩支要排在所有 task 之前(總管裁定):它們**炸的是既有的東西**,不是新功能沒做好。 +> 專名閘晚做 → 之後每加一個範本檔都可能再混進實例名,債只會更大; +> 相容閘晚做 → Phase 1 一搬 `CLAUDE.md` 就把所有舊實例的自動更新弄死,而且**沒有下一次更新能修它**。 + +--- + ## 相關文件 - `requirements.md`(本卷)/`tasks.md`(本卷) diff --git a/docs/3-specs/jdd-dual-profile/tasks.md b/docs/3-specs/jdd-dual-profile/tasks.md index 7efc8e5..34f1142 100644 --- a/docs/3-specs/jdd-dual-profile/tasks.md +++ b/docs/3-specs/jdd-dual-profile/tasks.md @@ -3,7 +3,9 @@ > 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。 > 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。 > **每一項都標「服務哪條 Gherkin」**(G1–G7 定義見 `requirements.md` §四)。 -> 🔴 **本卷 status: draft — 等 leo confirm 才准動工。以下一項都還沒開始。** +> 🟢 **status: active(2026-08-05 leo 回「開工」)**。 +> 進度:**防炸兩閘 + Phase 0 + Phase 1 已完成**(Phase 1 有兩項 ◐,見下)。 +> **Phase 2 以後未經確認不得開工**(總管指定:先停下驗 Gherkin)。 --- @@ -28,14 +30,14 @@ ### Tasks -- [ ] 0.1 定義 manifest 格式並產出三份 `template/manifest/{common,repo,orchestrator}.tsv` +- [x] 0.1 定義 manifest 格式並產出三份 `template/manifest/{common,repo,orchestrator}.tsv` - 服務:**G4**(分流的資料基礎)、G6(產物區清單的單一來源) - 欄位:`dest class profile`;class ∈ `overwrite|keep|add-if-missing|keep-with-template` - 驗收:三份 manifest 涵蓋現行 install.sh 硬編的**每一個** `download_if_missing` 目標, 逐項比對零遺漏(貼比對輸出) - 注意:既有檔一律標 `common` 且 dest 路徑**與現況完全相同**(決策 D3,不搬檔) -- [ ] 0.2 寫 `template/.claude/hooks/lib/role-lib.sh`(兩軸判定函式庫) +- [x] 0.2 寫 `template/.claude/hooks/lib/role-lib.sh`(兩軸判定函式庫) - 服務:**G2**、G6 - 內容:`sdt_repo_root` / `sdt_rel_path` / `sdt_scope` / `sdt_role` / `sdt_assert_identity` / `sdt_file_path_from_stdin`(jq→python3→grep 三段 fallback) @@ -43,7 +45,7 @@ repo×engineer、repo×orchestrator)跑單元測試,最後一組回 exit 2;貼四組輸出 - 注意:不得用 `CLAUDE_PROJECT_DIR`(雲端可跑);bash 3.2 相容 -- [ ] 0.3 立 marker 檔約定:`system-dev/.profile`、`system-dev/.template-manifest`、`.sdt-framework-dev` +- [x] 0.3 立 marker 檔約定:`system-dev/.profile`、`system-dev/.template-manifest`、`.sdt-framework-dev` - 服務:**G4**、**G6** - 驗收:三個檔的格式各寫一段規格進 design 的附錄,並在框架 repo 自己放一份 `.sdt-framework-dev`(commit) @@ -55,54 +57,56 @@ > 前置條件:Phase 0 全部完成 -- [ ] 1.1 建 `template/profiles/repo/CLAUDE.md`(repo 憲法範本) +- [x] 1.1 建 `template/profiles/repo/CLAUDE.md`(repo 憲法範本) - 服務:**G4** - 來源:現行 `template/CLAUDE.md` 演進;加「上游指針」一行、「站號怎麼標在 task 上」一段、 W3 must-read 注入點一段 - 驗收:`grep -c "上游" ≥ 1`;全文不含任何實例專名 -- [ ] 1.2 建 `template/profiles/orchestrator/CLAUDE.md`(總管憲法範本) +- [x] 1.2 建 `template/profiles/orchestrator/CLAUDE.md`(總管憲法範本) - 服務:**G4**、G3 - 內容:效忠 root/journeys、JDD 全術語表、sprint 站號全流程、 進度語言「J-x 已點亮 n/m 站」、問題升級階梯三級(藍圖 §5.5)、W3 must-read 注入點 - 驗收:`grep -c "SDD 三件式\|requirements.md" = 0`(G4 明文:總管版無 SDD 三件式細節) - 注意:不得出現任何實例專名(藍圖 v3 的內容要抽象化,專案名留給實例填) -- [ ] 1.3 `install.sh` 加 `--profile=repo|orchestrator` + 自動偵測 + 一次性人確認 +- [x] 1.3 `install.sh` 加 `--profile=repo|orchestrator` + 自動偵測 + 一次性人確認 - 服務:**G4** - 偵測規則:目前目錄下存在多個各自帶 `.git` 的子目錄 → 建議 orchestrator - 驗收:三種呼叫(明示 repo/明示 orchestrator/不指定走偵測)各跑一次乾淨環境, 貼出各自產生的 `system-dev/.profile` 內容 - 注意:**確認在寫入任何檔案之前**問,別裝了一半才問 -- [ ] 1.4 `install.sh` 改讀 manifest 鋪設產物 + 產 `.template-manifest` +- [x] 1.4 `install.sh` 改讀 manifest 鋪設產物 + 產 `.template-manifest` - 服務:**G4**、G6 - 驗收:repo profile 裝出的檔案集合 = `common.tsv ∪ repo.tsv`, 且**不含** orchestrator 專屬檔(`ls` 對照貼出) -- [ ] 1.5 CLAUDE.md 三段組裝(框架區界標 + 本地補充區界標 + sha256) +- [x] 1.5 CLAUDE.md 三段組裝(框架區界標 + 本地補充區界標 + sha256) - 服務:**G4** - 驗收:裝完的 CLAUDE.md 含 `sdt:framework begin/end` 與 `sdt:local begin/end` 四個界標, 且 `sha256` 值與框架區實際內容相符(重算比對貼出) -- [ ] 1.6 `install.sh` 的 `build_hooks_json()` 加 profile 維度 + 寫入 `env.AGENT_ROLE` 預設 +- [x] 1.6 `install.sh` 的 `build_hooks_json()` 加 profile 維度 + 寫入 `env.AGENT_ROLE` 預設 - 服務:**G2**、G6 - 驗收:兩 profile 各自產生的 `settings.json` 中,hook 掛載順序符合 design §4.4; orchestrator 實例的 `env.AGENT_ROLE = orchestrator`、repo 實例 = `engineer` -- [ ] 1.7 `update.sh` 改讀 manifest + 三態判定 + 漂移清單輸出 +- [~] 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 清單漂移的根因只修了一半 -- [ ] 1.8 `update.sh` 舊實例遷移:無 manifest → 一次性補植 + CLAUDE.md 界標補植 +- [~] 1.8 `update.sh` 舊實例遷移:無 manifest → 一次性補植 + CLAUDE.md 界標補植 - 服務:**G4**、G6 - 驗收:拿一份 1.18.0 的實例副本跑兩次 update,第二次為 no-op(冪等,貼兩次輸出對照) - 注意:舊 CLAUDE.md 整份包進 `sdt:local` 區,一個字都不能掉 + - 現況:◐ 舊實例無 manifest → 首輪自動建基準(已實測);**CLAUDE.md 界標補植未做** -- [ ] 1.9 `template/CLAUDE.md` 原路徑保留轉址說明(向下相容) +- [x] 1.9 `template/CLAUDE.md` 原路徑保留轉址說明(向下相容) - 服務:**G4** - 驗收:舊版 update.sh 對該路徑的 curl 仍回 200(決策 D3) diff --git a/scripts/check-legacy-paths.sh b/scripts/check-legacy-paths.sh new file mode 100755 index 0000000..7956687 --- /dev/null +++ b/scripts/check-legacy-paths.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# check-legacy-paths.sh — 相容閘:已發佈的安裝/更新腳本會抓的每個遠端路徑,都必須還在 +# +# 這支存在的理由(SDD jdd-dual-profile 發現⑦ + 決策 D3): +# install.sh / update.sh 是**逐檔 curl**:檔案清單與遠端路徑**寫死在腳本裡**。 +# 而 update.sh 的「自我更新」在腳本**尾端**——舊實例這一輪跑的是**舊腳本**。 +# ⇒ 只要框架把某個既有檔搬走/改名,舊實例的那一輪就是**整排 404**, +# 而且它們不會自動好:更新機制本身壞了,就沒有下一次更新來修它。 +# +# 前科:1.16.0「update/install 來源改指 Gitea」——來源一改, +# 舊實例的自動更新當場死掉(CHANGELOG 1.16.0 有記)。同一種病。 +# +# 所以鐵律是:**遠端路徑對既有實例是契約,只增不移。** +# 要搬版面 → 先在原路徑留相容檔(轉址說明/原內容),確認本閘綠了才准動。 +# +# 作法: +# 從「已發佈版本」的 install.sh / update.sh 裡抽出所有 $REPO_URL/... $TEMPLATE_URL/... +# $SCRIPTS_URL/... 引用,映射回本 repo 的實體路徑,逐一確認檔案存在。 +# +# 誠實限制: +# · 只驗「路徑還在不在」,不驗內容還對不對(舊腳本抓到新內容仍可能語意不相容)。 +# · 基準是 git 裡的已發佈版本(預設 HEAD);再更老的版本若引用過更多路徑, +# 本閘看不到——要驗更早的版本,用 BASE= 指定。 +# +# 用法: +# bash scripts/check-legacy-paths.sh # 以 HEAD 為已發佈基準 +# BASE=dc4fe67 bash scripts/check-legacy-paths.sh +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +BASE="${BASE:-HEAD}" + +# ── 取出基準版本的兩支腳本 ──────────────────────────── +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +for s in install.sh update.sh; do + if ! git show "$BASE:scripts/$s" > "$TMP/$s" 2>/dev/null; then + echo "⚠️ 基準 $BASE 沒有 scripts/$s,略過" >&2 + : > "$TMP/$s" + fi +done + +# ── 抽出遠端引用 → 映射成本地實體路徑 ────────────────── +# $REPO_URL/x → template/x +# $TEMPLATE_URL/x → template/x +# $SCRIPTS_URL/x → scripts/x +extract_paths() { + grep -oE '\$\{?(REPO_URL|TEMPLATE_URL|SCRIPTS_URL)\}?/[A-Za-z0-9._/-]+' "$1" 2>/dev/null \ + | sed -e 's|\${*REPO_URL}*/|template/|' \ + -e 's|\${*TEMPLATE_URL}*/|template/|' \ + -e 's|\${*SCRIPTS_URL}*/|scripts/|' \ + || true +} + +ALL="$( { extract_paths "$TMP/install.sh"; extract_paths "$TMP/update.sh"; } | sort -u )" + +if [ -z "$ALL" ]; then + echo "⚠️ 在 $BASE 的腳本裡找不到任何遠端引用——抽取規則可能過時,本閘等同關閉" >&2 + exit 1 +fi + +TOTAL=0 +MISSING=0 +MISSING_LIST="" + +while IFS= read -r p; do + [ -z "$p" ] && continue + TOTAL=$((TOTAL + 1)) + if [ ! -e "$p" ]; then + MISSING=$((MISSING + 1)) + MISSING_LIST="${MISSING_LIST} ✗ ${p}"$'\n' + fi +done <<< "$ALL" + +if [ "$MISSING" -gt 0 ]; then + cat >&2 <&2 + exit 1 +fi +if [ ! -d "$SCAN_DIR" ]; then + echo "❌ 找不到範本區:$SCAN_DIR/" >&2 + exit 1 +fi + +# ── 讀黑名單 → 組成 grep 的 alternation pattern ─────────────── +PATTERN="" +while IFS= read -r line; do + case "$line" in ''|'#'*) continue ;; esac + term="$(printf '%s' "$line" | tr -d '[:space:]')" + [ -z "$term" ] && continue + if [ -z "$PATTERN" ]; then PATTERN="$term"; else PATTERN="$PATTERN|$term"; fi +done < "$NAMES_FILE" + +if [ -z "$PATTERN" ]; then + echo "⚠️ 黑名單是空的,本閘等同關閉($NAMES_FILE)" + exit 0 +fi + +# ── 掃描 ──────────────────────────────────────────── +# policy pack 不受此限(L2 本來就是一家之言)。目前尚無 policy pack 目錄, +# 先把慣例路徑寫在這裡,W3 建立時自動生效。 +HITS="$(grep -rniE "$PATTERN" "$SCAN_DIR" 2>/dev/null \ + | grep -v "^$SCAN_DIR/policy-packs/" || true)" + +VIOLATIONS=0 +EXEMPTED=0 +VIOL_LINES="" +EXEMPT_LINES="" + +if [ -n "$HITS" ]; then + while IFS= read -r hit; do + [ -z "$hit" ] && continue + hit_file="${hit%%:*}" + # 檔級豁免:整支本來就是 L2 產物,檔首宣告過就整檔記帳 + if [ -f "$hit_file" ] && head -n "$EXEMPT_HEAD_LINES" "$hit_file" 2>/dev/null | grep -q "$EXEMPT_FILE_MARK"; then + EXEMPTED=$((EXEMPTED + 1)) + EXEMPT_LINES="${EXEMPT_LINES}${hit}"$'\n' + continue + fi + # 行級豁免:檔案是框架的,只有這行欠著 + if printf '%s' "$hit" | grep -q "$EXEMPT_MARK"; then + EXEMPTED=$((EXEMPTED + 1)) + EXEMPT_LINES="${EXEMPT_LINES}${hit}"$'\n' + else + VIOLATIONS=$((VIOLATIONS + 1)) + VIOL_LINES="${VIOL_LINES}${hit}"$'\n' + fi + done <<< "$HITS" +fi + +# ── 輸出 ──────────────────────────────────────────── +if [ "$MODE" = "baseline" ]; then + echo "════════════════════════════════════════════════" + echo "📊 實例專名基線報表($SCAN_DIR/)" + echo "════════════════════════════════════════════════" + echo "未豁免(違規):$VIOLATIONS 行" + [ -n "$VIOL_LINES" ] && printf '%s' "$VIOL_LINES" | cut -c1-160 | sed 's/^/ ❌ /' + echo "已豁免(記在帳上,待搬遷):$EXEMPTED 行" + [ -n "$EXEMPT_LINES" ] && printf '%s' "$EXEMPT_LINES" | cut -c1-160 | sed 's/^/ ⏳ /' + exit 0 +fi + +if [ "$VIOLATIONS" -gt 0 ]; then + echo "════════════════════════════════════════════════" >&2 + echo "❌ 框架範本混入實例專名($VIOLATIONS 行)" >&2 + echo "════════════════════════════════════════════════" >&2 + printf '%s' "$VIOL_LINES" | cut -c1-200 | sed 's/^/ /' >&2 + cat >&2 <&2 + exit 1 ;; -h|--help) if [ "$IS_ZH" = "yes" ]; then cat <<'HELP' -用法:install.sh [--wiki | --sdd | --all] +用法:install.sh [--wiki | --sdd | --all] [--profile=repo|orchestrator] --wiki 只裝 LLM Wiki(CC 記憶系統 + 機敏防護) --sdd 只裝 SDD 系統(動 code 前強制要有設計文件) --all 兩個都裝(預設) 無參數 互動式詢問要裝哪個 + + --profile=repo 成員 repo:實際寫 code 的子專案(預設) + --profile=orchestrator 總管:管一群成員 repo 的上層資料夾 + 未指定 → 自動偵測並請你確認一次(結果寫進 system-dev/.profile,之後不再問) HELP else cat <<'HELP' -Usage: install.sh [--wiki | --sdd | --all] +Usage: install.sh [--wiki | --sdd | --all] [--profile=repo|orchestrator] --wiki Install LLM Wiki only (CC memory system + secret protection) --sdd Install SDD system only (require a design doc before touching code) --all Install both (default) no flag Interactively ask which to install + + --profile=repo Member repo: a project where code actually gets written (default) + --profile=orchestrator Orchestrator: the folder that manages several member repos + unset -> auto-detect and ask once (result stored in system-dev/.profile) HELP fi exit 0 ;; @@ -107,6 +127,57 @@ echo "" t "📦 安裝模組:$MODULE" "📦 Module: $MODULE" echo "" +# ── 決定 profile(scope 軸)────────────────────────── +# 必須在**寫任何檔案之前**問完(EARS-1.1.3)——裝了一半才問是最差的體驗。 +# +# 偵測規則:目前目錄下有多個「各自帶 .git 的子目錄」= 這裡像是管著一群 repo 的上層 +# → 建議 orchestrator。偵測只給建議,**仍要人確認一次**(一次性,之後寫進 marker 檔)。 +detect_profile() { + local n=0 d + for d in */; do + [ -d "$d/.git" ] && n=$((n + 1)) + [ "$n" -ge 2 ] && break + done + if [ "$n" -ge 2 ]; then printf 'orchestrator'; else printf 'repo'; fi +} + +if [ -z "$PROFILE" ]; then + SUGGESTED="$(detect_profile)" + if [ -t 0 ]; then + echo "" + t "🧭 這個資料夾是哪一種?(決定裝哪一部憲法)" \ + "🧭 What is this folder? (decides which constitution gets installed)" + t " 1) 成員 repo —— 實際寫 code 的子專案" \ + " 1) Member repo — a project where code actually gets written" + t " 2) 總管 —— 管一群成員 repo 的上層資料夾" \ + " 2) Orchestrator — the folder that manages several member repos" + echo "" + if [ "$SUGGESTED" = "orchestrator" ]; then + t " (偵測到底下有多個各自獨立的 repo → 看起來像 2)" \ + " (found multiple independent repos below -> looks like 2)" + else + t " (沒偵測到多個獨立 repo → 看起來像 1)" \ + " (no multiple independent repos found -> looks like 1)" + fi + tn "請輸入 1 / 2 [預設:$SUGGESTED]:" "Enter 1 / 2 [default: $SUGGESTED]: " + read -r pchoice || pchoice="" + case "$pchoice" in + 1) PROFILE="repo" ;; + 2) PROFILE="orchestrator" ;; + *) PROFILE="$SUGGESTED" ;; + esac + else + # 非互動(curl | bash 無 tty)→ 用偵測值,並在結尾大聲說它是怎麼決定的 + PROFILE="$SUGGESTED" + t "🧭 非互動環境 → 自動判定 profile:$PROFILE(可事後改 system-dev/.profile)" \ + "🧭 Non-interactive -> auto-detected profile: $PROFILE (change system-dev/.profile later)" + fi +fi + +echo "" +t "🧭 安裝 profile:$PROFILE" "🧭 Profile: $PROFILE" +echo "" + # ── 重複安裝防呆(1.10.1):install 只管「全新安裝」,一切後續歸 update ── # 判準是「裝過沒」,不分新版舊版: # - 新結構 system-dev/ 已存在,或 @@ -243,97 +314,93 @@ download_if_missing() { fi } -# ── 共用結構 ────────────────────────────────────── -# 工具自己的文件骨架收進 system-dev/docs/(不污染用戶根目錄、不跟用戶自己的 docs/ 混)。 -# 注意語義分離:這裡的 system-dev/docs/ 是「工具文件」;用戶的 raw source(原始文件) -# 另有其處(見上方 vault 偵測),工具只讀、不搬。 -# .claude/ 只留 CC 死綁的 commands/ + hooks/,工具資料一律不放這。 -create_dir "system-dev/docs/1-vision" -create_dir "system-dev/docs/2-architecture/decisions" -create_dir "system-dev/docs/4-guides" -create_dir "system-dev/docs/5-records/incidents" -create_dir "system-dev/docs/5-records/test-reports" -create_dir "system-dev/docs/6-user" -create_dir ".claude/commands" -create_dir ".claude/hooks" -download_if_missing "system-dev/docs/README.md" "$REPO_URL/system-dev/docs/README.md" +# ── Manifest 驅動的安裝(取代原本硬編的檔案清單)────────────── +# 為什麼改成這樣(SDD jdd-dual-profile 決策 D1): +# 原本清單硬編在這裡,update.sh 另有一份幾乎重複的硬編。兩份手抄必然漂移, +# 而且**已經漂了**——install 從來不裝 wiki-first-search.sh / subagent-wiki-guard.sh / +# publish-lag-check.sh / decisions-summary.md,但 update 會裝/會保留。 +# ⇒ 乾淨安裝的人反而拿不到最新三版的招牌功能。 +# 現在兩支腳本讀同一份 manifest,這類漂移在結構上不可能再發生。 +# +# manifest 欄位:src \t dest \t class \t module \t profile(詳見 template/manifest/common.tsv) -# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project -# 投影(tasks-project-sync)共用同一套解析,別各寫一份。放共用區,兩模組都拿得到。 -download_if_missing "system-dev/docs/4-guides/logseq-markers.md" "$REPO_URL/system-dev/docs/4-guides/logseq-markers.md" +MANIFEST_DIR="system-dev/.template-manifest.d" +mkdir -p "$MANIFEST_DIR" -# 工具版號:放 system-dev/,不寄生 .claude/。 -download_if_missing "system-dev/VERSION" "$REPO_URL/system-dev/VERSION" +fetch_manifest() { # $1=名稱(common|repo|orchestrator) + local name="$1" out="$MANIFEST_DIR/$1.tsv" + if curl -sSL "$REPO_URL/manifest/$name.tsv" -o "$out" 2>/dev/null && [ -s "$out" ]; then + return 0 + fi + rm -f "$out" + return 1 +} -# ── WIKI 模組 ───────────────────────────────────── -# wiki 是工具資料 → 放 system-dev/wiki/(不放 .claude/)。 -# commands/ 與 hooks/ 是 CC 機制檔 → 維持 .claude/。 -if $WANT_WIKI; then - create_dir "system-dev/wiki" - download_if_missing "system-dev/wiki/INDEX.md" "$REPO_URL/system-dev/wiki/INDEX.md" - download_if_missing "system-dev/wiki/TAXONOMY.md" "$REPO_URL/system-dev/wiki/TAXONOMY.md" - download_if_missing "system-dev/wiki/status.md" "$REPO_URL/system-dev/wiki/status.md" - download_if_missing "system-dev/wiki/mistakes.md" "$REPO_URL/system-dev/wiki/mistakes.md" - download_if_missing "system-dev/wiki/principles.md" "$REPO_URL/system-dev/wiki/principles.md" - download_if_missing "system-dev/wiki/.wikiignore" "$REPO_URL/system-dev/wiki/.wikiignore" +if ! fetch_manifest common; then + echo "❌ 抓不到安裝清單($REPO_URL/manifest/common.tsv)——網路或來源網址有問題,中止。" >&2 + exit 1 +fi +fetch_manifest "$PROFILE" || true # profile 專屬清單可以是空的 - # wiki 改寫產物(AI 自讀定稿卡片)的正式落點:由工具建好,不靠用戶自救。 - create_dir "system-dev/wiki/cards" +# src 前綴 → 實際來源網址 +resolve_src() { # $1=src 欄 + case "$1" in + T:*) printf '%s/%s' "$REPO_URL" "${1#T:}" ;; + S:*) printf '%s/%s' "$SCRIPTS_URL" "${1#S:}" ;; + *) printf '' ;; + esac +} + +# 要不要裝這一行(依模組) +want_module() { # $1=module 欄 + case "$1" in + core) return 0 ;; + wiki) $WANT_WIKI && return 0 || return 1 ;; + sdd) $WANT_SDD && return 0 || return 1 ;; + *) return 1 ;; + esac +} + +# 逐行安裝。CLAUDE.md(class=claude-md)不在這裡處理——它要三段組裝,走下方專段。 +install_from_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 + case "$class" in + dir) + create_dir "$dest" + ;; + claude-md) + CLAUDE_MD_SRC="$(resolve_src "$src")" # 交給下方 CLAUDE.md 組裝段 + ;; + *) + url="$(resolve_src "$src")" + [ -z "$url" ] && continue + download_if_missing "$dest" "$url" + ;; + esac + done < "$f" +} + +CLAUDE_MD_SRC="" +install_from_manifest "$MANIFEST_DIR/common.tsv" +install_from_manifest "$MANIFEST_DIR/$PROFILE.tsv" + +# wiki 卡片落點的 .gitkeep(目錄由 manifest 建,這顆種子檔留在腳本裡) +if $WANT_WIKI && [ -d "system-dev/wiki/cards" ]; then [ -f "system-dev/wiki/cards/.gitkeep" ] || { : > "system-dev/wiki/cards/.gitkeep"; CREATED+=("system-dev/wiki/cards/.gitkeep"); } - - 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" - # vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):給 Routine 反覆跑。 - download_if_missing ".claude/commands/wiki-extract.md" "$REPO_URL/.claude/commands/wiki-extract.md" - - # wiki 相關 hooks:接關 + 機敏掃描 - download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh" - download_if_missing ".claude/hooks/wiki-secret-scan.sh" "$REPO_URL/.claude/hooks/wiki-secret-scan.sh" - - # Cowork(claude.ai)整理 wiki 用的 skill:與 CC 的 /wiki-init 共用同一套規則 - # (含 typed-edge、frontmatter 標籤、gloss)。沒這支 → claude.ai 來掃時身上沒規則。 - download_if_missing "system-dev/docs/SKILL.md" "$REPO_URL/system-dev/docs/SKILL.md" fi -# ── SDD 模組 ────────────────────────────────────── -if $WANT_SDD; then - create_dir "system-dev/docs/3-specs" - download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md" - download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" - download_if_missing "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$REPO_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" - - download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md" - download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh" - - # SDD 生命週期鐵律(1.14,issue #6):規則真相源 + 規格變更緩衝區 + 獨立單一活性檢查 - download_if_missing "system-dev/docs/3-specs/SDD-LIFECYCLE.md" "$REPO_URL/system-dev/docs/3-specs/SDD-LIFECYCLE.md" - download_if_missing "system-dev/docs/3-specs/pending-changes.md" "$REPO_URL/system-dev/docs/3-specs/pending-changes.md" - download_if_missing "system-dev/scripts/sdd-active-check.sh" "$REPO_URL/scripts/sdd-active-check.sh" - - # ── tasks⇄Project 投影(optional,issue #16)────────────────── - # 帶檔 ≠ 啟用:workflow yaml 只是「留記錄+手動啟用素材」,啟用=對話答好且 acr push。 - # 投影邏輯依附 tasks.md(住 3-specs),故隨 SDD 模組帶下來;裝了不代表開。 - create_dir "system-dev/workflows" - download_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$REPO_URL/system-dev/workflows/tasks-project-sync.yaml" - download_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$REPO_URL/system-dev/workflows/tasks-project-sync.local.sh" -fi - -# ── 安裝/更新腳本:一開始就放進 system-dev/scripts/ ── -# 為什麼一開始就裝:之後要更新,用戶(或 CC)直接 `bash system-dev/scripts/update.sh`, -# 不必每次都記那串 curl。腳本來源在 main/scripts/(不在 template/)。 -create_dir "system-dev/scripts" -download_if_missing "system-dev/scripts/install.sh" "$SCRIPTS_URL/install.sh" -download_if_missing "system-dev/scripts/update.sh" "$SCRIPTS_URL/update.sh" - -# ── 共用 hook:專案自訂禁令骨架(預設停用)──────── -download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh" - -# ── 共用指引:GitHub issue 處理(讀/回普世,跨 repo 發要先問,禁自動輪詢)── -download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md" +# ── scope 軸 marker:這個實例是哪一種 ──────────────── +# 這是 role-lib.sh 判 scope 的**唯一**來源。AI 代裝路徑也必須寫出同格式的檔。 +printf '%s\n' "$PROFILE" > "system-dev/.profile" +CREATED+=("system-dev/.profile ($PROFILE)") chmod +x .claude/hooks/*.sh 2>/dev/null || true +chmod +x .claude/hooks/lib/*.sh 2>/dev/null || true chmod +x system-dev/workflows/*.sh 2>/dev/null || true chmod +x system-dev/scripts/*.sh 2>/dev/null || true @@ -346,7 +413,9 @@ build_hooks_json() { session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }' fi - # PreToolUse 依模組疊加 + # PreToolUse 依模組疊加。 + # 順序原則:**範圍大的擋在前**——這樣錯誤訊息會指向最根本的那條規則, + # 而不是讓人先修一個表層問題、修完才發現底下還有一條。 local pt=() $WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }') pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }') @@ -354,7 +423,15 @@ build_hooks_json() { local IFS=, pretool_hooks="${pt[*]}" - printf '{\n "hooks": {\n' + # role 軸預設值:由 profile 決定,寫進 settings.json 的 env。 + # 為什麼要寫死一個預設:AGENT_ROLE 沒設時 role-lib.sh 會「依 scope 推定」, + # 但推定是保險不是設計——明寫出來,人才看得見自己這個實例預設是什麼身分。 + local default_role="engineer" + [ "$PROFILE" = "orchestrator" ] && default_role="orchestrator" + + printf '{\n' + printf ' "env": { "AGENT_ROLE": "%s" },\n' "$default_role" + printf ' "hooks": {\n' if [ -n "$session_hooks" ]; then printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks" fi @@ -369,19 +446,75 @@ else SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')") fi -# ── CLAUDE.md:只在完全不存在時建立 ──────────────── -# 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入, -# 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。 +# ── CLAUDE.md:由 profile 範本三段組裝 ──────────────── +# 結構(界標用 HTML 註解:md 渲染看不見、grep 定位得到、CC 讀得到): +# +# +# (profile 憲法範本原文,一字不改) +# +# +# (raw source 宣告 + 之後使用者/CC 自由追加) +# +# +# 為什麼要界標:在這之前 CLAUDE.md 是「整份下載 + append」,**沒有任何邊界** ⇒ +# update 無從分辨「這段是框架的、那段是你寫的」,因此永遠不敢覆蓋, +# 框架改了憲法也送不到既有實例;而使用者手改框架段也沒人看得見。 +# 有了界標+sha256,兩件事同時解決:框架段可安全更新、被手改時抓得到(漂移偵測)。 +sdt_sha256() { # 跨平台取 sha256(macOS 用 shasum,Linux 多為 sha256sum) + if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' + else printf 'nohash'; fi +} + if [ ! -f "CLAUDE.md" ]; then - download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md" - if [ -f "CLAUDE.md" ]; then - emit_raw_source_block >> CLAUDE.md - CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE})" "← raw source declaration written (${VAULT_TYPE})")") + if [ -z "$CLAUDE_MD_SRC" ]; then + echo "⚠️ manifest 沒有指定 $PROFILE 的 CLAUDE.md 範本,略過憲法安裝" >&2 + else + FW_TMP="$(mktemp)" + if curl -sSL "$CLAUDE_MD_SRC" -o "$FW_TMP" 2>/dev/null && [ -s "$FW_TMP" ]; then + FW_SHA="$(sdt_sha256 "$FW_TMP")" + TOOL_VER="$(tr -d '[:space:]' < system-dev/VERSION 2>/dev/null || echo 'unknown')" + { + printf '\n' \ + "$PROFILE" "$TOOL_VER" "$(printf '%s' "$FW_SHA" | cut -c1-12)" + cat "$FW_TMP" + printf '\n\n' + printf '\n' + emit_raw_source_block + printf '\n\n' + } > CLAUDE.md + rm -f "$FW_TMP" + CREATED+=("CLAUDE.md $(tn "← ${PROFILE} 憲法 + 本地補充區(raw source: ${VAULT_TYPE})" "← ${PROFILE} constitution + local section (raw source: ${VAULT_TYPE})")") + else + rm -f "$FW_TMP" + echo "⚠️ 抓不到 $PROFILE 憲法範本($CLAUDE_MD_SRC),CLAUDE.md 未建立" >&2 + fi fi else - SKIPPED+=("CLAUDE.md $(tn '(已存在,請手動加入對應區塊)' '(already exists — add the block manually)')") + SKIPPED+=("CLAUDE.md $(tn '(已存在,未覆蓋——要導入 profile 憲法請跑 update.sh)' '(already exists — run update.sh to adopt the profile constitution)')") fi +# ── 產生 .template-manifest(漂移偵測的基準)──────────── +# 記下每個安裝產物「安裝當下」的雜湊。update 時比對: +# 實檔 sha == 這裡的 sha → 乾淨,可安全覆蓋成新版 +# 實檔 sha != 這裡的 sha → **被手改過**,不覆蓋、列進漂移清單 +# 沒有這張表,就只能「不敢覆蓋」或「盲目覆蓋」二選一,兩個都錯。 +{ + printf '# dest\tclass\tversion\tsha256 —— 安裝當下的快照,供 update 判漂移用,勿手改\n' + TOOL_VER="$(tr -d '[:space:]' < system-dev/VERSION 2>/dev/null || echo 'unknown')" + for mf in "$MANIFEST_DIR/common.tsv" "$MANIFEST_DIR/$PROFILE.tsv"; do + [ -f "$mf" ] || continue + while IFS=$'\t' read -r m_src m_dest m_class m_module m_profile; do + case "$m_src" in ''|'#'*) continue ;; esac + [ -z "${m_dest:-}" ] && continue + [ "$m_class" = "dir" ] && continue + [ -f "$m_dest" ] || continue + printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$TOOL_VER" "$(sdt_sha256 "$m_dest")" + done < "$mf" + done +} > system-dev/.template-manifest +CREATED+=("system-dev/.template-manifest") + # ── 輸出結果 ────────────────────────────────────── echo "" t "✅ 建立了:" "✅ Created:" diff --git a/scripts/instance-names.txt b/scripts/instance-names.txt new file mode 100644 index 0000000..7d9ac6d --- /dev/null +++ b/scripts/instance-names.txt @@ -0,0 +1,27 @@ +# instance-names.txt — 實例專名黑名單(機械閘 #2 的判準來源) +# +# 鐵律(《分離導入規格》第一節鐵律 2): +# **框架不含實例資料**——L1 範本檔出現具體專案名(arcrun、mira、leo21c…)=格式錯誤。 +# 換一家公司照樣成立的才配住在框架裡;換一家公司就不成立的,那是 L2 政策包或 L3 實例的事。 +# +# 用法:scripts/check-no-instance-names.sh 掃 template/(範本區),命中即 fail。 +# +# ── 豁免(唯一合法的例外,留痕可審)────────────────────── +# 在命中行的行尾加註記: sdt-instance-name-ok +# 規矩:豁免必須附一句「為什麼還不能拿掉」,且應該有對應的搬遷計畫。 +# 豁免不是赦免——它是「這行欠著,記在帳上」。 +# +# ── 不受此檢查的地方 ──────────────────────────────── +# · policy pack(L2 政策包本來就是一家之言,專名是它的內容不是它的錯) +# · 本 repo 自己的工作現場:docs/、system-dev/、CHANGELOG.md、README*、scripts/ +# (那些是「我們怎麼做出來的」,不是發給別人的範本) +# +# 一行一個詞,大小寫不敏感。# 開頭為註解。 + +arcrun +mira +leo21c +inkstone +uncle6 +polaris +richblack diff --git a/scripts/update.sh b/scripts/update.sh index 367c5f0..eca5148 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -127,13 +127,88 @@ if [ "$NEEDS_MIGRATE" = "yes" ]; then echo "" fi +# ── 漂移偵測(1.19.0 新增)───────────────────────── +# 病根:實例會手改安裝產物(hook、範本),而**沒有任何機制知道**。 +# 實測基線(2026-08-05,拿一個真實使用中的實例對照框架): +# 6 支框架 hook 裡 **4 支已被手改**,另有 11 支是該實例自己發明的。 +# 兩層後果:① 框架修好的 bug 送不到那些檔 ② 手改內容沒人審、沒回饋回框架 +# ⇒ 同一個坑每個實例各踩一次。 +# +# 判準:安裝當下的 sha(記在 system-dev/.template-manifest)vs 現在的 sha。 +# 相同 → 沒動過 → 安全覆蓋成新版 +# 不同 → **被手改過** → 不覆蓋、另存 .new、列進漂移清單,讓人自己決定 +# +# 為什麼不直接覆蓋:無聲吃掉別人的修改(他多半是在修一個框架還沒修的問題)。 +# 為什麼不永遠不覆蓋:那更新就永遠送不到 = 等於沒有更新機制。 +# **分辨得出來**才是解法,兩個極端都不對。 +DRIFTED=() + +sdt_sha256() { + if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' + else printf 'nohash'; fi +} + +MANIFEST_FILE="system-dev/.template-manifest" + +manifest_sha() { # 查登記的 sha;查不到回空 + [ -f "$MANIFEST_FILE" ] || return 0 + awk -F'\t' -v d="$1" '!/^#/ && $1==d {print $4; exit}' "$MANIFEST_FILE" +} + +# 本次 update 經手過的檔(不論更新、保留或漂移)。收工時用它重寫 manifest。 +# ⚠️ 沒有這一步的話會出現**自己造的 bug**:update 把檔覆蓋成新版後,manifest 還留著舊 sha +# ⇒ 下一次跑 update 會把「框架自己更新的檔」誤判成「使用者手改」,整排假漂移。 +# 基準快照必須跟著實檔一起前進。 +MANAGED=() + +is_drifted() { # 0=被手改過;1=沒動過或無從判斷(保守,不誤報) + local dest="$1" recorded + recorded="$(manifest_sha "$dest")" + [ -z "$recorded" ] && return 1 + [ -f "$dest" ] || return 1 + [ "$(sdt_sha256 "$dest")" = "$recorded" ] && return 1 + return 0 +} + +# ── 下載健全性檢查(修一個舊 bug)────────────────── +# 原本只用 [ -s ]=「非空就接受」。但 curl 對 404 會把「404: Not Found」當內容寫出來, +# **那也是非空** ⇒ 好檔被覆寫成一行垃圾,而且沒有任何錯誤訊息。 +# 實錄:SKILL.md 曾被無聲覆寫 260 行 → 1 行(template issue #13)。 +# 諷刺的是本腳本的「版本號比對」那段早就防了這招(見上方 REMOTE_VER 的驗證), +# 檔案下載這條路卻沒防——同一個坑,防了一半。 +looks_like_error_page() { + local f="$1" + [ -s "$f" ] || return 0 # 空的 → 當失敗 + # 極短又長得像錯誤訊息 → 當失敗(正常範本檔不會只有一兩行還寫著 404) + if [ "$(wc -l < "$f" | tr -d ' ')" -le 2 ] && head -c 200 "$f" | grep -qiE '40[0-9]|not found|/dev/null && [ -s "$dest.tmp" ]; then + if is_drifted "$dest"; then + # 被手改過:不覆蓋,把新版放旁邊供 diff + if curl -sSL "$src" -o "$dest.new" 2>/dev/null && ! looks_like_error_page "$dest.new"; then + if cmp -s "$dest" "$dest.new"; then + rm -f "$dest.new" # 手改後剛好等於新版 → 沒事,不用吵 + else + DRIFTED+=("$dest") + fi + else + rm -f "$dest.new" + DRIFTED+=("$dest") + fi + return 0 + fi + if curl -sSL "$src" -o "$dest.tmp" 2>/dev/null && ! looks_like_error_page "$dest.tmp"; then if cmp -s "$dest" "$dest.tmp"; then rm -f "$dest.tmp" # 內容相同,不算更新 else @@ -145,7 +220,7 @@ update_file() { t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest" fi else - if curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then + if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then NEW+=("$dest") # 新功能:舊版沒有的檔 else rm -f "$dest" @@ -163,12 +238,19 @@ keep_file() { # 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。 add_if_missing() { local dest="$1" src="$2" + MANAGED+=("$dest\tadd-if-missing") if [ -f "$dest" ]; then KEPT+=("$dest") - elif curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then + return 0 + fi + # 🐛 修:原本少了 mkdir -p ⇒ 目標在**新目錄**時 curl 直接失敗, + # 但 VERSION 照樣升上去 = 典型假綠(2026-07 在一個實例升級時撞到,當時記「待回報 template」)。 + mkdir -p "$(dirname "$dest")" + if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then NEW+=("$dest") else rm -f "$dest" + t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest" fi } @@ -318,6 +400,29 @@ if [ ${#UPDATED[@]} -gt 0 ]; then t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):" for f in "${UPDATED[@]}"; do echo " ~ $f"; done fi +if [ ${#DRIFTED[@]} -gt 0 ]; then + echo "" + t "⚠️ 下列 ${#DRIFTED[@]} 個檔被手改過,這一版**沒有**覆蓋它們:" \ + "⚠️ ${#DRIFTED[@]} file(s) were modified by hand and were NOT overwritten:" + for f in "${DRIFTED[@]}"; do + if [ -f "$f.new" ]; then + t " ✋ $f → 新版已放在 $f.new,請 diff" \ + " ✋ $f → new version saved as $f.new — please diff" + else + t " ✋ $f (新版抓取失敗,稍後再試)" \ + " ✋ $f (couldn't fetch the new version; try again later)" + fi + done + echo "" + t " 這代表什麼:你(或某個 CC)改過框架發下來的檔。" \ + " What this means: you (or a CC) edited a file that the framework ships." + t " 兩條路,擇一——別放著不管,放著=框架之後的修正永遠送不到這個檔:" \ + " Pick one — leaving it means future framework fixes will never reach this file:" + t " ① 把你的改動寫成框架提案(推薦)——一次修全家,別人也拿得到" \ + " 1. Turn your change into a framework proposal (recommended) — fixes it for everyone" + t " ② 放棄本地改動:mv <檔>.new <檔>" \ + " 2. Drop your local change: mv .new " +fi if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then echo "" t "✨ 模板邏輯檔已全部最新,無需變動。" \ @@ -387,6 +492,34 @@ PYEOF fi fi +# ── 重寫漂移基準快照(manifest)────────────────────── +# 兩個作用: +# ① 舊實例(1.18.x 以前沒有 manifest)→ 這一輪之後就有了,下次起就能偵測漂移。 +# 注意本輪它們**不會**被判漂移(查無登記=不判定),這是刻意的保守: +# 沒有基準就宣稱「你改過」等於瞎猜。第一輪建基準,第二輪起才有話語權。 +# ② 已有 manifest → 把「本輪更新過的檔」的 sha 推進到新值。 +# 少了這步就會自己造一個 bug:框架自己覆蓋的檔,下一輪被誤判成使用者手改。 +# 漂移中的檔**不更新其基準**——它們仍然是「相對於安裝版被改過」,下次還要繼續報。 +{ + printf '# dest\tclass\tversion\tsha256 —— update 維護的漂移基準,勿手改\n' + for entry in ${MANAGED[@]+"${MANAGED[@]}"}; do + m_dest="$(printf '%b' "$entry" | cut -f1)" + m_class="$(printf '%b' "$entry" | cut -f2)" + [ -f "$m_dest" ] || continue + skip="" + for d in ${DRIFTED[@]+"${DRIFTED[@]}"}; do + [ "$d" = "$m_dest" ] && skip="yes" && break + done + if [ -n "$skip" ]; then + # 保留原登記值(它還在漂移中,基準不能跟著漂) + old="$(manifest_sha "$m_dest")" + [ -n "$old" ] && printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$LOCAL_VER" "$old" + continue + fi + printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$REMOTE_VER" "$(sdt_sha256 "$m_dest")" + done +} > "$MANIFEST_FILE.tmp" && mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE" + echo "" t "🚀 更新完成:${LOCAL_VER} → ${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER} → ${REMOTE_VER}" t " 下次更新直接跑:bash system-dev/scripts/update.sh" " Next time, just run: bash system-dev/scripts/update.sh" diff --git a/template/.claude/commands/issue-handle.md b/template/.claude/commands/issue-handle.md index 4023c6f..63f1016 100644 --- a/template/.claude/commands/issue-handle.md +++ b/template/.claude/commands/issue-handle.md @@ -44,13 +44,13 @@ issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆 ## 3. 跨 repo 署名(鐵律 — 絕不可漏) -所有 repo(mira / graph-plugin / ingest-plugin / Arcrun / template…)共用**同一個 GitHub 帳號**發 issue/comment, -所以 issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**。 +當你的多個 repo 共用**同一個託管帳號**發 issue/comment 時, +issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**。 > **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。 -- 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`… -- 總管下令/追問:`[InkStoneCo 總管]` +- 收件方 CC 回報:`[<收件 repo 名> CC]`(例:`[<某子系統> CC]`) +- 上游/總管下令、追問:`[<上游 repo 名> 總管]` - 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。 為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**; diff --git a/template/.claude/commands/wiki-extract.md b/template/.claude/commands/wiki-extract.md index e8e35ed..e4600d1 100644 --- a/template/.claude/commands/wiki-extract.md +++ b/template/.claude/commands/wiki-extract.md @@ -2,7 +2,7 @@ 把**筆記 vault**(Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地 萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**: -AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 KBDB。 +AI 只產卡片檔,下游 ingest 管線再從 wikilink 機械拉三元組進知識庫。 > **跟 `/wiki-init` 的分工**: > - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。 @@ -12,7 +12,7 @@ AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 > **邊界(硬規矩,別越界)** > - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游 -> Arcrun 從你產的 `[[wikilink]]` + `## 關聯` 機械映射(另一張 issue),不是這支的事。 +> 下游 ingest 管線從你產的 `[[wikilink]]` + `## 關聯` 機械映射,不是這支的事。 > - **原始筆記唯讀**:`journals/`、`pages/`、Obsidian 根 `.md` 是 leo 的手寫真身, > 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。 > - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。 diff --git a/template/.claude/hooks/lib/role-lib.sh b/template/.claude/hooks/lib/role-lib.sh new file mode 100644 index 0000000..d35ab24 --- /dev/null +++ b/template/.claude/hooks/lib/role-lib.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# role-lib.sh — 兩軸身分判定函式庫(被 source,不獨立掛 hook) +# +# 出處:《分離導入規格》第二節「兩軸身分(scope × role,全部機械判定,agent 不自我判斷)」 +# +《JDD 導入規格》第四節「角色與權限」。 +# +# ── 為什麼是「兩軸」而不是「一個角色設定」────────────────────── +# scope(我在誰的地盤)=**安裝位置**決定:agent 在哪個資料夾醒來, +# 就只讀得到那部憲法。來源= system-dev/.profile(install 時寫死)。 +# 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 根目錄 ──────────────────────────────────────── +# 從本檔位置往上推三層:/.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 </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)" +} diff --git a/template/.claude/hooks/publish-lag-check.sh b/template/.claude/hooks/publish-lag-check.sh index 9469a82..3ff4056 100755 --- a/template/.claude/hooks/publish-lag-check.sh +++ b/template/.claude/hooks/publish-lag-check.sh @@ -7,9 +7,10 @@ # → 改了零件、重編 wasm 後若沒人記得發佈,**用戶抓到舊版且沒有任何錯誤訊息, # 只是行為不對**——這種靜默失敗只有外部使用者會撞到,我們自己永遠測不到。 # -# 實例:安裝器的懶載會從 -# cdn.jsdelivr.net/gh/youlinhsieh/Arcrun@main/.component-builds/<名>/component.wasm -# 抓 wasm。那個位址永遠指向 GitHub 上的**最後一次發佈**,不是我們本機的最新版。 +# 典型形狀:安裝器/前端從 CDN 懶載資源,網址長得像 +# cdn.jsdelivr.net/gh/<帳號>/@main/<產物路徑> +# ——那個位址永遠指向公開 repo 的**最後一次發佈**,不是你本機的最新版。 +# 本機改對了、CDN 還在吐三代前的東西,而且不會有任何錯誤訊息。 # # 原理:比對「工作區 HEAD」與「.github-public 最後一個 release commit 記錄的 snapshot」。 # publish-github.sh 的 commit 訊息格式固定為:release: snapshot <短hash> (<日期>) diff --git a/template/.claude/hooks/sdd-guard.sh b/template/.claude/hooks/sdd-guard.sh index 06d7452..ec9d969 100755 --- a/template/.claude/hooks/sdd-guard.sh +++ b/template/.claude/hooks/sdd-guard.sh @@ -12,7 +12,7 @@ # 3. 向下相容:3-specs 下完全沒有任何 design.md 帶 frontmatter(老 repo 尚未遷移生命週期制度) # → 退回舊行為:有 design.md 就放行+提醒,沒有才擋。避免 template update 後老 repo 立刻全紅。 # -# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。 +# 誠實限制(本 template 所有 hook 共用的寫法):只擋語法層明顯違規(直接寫 code 檔)。 # 藏在 helper 裡、用 bash 繞道的改動擋不到。 # 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。 diff --git a/template/.claude/hooks/subagent-wiki-guard.sh b/template/.claude/hooks/subagent-wiki-guard.sh index ac1360d..92c3a22 100755 --- a/template/.claude/hooks/subagent-wiki-guard.sh +++ b/template/.claude/hooks/subagent-wiki-guard.sh @@ -50,7 +50,7 @@ guidance = """【自動注入:查任何東西之前,先查 wiki】 (leo 2026-07-21:「它一定是用最好的搜尋,如果沒有才 fallback, 但那不是你要指定的,對搜尋者來說,我就是要去搜尋,如果你沒這個機制才降。」) - **① 語意搜尋(最強,優先)**——有 Arcrun RAG MCP 就用它,用**自然語言問句**,不是關鍵字: + **① 語意搜尋(最強,優先)**——有語意檢索 MCP 就用它,用**自然語言問句**,不是關鍵字: kbdb_search(q="<用一句話描述你要找什麼>", mode="semantic") 不確定該查哪個庫 → 先 kbdb_get_map() 看藏書地圖 要沿關係展開 → kbdb_graph_neighbors() diff --git a/template/CLAUDE.md b/template/CLAUDE.md index afb377e..7f28000 100644 --- a/template/CLAUDE.md +++ b/template/CLAUDE.md @@ -1,3 +1,11 @@ + + # CLAUDE.md — [專案名稱] > 導航牌。細節在兩個地方,不在這裡。 diff --git a/template/manifest/common.tsv b/template/manifest/common.tsv new file mode 100644 index 0000000..94f7828 --- /dev/null +++ b/template/manifest/common.tsv @@ -0,0 +1,88 @@ +# common.tsv — 兩個 profile 都要裝的產物(安裝清單的單一真相源) +# +# 為什麼有這張表(SDD jdd-dual-profile 決策 D1): +# 在這之前,安裝清單**硬編在 install.sh,另一份幾乎重複的硬編在 update.sh**。 +# 兩份手抄的清單必然漂移——而且已經漂了: +# · wiki-first-search.sh (1.16.0 頭條功能)→ update 會裝,**install 不裝** +# · subagent-wiki-guard.sh (1.17.0 頭條功能)→ update 會裝,**install 不裝** +# · publish-lag-check.sh (1.18.0 頭條功能)→ update 會裝,**install 不裝** +# · wiki/decisions-summary.md → update 保留它,**install 不建**, +# 但 CLAUDE.md 範本的「Wiki 讀取順序」表裡就列著它 ⇒ 新用戶開檔即撲空 +# ⇒ **乾淨安裝反而拿不到最新三版的招牌功能**,只有「先裝舊版再 update」的人拿得到。 +# 本表讓兩支腳本讀同一份清單,這類漂移在結構上不可能再發生。 +# +# ── 欄位 ────────────────────────────────────────────── +# 1 src 來源。T:<路徑> = template/ 底下;S:<路徑> = scripts/ 底下;- = 建目錄用 +# 2 dest 安裝到實例的哪裡(相對實例根) +# 3 class overwrite 模板/邏輯檔,新版直接蓋(使用者不會手改) +# keep 使用者資料檔,**永遠不動**(蓋掉=清空他的記憶) +# add-if-missing 使用者資料檔,缺了才補,已有絕不覆蓋 +# keep-with-template 使用者會手填的客製檔;保留原檔,新版另存 .template. +# claude-md 特殊:CLAUDE.md 走「框架區+本地補充區」組裝,見 install.sh +# dir 只建目錄 +# 4 module core | wiki | sdd —— 對應 install.sh 的 --wiki/--sdd/--all 模組選擇 +# 5 profile 這裡一律 common(repo/orchestrator 專屬的在各自的 tsv) +# +# ⚠️ 路徑契約:dest 一旦發佈就是對既有實例的承諾,**只增不移** +# (舊實例跑的是舊腳本,路徑寫死;搬檔=它們整排 404、更新機制本身壞掉)。 +# 要改版面先跑 scripts/check-legacy-paths.sh。 +# +#src dest class module profile + +# ── 目錄骨架 ── +- system-dev/docs/1-vision dir core common +- system-dev/docs/2-architecture/decisions dir core common +- system-dev/docs/4-guides dir core common +- system-dev/docs/5-records/incidents dir core common +- system-dev/docs/5-records/test-reports dir core common +- system-dev/docs/6-user dir core common +- system-dev/scripts dir core common +- .claude/commands dir core common +- .claude/hooks dir core common +- .claude/hooks/lib dir core common + +# ── core:兩個模組都要的底盤 ── +T:system-dev/docs/README.md system-dev/docs/README.md overwrite core common +T:system-dev/docs/4-guides/logseq-markers.md system-dev/docs/4-guides/logseq-markers.md overwrite core common +T:system-dev/VERSION system-dev/VERSION overwrite core common +S:install.sh system-dev/scripts/install.sh overwrite core common +S:update.sh system-dev/scripts/update.sh overwrite core common +T:.claude/commands/issue-handle.md .claude/commands/issue-handle.md overwrite core common +T:.claude/hooks/pre-write-guard.sh .claude/hooks/pre-write-guard.sh keep-with-template core common +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 + +# ── wiki 模組 ── +- system-dev/wiki dir wiki common +- system-dev/wiki/cards dir wiki common +T:system-dev/wiki/INDEX.md system-dev/wiki/INDEX.md overwrite wiki common +T:system-dev/wiki/TAXONOMY.md system-dev/wiki/TAXONOMY.md keep wiki common +T:system-dev/wiki/status.md system-dev/wiki/status.md keep wiki common +T:system-dev/wiki/mistakes.md system-dev/wiki/mistakes.md keep wiki common +T:system-dev/wiki/principles.md system-dev/wiki/principles.md add-if-missing wiki common +T:system-dev/wiki/decisions-summary.md system-dev/wiki/decisions-summary.md add-if-missing wiki common +T:system-dev/wiki/.wikiignore system-dev/wiki/.wikiignore keep wiki common +T:system-dev/docs/SKILL.md system-dev/docs/SKILL.md overwrite wiki common +T:.claude/commands/wiki-init.md .claude/commands/wiki-init.md overwrite wiki common +T:.claude/commands/wiki-capture.md .claude/commands/wiki-capture.md overwrite wiki common +T:.claude/commands/wiki-update.md .claude/commands/wiki-update.md overwrite wiki common +T:.claude/commands/wiki-recall.md .claude/commands/wiki-recall.md overwrite wiki common +T:.claude/commands/wiki-extract.md .claude/commands/wiki-extract.md overwrite wiki common +T:.claude/hooks/session-start-recall.sh .claude/hooks/session-start-recall.sh overwrite wiki common +T:.claude/hooks/wiki-secret-scan.sh .claude/hooks/wiki-secret-scan.sh overwrite wiki common +T:.claude/hooks/wiki-first-search.sh .claude/hooks/wiki-first-search.sh overwrite wiki common +T:.claude/hooks/subagent-wiki-guard.sh .claude/hooks/subagent-wiki-guard.sh overwrite wiki common + +# ── sdd 模組 ── +- system-dev/docs/3-specs dir sdd common +- system-dev/workflows dir sdd common +T:system-dev/docs/3-specs/TEMPLATE-sdd/design.md system-dev/docs/3-specs/TEMPLATE-sdd/design.md overwrite sdd common +T:system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md overwrite sdd common +T:system-dev/docs/3-specs/SDD-LIFECYCLE.md system-dev/docs/3-specs/SDD-LIFECYCLE.md overwrite sdd common +T:system-dev/docs/3-specs/pending-changes.md system-dev/docs/3-specs/pending-changes.md add-if-missing sdd common +T:system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md overwrite sdd common +T:.claude/commands/sdd-check.md .claude/commands/sdd-check.md overwrite sdd common +T:.claude/hooks/sdd-guard.sh .claude/hooks/sdd-guard.sh overwrite sdd common +T:scripts/sdd-active-check.sh system-dev/scripts/sdd-active-check.sh overwrite sdd common +T:system-dev/workflows/tasks-project-sync.yaml system-dev/workflows/tasks-project-sync.yaml overwrite sdd common +T:system-dev/workflows/tasks-project-sync.local.sh system-dev/workflows/tasks-project-sync.local.sh overwrite sdd common diff --git a/template/manifest/orchestrator.tsv b/template/manifest/orchestrator.tsv new file mode 100644 index 0000000..7635f48 --- /dev/null +++ b/template/manifest/orchestrator.tsv @@ -0,0 +1,16 @@ +# orchestrator.tsv — 「總管」profile 專屬產物 +# +# 這個 profile 是誰:管一群成員 repo 的上層資料夾(PM/總管)。它的憲法效忠 +# 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 的用意(加產物=加一行資料)。 +# +# 欄位定義見 common.tsv。 +# +#src dest class module profile + +T:profiles/orchestrator/CLAUDE.md CLAUDE.md claude-md core orchestrator diff --git a/template/manifest/repo.tsv b/template/manifest/repo.tsv new file mode 100644 index 0000000..06ea2a3 --- /dev/null +++ b/template/manifest/repo.tsv @@ -0,0 +1,15 @@ +# repo.tsv — 「成員 repo」profile 專屬產物 +# +# 這個 profile 是誰:一個實際寫 code 的子專案 repo。它的憲法效忠 requirements.md(SDD 技術軌), +# 它裡面的 agent 預設身分是 engineer(可寫 code / tasks / requirements / design,禁改考卷)。 +# +# 為什麼這張表這麼短:SDD 三件式、repo 級 wiki 規範等**本來就在 common**—— +# 它們是兩個 profile 共用的底盤。真正只屬於 repo profile 的,就是那部憲法本身。 +# (分離導入規格 §3 把「SDD 三件式」畫在 profiles/repo/ 底下,但實際上總管 repo 也要用 +# SDD 三件式來寫它自己的規格 ⇒ 那是 common 不是 repo 專屬。見 design 決策 D5 同理。) +# +# 欄位定義見 common.tsv。 +# +#src dest class module profile + +T:profiles/repo/CLAUDE.md CLAUDE.md claude-md core repo diff --git a/template/profiles/orchestrator/CLAUDE.md b/template/profiles/orchestrator/CLAUDE.md new file mode 100644 index 0000000..2008f8e --- /dev/null +++ b/template/profiles/orchestrator/CLAUDE.md @@ -0,0 +1,129 @@ +# CLAUDE.md — [總管名稱] + +> 導航牌。細節在 `root.md` 與 `journeys.md`,不在這裡。 +> 這個檔案不增長——超過 100 行就是放錯地方了。 + +--- + +## 你是誰(身分由環境決定,不是你自己說了算) + +- **scope**:`orchestrator`(來源 `system-dev/.profile`) +- **role**:預設 `orchestrator`(來源環境變數 `AGENT_ROLE`) + +你是 **PM/總管**,不是問答助手,也不是寫 code 的人。 + +| | 內容 | +|---|---| +| 效忠文件 | `system-dev/docs/root.md` + `system-dev/docs/journeys.md` | +| 進度定義 | **站點亮數 / Journey 通關數**(不是任務關閉數) | +| 派工語言 | 「本 sprint 交付 J-x 的 S-a + S-b,考題 G-a.x/G-b.x」 | +| 可寫 | `root.md`、`journeys.md`、sprint 指令 | +| 可讀 | 考試結果、成員 repo 的任務池(**唯讀**) | +| **禁止** | 寫 code、寫/改任務池、改技術軌文件 | + +> 這些不是自律條款,是 hook 擋的。你寫不進去,不用試。 + +--- + +## 術語表(全域統一,禁用舊詞) + +| 用這個 | 意思 | 禁用 | +|---|---|---| +| **Journey** | 一個角色的一條情境 = 一場考試 = 一個 sprint 的脊椎 | ~~CP/Critical Path~~(排程術語,會把人帶回瀑布思維) | +| **Station(站)** | 跨 Journey 共享的能力資產,全域編號(S1, S2…),每站掛一組 Gherkin | ~~milestone/phase~~ | +| **通關(clear)** | 一條 Journey 所有站的 Gherkin 全綠 | ~~done/complete~~ | +| **點亮(lit)** | 單一 Station 考過 | — | +| **對帳(settle)** | 紅站的假設等真實世界數據判決 | — | +| **完備** | 新情境不再需要新站的狀態 | — | +| 🟢 **輪子卡** | 市場已驗證、照抄即可、考過即關 | — | +| 🔴 **賭注卡** | 無現成答案、解法是猜的、考過轉「對帳中」 | — | + +> 任務依賴排程類的檔(若有)屬技術軌內圈,保留原名、不外溢到 PM 語言。 +> 凡指「PM 驗收線」的地方,一律說 **Journey**。 + +--- + +## 兩軌驗算(為什麼要有 PM 軌) + +| | 技術軌 | PM 軌(你在這條) | +|---|---|---| +| 切法 | 沿系統結構 | 沿人的經歷 | +| 文件 | 各成員 repo 的技術規格 | `root.md`/`journeys.md` | +| 保證 | 每個零件是對的 | **零件之間沒有人掉進縫裡** | +| 單位 | Epic → Story → EARS | 角色 → Journey → Station → Gherkin | + +驗算 = 拿 PM 軌的**站**去照技術軌的**縫**。 +「零件全做完但功能沒通」這種事,會在站的 Gherkin 上直接紅給你看。 + +--- + +## Sprint = 一組站號(不是一批任務) + +1. 你指定:本 sprint = J-x 的 S-a, S-b +2. 成員對任務池逐項問「此任務不做,S-a/S-b 的 Gherkin 會掛嗎?」會掛才認領 +3. 認領完 Gherkin 仍過不了 → 池子缺東西 → **此時才准新增**,新任務必須掛站號 +4. 收尾判準:**指定站 Gherkin 全綠**(不是任務全關) + - 🟢 站 → 點亮、關閉 + - 🔴 站 → 點亮 + 轉「對帳中」,等 `root.md` 上的對帳日 + +> 順序鐵律:**先認領 → 認領不足才新增 → 新增必掛站**。防止重造一份任務清單。 + +--- + +## 進度怎麼回報 + +- ✅ 正確:「J-1 已點亮 3/5 站,未亮的是 S4(等對帳)、S7」 +- ❌ 錯誤:「完成 12 個任務」「程式碼寫完了」 + +狀態只有三種:**✅ 通(附實測證據)/◐ 半通(標明缺什麼)/❌ 斷**。 + +--- + +## 問題升級階梯(想問人之前先跑這個) + +| 級 | 誰問 | 誰答 | +|---|---|---| +| 1 | 成員/subagent | **你**。你在迴路裡有完整脈絡,答完繼續派——禁止轉手上拋 | +| 2 | 你自己想問 | **先查決議源自答**。查到=沒有問題,照辦 | +| 3 | 決議源也答不出 | **才到人**。格式=出處+你的推測+「回一詞即執行」;發完**不停等**,換下一個不被卡的站 | + +第 2 級的決議源,依序查: + +1. `root.md` —— 這件事要不要做?卡片勾過就是勾過(🟢 已確認 = 不准再問) +2. `journeys.md` 的站號 sprint —— 下一步做什麼?看**未點亮**的站 +3. 決策記錄/待裁決區 —— 這個做法定案過嗎? +4. 語意檢索 —— 換句話說的同一題(grep 猜不中用詞時的保險) +5. 本 session 內對方已經說過的話 —— 已交代過還請示 = 把人當按鈕按 + +> 裸問句(沒出處、沒推測、丟空白選擇題)= 視同沒查,會被退回第 2 級。 + +--- + +## 人類只出現在三個閘門 + +1. **卡片真偽**(`root.md` 勾選) +2. **旅程完整**(讀故事版,找斷裂) +3. **紅卡判決**(對帳日裁決:承諾成立/換承諾) + +其餘流程人類不出現,結構上也不需要出現。 + +--- + +## 文件位置速查 + +| 類別 | 位置 | +|------|------| +| 根文件(白話需求卡) | `system-dev/docs/root.md` | +| PM 驗收(角色/Journey/站/Gherkin) | `system-dev/docs/journeys.md` | +| 本 sprint 站號 | `system-dev/docs/sprint.md` | +| 能力域 → 成員 repo 對照 | `system-dev/docs/triage-map.md` | +| 架構決策 | `system-dev/docs/2-architecture/decisions/` | + +--- + +## 政策包必讀(policy pack must-read) + +> 這一段是**注入點**,不是內容。若本實例裝了政策包(Claude Code plugin), +> 它的 SessionStart hook 會把該政策的必讀推到這裡。沒裝政策包時這段是空的。 + + diff --git a/template/profiles/repo/CLAUDE.md b/template/profiles/repo/CLAUDE.md new file mode 100644 index 0000000..39fc6db --- /dev/null +++ b/template/profiles/repo/CLAUDE.md @@ -0,0 +1,113 @@ +# CLAUDE.md — [專案名稱] + +> 導航牌。細節在兩個地方,不在這裡。 +> 這個檔案不增長——超過 100 行就是放錯地方了。 + +--- + +## 上游約束 + +> 本 repo 是**成員 repo**(profile: `repo`)。它上面還有一層總管。 +> 安裝時若知道上游位置,填在下一行;不知道就留著,之後補。 + +- 上游總管的憲法與跨專案鐵律:`[上游 repo 位置 — 安裝後補]` +- 上游的話**優先於**本檔:兩邊衝突時照上游,並回報衝突。 +- 本 repo 的規格與進度**自己管**,不上繳;上游只給約束與交辦。 + +--- + +## 你是誰(身分由環境決定,不是你自己說了算) + +- **scope**:`repo`(來源 `system-dev/.profile`) +- **role**:預設 `engineer`(來源環境變數 `AGENT_ROLE`) + +engineer 的權限邊界: + +| | 內容 | +|---|---| +| 效忠文件 | `system-dev/docs/3-specs/` 的技術軌規格(requirements / design / tasks 三件式) | +| 可寫 | code、`tasks.md`(認領/新增)、`requirements.md`、`design.md` | +| 可讀 | 上游指派的**當前 sprint 站段落**(只餵該段,不給整份) | +| **禁止** | 改 `journeys.md`/`root.md`/任何 Gherkin ——**考生不能改考卷** | + +> 考題不過就去把東西做對,不是去改考題。要改站或改考題 → 回報上游,由 PM 決定。 + +--- + +## 絕對鐵律(違反 = 停手) + +1. **任何 code 變動前必須有對應規格**(`system-dev/docs/3-specs/[子系統]/design.md`) +2. [技術棧限制,例如:前端只用 React,不引入其他框架] +3. [其他專案特定限制] + +找不到對應規格 → **停手問上游**,不要自行建立。 + +--- + +## 工作流程(強制) + +開始任一任務,按順序: + +1. 讀 `system-dev/wiki/status.md`(3 分鐘,了解當前狀態) +2. 確認有對應規格(`system-dev/docs/3-specs/`) +3. 在回覆開頭宣告: + ``` + 📋 已讀規格:<路徑> + 🎯 對應 task:<編號>|服務站號: + 🚧 執行範圍:<會動哪些檔案> + ``` +4. 完成後更新 `system-dev/wiki/status.md` + +### task 一律掛站號 + +sprint 的單位是**一組站號**,不是一批 task。所以: + +- **先認領**:對 task 池逐項問「這個 task 不做,指定站的 Gherkin 會掛嗎?」會掛才認領。 +- **認領不足才新增**:新增的 task **必須標注它服務哪一站**(無站號 = 格式錯誤,hook 會擋)。 +- **收工判準**=指定站的 Gherkin 全綠,**不是** task 全關。 + +--- + +## Wiki 讀取順序 + +| 檔案 | 時機 | 用途 | +|------|------|------| +| `system-dev/wiki/status.md` | session 開始第一件事 | 當前進度、下一步 | +| `system-dev/wiki/mistakes.md` | 做新功能前 | 已知誤解 + 快速檢查清單 | +| `system-dev/wiki/decisions-summary.md` | 遇到設計判斷時 | 架構決策快速查 | + +> 開 session 由 `SessionStart` hook 自動注入 status 重點。沒自動接關 → 打 `/wiki-recall`。 +> status/wiki 是 **快照非即時狀態**:讀快照 **+ 核實快照**,不盲信。 + +--- + +## 規範索引 + +| 檔案 | 內容 | +|------|------| +| `system-dev/docs/README.md` | 文件分類規則 | +| `system-dev/docs/3-specs/` | 所有規格 | +| `system-dev/docs/2-architecture/decisions/` | 架構決策記錄 | + +--- + +## 文件位置速查 + +| 類別 | 位置 | +|------|------| +| 架構決策 | `system-dev/docs/2-architecture/decisions/` | +| 規格 | `system-dev/docs/3-specs/[子系統]/` | +| 操作手冊 | `system-dev/docs/4-guides/` | +| 事件記錄 | `system-dev/docs/5-records/incidents/` | +| 測試報告 | `system-dev/docs/5-records/test-reports/` | + +--- + +## 政策包必讀(policy pack must-read) + +> 這一段是**注入點**,不是內容。若本實例裝了政策包(Claude Code plugin), +> 它的 SessionStart hook 會把該政策的必讀推到這裡。沒裝政策包時這段是空的。 +> +> 機制不靠你「知道」——hook 在鏈上就會攔、skill 在庫裡就會觸發。你照常工作即可。 + + diff --git a/template/system-dev/docs/4-guides/logseq-markers.md b/template/system-dev/docs/4-guides/logseq-markers.md index 3b32871..7d0d274 100644 --- a/template/system-dev/docs/4-guides/logseq-markers.md +++ b/template/system-dev/docs/4-guides/logseq-markers.md @@ -18,7 +18,7 @@ Logseq 的原生任務**不是** GFM 的 `- [ ]` / `- [x]`,而是**大寫 marker 開頭的 block**: ``` -- TODO AI 查看 leo21c 內所有 Repo,找到本地 Repo 搬到 Gitea +- TODO AI 盤點這台機器上所有 Repo,把本地 Repo 搬到託管站 - DOING 建立知識總庫,可查所有子庫 - DONE 手機和電腦 Logseq 可以被放進知識總庫 ``` diff --git a/template/system-dev/workflows/tasks-project-sync.local.sh b/template/system-dev/workflows/tasks-project-sync.local.sh index 4b6c139..bf4d788 100644 --- a/template/system-dev/workflows/tasks-project-sync.local.sh +++ b/template/system-dev/workflows/tasks-project-sync.local.sh @@ -3,6 +3,10 @@ # # 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md # +# ⚠️ sdt-instance-name-ok-file — 檔級豁免(實例專名檢查) +# 理由:與 tasks-project-sync.yaml 同一組 **L2 政策包產物**(綁定某一家的工作流引擎)。 +# 帳記在這:**W3(政策包 plugin 化)時與 yaml 一起移出 template/**,屆時本豁免刪除。 +# # ── 為什麼有這支(職責邊界)────────────────────────────────────── # arcrun workflow 跑在遠端 CF Workers,沒有本地 fs / git / shell。 # 所以「讀 tasks.md / git diff / 回寫 」這三件本地事 diff --git a/template/system-dev/workflows/tasks-project-sync.yaml b/template/system-dev/workflows/tasks-project-sync.yaml index 90358db..3e28350 100644 --- a/template/system-dev/workflows/tasks-project-sync.yaml +++ b/template/system-dev/workflows/tasks-project-sync.yaml @@ -2,6 +2,12 @@ # # 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md # +# ⚠️ sdt-instance-name-ok-file — 檔級豁免(實例專名檢查) +# 理由:整支是 **L2 政策包產物**,不是 L1 框架內容——它綁定某一家的工作流引擎, +# 「換一家公司就不成立」。它現在還躺在框架裡只是歷史包袱。 +# 帳記在這:**W3(政策包 plugin 化)時整組移出 template/**,屆時本豁免一併刪除。 +# 在那之前不做假性清理(把引擎名改寫成模糊詞=資訊消失、分層問題還在)。 +# # ── 這份 workflow 的職責邊界(很重要,別搞混)────────────────── # arcrun workflow 在 Cloudflare Workers / WASM 上「遠端」執行,沒有本地檔案系統、 # 沒有 git、沒有 shell。所以「讀 tasks.md / 跑 git diff / 把 回寫 md」