AI Workflow · part 9
[Dev Workflow] 額度斷在半路:讓 Claude Code 跟 Codex 交接任務的 handoff 協議
❯ cat --toc
TL;DR
額度斷在任務半路,蒸發的不是知識庫(codebase 形狀、部署步驟、教訓都還在),是這個任務的 live state:做到哪、排除了哪些死路、背景在跑什麼、下一步跑哪個命令——只活在 context window 裡。修法是把 state 外部化成一個 handoff 檔:一個任務一個,七節,存檔只卡在乾淨可續跑的那一刻(一步做完、剛發射長跑命令、換方向),不定時 dump。鐵律兩條——Done 每項附證據(file:line / commit / output),接手先重跑最後一項證據才往上疊。⚠️ 用 hook 上鎖後踩到一個 bug:「2 小時沒動=可接手」對長跑訓練是錯的,一個好好跑著的 19 小時 job 被標為可接手;staleness 該看 phase 不看時鐘。
白話版:交接班留一張便條,不是把整本病歷重抄一遍
醫院護士換班,會留一張交班便條:幾號床剛打了針、哪個病人在等檢查結果、哪張單還沒回。她不會把整本病歷從頭抄一遍——病歷本來就在那,查得到。要交接的只有「此刻正在發生、下一班要接著做」的那幾件事。
用 AI 寫 code 也一樣。你叫 Claude Code 幫你做一件跨好幾步的活,做到一半,額度突然斷了。你重開一個 session,想接著做——結果它一問三不知。奇怪的是,你的 codebase 一行沒少,筆記還在,git 紀錄好好的。那到底掉了什麼?
掉的就是那張交班便條。做到第幾步、剛剛排除了哪條走不通的路、有個東西在背景跑你在等它、下一步該敲哪個命令——這些不是知識,是「這個任務此刻」的易逝細節。它們只活在對話的 context window 裡,window 一蒸發,新 session 從白紙開始。
這篇講我怎麼替這件事寫了一套協議:把那張便條變成一個檔,讓 Claude Code 跟 Codex 兩個工具讀寫同一份、互相接手彼此做一半的活。然後——好笑的是——協議跑的第一張單就是它自己;跑順了之後,我又用 hook 幫它上鎖,結果早上八點,一個跑得好好的訓練被 hook 準時標成「可接手」。那個 bug 比協議本身還值得講。
額度斷在半路,蒸發的不是知識庫,是任務的 live state
你正做到一半,額度斷了。新開一個 session,一切從白紙開始——即使 codebase 還在、記憶還在、git history 一行沒少。這口氣嚥不下去,但先別急著怪工具,先分清楚到底掉了什麼。
有兩種東西容易被混為一談。一種是 durable knowledge:查得到的、活過一個 session 的東西——codebase 的形狀、某個服務怎麼部署、上次踩過什麼坑學到什麼。另一種是 live state:五個子步驟做到哪、排除了哪兩條死路和為什麼、有個 build 在背景跑你正在等它、下一步是重跑哪一個特定命令。前者是知識,後者不是——後者是「這個任務、這一刻」的易逝細節,只活在 session 的 context window 裡。額度一斷、window 一蒸發,後者就沒了。
想通這一點,一件事也跟著清楚:把知識庫養大,永遠修不好交接。你可以多存十倍的 durable knowledge,還是每個做一半的任務都掉——因為蒸發掉的那個東西從來不在知識庫的守備範圍,它是 state。一旦這樣命名,修法就明顯了:把 state 外部化成檔案,就像你把任何「要活過一個 process」的東西寫進檔案那樣。
好笑的是,起源一點都不高科技。前一天我已經手動做過一份了:一個做一半的 blog 任務,那時是 Codex 在維護,連「done」那行都附了 commit hash。協議沒發明任何東西,只是把我本來就會手動做、卻總有一天會忘記做的那件事,制度化。
存檔要卡在「乾淨可續跑」那一刻,不是每五分鐘 dump 一次
直覺會想 autosave:每幾分鐘 dump 一次 state,穩。這個直覺是錯的,而且錯得會回頭咬你。
定時器什麼時候響,是它自己決定的——通常正好在你想到一半、計畫還沒成形的時候。它寫下的那個檔,是一張困惑的快照:半成形的做法、一個你正要跑但還沒跑的命令、一個你再 30 秒就要推翻的假設。接棒的人——不管是新 session 還是另一個 CLI——會把你當下的不確定,當成你已經下的決定,原封不動繼承下去。
state-transition 存檔剛好相反:只在該存的時刻才存,存下來的都是整理好、下一個人能直接續跑的狀態。一個步驟做完後(附上證據:file:line、commit、命令輸出)、剛發射一個長跑命令後(檔案記一句「這個在飛,這樣收」)、換方向時(檔案永遠不會宣稱一條死路還活著)。定時器不知道現在是不是這種時刻——這就是全部的差別。

