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>
This commit is contained in:
@@ -1,21 +1,40 @@
|
||||
# ADR-0001:ISEP 自建 wiki,不繼承 InkStoneCo 的內容
|
||||
# ADR-0001:ISEP 這個 repo 自己維護一份 wiki(記 ISEP 自己的事,跟「裝 plugin」無關)
|
||||
|
||||
- **狀態**:已採納
|
||||
- **狀態**:已採納(決策未變,本次僅修訂標題與內文的誤導處,見文末「常見誤解」)
|
||||
- **日期**:2026-08-20
|
||||
- **票**:`inkstone/ISEP#3`
|
||||
- **票**:`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,裝的是「環境」(hooks/commands/skills/scripts),本來刻意不放
|
||||
「知識」(wiki/docs/`_archive`)——見 `README.md`「裝什麼」段。但接手 ISEP 的 session
|
||||
(含雲端)若要查「這裡的決定、踩過的坑、現在什麼狀態」,過去只能回頭 clone InkStoneCo
|
||||
頂層知識庫,多一層跳轉、且 ISEP 自己的事並不天然屬於 InkStoneCo 頂層(那裡管的是跨專案決策)。
|
||||
ISEP 是獨立 repo,對外扮演的角色是「環境」(hooks/commands/skills/scripts,
|
||||
`README.md`「裝什麼」段列了清單,白紙黑字排除 `wiki/`/`docs/`/`_archive/`——
|
||||
那些是「知識」不是「環境」)。但 ISEP**自己也是一個在持續開發的 repo**:它有自己的
|
||||
決策(例如這份 ADR 本身)、踩過的坑、現在的狀態。過去要查「ISEP 這裡為什麼這樣設計、
|
||||
之前討論到哪」,只能回頭 clone InkStoneCo 頂層知識庫,多一層跳轉,而且 ISEP 自己的
|
||||
開發細節並不天然屬於 InkStoneCo 頂層(那裡管的是跨專案決策,不是單一 repo 的施工細節)。
|
||||
|
||||
## 決策
|
||||
|
||||
ISEP 建立自己的 `system-dev/wiki/`,骨架取自 `inkstone/system-dev-template` 的 wiki
|
||||
template(三層 + 標籤橫切:`INDEX.md`/`TAXONOMY.md`/`status.md`/`mistakes.md`/
|
||||
`principles.md`/`cards/<bucket>/`),照它的規約裝,不自創格式。
|
||||
**`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`)要解的病
|
||||
@@ -23,12 +42,47 @@ template(三層 + 標籤橫切:`INDEX.md`/`TAXONOMY.md`/`status.md`/`m
|
||||
|
||||
## 後果
|
||||
|
||||
- 好處:接手 session 在 ISEP 內就能查到 ISEP 自己的歷史,不必先 clone 別的 repo。
|
||||
- 代價:多一份骨架要維護(跟 InkStoneCo 頂層、以及其他裝了 template 的子 repo 一樣)。
|
||||
- 好處:接手 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 就跟著建到哪」)
|
||||
|
||||
Reference in New Issue
Block a user