Files
Leo 87186585d6 fix(hooks): sdd-guard.sh 修「解析失敗仍照擋、且訊息洩漏 /nonexistent」(InkStoneCo#22)
症狀(總管 2026-08-12 實撞):寫暫存腳本進 scratchpad
(/private/tmp/.../scratchpad/foo.py)被 sdd-guard.sh 攔下,訊息印出字面的
「/nonexistent/3-specs/ 下找不到任何 SDD」。

兩個洞:
- 洞 A:scratchpad 不在任何 git repo 裡,卻被當成「repo 裡的 code 變動」誤判
  需要 SDD。改成先問 path_in_git_worktree()(見 hooks/lib/path-resolve.sh):
  不在任何 git repo 裡 → SDD 天生管不到,直接放行,不必先猜專案根。
  這個檢查放在 $_root 的 case 分岔之前、對兩邊都適用——第一版只放進「專案外」
  分支,被本次新增的 hooks/tests/sdd-guard.test.sh 抓到一個不對稱漏洞(cwd 剛好
  等於 scratchpad 祖先目錄時會漏判),改成統一檢查後修掉。
- 洞 B:舊版用內部 sentinel `/nonexistent/3-specs` 重用既有的擋下路徑,但這個
  假路徑被直接印進使用者看到的訊息。改用 RESOLVED 旗標記解析成不成功,訊息
  改用人話描述原因,不洩漏假路徑。

fail-closed / fail-open 的判準(票上明確要求回答,不能各憑運氣):
真的落在某個 git repo 裡、但那個 repo 沒有 3-specs(或沒有 active SDD)→
仍然 fail-closed(擋)。理由:這道閘存在的目的就是防止「沒有 SDD 卻能動
code」,把「判斷不出來」直接放行,等於把環境跑歪(cwd 被切走、
$CLAUDE_PROJECT_DIR 沒設)悄悄變成「這道閘關掉了、且沒人知道」——silent
bypass 的代價遠高於多打一次確認。#22 紅線亦明寫「不要把閘改成解析失敗就
放行」。

順手修的殘留 cwd 依賴:SPECS_DIR 的預設值原本是相對路徑
「system-dev/docs/3-specs」,專案內迴圈找不到時會被拿去跟 hook 執行當下的
cwd 兜;改成絕對路徑 $_root/system-dev/docs/3-specs。

同時修 ADR-0001(ISEP 自建 wiki):標題與內文原本會讓人誤解成「ISEP plugin
裝到哪個 repo,就會在那裡自建一份 wiki」,但實際查證(marketplace.json 只宣告
hooks/commands/skills、README 明文排除 wiki/docs、hooks 一律用
${CLAUDE_PLUGIN_ROOT} 讀自己不是寫別處)並非如此——那份 wiki 只是 ISEP 這個
repo自己的開發歷史,跟裝 plugin 無關。唯一真的會在某 repo 建 wiki 的
scripts/install.sh 是 system-dev-template 的獨立安裝器殘留,要手動執行,
作用對象是 cwd 不是「plugin 裝到的地方」——這多半是誤解的真正來源,已在
ADR 的「常見誤解」段說明。

驗證:
- 造出 08-12 原始事故情境(cwd=InkStoneCo、CLAUDE_PROJECT_DIR 未設、寫
  scratchpad),修前擋(印 /nonexistent)、修後放行——實測輸出見票留言。
- 造出「真的在 git repo 裡但沒有 3-specs」情境,修後仍擋、訊息不含
  /nonexistent。
- 新增 hooks/tests/sdd-guard.test.sh:8 案例全過(洞 A/洞 B/fail-open
  陷阱/單一活性違反/恰好一份 active/改文件放行)。
- 既有六套 scripts/test-*.sh 全過,無退步。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 21:03:14 +08:00

89 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0001ISEP 這個 repo 自己維護一份 wiki(記 ISEP 自己的事,跟「裝 plugin」無關)
- **狀態**:已採納(決策未變,本次僅修訂標題與內文的誤導處,見文末「常見誤解」)
- **日期**2026-08-20
- **票**`inkstone/ISEP#3`(原案)、`inkstone/InkStoneCo#22`(本次修訂)
## 先講結論,避免讀到一半就會錯意
本 ADR 談的「wiki」,是 **`inkstone/ISEP` 這個 git repo 自己的開發歷史**——
跟其他任何 repo`InkStoneCo``arcrun`…)在自己 repo 底下放一份
`system-dev/wiki/` 記自己的事,是同一種、完全獨立的東西。
🔴 **這件事不會發生**:把 ISEP 這個 Claude Code plugin「裝」到別的 repo(本機或雲端的
Claude Code session 啟用這個 plugin),**不會在那個 repo 裡多寫出任何檔案**,
更不會在那裡生出一份 `system-dev/wiki/`。「plugin 裝到哪、wiki 就跟著長在哪,
所以每個 repo 都會有兩份」是誤讀——見文末「常見誤解」段的查證。
## 背景
ISEP 是獨立 repo,對外扮演的角色是「環境」(hookscommandsskillsscripts
`README.md`「裝什麼」段列了清單,白紙黑字排除 `wiki/``docs/``_archive/`——
那些是「知識」不是「環境」)。但 ISEP**自己也是一個在持續開發的 repo**:它有自己的
決策(例如這份 ADR 本身)、踩過的坑、現在的狀態。過去要查「ISEP 這裡為什麼這樣設計、
之前討論到哪」,只能回頭 clone InkStoneCo 頂層知識庫,多一層跳轉,而且 ISEP 自己的
開發細節並不天然屬於 InkStoneCo 頂層(那裡管的是跨專案決策,不是單一 repo 的施工細節)。
## 決策
**`inkstone/ISEP` 這個 repo 自己**建立 `system-dev/wiki/`,骨架取自
`inkstone/system-dev-template` 的 wiki template(三層 + 標籤橫切:`INDEX.md`
`TAXONOMY.md``status.md``mistakes.md``principles.md``cards/<bucket>/`),
照它的規約裝,不自創格式。
這份 wiki 只在 ISEP 這個 repo 的 git 歷史裡,跟著 `git clone inkstone/ISEP` 走;
它**不是** plugin payload 的一部分(`plugin.json``marketplace.json` 只宣告
`hooks/``commands/``skills/`,任何 Claude Code session 啟用這個 plugin 時載入的
也只有這些),所以其他 repo 啟用 ISEP plugin 時,這份 wiki 不會、也無法出現在那裡。
**紅線**:這份 wiki 只記 ISEP 自己的事。不把 InkStoneCo 頂層 wiki 的內容複製過來——
複製即 fork,fork 即漂移,跟「真身薄殼合一」(見 `cards/isep/真身薄殼合一.md`)要解的病
是同一種結構性錯誤,只是對象從 hook 換成知識庫。
## 後果
- 好處:接手 ISEP 這個 repo 的 session,在它自己的 checkout 裡就查得到它自己的歷史,
不必先 clone 別的 repo。
- 代價:多一份骨架要維護(跟 InkStoneCo 頂層、以及其他裝了 template 的子 repo 一樣,
各自一份、各自維護,不互相複製)。
- 邊界:跨專案的決策、鐵律、部署架構全局,仍然只在 InkStoneCo 頂層記錄,ISEP 不重複。
## 常見誤解,與查證
**誤解**:「ISEP 這個 plugin 裝到哪個 repo,就會在那個 repo 裡自建一份 wiki,
於是每個裝了 ISEP 的 repo 都會多出兩份(自己的 + ISEP 幫它建的)。」
**這不是實際行為。查證如下(2026-08-20 實查,不是抄口述)**
1. `.claude-plugin/marketplace.json` 把整個 repo 根目錄(`"source": "./"`)宣告成
plugin 來源,Claude Code 依慣例目錄(`hooks/``commands/``skills/`)載入內容;
`README.md`「裝什麼」表列出的也正是這幾個目錄(外加 `scripts/` 供它們呼叫)——
**沒有任何一項是 wiki 或 docs**。啟用這個 plugin,載入的是 hook 腳本的路徑
`${CLAUDE_PLUGIN_ROOT}/hooks/*.sh`)、command/skill 的定義;這個載入過程本身
不涉及「往目前工作的 repo 寫入任何檔案」——它是讀,不是寫。
2. `README.md`「裝什麼」段明文把 `wiki/``docs/``_archive/` 列在「不放」——
這條界線本來就是刻意畫的(環境 vs 知識分離),不是本 ADR 才立的。
3. 全部 hooks 對「自己這支腳本」的路徑一律用 `${CLAUDE_PLUGIN_ROOT}`(不用
`$CLAUDE_PROJECT_DIR`,見 `README.md`「路徑規約」段)——這條規約本身就代表
hook 的邏輯設計上就是「讀 plugin 自己的檔案」,不是「往目前工作的 repo 寫東西」。
4. **唯一一支「真的會在某個 repo 裡建出 wiki」的腳本是 `scripts/install.sh`**——
但它是 `system-dev-template` 的獨立安裝器(不是 ISEP 的功能),要**人或 AI 手動執行
一次**才會動作,且動作對象是**執行當下的 cwd**,不是「ISEP 被啟用的地方」。
它會混進這個 repo,是搬家時帶過來的殘留(`docs/governance/DIVERGENCE-v0.5.0-to-v0.6.0.md`
A6 節已標記它是待清理項,跟 `.claude-plugin` 宣告的 plugin 功能無關)。
**這支腳本的存在,多半就是本誤解真正的來源**——它看起來像「ISEP 會建 wiki」,
但觸發方式(手動跑一次)與作用對象(cwd,不是「plugin 裝到的地方」)都跟
「裝 plugin 就自動建」完全不同。
⇒ 結論:本 ADR 的「wiki」只指 ISEP 這個 repo 自己 checkout 裡的那一份,
跟其他任何 repo 有沒有、要不要各自裝一份 wiki(那是它們自己的 `/wiki-init` 決定),
兩件事互不影響、也不會因為裝了 ISEP plugin 而自動被牽動。
## 相關
- `cards/isep/真身薄殼合一.md`
- `cards/isep/repo邊界與紅線.md`
- `cards/isep/hook路徑規約.md`
- `inkstone/InkStoneCo#22`(本次修訂的來由:leo 讀完舊版誤解成「plugin 裝到哪、
wiki 就跟著建到哪」)