上週一次 Claude Code 工作階段告訴我,我的訂單佇列有 180 筆待處理項目,沒有一筆超過 13 天,而且 254 筆紀錄裡有 221 筆是由 Claude Code 工作階段寫入的。它自己算出了這個數字。系統裡最大的單一信箱,正是系統在處理自己:36 筆訂單,其中約 30 筆是工具正在改進自己的工具。
我已經有一段時間沒再看那份清單了。這才是值得記下來的部分。
這篇文章的內容: 為什麼每個專案一個 TODO.md 在十個專案時就失效了,替代方案最後變成什麼樣子,有哪三件事出了問題,以及每件事留下了什麼閘門,還有這整套系統至今仍讓我付出的代價。中間那段,如果你同時跨多個儲存庫工作、腦中無法一次記住全部內容,不論你用什麼工具驅動它們,都值得拿去參考。
十個專案與一個 Markdown 檔案
去年底我開始用 Claude Code 跨多個專案工作。那時整套系統就是每個專案資料夾裡放一個 TODO.md。我開啟工作階段,告訴它要做什麼,剩下的就由檔案記錄。兩三個專案時這樣完全沒問題,我會推薦這種做法。
後來變成十個專案。十個進行中的專案、Mac Studio 上的十個資料夾、十個 TODO 檔案,三件事同時開始出問題。
想法在回家路上就遺失了。我在外面時突然想到什麼事,而能寫下來的地方卻是另一台機器上某個專案資料夾裡的 Markdown 檔案。等我回到那台機器前,想法已經消失了。
早晨變成我不想做的決定。十份清單,沒有共用檢視,也沒有跨清單的排序。今天該處理哪個專案?誠實的答案通常是我上次碰過的那個,這恰恰與優先順序背道而馳。
專案會安靜好幾週。不是因為它們已經完成,而是我的設定從來不會讓它們舉手。檔案不會告訴你它已被忽略。
大約一個月前,我決定想要一個能告訴我什麼事最重要、涵蓋所有專案的東西。不是另一份清單,而是位於清單之上的一層。
現在的樣貌
上週我做了之前一直拖著的事:我寫了手冊。不是程式碼的文件,而是給操作者的手冊,而操作者就是我。哪裡做什麼、存在哪些指令、筆記如何變成訂單,以及哪四個地方會停止並等待人工介入。它總共有八個章節。撰寫的過程讓人感到不舒服,但這種不舒服是有用的,因為一個需要手冊才能給唯一使用者使用的工具,正在告訴你它是如何成長起來的。
接下來要講的是手冊中可通用的部分。任何可能變成工作的事,都會進入一個收集點,從那裡開始它只有一條路可走。
(此處省略圖表。兩張流程圖收錄於原文。)
在這背後有兩個時鐘機制在運轉,而且它們都不思考。有一個 launch agent 每 15 分鐘清空一次投遞箱(StartInterval 900),把項目分類到對應專案的清單裡。另一個每 30 分鐘(StartInterval 1800)取出已核准的訂單,在 ~/.cache/master-dispatch-worktrees 下的 git worktree 中以無頭模式執行,故意放在所有儲存庫之外。排序步驟並不涉及模型,因此可以免費頻繁執行。
我使用最頻繁的部分其實最不聰明:我寄一封郵件給自己。
(此處省略圖表。兩張流程圖收錄於原文。)
這解決了我三個問題中的第一個。我永遠能觸及的東西就是我自己的收件匣,因此它成了入口。它無法做到的是把一則筆記轉成工作。它會變成一筆訂單,然後停在那裡,這也帶出了為什麼這套系統要有閘門的原因。
三個工作階段,一個檔案
7 月 17 日,我有三個 Claude Code 工作階段同時在跑,它們都在修改同一個存放各專案支出限制的共用設定檔。每個工作階段讀取檔案、修改自己的那一行,再把整個檔案寫回去。這是典型的讀取-修改-寫入,只不過寫入者是代理而非執行緒,而且我當時根本沒把它當成並行問題來思考。最後寫入的那一個勝出。一個工作階段的例外狀況,就這樣被下一個工作階段默默抹除了。
修正方法很普通:共享資源現在只能透過一個小型的 mit-lock 包裝器來寫入,這是一個基於 mkdir 的鎖,帶有 10 分鐘的過時逾時與稽核記錄。有一個 pre-tool hook 會拒絕任何未經這個包裝器、卻要寫入受保護路徑的 Bash 指令。
真正不普通的,是背後的領悟。我之前把平行工作階段當成平行的人類,以為它們會注意到。它們不會注意到。它們只是程序,而當兩個程序共享狀態時,你就必須給它們與兩個執行緒相同的紀律。這就是後續所有規則的由來:每個儲存庫只有一個寫入者、每個共享資源只有一個擁有者、跨專案工作只有一個通道。
產生的工作比完成的工作還多的工具
這又回到開頭提到的數字。
到 7 月 28 日,佇列裡有 180 筆待處理訂單,而且沒有一筆超過 13 天。這表示從 7 月 20 日以來,系統每天大約產生 20 筆新訂單,而我一天只關閉 2 到 5 筆。254 筆投遞箱紀錄裡有 221 筆是由 AI 工作階段而非我本人寫入的,其中 163 筆來自協調儲存庫本身的工作階段。
沒有任何東西壞掉。每一筆訂單都有其合理性。這恰恰就是問題所在。Claude Code 工作階段想要派上用場,而只要你在一座真正的儲存庫裡開啟工作階段,它就會找出十個真正的改進點:一次可以更乾淨的重構、一份過時的文件、一個可以更嚴謹的測試。把十個合理的觀察乘以十五個儲存庫,你就會得到單一個人永遠清不完的洪流。我的收件匣裡填滿的不是錯誤,而是好點子。
修正方法是把舉證責任反轉。在那之前,一筆訂單只要沒有人主動刪除就會存在。現在,它只有在能指出觸發條件時才會出現:影響使用者或客戶的故障、對安全性、金錢或資料的風險、部署阻礙,或是我親口說出。「會更乾淨」、「順便注意到」、「為了保持一致」都不是觸發條件。這些只會進入我閱讀的工作階段摘要,而不會進入管線。套用這條規則後,一次就封存了 19 筆元訂單,總數從 165 降到 144。
我刻意沒有在這裡建造的東西,才是最有趣的。顯而易見的做法是放一個 AI 守門員:讓模型判斷每一筆進來的訂單,並拒絕雜訊。我沒有這麼做,因為同一週有一筆措辭不佳的訂單通過了,它很長、結構良好且完全合理,但如果照字面執行,會把 186 個客戶信箱的外寄郵件導向一個本來就不該承載這些郵件的服務。模型守門員會直接讓這筆訂單通過。它會包含它本來應該攔截的那一類失敗。有些問題不會因為再多加一個模型而變得更好。
看錯東西的守衛
最近這件事發生在昨天,而且這是我第四次遇到同一類的失敗。
「一個工作階段只能寫入自己的儲存庫」這條規則,是由一個 pre-tool hook 來強制執行,而直到昨天,這個 hook 是透過查看指令來判斷。它知道哪些指令型態會寫入外部路徑:輸出重新導向、tee、git -C、sed -i。7 月 28 日,一個部署腳本透過 Python heredoc 寫入了 21 個外部儲存庫。hook 看到一個帶有字串的 python3 呼叫就讓它通過,因為 heredoc 不在看起來像寫入的清單中。
修正方法是停止猜測意圖。現在會在自主執行前記錄每個儲存庫的 git 狀態,並在執行後進行比對。它不在乎變更是如何產生的,只在乎不該出現變更的地方是否出現了變更。它在三個地方採取「失敗即關閉」:缺少檢查會阻止 dispatcher 啟動、基準比對失敗會中止訂單、缺少基準則視為差異而非全數通過。
模式比對是在猜測指令會做什麼。比對前後狀態則是測量它實際做了什麼。我花了四次嘗試才學到這件事,而且我在一開始絕對不會這樣設計。
成本與仍未解決的問題
協調層有 32 個 shell 腳本,大約 6300 行程式碼,以及 145 個針對複本執行的測試,永遠不會碰觸真實訂單。有 21 個 hook 會在工具呼叫前、中、後觸發:儲存庫邊界、資源鎖定、阻擋曾經摧毀 199/200 筆資料列的資料庫傾印旗標的檢查、拒絕包含未經驗證清單內事實的外寄郵件的檢查。這些都不是設計出來的。每一個零件都出現在某件事曾經穿過的地方。
仍有四個地方會停止並等待我,而這正是沒有意外發生的原因。一筆訂單在獲得我核准之前不會做任何事,包括我自己提出的訂單。標記為 dialogue 的工作不會在沒有我的情況下執行。完成的工作不會自行合併。任何文字提及付款、客戶或正式部署的項目,即使已核准也會在開始前被擱置,因為 7 月 27 日曾有一筆 50 歐元的客戶退款出現在佇列中,已核准、標記為自主、準備執行。
今天誠實的狀態是:帳面上有 149 筆訂單,其中 122 筆已核准並等待中。觸發條件規則減緩了流入,但沒有逆轉它。底層的緊張關係並未解決,而我也不確定能否解決,因為這不是工具的 bug。一台產生候選工作速度比人類評估速度還快的機器,永遠會以佇列收場,而我建造的每一道閘門,都是我刻意選擇成為瓶頸的地方。
如果我去年底就用十個專案、每個專案一個 Markdown 檔案來開始這件事,我會做的不一樣:先建立投遞箱與核准閘門,然後什麼都不要做。這兩部分是我唯一從未需要修復的。這個系統裡的其他東西,我都造了兩次。
如果你正在運行類似的東西,並且找到了一種不用讓人類閱讀每一筆項目就能保持流入誠實的方法,我很想聽聽。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.