Compare commits
15 Commits
146d3c4875
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 593defceb1 | |||
| 2f5d9f3bb2 | |||
| 6a49f25aef | |||
| 2a8c259d08 | |||
| dc4fe67e15 | |||
| 27c207c3e9 | |||
| 3eeded0432 | |||
| c25babf4bb | |||
| b027044cb4 | |||
| 0a04c9c594 | |||
| 4d14b05d81 | |||
| ed3a97c6a2 | |||
| 7b83465acf | |||
| 989a6fbfed | |||
| 9a5219d8bd |
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# pre-commit — commit 前跑框架的機械閘
|
||||||
|
#
|
||||||
|
# 啟用(每個 clone 做一次,之後跟著 repo 走):
|
||||||
|
# git config core.hooksPath .githooks
|
||||||
|
#
|
||||||
|
# 為什麼版控在 .githooks/ 而不是 .git/hooks/:
|
||||||
|
# .git/hooks/ 不會被 clone 帶走 ⇒ 換一台機器、換一個人,閘就悄悄消失了,
|
||||||
|
# 而且沒有任何跡象。放進版控,至少「有沒有啟用」是查得到的事實。
|
||||||
|
#
|
||||||
|
# 為什麼不掛雲端 workflow:
|
||||||
|
# 本組織禁止在 repo 上掛自動化 workflow(歷史上「一個 push 觸發大量自動化」
|
||||||
|
# 正是帳號被停權的原因)。閘跑在人的機器上、commit 的那一刻。
|
||||||
|
#
|
||||||
|
# 真的要跳過(例如緊急修):git commit --no-verify
|
||||||
|
# ——留痕可審,不是不可能繞過。
|
||||||
|
|
||||||
|
exec bash scripts/check-all.sh
|
||||||
+21
-1
@@ -31,7 +31,27 @@ skills/editorial-image/
|
|||||||
/docs/README.md
|
/docs/README.md
|
||||||
/docs/1-vision/
|
/docs/1-vision/
|
||||||
/docs/2-architecture/
|
/docs/2-architecture/
|
||||||
/docs/3-specs/
|
|
||||||
|
# ── /docs/3-specs/ 例外(2026-08-05,依 D22 翻正舊政策)──────────────
|
||||||
|
# D22 已翻案:Gitea private 除機敏值外全 push——雲端工人靠 clone,docs 缺=斷糧。
|
||||||
|
# 舊規則把整個 3-specs 擋掉,害「本 repo 自己的 SDD」只活在一台硬碟上:
|
||||||
|
# clone 不到、雲端 CC 讀不到、沒備份。SDD 是進度真相源,必須進版控。
|
||||||
|
# 公開外洩風險已由 scripts/github-publish-exclude.txt 蓋掉(docs 整包不進 GitHub mirror)。
|
||||||
|
#
|
||||||
|
# 作法:只擋子項(用 /*),再逐個放行真 SDD。
|
||||||
|
# 放行=本 repo 自己寫的規格(真相源)
|
||||||
|
# 續擋=與 template/system-dev/docs/3-specs/ 逐位元組相同的自裝副本(零資訊、徒增重複)
|
||||||
|
/docs/3-specs/*
|
||||||
|
!/docs/3-specs/cross-repo-signing/
|
||||||
|
!/docs/3-specs/install-layout/
|
||||||
|
!/docs/3-specs/tasks-project-projection/
|
||||||
|
!/docs/3-specs/wiki-architecture/
|
||||||
|
!/docs/3-specs/jdd-dual-profile/
|
||||||
|
!/docs/3-specs/pending-changes.md
|
||||||
|
|
||||||
/docs/4-guides/
|
/docs/4-guides/
|
||||||
/docs/5-records/
|
/docs/5-records/
|
||||||
/docs/6-user/
|
/docs/6-user/
|
||||||
|
|
||||||
|
# GitHub public mirror 產出(本機暫存,不進私有真相源)
|
||||||
|
.github-public/
|
||||||
|
|||||||
@@ -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 上看得見。
|
||||||
+170
@@ -10,6 +10,176 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1.19.0 — 一套模板,兩種身分:總管版與成員版
|
||||||
|
|
||||||
|
**這一版你會多出什麼**
|
||||||
|
|
||||||
|
- **裝的時候可以選「這是什麼」**:`--profile=repo`(實際寫程式的專案)或
|
||||||
|
`--profile=orchestrator`(管一群專案的上層資料夾)。不選就自動偵測、問你一次,
|
||||||
|
之後記住不再問。**兩種身分讀到的規則完全不同**——總管版看不到技術細節那一套,
|
||||||
|
成員版會帶一行指回上游。
|
||||||
|
- **CLAUDE.md 從此分成兩區**:上面是「模板維護的」、下面是「你自己寫的」,
|
||||||
|
中間有看不見的分隔標記。以前這兩者混在一起,導致更新**永遠不敢覆蓋**,
|
||||||
|
模板後來改的規則就送不到你手上。現在上面那區可以安全更新,下面那區永遠不動。
|
||||||
|
- **更新會告訴你「哪些檔被手改過」**:改過的**不覆蓋**,新版另存 `.new` 讓你自己比對。
|
||||||
|
(實測一個真實使用中的專案:6 支模板 hook 裡 **4 支已被手改**,而在這之前
|
||||||
|
沒有任何機制知道這件事——那 4 支從此收不到任何修正。)
|
||||||
|
- **PM 那一套工作方法有了現成範本**(總管版才裝):白話需求卡、使用者旅程與考題、
|
||||||
|
以站為單位的衝刺表、能力域分派表。填了之後更新永遠不會覆蓋你的內容。
|
||||||
|
- **多了六道自動攔截**,全部是「做錯的當下就擋」而不是事後提醒:
|
||||||
|
- 寫程式的人**改不了驗收考題**(改考題就能「通過」的漏洞堵住了)
|
||||||
|
- 當 PM 的**寫不了程式、改不了任務清單**
|
||||||
|
- 專案**改不了模板發下來的機制**(要改就回上游提案,一次修好所有人的)
|
||||||
|
- 需求卡如果是「賭注」卻沒寫對帳日 → 擋
|
||||||
|
- 新任務沒說它服務哪一站 → 擋
|
||||||
|
- 給人看的文件混進技術術語 → 擋
|
||||||
|
- 動了程式 → 主動告訴你要重驗哪幾題
|
||||||
|
- **收工的判準換了**:不再是「任務都關了」,而是「**指定的那幾站考題全綠**」。
|
||||||
|
|
||||||
|
**修掉的老問題(都是靜默失敗,你不會收到錯誤訊息的那種)**
|
||||||
|
|
||||||
|
- **新安裝的人拿不到最近三版的招牌功能**:安裝和更新各有一份手抄的檔案清單、
|
||||||
|
早就對不上——更新會裝的四個檔,安裝從來不裝。**先裝舊版再更新的人反而拿得比較多。**
|
||||||
|
現在兩邊讀同一份清單,這種漂移在結構上不可能再發生。
|
||||||
|
- **更新會被「找不到頁面」騙**:以前只檢查「下載回來的檔案不是空的」,
|
||||||
|
但錯誤頁面也不是空的 ⇒ **好檔案被無聲覆寫成一行垃圾**(曾經有份文件從 260 行變 1 行)。
|
||||||
|
- **更新遇到新資料夾會失敗,但版本號照升**(典型的假成功,半年前就被記下來、一直沒修)。
|
||||||
|
|
||||||
|
**給模板維護者**
|
||||||
|
|
||||||
|
- 安裝清單改成資料表(`template/manifest/*.tsv`),加產物 = 加一行,不必改腳本
|
||||||
|
- 兩道機械閘:範本混入特定專案名 → 擋;已發佈的檔案路徑被搬走 → 擋
|
||||||
|
(後者防的是「舊版使用者更新時整排失敗,而且不會有下一次更新來修它」)
|
||||||
|
- `git config core.hooksPath .githooks` 啟用 commit 前自動檢查
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.18.0 — 提醒你「改好的東西還沒發佈出去」
|
||||||
|
|
||||||
|
> 📌 補記於 2026-08-06:這一版當時發佈了(版號升了、功能也出貨了),
|
||||||
|
> **但漏了寫這則紀錄**。而更新跑完的最後一行正是叫你「改了什麼看 CHANGELOG.md」——
|
||||||
|
> 版號動了卻查不到動了什麼,等於沒交代。依實際 commit 內容補回。
|
||||||
|
|
||||||
|
**這一版你會多出什麼**
|
||||||
|
|
||||||
|
- **開啟工作階段時,會告訴你「公開版落後了」**:
|
||||||
|
草稿區與公開區是**手動同步**的,改完東西若沒人記得發佈,
|
||||||
|
外部使用者抓到的還是舊版——**而且不會有任何錯誤訊息,只是行為不對**,
|
||||||
|
所以自己人永遠測不出來。這個提醒就是來消滅「忘了發佈」這個失敗模式。
|
||||||
|
- 如果這批改動含編譯產物,會**額外標紅**(因為安裝器是直接去公開位址抓那個檔的)。
|
||||||
|
- **只提醒、不阻擋**——發不發佈是人的決定。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.17.0 — 查詢一律從最強的查法開始(語意 → 關鍵字 → grep)
|
||||||
|
|
||||||
|
**leo 2026-07-21**:「它一定是用最好的搜尋,如果沒有才 fallback,
|
||||||
|
**但那不是你要指定的**——對搜尋者來說,我就是要去搜尋,如果你沒這個機制才降。」
|
||||||
|
|
||||||
|
**事故**:查「CF 上的 git 託管」時只用 grep,搜 Gitea/freeze 等字面詞 → 零命中,
|
||||||
|
結論寫成「這件事沒查過、申請表沒送」=指控負責人沒做他早就做過的事。
|
||||||
|
事後用**同一個問題**跑語意搜尋,**第一筆就命中**(score 0.858):
|
||||||
|
「Cloudflare Artifacts:假設內建 git 倉庫機制的 CF 功能,成立則可全 CF 化」,
|
||||||
|
還帶出三元組「Artifacts >> 若提供 git 倉庫則可取代 >> Gitea」——負責人 15 天前就記了。
|
||||||
|
|
||||||
|
**根因不是「關鍵字選錯」,是「用了三種查詢裡最弱的那種」**——
|
||||||
|
grep 只認字面,**要求你先猜對那個詞**;語意搜尋不需要你猜對。
|
||||||
|
|
||||||
|
修正:
|
||||||
|
- `subagent-wiki-guard`:注入的指示改為**分級查法**——
|
||||||
|
①語意搜尋(`kbdb_search mode=semantic`,用自然語言問句)②關鍵字 ③grep(最後手段)
|
||||||
|
- `wiki-first-search`:① **grep 零命中不再靜默退出**(那正是最該改用語意搜尋的時刻,
|
||||||
|
查不到 ≠ 沒記載,只代表沒猜中用詞)② 有命中時明說「這是最弱的查法、
|
||||||
|
搜尋詞是猜的,重要判斷請補語意搜尋」
|
||||||
|
- 兩處都附真實案例,讓讀到的人知道代價
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.16.1 — 補破口:wiki-first-search 漏掉 Bash(隔天就被自己繞過)
|
||||||
|
|
||||||
|
**1.16.0 上線隔天即實證失效**:hook 只掛 `Grep|Glob|Read`,但「用 curl/wrangler
|
||||||
|
亂試部署方法」走的是 **Bash** → 整支 hook 不觸發。
|
||||||
|
|
||||||
|
leo 當場點破:「昨天我已經發信給你看了,你今天還說系統不對,**就表示這件事沒記錄下來?
|
||||||
|
還是你凡是要查就會去查 wiki?**」
|
||||||
|
→ 查證:wiki `status.md` 早記著「landing 寄信 live、實測 leo21c 連 4 次成功」,
|
||||||
|
**記錄在、我沒查**;而昨天做的 hook **蓋不到我實際用的工具**。
|
||||||
|
|
||||||
|
修正:
|
||||||
|
- matcher 加 `Bash`;只認會動外部系統的高風險指令
|
||||||
|
(`wrangler|curl|npx|acr|gh|deploy|push`),避免每個 `ls` 洗版
|
||||||
|
- 從指令中取最具識別度的詞當搜尋詞(濾掉 https/accounts/workers 等雜訊)
|
||||||
|
- `update.sh` 對**既有註冊**就地補 `Bash`(不只新裝才有)
|
||||||
|
|
||||||
|
> 教訓:**防「跳過 wiki」的機制,本身要蓋到所有實際查詢途徑**,
|
||||||
|
> 否則就是換個工具照樣跳過。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.16.0 — 讓 wiki 真的被讀到:查詢即搜尋+subagent 自動注入
|
||||||
|
|
||||||
|
**病根(leo 2026-07-20 點破,真實事故)**:總管三次擋回 leo「某機制早已棄用」的正確判斷,
|
||||||
|
查證後 leo 全對。根因不是知識不足,是**讀取流程**:
|
||||||
|
① wiki 只讀開頭就開工(關鍵記載在第 56 行,答案一直在那裡)
|
||||||
|
② 派 subagent 只叫它讀 code、沒叫讀 wiki → agent 從稿子推論,**必然**得出過時結論
|
||||||
|
③ 把 wiki 的「當時狀態」當永久事實(沒核對解除條件)
|
||||||
|
|
||||||
|
leo:「我需要的不是你記住,而是如何解這題不再發生,**機制面的解法**」
|
||||||
|
「如果你不是讀而是**搜尋** wiki,就不會只讀 50 行就下定論,而是像 cmd+F 那樣高亮。」
|
||||||
|
|
||||||
|
**新增兩支 hook(皆不依賴任何人自覺)**:
|
||||||
|
|
||||||
|
- **`wiki-first-search.sh`**(PreToolUse: `Grep|Glob|Read`)
|
||||||
|
在「正要去翻 code/文件」的當下,用同一組關鍵字 grep `system-dev/wiki/`,
|
||||||
|
**只推命中行**(非開場 push 全文——那必然只被讀開頭)。提醒不阻擋:
|
||||||
|
wiki 沒記載時本來就該翻原文,唯一目的是消滅「不知道 wiki 有寫」。
|
||||||
|
|
||||||
|
- **`subagent-wiki-guard.sh`**(PreToolUse: `Task`)
|
||||||
|
偵測查證/實作類任務 → **注入**「先查 wiki」指示給 subagent。
|
||||||
|
⚠️ 第一版設計為「上游 prompt 沒交代就擋下」,經 leo 指正改為注入式:
|
||||||
|
「subagent 的問題跟你一樣——**它只要聽到查,就應該主動查 wiki**,
|
||||||
|
因為每個 repo 都有維護自己的 wiki。」依賴上游記得寫指示 = 同一個病。
|
||||||
|
|
||||||
|
**安裝行為**:`update.sh` 除同步兩支 hook 外,**自動註冊進 settings.json**(不只提醒)。
|
||||||
|
理由同上——靠人看提醒手動補,等於把同一個病搬到安裝環節。
|
||||||
|
|
||||||
|
**注入給 subagent 的三條硬規則**:
|
||||||
|
1. wiki 與程式碼衝突 → **以 wiki 為準**,回報衝突,不自行用 code 推翻 wiki
|
||||||
|
2. wiki 寫「不可動/待廢除/進行中」→ 讀它的**解除條件**逐條核對(那是當時狀態,非永久禁令)
|
||||||
|
3. 翻原文後得到新結論 → 回報「wiki 該更新」(wiki 過時是債,要還)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.15.0 — SDD 生命週期鐵律:單一活性 SDD(issue #6)
|
||||||
|
|
||||||
|
leo 拍板全體系採「單一活性 SDD」制度:任何時刻每個 repo 只有一份現行 SDD(`status: active`),所有開發任務唯一對應它的 tasks。prompt 軟約束+檔案系統硬約束(hook)雙層。
|
||||||
|
|
||||||
|
- **新增 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`**:規則真相源。frontmatter 狀態標記(`active | draft | paused | closed` + `superseded_by`,機器可查)+五條鐵律(單一活性/禁止 CC 自建 SDD/規格變更只有 pending-changes.md → confirm 一條路/開新 SDD 先逐條搬舊任務才准寫 code/session 開始回報「現行規格+未完成任務 N+待裁決 proposal M」三數字)。
|
||||||
|
- **新增 `system-dev/docs/3-specs/pending-changes.md`**:規格變更緩衝區骨架(待裁決/已裁決留底)。update 走 `add_if_missing`——proposal 是用戶資料,絕不覆蓋。
|
||||||
|
- **TEMPLATE-sdd/design.md 掛 frontmatter**:`status: draft` 起手,原「> 狀態:」blockquote 移除(被 frontmatter 取代)。
|
||||||
|
- **`sdd-guard.sh` 升級**:① active >1 → **不論寫什麼檔一律 exit 2**(先收斂)② 寫 code 檔需「恰好 1 份」active,=0 擋並指向 SDD-LIFECYCLE.md ③ **向下相容**:3-specs 下沒有任何 design.md 帶 frontmatter(老 repo 未遷移)→ 退回舊行為(有 design.md 就放行+提醒補標記),避免 update 後老 repo 立刻全紅;統計一律排除 archive/ 與 TEMPLATE(否則 update 鋪新 TEMPLATE 就誤判已遷移)。
|
||||||
|
- **新增 `template/scripts/sdd-active-check.sh`**:獨立硬約束,pre-commit / CI 可掛(掛法見檔頭),active >1 → 列清單 exit 1。下游落地 `system-dev/scripts/`。
|
||||||
|
- **`/sdd-check` 加「生命週期」段**:五條鐵律摘要+session 開始三數字回報格式+「兩份 active=違規,當場糾正」。
|
||||||
|
- **template/CLAUDE.md 絕對鐵律更新**:指向 SDD-LIFECYCLE.md,濃縮五條。
|
||||||
|
- install / update 均鋪齊新檔(SDD-LIFECYCLE.md、pending-changes.md、sdd-active-check.sh、新版 hook),杜絕 1.12 時代「update 不補新檔→結構斷層」(issue #13 教訓)。
|
||||||
|
- 誠實限制不變:hook 只擋語法層明顯違規,繞道可行但留痕可審,不聲稱不可繞過。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.14.0 — vault 萃取能力:raw Logseq 筆記 → system-dev/wiki(知識一庫 ingest 前段;issue #5)
|
||||||
|
|
||||||
|
筆記 vault(Logseq graph 如 `notes`/`kb`、或 Obsidian)只有原文、需要萃。新增可**重跑、冪等**的 vault 增量萃取,把原始筆記萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`,供下游 Arcrun ingest 從 wikilink 機械拉三元組進 KBDB。**AI 只產卡片檔,不寫 KBDB、不拉三元組**(那是下游的事)。
|
||||||
|
|
||||||
|
- **新增 `/wiki-extract` 命令**(`template/.claude/commands/wiki-extract.md`):vault 專用的增量重萃入口,給 Routine / cloud-worker 反覆跑。跟 `/wiki-init`(一次性首萃)分工——首萃後 vault 持續進新筆記,改用這支做增量。跑它的是 CC/Routine=LLM 本人,**不需任何 token**。
|
||||||
|
- **content_hash 冪等**:讀 `system-dev/wiki/.extract-manifest.json`,逐檔比 sha256;hash 沒變的檔**完全不進 AI**(先擋、不是讀了才發現)。**重跑沒變動的 vault = 零 AI 呼叫、零 diff**。省 run。
|
||||||
|
- **卡片格式不重造**:完全沿用 `/wiki-init` 第五步(frontmatter tags/gloss、`## 實體`、`## 關聯` typed-edge、麵包屑、TAXONOMY 受控標籤)——讓 dev repo 與 vault repo 產出同一種卡,下游一套 ingest 通吃。
|
||||||
|
- **原文唯讀 + D16**:只寫 `system-dev/wiki/`,`journals/`/`pages/` git status 須 0 異動(改了會被 Syncthing 推回 leo 手機污染筆記);精耕知識點、不地毯灌全文。
|
||||||
|
- **新增 Logseq 任務 marker 解析(單一真相源)**:`template/system-dev/docs/4-guides/logseq-markers.md`。Logseq 原生任務是**大寫 marker** block(`- TODO …`/`- DOING …`/`- DONE …`),**不是** GFM `- [ ]`;只抓 checkbox 會漏抓 notes/kb 全部任務(leo 2026-07-04 發現,issue #4 更正)。狀態映射:TODO/LATER→`todo`、DOING/NOW→`in-progress`、WAITING→`blocked`、DONE→`done`、CANCELED/CANCELLED→`closed`;跳過 `:LOGBOOK:…:END:` 與 `key:: value` 屬性行。**這份是唯一權威**——vault 萃取(#5)與 tasks→Project 投影(#4)**共用同一套解析,不各寫一份**。marker → 任務卡 frontmatter `task_status`。
|
||||||
|
- install/update 隨 WIKI 模組帶下 `/wiki-extract`;`logseq-markers.md` 放共用區,wiki 或 sdd 任一模組在用都拿得到。
|
||||||
|
- ⚠️ **邊界**:本能力只產 `system-dev/wiki/` 卡片給下游機械撈;三元組拉取、KBDB 寫入歸下游 Arcrun ingest(另立 issue),不在此。頂層 SDD:`kb-ingest-architecture`(R1 AI 只萃/R2 萃冪等/R5 Logseq 正確性歸 template)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1.13.0 — tasks.md ⇄ GitHub Project 單向投影(optional 模組,需 Arcrun;issue #16)
|
## 1.13.0 — tasks.md ⇄ GitHub Project 單向投影(optional 模組,需 Arcrun;issue #16)
|
||||||
|
|
||||||
裝了 SDD 的專案可把 `system-dev/docs/3-specs/*/tasks.md` 的待辦**單向投影**成唯讀 GitHub Project(看板/dashboard 好抓)。md 唯一真相源、Project 永遠唯讀,不存在反向同步、不會兩個真相源打架。
|
裝了 SDD 的專案可把 `system-dev/docs/3-specs/*/tasks.md` 的待辦**單向投影**成唯讀 GitHub Project(看板/dashboard 好抓)。md 唯一真相源、Project 永遠唯讀,不存在反向同步、不會兩個真相源打架。
|
||||||
|
|||||||
+3
-3
@@ -32,7 +32,7 @@ This template solves both problems with two systems:
|
|||||||
### Option 1: New project
|
### Option 1: New project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/uncle6me-web/system-dev-template
|
git clone https://github.com/youlinhsieh/system-dev-template
|
||||||
cp -r system-dev-template/template/. your-new-project/
|
cp -r system-dev-template/template/. your-new-project/
|
||||||
cd your-new-project
|
cd your-new-project
|
||||||
```
|
```
|
||||||
@@ -46,7 +46,7 @@ Then in a CC conversation:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd your-existing-project
|
cd your-existing-project
|
||||||
curl -sSL https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts/install.sh | bash
|
curl -sSL https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main/scripts/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
The script only creates what's missing — **it never touches files you already have**.
|
The script only creates what's missing — **it never touches files you already have**.
|
||||||
@@ -80,7 +80,7 @@ Heard there are new features and want the latest? Just run this:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd your-project
|
cd your-project
|
||||||
curl -sSL https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts/update.sh | bash
|
curl -sSL https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main/scripts/update.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
It first compares your version with the latest, tells you **which new features you'd gain**, and then:
|
It first compares your version with the latest, tells you **which new features you'd gain**, and then:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ CC 是個優秀的工程師,但不是個好的專案經理。它會猛衝完
|
|||||||
### 方式一:新專案
|
### 方式一:新專案
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/uncle6me-web/system-dev-template
|
git clone https://github.com/youlinhsieh/system-dev-template
|
||||||
cp -r system-dev-template/template/. your-new-project/
|
cp -r system-dev-template/template/. your-new-project/
|
||||||
cd your-new-project
|
cd your-new-project
|
||||||
```
|
```
|
||||||
@@ -45,7 +45,7 @@ cd your-new-project
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd your-existing-project
|
cd your-existing-project
|
||||||
curl -sSL https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts/install.sh | bash
|
curl -sSL https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main/scripts/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
腳本只建立缺少的東西,**已有的檔案一律不動**。
|
腳本只建立缺少的東西,**已有的檔案一律不動**。
|
||||||
@@ -79,7 +79,7 @@ CC 會掃描現有文件、建立 wiki、整理 docs 結構。
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd your-project
|
cd your-project
|
||||||
curl -sSL https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts/update.sh | bash
|
curl -sSL https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main/scripts/update.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
它會先比對你的版本和最新版,告訴你**多了哪些新功能**,然後:
|
它會先比對你的版本和最新版,告訴你**多了哪些新功能**,然後:
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# cross-repo-signing — Design
|
||||||
|
|
||||||
|
> 狀態:已採納
|
||||||
|
> 建立:2026-06-26 | 最後更新:2026-06-26
|
||||||
|
> 負責人:leo(uncle6me-web)
|
||||||
|
> 來源:issue #12(InkStoneCo 總管)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一句話說明
|
||||||
|
|
||||||
|
所有 repo(mira / graph-plugin / ingest-plugin / Arcrun / template…)共用 `uncle6me-web` 一個 GitHub 帳號發 issue/comment,author 全顯示同一帳號、看不出來源;制度化一條鐵律——**跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容層署名溯源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景與問題
|
||||||
|
|
||||||
|
- GitHub issue/comment 的 author = 發送帳號(gh token),**沒有 per-repo 身份這設定**。
|
||||||
|
- `git config user.name` 只影響 commit 作者,**不影響 issue/comment author**。
|
||||||
|
- 給每個 repo 開獨立帳號 = 多帳號自動化 = 踩「避免被 flag」鐵律,**不可**(見 issue-handle skill 第 3 節)。
|
||||||
|
|
||||||
|
→ 身份只能在**內容層自報**(約定),不是平台層。
|
||||||
|
|
||||||
|
現狀:CC 們已部分自發(「## 回報(arcrun CC)」「## 回報(graph CC)」),但靠當次自律、不一致(總管自己也常漏署)。本 SDD 把它制度化:寫成鐵律、一處改全 repo 繼承。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 範圍
|
||||||
|
|
||||||
|
### 包含(In Scope)
|
||||||
|
- 在 template 的 issue 處理指引(`template/.claude/commands/issue-handle.md`)加一節「跨 repo 署名鐵律」。
|
||||||
|
- 因 issue-handle 是「讀/回/結案」的權威指引、所有 repo 透過 template 繼承,這是規則的正確落點。
|
||||||
|
|
||||||
|
### 不包含(Out of Scope)
|
||||||
|
- 不改 CLAUDE.md(導航牌,不增長;規則細節歸 skill 內文,與既有「採集規則放 skill」一致)。
|
||||||
|
- 不做任何平台層/自動化(不掛 hook、不改 gh 設定)——純內容約定。
|
||||||
|
- 不回改各下游 repo 的歷史 comment。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 設計
|
||||||
|
|
||||||
|
新增第 4 節「跨 repo 署名(鐵律)」於 issue-handle skill:
|
||||||
|
|
||||||
|
> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,因所有 repo 共用同一帳號、author 看不出來源,靠內容署名溯源。
|
||||||
|
> - 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`…
|
||||||
|
> - 總管下令/追問:`[InkStoneCo 總管]`
|
||||||
|
> - 署名放 comment 第一行或標題式開頭(既有「## 回報(graph CC)」即合格)。
|
||||||
|
|
||||||
|
放在第 2 節(發給別的 repo)之後、第 3 節(flag 界線)之前——順著「跨 repo 互動」的脈絡。原第 3、4 節順延。
|
||||||
|
|
||||||
|
並把第 1 節「讀/回/結案」的 comment 範例帶上署名,讓署名在最常用路徑就被看見(不只躲在後面的鐵律節)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 決策理由
|
||||||
|
|
||||||
|
- **落點選 issue-handle skill 而非 CLAUDE.md**:CLAUDE.md 是導航牌、明令不增長;issue 互動規則屬 skill 內文,與「採集規則放 skill」同構。
|
||||||
|
- **內容層而非平台層**:唯一不踩多帳號 flag 鐵律的解法。
|
||||||
|
- **署名格式 `[<repo> CC]`**:沿用 CC 們已自發的形態,降低改變成本;總管用 `[InkStoneCo 總管]` 區隔下令角色。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 升版
|
||||||
|
|
||||||
|
併入下一個 template 版本:1.11.0 → 1.12.0(規範新增、跨 repo 行為改變)。兩個 VERSION 同步。
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# install-layout — Design
|
||||||
|
|
||||||
|
> 狀態:已採納
|
||||||
|
> 建立:2026-06-26 | 最後更新:2026-06-26
|
||||||
|
> 負責人:leo(uncle6me-web)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一句話說明
|
||||||
|
|
||||||
|
把工具安裝產物從「散落在用戶根目錄(docs/、scripts/、.claude/wiki、.claude/VERSION)」收斂成「只留 CC 死綁的 .claude/ + CLAUDE.md,其餘全進 system-dev/」,並為 wiki 改寫產物正式準備落點。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景與問題
|
||||||
|
|
||||||
|
目標用戶是 low-code、只會叫 CC 做事的無技術用戶(見 memory user-profile-lowcode)。現行安裝有三個結構問題:
|
||||||
|
|
||||||
|
1. **污染用戶根目錄**:install 在根目錄鋪 `docs/`(七層子目錄)、update 時又生 `scripts/`,跟用戶自己的檔案混在一起,用戶分不清「哪個 docs 是工具的、哪個是我的」。
|
||||||
|
2. **工具資料寄生在 CC 原生資料夾**:`wiki/` 和 `VERSION` 放在 `.claude/` 裡。`.claude/` 是 CC 原生機制目錄,不該塞工具自己的資料與版號。
|
||||||
|
3. **wiki 改寫產物沒有正式落點**:`cards/`(改寫成 AI 自讀定稿 wiki 的落地處)install 從沒建立,導致實機(KB)自己長出 `.claude/wiki/cards/` 還自行 `git init`。位置該由工具準備,不靠用戶自救。
|
||||||
|
|
||||||
|
關鍵語義陷阱:現行 `docs/` 同時是「工具文件結構」與「用戶 raw source(原始文件來源)」。重構必須把這兩義拆開。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 範圍
|
||||||
|
|
||||||
|
### 包含(In Scope)
|
||||||
|
- 新增 `system-dev/` 作為工具所有「資料」的根:`system-dev/{VERSION,wiki/,docs/,scripts/}`。
|
||||||
|
- `.claude/` 只保留 CC 死綁的三樣:`settings.json`、`commands/`、`hooks/`。
|
||||||
|
- `wiki/`(含 cards/)、`VERSION`、工具的 `docs/`、`scripts/` 全部移到 `system-dev/`。
|
||||||
|
- install.sh 正式建立 `system-dev/wiki/cards/`(放 `.gitkeep` 空桶佔位)。
|
||||||
|
- 舊用戶遷移:update.sh 自動遷移 + session-start hook 自我防呆(雙保險)。
|
||||||
|
- 同步更新所有路徑引用(CLAUDE.md、SKILL.md、wiki-init、sdd-check、wiki-capture、sdd-guard、session-start-recall、INDEX、decisions-summary、README)。
|
||||||
|
|
||||||
|
### 不包含(Out of Scope)
|
||||||
|
- **不動用戶 raw source**:用戶自己的 `docs/`、Logseq `pages/+journals/`、Obsidian vault 根——工具只讀、不搬、不改名。
|
||||||
|
- **不搬 commands/ 與 hooks/ 出 .claude/**:CC slash command 與 hook 註冊路徑死綁 `.claude/`(hooks 經討論決定務實留 .claude/)。
|
||||||
|
- **不改 CLAUDE.md 位置**:CC 開 session 只讀根目錄/.claude 的 CLAUDE.md,留根。
|
||||||
|
- 不重寫 wiki 內容本身(只準備落點與路徑;既有卡片內容遷移不改寫)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 設計
|
||||||
|
|
||||||
|
### 架構概覽
|
||||||
|
|
||||||
|
```
|
||||||
|
用戶專案/
|
||||||
|
├── CLAUDE.md ← CC 原生,留根
|
||||||
|
├── .claude/ ← 只放 CC 機制檔
|
||||||
|
│ ├── settings.json ← CC 原生(hook 在此註冊)
|
||||||
|
│ ├── commands/*.md ← slash 指令,CC 死綁此路徑
|
||||||
|
│ └── hooks/*.sh ← 留 .claude/(務實決定)
|
||||||
|
└── system-dev/ ← 工具所有資料(新)
|
||||||
|
├── VERSION ← 工具版號(從 .claude/VERSION 搬出)
|
||||||
|
├── wiki/ ← 工具 wiki(從 .claude/wiki/ 搬出)
|
||||||
|
│ ├── INDEX.md TAXONOMY.md status.md mistakes.md decisions-summary.md .wikiignore
|
||||||
|
│ └── cards/ ← 【新】改寫產物落點,install 建好,.gitkeep 佔位
|
||||||
|
├── docs/ ← 工具文件(從根 docs/ 搬出)
|
||||||
|
│ ├── README.md SKILL.md
|
||||||
|
│ └── 1-vision/ 2-architecture/ 3-specs/ 4-guides/ 5-records/ 6-user/
|
||||||
|
└── scripts/ ← install.sh + update.sh,一開始就裝
|
||||||
|
```
|
||||||
|
|
||||||
|
### 關鍵決策
|
||||||
|
|
||||||
|
| 決策 | 選擇 | 原因 | 放棄的選項 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 工具資料落點 | 收進 `system-dev/` | 不污染用戶根目錄;用戶一眼分清工具 vs 自己的檔 | 散在根目錄(現狀,壞習慣) |
|
||||||
|
| 資料夾命名 | `system-dev/`(明碼) | 用戶 ls 看得到、好找 | `.sdt/`、`.system-dev/`(隱藏,low-code 用戶不易發現) |
|
||||||
|
| wiki/VERSION | 搬出 .claude/ | 工具資料不該寄生 CC 原生目錄 | 留 .claude/(現狀,職責混淆) |
|
||||||
|
| commands/hooks | 留 .claude/ | CC 機制死綁;hooks 搬走只多一層路徑、無實益 | 全搬 system-dev/(settings.json 走不了,得留跳板) |
|
||||||
|
| CLAUDE.md | 留根 | CC 自動讀,搬走整套導航/鐵律失效 | 搬 system-dev/(要留極薄跳板,等於沒搬) |
|
||||||
|
| docs 雙語義 | 拆開:工具→system-dev/docs/,raw source 維持用戶處 | 解決「哪個 docs 是誰的」 | 全搬(會誤搬用戶內容)/ 全留(污染依舊) |
|
||||||
|
| cards/ 落點 | install 正式建 + .gitkeep | 位置由工具準備,不靠用戶自救 | 不建(現狀,用戶自己長、自行 git init) |
|
||||||
|
| 舊用戶遷移 | update 自動遷移 + hook 防呆雙保險 | low-code 用戶不會手動遷;不遷會默默壞 | 只靠 update(沒跑的人壞)/ 只靠手動(用戶不會做) |
|
||||||
|
| 版本語意 | 1.9.0(中版號 + 自動遷移、向下相容到能升上來) | 提供自動遷移即非破壞性手動 | 2.0.0(若要求用戶手動遷才算) |
|
||||||
|
| 舊腳本升級撞 404(1.9.1) | ① 發佈源保留 `template/.claude/VERSION` 相容墊片 ② 新腳本驗 REMOTE_VER 須像版號 | 1.8.x 舊腳本寫死抓舊 VERSION 路徑,搬走後 curl 回「404」字串、被當內容寫進 VERSION;墊片讓舊腳本不 404,格式驗證讓未來任何路徑變動都不污染 VERSION。**bump 時兩個 VERSION 檔須同步**(system-dev/VERSION 權威 + .claude/VERSION 墊片) | 不留墊片(舊用戶撞 404);只靠 README 改 curl(救不了已跑本機舊腳本的人) |
|
||||||
|
| CLAUDE.md 等用戶檔遷移後仍寫舊路徑(1.9.2) | session-start hook 偵測 + 提示 CC 代修 | 遷移搬檔案位置,但 update.sh 鐵則「絕不碰 CLAUDE.md」(用戶資料)→ CLAUDE.md 內 `.claude/wiki` 變死引用,CC 照它找錯位置。讓腳本盲 sed 改用戶 CLAUDE.md 風險高(可能誤傷用戶自寫內容、破壞鐵則),改由 CC(懂語義、知道 raw source 的 `docs/` 要保留)代改 | 腳本自動 sed 改(誤傷風險 + 破鐵則);只寫 README(low-code 用戶不看) |
|
||||||
|
| 重複 install 製造 wiki 並存(1.10.1) | **職責切分:install 只管「全新安裝」,一切已裝過的後續(更新/遷移/補新檔)歸 update。** ① install 偵測「裝過沒」(system-dev/ 或 .claude/wiki/ 或 .claude/VERSION 任一存在,不分新舊版)→ 不動任何東西,導去 update 並 exit ② update 的 migrate_dir:目的地已存在但舊位置仍有真資料 → 記 COEXIST 警告「並存需合併」,不靜默跳過、不自動合併 ③ install↔update 互相導向(update 遇全新專案也導回 install),閉環無死結 | 根因不是「舊結構」而是「重複 install」:判準該是裝過沒、不是哪個版本。用戶先 install(建空殼)→ 再 update(遷移被冪等擋掉)→ 真資料卡舊位置、空殼佔新位置、並存。切乾淨職責後,install 永不在已裝專案動手,從源頭杜絕並存 | 只擋舊結構(新版重裝照樣亂);腳本自動合併(覆蓋風險);migrate 靜默跳過(並存無聲、用戶不知資料分裂) |
|
||||||
|
|
||||||
|
### 介面定義:遷移行為(雙保險)
|
||||||
|
|
||||||
|
**第 1 層 — update.sh 自動遷移(冪等)**
|
||||||
|
- 偵測舊位置存在 → 搬到 system-dev/:
|
||||||
|
- `.claude/wiki/` → `system-dev/wiki/`(含 cards/、含 wiki/.git,用保留 .git 的搬法)
|
||||||
|
- `.claude/VERSION` → `system-dev/VERSION`
|
||||||
|
- 根 `docs/` 中**工具自己鋪的白名單路徑**(README.md、SKILL.md、3-specs/、2-architecture/、1-vision/、4-guides/、5-records/、6-user/)→ `system-dev/docs/`
|
||||||
|
- **用戶自填的 docs 內容不搬**(白名單外的一律不動)
|
||||||
|
- system-dev/ 已存在對應檔 → 略過(可重複跑)
|
||||||
|
- 印出「已遷移 X → system-dev/」
|
||||||
|
|
||||||
|
**第 2 層 — session-start-recall.sh 自我防呆**
|
||||||
|
- 開 session 檢查舊路徑 `.claude/wiki/` 是否還在:
|
||||||
|
- 在 → 印「⚠️ 偵測到舊結構未遷移,跑 update.sh 或叫 CC 幫你遷移」
|
||||||
|
- CC 見此訊息即知該遷,可當場用檔案工具搬
|
||||||
|
|
||||||
|
### 資料模型:受影響路徑引用清單
|
||||||
|
|
||||||
|
| 檔案 | 改動類別 |
|
||||||
|
|------|---------|
|
||||||
|
| scripts/install.sh | create_dir/download 落點 docs/→system-dev/docs/、wiki→system-dev/wiki/、VERSION、新增 cards/、scripts/ 落點 |
|
||||||
|
| scripts/update.sh | update_file/keep_file 路徑、自我更新路徑、VERSION 讀點、**新增遷移段** |
|
||||||
|
| template/CLAUDE.md + 根 CLAUDE.md | 鐵律/速查表/wiki 讀取表路徑;raw source 宣告維持 |
|
||||||
|
| template/docs/SKILL.md | cards/ 落點 → system-dev/wiki/cards/;raw source 偵測語義維持 |
|
||||||
|
| .claude/commands/wiki-init.md(+template) | cards/ 落點、工具 docs 示意、SKILL 引用;raw source 偵測維持 |
|
||||||
|
| .claude/commands/wiki-capture.md、sdd-check.md(+template) | docs/2-architecture、docs/3-specs → system-dev/docs/ |
|
||||||
|
| .claude/hooks/sdd-guard.sh(+template) | 7 處 docs/3-specs → system-dev/docs/3-specs |
|
||||||
|
| .claude/hooks/session-start-recall.sh | STATUS_FILE 路徑 + 新增防呆檢查 |
|
||||||
|
| .claude/wiki/INDEX.md、decisions-summary.md(+template) | docs/2-architecture 引用 |
|
||||||
|
| README.md、README.en.md | 目錄樹示意、安裝後說明 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技術限制
|
||||||
|
|
||||||
|
- **bash 3.2 相容**(macOS 內建):腳本改動後必須 `bash -n` 過,且多位元組字元旁變數一律 `${VAR}` 包好(1.7.0/1.8.2 崩潰教訓)。
|
||||||
|
- **不可破壞 wiki/.git**:實機 KB 的 wiki 自帶獨立 git repo(192 張卡),遷移時用保留 .git 的搬法。
|
||||||
|
- **必須相容 CC 原生機制**:commands/hooks/settings.json/CLAUDE.md 路徑不可違反 CC 載入慣例。
|
||||||
|
- **遷移必須冪等**:update.sh 可被重複跑(含「以為沒更新再跑一次」)而不出錯。
|
||||||
|
- **curl | bash 串流安全**:update.sh/install.sh 走遠端串流執行,不可在多位元組邊界出 unbound variable。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 驗收標準
|
||||||
|
|
||||||
|
完成的定義:
|
||||||
|
- [ ] 新裝(install.sh):根目錄只出現 `.claude/` + `CLAUDE.md`,其餘全在 `system-dev/`;`system-dev/wiki/cards/` 存在且有 .gitkeep。
|
||||||
|
- [ ] `system-dev/VERSION` 存在且 .claude/ 下無 VERSION。
|
||||||
|
- [ ] 舊用戶跑 update.sh:舊 `.claude/wiki/`、根工具 `docs/`、`scripts/` 自動遷入 system-dev/,wiki/.git 完好,可重複跑不出錯。
|
||||||
|
- [ ] 未遷移用戶開 session:hook 印出防呆提示,不默默失敗。
|
||||||
|
- [ ] 所有路徑引用無殘留死連結(grep 不到非 raw-source 語義的舊 `docs/3-specs`、`.claude/wiki` 寫死路徑)。
|
||||||
|
- [ ] raw source 語義引用維持指向用戶原始文件(未被誤改成 system-dev/)。
|
||||||
|
- [ ] `bash -n` 通過 install.sh 與 update.sh。
|
||||||
|
- [ ] 實機 KB 套用後:開 session 接關正常、/wiki-init 寫入 system-dev/wiki/cards/、192 張卡與其 .git 完整。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相關文件
|
||||||
|
|
||||||
|
- memory: user-profile-lowcode(目標用戶輪廓)
|
||||||
|
- memory: sdd-rule-applies-to-this-repo(本 repo 也守 SDD 鐵律)
|
||||||
|
- 前置 hotfix:1.8.2(update.sh bash 3.2 崩潰修復,已 push)
|
||||||
|
- 前置:1.8.1(補裝 Cowork SKILL.md,已 push)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# install-layout — Tasks
|
||||||
|
|
||||||
|
> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。
|
||||||
|
> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1:腳本核心(install / update + 遷移)
|
||||||
|
|
||||||
|
### 前置條件
|
||||||
|
- [x] design.md 已審核(用戶批准)
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [x] 1.1 install.sh:docs/ create_dir+download 落點全改 system-dev/docs/
|
||||||
|
- 驗收:✅ grep 無殘留;raw source 偵測段 `RAW_SOURCE="docs/"` 正確保留
|
||||||
|
- [x] 1.2 install.sh:.claude/wiki/* 與 .claude/VERSION 落點改 system-dev/
|
||||||
|
- 驗收:✅ 落點全 system-dev/;新增 system-dev/VERSION download
|
||||||
|
- [x] 1.3 install.sh:新增 create_dir system-dev/wiki/cards + 寫 .gitkeep
|
||||||
|
- 驗收:✅ 已加 create_dir + .gitkeep 寫入
|
||||||
|
- [x] 1.4 install.sh:scripts 落點改 system-dev/scripts/(一開始就裝)
|
||||||
|
- 驗收:✅ 加 SCRIPTS_URL + download install/update 到 system-dev/scripts/
|
||||||
|
- [x] 1.5 update.sh:所有路徑改 system-dev/(wiki/docs/scripts/VERSION)
|
||||||
|
- 驗收:✅ grep 無殘留;bash -n 過;commands/hooks 正確留 .claude/
|
||||||
|
- [x] 1.6 update.sh:新增冪等遷移段(舊位置→system-dev/,保留 wiki/.git,白名單只搬工具 docs)
|
||||||
|
- 驗收:✅ 沙盒測試全綠——wiki(含.git commit一致)/VERSION/SKILL/docs 搬移正確、冪等(第二次0項)、用戶自填 docs 保留
|
||||||
|
- [x] 1.7 兩腳本 bash -n + 多位元組旁變數 ${} 包好
|
||||||
|
- 驗收:✅ install.sh + update.sh bash -n 通過
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2:路徑引用同步(.md / .sh)
|
||||||
|
|
||||||
|
> 前置條件:Phase 1 完成
|
||||||
|
|
||||||
|
- [x] 2.1 CLAUDE.md(template + 根):鐵律/速查/wiki 讀取表路徑;raw source 宣告維持
|
||||||
|
- [x] 2.2 SKILL.md(template + 根):cards/ 落點改 system-dev/wiki/cards/;raw source 偵測維持
|
||||||
|
- [x] 2.3 wiki-init.md(.claude + template):cards/、工具 docs 示意、SKILL 引用;raw source 偵測維持
|
||||||
|
- [x] 2.4 wiki-capture.md / sdd-check.md(.claude + template):docs/ → system-dev/docs/
|
||||||
|
- [x] 2.5 sdd-guard.sh(.claude + template):7 處 docs/3-specs → system-dev/docs/3-specs
|
||||||
|
- [x] 2.6 session-start-recall.sh:STATUS_FILE 路徑 + 新增防呆檢查
|
||||||
|
- [x] 2.7 INDEX.md / decisions-summary.md(.claude + template):docs/2-architecture 引用
|
||||||
|
- [x] 2.8 README.md / README.en.md:目錄樹示意 + 安裝後說明
|
||||||
|
- 驗收(2.1–2.8):全 repo grep 無殘留非 raw-source 語義的舊路徑;raw source 引用未被誤改
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3:版本、文件、提交
|
||||||
|
|
||||||
|
> 前置條件:Phase 1+2 完成、bash -n 過
|
||||||
|
|
||||||
|
- [x] 3.1 bump template/.claude/VERSION → 1.9.0(注意:VERSION 檔本身也要隨結構搬到 system-dev/,但發佈源 template 內的相對位置同步調整)
|
||||||
|
- [x] 3.2 CHANGELOG 記 1.9.0(結構重構 + 遷移行為)
|
||||||
|
- [x] 3.3 commit(先不 push,等實機驗證)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4:實機 KB 套用與驗證
|
||||||
|
|
||||||
|
> 前置條件:Phase 1–3 完成;用戶點頭才動實機
|
||||||
|
|
||||||
|
- [ ] 4.1 KB:.claude/wiki/(含 192 卡 + .git)搬 system-dev/wiki/,保留 .git
|
||||||
|
- 驗收:192 張卡與 wiki/.git 完整,git log 不斷
|
||||||
|
- [ ] 4.2 KB:.claude/VERSION、根工具 docs 搬 system-dev/;用戶自有 docs 不動
|
||||||
|
- [ ] 4.3 KB:開 session 接關正常、/wiki-init 寫入 system-dev/wiki/cards/
|
||||||
|
- 驗收:hook 接關輸出正常、無防呆警告(表示已遷移完成)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成定義
|
||||||
|
|
||||||
|
整個 SDD 完成 = 以下全部達成:
|
||||||
|
- [ ] 所有 tasks 標 [x]
|
||||||
|
- [ ] design.md 驗收標準全通過(有客觀證據)
|
||||||
|
- [ ] design.md 與實作一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 狀態說明
|
||||||
|
|
||||||
|
| 標記 | 意義 |
|
||||||
|
|------|------|
|
||||||
|
| `[ ]` | 未開始 |
|
||||||
|
| `[🔄]` | 進行中(當前 session)|
|
||||||
|
| `[x]` | 完成(有驗收證據)|
|
||||||
|
| `[~]` | 暫緩(說明原因)|
|
||||||
|
| `[!]` | 阻擋中(說明阻擋原因)|
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
---
|
||||||
|
status: active # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md)
|
||||||
|
superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名
|
||||||
|
---
|
||||||
|
|
||||||
|
# jdd-dual-profile — Design
|
||||||
|
|
||||||
|
> 建立:2026-08-05 | 最後更新:2026-08-05
|
||||||
|
> 負責人:system-dev-template CC
|
||||||
|
> 來源:InkStoneCo 總管交辦 **W2**
|
||||||
|
> **狀態:active(2026-08-05 leo 回「開工」升活性)**
|
||||||
|
>
|
||||||
|
> 升活性前實查:全 repo 帶 frontmatter 的只有 `TEMPLATE-sdd`(draft,範本不算數)
|
||||||
|
> ⇒ active 數 = 0,**無衝突、無未完成任務需搬移**(D35 ④ a 步驟空集合)。
|
||||||
|
>
|
||||||
|
> 施工順序由總管指定:**發現①、發現⑦ 的防炸工程 → Phase 0 → Phase 1 → 停下驗 Gherkin**。
|
||||||
|
> Phase 2 以後未經確認不得開工。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一句話說明
|
||||||
|
|
||||||
|
把 system-dev-template 從「單一 repo 級框架」改造成 **雙 profile 框架(總管級/repo 級)**,
|
||||||
|
並在其上裝入 **JDD PM 軌**(root.md/journeys.md/站號 sprint/角色權限封路),
|
||||||
|
讓「憲法分流」「角色權限」「實例不改機制」三件事全部由**檔案結構與 hook 機械決定**,
|
||||||
|
沒有任何一格靠 agent 自我判斷。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 現況實查(動手前先搞清楚現在長什麼樣)
|
||||||
|
|
||||||
|
> 本節是設計的事實基礎,也是「與規格假設不符」的清單來源。每條都是本次實測,不是回憶。
|
||||||
|
|
||||||
|
### 0.1 SDD 現況
|
||||||
|
|
||||||
|
- `docs/3-specs/` 下 5 個資料夾,**帶 frontmatter 的只有 `TEMPLATE-sdd`(status: draft)**。
|
||||||
|
`cross-repo-signing` / `install-layout` / `wiki-architecture` / `tasks-project-projection`
|
||||||
|
四份都只在正文寫「狀態:已採納/已結案」,**沒有 frontmatter** ⇒ 機器查得到的 `active` 數 = **0**。
|
||||||
|
- 本 repo 的 SDD 住 `docs/3-specs/`,但它發給別人的 `sdd-guard.sh` 與 `sdd-active-check.sh`
|
||||||
|
預設路徑是 `system-dev/docs/3-specs` ⇒ **框架 repo 的 D35 閘從來沒對自己開過火**。
|
||||||
|
- 現有 SDD 的實際慣例是 **design.md + tasks.md 兩件式**(`TEMPLATE-sdd` 也只有這兩支),
|
||||||
|
`requirements.md` 在本 repo **沒有先例**。本案依交辦要求補齊三件式。
|
||||||
|
|
||||||
|
### 0.2 安裝機制現況
|
||||||
|
|
||||||
|
| 事實 | 影響本設計的地方 |
|
||||||
|
|---|---|
|
||||||
|
| `install.sh` 逐檔 `download_if_missing "<dest>" "$REPO_URL/<path>"`,**沒有 manifest**,檔案清單硬編在腳本裡 | 加 profile ⇒ 清單要分四份(common/repo/orchestrator/模組交叉),硬編必然漂移 → **必須先做 manifest** |
|
||||||
|
| `update.sh` 另有一份**幾乎重複**的清單(update_file/keep_file/add_if_missing/keep_with_template 四類) | 同上;manifest 的「類別」欄正好就是這四類 |
|
||||||
|
| `REPO_URL = $TEMPLATE_SOURCE/template`,所有安裝產物的遠端路徑都掛在 `template/` 底下 | 分離 §三把 `common/`、`profiles/` 畫在 repo 根 ⇒ 會變成第二個 base URL;本設計改掛 `template/` 底下(見決策 D2) |
|
||||||
|
| `update.sh` 的**自我更新在腳本尾端**(先跑完所有下載才更新自己) | 若這一版搬動既有檔案路徑,舊實例這一輪會整排 404;1.16.0 已被同型問題咬過(來源改動=自動更新死掉)⇒ **既有檔一律不搬**(決策 D3) |
|
||||||
|
| `CLAUDE.md` 是整份下載 + `emit_raw_source_block` append,**沒有任何區段界標** | 「本地補充區」目前不存在邊界,漂移偵測無從談起 ⇒ 必須先立界標(設計 §2) |
|
||||||
|
| `build_hooks_json()` 依模組(wiki/sdd)條件組裝 settings.json | profile 只是加第三個維度,**沿用同一支函式**,不另造 |
|
||||||
|
|
||||||
|
### 0.3 hook 現況(template 內共 7 支)
|
||||||
|
|
||||||
|
`pre-write-guard.sh`(空殼,`FORBIDDEN_PATTERNS` 為空=不攔任何東西)、`publish-lag-check.sh`、
|
||||||
|
`sdd-guard.sh`、`session-start-recall.sh`、`subagent-wiki-guard.sh`、`wiki-first-search.sh`、
|
||||||
|
`wiki-secret-scan.sh`。
|
||||||
|
|
||||||
|
- **`AGENT_ROLE` 在整個 repo 與 InkStoneCo 實例中 0 次出現** ⇒ 角色軸完全從零開始。
|
||||||
|
- **`guard-cross-project.sh` 不在 template 裡**——它只存在於 InkStoneCo 實例的 `.claude/hooks/`。
|
||||||
|
分離 §六.4 標它 `[修改]`,實際動作是「**從實例上收進框架**」,不是就地改(決策 D6)。
|
||||||
|
- 同理,`delivery-police.sh`/`self-drive-police.sh`/`unpushed-police.sh`/`history-first-guard.sh`
|
||||||
|
等 **11 支都是實例自行發明的**,框架不知道它們存在。
|
||||||
|
|
||||||
|
### 0.4 漂移基線(機械閘 #3 的今日實測值)
|
||||||
|
|
||||||
|
比對 InkStoneCo 實例的 `.claude/hooks/` 與本 repo `template/.claude/hooks/`:
|
||||||
|
|
||||||
|
| 狀態 | 數量 | 檔 |
|
||||||
|
|---|---|---|
|
||||||
|
| 與框架一致 | 2 | `session-start-recall.sh`、`wiki-secret-scan.sh` |
|
||||||
|
| **已被手改(漂移)** | **4** | `pre-write-guard.sh`、`sdd-guard.sh`、`subagent-wiki-guard.sh`、`wiki-first-search.sh` |
|
||||||
|
| 實例自行發明 | 11 | 見 §0.3 |
|
||||||
|
|
||||||
|
⇒ **今天沒有任何機制知道這 4 支已經漂移**。這就是分離 §八 預測②(「漂移數歸零」)的基線值 = 4。
|
||||||
|
|
||||||
|
### 0.5 實例專名基線(機械閘 #2 的今日實測值)
|
||||||
|
|
||||||
|
`template/` 底下命中 `arcrun|mira|leo21c|inkstone|uncle6|polaris`:**8 檔 20 行**。分三類:
|
||||||
|
|
||||||
|
| 類 | 內容 | 處置 |
|
||||||
|
|---|---|---|
|
||||||
|
| (a) 註解/舉例(6 行) | `sdd-guard.sh`「誠實限制(抄 arcrun)」、`publish-lag-check.sh` 的 jsDelivr 例、`logseq-markers.md` 的 TODO 例句、`issue-handle.md` 的 repo 名清單 | **本波清理**(改寫成通用敘述) |
|
||||||
|
| (b) 政策內容混進框架(3 行) | `subagent-wiki-guard.sh`「有 Arcrun RAG MCP 就用它」、`wiki-extract.md`「下游 Arcrun ingest」 | **標逐行豁免,W3 隨 policy pack 搬走** |
|
||||||
|
| (c) 整支是 L2 產物(13 行) | `system-dev/workflows/tasks-project-sync.{yaml,local.sh}`(本體就是 arcrun workflow) | **整組標豁免,W3 移入 policy pack** |
|
||||||
|
|
||||||
|
⇒ 閘 #2 **不能一上線就全紅**。必須配「逐行豁免標記 + 基線報表」,否則為了讓 CI 綠會出現
|
||||||
|
假性清理(把 arcrun 換成「某工作流引擎」=資訊消失但問題還在)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 範圍
|
||||||
|
|
||||||
|
### 包含(In Scope)
|
||||||
|
|
||||||
|
- template 的雙 profile 化(宣告式,非搬檔):manifest、`--profile`、profile 專屬產物
|
||||||
|
- CLAUDE.md 生成、框架區/本地補充區界標、update 漂移偵測
|
||||||
|
- JDD 文件範本:`root.md`、`journeys.md`(含站點索引表)、站號 sprint 範本、術語表
|
||||||
|
- 兩軸身分(scope × role)的機械判定函式庫
|
||||||
|
- JDD §六 8 條封路規則 + 分離 §六 4 條防糾纏閘的實作
|
||||||
|
- policy plugin 的**插槽**:載入順序文件化 + profile 憲法 must-read 注入點
|
||||||
|
- CHANGELOG + VERSION bump + 乾淨環境雙 profile 安裝實測
|
||||||
|
|
||||||
|
### 不包含(Out of Scope)
|
||||||
|
|
||||||
|
- **arcrun-policy plugin 本體**(W3):`plugin.json`、marketplace 發行、白名單 hook 內容、
|
||||||
|
primer/dispatch-checklist 搬遷、`.mcp.json`
|
||||||
|
- **實例側落地**(W4):InkStoneCo 的 root.md/journeys.md 實填、routine 取任務源改站號、
|
||||||
|
實例 11 支自製 hook 的退役與上收
|
||||||
|
- **雲端側**(W5):LLM Wiki 總編輯管線、儀表 job、`arch_baseline`/`arch_iteration` 卡
|
||||||
|
- **記憶階梯改序(KBDB-first)**:藍圖 §7,屬另一波
|
||||||
|
- **既有 SDD 的 frontmatter 補齊**(§0.1 撿到的斷層):本波只回報,不順手改——
|
||||||
|
那會動到四份別的 SDD 的生命週期狀態,屬規格層,另走 pending-changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 架構總覽
|
||||||
|
|
||||||
|
```
|
||||||
|
system-dev-template/ ← 框架 repo(本 repo)
|
||||||
|
├─ scripts/install.sh --profile=repo|orchestrator ← 改造:讀 manifest
|
||||||
|
├─ scripts/update.sh ← 改造:讀 manifest + 漂移偵測
|
||||||
|
├─ scripts/check-no-instance-names.sh ← 新增:機械閘 #2(CI)
|
||||||
|
├─ scripts/instance-names.txt ← 新增:黑名單設定檔
|
||||||
|
└─ template/ ← 所有安裝產物的遠端根($REPO_URL)
|
||||||
|
├─ manifest/
|
||||||
|
│ ├─ common.tsv ← 新增:兩 profile 共裝
|
||||||
|
│ ├─ repo.tsv ← 新增:repo profile 專屬
|
||||||
|
│ └─ orchestrator.tsv ← 新增:orchestrator profile 專屬
|
||||||
|
├─ .claude/hooks/… ← 既有 7 支【原地不動】= common 的實體
|
||||||
|
│ ├─ lib/role-lib.sh ← 新增:兩軸判定函式庫(被 source)
|
||||||
|
│ ├─ role-guard.sh ← 新增:J1+J2+J3
|
||||||
|
│ ├─ jdd-format-guard.sh ← 新增:J4+J5+J8
|
||||||
|
│ ├─ station-done-guard.sh ← 新增:J6
|
||||||
|
│ ├─ regression-scope.sh ← 新增:J7
|
||||||
|
│ └─ install-artifact-guard.sh ← 新增:S1
|
||||||
|
├─ profiles/
|
||||||
|
│ ├─ repo/
|
||||||
|
│ │ ├─ CLAUDE.md ← repo 憲法範本(=現行 template/CLAUDE.md 演進)
|
||||||
|
│ │ └─ (SDD 三件式、repo wiki 規範:沿用 common 既有產物,manifest 宣告即可)
|
||||||
|
│ └─ orchestrator/
|
||||||
|
│ ├─ CLAUDE.md ← 總管憲法範本(藍圖 v3 指針+術語表)
|
||||||
|
│ ├─ docs/root.md.template
|
||||||
|
│ ├─ docs/journeys.md.template
|
||||||
|
│ ├─ docs/sprint.md.template ← 站號 sprint
|
||||||
|
│ ├─ docs/triage-map.md.template ← 格式範本,內容留實例
|
||||||
|
│ ├─ docs/plugin-load-order.md ← 載入順序文件(W3 插槽)
|
||||||
|
│ └─ hooks/orchestrator-scope-guard.sh ← 新增:S4(上收 guard-cross-project)
|
||||||
|
└─ system-dev/… ← 既有【原地不動】
|
||||||
|
|
||||||
|
實例安裝後:
|
||||||
|
CLAUDE.md ← 框架區(profile 範本)+ 本地補充區(界標分隔)
|
||||||
|
system-dev/.profile ← 單行:repo | orchestrator(scope 軸唯一來源)
|
||||||
|
system-dev/.template-manifest ← 每個產物的 path / version / sha256(漂移偵測依據)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 設計答案①:雙 profile 的 CLAUDE.md 怎麼生成、本地補充區邊界、漂移怎麼偵測
|
||||||
|
|
||||||
|
### 2.1 生成:組裝而非下載
|
||||||
|
|
||||||
|
現行是「整份下載 `template/CLAUDE.md` + append raw source 區塊」。改為三段組裝:
|
||||||
|
|
||||||
|
```
|
||||||
|
<!-- sdt:framework begin profile=<repo|orchestrator> version=1.19.0 sha256=<前12碼> -->
|
||||||
|
(profiles/<profile>/CLAUDE.md 的完整內容,一字不改)
|
||||||
|
<!-- sdt:framework end -->
|
||||||
|
|
||||||
|
<!-- sdt:local begin — 這一區是你的,update 永遠不會動它 -->
|
||||||
|
(install 產生的 raw source 宣告;之後由使用者/CC 自由追加)
|
||||||
|
<!-- sdt:local end -->
|
||||||
|
```
|
||||||
|
|
||||||
|
- 界標用 HTML 註解:md 渲染看不見、CC 讀得到、`grep -n` 定位得到。
|
||||||
|
- `sha256` 記的是**框架區內容本身**(不含界標行),是漂移偵測的比對基準。
|
||||||
|
- 兩區順序固定:框架區在上(agent 先讀到憲法),本地區在下。
|
||||||
|
|
||||||
|
### 2.2 兩份 profile 憲法的內容分界(G4 的判準)
|
||||||
|
|
||||||
|
| | `profiles/repo/CLAUDE.md` | `profiles/orchestrator/CLAUDE.md` |
|
||||||
|
|---|---|---|
|
||||||
|
| 效忠文件 | `requirements.md`(SDD 技術軌) | `root.md` + `journeys.md`(PM 軌) |
|
||||||
|
| 含 SDD 三件式細節 | ✅ 含(現行內容) | ❌ **不含**(G4 明文:總管版無 SDD 三件式細節) |
|
||||||
|
| 上游指針 | ✅ 一行(指向總管 repo 的憲法位置,**不寫死專案名**,由 install 問一次或留空) | ❌ 無(它自己就是上游) |
|
||||||
|
| JDD 術語表 | 只放「站號怎麼標在 task 上」一段 | 全表(Journey/Station/通關/點亮/對帳/完備) |
|
||||||
|
| sprint 機制 | 「認領 loop」段(engineer 的動作) | 全流程(指定站 → 認領 → 新增必掛站 → 收尾判準) |
|
||||||
|
| 角色 | engineer(可寫 code/tasks/requirements/design;禁改考卷) | orchestrator(可寫 root/journeys/sprint;禁寫 code、禁改 tasks) |
|
||||||
|
|
||||||
|
> G4 的機械驗法:`grep -c "SDD 三件式\|requirements.md" CLAUDE.md`
|
||||||
|
> 在 orchestrator 實例上 = 0;`grep -c "上游" CLAUDE.md` 在 repo 實例上 ≥ 1。
|
||||||
|
|
||||||
|
### 2.3 邊界規則(寫進兩份憲法,並由 hook 兌現)
|
||||||
|
|
||||||
|
1. **框架區唯讀**:任何內容變更走框架 repo 提案 → bump → update 拉下來。
|
||||||
|
2. **本地補充區隨便寫**:update 永不讀、永不寫、永不比對。
|
||||||
|
3. 實例要覆寫框架區的某條規則 → 不准就地改,**在本地補充區寫「例外聲明 + 理由 + 日期」**。
|
||||||
|
這樣 diff 永遠乾淨,而例外仍然留痕可審。
|
||||||
|
|
||||||
|
### 2.4 漂移偵測:manifest + sha256
|
||||||
|
|
||||||
|
`system-dev/.template-manifest`(install 產生,update 維護),TSV 一行一產物:
|
||||||
|
|
||||||
|
```
|
||||||
|
<dest 路徑> <class> <安裝時 version> <安裝時 sha256>
|
||||||
|
```
|
||||||
|
|
||||||
|
`class` 沿用 update.sh 既有四類語意:`overwrite`(模板/邏輯檔)/`keep`(使用者資料檔)/
|
||||||
|
`add-if-missing`(新資料檔)/`keep-with-template`(使用者會手填的客製檔)。
|
||||||
|
|
||||||
|
update 對每個 `overwrite` 類產物跑三態判定:
|
||||||
|
|
||||||
|
| 實檔 sha vs manifest sha | 遠端 sha vs manifest sha | 判定 | 動作 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 相同 | 相同 | 沒變 | **no-op**(不下載、不列報表)← 兌現 EARS-1.2.2 |
|
||||||
|
| 相同 | 不同 | 乾淨、有新版 | 覆蓋,列「已更新」 |
|
||||||
|
| **不同** | 任意 | **漂移** | **不覆蓋**;新版另存 `<檔>.new`;列入 ⚠️ 漂移清單 |
|
||||||
|
|
||||||
|
漂移清單的輸出格式(白話,leo 讀得懂):
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 下列 3 個檔被手改過,這一版沒有覆蓋它們:
|
||||||
|
.claude/hooks/sdd-guard.sh → 新版已放在 sdd-guard.sh.new,請 diff
|
||||||
|
你可以:① 把你的改動寫成框架提案(推薦,一次修全家)
|
||||||
|
② 放棄本地改動:mv sdd-guard.sh.new sdd-guard.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**舊實例遷移(沒有 manifest 的 1.18.x)**:update 偵測到無 manifest → 進「一次性補植」:
|
||||||
|
以本版遠端內容為基準建 manifest,**凡當下與遠端不一致者一律先標成漂移**(保守:寧可多報不漏報),
|
||||||
|
並把現有 `CLAUDE.md` 整份包進 `sdt:local` 區、框架區從 profile 重鋪,
|
||||||
|
輸出「你的舊 CLAUDE.md 已完整保留在本地補充區,請自行搬移重複段落」。整段冪等,重跑不再動。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 設計答案②:AGENT_ROLE 兩軸身分在 hook 裡怎麼機械判定
|
||||||
|
|
||||||
|
### 3.1 兩個來源,零自陳
|
||||||
|
|
||||||
|
| 軸 | 來源 | 誰寫 | 讀不到時 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **scope** | `system-dev/.profile`(單行 `repo`/`orchestrator`) | install.sh(`--profile` 或偵測+人確認一次) | 視為 `repo`(多數實例;且此時 orchestrator 專屬閘不觸發,仍有 common 閘在) |
|
||||||
|
| **role** | 環境變數 `AGENT_ROLE`(`orchestrator`/`engineer`) | ① install 依 profile 寫進 `.claude/settings.json` 的 `env` 當預設<br>② 派工端 spawn subagent 時注入<br>③ 人工 override | **依 scope 推定**:orchestrator profile → `orchestrator`;repo profile → `engineer` |
|
||||||
|
|
||||||
|
> 為什麼 scope 不放 `settings.json`:settings.json 是「使用者資料檔」,update 永不覆蓋,
|
||||||
|
> 而且 CI/獨立腳本也要讀得到。`system-dev/.profile` 與 `VERSION` 同層,一致且好找。
|
||||||
|
|
||||||
|
### 3.2 身分矩陣(含那個不存在的格子)
|
||||||
|
|
||||||
|
| | `AGENT_ROLE=orchestrator` | `AGENT_ROLE=engineer` |
|
||||||
|
|---|---|---|
|
||||||
|
| **orchestrator profile** | PM 本尊 ✅ | 總管 repo 裡的技術 subagent ✅ |
|
||||||
|
| **repo profile** | **❌ 不存在** → exit 2,要求修正環境 | 寫 code 的 subagent ✅ |
|
||||||
|
|
||||||
|
「repo profile × orchestrator」被攔的訊息要說清楚:成員 repo 沒有 PM——
|
||||||
|
要 PM 的動作請回總管 repo 做(分離 §二「空格也是封路」)。
|
||||||
|
|
||||||
|
### 3.3 `lib/role-lib.sh`(common,被 source 不獨立掛)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sdt_repo_root() # 由 ${BASH_SOURCE} 往上找,不假設 CLAUDE_PROJECT_DIR(雲端可跑)
|
||||||
|
sdt_rel_path "$f" # 絕對/相對 → repo 相對路徑(抄 guard-cross-project 的 case 寫法)
|
||||||
|
sdt_scope() # 讀 system-dev/.profile,trim;讀不到回 repo
|
||||||
|
sdt_role() # 讀 $AGENT_ROLE;空 → 依 sdt_scope 推定
|
||||||
|
sdt_assert_identity() # 檢查矩陣空格,命中 → 印訊息 exit 2
|
||||||
|
sdt_file_path_from_stdin # 統一的 JSON 解析(jq → python3 → grep 三段 fallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
**為什麼是函式庫不是 hook**:六支新 hook 都要做同樣四件事(解析 JSON、算相對路徑、判 scope、判 role)。
|
||||||
|
各寫一份=四處維護同一條規則,正是分離 §六.4 要避免的病。
|
||||||
|
|
||||||
|
### 3.4 為什麼角色 hook 屬 common,不按規格放進各自 profile
|
||||||
|
|
||||||
|
分離 §三把 `engineer 角色 hooks` 畫在 `profiles/repo/`、`orchestrator 角色 hooks` 畫在
|
||||||
|
`profiles/orchestrator/`。**照做會漏一格**:總管 repo 裡也會 spawn engineer subagent
|
||||||
|
(矩陣右上角),若 engineer 的封路 hook 只裝在 repo profile,那顆 subagent 在總管 repo 裡
|
||||||
|
**改得動 journeys.md** ⇒ G2「考生改考卷被攔截」在最該生效的地方失效。
|
||||||
|
|
||||||
|
⇒ 本設計改為:**role 軸的 hook 全部屬 common(兩 profile 都裝),scope 軸的 hook 才按 profile 分**。
|
||||||
|
唯一 scope 專屬的是 `orchestrator-scope-guard.sh`(總管禁入成員 repo 寫實作)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 設計答案③:12 條規則落成哪些 hook(改既有 vs 新增)
|
||||||
|
|
||||||
|
> **規則數 ≠ 檔案數**。J1/J2/J3 都是「PreToolUse Write|Edit 依 role×path 判定」,
|
||||||
|
> 拆三支=三次解析同一包 JSON、三處維護同一張路徑表。合併為一支、規則編號保留在程式碼註解與訊息裡。
|
||||||
|
|
||||||
|
### 4.1 JDD §六 八條
|
||||||
|
|
||||||
|
| 規則 | 內容 | 落點 | 既有/新增 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| J1 | orchestrator 寫 `src/**`、`*.py`、`*.ts`… → 攔 | `role-guard.sh` | **新增**(規格說「既有 hook 已涵蓋則跳過」——已確認 `pre-write-guard.sh` 是空殼且不認 role,**不涵蓋**) |
|
||||||
|
| J2 | orchestrator 寫 `tasks.md`/`requirements.md`/`design.md` → 攔 | `role-guard.sh` | 新增(同上) |
|
||||||
|
| J3 | engineer 寫 `journeys.md`/`root.md`/`*.feature`/md 內 Gherkin 區塊 → 攔(**命門**) | `role-guard.sh` | 新增 |
|
||||||
|
| J4 | `root.md` 🔴 卡缺【要驗證+對帳日】→ 攔 | `jdd-format-guard.sh` | 新增 |
|
||||||
|
| J5 | `tasks.md` **新增**的 task 缺站號 → 攔 | `jdd-format-guard.sh` | 新增 |
|
||||||
|
| J6 | sprint 收尾判準:tasks 全關 → **指定站 Gherkin 全過** | `station-done-guard.sh` | **新增**(規格標 `[修改]`,但 template 內**沒有**任何 sprint 收尾 hook——`delivery-police.sh` 只存在於 InkStoneCo 實例 ⇒ 對框架而言是新增,見決策 D6) |
|
||||||
|
| J7 | 站相關實作變動 → 查站點索引表 → 列重考清單 | `regression-scope.sh` | 新增(提醒不擋) |
|
||||||
|
| J8 | `root.md`/`journeys.md` 出現技術名詞 → 警告/攔 | `jdd-format-guard.sh` | 新增 |
|
||||||
|
|
||||||
|
### 4.2 分離 §六 四條
|
||||||
|
|
||||||
|
| 閘 | 內容 | 落點 | 既有/新增 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| S1 | 實例寫入安裝產物區 → 攔;`--framework-dev` 例外 | `install-artifact-guard.sh` | 新增 |
|
||||||
|
| S2 | 框架範本混入實例專名 → CI fail | `scripts/check-no-instance-names.sh`(**非 hook**) | 新增 |
|
||||||
|
| S3 | update 漂移偵測 | `update.sh` + `.template-manifest` | **改既有腳本** |
|
||||||
|
| S4 | guard-cross-project 職責不重疊 | `profiles/orchestrator/hooks/orchestrator-scope-guard.sh` | **新增於框架**(實體改寫自實例那支,見 D6) |
|
||||||
|
|
||||||
|
### 4.3 既有檔改動清單
|
||||||
|
|
||||||
|
| 檔 | 改什麼 | 為什麼 |
|
||||||
|
|---|---|---|
|
||||||
|
| `template/.claude/hooks/session-start-recall.sh` | 依 `sdt_scope()` 分流注入:orchestrator → root/journeys 摘要 + 本 sprint **未點亮**站;repo → 現行 principles/status/mistakes | 唯一真正的「改既有 hook」;藍圖 §0「routine 起牀讀站號」的落點 |
|
||||||
|
| `scripts/install.sh` | 加 `--profile`、自動偵測+人確認、改讀 manifest、產 `.profile`/`.template-manifest`、CLAUDE.md 三段組裝、`build_hooks_json()` 加 profile 維度與 `env.AGENT_ROLE` | E1 主體 |
|
||||||
|
| `scripts/update.sh` | 改讀 manifest、三態判定、漂移報表、舊實例 marker 補植遷移 | S3 + EARS-1.2.2 |
|
||||||
|
| `template/CLAUDE.md` | 演進為 `template/profiles/repo/CLAUDE.md`;**原路徑保留一份轉址說明**,避免舊 update.sh 404 | D3 向下相容 |
|
||||||
|
| §0.5 (a) 類 6 行註解 | 改寫成通用敘述 | S2 前置清潔 |
|
||||||
|
|
||||||
|
**合計:新增 hook 6 支**(`role-guard`、`jdd-format-guard`、`station-done-guard`、
|
||||||
|
`regression-scope`、`install-artifact-guard`、`orchestrator-scope-guard`)
|
||||||
|
+ **共用函式庫 1 支**(`lib/role-lib.sh`,不獨立掛);
|
||||||
|
**改既有 hook 1 支**(`session-start-recall.sh`);**改既有腳本 2 支**(install/update);
|
||||||
|
**新增非 hook 腳本 1 支**(`check-no-instance-names.sh`)。
|
||||||
|
|
||||||
|
### 4.4 settings.json 掛載順序(install 依 profile 組裝)
|
||||||
|
|
||||||
|
```
|
||||||
|
SessionStart: session-start-recall.sh(profile 分流)
|
||||||
|
[W3 插槽:policy plugin 的 SessionStart hook 自動排在框架 hook 之後]
|
||||||
|
PreToolUse(Write|Edit|MultiEdit):
|
||||||
|
1. install-artifact-guard.sh ← 先擋「改機制」,最外層
|
||||||
|
2. role-guard.sh ← 再判角色
|
||||||
|
3. jdd-format-guard.sh ← 再驗格式
|
||||||
|
4. orchestrator-scope-guard.sh ← 僅 orchestrator profile
|
||||||
|
5. sdd-guard.sh(既有)
|
||||||
|
6. pre-write-guard.sh(既有空殼)
|
||||||
|
7. wiki-secret-scan.sh(既有)
|
||||||
|
PreToolUse(Grep|Glob|Read|Bash): wiki-first-search.sh(既有)
|
||||||
|
PreToolUse(Task): subagent-wiki-guard.sh(既有)
|
||||||
|
Stop / TaskCompleted: station-done-guard.sh、regression-scope.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
順序原則:**範圍大的擋在前**(改機制 → 角色 → 格式),讓錯誤訊息指向最根本的那條規則。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. W3 插槽(本波只留座位,不做 plugin)
|
||||||
|
|
||||||
|
分離 §五 載入順序原樣文件化到 `profiles/*/docs/plugin-load-order.md`:
|
||||||
|
|
||||||
|
```
|
||||||
|
SessionStart
|
||||||
|
1. profile 憲法(scope 軸:這個資料夾的 CLAUDE.md)
|
||||||
|
2. framework hooks 上鏈(common + profile)
|
||||||
|
3. 已安裝 policy plugin 注入(primer 推到眼前、白名單 hook 排入鏈尾、skill 就緒)
|
||||||
|
4. session-start-recall(wiki status 快照,現行機制)
|
||||||
|
```
|
||||||
|
|
||||||
|
框架要做的只有兩件(分離 §四明文):
|
||||||
|
1. 文件化「政策包= Claude Code 官方 plugin」的約定與載入順序——**框架不發明平行外掛格式**;
|
||||||
|
2. 在兩份 profile 憲法留 must-read 注入點(一段標題 + 一行說明「政策包的必讀會出現在這裡」),
|
||||||
|
讓 plugin 的 SessionStart hook 有地方推內容。
|
||||||
|
|
||||||
|
⚠️ **W3 動手前必讀官方文件**(`code.claude.com/docs/en/plugins.md`、`plugins-reference.md`),
|
||||||
|
不憑記憶寫 `plugin.json`。本 SDD **不**預先規定 plugin 的 schema。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 關鍵決策
|
||||||
|
|
||||||
|
| # | 決策 | 選擇 | 原因 | 放棄的選項 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| D1 | 安裝清單怎麼管 | **manifest TSV**(common/repo/orchestrator 各一份),install 與 update 共讀 | 現行清單硬編在兩支腳本裡已經重複;加 profile 會變四份必然漂移。manifest 讓「加一個產物」=加一行資料 | 繼續硬編(四份清單手動同步) |
|
||||||
|
| D2 | `common/`、`profiles/` 放哪 | 放 **`template/` 底下** | 所有安裝產物的遠端根是 `$TEMPLATE_SOURCE/template`;放 repo 根會開第二個 base URL,publish-exclude 與 mirror 規則也要跟著改。分離 §三的樹是示意(它把 install.sh 也畫在根,實際在 `scripts/`) | 照規格字面放 repo 根 |
|
||||||
|
| D3 | 既有檔要不要實體搬進 `common/` | **不搬**。既有檔原地不動,靠 manifest **宣告**它屬 common | `update.sh` 的自我更新在腳本尾端 ⇒ 舊實例跑的是舊腳本、路徑寫死,搬檔=這一輪整排 404。1.16.0 已經被「來源改動=自動更新死掉」咬過一次 | 照規格字面實體搬移(斷所有舊實例的更新) |
|
||||||
|
| D4 | 「本地補充區」怎麼劃 | **HTML 註解界標** + 框架區 sha256 | md 渲染不可見、grep 定位得到、CC 讀得到;sha 讓漂移可機械判定 | 靠檔尾約定(無邊界=無法偵測)/另開 `CLAUDE.local.md`(agent 不保證會讀) |
|
||||||
|
| D5 | 角色 hook 屬 common 還是各 profile | **屬 common** | 總管 repo 裡也會 spawn engineer;照規格分裝會讓那顆 subagent 改得動 journeys.md,G2 在最該生效處失效(§3.4) | 照規格分裝進各 profile |
|
||||||
|
| D6 | `guard-cross-project` 怎麼「修改」 | **上收進框架** `orchestrator-scope-guard.sh`,實例版於 W4 退役 | 它根本不在 template 裡,是實例自己發明的。就地改=在實例改機制=違反本案要立的第一條鐵律 | 在實例上就地改(自打嘴巴) |
|
||||||
|
| D7 | J1/J2/J3 三條規則的檔案數 | **合成一支 `role-guard.sh`** | 同一個 hook 事件、同一份身分判定、同一張路徑表;拆三支=三處維護一條規則 | 一條一支(規格字面「逐條實作」) |
|
||||||
|
| D8 | 閘 #2(實例專名)怎麼上線 | **腳本 + 逐行豁免標記 + 基線報表**,(a) 類本波清、(b)(c) 類標豁免待 W3 | 現況 8 檔 20 行命中;一上線全紅會逼出假性清理(把 arcrun 換成「某工作流引擎」=資訊沒了問題還在) | 硬上線(CI 立刻全紅)/先全部改寫(假性清理) |
|
||||||
|
| D9 | `--framework-dev` 怎麼實作 | **repo 根的 `.sdt-framework-dev` 檔**(框架 repo 自帶並 commit),輔以 env `SDT_FRAMEWORK_DEV=1` | Claude Code **沒有** `--framework-dev` 這個官方 flag(規格假設不成立)。用檔案=框架 repo 天生就有,零記憶負擔 | 造一個 CLI flag(做不到)/純靠環境變數(每次要記得設) |
|
||||||
|
| D10 | 本 SDD 的 status | **draft**,confirm 後升 active | D35 ②③;本 repo 現有 0 份 active,升活性不需搬移任何任務 | 直接寫 active(搶活性) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技術限制
|
||||||
|
|
||||||
|
- 相容 macOS bash 3.2(`set -u` 下空陣列展開要先判長度——install.sh 既有踩過)。
|
||||||
|
- 不假設有 `jq`:JSON 解析走 `jq → python3 → grep` 三段 fallback(沿既有 hook 慣例)。
|
||||||
|
- 不假設 `CLAUDE_PROJECT_DIR` 存在:路徑一律由 `${BASH_SOURCE}` 往上推(雲端可跑)。
|
||||||
|
- hook 一律「解析失敗即放行」,寧可漏擋不誤殺;每支頂部寫誠實限制。
|
||||||
|
- 遠端檔案路徑(`$REPO_URL/...`)對既有實例是**契約**,本波只增不移。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 驗收標準
|
||||||
|
|
||||||
|
以 requirements.md §四 的 **G1–G7** 為唯一驗收線。收工時每題必須附**實測輸出**,
|
||||||
|
狀態只有三種:`✅ 通(附證據)` / `◐ 半通(標明缺什麼)` / `❌ 斷`。
|
||||||
|
|
||||||
|
- G5 本波上限為 `◐ 半通`(插槽就位、政策包在 W3)——**不得標 ✅**。
|
||||||
|
- 另加兩條非功能驗收:
|
||||||
|
- 乾淨環境雙 profile 安裝各 < 10 分鐘(分離 §八 預測①,實測計時)
|
||||||
|
- `bash scripts/update.sh` 在 InkStoneCo 實例上跑出的漂移清單 = **4 支**(§0.4 基線,
|
||||||
|
對得上=偵測正確;對不上=偵測有偽陰/偽陽)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附錄 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`(本卷)
|
||||||
|
- `docs/3-specs/SDD-LIFECYCLE.md`(D35 生命週期)
|
||||||
|
- `docs/3-specs/install-layout/design.md`(安裝產物佈局的既有決策,本案沿用其「不污染用戶根目錄」原則)
|
||||||
|
- 上游提案:`InkStoneCo/system-dev/docs/3-specs/pending-changes.md` §三 W2
|
||||||
|
- 需求輸入:`~/Desktop/總管/{JDD-template-upgrade,分離導入規格,總管系統藍圖v3}.md`
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# jdd-dual-profile — Requirements
|
||||||
|
|
||||||
|
> 建立:2026-08-05 | 最後更新:2026-08-05
|
||||||
|
> 負責人:system-dev-template CC
|
||||||
|
> 來源:InkStoneCo 總管交辦 **W2**(`InkStoneCo/system-dev/docs/3-specs/pending-changes.md`
|
||||||
|
> 「[proposal] 總管系統藍圖 v3 三件套落地安排」§三 W2)
|
||||||
|
> 需求輸入:《JDD 導入規格》§三/四/五/六 +《分離導入規格》§三/六 +《總管系統藍圖 v3》§3/§4/§5.5
|
||||||
|
> 狀態:**等 leo confirm,未實作**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 為什麼合成一份(不是兩份 SDD)
|
||||||
|
|
||||||
|
JDD 的角色權限 hook 必須靠 profile 分流才成立——「總管版憲法」與「repo 版憲法」是同一支
|
||||||
|
CLAUDE.md 生成流程的兩個輸出。拆兩份 SDD 會把 CLAUDE.md 生成、manifest、install/update
|
||||||
|
改造各做兩遍,且第二遍必然要推翻第一遍的檔案佈局。總管裁定:合成一份。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、Epic
|
||||||
|
|
||||||
|
| Epic | 一句話 | 需求輸入 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| **E1 雙 profile 框架** | 同一個 template 能裝出「總管級」或「repo 級」兩種實例,憲法由安裝位置決定、不靠 agent 自我判斷 | 分離 §三 |
|
||||||
|
| **E2 PM 軌文件(JDD)** | 框架提供 `root.md` / `journeys.md` 兩份範本與格式紀律,讓驗收線從「tasks 全關」換成「站的 Gherkin 全綠」 | JDD §三、§五 |
|
||||||
|
| **E3 角色封路** | orchestrator/engineer 的可寫範圍用 hook 機械強制,不靠 prompt 叮嚀 | JDD §四、§六 |
|
||||||
|
| **E4 防糾纏** | 實例不改機制、框架不含實例資料,兩條鐵律各配機械閘;update 能報出被手改過的檔 | 分離 §一、§六 |
|
||||||
|
|
||||||
|
**明確不在本 SDD 範圍**(W3 才做):arcrun-policy plugin 本體、政策包內容搬遷、
|
||||||
|
marketplace 發行。本 SDD 只負責**留好插槽**(載入順序文件化 + profile 憲法的 must-read 注入點)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、User Story + EARS
|
||||||
|
|
||||||
|
### E1 雙 profile 框架
|
||||||
|
|
||||||
|
**US-1.1**:身為安裝者,我要一條指令就裝出正確的那部憲法,不必自己判斷該裝哪些檔。
|
||||||
|
|
||||||
|
- `EARS-1.1.1` When 安裝者執行 `install.sh --profile=orchestrator`,the system shall
|
||||||
|
只鋪設 orchestrator profile 宣告的產物,且不鋪設 repo profile 專屬的產物。
|
||||||
|
- `EARS-1.1.2` When 安裝者執行 `install.sh --profile=repo`,the system shall
|
||||||
|
只鋪設 repo profile 宣告的產物(含 SDD 三件式與 repo 級 wiki 規範)。
|
||||||
|
- `EARS-1.1.3` When 安裝者未指定 `--profile`,the system shall 自動偵測
|
||||||
|
(目前目錄下存在多個各自帶 `.git` 的子目錄 → 建議 orchestrator),
|
||||||
|
**並在寫入任何檔案前要求人確認一次**;確認結果寫入 marker 檔,之後不再問。
|
||||||
|
- `EARS-1.1.4` The system shall 在安裝完成後於 `system-dev/.profile` 留下單行 profile 名,
|
||||||
|
作為所有 hook 判定 scope 的唯一機器可讀來源。
|
||||||
|
|
||||||
|
**US-1.2**:身為框架維護者,我要 common 與兩個 profile 共用**一條版本流**,不開第二個 repo。
|
||||||
|
|
||||||
|
- `EARS-1.2.1` The system shall 以單一 `VERSION` 檔涵蓋 common 與所有 profile 的變更。
|
||||||
|
- `EARS-1.2.2` When 執行 update 而該實例所屬 profile 的產物本版未變動,the system shall
|
||||||
|
對該 profile 的產物 no-op(不下載、不覆蓋、不在報表列為「已更新」)。
|
||||||
|
|
||||||
|
**US-1.3**:身為安裝者,我要 CLAUDE.md 由 profile 範本生成,而我自己補的內容永遠不會被更新洗掉。
|
||||||
|
|
||||||
|
- `EARS-1.3.1` The system shall 把生成的 CLAUDE.md 切成「框架區」與「本地補充區」,
|
||||||
|
兩區以機器可辨識的界標分隔。
|
||||||
|
- `EARS-1.3.2` While 執行 update,the system shall 只覆蓋框架區、**絕不動本地補充區**。
|
||||||
|
- `EARS-1.3.3` If 框架區內容與本版原始範本不一致(=被手改過),then the system shall
|
||||||
|
不覆蓋該檔、將它列入漂移清單,並提示「回框架提案 or 放棄本地改動」。
|
||||||
|
|
||||||
|
### E2 PM 軌文件(JDD)
|
||||||
|
|
||||||
|
**US-2.1**:身為總管(PM),我要一份白話根文件,讓不懂技術的人讀了能勾或搖頭。
|
||||||
|
|
||||||
|
- `EARS-2.1.1` The system shall 於 orchestrator profile 提供 `root.md` 範本,
|
||||||
|
格式為「一句白話 + 來源標記 + 紅綠燈」。
|
||||||
|
- `EARS-2.1.2` If `root.md` 中任一 🔴 卡缺少【要驗證 + 對帳日】,then the system shall
|
||||||
|
在寫入當下攔截並指出行號。
|
||||||
|
- `EARS-2.1.3` If `root.md` 或 `journeys.md` 出現技術名詞黑名單詞(API/DB/WASM/MCP/
|
||||||
|
endpoint/schema…),then the system shall 攔截並列出命中詞與行號。
|
||||||
|
|
||||||
|
**US-2.2**:身為總管,我要 `journeys.md` 承載「角色 → Journey → Station → Gherkin」三層,
|
||||||
|
並附站點索引表供回歸考查範圍。
|
||||||
|
|
||||||
|
- `EARS-2.2.1` The system shall 於 orchestrator profile 提供 `journeys.md` 範本,
|
||||||
|
含三層巢狀結構與「附:站點索引表」段。
|
||||||
|
- `EARS-2.2.2` The system shall 在範本內以註記聲明:站全域編號、跨 Journey 共享、
|
||||||
|
重複出現只寫引用不重抄;Gherkin 的 Then 只寫使用者看得到/感覺到的結果。
|
||||||
|
|
||||||
|
**US-2.3**:身為 routine/總管,我要 sprint 的單位是「一組站號」,起牀就有明確的「離通關還缺什麼」。
|
||||||
|
|
||||||
|
- `EARS-2.3.1` The system shall 於 orchestrator profile 提供 sprint 範本,
|
||||||
|
以站號(而非 task 批次)為單位,含起訖日與到期結算欄。
|
||||||
|
- `EARS-2.3.2` If `tasks.md` 中**新增**的 task 條目未標注它服務哪一站,then the system shall 攔截。
|
||||||
|
- `EARS-2.3.3` When sprint 收尾驗收,the system shall 以「指定站的 Gherkin 全綠」為判準,
|
||||||
|
而非「tasks 全關」。
|
||||||
|
- `EARS-2.3.4` When 偵測到某站相關實作變動,the system shall 查站點索引表並列出需重考的
|
||||||
|
Journey/Gherkin 清單(提醒,不阻擋)。
|
||||||
|
|
||||||
|
### E3 角色封路
|
||||||
|
|
||||||
|
**US-3.1**:身為系統,我要 agent 的身分由兩個機械來源決定,沒有任何一格靠 agent 自陳。
|
||||||
|
|
||||||
|
- `EARS-3.1.1` The system shall 以 `system-dev/.profile` 決定 **scope 軸**,
|
||||||
|
以環境變數 `AGENT_ROLE` 決定 **role 軸**。
|
||||||
|
- `EARS-3.1.2` If `AGENT_ROLE` 未設定,then the system shall 依 scope 推定預設 role
|
||||||
|
(orchestrator profile → orchestrator;repo profile → engineer),不詢問 agent。
|
||||||
|
- `EARS-3.1.3` If 身分組合落在「repo profile × orchestrator」這個**不存在的格子**,
|
||||||
|
then the system shall 攔截並要求修正環境變數,不得靜默降級。
|
||||||
|
|
||||||
|
**US-3.2**:身為系統,我要 orchestrator 寫不了 code、改不了技術軌文件。
|
||||||
|
|
||||||
|
- `EARS-3.2.1` If role 為 orchestrator 且寫入目標是程式碼路徑(`src/**`、`*.py`、`*.ts`、
|
||||||
|
`*.go`、`*.sh` 等),then the system shall 攔截(exit 2)。
|
||||||
|
- `EARS-3.2.2` If role 為 orchestrator 且寫入目標是 `tasks.md` / `requirements.md` /
|
||||||
|
`design.md`,then the system shall 攔截。
|
||||||
|
|
||||||
|
**US-3.3**:身為系統,我要 engineer 改不了考卷。
|
||||||
|
|
||||||
|
- `EARS-3.3.1` If role 為 engineer 且寫入目標是 `journeys.md` / `root.md` /
|
||||||
|
任何 `*.feature` 或 md 內的 Gherkin 區塊,then the system shall 攔截。
|
||||||
|
- `EARS-3.3.2` The system shall 在攔截訊息中說明「考生不能改考卷」與正確做法
|
||||||
|
(回報給 PM,由 PM 改站或改考題)。
|
||||||
|
|
||||||
|
**US-3.4**:身為 session,我醒來時世界已就位,不需要「知道」有哪些機制存在。
|
||||||
|
|
||||||
|
- `EARS-3.4.1` When SessionStart,the system shall 依 profile 注入對應的必讀
|
||||||
|
(orchestrator:root/journeys 與本 sprint 未點亮站;repo:現行 status/principles/mistakes)。
|
||||||
|
- `EARS-3.4.2` The system shall 於 profile 憲法留下 policy plugin 的 must-read 注入點,
|
||||||
|
使 W3 的 plugin SessionStart hook 能把政策必讀推到眼前,而框架本身不新造外掛格式。
|
||||||
|
|
||||||
|
### E4 防糾纏
|
||||||
|
|
||||||
|
**US-4.1**:身為框架維護者,我要實例改不了機制——要改就回框架提案。
|
||||||
|
|
||||||
|
- `EARS-4.1.1` If 寫入目標落在安裝產物區(`.claude/hooks/`、`system-dev/` 範本區、
|
||||||
|
plugin 安裝目錄),then the system shall 攔截並提示「機制變更走框架/政策包 repo 提案」。
|
||||||
|
- `EARS-4.1.2` While session 帶有框架開發標記(框架 repo 根目錄存在 `.sdt-framework-dev`
|
||||||
|
或環境變數 `SDT_FRAMEWORK_DEV=1`),the system shall 放行 EARS-4.1.1。
|
||||||
|
|
||||||
|
**US-4.2**:身為框架維護者,我要框架範本裡混進實例專名時 CI 就擋下。
|
||||||
|
|
||||||
|
- `EARS-4.2.1` When CI 執行,the system shall 掃描範本區,命中實例專名黑名單 → fail
|
||||||
|
並指出檔案與行號。
|
||||||
|
- `EARS-4.2.2` The system shall 讓黑名單住在可維護的設定檔,並提供**逐行豁免標記**
|
||||||
|
(留痕可審),供尚未搬遷的政策內容過渡使用。
|
||||||
|
- `EARS-4.2.3` The system shall 不對 policy pack 套用本檢查(政策包本來就是一家之言)。
|
||||||
|
|
||||||
|
**US-4.3**:身為安裝者,我要 update 告訴我「哪些檔被手改過」。
|
||||||
|
|
||||||
|
- `EARS-4.3.1` When 執行 update,the system shall 比對每個安裝產物的實際雜湊與 manifest
|
||||||
|
記錄的雜湊,差異者列入漂移清單並逐項提示處置選項。
|
||||||
|
- `EARS-4.3.2` The system shall 對漂移檔採「不覆蓋 + 另存新版供 diff」策略,不靜默覆寫。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、非功能需求
|
||||||
|
|
||||||
|
| 項 | 要求 | 為什麼 |
|
||||||
|
|---|---|---|
|
||||||
|
| 向下相容 | 既有實例(1.18.x)跑 update **不得 404**;已安裝檔案的遠端路徑不得搬移 | 舊實例跑的是**舊** update.sh,檔案清單與 base URL 寫死在裡面;1.16.0 已被「來源改動=自動更新死掉」咬過一次 |
|
||||||
|
| 雲端可跑 | 所有 hook 只用 repo 相對路徑與 POSIX 工具,不假設本機絕對路徑 | 分離 §三 common「封路 hook 工具箱(雲端可跑)」 |
|
||||||
|
| 容錯 | hook 解析失敗(拿不到 file_path/無 jq)一律放行並留痕,不誤殺 | 沿既有 hook 慣例(sdd-guard、wiki-secret-scan) |
|
||||||
|
| 誠實限制 | 每支 hook 頂部註明「擋語法層、擋不了 bash 繞道」,不宣稱不可繞過 | 沿既有 hook 慣例 |
|
||||||
|
| 安裝時間 | 乾淨環境雙 profile 各 < 10 分鐘 | 分離 §八 arch_iteration 預測① |
|
||||||
|
| bash 版本 | 相容 macOS bash 3.2(空陣列展開需先判長度) | install.sh 既有踩過的坑 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、驗收 Gherkin(唯一驗收線,tasks 逐條掛號)
|
||||||
|
|
||||||
|
> 來源:《JDD 導入規格》§七 三題(G1–G3)+《分離導入規格》§七 四題(G4–G7),原文照抄。
|
||||||
|
|
||||||
|
```gherkin
|
||||||
|
# ── G1(JDD §七之一)
|
||||||
|
Scenario: PM 總管優先補接縫而非做新功能
|
||||||
|
Given 安裝流程的所有零件均已 commit(部分未 push、部分接口未串)
|
||||||
|
And root.md 與 journeys.md 已就位,J-1(安裝者一次通關)已定義
|
||||||
|
When PM 總管接到指令「交付 J-1」
|
||||||
|
Then 它派出的第一批工作是補接縫(push 缺漏、串斷點)
|
||||||
|
And 不包含任何新功能開發
|
||||||
|
|
||||||
|
# ── G2(JDD §七之二)
|
||||||
|
Scenario: 考生改考卷被攔截
|
||||||
|
Given engineer subagent 的某站 Gherkin 未過
|
||||||
|
When 它嘗試修改 journeys.md 中的該條 Gherkin
|
||||||
|
Then hook 攔截,修改不生效
|
||||||
|
|
||||||
|
# ── G3(JDD §七之三)
|
||||||
|
Scenario: 進度以站計量
|
||||||
|
Given sprint 進行中
|
||||||
|
When 詢問 PM 總管目前進度
|
||||||
|
Then 回答形式為「J-x 已點亮 n/m 站」,而非 tasks 完成數
|
||||||
|
|
||||||
|
# ── G4(分離 §七之一)
|
||||||
|
Scenario: 憲法分流不靠判斷
|
||||||
|
Given template 已裝雙 profile
|
||||||
|
When 在總管 repo 開 session
|
||||||
|
Then CLAUDE.md 只含總管憲法(無 SDD 三件式細節)
|
||||||
|
When 在成員 repo 開 session
|
||||||
|
Then CLAUDE.md 只含 repo 憲法+一行上游指針
|
||||||
|
|
||||||
|
# ── G5(分離 §七之二)— 本 SDD 只負責插槽,Then 的後半由 W3 兌現
|
||||||
|
Scenario: 政策包即插即用
|
||||||
|
Given 乾淨實例已裝 repo profile
|
||||||
|
When 安裝 arcrun-policy plugin 並 spawn engineer 實作小功能
|
||||||
|
Then 白名單 hook 攔下 python 路徑、primer 出現在 session 開頭
|
||||||
|
And engineer 的交付物走 Arcrun 零件
|
||||||
|
|
||||||
|
# ── G6(分離 §七之三)
|
||||||
|
Scenario: 實例改機制被攔
|
||||||
|
Given 實例 session(非 --framework-dev)
|
||||||
|
When 嘗試編輯 .claude/hooks/ 下任一檔
|
||||||
|
Then hook 攔截並提示走框架提案
|
||||||
|
|
||||||
|
# ── G7(分離 §七之四)
|
||||||
|
Scenario: 框架混入實例名被 CI 擋
|
||||||
|
Given 範本檔新增一行含 "arcrun"
|
||||||
|
When CI 執行
|
||||||
|
Then 檢查 fail 並指出檔案與行號
|
||||||
|
```
|
||||||
|
|
||||||
|
**G5 的範圍切割(重要)**:本 SDD 交付的是 Given/When 能成立的**插槽**——
|
||||||
|
乾淨實例裝得出 repo profile、載入順序文件化、profile 憲法有 must-read 注入點。
|
||||||
|
`Then` 的兩句(白名單 hook 攔 python、primer 出現)由 **W3 的 arcrun-policy plugin** 兌現。
|
||||||
|
W2 收工時 G5 記 `◐ 半通(插槽就位,政策包未做)`,不得標 ✅。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、關聯
|
||||||
|
|
||||||
|
- 上游提案:`InkStoneCo/system-dev/docs/3-specs/pending-changes.md` §三 W2
|
||||||
|
- 需求輸入原件:`~/Desktop/總管/JDD-template-upgrade.md`、`~/Desktop/總管/分離導入規格.md`、
|
||||||
|
`~/Desktop/總管/總管系統藍圖v3.md`
|
||||||
|
- 生命週期規則:`docs/3-specs/SDD-LIFECYCLE.md`
|
||||||
|
- 下一波:W3 arcrun-policy plugin(本 SDD 的 G5 後半)
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
# jdd-dual-profile — Tasks
|
||||||
|
|
||||||
|
> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。
|
||||||
|
> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。
|
||||||
|
> **每一項都標「服務哪條 Gherkin」**(G1–G7 定義見 `requirements.md` §四)。
|
||||||
|
> 🟢 **status: active(2026-08-05 leo 回「開工」)**。
|
||||||
|
> 進度:**33/33 編號 task 全數完成**(2026-08-06)。版本 1.18.0 → **1.19.0**。
|
||||||
|
> 七題驗收:**✅ 5 題(G2/G3/G4/G6/G7,皆附實測輸出)/◐ 2 題**——
|
||||||
|
> G1 是行為題(要真的派一次工才驗得到)、G5 的政策包本體在 W3。
|
||||||
|
> 🔴 **狀態=等發佈**:更新來源是公開 GitHub raw,需 leo 開閘才送得到外部實例。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gherkin 對照速查
|
||||||
|
|
||||||
|
| 號 | 一句話 | 來源 |
|
||||||
|
|---|---|---|
|
||||||
|
| G1 | PM 總管優先補接縫而非做新功能 | JDD §七 |
|
||||||
|
| G2 | 考生改考卷被攔截 | JDD §七 |
|
||||||
|
| G3 | 進度以站計量 | JDD §七 |
|
||||||
|
| G4 | 憲法分流不靠判斷 | 分離 §七 |
|
||||||
|
| G5 | 政策包即插即用(本波只到 ◐ 半通) | 分離 §七 |
|
||||||
|
| G6 | 實例改機制被攔 | 分離 §七 |
|
||||||
|
| G7 | 框架混入實例名被 CI 擋 | 分離 §七 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0:地基(manifest + 兩軸判定)
|
||||||
|
|
||||||
|
### 前置條件
|
||||||
|
- [ ] leo confirm 本 SDD,frontmatter 由 `draft` 改 `active`
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [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,不搬檔)
|
||||||
|
|
||||||
|
- [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)
|
||||||
|
- 驗收:以四組身分(orchestrator×orchestrator、orchestrator×engineer、
|
||||||
|
repo×engineer、repo×orchestrator)跑單元測試,最後一組回 exit 2;貼四組輸出
|
||||||
|
- 注意:不得用 `CLAUDE_PROJECT_DIR`(雲端可跑);bash 3.2 相容
|
||||||
|
|
||||||
|
- [x] 0.3 立 marker 檔約定:`system-dev/.profile`、`system-dev/.template-manifest`、`.sdt-framework-dev`
|
||||||
|
- 服務:**G4**、**G6**
|
||||||
|
- 驗收:三個檔的格式各寫一段規格進 design 的附錄,並在框架 repo 自己放一份
|
||||||
|
`.sdt-framework-dev`(commit)
|
||||||
|
- 注意:`--framework-dev` 不是官方 CLI flag(決策 D9),別去找那個參數
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1:雙 profile 與 CLAUDE.md 生成
|
||||||
|
|
||||||
|
> 前置條件:Phase 0 全部完成
|
||||||
|
|
||||||
|
- [x] 1.1 建 `template/profiles/repo/CLAUDE.md`(repo 憲法範本)
|
||||||
|
- 服務:**G4**
|
||||||
|
- 來源:現行 `template/CLAUDE.md` 演進;加「上游指針」一行、「站號怎麼標在 task 上」一段、
|
||||||
|
W3 must-read 注入點一段
|
||||||
|
- 驗收:`grep -c "上游" ≥ 1`;全文不含任何實例專名
|
||||||
|
|
||||||
|
- [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 的內容要抽象化,專案名留給實例填)
|
||||||
|
|
||||||
|
- [x] 1.3 `install.sh` 加 `--profile=repo|orchestrator` + 自動偵測 + 一次性人確認
|
||||||
|
- 服務:**G4**
|
||||||
|
- 偵測規則:目前目錄下存在多個各自帶 `.git` 的子目錄 → 建議 orchestrator
|
||||||
|
- 驗收:三種呼叫(明示 repo/明示 orchestrator/不指定走偵測)各跑一次乾淨環境,
|
||||||
|
貼出各自產生的 `system-dev/.profile` 內容
|
||||||
|
- 注意:**確認在寫入任何檔案之前**問,別裝了一半才問
|
||||||
|
|
||||||
|
- [x] 1.4 `install.sh` 改讀 manifest 鋪設產物 + 產 `.template-manifest`
|
||||||
|
- 服務:**G4**、G6
|
||||||
|
- 驗收:repo profile 裝出的檔案集合 = `common.tsv ∪ repo.tsv`,
|
||||||
|
且**不含** orchestrator 專屬檔(`ls` 對照貼出)
|
||||||
|
|
||||||
|
- [x] 1.5 CLAUDE.md 三段組裝(框架區界標 + 本地補充區界標 + sha256)
|
||||||
|
- 服務:**G4**
|
||||||
|
- 驗收:裝完的 CLAUDE.md 含 `sdt:framework begin/end` 與 `sdt:local begin/end` 四個界標,
|
||||||
|
且 `sha256` 值與框架區實際內容相符(重算比對貼出)
|
||||||
|
|
||||||
|
- [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`
|
||||||
|
|
||||||
|
- [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 一眼看得懂該做什麼)
|
||||||
|
- 2026-08-06 還清:update.sh 檔案清單已改讀 manifest(舊硬編清單降為抓不到來源時的 fallback),根因全修
|
||||||
|
|
||||||
|
- [x] 1.8 `update.sh` 舊實例遷移:無 manifest → 一次性補植 + CLAUDE.md 界標補植
|
||||||
|
- 服務:**G4**、G6
|
||||||
|
- 驗收:拿一份 1.18.0 的實例副本跑兩次 update,第二次為 no-op(冪等,貼兩次輸出對照)
|
||||||
|
- 注意:舊 CLAUDE.md 整份包進 `sdt:local` 區,一個字都不能掉
|
||||||
|
- 2026-08-06 還清:界標補植已實作並實測(舊全文原封包進本地區、原檔備份、冪等)
|
||||||
|
|
||||||
|
- [x] 1.9 `template/CLAUDE.md` 原路徑保留轉址說明(向下相容)
|
||||||
|
- 服務:**G4**
|
||||||
|
- 驗收:舊版 update.sh 對該路徑的 curl 仍回 200(決策 D3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2:JDD 文件範本(orchestrator profile)
|
||||||
|
|
||||||
|
> 前置條件:Phase 1 完成(範本要靠 manifest 才鋪得下去)
|
||||||
|
|
||||||
|
- [x] 2.1 `docs/root.md.template`(白話根文件範本)
|
||||||
|
- 服務:**G1**
|
||||||
|
- 內容:JDD §3.1 格式原樣 + 三條規則(來源標記必附、禁技術名詞、🔴 卡必附對帳日)
|
||||||
|
- 驗收:範本自身通得過 task 3.2 的 J4/J8 檢查(自己吃自己狗糧)
|
||||||
|
|
||||||
|
- [x] 2.2 `docs/journeys.md.template`(PM 驗收文件範本)
|
||||||
|
- 服務:**G1**、**G3**
|
||||||
|
- 內容:JDD §3.2 三層巢狀 +「附:站點索引表」段 + 站全域編號/引用不重抄的註記
|
||||||
|
- 驗收:範本含站點索引表且欄位為「站|被哪些 Journey 經過|改動時重考範圍」
|
||||||
|
|
||||||
|
- [x] 2.3 `docs/sprint.md.template`(站號 sprint)+ tasks.md 站號欄位約定
|
||||||
|
- 服務:**G3**
|
||||||
|
- 內容:JDD §五 四步流程、順序鐵律「先認領 → 認領不足才新增 → 新增必掛站」、
|
||||||
|
起訖日與到期結算欄
|
||||||
|
- 驗收:範本能被 task 3.4 的 J6 判準讀出「本 sprint 指定站」與「未點亮站」
|
||||||
|
|
||||||
|
- [x] 2.4 `docs/triage-map.md.template`(分診表格式,內容留實例)
|
||||||
|
- 服務:**G1**
|
||||||
|
- 驗收:只有欄位與規則,**零實例內容**(通得過 task 4.1 的專名檢查)
|
||||||
|
|
||||||
|
- [x] 2.5 `docs/plugin-load-order.md`(W3 插槽文件)+ 兩份憲法的 must-read 注入點
|
||||||
|
- 服務:**G5(半通的那一半)**
|
||||||
|
- 內容:分離 §五 四步載入順序原文 +「政策包=官方 plugin,框架不發明平行格式」的約定
|
||||||
|
- 驗收:兩份 profile CLAUDE.md 各含一段可被 plugin SessionStart hook 填入的注入點標題
|
||||||
|
|
||||||
|
- [x] 2.6 JDD 術語表寫進 orchestrator 憲法(Journey 取代 CP,禁用舊詞)
|
||||||
|
- 服務:**G3**
|
||||||
|
- 驗收:術語表七詞(Journey/Station/通關/點亮/對帳/完備/輪子卡·賭注卡)齊全,
|
||||||
|
且標明 `CP.yaml`/`CPDO.md` 屬技術軌內圈保留原名
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3:封路 hook
|
||||||
|
|
||||||
|
> 前置條件:Phase 0(role-lib)+ Phase 2(有檔可擋)
|
||||||
|
|
||||||
|
- [x] 3.1 `role-guard.sh`(J1+J2+J3,common)
|
||||||
|
- 服務:**G2**(命門)
|
||||||
|
- 內容:orchestrator 禁寫 code 路徑/禁寫 tasks·requirements·design;
|
||||||
|
engineer 禁寫 journeys·root·`*.feature`·md 內 Gherkin 區塊;矩陣空格 exit 2
|
||||||
|
- 驗收:**六組實測**各貼 stdout/exit code——
|
||||||
|
① orchestrator 寫 `.py` → 擋 ② orchestrator 寫 `tasks.md` → 擋
|
||||||
|
③ engineer 改 `journeys.md` 的 Gherkin → 擋(**這條就是 G2**)
|
||||||
|
④ engineer 寫 `.py` → 放行 ⑤ orchestrator 寫 `journeys.md` → 放行
|
||||||
|
⑥ repo profile × `AGENT_ROLE=orchestrator` → 擋並要求修正環境
|
||||||
|
- 注意:Gherkin 區塊偵測要涵蓋 md 內的 ```gherkin fence 與 `- **G-x.y** Given` 行式
|
||||||
|
|
||||||
|
- [x] 3.2 `jdd-format-guard.sh`(J4+J5+J8,common)
|
||||||
|
- 服務:**G1**、G3
|
||||||
|
- J4:`root.md` 🔴 卡缺【要驗證+對帳日】→ 擋並指行號
|
||||||
|
- J5:`tasks.md` **新增**行缺站號 → 擋(Edit 看 `new_string`,Write 比對現檔差異)
|
||||||
|
- J8:`root.md`/`journeys.md` 技術名詞黑名單命中 → 擋並列詞+行號
|
||||||
|
- 驗收:三條各一組正例一組反例,共六次實測貼輸出
|
||||||
|
- 注意:J5 只判**新增**行,改既有行不擋(否則格式修正都做不了);誠實限制寫進註解
|
||||||
|
|
||||||
|
- [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 時實例那支要退役,別兩處維護
|
||||||
|
|
||||||
|
- [x] 3.4 `regression-scope.sh`(J7,common)
|
||||||
|
- 服務:**G3**
|
||||||
|
- 行為:偵測站相關實作變動 → 讀 journeys.md 站點索引表 → 列需重考的 Journey/Gherkin
|
||||||
|
- 驗收:改動某站的實作檔後,輸出正確列出該站被哪些 Journey 經過(貼輸出)
|
||||||
|
- 注意:**提醒不阻擋**(exit 0)
|
||||||
|
|
||||||
|
- [x] 3.5 `install-artifact-guard.sh`(S1,common)
|
||||||
|
- 服務:**G6**
|
||||||
|
- 行為:寫入 `.claude/hooks/`/`system-dev/` 範本區/plugin 安裝目錄 → 擋,
|
||||||
|
提示「機制變更走框架/政策包 repo 提案」;`.sdt-framework-dev` 或 `SDT_FRAMEWORK_DEV=1` 放行
|
||||||
|
- 驗收:① 實例 session 編輯 `.claude/hooks/` 任一檔 → 擋(**這條就是 G6**)
|
||||||
|
② 框架 repo(有 marker)編輯同路徑 → 放行。兩組都貼輸出
|
||||||
|
- 注意:「範本區」的定義來自 manifest(class=overwrite 者),不要另寫一份路徑表
|
||||||
|
|
||||||
|
- [x] 3.6 `orchestrator-scope-guard.sh`(S4,orchestrator profile 專屬)
|
||||||
|
- 服務:**G6**
|
||||||
|
- 來源:改寫自 InkStoneCo 實例的 `guard-cross-project.sh`(上收進框架,決策 D6)
|
||||||
|
- 抽象化重點:子 repo 目錄清單、autodispatch 白名單**由實例設定檔提供**,
|
||||||
|
範本內**零實例專名**(通得過 task 4.1)
|
||||||
|
- 驗收:以假造的成員目錄結構跑三組——寫子 repo 的 `.py` → 擋/寫子 repo 的 `.md` → 放行/
|
||||||
|
`CHILD_SESSION=1` 且在白名單內 → 放行
|
||||||
|
- 注意:職責與 3.1 不重疊——3.1 管「角色能寫什麼**類型**」,本支管「總管能進哪個**位置**」
|
||||||
|
|
||||||
|
- [x] 3.7 改 `session-start-recall.sh`:依 profile 分流注入【改既有】
|
||||||
|
- 服務:**G3**、G4
|
||||||
|
- orchestrator:root/journeys 摘要 + 本 sprint **未點亮**站(治「起牀沒事做」)
|
||||||
|
- repo:維持現行 principles/status/mistakes
|
||||||
|
- 驗收:兩 profile 各開一次 session,貼注入內容對照(orchestrator 那份要出現站號)
|
||||||
|
|
||||||
|
- [x] 3.8 hook 全鏈掛載順序實測(design §4.4)
|
||||||
|
- 服務:**G2**、G6
|
||||||
|
- 驗收:故意觸發多條規則的一次寫入,確認錯誤訊息來自**最外層**那條(範圍大的先擋)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4:框架側 CI 與清潔
|
||||||
|
|
||||||
|
> 前置條件:Phase 1(manifest 定義了「範本區」)
|
||||||
|
|
||||||
|
- [x] 4.1 `scripts/check-no-instance-names.sh` + `scripts/instance-names.txt`
|
||||||
|
- 服務:**G7**
|
||||||
|
- 行為:掃 `template/`,命中黑名單 → exit 1 並印「檔案:行號:命中詞」;
|
||||||
|
支援逐行豁免標記(行尾 `# sdt-instance-name-ok`);policy pack 路徑不受檢
|
||||||
|
- 驗收:在範本檔新增一行含 "arcrun" → 檢查 fail 並指出檔案與行號(**這條就是 G7**),貼輸出
|
||||||
|
|
||||||
|
- [x] 4.2 現況 20 行命中的分診處置(design §0.5)
|
||||||
|
- 服務:**G7**
|
||||||
|
- (a) 6 行註解/舉例 → 改寫成通用敘述
|
||||||
|
- (b) 3 行政策內容 + (c) 13 行 L2 產物 → 標 `# sdt-instance-name-ok` + 一行「W3 搬遷」註記
|
||||||
|
- 驗收:處置後 `check-no-instance-names.sh` 回 exit 0,且豁免行數 = 16(貼清單)
|
||||||
|
- 注意:**禁止假性清理**(把 arcrun 改寫成「某工作流引擎」=資訊消失、問題還在)
|
||||||
|
|
||||||
|
- [x] 4.3 掛 pre-commit / CI
|
||||||
|
- 服務:**G7**
|
||||||
|
- 驗收:`bash scripts/check-no-instance-names.sh` 在 CI 步驟中被呼叫,故意違規的 commit 被擋
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5:出貨(ship-check)
|
||||||
|
|
||||||
|
> 前置條件:Phase 0–4 全部完成
|
||||||
|
|
||||||
|
- [x] 5.1 CHANGELOG + VERSION bump(**兩處**:`template/.claude/VERSION` + `template/system-dev/VERSION`)
|
||||||
|
- 服務:全部
|
||||||
|
- 版號:1.18.0 → **1.19.0**(新功能、向下相容)
|
||||||
|
- 驗收:兩個 VERSION 檔內容一致;CHANGELOG 用用戶語言寫「這一版你會多出什麼」
|
||||||
|
- 注意:版本號是 leo 唯一的驗收介面——沒動=等於沒交付
|
||||||
|
|
||||||
|
- [x] 5.2 乾淨環境雙 profile 安裝實測(含計時)
|
||||||
|
- 服務:**G4**
|
||||||
|
- 驗收:兩個全新空目錄各裝一次,**各記錄實際耗時**(分離 §八 預測①:各 < 10 分鐘),
|
||||||
|
貼安裝輸出 + `ls -R` 檔案清單對照
|
||||||
|
|
||||||
|
- [x] 5.3 七題 Gherkin 逐條實測並記錄三態
|
||||||
|
- 服務:**G1–G7**
|
||||||
|
- 驗收:每題貼實測輸出並標 `✅ 通 / ◐ 半通(缺什麼)/ ❌ 斷`
|
||||||
|
- 注意:**G5 本波上限 `◐`**(插槽就位、政策包在 W3),標 ✅ 就是假綠;
|
||||||
|
G1 需要 root/journeys 有實內容才驗得到端到端 → 若實例尚未落地(W4),
|
||||||
|
以框架附的示範 fixture 驗,並在報告標明「以 fixture 驗,實例端到端待 W4」
|
||||||
|
|
||||||
|
- [x] 5.4 回報總管:交付物、三態表、與規格假設不符的清單
|
||||||
|
- 服務:全部
|
||||||
|
- 驗收:報告含 design §0 全部實查發現 + 本波實際落地的 hook 數(改既有 vs 新增)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成定義
|
||||||
|
|
||||||
|
整個 SDD 完成 = 以下全部達成:
|
||||||
|
- [ ] 所有 tasks 標 [x]
|
||||||
|
- [ ] G1–G7 逐題有實測證據,且無任何一題為 `❌ 斷`(G5 可為 `◐`)
|
||||||
|
- [ ] `bash scripts/check-no-instance-names.sh` 回 exit 0
|
||||||
|
- [ ] `bash scripts/sdd-active-check.sh docs/3-specs` 回 exit 0
|
||||||
|
- [ ] 兩處 VERSION = 1.19.0,CHANGELOG 已寫
|
||||||
|
- [ ] design.md 與實作一致(如有出入需更新 design,不是默默改 code)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 狀態說明
|
||||||
|
|
||||||
|
| 標記 | 意義 |
|
||||||
|
|------|------|
|
||||||
|
| `[ ]` | 未開始 |
|
||||||
|
| `[🔄]` | 進行中(當前 session)|
|
||||||
|
| `[x]` | 完成(有驗收證據)|
|
||||||
|
| `[~]` | 暫緩(說明原因)|
|
||||||
|
| `[!]` | 阻擋中(說明阻擋原因)|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Pending Changes(規格變更緩衝區)
|
||||||
|
|
||||||
|
> 規則來源:`SDD-LIFECYCLE.md` 第 3、4 條。
|
||||||
|
> 規格層變更(核心設計/方向改變)**只有這一條路**:CC 把 change proposal 寫進「待裁決」——
|
||||||
|
> 變更摘要與觸發原因+影響分析(現行 SDD 哪些任務作廢/修改/不受影響/尚未完成)——然後**停止**,
|
||||||
|
> 等使用者明說「confirm」才依第 4 條開新 SDD;沒 confirm 就繼續依現行 SDD 工作。
|
||||||
|
> 多個 proposal 可並存,由人一次裁決。本檔不是 SDD,不掛 status。
|
||||||
|
|
||||||
|
## 待裁決
|
||||||
|
|
||||||
|
(無)
|
||||||
|
|
||||||
|
## 已裁決
|
||||||
|
|
||||||
|
(無——裁決後從「待裁決」移到這裡留底,標 confirmed / rejected + 日期。)
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# tasks-project-projection — Design
|
||||||
|
|
||||||
|
> 狀態:草稿 v2(依總管 2026-06-27 設計修正改寫;待 leo 審核)
|
||||||
|
> 建立:2026-06-27 | 最後更新:2026-06-27
|
||||||
|
> 負責人:leo(uncle6me-web)
|
||||||
|
> 來源:issue #16(InkStoneCo 總管);脈絡 InkStoneCo 北極星 §5.2
|
||||||
|
>
|
||||||
|
> v2 變更:廢棄「`HAS_ARCRUN` 檔案指紋偵測」整條路(arcrun workflow 存遠端 KV、不需本地檔,掃檔會 false negative)。改為「裝/init 對話 + 能力查詢(cli/mcp)+ 一次性廣告 + 手動啟用入口」。兩個 🔴 blocker(安裝指紋路徑 / 專案層 `.arcrun.yaml`)連帶消失。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一句話說明
|
||||||
|
|
||||||
|
把 `system-dev/docs/3-specs/*/tasks.md` **單向投影**成唯讀 GitHub Project(機器/dashboard 好抓);md 永遠是唯一真相源。是否啟用=**裝/init 時 AI 問用戶一句、查環境有沒有 arcrun 能力**(cli/mcp),不靠掃本地檔。沒 arcrun/用戶不要 → 純 md、完全 no-op,並做一次性溫和廣告,之後閉嘴。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景與問題
|
||||||
|
|
||||||
|
- 純 md 的 tasks.md 對人友善,但機器/dashboard 難抓進度(要 parse markdown checkbox、跨多組 SDD 聚合)。
|
||||||
|
- 想要 GitHub Project 的看板視圖,又**不想開第二個真相源**——人手動拖 Project 卡 ⇄ md 改字會兩邊打架。
|
||||||
|
- 既有自動化紅線:**禁定期輪詢、禁 Actions 因事件 fan-out**(避免被 GitHub flag)。任何投影方案不得違反。
|
||||||
|
- 目標用戶 low-code、只會叫 CC 做事 → 不能要求用戶手動建 Project、手動填 id、手動跑 sync。
|
||||||
|
|
||||||
|
→ 解法限定為:**單向(md → Project)、md 當家、push 後本機觸發一次、optional 模組預設不逼**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 範圍
|
||||||
|
|
||||||
|
### 包含(In Scope)
|
||||||
|
- 在 **template** 新增一個 optional 模組:arcrun 投影工作流(YAML 工作流骨架 + README 標「需 arcrun,`acr push` 啟用」)。
|
||||||
|
- **裝/init 對話**:安裝或第一次 init 時 AI 問一句白話「要不要把待辦同步到 GitHub」;答好 → 查環境有沒有 arcrun(自己的 mcp 設定 / `acr` 在不在 PATH)→ 有就設定、沒有就一次性廣告。
|
||||||
|
- **手動啟用入口**:用戶第一次答「不好」、之後想開,有路可走(叫 AI 啟用,不需重裝)。
|
||||||
|
- 單向同步邏輯設計:md → GitHub Project,按穩定 id 增量(非全量重寫),glob 掃多組 `tasks.md`。
|
||||||
|
- arcrun 唯一對 md 的寫入:新 task 首次同步時,把 `<!-- gh:<id> -->` 註解 append 到那一行末(不碰既有內容)。
|
||||||
|
|
||||||
|
### 不包含(Out of Scope)
|
||||||
|
- **反向同步**(Project → md):永不做。Project 唯讀,避免雙真相源。
|
||||||
|
- 定期輪詢、GitHub Actions 觸發、cron:全部禁止(守 flag 紅線)。
|
||||||
|
- 不要同步的用戶任何行為改變:完全 no-op;除裝/init 那**一次**問句與沒裝時的**一次**廣告外,不再追問、不重複廣告。
|
||||||
|
- arcrun 本體的安裝/工作流引擎(那是 arcrun 的事,本模組只是「給 arcrun 跑的一份 YAML 工作流」)。
|
||||||
|
- 把投影邏輯實作成獨立輪詢 daemon 或 GitHub App。
|
||||||
|
- **掃本地檔判斷有沒有 arcrun**(廢棄):arcrun workflow 存遠端 KV、`arcrun_push_workflow` 收字串不讀本地檔,一個專案可零 arcrun 檔卻在用 arcrun → 掃檔 false negative。判準改「能力(cli/mcp)」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 設計
|
||||||
|
|
||||||
|
### 架構概覽
|
||||||
|
|
||||||
|
```
|
||||||
|
push 後本機觸發(單次,非輪詢)
|
||||||
|
│
|
||||||
|
tasks.md(唯一真相源)──┘
|
||||||
|
*.md 多組 ──► git diff(只看改了哪幾行)
|
||||||
|
│
|
||||||
|
┌───────┴────────┐
|
||||||
|
│ 投影工作流 │(Arcrun workflow 格式)
|
||||||
|
│ classify 四動作 │
|
||||||
|
└───────┬────────┘
|
||||||
|
▼
|
||||||
|
gh issue create / close / edit / archive
|
||||||
|
▼
|
||||||
|
GitHub Project(唯讀投影,給 dashboard 抓)
|
||||||
|
│
|
||||||
|
新 task → 把 <!-- gh:id --> 寫回 md 那一行(唯一回寫)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 啟用判準:對話 + 能力查詢(不掃檔,取代舊 `HAS_ARCRUN`)
|
||||||
|
|
||||||
|
> ⚠️ 廢棄舊設計。原本想抄 `HAS_WIKI`/`HAS_SDD` 的「掃本地檔指紋」模式,但**對 arcrun 不成立**:
|
||||||
|
> - `arcrun_push_workflow` 收 `yaml_content` 字串或 `graph` 物件,**不讀本地檔**(mcp/src/tools/arcrun_workflow_crud.ts:35-146)。
|
||||||
|
> - workflow 真身存**遠端 KV**(`{api_key}:wf:*`,webhooks-named.ts:52-104);list/run/delete 全是 API。
|
||||||
|
> - → 一個專案可**零 arcrun 檔案**卻完全在用 arcrun。掃檔當指紋會 **false negative**。
|
||||||
|
>
|
||||||
|
> 判準改為**「碰不碰得到 arcrun 能力(cli/mcp)」**,落地成一段對話,不是 shell 偵測。
|
||||||
|
|
||||||
|
**流程(裝/init 時跑一次)**
|
||||||
|
|
||||||
|
```
|
||||||
|
安裝 或 第一次 init
|
||||||
|
│
|
||||||
|
└─ AI 問白話一句:「您需要把本專案的待辦事項同步到 GitHub 嗎?」
|
||||||
|
│
|
||||||
|
├─ 答「好」→ AI 查環境有沒有 arcrun(自己的 mcp 設定有沒有 arcrun tool / `acr` 在不在 PATH)
|
||||||
|
│ ├─ 有 → 啟動同步設定(push 投影 workflow),完成。
|
||||||
|
│ └─ 沒有 → 一次性溫和廣告:
|
||||||
|
│ 「抱歉,您還沒安裝 Arcrun,無法啟用。Arcrun 是免費的 AI-friendly
|
||||||
|
│ 工作流套件——想裝直接跟 Claude 說就行。之後也可手動啟用同步。」
|
||||||
|
│
|
||||||
|
└─ 答「不好」→ 不做,且不再追問。
|
||||||
|
```
|
||||||
|
|
||||||
|
**三個設計意圖(務必守住)**
|
||||||
|
|
||||||
|
1. **判準=能力不是檔案**:沒設定檔/沒 readme ≠ 沒 arcrun。問「cli/mcp 裝了沒」。
|
||||||
|
2. **讓用戶知道有這東西**:非專家不知道「有 arcrun、有同步功能」→ 問一次=自然揭露。
|
||||||
|
3. **一次廣告、不一直廣告**:沒裝時做**一次**溫和廣告(免費/跟 Claude 說就能裝/以後可手動啟用),之後閉嘴,別每次 install 都騷擾。
|
||||||
|
|
||||||
|
**手動啟用入口**:第一次答「不好」後想開 → 叫 AI 啟用(AI 重跑「查能力 → push workflow」那段),不需重裝。
|
||||||
|
(具體入口形態——slash command vs 純對話——待實作細化;low-code 用戶只需「跟 AI 說」。)
|
||||||
|
|
||||||
|
**對齊北極星**:install 完即可用、單一 AI 入口、不留抽象前置步驟。用戶不碰任何 `HAS_*` 抽象檔,就是被 AI 問一句、答一句。
|
||||||
|
|
||||||
|
### 穩定 id 與增量同步
|
||||||
|
|
||||||
|
- **id 埋在 md 行內**:`- [ ] 實作 X <!-- gh:42 -->`。首次同步前無 id;create 後 Arcrun 回寫。
|
||||||
|
- **增量判準=git diff**:push 後本機觸發拿 `git diff` 的前後版,只處理「動到的行」,不全量重掃(省 API、避免無謂 edit)。
|
||||||
|
- **四種動作**(issue 定案,照抄):
|
||||||
|
|
||||||
|
| md 狀態 | 動作 |
|
||||||
|
|---------|------|
|
||||||
|
| 有文字、無 id | `gh issue create` → 把 `<!-- gh:id -->` 寫回該行 |
|
||||||
|
| 有 id 且 `[ ]→[x]` | `gh issue close` |
|
||||||
|
| 有 id 且 文字/負責人/日期改 | `gh issue edit` |
|
||||||
|
| id 在、但整行不見 | `gh issue close`/archive |
|
||||||
|
|
||||||
|
### 多組 SDD 全同步
|
||||||
|
|
||||||
|
- glob 掃 `system-dev/docs/3-specs/*/tasks.md`,每組獨立。
|
||||||
|
- 每組帶**子系統 label**(取 folder 名)分組,方便 Project 過濾。
|
||||||
|
- 新開 SDD folder → 新 `tasks.md` 首次 commit 即自動成新組,**無需手動登記**(守 low-code)。
|
||||||
|
|
||||||
|
### 守紅線:觸發方式
|
||||||
|
|
||||||
|
- **本機 push 後觸發單次**(git post-push 類 hook 或 Arcrun 的 push 事件鉤子),**單一目標**。
|
||||||
|
- 明令禁止:cron/定期輪詢/GitHub Actions on push fan-out。
|
||||||
|
- 觸發後做完即止,不常駐、不重試輪詢。
|
||||||
|
|
||||||
|
### 關鍵決策
|
||||||
|
|
||||||
|
| 決策 | 選擇 | 原因 | 放棄的選項 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 同步方向 | 單向 md→Project | md 當家,杜絕雙真相源打架 | 雙向同步(會兩邊衝突) |
|
||||||
|
| 真相源 | tasks.md | 人/CC 都只改 md,Project 唯讀 | Project 當真相源(low-code 用戶碰不到) |
|
||||||
|
| 觸發 | push 後本機單次 | 守 flag 紅線 | 定期輪詢/Actions(踩紅線) |
|
||||||
|
| 增量依據 | 行內穩定 id + git diff | 省 API、不全量重寫 | 全量 diff title 比對(脆、易撞名) |
|
||||||
|
| 啟用判準 | 裝/init 對話 + 查 cli/mcp 能力 | 能力≠檔案;arcrun 存遠端 KV,掃檔 false negative | `HAS_ARCRUN` 檔案指紋(漏判用遠端沒落檔者,**廢棄**) |
|
||||||
|
| 沒裝時 | 一次性溫和廣告 + 之後閉嘴 | 揭露功能存在又不騷擾 | 每次 install 都廣告(騷擾)/完全靜默(用戶不知有此功能) |
|
||||||
|
| 腳本形態 | arcrun workflow(YAML:name/description/flow/config) | 守「什麼都叫 arcrun」;`acr push` 部署 | 獨立 daemon/App(多一套要維護) |
|
||||||
|
| md 回寫 | 只在新 task 加 `<!-- gh:id -->` | 最小侵入,leo 已接受 | 在 md 維護更多 metadata(污染 md) |
|
||||||
|
|
||||||
|
### 介面 / 落點(待實作細化)
|
||||||
|
|
||||||
|
- 工作流檔形態:arcrun YAML(`name/description/flow/config` 結構)。template 放 `workflows/<投影>.yaml`,README 標「需 arcrun,`acr push <檔>` 啟用」。(最終以 arcrun 端 SDD 定案為準。)
|
||||||
|
- README:optional 模組區塊標「此功能需 arcrun;不要/沒裝則純 md no-op」。
|
||||||
|
- install/update:**不靠 `HAS_*` 分支裝檔**。改由裝/init 對話驅動——AI 在用戶答「好」且查到 arcrun 能力時,`acr push` 那份 workflow;workflow YAML 本身可隨 template 一起帶(留作記錄+手動啟用素材),但帶檔 ≠ 啟用。
|
||||||
|
- 啟用=遠端 push 了 workflow,不是本地有檔。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 風險與待解
|
||||||
|
|
||||||
|
| 項目 | 狀態 |
|
||||||
|
|------|------|
|
||||||
|
| ~~arcrun 安裝指紋路徑~~ | ✅ 消失(廢棄掃檔,改能力查詢) |
|
||||||
|
| ~~arcrun workflow 檔標準落點 blocker~~ | ✅ 降級:慣例 `workflows/*.yaml` + `acr push`,最終以 arcrun 端 SDD 為準 |
|
||||||
|
| 「查 arcrun 能力」的具體判準(mcp tool 名/`acr` PATH 偵測法) | 🟡 待 arcrun 端 template 對接定案 |
|
||||||
|
| push 後本機觸發的具體掛載點(git hook vs arcrun 事件鉤) | 🟡 待 arcrun 觸發能力確認 |
|
||||||
|
| id 回寫造成 working tree 變動(觸發後 md 有新 diff) | 🟡 需設計:回寫不應再觸發一輪(避免迴圈,§迴圈防護) |
|
||||||
|
| 手動啟用入口形態(slash command vs 純對話) | 🟡 待實作細化 |
|
||||||
|
|
||||||
|
> 不再有 🔴 blocker。施工順序:**leo 定調「等 arcrun 動工完再施工」**——等 arcrun 端把 template 對接 + 觸發/能力查詢定案,回來確認後再寫骨架。本 SDD 先把設計改成這版對話式、定案待審。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 與既有模式的對齊檢查
|
||||||
|
|
||||||
|
- ⚠️ **不沿用** `HAS_WIKI`/`HAS_SDD` 的掃檔指紋模式——對 arcrun 不成立(存遠端 KV)。改「能力查詢 + 對話」,這是與既有兩模組的**刻意分歧**,原因見上。
|
||||||
|
- ✅ optional 預設關:不要/沒裝 arcrun 完全 no-op(對齊「沒 wiki 就不裝 wiki hooks」的精神,只是判準從檔案換成能力+意願)。
|
||||||
|
- ✅ 守 flag 紅線(issue-handle skill 既有的避免被 flag 鐵律):單向、push 後本機單次、禁輪詢/Actions。
|
||||||
|
- ✅ low-code 友善:被 AI 問一句答一句、自動建組、id 自動回寫,用戶不碰抽象前置步驟。
|
||||||
|
- ✅ 不騷擾:沒裝時一次廣告即止(對齊「不增加用戶負擔」)。
|
||||||
|
- ✅ 本 SDD 為內部記錄,落 `docs/3-specs/`(gitignore,不推 GitHub)。
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# tasks-project-projection — Tasks
|
||||||
|
|
||||||
|
> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。
|
||||||
|
> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。
|
||||||
|
> 來源:issue #16;design.md 同目錄。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1:投影 workflow + 本地觸發端(template 側,可獨立)
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
|
||||||
|
- [x] 1.1 寫投影 workflow yaml(`template/system-dev/workflows/tasks-project-sync.yaml`)
|
||||||
|
- 驗收:`foreach` 增量 → `switch` 動作 → 四動作(create/close/edit/archive)+ Projects v2;`acr validate --offline` 過;**每個 component 經 `acr parts` 核實存在**。
|
||||||
|
- 注意(已踩坑):arcrun 沒有 `github` 零件,打 API 一律 `component: http_request`;credential 引用 `{{creds.github_token}}`(非 `{{secret.}}`)。
|
||||||
|
|
||||||
|
- [x] 1.2 寫本地觸發端(`tasks-project-sync.local.sh`)
|
||||||
|
- 驗收:讀 tasks.md / git diff / 分類四動作 / `acr run -i` 餵增量 / 回寫 id 的骨架就位;薄殼、不自刻 parser(複雜分類交 CC)。
|
||||||
|
- 注意:arcrun 跑遠端 CF Workers 無本地 fs/git,這三件本來就歸本地端,非 arcrun 缺口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2:裝/init 對話 + 一次性廣告 + 帶檔
|
||||||
|
|
||||||
|
- [x] 2.1 install.sh 加裝/init 對話指引 + 一次性廣告 + 隨 SDD 模組帶 workflow 檔
|
||||||
|
- 驗收:`bash -n` 過;下一步段有「問一句→查 arcrun 能力→有就設定/沒有就一次廣告」;帶檔 ≠ 啟用。
|
||||||
|
|
||||||
|
- [x] 2.2 update.sh 隨 SDD 模組 `add_if_missing` workflow 檔 + chmod
|
||||||
|
- 驗收:`bash -n` 過;覆蓋不會關掉誰的同步(啟用狀態存遠端)。
|
||||||
|
|
||||||
|
- [x] 2.3 README(中英)標 optional 模組 + bump 1.13.0 + CHANGELOG
|
||||||
|
- 驗收:兩份 README 對稱;VERSION 兩檔同步 1.13.0;CHANGELOG 記 http_request 修正與核實紀律。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3:端到端驗證(待連 arcrun 後端的人)
|
||||||
|
|
||||||
|
> 前置條件:Phase 1、2 完成(已完成)
|
||||||
|
|
||||||
|
- [!] 3.1 `acr creds push`(github_token)+ `acr push`(部署 workflow)+ `acr run`(真打通 GitHub)
|
||||||
|
- 阻擋:本 repo 沒 `.arcrun.yaml`、沒連 arcrun 後端,跑不了端到端。**待 leo21c**(連了 arcrun 的環境)。
|
||||||
|
- 驗收:真建一個 issue + 投影進 Project 成功;Projects v2 `addProjectV2ItemById` 的 content node id(非 issue number)實測確認;github API 回傳欄位形狀(`{{gh_create.node_id}}` 取法)確認。
|
||||||
|
|
||||||
|
- [!] 3.2 本地觸發掛載點 + id 回寫防迴圈定稿
|
||||||
|
- 阻擋:依賴 3.1 的真跑結果。
|
||||||
|
- 驗收:push 後本機觸發一次(非 cron/Actions);回寫 `<!-- gh:id -->` 的 diff 不得再觸發一輪。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成定義
|
||||||
|
|
||||||
|
整個 SDD 完成 = 以下全部達成:
|
||||||
|
- [x] Phase 1、2 所有 tasks 標 [x](template 側 code-done)
|
||||||
|
- [x] `acr parts` 核實所有 component 存在 + `acr validate` 過(客觀證據)
|
||||||
|
- [!] Phase 3 端到端通過(待 leo21c)→ **未完成,不宣布 done**
|
||||||
|
- [ ] design.md 與實作一致(端到端驗後若 API 細節有出入需回頭更新 design.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 狀態說明
|
||||||
|
|
||||||
|
| 標記 | 意義 |
|
||||||
|
|------|------|
|
||||||
|
| `[ ]` | 未開始 |
|
||||||
|
| `[🔄]` | 進行中(當前 session)|
|
||||||
|
| `[x]` | 完成(有驗收證據)|
|
||||||
|
| `[~]` | 暫緩(說明原因)|
|
||||||
|
| `[!]` | 阻擋中(說明阻擋原因)|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# wiki-architecture — Design
|
||||||
|
|
||||||
|
> 狀態:已結案(2026-06-26,KB 端 183 卡實跑壓測全綠,1,029 條三元組、端點 0 缺陷;驗收明細見 tasks.md)
|
||||||
|
> 建立:2026-06-26 | 最後更新:2026-06-26
|
||||||
|
> 負責人:leo(uncle6me-web)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一句話說明
|
||||||
|
|
||||||
|
在「用戶所有檔案一律改寫成 wiki cards」的新架構下,用 **push(CC 行動前必主動看見)vs pull(CC 按需檢索)** 為唯一判準,重新決定 wiki/ 下每個檔的存廢——只有「不看就出事的盲區」獨立 push,其餘知識(含決策、原則)全是 cards、由 INDEX 多角度索引。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景與問題
|
||||||
|
|
||||||
|
舊架構(1.4.0 前)部分內容「指向原文」,wiki/ 下因此長出一組固定特殊檔(status / mistakes / decisions-summary / TAXONOMY / INDEX),當時把它們當「天經地義的結構」。
|
||||||
|
|
||||||
|
1.4.1 起改成**所有原文一律改寫成 cards**,但**沒有人回頭重新推導這組特殊檔在新架構下還成不成立**。結果:
|
||||||
|
- 「原則 / 願景」(如「不污染用戶根目錄」「目標用戶 low-code」)**無處可記**——decisions-summary 裝「單筆決策」、cards 裝「概念知識」,原則落在縫裡。
|
||||||
|
- 每次想記原則,就糾結「要不要再加一個特殊檔」——這是打補丁,不是設計。
|
||||||
|
|
||||||
|
**根因**:用「是不是特殊知識」當分類判準是錯的維度。正確判準是 retrieval 行為:**CC 做事時,這東西會不會被動看見?**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 範圍
|
||||||
|
|
||||||
|
### 包含(In Scope)
|
||||||
|
- 確立 push/pull 判準,重新決定 wiki/ 每個檔的存廢。
|
||||||
|
- 把 decisions-summary 降級為 cards + INDEX 視圖。
|
||||||
|
- 新增 principles 進 push 清單(hook 主動注入)。
|
||||||
|
- INDEX 升級為「多角度視圖」的家:新增角度 = CC 改 INDEX,不必問用戶開檔。
|
||||||
|
- 同步 wiki-init.md / SKILL.md / INDEX 範本 / session-start hook。
|
||||||
|
|
||||||
|
### 不包含(Out of Scope)
|
||||||
|
- 不改 cards 本身的三層+標籤架構(issue #8 的設計不動)。
|
||||||
|
- 不改 TAXONOMY 的字典機制。
|
||||||
|
- 不動 install-layout 的檔案落點(那是另一份 SDD)。
|
||||||
|
- 不強制遷移既有用戶的 decisions-summary 內容(向後相容,見下)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 設計
|
||||||
|
|
||||||
|
### 核心判準:push vs pull
|
||||||
|
|
||||||
|
> **push**:CC 行動前必須主動出現在 context(session 開始就注入)。
|
||||||
|
> **pull**:CC 想到要查、或載入相關卡時才看見。
|
||||||
|
|
||||||
|
判準的邏輯支點——**mistakes 必須 push**:mistakes 防的是「CC 不自覺的盲區」。一個你不知道存在的錯,你不會主動去檢索。靠 CC 自覺去查「自己沒自覺的盲區」是自相矛盾的 → 所以 pull 模式對 mistakes 邏輯上失效,必須 push。
|
||||||
|
|
||||||
|
同理推 principles:「不污染用戶根目錄」這種準繩,不主動注入,CC 設計時很可能**沒想到要服從就做了**(本專案實證:CC 這幾輪反覆忘記 low-code 用戶與不污染原則,正因它們沒被 push)。**原則也是不自覺的盲區** → 必須 push。
|
||||||
|
|
||||||
|
### 每個檔的存廢(依判準推導)
|
||||||
|
|
||||||
|
| 檔案 | push / pull | 邏輯理由 | 命運 |
|
||||||
|
|------|-------------|---------|------|
|
||||||
|
| **status.md** | push | 不是知識,是專案「此刻時態狀態」;不看會重做已完成的事 | **留**(hook 注入) |
|
||||||
|
| **mistakes.md** | push | 防不自覺盲區;pull 邏輯失效(見上) | **留**(hook 注入) |
|
||||||
|
| **principles.md** | push | 原則是會被遺忘的盲區;不注入 CC 設計時不服從(實證) | **新增**(hook 注入) |
|
||||||
|
| **decisions-summary.md** | pull | 「遇設計判斷才查」——CC 面對決策時自然會查既有決策,pull 夠用;決策本身是知識內容=card | **降級**:內容歸 cards,INDEX 提供「決策角度」視圖 |
|
||||||
|
| **TAXONOMY.md** | (元資料) | 是 cards 的分類字典=cards 的前提,邏輯上不可能是 card | **留**(元資料,非 push 非 pull) |
|
||||||
|
| **INDEX.md** | (入口) | 索引本身;升級為多角度視圖的家 | **留並強化** |
|
||||||
|
| **cards/** | pull | 一切知識內容(原文摘要、AI 筆記、lesson、決策、原則內容…)都在這 | 不變 |
|
||||||
|
|
||||||
|
**收斂結論**:只有「不是知識(status、TAXONOMY)」和「索引本身(INDEX)」獨立;**所有知識內容都是 cards**。push 清單=會變的狀態 + 會重犯的錯 + 會忘記的原則(status / mistakes / principles)。
|
||||||
|
|
||||||
|
### push 的實作機制(propose,非待定)
|
||||||
|
|
||||||
|
從 AI 的 context 行為推導,三類 push 的注入形態不同——判準是「**這東西需要全文才能避免出事,還是一行就夠觸發 CC 去查**」:
|
||||||
|
|
||||||
|
| push 項 | 注入形態 | 理由(從 AI 行為推) |
|
||||||
|
|---------|---------|---------------------|
|
||||||
|
| **status** | **全文** | 短、且 CC 必須知道精確的「下一步是什麼」才不重做;摘要會漏掉關鍵的當前 task 編號 |
|
||||||
|
| **principles** | **全文(一行一條)** | 原則本身就該寫成「一行一條」的精煉準繩(不污染根目錄、用戶是 low-code…)。全文注入成本低、且原則是「行動前必服從」的硬約束,不能只給標題讓 CC 自己決定要不要展開——它不會 |
|
||||||
|
| **mistakes** | **標題清單 + 一行症狀,全文按需 pull** | mistakes 可能累積到數十條、含長 context;全文會撐爆。但「標題 + 症狀」一行足以讓 CC 認出「我正要做的事撞到某條」→ 再展開讀全文。這裡 push 的是「觸發認出」,不是「完整內容」 |
|
||||||
|
|
||||||
|
**關鍵**:principles 全文 push、mistakes 摘要 push,差異來自——原則是「短而硬的約束」(全文成本低、漏一條就違反),mistakes 是「長而多的教訓」(全文太貴,但摘要足以觸發檢索)。這個區分本身就是「為 AI 設計」的判斷。
|
||||||
|
|
||||||
|
context 預算保護:principles 應設「條數上限」(如 ≤15 條,超過代表該合併或下放成 card);mistakes 摘要每條限一行。
|
||||||
|
|
||||||
|
### INDEX 作為「多角度視圖」的家
|
||||||
|
|
||||||
|
- INDEX 不再只是「標籤視圖」,而是所有檢索角度的入口:標籤角度、決策角度、(未來)任何角度。
|
||||||
|
- **新增一個角度 = CC 在 INDEX.md 加一節**,不必新增實體特殊檔、不必問用戶。這直接解決「AI 想累積新類別卻要問用戶開檔」的問題。
|
||||||
|
- principles/mistakes 雖獨立 push,但在 INDEX 也該有指標(讓 pull 路徑也找得到)。
|
||||||
|
|
||||||
|
### 原則的歸屬(回答原始提問)
|
||||||
|
|
||||||
|
「不污染用戶根目錄」「目標用戶 low-code」等:
|
||||||
|
- **內容**寫成 principles(push 檔)+ 必要時 card。
|
||||||
|
- CC 思考「怎麼設計」時,因 principles 被 push,行動前就看見,不必主動查。
|
||||||
|
- 累積新原則 = CC 寫進 principles + INDEX 補指標,**永不問用戶開檔**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技術限制
|
||||||
|
|
||||||
|
- push 注入量受 context 預算限制:不可無腦全文注入 mistakes+principles+status(會撐爆每 session context)。需「摘要 push + 全文按需 pull」。
|
||||||
|
- 向後相容:既有用戶的 decisions-summary.md 已有內容 → 不可刪。降級指「不再是必備特殊檔」,既有的保留為一張「決策彙總卡」或 INDEX 視圖,內容不丟。
|
||||||
|
- bash 3.2 相容(hook 改動)。
|
||||||
|
- 不破壞 issue #8 的 cards 三層+標籤架構。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 採集規範升級:內文實體關係 + ## 實體 區塊(issue #11,183 卡實證)
|
||||||
|
|
||||||
|
現行三元組/gloss 做窄了。從 Logseq vault 183 卡落地暴露:三元組只示範「卡對卡」(把既有 `[[雙鏈]]` 加動詞,資訊量沒增加);gloss 只描述卡標題這一個 node,內文實體(graph node)無處放描述。
|
||||||
|
|
||||||
|
| 決策 | 選擇 | 理由 |
|
||||||
|
|------|------|------|
|
||||||
|
| 三元組抓什麼 | **內文實體間關係**(卡對卡只是其中一類)| 知識圖譜價值在內文概念關係(`原子筆記 >> 對立於 >> 傳統筆記`),不是重複雙鏈 |
|
||||||
|
| 內文實體描述放哪 | 卡片新增 **`## 實體`** 區塊:`正規名(同義詞)— 一句描述`,**集中放、不縮排、不重複** | 內文實體也是 graph node,需描述句供下游 embedding normalize(`黃仁勳` vs `Jensen Huang`)。生產者是 AI 不會邊寫邊漏,不需縮排防漏;集中最利下游一實體一 embedding |
|
||||||
|
| `## 關聯` 結構 | 拆兩層:**內文知識關係**(端點裸文字,對應 `## 實體` 詞條)+ **卡片關係**(卡對卡 `[[]]`)| 內文三元組端點用裸文字避免 Logseq 紅色斷鏈;靠字面一致對應實體表 |
|
||||||
|
| 端點對齊 | **升級成「強制自檢動作」**:寫完逐條把 A/B 拿去 `## 實體` 比對,沒完全相同的正規名 → 改詞或補實體表 | comment 實證:光寫規則 Haiku 會略過(端點對不齊 14 條);寫成自檢動作後 14→0。這是 Haiku 量產的盲點,跑 1-2 張看不出、跑 12 張才暴露 |
|
||||||
|
| 謂詞 | **明寫「用動詞、禁名詞」** | 否則 Haiku 寫出 `>> 存儲格式 >>`、`>> 操作體驗 >>` 讀不通的名詞謂詞 |
|
||||||
|
| 實體要描述、謂詞不要 | 實體補 gloss 描述句,謂詞裸詞即可 | 實體同義詞字面差遠需描述拉近;謂詞同義詞(參考/參照)字面本就近,裸詞 embed 自動聚類 |
|
||||||
|
|
||||||
|
**範圍界線**:本升級只談**採集端**(卡片該寫什麼)。「哪些 token 進向量庫、怎麼去重」屬下游 ingest(另立 kbdb-ingest-plugin#1),不混進 skill。
|
||||||
|
**兩路徑同步**:SKILL.md(Cowork)+ wiki-init.md(CC)一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 驗收標準
|
||||||
|
|
||||||
|
- [ ] push/pull 判準寫進 wiki-init.md 與 SKILL.md,成為 CC/Cowork 共同規則。
|
||||||
|
- [ ] principles 進 push:session-start hook 注入 status + mistakes重點 + principles重點,且總注入量有節制(摘要而非全文)。
|
||||||
|
- [ ] INDEX 範本含「多角度視圖」說明 + 明示「新增角度改 INDEX 不開新檔」。
|
||||||
|
- [ ] decisions-summary 在文件中重新定位為「pull / INDEX 視圖」,既有內容相容保留。
|
||||||
|
- [ ] 一個原則(如「不污染用戶根目錄」)實際寫進 principles,驗證 CC 開 session 會被動看見。
|
||||||
|
- [ ] hook bash -n 過。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相關文件
|
||||||
|
|
||||||
|
- SDD: install-layout(檔案落點,姊妹 SDD)
|
||||||
|
- memory: user-profile-lowcode、internal-docs-not-pushed
|
||||||
|
- 觸發:本專案 CC 反覆遺忘 low-code 用戶與不污染原則 → 暴露「原則無 push 通道」
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# wiki-architecture — Tasks
|
||||||
|
|
||||||
|
> 權威來源:此檔案是進度真相。動手前標 [🔄],完成立刻標 [x]。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0:審核(前置)
|
||||||
|
|
||||||
|
- [x] 0.1 design.md 經用戶審核通過(push/pull 判準、push 三項形態、INDEX 多角度)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1:規則文件(wiki-init + SKILL)
|
||||||
|
|
||||||
|
> 前置:Phase 0
|
||||||
|
|
||||||
|
- [x] 1.1 wiki-init.md 寫入 push/pull 判準 + 三類 push(status全文/principles全文/mistakes摘要)
|
||||||
|
- 驗收:CC 讀 wiki-init 能判斷一個內容該 push 還 pull
|
||||||
|
- [x] 1.2 wiki-init.md 新增 principles 檔的建立與維護規則(一行一條、條數上限)
|
||||||
|
- [x] 1.3 wiki-init.md decisions-summary 重定位為「pull / INDEX 決策視圖」,既有內容相容
|
||||||
|
- [x] 1.4 SKILL.md(Cowork)同步上述規則,CC/Cowork 一致
|
||||||
|
- 驗收:兩來源檔 push/pull 規則 byte 對齊(除路徑)
|
||||||
|
|
||||||
|
## Phase 2:INDEX 升級為多角度視圖
|
||||||
|
|
||||||
|
> 前置:Phase 1
|
||||||
|
|
||||||
|
- [x] 2.1 INDEX.md 範本:從「標籤視圖」升級為「多角度入口」(標籤/決策/原則…角度)
|
||||||
|
- [x] 2.2 明示「新增角度 = 改 INDEX 一節,不開新檔、不問用戶」
|
||||||
|
- [x] 2.3 principles/mistakes 在 INDEX 留 pull 指標
|
||||||
|
- 驗收:INDEX 範本含多角度說明 + 新增角度的自助規則
|
||||||
|
|
||||||
|
## Phase 3:push 機制(session-start hook)
|
||||||
|
|
||||||
|
> 前置:Phase 1
|
||||||
|
|
||||||
|
- [x] 3.1 session-start-recall.sh 擴充:注入 status 全文 + principles 全文 + mistakes 標題清單
|
||||||
|
- [x] 3.2 context 保護:principles 條數上限檢查、mistakes 只注入標題+一行
|
||||||
|
- [x] 3.3 bash -n + 沙盒測試(三類都有時注入正確、量受控)
|
||||||
|
- 驗收:開 session 三類都被動出現,總量有節制
|
||||||
|
|
||||||
|
## Phase 4:新增 principles 範本 + 種子原則
|
||||||
|
|
||||||
|
> 前置:Phase 1-3
|
||||||
|
|
||||||
|
- [x] 4.1 建 template/system-dev/wiki/principles.md 範本
|
||||||
|
- [x] 4.2 install.sh download + update.sh keep_file(用戶資料,永不覆蓋)
|
||||||
|
- [x] 4.3 種子原則寫入(dev repo 自用 wiki):不污染用戶根目錄、目標用戶 low-code、wiki 主要給 AI 看、內部文件不推
|
||||||
|
- 驗收:開 session 這些原則被動出現在 context
|
||||||
|
|
||||||
|
## Phase 5:版本、文件、提交
|
||||||
|
|
||||||
|
- [x] 5.1 bump(中版號,新增 principles 功能)+ 兩 VERSION 同步
|
||||||
|
- [x] 5.2 CHANGELOG
|
||||||
|
- [x] 5.3 commit + push(已隨 1.10.0~1.11.0 推送)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成定義
|
||||||
|
- [x] 所有 tasks [x]
|
||||||
|
- [x] design.md 驗收標準全過
|
||||||
|
- [x] 實證:CC 開 session 會被動看見 principles(本 session SessionStart hook 已注入 9 條,成立)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下游驗收(2026-06-26,KB 端 183 卡實跑壓測)
|
||||||
|
|
||||||
|
> 採集規範升級(gloss / `## 實體` / typed-edge / 端點對齊護欄)首次真實 ingest 全量驗證,全綠:
|
||||||
|
|
||||||
|
| 驗收項 | 結果 |
|
||||||
|
|--------|------|
|
||||||
|
| 結構齊全(gloss + 實體 + 兩層各 1 份) | ✅ 183/183 |
|
||||||
|
| 有 gloss | ✅ 183/183 |
|
||||||
|
| 內文三元組總數 | 1,029 條 |
|
||||||
|
| 端點對不齊 / 三段式 | ✅ 0 |
|
||||||
|
| raw source 鐵律(pages/journals 0 異動) | ✅ |
|
||||||
|
| 內文層無誤用 wikilink | ✅ 乾淨 |
|
||||||
|
|
||||||
|
- 放量分三批、驗收驅動:試跑 12 → 補硬自檢 → 0;放量 170 → 殘留 26 → 修正批 15 → 殘留 1 → 手修;全量 0 缺陷。
|
||||||
|
- 印證 issue #11 的「端點硬自檢」護欄(Haiku 量產 14→0)在真實規模成立。
|
||||||
|
- 連帶修了 KB 端 CLAUDE.md 6 處過時路徑(`.claude/wiki/` → `system-dev/wiki/`),即 1.9.3 hook 提示的場景。
|
||||||
|
|
||||||
|
**狀態:結案。** 採集規範 / push 機制 / `## 實體` 都已下游實證,無待驗證項。
|
||||||
+1
-1
@@ -44,7 +44,7 @@ SDD 強制 CC 在動手前先思考:這個修改影響哪些地方?有沒有
|
|||||||
|
|
||||||
**雙空間設計:**
|
**雙空間設計:**
|
||||||
- `docs/`:人寫的原始文件,依照開發生命週期分類
|
- `docs/`:人寫的原始文件,依照開發生命週期分類
|
||||||
- `.claude/wiki/`:CC 整理的 LLM 友善知識,每次 session 累積
|
- `system-dev/wiki/`:CC 整理的 LLM 友善知識,每次 session 累積
|
||||||
|
|
||||||
每次 session 開始,CC 讀 wiki,不需要重新學習。
|
每次 session 開始,CC 讀 wiki,不需要重新學習。
|
||||||
每次 session 結束,CC 更新 wiki,知識不再消失。
|
每次 session 結束,CC 更新 wiki,知識不再消失。
|
||||||
|
|||||||
+4
-4
@@ -57,15 +57,15 @@ system-dev-template 自身要補的功能。
|
|||||||
|
|
||||||
| 層 | 機制 | 擋什麼 | 性質 |
|
| 層 | 機制 | 擋什麼 | 性質 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **L1** | `.claude/wiki/.wikiignore`(glob,像 .gitignore) | 整個機敏檔不編入 | 協議(CC 遵守) |
|
| **L1** | `system-dev/wiki/.wikiignore`(glob,像 .gitignore) | 整個機敏檔不編入 | 協議(CC 遵守) |
|
||||||
| **L2** | 行內標記 `<!-- wiki:ignore -->`…`<!-- wiki:end -->` | 檔案內某段不編入 | 協議(CC 遵守) |
|
| **L2** | 行內標記 `<!-- wiki:ignore -->`…`<!-- wiki:end -->` | 檔案內某段不編入 | 協議(CC 遵守) |
|
||||||
| **L3** | `wiki-secret-scan.sh` hook(PreToolUse) | 機敏值真寫進 `.claude/wiki/` → exit 2 擋 | **硬攔截(機械偵測)** |
|
| **L3** | `wiki-secret-scan.sh` hook(PreToolUse) | 機敏值真寫進 `system-dev/wiki/` → exit 2 擋 | **硬攔截(機械偵測)** |
|
||||||
|
|
||||||
L1/L2 是使用者主動標記,L3 是自動兜底——前兩層漏掉時,寫進 wiki 那一刻 regex 攔下(密碼賦值、PEM 私鑰、AWS/GitHub/Slack/Google/Stripe 金鑰、JWT、連線字串帳密、台灣身分證、信用卡號)。
|
L1/L2 是使用者主動標記,L3 是自動兜底——前兩層漏掉時,寫進 wiki 那一刻 regex 攔下(密碼賦值、PEM 私鑰、AWS/GitHub/Slack/Google/Stripe 金鑰、JWT、連線字串帳密、台灣身分證、信用卡號)。
|
||||||
|
|
||||||
**已做**:
|
**已做**:
|
||||||
- `template/.claude/wiki/.wikiignore`:L1 範本(預設排除 `.env`/`*.pem`/`*secret*` 等)。
|
- `template/system-dev/wiki/.wikiignore`:L1 範本(預設排除 `.env`/`*.pem`/`*secret*` 等)。
|
||||||
- `template/.claude/hooks/wiki-secret-scan.sh`:L3 hook,只掃 `.claude/wiki/**` 寫入,`wiki-secret-ok` 行尾標記可豁免誤判。
|
- `template/.claude/hooks/wiki-secret-scan.sh`:L3 hook,只掃 `system-dev/wiki/**` 寫入,`wiki-secret-ok` 行尾標記可豁免誤判。
|
||||||
- `settings.json`:PreToolUse(Write|Edit) 加掛。
|
- `settings.json`:PreToolUse(Write|Edit) 加掛。
|
||||||
- `wiki-init.md` / `wiki-capture.md` / `SKILL.md`:寫入 L1+L2 協議。
|
- `wiki-init.md` / `wiki-capture.md` / `SKILL.md`:寫入 L1+L2 協議。
|
||||||
|
|
||||||
|
|||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# check-all.sh — 框架的機械閘總入口(commit 前跑這一支就好)
|
||||||
|
#
|
||||||
|
# 為什麼要有總入口:三支閘分開跑,遲早有人只記得跑其中一支。
|
||||||
|
# 一個入口 = 一個習慣,而不是三個要記得的步驟。
|
||||||
|
#
|
||||||
|
# 掛法(版控在 .githooks/,跟著 repo 走,不必每台機器重設):
|
||||||
|
# git config core.hooksPath .githooks
|
||||||
|
#
|
||||||
|
# 為什麼不用 CI workflow:本組織的鐵律禁止在 repo 上掛自動化 workflow
|
||||||
|
#(歷史上正是「一個 push 觸發大量自動化」把帳號弄到被停權)。
|
||||||
|
# 閘要跑在**人的機器上、commit 的那一刻**,不是掛在雲端等事件。
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
run() { # $1=名稱 $2...=命令
|
||||||
|
local name="$1"; shift
|
||||||
|
printf '── %s\n' "$name"
|
||||||
|
if "$@"; then :; else FAIL=1; fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "🔎 框架機械閘(commit 前檢查)"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
run "框架不含實例資料" bash scripts/check-no-instance-names.sh
|
||||||
|
run "已發佈路徑只增不移" bash scripts/check-legacy-paths.sh
|
||||||
|
run "單一活性規格" bash system-dev/scripts/sdd-active-check.sh docs/3-specs
|
||||||
|
|
||||||
|
# 腳本語法(壞掉的 shell 推出去,實例的更新機制就跟著壞)
|
||||||
|
printf '── 腳本語法\n'
|
||||||
|
SYNTAX_BAD=0
|
||||||
|
for f in scripts/*.sh template/.claude/hooks/*.sh template/.claude/hooks/lib/*.sh \
|
||||||
|
template/profiles/*/hooks/*.sh template/scripts/*.sh; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
bash -n "$f" 2>/dev/null || { echo " ❌ 語法錯誤:$f"; SYNTAX_BAD=1; FAIL=1; }
|
||||||
|
done
|
||||||
|
[ $SYNTAX_BAD -eq 0 ] && echo " ✅ 全部通過"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $FAIL -ne 0 ]; then
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "❌ 有檢查沒過——先修,別 commit"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ 全部通過"
|
||||||
|
exit 0
|
||||||
Executable
+102
@@ -0,0 +1,102 @@
|
|||||||
|
#!/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=<commit> 指定。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# ⚠️ 基準**不能**用 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)"
|
||||||
|
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 <<EOF
|
||||||
|
════════════════════════════════════════════════
|
||||||
|
❌ 相容閘:已發佈腳本會抓的路徑不見了($MISSING / $TOTAL)
|
||||||
|
════════════════════════════════════════════════
|
||||||
|
$MISSING_LIST
|
||||||
|
後果(不是理論,是 1.16.0 真的發生過):
|
||||||
|
舊實例跑 update 時對上面每個路徑 curl 404 → 更新失敗 →
|
||||||
|
**更新機制本身壞掉,之後也不會有下一次更新來修它**。
|
||||||
|
|
||||||
|
正解:搬版面時在原路徑留相容檔(轉址說明或原內容),
|
||||||
|
本閘綠了才准繼續。遠端路徑是契約,只增不移。
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ 相容閘:已發佈腳本(基準 $BASE)引用的 $TOTAL 個路徑全部還在"
|
||||||
|
exit 0
|
||||||
Executable
+136
@@ -0,0 +1,136 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# check-no-instance-names.sh — 機械閘 #2:框架範本不得混入實例專名
|
||||||
|
#
|
||||||
|
# 出處:《分離導入規格》第六節第 2 條、第七節第四題 Gherkin。
|
||||||
|
# Scenario: 框架混入實例名被 CI 擋
|
||||||
|
# Given 範本檔新增一行含 "arcrun"
|
||||||
|
# When CI 執行
|
||||||
|
# Then 檢查 fail 並指出檔案與行號
|
||||||
|
#
|
||||||
|
# 為什麼需要它(不是潔癖,是分層會自己塌):
|
||||||
|
# L1 框架的判準是「換一家公司照樣成立」。一旦範本裡寫著某個實例的專案名,
|
||||||
|
# 下一個裝這套 template 的人就會讀到一段對他不成立的規則——而他無從分辨
|
||||||
|
# 「這條是普世方法論」還是「這是別人家的家規」。分層一旦糊掉就回不去了。
|
||||||
|
#
|
||||||
|
# ── 豁免機制(為什麼要有)─────────────────────────────
|
||||||
|
# 本閘上線時,範本區已經有 22 行命中(8 個檔)。若不給豁免就硬上線,唯一的
|
||||||
|
# 通關方式會是「把 arcrun 改寫成『某工作流引擎』」——**資訊消失了,問題還在**
|
||||||
|
# (假性清理)。所以:真正該搬走的東西標豁免、記在帳上,隨 policy pack 一起搬;
|
||||||
|
# 純粹是註解舉例的,當場改寫成通用敘述。
|
||||||
|
# 豁免有兩級:
|
||||||
|
# · 行級 `sdt-instance-name-ok` → 該行豁免。用在「檔案本身是框架的,只有這行欠著」。
|
||||||
|
# · 檔級 `sdt-instance-name-ok-file` → 整檔豁免(寫在檔首 30 行內,需附理由)。
|
||||||
|
# 用在「整支本來就是 L2 政策包產物、只是還沒搬走」——逐行標 13 次是雜訊不是紀律。
|
||||||
|
#
|
||||||
|
# 誠實限制:這支只認「字面出現」。它擋不了「把實例假設寫成通用語氣」
|
||||||
|
# (例如寫「你的工作流引擎會在遠端執行」其實只有某一家成立)。
|
||||||
|
# 它是底線不是萬能——真正的分層判斷仍要人/CC 動腦。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# bash scripts/check-no-instance-names.sh # CI 模式,有未豁免命中 → exit 1
|
||||||
|
# bash scripts/check-no-instance-names.sh --baseline # 只列清單(含已豁免),永遠 exit 0
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
NAMES_FILE="scripts/instance-names.txt"
|
||||||
|
SCAN_DIR="template"
|
||||||
|
EXEMPT_MARK="sdt-instance-name-ok"
|
||||||
|
EXEMPT_FILE_MARK="sdt-instance-name-ok-file"
|
||||||
|
EXEMPT_HEAD_LINES=30 # 檔級豁免標記必須寫在檔首這麼多行內(強迫它顯眼、不許藏在檔尾)
|
||||||
|
|
||||||
|
MODE="ci"
|
||||||
|
[ "${1:-}" = "--baseline" ] && MODE="baseline"
|
||||||
|
|
||||||
|
if [ ! -f "$NAMES_FILE" ]; then
|
||||||
|
echo "❌ 找不到黑名單設定檔:$NAMES_FILE" >&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 <<EOF
|
||||||
|
|
||||||
|
鐵律:框架不含實例資料(《分離導入規格》第一節鐵律 2)。
|
||||||
|
L1 範本要「換一家公司照樣成立」;上面這些行只對某一家成立。
|
||||||
|
|
||||||
|
怎麼修(擇一,別選第四條「把詞換掉了事」):
|
||||||
|
1. 只是註解/舉例 → 改寫成通用敘述(例:「某個 repo」而非具體專案名)
|
||||||
|
2. 其實是政策包的內容 → 搬進 policy pack(L2),不要留在框架
|
||||||
|
3. 真的暫時搬不走 → 行尾加註記 $EXEMPT_MARK + 一句「為什麼還不能拿掉」
|
||||||
|
⚠️ 不准做的:把專名換成模糊詞讓檢查過關——資訊消失了,分層問題還在。
|
||||||
|
|
||||||
|
已豁免(不計違規):$EXEMPTED 行
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ 範本區無未豁免的實例專名(已豁免 $EXEMPTED 行,記在帳上待搬遷)"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# github-publish-exclude.txt — 公開 mirror 要濾掉的路徑(一行一個,相對 repo 根)
|
||||||
|
#
|
||||||
|
# 原則(leo 2026-07-21):
|
||||||
|
# 「應該要給的是一個適合別人用的**濃縮結果,不需要有過程**。
|
||||||
|
# 以後我還是在私有的 template 工作,但 publish 到 GitHub 就把它當**貼文**。」
|
||||||
|
#
|
||||||
|
# → GitHub = 給別人用的成品櫥窗;Gitea(未來 CF 版)= 我們的工作現場。
|
||||||
|
# 凡屬「我們怎麼做出來的」一律不出去;凡屬「別人怎麼用」一律留下。
|
||||||
|
|
||||||
|
# ── 我們自己的工作現場(過程,不是成品)──
|
||||||
|
# 本 repo 自用的 wiki/SDD/內部決策紀錄。別人裝了 template 會長出他自己的,
|
||||||
|
# 不需要看我們的內容(且含真實事故、人名、內部帳號脈絡)。
|
||||||
|
system-dev
|
||||||
|
|
||||||
|
# 本 repo 自用的 CC 設定與 hook 註冊(含本機路徑、內部 guard)。
|
||||||
|
# 注意:template/.claude/ 是「要發給別人的樣板」,不在此列,照樣公開。
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# 開發過程文件(設計討論、內部指南)
|
||||||
|
docs
|
||||||
|
|
||||||
|
# 我們自己給 CC 的工作指示(含內部鐵律與事故引用)
|
||||||
|
CLAUDE.md
|
||||||
|
|
||||||
|
# 內部發版沿革(含事故經過與人名對話)——公開端改用 README 的「更新重點」概述
|
||||||
|
CHANGELOG.md
|
||||||
|
|
||||||
|
# 發佈管線本身不需要出現在成品裡
|
||||||
|
scripts/publish-github.sh
|
||||||
|
scripts/github-publish-exclude.txt
|
||||||
|
scripts/github-publish-sanitize.py
|
||||||
|
|
||||||
|
# mirror 產出目錄(若曾在本機產生)
|
||||||
|
.github-public
|
||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""github-publish-sanitize.py — 對匯出樹做內容級改寫,讓公開版真的能被別人使用。
|
||||||
|
|
||||||
|
由 publish-github.sh 自動呼叫(步驟 3.5),參數=匯出樹的暫存目錄。
|
||||||
|
|
||||||
|
🔑 定位(leo 2026-07-21 校正):**這支是安全網,不是主要手段。**
|
||||||
|
|
||||||
|
leo:「要發佈的正稿,**從頭就不要用奇怪的網址,以免改來改去**。」
|
||||||
|
|
||||||
|
→ 已於同日**源頭修正**:install.sh/update.sh 的來源預設就是公開 GitHub raw
|
||||||
|
(內部要指私有草稿源時走 `TEMPLATE_SOURCE=` 環境變數覆寫,不改檔);
|
||||||
|
README 的死帳號 `uncle6me-web` 也直接改掉。
|
||||||
|
**正常情況下本腳本應該「無需改寫」** ——若它報告改了東西,代表源頭又混進錯網址,
|
||||||
|
那是要回頭修源頭的信號,不是「反正發佈時會自動改」。
|
||||||
|
|
||||||
|
保留它的理由=**發佈前的最後一道驗證**:任何殘留失效來源就 exit 1 中止,
|
||||||
|
避免「別人照著裝卻裝不起來」這種只有外部使用者才會撞到、我們自己永遠測不到的錯。
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
GITEA_BASE = "https://git.uncle6.me/Leo/system-dev-template/raw/branch/main"
|
||||||
|
GITHUB_BASE = "https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main"
|
||||||
|
|
||||||
|
# 純文字取代(來源網址)。放這裡的規則要「冪等」——重跑不會壞。
|
||||||
|
REPLACEMENTS = [
|
||||||
|
(GITEA_BASE, GITHUB_BASE),
|
||||||
|
# 保險:任何殘留的 Gitea 主機名(例如註解、文件裡的說明連結)
|
||||||
|
("https://git.uncle6.me/Leo/system-dev-template", "https://github.com/youlinhsieh/system-dev-template"),
|
||||||
|
# 🔴 舊 GitHub 帳號 uncle6me-web **已被 suspend**:README 的安裝指令仍指向它
|
||||||
|
# → 別人照著跑會抓不到(2026-07-20 已在 update.sh 撞過同一顆雷)。
|
||||||
|
("raw.githubusercontent.com/uncle6me-web/", "raw.githubusercontent.com/youlinhsieh/"),
|
||||||
|
("github.com/uncle6me-web/", "github.com/youlinhsieh/"),
|
||||||
|
# 文件註解裡拿舊帳號當範例 → 對外改通用佔位符(別人看到我們的帳號名沒意義)
|
||||||
|
("(例:uncle6me-web)", "(例:your-github-account)"),
|
||||||
|
("(e.g. uncle6me-web)", "(e.g. your-github-account)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
TEXT_SUFFIXES = {".sh", ".md", ".json", ".py", ".yaml", ".yml", ".txt"}
|
||||||
|
|
||||||
|
|
||||||
|
def main(root_arg: str) -> int:
|
||||||
|
root = pathlib.Path(root_arg)
|
||||||
|
if not root.is_dir():
|
||||||
|
print(f"❌ sanitize:找不到匯出樹 {root}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
changed = []
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if not path.is_file() or path.suffix not in TEXT_SUFFIXES:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
original = path.read_text(encoding="utf-8")
|
||||||
|
except (UnicodeDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = original
|
||||||
|
for old, new in REPLACEMENTS:
|
||||||
|
text = text.replace(old, new)
|
||||||
|
|
||||||
|
if text != original:
|
||||||
|
path.write_text(text, encoding="utf-8")
|
||||||
|
changed.append(str(path.relative_to(root)))
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
print(f"🧼 sanitize:改寫來源網址 → GitHub({len(changed)} 檔)")
|
||||||
|
for c in changed:
|
||||||
|
print(f" {c}")
|
||||||
|
else:
|
||||||
|
print("🧼 sanitize:無需改寫")
|
||||||
|
|
||||||
|
# 驗證:公開樹不得殘留「別人抓不到的來源」——漏了就是別人裝不起來。
|
||||||
|
# ① git.uncle6.me = 我們的 private Gitea(且即將換 CF 版)
|
||||||
|
# ② uncle6me-web = 已被 suspend 的舊 GitHub 帳號
|
||||||
|
BAD_HOSTS = ("git.uncle6.me", "uncle6me-web")
|
||||||
|
leaked = []
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if not path.is_file() or path.suffix not in TEXT_SUFFIXES:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
content = path.read_text(encoding="utf-8")
|
||||||
|
if any(bad in content for bad in BAD_HOSTS):
|
||||||
|
leaked.append(str(path.relative_to(root)))
|
||||||
|
except (UnicodeDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if leaked:
|
||||||
|
print("❌ sanitize:公開樹仍殘留失效來源(Gitea/suspend 帳號),中止發佈:", file=sys.stderr)
|
||||||
|
for f in leaked:
|
||||||
|
print(f" {f}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))
|
||||||
+254
-92
@@ -26,35 +26,59 @@ t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2";
|
|||||||
# tn = 不換行版(給 prompt 用)
|
# tn = 不換行版(給 prompt 用)
|
||||||
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
||||||
|
|
||||||
REPO_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/template"
|
# 來源預設=公開 GitHub(leo 2026-07-21:「要發佈的正稿,從頭就不要用奇怪的網址,
|
||||||
|
# 以免改來改去」)。內部指向私有草稿源時用環境變數覆寫,不改檔:
|
||||||
|
# TEMPLATE_SOURCE=https://<私有 raw base> bash scripts/install.sh
|
||||||
|
TEMPLATE_SOURCE="${TEMPLATE_SOURCE:-https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main}"
|
||||||
|
REPO_URL="$TEMPLATE_SOURCE/template"
|
||||||
# install.sh / update.sh 住在 main/scripts/(不在 template/)。
|
# install.sh / update.sh 住在 main/scripts/(不在 template/)。
|
||||||
SCRIPTS_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts"
|
SCRIPTS_URL="$TEMPLATE_SOURCE/scripts"
|
||||||
CREATED=()
|
CREATED=()
|
||||||
SKIPPED=()
|
SKIPPED=()
|
||||||
|
|
||||||
# ── 解析模組參數 ──────────────────────────────────
|
# ── 解析模組 / profile 參數 ────────────────────────
|
||||||
|
# profile = scope 軸(這個資料夾是「成員 repo」還是「總管」)。
|
||||||
|
# 它決定裝哪一部憲法——agent 在哪裡醒來就只讀得到哪部,**不靠 agent 自我判斷**。
|
||||||
|
#
|
||||||
|
# ⚠️ 契約在檔案,不在這支腳本:真正的 scope 真相源是 `system-dev/.profile`。
|
||||||
|
# 本腳本只是**其中一個寫入者**——「把網址丟給 AI 幫我裝」那條路(主打入口)
|
||||||
|
# 同樣必須寫出這個檔,格式一致即可互通。腳本入口是備援與參考實作,不是唯一路徑。
|
||||||
MODULE=""
|
MODULE=""
|
||||||
|
PROFILE=""
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--wiki|--wiki-only) MODULE="wiki" ;;
|
--wiki|--wiki-only) MODULE="wiki" ;;
|
||||||
--sdd|--sdd-only) MODULE="sdd" ;;
|
--sdd|--sdd-only) MODULE="sdd" ;;
|
||||||
--all) MODULE="all" ;;
|
--all) MODULE="all" ;;
|
||||||
|
--profile=repo) PROFILE="repo" ;;
|
||||||
|
--profile=orchestrator) PROFILE="orchestrator" ;;
|
||||||
|
--profile=*)
|
||||||
|
echo "❌ 不認得的 profile:${arg#--profile=}(只接受 repo / orchestrator)" >&2
|
||||||
|
exit 1 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
if [ "$IS_ZH" = "yes" ]; then
|
if [ "$IS_ZH" = "yes" ]; then
|
||||||
cat <<'HELP'
|
cat <<'HELP'
|
||||||
用法:install.sh [--wiki | --sdd | --all]
|
用法:install.sh [--wiki | --sdd | --all] [--profile=repo|orchestrator]
|
||||||
--wiki 只裝 LLM Wiki(CC 記憶系統 + 機敏防護)
|
--wiki 只裝 LLM Wiki(CC 記憶系統 + 機敏防護)
|
||||||
--sdd 只裝 SDD 系統(動 code 前強制要有設計文件)
|
--sdd 只裝 SDD 系統(動 code 前強制要有設計文件)
|
||||||
--all 兩個都裝(預設)
|
--all 兩個都裝(預設)
|
||||||
無參數 互動式詢問要裝哪個
|
無參數 互動式詢問要裝哪個
|
||||||
|
|
||||||
|
--profile=repo 成員 repo:實際寫 code 的子專案(預設)
|
||||||
|
--profile=orchestrator 總管:管一群成員 repo 的上層資料夾
|
||||||
|
未指定 → 自動偵測並請你確認一次(結果寫進 system-dev/.profile,之後不再問)
|
||||||
HELP
|
HELP
|
||||||
else
|
else
|
||||||
cat <<'HELP'
|
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)
|
--wiki Install LLM Wiki only (CC memory system + secret protection)
|
||||||
--sdd Install SDD system only (require a design doc before touching code)
|
--sdd Install SDD system only (require a design doc before touching code)
|
||||||
--all Install both (default)
|
--all Install both (default)
|
||||||
no flag Interactively ask which to install
|
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
|
HELP
|
||||||
fi
|
fi
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -103,6 +127,57 @@ echo ""
|
|||||||
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
|
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
|
||||||
echo ""
|
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 ──
|
# ── 重複安裝防呆(1.10.1):install 只管「全新安裝」,一切後續歸 update ──
|
||||||
# 判準是「裝過沒」,不分新版舊版:
|
# 判準是「裝過沒」,不分新版舊版:
|
||||||
# - 新結構 system-dev/ 已存在,或
|
# - 新結構 system-dev/ 已存在,或
|
||||||
@@ -115,7 +190,7 @@ if [ -d "system-dev" ] || [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ]; the
|
|||||||
t " 後續的更新、遷移、補新檔,一律由「更新腳本」處理(不要重跑 install):" \
|
t " 後續的更新、遷移、補新檔,一律由「更新腳本」處理(不要重跑 install):" \
|
||||||
" All updates, migrations, and new-file additions are handled by the UPDATER (don't re-run install):"
|
" All updates, migrations, and new-file additions are handled by the UPDATER (don't re-run install):"
|
||||||
echo ""
|
echo ""
|
||||||
echo " curl -sSL https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts/update.sh | bash"
|
echo " curl -sSL https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main/scripts/update.sh | bash"
|
||||||
echo ""
|
echo ""
|
||||||
t " (重跑 install 可能建出空白範本、跟你的真資料並存,故在此停止。)" \
|
t " (重跑 install 可能建出空白範本、跟你的真資料並存,故在此停止。)" \
|
||||||
" (Re-running install could create empty templates alongside your real data, so it stops here.)"
|
" (Re-running install could create empty templates alongside your real data, so it stops here.)"
|
||||||
@@ -239,87 +314,95 @@ download_if_missing() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 共用結構 ──────────────────────────────────────
|
# ── Manifest 驅動的安裝(取代原本硬編的檔案清單)──────────────
|
||||||
# 工具自己的文件骨架收進 system-dev/docs/(不污染用戶根目錄、不跟用戶自己的 docs/ 混)。
|
# 為什麼改成這樣(SDD jdd-dual-profile 決策 D1):
|
||||||
# 注意語義分離:這裡的 system-dev/docs/ 是「工具文件」;用戶的 raw source(原始文件)
|
# 原本清單硬編在這裡,update.sh 另有一份幾乎重複的硬編。兩份手抄必然漂移,
|
||||||
# 另有其處(見上方 vault 偵測),工具只讀、不搬。
|
# 而且**已經漂了**——install 從來不裝 wiki-first-search.sh / subagent-wiki-guard.sh /
|
||||||
# .claude/ 只留 CC 死綁的 commands/ + hooks/,工具資料一律不放這。
|
# publish-lag-check.sh / decisions-summary.md,但 update 會裝/會保留。
|
||||||
create_dir "system-dev/docs/1-vision"
|
# ⇒ 乾淨安裝的人反而拿不到最新三版的招牌功能。
|
||||||
create_dir "system-dev/docs/2-architecture/decisions"
|
# 現在兩支腳本讀同一份 manifest,這類漂移在結構上不可能再發生。
|
||||||
create_dir "system-dev/docs/4-guides"
|
#
|
||||||
create_dir "system-dev/docs/5-records/incidents"
|
# manifest 欄位:src \t dest \t class \t module \t profile(詳見 template/manifest/common.tsv)
|
||||||
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"
|
|
||||||
|
|
||||||
# 工具版號:放 system-dev/,不寄生 .claude/。
|
MANIFEST_DIR="system-dev/.template-manifest.d"
|
||||||
download_if_missing "system-dev/VERSION" "$REPO_URL/system-dev/VERSION"
|
mkdir -p "$MANIFEST_DIR"
|
||||||
|
|
||||||
# ── WIKI 模組 ─────────────────────────────────────
|
fetch_manifest() { # $1=名稱(common|repo|orchestrator)
|
||||||
# wiki 是工具資料 → 放 system-dev/wiki/(不放 .claude/)。
|
local name="$1" out="$MANIFEST_DIR/$1.tsv"
|
||||||
# commands/ 與 hooks/ 是 CC 機制檔 → 維持 .claude/。
|
if curl -sSL "$REPO_URL/manifest/$name.tsv" -o "$out" 2>/dev/null && [ -s "$out" ]; then
|
||||||
if $WANT_WIKI; then
|
return 0
|
||||||
create_dir "system-dev/wiki"
|
fi
|
||||||
download_if_missing "system-dev/wiki/INDEX.md" "$REPO_URL/system-dev/wiki/INDEX.md"
|
rm -f "$out"
|
||||||
download_if_missing "system-dev/wiki/TAXONOMY.md" "$REPO_URL/system-dev/wiki/TAXONOMY.md"
|
return 1
|
||||||
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"
|
|
||||||
|
|
||||||
# wiki 改寫產物(AI 自讀定稿卡片)的正式落點:由工具建好,不靠用戶自救。
|
if ! fetch_manifest common; then
|
||||||
create_dir "system-dev/wiki/cards"
|
echo "❌ 抓不到安裝清單($REPO_URL/manifest/common.tsv)——網路或來源網址有問題,中止。" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fetch_manifest "$PROFILE" || true # profile 專屬清單可以是空的
|
||||||
|
|
||||||
|
# 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"); }
|
[ -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"
|
|
||||||
|
|
||||||
# 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
|
fi
|
||||||
|
|
||||||
# ── SDD 模組 ──────────────────────────────────────
|
# ── scope 軸 marker:這個實例是哪一種 ────────────────
|
||||||
if $WANT_SDD; then
|
# 這是 role-lib.sh 判 scope 的**唯一**來源。AI 代裝路徑也必須寫出同格式的檔。
|
||||||
create_dir "system-dev/docs/3-specs"
|
printf '%s\n' "$PROFILE" > "system-dev/.profile"
|
||||||
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
|
CREATED+=("system-dev/.profile ($PROFILE)")
|
||||||
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"
|
|
||||||
|
|
||||||
# ── 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"
|
|
||||||
|
|
||||||
chmod +x .claude/hooks/*.sh 2>/dev/null || true
|
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/workflows/*.sh 2>/dev/null || true
|
||||||
|
chmod +x system-dev/scripts/*.sh 2>/dev/null || true
|
||||||
|
|
||||||
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
|
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
|
||||||
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
|
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
|
||||||
@@ -330,19 +413,42 @@ build_hooks_json() {
|
|||||||
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
|
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# PreToolUse 依模組疊加
|
# PreToolUse 依模組疊加。
|
||||||
|
# 順序原則:**範圍大的擋在前**——錯誤訊息才會指向最根本的那條規則,
|
||||||
|
# 而不是讓人先修一個表層問題、修完才發現底下還有一條。
|
||||||
|
# ① 改機制 這個檔你根本不該動(最外層)
|
||||||
|
# ② 角色 你這個身分不該寫這種檔
|
||||||
|
# ③ 位置 總管不該進成員 repo 動實作(僅總管實例)
|
||||||
|
# ④ 格式 檔可以寫,但寫進去的內容格式不對
|
||||||
|
# ⑤ 既有的 SDD/自訂禁令/機敏掃描
|
||||||
local pt=()
|
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" }')
|
$WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }')
|
||||||
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
|
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
|
||||||
$WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }')
|
$WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }')
|
||||||
local IFS=,
|
local IFS=,
|
||||||
pretool_hooks="${pt[*]}"
|
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
|
if [ -n "$session_hooks" ]; then
|
||||||
printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks"
|
printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks"
|
||||||
fi
|
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'
|
printf ' }\n}\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,19 +459,75 @@ else
|
|||||||
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
|
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── CLAUDE.md:只在完全不存在時建立 ────────────────
|
# ── CLAUDE.md:由 profile 範本三段組裝 ────────────────
|
||||||
# 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入,
|
# 結構(界標用 HTML 註解:md 渲染看不見、grep 定位得到、CC 讀得到):
|
||||||
# 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。
|
#
|
||||||
|
# <!-- sdt:framework begin profile=X version=Y sha256=Z -->
|
||||||
|
# (profile 憲法範本原文,一字不改)
|
||||||
|
# <!-- sdt:framework end -->
|
||||||
|
# <!-- sdt:local begin -->
|
||||||
|
# (raw source 宣告 + 之後使用者/CC 自由追加)
|
||||||
|
# <!-- sdt:local end -->
|
||||||
|
#
|
||||||
|
# 為什麼要界標:在這之前 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
|
if [ ! -f "CLAUDE.md" ]; then
|
||||||
download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md"
|
if [ -z "$CLAUDE_MD_SRC" ]; then
|
||||||
if [ -f "CLAUDE.md" ]; then
|
echo "⚠️ manifest 沒有指定 $PROFILE 的 CLAUDE.md 範本,略過憲法安裝" >&2
|
||||||
emit_raw_source_block >> CLAUDE.md
|
else
|
||||||
CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE})" "← raw source declaration written (${VAULT_TYPE})")")
|
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 '<!-- sdt:framework begin profile=%s version=%s sha256=%s -->\n' \
|
||||||
|
"$PROFILE" "$TOOL_VER" "$(printf '%s' "$FW_SHA" | cut -c1-12)"
|
||||||
|
cat "$FW_TMP"
|
||||||
|
printf '<!-- sdt:framework end -->\n\n'
|
||||||
|
printf '<!-- sdt:local begin — 這一區是你的,update 永遠不會動它 -->\n'
|
||||||
|
emit_raw_source_block
|
||||||
|
printf '\n<!-- sdt:local end -->\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
|
fi
|
||||||
else
|
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
|
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 ""
|
echo ""
|
||||||
t "✅ 建立了:" "✅ Created:"
|
t "✅ 建立了:" "✅ Created:"
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+87
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# publish-github.sh — 產生過濾後的公開樹,維護 GitHub public mirror(乾淨歷史)
|
||||||
|
#
|
||||||
|
# 模型(D22 拍板):Gitea = 私有真相源(全量,含內部 wiki/SDD)
|
||||||
|
# GitHub = 公開櫥窗(過濾樹 + 每次發版一個 release commit,不帶內部歷史)
|
||||||
|
# 流量紅線(D20):push 前需 leo 親跑 github-arm.sh 解保險;低頻手動、單 repo、不掛 Actions。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# scripts/publish-github.sh # 只建/更新本機 mirror(.github-public/),不 push
|
||||||
|
# scripts/publish-github.sh --push # 另加 push(需 env GITHUB_REMOTE=https://github.com/<帳號>/<repo>.git)
|
||||||
|
#
|
||||||
|
# 排除清單:scripts/github-publish-exclude.txt(一行一個路徑,相對 repo 根;# 開頭為註解)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
EXCLUDE_FILE="$REPO_ROOT/scripts/github-publish-exclude.txt"
|
||||||
|
MIRROR_DIR="${MIRROR_DIR:-$REPO_ROOT/.github-public}"
|
||||||
|
GITHUB_REMOTE="${GITHUB_REMOTE:-}"
|
||||||
|
|
||||||
|
TMP_EXPORT="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP_EXPORT"' EXIT
|
||||||
|
|
||||||
|
# 1) 只匯出 HEAD 的 tracked 檔(.env / credentials 等 untracked 永不混入)
|
||||||
|
git -C "$REPO_ROOT" archive HEAD | tar -x -C "$TMP_EXPORT"
|
||||||
|
|
||||||
|
# 2) LFS 檔用實體內容取代 pointer(公開端不吃 LFS)
|
||||||
|
if command -v git-lfs >/dev/null 2>&1; then
|
||||||
|
git -C "$REPO_ROOT" lfs ls-files --name-only 2>/dev/null | while IFS= read -r f; do
|
||||||
|
if [ -n "$f" ] && [ -f "$REPO_ROOT/$f" ]; then
|
||||||
|
mkdir -p "$TMP_EXPORT/$(dirname "$f")"
|
||||||
|
cp "$REPO_ROOT/$f" "$TMP_EXPORT/$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2.5) 公開端不吃 LFS:刪掉 .gitattributes,否則 mirror commit 時 git-lfs 會把
|
||||||
|
# 實體檔又轉回 pointer 上傳(GitHub README 圖就破了——2026-07-18 實撞)
|
||||||
|
rm -f "$TMP_EXPORT/.gitattributes"
|
||||||
|
|
||||||
|
# 3) 套排除清單
|
||||||
|
while IFS= read -r p; do
|
||||||
|
case "$p" in ''|\#*) continue ;; esac
|
||||||
|
rm -rf "${TMP_EXPORT:?}/$p"
|
||||||
|
done < "$EXCLUDE_FILE"
|
||||||
|
|
||||||
|
# 3.5) repo 自備的遮蔽腳本(可選):對匯出樹做內容級遮蔽(如指南去帳密)
|
||||||
|
if [ -f "$REPO_ROOT/scripts/github-publish-sanitize.py" ]; then
|
||||||
|
# sanitize 失敗=公開樹仍含「別人抓不到的來源」或未遮蔽內容。
|
||||||
|
# 必須中止:原版沒擋 → 會把未淨化的樹 rsync 進 mirror,下次 --push 就送出去了。
|
||||||
|
if ! python3 "$REPO_ROOT/scripts/github-publish-sanitize.py" "$TMP_EXPORT"; then
|
||||||
|
echo "❌ sanitize 未通過 → 中止發佈(mirror 未被更動)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4) 同步進常駐 mirror(mirror 自己的 .git = 公開端乾淨歷史)
|
||||||
|
mkdir -p "$MIRROR_DIR"
|
||||||
|
[ -d "$MIRROR_DIR/.git" ] || git -C "$MIRROR_DIR" init -q -b main
|
||||||
|
rsync -a --delete --exclude='.git' "$TMP_EXPORT/" "$MIRROR_DIR/"
|
||||||
|
|
||||||
|
cd "$MIRROR_DIR"
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet && git rev-parse -q --verify HEAD >/dev/null; then
|
||||||
|
echo "ℹ️ 無變更,mirror 已是最新"
|
||||||
|
else
|
||||||
|
git -c user.name="Arcrun Release" -c user.email="release@arcrun.dev" \
|
||||||
|
commit -q -m "release: snapshot $(git -C "$REPO_ROOT" rev-parse --short HEAD) ($(git -C "$REPO_ROOT" log -1 --format=%cd --date=short))"
|
||||||
|
echo "✅ mirror 已更新:$(git log --oneline -1)"
|
||||||
|
fi
|
||||||
|
echo "📁 mirror:${MIRROR_DIR}($(git rev-list --count HEAD) 個公開 commit)"
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--push" ]; then
|
||||||
|
if [ -z "$GITHUB_REMOTE" ]; then
|
||||||
|
echo "❌ 缺 GITHUB_REMOTE(例:https://github.com/<帳號>/<repo>.git)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
git remote remove github 2>/dev/null || true
|
||||||
|
git remote add github "$GITHUB_REMOTE"
|
||||||
|
# 憑證不進 URL/指令行:有 GITHUB_MIRROR_TOKEN 就用 Basic header 注入(PAT)
|
||||||
|
if [ -n "${GITHUB_MIRROR_TOKEN:-}" ]; then
|
||||||
|
AUTH_B64="$(printf '%s:%s' "${GITHUB_ACCOUNT_NAME:-git}" "$GITHUB_MIRROR_TOKEN" | base64 | tr -d '\n')"
|
||||||
|
git -c http."$GITHUB_REMOTE".extraheader="Authorization: Basic $AUTH_B64" push -u github main
|
||||||
|
else
|
||||||
|
git push -u github main
|
||||||
|
fi
|
||||||
|
echo "🚀 已 push → $GITHUB_REMOTE"
|
||||||
|
fi
|
||||||
+353
-42
@@ -23,7 +23,11 @@ esac
|
|||||||
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
|
t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2"; fi; }
|
||||||
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
tn() { if [ "$IS_ZH" = "yes" ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }
|
||||||
|
|
||||||
REPO_RAW="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main"
|
# 來源預設=公開 GitHub(leo 2026-07-21:「要發佈的正稿,從頭就不要用奇怪的網址,
|
||||||
|
# 以免改來改去」)。發佈時不再需要改寫網址——寫死的就是對外正確的那一個。
|
||||||
|
# 內部要改指私有草稿源(Gitea/未來 CF git)時,用環境變數覆寫,不改檔:
|
||||||
|
# TEMPLATE_SOURCE=https://<你的私有 raw base> bash scripts/update.sh
|
||||||
|
REPO_RAW="${TEMPLATE_SOURCE:-https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main}"
|
||||||
TEMPLATE_URL="$REPO_RAW/template"
|
TEMPLATE_URL="$REPO_RAW/template"
|
||||||
|
|
||||||
UPDATED=()
|
UPDATED=()
|
||||||
@@ -123,13 +127,88 @@ if [ "$NEEDS_MIGRATE" = "yes" ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
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|<html'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# ── 工具函式 ───────────────────────────────────────
|
# ── 工具函式 ───────────────────────────────────────
|
||||||
# 覆蓋更新:模板/邏輯檔,無條件抓最新版蓋掉。
|
# 覆蓋更新:模板/邏輯檔,抓最新版蓋掉——**除非它被手改過**。
|
||||||
update_file() {
|
update_file() {
|
||||||
local dest="$1" src="$2"
|
local dest="$1" src="$2"
|
||||||
mkdir -p "$(dirname "$dest")"
|
mkdir -p "$(dirname "$dest")"
|
||||||
|
MANAGED+=("$dest\toverwrite")
|
||||||
if [ -f "$dest" ]; then
|
if [ -f "$dest" ]; then
|
||||||
if curl -sSL "$src" -o "$dest.tmp" 2>/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
|
if cmp -s "$dest" "$dest.tmp"; then
|
||||||
rm -f "$dest.tmp" # 內容相同,不算更新
|
rm -f "$dest.tmp" # 內容相同,不算更新
|
||||||
else
|
else
|
||||||
@@ -141,7 +220,7 @@ update_file() {
|
|||||||
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
|
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
|
||||||
fi
|
fi
|
||||||
else
|
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") # 新功能:舊版沒有的檔
|
NEW+=("$dest") # 新功能:舊版沒有的檔
|
||||||
else
|
else
|
||||||
rm -f "$dest"
|
rm -f "$dest"
|
||||||
@@ -159,12 +238,19 @@ keep_file() {
|
|||||||
# 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。
|
# 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。
|
||||||
add_if_missing() {
|
add_if_missing() {
|
||||||
local dest="$1" src="$2"
|
local dest="$1" src="$2"
|
||||||
|
MANAGED+=("$dest\tadd-if-missing")
|
||||||
if [ -f "$dest" ]; then
|
if [ -f "$dest" ]; then
|
||||||
KEPT+=("$dest")
|
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")
|
NEW+=("$dest")
|
||||||
else
|
else
|
||||||
rm -f "$dest"
|
rm -f "$dest"
|
||||||
|
t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,47 +298,148 @@ echo ""
|
|||||||
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
|
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
|
||||||
keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh"
|
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 與指引
|
# 共用 hook 與指引
|
||||||
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
|
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 "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
|
||||||
|
|
||||||
if $HAS_WIKI; then
|
# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project
|
||||||
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/,hooks/commands 留 .claude/。
|
# 投影共用同一套;任一模組在用就補/更新(邏輯檔,可覆蓋)。
|
||||||
update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md"
|
if $HAS_WIKI || $HAS_SDD; then
|
||||||
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
|
update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md"
|
||||||
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
|
fi
|
||||||
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"
|
|
||||||
# Cowork(claude.ai)的 wiki 整理 skill:規則檔,可覆蓋
|
|
||||||
update_file "system-dev/docs/SKILL.md" "$TEMPLATE_URL/system-dev/docs/SKILL.md"
|
|
||||||
|
|
||||||
# wiki 的「使用者資料」:絕不碰
|
if $HAS_WIKI; then
|
||||||
keep_file "system-dev/wiki/status.md"
|
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/,hooks/commands 留 .claude/。
|
||||||
keep_file "system-dev/wiki/mistakes.md"
|
update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md"
|
||||||
# principles.md(1.10):舊版沒有 → 補範本;已有 → 當用戶資料保留
|
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
|
||||||
add_if_missing "system-dev/wiki/principles.md" "$TEMPLATE_URL/system-dev/wiki/principles.md"
|
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
|
||||||
keep_file "system-dev/wiki/decisions-summary.md"
|
# 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG)
|
||||||
keep_file "system-dev/wiki/TAXONOMY.md"
|
update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh"
|
||||||
keep_file "system-dev/wiki/.wikiignore"
|
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
|
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"
|
|
||||||
|
|
||||||
# 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/ ──
|
# ── 自我更新:把最新的 update.sh / install.sh 抓到 system-dev/scripts/ ──
|
||||||
# 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。
|
# 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。
|
||||||
update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh"
|
update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh"
|
||||||
@@ -262,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/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 '<!-- sdt:framework begin profile=%s version=%s sha256=%s -->\n' \
|
||||||
|
"$SDT_PROFILE" "$REMOTE_VER" "$(sdt_sha256 "$FW_TMP" | cut -c1-12)"
|
||||||
|
cat "$FW_TMP"
|
||||||
|
printf '<!-- sdt:framework end -->\n\n'
|
||||||
|
printf '<!-- sdt:local begin — 這一區是你的,update 永遠不會動它 -->\n'
|
||||||
|
printf '\n<!-- ⬇️ 以下是你原本的 CLAUDE.md,一字未動地搬進來。\n'
|
||||||
|
printf ' 上面的框架區從現在起由 template 維護;兩邊若有重複的段落,\n'
|
||||||
|
printf ' 請自己刪掉這裡的舊版本(我們不敢代你刪)。\n'
|
||||||
|
printf ' 原檔備份:CLAUDE.md.before-markers -->\n\n'
|
||||||
|
cat "CLAUDE.md.before-markers"
|
||||||
|
printf '\n<!-- sdt:local end -->\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 ""
|
echo ""
|
||||||
@@ -294,6 +521,29 @@ if [ ${#UPDATED[@]} -gt 0 ]; then
|
|||||||
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
|
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
|
||||||
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
|
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
|
||||||
fi
|
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 <file>.new <file>"
|
||||||
|
fi
|
||||||
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
|
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
t "✨ 模板邏輯檔已全部最新,無需變動。" \
|
t "✨ 模板邏輯檔已全部最新,無需變動。" \
|
||||||
@@ -322,6 +572,39 @@ if [ -f ".claude/settings.json" ]; then
|
|||||||
$HAS_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING+=("SessionStart: session-start-recall.sh")
|
$HAS_WIKI && ! grep -q "session-start-recall.sh" .claude/settings.json && MISSING+=("SessionStart: session-start-recall.sh")
|
||||||
$HAS_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
|
$HAS_WIKI && ! grep -q "wiki-secret-scan.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): wiki-secret-scan.sh")
|
||||||
$HAS_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): sdd-guard.sh")
|
$HAS_SDD && ! grep -q "sdd-guard.sh" .claude/settings.json && MISSING+=("PreToolUse(Write|Edit): sdd-guard.sh")
|
||||||
|
|
||||||
|
# ── 1.16.0:兩支 wiki 讀取 hook 自動註冊(不只提醒)──
|
||||||
|
# 理由:這兩支的整個存在意義就是「不依賴任何人記得」。
|
||||||
|
# 若靠人看提醒去手動補 settings.json,等於把同一個病搬到安裝環節。
|
||||||
|
if $HAS_WIKI && command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 - <<'PYEOF' 2>/dev/null || true
|
||||||
|
import json, os
|
||||||
|
p = ".claude/settings.json"
|
||||||
|
try:
|
||||||
|
with open(p) as f: d = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
raise SystemExit(0) # 壞掉的 settings 不碰,交給下方提醒
|
||||||
|
pre = d.setdefault("hooks", {}).setdefault("PreToolUse", [])
|
||||||
|
blob = json.dumps(pre)
|
||||||
|
added = []
|
||||||
|
# 1.16.1:既有註冊若漏 Bash(原版只掛 Grep|Glob|Read)就地補上——
|
||||||
|
# 破口實例:用 curl/wrangler 亂試部署方法走 Bash,整支 hook 不觸發。
|
||||||
|
for _e in pre:
|
||||||
|
if "wiki-first-search" in json.dumps(_e) and "Bash" not in _e.get("matcher", ""):
|
||||||
|
_e["matcher"] = "Grep|Glob|Read|Bash"; added.append("wiki-first-search(補Bash)")
|
||||||
|
if "wiki-first-search" not in blob:
|
||||||
|
pre.append({"matcher": "Grep|Glob|Read|Bash", "hooks": [
|
||||||
|
{"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh"}]})
|
||||||
|
added.append("wiki-first-search")
|
||||||
|
if "subagent-wiki-guard" not in blob:
|
||||||
|
pre.append({"matcher": "Task", "hooks": [
|
||||||
|
{"type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh"}]})
|
||||||
|
added.append("subagent-wiki-guard")
|
||||||
|
if added:
|
||||||
|
with open(p, "w") as f: json.dump(d, f, ensure_ascii=False, indent=2)
|
||||||
|
print(" ✅ 已自動註冊 wiki 讀取 hook:" + "、".join(added))
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
if [ ${#MISSING[@]} -gt 0 ]; then
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
|
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
|
||||||
@@ -330,6 +613,34 @@ if [ -f ".claude/settings.json" ]; then
|
|||||||
fi
|
fi
|
||||||
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 ""
|
echo ""
|
||||||
t "🚀 更新完成:${LOCAL_VER} → ${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER} → ${REMOTE_VER}"
|
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"
|
t " 下次更新直接跑:bash system-dev/scripts/update.sh" " Next time, just run: bash system-dev/scripts/update.sh"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ description: >-
|
|||||||
│ ├── 4-guides/
|
│ ├── 4-guides/
|
||||||
│ ├── 5-records/{incidents,test-reports}/
|
│ ├── 5-records/{incidents,test-reports}/
|
||||||
│ └── 6-user/
|
│ └── 6-user/
|
||||||
└── .claude/wiki/ ← CC 整理的知識(CC 寫)
|
└── system-dev/wiki/ ← CC 整理的知識(CC 寫)
|
||||||
├── INDEX.md
|
├── INDEX.md
|
||||||
├── mistakes.md
|
├── mistakes.md
|
||||||
├── status.md
|
├── status.md
|
||||||
@@ -63,7 +63,7 @@ description: >-
|
|||||||
|
|
||||||
| 層 | 機制 | 擋什麼 | 性質 |
|
| 層 | 機制 | 擋什麼 | 性質 |
|
||||||
|----|------|--------|------|
|
|----|------|--------|------|
|
||||||
| **L1** | `.claude/wiki/.wikiignore`(glob,像 .gitignore) | 整個機敏檔不編入 | 協議(CC 遵守) |
|
| **L1** | `system-dev/wiki/.wikiignore`(glob,像 .gitignore) | 整個機敏檔不編入 | 協議(CC 遵守) |
|
||||||
| **L2** | 行內標記 `<!-- wiki:ignore -->` … `<!-- wiki:end -->` | 檔案內某段不編入 | 協議(CC 遵守) |
|
| **L2** | 行內標記 `<!-- wiki:ignore -->` … `<!-- wiki:end -->` | 檔案內某段不編入 | 協議(CC 遵守) |
|
||||||
| **L3** | `wiki-secret-scan.sh` hook(PreToolUse) | 機敏值真的寫進 wiki → exit 2 擋 | 硬攔截(機械偵測) |
|
| **L3** | `wiki-secret-scan.sh` hook(PreToolUse) | 機敏值真的寫進 wiki → exit 2 擋 | 硬攔截(機械偵測) |
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ Session 結束時固定更新 status.md:
|
|||||||
## 維護規則
|
## 維護規則
|
||||||
|
|
||||||
1. `docs/` 只讀不改
|
1. `docs/` 只讀不改
|
||||||
2. `.claude/wiki/` 只增不刪
|
2. `system-dev/wiki/` 只增不刪
|
||||||
3. `CLAUDE.md` 不增長,超過 100 行就是放錯地方
|
3. `CLAUDE.md` 不增長,超過 100 行就是放錯地方
|
||||||
4. 分類不確定就問
|
4. 分類不確定就問
|
||||||
5. 對話結論當場 capture
|
5. 對話結論當場 capture
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
開始任一任務,按順序:
|
開始任一任務,按順序:
|
||||||
|
|
||||||
1. 讀 `.claude/wiki/status.md`(3 分鐘)
|
1. 讀 `system-dev/wiki/status.md`(3 分鐘)
|
||||||
2. 確認有對應 SDD(`docs/3-specs/`)
|
2. 確認有對應 SDD(`docs/3-specs/`)
|
||||||
3. 動手前宣告範圍
|
3. 動手前宣告範圍
|
||||||
4. 完成後更新 wiki
|
4. 完成後更新 wiki
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
|
|
||||||
| 檔案 | 時機 | 用途 |
|
| 檔案 | 時機 | 用途 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `.claude/wiki/status.md` | session 開始第一件事 | 當前進度、下一步 |
|
| `system-dev/wiki/status.md` | session 開始第一件事 | 當前進度、下一步 |
|
||||||
| `.claude/wiki/mistakes.md` | 做新功能前 | 已知誤解 + 快速檢查清單 |
|
| `system-dev/wiki/mistakes.md` | 做新功能前 | 已知誤解 + 快速檢查清單 |
|
||||||
| `.claude/wiki/decisions-summary.md` | 遇到設計判斷時 | 架構決策快速查 |
|
| `system-dev/wiki/decisions-summary.md` | 遇到設計判斷時 | 架構決策快速查 |
|
||||||
| `.claude/wiki/INDEX.md` | 找不到東西時 | wiki 導引 |
|
| `system-dev/wiki/INDEX.md` | 找不到東西時 | wiki 導引 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -96,10 +96,10 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## .claude/wiki/INDEX.md 範本
|
## system-dev/wiki/INDEX.md 範本
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# .claude/wiki/ — [專案名稱] LLM 記憶系統
|
# system-dev/wiki/ — [專案名稱] LLM 記憶系統
|
||||||
|
|
||||||
> 新 session 開始時從這裡導航。
|
> 新 session 開始時從這裡導航。
|
||||||
> 目的:讓 CC 不需要重新學習已知的事。
|
> 目的:讓 CC 不需要重新學習已知的事。
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## .claude/wiki/mistakes.md 範本
|
## system-dev/wiki/mistakes.md 範本
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# CC 已知誤解 + 避坑方法
|
# CC 已知誤解 + 避坑方法
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## .claude/wiki/decisions-summary.md 範本
|
## system-dev/wiki/decisions-summary.md 範本
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# 架構決策摘要
|
# 架構決策摘要
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.13.0
|
1.19.0
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆
|
|||||||
|
|
||||||
## 3. 跨 repo 署名(鐵律 — 絕不可漏)
|
## 3. 跨 repo 署名(鐵律 — 絕不可漏)
|
||||||
|
|
||||||
所有 repo(mira / graph-plugin / ingest-plugin / Arcrun / template…)共用**同一個 GitHub 帳號**發 issue/comment,
|
當你的多個 repo 共用**同一個託管帳號**發 issue/comment 時,
|
||||||
所以 issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**。
|
issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**。
|
||||||
|
|
||||||
> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。
|
> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。
|
||||||
|
|
||||||
- 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`…
|
- 收件方 CC 回報:`[<收件 repo 名> CC]`(例:`[<某子系統> CC]`)
|
||||||
- 總管下令/追問:`[InkStoneCo 總管]`
|
- 上游/總管下令、追問:`[<上游 repo 名> 總管]`
|
||||||
- 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。
|
- 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。
|
||||||
|
|
||||||
為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**;
|
為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**;
|
||||||
|
|||||||
@@ -4,6 +4,26 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 生命週期(單一活性鐵律,全文見 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`)
|
||||||
|
|
||||||
|
五條鐵律摘要:
|
||||||
|
|
||||||
|
1. **單一活性**:任何時刻整個 repo 只允許一份 `status: active` 的 SDD;所有開發任務對應它的 tasks,找不到對應任務 → 停下來問,不准直接做。
|
||||||
|
2. **禁止自行建立 SDD**:澄清問題→回答不動文件;任務層變更→更新現行 SDD 的 tasks(標日期與原因);規格層變更→走第 3 條。
|
||||||
|
3. **規格變更只有一條路**:change proposal 寫進 `system-dev/docs/3-specs/pending-changes.md`(摘要+觸發原因+影響分析),然後**停止**等使用者「confirm」。
|
||||||
|
4. **開新 SDD 的唯一時機**:使用者 confirm 後——先把舊 SDD 未完成任務逐條搬入新 SDD(做完前不准寫 code)→ 舊的標 `closed` + `superseded_by` 移入 `archive/` → 新 SDD changelog 記繼承 → 列搬移/作廢清單請最終確認。
|
||||||
|
5. **每次 session 開始**先讀 active SDD 與 pending-changes.md,回報三個數字:
|
||||||
|
|
||||||
|
```
|
||||||
|
📐 現行規格:〈SDD 名稱〉
|
||||||
|
📋 未完成任務:N
|
||||||
|
⚖️ 待裁決 proposal:M
|
||||||
|
```
|
||||||
|
|
||||||
|
若出現**兩份 active=規則已被違反,當場糾正**(收斂到一份,其餘 paused/closed)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 執行流程
|
## 執行流程
|
||||||
|
|
||||||
### 第一步:理解任務
|
### 第一步:理解任務
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# /wiki-extract — vault 增量萃取(Logseq / Obsidian → system-dev/wiki)
|
||||||
|
|
||||||
|
把**筆記 vault**(Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地
|
||||||
|
萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**:
|
||||||
|
AI 只產卡片檔,下游 ingest 管線再從 wikilink 機械拉三元組進知識庫。
|
||||||
|
|
||||||
|
> **跟 `/wiki-init` 的分工**:
|
||||||
|
> - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。
|
||||||
|
> - `/wiki-extract` 是**之後每次**的增量重萃——vault 會被 Syncthing/cron 持續灌新筆記,
|
||||||
|
> 這支負責「只萃變動的、沒變的不碰、不浪費 AI run」。給 Routine / cloud-worker 反覆跑。
|
||||||
|
> - **跑它的是你(CC / Routine)=LLM 本人,不需任何 token**。
|
||||||
|
|
||||||
|
> **邊界(硬規矩,別越界)**
|
||||||
|
> - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游
|
||||||
|
> 下游 ingest 管線從你產的 `[[wikilink]]` + `## 關聯` 機械映射,不是這支的事。
|
||||||
|
> - **原始筆記唯讀**:`journals/`、`pages/`、Obsidian 根 `.md` 是 leo 的手寫真身,
|
||||||
|
> 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。
|
||||||
|
> - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 執行流程
|
||||||
|
|
||||||
|
### 第一步:確認這是 vault repo,定位 raw source
|
||||||
|
|
||||||
|
偵測邏輯**同 install.sh / wiki-init**:
|
||||||
|
|
||||||
|
| 偵測到 | 型態 | raw source(要掃的原文) |
|
||||||
|
|--------|------|--------------------------|
|
||||||
|
| 根目錄有 `logseq/` | Logseq vault | `journals/*.md` + `pages/*.md` |
|
||||||
|
| 根目錄有 `.obsidian/` | Obsidian vault | vault 根下所有 `.md` |
|
||||||
|
| 都沒有 | **不是 vault** | → 停手。這支只處理 vault;一般 dev repo 開發時就手寫 `.claude`/`system-dev/wiki`,不需萃取 |
|
||||||
|
|
||||||
|
沒有 `system-dev/wiki/`?→ 先跑 `/wiki-init`(首次建結構+首萃),再回來用這支做增量。
|
||||||
|
|
||||||
|
### 第二步:content_hash 冪等 —— 決定哪些檔要萃(省 run 的核心)
|
||||||
|
|
||||||
|
讀萃取 manifest:`system-dev/wiki/.extract-manifest.json`(不存在=首次,視同全部要萃)。
|
||||||
|
格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"algo": "sha256",
|
||||||
|
"sources": {
|
||||||
|
"journals/2026_07_01.md": {
|
||||||
|
"content_hash": "<sha256 of file bytes>",
|
||||||
|
"extracted_at": "2026-07-06",
|
||||||
|
"cards": ["Prompt能力即拆解自己邏輯的能力", "程式化邏輯可圖解任何主題不限AI"],
|
||||||
|
"skipped_reason": null
|
||||||
|
},
|
||||||
|
"journals/2026_06_25.md": {
|
||||||
|
"content_hash": "<sha256>",
|
||||||
|
"extracted_at": "2026-07-06",
|
||||||
|
"cards": [],
|
||||||
|
"skipped_reason": "空檔/訊息量不足,無可萃知識點"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
對每個 raw source 檔:
|
||||||
|
|
||||||
|
1. 算目前 `content_hash`(`sha256sum <file>`,取檔案 bytes 的 hash)。
|
||||||
|
2. 跟 manifest 裡該檔的 `content_hash` 比:
|
||||||
|
- **相同 → skip,不讀不萃、不呼叫任何 AI 推理**(就算它上次 `cards: []` 也 skip——空檔沒變還是空)。
|
||||||
|
- **不同或不在 manifest → 這檔要(重)萃**。
|
||||||
|
3. manifest 有、但檔已不存在 → 該檔被刪,把它的 entry 從 manifest 移除(卡片是否連帶處理見第五步)。
|
||||||
|
|
||||||
|
> **這一步是「省 run」的重點**:vault 每天可能只動 1~2 個 journal,其餘幾十個檔 hash 沒變
|
||||||
|
> 就整批跳過,AI 只對真正變動的檔動腦。**重跑一個沒變動的 vault = 零 AI 呼叫、零 diff。**
|
||||||
|
|
||||||
|
### 第三步:對「要萃」的檔,抓知識點 + 任務
|
||||||
|
|
||||||
|
逐個變動檔讀原文,分兩類抽取:
|
||||||
|
|
||||||
|
**(a) 知識點 → 概念原子卡**
|
||||||
|
判準與卡片格式**完全依 `/wiki-init` 第五步**(frontmatter `tags:`/`gloss:`、H1、麵包屑
|
||||||
|
`← [[<bucket>/00-INDEX]]`、`**來源**`、`## 摘要`、`## 重點`、`## 實體`、`## 關聯` 的
|
||||||
|
typed-edge 三元組、TAXONOMY 受控標籤、硬自檢等)——**不在這裡重寫格式,一律回去讀那份**。
|
||||||
|
廢話/訊息量薄的段落略過(在 manifest 記 `skipped_reason`,誠實留痕、不留卡)。
|
||||||
|
|
||||||
|
**(b) Logseq 任務 marker → 任務卡(task_status)**
|
||||||
|
解析**完全依** `system-dev/docs/4-guides/logseq-markers.md`(單一真相源,與 template#4
|
||||||
|
tasks 投影共用同一套;**別自己另寫 mapping**)。摘要:
|
||||||
|
|
||||||
|
- 任務行 regex:`^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+`
|
||||||
|
- 狀態正規化:TODO/LATER→`todo`、DOING/NOW→`in-progress`、WAITING→`blocked`、
|
||||||
|
DONE→`done`、CANCELED/CANCELLED→`closed`。
|
||||||
|
- 跳過 `:LOGBOOK:…:END:` 區塊與 `key:: value` 屬性行(`collapsed::`、`id::`、
|
||||||
|
`SCHEDULED::`、`DEADLINE::`…),**別把 marker 或屬性當任務內文**。
|
||||||
|
|
||||||
|
有實質內容的任務 → 產一張任務卡進 `cards/tasks/` bucket,frontmatter 帶 `task_status`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
tags: [<領域標籤,依 TAXONOMY>]
|
||||||
|
task_status: todo # ← 依上表正規名;這是任務卡才有的欄位
|
||||||
|
gloss: 一句話定義這個任務要達成什麼(供下游 normalize)
|
||||||
|
---
|
||||||
|
# <任務一句話標題(marker 後的內文,去掉 marker)>
|
||||||
|
|
||||||
|
← [[tasks/00-INDEX]]
|
||||||
|
|
||||||
|
**來源**:`journals/2026_07_01.md`(TODO block)
|
||||||
|
**最後更新**:YYYY-MM-DD
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
[任務要做什麼、脈絡]
|
||||||
|
|
||||||
|
## 實體
|
||||||
|
- **<關鍵實體正規名>**(<同義詞>)— <一句描述>
|
||||||
|
|
||||||
|
## 關聯
|
||||||
|
### 內文知識關係(端點=上方 `## 實體` 正規名,一字不差)
|
||||||
|
- <實體A> >> <謂詞> >> <實體B>
|
||||||
|
### 卡片關係(卡對卡)
|
||||||
|
- [[本任務卡]] >> 涉及 >> [[相關概念卡]]
|
||||||
|
```
|
||||||
|
|
||||||
|
> 純瑣事任務(「買菜」這種無知識量)不必成獨立卡——可在 `cards/tasks/00-INDEX.md`
|
||||||
|
> 列一行帶狀態即可,避免灌垃圾卡。判準同 D16:有沒有知識/專案價值。
|
||||||
|
|
||||||
|
### 第四步:更新桶索引與 INDEX
|
||||||
|
|
||||||
|
- 每個動到的 bucket(如 `cards/notes/`、`cards/tasks/`)更新其 `00-INDEX.md`
|
||||||
|
(容器:只連不重寫,H2/H3 分節)。
|
||||||
|
- 更新 `system-dev/wiki/INDEX.md` 的標籤視圖與卡片清單。
|
||||||
|
- 任務卡可在 INDEX 開一個「任務視圖」按 `task_status` 聚類。
|
||||||
|
|
||||||
|
### 第五步:寫回 manifest + 驗證原文 0 動
|
||||||
|
|
||||||
|
1. 把這次萃過的每個檔的**新 `content_hash`**、`extracted_at`、產出的 `cards`、
|
||||||
|
(或 `skipped_reason`)寫回 `system-dev/wiki/.extract-manifest.json`。
|
||||||
|
**沒動到的檔的 entry 原樣保留**(別整檔重寫掉別人的 hash)。
|
||||||
|
2. 驗證原文零異動(踩過的坑):
|
||||||
|
```
|
||||||
|
git status --short journals/ pages/ # Obsidian 則看根目錄 .md ——須 0 新增 0 修改
|
||||||
|
```
|
||||||
|
有任何原文變動 → 你誤寫了 raw source,回滾。
|
||||||
|
|
||||||
|
### 第六步:完成報告
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ wiki-extract 完成(增量)
|
||||||
|
掃描:N 個 raw source 檔
|
||||||
|
萃取:M 個(content_hash 變動)→ 產出 X 張概念卡 + Y 張任務卡
|
||||||
|
跳過:K 個(hash 未變,零 AI 呼叫)
|
||||||
|
任務狀態分布:todo A / in-progress B / done C / …
|
||||||
|
原文驗證:journals/ pages/ git status 0 異動 ✅
|
||||||
|
manifest:system-dev/wiki/.extract-manifest.json 已更新
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 冪等自檢(Routine 反覆跑必守)
|
||||||
|
|
||||||
|
- [ ] 跑之前先讀 manifest,hash 相同的檔**完全不進 AI**(不是「讀了才發現一樣」,是靠 hash 先擋)。
|
||||||
|
- [ ] 對「同一個沒變動的 vault」連跑兩次:第二次應是**零萃取、零卡片 diff、零 manifest 變化**。
|
||||||
|
- [ ] 只有 `system-dev/wiki/` 有寫入;`journals/`、`pages/` git status 全乾淨。
|
||||||
|
- [ ] 任務狀態用正規名,marker/屬性沒混進內文(照 `logseq-markers.md` 自檢)。
|
||||||
@@ -228,3 +228,8 @@ git status --short pages/ journals/ # 或一般專案的 docs/ ——須 0
|
|||||||
原文驗證:pages/ journals/ git status 0 異動 ✅
|
原文驗證:pages/ journals/ git status 0 異動 ✅
|
||||||
下一步:用 /wiki-capture 把重要決策存進 wiki
|
下一步:用 /wiki-capture 把重要決策存進 wiki
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **vault repo 首萃後的增量重萃**:Logseq / Obsidian vault 會被持續灌新筆記。首萃(本命令)
|
||||||
|
> 之後,改用 **`/wiki-extract`** 做增量——它靠 content_hash 只萃變動的檔(沒變=零 AI 呼叫),
|
||||||
|
> 並解析 Logseq 大寫任務 marker(TODO/DOING/DONE…→ `task_status`,見
|
||||||
|
> `system-dev/docs/4-guides/logseq-markers.md`)。適合掛給 Routine / cloud-worker 反覆跑。
|
||||||
|
|||||||
@@ -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 <<EOF
|
||||||
|
❌ BLOCKED by install-artifact-guard(防糾纏閘 S1)
|
||||||
|
|
||||||
|
這個檔是**框架發下來的機制**,實例不改機制。
|
||||||
|
|
||||||
|
路徑:$REL
|
||||||
|
|
||||||
|
正確做法:
|
||||||
|
· 機制要改 → 回框架/政策包 repo 提案,bump 版本,實例再 update
|
||||||
|
(一次修全家,別人也拿得到;在這裡改只有你受惠,而且下次更新就孤立)
|
||||||
|
· 只是這個專案的特殊禁令 → 寫進 .claude/hooks/pre-write-guard.sh 的自訂區
|
||||||
|
(那支就是留給你填的插槽,不會被 update 覆蓋)
|
||||||
|
· 你就是在開發框架本身 → 這個 repo 根目錄該有 .sdt-framework-dev
|
||||||
|
|
||||||
|
為什麼擋(實測,不是原則潔癖):
|
||||||
|
對照過一個真實使用中的實例——6 支框架 hook 裡 4 支已被手改,
|
||||||
|
而沒有任何人知道。那 4 支從此收不到框架的任何修正。
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# jdd-format-guard.sh — PM 軌文件的格式閘(JDD 規則 J4+J5+J8)
|
||||||
|
#
|
||||||
|
# 掛 PreToolUse(matcher: Write|Edit|MultiEdit)。
|
||||||
|
#
|
||||||
|
# J4 root.md 的 🔴 卡缺【要驗證 + 對帳日】 → 擋
|
||||||
|
# 為什麼:紅卡=賭注。賭注沒有「賭輸了怎麼知道」和「什麼時候結算」,
|
||||||
|
# 就會永遠是「還在做」,永遠不必認賠——那不是賭注,是藉口。
|
||||||
|
#
|
||||||
|
# J5 tasks.md **新增**的任務缺站號 → 擋
|
||||||
|
# 為什麼:sprint 的單位是站。任務不掛站,就沒有人答得出
|
||||||
|
# 「這個任務不做,哪一站會掛?」——那正是認領流程唯一的問題。
|
||||||
|
#
|
||||||
|
# J8 root.md/journeys.md 出現技術名詞 → 擋
|
||||||
|
# 為什麼:這兩份是給不懂技術的人讀的。技術是達成手段,寫進各專案自己的規格。
|
||||||
|
#
|
||||||
|
# ── J8 的關鍵細節:只掃「卡片/站的本體」,其餘一概不掃 ──────────
|
||||||
|
# 踩過兩次同一類坑(第二次是本閘自己被真實文件抓包):
|
||||||
|
# ① 文件開頭的規矩說明裡寫「各 repo 的規格」→ 被自己的自檢抓到
|
||||||
|
# ② 文件結尾「這卷還缺什麼」的自述裡提到跨 repo 鏈路 → 又被抓到
|
||||||
|
# 兩者都不是卡片內容,是**文件在講自己**。
|
||||||
|
# 閘要是連這些都掃,人就只能把說明寫得不清不楚來換綠燈,本末倒置。
|
||||||
|
#
|
||||||
|
# 第一版用「排除法」(跳過 > 引言、註解、標題)——不夠,因為自述段是普通條列。
|
||||||
|
# 改用**正面圈定**:只掃真正的內容體
|
||||||
|
# · root.md = `- **P<n>**` 卡片行 + 它底下的縮排子項
|
||||||
|
# · journeys.md = `#### S<n>` 站標題以下、到下一個標題之前的內文
|
||||||
|
# 自述段、說明區、索引表因為不在這兩種範圍裡,自然就不會被掃到——
|
||||||
|
# 不必為它們一個個開例外。
|
||||||
|
#
|
||||||
|
# 誠實限制:只認字面與行首形狀。
|
||||||
|
# 「把技術概念用白話包裝起來」它看不出來(那要人讀);
|
||||||
|
# 用 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<n>**` 卡片行 + 其縮排子項
|
||||||
|
# journeys.md → `#### S<n>` 站標題以下到下一個標題之前的內文
|
||||||
|
card_body_only() { # $1=root|journeys
|
||||||
|
if [ "$1" = "root" ]; then
|
||||||
|
awk '
|
||||||
|
/<!--/ { inc=1 } inc { if (/-->/) 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=1 } inc { if (/-->/) 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 <<EOF
|
||||||
|
❌ BLOCKED by jdd-format-guard(規則 $1)
|
||||||
|
|
||||||
|
$2
|
||||||
|
|
||||||
|
檔案:$REL
|
||||||
|
|
||||||
|
$3
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── J4:root.md 的紅卡必須附【要驗證 + 對帳日】────────
|
||||||
|
if printf '%s' "$REL" | grep -q 'root\.md$'; then
|
||||||
|
# 逐張紅卡檢查:紅卡行之後、下一張卡之前,要出現「對帳日」
|
||||||
|
MISSING="$(printf '%s' "$CONTENT" | awk '
|
||||||
|
/^[[:space:]]*-[[:space:]]*\*\*P[0-9]+\*\*/ {
|
||||||
|
if (pending != "" && !found) { print pending }
|
||||||
|
found = 0
|
||||||
|
if ($0 ~ /🔴/) { pending = NR "\t" $0 } else { pending = "" }
|
||||||
|
next
|
||||||
|
}
|
||||||
|
pending != "" && /對帳日/ { found = 1 }
|
||||||
|
END { if (pending != "" && !found) print pending }
|
||||||
|
')"
|
||||||
|
if [ -n "$MISSING" ]; then
|
||||||
|
fail "J4" "紅卡(🔴)少了【要驗證 + 對帳日】。" \
|
||||||
|
" 下列紅卡沒有對帳行:
|
||||||
|
$(printf '%s' "$MISSING" | sed 's/^/ 行 /' | cut -c1-120)
|
||||||
|
|
||||||
|
補成這樣:
|
||||||
|
- **P9** 🔴 <一句白話>。
|
||||||
|
- 【要驗證:<能用真實數字或事實判真假的判準> | 對帳日 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<n> / S-<n> / 「服務: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
|
||||||
@@ -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 根目錄 ────────────────────────────────────────
|
||||||
|
# 從本檔位置往上推三層:<root>/.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 <<EOF
|
||||||
|
❌ 身分組合不存在:成員 repo × orchestrator
|
||||||
|
|
||||||
|
scope(安裝位置):repo ← system-dev/.profile
|
||||||
|
role (AGENT_ROLE):orchestrator ← 環境變數
|
||||||
|
|
||||||
|
成員 repo 裡沒有 PM。PM(orchestrator)的動作——改 root.md/journeys.md/
|
||||||
|
排 sprint 站號——只能在總管 repo 做。
|
||||||
|
|
||||||
|
怎麼修(擇一):
|
||||||
|
· 你其實是來寫 code 的 → unset AGENT_ROLE,或設成 engineer
|
||||||
|
· 你真的要做 PM 的事 → 回總管 repo 做,不要在成員 repo 裡做
|
||||||
|
· 這個 repo 其實是總管 repo → system-dev/.profile 內容應為 orchestrator
|
||||||
|
(裝錯 profile 了,重跑 install.sh --profile=orchestrator)
|
||||||
|
EOF
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 從 hook 的 JSON 取 file_path ────────────────────────
|
||||||
|
# 三段 fallback:jq → python3 → grep。沿本 template 既有 hook 的容錯慣例
|
||||||
|
#(拿不到就回空字串,讓呼叫端放行——寧可漏擋也不誤殺)。
|
||||||
|
# 用法:FILE_PATH="$(sdt_file_path "$INPUT")" ← 傳字串,不是讀 stdin
|
||||||
|
#(stdin 只能讀一次,統一由呼叫端 `INPUT="$(cat)"` 讀走再傳進來)
|
||||||
|
sdt_file_path() {
|
||||||
|
local input="$1"
|
||||||
|
[ -z "$input" ] && return 0
|
||||||
|
if command -v jq >/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)"
|
||||||
|
}
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# publish-lag-check.sh — SessionStart hook:偵測「公開 mirror 落後工作區」並出聲
|
||||||
|
#
|
||||||
|
# 病根(leo 2026-07-21 點名的真實風險):
|
||||||
|
# Gitea(草稿/工作現場)與 GitHub(正稿/成品櫥窗)是**手動同步**的
|
||||||
|
# (靠人跑 scripts/publish-github.sh --push,且需 D20 arm)。
|
||||||
|
# → 改了零件、重編 wasm 後若沒人記得發佈,**用戶抓到舊版且沒有任何錯誤訊息,
|
||||||
|
# 只是行為不對**——這種靜默失敗只有外部使用者會撞到,我們自己永遠測不到。
|
||||||
|
#
|
||||||
|
# 典型形狀:安裝器/前端從 CDN 懶載資源,網址長得像
|
||||||
|
# cdn.jsdelivr.net/gh/<帳號>/<repo>@main/<產物路徑>
|
||||||
|
# ——那個位址永遠指向公開 repo 的**最後一次發佈**,不是你本機的最新版。
|
||||||
|
# 本機改對了、CDN 還在吐三代前的東西,而且不會有任何錯誤訊息。
|
||||||
|
#
|
||||||
|
# 原理:比對「工作區 HEAD」與「.github-public 最後一個 release commit 記錄的 snapshot」。
|
||||||
|
# publish-github.sh 的 commit 訊息格式固定為:release: snapshot <短hash> (<日期>)
|
||||||
|
# → 從中取出 hash,看它是不是工作區 HEAD 的祖先/相同。
|
||||||
|
#
|
||||||
|
# 只提醒不阻擋(exit 0):發不發佈是人的決定(且 push GitHub 需 leo 親跑 arm),
|
||||||
|
# hook 的職責只是消滅「忘了」這個失敗模式。
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MIRROR_DIR=".github-public"
|
||||||
|
|
||||||
|
# 沒裝發佈管線的 repo 直接安靜退出
|
||||||
|
[ -d "$MIRROR_DIR/.git" ] || exit 0
|
||||||
|
[ -f "scripts/publish-github.sh" ] || exit 0
|
||||||
|
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
|
HEAD_SHORT="$(git rev-parse --short HEAD 2>/dev/null || echo '')"
|
||||||
|
[ -z "$HEAD_SHORT" ] && exit 0
|
||||||
|
|
||||||
|
# 從 mirror 最後一個 commit 訊息取出它當初發佈的來源 hash
|
||||||
|
LAST_MSG="$(git -C "$MIRROR_DIR" log -1 --format=%s 2>/dev/null || echo '')"
|
||||||
|
PUBLISHED="$(printf '%s' "$LAST_MSG" | sed -n 's/.*snapshot \([0-9a-f]\{6,\}\).*/\1/p')"
|
||||||
|
|
||||||
|
if [ -z "$PUBLISHED" ]; then
|
||||||
|
# mirror 存在但沒有可辨識的 release commit(可能還沒發過)
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "📦 這個 repo 有公開發佈管線,但 mirror 還沒發過任何版本"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo " 若已有用戶依賴公開版(例如安裝器從 jsDelivr 抓 wasm),現在是空的。"
|
||||||
|
echo " 發佈:leo 在頂層跑 scripts/github-arm.sh,再於本 repo 跑"
|
||||||
|
echo " GITHUB_REMOTE=... bash scripts/publish-github.sh --push"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 已發佈的那個 commit 就是現在的 HEAD → 同步,安靜
|
||||||
|
if [ "$PUBLISHED" = "$HEAD_SHORT" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 算出落後幾個 commit(發佈點 → HEAD)。取不到就不顯示數字。
|
||||||
|
BEHIND="$(git rev-list --count "${PUBLISHED}..HEAD" 2>/dev/null || echo '')"
|
||||||
|
|
||||||
|
# 落後 0 且 hash 不同 → 可能是 mirror 比工作區新(罕見,例如剛 rebase),一樣提醒
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
if [ -n "$BEHIND" ] && [ "$BEHIND" != "0" ]; then
|
||||||
|
printf '📤 公開 mirror 落後工作區 %s 個 commit(最後發佈:%s,現在:%s)\n' \
|
||||||
|
"$BEHIND" "$PUBLISHED" "$HEAD_SHORT"
|
||||||
|
else
|
||||||
|
printf '📤 公開 mirror 與工作區不一致(最後發佈:%s,現在:%s)\n' "$PUBLISHED" "$HEAD_SHORT"
|
||||||
|
fi
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "⚠️ 外部使用者拿到的仍是舊版,而且**不會有任何錯誤訊息**——只是行為不對。"
|
||||||
|
echo " (安裝器的懶載直接從公開位址抓 wasm,落後=裝到舊零件。)"
|
||||||
|
echo ""
|
||||||
|
echo " 要發佈:① leo 在頂層跑 bash scripts/github-arm.sh \"<任務描述>\" 30"
|
||||||
|
echo " ② 本 repo 跑 GITHUB_REMOTE=https://github.com/<帳號>/<repo>.git \\"
|
||||||
|
echo " bash scripts/publish-github.sh --push"
|
||||||
|
echo " 不急著發也沒關係——這只是提醒,別讓它靜默漏掉。"
|
||||||
|
|
||||||
|
# 若這次落後的內容碰到 wasm,額外警告(那是用戶會直接抓的東西)
|
||||||
|
if git diff --name-only "${PUBLISHED}..HEAD" 2>/dev/null | grep -q '\.wasm$'; then
|
||||||
|
echo ""
|
||||||
|
echo " 🔴 這批改動**包含 .wasm 變更** → 用戶抓到的零件會跟你本機不同,優先發佈。"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -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
|
||||||
@@ -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 <<EOF
|
||||||
|
❌ BLOCKED by role-guard(規則 $1)
|
||||||
|
|
||||||
|
$2
|
||||||
|
|
||||||
|
身分:role=$ROLE(來源:AGENT_ROLE 環境變數/依安裝位置推定)
|
||||||
|
路徑:$REL
|
||||||
|
|
||||||
|
$3
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── J1/J2:orchestrator 的禁區 ─────────────────────
|
||||||
|
if [ "$ROLE" = "orchestrator" ]; then
|
||||||
|
if is_code_path "$REL"; then
|
||||||
|
block "J1" "總管(PM)不寫 code。" \
|
||||||
|
" 你的工作是決定「要點亮哪幾站、考題是什麼」,不是自己下去寫。
|
||||||
|
|
||||||
|
正確做法:
|
||||||
|
· 把它變成一個掛站號的任務,交給該 repo 的 engineer
|
||||||
|
· 你要的結果寫成考題(Gherkin),不是寫成實作
|
||||||
|
|
||||||
|
為什麼擋:PM 自己下去寫 code,就沒有人在看「零件之間有沒有人掉進縫裡」了
|
||||||
|
——那正是這個角色唯一不能被取代的價值。"
|
||||||
|
fi
|
||||||
|
if is_tech_track_doc "$REL"; then
|
||||||
|
block "J2" "總管(PM)不寫技術軌文件(任務池/規格)。" \
|
||||||
|
" 任務池與規格屬於 engineer。你對它們**只讀**。
|
||||||
|
|
||||||
|
正確做法:
|
||||||
|
· 要調整優先順序 → 改 sprint.md 的指定站,讓認領流程自己去挑任務
|
||||||
|
· 覺得少了任務 → 說出「哪一站的考題會掛」,由 engineer 認領或新增
|
||||||
|
· 要改驗收標準 → 改 journeys.md 的考題(那是你的檔)
|
||||||
|
|
||||||
|
為什麼擋:你一旦動手改任務清單,就等於繞過「先認領→不足才新增」的順序,
|
||||||
|
很快會長出第二份平行的任務池,兩邊不同步。"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── J3:engineer 不准改考卷(命門)─────────────────
|
||||||
|
if [ "$ROLE" = "engineer" ]; then
|
||||||
|
if is_exam_paper "$REL"; then
|
||||||
|
block "J3" "考生不能改考卷。" \
|
||||||
|
" journeys.md/root.md/考題檔是 PM 的檔案,engineer 只能讀當期指定站的段落。
|
||||||
|
|
||||||
|
正確做法:
|
||||||
|
· 考題沒過 → 去把東西做對,不是把題目改掉
|
||||||
|
· 真的認為題目出錯了(判準不合理、站定義有誤)→ **回報 PM**,由他決定改不改
|
||||||
|
· 你需要的是「這站到底要什麼」→ 讀,不要寫
|
||||||
|
|
||||||
|
為什麼這條是命門:考題沒過的人只要改一下考題就「過了」,
|
||||||
|
而且沒有任何人會發現。這樣整套驗收就是裝飾品。"
|
||||||
|
fi
|
||||||
|
# md 檔本身不是考卷,但內容含考題 → 一樣擋(考題常寄住在別的文件裡)
|
||||||
|
case "$REL" in
|
||||||
|
*.md)
|
||||||
|
if content_has_gherkin; then
|
||||||
|
block "J3" "考生不能改考卷(這次寫入的內容含考題)。" \
|
||||||
|
" 這個檔名不是考卷,但你要寫進去的內容裡有 Gherkin 考題(Given/When/Then 或 G-x.y)。
|
||||||
|
|
||||||
|
正確做法:
|
||||||
|
· 要記錄「我做到哪了」→ 寫 wiki/status,不要在文件裡複寫考題
|
||||||
|
· 要提出新考題 → 回報 PM,由他寫進 journeys.md
|
||||||
|
|
||||||
|
為什麼擋:考題散落成兩份就會不同步,
|
||||||
|
而不同步的那一刻起,「全綠」代表什麼就沒人說得準了。"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# PreToolUse hook — 動 code 前檢查有沒有對應 SDD
|
# PreToolUse hook — 動 code 前檢查 SDD + 單一活性 SDD 鐵律(issue #6)
|
||||||
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
|
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
|
||||||
|
# 生命週期規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md
|
||||||
#
|
#
|
||||||
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
||||||
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, ... } }
|
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, ... } }
|
||||||
# 行為:動到 code 檔(.ts/.go/...)但 system-dev/docs/3-specs/ 下沒有任何 SDD → 警告(exit 2 擋)。
|
# 行為:
|
||||||
|
# 1. status: active 的 SDD > 1 份 → 單一活性鐵律已被違反,**不論寫什麼檔**一律擋(exit 2),
|
||||||
|
# 先收斂到一份再說。
|
||||||
|
# 2. 動 code 檔(.ts/.go/...)→ 需要「恰好 1 份」active SDD;0 份 → 擋。
|
||||||
|
# 3. 向下相容:3-specs 下完全沒有任何 design.md 帶 frontmatter(老 repo 尚未遷移生命週期制度)
|
||||||
|
# → 退回舊行為:有 design.md 就放行+提醒,沒有才擋。避免 template update 後老 repo 立刻全紅。
|
||||||
#
|
#
|
||||||
# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。
|
# 誠實限制(本 template 所有 hook 共用的寫法):只擋語法層明顯違規(直接寫 code 檔)。
|
||||||
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
|
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
|
||||||
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
|
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
|
||||||
|
|
||||||
@@ -24,6 +30,42 @@ fi
|
|||||||
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
||||||
[ -z "$FILE_PATH" ] && exit 0
|
[ -z "$FILE_PATH" ] && exit 0
|
||||||
|
|
||||||
|
SPECS_DIR="system-dev/docs/3-specs"
|
||||||
|
|
||||||
|
# ── 統計 active / frontmatter ──────────────────────
|
||||||
|
# 排除 archive/(已封存)與 TEMPLATE(範本自帶 status: draft frontmatter,不算數——
|
||||||
|
# 否則 update 一鋪新版 TEMPLATE-sdd,老 repo 就被誤判「已遷移」而全紅,向下相容破功)。
|
||||||
|
# frontmatter 判定=design.md 前 10 行有 ^status: 行(機器可查,見 SDD-LIFECYCLE.md)。
|
||||||
|
ACTIVE_COUNT=0
|
||||||
|
FM_COUNT=0
|
||||||
|
ACTIVE_LIST=""
|
||||||
|
if [ -d "$SPECS_DIR" ]; then
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -n "$f" ] || continue
|
||||||
|
HEAD10=$(head -10 "$f" 2>/dev/null || true)
|
||||||
|
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*'; then
|
||||||
|
FM_COUNT=$((FM_COUNT + 1))
|
||||||
|
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*active'; then
|
||||||
|
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
||||||
|
ACTIVE_LIST="${ACTIVE_LIST} • ${f}
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 鐵律 1:單一活性被違反(active > 1)→ 不論寫什麼檔一律擋 ──
|
||||||
|
if [ "$ACTIVE_COUNT" -gt 1 ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
🚫 SDD 單一活性鐵律違反:偵測到 ${ACTIVE_COUNT} 份 status: active 的 SDD(任何時刻整個 repo 最多一份):
|
||||||
|
${ACTIVE_LIST}
|
||||||
|
請先收斂到一份:其餘改 status: paused / closed(closed 且被取代者填 superseded_by 並移入 3-specs/archive/)。
|
||||||
|
規則全文見 system-dev/docs/3-specs/SDD-LIFECYCLE.md。收斂前擋下所有寫檔。
|
||||||
|
(本 hook 攔 Write/Edit;修 frontmatter 可用 bash 直改,或由人裁決哪份是現行。)
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
# 只管 code 檔。docs/markdown/設定檔等放行。
|
# 只管 code 檔。docs/markdown/設定檔等放行。
|
||||||
case "$FILE_PATH" in
|
case "$FILE_PATH" in
|
||||||
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;;
|
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;;
|
||||||
@@ -36,28 +78,54 @@ case "$FILE_PATH" in
|
|||||||
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
|
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# system-dev/docs/3-specs/ 下完全沒有 design.md → 攔
|
# ── 向下相容:整個 3-specs 沒有任何帶 frontmatter 的 design.md ──
|
||||||
SDD_COUNT=0
|
# =老 repo 還沒遷移生命週期制度 → 退回舊行為(有 design.md 就放行+提醒),
|
||||||
if [ -d "system-dev/docs/3-specs" ]; then
|
# 避免 template update 一裝新 hook,老 repo 所有 code 寫入立刻全紅。
|
||||||
SDD_COUNT=$(find system-dev/docs/3-specs -name 'design.md' -not -path '*TEMPLATE*' 2>/dev/null | wc -l | tr -d ' ')
|
if [ "$FM_COUNT" -eq 0 ]; then
|
||||||
fi
|
SDD_COUNT=0
|
||||||
|
if [ -d "$SPECS_DIR" ]; then
|
||||||
|
SDD_COUNT=$(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$SDD_COUNT" -eq 0 ]; then
|
if [ "$SDD_COUNT" -eq 0 ]; then
|
||||||
cat >&2 <<EOF
|
cat >&2 <<EOF
|
||||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 system-dev/docs/3-specs/ 下找不到任何 SDD。
|
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下找不到任何 SDD。
|
||||||
|
|
||||||
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md)。
|
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md),且遵守單一活性生命週期
|
||||||
|
(system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||||
|
|
||||||
請先:
|
請先:
|
||||||
1. 確認這個改動屬於哪個子系統
|
1. 確認這個改動屬於哪個子系統
|
||||||
2. 在 system-dev/docs/3-specs/[子系統]/ 建立 design.md(可用 /sdd-check 協助)
|
2. 在 ${SPECS_DIR}/[子系統]/ 建立 design.md(可用 /sdd-check 協助),frontmatter 標 status: active
|
||||||
3. 在回覆開頭宣告已讀 SDD + 對應 task
|
3. 在回覆開頭宣告已讀 SDD + 對應 task
|
||||||
|
|
||||||
|
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 舊行為放行 + 提醒遷移(stderr 警告,不擋)
|
||||||
|
echo "📋 提醒:${SPECS_DIR}/ 有 SDD 但尚未掛生命週期 frontmatter(老結構)。動手前確認已讀對應 design.md;建議依 SDD-LIFECYCLE.md 補 status 標記(現行那份標 active)。" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 新行為:寫 code 檔需「恰好 1 份」active SDD ──
|
||||||
|
if [ "$ACTIVE_COUNT" -eq 0 ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下沒有任何 status: active 的 SDD。
|
||||||
|
|
||||||
|
單一活性鐵律:所有開發任務唯一對應源=那份 active SDD(規則見 system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||||
|
|
||||||
|
請先(擇一,都是人的決定,CC 不得自行建 SDD):
|
||||||
|
1. 把現行規格的 design.md frontmatter 標成 status: active(一份、只能一份)
|
||||||
|
2. 或依 SDD-LIFECYCLE.md 第 3、4 條:proposal 進 pending-changes.md → 使用者 confirm → 開新 SDD 標 active
|
||||||
|
然後在回覆開頭宣告已讀 active SDD + 對應 task。
|
||||||
|
|
||||||
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||||
EOF
|
EOF
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 有 SDD:放行,但留痕提醒要宣告(stderr 警告,不擋)
|
# 恰好 1 份 active:放行,留痕提醒要宣告(stderr 警告,不擋)
|
||||||
echo "📋 提醒:system-dev/docs/3-specs/ 下有 SDD。動手前請確認已讀對應 design.md 並在回覆宣告。" >&2
|
printf '📋 提醒:現行 active SDD=\n%s動手前請確認已讀它的 design.md、對應到 tasks,並在回覆宣告。\n' "$ACTIVE_LIST" >&2
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -26,6 +26,35 @@ if [ -d ".claude/wiki" ] && [ ! -f "$STATUS_FILE" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── profile 分流(1.19.0):總管起牀先看「還缺哪幾站」──────────
|
||||||
|
# 為什麼加這段:自動起牀的排程/新 session 最常見的失敗模式是
|
||||||
|
# 「醒來讀到一堆過期任務,只能空轉收工」。治它的不是更勤勞的提醒,
|
||||||
|
# 是**讓它一睜眼就有明確的『離通關還缺什麼』**——那就是本期指定站。
|
||||||
|
# 成員 repo 不需要這段(它的工作單位是任務不是站),維持原行為。
|
||||||
|
SDT_PROFILE="repo"
|
||||||
|
[ -f "system-dev/.profile" ] && SDT_PROFILE="$(tr -d '[:space:]' < system-dev/.profile 2>/dev/null || echo repo)"
|
||||||
|
|
||||||
|
if [ "$SDT_PROFILE" = "orchestrator" ] && [ -f "system-dev/docs/sprint.md" ]; then
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "🎯 本期要點亮哪幾站(進度語言=站,不是任務數)"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
# 期間、交付、現在幾站亮了——只取前 25 行,不整份灌
|
||||||
|
sed -n '/^## 本期/,/^## 認領流程/p' system-dev/docs/sprint.md 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 的專案
|
# 三個 push 檔都沒有 → 安靜退出,不干擾還沒 /wiki-init 的專案
|
||||||
if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then
|
if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -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
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# subagent-wiki-guard.sh — PreToolUse(Task) hook:subagent 聽到「查」就自己先查 wiki
|
||||||
|
#
|
||||||
|
# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEY,prompt 都只叫它「去查 repo 程式碼」。
|
||||||
|
# agent 於是從**稿子**推論出「這東西還活著、不能動」,總管照單全收去擋 leo 三輪。
|
||||||
|
#
|
||||||
|
# 🔑 設計轉向(leo 2026-07-21):
|
||||||
|
# 第一版是「上游沒交代讀 wiki 就擋下」——但那**還是依賴上游記得寫**,
|
||||||
|
# 跟「我記得讀 wiki」是同一個病。leo 點破:
|
||||||
|
# 「subagent 的問題跟你一樣。你叫它去查,就算你沒說要先查 wiki,
|
||||||
|
# 但它**只要聽到查,就應該主動查 wiki**,因為每個 repo 都有維護自己的 wiki。」
|
||||||
|
# → 改成 **注入式**:不擋、不要求上游改 prompt,直接把「先查 wiki」這條
|
||||||
|
# 以 additionalContext 注入給 subagent,讓它自己做。零依賴任何人記得。
|
||||||
|
#
|
||||||
|
# 行為:偵測到查證/實作類任務 → exit 0 並用 hookSpecificOutput 注入指示。
|
||||||
|
# 已含 wiki 指示、或非查證類任務 → 靜默放行(不重複注入)。
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
|
||||||
|
PROMPT=$(printf '%s' "$INPUT" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
try:
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
print(d.get('tool_input',{}).get('prompt',''))
|
||||||
|
except Exception: print('')
|
||||||
|
" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
[ -z "$PROMPT" ] && exit 0
|
||||||
|
|
||||||
|
# 上游已經交代了 → 不必重複注入
|
||||||
|
if printf '%s' "$PROMPT" | grep -qiE "wiki|agent-memory|mistakes\.md|decisions-summary"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 只對「查證/實作」類任務注入(純寫作、計算、潤稿等不需要)
|
||||||
|
if ! printf '%s' "$PROMPT" | grep -qiE "查|盤點|核實|確認|調查|研究|找出|repo|程式碼|原始碼|source|實作|移除|刪除|重構|修|grep|codebase|\.ts|\.go|src/"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
|
||||||
|
guidance = """【自動注入:查任何東西之前,先查 wiki】
|
||||||
|
|
||||||
|
你所在的 repo 有維護自己的 wiki(通常在 `system-dev/wiki/`,舊結構在 `.claude/wiki/`)。
|
||||||
|
**接到「查/盤點/核實/實作」類任務時,第一個動作是搜尋 wiki,不是翻程式碼。**
|
||||||
|
|
||||||
|
🔴 **查法有強弱之分,一律從最強的開始——沒有那個能力才降級。**
|
||||||
|
(leo 2026-07-21:「它一定是用最好的搜尋,如果沒有才 fallback,
|
||||||
|
但那不是你要指定的,對搜尋者來說,我就是要去搜尋,如果你沒這個機制才降。」)
|
||||||
|
|
||||||
|
**① 語意搜尋(最強,優先)**——有語意檢索 MCP 就用它,用**自然語言問句**,不是關鍵字:
|
||||||
|
kbdb_search(q="<用一句話描述你要找什麼>", mode="semantic")
|
||||||
|
不確定該查哪個庫 → 先 kbdb_get_map() 看藏書地圖
|
||||||
|
要沿關係展開 → kbdb_graph_neighbors()
|
||||||
|
**② 關鍵字搜尋**——語意不可用時:kbdb_search(q="...", mode="keyword")
|
||||||
|
**③ grep(最弱,最後手段)**——連 MCP 都沒有時:
|
||||||
|
grep -rin "<關鍵字>" system-dev/wiki/ 2>/dev/null || grep -rin "<關鍵字>" .claude/wiki/
|
||||||
|
|
||||||
|
🔴 **為什麼順序是硬規定(2026-07-21 實際事故)**:
|
||||||
|
查「CF 上的 git 託管」時只用了 grep,搜 Gitea/freeze/D43 等字面詞 → **零命中**,
|
||||||
|
結論寫成「這件事沒查過、申請表沒送」。
|
||||||
|
事後用**同一個問題**跑語意搜尋,**第一筆就命中**(score 0.858):
|
||||||
|
「Cloudflare Artifacts:假設內建 git 倉庫機制的 CF 功能,成立則可全 CF 化」,
|
||||||
|
還帶出三元組「Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea」——
|
||||||
|
**負責人 15 天前就記在筆記裡了。**
|
||||||
|
→ **grep 只認字面,要求你先猜對那個詞;語意搜尋不需要你猜對。**
|
||||||
|
用 grep 查不到 ≠ wiki 沒記載,只代表你沒猜中用詞。
|
||||||
|
|
||||||
|
🔴 **凡結論涉及「某人沒做某事」,回報前必須先用語意搜尋查該事的記載**——
|
||||||
|
這種結論錯了會變成**指控**,成本遠高於技術判斷錯誤。
|
||||||
|
|
||||||
|
為什麼這是划算的:
|
||||||
|
• wiki 是前人已經查過、驗證過、被負責人糾正過的結論——**判準**。
|
||||||
|
• 程式碼與歷史文件是**稿子**:它反映「還沒清乾淨」,不等於「還在用」。
|
||||||
|
從稿子推論會系統性得出過時結論。
|
||||||
|
• wiki 沒記載,才值得花力氣翻原文。
|
||||||
|
• **凡結論涉及「某人沒做某事」,回報前必須先 grep 該事在 wiki 的記載**——
|
||||||
|
這種結論錯了會變成指控,成本遠高於技術判斷錯誤。
|
||||||
|
|
||||||
|
三條硬規則:
|
||||||
|
1. **wiki 與程式碼衝突 → 以 wiki 為準**,並在回報中明確指出衝突,
|
||||||
|
不要自行用 code 推翻 wiki。
|
||||||
|
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。
|
||||||
|
那是當時狀態,不是永久禁令;條件已滿足就是可動。
|
||||||
|
(2026-07-20 實際事故:agent 只看到「不可動」就回報不能動,
|
||||||
|
實際上解除條件早已滿足,害負責人被擋三輪。)
|
||||||
|
3. 翻原文後若得到**新結論**,回報時明講「wiki 該更新」——wiki 過時是債,要還。
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(json.dumps({
|
||||||
|
"hookSpecificOutput": {
|
||||||
|
"hookEventName": "PreToolUse",
|
||||||
|
"additionalContext": guidance
|
||||||
|
}
|
||||||
|
}, ensure_ascii=False))
|
||||||
|
PY
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+112
@@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# wiki-first-search.sh — PreToolUse hook:要去翻原文/程式碼前,先把 wiki 命中結果推到眼前
|
||||||
|
#
|
||||||
|
# 病根(2026-07-20 leo 點破,mistakes 第一鐵律):
|
||||||
|
# 總管 session 開頭讀了 agent-memory 前 50 行就開工,關鍵那條在第 56 行 → 拿過期記憶擋了 leo 三輪。
|
||||||
|
# leo:「如果你不是讀而是**搜尋** wiki 就不會只讀 50 行就下定論,
|
||||||
|
# 而是就像我直接在頁面 cmd+F,那些都會高亮。」
|
||||||
|
#
|
||||||
|
# 設計要點(為什麼是這個形狀):
|
||||||
|
# 1. **搜尋 ≠ 通讀**:開場 push 全文(session-start-recall.sh)解決不了這題——量大必然只讀開頭。
|
||||||
|
# 這支反過來:在「你正要去查 code/原文」的當下,用你自己的關鍵字 grep wiki,只推命中行。
|
||||||
|
# 2. **時機是關鍵**:不是開場推、不是寫入時擋,而是**查詢動作發生的那一刻**介入。
|
||||||
|
# 3. **提醒不阻擋**(exit 0):wiki 沒記載時本來就該去翻原文,擋下來反而礙事。
|
||||||
|
# 唯一目的是消滅「不知道 wiki 有寫」這件事。
|
||||||
|
#
|
||||||
|
# 觸發:Grep / Glob / Read 打向 code 或 docs 時(見下方 should_check)。
|
||||||
|
# 輸出:stdout 注入 context(命中的 wiki 行 + 檔名:行號)。
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INPUT=$(cat)
|
||||||
|
TOOL=$(printf '%s' "$INPUT" | python3 -c "import json,sys;print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# 取出這次查詢的關鍵字:Grep 用 pattern,Glob/Read 用路徑的檔名部分
|
||||||
|
QUERY=$(printf '%s' "$INPUT" | python3 -c "
|
||||||
|
import json,sys,os,re
|
||||||
|
try:
|
||||||
|
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||||
|
q = ti.get('pattern') or ''
|
||||||
|
if not q:
|
||||||
|
p = ti.get('file_path') or ti.get('path') or ''
|
||||||
|
q = os.path.splitext(os.path.basename(p))[0] if p else ''
|
||||||
|
if not q:
|
||||||
|
# Bash:2026-07-21 補的破口——原版只掛 Grep|Glob|Read,
|
||||||
|
# 但「用 curl/wrangler 亂試部署方法」走的是 Bash,整支 hook 不觸發。
|
||||||
|
# leo 當場點破:wiki 早記著「寄信已驗證可用」,我卻沒查又自創方法。
|
||||||
|
# 只認「會動到外部系統/部署」的高風險指令,避免每個 ls 都洗版。
|
||||||
|
cmd = ti.get('command') or ''
|
||||||
|
if re.search(r'\b(wrangler|curl|npx|acr|gh|deploy|push)\b', cmd):
|
||||||
|
# 取指令中最具識別度的詞(worker 名/資源名/子命令)當搜尋詞
|
||||||
|
cand = re.findall(r'[A-Za-z_][A-Za-z0-9_-]{4,}', cmd)
|
||||||
|
skip = {'https','http','client','accounts','workers','scripts',
|
||||||
|
'application','content','Authorization','Bearer','python3',
|
||||||
|
'curl','npx','bash','echo','grep','local','branch','origin'}
|
||||||
|
cand = [c for c in cand if c not in skip and not c.startswith('-')]
|
||||||
|
q = max(cand, key=len) if cand else ''
|
||||||
|
# grep pattern 常含 regex 元字元;取最長的英數/底線詞當搜尋詞
|
||||||
|
words = re.findall(r'[A-Za-z_][A-Za-z0-9_]{3,}', q)
|
||||||
|
print(max(words, key=len) if words else '')
|
||||||
|
except Exception:
|
||||||
|
print('')
|
||||||
|
" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
[ -z "$QUERY" ] && exit 0
|
||||||
|
|
||||||
|
WIKI_DIR="system-dev/wiki"
|
||||||
|
[ -d "$WIKI_DIR" ] || exit 0
|
||||||
|
|
||||||
|
# 只在「查程式碼/文件」時提醒;查 wiki 本身就不用了(已經在讀了)
|
||||||
|
TARGET=$(printf '%s' "$INPUT" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
try:
|
||||||
|
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||||
|
print(ti.get('file_path') or ti.get('path') or '')
|
||||||
|
except Exception: print('')
|
||||||
|
" 2>/dev/null || echo "")
|
||||||
|
case "$TARGET" in
|
||||||
|
*system-dev/wiki*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# grep wiki(不分大小寫、含行號),最多 12 行避免洗版
|
||||||
|
HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -12 || true)
|
||||||
|
|
||||||
|
# 🔴 grep 零命中時**不能靜默退出**——那正是今天失敗的模式(2026-07-21):
|
||||||
|
# grep 查不到 → 以為 wiki 沒記載 → 結論「這件事沒查過」。
|
||||||
|
# 但 grep 只認字面,查不到往往只代表「沒猜中用詞」。
|
||||||
|
# → 零命中反而是**最該改用語意搜尋**的時刻,必須出聲。
|
||||||
|
if [ -z "$HITS" ]; then
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
printf '🔍 grep 在 wiki 找不到「%s」——但這**不代表沒記載**\n' "$QUERY"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
echo "grep 只認字面,查不到通常只是「沒猜中用詞」。**改用語意搜尋再確認一次**:"
|
||||||
|
echo " kbdb_search(q=\"<用一句話描述你要找什麼>\", mode=\"semantic\")"
|
||||||
|
echo " 不知道該查哪個庫 → kbdb_get_map()|要沿關係展開 → kbdb_graph_neighbors()"
|
||||||
|
echo ""
|
||||||
|
echo "實例:查「CF 上的 git 託管」時 grep 全零命中,語意搜尋第一筆就命中"
|
||||||
|
echo "(Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea,負責人 15 天前就記了)。"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
COUNT=$(printf '%s\n' "$HITS" | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
printf '📚 wiki 已有「%s」的記載(%s 處,先看這裡再翻原文)\n' "$QUERY" "$COUNT"
|
||||||
|
echo "════════════════════════════════════════════════"
|
||||||
|
printf '%s\n' "$HITS" | sed 's|^system-dev/wiki/| |'
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ wiki 是判準,程式碼與歷史文件只是稿子(mistakes 第一鐵律)。"
|
||||||
|
echo " • 上面若與你將要查的原文衝突 → **以 wiki 為準**,別用 code 推翻 wiki。"
|
||||||
|
echo " • 看到「不可動/待廢除/進行中」→ 先讀它的**解除條件**並逐條核對,"
|
||||||
|
echo " 那是當時狀態不是永久禁令;條件已滿足就是可動。"
|
||||||
|
echo " • wiki 沒答案才值得翻原文——翻完若得到新結論,**回頭更新 wiki**。"
|
||||||
|
echo ""
|
||||||
|
echo "🔎 以上是 **grep(最弱的查法)** 的結果,只認字面,且搜尋詞是從你的指令**猜**出來的"
|
||||||
|
echo " (很可能太籠統而命中一堆無關的,同時漏掉真正的主題詞)。"
|
||||||
|
echo " **重要判斷一律補一次語意搜尋**——它不需要你猜對用詞:"
|
||||||
|
echo " kbdb_search(q=\"<一句話描述你要找什麼>\", mode=\"semantic\")"
|
||||||
|
echo " 實例:查「CF 的 git 託管」時 grep 猜到的詞是 cloudflare → 命中 12 處全無關、"
|
||||||
|
echo " 真正的答案(Artifacts)一筆沒撈到;語意搜尋第一筆就命中。"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -9,6 +9,15 @@
|
|||||||
"command": ".claude/hooks/session-start-recall.sh"
|
"command": ".claude/hooks/session-start-recall.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "startup|resume|clear",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/publish-lag-check.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"PreToolUse": [
|
"PreToolUse": [
|
||||||
@@ -28,7 +37,25 @@
|
|||||||
"command": ".claude/hooks/wiki-secret-scan.sh"
|
"command": ".claude/hooks/wiki-secret-scan.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Grep|Glob|Read|Bash",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Task",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+37
-3
@@ -1,3 +1,11 @@
|
|||||||
|
<!-- ⚠️ 相容凍結檔(1.19.0 起)
|
||||||
|
這份是「雙 profile」之前的舊憲法範本。**現役版本在 template/profiles/repo/CLAUDE.md**。
|
||||||
|
它留在這個路徑不動,唯一理由是向下相容:1.18.x 以前的實例跑的是**舊的** install.sh/
|
||||||
|
update.sh,路徑寫死指向這裡;一旦搬走,那些實例整排 404 ⇒ 更新機制本身壞掉,
|
||||||
|
而且不會有下一次更新來修它(1.16.0 已被同型問題咬過一次)。
|
||||||
|
機械保證:scripts/check-legacy-paths.sh。
|
||||||
|
要改憲法內容 → 改 profiles/repo/CLAUDE.md,不要改這裡。 -->
|
||||||
|
|
||||||
# CLAUDE.md — [專案名稱]
|
# CLAUDE.md — [專案名稱]
|
||||||
|
|
||||||
> 導航牌。細節在兩個地方,不在這裡。
|
> 導航牌。細節在兩個地方,不在這裡。
|
||||||
@@ -7,12 +15,15 @@
|
|||||||
|
|
||||||
## 絕對鐵律(違反 = 停手)
|
## 絕對鐵律(違反 = 停手)
|
||||||
|
|
||||||
1. **任何 code 變動前必須有對應 SDD**(`system-dev/docs/3-specs/[子系統]/design.md`)
|
1. **任何 code 變動前必須有對應 SDD**,且遵守 **SDD 生命週期鐵律**(全文:`system-dev/docs/3-specs/SDD-LIFECYCLE.md`):
|
||||||
|
- **單一活性**:任何時刻只有一份 `status: active` 的 SDD,所有任務對應它的 tasks
|
||||||
|
- **禁止自行建立 SDD**:找不到對應 → 停手問 [負責人]
|
||||||
|
- **規格層變更**:proposal 寫進 `3-specs/pending-changes.md`,等使用者「confirm」才動
|
||||||
|
- **開新 SDD**(confirm 後):先把舊 SDD 未完成任務搬進新 SDD,才准寫 code
|
||||||
|
- **session 開始**回報:「現行規格〈名稱〉+未完成任務 N+待裁決 proposal M」
|
||||||
2. [技術棧限制,例如:前端只用 React,不引入其他框架]
|
2. [技術棧限制,例如:前端只用 React,不引入其他框架]
|
||||||
3. [其他專案特定限制]
|
3. [其他專案特定限制]
|
||||||
|
|
||||||
找不到對應 SDD → **停手問 [負責人]**,不要自行建立。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 工作流程(強制)
|
## 工作流程(強制)
|
||||||
@@ -31,6 +42,29 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔴 第一鐵律:wiki 是判準,不准跳過(2026-07-20/21 leo 兩度點破)
|
||||||
|
|
||||||
|
**要查任何東西之前,先搜尋 wiki——用 grep,不是只讀開頭幾行。**
|
||||||
|
|
||||||
|
> leo:「花很多力氣去產生 wiki,最重要的就是要可以查詢,**結果要查的時候就跳過,那就白寫了**。」
|
||||||
|
> 「重點是你自己的記憶對嗎?而你有按照規定去切實讀 wiki 嗎?」
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rin "<本題關鍵字>" system-dev/wiki/
|
||||||
|
```
|
||||||
|
|
||||||
|
**三條硬規則**:
|
||||||
|
1. **wiki 與程式碼/歷史文件衝突 → 以 wiki 為準**。程式碼反映「還沒清乾淨」,不等於「還在用」。
|
||||||
|
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。那是當時狀態,不是永久禁令。
|
||||||
|
3. 翻原文後得到新結論 → **回頭更新 wiki**(wiki 過時是債,要還)。
|
||||||
|
|
||||||
|
**動外部系統(部署/curl/wrangler/acr/gh)前**:先找 repo 有沒有**現成腳本或 README 部署段**,
|
||||||
|
別自創方法。(實例:2026-07-21 明明有 `npx wrangler deploy` 這條驗過的路,卻自己 curl 硬幹踩坑。)
|
||||||
|
|
||||||
|
> hook `wiki-first-search.sh` 會在你查 code/下高風險指令時自動推 wiki 命中行;
|
||||||
|
> **但機制只是提醒,判斷是你的責任**。
|
||||||
|
|
||||||
|
|
||||||
## Wiki 讀取順序
|
## Wiki 讀取順序
|
||||||
|
|
||||||
| 檔案 | 時機 | 用途 |
|
| 檔案 | 時機 | 用途 |
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 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.<ext>
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
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
|
||||||
|
@@ -0,0 +1,22 @@
|
|||||||
|
# orchestrator.tsv — 「總管」profile 專屬產物
|
||||||
|
#
|
||||||
|
# 這個 profile 是誰:管一群成員 repo 的上層資料夾(PM/總管)。它的憲法效忠
|
||||||
|
# root.md + journeys.md(PM 軌),裡面的 agent 預設身分是 orchestrator
|
||||||
|
# (可寫 root/journeys/sprint,**禁寫 code、禁改 tasks/requirements/design**)。
|
||||||
|
#
|
||||||
|
# ⚠️ 四份 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
|
||||||
|
@@ -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
|
||||||
|
@@ -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 會把該政策的必讀推到這裡。沒裝政策包時這段是空的。
|
||||||
|
|
||||||
|
<!-- policy-pack:must-read -->
|
||||||
@@ -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 通關再立——規矩是「**需要新站才提案新站**」,
|
||||||
|
不是先把表格畫滿。
|
||||||
|
- [其他你知道還缺、但這一版先不做的東西。寫出來,別讓下一個人以為這卷是完整的。]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- ════════ 寫這份文件的規矩(給總管看,不是內容的一部分)════════
|
||||||
|
|
||||||
|
【三層結構】角色(A)→ 旅程(J)→ 站(S)→ 考題(G)
|
||||||
|
與技術軌的 Epic → Story → EARS 對稱,但**切法正交**:
|
||||||
|
技術軌沿系統結構切(保證每個零件是對的),
|
||||||
|
PM 軌沿人的經歷切(保證零件之間沒有人掉進縫裡)。
|
||||||
|
|
||||||
|
【站的編號是全域的】
|
||||||
|
S 不隸屬於某條 J。同一個能力被兩條旅程經過,就是同一個 S、同一組考題。
|
||||||
|
重複出現只寫「S1 →(引用,見 J-1)」,**不重抄**——抄第二份就會有兩份不同步的考題。
|
||||||
|
|
||||||
|
【Gherkin 的 Then 只能寫使用者感受得到的事】
|
||||||
|
✅ 「我在信箱收到一組號碼」「我看得到它是從我哪個檔案來的」
|
||||||
|
❌ 「回傳 200」「部署成功」「資料寫入成功」
|
||||||
|
這條就是「HTTP 200 不算驗過」的正式化——技術上通了但使用者沒感覺到,等於沒通。
|
||||||
|
|
||||||
|
【紅站的對帳行寫在站上,不寫進考題】
|
||||||
|
法條(Gherkin)管「做到沒」,對帳管「賭對沒」,物理分離。
|
||||||
|
🔴 站缺對帳行 = 格式錯誤。
|
||||||
|
|
||||||
|
【定 Journey 之前】
|
||||||
|
先寫一篇該角色的**敘事故事**給人讀一次,驗「完整性」(讀完找不找得到斷裂)。
|
||||||
|
驗完歸檔到 docs/archive/stories/,不進日常維護。
|
||||||
|
|
||||||
|
【誰能寫】
|
||||||
|
只有 orchestrator。engineer/subagent 寫這個檔會被 hook 擋下——考生不能改考卷。
|
||||||
|
考題不過就去把東西做對,不是去改考題。
|
||||||
|
════════════════════════════════════════════════════════ -->
|
||||||
@@ -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)
|
||||||
|
<!-- policy-pack:must-read -->
|
||||||
|
```
|
||||||
|
|
||||||
|
- 沒裝政策包時它是空的,不影響任何事。
|
||||||
|
- 框架**不需要為此新造機制**——官方 hook 就做得到。
|
||||||
@@ -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 推測補完、而非本人明示的內容,**必須帶「請確認」類標記**,不得混充原意。
|
||||||
|
- 紅卡的對帳日到了,會拿真實數據來對帳:**承諾成立,或換一個承諾**。賭錯不丟臉,賭了不認才是。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- ════════ 寫這份文件的規矩(給總管看,不是內容的一部分)════════
|
||||||
|
|
||||||
|
【卡片格式】
|
||||||
|
- **P<編號>** <🟢或🔴> <一句白話>。
|
||||||
|
- ——(來源標記) ← 🟢 卡用這行
|
||||||
|
- 【要驗證:<判準> | 對帳日 <日期>】 ← 🔴 卡改用這行,**缺了就是格式錯誤,會被擋**
|
||||||
|
|
||||||
|
【來源標記怎麼寫】
|
||||||
|
常見形式:(你說的)(你舉的例子:X)(從你的抱怨反推:X)(我猜的,請確認)
|
||||||
|
形式不限於此,可依實際來源自由描述。
|
||||||
|
唯一鐵律:**凡是推測補完、而非本人明示的內容,必須帶「請確認」類標記。**
|
||||||
|
|
||||||
|
【禁止技術名詞】
|
||||||
|
不准出現:API、DB、資料庫、WASM、MCP、endpoint、schema、SDK、CLI…
|
||||||
|
技術是達成手段,寫進各專案自己的規格,不寫進根。
|
||||||
|
⚠️ 自檢只掃**卡片本體**(`- **P...` 開頭那些行及其子項),
|
||||||
|
不掃這段說明區與 <!-- --> 註解——否則你在解釋規矩時提到的詞會被自己抓到。
|
||||||
|
|
||||||
|
【紅綠怎麼判】
|
||||||
|
能指著市場上活得好好的先例說「照這個做」=🟢(輪子卡,考過即關)
|
||||||
|
不能、解法是猜的=🔴(賭注卡,考過轉「對帳中」,等對帳日拿真實數據判決)
|
||||||
|
|
||||||
|
【誰能寫】
|
||||||
|
只有 orchestrator。engineer/subagent 寫這個檔會被 hook 擋下——考生不能改考卷。
|
||||||
|
════════════════════════════════════════════════════════ -->
|
||||||
@@ -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 標記,附**實測證據**
|
||||||
|
- 沒亮的站 → **搬移**到下一期,並寫一行「為什麼沒亮」
|
||||||
|
- 沒人認領也沒亮的任務 → **作廢或退回池子**,不准留在這裡假裝還活著
|
||||||
|
- 狀態只有三種:**✅ 通(附實測證據)/◐ 半通(標明缺什麼)/❌ 斷**
|
||||||
|
- 「程式碼寫完了」不是狀態
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# triage-map.md — 能力域 → 誰做
|
||||||
|
|
||||||
|
> 新需求進來時查這張表:這件事該落在哪個成員 repo。
|
||||||
|
> **表上沒有 ⇒ 不准自己開新 repo**,走人閘提案。
|
||||||
|
> **誰能寫**:只有總管(orchestrator)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 對照
|
||||||
|
|
||||||
|
- **[能力域名稱]**
|
||||||
|
- 落在:`[成員 repo 路徑]`
|
||||||
|
- 邊界:[什麼算它的、什麼不算——寫清楚才不會兩個 repo 搶同一件事或互推]
|
||||||
|
- **[能力域名稱]**
|
||||||
|
- 落在:`[成員 repo 路徑]`
|
||||||
|
- 邊界:[...]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分診的第二問(別漏)
|
||||||
|
|
||||||
|
查完「誰做」,還要問**這個需求動到哪條旅程的哪些站**:
|
||||||
|
|
||||||
|
- 動到既有的站 → 查 journeys.md 的**站點索引**,決定重考範圍
|
||||||
|
- 需要新的站 → 走 journeys.md 提案(只有總管能寫)
|
||||||
|
- 連角色都是新的 → **先寫一篇敘事故事**給人驗完整性,再立新旅程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 表上沒有的怎麼辦
|
||||||
|
|
||||||
|
- 先確認真的沒有——多數「新能力」其實是既有能力域的延伸,硬開新 repo 只是把邊界問題往後推。
|
||||||
|
- 真的沒有 → 提案開新 repo,**這是人閘**(動到大家共用的結構)。
|
||||||
|
- 提案要寫:這個能力域的邊界在哪、為什麼塞不進既有任何一個、誰維護。
|
||||||
@@ -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 <<EOF
|
||||||
|
❌ BLOCKED by orchestrator-scope-guard(防糾纏閘 S4)
|
||||||
|
|
||||||
|
總管不進成員 repo 動實作。
|
||||||
|
|
||||||
|
成員 repo:$top
|
||||||
|
路徑:$REL
|
||||||
|
|
||||||
|
正確做法:
|
||||||
|
· 要那個 repo 改什麼 → 在**那個 repo** 開一張交辦(.md 放行),
|
||||||
|
給判準與考題,成品由該 repo 的 CC 按它自己的規格產出
|
||||||
|
· 你能派 subagent 去做 → 那才是正解,不是自己下去改,也不是叫人去開另一個 session
|
||||||
|
· 真要放行 subagent 自動寫某個 repo → 把路徑加進 system-dev/.autodispatch-allow
|
||||||
|
(⚠️ 加=放權,想清楚它影響誰)
|
||||||
|
|
||||||
|
為什麼擋:你直接改人家的實作,該 repo 的 CC 完全不知道發生過什麼事——
|
||||||
|
它的規格、wiki、驗收全部被繞過,下次它照自己的紀律工作時就會把你的改動蓋掉。
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
@@ -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:<編號>|服務站號:<S-n>
|
||||||
|
🚧 執行範圍:<會動哪些檔案>
|
||||||
|
```
|
||||||
|
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 在庫裡就會觸發。你照常工作即可。
|
||||||
|
|
||||||
|
<!-- policy-pack:must-read -->
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# sdd-active-check.sh — 單一活性 SDD 獨立硬約束(SDD 生命週期鐵律,issue #6)
|
||||||
|
# 規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md
|
||||||
|
#
|
||||||
|
# 用法:bash sdd-active-check.sh [specs目錄]
|
||||||
|
# 參數 1(可選)=specs 目錄,預設 system-dev/docs/3-specs
|
||||||
|
#
|
||||||
|
# 行為:統計 status: active 的 design.md(design.md 前 10 行有 ^status: active,
|
||||||
|
# 排除 archive/ 與 TEMPLATE)——
|
||||||
|
# >1 份 → stderr 列出清單,exit 1(違反單一活性)
|
||||||
|
# ≤1 份 → exit 0
|
||||||
|
#
|
||||||
|
# pre-commit 掛法(.git/hooks/pre-commit,記得 chmod +x):
|
||||||
|
# #!/bin/sh
|
||||||
|
# bash system-dev/scripts/sdd-active-check.sh || exit 1
|
||||||
|
# CI 也是同一行,違反即紅。
|
||||||
|
#
|
||||||
|
# 誠實限制:與 sdd-guard.sh 同精神——只做語法層機械檢查,繞道可行但留痕可審,
|
||||||
|
# 不聲稱不可繞過。價值是「不變量被違反時一定有機器出聲」。
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SPECS_DIR="${1:-system-dev/docs/3-specs}"
|
||||||
|
|
||||||
|
# 沒有 specs 目錄(沒裝 SDD 模組)→ 無事可查,放行
|
||||||
|
[ -d "$SPECS_DIR" ] || exit 0
|
||||||
|
|
||||||
|
ACTIVE_COUNT=0
|
||||||
|
ACTIVE_LIST=""
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -n "$f" ] || continue
|
||||||
|
if head -10 "$f" 2>/dev/null | grep -q '^status:[[:space:]]*active'; then
|
||||||
|
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
||||||
|
ACTIVE_LIST="${ACTIVE_LIST} • ${f}
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
done < <(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$ACTIVE_COUNT" -gt 1 ]; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
🚫 SDD 單一活性鐵律違反:${SPECS_DIR}/ 下有 ${ACTIVE_COUNT} 份 status: active 的 SDD(任何時刻最多一份):
|
||||||
|
${ACTIVE_LIST}
|
||||||
|
請收斂到一份:其餘改 status: paused / closed(closed 且被取代者填 superseded_by 並移入 archive/)。
|
||||||
|
規則見 system-dev/docs/3-specs/SDD-LIFECYCLE.md。
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1 +1 @@
|
|||||||
1.13.0
|
1.19.0
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# SDD 生命週期鐵律(不可違反)
|
||||||
|
|
||||||
|
> 來源:leo 2026-07-17 拍板。
|
||||||
|
> 適用:`system-dev/docs/3-specs/` 下的「規格 SDD」(requirements/design/tasks 三件式資料夾)。
|
||||||
|
> **不適用**:派工表/sprint 檔、journeys/ 卷宗、TEMPLATE-sdd、README、pending-changes.md——它們不是 SDD,不掛 status。
|
||||||
|
|
||||||
|
## 狀態標記(機器可查)
|
||||||
|
|
||||||
|
每個 SDD 資料夾的 `design.md` 最上方掛 YAML frontmatter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
status: active # active | draft | paused | closed
|
||||||
|
superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- `active`:現行規格,全 repo 開發任務唯一對應源。**任何時刻整個 repo 最多一份。**
|
||||||
|
- `draft`:起草中,尚未採納。
|
||||||
|
- `paused`:動過工、暫停中;恢復=升回 active(先收掉現任 active)或被新 SDD 繼承。
|
||||||
|
- `closed`:已完成或被取代;被取代者填 `superseded_by` 並移入 `3-specs/archive/`。
|
||||||
|
|
||||||
|
## 五條鐵律
|
||||||
|
|
||||||
|
1. **單一活性**:任何時刻只允許一份 `status: active`。所有開發任務必須對應這份 SDD 的 tasks。找不到對應任務 → 停下來問,不准直接做。
|
||||||
|
2. **禁止自行建立 SDD**:CC 在任何情況下不得主動建新 SDD。收到使用者意見先分類:澄清問題→回答即可不動文件;任務層變更(不影響核心設計)→更新現行 SDD 的 tasks 區段並標日期與原因;規格層變更(核心設計/方向改變)→走第 3 條,不准直接改 spec。
|
||||||
|
3. **規格變更只有一條路**:產出 change proposal 寫入 `system-dev/docs/3-specs/pending-changes.md`(變更摘要與觸發原因+影響分析:現行 SDD 哪些任務作廢/修改/不受影響/尚未完成),然後**停止**,等使用者明說「confirm」。沒 confirm 就繼續依現行 SDD 工作。多個 proposal 可並存緩衝區、由人一次裁決——CC 的速度導向影響分析,不是規格增生。
|
||||||
|
4. **開新 SDD 的唯一時機**:使用者 confirm 一份規格層 proposal 時,依序:
|
||||||
|
a. 舊 SDD 未完成且仍有效的任務**逐條搬入**新 SDD 的 tasks——**這步做完前不准寫任何程式碼**(強迫顯式盤點,遺漏會在 d 的清單被看到,而不是三天後才發現)。
|
||||||
|
b. 舊 SDD frontmatter 改 `status: closed, superseded_by: <新SDD>`,資料夾移入 `3-specs/archive/`。
|
||||||
|
c. 新 SDD 的 changelog 首行記錄:繼承自哪份、為何取代。
|
||||||
|
d. 向使用者列出「已搬移任務清單」與「已作廢任務清單」請求最終確認。
|
||||||
|
5. **每次 session 開始**:先讀現行 active SDD 與 pending-changes.md,回報三個數字——「現行規格〈名稱〉+未完成任務 N+待裁決 proposal M」——再開始工作。若回報出現兩份 active=規則已被違反,當場糾正。
|
||||||
|
|
||||||
|
## 硬約束(不信任單點自律,用結構保證不變量)
|
||||||
|
|
||||||
|
- `.claude/hooks/sdd-guard.sh`(PreToolUse Write|Edit):active 數 >1 → 任何寫檔一律擋;寫 code 檔需恰好 1 份 active。
|
||||||
|
- `scripts/sdd-active-check.sh`:獨立檢查,pre-commit / CI 可掛,違反 exit 1。
|
||||||
|
- 誠實限制:hook 只擋語法層明顯違規,繞道可行但留痕可審;不聲稱不可繞過。
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
|
---
|
||||||
|
status: draft # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md)
|
||||||
|
superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名
|
||||||
|
---
|
||||||
|
|
||||||
# [子系統名稱] — Design
|
# [子系統名稱] — Design
|
||||||
|
|
||||||
> 狀態:[草稿 / 審核中 / 已採納 / 已廢棄]
|
|
||||||
> 建立:[YYYY-MM-DD] | 最後更新:[YYYY-MM-DD]
|
> 建立:[YYYY-MM-DD] | 最後更新:[YYYY-MM-DD]
|
||||||
> 負責人:[名稱]
|
> 負責人:[名稱]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Pending Changes(規格變更緩衝區)
|
||||||
|
|
||||||
|
> 規則來源:`SDD-LIFECYCLE.md` 第 3、4 條。
|
||||||
|
> 規格層變更(核心設計/方向改變)**只有這一條路**:CC 把 change proposal 寫進「待裁決」——
|
||||||
|
> 變更摘要與觸發原因+影響分析(現行 SDD 哪些任務作廢/修改/不受影響/尚未完成)——然後**停止**,
|
||||||
|
> 等使用者明說「confirm」才依第 4 條開新 SDD;沒 confirm 就繼續依現行 SDD 工作。
|
||||||
|
> 多個 proposal 可並存,由人一次裁決。本檔不是 SDD,不掛 status。
|
||||||
|
|
||||||
|
## 待裁決
|
||||||
|
|
||||||
|
(無)
|
||||||
|
|
||||||
|
## 已裁決
|
||||||
|
|
||||||
|
(無——裁決後從「待裁決」移到這裡留底,標 confirmed / rejected + 日期。)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Logseq 任務 marker 解析(單一真相源)
|
||||||
|
|
||||||
|
> **這是「Logseq 原生任務語法」解析的唯一權威規格。** 任何要從 Logseq graph
|
||||||
|
> 抓任務狀態的功能,一律 import 這份、不得各寫一份自己的 mapping。
|
||||||
|
>
|
||||||
|
> **已知兩個消費者**(共用同一套解析,見各自 issue):
|
||||||
|
> 1. **vault 萃取**(`/wiki-extract`,template#5):marker → 卡片 frontmatter `task_status`。
|
||||||
|
> 2. **tasks→Project 投影**(`system-dev/workflows/tasks-project-sync.*`,template#4):
|
||||||
|
> 當投影來源是 Logseq graph(notes/kb)時,用這份判斷任務與狀態。
|
||||||
|
>
|
||||||
|
> 兩者**只共用「怎麼 parse」**(哪幾行是任務、marker 是什麼、正規狀態是什麼、跳過什麼);
|
||||||
|
> parse 完各自要「拿狀態做什麼」(寫卡 vs 投影 issue)不同,那部分各管各的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 為什麼不是 GFM checkbox(規格更正,leo 2026-07-04 發現)
|
||||||
|
|
||||||
|
Logseq 的原生任務**不是** GFM 的 `- [ ]` / `- [x]`,而是**大寫 marker 開頭的 block**:
|
||||||
|
|
||||||
|
```
|
||||||
|
- TODO AI 盤點這台機器上所有 Repo,把本地 Repo 搬到託管站
|
||||||
|
- DOING 建立知識總庫,可查所有子庫
|
||||||
|
- DONE 手機和電腦 Logseq 可以被放進知識總庫
|
||||||
|
```
|
||||||
|
|
||||||
|
若照舊規格只抓 `- [ ]` checkbox,**notes / kb 兩個 Logseq graph 的任務會全數漏抓**。
|
||||||
|
|
||||||
|
> 兩種源、兩套語法、同一條下游管線:
|
||||||
|
> - **SDD `tasks.md`**(各 repo `system-dev/docs/3-specs/**`)→ GFM checkbox(現行不變)。
|
||||||
|
> - **Logseq graph(notes / kb)** → 本檔的大寫 marker。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 解析規格
|
||||||
|
|
||||||
|
### 1. 任務行辨識(regex)
|
||||||
|
|
||||||
|
```
|
||||||
|
^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+
|
||||||
|
```
|
||||||
|
|
||||||
|
- marker 必須是 block(`-` bullet)的**開頭第一個 token**、全大寫、後接空白。
|
||||||
|
- `CANCELED` 與英式 `CANCELLED` 皆收(Logseq 兩種都產)。
|
||||||
|
- marker 後面到行尾(或到子 bullet 之前)是**任務內文**。
|
||||||
|
|
||||||
|
### 2. marker → 正規狀態(task_status)
|
||||||
|
|
||||||
|
| Logseq marker | 正規 task_status |
|
||||||
|
|---------------|------------------|
|
||||||
|
| `TODO`、`LATER` | `todo` |
|
||||||
|
| `DOING`、`NOW` | `in-progress` |
|
||||||
|
| `WAITING` | `blocked` |
|
||||||
|
| `DONE` | `done` |
|
||||||
|
| `CANCELED`、`CANCELLED` | `closed` |
|
||||||
|
|
||||||
|
> `LATER`/`NOW` 是 Logseq「排程視圖」用的同義 marker(LATER≈TODO、NOW≈DOING),
|
||||||
|
> 正規化後與 TODO/DOING 併軌,下游不必區分。
|
||||||
|
|
||||||
|
### 3. 必須跳過的東西(別當任務內文)
|
||||||
|
|
||||||
|
Logseq 的任務 block 底下常掛時間戳與屬性行,這些**不是內文**,解析時整段略過:
|
||||||
|
|
||||||
|
- **`:LOGBOOK:` … `:END:` 區塊**:marker 被點擊計時產生的時間戳紀錄。
|
||||||
|
遇到 `:LOGBOOK:` 那行起、到 `:END:` 那行止(含兩端),整塊丟掉。
|
||||||
|
- **屬性行 `key:: value`**:如 `collapsed:: true`、`id:: 65a...`、`SCHEDULED:: <...>`、
|
||||||
|
`DEADLINE:: <...>`。凡符合 `^\s*[\w-]+:: ` 的行都是屬性,不是內文。
|
||||||
|
(`SCHEDULED`/`DEADLINE` 的日期若下游要用可另抓,但**不得當任務描述文字**。)
|
||||||
|
|
||||||
|
### 4. 巢狀子 bullet
|
||||||
|
|
||||||
|
任務 block 底下縮排的子 bullet 是該任務的補充說明(非獨立任務,除非子 bullet 自己也帶 marker)。
|
||||||
|
萃取時可併入該任務的描述脈絡;投影時只取母 block 那行當任務標題。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自檢(實作或 LLM 執行前跑一遍)
|
||||||
|
|
||||||
|
- [ ] 用的是大寫 marker regex,**不是** `- [ ]` checkbox。
|
||||||
|
- [ ] 八個 marker 全部覆蓋(含 `LATER`/`NOW`/`CANCELLED` 別漏)。
|
||||||
|
- [ ] `:LOGBOOK:...:END:` 與 `key:: value` 屬性行有跳過,沒混進任務文字。
|
||||||
|
- [ ] 狀態用上表**正規名**(`todo`/`in-progress`/`blocked`/`done`/`closed`),不是原始 marker 字面。
|
||||||
@@ -46,7 +46,7 @@ CC 動手前必須有這兩個檔案。找不到就停手。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## .claude/wiki/ — CC 的記憶空間(CC 維護,人不手動編輯)
|
## system-dev/wiki/ — CC 的記憶空間(CC 維護,人不手動編輯)
|
||||||
|
|
||||||
| 檔案 | 用途 | 更新時機 |
|
| 檔案 | 用途 | 更新時機 |
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
# <!-- wiki:ignore -->
|
# <!-- wiki:ignore -->
|
||||||
# 這幾行不會被編入 wiki
|
# 這幾行不會被編入 wiki
|
||||||
# <!-- wiki:end -->
|
# <!-- wiki:end -->
|
||||||
# L3 機械底線:萬一機敏值仍被寫進 .claude/wiki/,wiki-secret-scan.sh 會 exit 2 擋下。
|
# L3 機械底線:萬一機敏值仍被寫進 system-dev/wiki/,wiki-secret-scan.sh 會 exit 2 擋下。
|
||||||
#
|
#
|
||||||
# 三層的分工:L1 整檔排除(你主動列)|L2 局部遮蔽(你標記)|L3 兜底攔截(自動掃)。
|
# 三層的分工:L1 整檔排除(你主動列)|L2 局部遮蔽(你標記)|L3 兜底攔截(自動掃)。
|
||||||
# ────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
#
|
#
|
||||||
# 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md
|
# 來源: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。
|
# arcrun workflow 跑在遠端 CF Workers,沒有本地 fs / git / shell。
|
||||||
# 所以「讀 tasks.md / git diff / 回寫 <!-- gh:id -->」這三件本地事
|
# 所以「讀 tasks.md / git diff / 回寫 <!-- gh:id -->」這三件本地事
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
#
|
#
|
||||||
# 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md
|
# 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md
|
||||||
#
|
#
|
||||||
|
# ⚠️ sdt-instance-name-ok-file — 檔級豁免(實例專名檢查)
|
||||||
|
# 理由:整支是 **L2 政策包產物**,不是 L1 框架內容——它綁定某一家的工作流引擎,
|
||||||
|
# 「換一家公司就不成立」。它現在還躺在框架裡只是歷史包袱。
|
||||||
|
# 帳記在這:**W3(政策包 plugin 化)時整組移出 template/**,屆時本豁免一併刪除。
|
||||||
|
# 在那之前不做假性清理(把引擎名改寫成模糊詞=資訊消失、分層問題還在)。
|
||||||
|
#
|
||||||
# ── 這份 workflow 的職責邊界(很重要,別搞混)──────────────────
|
# ── 這份 workflow 的職責邊界(很重要,別搞混)──────────────────
|
||||||
# arcrun workflow 在 Cloudflare Workers / WASM 上「遠端」執行,沒有本地檔案系統、
|
# arcrun workflow 在 Cloudflare Workers / WASM 上「遠端」執行,沒有本地檔案系統、
|
||||||
# 沒有 git、沒有 shell。所以「讀 tasks.md / 跑 git diff / 把 <!-- gh:id --> 回寫 md」
|
# 沒有 git、沒有 shell。所以「讀 tasks.md / 跑 git diff / 把 <!-- gh:id --> 回寫 md」
|
||||||
@@ -17,7 +23,7 @@
|
|||||||
# 單向:只寫 GitHub,永不回改 tasks.md(回寫 id 是本地端的事,且只在新 task 做一次)。
|
# 單向:只寫 GitHub,永不回改 tasks.md(回寫 id 是本地端的事,且只在新 task 做一次)。
|
||||||
#
|
#
|
||||||
# ── 輸入(由本地觸發端用 acr run -i 餵)──────────────────────────
|
# ── 輸入(由本地觸發端用 acr run -i 餵)──────────────────────────
|
||||||
# owner GitHub repo owner(例:uncle6me-web)
|
# owner GitHub repo owner(例:your-github-account)
|
||||||
# repo GitHub repo 名
|
# repo GitHub repo 名
|
||||||
# project_id GitHub Projects v2 的 node id(投影目標,唯讀看板)
|
# project_id GitHub Projects v2 的 node id(投影目標,唯讀看板)
|
||||||
# tasks_json 本地分類好的增量陣列,每筆形如:
|
# tasks_json 本地分類好的增量陣列,每筆形如:
|
||||||
|
|||||||
Reference in New Issue
Block a user