Files
ISEP/system-dev/docs/2-architecture/decisions/ADR-0001-isep-自建wiki.md
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

5.8 KiB
Raw Permalink Blame History

ADR-0001ISEP 這個 repo 自己維護一份 wiki(記 ISEP 自己的事,跟「裝 plugin」無關)

  • 狀態:已採納(決策未變,本次僅修訂標題與內文的誤導處,見文末「常見誤解」)
  • 日期2026-08-20
  • inkstone/ISEP#3(原案)、inkstone/InkStoneCo#22(本次修訂)

先講結論,避免讀到一半就會錯意

本 ADR 談的「wiki」,是 inkstone/ISEP 這個 git repo 自己的開發歷史—— 跟其他任何 repoInkStoneCoarcrun…)在自己 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.mdstatus.mdmistakes.mdprinciples.mdcards/<bucket>/), 照它的規約裝,不自創格式。

這份 wiki 只在 ISEP 這個 repo 的 git 歷史裡,跟著 git clone inkstone/ISEP 走; 它不是 plugin payload 的一部分(plugin.jsonmarketplace.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 就跟著建到哪」)