一個任務一個檔,七節,Done 沒證據就不算數
格式很土:一個任務一個檔,~/.claude/handoffs/<slug>.md,固定七節。
- frontmatter:task / status / owner / updated / project / ticket
- Goal:「done」的定義,一兩行講完
- Now:此刻在飛什麼、怎麼收
- Done:做完的每一項,附證據——file:line、commit、或命令 output
- Next:接下來每一項都自包含,不看對話也能照著跑
- Gotchas:下一個 owner 會重新踩到的雷
- Resume:literally 第一個要敲的命令
Codex 經一個 symlink(~/.codex/handoffs 指向 ~/.claude/handoffs)讀寫同一個目錄,兩個 CLI 共用一份 source of truth。任務結束就把檔移進 archive/。
真正做掉大半工作的,是兩條規則。
第一,Done 沒證據就沒價值。「修好 parser」是一句宣稱;「修好 parser,parser.py:88,commit a1b2c3d,fixtures 11/11」才是一份你敢信的 state。差別不在文筆,在你能不能不重讀對話就相信它。
**第二,接手的人往上疊之前,先重跑最後一項 Done 的證據。**你不繼承前朝「它會動」的一面之詞;你親手敲它附的證據命令、看它 pass,才往上加東西。
所以接手時固定做四步:讀全文 → 把 owner 設成自己、順手 bump updated(這一步就是防兩個 agent 同時動的鎖)→ 重跑最後一項 Done 的證據 → 從 Next 第一項開始。
背景 AI 不會告訴你它卡死了:三個我踩過的雷
把活丟給背景跑的 codex exec,舒服,但有三個雷會讓「看起來在跑」跟「真的在跑」對不上。
一、detached 的 codex exec 要把 stdin 導向 /dev/null。 如果 stdin 還開著、又不是接在終端機上就啟動它——背景啟動器常留下這種狀態——Codex 會堵在 Reading additional input from stdin...,等一個永遠不會來的 EOF,永遠不開工。process 不會死,所以從外面看它像在跑。指紋很好認:log 卡在大約 39 bytes,而且那張 handoff 的 owner 一直沒人認領。把 stdin 導向 /dev/null 等於直接餵它一個 EOF;我頭兩張單沒中,純粹是運氣。
二、卡死不會叫醒你。 崩掉的 process 會退出,退出會叫醒你;卡死的只是安安靜靜坐在那。所以每一張丟到背景跑的 codex exec 單,我都加一個啟動確認:大約 45 秒後看它的 log 有沒有繼續更新。「它沒失敗」跟「它有在進展」是兩回事,而背景任務只會主動告訴你前者。
三、「報告說 done」不等於 done。 委派出去的活回報成功時,orchestrator 親手重跑一次驗收命令——fixture、round-trip、或檔案對帳。這次 Codex 回來的數字全對,但我是驗了才知道全對的。報告是一句宣稱;驗收是一個你親手敲下去的命令。
協議跑的第一張單,就是它自己
協議寫好,第一張單我就丟給它自己:「寫那些讓協議誠實的 staleness hook」,交給 Codex。Codex 讀 handoff → 把 owner 設成 codex → 每一步附證據 → 收 done,全程沒有人工插手。我獨立重驗它的 fixture:5/5、8/8、11 pass。同一天晚上五張過夜訓練單也全程 handoff 驅動,每一張我都先驗前一個 owner 的證據才動手。
這批 hook 補的,就是大家最容易忘記的三個時間點:
- 開工忘記開檔——open-nudge:你摸了三個不同的檔卻還沒有一張 active handoff 時,提醒你一次。
- 中途忘記更新——stale-check:20 分鐘沒動就推你一下。
- 收攤忘記存——stop-guard:handoff 還 stale 就擋一次 session 結束。
最後那個是我唯一允許 exit 非零的腳本。「拒絕讓你停下來」是一把大槌,所以它節流得很兇、也尊重 stop-loop guard——絕不會把你困在原地出不去。
早上八點,一個跑得好好的 19 小時 job 被標為「可接手」
有天早上,session 開場摘要把一個過夜的 GPU 訓練標成「⚠️ 可接手」。那個 run 好好的,還有大約 19 小時要跑。我第一反應是去翻 hook,以為它壞了。hook 也好好的。
兇手是我自己。是我在協議裡用白話寫的一條規則:「owner 不是你、而且 updated 落後超過 2 小時 → abandoned,可接手」。
這條規則對互動式的活是對的:一個兩小時沒碰 handoff 的 session,幾乎一定死了。但長跑命令把這個假設整個顛倒過來——它的重點就是沒人會去碰那個檔,而且一碰就是好幾個小時。規則把「安靜兩小時」讀成「死了」;可是對訓練來說,「安靜兩小時」等於「正照計畫在跑」。

