13 Commits

Author SHA1 Message Date
Leo 593defceb1 release(1.19.0): 掛上機械閘+出貨版號與 changelog(W2 Phase 4-5,收尾)
SDD: docs/3-specs/jdd-dual-profile — 33/33 編號 task 全數完成。

■ Phase 4 把閘掛上去
- scripts/check-all.sh:三道閘+腳本語法的總入口(一個習慣,不是三個要記得的步驟)
- .githooks/pre-commit + core.hooksPath:跑在人的機器上、commit 那一刻
  不掛雲端 workflow——本組織禁止 repo 掛自動化(歷史上那正是帳號被停權的原因)

■ Phase 5 出貨
- VERSION 1.18.0 → 1.19.0(兩處:template/.claude/ 與 template/system-dev/)
- CHANGELOG 1.19.0:用「你會多出什麼」的語言寫,不列檔名
- 🐛 順手補回 **1.18.0 完全沒有 changelog 紀錄**這件事——
  那一版發佈了、功能也出貨了,但更新跑完最後一行正是叫使用者「改了什麼看 CHANGELOG」,
  版號動了卻查不到動了什麼=等於沒交代。依實際 commit 內容補寫。

■ 七題驗收(全部附實測輸出)
-  G2 考生改考卷被攔(命門):engineer 改 journeys.md → exit 2;
     連「把考題藏進 status.md」也擋,6/6
-  G3 進度以站計量:起牀推「J-1 已點亮 n/m 站」,收工列未亮站並禁用任務數當理由
-  G4 憲法分流:總管版技術軌關鍵字 0、成員版上游指針 8,界標各 4/4
-  G6 實例改機制被攔:4/4(框架開發放行、使用者自訂插槽放行)
-  G7 框架混入實例名:指名 sdd-check.md:77,exit 1
- ◐ G1 PM 優先補接縫:機制齊了,但這是行為題,腳本證明不了 → 不標綠
- ◐ G5 政策包即插即用:插槽就位,政策包本體在 W3

■ 安裝實測:兩個乾淨環境各 1 秒、48/54 檔、版本 1.19.0、profile 正確
  (預測①「各 < 10 分鐘」達標;誠實註記:來源走本機檔案,比真實網路快)

🔴 狀態=**等發佈**,不是 :更新來源是公開 GitHub raw,
   只推 Gitea 的話外部實例抓不到 1.19.0。開閘由 leo 決定。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:34:23 +08:00
Leo 2f5d9f3bb2 feat(W2 Phase 2-3): JDD 文件範本+八條封路 hook+還清兩件舊債
SDD: docs/3-specs/jdd-dual-profile(active)。編號 task 26/33 完成,Phase 4-5 未開工。

■ Phase 2 JDD 文件範本(orchestrator profile)
範本形狀對齊「實際跑出來的那兩份」(總管已寫的 root.md 15 卡、journeys.md J-1 九站),
不是照規格憑空造:
- 卡片是巢狀 bullet(`- **P1** 🟢 …` + 子項放來源/對帳),非規格畫的平行文字行
- 站點索引**巢狀 bullet 不用表格**(表格會把層級壓平,看不出從屬)
- 兩份都保留「這卷還缺什麼(誠實記)」收尾段——規格沒有,但那是防假綠的地方
新增:root.md / journeys.md / sprint.md / triage-map.md 四範本(add-if-missing,
填了就永不覆蓋)+ plugin-load-order.md(W3 插槽,框架不發明平行外掛格式)

■ Phase 3 封路 hook(八條規則落六支檔)
- role-guard(J1+J2+J3)★命門:考生不能改考卷。六組實測含「考題藏在別的 md 裡」也擋
- jdd-format-guard(J4+J5+J8):紅卡缺對帳日/任務缺站號/PM 文件混技術名詞
- station-done-guard(J6):收工判準是站的考題全綠,不是任務全關
- regression-scope(J7):動實作 → 列出要重考哪幾題(只提醒不擋)
- install-artifact-guard(S1):實例不改機制
- orchestrator-scope-guard(S4):總管不進成員 repo 動實作(從實例上收進框架,
  路徑清單改由實例自填,範本零專名)
掛載鏈依「範圍大的擋在前」:改機制 → 角色 → 位置 → 格式 → 既有三支

■ 還清兩件舊債
- update.sh 檔案清單改讀 manifest(舊硬編降為抓不到來源時的 fallback)
  ——install/update 兩份手抄清單漂移的根因全修
- CLAUDE.md 界標補植:舊實例全文原封包進本地區、框架區重鋪、原檔備份、冪等
  ——解開「沒界標⇒不敢覆蓋⇒框架改的憲法永遠送不到既有實例」這個死結

■ 修掉三個自己造的問題(實測抓出來的,不是想出來的)
- jdd-format-guard 誤擋真實 journeys.md 的「這卷還缺什麼」自述段
  → 排除法改**正面圈定**(只掃卡片本體與站內文),說明區/自述段/索引自然不在範圍
- install-artifact-guard 把 pre-write-guard.sh 也擋了——而它的錯誤訊息正叫人去改那支
  → 使用者自訂插槽列為最優先放行
- check-legacy-paths 用 HEAD 當基準會**自我弱化**:改成清單驅動後保護範圍 35→29 條
  → 基準改指最後一次真正發佈的版本

■ 實測(全部貼過輸出)
- G2 考生改考卷:6/6,含 orchestrator 寫 code/engineer 改考題/考題藏別處
- G4 憲法分流:兩環境重裝,總管版技術軌關鍵字 0、成員版上游指針 8,界標 4/4
- G6 實例改機制:4/4,含框架開發標記放行與自訂插槽放行
- G7 CI 擋實例名:注入違規 → 指出檔案行號 exit 1
- G3 進度以站計量:起牀推「J-1 已點亮 2/9 站」、收工列未亮站並禁用任務數當理由
- 回歸考、界標補植冪等、orchestrator-scope-guard 四組:全通

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:26:56 +08:00
Leo 6a49f25aef feat(W2 Phase 0-1): 雙 profile 地基+JDD 兩軸身分+兩支防炸閘
SDD: docs/3-specs/jdd-dual-profile(draft → active,leo 2026-08-05 回「開工」)
範圍:總管指定的「防炸兩件 → Phase 0 → Phase 1」,Phase 2 以後未開工。

■ 防炸(排在所有 task 之前,因為它們炸的是既有的東西)
- check-no-instance-names.sh + instance-names.txt:框架範本不得混入實例專名
  基線實測 22 行命中(非先前誤報的 20)→ 9 行無損泛化改寫、13 行檔級豁免記帳待 W3 搬走
  拒絕假性清理(把專名換成模糊詞=資訊消失、分層問題還在)
- check-legacy-paths.sh:已發佈腳本引用的 35 條遠端路徑只增不移
  舊實例跑的是舊腳本、路徑寫死;搬檔=整排 404 且不會有下一次更新來修它(1.16.0 前科)

■ Phase 0 地基
- template/manifest/{common,repo,orchestrator}.tsv:安裝清單單一真相源
  修好 install/update 兩份硬編清單的既有漂移——install 從不裝 wiki-first-search /
  subagent-wiki-guard / publish-lag-check / decisions-summary,但 update 會
  ⇒ 乾淨安裝反而拿不到 1.16/1.17/1.18 的招牌功能
- .claude/hooks/lib/role-lib.sh:scope×role 兩軸機械判定,零自陳
  身分矩陣六組實測全通過,含「成員 repo × orchestrator」不存在的格子擋下
- .sdt-framework-dev:框架開發標記(官方沒有 --framework-dev 這個參數,實查非記憶)

■ Phase 1 雙 profile
- profiles/{repo,orchestrator}/CLAUDE.md 兩部憲法
- install.sh:--profile + 自動偵測+寫檔前確認、manifest 驅動、
  CLAUDE.md 三段組裝(框架區/本地補充區界標+sha256)、.profile、.template-manifest、
  settings.json 寫入 env.AGENT_ROLE 預設
- update.sh:漂移偵測(不覆蓋手改檔、另存 .new、白話清單)+ 基準快照隨更新前進
- template/CLAUDE.md 原路徑凍結留底(相容)

■ 順手修掉兩個舊 bug(都在本次要動的函式裡)
- add_if_missing 少了 mkdir -p ⇒ 新目錄的檔 curl 失敗但 VERSION 照升(2026-07 記「待回報」至今未修)
- 下載健全性只用 [ -s ]=非空即接受 ⇒ 404 頁面會無聲覆寫好檔
  (SKILL.md 260→1 行的機制;同一支腳本的版本號那條路早就防了,檔案這條沒防)

■ 實測(非推論)
- G4 憲法分流:兩個乾淨環境各裝一次,orchestrator 版含 SDD 三件式關鍵字 0 次、
  repo 版含上游指針 8 次;界標 4/4;sha 宣告與實算相符
- G7 CI 擋實例名:注入違規行 → fail 並指出 sdd-check.md:77,exit 1;還原後 exit 0
- 漂移偵測:手改兩支 hook → 正確報 2 支、手改內容保住、產 .new;
  解掉後歸零;連跑三輪冪等

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:56:48 +08:00
Leo 2a8c259d08 依 D22 翻正舊 ignore 政策:本 repo 自己的 SDD 進版控 + 新增 jdd-dual-profile 卷
🔴 修的病:這個框架 repo 自己的 5 份 SDD 全部被 .gitignore 擋在版控之外,
只活在一台硬碟上——clone 不到、雲端 CC 讀不到、沒備份。SDD 是進度真相源,
「框架 repo 沒吃自己的狗糧」。

依據 D22(已翻案):Gitea private 除機敏值外全 push,雲端工人靠 clone,docs 缺=斷糧;
只有 GitHub mirror 才嚴篩,而 docs 整包已在 github-publish-exclude.txt。

