diff --git a/direct.go b/direct.go index bd0563f..2e1b2f2 100644 --- a/direct.go +++ b/direct.go @@ -221,7 +221,13 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool) ([]DirectRes } payload, err := Scan(absRoot, m, ScanOptions{ MaxRemovedRatio: cfg.MaxRemoved, - SkipPaths: map[string]bool{absManifest: true}, + SkipPaths: map[string]bool{ + absManifest: true, + // template 代裝的根層 CLAUDE.md 是 CC 設定檔,永遠不是用戶知識(task 2) + filepath.Join(absRoot, "CLAUDE.md"): true, + }, + // template 代裝後 system-dev/(wiki 產物區)不得被當原稿掃進 ingest(task 2) + SkipDirNames: map[string]bool{"system-dev": true}, }) if err != nil { return append(results, DirectResult{Status: "failed", Error: err.Error()}), 1, nil @@ -332,6 +338,21 @@ func runDirect(args []string) int { return exit } + // 四步定稿第 1 步:daemon 代裝 template——常駐看守前確保每根都鋪好(冪等,不覆寫既有檔)。 + // dry-run/--once 測試情境不代裝(不留副作用),由 template-install 子命令顯式做。 + if !*once && !*dryRun { + for _, root := range cfg.Folders() { + if TemplateInstalled(root) { + continue + } + if res, ierr := InstallTemplate(root); ierr != nil { + fmt.Fprintf(os.Stderr, "template 代裝失敗(%s):%v\n", root, ierr) + } else { + fmt.Fprintf(os.Stderr, "template v%s 已鋪進 %s(新 %d 檔)\n", res.Version, root, len(res.Installed)) + } + } + } + if *once { return runOne() } diff --git a/main.go b/main.go index f0ab7a3..78ec359 100644 --- a/main.go +++ b/main.go @@ -53,6 +53,8 @@ func main() { os.Exit(run(os.Args[2:], runMode{withUpload: true, withTrigger: true})) case "direct": os.Exit(runDirect(os.Args[2:])) + case "template-install": + os.Exit(runTemplateInstall(os.Args[2:])) default: usage() os.Exit(2) diff --git a/scan.go b/scan.go index 710a465..9be8ba2 100644 --- a/scan.go +++ b/scan.go @@ -60,6 +60,9 @@ type ScanOptions struct { MaxRemovedRatio float64 // SkipPaths:絕對路徑黑名單(如 manifest 檔自己住在 root 底下時)。 SkipPaths map[string]bool + // SkipDirNames:目錄名黑名單(任一層命中整棵跳過)。daemon-beta task 2: + // template 代裝後 `system-dev/`(wiki 產物區)不得被當成原稿掃進 ingest。 + SkipDirNames map[string]bool } const DefaultMaxRemovedRatio = 0.4 @@ -108,6 +111,9 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) { if p != root && strings.HasPrefix(name, ".") { return filepath.SkipDir // 隱藏目錄(.git、.obsidian…)整棵跳過 } + if p != root && opts.SkipDirNames[name] { + return filepath.SkipDir // 名單目錄(system-dev…)整棵跳過 + } return nil } if strings.HasPrefix(name, ".") { diff --git a/template_install.go b/template_install.go new file mode 100644 index 0000000..b103268 --- /dev/null +++ b/template_install.go @@ -0,0 +1,116 @@ +// template_install.go — daemon 代裝 system-dev-template(daemon-beta task 2)。 +// +// leo 四步定稿第 1 步:「幫你在指定資料夾安裝我們現有的 template,這個測過很多次, +// 但要讓 daemon 可用」——即**不走 git clone**:template 快照(templatefs/,vendored) +// 打包進二進位,daemon 直接鋪檔。 +// +// 冪等鐵律:**已存在的檔案一律不覆寫**(用戶的 status.md/wiki 是他的資產; +// 升級 template 版本=另案 update 流程,不在本函式)。 +package main + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path/filepath" +) + +// templateFS 是 system-dev-template 的 vendored 快照(版本見 templatefs/system-dev/VERSION)。 +// +//go:embed all:templatefs +var templateFS embed.FS + +const templateFSRoot = "templatefs" + +// TemplateInstallResult 是一次代裝的結果帳目。 +type TemplateInstallResult struct { + Installed []string `json:"installed"` // 本次新鋪的檔(相對路徑) + Skipped []string `json:"skipped"` // 已存在故跳過的檔 + Version string `json:"version"` // 快照版本(templatefs/system-dev/VERSION) +} + +// TemplateVersion 讀出內嵌快照的版本號。 +func TemplateVersion() string { + data, err := templateFS.ReadFile(templateFSRoot + "/system-dev/VERSION") + if err != nil { + return "unknown" + } + v := string(data) + for len(v) > 0 && (v[len(v)-1] == '\n' || v[len(v)-1] == '\r') { + v = v[:len(v)-1] + } + return v +} + +// InstallTemplate 把內嵌 template 鋪進 root。冪等:既有檔案跳過不覆寫。 +func InstallTemplate(root string) (*TemplateInstallResult, error) { + res := &TemplateInstallResult{Version: TemplateVersion()} + absRoot, err := filepath.Abs(root) + if err != nil { + return nil, err + } + if err := os.MkdirAll(absRoot, 0o755); err != nil { + return nil, fmt.Errorf("建目標資料夾失敗:%w", err) + } + err = fs.WalkDir(templateFS, templateFSRoot, func(p string, d fs.DirEntry, werr error) error { + if werr != nil { + return werr + } + if d.IsDir() { + return nil + } + rel, rerr := filepath.Rel(templateFSRoot, p) + if rerr != nil { + return rerr + } + dest := filepath.Join(absRoot, rel) + if _, serr := os.Stat(dest); serr == nil { + res.Skipped = append(res.Skipped, rel) + return nil // 冪等:不覆寫用戶既有檔 + } + if merr := os.MkdirAll(filepath.Dir(dest), 0o755); merr != nil { + return merr + } + data, derr := templateFS.ReadFile(p) + if derr != nil { + return derr + } + if werr2 := os.WriteFile(dest, data, 0o644); werr2 != nil { + return werr2 + } + res.Installed = append(res.Installed, rel) + return nil + }) + if err != nil { + return nil, err + } + return res, nil +} + +// TemplateInstalled 判斷 root 是否已鋪過(以 wiki 標記檔存在為準)。 +func TemplateInstalled(root string) bool { + _, err := os.Stat(filepath.Join(root, "system-dev", "wiki", "status.md")) + return err == nil +} + +// runTemplateInstall 是 `collector template-install` 子命令主體。 +func runTemplateInstall(args []string) int { + fs2 := newFlagSet() + folder := fs2.String("folder", "", "要鋪 template 的資料夾(必填)") + if err := fs2.Parse(args); err != nil { + return 2 + } + if *folder == "" { + fmt.Fprintln(os.Stderr, "錯誤:--folder 為必填") + return 2 + } + res, err := InstallTemplate(*folder) + if err != nil { + fmt.Fprintln(os.Stderr, "collector template-install:", err) + return 1 + } + fmt.Printf("template v%s:新鋪 %d 檔、跳過既有 %d 檔 → %s\n", + res.Version, len(res.Installed), len(res.Skipped), *folder) + return 0 +} diff --git a/template_install_test.go b/template_install_test.go new file mode 100644 index 0000000..d02a022 --- /dev/null +++ b/template_install_test.go @@ -0,0 +1,82 @@ +// template_install_test.go — daemon-beta task 2(代裝冪等/wiki 產物區不被掃)。 +package main + +import ( + "os" + "path/filepath" + "testing" +) + +// 空資料夾一鍵鋪好:關鍵檔齊、版本讀得到。 +func TestInstallTemplateFresh(t *testing.T) { + root := t.TempDir() + res, err := InstallTemplate(root) + if err != nil { + t.Fatal(err) + } + if len(res.Installed) == 0 || len(res.Skipped) != 0 { + t.Fatalf("首鋪帳目異常:installed=%d skipped=%d", len(res.Installed), len(res.Skipped)) + } + if res.Version == "unknown" || res.Version == "" { + t.Fatalf("版本讀不到:%q", res.Version) + } + for _, must := range []string{ + "system-dev/wiki/status.md", + "system-dev/wiki/mistakes.md", + ".claude/commands/wiki-capture.md", // claude 萃取路(task 3)依賴它 + } { + if _, err := os.Stat(filepath.Join(root, must)); err != nil { + t.Fatalf("缺關鍵檔 %s:%v", must, err) + } + } + if !TemplateInstalled(root) { + t.Fatal("TemplateInstalled 應為 true") + } +} + +// 冪等:重跑不覆寫;用戶改過的檔保持原樣。 +func TestInstallTemplateIdempotent(t *testing.T) { + root := t.TempDir() + if _, err := InstallTemplate(root); err != nil { + t.Fatal(err) + } + marker := filepath.Join(root, "system-dev", "wiki", "status.md") + if err := os.WriteFile(marker, []byte("用戶自己的進度,不准動"), 0o644); err != nil { + t.Fatal(err) + } + res, err := InstallTemplate(root) + if err != nil { + t.Fatal(err) + } + if len(res.Installed) != 0 { + t.Fatalf("重跑不應再鋪檔:installed=%v", res.Installed) + } + data, _ := os.ReadFile(marker) + if string(data) != "用戶自己的進度,不准動" { + t.Fatal("用戶檔被覆寫=冪等鐵律破功") + } +} + +// 代裝後的 system-dev/ 不得被 direct 掃描當成原稿。 +func TestScanSkipsTemplateArtifacts(t *testing.T) { + root := t.TempDir() + if _, err := InstallTemplate(root); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "我的筆記.md"), []byte("# hi"), 0o644); err != nil { + t.Fatal(err) + } + cfg := &DirectConfig{ + WatchFolders: []string{root}, + Manifest: filepath.Join(t.TempDir(), "m.json"), + CypherURL: "https://x.example", Namespace: "demo", + MaxRemoved: DefaultMaxRemovedRatio, + } + results, exit, _ := RunDirectOnce(cfg, true) + if exit != 0 { + t.Fatalf("dry-run 失敗:%+v", results) + } + if len(results) != 1 || results[0].Path != "我的筆記.md" { + t.Fatalf("應只掃到用戶檔(template 產物須跳過),got %+v", results) + } +} diff --git a/templatefs/.claude/VERSION b/templatefs/.claude/VERSION new file mode 100644 index 0000000..84cc529 --- /dev/null +++ b/templatefs/.claude/VERSION @@ -0,0 +1 @@ +1.18.0 diff --git a/templatefs/.claude/commands/issue-handle.md b/templatefs/.claude/commands/issue-handle.md new file mode 100644 index 0000000..4023c6f --- /dev/null +++ b/templatefs/.claude/commands/issue-handle.md @@ -0,0 +1,80 @@ +--- +description: 處理本 repo 的 GitHub issue(讀/回/結案),跨 repo 發要先問人 +--- + +# /issue-handle — GitHub issue 處理指引 + +你(CC)可以、也該主動用內建的 `gh` CLI 讀寫**自己 repo** 的 GitHub issue。 +很多人不知道這件事——`gh` 已內建認證,零開發、零外部依賴。issue 同源於 repo, +比 Notion / Sheets 更適合做交辦與待辦,不必引入外部 SaaS。 + +這份指引分四層,界線要守住。 + +--- + +## 1. 讀 / 回 / 結案(普世基本功 — 直接做,不用問) + +對**自己這個 repo**,主動處理 open issue: + +```bash +gh issue list --state open # 看有哪些待辦 +gh issue view # 讀完整內容 +# …實作… +gh issue comment --body "[<本 repo> CC] 做了什麼、怎麼決定的、改了哪些檔" +gh issue close # 確認解決後結案 +``` + +回覆要有料:說清楚**做了什麼、為什麼這樣決定、動了哪些檔**,而不是只回「done」。 +issue 作者(可能是另一個 repo 的 CC,或人類)要靠你的回覆判斷對不對。 +跨 repo 的 issue/comment 開頭一律署名 `[<本 repo> CC]`(見第 3 節鐵律)。 + +--- + +## 2. 發 issue 給「別的 repo」(要先問人 — 不可擅自) + +當你發現**別的 repo** 有值得修正的地方時: + +> ❌ 不要擅自 `gh issue create -R other/repo …` +> ✅ 先問人類:「我發現 X repo 有 Y 問題,要我幫你去那邊發 issue 嗎?」得到同意才發。 + +理由:通用 template 不知道使用者對那個 repo 有沒有權限、想不想發。留一道人類確認最安全。 +(對**自己 repo** 開 issue 記待辦則可直接做——那是自己的 repo。) + +--- + +## 3. 跨 repo 署名(鐵律 — 絕不可漏) + +所有 repo(mira / graph-plugin / ingest-plugin / Arcrun / template…)共用**同一個 GitHub 帳號**發 issue/comment, +所以 issue/comment 的 author **全顯示同一個帳號、看不出是哪個 repo 的 CC 發的**。 + +> **跨 repo 的 issue/comment 一律在開頭署名 `[<本 repo> CC]`**,靠內容署名溯源。 + +- 收件方 CC 回報:`[graph-plugin CC]` / `[mira CC]` / `[ingest CC]` / `[arcrun CC]`… +- 總管下令/追問:`[InkStoneCo 總管]` +- 署名放 comment **第一行或標題式開頭**(既有的「## 回報(graph CC)」即合格)。 + +為什麼只能這樣:GitHub issue/comment 的 author = 發送帳號,**沒有 per-repo 身份這設定**; +`git config user.name` 只影響 commit 作者、不影響 issue/comment author; +給每個 repo 開獨立帳號 = 多帳號自動化 = 踩下方第 4 節 flag 鐵律,**不可**。 +身份只能在**內容層自報**。本 repo 名稱 → 看 `git remote -v` 或 repo 根目錄名。 + +--- + +## 4. flag 安全界線(最重要 — 絕不可越) + +**「有事才讀」,禁止自動輪詢。** + +- ✅ 想看 issue → 當下主動 `gh issue list`。 +- 🚫 **禁止**掛 GitHub Actions / cron / webhook 去**自動輪詢** issue。 + +為什麼這條是硬底線:自動輪詢 + 事件 fan-out 正是會觸發 GitHub 異常偵測、 +害你的 token 被 rate limit 砍掉的流量模式。template 給很多人用,這條界線必須守住, +保護使用者不踩雷。需要「定期檢查」就由人類主動跑這個指令,不要自動化。 + +--- + +## (可選)用 label 分來源 + +若想用同一套流程同時管「內部交辦」與「外部回報」,可建兩個 label: +`internal`(協作/交辦)、`user`(外部使用者回報),靠 label 分流。 +這對通用 template 不預設——你的 repo 需要再建。 diff --git a/templatefs/.claude/commands/sdd-check.md b/templatefs/.claude/commands/sdd-check.md new file mode 100644 index 0000000..844b7c8 --- /dev/null +++ b/templatefs/.claude/commands/sdd-check.md @@ -0,0 +1,75 @@ +# /sdd-check — 確認當前任務有沒有對應 SDD + +動手前執行。確保 CC 有全局觀,不會在沒有設計文件的情況下猛衝。 + +--- + +## 生命週期(單一活性鐵律,全文見 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`) + +五條鐵律摘要: + +1. **單一活性**:任何時刻整個 repo 只允許一份 `status: active` 的 SDD;所有開發任務對應它的 tasks,找不到對應任務 → 停下來問,不准直接做。 +2. **禁止自行建立 SDD**:澄清問題→回答不動文件;任務層變更→更新現行 SDD 的 tasks(標日期與原因);規格層變更→走第 3 條。 +3. **規格變更只有一條路**:change proposal 寫進 `system-dev/docs/3-specs/pending-changes.md`(摘要+觸發原因+影響分析),然後**停止**等使用者「confirm」。 +4. **開新 SDD 的唯一時機**:使用者 confirm 後——先把舊 SDD 未完成任務逐條搬入新 SDD(做完前不准寫 code)→ 舊的標 `closed` + `superseded_by` 移入 `archive/` → 新 SDD changelog 記繼承 → 列搬移/作廢清單請最終確認。 +5. **每次 session 開始**先讀 active SDD 與 pending-changes.md,回報三個數字: + + ``` + 📐 現行規格:〈SDD 名稱〉 + 📋 未完成任務:N + ⚖️ 待裁決 proposal:M + ``` + + 若出現**兩份 active=規則已被違反,當場糾正**(收斂到一份,其餘 paused/closed)。 + +--- + +## 執行流程 + +### 第一步:理解任務 + +確認使用者要做什麼: +- 涉及哪個子系統? +- 是新功能還是修改現有功能? +- 影響範圍? + +### 第二步:尋找對應 SDD + +在 `system-dev/docs/3-specs/` 下尋找對應的子系統目錄,確認有沒有: +- `design.md`(設計文件) +- `tasks.md`(任務清單) + +### 第三步:根據結果回應 + +**情況 A:找到對應 SDD** +``` +✅ 找到 SDD:system-dev/docs/3-specs/[子系統]/ +📋 design.md:[確認] +📋 tasks.md:[確認,列出相關 task] +🎯 對應 task:[編號和描述] +繼續嗎? +``` + +**情況 B:找不到 SDD,任務明確** +``` +⚠️ 找不到對應 SDD +任務:[描述] +建議在 system-dev/docs/3-specs/[建議子系統名]/ 建立 SDD + +要我幫你起草 design.md 嗎?(需要你確認後才動手) +``` + +**情況 C:找不到 SDD,任務模糊** +``` +⚠️ 找不到對應 SDD,而且任務範圍不夠清楚 +請先回答: +1. 這個功能屬於哪個子系統? +2. 完成的標準是什麼? +3. 有沒有不能動的邊界? +``` + +### 注意 + +- 找不到 SDD **不等於可以直接動手** +- 小修改(修 bug、改文字)可以豁免,但要明確說「這是小修改,範圍是 X」 +- 新功能、架構變動、跨模組的修改 → 一定要有 SDD diff --git a/templatefs/.claude/commands/wiki-capture.md b/templatefs/.claude/commands/wiki-capture.md new file mode 100644 index 0000000..d10e13b --- /dev/null +++ b/templatefs/.claude/commands/wiki-capture.md @@ -0,0 +1,69 @@ +# /wiki-capture — 把對話結論存進 wiki + +把這次對話中產生的決策、誤解釐清、或重要結論存入 wiki。 +解決「討論過了但知識消失」的問題。 + +--- + +## 執行流程 + +### 第零步:機敏檢查(寫入前一律先過) + +把任何內容寫進 wiki 前,先確認**不含**密碼 / API 金鑰 / 私鑰 / 連線字串帳密 / 個資(身分證、信用卡)。 +- 命中 → 不要記「值」,改記「位置」(例:「DB 密碼放 `.env`,不入 wiki」) +- 來源整檔機敏 → 提醒使用者加進 `system-dev/wiki/.wikiignore` +- 真要保留示範格式 → 該行尾加 `wiki-secret-ok` 標記 +> 這是協議層自律。最後一道 `wiki-secret-scan.sh` hook 會在寫入 `system-dev/wiki/` 時機械攔截,但別依賴它兜底——當場就不要把機敏值帶進來。 + +### 第一步:辨識對話中的可記錄內容 + +掃描當前對話,找出: + +| 類型 | 判斷標準 | 存到哪 | +|------|---------|-------| +| 架構決策 | 「為什麼選A不選B」「我們決定用X」 | `decisions-summary.md` + `system-dev/docs/2-architecture/decisions/` | +| CC 的誤解被糾正 | CC 說了某件事,使用者說「不是,是...」 | `mistakes.md` | +| 重要狀態更新 | 完成了某件事、阻擋了某件事 | `status.md` | +| 技術發現 | 踩到坑、找到解法、重要行為確認 | `mistakes.md` 或對應 SDD | + +### 第二步:列出清單給使用者確認 + +格式: +``` +這次對話我整理了以下內容要存入 wiki: + +1. [MISTAKE] CC 誤解了 X,正確是 Y +2. [DECISION] 決定用 A 不用 B,原因是 C +3. [STATUS] 完成了 task 2.3,下一步是 2.4 + +確認後存入,有需要修改的嗎? +``` + +**停下來等確認。** + +### 第三步:寫入 + +確認後,依照格式寫入對應檔案: + +**mistakes.md 格式:** +``` +⚠️ MISTAKE: [錯誤描述] + 症狀: [CC 的表現] + 正確做法: [應該怎麼做] + 原因: [背景] + 日期: [YYYY-MM-DD] +``` + +**decisions-summary.md 格式:** +``` +## [主題] — [YYYY-MM-DD] +**結論**:[一句話] +**原因**:[簡短說明] +**詳細**:system-dev/docs/2-architecture/decisions/[檔名] +``` + +重大決策同時在 `system-dev/docs/2-architecture/decisions/` 建立 ADR 檔案。 + +### 第四步:確認 + +告知存到哪些檔案,共幾條記錄。 diff --git a/templatefs/.claude/commands/wiki-extract.md b/templatefs/.claude/commands/wiki-extract.md new file mode 100644 index 0000000..e8e35ed --- /dev/null +++ b/templatefs/.claude/commands/wiki-extract.md @@ -0,0 +1,161 @@ +# /wiki-extract — vault 增量萃取(Logseq / Obsidian → system-dev/wiki) + +把**筆記 vault**(Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地 +萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**: +AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 KBDB。 + +> **跟 `/wiki-init` 的分工**: +> - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。 +> - `/wiki-extract` 是**之後每次**的增量重萃——vault 會被 Syncthing/cron 持續灌新筆記, +> 這支負責「只萃變動的、沒變的不碰、不浪費 AI run」。給 Routine / cloud-worker 反覆跑。 +> - **跑它的是你(CC / Routine)=LLM 本人,不需任何 token**。 + +> **邊界(硬規矩,別越界)** +> - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游 +> Arcrun 從你產的 `[[wikilink]]` + `## 關聯` 機械映射(另一張 issue),不是這支的事。 +> - **原始筆記唯讀**:`journals/`、`pages/`、Obsidian 根 `.md` 是 leo 的手寫真身, +> 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。 +> - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。 + +--- + +## 執行流程 + +### 第一步:確認這是 vault repo,定位 raw source + +偵測邏輯**同 install.sh / wiki-init**: + +| 偵測到 | 型態 | raw source(要掃的原文) | +|--------|------|--------------------------| +| 根目錄有 `logseq/` | Logseq vault | `journals/*.md` + `pages/*.md` | +| 根目錄有 `.obsidian/` | Obsidian vault | vault 根下所有 `.md` | +| 都沒有 | **不是 vault** | → 停手。這支只處理 vault;一般 dev repo 開發時就手寫 `.claude`/`system-dev/wiki`,不需萃取 | + +沒有 `system-dev/wiki/`?→ 先跑 `/wiki-init`(首次建結構+首萃),再回來用這支做增量。 + +### 第二步:content_hash 冪等 —— 決定哪些檔要萃(省 run 的核心) + +讀萃取 manifest:`system-dev/wiki/.extract-manifest.json`(不存在=首次,視同全部要萃)。 +格式: + +```json +{ + "version": 1, + "algo": "sha256", + "sources": { + "journals/2026_07_01.md": { + "content_hash": "", + "extracted_at": "2026-07-06", + "cards": ["Prompt能力即拆解自己邏輯的能力", "程式化邏輯可圖解任何主題不限AI"], + "skipped_reason": null + }, + "journals/2026_06_25.md": { + "content_hash": "", + "extracted_at": "2026-07-06", + "cards": [], + "skipped_reason": "空檔/訊息量不足,無可萃知識點" + } + } +} +``` + +對每個 raw source 檔: + +1. 算目前 `content_hash`(`sha256sum `,取檔案 bytes 的 hash)。 +2. 跟 manifest 裡該檔的 `content_hash` 比: + - **相同 → skip,不讀不萃、不呼叫任何 AI 推理**(就算它上次 `cards: []` 也 skip——空檔沒變還是空)。 + - **不同或不在 manifest → 這檔要(重)萃**。 +3. manifest 有、但檔已不存在 → 該檔被刪,把它的 entry 從 manifest 移除(卡片是否連帶處理見第五步)。 + +> **這一步是「省 run」的重點**:vault 每天可能只動 1~2 個 journal,其餘幾十個檔 hash 沒變 +> 就整批跳過,AI 只對真正變動的檔動腦。**重跑一個沒變動的 vault = 零 AI 呼叫、零 diff。** + +### 第三步:對「要萃」的檔,抓知識點 + 任務 + +逐個變動檔讀原文,分兩類抽取: + +**(a) 知識點 → 概念原子卡** +判準與卡片格式**完全依 `/wiki-init` 第五步**(frontmatter `tags:`/`gloss:`、H1、麵包屑 +`← [[/00-INDEX]]`、`**來源**`、`## 摘要`、`## 重點`、`## 實體`、`## 關聯` 的 +typed-edge 三元組、TAXONOMY 受控標籤、硬自檢等)——**不在這裡重寫格式,一律回去讀那份**。 +廢話/訊息量薄的段落略過(在 manifest 記 `skipped_reason`,誠實留痕、不留卡)。 + +**(b) Logseq 任務 marker → 任務卡(task_status)** +解析**完全依** `system-dev/docs/4-guides/logseq-markers.md`(單一真相源,與 template#4 +tasks 投影共用同一套;**別自己另寫 mapping**)。摘要: + +- 任務行 regex:`^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+` +- 狀態正規化:TODO/LATER→`todo`、DOING/NOW→`in-progress`、WAITING→`blocked`、 + DONE→`done`、CANCELED/CANCELLED→`closed`。 +- 跳過 `:LOGBOOK:…:END:` 區塊與 `key:: value` 屬性行(`collapsed::`、`id::`、 + `SCHEDULED::`、`DEADLINE::`…),**別把 marker 或屬性當任務內文**。 + +有實質內容的任務 → 產一張任務卡進 `cards/tasks/` bucket,frontmatter 帶 `task_status`: + +```markdown +--- +tags: [<領域標籤,依 TAXONOMY>] +task_status: todo # ← 依上表正規名;這是任務卡才有的欄位 +gloss: 一句話定義這個任務要達成什麼(供下游 normalize) +--- +# <任務一句話標題(marker 後的內文,去掉 marker)> + +← [[tasks/00-INDEX]] + +**來源**:`journals/2026_07_01.md`(TODO block) +**最後更新**:YYYY-MM-DD + +## 摘要 +[任務要做什麼、脈絡] + +## 實體 +- **<關鍵實體正規名>**(<同義詞>)— <一句描述> + +## 關聯 +### 內文知識關係(端點=上方 `## 實體` 正規名,一字不差) +- <實體A> >> <謂詞> >> <實體B> +### 卡片關係(卡對卡) +- [[本任務卡]] >> 涉及 >> [[相關概念卡]] +``` + +> 純瑣事任務(「買菜」這種無知識量)不必成獨立卡——可在 `cards/tasks/00-INDEX.md` +> 列一行帶狀態即可,避免灌垃圾卡。判準同 D16:有沒有知識/專案價值。 + +### 第四步:更新桶索引與 INDEX + +- 每個動到的 bucket(如 `cards/notes/`、`cards/tasks/`)更新其 `00-INDEX.md` + (容器:只連不重寫,H2/H3 分節)。 +- 更新 `system-dev/wiki/INDEX.md` 的標籤視圖與卡片清單。 +- 任務卡可在 INDEX 開一個「任務視圖」按 `task_status` 聚類。 + +### 第五步:寫回 manifest + 驗證原文 0 動 + +1. 把這次萃過的每個檔的**新 `content_hash`**、`extracted_at`、產出的 `cards`、 + (或 `skipped_reason`)寫回 `system-dev/wiki/.extract-manifest.json`。 + **沒動到的檔的 entry 原樣保留**(別整檔重寫掉別人的 hash)。 +2. 驗證原文零異動(踩過的坑): + ``` + git status --short journals/ pages/ # Obsidian 則看根目錄 .md ——須 0 新增 0 修改 + ``` + 有任何原文變動 → 你誤寫了 raw source,回滾。 + +### 第六步:完成報告 + +``` +✅ wiki-extract 完成(增量) +掃描:N 個 raw source 檔 + 萃取:M 個(content_hash 變動)→ 產出 X 張概念卡 + Y 張任務卡 + 跳過:K 個(hash 未變,零 AI 呼叫) +任務狀態分布:todo A / in-progress B / done C / … +原文驗證:journals/ pages/ git status 0 異動 ✅ +manifest:system-dev/wiki/.extract-manifest.json 已更新 +``` + +--- + +## 冪等自檢(Routine 反覆跑必守) + +- [ ] 跑之前先讀 manifest,hash 相同的檔**完全不進 AI**(不是「讀了才發現一樣」,是靠 hash 先擋)。 +- [ ] 對「同一個沒變動的 vault」連跑兩次:第二次應是**零萃取、零卡片 diff、零 manifest 變化**。 +- [ ] 只有 `system-dev/wiki/` 有寫入;`journals/`、`pages/` git status 全乾淨。 +- [ ] 任務狀態用正規名,marker/屬性沒混進內文(照 `logseq-markers.md` 自檢)。 diff --git a/templatefs/.claude/commands/wiki-init.md b/templatefs/.claude/commands/wiki-init.md new file mode 100644 index 0000000..3a913b2 --- /dev/null +++ b/templatefs/.claude/commands/wiki-init.md @@ -0,0 +1,235 @@ +# /wiki-init — 初始化或接入 LLM Wiki 系統 + +初始化這個專案的 LLM Wiki 記憶系統。 +新專案建立空白結構,已有專案掃描現有文件並**改寫**成 wiki。 + +--- + +## 核心概念:wiki 是 AI 改寫過的記憶,不是原文索引 + +記憶系統的目的,是讓 AI **之後讀得快**。但人類寫的原始文件——不管是 vault 的隨手記、開發專案的會議記錄、規格草稿、散落的 `.md`——天生是亂的:重複、流水帳、半成品、口語。 + +如果 wiki 只是一份 `[[原文檔名]]` 指回原文的**索引**,那每次未來要用都得重新解析那團亂,等於沒省到。**wiki 的價值在於「改寫一次,之後每次讀都便宜」**。 + +所以原則對**所有專案**一致(不分 vault 或一般開發): + +> 人類寫的原文是 **SSoT**(真理來源,永遠唯讀)。 +> 但實際要長期保存、被 AI 反覆讀的是 **AI 改寫整理過的 wiki**。 +> **AI 是總編輯**——把原文改寫成自包含、概念原子化、互相連結、適於 AI 讀的知識條目。 + +唯一例外:原文是**不可改動的正式文件**(簽署過的規格、法規、合約),必須逐字讀原文——這種才在 wiki 裡用指針指回去,並註明「逐字依原文」。除此之外,一律改寫。 + +**raw source 永遠唯讀**:所有產出只往 `system-dev/wiki/` 寫,絕不改動、搬移、重新命名原文。 + +--- + +## 執行流程 + +### 第一步:偵測專案狀態 + +檢查以下項目,判斷是新專案還是已有專案: +- 根目錄有沒有 `system-dev/wiki/` +- 根目錄有沒有 `docs/`(或 vault 的 `pages/`、`journals/`、根目錄 `.md`) +- 有沒有散落的 `.md` 檔案 + +同時**偵測 raw source 路徑**(同 install.sh 邏輯): +- 根目錄有 `logseq/` → Logseq vault,raw source = `pages/` + `journals/` +- 根目錄有 `.obsidian/` → Obsidian vault,raw source = 根目錄所有 `.md` +- 都沒有 → 一般專案,raw source = `docs/` 下所有 `.md`(及散落的 `.md`) + +**新專案**(幾乎空的)→ 直接建立結構,跳到第三步 +**已有專案**(有文件)→ 執行第二步 + +### 第二步:已有專案的掃描(已有專案才執行) + +1. 遞迴找出 raw source 裡所有 `.md` 檔案 +2. **先套用 `system-dev/wiki/.wikiignore`**:命中 pattern 的檔案整個排除,不讀不編入。 + - 若 `.wikiignore` 不存在,從範本建立一份(預設排除 `.env`/`*.pem`/`*secret*` 等) + - 被排除的檔案在清單裡標「🚫 .wikiignore 排除」,**不可被覆蓋** +3. 對其餘檔案標注**改寫計畫**:會萃取成哪些 wiki 條目。一份原文可能拆成多個概念原子條目,多份相關原文也可能合併成一條。 +4. 列出清單給使用者確認,**停下來等確認** + +> **量大時建議用 Haiku 改寫**:逐份原文「改寫成 wiki 格式」是重複、機械、判斷成本低的工作——正適合 Haiku。原文數量多(如數十、上百份)時,主動建議: +> 「共 N 份原文要改寫,這類逐份萃取很適合用 Haiku 並行處理(便宜、夠快)。要我派 Haiku subagent 改寫嗎?」 +> 得同意後,用 Task / subagent 把每份原文(或每批)丟給 Haiku 改寫,主模型只負責切分概念、定條目邊界、最後審稿與互連。 + +> 機敏防護(三層): +> - **L1 .wikiignore**:整檔排除(這一步) +> - **L2 行內標記**:檔案要編入但某段不要 → 遇到 `` … `` 之間的內容**略過**,只留「(此處機敏,已略過)」 +> - **L3 hook**:萬一機敏值仍被寫進 wiki,`wiki-secret-scan.sh` 會 exit 2 擋下 +> 編入任何檔案前,先檢查是否含密碼/金鑰/個資——有就改記「位置」而非「值」。 + +### 第三步:建立缺少的結構 + +只建立不存在的目錄和檔案,**已有的一律不動**。 + +wiki 採**三層 + 標籤橫切**架構(183 卡實證,issue #8): + +``` +system-dev/wiki/ +├── INDEX.md ← 索引:多角度視圖的家(標籤角度、決策角度、…) +├── TAXONOMY.md ← 標籤字典(cards 的分類元資料,受控擴充) +├── status.md ← [push] 時態狀態:當前進度、下一步 +├── mistakes.md ← [push] 踩過的坑、被糾正的誤解(防不自覺盲區) +├── principles.md ← [push] 跨全局的設計原則(行動前必服從) +└── cards/ ← [pull] 一切知識內容:原文摘要、AI 筆記、決策、概念… + └── / ← 儲存桶(分類由 frontmatter 標籤承載) + ├── 00-INDEX.md ← 桶子索引(固定名,容器:只連不重寫,H2/H3 分節) + └── <概念全名>.md ← 概念原子卡(一概念一檔,自包含) +``` + +> **[push] / [pull] 是這套 wiki 的核心判準——因為 wiki 主要是給 AI(CC)看的。** +> 見下方「核心判準:push vs pull」。`decisions-summary.md` 已**降級為 cards + INDEX 決策視圖**(決策是知識內容=card);既有的 decisions-summary 若存在,保留為相容,不刪。 + +關鍵原則:**資料夾只是儲存桶,分類由 frontmatter 標籤承載**。資料夾名不該硬繼承原稿目錄——原稿目錄是「人為了整理草稿」分的,wiki 連分類都該由 AI 重新組織。 + +> **桶子索引固定叫 `00-INDEX.md`**(issue #6):`00-` 前綴讓它排序最前、一眼可辨(像 README 之於資料夾),AI 載入任何 `cards//` 一律先讀它,不必猜。檔內 H1 仍寫主題名(如 `# PKM 知識管理`),語意不丟。 + +一般專案仍可同時建 `system-dev/docs/` 分類樹(SDD 等): +``` +system-dev/docs/{1-vision,2-architecture/decisions,3-specs,4-guides,5-records/{incidents,test-reports},6-user} +``` +(純 PKM vault 不需要 `system-dev/docs/` 分類樹時,只建 `system-dev/wiki/`。) + +檔案(不存在才建): +- `system-dev/wiki/INDEX.md`、`TAXONOMY.md` +- `system-dev/wiki/status.md`、`mistakes.md`、`principles.md`(三個 push 檔) +- `system-dev/docs/README.md`(一般專案才需要) + +--- + +## 核心判準:push vs pull(wiki 是給 AI 看的) + +整理任何內容前,先判斷它該 **push** 還 **pull**——判準是「**CC 做事時會不會被動看見**」: + +- **push**:CC 行動前必須主動出現在 context(session 開始就由 hook 注入)。給「CC 不會主動去查、但不看就出事」的東西。 +- **pull**:CC 想到要查、或載入相關卡時才看見。給「CC 面對它時自然會查」的知識。 + +**為什麼這是核心**:mistakes 防的是 CC「不自覺的盲區」——一個你不知道存在的錯,你不會主動去檢索它。靠 CC 自覺去查自己沒自覺的盲區是自相矛盾的,所以 pull 對盲區失效,**必須 push**。原則同理:沒被推到眼前的準繩,CC 設計時很可能沒想到要服從就做了。 + +| 內容 | push/pull | 注入形態(hook)| +|------|-----------|----------------| +| **status** | push | **全文**——CC 必須知道精確的下一步,摘要會漏 task 編號 | +| **principles** | push | **全文(一行一條)**——短而硬的約束,漏一條就違反;≤15 條,超過代表該下放成 card | +| **mistakes** | push | **標題清單 + 一行症狀**,全文按需 pull——量可能大,摘要足以觸發「我正撞到某條」的認出 | +| **decisions、原文摘要、概念知識、一切其餘** | pull | 寫成 cards;CC 面對時自然會查,INDEX 提供角度入口 | + +**principles 維護規則**:一行一條精煉準繩(如「不污染用戶根目錄」「目標用戶 low-code」「wiki 主要給 AI 看」)。發現新的跨全局原則 → append 一行;超過 ~15 條代表某些該合併或下放成 card。**累積原則只改 principles.md,不必問用戶開新檔。** + +### 第四步:訪談(每次一個問題) + +依序問: +1. 這個專案做什麼?(一句話) +2. 有哪些絕對不能違反的限制?(技術棧、架構原則等) +3. 現在進行到哪個階段? +4. 有沒有 CC 曾經犯過的錯要先記下來? + +把答案填進 `CLAUDE.md`(如果存在)或建立新的。 + +### 第五步:改寫成 wiki(AI 當總編輯) + +(第二步確認後執行) + +**不搬動原文**。逐份讀 raw source,改寫萃取成 `cards//` 裡的自包含原子卡: + +- **概念原子化**:一張卡講一個概念,不是一篇原文對一張卡。原文太雜就拆,多份相關原文就合。 +- **自包含**:讀卡就懂,不必回去翻原文。把口語、重複、流水帳改寫成結構化知識,**不寫「詳見原文」**。 +- **保留來源指針**:每卡標 `**來源**:原文相對路徑`,為可追溯,不是要使用者回去讀。 +- **frontmatter 標籤分類**(見下方):分類走 frontmatter `tags:`,不靠資料夾、不靠行內 `#tag`。 +- **互相連結(typed-edge 三元組)**:`## 關聯` 不只列裸 `[[頁面]]`,改寫成帶語義的三元組(見下方)。 +- **萃 gloss(node 一句說明)**:frontmatter 放 `gloss:` —— 這張卡(= 一個 entity / graph node)的一句話定義,供下游語義 normalize(見下方)。 + +卡片格式(每張卡): +```markdown +--- +tags: [知識管理, AI協作, 方法論] +gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,選填、deep tier 才產) +--- +# 概念全名 + +← [[/00-INDEX]] + +**來源**:`[raw source 相對路徑]` +**最後更新**:YYYY-MM-DD + +## 摘要 +[一句話核心] + +## 重點 +- [自包含改寫的要點,不依賴原文] + +## 實體 +> 本卡內文的關鍵實體(也是 graph node)。名+描述供下游 embedding normalize。集中放、一行一個、不縮排、不重複。 +- **原子筆記**(atomic note/卡片原子化)— 每張卡只承載一個不可再分論點的知識記錄單元。 +- **傳統筆記**(大鍋炒筆記)— 把多主題混雜在同一篇、難精確引用的記錄方式。 + +## 關聯 +### 內文知識關係(內文實體間;端點=上方 `## 實體` 正規名,一字不差) +- 原子筆記 >> 對立於 >> 傳統筆記 +### 卡片關係(卡對卡) +- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]] +``` + +**麵包屑用帶路徑 wikilink**(issue #7):H1 次行放 `← [[/00-INDEX]]` 指回桶子索引。 +桶子索引固定名 `00-INDEX` 跨桶會撞名,故**指 00-INDEX 一律帶路徑**(`[[pkm/00-INDEX]]`,Logseq 原生支援、下游 ingest 也能對應到具體檔)。普通卡片間連結仍用裸 `[[卡名]]`(卡名唯一,不需路徑)。 + +**frontmatter 標籤分類**(issue #8): +- **用 frontmatter `tags:` 而非行內 `#tag`**:卡片內文常大量用 `#`(講筆記法時的 `#猜想`、`#book100`),分類標籤若也行內 `#`,下游 ingest 無法區分「分類」與「內文範例」會污染 graph。frontmatter 與內文完全分開,零歧義。 +- **用標籤而非資料夾分類**:資料夾=強制單一歸屬;標籤=多重歸屬。一張卡可同時屬知識管理+AI協作+架構設計,硬塞一個資料夾會在其他檢索角度漏掉。 +- **雙軸 taxonomy**(寫進 `TAXONOMY.md` 當字典;**受控擴充**,非凍結): + - 領域(主軸,1-3 個):如 知識管理/學習認知/AI協作/生產力/系統設計/工具教學 + - 形態(副軸,0-2 個):方法論/工具實作/觀點主張/架構設計/案例經驗 + - 一般開發專案的軸可不同(如 子系統/層級/決策類型),由 AI 依專案性質提出、寫進 TAXONOMY.md。 + - **遇到現有軸裝不下的內容**:先查是否只是現有標籤的同義詞;確實是新軸才加進 TAXONOMY.md(附定義)再用——**禁止繞過字典在卡片直接冒新標籤**。字典是 per-repo,跨 repo 不必共用。 + +**typed-edge 規則**(issue #5/#11,把「關係」也預編譯,下游 ingest 直接 parse 出帶類型的有向邊): +- **重點抓內文實體關係,不只卡對卡**:卡對卡(`[[卡A]] >> 謂詞 >> [[卡B]]`)只是既有雙鏈加動詞、資訊量幾乎沒增加;價值在內文概念關係(`原子筆記 >> 對立於 >> 傳統筆記`,A/B 是內文概念非卡標題)。 +1. **方向性**:`A >> 謂詞 >> B` 必須讀成「A(謂詞)B」一句通順的話;A、B 順序就是主→賓真實方向。 +2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、犧牲)。**禁名詞當謂詞**——`>> 存儲格式 >>`、`>> 操作體驗 >>` 讀不通,是錯的。 +3. **謂詞自由但別太天馬行空**:「參考/參照」皆可(下游 embed 自動聚類),別寫「瞄了一眼」這種抓不到同義的。 +4. **內文三元組端點用裸文字**(非 `[[wikilink]]`),避免 Logseq 紅色斷鏈;卡對卡那層才用 `[[]]`。 +5. **向後相容**:純 `[[A]]` 仍合法(視為無類型邊),盡量補謂詞。 + +> **★ 硬自檢(Haiku 量產必備)★** 內文三元組端點必須與 `## 實體` 某粗體正規名【一字不差】。**寫完逐條把 A、B 拿去 `## 實體` 比對**,沒有完全相同的 → 這條錯了,改用實體表已有的詞、或把端點補進 `## 實體` 再指它。禁止端點帶括號註解/整句補語/形容詞短語。(實證:光寫規則 Haiku 會略過,端點對不齊 14 條;寫成自檢動作後 14→0。跑 12 張才暴露。) +> `>>` 是分隔語法,repo 可自選符號,但全程一致。 + +**萃 gloss 規則**(issue #9/#11,把「node 的一句說明」也預編譯,供下游 KBDB 語義 normalize): +- **gloss = 這個 entity / graph node 是什麼的一句話**。下游對「entity 名 + gloss」一起做 embedding 求相似度,自動歸一同義詞(比只對名字準、比手維護 alias 表自動)。 +- **兩層 gloss**:① frontmatter `gloss:` 描述卡標題這個 node;② `## 實體` 每行描述句描述內文實體 node。**內文實體也是 graph node、也需描述句**才能 normalize(`黃仁勳` vs `Jensen Huang` 靠描述拉近向量)。 +- **實體要描述、謂詞不用**:實體同義詞字面差遠需描述拉近;謂詞同義詞字面本就近,裸詞 embed 自動聚類。 +- **在知識生產的當下、由 local CC 建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔 / 跨庫視角,編不出貼合的 gloss(=胡扯)。 +- **選填、deep tier 才產**:淺萃(只要結構)時不浪費;deep 改寫時每張卡補。 +- **gloss ≠ 摘要**:`gloss` 是給機器 normalize 的定義句(「X 是…」);`## 摘要` 是給人讀的核心一句。 +- **格式對齊下游 envelope**:frontmatter `gloss:` 與 `## 實體` 詞條對應下游 ingest envelope 的 `nodes[].gloss`,ingest 直接取用。 + +**INDEX.md 是標籤視圖**(非資料夾列表),`00-INDEX.md` 是桶內容器(只連不重寫,H2/H3 分節)。 +頂層索引指桶子索引帶路徑:`[[pkm/00-INDEX]]`。 + +> 與 claude.ai Cowork 的 `system-dev/docs/SKILL.md` 改寫邏輯一致,兩條路徑(CC / Cowork)產出同一種 wiki。 + +### 第六步:完成報告 + 驗證 + +完成後**驗證原文 0 動**(踩過的坑,issue #8): +``` +git status --short pages/ journals/ # 或一般專案的 docs/ ——須 0 新增 0 修改 +``` + +> **改寫時必守**(subagent 尤其): +> 1. **絕不寫入 raw source**:subagent 目標一律給絕對路徑到 `cards//`,明寫「絕不寫入 pages/journals/docs 原稿」;事後用上面的 `git status` 驗。 +> 2. **檔名 = 卡片全名**,否則 `[[全名]]` 對不到檔。冒號用全形「:」、斜線用全形「/」,**全程一種字元**,避免 `/`、`∕`、`:` 混用斷鏈。 +> 3. **量大用 Haiku 並行改寫**,主模型只切概念邊界+審稿+修跨資料夾斷鏈。 + +告知: +``` +✅ wiki-init 完成 +建立了:[列出新建的目錄和檔案] +跳過了:[列出已有因此不動的] +改寫了:[N 份原文 → M 張原子卡、K 條 typed-edge、M 條 gloss(deep tier)] +原文驗證:pages/ journals/ git status 0 異動 ✅ +下一步:用 /wiki-capture 把重要決策存進 wiki +``` + +> **vault repo 首萃後的增量重萃**:Logseq / Obsidian vault 會被持續灌新筆記。首萃(本命令) +> 之後,改用 **`/wiki-extract`** 做增量——它靠 content_hash 只萃變動的檔(沒變=零 AI 呼叫), +> 並解析 Logseq 大寫任務 marker(TODO/DOING/DONE…→ `task_status`,見 +> `system-dev/docs/4-guides/logseq-markers.md`)。適合掛給 Routine / cloud-worker 反覆跑。 diff --git a/templatefs/.claude/commands/wiki-recall.md b/templatefs/.claude/commands/wiki-recall.md new file mode 100644 index 0000000..2159a5e --- /dev/null +++ b/templatefs/.claude/commands/wiki-recall.md @@ -0,0 +1,58 @@ +# /wiki-recall — Session 開始,手動接關 + +開新對話時接上次進度。**Fallback 命令**:SessionStart hook 沒啟動時手動接關;要完整脈絡時也用。 + +> 主路徑是 SessionStart hook 自動注入 status 重點,不靠你打命令。 +> 這支命令應對 hook 失效,以及需要比「status 重點」更完整脈絡的時候。 + +--- + +## 命名閉環 + +init(建) → update(存,session 末) ↔ **recall(接,session 初)** → capture(隨時存結論) + +--- + +## 執行流程 + +### 第一步:讀 status.md(當前進度) + +讀 `system-dev/wiki/status.md`,掌握: +- 正在做什麼、阻擋點 +- 下次 session 第一件事 +- 待負責人確認、已知問題 + +### 第二步:讀 decisions-summary.md(為什麼這樣做) + +讀 `system-dev/wiki/decisions-summary.md`,掌握相關的架構決策——避免重新討論已定案的事。 + +### 第三步:讀 mistakes.md(別重犯) + +讀 `system-dev/wiki/mistakes.md`,掌握已知誤解 + 快速檢查清單。 + +### 第四步:掃 wishlist / HANDOFF(如果有) + +- `docs/wishlist.md`:待補功能 +- 任何 `HANDOFF.md` / 交接note:上一棒留下的脈絡 + +### 第五步:回報接關結果 + +``` +📍 接關完成 +🔄 上次正在做:[status 的「正在做」] +🎯 下次第一件事:[status 的「下次 session 第一件事」] +⚠️ 待確認:[如有] +``` + +--- + +## 鐵律:快照非即時狀態 + +status / wiki 是 **point-in-time 快照,不是即時狀態**。 + +接關 = 讀快照 **+ 核實快照**,**不盲信**。 + +> 實例:某專案 status 曾寫「待 A 收尾 X」,實際 X 早已完成。 +> 照舊資訊行動會去催一件已完成的事。 + +動手前,先用當前 code / git / 檔案核實快照寫的事項是否仍成立。發現落差 → 先更新 status,再動手。 diff --git a/templatefs/.claude/commands/wiki-update.md b/templatefs/.claude/commands/wiki-update.md new file mode 100644 index 0000000..1d5ecfe --- /dev/null +++ b/templatefs/.claude/commands/wiki-update.md @@ -0,0 +1,50 @@ +# /wiki-update — Session 結束,更新狀態 + +每次 session 結束時執行。更新 status.md,確保下次 session 能無縫接上。 + +--- + +## 執行流程 + +### 第一步:整理這次 session 的結果 + +從對話中提取: +- 完成了哪些 tasks(標記為 [x]) +- 進行中但未完成的(標記為 [🔄]) +- 遇到什麼問題或阻擋 +- 下次應該從哪裡開始 + +### 第二步:更新 tasks.md + +把對應 SDD 的 tasks.md 狀態更新(如果這次有動到的話)。 + +### 第三步:更新 status.md + +用以下格式覆蓋 status.md: + +```markdown +# 當前狀態 +> 更新時間:[YYYY-MM-DD] + +## 正在做 +- [🔄] [task 描述] — 阻擋點:[如果有] + +## 下次 session 第一件事 +[具體的第一個動作,越具體越好] + +## 待負責人確認 +- [描述] — 等待:[什麼決定] + +## 已知問題 +| 問題 | 優先級 | 狀態 | +|------|--------|------| +| [問題] | 🔴/🟡/⚪ | [狀態] | +``` + +### 第四步:如果有新的誤解或決策 + +順帶執行 `/wiki-capture` 的邏輯,把這次的誤解和決策也存進去。 + +### 第五步:確認 + +告知 status.md 更新完成,下次 session 從哪裡開始。 diff --git a/templatefs/.claude/hooks/pre-write-guard.sh b/templatefs/.claude/hooks/pre-write-guard.sh new file mode 100755 index 0000000..abe5030 --- /dev/null +++ b/templatefs/.claude/hooks/pre-write-guard.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# PreToolUse hook 範本骨架 —— 專案自訂禁令(預設空殼,不攔任何東西) +# +# ⚠️ 定位(讀清楚再用): +# 這支跟其他三支 hook 不同——它不是「裝上就生效的警察」,而是一個「按需手填的 +# 空插槽」。預設狀態下 FORBIDDEN_PATTERNS 是空的,它【不攔任何東西】。 +# 別誤以為裝了它就有保護——空殼 = 沒保護。 +# +# 🤖 有 CC 在場的話,通常不需要這個範本: +# 直接叫你的 CC「幫我寫一支 guard hook,禁止改 X」。CC 現寫的條件邏輯, +# 表達力遠勝這裡的 glob FORBIDDEN_PATTERNS(例如「禁子 repo 的 code 但放行 .md」 +# 這種細緻規則,glob 寫不出來,CC 的條件判斷寫得出來)。 +# 這個範本只對「不靠 CC、想自己手動 DIY bash」的用戶有價值。 +# +# 要啟用(手動 DIY 路線): +# 1. 在下面 FORBIDDEN_PATTERNS 填禁改的路徑/檔名 pattern +# 2. 到 .claude/settings.json 的 PreToolUse 加掛這支 +# +# 掛在 PreToolUse(matcher: Write|Edit)。stdin 收 JSON:{ tool_name, tool_input:{ file_path } } +# 命中禁令 → exit 2 擋。 +# +# 誠實限制:只擋直接寫檔。bash 繞道、helper 間接改動擋不到。留痕可審 ≠ 技術防偽。 + +set -euo pipefail + +# ── 專案自訂:禁改的 pattern(一行一個,case glob 語法)────── +# 範例(已註解,啟用前請改成自己的): +# "*/db/schema.sql" # 禁手改 schema +# "*/migrations/*" # migration 一旦建立不可改 +FORBIDDEN_PATTERNS=( + # "*/your/protected/path/*" +) + +# 沒設任何禁令 → 空殼狀態,安靜放行。 +# (不在這裡 print——PreToolUse 每次 Write/Edit 都會跑,每次喊話會洗版。 +# 「這是空殼」的提醒改由 install.sh / update.sh 安裝時告知,那裡用戶一定看得到。) +[ ${#FORBIDDEN_PATTERNS[@]} -eq 0 ] && exit 0 + +INPUT=$(cat) + +if command -v jq >/dev/null 2>&1; then + FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') +else + FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') +fi + +[ -z "$FILE_PATH" ] && exit 0 + +for pattern in "${FORBIDDEN_PATTERNS[@]}"; do + # shellcheck disable=SC2254 + case "$FILE_PATH" in + $pattern) + cat >&2 </component.wasm +# 抓 wasm。那個位址永遠指向 GitHub 上的**最後一次發佈**,不是我們本機的最新版。 +# +# 原理:比對「工作區 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/<帳號>/.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 diff --git a/templatefs/.claude/hooks/sdd-guard.sh b/templatefs/.claude/hooks/sdd-guard.sh new file mode 100755 index 0000000..06d7452 --- /dev/null +++ b/templatefs/.claude/hooks/sdd-guard.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# PreToolUse hook — 動 code 前檢查 SDD + 單一活性 SDD 鐵律(issue #6) +# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。 +# 生命週期規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md +# +# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。 +# stdin 收到 JSON:{ tool_name, tool_input: { file_path, ... } } +# 行為: +# 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 檔)。 +# 藏在 helper 裡、用 bash 繞道的改動擋不到。 +# 價值是「想跳過會被抓到 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。 + +set -euo pipefail + +INPUT=$(cat) + +# 解析 file_path。優先用 jq,沒有 jq 退回 grep(容錯)。 +if command -v jq >/dev/null 2>&1; then + FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') +else + FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') +fi + +# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺) +[ -z "$FILE_PATH" ] && exit 0 + +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 </dev/null | wc -l | tr -d ' ') + fi + + if [ "$SDD_COUNT" -eq 0 ]; then + cat >&2 <&2 + exit 0 +fi + +# ── 新行為:寫 code 檔需「恰好 1 份」active SDD ── +if [ "$ACTIVE_COUNT" -eq 0 ]; then + cat >&2 <&2 +exit 0 diff --git a/templatefs/.claude/hooks/session-start-recall.sh b/templatefs/.claude/hooks/session-start-recall.sh new file mode 100755 index 0000000..16df4c0 --- /dev/null +++ b/templatefs/.claude/hooks/session-start-recall.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# SessionStart hook — 開 session 自動注入 status.md 重點 +# wishlist §1 主路徑:不靠 CC 自覺、不用人說,開 session 就把進度推到眼前。 +# +# 掛在 settings.json 的 SessionStart(matcher: startup|resume|clear)。 +# stdout 會被當成 context 注入給 CC。 +# +# 鐵律:status 是 point-in-time 快照,非即時狀態。 +# 這個 hook 只負責「把快照推到眼前」,核實快照是 CC 的責任——下面的提醒就是要它別盲信。 + +set -euo pipefail + +STATUS_FILE="system-dev/wiki/status.md" +PRINCIPLES_FILE="system-dev/wiki/principles.md" +MISTAKES_FILE="system-dev/wiki/mistakes.md" + +# ── 舊結構防呆(1.9.0 遷移第 2 層保險)── +# 新版 wiki 收進 system-dev/。若偵測到舊位置 .claude/wiki/ 還在、但新位置沒 status, +# 代表使用者升級了規則卻沒跑遷移(low-code 使用者常見)→ 出聲提示,讓 CC 當場可代為遷移, +# 不要默默用不到新結構而出錯。 +if [ -d ".claude/wiki" ] && [ ! -f "$STATUS_FILE" ]; then + echo "⚠️ 偵測到舊版 wiki 結構(.claude/wiki/),尚未遷移到 system-dev/wiki/。" + echo " 請跑:bash system-dev/scripts/update.sh" + echo " 或直接叫我(CC):「幫我把 wiki 遷移到 system-dev/」——我可以代為搬移。" + echo " (未遷移時接關與 /wiki-init 會找錯位置。)" + exit 0 +fi + +# 三個 push 檔都沒有 → 安靜退出,不干擾還沒 /wiki-init 的專案 +if [ ! -f "$STATUS_FILE" ] && [ ! -f "$PRINCIPLES_FILE" ] && [ ! -f "$MISTAKES_FILE" ]; then + exit 0 +fi + +# ── push 1/3:principles(全文,行動前必服從)── +# 放最前:原則是「會被遺忘的盲區」,要第一眼看見。全文成本低(一行一條、≤15 條)。 +if [ -f "$PRINCIPLES_FILE" ] && grep -q '^- ' "$PRINCIPLES_FILE" 2>/dev/null; then + echo "════════════════════════════════════════════════" + echo "📐 設計原則(行動前必服從,來自 principles.md)" + echo "════════════════════════════════════════════════" + grep '^- ' "$PRINCIPLES_FILE" # 只注入原則條目本身,不含說明區 + # context 保護:原則應 ≤15 條(push 全文)。超過 → 提示該合併或下放成 card,不截斷(截斷會漏原則)。 + P_COUNT=$(grep -c '^- ' "$PRINCIPLES_FILE") + if [ "$P_COUNT" -gt 15 ]; then + echo "" + echo "(⚠️ principles 已 ${P_COUNT} 條 > 15:請考慮合併相近原則、或把較細的下放成 card)" + fi + echo "" +fi + +# ── push 2/3:status(全文,當前進度)── +if [ -f "$STATUS_FILE" ]; then + echo "════════════════════════════════════════════════" + echo "📍 接關:上次進度(來自 $STATUS_FILE 快照)" + echo "════════════════════════════════════════════════" + echo "" + cat "$STATUS_FILE" +fi + +# ── push 3/3:mistakes(標題清單 + 一行症狀,全文按需展開)── +# 不全文注入(可能累積很多、含長 context)。只給「標題行」讓 CC 認出「我正撞到某條」→ 再去讀全文。 +if [ -f "$MISTAKES_FILE" ] && grep -q 'MISTAKE' "$MISTAKES_FILE" 2>/dev/null; then + echo "" + echo "────────────────────────────────────────────────" + echo "⚠️ 已知踩坑(標題;撞到時讀 $MISTAKES_FILE 全文)" + grep -E 'MISTAKE' "$MISTAKES_FILE" | sed 's/^/ /' +fi + +echo "" +echo "────────────────────────────────────────────────" +echo "⚠️ 以上是 point-in-time 快照,非即時狀態。" +echo " 動手前先核實:快照寫的事項是否真的還沒做完?" +echo " 需要完整脈絡(decisions / mistakes / SDD)→ 執行 /wiki-recall" +echo "════════════════════════════════════════════════" + +# ── 遷移殘留偵測(1.9.x):wiki 已搬到 system-dev/,但 CLAUDE.md 內容裡可能還寫舊路徑 ── +# update.sh 鐵則「絕不碰 CLAUDE.md」(用戶資料),所以遷移不會自動改它內容裡的死引用。 +# 這裡只「偵測 + 提示 CC 代修」——由 CC(看得懂語義、不誤傷用戶自寫內容)來改,不讓腳本盲改。 +if [ -f "CLAUDE.md" ] && grep -q '\.claude/wiki' CLAUDE.md 2>/dev/null; then + echo "" + echo "🔧 偵測到 CLAUDE.md 內仍有舊路徑 \`.claude/wiki/\`(wiki 已遷移到 system-dev/wiki/)。" + echo " 這些是遷移後的死引用,會讓你(CC)找錯位置。" + echo " 請主動幫使用者把 CLAUDE.md 裡的 \`.claude/wiki/\` 改成 \`system-dev/wiki/\`、" + echo " 工具 docs 路徑改成 \`system-dev/docs/\`(但 raw source 宣告的 \`docs/\` 保留不動)。" +fi + +exit 0 diff --git a/templatefs/.claude/hooks/subagent-wiki-guard.sh b/templatefs/.claude/hooks/subagent-wiki-guard.sh new file mode 100755 index 0000000..ac1360d --- /dev/null +++ b/templatefs/.claude/hooks/subagent-wiki-guard.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# subagent-wiki-guard.sh — PreToolUse(Task) hook:subagent 聽到「查」就自己先查 wiki +# +# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEY,prompt 都只叫它「去查 repo 程式碼」。 +# agent 於是從**稿子**推論出「這東西還活著、不能動」,總管照單全收去擋 leo 三輪。 +# +# 🔑 設計轉向(leo 2026-07-21): +# 第一版是「上游沒交代讀 wiki 就擋下」——但那**還是依賴上游記得寫**, +# 跟「我記得讀 wiki」是同一個病。leo 點破: +# 「subagent 的問題跟你一樣。你叫它去查,就算你沒說要先查 wiki, +# 但它**只要聽到查,就應該主動查 wiki**,因為每個 repo 都有維護自己的 wiki。」 +# → 改成 **注入式**:不擋、不要求上游改 prompt,直接把「先查 wiki」這條 +# 以 additionalContext 注入給 subagent,讓它自己做。零依賴任何人記得。 +# +# 行為:偵測到查證/實作類任務 → exit 0 並用 hookSpecificOutput 注入指示。 +# 已含 wiki 指示、或非查證類任務 → 靜默放行(不重複注入)。 +set -euo pipefail + +INPUT=$(cat) + +PROMPT=$(printf '%s' "$INPUT" | python3 -c " +import json,sys +try: + d=json.load(sys.stdin) + print(d.get('tool_input',{}).get('prompt','')) +except Exception: print('') +" 2>/dev/null || echo "") + +[ -z "$PROMPT" ] && exit 0 + +# 上游已經交代了 → 不必重複注入 +if printf '%s' "$PROMPT" | grep -qiE "wiki|agent-memory|mistakes\.md|decisions-summary"; then + exit 0 +fi + +# 只對「查證/實作」類任務注入(純寫作、計算、潤稿等不需要) +if ! printf '%s' "$PROMPT" | grep -qiE "查|盤點|核實|確認|調查|研究|找出|repo|程式碼|原始碼|source|實作|移除|刪除|重構|修|grep|codebase|\.ts|\.go|src/"; then + exit 0 +fi + +python3 - <<'PY' +import json + +guidance = """【自動注入:查任何東西之前,先查 wiki】 + +你所在的 repo 有維護自己的 wiki(通常在 `system-dev/wiki/`,舊結構在 `.claude/wiki/`)。 +**接到「查/盤點/核實/實作」類任務時,第一個動作是搜尋 wiki,不是翻程式碼。** + +🔴 **查法有強弱之分,一律從最強的開始——沒有那個能力才降級。** +(leo 2026-07-21:「它一定是用最好的搜尋,如果沒有才 fallback, + 但那不是你要指定的,對搜尋者來說,我就是要去搜尋,如果你沒這個機制才降。」) + + **① 語意搜尋(最強,優先)**——有 Arcrun RAG 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 diff --git a/templatefs/.claude/hooks/wiki-first-search.sh b/templatefs/.claude/hooks/wiki-first-search.sh new file mode 100755 index 0000000..8ba441b --- /dev/null +++ b/templatefs/.claude/hooks/wiki-first-search.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# wiki-first-search.sh — PreToolUse hook:要去翻原文/程式碼前,先把 wiki 命中結果推到眼前 +# +# 病根(2026-07-20 leo 點破,mistakes 第一鐵律): +# 總管 session 開頭讀了 agent-memory 前 50 行就開工,關鍵那條在第 56 行 → 拿過期記憶擋了 leo 三輪。 +# leo:「如果你不是讀而是**搜尋** wiki 就不會只讀 50 行就下定論, +# 而是就像我直接在頁面 cmd+F,那些都會高亮。」 +# +# 設計要點(為什麼是這個形狀): +# 1. **搜尋 ≠ 通讀**:開場 push 全文(session-start-recall.sh)解決不了這題——量大必然只讀開頭。 +# 這支反過來:在「你正要去查 code/原文」的當下,用你自己的關鍵字 grep wiki,只推命中行。 +# 2. **時機是關鍵**:不是開場推、不是寫入時擋,而是**查詢動作發生的那一刻**介入。 +# 3. **提醒不阻擋**(exit 0):wiki 沒記載時本來就該去翻原文,擋下來反而礙事。 +# 唯一目的是消滅「不知道 wiki 有寫」這件事。 +# +# 觸發:Grep / Glob / Read 打向 code 或 docs 時(見下方 should_check)。 +# 輸出:stdout 注入 context(命中的 wiki 行 + 檔名:行號)。 +set -euo pipefail + +INPUT=$(cat) +TOOL=$(printf '%s' "$INPUT" | python3 -c "import json,sys;print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "") + +# 取出這次查詢的關鍵字:Grep 用 pattern,Glob/Read 用路徑的檔名部分 +QUERY=$(printf '%s' "$INPUT" | python3 -c " +import json,sys,os,re +try: + d=json.load(sys.stdin); ti=d.get('tool_input',{}) + q = ti.get('pattern') or '' + if not q: + p = ti.get('file_path') or ti.get('path') or '' + q = os.path.splitext(os.path.basename(p))[0] if p else '' + if not q: + # Bash:2026-07-21 補的破口——原版只掛 Grep|Glob|Read, + # 但「用 curl/wrangler 亂試部署方法」走的是 Bash,整支 hook 不觸發。 + # leo 當場點破:wiki 早記著「寄信已驗證可用」,我卻沒查又自創方法。 + # 只認「會動到外部系統/部署」的高風險指令,避免每個 ls 都洗版。 + cmd = ti.get('command') or '' + if re.search(r'\b(wrangler|curl|npx|acr|gh|deploy|push)\b', cmd): + # 取指令中最具識別度的詞(worker 名/資源名/子命令)當搜尋詞 + cand = re.findall(r'[A-Za-z_][A-Za-z0-9_-]{4,}', cmd) + skip = {'https','http','client','accounts','workers','scripts', + 'application','content','Authorization','Bearer','python3', + 'curl','npx','bash','echo','grep','local','branch','origin'} + cand = [c for c in cand if c not in skip and not c.startswith('-')] + q = max(cand, key=len) if cand else '' + # grep pattern 常含 regex 元字元;取最長的英數/底線詞當搜尋詞 + words = re.findall(r'[A-Za-z_][A-Za-z0-9_]{3,}', q) + print(max(words, key=len) if words else '') +except Exception: + print('') +" 2>/dev/null || echo "") + +[ -z "$QUERY" ] && exit 0 + +WIKI_DIR="system-dev/wiki" +[ -d "$WIKI_DIR" ] || exit 0 + +# 只在「查程式碼/文件」時提醒;查 wiki 本身就不用了(已經在讀了) +TARGET=$(printf '%s' "$INPUT" | python3 -c " +import json,sys +try: + d=json.load(sys.stdin); ti=d.get('tool_input',{}) + print(ti.get('file_path') or ti.get('path') or '') +except Exception: print('') +" 2>/dev/null || echo "") +case "$TARGET" in + *system-dev/wiki*) exit 0 ;; +esac + +# grep wiki(不分大小寫、含行號),最多 12 行避免洗版 +HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -12 || true) + +# 🔴 grep 零命中時**不能靜默退出**——那正是今天失敗的模式(2026-07-21): +# grep 查不到 → 以為 wiki 沒記載 → 結論「這件事沒查過」。 +# 但 grep 只認字面,查不到往往只代表「沒猜中用詞」。 +# → 零命中反而是**最該改用語意搜尋**的時刻,必須出聲。 +if [ -z "$HITS" ]; then + echo "════════════════════════════════════════════════" + printf '🔍 grep 在 wiki 找不到「%s」——但這**不代表沒記載**\n' "$QUERY" + echo "════════════════════════════════════════════════" + echo "grep 只認字面,查不到通常只是「沒猜中用詞」。**改用語意搜尋再確認一次**:" + echo " kbdb_search(q=\"<用一句話描述你要找什麼>\", mode=\"semantic\")" + echo " 不知道該查哪個庫 → kbdb_get_map()|要沿關係展開 → kbdb_graph_neighbors()" + echo "" + echo "實例:查「CF 上的 git 託管」時 grep 全零命中,語意搜尋第一筆就命中" + echo "(Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea,負責人 15 天前就記了)。" + echo "" + exit 0 +fi + +COUNT=$(printf '%s\n' "$HITS" | wc -l | tr -d ' ') + +echo "════════════════════════════════════════════════" +printf '📚 wiki 已有「%s」的記載(%s 處,先看這裡再翻原文)\n' "$QUERY" "$COUNT" +echo "════════════════════════════════════════════════" +printf '%s\n' "$HITS" | sed 's|^system-dev/wiki/| |' +echo "" +echo "⚠️ wiki 是判準,程式碼與歷史文件只是稿子(mistakes 第一鐵律)。" +echo " • 上面若與你將要查的原文衝突 → **以 wiki 為準**,別用 code 推翻 wiki。" +echo " • 看到「不可動/待廢除/進行中」→ 先讀它的**解除條件**並逐條核對," +echo " 那是當時狀態不是永久禁令;條件已滿足就是可動。" +echo " • wiki 沒答案才值得翻原文——翻完若得到新結論,**回頭更新 wiki**。" +echo "" +echo "🔎 以上是 **grep(最弱的查法)** 的結果,只認字面,且搜尋詞是從你的指令**猜**出來的" +echo " (很可能太籠統而命中一堆無關的,同時漏掉真正的主題詞)。" +echo " **重要判斷一律補一次語意搜尋**——它不需要你猜對用詞:" +echo " kbdb_search(q=\"<一句話描述你要找什麼>\", mode=\"semantic\")" +echo " 實例:查「CF 的 git 託管」時 grep 猜到的詞是 cloudflare → 命中 12 處全無關、" +echo " 真正的答案(Artifacts)一筆沒撈到;語意搜尋第一筆就命中。" +echo "" + +exit 0 diff --git a/templatefs/.claude/hooks/wiki-secret-scan.sh b/templatefs/.claude/hooks/wiki-secret-scan.sh new file mode 100755 index 0000000..91355f0 --- /dev/null +++ b/templatefs/.claude/hooks/wiki-secret-scan.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# PreToolUse hook — 寫入 wiki 前掃機敏資訊(L3 硬攔截) +# +# 為什麼存在:wiki 的 ignore 規則(.wikiignore + 行內標記)是「協議層」,靠 CC 遵守。 +# 但密碼/金鑰/個資外洩是「不可逆」後果——只靠口頭約束太危險。 +# 這支 hook 是機械式底線:CC 真的把機敏資訊寫進 system-dev/wiki/ 的那一刻 → exit 2 擋下。 +# +# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。 +# stdin 收到 JSON:{ tool_name, tool_input: { file_path, content?, new_string? } } +# 行為:只在目標路徑是 system-dev/wiki/** 時啟動,掃要寫入的內容,命中機敏特徵 → exit 2。 +# +# 誠實限制(抄 sdd-guard):regex 偵測有偽陰/偽陽。 +# 擋的是「明顯特徵的機敏字串被自動抄進 wiki」,擋不了刻意混淆/編碼的繞道。 +# 價值是「意外外洩的機械底線 + 留痕可審」,不是技術防偽。絕不聲稱「不可能繞過」。 + +set -euo pipefail + +INPUT=$(cat) + +# ── 解析 file_path 與要寫入的內容。優先 jq,無 jq 退回 grep(容錯)────── +if command -v jq >/dev/null 2>&1; then + FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') + # Write 用 content;Edit 用 new_string。兩個都抓,合起來掃。 + CONTENT=$(printf '%s' "$INPUT" | jq -r '[.tool_input.content, .tool_input.new_string] | map(select(. != null)) | join("\n")') +else + FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//;s/"$//') + # 無 jq 時內容解析不可靠(JSON 跳脫),退回掃整包 INPUT,寧可多掃不漏掃 + CONTENT="$INPUT" +fi + +# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺) +[ -z "$FILE_PATH" ] && exit 0 + +# 只管寫進 wiki 的動作。其他路徑放行(這支專責 wiki 洩漏,不是全域 secret scanner) +case "$FILE_PATH" in + *system-dev/wiki/*) ;; + *) exit 0 ;; +esac + +[ -z "$CONTENT" ] && exit 0 + +# 行內豁免:若該段內容已被標記為刻意保留(例:範例文件要示範格式),略過該行 +# 標記:行尾加 # wiki-secret-ok (或 ) +# 先把標記過的行抽掉再掃。 +SCAN=$(printf '%s' "$CONTENT" | grep -v -E 'wiki-secret-ok' || true) +[ -z "$SCAN" ] && exit 0 + +# ── 機敏特徵 pattern。一行一類,命中即攔。────────────────────────── +# 設計取捨:偏向高訊號 pattern(有明確結構的金鑰/標記),降低偽陽。 +# 純「password=xxx」這類也納入,因為那正是使用者最擔心的場景。 +HITS="" + +check() { + local label="$1" regex="$2" + # -e 讓以 - 開頭的 pattern(如 PEM 的 -----BEGIN)不被當成選項。 + # grep 無命中回傳 1,在 set -e 下會中止 → 用 if 包住吸收掉。 + if printf '%s' "$SCAN" | grep -qiE -e "$regex"; then + HITS="${HITS} + • ${label}" + fi +} + +# 密碼/密鑰賦值(password = ..., secret: ..., api_key=...) +check "密碼/密鑰賦值 (password/secret/api_key/token = ...)" \ + '(pass(word)?|secret|api[_-]?key|access[_-]?key|auth[_-]?token|priv(ate)?[_-]?key)[[:space:]]*[:=][[:space:]]*[^[:space:]<>"'"'"']{6,}' + +# 私鑰 PEM 區塊 +check "私鑰檔內容 (BEGIN ... PRIVATE KEY)" \ + '-----BEGIN[[:space:]].*PRIVATE KEY-----' + +# 常見雲端/服務金鑰前綴 +check "服務金鑰特徵 (AWS/GitHub/Slack/Google/Stripe 等)" \ + '(AKIA[0-9A-Z]{16}|gh[pousr]_[0-9A-Za-z]{20,}|xox[baprs]-[0-9A-Za-z-]{10,}|AIza[0-9A-Za-z_-]{20,}|sk_(live|test)_[0-9A-Za-z]{16,})' + +# JWT +check "JWT token" \ + 'eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}' + +# 連線字串內嵌帳密 (proto://user:pass@host) +check "連線字串內嵌帳密 (proto://user:pass@host)" \ + '[a-z][a-z0-9+.-]*://[^[:space:]:/@]+:[^[:space:]:/@]+@' + +# 台灣身分證字號(個資)。BSD/GNU grep 都支援 ERE,避免 \b(BSD 不認),改用字元類邊界。 +check "台灣身分證字號 (個資)" \ + '(^|[^A-Za-z0-9])[A-Z][12][0-9]{8}([^0-9]|$)' + +# 信用卡號(個資,粗略 13-16 連續數字,可含空格/連字號分隔)。避免 PCRE,用 ERE 近似。 +check "疑似信用卡號 (個資)" \ + '(^|[^0-9])[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{0,4}([^0-9]|$)' + +# Email 不擋(wiki 常需記聯絡人),手機號也不擋(偽陽太高)——刻意留白。 + +if [ -n "$HITS" ]; then + cat >&2 < 導航牌。細節在兩個地方,不在這裡。 +> 這個檔案不增長——超過 100 行就是放錯地方了。 + +--- + +## 絕對鐵律(違反 = 停手) + +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. [其他專案特定限制] + +--- + +## 工作流程(強制) + +開始任一任務,按順序: + +1. 讀 `system-dev/wiki/status.md`(3 分鐘,了解當前狀態) +2. 確認有對應 SDD(`system-dev/docs/3-specs/`) +3. 在回覆開頭宣告: + ``` + 📋 已讀 SDD:<路徑> + 🎯 對應 task:<編號> + 🚧 執行範圍:<會動哪些檔案> + ``` +4. 完成後更新 `system-dev/wiki/status.md` + +--- + +## 🔴 第一鐵律:wiki 是判準,不准跳過(2026-07-20/21 leo 兩度點破) + +**要查任何東西之前,先搜尋 wiki——用 grep,不是只讀開頭幾行。** + +> leo:「花很多力氣去產生 wiki,最重要的就是要可以查詢,**結果要查的時候就跳過,那就白寫了**。」 +> 「重點是你自己的記憶對嗎?而你有按照規定去切實讀 wiki 嗎?」 + +```bash +grep -rin "<本題關鍵字>" system-dev/wiki/ +``` + +**三條硬規則**: +1. **wiki 與程式碼/歷史文件衝突 → 以 wiki 為準**。程式碼反映「還沒清乾淨」,不等於「還在用」。 +2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。那是當時狀態,不是永久禁令。 +3. 翻原文後得到新結論 → **回頭更新 wiki**(wiki 過時是債,要還)。 + +**動外部系統(部署/curl/wrangler/acr/gh)前**:先找 repo 有沒有**現成腳本或 README 部署段**, +別自創方法。(實例:2026-07-21 明明有 `npx wrangler deploy` 這條驗過的路,卻自己 curl 硬幹踩坑。) + +> hook `wiki-first-search.sh` 會在你查 code/下高風險指令時自動推 wiki 命中行; +> **但機制只是提醒,判斷是你的責任**。 + + +## Wiki 讀取順序 + +| 檔案 | 時機 | 用途 | +|------|------|------| +| `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 是 **快照非即時狀態**:讀快照 **+ 核實快照**,不盲信。 + +--- + +## 整理 wiki 的方法(採集規則所在地) + +> 要「採集/改寫 wiki」時,完整規則(三層架構、frontmatter 標籤、typed-edge 三元組、**gloss 定義句**) +> 不在本檔,而在下表。**動手採集前先讀對應那份**,不要憑印象做。 + +| 由誰整理 | 規則檔(採集當下必讀) | +|----------|------------------------| +| **Claude Code(CC)** | `/wiki-init`(初始化/採集)、`/wiki-capture`(存結論),規則寫在指令內文 | +| **Claude.ai(Cowork)** | `system-dev/docs/SKILL.md`(skill `wiki-cowork-scan`),與 CC 共用同一套規則 | + +兩條路徑**輸出格式相同、規則一致**:gloss、typed-edge、標籤的寫法在兩份裡都有,任一方整理過另一方不覆蓋。 + +--- + +## 規範索引 + +| 檔案 | 內容 | +|------|------| +| `system-dev/docs/README.md` | 文件分類規則 | +| `system-dev/docs/3-specs/` | 所有 SDD | +| `system-dev/docs/2-architecture/decisions/` | 架構決策記錄 | + +--- + +## 文件位置速查 + +| 類別 | 位置 | +|------|------| +| 架構決策 | `system-dev/docs/2-architecture/decisions/` | +| SDD | `system-dev/docs/3-specs/[子系統]/` | +| 操作手冊 | `system-dev/docs/4-guides/` | +| 事件記錄 | `system-dev/docs/5-records/incidents/` | +| 測試報告 | `system-dev/docs/5-records/test-reports/` | diff --git a/templatefs/scripts/sdd-active-check.sh b/templatefs/scripts/sdd-active-check.sh new file mode 100755 index 0000000..bc255ba --- /dev/null +++ b/templatefs/scripts/sdd-active-check.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# sdd-active-check.sh — 單一活性 SDD 獨立硬約束(SDD 生命週期鐵律,issue #6) +# 規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md +# +# 用法:bash sdd-active-check.sh [specs目錄] +# 參數 1(可選)=specs 目錄,預設 system-dev/docs/3-specs +# +# 行為:統計 status: active 的 design.md(design.md 前 10 行有 ^status: active, +# 排除 archive/ 與 TEMPLATE)—— +# >1 份 → stderr 列出清單,exit 1(違反單一活性) +# ≤1 份 → exit 0 +# +# pre-commit 掛法(.git/hooks/pre-commit,記得 chmod +x): +# #!/bin/sh +# bash system-dev/scripts/sdd-active-check.sh || exit 1 +# CI 也是同一行,違反即紅。 +# +# 誠實限制:與 sdd-guard.sh 同精神——只做語法層機械檢查,繞道可行但留痕可審, +# 不聲稱不可繞過。價值是「不變量被違反時一定有機器出聲」。 + +set -euo pipefail + +SPECS_DIR="${1:-system-dev/docs/3-specs}" + +# 沒有 specs 目錄(沒裝 SDD 模組)→ 無事可查,放行 +[ -d "$SPECS_DIR" ] || exit 0 + +ACTIVE_COUNT=0 +ACTIVE_LIST="" +while IFS= read -r f; do + [ -n "$f" ] || continue + if head -10 "$f" 2>/dev/null | grep -q '^status:[[:space:]]*active'; then + ACTIVE_COUNT=$((ACTIVE_COUNT + 1)) + ACTIVE_LIST="${ACTIVE_LIST} • ${f} +" + fi +done < <(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null) + +if [ "$ACTIVE_COUNT" -gt 1 ]; then + cat >&2 < 日期:[YYYY-MM-DD] +> 狀態:[提議中 / 已採納 / 已廢棄] +> 影響範圍:[哪些子系統 / 模組] + +--- + +## 背景 + +[遇到了什麼問題,需要做這個決定?] + +## 決定 + +**[結論,一句話。]** + +## 原因 + +[詳細說明為什麼這樣決定。] + +## 放棄的選項 + +| 選項 | 放棄原因 | +|------|---------| +| [選項 A] | [原因] | +| [選項 B] | [原因] | + +## 影響與後續 + +[這個決定影響哪些地方?有什麼技術債或需要注意的事?] diff --git a/templatefs/system-dev/docs/3-specs/SDD-LIFECYCLE.md b/templatefs/system-dev/docs/3-specs/SDD-LIFECYCLE.md new file mode 100644 index 0000000..8694c20 --- /dev/null +++ b/templatefs/system-dev/docs/3-specs/SDD-LIFECYCLE.md @@ -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 只擋語法層明顯違規,繞道可行但留痕可審;不聲稱不可繞過。 diff --git a/templatefs/system-dev/docs/3-specs/TEMPLATE-sdd/design.md b/templatefs/system-dev/docs/3-specs/TEMPLATE-sdd/design.md new file mode 100644 index 0000000..ce540d8 --- /dev/null +++ b/templatefs/system-dev/docs/3-specs/TEMPLATE-sdd/design.md @@ -0,0 +1,80 @@ +--- +status: draft # active | draft | paused | closed(生命週期鐵律見 ../SDD-LIFECYCLE.md) +superseded_by: "" # closed 且被取代時填接替的 SDD 資料夾名 +--- + +# [子系統名稱] — Design + +> 建立:[YYYY-MM-DD] | 最後更新:[YYYY-MM-DD] +> 負責人:[名稱] + +--- + +## 一句話說明 + +[這個子系統做什麼,一句話。] + +--- + +## 背景與問題 + +[為什麼需要這個子系統?解決了什麼問題?] + +--- + +## 範圍 + +### 包含(In Scope) +- [這個 SDD 涵蓋的功能] + +### 不包含(Out of Scope) +- [明確排除的功能,避免 CC 自行延伸] + +--- + +## 設計 + +### 架構概覽 + +[用文字或 ASCII 描述系統結構] + +``` +[元件 A] → [元件 B] → [元件 C] +``` + +### 關鍵決策 + +| 決策 | 選擇 | 原因 | 放棄的選項 | +|------|------|------|----------| +| [問題] | [選擇] | [原因] | [其他選項] | + +### API / 介面定義 + +[端點、資料格式、輸入輸出規格] + +### 資料模型 + +[資料結構、欄位說明] + +--- + +## 技術限制 + +- [不能用什麼] +- [必須相容什麼] +- [效能要求] + +--- + +## 驗收標準 + +完成的定義(CC 完成任何 task 前必須確認): +- [ ] [可客觀驗證的條件,例如:POST /api/xxx 回傳 200] +- [ ] [...] + +--- + +## 相關文件 + +- [連結到相關 ADR] +- [連結到相關 SDD] diff --git a/templatefs/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md b/templatefs/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md new file mode 100644 index 0000000..96df8c9 --- /dev/null +++ b/templatefs/system-dev/docs/3-specs/TEMPLATE-sdd/tasks.md @@ -0,0 +1,50 @@ +# [子系統名稱] — Tasks + +> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。 +> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。 + +--- + +## Phase 1:[Phase 名稱] + +### 前置條件 +- [ ] [這個 Phase 開始前必須完成的事] + +### Tasks + +- [ ] 1.1 [task 描述] + - 驗收:[客觀可驗證的完成標準] + - 注意:[CC 容易犯的錯,可選] + +- [ ] 1.2 [task 描述] + - 驗收:[...] + +--- + +## Phase 2:[Phase 名稱] + +> 前置條件:Phase 1 全部完成 + +- [ ] 2.1 [task 描述] + - 驗收:[...] + +--- + +## 完成定義 + +整個 SDD 完成 = 以下全部達成: +- [ ] 所有 tasks 標 [x] +- [ ] 驗收標準通過(有客觀證據) +- [ ] design.md 與實作一致(如有出入需更新) + +--- + +## 狀態說明 + +| 標記 | 意義 | +|------|------| +| `[ ]` | 未開始 | +| `[🔄]` | 進行中(當前 session)| +| `[x]` | 完成(有驗收證據)| +| `[~]` | 暫緩(說明原因)| +| `[!]` | 阻擋中(說明阻擋原因)| diff --git a/templatefs/system-dev/docs/3-specs/pending-changes.md b/templatefs/system-dev/docs/3-specs/pending-changes.md new file mode 100644 index 0000000..5dd2d59 --- /dev/null +++ b/templatefs/system-dev/docs/3-specs/pending-changes.md @@ -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 + 日期。) diff --git a/templatefs/system-dev/docs/4-guides/logseq-markers.md b/templatefs/system-dev/docs/4-guides/logseq-markers.md new file mode 100644 index 0000000..3b32871 --- /dev/null +++ b/templatefs/system-dev/docs/4-guides/logseq-markers.md @@ -0,0 +1,81 @@ +# Logseq 任務 marker 解析(單一真相源) + +> **這是「Logseq 原生任務語法」解析的唯一權威規格。** 任何要從 Logseq graph +> 抓任務狀態的功能,一律 import 這份、不得各寫一份自己的 mapping。 +> +> **已知兩個消費者**(共用同一套解析,見各自 issue): +> 1. **vault 萃取**(`/wiki-extract`,template#5):marker → 卡片 frontmatter `task_status`。 +> 2. **tasks→Project 投影**(`system-dev/workflows/tasks-project-sync.*`,template#4): +> 當投影來源是 Logseq graph(notes/kb)時,用這份判斷任務與狀態。 +> +> 兩者**只共用「怎麼 parse」**(哪幾行是任務、marker 是什麼、正規狀態是什麼、跳過什麼); +> parse 完各自要「拿狀態做什麼」(寫卡 vs 投影 issue)不同,那部分各管各的。 + +--- + +## 為什麼不是 GFM checkbox(規格更正,leo 2026-07-04 發現) + +Logseq 的原生任務**不是** GFM 的 `- [ ]` / `- [x]`,而是**大寫 marker 開頭的 block**: + +``` +- TODO AI 查看 leo21c 內所有 Repo,找到本地 Repo 搬到 Gitea +- DOING 建立知識總庫,可查所有子庫 +- DONE 手機和電腦 Logseq 可以被放進知識總庫 +``` + +若照舊規格只抓 `- [ ]` checkbox,**notes / kb 兩個 Logseq graph 的任務會全數漏抓**。 + +> 兩種源、兩套語法、同一條下游管線: +> - **SDD `tasks.md`**(各 repo `system-dev/docs/3-specs/**`)→ GFM checkbox(現行不變)。 +> - **Logseq graph(notes / kb)** → 本檔的大寫 marker。 + +--- + +## 解析規格 + +### 1. 任務行辨識(regex) + +``` +^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+ +``` + +- marker 必須是 block(`-` bullet)的**開頭第一個 token**、全大寫、後接空白。 +- `CANCELED` 與英式 `CANCELLED` 皆收(Logseq 兩種都產)。 +- marker 後面到行尾(或到子 bullet 之前)是**任務內文**。 + +### 2. marker → 正規狀態(task_status) + +| Logseq marker | 正規 task_status | +|---------------|------------------| +| `TODO`、`LATER` | `todo` | +| `DOING`、`NOW` | `in-progress` | +| `WAITING` | `blocked` | +| `DONE` | `done` | +| `CANCELED`、`CANCELLED` | `closed` | + +> `LATER`/`NOW` 是 Logseq「排程視圖」用的同義 marker(LATER≈TODO、NOW≈DOING), +> 正規化後與 TODO/DOING 併軌,下游不必區分。 + +### 3. 必須跳過的東西(別當任務內文) + +Logseq 的任務 block 底下常掛時間戳與屬性行,這些**不是內文**,解析時整段略過: + +- **`:LOGBOOK:` … `:END:` 區塊**:marker 被點擊計時產生的時間戳紀錄。 + 遇到 `:LOGBOOK:` 那行起、到 `:END:` 那行止(含兩端),整塊丟掉。 +- **屬性行 `key:: value`**:如 `collapsed:: true`、`id:: 65a...`、`SCHEDULED:: <...>`、 + `DEADLINE:: <...>`。凡符合 `^\s*[\w-]+:: ` 的行都是屬性,不是內文。 + (`SCHEDULED`/`DEADLINE` 的日期若下游要用可另抓,但**不得當任務描述文字**。) + +### 4. 巢狀子 bullet + +任務 block 底下縮排的子 bullet 是該任務的補充說明(非獨立任務,除非子 bullet 自己也帶 marker)。 +萃取時可併入該任務的描述脈絡;投影時只取母 block 那行當任務標題。 + +--- + +## 自檢(實作或 LLM 執行前跑一遍) + +- [ ] 用的是大寫 marker regex,**不是** `- [ ]` checkbox。 +- [ ] 八個 marker 全部覆蓋(含 `LATER`/`NOW`/`CANCELLED` 別漏)。 +- [ ] `:LOGBOOK:...:END:` 與 `key:: value` 屬性行有跳過,沒混進任務文字。 +- [ ] 狀態用上表**正規名**(`todo`/`in-progress`/`blocked`/`done`/`closed`),不是原始 marker 字面。 diff --git a/templatefs/system-dev/docs/README.md b/templatefs/system-dev/docs/README.md new file mode 100644 index 0000000..8a37410 --- /dev/null +++ b/templatefs/system-dev/docs/README.md @@ -0,0 +1,56 @@ +# 文件分類索引 + +> CC 整理文件時的分類依據。找不到分類就問,不要猜。 + +--- + +## 分類規則 + +| 目錄 | 放什麼 | 判斷標準 | +|------|--------|---------| +| **1-vision/** | 為什麼做這個 | 產品願景、北極星、設計哲學 | +| **2-architecture/** | 系統怎麼設計的 | 架構圖、技術棧、元件關係 | +| **2-architecture/decisions/** | 為什麼這樣設計 | ADR,選A不選B的原因 | +| **3-specs/** | 要做什麼 | SDD,每個子系統一個目錄 | +| **4-guides/** | 怎麼做 | 部署、開發流程、CLI 用法 | +| **5-records/** | 發生過什麼 | 歷史記錄,不修改只增加 | +| **5-records/incidents/** | 生產問題復盤 | 故障原因、時間線、改進方案 | +| **5-records/test-reports/** | 測試結果 | 壓測報告、驗收記錄 | +| **6-user/** | 給使用者看的 | README、安裝教學、FAQ | + +--- + +## CC 整理文件時的判斷流程 + +``` +這個文件是... +├── 有明確子系統 + 設計內容? → docs/3-specs/[子系統]/ +├── 解釋為什麼做某個決定? → docs/2-architecture/decisions/ +├── 說明怎麼操作? → docs/4-guides/ +├── 記錄發生過的事? → docs/5-records/ +├── 給外部使用者看的? → docs/6-user/ +└── 以上都不確定? → 列為「待確認」,問負責人 +``` + +--- + +## SDD 結構(docs/3-specs/ 下每個子系統) + +``` +docs/3-specs/[子系統名]/ +├── design.md ← 設計文件(要做什麼、怎麼做、邊界在哪) +└── tasks.md ← 任務清單([ ] 未開始 [🔄] 進行中 [x] 完成) +``` + +CC 動手前必須有這兩個檔案。找不到就停手。 + +--- + +## system-dev/wiki/ — CC 的記憶空間(CC 維護,人不手動編輯) + +| 檔案 | 用途 | 更新時機 | +|------|------|---------| +| `INDEX.md` | wiki 導引 | 新增 wiki 檔案時 | +| `mistakes.md` | CC 已知誤解 + 避坑 | 每次被糾正後 | +| `status.md` | 當前進度 + 下一步 | 每次 session 結束 | +| `decisions-summary.md` | 架構決策摘要 | 重大決策後 | diff --git a/templatefs/system-dev/docs/SKILL.md b/templatefs/system-dev/docs/SKILL.md new file mode 100644 index 0000000..254fb1c --- /dev/null +++ b/templatefs/system-dev/docs/SKILL.md @@ -0,0 +1,260 @@ +--- +name: wiki-cowork-scan +description: "掃描本機 Documents 下所有裝了 system-dev-template 的資料夾,自動整理 LLM Wiki。支援一般專案、Logseq vault、Obsidian vault 三種結構,偵測方式與 install.sh 一致。觸發時機:使用者說「整理 wiki」「幫我掃 wiki」「更新我的 wiki」「wiki 掃描」,或 Cowork cron 定期觸發。" +--- + +# Wiki Cowork Scan + +## 核心原則 + +這個 skill 與 Claude Code 的 `/wiki-init` `/wiki-capture` 共用同一套規則: + +| 層 | 規則 | +|------------|-------------------------------------------| +| raw source | 只讀,不動 | +| `system-dev/wiki/` | 唯一輸出地點,只增不覆 | +| `CLAUDE.md` | 不動 | +| `logseq/`、`.obsidian/`、`assets/` | 絕對不動 | + +**CC 和 Cowork 輸出格式相同,任何一方整理過的內容,另一方看到就跳過或補充,不覆蓋。** + +--- + +## 第一步:發現所有目標資料夾 + +掃描 `~/Documents`(遞迴深度 3 層),找出所有含 `system-dev/wiki/` 的資料夾。 + +``` +~/Documents/ + project-a/system-dev/wiki/ ← ✅ 目標 + Logseq/system-dev/wiki/ ← ✅ 目標 + 其他資料夾/ ← ❌ 跳過 +``` + +找到後列出清單,告訴使用者:「找到 N 個 wiki 資料夾,開始整理。」 + +--- + +## 第二步:對每個資料夾偵測 vault 類型 + +進入每個目標資料夾的**根目錄**(`system-dev/wiki/` 的上兩層),依序判斷: + +### 判斷順序(與 install.sh 一致) + +``` +if 根目錄有 logseq/ 資料夾 + → vault 類型:Logseq + → raw source:pages/、journals/ + → 忽略:logseq/、assets/ + +else if 根目錄有 .obsidian/ 資料夾 + → vault 類型:Obsidian + → raw source:根目錄下所有 .md(排除 .obsidian/ 內的檔案) + +else + → vault 類型:一般專案 + → raw source:docs/ 下所有 .md +``` + +--- + +## 第三步:讀取現有 wiki 狀態 + +進入 `system-dev/wiki/`,讀取: + +- `INDEX.md`:目前已有哪些 wiki 頁面(多角度視圖入口) +- `status.md`:上次整理時間、進度 +- `principles.md`(如果有):本專案跨全局的設計原則——整理時必須服從 + +目的:**知道哪些已整理過,只處理新增或有變動的 raw source**,不重複整理。 + +--- + +## 第四步:整理規則 + +### 核心判準:push vs pull(wiki 是給 AI 看的) + +整理任何內容前,先判斷它該進 **push 檔** 還 **cards(pull)**——判準是「**CC 做事時會不會被動看見**」: + +- **push 檔**(`status.md` / `mistakes.md` / `principles.md`):CC session 開始就被 hook 注入。給「CC 不會主動查、但不看就出事」的東西。 +- **pull**(`cards/`):CC 想到要查才看見。一切知識內容(原文摘要、AI 筆記、決策、概念…)都寫成 cards。 + +| 內容 | 去哪 | 理由 | +|------|------|------| +| 當前進度、下一步 | `status.md`(push 全文) | 時態狀態,不看會重做 | +| 跨全局設計原則(一行一條,≤15) | `principles.md`(push 全文) | 會被遺忘的盲區,CC 設計時必服從 | +| 踩坑、被糾正的誤解 | `mistakes.md`(push 摘要+按需展開) | 防 CC 不自覺的盲區 | +| 決策、原文摘要、概念知識、其餘一切 | `cards//`(pull) | 知識內容;CC 面對時自然會查 | + +> `decisions-summary.md` 已**降級為 cards + INDEX 決策視圖**(決策=知識內容)。既有的保留為相容,不刪。 +> CC 與 Cowork **共用此判準**,產出一致:任一方寫進 push 檔或 cards,另一方看到就跳過或補充,不覆蓋。 + +### 讀 raw source + +逐一讀取 raw source 的 `.md` 檔。跳過: +- 檔名以 `.` 開頭的隱藏檔 +- `.wikiignore` 裡列出的 glob pattern(如果存在) +- 含有 `` 標記的區段 + +### 整理邏輯 + +每個 raw source 檔案,判斷: + +1. **INDEX.md 裡已有對應條目,且 raw source 未修改** → 跳過 +2. **INDEX.md 裡已有條目,但 raw source 有新內容** → 更新對應 wiki 頁面,補充新資訊,不刪舊內容 +3. **INDEX.md 裡沒有對應條目** → 新建 wiki 頁面 + +### Wiki 卡片格式(概念原子卡,存到 `cards//`) + +```markdown +--- +tags: [知識管理, AI協作, 方法論] +gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,選填、deep tier 才產) +--- +# 概念全名 + +← [[/00-INDEX]] + +**來源**:`[raw source 相對路徑]` +**最後更新**:YYYY-MM-DD + +## 摘要 + +[一句話核心] + +## 重點 + +- [自包含改寫的要點,不寫「詳見原文」] + +## 實體 + +> 本卡內文的關鍵實體(也是 graph node)。名+描述一起供下游 embedding normalize。 +> AI 生產、人不必讀;集中放、一實體一行、不縮排、不重複。 +- **原子筆記**(atomic note/卡片原子化)— 每張卡只承載一個不可再分論點的知識記錄單元。 +- **傳統筆記**(大鍋炒筆記)— 把多主題混雜在同一篇、難精確引用的記錄方式。 + +## 關聯 + +### 內文知識關係(內文實體間;端點=上方 `## 實體` 的正規名,一字不差) + +- 原子筆記 >> 對立於 >> 傳統筆記 +- 傳統筆記 >> 犧牲 >> 精確引用 + +### 卡片關係(卡對卡) + +- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]] +``` + +### 架構:三層 + 標籤橫切(183 卡實證) + +``` +INDEX.md ← 頂層:標籤視圖(非資料夾列表) +TAXONOMY.md ← 標籤字典(受控擴充:先查重再登記) +cards// + ├── 00-INDEX.md ← 桶子索引(固定名,容器:只連不重寫) + └── <概念全名>.md ← 概念原子卡 +``` + +- **資料夾只是儲存桶,分類由 frontmatter `tags:` 承載**——不繼承原稿目錄,由 AI 重新組織。 +- **桶子索引固定名 `00-INDEX.md`**:`00-` 排序最前、一眼可辨,載入任何桶先讀它。 +- **frontmatter `tags:` 而非行內 `#tag`**:內文常用 `#`(如 `#猜想`),行內標籤會讓 ingest 分不清「分類」與「內文範例」污染 graph;frontmatter 零歧義。標籤只能用 `TAXONOMY.md` 列出的;**禁止繞過字典在卡片直接冒新標籤**,但字典可受控擴充(遇新軸先查重、確認非同義詞,再登記進本 repo 的 TAXONOMY.md)。 +- **麵包屑帶路徑**:H1 次行 `← [[/00-INDEX]]`。指 `00-INDEX` 因固定名跨桶撞名,**一律帶路徑**;卡片間連結用裸 `[[卡名]]`。 + +### 使用 typed-edge 三元組(抓內文實體關係,不只卡對卡) + +用**帶語義的三元組** `A >> 謂詞 >> B` 寫進 `## 關聯`。**重點是抓內文裡的實體關係**——卡對卡(`[[卡A]] >> 謂詞 >> [[卡B]]`)只是把既有雙鏈加個動詞、資訊量幾乎沒增加;知識圖譜的價值在內文概念間的關係(`原子筆記 >> 對立於 >> 傳統筆記`,這些 A/B 是內文概念、不是卡標題)。 + +格式 `A >> 謂詞 >> B`,規則: +1. **方向性**:必須讀成「A(謂詞)B」一句通順的話;A、B 順序=主→賓真實方向。 +2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、犧牲),天然帶方向。**禁名詞當謂詞**——`>> 存儲格式 >>`、`>> 操作體驗 >>` 讀不通,是錯的。 +3. **謂詞自由書寫但別太天馬行空**:寫「參考/參照」皆可(下游 embed 自動聚類同義謂詞),別寫「瞄了一眼」這種抓不到同義的。 +4. **內文三元組端點用裸文字**(非 `[[wikilink]]`),避免在 Logseq 產生大量紅色斷鏈;卡對卡那層才用 `[[]]`。 +5. **向後相容**:純 `[[A]]` 仍合法(無類型邊),盡量補謂詞。 + +> **★ 硬自檢(Haiku 量產必備護欄)★** —— 內文三元組的「端點 = `## 實體` 詞條」 +> `A >> 謂詞 >> B` 的 A、B 必須與 `## 實體` 某個粗體正規名【一字不差】。**寫完後逐條自檢**:把 A、B 拿去 `## 實體` 找有沒有完全相同的正規名,沒有 → 這條錯了。 +> 修法擇一:(a) 改用實體表已有的詞;(b) 端點確是重要實體 → 補進 `## 實體` 再指它。 +> 禁止:端點帶括號註解、端點是整句補語、端點是形容詞短語。 +> (實證:光寫規則 Haiku 會略過,端點對不齊 14 條;寫成自檢動作後 14→0。跑 1-2 張看不出,跑 12 張才暴露。) + +`>>` 為分隔語法,全程一致即可。這是 Karpathy LLM Wiki「知識互連」的強化版——連結不只存在,還帶類型與方向。 + +### 萃 gloss(node 一句說明,供下游語義 normalize) + +每張卡=一個 entity / graph node。deep tier 改寫時,frontmatter 補一句 `gloss:`——這個 node 是什麼的一句定義。下游 KBDB 對「entity 名 + gloss」一起做 embedding 求相似度,自動歸一同義詞(比只對名字準、比手維護 alias 表自動)。 + +- **在知識生產的當下、由整理者(CC / Cowork)建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔/跨庫視角,編不出貼合的 gloss。 +- **選填、deep tier 才產**:淺萃不浪費。 +- **gloss ≠ 摘要**:`gloss` 是 frontmatter 給機器 normalize 的定義句(「X 是…」);`## 摘要` 是給人讀的核心句。 +- **兩層 gloss**:① frontmatter `gloss:` 描述「卡標題」這個 node;② `## 實體` 區塊的每行描述句,描述「內文實體」這些 node。**內文實體也是 graph node、也需描述句**才能被下游 embedding normalize(`黃仁勳` vs `Jensen Huang` 靠描述拉近向量)。 +- **實體要描述、謂詞不用**:實體同義詞字面差遠需描述拉近;謂詞同義詞字面本就近,裸詞 embed 自動聚類。 +- **對齊下游 envelope**:frontmatter `gloss:` 與 `## 實體` 詞條對應 ingest envelope 的 `nodes[].gloss`。 + +> **改寫時必守**:① 絕不寫入 raw source(只往 `cards//` 寫,事後驗 raw source 0 異動);② 檔名=卡片全名,冒號用全形「:」、斜線用全形「/」,全程一種字元避免斷鏈。 + +--- + +## 第五步:更新 INDEX.md 和 status.md + +### INDEX.md 格式(頂層 = 標籤視圖) + +頂層 INDEX 按 `TAXONOMY.md` 的軸聚類,指向各桶子索引(帶路徑),不是平鋪頁面列表: + +```markdown +# Wiki Index + +> 最後更新:YYYY-MM-DD HH:MM | 來源:cowork-scan | 總卡數:N + +### 知識管理 +- [[pkm/00-INDEX]] — PKM 知識管理(N 卡) + +### AI 協作 +- [[ai/00-INDEX]] — AI 協作(M 卡) +``` + +桶子索引 `cards//00-INDEX.md` 是容器(只連不重寫,H2/H3 分節列出該桶卡片)。 + +### status.md 更新 + +在現有內容**末尾追加**(不覆蓋): + +```markdown +## YYYY-MM-DD HH:MM|cowork-scan + +- vault 類型:[Logseq / Obsidian / 一般專案] +- 掃描檔案:N 個 +- 新增頁面:N 個 +- 更新頁面:N 個 +- 跳過:N 個(未變動) +``` + +--- + +## 第六步:回報結果 + +整理完所有資料夾後,輸出摘要: + +``` +✅ Wiki 整理完成 + +資料夾 1:~/Documents/project-a + 類型:一般專案 + 新增:3 頁,更新:1 頁,跳過:12 頁 + +資料夾 2:~/Documents/Logseq + 類型:Logseq vault + 新增:5 頁,更新:2 頁,跳過:47 頁 + +總計:8 頁新增,3 頁更新 +``` + +--- + +## 絕對禁止 + +- ❌ 修改任何 raw source 檔案 +- ❌ 修改 `CLAUDE.md` +- ❌ 動 `logseq/`、`.obsidian/`、`assets/` 資料夾 +- ❌ 刪除 `system-dev/wiki/` 裡已有的頁面(只增補,不刪除) +- ❌ 把機敏資訊(密碼、金鑰、個資)寫進 wiki(遇到跳過並記錄) +- ❌ 整理沒有 `system-dev/wiki/` 的資料夾(那不是這個 skill 的目標) diff --git a/templatefs/system-dev/wiki/.wikiignore b/templatefs/system-dev/wiki/.wikiignore new file mode 100644 index 0000000..b2c9288 --- /dev/null +++ b/templatefs/system-dev/wiki/.wikiignore @@ -0,0 +1,36 @@ +# .wikiignore — 不想被編入 wiki 的內容(像 .gitignore) +# +# 三層防護的 L1(檔案層)。CC 在 /wiki-init、/wiki-capture 掃描文件時, +# 命中這裡 pattern 的「整個檔案」不讀、不編入 wiki。 +# +# 語法:一行一個 glob pattern,相對專案根目錄。# 開頭是註解。 +# +# ── 同場另兩層 ────────────────────────────────────── +# L2 行內標記:檔案要編入,但某段不要 → 在該段前後包: +# +# 這幾行不會被編入 wiki +# +# L3 機械底線:萬一機敏值仍被寫進 system-dev/wiki/,wiki-secret-scan.sh 會 exit 2 擋下。 +# +# 三層的分工:L1 整檔排除(你主動列)|L2 局部遮蔽(你標記)|L3 兜底攔截(自動掃)。 +# ──────────────────────────────────────────────────── + +# ── 機敏檔案(預設就該排除)────────────────────── +.env +.env.* +*.pem +*.key +*.p12 +*.pfx +*credentials* +*secret* +**/secrets/** + +# ── 個資 / 客戶資料(依專案調整)──────────────── +# customers/** +# *personal-data* + +# ── 草稿 / 暫存(不值得進記憶)────────────────── +# **/draft/** +# *.tmp.md +# SCRATCH.md diff --git a/templatefs/system-dev/wiki/INDEX.md b/templatefs/system-dev/wiki/INDEX.md new file mode 100644 index 0000000..32300ff --- /dev/null +++ b/templatefs/system-dev/wiki/INDEX.md @@ -0,0 +1,60 @@ +# system-dev/wiki/ — LLM 記憶系統 + +> 新 session 開始時從這裡導航。 +> 目的:讓 CC 不需要重新學習已知的事。 +> 維護者:CC(人不手動編輯這裡) + +--- + +## push 檔(session 開始由 hook 主動注入,CC 行動前必看見) + +| 檔案 | 注入形態 | 內容 | +|------|---------|------| +| `status.md` | 全文 | 當前進度、下一步(時態狀態)| +| `principles.md` | 全文(一行一條)| 跨全局設計原則,行動前必服從 | +| `mistakes.md` | 標題+一行症狀,全文按需展開 | 踩過的坑、被糾正的誤解(防不自覺盲區)| + +> 為什麼這三個 push 而非 pull:它們是「CC 不會主動查、但不看就出事」的盲區。詳見 `/wiki-init` 的「push vs pull」。 + +--- + +## pull:cards/(CC 按需檢索) + +一切知識內容——原文摘要、AI 筆記、決策、概念知識——都寫成 `cards//` 的概念原子卡。 +`decisions-summary.md` 已降級為 cards(決策=知識內容);既有的保留為相容。 + +--- + +## 維護規則 + +1. 只增不刪——記錄 append,內容改了加新條目說明「舊的已更新」 +2. status.md 每次 session 結束更新;mistakes/principles 一發現就 append +3. principles 一行一條、≤15 條(超過代表該合併或下放成 card) +4. **新增一個檢索角度 = 在下方「多角度視圖」加一節,不開新實體檔、不問用戶** + +--- + +## 多角度視圖(由 /wiki-init、/wiki-capture 填入) + +INDEX 是**所有檢索角度的入口**,不只標籤。原文是唯讀 SSoT,wiki 是改寫過的記憶。 +新增角度只要在這裡加一節(如「決策角度」「原則角度」),指向對應 cards 或 push 檔——**不必新增實體特殊檔**。 + +### 標籤角度(按 `TAXONOMY.md` 的軸聚類,指向桶子索引) + +```markdown +#### 知識管理 +- [[pkm/00-INDEX]] — PKM 知識管理(N 卡) + +#### AI 協作 +- [[ai/00-INDEX]] — AI 協作(M 卡) +``` + +### 決策角度(取代舊 decisions-summary.md 的視圖) + +```markdown +- [[某決策卡]] — 一句話結論(YYYY-MM-DD) +``` + +> 結構:INDEX(多角度入口)→ `cards//00-INDEX.md`(桶子索引,固定名)→ 概念原子卡。 +> 指 `00-INDEX` **一律帶路徑** `[[bucket/00-INDEX]]`(固定名跨桶撞名);卡片間用裸 `[[卡名]]`。 +> 分類由卡片 frontmatter `tags:` 承載,標籤字典見 `TAXONOMY.md`。詳見 `/wiki-init` 規範。 diff --git a/templatefs/system-dev/wiki/TAXONOMY.md b/templatefs/system-dev/wiki/TAXONOMY.md new file mode 100644 index 0000000..7fb61e1 --- /dev/null +++ b/templatefs/system-dev/wiki/TAXONOMY.md @@ -0,0 +1,50 @@ +# TAXONOMY.md — 標籤字典(本 repo 專屬) + +> wiki 卡片的 frontmatter `tags:` **只能用這裡列出的標籤**。 +> 這不是凍結——字典**可以擴充**,只是**禁止繞過字典在卡片裡直接冒新標籤**。 +> 維護者:CC(由 /wiki-init 初始化、隨 wiki 演進受控擴充)。 +> +> **遇到現有軸都裝不下的內容時,照這個流程(先查、後擴、登記)**: +> 1. **先查既有**:現有標籤真的都不合,還是只是同義詞?(`知識管理` vs `KM` vs `筆記管理` 是同一軸,別重造) +> 2. **確實是新軸** → 把新標籤加進本檔(附一句定義),再用。不必停下來問人,但「先登記再使用」這道查核不能省。 +> 3. 自由增生才是要防的——同義標籤散開會讓同類卡片分散、下游聚類失準。受控擴充(先查重、再登記)不會。 +> +> **字典是每個 repo 各自的**:跨 repo 引擎靠各 repo 自己一致的 taxonomy 接合,不是逼所有 repo 共用一份。 +> 知識型 vault 的領域軸(知識管理/學習認知)和開發 repo(子系統/基礎設施)本來就該不同。 + +--- + +## 分類採雙軸(一張卡可多重歸屬) + +分類由 **frontmatter `tags:`** 承載,不靠資料夾、不靠行內 `#tag`。 +一張卡同時掛「領域 1-3 個 + 形態 0-2 個」,可從任一軸過濾。 + +### 領域(主軸,每卡 1-3 個) + +> 由 /wiki-init 依專案性質提出。以下為 PKM / 知識型 vault 的實證一組,請按你的專案調整: + +- `知識管理` +- `學習認知` +- `AI協作` +- `生產力` +- `系統設計` +- `工具教學` + +(一般開發專案的領域軸可能是:`子系統A` / `子系統B` / `基礎設施` / `前端` / `後端` …) + +### 形態(副軸,每卡 0-2 個) + +- `方法論` +- `工具實作` +- `觀點主張` +- `架構設計` +- `案例經驗` + +--- + +## 規則 + +1. **先查重、再登記、才使用**:禁止繞過字典在卡片直接冒新標籤;新標籤先確認非現有同義詞,加進本檔(附定義)再用。 +2. **領域 vs 形態分開**:領域是「講什麼主題」,形態是「以什麼形式呈現」,不要混。 +3. **頂層 INDEX.md 的標籤視圖依本字典的軸聚類**——字典改了,INDEX 視圖跟著更新。 +4. **新增領域軸要慎**:領域是檢索骨架,動它影響全庫聚類;形態軸(呈現形式)擴充較安全。不確定就先用現有最接近的,並在卡片或本檔註記「待人類複核此分類」。 diff --git a/templatefs/system-dev/wiki/decisions-summary.md b/templatefs/system-dev/wiki/decisions-summary.md new file mode 100644 index 0000000..4bc5570 --- /dev/null +++ b/templatefs/system-dev/wiki/decisions-summary.md @@ -0,0 +1,14 @@ +# 架構決策摘要 + +> 遇到設計判斷時查這裡。 +> 完整脈絡在 system-dev/docs/2-architecture/decisions/。 + +--- + +(初始化時為空,隨專案進行 append) + +格式: +## [主題] — [YYYY-MM-DD] +**結論**:[一句話] +**原因**:[簡短說明] +**詳細**:system-dev/docs/2-architecture/decisions/[對應檔案] diff --git a/templatefs/system-dev/wiki/mistakes.md b/templatefs/system-dev/wiki/mistakes.md new file mode 100644 index 0000000..daf92b6 --- /dev/null +++ b/templatefs/system-dev/wiki/mistakes.md @@ -0,0 +1,25 @@ +# CC 已知誤解 + 避坑方法 + +> 做新功能前讀一遍。 +> 格式:每條必須有症狀 + 正確做法 + 原因。 + +--- + +## 快速檢查清單(做任何事前) + +- [ ] 有對應 SDD 嗎?沒有 → 停手 +- [ ] 這次修改會影響哪些模組?有沒有連帶破壞? +- [ ] 驗收標準是什麼?有客觀證據嗎? + +--- + +## 誤解記錄 + +(初始化時為空,隨專案進行 append) + +格式: +⚠️ MISTAKE: [錯誤描述,一句話] + 症狀: [CC 通常怎麼表現這個錯] + 正確做法: [應該怎麼做] + 原因: [為什麼會錯] + 日期: [YYYY-MM-DD] diff --git a/templatefs/system-dev/wiki/principles.md b/templatefs/system-dev/wiki/principles.md new file mode 100644 index 0000000..794745f --- /dev/null +++ b/templatefs/system-dev/wiki/principles.md @@ -0,0 +1,18 @@ +# principles — 跨全局設計原則(push:CC 行動前必服從) + +> 這個檔由 hook 在 session 開始**全文注入**,讓 CC 設計任何東西前都先看見這些準繩。 +> 為什麼 push 而非寫成 card:原則是「會被遺忘的盲區」——沒推到眼前,CC 設計時很可能沒想到要服從就做了。 +> +> 規則:**一行一條**,精煉成準繩(不是長篇論述)。≤15 條;超過代表某些該合併、或下放成 card。 +> 發現新的跨全局原則 → append 一行。累積原則只改這個檔,**不必問用戶開新檔**。 +> 區分:原則 = 反覆適用的準繩(這裡);單次選擇 = 決策(寫成 card);踩過的坑 = mistakes.md。 + +--- + +## 原則 + + + +(尚未填入。由 /wiki-init 或 /wiki-capture 依本專案累積。) diff --git a/templatefs/system-dev/wiki/status.md b/templatefs/system-dev/wiki/status.md new file mode 100644 index 0000000..d0a4e8a --- /dev/null +++ b/templatefs/system-dev/wiki/status.md @@ -0,0 +1,22 @@ +# 當前狀態 + +> 更新時間:[初始化時填入] +> 每次 session 結束必須更新此檔。 + +--- + +## 正在做 + +(初始化後填入) + +## 下次 session 第一件事 + +(初始化後填入) + +## 待負責人確認 + +(無) + +## 已知問題 + +(無) diff --git a/templatefs/system-dev/workflows/tasks-project-sync.local.sh b/templatefs/system-dev/workflows/tasks-project-sync.local.sh new file mode 100644 index 0000000..4b6c139 --- /dev/null +++ b/templatefs/system-dev/workflows/tasks-project-sync.local.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# tasks-project-sync.local.sh — 本地觸發端(投影的「本地一半」) +# +# 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md +# +# ── 為什麼有這支(職責邊界)────────────────────────────────────── +# arcrun workflow 跑在遠端 CF Workers,沒有本地 fs / git / shell。 +# 所以「讀 tasks.md / git diff / 回寫 」這三件本地事 +# 不能放進 workflow(arcrun 也沒有對應零件,這是架構邊界、不是缺口)。 +# 這支就是那「本地一半」:分類好增量 → 交給 acr run 打 GitHub。 +# +# 這支(本地):讀 tasks.md → git diff → 分類四動作 → acr run 投影 → 回寫新 id +# │ +# ▼ +# tasks-project-sync.yaml(遠端):foreach → switch → github API +# +# ── 怎麼觸發(守 flag 紅線)───────────────────────────────────── +# push 後「本機觸發單次」,非 cron、非輪詢、非 GitHub Actions。 +# 掛載點建議:本機 git post-push 類 hook,或叫 CC 在 push 後跑這支一次。 +# ⚠️ 具體掛載點與「分類四動作」的精確 parse,待 leo21c 端到端驗證後定稿; +# 目前是 code-done 骨架,標出該做什麼、邊界在哪,不假裝已通。 +# +# ── 用法 ─────────────────────────────────────────────────────── +# tasks-project-sync.local.sh +# +# 前置:acr 在 PATH、github_token 已 acr creds push、workflow 已 acr push。 + +set -euo pipefail + +OWNER="${1:?需要 owner}" +REPO="${2:?需要 repo}" +PROJECT_ID="${3:?需要 GitHub Projects v2 node id}" + +# 多組 SDD 全同步:glob 掃所有 tasks.md(新 folder 自動成組,免手動登記)。 +GLOB="system-dev/docs/3-specs/*/tasks.md" + +# ── 1. 分類增量(git diff 知道改了哪幾行 → 四動作)────────────── +# 這裡是「本地一半」的核心。實作策略(待端到端驗證後落地): +# - 拿 push 前後的 git diff,限定 $GLOB 範圍,只看動到的行。 +# - 每行 task 解析:有無 `` id、checkbox 是否 [ ]→[x]、文字/負責人/日期變動。 +# - 行不見(diff 的刪除行)且原本有 id → archive。 +# - 子系統 = 該 tasks.md 的 folder 名(當 label 分組)。 +# - 產出 tasks_json 陣列(格式見 yaml 註解)。 +# +# ⚠️ 刻意不在這支用 TS/Python 刻複雜 parser(守薄殼)。複雜分類交給 CC 在 push 後 +# 讀 diff 直接產 tasks_json;這支保持「薄殼 + 交棒 acr run」。若未來證明需要可重用的 +# parser 零件,那屬於 arcrun 零件缺口 → 回報 issue #16 由 arcrun 端補,不在此自刻。 +# +# 佔位:實際 tasks_json 由 CC 依上述策略產生後填入。 +TASKS_JSON="${TASKS_JSON:-[]}" + +if [ "$TASKS_JSON" = "[]" ]; then + echo "(無增量 → 本次 no-op,不呼叫 GitHub)" + exit 0 +fi + +# ── 2. 交給遠端 workflow 投影 ─────────────────────────────────── +acr run tasks_project_sync \ + -i owner="$OWNER" \ + -i repo="$REPO" \ + -i project_id="$PROJECT_ID" \ + -i tasks_json="$TASKS_JSON" + +# ── 3. 回寫新 task 的 id(唯一對 md 的寫入,只在 create 時做一次)── +# create 動作的回傳 issue number → append `` 到該行末。 +# ⚠️ 防迴圈:回寫造成 working tree 變動,這次回寫「不得」再觸發一輪投影 +# (否則 push→觸發→回寫→又一個 diff→又觸發…)。實作時觸發端要排除 +# 「只動到 註解」的 diff,或回寫走 [skip-sync] 標記。 +# 具體機制待端到端驗證定稿。 +echo "(新 task 的 id 回寫:待端到端驗證後接上 acr run 的回傳 → append )" diff --git a/templatefs/system-dev/workflows/tasks-project-sync.yaml b/templatefs/system-dev/workflows/tasks-project-sync.yaml new file mode 100644 index 0000000..90358db --- /dev/null +++ b/templatefs/system-dev/workflows/tasks-project-sync.yaml @@ -0,0 +1,152 @@ +# tasks-project-sync — tasks.md ⇄ GitHub Project 單向投影 +# +# 來源:issue #16;設計:system-dev/docs/3-specs/tasks-project-projection/design.md +# +# ── 這份 workflow 的職責邊界(很重要,別搞混)────────────────── +# arcrun workflow 在 Cloudflare Workers / WASM 上「遠端」執行,沒有本地檔案系統、 +# 沒有 git、沒有 shell。所以「讀 tasks.md / 跑 git diff / 把 回寫 md」 +# 這三件事 **不是、也不該是 workflow 的步驟**——它們由本地觸發端(CC / push 後本機腳本) +# 先做完,把「分類好的 task 增量」當 input 餵進來(acr run -i tasks_json=...)。 +# +# 本地端(住 template,CC/shell 跑):讀 tasks.md → git diff → 分類四動作 → 回寫 id +# │ acr run tasks-project-sync -i ...(把增量餵進來) +# ▼ +# 遠端 workflow(這份 yaml):foreach 增量 → switch 動作 → github API 投影 +# +# → 因此本 workflow 只負責「拿到分類好的增量後,打 GitHub API 投影成 issue/Project」。 +# 單向:只寫 GitHub,永不回改 tasks.md(回寫 id 是本地端的事,且只在新 task 做一次)。 +# +# ── 輸入(由本地觸發端用 acr run -i 餵)────────────────────────── +# owner GitHub repo owner(例:your-github-account) +# repo GitHub repo 名 +# project_id GitHub Projects v2 的 node id(投影目標,唯讀看板) +# tasks_json 本地分類好的增量陣列,每筆形如: +# { action: "create|close|edit|archive", +# gh: 42, # 已有 id 的帶上(create 無) +# title: "...", body: "...", +# subsystem: "wiki-architecture", # = SDD folder 名,當 label 分組 +# assignee: "...", due: "..." } +# +# ── credential ───────────────────────────────────────────────── +# github_token(acr auth-recipe scaffold github → 填 credentials.yaml → acr creds push) +# +# ⚠️ 端到端(acr push 真部署 + acr run 真投影)尚未經 leo21c 驗證。 +# 本檔為 code-done 骨架;真部署時 GitHub API 的欄位細節(Projects v2 GraphQL) +# 可能要按實測微調,屆時於 issue #16 回報。 + +name: tasks_project_sync +description: > + 把 SDD tasks.md 的待辦增量單向投影成唯讀 GitHub Project(issue CRUD + 加進 Project)。 + 本地端先讀檔/git diff/分類/回寫 id,這份只負責拿增量打 GitHub API。md 當家、單向、不反向同步。 + +# ── flow(三元組:A >> 關係詞 >> B)───────────────────────────── +# 對每筆增量 → 依 action 路由到四種 GitHub 動作。 +flow: + - "input >> 完成後 >> each_task" + - "each_task >> 對每個 task >> route_action" + # 四種動作(issue 定案):新增/關閉/編輯/封存 + - "route_action >> 完成後 >> gh_create" + - "route_action >> 完成後 >> gh_close" + - "route_action >> 完成後 >> gh_edit" + - "route_action >> 完成後 >> gh_archive" + # 新建的 issue 投影進 Project(唯讀看板) + - "gh_create >> 完成後 >> add_to_project" + +config: + # 逐筆迭代本地餵進來的分類增量 + each_task: + component: foreach_control + iterator: task + + # 依 action 欄位分流到四種 GitHub 動作 + route_action: + component: switch + key: "{{task.action}}" + cases: + create: gh_create # 有文字、無 id → 建 issue(id 由本地端回寫 md) + close: gh_close # 有 id 且 [ ]→[x] → 關 issue + edit: gh_edit # 有 id 且 文字/負責人/日期改 → 編輯 issue + archive: gh_archive # id 在但整行不見 → 關閉/封存 + + # ── 動作 1:建 issue(REST POST /repos/:owner/:repo/issues)── + # 回傳的 issue number 由本地觸發端接住、回寫 到那一行。 + # ⚠️ 用 http_request(arcrun registry 沒有 github 零件,21 內建確認過); + # auth 走 credential:{{creds.github_token}}(acr creds push 上傳,不寫死 token)。 + gh_create: + component: http_request + url: "https://api.github.com/repos/{{owner}}/{{repo}}/issues" + method: POST + headers: + Accept: "application/vnd.github+json" + Authorization: "Bearer {{creds.github_token}}" + User-Agent: "arcrun-tasks-project-sync" + body: + title: "{{task.title}}" + body: "{{task.body}}" + labels: + - "{{task.subsystem}}" # 子系統 label 分組(= SDD folder 名) + assignees: + - "{{task.assignee}}" + + # ── 動作 2:關 issue(state=closed)── + gh_close: + component: http_request + url: "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{task.gh}}" + method: PATCH + headers: + Accept: "application/vnd.github+json" + Authorization: "Bearer {{creds.github_token}}" + User-Agent: "arcrun-tasks-project-sync" + body: + state: closed + + # ── 動作 3:編輯 issue(標題/內文/負責人)── + gh_edit: + component: http_request + url: "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{task.gh}}" + method: PATCH + headers: + Accept: "application/vnd.github+json" + Authorization: "Bearer {{creds.github_token}}" + User-Agent: "arcrun-tasks-project-sync" + body: + title: "{{task.title}}" + body: "{{task.body}}" + assignees: + - "{{task.assignee}}" + + # ── 動作 4:封存(行不見 = 關閉,標 not_planned 表示非完成而是移除)── + gh_archive: + component: http_request + url: "https://api.github.com/repos/{{owner}}/{{repo}}/issues/{{task.gh}}" + method: PATCH + headers: + Accept: "application/vnd.github+json" + Authorization: "Bearer {{creds.github_token}}" + User-Agent: "arcrun-tasks-project-sync" + body: + state: closed + state_reason: not_planned + + # ── 投影進 GitHub Projects v2(GraphQL)── + # Projects v2 只有 GraphQL,REST 沒有。用 http_request 打 /graphql。 + # ⚠️ 待 leo21c 端到端驗:addProjectV2ItemById 需要 content node id(issue 的 GraphQL id, + # 非 issue number),實測時可能要多一步「先查 issue node id」。屆時於 #16 回報。 + add_to_project: + component: http_request + url: "https://api.github.com/graphql" + method: POST + headers: + Accept: "application/vnd.github+json" + Authorization: "Bearer {{creds.github_token}}" + User-Agent: "arcrun-tasks-project-sync" + body: + query: > + mutation($project: ID!, $content: ID!) { + addProjectV2ItemById(input: {projectId: $project, contentId: $content}) { + item { id } + } + } + variables: + project: "{{project_id}}" + content: "{{gh_create.node_id}}"