hook 沒有 misfire。它忠實地執行了一條壞規則。這就是把 doctrine 自動化最不舒服的地方:它不會替你把錯的地方圓過去,只會放大你的錯,還準時幫它排程。一條你在對話裡會隨口自己修正掉的規則,一旦進了 hook,就變成早上八點對著一個跑得好好的 job 準時開火的東西。
還有第二個徵兆,我一直忽略它:上面那個 stdin 雷,在我的 log 裡躺了整整一個月。每個 session 都「知道」這件事——直到有一個 session 不知道。說穿了,一個只活在 log 裡的教訓,離「被重新忘掉」只差一個不走運的 session。
先修規則再修 hook:staleness 從看時鐘變成看 phase
改 hook 是修症狀。bug 是那條規則,所以要動的是規則。「stale」不再是一個純時鐘函數,它變成 phase 的函數。
frontmatter 因此長出一個 phase:running_external | waiting_user | blocked | working,再配一個 expected_until timestamp——ISO 8601、帶明確的 UTC offset;少了它,「逾期」就沒有一個明確的比較基準。終態也從光禿禿一個 done,擴成 done | failed | cancelled | superseded。
session 開場摘要也改成按 phase 顯示:長跑 job 還沒到 ETA,就顯示 🏃 長跑中,不標可接手;只有過了 expected_until 才標,而且措辭是「需要看一下」而不是「拿去」;waiting_user 跟 blocked 的 job 也從隱形變成可見。
有沒有修對,看 fixture 輸出就知道:裡面再也 grep 不到「可接手」三個字。誰接手還是由人決定,owner 欄位只拿來當 routing hint。昨天被標可接手的那同一個 19 小時 run,現在讀起來是:gx-fit-ladder-launch … 🏃 長跑中(ETA 2026-07-11T17:55:00+08:00)。
順帶一提,現在判斷 staleness 一律只看 frontmatter 的 updated。之前發現兩個 hook 在打架:一個看檔案 mtime、一個看 updated,某個實際的 handoff 這兩個值差了兩小時。Codex 做這張單時跑了對抗式自審,自己抓到兩個 medium bug(切目錄時 counter 重複計數、fractional timestamp 的契約不一致),先寫紅燈 fixture、再修。
教訓不是「學到」就結束:它有四階,大多數卡在第一階
這兩天貫穿始終的,其實是同一句話:一個教訓,不是「學到」就結束了。它有四階,而大多數教訓一輩子卡在第一階。
- Logged——寫在一個你找得到的地方(那個 stdin 雷就在這一階躺了一個月)。
- Documented——進了手冊,一個你會刻意去翻的地方。
- Templated——正確的做法變成預設,焊進工單模板,讓你想忘也忘不掉(
< /dev/null、sandbox 的--add-dir、45 秒確認器,現在綁成固定的一組)。 - Enforced——hook 讓錯的做法變得不可能,或至少變得很吵。
一條每個 session 都會重讀的 log,跟一個每個 session 都會用的模板,不是同一回事。stdin bug 明明「logged」了一個月,還是咬了我——因為 logged 跟 enforced 之間,差了整整三階。而 false-takeover bug 也是同一類問題,只是又多一層:規則就算寫下來,還是可能是錯的;把錯的規則自動化,只會讓系統更快照著錯的方式跑。
收工前照著跑一遍:六條
- 外部化 live state,不只是知識。 知識庫活過 session,任務的 live state 不會。寫進檔案。
- state-transition 存檔,絕不定時。 一步做完、發射長跑命令後、換方向時——絕不在想到一半的時候。
- 有證據才算數。 每項 Done 帶 file:line / commit / output;下一個 owner 疊上去之前,先重跑它。
- 卡死的背景 AI 不會告訴你。 每個 detached run 啟動後先看 log 有沒有繼續更新,stdin 從
/dev/null讀。 - 先修規則,再修 hook。 自動化做了蠢事,先看它是不是忠實執行了一條壞規則——通常就是。
- 把教訓推到 enforcement。 Logged 只是四階的第一階;真的重要,就焊進模板或 hook,否則你離重學它一次,只差一個不走運的 session。
常見問題
- AI CLI 的額度斷在任務做一半,會丟掉什麼?
- 不是知識庫。codebase 形狀、部署步驟、學過的教訓這些查得到的東西都還在。丟掉的是任務的 live state——五個子步驟做到哪、排除了哪兩條死路、背景有個命令在跑你在等它、下一步該跑哪個命令。這些只活在 session 的 context window 裡,window 一蒸發就沒了。修法是把 state 寫進一個 handoff 檔。
- handoff 檔什麼時候該存?
- 卡在 state-transition,不是定時。一步做完後、剛發射一個長跑命令後、換方向時——這些時刻存下來的檔,本來就是整理好、可以直接續跑的。絕不要 autosave:定時器通常在你想到一半、計畫還沒成形時開火,寫下的是一張困惑的快照,接手的人會把你的不確定當成決定繼承下去。
- Claude Code 跟 Codex 怎麼共用同一份 handoff?
- 一個任務一個 markdown 檔放在 ~/.claude/handoffs/,Codex 經 symlink(~/.codex/handoffs 指向同一目錄)讀寫同一份,兩個 CLI 一份 source of truth。接手四步:讀全文、把 owner 設成自己並 bump updated(這就是防兩個 agent 同時動的鎖)、重跑最後一項 Done 的證據、從 Next 第一項開始。
- 為什麼一個跑得好好的訓練 job 會被 hook 標成「可接手」?
- 因為那條規則是「owner 不是你、updated 落後超過 2 小時就算 abandoned」。這對互動式工作對,但長跑命令的重點就是沒人會去碰那個檔好幾個小時。hook 沒有 misfire,它忠實執行了一條壞規則。修法是先改規則不是改 hook:讓 staleness 看 phase(running_external 沒到 expected_until 就不算 stale),不看時鐘。
接著讀
- 2026-05-19[Claude Code] 規則我會跳過,hook 我跳不過 — 我替自己裝了一個發文閘
我有一條「發文前要查證」的規則,還是寫了三次說錯。問題不在規則,在它放在哪一層。這篇講我怎麼把它從 skill 升級成 hook — 一段守在「按下送出」那一刻的小程式,沒查證連嘗試都不准。
- 2026-02-26[Dev Workflow] 讓兩個 AI 吵架。它們不同意的地方才是重點。
一個 /debate 指令讓 Codex CLI 和 Gemini CLI 對同一份程式碼互相辯論。訓練資料不同、盲點不同——它們的分歧通常是最有用的輸出。
- 2026-05-05[Skill] 我的繁中不夠台 — zhtw-mcp 掃 72 篇文章修了 128 處陸用詞
sysprog21/zhtw-mcp 把繁中規則編譯成執行檔。掃 ai-muninn 全部 72 篇繁中文章三輪修了 128 處陸用詞,但真正的收穫不是修了多少 — 是發現我的盲點不是「不知道台灣怎麼說」,是「碰到陸用詞時預設不會主動懷疑」。
- 2026-04-13Claude Code Token 燒太快?8 招讓你的 Session 撐 10 倍久
剛開始用 Claude Code,context window 一直滿。這篇解釋 token 花去哪、哪些浪費了、怎麼讓 Claude 更精準地找資料而不是暴力讀檔。
不想錯過新文章?
訂閱我確保不漏接!
隨時一鍵退訂。