5 Commits

Author SHA1 Message Date
Leo 3bc7f3c5f5 派工單只剩票號——閘從驗「有沒有票號」改成驗「是不是只有票號」
leo 2026-08-27(inkstone/ISEP#30 comment 4322/4325/4327):
「這些話票上都沒有,你根本沒照規則做事,你的 hook 讓你這樣搞?」
「你用一個 output parser 把你給 subagent 的指令規範,分作幾點,每一點規定格式,
  照這種散文寫法根本無法迭代」「警察也不能抓」
「交件方式不需要寫,定義在原則裡⋯⋯每次都一樣提取出來變成共通規定」
「(那些 session 事實)這些為什麼不寫到票裡?」「subagent 回覆時要表明身份」

病根:no-ticket-no-dispatch.sh 驗的是「有沒有一行【工單】owner/repo#N」,
而規則的原文是「派工單只寫票號」。⇒ 把 40 行任務全寫在 prompt 裡、票號補一行,
閘照樣放行。2026-08-27 一天內這樣做了 5 次,每次票上都沒有那份任務。
規則存在,閘只驗了它的殼——同款第 N 次(history-first/KBDB-first/stage-first)。

新增 hooks/dispatch-format-guard.sh(PreToolUse Task|Agent),兩件事:
- 擋:【工單】以外還有實質內容就 exit 2,並指出那些內容該搬去哪
  (每次都一樣 → 共通規定;這次才知道 → 寫進那張票。
   判準「這句話換一張票還成立嗎?」)
- 注入:合規的派工自動把共通規定送給收工方(交件方式、不准 push main、
  org 是 inkstone…)——這是「派工單只剩票號」能成立的前提,
  leo 的驗收條件之一就是「收工方沒讀派工單也知道要貼回原票」

判準是結構不是文字(leo 2026-08-17 那條檢驗):問的是「這一行是不是【工單】欄位」
——在不在,不是寫什麼。hooks/lib/dispatch_parse.py 全檔零個「命中某個詞就違規」的比對。
⇒ 也因此不需要語意判官:免費、瞬間、每次結果一樣。

新增 hooks/reply-identity-guard.sh(PreToolUse Bash)+ scripts/ticket 內建檢查:
票上每一則留言第一行要有【身份】(總管/subagent/leo)。貼留言有兩條路,兩條都封
——ticket-api-bypass-guard 是刻意放行「對既有票留言」的,只封正門等於沒封。
實害:多條線並行時總管寫的診斷被當成 subagent 的結論,而其中一則是錯的。

規約寫成文件:docs/governance/dispatch-and-reply-format.md
(§2 那段就是被注入的那份共通規定本體——只有一份,改那裡等於改所有派工)

實測(離線、不打網路、不花錢):
  hooks/tests/dispatch-format-guard.test.sh   19/19
  hooks/tests/reply-identity.test.sh          11/11
測資裡的 B⑨ 是真跡:產生 ISEP#30 這條線的那一次派工,一字未改。
另 4 份 leo 點名的違規派工拿不回來了——它們住在 prompt 裡,agent 一停就沒了,
這件事本身就是這條規則的證據(見 hooks/tests/fixtures/README.md,不用想像的例子替補)。

升版 0.4.0 → 0.5.0(產物按版本號分資料夾,不升版新閘不會被載入)。
tag 照慣例打在 merge commit 上,所以這條分支上 check-version-consistency.sh 是紅的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:35:03 +08:00
Leo 41c56acd32 推 main 的戳記改綁「push 真正的目標 repo」,不再綁 hook 自己的 cwd
跨 repo 交辦時(總管站在 A repo,要推 B repo 的 main)main-and-prod-push-guard
的戳記機制永遠對不上:HERE 讀的是 hook 自己的 cwd(=session 的真身,不會變),
WANT 是總管替目標 repo(B)寫進戳記的路徑——兩者結構性地不可能相等,不是
判斷錯,是這個情境在舊模型裡根本不存在(inkstone/ISEP#30 comment 3949,
脈絡 inkstone/InkStoneCo#57,2026-08-21 實撞)。

新增 hooks/lib/push_target_dir.py:純 tokenize(不執行任何指令)解析指令裡
`cd <path> && git push` 或 `git -C <path> push` 真正會落地的目錄,對多層 cd
鏈與子殼(`(cd A && ...); git push` 這種子殼 cd 不能外洩出去)都做了範圍化——
這條範圍化是防穿透的關鍵,不是順手:沒有它,`(cd A && true); git push`
會被誤判成推向 A,讓替 A 開的舊戳記錯誤地放行推到殼外真正的目標。解不出來
一律退回舊行為(hook 自己的 cwd),維持 fail-closed 方向不變。

順手修掉補測時自己抓到的另一個洞:`(git push origin HEAD:main)`——單純加一層
括號——舊版目的地判斷完全偵測不到,整段直接放行,跟戳記無關。成因是截斷
refspec 尾巴的 sed 只認 `;`/`&`/`|` 三種字元,沒算到 `)`;補上即可,git 的
refspec 語法本來就不允許出現 `)`,這裡截斷永遠安全。

綁 repo+單次用完即丟兩條 2026-08-11/12 用血換來的性質完全沒有鬆動:只是把
「現在人在哪個 repo」問得更準,比對邏輯一個字沒動。

實測:
- hooks/tests/main-and-prod-push-guard.test.sh 舊有 8 向:8/8
- scripts/test-main-and-prod-push-guard.sh 舊有 11 向:11/11
- 新增 hooks/tests/main-and-prod-push-guard-cross-repo.test.sh 17 向
  (跨 repo 正向/反向不准鬆/git -C/子殼範圍化/括號洞/單次用完即丟/
  900 秒逾時/空戳記/既有行為零回歸):17/17

本輪只驗證,未拿去放行任何真實推送;plugin.json 隨慣例 bump 0.3.4 -> 0.3.5
並重跑 vendor-to-shell.py(.shell-payload 為 gitignore 產物,不入版控)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:18:28 +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 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