系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。
我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。
本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。
Base Repo: paulshaclaw
Change Ref: PR #112-feat(coordinator): persona-manager headless 自主派工通電(Phase A+B)
Issue: 做事、執行與查證被拆給不同 Agent 後,工作已經可以平行處理;但「哪個 Agent 在做哪件事、跑到哪裡、Log 在哪、是不是已經結束」仍然主要靠我自己記。Agent 愈多,我反而愈像值班總機。
Root Cause: Interactive Terminal、Agent Process 與 Work Identity 混在一起;tmux Pane 很適合讓人互動,卻只能表示 Agent 暫時坐在哪裡,不能成為一份工作可持續追蹤的 Authority。
Solution: 保留 Human Interactive 路徑;Autonomous Dispatch 改走 Headless Executor。派工時帶入 Persona Contract,以 Job Registry 留下 Executor、Session、PID、Log 與 Exit Result,再以跨 Process 可讀的 Exit Sentinel 保存 Completion。
Evidence: PR 紀錄全套 1221 passed, 15 skipped;2 個 operator_cockpit 既有 Failure 可在 main 重現,與本 PR 無關。Adversarial Review 抓出 CLI Fanout 未接 Headless,以及 waitpid 無法跨 Process Poll 兩個 High Finding,修正後加入對應回歸。部分真實 Executor Flag/Hook Smoke Test 當時仍列為 Pending,因此本文不把 Unit Test 寫成三家 Agent 已完成完整 Live 驗證。
上一篇做到最後,群裡已經不能再用一個模糊的 Agent 包辦所有事情。
我開始為Agent取名,給他固定的角色,小bu是builder,專責實作。
小bu看到問題,第一反應永遠是:
「我來。」
然後開始改。
小re則站在旁邊問:
「所以呢?這能證明什麼?」
小re是reviewer,它不幫忙修,只負責找洞、看 Evidence、確認小bu是不是又很有效率地解錯問題。
分工是對的。
工作也真的可以平行。
我會同時開幾個 Terminal:一隻改 Code,一隻跑 Test,一隻讀 Source,一隻 Review 前一隻的結果;另外可能還有 Codex 或 Copilot 在另一個 Worktree 處理別的 Issue。
剛開始很爽。
那種感覺很像修真群裡終於不再只有我一個人在搬磚。大家各自領了任務,閉關的閉關、找碴的找碴,看起來宗門蒸蒸日上。
然後我的 tmux 開始變成記憶力測驗:
Claude → 這隻在做哪個?
Copilot → 剛才是不是卡 Permission?
Codex #1 → 它跑完了嗎?
Codex #2 → 這又是哪一份 Worktree?
小re → 它現在 Review 哪一版?
一隻 Agent 的時候,我只要記一件事:
它現在在做什麼?
五隻一起跑之後,我得記的是:
誰拿了哪個 Task
用哪一份 Context
在哪個 Worktree
目前 Running 還是 Waiting
Log 要去哪裡找
做完後輪到誰
有時它不是做完。
只是卡在 Permission。
有時也不是卡住。
只是正在等我,而我忘了。
Agent 的 Context Window 很大。
但我沒有,人力終有時而窮。
所以表面上的架構是:
Task A → Claude
Task B → Copilot
Task C → Codex #1
Task D → Codex #2
Task E → 小re
實際上比較像:
Claude ─┐
Copilot ─┤
Codex #1 ─┼──> 一顆普通工程師的腦袋
Codex #2 ─┤ ↓
小re ─┘ 我是誰?我在哪?下一隻要叫誰?
很好。
五個 Agent 平行工作,最後共用一顆人肉 Scheduler。
我原本想把工作分出去。
結果只是把 Coding 的 Cognitive Load,換成 Monitoring 的 Cognitive Load,再很有禮貌地寄回來給我。
既然所有 Agent 都在 tmux 裡,那第一個直覺非常合理:
把 Pane 管好,不就好了?
Manager 派工作時找一個空 Pane,把 Agent 丟進去,再記住:
Task A → Pane %1
Task B → Pane %2
Task C → Pane %3
這至少解決一件事:
我不用再自己記哪一隻 Agent 躲在哪一格。
看起來很好。
老Go看了一眼。
「那
%3現在是 Running、卡在 Permission,還是其實早就做完了?」
「……我要切過去看。」
「那這份 Task 做完後,Pane 會留著還是重用?」
「應該會重用。」
「所以明天的
%3,跟今天的%3,可能根本不是同一份工作?」
「對。」
「那你現在記到的是工作,還是工作暫時坐在哪張椅子?」
欸!!!!這盲腸,破了!!!
Pane ID 很適合回答:
我要去哪裡找這隻 Agent?
但 Orchestration 真正需要回答的是:
這份 Work 是誰、交給誰、目前什麼狀態、Log 在哪裡、最後怎麼結束?
PaneAllocator 可以替我管理座位。
我的腦袋還是在管理工作。
send-keys 也開始斷章取義原本 Manager 派工可能只有一句:
fix issue #123
這種東西透過 tmux send-keys 很好用。
但角色拆開後,我不能再只說「幫我修這個」,還得一起帶上 Persona、責任範圍、Plan 與 Task:
Role: reviewer
Responsibility: ...
Constraints: ...
Task: ...
Plan: ...
問題是,Pane Sender 原本只是拿來在 Terminal 打一行字,不是拿來傳一份多行 Contract。
對 Terminal 而言,換行很可能就是 Enter。
所以我以為送進去的是一份完整 Persona Contract,Agent 收到的卻可能是:
Role: reviewer
Enter。
Responsibility: ...
再 Enter。
整份 Prompt 被切成數段,像群裡有人一次只貼一句,還不等前一句看完就連發。
小re看了都不知道自己該先 Review Code,還是先 Review 我的輸入法。
前面才發現 Pane 只能告訴我 Agent 在哪裡,現在連派工本身也開始證明:
Human Interaction 的介面,不一定適合拿來當 Autonomous Dispatch 的介面。
這次沒有把 tmux 一刀砍掉。
本來有想過,是不是乾脆替 Agent 重做一套全新的平台。
但我跟 Agent 互動時,Pane 其實很好用:
我看輸出
→ 臨時插一句
→ 改方向
→ 再看它回什麼
追求在現存工作流裡讓 Agent 無縫接手,才是男人的浪漫。
所以 Human Interactive 路徑保留。
但 Autonomous Dispatch 改成直接啟動 Agent CLI,也就是 Headless Executor。
先講功能再講名字:
它不是替 Agent 開一格畫面,而是把完整 Prompt 當輸入,啟動一個可以追蹤的 Process。
Claude、Copilot、Codex 的 CLI 參數各有各的脾氣,所以 Manager 不直接到處拼三家的 Command,而是透過 Launcher 把同一份 Work 翻成對應 Executor 的 argv。
這一步看起來只是把 Agent 從前景搬到背景。
真正重要的不是 Background。
是派工終於可以脫離「左邊第二格那隻」這種民間辨識法。
Headless 解掉的是 Transport。
但我原本最痛苦的那件事還沒完全消失:
剛才到底派了誰去做什麼?
Headless 啟動 Agent 時,本質上就是啟一個 subprocess。
Process 啟動後,OS 會給它一個 PID;Launcher 再把 PID、Executor、Session 與 Log Path 一起留下來。
每一份派工開始有固定的 Metadata:
executor
session_name
pid
log_path
exit_code
這些資訊被收進 Job Registry。
先講功能再講名字:
它就是讓一份工作不再靠「左邊第二個 Pane 那隻」辨認,而是有一筆之後還查得到的紀錄。
以前是:
%1 = Claude = Issue A?
%2 = Copilot = 好像是 Review?
%3 = 剛才那個 Codex 去哪了?
現在至少可以從 Registry 查:
這份 Work
→ 交給哪個 Executor
→ 現在是哪個 Process
→ Log 在哪裡
工作第一次開始脫離「我還記不記得」。
Parallelism 終於沒有再直接跟 Human Memory Capacity 綁死。
但 Registry 裡還有一格很麻煩:
exit_code = ?
Process 到底什麼時候跑完,又要由誰把結果填進來?
waitpid 很誠實:你不是它爸,就別一直問有了 PID,這題看起來超簡單。
Process 跑完沒?
用 waitpid 看 Exit Code 不就好了。
第一版真的這樣做。
測試也可以跑。
直到 Adversarial Review 問了一個很關鍵的問題:
「啟動 Agent 的 Process,跟之後 Poll 它的 Process,是同一個嗎?」
不一定。
可能是 Process A 啟動 Agent,自己先結束;過一陣子 Process B 再回來查狀態。
Process A
→ spawn Agent
→ exit
Process B
→ 拿著 PID 回來 Poll
→ 但 Agent 不是它的 Child
waitpid 等的是自己的 Child Process。
知道 PID,不代表你有資格替它收屍。
所以 PID 雖然進了 Registry,Completion 還是綁在啟動它的短命 Process 裡。
這跟我前面的問題其實同一個味道。
以前是:
哪隻 Agent 在做什麼,全部放在我的 Working Memory。
現在變成:
Agent 有沒有跑完,全部放在 Parent Process 才拿得到的狀態。
看起來比較 Software。
本質上沒有比較高尚。
只是換一個地方失憶。
最後修法其實還滿接地氣的。
Executor 結束時,把 Exit Code 寫成一個獨立檔案;之後不管哪個 Manager Process 回來,都能重新讀取。
概念上就是:
啟動 Executor
→ Executor 結束
→ 寫下 Exit Code
另一個 Process 之後再 Poll:
PID 還活著嗎?
→ 用 Process Liveness 檢查
Exit Result 有了嗎?
→ 讀持久化的 Exit Sentinel
這個小檔案就是 Exit Sentinel。
不是什麼很炫的 Distributed Workflow Engine。
它只是在做一件很重要的事:
不要把明天還要知道的結果,只放在今天晚上會死掉的 Process 記憶裡。
同一輪 Review 還抓到另一個 High Finding:Headless 明明已經實作,CLI 的 fanout --executor 卻根本沒接上去。
也就是 API/Test 可以抵達新路徑,使用者真正下 Command 時卻還走不到。
架構圖已經到未來。
入口還住在過去。
修完之後,CLI Fanout 才真的會建立 Headless Launcher,而不是只有測試知道新路徑存在。
PR #112 最後紀錄:
1221 passed
15 skipped
另外 2 個 operator_cockpit Failure,在 main 上同樣可以重現,確認不是這次 PR 帶進來的回歸。
新增測試涵蓋 Persona Contract、三家 Executor argv、Job Registry Headless 欄位、跨 Process Completion、Progress Relay,以及 CLI Fanout 到 Headless Launcher 的接線。
Progress Relay 這裡只負責報平安。
Agent Start/Stop 可以回報,但這類 Event 只能說「它有活動」,不能直接升級成「工作正確完成」。
另外,PR 當時也明列部分 Claude/Codex Remote Flag,以及 Copilot/Codex Hook 在真實 Headless 環境仍需要 Smoke Test。
所以 1221 passed 能證明這條控制路徑與契約有被測到。
不能被我翻譯成:
三家 Agent 已經二十四小時自主運轉,穩如老狗。
沒有這個 Evidence。
先不要替它寫績效自評。
不過做到這裡,群裡確實多了一個習慣很奇怪的傢伙。
它不改 Code,也不替小bu辯護,更不跟小re爭論 Finding。
它只會先問:
這是哪一份 Work?
交給誰?
Process 還活著嗎?
Log 在哪裡?
Exit Result 是什麼?
我叫它 小ma。
正式角色是 Manager,不是小媽,不過也對,她就是媽媽的角色,管東管西。
它不是因為我們缺一個主管才出現。
剛好相反。
是因為我不想再當所有 Agent 的主管、秘書、總機、簽收人與失物招領處,它才被現實逼出來。
但今天的小ma只知道工作活著還是死了。
它還不知道 Builder 做完後,誰該跑 Gate;Gate 過了,誰留下 Handoff;下一份相依工作,又該由誰叫醒。
老Go看著一筆已經 done 的 Job。
「好,現在你知道它做完了。」
「嗯。」
「然後呢?」
「……」
我的人肉 Scheduler 才剛下掉一半的班。
下一篇,再讓它繼續失業。
Have a nice day.