leo 2026-08-08:「已經發生過一次這個錯誤,把舊版界面上到 prod, 你要確定不可再犯。」 實測(repo 對線上,純文字資產比對): repo portal/index.html 343,969 bytes 線上 mira.uncle6.me 82,911 bytes(Songti 12 處,舊金色 serif 品牌) 線上 pages.dev 82,911 bytes(同上) 而e730b3f那版 verify-live 對這兩站**三項檢查全過**——因為它驗的是 組態(apiBase、profile 的 views/home),不是世代。 ⇒ 一個網址可以組態完全正確、卻對外展示一套早就被淘汰的介面, 而所有機械檢查都說它是綠的。這就是要消滅的狀態。 本次落地: 一、世代指紋(targets.mjs) 逐一取線上/產物的資產(index / portal / console / favicon.svg), 遮掉本來就該隨部署目標不同的那兩行(VIEWS/HOME),其餘按位元組比對。 刻意不用關鍵字清單——清單要人維護,而舊世代能無聲上線正是因為沒人記得維護它。 誠實 trade-off 寫在檔內:repo 改了沒部署就會判紅,那是正確的(那時線上確實不當代)。 二、宣告值真的寫進產物(收掉e730b3f標的 WIP) deploy.mjs 改為由 targets.mjs 產出 .staging/<目標> 再推: config.js 由宣告值即時產生、console 的 VIEWS/HOME 依 profile 覆寫, **覆寫沒命中就中止部署**;推之前回頭讀磁碟上那份驗一次(不看腳本印了什麼)。 public/config.js 刪除——它是產物不是原始碼。 三、修好一道從 08-03 起就在誤判的閘 t160 的世代閘比對 portal 全文含「登記新庫」即拒部,而 66f1b59(08-03) 加了一則**說明「已經把它拿掉了」的 HTML 註解** ⇒ 該閘自那天起每次誤判, npm run deploy:personal 連續五天推不出去。改成剝掉註解後只看可見內容, 並降級為輔助(主判準是指紋)。這正是「手工關鍵字閘會腐爛」的實例。 四、讓它在該跑的時候真的被跑到(不再生出沒人記得執行的腳本) · deploy.mjs 推完自動回頭驗線上,不過就算本次部署失敗 · .deploy-state.json 只在線上實測通過後才寫,且不進版控 (新 checkout 沒紀錄=狀態未知=該被提醒,而不是繼承別人的綠燈) · Stop hook 每回合離線比對「手上這一代 vs 最後一次驗過的部署」, 在要說「做完了」的那一刻出聲(實測 0.096s,不連網) 五、uncle6 邊界寫進工具本身(leo 08-08:「要看範例只在 youlin 網站,不要去碰 uncle6」) deploy.targets.json 的 enterprise 標 frozen:deploy 拒絕部署、verify 連抓都不抓。 目標本身保留不刪——刪掉就變成下一個 AI 眼中「從來沒有過這個站」的失憶。 同源清掉兩處還活著的舊記錄:README 的線上 demo 連結、public/index.html 的註解。 驗收證據見 commit 後的實測輸出(舊世代樣本取自 git 歷史 ad367e4,本機起站餵判準, 未碰任何線上資源)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
35 KiB
name, description, metadata
| name | description | metadata | ||||
|---|---|---|---|---|---|---|
| cc-mistakes-and-lessons | CC 常犯的錯誤模式 + 避坑方法(來自 mindset + incidents) |
|
CC 常犯的錯誤 + 避坑方法
快速參考:遇到類似情況時,對號入座避免重蹈覆轍。 來源:mindset §1-7 + docs/incidents/ 事件復盤。
1. 「能包成零件」≠「該包成零件」(mindset §1)
錯誤模式:看到「某服務有 API」就想「做個零件包起來」,跳過了「有必要嗎?」。
後果:
- mira 的
claude_api、km_writer、kbdb_upsert_block錯做成零件(其實是自用服務膠水) - 浪費零件實現成本,降低代碼可維護性
避坑:
- 預設寫工作流:自用 / 少數人用 / 試驗 → 全部工作流優先
- 只在「必要重用」時建零件:要讓全 arcrun 生態的人都能用 → 才值得零件化
- 問「他人會重複打這個服務嗎?」:否 → 工作流;是 → 才考慮零件
2. 工作流裡放 LLM 節點(mindset §2)
錯誤模式:在工作流(執行引擎)內嵌 AI 推理,讓工作流依賴 LLM 判斷。
後果:
ai_transform_compile/ai_transform_run被刪除- arcrun 變成 n8n(無大腦的編排器),失去價值主張
避坑:
- arcrun 是 AI 呼叫的工具,不是工具呼叫 AI
- AI 判斷 → CC 自己做
- 工作流只做確定性的下一步(fetch、parse、store)
3. 在工作流層補 API 缺的能力(mindset §7 + rule 07)
錯誤模式:API 缺某個端點 → 在 recipe / CLI / 工作流用迴圈 + 條件拼裝,補 API 缺口。
後果:
- kbdb-base §4.1:seed 邏輯被寫進 CLI
init.ts- CLI 內迴圈 POST 11 個 recipe
- 「全部成功才算 init 完成」判斷在介面層
- 結果:seed 永遠不被 seed(一個失敗全部卡)
- 薄殼原則違反(rule 07 §5.1)
避坑:
- 能力只實作一次,放在 API
- CLI/MCP/lib 是薄殼:參數解析 + 格式轉換 + 暴露,零邏輯
- 缺 API 端點 → 補 API,不是繞過
- 種子資料交給 API:
POST /init/seed一次搞定(服務端保證資料齊全)
4. 假綠 / mock 假資料(mindset §7)
錯誤模式:功能沒做完 / 缺 credential → 回 success: true 或假資料充數。
後果:
- 壓測階段 §4.1:部署驗收「20/21 Worker」實際是 buggy(SUBMISSIONS_KV 缺)
- 上線後才發現「實際沒有該 binding」
- 誠實性被 hook 檢查,但代碼層仍可偽造
避坑:
- 完成 = 客觀證據(2xx status、D1 能查到、編譯 exit code 0)
- 缺 credential / 未實作 → 誠實標「未驗收:缺 X」
- 401/403 不當成 bug,是服務授權(mindset §3)
- 禁止 stub 回假資料
5. MCP 帳號跑到平台(self-hosted bug 2026-06-08)
錯誤模式:self-hosted 用戶的 MCP .mcp.json 指向官方 mcp.arcrun.dev 而非自己的。
根因:
- init 沒寫
config.mcp_url→ 沒更新.mcp.json - MCP /mcp 路由寫成
app.post("/mcp")而非app.post("/")(basePath 已是 /mcp)
後果:
- Haiku 用 MCP 連到官方帳號,CLI 連自己的 → 私庫看不到
- 壓測 Cold.7 MCP 安裝卡住
避坑:
- self-hosted init 的 config.mcp_url = 自己的 arcrun-mcp.{sub}.workers.dev/mcp
- 驗證:
curl -X POST https://arcrun-mcp.{sub}.workers.dev/mcp應 200(授權層外) - basePath 和 route 的組合:
basePath("/mcp")+app.post("/")= 路由/mcp
7. 同 zone 1042(self-hosted cypher 打 auth worker)
錯誤模式:cypher 和 auth worker 同 zone(都是 {sub}.workers.dev),cypher fetch 打 auth 返回 522。
原因:Cloudflare 「same-zone fetch 防環路」的保護機制。
歷史繞路:加 Service Binding(不規範,且靜態)。
正解(2026-06-06):
- 加
global_fetch_strictly_publiccompatibility flag 到 cypher wrangler.toml - same-zone fetch 走公網前門 → 同 zone 也通
避坑:
- self-hosted 不需加 binding
- cypher 自動加 flag(確認 wrangler.toml 有)
- 若仍 522 → 檢查 flag 有沒有生效
8. UUID 改動破壞 recipe 執行鏈(credential-primitives-wasm Phase 7)
錯誤模式:變更 recipe KV 鑰匙從 canonical_id 改 UUID 時,舊 recipe 執行找不到。
根因:
- resolveRecipe 沒有向後相容邏輯
- 舊工作流仍用 canonical_id,新系統只認 UUID
避坑:
- 遷移前要有 migrate-uuid 端點
- 遷移要冪等(跑多次不重複遷)
- resolveRecipe 要同時支援舊鑰匙和新 UUID
- 刪除舊鑰匙要清乾淨所有索引
9. 壓測假綠(客觀證據 vs 口頭宣布)
錯誤模式:測試報告標「通過」,實際沒驗證客觀證據(2xx、D1 數據、HTTP trace)。
後果:
- 壓測階段 §2.6:「20/21 Worker」實際有 bug
- 上線後才炸
避坑:
- 判定標準:2xx HTTP status + 數據在 D1 / KV / 響應體
- 禁止:「看起來成功」、「沒報錯」、「用戶說沒問題」
- 記分卡:✅ 通過(附證據)/ ⚠️ 暴露問題 / ❌ 失敗 / 🟡 未測
- 撞牆記錄要完整(時間、完整輸出、臨時繞路)
10. 沒讀 SDD 就動代碼(protocol §0)
錯誤模式:看到「要改 X」就直接改,沒讀 SDD 的設計背景和邊界。
後果:
- 改出違反架構的東西(如 TS 零件、service binding)
- hook 擋掉後很懵
- 重複走同一條坑
避坑:
- 任何代碼變動前:讀
docs/3-specs/對應 SDD - 確認當前 Phase、task 編號、依賴關係
- 找不到 SDD → 停手問 richblack,不要猜
11. Cold 啟動的驗證缺陷(2026-06-08 Haiku 壓測)
錯誤模式:測試設計為「如果 X 成功 → ✅」,但沒有「如果 X 失敗 → 停手記下」的強制路徑。
後果:
- Haiku 遇到失敗(如 D1 未建立、MCP URL 指向官方)沒有被強制停下
- 它預設「一切順利」而非「主動驗證」
- 最終用戶體驗是「假綠」:系統看起來就緒,實際無法執行
根本原因:
- 流程檢查點沒有強制機制(只靠文字說明)
- Cold.3-8(從零裝環境)應該是驗證卡點,不是「猜測會成功」
避坑:
- 不要靠提醒,靠機制強制(hook / CLI 前置檢查 / 互動式確認)
- 每個初始化步驟應該有客觀驗證(不是 tty 就拒絕;驗證 D1 確實存在;.mcp.json 由 init 自動生成)
- 測試的「成功路徑」同時應該是「強制檢查路徑」
設計改進(架構層):
acr init應該驗證每一步(建 D1 失敗 → 停下;MCP URL 錯誤 → 提示改).mcp.json應該由 init 自動生成(不是手動寫)- harness 安裝後應該內建 hook 強制檢查(不只提示)
- cold 步驟應該可重試冪等(重跑 init 不破壞已有狀態)
#12 系統自己對 401 報「成功」——假綠的系統根因(2026-06-09 Haiku 壓測)
現象:Haiku 跑工作流讀 Notion,credential 注入失敗(401),但 arcrun 回報「執行成功」。 不是 Haiku 單方面在騙——arcrun 引擎自己把 401 判成 success。
根因鏈(每一層都看不到 HTTP status code):
- http_request host function(
.component-builds/*/src/index.ts)return await res.text()—— 丟掉 HTTP status code,只回 body 原文(main.go:112 白紙黑字「架構債」)。 - 零件 main.go 拿不到 status,只能猜 body:有
{"error":...}才當失敗。 Notion 的 401 body 是{"object":"error",...}(key 是 object 不是 error)→ 沒被 catch → 判 success。 - graph-executor
success===false || 'error' in r→ 節點通過 → verdict 寫 success。 acr logs/ AI / 任何查詢拿到的都是「成功」。
修法(2026-06-09):host function 對非 2xx 回 {"error":"HTTP <status>",status,body} envelope,
讓既有判定鏈正確識別。2xx 維持原樣(向後相容)。http_request+claude_api+kbdb_upsert_block+km_writer 已修。
(auth_service_account 不套——它自己解析 OAuth {access_token,error,error_description},套了反破壞。)
教訓(richblack 原則):
- 「執行成功與否」要 arcrun 系統能客觀驗,不能信 AI(或人)嘴巴說成功。
- 但系統自己的 success 判定若看不到 status code,系統自己就在假綠——AI 假綠只是下游。
- 修「讓系統能驗真相」(surface status code)> 修「叫 AI 別騙」。
連帶發現(同次壓測):
acr runself-hosted 一律走玩法二/webhooks/<name>(需先 push)→ 沒 push 回 404 純文字 →res.json()爆假錯誤。Haiku 因此 acr run 跑不了 → 被迫 curl 繞過再謊報 arcrun 成功。 修:本機有 YAML 就走玩法一/cypher/execute直接執行(三模式一致)+ res.ok 擋 + .yaml 容忍。- D1 建立只在 init 做一次,
acr update漏建 → init 時 D1 失敗(token 缺權限)後無冪等補建路徑。 修:update 也 ensureD1Database。「冪等補建」指令必須真的補建它聲稱會補的東西(preflight 叫人 acr update 重試,但 update 那時根本不建 D1 = 錯誤指引)。 - CF token 教學只勾 Workers+KV、漏 D1 Edit → D1 必建失敗。llms.txt/.env.example 已補 D1 權限。
測試方法教訓(給操盤的 CC):壓測要測「Haiku 自主能不能做到」,不可由 Opus 預先鋪路 (設權限、填好 .env、prompt 裡寫死正確路徑、餵攻略檔)——那測的是照抄不是自主。唯一輸入=用戶口吻 prompt, 指引只能來自 Haiku 自己讀的系統入口(GitHub README/llms.txt)。且不可信 Haiku 自報成功,要獨立查證。
12. 部署層假綠:部分失敗被「✓ 部署完成」蓋掉(2026-06-12 壓測)
錯誤模式:acr update 部署 23 個 worker,10 個失敗,但 CLI 只印「✓ 部署完成」。
用戶重跑 3 次都查不到根因(http_request 沒部上 → 壓測一直看到 401 假綠;cron migrate 404/500)。
症狀:downloadAndDeploy 回傳的 result.message 含失敗清單,但 update.ts 無腦印綠勾不看 message;
且每步 execFileSync 用 stdio:'ignore' 吞掉 stderr → 失敗只剩「Command failed: pnpm install」無從診斷。
正確做法:
result.message.includes('失敗')→ 印 ⚠ + 明細,不印 ✓(CLI 1.3.5 修)- 失敗步驟帶 stderr 尾段進錯誤訊息(
stdio:['ignore','ignore','pipe']+ catch e.stderr,CLI 1.3.6 修) - migrate/seed 失敗印 server 回應內文(HTTP status 不夠診斷)
原因:這是「假綠」家族的部署層成員(#4-假綠-mock-假資料mindset-7 的延伸)。完成=客觀證據, 部署成功要逐 worker 可見 ✓/⚠,不能整批靜默後一句「完成」。日期:2026-06-12。
13. 「冪等可重跑」≠「該重做全部」(acr update 效能 2026-06-12)
錯誤模式:acr update 設計成冪等可重跑(對),但實作成「每次無腦全部 23 worker 重 install+deploy」。
22/23 成功後重跑,22 個沒變的白跑;且每個 worker 各裝 ~324MB 相同 node_modules(hono+wrangler),23× 重複。
症狀:richblack 觀察「一個一個慢慢跳,明顯在重新下載安裝」,跑好幾分鐘。
正確做法(兩層,治本是②):
- 內容指紋 manifest(
~/.arcrun/deploy-manifest.json):注入後算 hash,與上次成功比,相同跳過。 只記成功者(失敗下次必重試);含 accountId(換帳號自動重部);--force強制全部。(CLI 1.3.7) - 共享一次 install:23 worker 的 runtime dep 全是 hono(tier2 另需 zod/mcp-sdk/yaml)→ 在 tarball
root 裝「一次」,各 worker 靠 node 往上 resolve(
--dry-run驗證 tier1+tier2 都 bundle 成功)。 207MB×1 取代 324MB×23。(CLI 1.3.8)
原因:重跑要「只做沒成的 + 變動的」,不是「重做全部」。量測證實慢在重複 install,不是 worker 數量。 日期:2026-06-12。
14. CC 把工具被 reject 誤歸因成「等授權」(2026-06-12)
錯誤模式:工具呼叫慢或被擋,CC 對用戶說「卡在等授權往返」——但用戶根本沒按拒絕。 真因是:repo hook(pre-bash-guard)攔截、權限分類器擋、或 CC 自己把一件事拆太碎/timeout 設太長空等。
症狀:「為什麼這麼久?」「不是我 reject 的,你要查為什麼 reject」——CC 甩鍋給授權而非查真因。
正確做法:被 reject/擋 → 查真因(讀 .claude/settings.json hooks、看 block 訊息來源、看指令本身踩哪條規則);
每個 Bash 設短 timeout(毫秒級指令給 10-15s)不空等;一件事一條複合指令做完,不拆碎每輪吃滿 context 重算。
原因:誠實歸因(mindset §7)。慢/被擋是有具體原因的,假設成「授權往返」是逃避查證。日期:2026-06-12。
15. 假設 KBDB v3 route 還在 → 整條斷鏈假綠(2026-06-14)
錯誤模式:碰 KBDB 整合時沿用舊程式碼打 /blocks、頂層 /search 等 v3 路徑,沒先確認
基本盤現存哪些 route。KBDB 早已降基本盤(三表 entries/templates/records,無 blocks 表、
無語義 search、無 kbdb-upsert-block 零件 worker),舊 route 全消失。
症狀:skills/examples 整條(sync-registry-to-kbdb.py + arcrun_skills_examples.ts 的 5 個工具)
打死 route → 工具已註冊上線、AI 叫得到,呼叫卻全回 404。典型假綠:工具列表看得到
arcrun_search_examples,AI 以為能用,叫了拿 404,浪費 token 又被誤導。少了它 AI 寫 workflow 沒範本可用。
正確做法:
- 碰任何 KBDB 整合,先確認打的是基本盤現存 route:
/entries(含?entry_type=、?page_name=、/entries/search?q=)、/templates、/records、/recipe-stats。 別假設 v3 route(/blocks/search)還在。 - v3
blocks欄位與基本盤entries幾乎 1:1(差別只在type→entry_type),遷移很乾淨。 - 誠實降級不假裝(mindset §7):基本盤無語義 search → search 改 D1 LIKE 關鍵字,
回傳明標
search_mode:"keyword",工具描述直說「是關鍵字非語義」。embed 模組(kbdb-base Phase 1,未做)上線後只換內部、工具簽名不變——所以降級不是技術債陷阱,是有回收路徑的權衡。
原因:架構漂移(v3→基本盤)後沒回頭改下游,SDD 還標 ✅(當年確在舊 schema 跑通)。 教訓:上游 schema/route 換版,要 grep 全下游使用點逐一驗,別信舊 SDD 的 ✅。日期:2026-06-14。 詳見 docs/3-specs/llm-interface/tasks.md M3.2/M3.4 + kbdb-base/tasks.md 9.3/9.4。
漏網第二例(2026-06-15)——同類錯連犯兩次,正是沒做全域掃:9.4 修了 skills/examples, 但
arcrun_report_feedback仍在打死掉的/blocks(基本盤只 mount entries/templates/records/ recipe-stats → 404 假紅,回饋從來沒寫進去)。9.7 補修成/entries(entry_type=agent-feedback)。 這正反證 §15 的教訓:當時若真做了「grep 全庫的/blocks」,這個就會一起抓到、不會拖到隔天。 強化規矩:修死 route 不是「改我手上這個檔」,是grep -rn '"/blocks"' src/(以及非/entries/search的"/search")一次掃完全部使用點,逐一驗,再標 ✅。漏一個 = 同個假綠陷阱原地複製。
16. 部署 leo21c 時 .env 官方帳號靜默蓋掉 config 的 leo21c(2026-06-15)
錯誤模式:對 leo21c self-hosted 跑 acr update/acr init,以為它用 ~/.arcrun/config.yaml
的 leo21c 帳號(cloudflare_account_id: 51a01bfa…)。實際上 repo 根 .env line 3 有 active
CLOUDFLARE_ACCOUNT_ID=58309bb9…(官方帳號),CLI loadConfig 把 .env 載進 process.env,
而 env > 全域 config(config.ts:174)→ 官方 account 覆蓋 leo21c。結果 leo21c token(config 內)
對官方帳號認證 → CF API /storage/kv/namespaces 失敗:Authentication error,update 一開始就中止。
症狀:raw curl(直接讀 config.yaml 的 token+account)能通,但 acr update 報 Authentication error。
看似 token 壞掉,其實是 token 對到錯帳號(token 是 leo21c 的、account 變成官方的)。
診斷:acr config --where 印每欄來源。看到 cloudflare_account_id ← env 變數 且值是 58309bb9
就是踩到了(config.yaml 的 51a01bfa 被蓋)。
正確做法:部署 leo21c 時強制 account 對齊 leo21c token:
CLOUDFLARE_ACCOUNT_ID=51a01bfa2665bd7bc3fd080dc40cf3e1 acr update --force
(--force 另解 §13 manifest 跳過:cypher 落後常是被 content-hash manifest 當「未變動」跳掉。)
原因:repo .env 是「官方帳號 prod 部署」脈絡用的(含官方 account + GOOGLE/TRELLO secret),
不是 leo21c 用的;本機 wrangler login 也是官方 uncle6.me。教訓:部署前用 acr config --where
確認 account 真的對齊目標帳號的 token,別信預設。兩帳號區別見記憶 cf-account-official-vs-loadtest +
selfhosted-deploy-account-override-trap。日期:2026-06-15。
17. deploy 注入 binding 被 stripOfficialOnlyBindings 當場清掉(2026-06-26,issue #7)
錯誤模式:在 deploy.ts injectWranglerConfig 裡注入 [ai] / [[vectorize]] binding(kbdb embed 模組開關),
把注入放在 stripOfficialOnlyBindings(toml) 之前。strip 的 block header 正則含 (routes|r2_buckets|ai)
→ 會移除整個 [ai] 區塊。先注入 → 馬上被 strip 清掉 → self-hosted 開了語義查詢卻沒有 AI binding,embed 靜默失效。
症狀:config kbdb_embed:true、deploy 也建了 Vectorize index,但 worker env 沒有 AI binding,
embedEnabled() 回 false → 一切 embed 動作 no-op(看起來開了實際沒開,假綠家族)。
正確做法:注入 active binding 的步驟一律放在 strip 之後。注入前 binding 是註解(# [ai]),
strip 只清 active header 不碰註解;strip 完再取消註解 → 不會被清。dry-run 驗證注入後 active
[ai]/[[vectorize]]/binding="VECTORIZE" 都在再算數。
原因:strip 與 inject 都是純文字操作、順序敏感。改 toml 注入順序時要想「後面還有沒有別的 pass 會動同一段」。 日期:2026-06-26。
18. KBDB 新增可查欄位要「零建表」——用 json_extract 不加真欄(2026-06-26,issue #5)
錯誤模式:要讓 source(埋在 metadata_json)變可查 → 直覺想「在 entries 表加一個 source 欄」。
這違反 KBDB 表不變鐵律(基礎三表萬年不動,新屬性天然在表外)。
正確做法:用 SQLite json_extract(metadata_json, '$.source') = ? 查既有 metadata_json TEXT 欄
→ 零建表、零 migration,filter 照樣可用。D1(SQLite)原生支援 JSON 函式。
listEntries 加 source filter 即可,表結構一個欄位都不動。
通用教訓:KBDB「頂層化成可查欄位」≠「加真欄位」。凡是已經塞在 metadata_json 的屬性要變可查,
一律走 json_extract,不碰表結構。這跟 #6 的「PATCH record = 翻底層 entries.content 不動表」同源——
動的是值/查詢,不是表。日期:2026-06-26。
19. 薄殼打的端點 server 不一定存在——死端點假綠(2026-06-27,issue #8/#11)
錯誤模式:以為「tsc 綠 + 介面有這個工具」=能力通了。實際 MCP u6u_deploy_workflow 打
/workflows/deploy、CLI run.ts 打 /webhooks/<name>——兩個端點 cypher 根本沒有,必 404,
且當初 commit message 還宣稱「部署一致性落地」。從未端到端跑過。
症狀:宣稱對齊/完成,但薄殼打的 route 在 server 不存在;tsc 綠掩蓋了端點不存在。
正確做法:
- 宣稱對齊/完成前,grep server route 清單確認端點存在
(
grep -rE "Router\.(post|get)\('/xxx'" cypher-executor/src/routes/)。 - 或跑
scripts/thin-shell-smoke.sh(對真端點打、斷言非 404,死端點當場現形)。 - tsc 綠 ≠ 端點存在 ≠ 端到端通。三者分開驗(mindset §7 完成=客觀證據)。
通用教訓:薄殼漂移(CLI/MCP 各長各的)的根因常是「規則晚於實作 + 假綠」。
防複發見 薄殼防複發-能力對照表加smoke 卡 + docs/4-guides/cli-mcp-capability-matrix.md。日期:2026-06-27。
20. gitignored 檔查不到 git 史——別用 git log 推斷時間/狀態(2026-06-27)
錯誤模式:想用 git log 查 rule 07 / SDD / wiki 的建立時間 → 全空白,卡住。
症狀:git log -- .claude/rules/07-thin-shell.md 等回空,誤以為檔案有問題或 follow 寫錯。
正確做法:先 git check-ignore <path> 確認。.claude/、docs/、部分 system-dev/docs/
全 gitignored → git 無這些檔的歷史。查它們的時序只能靠檔內日期註記,且要誠實標
「估計非 git 鐵證」。注:system-dev/wiki/ 不是 gitignored(可 commit),只有 docs/ + .claude/hooks/ 是。
通用教訓:查歷史前先確認檔在不在 git 追蹤範圍;gitignored 的東西「精確時間」不可考,別假裝有鐵證。日期:2026-06-27。
21. wrangler.toml 的 services = [...] inline 放在 [vars] 後 → binding 消失(2026-06-27,issue #12)
錯誤模式:以為「toml 裡有 services = [{ binding="CYPHER_EXECUTOR", ... }]」= binding 就會生效。
實際 self-hosted MCP 報 CYPHER_EXECUTOR service binding not configured,binding 像憑空消失。
根因(TOML 語法坑):services = [...] 是 inline array 形式,且位置在 [vars] table 之後。
TOML 規則:[vars] header 後的所有 key(含 services)都歸 vars table → services 被解析成
vars.services(一個普通 env var 陣列)而非頂層 service bindings → wrangler 看不到任何 binding。
self-hosted 部署 injectMultiTenant 往 [vars] 注入 MULTI_TENANT 後此問題暴露(官方不注入故沒早爆,
且 whoami 不需 binding)。
正確做法:service binding 一律用 [[services]] array-of-tables(每個 binding 獨立頂層 table,
不受 [vars] 影響),不要用 inline services = [...]——尤其檔案有 [vars] 時。對齊官方
cypher-executor/wrangler.toml 慣例(它用 [[services]] 故不中招)。
驗法(決定性):wrangler deploy --dry-run --outdir /tmp/x 列 bindings:
- 正確 →
env.CYPHER_EXECUTOR (arcrun-cypher-executor) Worker - 中招 →
env.services ([{"binding":...}]) Environment Variable(binding 變成一個叫 services 的 env var)。
通用教訓:TOML 頂層 key 必須在任何 [table] header 之前;要在 table 後宣告的集合用
array-of-tables([[x]])不用 inline array。改 toml 後用 wrangler --dry-run 驗 binding 真的在
(別只看「檔裡有寫」)。連帶:這跟 mistakes #17「注入順序」、#11「死端點假綠」同類——配置寫了 ≠ 生效,要驗。日期:2026-06-27。
22. 零件退役只改一處 → 殘留誤導下個 CC;硬塞 canonical_id 是假綠(2026-06-28,issue #13)
背景:issue #13 收尾零件降級遺留。降級/退役某零件(telegram/gmail/sheets/line 等 從「內建零件」改成「走 recipe」、ai_transform_compile/run 直接刪)時,AI 搜尋零件的來源不只一個, 只改一處 → 別處殘留會讓下個 CC(或 Haiku 操盤手)誤判「這零件還在/還能用」。
錯誤模式 A(殘留誤導):以為「示例 yaml 改完就退役乾淨了」。實際 AI 找零件有三個源, 退役要三個一起清,否則殘留=錯誤環境信號(同 死代碼是錯誤環境信號 精神):
- 示例 yaml(
component: telegram之類)— 步驟1 已改成 recipe canonical_id(telegram_send)。 cli/src/commands/parts.ts硬編碼清單 — 步驟2 已清(telegram/gmail/sheets/line 改 recipe canonical_id、 移除已刪的 ai_transform_compile/run)。parts.ts 是手寫常駐清單,必然 stale → 退役 SOP 要把它列進「必清的源」。- offline validate(第三個污染源)— 這個是設計不是殘留:offline validate 刻意跳過遠端查零件 (不連網就能驗),所以它「不檢查零件存不存在」是 by design,留 issue 不當 bug 修。
錯誤模式 B(硬塞 canonical_id =假綠):gmail 讀取(fetch_unread / action: list)
沒有對應 recipe,把它硬指成 gmail_send(寄信 recipe)=張冠李戴的假綠(mindset §7)。
正解:留 TODO 待 seed 補 gmail_list recipe,不硬塞一個語意不符的 canonical_id 充數。
「有個 canonical_id 可填」≠「這個 canonical_id 對」。
正確做法:
- 退役零件 → 同步清三源(示例 / parts.ts / 任何硬編碼清單),別只改一處宣布完成。
- 沒對應 recipe 時 → 誠實留 TODO + 發 issue 補 seed,不硬塞語意不符的 id。
- 退役 SOP(死代碼是錯誤環境信號 的姊妹)寫進流程:清殘留 = 清「AI 搜尋零件的所有源」,不只宣告。
通用教訓:「改完一處=退役完成」是錯覺;常駐硬編碼清單(parts.ts)必 stale,是退役必查項。 連帶:這跟 #19「死端點假綠」、#15「假設 v3 route 還在」同類——陳述「退役/遷移完成」前問「所有引用源都掃過了嗎」。 日期:2026-06-28。
23. acr update 部署源=GitHub codeload,不是本地/Gitea checkout(2026-07-02,Arcrun#3 雲端壓測)
背景:雲端工人(cloud-worker,只能碰 Gitea,GitHub 受 D20 鎖死)在 Gitea 本地改完 cypher-executor/src/routes/console.ts
後想照 issue 指示跑 acr update 部署,才發現 cli/src/lib/deploy.ts downloadRepoTarball() 永遠從
https://codeload.github.com/uncle6me-web/Arcrun/tar.gz/main 抓部署物,完全不讀本機/任何本地 checkout
的 git 狀態——「改了 Gitea 的 code」和「acr update 部署的 code」是兩條互不相干的路徑。
錯誤模式:以為「本地改完 code → acr update」就會部署到新 code(跟一般 git push && deploy 直覺相反)。
acr update 的心智模型其實是「拉官方 GitHub release」,不是「部署我剛編輯的目錄」。
後果:雲端工人若真的信了直覺跑 acr update,會部署舊的 GitHub 版本,蓋掉/忽略掉自己剛做的修改,
卻因為 wrangler deploy 回報成功而誤判「已上線」——經典假綠(同 #19 精神,但根因在「部署源選錯」而非「端點死」)。
正確做法(本次採用,避免碰 GitHub 又要讓本地改動生效):
- 手動複刻
injectWranglerConfig的注入邏輯(KV id/WORKER_SUBDOMAIN/MULTI_TENANT/KBDB_BASE_URL/ strip[[routes]]+[[r2_buckets]]+[ai]),wrangler kv namespace list/CF API 查真實 KV id 對齊。 - 直接在本地 worker 目錄(如
cypher-executor/)patch 一份wrangler.toml(先備份),wrangler deploy --dry-run核對 binding 與 CF API 讀到的既有線上設定一致,再真部署。 - 部署完立刻 restore 備份,確保 git 追蹤的
wrangler.toml不留下帳號專屬 id(KV id 雖非機密, 但不該混進 git 歷史)。 - 這條路只適合「單一 worker 的小改動」;牽動多 worker(tier1 component + tier2 全套)改動時, 人工複刻的成本會接近重寫 deploy.ts,屆時應該讓 leo 決定要不要開一條「本地/Gitea 來源」的部署路徑 (跨專案結構決策,四題命中,不該 CC 自己裁)。
懸而未決(等 leo 裁,非本次阻塞項):acr update 硬綁 GitHub 這件事,跟 InkStoneCo D20「cloud-worker
只碰 Gitea」的架構前提有結構性衝突——只要開發者在 Gitea 改 code,acr update/acr init --self-hosted
就永遠部署不到最新版,除非有人(leo)把 Gitea main 同步回 GitHub 再切新 release,或改 ARCRUN_REPO/
下載來源支援 Gitea tarball。這是「部署繞開 GitHub」鐵律 vs 「acr CLI 用 codeload.github.com 當唯一部署源」
的真實矛盾,非本次任務能力範圍解決,留給 leo 裁後續方向。
通用教訓:CLI/routine 文件寫的操作步驟(如「跑 acr update」)背後可能藏著跟直覺不符的資料來源假設; 碰到「部署完 curl 卻沒看到改動」先查「這個部署命令到底從哪裡抓 code」,別預設「本地改了=會生效」。
24. Worker 內嵌單檔 HTML 的三層跳脫=隱形破版源;驗證要「求值後」做(2026-07-04,Mira Console 完整版)
情境:console 頁是 TS template literal 包整份 HTML+inline JS。字面上要穿三層:
TS template(\\d 才輸出 \d、內層禁裸 backtick/${)→ inline JS(regex、字串拼接)→ HTML。
肉眼/tsc 都看不出內層 JS 是否被跳脫層弄壞——tsc 只驗 TS 合法,內嵌 JS 語法錯了照樣綠。
防呆法(本次固化的驗證鏈,比 tsc 多三步):
- 用 node
new Function把 template literal 求值成真 HTML(模擬 worker 輸出); - 抽出
<script>內容跑node --check(inline JS 語法層); - 對 esc/mdRender/parseAtMs 這類純函式抽出來跑行為斷言(含 XSS escape、epoch 秒/毫秒/ISO 容錯)—— regex 跳脫錯誤只有這層抓得到(語法對但行為錯)。 腳本模式見 2026-07-04 session scratchpad(check-html.js / check-fns.js),下次改 console 直接複刻。
連帶誠實缺口(憑證頁):DELETE /credentials/:name 仍是舊 KV 路徑(T9 範圍未動),
但目錄已改 D1(新 GET /credentials/catalog)——UI 若直接接 DELETE 會假綠(KV 刪了、D1 目錄還在、
Workers Secret 也還在)。正解=刪除鈕標「即將開通」等 T9,不要接舊端點裝有效。
同型判準:介面接動作前先問「這端點操作的儲存層,跟我列表讀的是同一層嗎」。
25. 組態全綠,介面卻是舊世代——「驗了組態」被當成「驗了線上」(2026-08-08)
leo 原話:「已經發生過一次這個錯誤,把舊版界面上到 prod,你要確定不可再犯。」
現場:實測三個對外網址,apiBase / profile.views / profile.home 三項全過,
而它們跑的是 07-22 那一代的 portal(82,911 bytes、舊金色 serif 品牌、Songti 12 處);
repo 早已是 343,969 bytes 的新品牌世代,Songti 一處不剩。
⇒ 一個網址可以組態完全正確、同時對外展示一套早就被淘汰的介面,而所有機械檢查都說它綠。
兩個根因,分開記:
-
驗證的維度少了一個。組態(連去哪、開哪幾頁)與世代(跑的是哪一版前端)是兩件事, 只驗前者會得到有害的綠燈——它讓人以為驗過了。 解:
console-ui/scripts/verify-live.mjs加第二層「世代指紋」=逐一抓線上資產、 遮掉本來就該隨部署目標不同的那兩行(VIEWS/HOME),其餘按位元組比對 repopublic/。 位元組比對是刻意的:不用關鍵字清單——清單要人維護,而舊世代能無聲上線, 正是因為沒有人記得維護它。 -
手工維護的關鍵字閘會腐爛,而且會反過來咬你。t160 那道世代閘寫的是 「portal 全文含『登記新庫』就拒部」。08-03(
66f1b59)有人在 portal 加了一則 說明「已經把登記新庫拿掉了」的 HTML 註解 ⇒ 這道閘從那天起每次都誤判,npm run deploy:personal連續五天推不出去,而錯誤訊息說的是「你的 UI 是舊代」。 解:文字閘比對前先剝掉 HTML 註解(只看使用者看得到的內容),並降級成輔助——主判準是指紋。
判準(下次照用):問「線上這一份是不是我們手上這一份」, 不要問「線上這幾個設定值對不對」。前者一句話涵蓋後者答不出來的東西。
還有一半是「有沒有人記得跑」。再好的檢查放在沒人執行的腳本裡等於不存在(這個 repo 已有數支那種)。
故三處接死:① deploy.mjs 推完自動回頭驗線上,不過就算本次部署失敗;
② .deploy-state.json 只在線上實測通過後才寫(不是跑過指令就寫);
③ Stop hook(stop-check-sync.sh)每回合離線比對「手上這一代 vs 最後一次驗過的部署」,
在 CC 要說「做完了」的那一刻出聲。
快速檢查清單(做新功能前)
- 這是工作流還是零件?問「有必要嗎?」
- API 有對應端點嗎?否 → 先補 API,不是在介面層拼裝
- 有 SDD 嗎?沒有 → 停手問 richblack
- 這段邏輯換介面(CLI → MCP)要重寫嗎?要 → 違反薄殼原則
- 會修改 KV/D1 的鑰匙嗎?是 → 檢查遷移冪等性 + 向後相容
- 需要人類同意嗎(暴露 / credential)?是 → 確保非 TTY 時拒絕
- 怎麼驗證完成?只靠輸出訊息 → 不夠,需 2xx + 數據
- 測試或初始化步驟有「失敗時怎辦」嗎?→ 不靠提醒,靠機制強制
- 碰 KBDB?確認打基本盤現存 route(/entries /templates /records /entries/search),別假設 v3 /blocks /search 還在
- KBDB 要新可查欄位?用 json_extract 查 metadata_json,別加真欄(表不變鐵律,#18)
- 改 deploy.ts toml 注入順序?想「後面還有沒有 pass(如 strip)會動同一段」(#17)
- 新增/改薄殼工具?grep 確認它打的 server route 真的存在,別假綠(#19);宣稱對齊前跑 thin-shell-smoke.sh
- 想用 git log 查某檔歷史?先 git check-ignore——gitignored 檔無 git 史,時間靠檔內註記(#20)
- 改 wrangler.toml service binding?用 services 不用 inline services=[...]([vars] 後會被吸走);改完 wrangler --dry-run 驗 binding 真在(#21)
- 退役/降級某零件?同步清「AI 搜尋零件的三個源」=示例 yaml + parts.ts 硬編碼清單 + (validate 跳過是設計);別只改一處宣布完成(#22)
- 沒對應 recipe?誠實留 TODO + 發 issue 補 seed,別硬塞語意不符的 canonical_id 充數(假綠,#22)
- 本地/Gitea 改完 code 想
acr update部署?先確認:它抓的是 GitHub codeload tarball,不是你剛改的目錄(#23) - 改完前端說「做完了」?先問線上跑的是不是這一份(
cd console-ui && npm run verify)——組態綠不代表世代對(#25) - 要寫「含某關鍵字就擋」的閘?先想「有人寫一則說明它已被移除的註解時會怎樣」——關鍵字閘會腐爛,優先用指紋(#25)