d4bf172990
leo 2026-09-07:「拆,誰說『同名跨 repo 是 mainline 機制的一部分』,早就定了只有一個 milestone,不同 repo 用指針」。上一版 scripts/mainline 檔頭把現場的樣子(5 個同名)寫成 「既有做法」,成員也照標題跨 repo 收;總管照它做、抄進 ops-facts、還在 arcrun-rag 補建了一個同名的。 改了什麼(判準全部是 Gitea 欄位,沒有標題比對): - hooks/lib/mainline.py:collect_members()=hub 里程碑裡的票+hub 票,沿 /dependencies 邊 (跨 repo)收下去;belongs() 要同時拿到「掛哪個 milestone」+「誰把它當相依」才准說「不屬於」, 只知一半回 None(閘放行);project_roots() 給主線檔與帳本共用 - scripts/mainline:set --hub <票>、adopt 別 repo ⇒ POST 相依到 hub 票(--via 可指定成員), 沒 hub 票時擋下並給兩條走得通的路,不建同名里程碑;has 查 /blocks; refresh --members 才重收(真 Gitea 25 張要 19 秒,SessionStart 成本不變) - scripts/ticket:add_dependency() 一份(subtask 與 adopt 共用,409 冪等);pick 主線組走成員清單, 逾期組一個里程碑物件一組;claim 的主線判定改看相依;pickable(None) 給靠邊進來的票 - hooks/mainline-focus-guard.sh:新鮮查詢問 milestone+/blocks,補收出路明講不要開同名里程碑 - scripts/milestone-account:帳本找家用同一把尺,退回原始碼目錄只在它有 .git(plugin 快取沒有) 測試:新 hooks/tests/mainline-members-by-dependency.test.sh 56 條(假 Gitea 照真的回 201/409); scripts/test-ticket-pick.sh 48→53 條(池子改用相依邊,補「同名沒邊 ⇒ 不抓」); 既有 mainline-focus-guard/repo-mirror/gitea-fallback/idle/milestone-account/debt-worklist/ handoff-writeback 全綠。真 Gitea 唯讀實跑:set --hub inkstone/InkStoneCo#44 收到 25 張、5 個 repo, mira#6/ISEP#130/ISEP#140/arcrun-rag#104 都在,不必新增任何邊。 版本:待總管定版(改了會被載入的 hook/scripts)。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
474 lines
34 KiB
Markdown
474 lines
34 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(被某道閘擋下)=這個病復發。⑲ 是它的鑑別力對照組。
|
||
|
||
📌 推廣:**一支閘的價值是「擋下來 + 給一條走得通的路」。**
|
||
出路走不通時它不是幫你,是擋你——而被擋的人不會停下來修閘,
|
||
他會去找繞過去的方法。所以「出路能不能跑」要跟「判準準不準」一樣被測。
|
||
## ⚠️ MISTAKE: 「雲端該有哪些憑證」的清單是唯一真相源,卻沒有任何東西在跟機器對帳
|
||
|
||
票: `inkstone/ISEP#115`(→ comment `5522`/`5526`)
|
||
日期: 2026-09-01
|
||
|
||
症狀: 雲端 session 要清空 youlin,`env | grep -ciE 'cloudflare|^CF_|wrangler'` → **0**。
|
||
而 `credentials-map.md` 明明寫著那把 token 在 `InkStoneCo/.env`。
|
||
|
||
實查: 不是雲端漏設。`scripts/make-cloud-env.sh` 的 `NEEDED` 當時**只有 1 個變數**,
|
||
裡面從來沒有任何 CF 憑證。而 `inkstone/InkStoneCo#14` comment `3890`
|
||
(2026-08-20)盤點出的 A 段是 **8 個**——那一版三段輸出的腳本,
|
||
到今天為止只活在**沒併進 `main` 的分支 `fix/cloud-env-parity-14`(PR #43)**上。
|
||
|
||
原因: 盤點做在票上、實作做在分支上、`main` 上的清單是另一回事——**三份,互不對帳**。
|
||
🔴 而清單漏一項的代價**在流程末端才付**:工人已經把腳本與測試都寫完了,
|
||
到要真的跑的那一刻才知道跑不了。
|
||
|
||
正確做法: 清單要有測試逼它跟這台機器對帳(`scripts/test-make-cloud-env.sh`,
|
||
`docs/TESTING.md` A26)——B 段逐一去 `.env` 找值,找不到就紅。
|
||
🔴 而「不准混進正式環境憑證」這一條,判準一律是**機器算得出來的事實**,
|
||
**不是名字裡有沒有某個字**——兩條分工:
|
||
B8 看**值從哪個檔案拿到的**(出自 `polaris/mira/.env` = leo21c 現役正式環境就紅);
|
||
B9 看**這個值是不是就是 `CLOUDFLARE_API_TOKEN_leo21c`**(那把住在頂層 `.env`,
|
||
B8 抓不到,而改個名字黑名單就放它過去——實跑證過)。黑名單擋不住沒被列進去的新名字,
|
||
「這個值出自那份 `.env`」是 grep 得出來的事實,換什麼名字都躲不掉。
|
||
同時釘住反面(B8x):拿一個已知住在那份 `.env` 的變數餵它,證明偵測**真的會亮**——
|
||
否則「全綠」有兩種可能(真的乾淨/偵測壞了),而分不開就等於沒驗。
|
||
|
||
⇒ **推廣**:repo 裡任何「這台機器該有什麼」的清單,都要有一支東西定期拿它去問機器。
|
||
清單自己不會知道它漏了什麼。
|
||
|
||
## ⚠️ MISTAKE: 兩支閘對同一條指令說不同的話,而讀的那一半從來沒被測過
|
||
|
||
票: `inkstone/ISEP#130`
|
||
日期: 2026-09-07
|
||
|
||
症狀: 09-04 雲端 Routine 的 run log:讀 `notify_leo` 定義被 `leo21c-write-guard.sh` 擋下。
|
||
閘的判準明寫「**讀可以,寫不行**」,卻連讀都擋 ⇒ 雲端什麼都拿不到。
|
||
|
||
實查: ① 舊判準 `-d[[:space:]]` 把 `| tr -d '\r'`、`cut -d=` 這種唯讀的 `-d` 當成 curl 的 body。
|
||
`prod-write-guard.sh` 在 2026-08-12 修過**一模一樣**的洞(它的檔頭寫著「一堆唯讀工具也用 -d」),
|
||
這支漏了——同一批補丁只改其中一份(`main-and-prod-push-guard.sh` 檔頭記過同款)。
|
||
② `…/webhooks/named/<ns>/notify_leo/trigger` 在 `prod-write-guard.sh` 是白名單(ISEP#63),
|
||
在這支卻因為尾巴是 `/trigger` 照擋 ⇒ 撞人閘時發 Telegram 叫 leo 會被擋(InkStoneCo#110 的病)。
|
||
③ 這支閘從 08-20 建立到現在**沒有任何測試**,`hooks/tests/` 裡 30 支測試沒有它的——
|
||
「讀會不會被誤攔」這格從來沒被驗過,只有「寫會不會被擋」被人肉試過。
|
||
|
||
原因: 同一個判準(「這條指令會不會真的寫到那台」)散在三支閘裡各寫一份,修一份不會帶動另外兩份;
|
||
而沒有測試的那支,誤攔發生時**不會有任何東西喊一聲**——被擋的人在雲端、沒有人在旁邊。
|
||
|
||
正確做法:
|
||
- 修判準時 `grep -l` 找同一族的閘(`prod-write-guard`/`main-and-prod-push-guard`/`leo21c-write-guard`),
|
||
一次改齊;本次三支都補了 youlin 09-02 起的新子網域 `arcrun-yuga3bse`。
|
||
- 每一支「擋」的閘都要有「不該擋」那半的測試,測資用**現場真的會下的指令**
|
||
(這次是 `cloud-worker.md`/`progress-guard.md` 裡的原句),不是自己挑好抓的例子。
|
||
- 修之前拿新測試跑舊閘,紅的那幾條才是真的改到的(本次 5 條紅);全綠就是測試順著新閘寫的。
|
||
|
||
## ⚠️ MISTAKE: 雲端連不到 youlin 被記成「HTTP 000」,而 000 有三種病,兩種都不是連線
|
||
|
||
票: `inkstone/ISEP#130`
|
||
日期: 2026-09-07
|
||
|
||
症狀: 09-04 run log「youlin 從雲端 HTTP 000」;09-07 總管查到舊子網域 `youlin-hsieh-dev` DNS 已死,
|
||
以為換名字就通。從雲端打新名 `arcrun-yuga3bse` **還是 000**。
|
||
|
||
實查: `curl` 印的是 `CONNECT tunnel failed, response 403`,`$HTTPS_PROXY/__agentproxy/status` 記為
|
||
`connect_rejected … gateway answered 403 (policy denial)`。同一個 session:leo21c 200、
|
||
git.uncle6.me 200、api.telegram.org 302——**只有 youlin 的主機沒被雲端 egress policy 放行**。
|
||
`/root/.ccr/README.md` 明寫:403/407 是組織的 egress policy,不要重試、不要繞路,回報主機名。
|
||
|
||
原因: `-w '%{http_code}'` 對「DNS 沒有」「proxy 拒絕 CONNECT」「對方沒回」三種都印 000,
|
||
而三種的修法各在不同人手上(改名字/leo 放行主機/看實例)。把它們記成同一個數字,
|
||
下一個人就會往錯的方向查(09-07 就差點只修名字收工)。
|
||
|
||
正確做法: 雲端看到 000 先跑 `curl -sS "$HTTPS_PROXY/__agentproxy/status" | jq .recentRelayFailures`,
|
||
`connect_rejected` + 403 ⇒ 是 policy,交給 leo 在 Cloud environment 放行 `*.arcrun-yuga3bse.workers.dev`;
|
||
不是 ⇒ 才去查名字或實例。記 log 時寫 curl 的錯誤字串(`response 403`/`Could not resolve host`),不要只寫 000。
|
||
|
||
## ⚠️ MISTAKE: 正門工具吃 `owner/repo` 會回 404,於是人走了側門
|
||
|
||
票: `inkstone/ISEP#130` → comment 6120 第 2 件
|
||
日期: 2026-09-07
|
||
|
||
症狀: `scripts/ticket new inkstone/ISEP -F …` 回 Gitea 404;同一天 `ticket say` 對 InkStoneCo 正常。
|
||
總管當場改成直接打 API 開了四張票。
|
||
|
||
實查: `cmd_new` 把第一個參數原樣塞進 `/repos/inkstone/{repo}/issues` ⇒ `/repos/inkstone/inkstone/ISEP/issues`。
|
||
404 讀起來像「ISEP 這個 repo 不存在」,跟真正的原因隔了一層。`subtask --to` 同款。
|
||
|
||
原因: 票號的鐵律是全稱 `owner/repo#N`(CLAUDE.md),人自然會把收件 repo 也寫成 `owner/repo`;
|
||
工具只收短名、錯了又不講清楚 ⇒ **正門壞了,人就走 `ticket-api-bypass-guard.sh` 在防的那條側門**。
|
||
一道閘把人逼去走它自己禁止的路,那道閘就是在製造違規(`docs/governance/cloud-wiring.md` ② 記過同一句)。
|
||
|
||
正確做法: `repo_arg()` 兩種寫法都收;org 寫錯(`Leo/`)當場講「org 是 inkstone」,不讓它變成一個要人猜的 404。
|
||
測試 `scripts/test-ticket-repo-arg.sh` 把 `api()` 換成假貨記路徑,離線驗路徑裡不准出現 `inkstone/inkstone`。
|
||
|
||
|
||
## ⚠️ MISTAKE: 權限白名單是「逐條比前綴」,複合指令永遠對不上——而假伺服器照自己的想像回 200
|
||
|
||
票: `inkstone/ISEP#137`(母票 `inkstone/ISEP#130`)
|
||
日期: 2026-09-07
|
||
|
||
症狀: 三條主線工人在雲端對 stage 的寫入全被 Claude Code auto mode 分類器擋:
|
||
`printf … | npx wrangler secret put …`、`python3 scripts/stage-deploy-artifacts.py all --confirm`、
|
||
`curl -X POST …arcrun-yuga3bse.workers.dev/records`。同一回合連 `ticket where <含 token 字樣>` 都被擋。
|
||
leo:「為什麼會擋你,這就是要修復的問題」「本機環境跟你不同應該不行」。
|
||
|
||
實查: `docs/permissions-allow.json` 的每一條是**完全比對前綴**。`cd … && printf … | VAR=… npx …` 這種複合指令
|
||
沒有任何一條對得上,任意寫法的 curl 更不可能;放整類(`Bash(npx *)`/`Bash(curl *)`)又是紅線。
|
||
⇒ 分類器擋的不是「這件事」,是「這個形狀沒被事先放行」。
|
||
|
||
原因: 正門工具的價值在**形狀固定**——一條規則就能事先放行。之前四個正門(ticket/mainline/gate-ok/gitea-pr-merge)
|
||
都是這樣活下來的;對 stage 的寫入沒有正門,人就只能拼複合指令,而複合指令天生對不上任何前綴。
|
||
|
||
正確做法:
|
||
- 每一種要放行的動作收成一支固定形狀(`scripts/stage secret …`/`stage api …`),白名單放它四種呼叫形狀。
|
||
「只打 stage」寫死在程式裡(帳號 ID 現查、主機後綴固定),不靠變數名字裡有沒有 `leo21c`。
|
||
- 別的 repo 已經有的正門(Arcrun `stage-deploy-artifacts.py` 自己寫死 youlin 帳號 ID)**直接放它的形狀**,不重造一支。
|
||
- 值只從環境變數名讀;`--value`/`--bearer`/`--header` 一律拒收——指令列會進 shell 歷史與 session log。
|
||
|
||
📌 同票第二個坑:**假伺服器要照真 API 回,不是照自己的想像回。**
|
||
離線測試的假 CF 對 secret PUT 回 200,27/27 全綠;第一次真跑 CF 回 **201**,工具把它講成「失敗」——
|
||
而獨立用 CF API 一列,那把 secret 明明種進去了。全綠的測試證明的是「工具跟假伺服器合得來」,
|
||
不是「工具跟 CF 合得來」。⇒ 假伺服器的回應碼要抄真的(現在回 201),而且**離線全綠之後一定要真跑一次**。
|
||
|
||
## ⚠️ MISTAKE: 前置條件判的是「手邊的樹」,而要送出去的東西是「那個 tag 的樹」
|
||
|
||
票: `inkstone/ISEP#67` → comment 6574
|
||
日期: 2026-09-07
|
||
|
||
症狀: 總管在 v0.23.0 的 checkout 跑 `release-ship v0.23.0 --dry-run` 被擋,訊息說
|
||
「最新 tag 是 v0.24.0」。v0.23.0 的樹明明一致,只因為它不是最新的那個 tag,
|
||
就永遠建不了 release——而 09-07 缺 release 的正是 v0.23.0 與 v0.24.0 兩個。
|
||
|
||
實查: 09-05 那版 release-ship 的前置①直接跑 `check-version-consistency.sh`
|
||
(工作樹 `plugin.json` vs **本機最新** tag)當判準,理由寫著「不另立判準」。
|
||
但那支守的是「打 tag 那一刻 main 跟最新 tag 一致」(release-tag-guard.sh 的事),
|
||
建 release 這一刻要問的是「**要送的那個 tag 的樹**,自己講的版本對不對」——
|
||
工作樹可能是任何分支(release-ship 本身就是在一條 feature 分支上寫的),跟那個 tag 無關。
|
||
|
||
原因: 「重用既有判準」是對的方向,但重用之前要問「**它判的是哪個對象**」。
|
||
對象錯了,判準再對也只是在檢查一個跟交付物無關的東西——而且會用一條看起來
|
||
很有道理的訊息把正確的操作擋掉(誤攔)。
|
||
|
||
正確做法: 前置①改成問 Gitea `contents/.claude-plugin/plugin.json?ref=<tag>`,
|
||
比的是 tag 自己的樹;工作樹的版本完全不參與(`scripts/test-release-ship.sh` 把工作樹
|
||
故意擺成 `9.9.9`,B 群全綠才算證明)。同時把「問 Gitea」收成一份
|
||
(`hooks/lib/release_chain.py`),release-ship/release-check/SessionStart 信標三處共用——
|
||
判準散在三處各寫一份,就是 `leo21c-write-guard.sh` 那一族漂掉的路。
|
||
|
||
📌 判準:**送出去之前,先問「我判的是不是我要送的那個東西」**。
|
||
工作樹、最新 tag、main——都不是;tag 指到的那棵樹才是。
|
||
|
||
## ⚠️ MISTAKE: 派工單的紅線建立在「Gitea 匿名讀得到」上,而那件事沒人打過一次
|
||
|
||
票: `inkstone/ISEP#140`(母票 `inkstone/ISEP#130`)
|
||
日期: 2026-09-07
|
||
|
||
症狀: 工單 comment 6611 寫「主線指標在 Gitea 上讀得到(匿名讀 raw 檔或 API contents,D20 讀不擋)」,
|
||
紅線據此寫成「讀 Gitea 一律匿名,不帶 token」。開工第一個指令匿名打
|
||
`/api/v1/repos/inkstone/InkStoneCo/raw/…`、`/contents/…`、`/repos/inkstone/InkStoneCo` ⇒ **三個都 404**。
|
||
`inkstone/ISEP` 同樣的三個端點 200 ⇒ 不是網路,是 InkStoneCo 在 Gitea 是 private。
|
||
|
||
實查: 照紅線字面做(純匿名)⇒ 這張票在真環境永遠讀不到,測試卻可以全綠(假伺服器照自己的想像回 200,
|
||
同日 #137 那條坑的翻版)。而 SessionStart 同一條鏈上的 `countdown-milestone-refresh.sh` 與
|
||
`mainline refresh` 本來就帶 `GITEA_TOKEN_CLAUDE_CODE` 打 Gitea;D20 管的是 GitHub 那個會被 flag 的帳號,
|
||
不是 Gitea 機器帳號的讀。
|
||
|
||
原因: 「讀得到」是一個可以一個指令驗掉的事實宣稱,卻被當成前提直接寫進紅線;收工方若不打那一下,
|
||
就會交出一個「測試全綠、雲端永遠退回舊線」的東西——形狀跟 #137「假伺服器回 200」一模一樣。
|
||
|
||
正確做法: 匿名優先、匿名被拒(401/403/404)且環境拿得到 token 才帶 token 再問一次(`fetch_gitea()`);
|
||
假伺服器要有 private 模式(匿名 404、帶 token 200)照真 Gitea 回;真跑兩條都跑(帶 token 讀到、拔 token 退回)。
|
||
收到「X 讀得到/X 不存在」這種宣稱先打一次再動手——那是一個 curl 的成本。
|
||
|
||
## ⚠️ MISTAKE: 閘看的是 hook 自己的環境/cwd,不是指令要動的那個 repo
|
||
|
||
票: `inkstone/ISEP#109`(comment 6587/6629);`inkstone/ISEP#125` 是缺陷 A 與 D 的原票(09-02 開)
|
||
日期: 2026-09-07
|
||
|
||
症狀: 同一天兩支閘同一個病。① `line-needs-own-worktree.sh` 印的出路
|
||
`WORKTREE_OK=1 git -C … checkout …` 照貼三次三次被擋。
|
||
② `github-contact-guard.sh` 把 `git -C <InkStoneCo> push origin x` 判成「remote origin 指向 GitHub」擋下
|
||
(總管站在薄殼 cwd;InkStoneCo 的 origin 明明是 Gitea),連 `-C` 都沒看;
|
||
同日 arcrun-hand 在 `Arcrun#176` comment 6614 撞到同一支。當天只能改用完整 Gitea 網址推,
|
||
而那種形狀分類器又時過時不過。
|
||
|
||
實查: ① hook 讀的是 `$WORKTREE_OK` 這個**環境變數**。PreToolUse hook 跟指令不是同一個行程,
|
||
指令字串裡的 `WORKTREE_OK=1` 是給 git 的前綴賦值,hook 的行程裡永遠沒有它
|
||
⇒ 那條出路在 Claude Code 底下**結構上**走不通,不是偶爾。(`NOT_MY_BRANCH_OK` 同形狀,見下)
|
||
② hook 拿 **payload 的 cwd** 去 `git remote get-url origin`,而 `-C`/`cd … &&` 改的是
|
||
指令自己的目錄。`main-and-prod-push-guard.sh` 08-23 就為同一個病接了
|
||
`lib/push_target_dir.py`(`inkstone/ISEP#30` comment 3949)——**同一族的另一支沒跟上**,
|
||
跟 `leo21c-write-guard.sh` 那條坑一模一樣(修一份不會帶動另外兩份)。
|
||
修之前拿新測試跑舊閘:① 3 條紅;② **7 條紅——4 條誤攔之外還有 3 條漏擋**
|
||
(站在 Gitea 那份 `git -C <薄殼> push origin x` 舊版放行)。cwd 不只讓它誤擋,也讓 D20 有洞。
|
||
|
||
原因: 兩支都在回答「這條指令會動到哪個 repo」,卻拿「hook 自己站在哪」當答案。
|
||
本機一份目錄時兩者恰好相等,所以測試全綠;雲端薄殼+真身兩份目錄一分開就露餡。
|
||
而「出路走不走得通」那一格從來沒被測過——測的只有「擋不擋」。
|
||
|
||
正確做法:
|
||
- 出路要是 hook **讀得到**的形狀:認指令字串裡的字面前綴(判準是位置——前綴必須掛在會移動 HEAD
|
||
的那條 git 指令上;印個字、掛在別的指令上、寫在引號裡都不算),或 `gate-ok` 同款的戳記檔。
|
||
- 「這條指令在哪個 repo 執行」一律走 `hooks/lib/push_target_dir.py`/`checkout_target_dir.py`
|
||
解 cd 鏈與 `-C`,解不出來才退回 cwd(退回的方向是照舊,不是放寬)。改判準時 `grep -l` 找同一族
|
||
(`main-and-prod-push-guard`/`github-contact-guard`/`line-needs-own-worktree`/`not-my-branch-guard`)一次看齊。
|
||
- 測試要有一條「把閘印出來的那一行原樣餵回去」(A24 (61)–(63)),還要有 payload 帶 `cwd`、
|
||
cwd 與目標 repo **不同**的案例(A39 後 12 條)——本機一份目錄的測試驗不出這個病。
|
||
|
||
📌 沒在本票修的同形狀:`not-my-branch-guard.sh` 的 `NOT_MY_BRANCH_OK=1 git commit …` 同樣只讀環境變數
|
||
(第 28 行),沒實測、沒動——它認的動作是 commit,不是 push_target_dir 那條線的事,另報。
|
||
|
||
## ⚠️ MISTAKE: 閘教了「開」,「收」只寫在訊息裡——每條線留一份,leo 在 Finder 看到一排分身
|
||
|
||
票: `inkstone/ISEP#147`
|
||
日期: 2026-09-07
|
||
|
||
症狀: leo 打開 `tech_projects/`,一個 repo 旁邊躺著 `ISEP-wt-122ship`/`ISEP-wt-117`/`ISEP-wt115`/
|
||
`InkStoneCo-wt-112`/`course_gen-ticket-3-notes`/`ax-courses-ticket1`…。他原話:
|
||
「你會產生大量的同一個頭不同尾巴的 repo,全部拆開就更亂了」「禁止這樣產出一大堆資料夾」。
|
||
|
||
實查: 那些不是 repo,是 `line-needs-own-worktree.sh`(ISEP#109,08-29)教工人開的 git worktree。
|
||
那支閘比較過「旁邊 vs /tmp」——/tmp 會被系統清掉、登記說謊;旁邊一份都沒掉。
|
||
**一份都沒掉,也一份都沒收**:「收工時 `worktree remove`」只寫在閘訊息裡,
|
||
沒有任何事件、任何 hook 在線收工那一刻去驗。
|
||
|
||
原因: 規則在文字裡,不在機器上(同日 ISEP#109 comment 6629 是同一種病的另一面:出路寫在訊息裡,機器讀不到)。
|
||
一道閘只管「開」不管「收」,等於把清理交給每一條線的記性——而線收工就消失了,記性跟著消失。
|
||
|
||
正確做法:
|
||
- 分身住 repo **裡面**(`<repo>/.worktrees/<票 repo>-<票號>`,走 `.git/info/exclude`):Finder 看不到、
|
||
`git status` 看不到、`git worktree list` 找得到。判準是「leo 在 Finder 只看得到一個資料夾」。
|
||
- **同一支閘**多掛 PostToolUse `Agent|Task`:從派工單的【工單】認出分身,推了且乾淨就收;
|
||
沒推就點名分支與路徑(exit 2),不 force、不刪。教你開的和替你收的是同一件事的兩半,不拆成兩支。
|
||
- 「推了沒」只有一份實作(`scripts/worktree`,三態:有/沒有/問不到——問不到不當成沒推,也不收)。
|
||
- 現場整理是同一支工具的 `sweep`:預設只列不動,`--apply` 只收已推乾淨的;`--skip` 是操作者點名,不是判準。
|
||
- 規約:`docs/governance/worktree-placement.md`。
|
||
|
||
📌 推廣:**一道閘若教人做一件「之後要還」的事(開 worktree、蓋戳記、借共用目錄),
|
||
「還」那一步也要長在某個事件上**,不然它必然被留下。收工的事件是 PostToolUse(Agent|Task),
|
||
票號就在那裡(`baton-handback-guard.sh`/`issue-status-autoflip.sh` 都站在同一個位置)。
|
||
|
||
## ⚠️ MISTAKE: 工具檔頭把「現場的樣子」寫成「既有做法」,於是它變成了規則
|
||
|
||
票: `inkstone/ISEP#133`(→ comment 6743)
|
||
日期: 2026-09-07
|
||
|
||
症狀: `scripts/mainline` 與 `hooks/lib/mainline.py` 檔頭寫「跨 repo 同名是既有做法,不動它」,
|
||
成員判定=「票所屬 milestone 的標題與主線相同(不分 repo)」。總管早上照它做,把那句
|
||
抄進 InkStoneCo 的 ops-facts(「同名里程碑跨 repo 是 mainline 機制的一部分」),
|
||
還在 arcrun-rag 補建了一個同名的(#61)。leo:「**拆,誰說『同名跨 repo 是 mainline
|
||
機制的一部分』,早就定了只有一個 milestone,不同 repo 用指針**」。
|
||
規則其實早就寫在票上:`inkstone/ISEP#30`(08-20)、`inkstone/InkStoneCo#44`(08-27)。
|
||
|
||
原因: ISEP#82 那天現場真的有 5 個同名的「Mira 現代化」,工具作者把「現場如此」記成
|
||
「既有做法」——而讀檔頭的人分不出「作者觀察到的」與「leo 定的」。
|
||
**工具是會被抄的文件**:它說什麼是規則,做的人就照什麼做;寫錯的方向會被放大成動作
|
||
(多建一個里程碑),不是只停在一句話。
|
||
同款:帳本路徑退到「原始碼所在 repo 的 system-dev/」——本機是真 checkout 所以對,
|
||
雲端的原始碼在 plugin 快取,那天記的 5 筆帳下一個 session 就不見。判準寫成
|
||
「原始碼在哪」而不是「哪裡會被 clone 走」,換一台機器就錯。
|
||
|
||
正確做法:
|
||
- 檔頭只准寫**規則與出處**(票號/leo 原話),不准把「現場現在長這樣」寫成「既有做法」。
|
||
要記現場,寫「2026-xx-xx 實查現場是 X」,並註明它不是判準。
|
||
- 判準用 Gitea 的欄位:成員=hub 里程碑裡的票 + hub 票 + 沿 `/dependencies` 收到的(跨 repo);
|
||
`belongs()` 要同時拿到「掛哪個 milestone」與「誰把它當相依」才准說「不屬於」,只知一半回 None。
|
||
- 帳本這種「隨 repo 走的狀態」找家用同一把尺(`mainline.py::project_roots()`),
|
||
退回原始碼目錄只在它有 `.git`(真的 checkout)時;plugin 快取沒有 `.git`。
|
||
- 驗法要驗「關掉同名里程碑之後成員還在不在」(A41 ㉛–㉞),不是「同名時列得出來」。
|
||
|