.gitignore 改法:整條擋 → 只擋子項(/*)+ 逐個放行真 SDD。
與 template/ 底下逐位元組相同的自裝副本(TEMPLATE-sdd/、SDD-LIFECYCLE.md)續擋。

新增 docs/3-specs/jdd-dual-profile/(status: draft,等 leo confirm 才升 active):
JDD(PM 軌 root/journeys/角色權限/站號 sprint)與雙 profile 合成一卷,
33 條 task/6 phase,每條掛服務哪條 Gherkin(G1–G7)。
新增 hook 6 支、改既有 hook 1 支。**0 行實作**。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:22:14 +08:00
Leo dc4fe67e15 feat(1.18.0): 公開 mirror 落後偵測(手動同步的靜默失敗,只有外部用戶會撞到)
Gitea(草稿)↔ GitHub(正稿)是手動同步,改了零件沒發佈=用戶抓到舊版
且無任何錯誤訊息,只是行為不對——我們自己永遠測不到。

SessionStart hook 比對工作區 HEAD 與 .github-public 最後 release snapshot,
落後就出聲,含發佈指令;若該批改動含 .wasm 額外標紅(安裝器懶載直接抓那個)。
只提醒不阻擋(發佈需 leo 親跑 arm)。

實測 Arcrun:落後 25 個 commit 且含 wasm 變更。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:51:10 +08:00
Leo 27c207c3e9 feat(1.17.0): 查詢一律從最強查法開始(語意→關鍵字→grep)
leo:「它一定是用最好的搜尋,如果沒有才 fallback,但那不是你要指定的。」

事故:查 CF git 託管只用 grep→零命中→結論「沒查過/申請表沒送」=指控 leo
沒做他早就做過的事。同一問題跑語意搜尋第一筆就命中(0.858),
帶出三元組「Artifacts >> 若提供 git 倉庫則可取代 >> Gitea」,leo 15 天前就記了。

根因不是關鍵字選錯,是用了三種查詢裡最弱的那種。
grep 要求先猜對詞;語意搜尋不需要。

- subagent 注入改分級查法(語意/關鍵字/grep)
- wiki-first-search:grep 零命中不再靜默退出(那正是最該用語意的時刻);
  有命中也明說「最弱查法、搜尋詞是猜的,請補語意搜尋」

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:59:21 +08:00
Leo 3eeded0432 fix: 源頭就寫對的網址,不靠發佈時改寫(leo 2026-07-21)
leo:「要發佈的正稿,從頭就不要用奇怪的網址,以免改來改去。」
=sanitize 每次改寫是補丁不是解法。改成源頭正確:

- README/README.en:死帳號 uncle6me-web → youlinhsieh(該帳號已 suspend,
  安裝指令指著它半年沒人發現=別人照做必失敗)
- install.sh/update.sh:來源預設改公開 GitHub raw;內部要指私有草稿源
  走 TEMPLATE_SOURCE= 環境變數覆寫,不改檔 → 兩邊不再需要維護兩份網址
- tasks-project-sync.yaml 註解範例帳號改通用佔位符

sanitize 降級為「安全網」:正常情況應無需改寫;若它報告改了東西=
源頭又混進錯網址的信號,回頭修源頭。保留它做發佈前驗證(殘留即中止)——
擋的是「只有外部使用者才會撞到、我們自己永遠測不到」的錯。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:32:27 +08:00
Leo c25babf4bb feat: GitHub public mirror 管線(成品櫥窗,非工作現場)
leo 2026-07-21:「應該要給的是一個適合別人用的濃縮結果,不需要有過程。
以後我還是在私有的 template 工作,但 publish 到 GitHub 就把它當貼文。」

- publish-github.sh:移植自 arcrun-rag 既有管線(乾淨歷史、憑證走 header 不進 URL)
  +修一個真 bug:原版 sanitize 失敗不中止 → 會把未淨化樹 rsync 進 mirror 並推出去
- github-publish-exclude.txt:濾掉工作現場(system-dev/ .claude/ docs/ CLAUDE.md CHANGELOG.md)
  保留成品(README/scripts/skills/template)
- github-publish-sanitize.py:改寫來源網址 + 發佈前驗證,殘留失效來源即中止
  ① git.uncle6.me(private 且即將換 CF 版,別人抓不到)
  ② uncle6me-web(已 suspend 的舊 GitHub 帳號——README 安裝指令原本還指著它,
     別人照著跑會失敗,與 07-20 update.sh 同一顆雷)

公開樹實測:只剩 README/scripts/skills/template,安裝指令指向 youlinhsieh,零殘留。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:18:37 +08:00
Leo b027044cb4 docs(template): CLAUDE.md 樣板加「第一鐵律:wiki 是判準,不准跳過」
leo 2026-07-21:「其他 repos 各自都要警覺不能跳過 wiki」。
hook 會攔,但 CLAUDE.md 是各 repo AI 開工必讀處,鐵律要寫在會被讀到的地方。
含:grep 查法、三條硬規則(衝突以 wiki 為準/核對解除條件/回頭更新 wiki)、
動外部系統前先找現成腳本(07-21 curl 硬幹實例)。

註:CLAUDE.md 屬使用者資料檔,update.sh 不覆蓋 → 既有 repo 由總管手動補齊。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:55:28 +08:00
Leo 0a04c9c594 fix(1.16.1): wiki-first-search 補 Bash 破口(1.16.0 隔天即被自己繞過)
leo 點破:wiki 早記著寄信已驗證可用,我卻沒查又自創 curl 部署法。
根因=昨天的 hook 只掛 Grep|Glob|Read,但我實際用的是 Bash → 完全不觸發。

- matcher 加 Bash,只認高風險指令(wrangler|curl|npx|acr|gh|deploy|push)
- update.sh 對既有註冊就地補 Bash,不只新裝生效

教訓:防跳過 wiki 的機制本身要蓋到所有實際查詢途徑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:51:33 +08:00
Leo 4d14b05d81 fix(1.16.0): update/install 來源改指 Gitea(GitHub uncle6me-web 已 suspend=自動更新早就死了)+補 system-dev/VERSION 同步
發現:所有下游 repo 的 update.sh 都指向已被 suspend 的 GitHub 帳號
raw.githubusercontent.com/uncle6me-web/… → 一律「取不到遠端版本」而中止。
去 GitHub 化時漏掉這條,等於整套自動更新機制靜默失效。

改指 git.uncle6.me/Leo/system-dev-template/raw/branch/main(實測 200)。
另:版本真相源有兩份(template/.claude/VERSION 與 template/system-dev/VERSION),
update.sh 讀的是後者但 1.15.0 只更新了前者 → 補齊,並列為之後要收斂的雙檔同步債。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:38:45 +08:00
Leo ed3a97c6a2 feat(1.16.0): 讓 wiki 真的被讀到——查詢即搜尋 wiki+subagent 自動注入
病根(leo 2026-07-20 點破,真實事故):總管三次擋回 leo「某機制早已棄用」的
正確判斷,查證後 leo 全對。根因不是知識不足,是讀取流程:
① wiki 只讀開頭就開工(關鍵記載在第 56 行,答案一直在那裡)
② 派 subagent 只叫它讀 code、沒叫讀 wiki → 從稿子推論必然得出過時結論
③ 把 wiki 的「當時狀態」當永久事實(沒核對解除條件)

leo:「我需要的不是你記住,而是機制面的解法」
「如果你不是讀而是搜尋 wiki,就不會只讀 50 行就下定論,像 cmd+F 那樣高亮。」

新增:
- wiki-first-search.sh(PreToolUse: Grep|Glob|Read)
  查 code/文件的當下,用同一組關鍵字 grep wiki,只推命中行。
  開場 push 全文解決不了「只讀開頭」——時機才是關鍵。提醒不阻擋。
- subagent-wiki-guard.sh(PreToolUse: Task)
  查證/實作類任務 → 注入「先查 wiki」指示。
  第一版為「上游沒交代就擋」,經 leo 指正改注入式:
  「它只要聽到查,就應該主動查 wiki」——依賴上游記得寫=同一個病。

update.sh 除同步兩支 hook 外,自動註冊進 settings.json(不只提醒)——
靠人看提醒手動補,等於把同一個病搬到安裝環節。
template/ 新裝樣板同步(新 repo 裝完即生效)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:30:46 +08:00
Leo 7b83465acf feat: SDD 生命週期鐵律——單一活性 SDD+pending-changes 緩衝+雙層硬約束(issue #6)+ bump 1.15.0
leo 2026-07-17 拍板:任何時刻每 repo 只有一份現行 SDD(status: active)。

- 新增 3-specs/SDD-LIFECYCLE.md:frontmatter 狀態標記(active|draft|paused|closed + superseded_by)+五條鐵律(單一活性/禁 CC 自建 SDD/規格變更走 pending-changes.md 等 confirm/開新 SDD 先逐條搬舊任務才准寫 code/session 開始回報三數字)
- 新增 3-specs/pending-changes.md:規格變更緩衝區骨架(待裁決/已裁決留底)
- TEMPLATE-sdd/design.md 掛 frontmatter(status: draft),移除舊「> 狀態」blockquote
- sdd-guard.sh 升級:active>1 不論寫什麼檔一律 exit 2;寫 code 檔需恰好 1 份 active;老 repo 無 frontmatter 退回舊行為(統計排除 archive/ 與 TEMPLATE)
- 新增 template/scripts/sdd-active-check.sh:獨立檢查,pre-commit/CI 可掛,>1 exit 1
- /sdd-check 加生命週期段(五鐵律摘要+三數字回報格式);template/CLAUDE.md 鐵律指向 SDD-LIFECYCLE.md
- install.sh/update.sh 鋪齊新檔(issue #13 教訓:update 不補新檔=結構斷層)
- 三情境 pipe-test 真跑通過:兩份 active 擋(2)/一份放行(0)/零 frontmatter 退舊行為(0)
- 版號:遠端已被 issue #5(vault 萃取)佔走 1.14.0,本案改記 1.15.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:53:18 +08:00
62 changed files with 5247 additions and 183 deletions
+18
View File
@@ -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
View File
@@ -31,7 +31,27 @@ skills/editorial-image/
/docs/README.md
/docs/1-vision/
/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/5-records/
/docs/6-user/
# GitHub public mirror 產出(本機暫存,不進私有真相源)
.github-public/
+18
View File
@@ -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 上看得見。
+156
View File
@@ -10,6 +10,162 @@
---
## 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 生命週期鐵律:單一活性 SDDissue #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
筆記 vaultLogseq graph 如 `notes`/`kb`、或 Obsidian)只有原文、需要萃。新增可**重跑、冪等**的 vault 增量萃取,把原始筆記萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`,供下游 Arcrun ingest 從 wikilink 機械拉三元組進 KBDB。**AI 只產卡片檔,不寫 KBDB、不拉三元組**(那是下游的事)。
+3 -3
View File
@@ -32,7 +32,7 @@ This template solves both problems with two systems:
### Option 1: New project
```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/
cd your-new-project
```
@@ -46,7 +46,7 @@ Then in a CC conversation:
```bash
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**.
@@ -80,7 +80,7 @@ Heard there are new features and want the latest? Just run this:
```bash
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:
+3 -3
View File
@@ -31,7 +31,7 @@ CC 是個優秀的工程師,但不是個好的專案經理。它會猛衝完
### 方式一:新專案
```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/
cd your-new-project
```
@@ -45,7 +45,7 @@ cd your-new-project
```bash
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
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
```
它會先比對你的版本和最新版,告訴你**多了哪些新功能**,然後:
+66
View File
@@ -0,0 +1,66 @@
# cross-repo-signing — Design
> 狀態:已採納
> 建立:2026-06-26 | 最後更新:2026-06-26
> 負責人:leouncle6me-web
> 來源:issue #12InkStoneCo 總管)
---
## 一句話說明
所有 repomira / graph-plugin / ingest-plugin / Arcrun / template…)共用 `uncle6me-web` 一個 GitHub 帳號發 issue/commentauthor 全顯示同一帳號、看不出來源;制度化一條鐵律——**跨 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 同步。
+146
View File
@@ -0,0 +1,146 @@
# install-layout — Design
> 狀態:已採納
> 建立:2026-06-26 | 最後更新:2026-06-26
> 負責人:leouncle6me-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 改(誤傷風險 + 破鐵則);只寫 READMElow-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 repo192 張卡),遷移時用保留 .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 鐵律)
- 前置 hotfix1.8.2update.sh bash 3.2 崩潰修復,已 push
- 前置:1.8.1(補裝 Cowork SKILL.md,已 push
+87
View File
@@ -0,0 +1,87 @@
# install-layout — Tasks
> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。
> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。
---
## Phase 1:腳本核心(install / update + 遷移)
### 前置條件
- [x] design.md 已審核(用戶批准)
### Tasks
- [x] 1.1 install.shdocs/ 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.shscripts 落點改 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.mdtemplate + 根):鐵律/速查/wiki 讀取表路徑;raw source 宣告維持
- [x] 2.2 SKILL.mdtemplate + 根):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.shSTATUS_FILE 路徑 + 新增防呆檢查
- [x] 2.7 INDEX.md / decisions-summary.md.claude + template):docs/2-architecture 引用
- [x] 2.8 README.md / README.en.md:目錄樹示意 + 安裝後說明
- 驗收(2.12.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]` | 完成(有驗收證據)|
| `[~]` | 暫緩(說明原因)|
| `[!]` | 阻擋中(說明阻擋原因)|
+445
View File
@@ -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**
> **狀態:active2026-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.mdjourneys.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 ⇒ 清單要分四份(commonrepoorchestrator/模組交叉),硬編必然漂移 → **必須先做 manifest** |
| `update.sh` 另有一份**幾乎重複**的清單(update_filekeep_fileadd_if_missingkeep_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 內容、
primerdispatch-checklist 搬遷、`.mcp.json`
- **實例側落地**W4):InkStoneCo 的 root.mdjourneys.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 | orchestratorscope 軸唯一來源)
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(可寫 codetasksrequirementsdesign;禁改考卷) | orchestrator(可寫 rootjourneyssprint;禁寫 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/.profiletrim;讀不到回 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.shprofile 分流)
[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-recallwiki 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 URLpublish-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.mdG2 在最該生效處失效(§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 §四 的 **G1G7** 為唯一驗收線。收工時每題必須附**實測輸出**,
狀態只有三種:`✅ 通(附證據)` / `◐ 半通(標明缺什麼)` / `❌ 斷`
- 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`(安裝當下的雜湊) | installupdate | `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 角色封路** | orchestratorengineer 的可寫範圍用 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 執行 updatethe 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
endpointschema…),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 查站點索引表並列出需重考的
JourneyGherkin 清單(提醒,不阻擋)。
### 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 → orchestratorrepo 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 SessionStartthe system shall 依 profile 注入對應的必讀
orchestratorroot/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 執行 updatethe 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
# ── G1JDD §七之一)
Scenario: PM 總管優先補接縫而非做新功能
Given commit push
And root.md journeys.md J-1
When PM J-1
Then push
And
# ── G2JDD §七之二)
Scenario: 考生改考卷被攔截
Given engineer subagent Gherkin
When journeys.md Gherkin
Then hook
# ── G3JDD §七之三)
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 交付的是 GivenWhen 能成立的**插槽**——
乾淨實例裝得出 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 後半)
+289
View File
@@ -0,0 +1,289 @@
# jdd-dual-profile — Tasks
> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。
> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。
> **每一項都標「服務哪條 Gherkin」**G1G7 定義見 `requirements.md` §四)。
> 🟢 **status: active2026-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 本 SDDfrontmatter 由 `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 2JDD 文件範本(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 0role-lib)+ Phase 2(有檔可擋)
- [x] 3.1 `role-guard.sh`J1J2J3common
- 服務:**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`J4J5J8common
- 服務:**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`J6common,掛 Stop/TaskCompleted
- 服務:**G3**
- 判準:本 sprint 指定站的 Gherkin 全綠才算收工;「tasks 全關」不算
- 驗收:造一個「tasks 全關但站 Gherkin 未綠」的情境 → 被退回(貼 exit 2 輸出)
- 注意:框架內**沒有**既有的 sprint 收尾 hook`delivery-police.sh` 只在 InkStoneCo 實例),
這是新增不是修改;W4 時實例那支要退役,別兩處維護
- [x] 3.4 `regression-scope.sh`J7common
- 服務:**G3**
- 行為:偵測站相關實作變動 → 讀 journeys.md 站點索引表 → 列需重考的 Journey/Gherkin
- 驗收:改動某站的實作檔後,輸出正確列出該站被哪些 Journey 經過(貼輸出)
- 注意:**提醒不阻擋**(exit 0)
- [x] 3.5 `install-artifact-guard.sh`S1common
- 服務:**G6**
- 行為:寫入 `.claude/hooks/``system-dev/` 範本區/plugin 安裝目錄 → 擋,
提示「機制變更走框架/政策包 repo 提案」;`.sdt-framework-dev``SDT_FRAMEWORK_DEV=1` 放行
- 驗收:① 實例 session 編輯 `.claude/hooks/` 任一檔 → 擋(**這條就是 G6**)
② 框架 repo(有 marker)編輯同路徑 → 放行。兩組都貼輸出
- 注意:「範本區」的定義來自 manifestclass=overwrite 者),不要另寫一份路徑表
- [x] 3.6 `orchestrator-scope-guard.sh`S4orchestrator 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
- orchestratorroot/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 逐條實測並記錄三態
- 服務:**G1G7**
- 驗收:每題貼實測輸出並標 `✅ 通 / ◐ 半通(缺什麼)/ ❌ 斷`
- 注意:**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.0CHANGELOG 已寫
- [ ] design.md 與實作一致(如有出入需更新 design,不是默默改 code
---
## 狀態說明
| 標記 | 意義 |
|------|------|
| `[ ]` | 未開始 |
| `[🔄]` | 進行中(當前 session|
| `[x]` | 完成(有驗收證據)|
| `[~]` | 暫緩(說明原因)|
| `[!]` | 阻擋中(說明阻擋原因)|
+15
View File
@@ -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
> 負責人:leouncle6me-web
> 來源:issue #16InkStoneCo 總管);脈絡 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 -->`。首次同步前無 idcreate 後 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 都只改 mdProject 唯讀 | Project 當真相源(low-code 用戶碰不到) |
| 觸發 | push 後本機單次 | 守 flag 紅線 | 定期輪詢/Actions(踩紅線) |
| 增量依據 | 行內穩定 id + git diff | 省 API、不全量重寫 | 全量 diff title 比對(脆、易撞名) |
| 啟用判準 | 裝/init 對話 + 查 cli/mcp 能力 | 能力≠檔案;arcrun 存遠端 KV,掃檔 false negative | `HAS_ARCRUN` 檔案指紋(漏判用遠端沒落檔者,**廢棄**) |
| 沒裝時 | 一次性溫和廣告 + 之後閉嘴 | 揭露功能存在又不騷擾 | 每次 install 都廣告(騷擾)/完全靜默(用戶不知有此功能) |
| 腳本形態 | arcrun workflowYAMLname/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 定案為準。)
- READMEoptional 模組區塊標「此功能需 arcrun;不要/沒裝則純 md no-op」。
- install/update**不靠 `HAS_*` 分支裝檔**。改由裝/init 對話驅動——AI 在用戶答「好」且查到 arcrun 能力時,`acr push` 那份 workflowworkflow 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 #16design.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.0CHANGELOG 記 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]` | 完成(有驗收證據)|
| `[~]` | 暫緩(說明原因)|
| `[!]` | 阻擋中(說明阻擋原因)|
+140
View File
@@ -0,0 +1,140 @@
# wiki-architecture — Design
> 狀態:已結案(2026-06-26,KB 端 183 卡實跑壓測全綠,1,029 條三元組、端點 0 缺陷;驗收明細見 tasks.md
> 建立:2026-06-26 | 最後更新:2026-06-26
> 負責人:leouncle6me-web
---
## 一句話說明
在「用戶所有檔案一律改寫成 wiki cards」的新架構下,用 **pushCC 行動前必主動看見)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」等:
- **內容**寫成 principlespush 檔)+ 必要時 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 #11183 卡實證)
現行三元組/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.mdCowork+ wiki-init.mdCC)一致。
---
## 驗收標準
- [ ] push/pull 判準寫進 wiki-init.md 與 SKILL.md,成為 CC/Cowork 共同規則。
- [ ] principles 進 pushsession-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 通道」
+83
View File
@@ -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 判準 + 三類 pushstatus全文/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.mdCowork)同步上述規則,CC/Cowork 一致
- 驗收:兩來源檔 push/pull 規則 byte 對齊(除路徑)
## Phase 2INDEX 升級為多角度視圖
> 前置:Phase 1
- [x] 2.1 INDEX.md 範本:從「標籤視圖」升級為「多角度入口」(標籤/決策/原則…角度)
- [x] 2.2 明示「新增角度 = 改 INDEX 一節,不開新檔、不問用戶」
- [x] 2.3 principles/mistakes 在 INDEX 留 pull 指標
- 驗收:INDEX 範本含多角度說明 + 新增角度的自助規則
## Phase 3push 機制(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-26KB 端 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 機制 / `## 實體` 都已下游實證,無待驗證項。
+52
View File
@@ -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
+102
View File
@@ -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
+136
View File
@@ -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
+34
View File
@@ -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
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""github-publish-sanitize.py — 對匯出樹做內容級改寫,讓公開版真的能被別人使用。
由 publish-github.sh 自動呼叫(步驟 3.5),參數=匯出樹的暫存目錄。
🔑 定位(leo 2026-07-21 校正):**這支是安全網,不是主要手段。**
leo:「要發佈的正稿,**從頭就不要用奇怪的網址,以免改來改去**。」
→ 已於同日**源頭修正**install.shupdate.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 "."))
+250 -94
View File
@@ -26,35 +26,59 @@ t() { if [ "$IS_ZH" = "yes" ]; then printf '%s\n' "$1"; else printf '%s\n' "$2";
# tn = 不換行版(給 prompt 用)
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"
# 來源預設=公開 GitHubleo 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/)。
SCRIPTS_URL="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main/scripts"
SCRIPTS_URL="$TEMPLATE_SOURCE/scripts"
CREATED=()
SKIPPED=()
# ── 解析模組參數 ──────────────────────────────────
# ── 解析模組 / profile 參數 ────────────────────────
# profile scope 軸(這個資料夾是「成員 repo」還是「總管」)。
# 它決定裝哪一部憲法——agent 在哪裡醒來就只讀得到哪部,**不靠 agent 自我判斷**。
#
# ⚠️ 契約在檔案,不在這支腳本:真正的 scope 真相源是 `system-dev/.profile`。
# 本腳本只是**其中一個寫入者**——「把網址丟給 AI 幫我裝」那條路(主打入口)
# 同樣必須寫出這個檔,格式一致即可互通。腳本入口是備援與參考實作,不是唯一路徑。
MODULE=""
PROFILE=""
for arg in "$@"; do
case "$arg" in
--wiki|--wiki-only) MODULE="wiki" ;;
--sdd|--sdd-only) MODULE="sdd" ;;
--all) MODULE="all" ;;
--profile=repo) PROFILE="repo" ;;
--profile=orchestrator) PROFILE="orchestrator" ;;
--profile=*)
echo "❌ 不認得的 profile${arg#--profile=}(只接受 repo / orchestrator" >&2
exit 1 ;;
-h|--help)
if [ "$IS_ZH" = "yes" ]; then
cat <<'HELP'
用法:install.sh [--wiki | --sdd | --all]
用法:install.sh [--wiki | --sdd | --all] [--profile=repo|orchestrator]
--wiki 只裝 LLM WikiCC 記憶系統 + 機敏防護)
--sdd 只裝 SDD 系統(動 code 前強制要有設計文件)
--all 兩個都裝(預設)
無參數 互動式詢問要裝哪個
--profile=repo 成員 repo:實際寫 code 的子專案(預設)
--profile=orchestrator 總管:管一群成員 repo 的上層資料夾
未指定 → 自動偵測並請你確認一次(結果寫進 system-dev/.profile,之後不再問)
HELP
else
cat <<'HELP'
Usage: install.sh [--wiki | --sdd | --all]
Usage: install.sh [--wiki | --sdd | --all] [--profile=repo|orchestrator]
--wiki Install LLM Wiki only (CC memory system + secret protection)
--sdd Install SDD system only (require a design doc before touching code)
--all Install both (default)
no flag Interactively ask which to install
--profile=repo Member repo: a project where code actually gets written (default)
--profile=orchestrator Orchestrator: the folder that manages several member repos
unset -> auto-detect and ask once (result stored in system-dev/.profile)
HELP
fi
exit 0 ;;
@@ -103,6 +127,57 @@ echo ""
t "📦 安裝模組:$MODULE" "📦 Module: $MODULE"
echo ""
# ── 決定 profile(scope 軸)──────────────────────────
# 必須在**寫任何檔案之前**問完(EARS-1.1.3)——裝了一半才問是最差的體驗。
#
# 偵測規則:目前目錄下有多個「各自帶 .git 的子目錄」= 這裡像是管著一群 repo 的上層
# → 建議 orchestrator。偵測只給建議,**仍要人確認一次**(一次性,之後寫進 marker 檔)。
detect_profile() {
local n=0 d
for d in */; do
[ -d "$d/.git" ] && n=$((n + 1))
[ "$n" -ge 2 ] && break
done
if [ "$n" -ge 2 ]; then printf 'orchestrator'; else printf 'repo'; fi
}
if [ -z "$PROFILE" ]; then
SUGGESTED="$(detect_profile)"
if [ -t 0 ]; then
echo ""
t "🧭 這個資料夾是哪一種?(決定裝哪一部憲法)" \
"🧭 What is this folder? (decides which constitution gets installed)"
t " 1) 成員 repo —— 實際寫 code 的子專案" \
" 1) Member repo — a project where code actually gets written"
t " 2) 總管 —— 管一群成員 repo 的上層資料夾" \
" 2) Orchestrator — the folder that manages several member repos"
echo ""
if [ "$SUGGESTED" = "orchestrator" ]; then
t " (偵測到底下有多個各自獨立的 repo → 看起來像 2)" \
" (found multiple independent repos below -> looks like 2)"
else
t " (沒偵測到多個獨立 repo → 看起來像 1)" \
" (no multiple independent repos found -> looks like 1)"
fi
tn "請輸入 1 / 2 [預設:$SUGGESTED]" "Enter 1 / 2 [default: $SUGGESTED]: "
read -r pchoice || pchoice=""
case "$pchoice" in
1) PROFILE="repo" ;;
2) PROFILE="orchestrator" ;;
*) PROFILE="$SUGGESTED" ;;
esac
else
# 非互動(curl | bash 無 tty)→ 用偵測值,並在結尾大聲說它是怎麼決定的
PROFILE="$SUGGESTED"
t "🧭 非互動環境 → 自動判定 profile:$PROFILE(可事後改 system-dev/.profile" \
"🧭 Non-interactive -> auto-detected profile: $PROFILE (change system-dev/.profile later)"
fi
fi
echo ""
t "🧭 安裝 profile$PROFILE" "🧭 Profile: $PROFILE"
echo ""
# ── 重複安裝防呆(1.10.1):install 只管「全新安裝」,一切後續歸 update ──
# 判準是「裝過沒」,不分新版舊版:
# - 新結構 system-dev/ 已存在,或
@@ -115,7 +190,7 @@ if [ -d "system-dev" ] || [ -d ".claude/wiki" ] || [ -f ".claude/VERSION" ]; the
t " 後續的更新、遷移、補新檔,一律由「更新腳本」處理(不要重跑 install):" \
" All updates, migrations, and new-file additions are handled by the UPDATER (don't re-run install):"
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 ""
t " (重跑 install 可能建出空白範本、跟你的真資料並存,故在此停止。)" \
" (Re-running install could create empty templates alongside your real data, so it stops here.)"
@@ -239,93 +314,95 @@ download_if_missing() {
fi
}
# ── 共用結構 ──────────────────────────────────────
# 工具自己的文件骨架收進 system-dev/docs/(不污染用戶根目錄、不跟用戶自己的 docs/ 混)。
# 注意語義分離:這裡的 system-dev/docs/ 是「工具文件」;用戶的 raw source(原始文件)
# 另有其處(見上方 vault 偵測),工具只讀、不搬。
# .claude/ 只留 CC 死綁的 commands/ + hooks/,工具資料一律不放這
create_dir "system-dev/docs/1-vision"
create_dir "system-dev/docs/2-architecture/decisions"
create_dir "system-dev/docs/4-guides"
create_dir "system-dev/docs/5-records/incidents"
create_dir "system-dev/docs/5-records/test-reports"
create_dir "system-dev/docs/6-user"
create_dir ".claude/commands"
create_dir ".claude/hooks"
download_if_missing "system-dev/docs/README.md" "$REPO_URL/system-dev/docs/README.md"
# ── Manifest 驅動的安裝(取代原本硬編的檔案清單)──────────────
# 為什麼改成這樣(SDD jdd-dual-profile 決策 D1):
# 原本清單硬編在這裡,update.sh 另有一份幾乎重複的硬編。兩份手抄必然漂移,
# 而且**已經漂了**——install 從來不裝 wiki-first-search.sh / subagent-wiki-guard.sh /
# publish-lag-check.sh / decisions-summary.md,但 update 會裝/會保留
# ⇒ 乾淨安裝的人反而拿不到最新三版的招牌功能。
# 現在兩支腳本讀同一份 manifest,這類漂移在結構上不可能再發生。
#
# manifest 欄位:src \t dest \t class \t module \t profile(詳見 template/manifest/common.tsv
# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project
# 投影(tasks-project-sync)共用同一套解析,別各寫一份。放共用區,兩模組都拿得到。
download_if_missing "system-dev/docs/4-guides/logseq-markers.md" "$REPO_URL/system-dev/docs/4-guides/logseq-markers.md"
MANIFEST_DIR="system-dev/.template-manifest.d"
mkdir -p "$MANIFEST_DIR"
# 工具版號:放 system-dev/,不寄生 .claude/。
download_if_missing "system-dev/VERSION" "$REPO_URL/system-dev/VERSION"
fetch_manifest() { # $1=名稱(common|repo|orchestrator
local name="$1" out="$MANIFEST_DIR/$1.tsv"
if curl -sSL "$REPO_URL/manifest/$name.tsv" -o "$out" 2>/dev/null && [ -s "$out" ]; then
return 0
fi
rm -f "$out"
return 1
}
# ── WIKI 模組 ─────────────────────────────────────
# wiki 是工具資料 → 放 system-dev/wiki/(不放 .claude/)。
# commands/ 與 hooks/ 是 CC 機制檔 → 維持 .claude/。
if $WANT_WIKI; then
create_dir "system-dev/wiki"
download_if_missing "system-dev/wiki/INDEX.md" "$REPO_URL/system-dev/wiki/INDEX.md"
download_if_missing "system-dev/wiki/TAXONOMY.md" "$REPO_URL/system-dev/wiki/TAXONOMY.md"
download_if_missing "system-dev/wiki/status.md" "$REPO_URL/system-dev/wiki/status.md"
download_if_missing "system-dev/wiki/mistakes.md" "$REPO_URL/system-dev/wiki/mistakes.md"
download_if_missing "system-dev/wiki/principles.md" "$REPO_URL/system-dev/wiki/principles.md"
download_if_missing "system-dev/wiki/.wikiignore" "$REPO_URL/system-dev/wiki/.wikiignore"
if ! fetch_manifest common; then
echo "❌ 抓不到安裝清單($REPO_URL/manifest/common.tsv)——網路或來源網址有問題,中止。" >&2
exit 1
fi
fetch_manifest "$PROFILE" || true # profile 專屬清單可以是空的
# wiki 改寫產物(AI 自讀定稿卡片)的正式落點:由工具建好,不靠用戶自救。
create_dir "system-dev/wiki/cards"
# src 前綴 → 實際來源網址
resolve_src() { # $1=src 欄
case "$1" in
T:*) printf '%s/%s' "$REPO_URL" "${1#T:}" ;;
S:*) printf '%s/%s' "$SCRIPTS_URL" "${1#S:}" ;;
*) printf '' ;;
esac
}
# 要不要裝這一行(依模組)
want_module() { # $1=module 欄
case "$1" in
core) return 0 ;;
wiki) $WANT_WIKI && return 0 || return 1 ;;
sdd) $WANT_SDD && return 0 || return 1 ;;
*) return 1 ;;
esac
}
# 逐行安裝。CLAUDE.mdclass=claude-md)不在這裡處理——它要三段組裝,走下方專段。
install_from_manifest() { # $1=manifest 檔
local f="$1" src dest class module profile url
[ -f "$f" ] || return 0
while IFS=$'\t' read -r src dest class module profile; do
case "$src" in ''|'#'*) continue ;; esac
[ -z "${dest:-}" ] && continue
want_module "$module" || continue
case "$class" in
dir)
create_dir "$dest"
;;
claude-md)
CLAUDE_MD_SRC="$(resolve_src "$src")" # 交給下方 CLAUDE.md 組裝段
;;
*)
url="$(resolve_src "$src")"
[ -z "$url" ] && continue
download_if_missing "$dest" "$url"
;;
esac
done < "$f"
}
CLAUDE_MD_SRC=""
install_from_manifest "$MANIFEST_DIR/common.tsv"
install_from_manifest "$MANIFEST_DIR/$PROFILE.tsv"
# wiki 卡片落點的 .gitkeep(目錄由 manifest 建,這顆種子檔留在腳本裡)
if $WANT_WIKI && [ -d "system-dev/wiki/cards" ]; then
[ -f "system-dev/wiki/cards/.gitkeep" ] || { : > "system-dev/wiki/cards/.gitkeep"; CREATED+=("system-dev/wiki/cards/.gitkeep"); }
download_if_missing ".claude/commands/wiki-init.md" "$REPO_URL/.claude/commands/wiki-init.md"
download_if_missing ".claude/commands/wiki-capture.md" "$REPO_URL/.claude/commands/wiki-capture.md"
download_if_missing ".claude/commands/wiki-update.md" "$REPO_URL/.claude/commands/wiki-update.md"
download_if_missing ".claude/commands/wiki-recall.md" "$REPO_URL/.claude/commands/wiki-recall.md"
# vault 增量萃取(Logseq/Obsidian → system-dev/wiki,冪等):給 Routine 反覆跑。
download_if_missing ".claude/commands/wiki-extract.md" "$REPO_URL/.claude/commands/wiki-extract.md"
# wiki 相關 hooks:接關 + 機敏掃描
download_if_missing ".claude/hooks/session-start-recall.sh" "$REPO_URL/.claude/hooks/session-start-recall.sh"
download_if_missing ".claude/hooks/wiki-secret-scan.sh" "$REPO_URL/.claude/hooks/wiki-secret-scan.sh"
# Coworkclaude.ai)整理 wiki 用的 skill:與 CC 的 /wiki-init 共用同一套規則
# (含 typed-edge、frontmatter 標籤、gloss)。沒這支 → claude.ai 來掃時身上沒規則。
download_if_missing "system-dev/docs/SKILL.md" "$REPO_URL/system-dev/docs/SKILL.md"
fi
# ── SDD 模組 ──────────────────────────────────────
if $WANT_SDD; then
create_dir "system-dev/docs/3-specs"
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/design.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/design.md"
download_if_missing "system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md" "$REPO_URL/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md"
download_if_missing "system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md" "$REPO_URL/system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md"
download_if_missing ".claude/commands/sdd-check.md" "$REPO_URL/.claude/commands/sdd-check.md"
download_if_missing ".claude/hooks/sdd-guard.sh" "$REPO_URL/.claude/hooks/sdd-guard.sh"
# ── tasks⇄Project 投影(optionalissue #16)──────────────────
# 帶檔 ≠ 啟用:workflow yaml 只是「留記錄+手動啟用素材」,啟用=對話答好且 acr push。
# 投影邏輯依附 tasks.md(住 3-specs),故隨 SDD 模組帶下來;裝了不代表開。
create_dir "system-dev/workflows"
download_if_missing "system-dev/workflows/tasks-project-sync.yaml" "$REPO_URL/system-dev/workflows/tasks-project-sync.yaml"
download_if_missing "system-dev/workflows/tasks-project-sync.local.sh" "$REPO_URL/system-dev/workflows/tasks-project-sync.local.sh"
fi
# ── 安裝/更新腳本:一開始就放進 system-dev/scripts/ ──
# 為什麼一開始就裝:之後要更新,用戶(或 CC)直接 `bash system-dev/scripts/update.sh`
# 不必每次都記那串 curl。腳本來源在 main/scripts/(不在 template/)。
create_dir "system-dev/scripts"
download_if_missing "system-dev/scripts/install.sh" "$SCRIPTS_URL/install.sh"
download_if_missing "system-dev/scripts/update.sh" "$SCRIPTS_URL/update.sh"
# ── 共用 hook:專案自訂禁令骨架(預設停用)────────
download_if_missing ".claude/hooks/pre-write-guard.sh" "$REPO_URL/.claude/hooks/pre-write-guard.sh"
# ── 共用指引:GitHub issue 處理(讀/回普世,跨 repo 發要先問,禁自動輪詢)──
download_if_missing ".claude/commands/issue-handle.md" "$REPO_URL/.claude/commands/issue-handle.md"
# ── scope 軸 marker:這個實例是哪一種 ────────────────
# 這是 role-lib.sh 判 scope 的**唯一**來源。AI 代裝路徑也必須寫出同格式的檔。
printf '%s\n' "$PROFILE" > "system-dev/.profile"
CREATED+=("system-dev/.profile ($PROFILE)")
chmod +x .claude/hooks/*.sh 2>/dev/null || true
chmod +x .claude/hooks/lib/*.sh 2>/dev/null || true
chmod +x system-dev/workflows/*.sh 2>/dev/null || true
chmod +x system-dev/scripts/*.sh 2>/dev/null || true
# ── 依模組產生 settings.json 的 hooks 區塊 ────────
# settings.json 因模組而異,不能直接下載單一靜態檔,改條件組裝。
@@ -336,19 +413,42 @@ build_hooks_json() {
session_hooks='{ "type": "command", "command": ".claude/hooks/session-start-recall.sh" }'
fi
# PreToolUse 依模組疊加
# PreToolUse 依模組疊加
# 順序原則:**範圍大的擋在前**——錯誤訊息才會指向最根本的那條規則,
# 而不是讓人先修一個表層問題、修完才發現底下還有一條。
# ① 改機制 這個檔你根本不該動(最外層)
# ② 角色 你這個身分不該寫這種檔
# ③ 位置 總管不該進成員 repo 動實作(僅總管實例)
# ④ 格式 檔可以寫,但寫進去的內容格式不對
# ⑤ 既有的 SDD/自訂禁令/機敏掃描
local pt=()
pt+=('{ "type": "command", "command": ".claude/hooks/install-artifact-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/role-guard.sh" }')
[ "$PROFILE" = "orchestrator" ] && pt+=('{ "type": "command", "command": ".claude/hooks/orchestrator-scope-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/jdd-format-guard.sh" }')
$WANT_SDD && pt+=('{ "type": "command", "command": ".claude/hooks/sdd-guard.sh" }')
pt+=('{ "type": "command", "command": ".claude/hooks/pre-write-guard.sh" }')
$WANT_WIKI && pt+=('{ "type": "command", "command": ".claude/hooks/wiki-secret-scan.sh" }')
local IFS=,
pretool_hooks="${pt[*]}"
printf '{\n "hooks": {\n'
# role 軸預設值:由 profile 決定,寫進 settings.json 的 env。
# 為什麼要寫死一個預設:AGENT_ROLE 沒設時 role-lib.sh 會「依 scope 推定」,
# 但推定是保險不是設計——明寫出來,人才看得見自己這個實例預設是什麼身分。
local default_role="engineer"
[ "$PROFILE" = "orchestrator" ] && default_role="orchestrator"
printf '{\n'
printf ' "env": { "AGENT_ROLE": "%s" },\n' "$default_role"
printf ' "hooks": {\n'
if [ -n "$session_hooks" ]; then
printf ' "SessionStart": [\n { "matcher": "startup|resume|clear",\n "hooks": [ %s ] }\n ],\n' "$session_hooks"
fi
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'
}
@@ -359,19 +459,75 @@ else
SKIPPED+=(".claude/settings.json $(tn '(已存在,請手動合併 hooks)' '(already exists — merge hooks manually)')")
fi
# ── CLAUDE.md只在完全不存在時建立 ────────────────
# 新建時把偵測到的 raw source 宣告 append 進去(在建立的當下寫入,
# 不回頭改使用者既有的 CLAUDE.md,維持「已有不覆蓋」原則)。
# ── CLAUDE.md由 profile 範本三段組裝 ────────────────
# 結構(界標用 HTML 註解:md 渲染看不見、grep 定位得到、CC 讀得到):
#
# <!-- 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() { # 跨平台取 sha256macOS 用 shasumLinux 多為 sha256sum
if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'
elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
else printf 'nohash'; fi
}
if [ ! -f "CLAUDE.md" ]; then
download_if_missing "CLAUDE.md" "$REPO_URL/CLAUDE.md"
if [ -f "CLAUDE.md" ]; then
emit_raw_source_block >> CLAUDE.md
CREATED+=("CLAUDE.md $(tn "← 已寫入 raw source 宣告(${VAULT_TYPE}" "← raw source declaration written (${VAULT_TYPE})")")
if [ -z "$CLAUDE_MD_SRC" ]; then
echo "⚠️ manifest 沒有指定 $PROFILE 的 CLAUDE.md 範本,略過憲法安裝" >&2
else
FW_TMP="$(mktemp)"
if curl -sSL "$CLAUDE_MD_SRC" -o "$FW_TMP" 2>/dev/null && [ -s "$FW_TMP" ]; then
FW_SHA="$(sdt_sha256 "$FW_TMP")"
TOOL_VER="$(tr -d '[:space:]' < system-dev/VERSION 2>/dev/null || echo 'unknown')"
{
printf '<!-- 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
else
SKIPPED+=("CLAUDE.md $(tn '(已存在,請手動加入對應區塊)' '(already exists — add the block manually)')")
SKIPPED+=("CLAUDE.md $(tn '(已存在,未覆蓋——要導入 profile 憲法請跑 update.sh)' '(already exists — run update.sh to adopt the profile constitution)')")
fi
# ── 產生 .template-manifest(漂移偵測的基準)────────────
# 記下每個安裝產物「安裝當下」的雜湊。update 時比對:
# 實檔 sha == 這裡的 sha → 乾淨,可安全覆蓋成新版
# 實檔 sha != 這裡的 sha → **被手改過**,不覆蓋、列進漂移清單
# 沒有這張表,就只能「不敢覆蓋」或「盲目覆蓋」二選一,兩個都錯。
{
printf '# dest\tclass\tversion\tsha256 —— 安裝當下的快照,供 update 判漂移用,勿手改\n'
TOOL_VER="$(tr -d '[:space:]' < system-dev/VERSION 2>/dev/null || echo 'unknown')"
for mf in "$MANIFEST_DIR/common.tsv" "$MANIFEST_DIR/$PROFILE.tsv"; do
[ -f "$mf" ] || continue
while IFS=$'\t' read -r m_src m_dest m_class m_module m_profile; do
case "$m_src" in ''|'#'*) continue ;; esac
[ -z "${m_dest:-}" ] && continue
[ "$m_class" = "dir" ] && continue
[ -f "$m_dest" ] || continue
printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$TOOL_VER" "$(sdt_sha256 "$m_dest")"
done < "$mf"
done
} > system-dev/.template-manifest
CREATED+=("system-dev/.template-manifest")
# ── 輸出結果 ──────────────────────────────────────
echo ""
t "✅ 建立了:" "✅ Created:"
+27
View File
@@ -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
+87
View File
@@ -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) 同步進常駐 mirrormirror 自己的 .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
+317 -14
View File
@@ -23,7 +23,11 @@ esac
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; }
REPO_RAW="https://raw.githubusercontent.com/uncle6me-web/system-dev-template/main"
# 來源預設=公開 GitHubleo 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"
UPDATED=()
@@ -123,13 +127,88 @@ if [ "$NEEDS_MIGRATE" = "yes" ]; then
echo ""
fi
# ── 漂移偵測(1.19.0 新增)─────────────────────────
# 病根:實例會手改安裝產物(hook、範本),而**沒有任何機制知道**。
# 實測基線(2026-08-05,拿一個真實使用中的實例對照框架):
# 6 支框架 hook 裡 **4 支已被手改**,另有 11 支是該實例自己發明的。
# 兩層後果:① 框架修好的 bug 送不到那些檔 ② 手改內容沒人審、沒回饋回框架
# ⇒ 同一個坑每個實例各踩一次。
#
# 判準:安裝當下的 sha(記在 system-dev/.template-manifestvs 現在的 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() {
local dest="$1" src="$2"
mkdir -p "$(dirname "$dest")"
MANAGED+=("$dest\toverwrite")
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
rm -f "$dest.tmp" # 內容相同,不算更新
else
@@ -141,7 +220,7 @@ update_file() {
t " ⚠️ 抓取失敗,保留原檔:$dest" " ⚠️ Download failed, keeping the original: $dest"
fi
else
if curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then
NEW+=("$dest") # 新功能:舊版沒有的檔
else
rm -f "$dest"
@@ -159,12 +238,19 @@ keep_file() {
# 不存在 → 抓範本下來(之後由使用者/CC 填);已存在 → 當用戶資料保留,絕不覆蓋。
add_if_missing() {
local dest="$1" src="$2"
MANAGED+=("$dest\tadd-if-missing")
if [ -f "$dest" ]; then
KEPT+=("$dest")
elif curl -sSL "$src" -o "$dest" 2>/dev/null && [ -s "$dest" ]; then
return 0
fi
# 🐛 修:原本少了 mkdir -p ⇒ 目標在**新目錄**時 curl 直接失敗,
# 但 VERSION 照樣升上去 = 典型假綠(2026-07 在一個實例升級時撞到,當時記「待回報 template」)。
mkdir -p "$(dirname "$dest")"
if curl -sSL "$src" -o "$dest" 2>/dev/null && ! looks_like_error_page "$dest"; then
NEW+=("$dest")
else
rm -f "$dest"
t " ⚠️ 抓取失敗:$dest" " ⚠️ Download failed: $dest"
fi
}
@@ -212,22 +298,32 @@ echo ""
# 改為:保留原檔不動,新版範本另存 pre-write-guard.template.sh,由使用者自行 diff 採納。
keep_with_template ".claude/hooks/pre-write-guard.sh" "$TEMPLATE_URL/.claude/hooks/pre-write-guard.sh"
# ── 舊版硬編清單(僅在抓不到 manifest 時使用的退路)──────────
# 保留它的唯一理由:來源不可達時,更新不該整個停擺。
# ⚠️ 新增產物請改 template/manifest/*.tsv**不要**再往這裡加——
# 這份清單就是當初漂移的來源。
legacy_update_list() {
# ── 模板/邏輯檔:覆蓋更新 ──────────────────────────
# 共用 hook 與指引
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
update_file ".claude/commands/issue-handle.md" "$TEMPLATE_URL/.claude/commands/issue-handle.md"
update_file "system-dev/VERSION" "$TEMPLATE_URL/system-dev/VERSION"
# Logseq 任務 marker 解析(單一真相源):vault 萃取(/wiki-extract)與 tasks→Project
# 投影共用同一套;任一模組在用就補/更新(邏輯檔,可覆蓋)。
if $HAS_WIKI || $HAS_SDD; then
if $HAS_WIKI || $HAS_SDD; then
update_file "system-dev/docs/4-guides/logseq-markers.md" "$TEMPLATE_URL/system-dev/docs/4-guides/logseq-markers.md"
fi
fi
if $HAS_WIKI; then
if $HAS_WIKI; then
# wiki 的「邏輯檔」:導航與 hooks,可覆蓋。wiki 資料在 system-dev/hooks/commands 留 .claude/。
update_file "system-dev/wiki/INDEX.md" "$TEMPLATE_URL/system-dev/wiki/INDEX.md"
update_file ".claude/hooks/session-start-recall.sh" "$TEMPLATE_URL/.claude/hooks/session-start-recall.sh"
update_file ".claude/hooks/wiki-secret-scan.sh" "$TEMPLATE_URL/.claude/hooks/wiki-secret-scan.sh"
# 1.16.0:讓 wiki 真的被讀到的兩支(開場 push 全文解決不了「只讀開頭」,見 CHANGELOG)
update_file ".claude/hooks/wiki-first-search.sh" "$TEMPLATE_URL/.claude/hooks/wiki-first-search.sh"
update_file ".claude/hooks/subagent-wiki-guard.sh" "$TEMPLATE_URL/.claude/hooks/subagent-wiki-guard.sh"
# 1.18.0:公開 mirror 落後偵測(手動同步的靜默失敗只有外部用戶會撞到)
update_file ".claude/hooks/publish-lag-check.sh" "$TEMPLATE_URL/.claude/hooks/publish-lag-check.sh"
update_file ".claude/commands/wiki-init.md" "$TEMPLATE_URL/.claude/commands/wiki-init.md"
update_file ".claude/commands/wiki-capture.md" "$TEMPLATE_URL/.claude/commands/wiki-capture.md"
update_file ".claude/commands/wiki-update.md" "$TEMPLATE_URL/.claude/commands/wiki-update.md"
@@ -245,9 +341,9 @@ if $HAS_WIKI; then
keep_file "system-dev/wiki/decisions-summary.md"
keep_file "system-dev/wiki/TAXONOMY.md"
keep_file "system-dev/wiki/.wikiignore"
fi
fi
if $HAS_SDD; then
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"
@@ -255,12 +351,95 @@ if $HAS_SDD; then
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
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
# ── 自我更新:把最新的 update.sh / install.sh 抓到 system-dev/scripts/ ──
# 這兩支在 main/scripts/ 下(不在 template/);落地位置新版收進 system-dev/scripts/。
update_file "system-dev/scripts/update.sh" "$REPO_RAW/scripts/update.sh"
@@ -270,7 +449,47 @@ chmod +x .claude/hooks/*.sh system-dev/scripts/*.sh system-dev/workflows/*.sh 2>
# ── 使用者資料檔:絕不碰,但提醒「設定可能有新欄位要手動補」──
keep_file ".claude/settings.json"
keep_file "CLAUDE.md"
# ── CLAUDE.md 界標補植(1.19.0)────────────────────────
# 1.18.x 以前的 CLAUDE.md 是「整份下載 + 往後 append」,**沒有任何區段界標**。
# 沒界標 ⇒ update 分不出「哪段是框架的、哪段是你寫的」⇒ 只能永遠不敢碰
# ⇒ 框架後來改的憲法**永遠送不到既有實例**。這一段就是來解這個死結。
#
# 保守到底,寧可什麼都不做也不弄丟一個字:
# · 已有界標 → 什麼都不做(冪等)
# · 沒有界標 → 現有全文**原封不動**包進本地補充區,框架區重鋪在它上面
# · 抓不到憲法範本 → 放棄補植、保留原檔(不留半殘的檔)
# 最壞情況是同一段內容在兩區各一份,由人自己刪——我們不代刪。
if [ -f "CLAUDE.md" ]; then
if grep -q 'sdt:framework begin' CLAUDE.md 2>/dev/null; then
keep_file "CLAUDE.md"
elif [ -n "${CLAUDE_MD_SRC:-}" ]; then
FW_TMP="$(mktemp)"
if curl -sSL "$CLAUDE_MD_SRC" -o "$FW_TMP" 2>/dev/null && ! looks_like_error_page "$FW_TMP"; then
cp CLAUDE.md "CLAUDE.md.before-markers"
{
printf '<!-- 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 ""
@@ -302,6 +521,29 @@ if [ ${#UPDATED[@]} -gt 0 ]; then
t "⬆️ 已更新(覆蓋成新版):" "⬆️ Updated (overwritten with the new version):"
for f in "${UPDATED[@]}"; do echo " ~ $f"; done
fi
if [ ${#DRIFTED[@]} -gt 0 ]; then
echo ""
t "⚠️ 下列 ${#DRIFTED[@]} 個檔被手改過,這一版**沒有**覆蓋它們:" \
"⚠️ ${#DRIFTED[@]} file(s) were modified by hand and were NOT overwritten:"
for f in "${DRIFTED[@]}"; do
if [ -f "$f.new" ]; then
t "$f → 新版已放在 $f.new,請 diff" \
"$f → new version saved as $f.new — please diff"
else
t "$f (新版抓取失敗,稍後再試)" \
"$f (couldn't fetch the new version; try again later)"
fi
done
echo ""
t " 這代表什麼:你(或某個 CC)改過框架發下來的檔。" \
" What this means: you (or a CC) edited a file that the framework ships."
t " 兩條路,擇一——別放著不管,放著=框架之後的修正永遠送不到這個檔:" \
" Pick one — leaving it means future framework fixes will never reach this file:"
t " ① 把你的改動寫成框架提案(推薦)——一次修全家,別人也拿得到" \
" 1. Turn your change into a framework proposal (recommended) — fixes it for everyone"
t " ② 放棄本地改動:mv <檔>.new <檔>" \
" 2. Drop your local change: mv <file>.new <file>"
fi
if [ ${#NEW[@]} -eq 0 ] && [ ${#UPDATED[@]} -eq 0 ]; then
echo ""
t "✨ 模板邏輯檔已全部最新,無需變動。" \
@@ -330,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 "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")
# ── 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
echo ""
t "📌 settings.json 是你的設定(沒動),但偵測到缺以下 hook,請手動補上:" \
@@ -338,6 +613,34 @@ if [ -f ".claude/settings.json" ]; then
fi
fi
# ── 重寫漂移基準快照(manifest)──────────────────────
# 兩個作用:
# ① 舊實例(1.18.x 以前沒有 manifest)→ 這一輪之後就有了,下次起就能偵測漂移。
# 注意本輪它們**不會**被判漂移(查無登記=不判定),這是刻意的保守:
# 沒有基準就宣稱「你改過」等於瞎猜。第一輪建基準,第二輪起才有話語權。
# ② 已有 manifest → 把「本輪更新過的檔」的 sha 推進到新值。
# 少了這步就會自己造一個 bug:框架自己覆蓋的檔,下一輪被誤判成使用者手改。
# 漂移中的檔**不更新其基準**——它們仍然是「相對於安裝版被改過」,下次還要繼續報。
{
printf '# dest\tclass\tversion\tsha256 —— update 維護的漂移基準,勿手改\n'
for entry in ${MANAGED[@]+"${MANAGED[@]}"}; do
m_dest="$(printf '%b' "$entry" | cut -f1)"
m_class="$(printf '%b' "$entry" | cut -f2)"
[ -f "$m_dest" ] || continue
skip=""
for d in ${DRIFTED[@]+"${DRIFTED[@]}"}; do
[ "$d" = "$m_dest" ] && skip="yes" && break
done
if [ -n "$skip" ]; then
# 保留原登記值(它還在漂移中,基準不能跟著漂)
old="$(manifest_sha "$m_dest")"
[ -n "$old" ] && printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$LOCAL_VER" "$old"
continue
fi
printf '%s\t%s\t%s\t%s\n' "$m_dest" "$m_class" "$REMOTE_VER" "$(sdt_sha256 "$m_dest")"
done
} > "$MANIFEST_FILE.tmp" && mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
echo ""
t "🚀 更新完成:${LOCAL_VER}${REMOTE_VER}" "🚀 Update complete: ${LOCAL_VER}${REMOTE_VER}"
t " 下次更新直接跑:bash system-dev/scripts/update.sh" " Next time, just run: bash system-dev/scripts/update.sh"
+1 -1
View File
@@ -1 +1 @@
1.14.0
1.19.0
+4 -4
View File
@@ -44,13 +44,13 @@ issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆
## 3. 跨 repo 署名(鐵律 — 絕不可漏)
所有 repomira / graph-plugin / ingest-plugin / Arcrun / template…)共用**同一個 GitHub 帳號**發 issue/comment
所以 issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**
當你的多個 repo 共用**同一個託管帳號**發 issue/comment
issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**
> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。
- 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`
- 總管下令/追問:`[InkStoneCo 總管]`
- 收件方 CC 回報:`[<收件 repo 名> CC]`(例:`[<某子系統> CC]`
- 上游/總管下令追問:`[<上游 repo 名> 總管]`
- 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。
為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**
+20
View File
@@ -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
⚖️ 待裁決 proposalM
```
若出現**兩份 active=規則已被違反,當場糾正**(收斂到一份,其餘 paused/closed)。
---
## 執行流程
### 第一步:理解任務
+2 -2
View File
@@ -2,7 +2,7 @@
把**筆記 vault**Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地
萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**
AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 KBDB
AI 只產卡片檔,下游 ingest 管線再從 wikilink 機械拉三元組進知識庫
> **跟 `/wiki-init` 的分工**
> - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。
@@ -12,7 +12,7 @@ AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進
> **邊界(硬規矩,別越界)**
> - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游
> Arcrun 從你產的 `[[wikilink]]` + `## 關聯` 機械映射(另一張 issue,不是這支的事。
> 下游 ingest 管線從你產的 `[[wikilink]]` + `## 關聯` 機械映射,不是這支的事。
> - **原始筆記唯讀**`journals/``pages/`、Obsidian 根 `.md` 是 leo 的手寫真身,
> 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。
> - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。
@@ -0,0 +1,92 @@
#!/bin/bash
# install-artifact-guard.sh — 實例不准改機制(分離規格 防糾纏閘 S1)
#
# 掛 PreToolUsematcher: 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
+167
View File
@@ -0,0 +1,167 @@
#!/bin/bash
# jdd-format-guard.sh — PM 軌文件的格式閘(JDD 規則 J4+J5+J8)
#
# 掛 PreToolUsematcher: Write|Edit|MultiEdit)。
#
# J4 root.md 的 🔴 卡缺【要驗證 + 對帳日】 → 擋
# 為什麼:紅卡=賭注。賭注沒有「賭輸了怎麼知道」和「什麼時候結算」,
# 就會永遠是「還在做」,永遠不必認賠——那不是賭注,是藉口。
#
# J5 tasks.md **新增**的任務缺站號 → 擋
# 為什麼:sprint 的單位是站。任務不掛站,就沒有人答得出
# 「這個任務不做,哪一站會掛?」——那正是認領流程唯一的問題。
#
# J8 root.mdjourneys.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
# ── J8root.mdjourneys.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
+172
View File
@@ -0,0 +1,172 @@
#!/bin/bash
# role-lib.sh — 兩軸身分判定函式庫(被 source,不獨立掛 hook
#
# 出處:《分離導入規格》第二節「兩軸身分(scope × role,全部機械判定,agent 不自我判斷)」
# +《JDD 導入規格》第四節「角色與權限」。
#
# ── 為什麼是「兩軸」而不是「一個角色設定」──────────────────────
# scope(我在誰的地盤)=**安裝位置**決定:agent 在哪個資料夾醒來,
# 就只讀得到那部憲法。來源= system-dev/.profileinstall 時寫死)。
# 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。PMorchestrator)的動作——改 root.mdjourneys.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 ────────────────────────
# 三段 fallbackjq → 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)"
}
+81
View File
@@ -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)
#
# 掛 PostToolUseWrite|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
+153
View File
@@ -0,0 +1,153 @@
#!/bin/bash
# role-guard.sh — 角色封路:誰能寫什麼(JDD 規則 J1+J2+J3)
#
# 掛 PreToolUsematcher: 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
}
# ── J1J2orchestrator 的禁區 ─────────────────────
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.mdroot.md/考題檔是 PM 的檔案,engineer 只能讀當期指定站的段落。
正確做法:
· 考題沒過 → 去把東西做對,不是把題目改掉
· 真的認為題目出錯了(判準不合理、站定義有誤)→ **回報 PM**,由他決定改不改
· 你需要的是「這站到底要什麼」→ 讀,不要寫
為什麼這條是命門:考題沒過的人只要改一下考題就「過了」,
而且沒有任何人會發現。這樣整套驗收就是裝飾品。"
fi
# md 檔本身不是考卷,但內容含考題 → 一樣擋(考題常寄住在別的文件裡)
case "$REL" in
*.md)
if content_has_gherkin; then
block "J3" "考生不能改考卷(這次寫入的內容含考題)。" \
" 這個檔名不是考卷,但你要寫進去的內容裡有 Gherkin 考題(GivenWhenThen 或 G-x.y)。
正確做法:
· 要記錄「我做到哪了」→ 寫 wiki/status,不要在文件裡複寫考題
· 要提出新考題 → 回報 PM,由他寫進 journeys.md
為什麼擋:考題散落成兩份就會不同步,
而不同步的那一刻起,「全綠」代表什麼就沒人說得準了。"
fi
;;
esac
fi
exit 0
+82 -14
View File
@@ -1,12 +1,18 @@
#!/bin/bash
# PreToolUse hook — 動 code 前檢查有沒有對應 SDD
# PreToolUse hook — 動 code 前檢查 SDD 單一活性 SDD 鐵律(issue #6
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
# 生命週期規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md
#
# 掛在 settings.json 的 PreToolUsematcher: Write|Edit)。
# 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 繞道的改動擋不到。
# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。
@@ -24,6 +30,42 @@ fi
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
[ -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 / closedclosed 且被取代者填 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/設定檔等放行。
case "$FILE_PATH" in
*.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 ;;
esac
# system-dev/docs/3-specs/ 下完全沒有 design.md → 攔
SDD_COUNT=0
if [ -d "system-dev/docs/3-specs" ]; then
SDD_COUNT=$(find system-dev/docs/3-specs -name 'design.md' -not -path '*TEMPLATE*' 2>/dev/null | wc -l | tr -d ' ')
fi
# ── 向下相容:整個 3-specs 沒有任何帶 frontmatter 的 design.md ──
# =老 repo 還沒遷移生命週期制度 → 退回舊行為(有 design.md 就放行+提醒),
# 避免 template update 一裝新 hook,老 repo 所有 code 寫入立刻全紅。
if [ "$FM_COUNT" -eq 0 ]; then
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
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 system-dev/docs/3-specs/ 下找不到任何 SDD。
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下找不到任何 SDD。
絕對鐵律:任何 code 變動前必須有對應 SDDdesign.md
絕對鐵律:任何 code 變動前必須有對應 SDDdesign.md,且遵守單一活性生命週期
system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
請先:
1. 確認這個改動屬於哪個子系統
2. 在 system-dev/docs/3-specs/[子系統]/ 建立 design.md(可用 /sdd-check 協助)
2. 在 ${SPECS_DIR}/[子系統]/ 建立 design.md(可用 /sdd-check 協助)frontmatter 標 status: active
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、改文字)若確定豁免,請明確說明範圍後由人放行。
EOF
exit 2
fi
# 有 SDD:放行,留痕提醒要宣告(stderr 警告,不擋)
echo "📋 提醒:system-dev/docs/3-specs/ 下有 SDD。動手前請確認已讀對應 design.md 並在回覆宣告。" >&2
# 恰好 1 份 active:放行,留痕提醒要宣告(stderr 警告,不擋)
printf '📋 提醒:現行 active SDD\n%s動手前請確認已讀它的 design.md、對應到 tasks,並在回覆宣告。\n' "$ACTIVE_LIST" >&2
exit 0
@@ -26,6 +26,35 @@ if [ -d ".claude/wiki" ] && [ ! -f "$STATUS_FILE" ]; then
exit 0
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 的專案
if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then
exit 0
@@ -0,0 +1,56 @@
#!/bin/bash
# station-done-guard.sh — 收工判準是「站的考題全綠」,不是「任務全關」(JDD 規則 J6)
#
# 掛 Stop / SubagentStop(收工那一刻),只在 orchestrator 實例生效。
#
# ── 它在攔什麼 ──────────────────────────────────
# 「任務都關了 ⇒ 這期做完了」——這句話是整套假綠的源頭。
# 任務是**技術軌**的單位(沿系統結構切),站是**PM 軌**的單位(沿人的經歷切)。
# 零件全部做完、每個都對,人還是可能掉進零件之間的縫裡。
# 所以收工只認一件事:**指定站的考題有沒有實測通過**。
#
# ── 它不做什麼(重要)────────────────────────────
# 它**不會**自己去跑考題判斷過沒過——Gherkin 的 Then 寫的是「使用者看到什麼」,
# 那本來就不是 shell 判得出來的。它做的是:在你要收工的那一刻,
# 把「指定站」和「它們現在的狀態」攤在你眼前,逼你面對還沒填的那幾格。
# 自動判綠反而危險:那會製造一個「機器說過了」的假權威。
#
# 誠實限制:讀的是 sprint.mdjourneys.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
View File
@@ -0,0 +1,100 @@
#!/bin/bash
# subagent-wiki-guard.sh — PreToolUse(Task) hooksubagent 聽到「查」就自己先查 wiki
#
# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEYprompt 都只叫它「去查 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
+112
View File
@@ -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 用 patternGlob/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:
# Bash2026-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
+27
View File
@@ -9,6 +9,15 @@
"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": [
@@ -28,6 +37,24 @@
"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
View File
@@ -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 — [專案名稱]
> 導航牌。細節在兩個地方,不在這裡。
@@ -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,不引入其他框架]
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 過時是債,要還)。
**動外部系統(部署/curlwrangleracrgh)前**:先找 repo 有沒有**現成腳本或 README 部署段**
別自創方法。(實例:2026-07-21 明明有 `npx wrangler deploy` 這條驗過的路,卻自己 curl 硬幹踩坑。)
> hook `wiki-first-search.sh` 會在你查 code/下高風險指令時自動推 wiki 命中行;
> **但機制只是提醒,判斷是你的責任**
## Wiki 讀取順序
| 檔案 | 時機 | 用途 |
+99
View File
@@ -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 這裡一律 commonrepo/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
1 # common.tsv — 兩個 profile 都要裝的產物(安裝清單的單一真相源)
2 #
3 # 為什麼有這張表(SDD jdd-dual-profile 決策 D1):
4 # 在這之前,安裝清單**硬編在 install.sh,另一份幾乎重複的硬編在 update.sh**。
5 # 兩份手抄的清單必然漂移——而且已經漂了:
6 # · wiki-first-search.sh (1.16.0 頭條功能)→ update 會裝,**install 不裝**
7 # · subagent-wiki-guard.sh (1.17.0 頭條功能)→ update 會裝,**install 不裝**
8 # · publish-lag-check.sh (1.18.0 頭條功能)→ update 會裝,**install 不裝**
9 # · wiki/decisions-summary.md → update 保留它,**install 不建**,
10 # 但 CLAUDE.md 範本的「Wiki 讀取順序」表裡就列著它 ⇒ 新用戶開檔即撲空
11 # ⇒ **乾淨安裝反而拿不到最新三版的招牌功能**,只有「先裝舊版再 update」的人拿得到。
12 # 本表讓兩支腳本讀同一份清單,這類漂移在結構上不可能再發生。
13 #
14 # ── 欄位 ──────────────────────────────────────────────
15 # 1 src 來源。T:<路徑> = template/ 底下;S:<路徑> = scripts/ 底下;- = 建目錄用
16 # 2 dest 安裝到實例的哪裡(相對實例根)
17 # 3 class overwrite 模板/邏輯檔,新版直接蓋(使用者不會手改)
18 # keep 使用者資料檔,**永遠不動**(蓋掉=清空他的記憶)
19 # add-if-missing 使用者資料檔,缺了才補,已有絕不覆蓋
20 # keep-with-template 使用者會手填的客製檔;保留原檔,新版另存 .template.<ext>
21 # claude-md 特殊:CLAUDE.md 走「框架區+本地補充區」組裝,見 install.sh
22 # dir 只建目錄
23 # 4 module core | wiki | sdd —— 對應 install.sh 的 --wiki/--sdd/--all 模組選擇
24 # 5 profile 這裡一律 common(repo/orchestrator 專屬的在各自的 tsv)
25 #
26 # ⚠️ 路徑契約:dest 一旦發佈就是對既有實例的承諾,**只增不移**
27 # (舊實例跑的是舊腳本,路徑寫死;搬檔=它們整排 404、更新機制本身壞掉)。
28 # 要改版面先跑 scripts/check-legacy-paths.sh。
29 #
30 #src dest class module profile
31 # ── 目錄骨架 ──
32 - system-dev/docs/1-vision dir core common
33 - system-dev/docs/2-architecture/decisions dir core common
34 - system-dev/docs/4-guides dir core common
35 - system-dev/docs/5-records/incidents dir core common
36 - system-dev/docs/5-records/test-reports dir core common
37 - system-dev/docs/6-user dir core common
38 - system-dev/scripts dir core common
39 - .claude/commands dir core common
40 - .claude/hooks dir core common
41 - .claude/hooks/lib dir core common
42 # ── core:兩個模組都要的底盤 ──
43 T:system-dev/docs/README.md system-dev/docs/README.md overwrite core common
44 T:system-dev/docs/4-guides/logseq-markers.md system-dev/docs/4-guides/logseq-markers.md overwrite core common
45 T:system-dev/VERSION system-dev/VERSION overwrite core common
46 S:install.sh system-dev/scripts/install.sh overwrite core common
47 S:update.sh system-dev/scripts/update.sh overwrite core common
48 T:.claude/commands/issue-handle.md .claude/commands/issue-handle.md overwrite core common
49 T:.claude/hooks/pre-write-guard.sh .claude/hooks/pre-write-guard.sh keep-with-template core common
50 T:.claude/hooks/lib/role-lib.sh .claude/hooks/lib/role-lib.sh overwrite core common
51 T:.claude/hooks/publish-lag-check.sh .claude/hooks/publish-lag-check.sh overwrite core common
52 # ── JDD 封路 hook(角色軸屬 common,兩個 profile 都裝)──
53 # 為什麼 role-guard 不按 profile 分裝(設計決策 D5):
54 # 總管 repo 裡也會派 engineer subagent(身分矩陣右上角那一格)。
55 # 若「考生不能改考卷」只裝在成員 repo,那顆 subagent 在總管 repo 裡就改得動考卷
56 # ——而總管 repo 正是考卷所在地,等於這條規則在最該生效的地方失效。
57 T:.claude/hooks/install-artifact-guard.sh .claude/hooks/install-artifact-guard.sh overwrite core common
58 T:.claude/hooks/role-guard.sh .claude/hooks/role-guard.sh overwrite core common
59 T:.claude/hooks/jdd-format-guard.sh .claude/hooks/jdd-format-guard.sh overwrite core common
60 T:.claude/hooks/station-done-guard.sh .claude/hooks/station-done-guard.sh overwrite core common
61 T:.claude/hooks/regression-scope.sh .claude/hooks/regression-scope.sh overwrite core common
62 # ── wiki 模組 ──
63 - system-dev/wiki dir wiki common
64 - system-dev/wiki/cards dir wiki common
65 T:system-dev/wiki/INDEX.md system-dev/wiki/INDEX.md overwrite wiki common
66 T:system-dev/wiki/TAXONOMY.md system-dev/wiki/TAXONOMY.md keep wiki common
67 T:system-dev/wiki/status.md system-dev/wiki/status.md keep wiki common
68 T:system-dev/wiki/mistakes.md system-dev/wiki/mistakes.md keep wiki common
69 T:system-dev/wiki/principles.md system-dev/wiki/principles.md add-if-missing wiki common
70 T:system-dev/wiki/decisions-summary.md system-dev/wiki/decisions-summary.md add-if-missing wiki common
71 T:system-dev/wiki/.wikiignore system-dev/wiki/.wikiignore keep wiki common
72 T:system-dev/docs/SKILL.md system-dev/docs/SKILL.md overwrite wiki common
73 T:.claude/commands/wiki-init.md .claude/commands/wiki-init.md overwrite wiki common
74 T:.claude/commands/wiki-capture.md .claude/commands/wiki-capture.md overwrite wiki common
75 T:.claude/commands/wiki-update.md .claude/commands/wiki-update.md overwrite wiki common
76 T:.claude/commands/wiki-recall.md .claude/commands/wiki-recall.md overwrite wiki common
77 T:.claude/commands/wiki-extract.md .claude/commands/wiki-extract.md overwrite wiki common
78 T:.claude/hooks/session-start-recall.sh .claude/hooks/session-start-recall.sh overwrite wiki common
79 T:.claude/hooks/wiki-secret-scan.sh .claude/hooks/wiki-secret-scan.sh overwrite wiki common
80 T:.claude/hooks/wiki-first-search.sh .claude/hooks/wiki-first-search.sh overwrite wiki common
81 T:.claude/hooks/subagent-wiki-guard.sh .claude/hooks/subagent-wiki-guard.sh overwrite wiki common
82 # ── sdd 模組 ──
83 - system-dev/docs/3-specs dir sdd common
84 - system-dev/workflows dir sdd common
85 T:system-dev/docs/3-specs/TEMPLATE-sdd/design.md system-dev/docs/3-specs/TEMPLATE-sdd/design.md overwrite sdd common
86 T:system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md overwrite sdd common
87 T:system-dev/docs/3-specs/SDD-LIFECYCLE.md system-dev/docs/3-specs/SDD-LIFECYCLE.md overwrite sdd common
88 T:system-dev/docs/3-specs/pending-changes.md system-dev/docs/3-specs/pending-changes.md add-if-missing sdd common
89 T:system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md system-dev/docs/2-architecture/decisions/TEMPLATE-adr.md overwrite sdd common
90 T:.claude/commands/sdd-check.md .claude/commands/sdd-check.md overwrite sdd common
91 T:.claude/hooks/sdd-guard.sh .claude/hooks/sdd-guard.sh overwrite sdd common
92 T:scripts/sdd-active-check.sh system-dev/scripts/sdd-active-check.sh overwrite sdd common
93 T:system-dev/workflows/tasks-project-sync.yaml system-dev/workflows/tasks-project-sync.yaml overwrite sdd common
94 T:system-dev/workflows/tasks-project-sync.local.sh system-dev/workflows/tasks-project-sync.local.sh overwrite sdd common
+22
View File
@@ -0,0 +1,22 @@
# orchestrator.tsv — 「總管」profile 專屬產物
#
# 這個 profile 是誰:管一群成員 repo 的上層資料夾(PM/總管)。它的憲法效忠
# root.md journeys.mdPM 軌),裡面的 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
1 # orchestrator.tsv — 「總管」profile 專屬產物
2 #
3 # 這個 profile 是誰:管一群成員 repo 的上層資料夾(PM/總管)。它的憲法效忠
4 # root.md + journeys.md(PM 軌),裡面的 agent 預設身分是 orchestrator
5 # (可寫 root/journeys/sprint,**禁寫 code、禁改 tasks/requirements/design**)。
6 #
7 # ⚠️ 四份 JDD 文件的 class 是 add-if-missing,這是刻意的:
8 # 給你一份可以直接動手填的骨架,但**一旦你填了內容,update 永遠不會覆蓋**。
9 # 理由:這幾份是**實例資料**不是框架邏輯——裡面寫的是你的需求、你的旅程、你的站。
10 # 來源檔名帶 .template 後綴、落地時去掉:來源是範本,落地就是你的文件了。
11 #
12 # 欄位定義見 common.tsv。
13 #
14 #src dest class module profile
15 T:profiles/orchestrator/CLAUDE.md CLAUDE.md claude-md core orchestrator
16 T:profiles/orchestrator/docs/root.md.template system-dev/docs/root.md add-if-missing core orchestrator
17 T:profiles/orchestrator/docs/journeys.md.template system-dev/docs/journeys.md add-if-missing core orchestrator
18 T:profiles/orchestrator/docs/sprint.md.template system-dev/docs/sprint.md add-if-missing core orchestrator
19 T:profiles/orchestrator/docs/triage-map.md.template system-dev/docs/triage-map.md add-if-missing core orchestrator
20 T:profiles/orchestrator/docs/plugin-load-order.md system-dev/docs/plugin-load-order.md overwrite core orchestrator
21 T:profiles/orchestrator/hooks/orchestrator-scope-guard.sh .claude/hooks/orchestrator-scope-guard.sh overwrite core orchestrator
+15
View File
@@ -0,0 +1,15 @@
# repo.tsv — 「成員 repo」profile 專屬產物
#
# 這個 profile 是誰:一個實際寫 code 的子專案 repo。它的憲法效忠 requirements.mdSDD 技術軌),
# 它裡面的 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
1 # repo.tsv — 「成員 repo」profile 專屬產物
2 #
3 # 這個 profile 是誰:一個實際寫 code 的子專案 repo。它的憲法效忠 requirements.md(SDD 技術軌),
4 # 它裡面的 agent 預設身分是 engineer(可寫 code / tasks / requirements / design,禁改考卷)。
5 #
6 # 為什麼這張表這麼短:SDD 三件式、repo 級 wiki 規範等**本來就在 common**——
7 # 它們是兩個 profile 共用的底盤。真正只屬於 repo profile 的,就是那部憲法本身。
8 # (分離導入規格 §3 把「SDD 三件式」畫在 profiles/repo/ 底下,但實際上總管 repo 也要用
9 # SDD 三件式來寫它自己的規格 ⇒ 那是 common 不是 repo 專屬。見 design 決策 D5 同理。)
10 #
11 # 欄位定義見 common.tsv。
12 #
13 #src dest class module profile
14 T:profiles/repo/CLAUDE.md CLAUDE.md claude-md core repo
+129
View File
@@ -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.xG-b.x」 |
| 可寫 | `root.md``journeys.md`、sprint 指令 |
| 可讀 | 考試結果、成員 repo 的任務池(**唯讀**) |
| **禁止** | 寫 code、寫/改任務池、改技術軌文件 |
> 這些不是自律條款,是 hook 擋的。你寫不進去,不用試。
---
## 術語表(全域統一,禁用舊詞)
| 用這個 | 意思 | 禁用 |
|---|---|---|
| **Journey** | 一個角色的一條情境 = 一場考試 = 一個 sprint 的脊椎 | ~~CPCritical Path~~(排程術語,會把人帶回瀑布思維) |
| **Station(站)** | 跨 Journey 共享的能力資產,全域編號(S1, S2…),每站掛一組 Gherkin | ~~milestonephase~~ |
| **通關(clear** | 一條 Journey 所有站的 Gherkin 全綠 | ~~donecomplete~~ |
| **點亮(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)。engineersubagent **禁改本檔與任何考題**——考生不能改考卷。
> **標記**:🟢 考過即關 / 🔴 考過轉「對帳中」(站上附真實世界判準與對帳日)
> **站全域編號**,跨 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。engineersubagent 寫這個檔會被 hook 擋下——考生不能改考卷。
考題不過就去把東西做對,不是去改考題。
════════════════════════════════════════════════════════ -->
@@ -0,0 +1,58 @@
# 政策包(policy pack)= 官方 plugin,以及它什麼時候被載入
> 這份是**約定**,不是實作。它回答一件事:機制是怎麼在 agent 醒來之前就已經在場的。
---
## 鐵律:政策包一律做成 Claude Code 官方 plugin
- 框架**不發明平行的外掛格式**。官方 plugin 已經涵蓋 hooksskillsagentscommands
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。engineersubagent 寫這個檔會被 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。掛 PreToolUseWrite|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=1subagent 標籤;總管主 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
+113
View File
@@ -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 -->
+49
View File
@@ -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.mddesign.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 / closedclosed 且被取代者填 superseded_by 並移入 archive/)。
規則見 system-dev/docs/3-specs/SDD-LIFECYCLE.md。
EOF
exit 1
fi
exit 0
+1 -1
View File
@@ -1 +1 @@
1.14.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
> 狀態:[草稿 / 審核中 / 已採納 / 已廢棄]
> 建立:[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 日期。)
@@ -18,7 +18,7 @@
Logseq 的原生任務**不是** GFM 的 `- [ ]` / `- [x]`,而是**大寫 marker 開頭的 block**
```
- TODO AI 查看 leo21c 內所有 Repo找到本地 Repo 搬到 Gitea
- TODO AI 盤點這台機器上所有 Repo本地 Repo 搬到託管站
- DOING 建立知識總庫,可查所有子庫
- DONE 手機和電腦 Logseq 可以被放進知識總庫
```
@@ -3,6 +3,10 @@
#
# 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md
#
# ⚠️ sdt-instance-name-ok-file — 檔級豁免(實例專名檢查)
# 理由:與 tasks-project-sync.yaml 同一組 **L2 政策包產物**(綁定某一家的工作流引擎)。
# 帳記在這:**W3(政策包 plugin 化)時與 yaml 一起移出 template/**,屆時本豁免刪除。
#
# ── 為什麼有這支(職責邊界)──────────────────────────────────────
# arcrun workflow 跑在遠端 CF Workers,沒有本地 fs / git / shell。
# 所以「讀 tasks.md / git diff / 回寫 <!-- gh:id -->」這三件本地事
@@ -2,6 +2,12 @@
#
# 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md
#
# ⚠️ sdt-instance-name-ok-file — 檔級豁免(實例專名檢查)
# 理由:整支是 **L2 政策包產物**,不是 L1 框架內容——它綁定某一家的工作流引擎,
# 「換一家公司就不成立」。它現在還躺在框架裡只是歷史包袱。
# 帳記在這:**W3(政策包 plugin 化)時整組移出 template/**,屆時本豁免一併刪除。
# 在那之前不做假性清理(把引擎名改寫成模糊詞=資訊消失、分層問題還在)。
#
# ── 這份 workflow 的職責邊界(很重要,別搞混)──────────────────
# arcrun workflow 在 Cloudflare Workers / WASM 上「遠端」執行,沒有本地檔案系統、
# 沒有 git、沒有 shell。所以「讀 tasks.md / 跑 git diff / 把 <!-- gh:id --> 回寫 md」
@@ -17,7 +23,7 @@
# 單向:只寫 GitHub,永不回改 tasks.md(回寫 id 是本地端的事,且只在新 task 做一次)。
#
# ── 輸入(由本地觸發端用 acr run -i 餵)──────────────────────────
# owner GitHub repo owner(例:uncle6me-web
# owner GitHub repo owner(例:your-github-account
# repo GitHub repo 名
# project_id GitHub Projects v2 的 node id(投影目標,唯讀看板)
# tasks_json 本地分類好的增量陣列,每筆形如: