Leo
|
47ed778cc4
|
改走「直接複製進薄殼 repo」,並修掉一支會偷跑指令的閘
leo 2026-08-21:「你應該把 Plugin 直接裝進 Github repo,從本地直接複製就好了」
為什麼這條對:雲端 session 是 fresh clone 薄殼 repo,而 setup script 讀不到
環境變數。走 marketplace 就得同時處理憑證、repo 可見性、環境快取三件事——
今天這三件各失敗過一次。複製進 repo 之後,clone 下來就有,沒有任何前置條件。
新增 scripts/vendor-to-shell.py:
把 hooks/skills/commands/scripts/.claude-plugin 整份複製到 .claude/isep/,
並把 54 條 hook 註冊改寫成薄殼裡的絕對路徑。
保留 CLAUDE_PLUGIN_ROOT 這個變數名(44 支閘內部靠它定位自己的 lib/),
只是把它指到複製過來的那份。
冒煙測試(54 條註冊全跑一遍,找路徑壞掉的):
第一輪 4 條壞 → 3 條是 log 目錄不存在(已補建 .claude/hooks/)
1 條在真身也一樣壞 ⇒ 不是複製造成的
第二輪 0 條壞
順手修掉那支既有 bug:wiki-first-search.sh
python3 -c 用雙引號,註解裡的反引號被 shell 當指令替換
⇒ 這支閘每次觸發都在偷跑 bge-m3 與 head changelog.md。
改成全形引號後實測靜默 exit 0。
(crude grep 掃出 6 支疑似,但冒煙測試證明只有這一支真的中——
再一次:證據勝過掃描。)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-21 00:51:00 +08:00 |
|
Leo
|
daa1674a20
|
雲端零閘的兩個真因:setup 從不驗證自己+沒有任何 release 撐版本號
2026-08-20 雲端 session 的閘全滅,而三個驗證步驟全部回綠。
今晚在隔離 HOME(GIT_CONFIG_NOSYSTEM=1)重現,把兩件事分開了:
① setup script 的寫法是對的
裸環境失敗、加了 url.insteadOf 就成功 —— x-access-token 這個使用者名稱
Gitea 也接受。所以先前我對 leo 說「URL 重寫沒作用到 marketplace 這條路徑」
是錯的,這裡更正。
(前兩次測試之所以誤導,是因為 /etc/gitconfig 的 macOS keychain helper
還在幫忙 —— 「隔離 HOME」並沒有隔離系統層設定。同一個病第三次。)
② 真正的缺陷是這支腳本從不驗證自己
設完就結束。token 沒生效也不出聲 ⇒ setup log 一片綠、
session 開起來才發現 marketplace 拉不下來,而那時已經沒有任何線索。
本次加兩道自我驗證:git 認證通不通、marketplace 有沒有就位,
任一不通就 exit 1 並印出該查什麼。
③ 新增 isep-presence-beacon.sh(信標,不是閘)
SessionStart 報「ISEP v幾 已載入、幾支閘」。
它的全部意義是鑑別力:這行住在 plugin 裡,所以看得到就一定載入了,
看不到就是零閘。不像 git tag(在三支閘的白名單裡,閘死了照樣過)。
④ plugin.json 0.0.0 → 0.3.0
查清楚了:0.0.0 不是漂移,是誠實 —— ISEP 一個 tag 都沒有,從沒發過 release。
而這正是「雲端拿不到更新」的另一半:claude plugin update 比對版本號,
沒有 release 就永遠沒有新號碼可比。所以這一刀的收工是真的打 tag 發版。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-20 22:59:21 +08:00 |
|
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 |
|
Leo
|
c1d80756d7
|
審核補件:第二個空殼閘(irreversible_dispatch_check.py)+既有測試套
總管複驗 PR 時發現 subagent 只補了一個空殼,還有第二個同款的:
arcrun-intent-guard.sh → exec 不存在的 .py ⇒ 擋掉全部(吵,PR 已修)
irreversible-dispatch-guard.sh → 同款,但寫法是
python3 <不存在> 2>/dev/null || echo '{"verdict":"OK"}'
⇒ **靜默放行全部**(危險,本 commit 補)
實測同一份派工單「驗過了就把舊分支刪掉」:
InkStoneCo 版(有 .py)exit=2 擋 / ISEP 版(缺 .py)exit=0 放行
補完後:不可逆派工 exit=2、正常派工 exit=0、合規工作流 exit=0。
系統性掃描 ISEP 全部 hooks 引用的同目錄檔案:補完後 0 個缺檔(InkStoneCo 本來就是 0)。
順帶把 InkStoneCo 的 hooks/tests/ 四支既有測試一併帶過來。
🔴 這件事很重要:若先併 InkStoneCo#64(刪掉 .claude/hooks/),
這台機器會失去那支唯一還能用的副本——變成真的沒有那道閘。
|
2026-08-20 20:06:23 +08:00 |
|
Leo
|
b790c3a78d
|
fix(hooks): 補回 arcrun_intent_guard.py 主體——這支閘自 ISEP 建立以來就是空殼
發現於 inkstone/InkStoneCo#57/inkstone/ISEP#32:要把 InkStoneCo 本機
`.claude/hooks/arcrun-intent-guard.sh`(連同它同目錄的 arcrun_intent_guard.py)
移除、改吃 ISEP 這份之前,實測 ISEP 版才發現 `hooks/arcrun-intent-guard.sh`
呼叫的 `arcrun_intent_guard.py` 從未進過 ISEP 的 git 歷史(`git log --all` 0
命中)——`exec python3 "$DIR/arcrun_intent_guard.py"` 找不到檔案,
python3 直接噴 OS 層錯誤、exit 2,**每一次 Write/Edit/MultiEdit 都被無條件擋下**,
不是「規則判定違規才擋」,是檔案不存在導致的硬當機。
這比 kbdb_cmd_check.py 那次(PR #39)更嚴重:那次是「該擋的沒擋」(靜默放行),
這次是「不管寫什麼都擋」——方向相反,但同一個病根:hook 的 shell 外殼進了
git,Python 主體沒有。
修法:把 InkStoneCo 本機那份(唯一存在的正本)原樣搬進來。這支腳本本來就設計
成離開 InkStoneCo 語境會優雅放行(`rules_path` 不存在 → return 0),只是
之前連跑到那行都做不到。
實測 3 種情境:
- ISEP 語境(沒有 intent-rules.json):exit 0(修好前是 exit 2,任何寫入都被當機式擋下)
- InkStoneCo 語境、合法內容:exit 0
- InkStoneCo 語境、真違規(`ON_FAILURE` 這種不存在的邊):exit 2(行為不變,沒有變寬鬆)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-20 19:49:34 +08:00 |
|
Leo
|
1dc97f0bd6
|
fix(hooks): 補上 leo21c-write-guard.sh 第二、三次誤攔的修法(InkStoneCo 反向移植)
InkStoneCo/.claude/hooks/leo21c-write-guard.sh 2026-08-20 當天已修過兩次
誤攔(bc8a529/5e62d28),但沒人把修法帶回 ISEP——ISEP 那份還是上線當天
的舊版,會誤擋「git commit 訊息提到 leo21c」「Gitea PATCH 內文提到
leo21c」這類完全碰不到 CF 帳號的動作。
發現於 inkstone/InkStoneCo#57/inkstone/ISEP#32「同一件事只留一份」:
要把 InkStoneCo 本機的閘收斂成只剩 ISEP 這份之前,先確認 ISEP 沒有落後
——結果這支剛好是唯一反向的(其餘 4 支已修過的閘 ISEP 都領先)。
判準改成只認「這個指令會不會真的寫到那台」:
① 寫入請求打到 *.leo21c.workers.dev(POST/PUT/PATCH/DELETE/--data/-d/trigger)
② 用它的 CF 帳號 id 跑 wrangler/acr
其餘一律放行,不管文字裡提到什麼。
實測 6/6(4 該放行/2 該擋,含這次要修的兩個誤攔案例):見 PR 說明。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-20 19:46:24 +08:00 |
|
Leo
|
0cdb6f2c05
|
fix(hooks): 讓四支閘認得出「指令位置」跟「heredoc/引號裡的文字」
inkstone/InkStoneCo#23、#56 同一個病:閘對整條指令字串做關鍵字掃描,
把「檔案內容/留言引用裡剛好提到某個關鍵字」當成「真的在執行」,
同時放過包一層讀取指令、或藏在 heredoc body 裡的真動作。
- 新增共用輔助 hooks/lib/strip_heredoc.py:heredoc body 是資料不是指令,
四支閘(github-contact / main-and-prod-push / stage-before-prod /
kbdb-api-wall 的 Bash 分支)呼叫前一律先拿掉 body 再比對。
- main-and-prod-push-guard.sh:修掉跟 release-tag-guard.sh 同款的
「開頭是讀取工具就整條放行」前綴繞過洞;git push 的偵測改成指令位置比對;
main/master 目標改用單字邊界,不再誤中 "domain" 這種子字串。
- github-contact-guard.sh:拿掉 gh CLI/git push 判準裡「前面隨便一個空白
就算數」的鬆散邊界,只認真正的指令分隔符。
- kbdb-api-wall-guard.sh:Bash 分支原本引用不存在的 kbdb_cmd_check.py,
python3 找不到檔案就吃掉錯誤印 "OK",該分支形同虛設——任何
`wrangler d1 execute` 直打 kbdb 都會被放行。邏輯搬進新檔
hooks/lib/kbdb_cmd_check.py(shlex 分詞、quote-aware),把 .sh 的
參照路徑改過去,補回 Bash 分支的 kbdb-sql-ok 逃生口。
四支各補 InkStoneCo#40 §1 要求的三行中文檔頭。
新增四支可重跑測試(scripts/test-*.sh),共 69 條斷言全過,
含 #23/#56 票上實撞的原始形狀(寫 docs/TESTING.md 的 heredoc、
貼引用 #56 敘述的留言、`grep git push`)。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-20 18:51:52 +08:00 |
|
Leo
|
1dfc4e373a
|
v0.2.1:43 支閘的白話盤點、測試手冊、補上兩個被抓到的洞
leo 2026-08-20 問「InkStoneCo#40 加入了嗎?如果是這樣我應該可以白話文看到 hooks 的內容?」
答案是不行——43 支閘沒有任何白話清單。這一版補上。
docs/hooks-inventory.md 43 支逐支一行,按「你會在什麼時候撞到它」分 9 組
抽驗 5 支逐行核對源碼;順帶抓到 3 支有檔案沒註冊
docs/TESTING.md A1-A8 + B1-B5,每格都有「怎麼跑/該看到什麼/什麼算失敗」
scripts/test-*.sh 兩支閘的測試,共 21 條,全過
兩個實撞的洞:
- release-tag-guard 的排除清單是前綴比對,x 整條放行
(A8 那個新 session 抓到的,總管複驗屬實)。改用 #23 驗證過的判準:
關鍵字要在指令位置才算執行。補 3 條複合指令測試,8/8。
⇒ 這是 InkStoneCo#36「包一層就繞過去」的同一個病,發生在同一天新寫的閘上。
- scripts/ticket 寫死只認名叫 gitea 的 remote,在 ISEP(remote 叫 origin)整個跑不起來
⇒「開票前先搜」那道閘在新 repo 等於不存在。改成掃所有指向本站的 remote + 環境變數 fallback。
A8 已通過:新 session 裡 plugin 的閘真的觸發(exit 2、tag 未建立、訊息來自 plugin 路徑)。
文件漂移訂正:plugin.json 與 README 寫 42 支/52 條,實際 43 支/53 條。
兩支新閘補上 #40 §1 要求的三行中文檔頭。
🔴 但仍違反 #40 §3「新規則一律先 warn」——兩支都是 block。理由記在 #40 留言,等 leo 裁。
|
2026-08-20 17:08:00 +08:00 |
|
Leo
|
2cd3af2764
|
開票的側門也封上:Gitea API 直接開票要有搜尋戳記
leo 2026-08-20 問「如何防止」。
規範早就有(leo 2026-08-16「開票前先去搜尋要開在哪裡」),閘也早就有——
但它長在 scripts/ticket 這支工具裡,而總管當天走 API 這條側門,
一口氣開了 12 張與舊票重疊的票,事後全部只能關掉指回舊票。
⇒ 同一個動作有兩條路,只封了一條。
本支封的是動作:Bash 指令只要打到 issues 集合端點且帶 POST,就要有新鮮的搜尋戳記
(與 scripts/ticket 共用 /tmp/.ticket-where-ok,30 分鐘)。
判準刻意簡單:端點 + 一個裸字 POST。原本寫成逐個比對 -X POST/"POST"/req("POST",
實測第二種就因為跳脫引號逃掉——那正是 leo 08-17 說的「自然語言的變體無限,
blacklist 追不完」。端點已經確定是開票那條,純讀取不會出現 POST,一個字就夠。
實測 13 條:4 種該擋全擋、8 種不該擋(GET/留言/改標籤/關票/scripts\/ticket/
文字提到/grep 自己/撈清單)全放行、有戳記時放行。
|
2026-08-20 16:43:49 +08:00 |
|
Leo
|
e4e3d69acf
|
fix(release): 版本只有 Gitea Releases 答得出來,不再靠 README 自報(inkstone/ISEP#6)
現況:README.md 宣稱「狀態 0.1.0」,但 repo release_counter=0、一個 tag
都沒打。leo 當場指出這是違規,命中規範自己的 E12(宣稱交付但沒有 tag);
leo 補充:「release 不是寫在 readme,要放在 release 裡」。
改法(結構性防漂移,不是靠人記得同步):
- README.md 不再自行宣告版本號,改成指向 Gitea Releases 頁面
- .claude-plugin/plugin.json 的 version 改回哨兵值 0.0.0
(=誠實承認目前沒有一個經過驗證、掛在 Releases 上的版本;
真正打 tag 那天才跟 tag 一起同步成那個號碼)
- 新增 scripts/check-version-consistency.sh:隨時可跑的一致性檢查
(plugin.json version 是否等於最新 tag/README 是否偷偷自報版本)
- 新增 hooks/release-tag-guard.sh:PreToolUse Bash 閘,在真正打 git tag
的那一刻擋下與 plugin.json 不一致的版本號,註冊進 hooks.json
紅線:本次不打 tag、不建 release——那是總管驗過整個 milestone 之後的動作,
這裡交的是機制與草稿。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-20 13:10:00 +08:00 |
|
Leo
|
c2638668e3
|
ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
你自己可以 dogfooding」
搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。
51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。
尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-20 11:41:46 +08:00 |
|