58c3689e4b
comment-carries-task-guard 擋下留言時教人跑 `scripts/ticket subtask …`,
而那一行跑不動。兩個獨立的原因,各自都足以害死它:
① 相對路徑對「貼上去那個人的 cwd」解析。總管的 cwd 是 InkStoneCo/,
那裡的 scripts/ticket 是停在 08-27 之前的舊複本(grep -c subtask → 0),
照著貼只印出它「四個動詞」的說明然後 exit 0。
② 用法是手抄的第二份,而它已經漂了:寫了 --assign 卻沒有 --next,
正本會因為「指派了人卻沒寫 --next」直接擋下(exit 2,在打 API 之前)。
修法是拿掉可以漂的那一格,不是把兩份對齊一次:
- 路徑 scripts/ticket 的 self_path()(__file__ 的絕對路徑)
⇒ 印指令的人與跑指令的人是同一個檔案
- 用法 USAGE / usage_text() 一份,閘去 `ticket usage subtask --example` 現要
- 內文模板 body_template() 由 REQUIRED_SECTIONS 現生,跟檢查同源
新增動詞 `ticket usage <verb> [--example] [--parent] [--draft]`。
閘的訊息改成整塊可貼可跑:腳本絕對路徑、母票已填、內文草稿附在同一塊裡,
只剩標題/目標/驗收條件三格是機器不可能知道的。
正本叫不動時 fail-open 的只有「訊息內容」(只指路,不補一份手抄用法),
擋不擋不受影響。
測試 scripts/test-comment-carries-task-guard.sh 22 → 29 條。
新增的 ⑮–㉑ 驗的是「出路走不走得通」,不是「有沒有擋」——
⑱ 把閘 stderr 裡那塊指令原樣抽出來、只填三格、真的執行一次,
離開碼必須是 1(走到網路才停);是 2 就代表這個病復發。
六個變異全部驗過會紅(相對路徑/閘自己抄用法/--assign 沒配 --next/
模板寫死少一段/fallback 補回手抄用法)。
版本:待總管定版(plugin update 比的是版本號不是內容)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SP7DiGMfSkWjZ15CHMbPFE
202 lines
13 KiB
Markdown
202 lines
13 KiB
Markdown
# 已知誤解 / 踩過的坑
|
||
|
||
> 這是 ISEP 自己的坑,不是 InkStoneCo 的(不轉抄,複製即 fork,fork 即漂移)。
|
||
> 撞到新坑就 append 一條;session 開場只推最近幾條標題(見 `hooks/session-start-recall.sh` push 5/5),全文在這裡。
|
||
|
||
⚠️ MISTAKE: 「環境設定」曾經拆成兩份,各自會漂
|
||
症狀: 本機在跑 `InkStoneCo/.claude/`(真身),雲端跑的是 `generate-shell-payload.py`
|
||
另外產生塞進 GitHub 私 repo 的一份(薄殼)。`inkstone/InkStoneCo#57` 實測:薄殼比真身
|
||
少 7 支閘,其中兩支是前一天才立的;`#14` 更早查到雲端 33 支 guard 一支都沒生效。
|
||
正確做法: 只留一份——ISEP 這個 repo 本身就是唯一真相源,本機與雲端裝同一個 plugin。
|
||
改動一律只改這裡,然後兩邊各自 `/plugin update`。不要再改
|
||
`InkStoneCo/.claude/hooks/`(退場中,早晚會刪)。
|
||
原因: 「一份東西兩個副本」在沒有機制強制同步的情況下必然漂移——差異不是誰疏忽,
|
||
是結構本身允許漂移發生。
|
||
日期: 2026-08-20(ISEP `c263866` 建立時就是為了解這個病)
|
||
|
||
⚠️ MISTAKE: hook 路徑寫死會在雲端斷
|
||
症狀: 舊版 hook 若用 `$CLAUDE_PROJECT_DIR/.claude/hooks/...` 或寫死的絕對路徑指向 hook
|
||
腳本自己,雲端執行時的 cwd 不是本機那個真身目錄,路徑就對不到、hook 直接失效
|
||
(正是上一條「雲端 33 支 guard 一支都沒生效」的根因)。
|
||
正確做法: hook 指自己(找到自己在哪、要 source 的其他 hook 檔)一律用官方
|
||
`${CLAUDE_PLUGIN_ROOT}`。腳本內部要指**專案裡的檔案**(如 `system-dev/wiki/`、
|
||
`system-dev/docs/`)才用 `$CLAUDE_PROJECT_DIR`——那些檔案本來就该住在被操作的
|
||
那個 repo 裡,跟 hook 自己的路徑是两回事,别混。
|
||
原因: 兩種路徑指的是完全不同的東西(「plugin 安裝到哪」vs「正在操作哪個專案」),
|
||
混用就是這條坑的直接原因。
|
||
日期: 2026-08-20(README「路徑規約」段記錄,ISEP 0.1.0 把 51 條 hook 路徑全部改過一輪)
|
||
|
||
## ⚠️ MISTAKE: 「裝好了」不等於「它在跑」
|
||
|
||
2026-08-20 實查:ISEP repo 建好、README 寫著 0.1.0、41 支閘都在裡面——
|
||
但 `claude plugin list` 裡**根本沒有 ISEP**。本機仍然在跑 `InkStoneCo/.claude/`,
|
||
Gitea 上**一個 release tag 都沒有**。
|
||
|
||
⇒ 「東西做出來了」與「有人在用它」是兩件事,而只有後者算交付。
|
||
⇒ 判準:**去執行環境查它有沒有被載入**(`claude plugin list` / `claude plugin details`),
|
||
不要從 repo 裡有什麼檔案去推論。
|
||
|
||
## ⚠️ MISTAKE: 判準寫在閘裡了,但那個閘掛在**做完之後**才跑的時機上
|
||
|
||
票: `inkstone/InkStoneCo#55`
|
||
日期: 2026-08-26
|
||
|
||
症狀: leo 一天內好幾次被丟純技術路徑選擇,當場問「**今天已經好幾次問我,
|
||
為什麼 hooks 沒有攔下來?**」,其中一次他直接說「這種問題不要問我,
|
||
我要的是你解決了以後給我 prod」。
|
||
|
||
實查: 總管問 leo 走的動作是 `AskUserQuestion` 這個工具,而
|
||
`hooks.json` 裡 `AskUserQuestion` 出現 **0 次**——沒有任何 matcher,它是裸的。
|
||
判準其實早就寫好了(`self-drive-police.sh` / `self-drive-judge.sh` 用的就是四題公式),
|
||
但那兩支只掛在 `Stop` 與 `SubagentStop`。
|
||
|
||
原因: **判準對了,時機錯了。** `Stop` 是回合結束後才跑——問題早就送到 leo 眼前、
|
||
他早就被打斷了,這時再反問 AI「你查過了嗎」,成本已經轉嫁出去了。
|
||
|
||
正確做法: 攔截點要長在**那個動作發生的那一刻**(`PreToolUse` / `AskUserQuestion`)。
|
||
新增 `hooks/ask-user-question-guard.sh`。
|
||
🔴 **推廣**:以後看到「規則寫了卻沒被攔下來」,先問的不是「判準對不對」,
|
||
而是「**這支閘掛在哪個事件上、那個事件發生時傷害造成了沒有**」。
|
||
|
||
## ⚠️ MISTAKE: hook 訊息用沒加引號的 heredoc,反引號會被當成命令執行
|
||
|
||
票: `inkstone/InkStoneCo#55`
|
||
日期: 2026-08-26
|
||
|
||
症狀: `ask-user-question-guard.sh` 擋下之後,stderr 冒出
|
||
`line 218: system-dev/wiki/: is a directory`,而訊息裡
|
||
「去查 `system-dev/wiki/`」和「`touch /tmp/.ask-ok-<session_id>`」兩行
|
||
**變成空白**。閘照擋 exit 2,所以測試若只看離開碼**完全看不出來**。
|
||
|
||
原因: 寫成 `cat >&2 <<EOF`(heredoc 標記沒加引號)⇒ shell 會對內容做展開,
|
||
而本 repo 的 hook 訊息**慣例上大量使用反引號**標路徑與指令
|
||
⇒ 每一組反引號都被當成命令替換真的去執行。
|
||
|
||
正確做法: hook 的訊息一律用 `cat <<'EOF'`(標記加單引號)。
|
||
需要塞變數就留 `__PLACEHOLDER__`,事後用 python 換掉——
|
||
**不要用 sed**,正體中文加上訊息裡的 `/`、`&`、`\` 讓跳脫非常脆。
|
||
迴歸測試要**檢查訊息內容**,不能只檢查離開碼
|
||
(`hooks/tests/ask-user-question-guard.test.sh` 的 ⑩b 就是這一條)。
|
||
|
||
## ⚠️ MISTAKE: 閘只驗了規則的**殼**,沒驗規則本身
|
||
|
||
`no-ticket-no-dispatch.sh` 掛在派工的當下,檢查「派工單裡有沒有一行 `【工單】owner/repo#N`」。
|
||
規則的原文卻是「**不准把票上已經有的東西再抄一遍進派工單**⋯⋯派工單只寫票號」。
|
||
|
||
⇒ 於是可以**把 40 行任務全寫在 prompt 裡、票號補一行**,閘照樣放行。
|
||
⇒ 2026-08-27 一天之內這樣做了 5 次,每一次票上都沒有那份任務。
|
||
leo:「**這些話票上都沒有,你根本沒照規則做事,你的 hook 讓你這樣搞?**」
|
||
|
||
**根因不是那支閘寫壞了,是它驗的東西比規則小。**
|
||
「有沒有票號」是規則最容易機械化的那一格,所以它被實作了;
|
||
「任務有沒有真的落在票上」比較難,所以沒有——而漏掉的那格才是規則的本體。
|
||
|
||
⇒ **判準:寫完一支閘,回頭把規則原文逐句對一次,問「這一句被驗到了嗎」。**
|
||
只驗得到最容易的那一格 ⇒ 那支閘會製造「有在管」的錯覺,比沒有閘更危險。
|
||
⇒ 同款:history-first/KBDB-first/stage-first/`AskUserQuestion` 裸奔,全是這個形狀。
|
||
|
||
修法(v0.5.0):`dispatch-format-guard.sh`——**派工單 = 票號,多一個字都擋**。
|
||
規則變得比原本更嚴,反而更好驗:判準從「內容夠不夠」變成「這一行是不是【工單】欄位」,
|
||
純結構、不用語意判官、每次結果一樣。
|
||
規約:`docs/governance/dispatch-and-reply-format.md`。
|
||
|
||
日期: 2026-08-27(`inkstone/ISEP#30` comment 4322/4325/4327)
|
||
|
||
## ⚠️ MISTAKE: 「這是 session 才知道的事」被當成寫進 prompt 的正當理由
|
||
|
||
派工鐵律允許派工單帶「這個 session 才知道、票上還沒有的事」。
|
||
總管照字面理解,把 TCC 權限、`main` 是哪顆 commit、正本能從哪裡 clone 三件事寫進 prompt。
|
||
|
||
leo 當場:「**這些為什麼不寫到票裡?**」
|
||
|
||
⇒ **「票上還沒有」不是把它寫進 prompt 的理由——它就是「去把它寫上票」的指令。**
|
||
⇒ 實害(同日):總管停掉重派 3 次,**前兩次的任務與 session 事實全部隨 prompt 蒸發**。
|
||
票活得比任何一個 agent 久,prompt 不是。
|
||
|
||
判準:**「這句話換一張票還成立嗎?」**
|
||
還成立 ⇒ 共通規定(`docs/governance/dispatch-and-reply-format.md` §2,機器自動注入)。
|
||
只有這次成立 ⇒ 寫進那張票。**兩種都不進派工單。**
|
||
|
||
日期: 2026-08-27(`inkstone/ISEP#30` comment 4327)
|
||
|
||
## ⚠️ MISTAKE: fail-closed 的閘,會把「執行裁決」這個動作本身變成不可執行
|
||
|
||
票: `inkstone/ISEP#91`(裁決原文在 `inkstone/InkStoneCo#40` → comment 2942)
|
||
日期: 2026-08-31
|
||
|
||
症狀: leo 2026-08-16 裁定「**取消 Active SDD**」——任務狀態搬到 Gitea 管,
|
||
SDD 只記「起初的樣子」,文件不帶任務 checkbox。
|
||
**11 天過去,這個裁決一個字都沒被執行。**
|
||
|
||
實查: `sdd-guard.sh` 是 fail-closed 的:動 code 檔時 `status: active` 的 SDD
|
||
**不是恰好 1 份就擋(0 份也擋)**,路徑所在的 repo 沒有 `3-specs` 也擋。
|
||
⇒ 照裁決把最後那份 active 拿掉 → 變 0 份 → **任何人動任何
|
||
`.ts`/`.py`/`.go` 全部被擋**。當時沒出事,純粹是因為剛好還剩 1 份。
|
||
而 **ISEP 這個 repo 自己根本沒有 `3-specs`**——本票開工第一件事就實測到:
|
||
改自己的 `hooks/lib/*.py` 當場被這支閘擋下(exit 2)。**它一直在誤攔,只是沒人回報。**
|
||
|
||
原因: **不是有人偷懶,是沒有人把裁決和那支閘連起來看。**
|
||
裁決被讀到了、也沒人反對,但執行它的第一步會當場鎖死自己,
|
||
於是每個人都在那一步前面停下來,而**「我停下來了」不會留下任何痕跡**。
|
||
|
||
正確做法:
|
||
- 收到「取消某個制度」的裁決,第一個動作是**去數還有幾支閘在執行那個制度**,
|
||
並且**先問那些閘是 fail-open 還是 fail-closed**。
|
||
fail-closed 的那幾支決定了執行順序:**先讓閘退役,再拿掉它要的東西**,顛倒就鎖死。
|
||
- 退役要驗的不是「檔案刪了沒有」,是「**那個擋還會不會發生**」。
|
||
`hooks/tests/sdd-guard-retired.test.sh` 把「裁決執行完之後的世界」丟給
|
||
`hooks.json` 上整組寫檔閘(清單當場從 `hooks.json` 讀,不寫死),
|
||
**不准有任何一支用 SDD 當理由擋下來** ⇒ 換個檔名種回來照樣紅。
|
||
|
||
📌 一個沒人敢執行的裁決,看起來跟一個沒人記得的裁決一模一樣。
|
||
差別只有在**去問「執行它的第一步會發生什麼」**的時候才看得出來。
|
||
|
||
📌 KBDB 缺這一段: 2026-08-31 用 `kbdb_search(mode='semantic')` 查
|
||
「SDD 生命週期/單一活性/取消 Active SDD」**0 命中**(最接近的是 2026-08-09
|
||
一張講 SDD × Gitea 整合摩擦的卡,那是裁決之前)。⇒ 這條開發史還沒進 KBDB。
|
||
|
||
## ⚠️ MISTAKE: 閘擋對了,但它印出來的那條出路**跑不動**
|
||
|
||
票: `inkstone/ISEP#112`
|
||
日期: 2026-08-31
|
||
|
||
症狀: `comment-carries-task-guard.sh` 擋下留言時教你跑
|
||
`scripts/ticket subtask …`。照著貼,會印出一段「四個動詞」的說明然後
|
||
什麼都沒發生。被擋的人當下正在做別的事,最可能的反應是**找個理由繞過去**。
|
||
|
||
實查: 兩個**各自獨立**的原因,任何一個單獨存在都足以害死它——
|
||
① **相對路徑**。閘印的是 `scripts/ticket`,而相對路徑是對
|
||
**貼上去那個人的 cwd** 解析的。總管的 cwd 是 `InkStoneCo/`,
|
||
那裡的 `scripts/ticket` 是正本停在 2026-08-27 之前的舊複本:
|
||
`grep -c subtask InkStoneCo/scripts/ticket` → **0**(正本是 10)。
|
||
⇒ 「`subtask` 這個子命令不存在」這個結論**在那個 cwd 底下是真的**。
|
||
② **用法各寫一份**。閘手抄了一段用法,`cmd_subtask` 的 `die()` 又寫了一段。
|
||
兩份已經漂了:閘那份寫 `--assign <誰做>` 卻**沒有 `--next`**,
|
||
而正本會因為「指派了人卻沒寫 --next」直接擋下(實測 exit 2)。
|
||
⇒ **就算路徑對了,那一行照樣跑不完。**
|
||
|
||
原因: 閘的訊息與它教的那支工具是**兩份各自維護的字**。
|
||
`hooks/lib/beacon_report.py` ② 早就會在 SessionStart 報「專案裡有舊複本」,
|
||
**但報告不會改掉閘印出來的那一行**——知道有這個坑,跟那一行會不會踩到它,
|
||
是兩件事。
|
||
|
||
正確做法:
|
||
- **路徑**:印出來要人複製的指令,一律用正本自己的 `__file__` 絕對路徑
|
||
(`scripts/ticket` 的 `self_path()`)。印指令的人與跑指令的人是同一個檔案,
|
||
沒有第二種可能。
|
||
⚠️ 分界線是「**這行字會被誰複製、在哪台機器上跑?**」——
|
||
要**貼進 Gitea 留言**的指令反而要維持相對寫法(留言會被別台機器讀到)。
|
||
- **用法**:只留一份(`scripts/ticket` 的 `USAGE`/`usage_text()`),
|
||
閘去 `ticket usage subtask --example` 現要,不自己抄。
|
||
- **模板**:子票內文草稿由 `REQUIRED_SECTIONS` 現生(`body_template()`),
|
||
跟檢查同源 ⇒ 不可能生出「照著貼卻過不了自己那道閘」的票。
|
||
|
||
🔴 **驗法要驗到「它跑不跑得動」,不是「它有沒有擋」。**
|
||
`scripts/test-comment-carries-task-guard.sh` ⑱ 把閘 stderr 裡那塊指令
|
||
**原樣抽出來、只填三格、真的執行一次**:離開碼 1(走到網路才停)=過,
|
||
2(被某道閘擋下)=這個病復發。⑲ 是它的鑑別力對照組。
|
||
|
||
📌 推廣:**一支閘的價值是「擋下來 + 給一條走得通的路」。**
|
||
出路走不通時它不是幫你,是擋你——而被擋的人不會停下來修閘,
|
||
他會去找繞過去的方法。所以「出路能不能跑」要跟「判準準不準」一樣被測。
|