系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。
我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。
本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。
Base Repo: paulsha-cortex
Change Ref: PR #513-fix(coordinator): planning artifact 拒收帶上原因與內容 evidence;背景 Issue #511
Issue: Planning Artifact 被 Gate 拒收時,Operator 只看得到一句 planning artifact is not accepted。真正的拒收原因、阻塞內容與 Planner 寫出的原文都沒有留下,現場只能 abandon、重新 claim,再把同一條流程重跑一次;實測連續四次得到相同失敗。
Root Cause: ArtifactAssessment 原本已經帶有 reasons 與 blocking_markers,Manager 卻只取 accepted 布林值;被拒內容又只存在 Temporary Directory,Context 結束後就消失。Planning 被當成一次性的中間文字,而不是一份必須可檢查、可拒收、可診斷的工程 Artifact。
Solution: 拒收訊息帶上有界的 reasons、markers 與 Evidence Path;同時將被拒 Artifact 以 cortex-planning-artifact-rejection/v1 寫入 Coordinator Root,保留內容、拒收理由、行號、Work ID 與 Run ID。Evidence 寫入失敗時採 fail-open,不得掩蓋原本的拒收結果。
Evidence: 新增 14 條 Regression Tests,完整測試結果為 2517 passed, 49 subtests passed,Policy Check Exit 0。這些證據支持拒收診斷與 Evidence 留存已接入既有 Planning Flow;本文不把它寫成 Planner 從此都能產出正確 Plan,也不代表 Issue #511 的回答通道與自動 Recovery 已全部完成。
上一篇,小ma好不容易學會:
Ready 與 Fanout 必須拿同一份 Plan 判。
這件事修好之後,我原本以為 Planning 這一段終於比較像正常工程流程了。
Plan 有 Authority。
Gate 會檢查。
不符合就拒收。
很好。
直到有一份 Planning Artifact 被擋下來。
小ma回了一句:
planning artifact is not accepted: <path>
我看著這句話幾秒。
「哪裡不接受?」
沒有下文。
「少哪一段?」
沒有。
「Planner 到底寫了什麼?」
也沒有。
這種時候,身為普通工程師的我,最自然的反應就是像家裡老人家一樣,關機,重開,再拍兩下電視。
於是我砍掉任務,重新 claim,再跑。
還是不收。
我再跑一次。
還是不收。
第四次,小ma依然很有原則:
「不收。」
理由則依然保持Unknown。
很好。
一套號稱有 Gate 的 Workflow,最後把我拒於門外四次,四次啊,真有原則。
老Go看了一眼紀錄。
「你現在是在 Debug,還是在抽卡?」
我沒有回答。
因為第四抽跟前三抽一模一樣。
至少抽卡偶爾還會歪出別的東西。
小bu對這種場面一向很有行動力。
「既然只是 Plan 被退,我可以先直接做。」
聽起來其實很合理。
Issue、Repo、目標檔案大致都知道。
Builder 也不是完全沒 Context。
先讓它開工,等做完再請小re Review,不就好了?
我腦中很快浮出一條便宜路徑:
Plan 被拒
→ 先人工看個大概
→ Builder 直接開始
→ Review 時再補洞
這條路我太熟了,它叫workaround。
Spec 不完整,先 Bring-up。
文件沒更新,先 Patch。
介面還沒定,兩邊先各自猜一版,晚點再說。
這真的能救急,直階,只是背後還要付出更多技術債。
例如,三天後大家開始爭論:
「你當初叫我做的,不就是這個嗎?」
而我會陷入一段很有修行感的內觀:
我當初到底叫它做了什麼?
小re沒有阻止小bu寫 Code。
它只問:
「被拒的是格式、缺少驗收條件,還是 Plan 裡本來就有一個沒決定的問題?」
「不知道。」
「那你現在要 Builder 繞過的是排版問題,還是尚未做出的工程決策?」
「……也不知道。」
這就麻煩了。
同一句 not accepted,背後可能是:
status-not-accepted
required-section-missing
blocking-decision
第一種可能只是狀態還沒進到可接受階段。
第二種是 Artifact 結構不完整。
第三種則代表 Plan 裡真的有一個 Decision 還懸著。
把三種情況都翻譯成「再跑一次」,跟看到所有 Kernel Error 都先 Reboot 差不多。
有時有效。
因為機器也只是衷實執行程式碼。
就在小bu準備繞過 Gate 時,群裡有個人沒有先開 Worktree。
它先問:
「這份工作完成時,Builder 應該交出什麼?」
我把 Issue 丟給它。
它又問:
「哪些 Decision 已經確定?哪些還要人決定?」
我把前面的討論也丟過去。
它繼續問:
「Reviewer 要拿什麼判斷完成?只看 Test,還是還有 Source、Runtime 或 Compatibility Boundary?」
老實說,問到第三題時,我已經有點想把它靜音。
我只是想讓 Agent 開工。
它卻像宗門任務堂那位特別難搞的執事,拿著玉簡逐條確認:
任務是什麼?
邊界在哪裡?
誰能做決定?
交付物是什麼?
什麼狀況算完成?
但煩歸煩,這些問題正好把前面的混亂照亮了。
小bu負責的是:
把已經定義好的東西做出來。
小re負責的是:
找出實作與證據裡的洞。
小ma負責的是:
哪個 Lifecycle Transition 還不能發生。
那麼,誰負責把一個模糊的「幫我處理這個 Issue」,整理成 Builder 真正可以接手的工作?
這個責任以前一直是我自己在扛。
只是我沒有替它命名。
現在它終於從群裡浮出來。
我叫它小pa,也可以叫他小趴,或是叫他小%。
正式角色叫 Planner。
它的偏見不是「我來修」。
而是:
「先把 Builder 到底要做什麼說清楚。」
我以前對 Planning 的理解,其實非常接近「把 Prompt 補完整」。
例如原本是:
修掉 Issue #511
補成:
請先讀 Issue、相關 Source、Tests 與 History,
整理 Root Cause,提出 Plan,再交給 Builder 實作。
字比較多。
看起來也比較專業。
但只要這些內容仍然活在某次 Session 裡,Planning 就還是一次性對話。
Session 關掉,誰都不確定當時接受的是哪一版。
Manager 也無法對它做明確 Gate。
Builder 只會拿到一段「大概是這個意思」的 Context。
真正能進 Workflow 的 Planning,必須留下 Artifact:
Requirement
→ Spec
→ Plan
→ Task
→ Acceptance Boundary
這份 Artifact 要能回答:
要改什麼,不改什麼。
哪些 Decision 已經確定。
哪些條件仍然阻塞。
Builder 要交什麼。
Reviewer 拿什麼查。
Manager 何時可以放行下一步。
所以 Planner 的工作不是替 Agent 多寫幾句鼓勵。
它是在 Work 進入 Build 前,替後面所有角色建立同一份工作定義。
上一篇講的是 Plan Authority:
Ready 與 Fanout 必須信同一份 Plan。
今天又多了一層:
那份被大家共同信任的 Plan,本身也必須是一份可以被檢查、拒收與追查的 Artifact。
不然我們只是很有紀律地共同相信一段看不到的文字。
修真一點的說法,叫全宗同心。
工程一點的說法,叫 Shared Blind Spot。
回頭看 Source,事情更尷尬。
Planning Gate 並不是只會回一個 True/False。
assess_planning_artifact() 原本就會產出:
ArtifactAssessment
├── accepted
├── reasons
└── blocking_markers
也就是說,它知道:
哪一類條件沒過。
哪幾行有阻塞標記。
原文大概寫了什麼。
問題出在 Manager 往上拋 Error 時,只拿了:
accepted == false
其他全部丟掉。
被拒的 Artifact 又只活在 TemporaryDirectory 裡。
Context 一結束,內容跟著消失。
所以系統內部其實發生的是:
Planner 寫出 Artifact
↓
Gate 看懂哪裡有問題
↓
Manager 把理由丟掉
↓
Temporary Directory 把原文刪掉
↓
Operator 收到:不接受
這條流程很好,分工完美,每一層都知道一點,大家都丟一點,最後我什麼都不知道。
小ma的「還沒」原本應該是 Lifecycle Discipline。
到了這裡卻變成一位只會蓋退件章、不附審查意見的長老。
你可以尊重它的權威。
但你很難因此進步。
這次沒有把 Planning Gate 改鬆,也沒有遇到拒收就自動放行。
相反地,拒收仍然照舊 Raise。
只是 Error Message 不再只剩 Path,而是帶上有界資訊:
reasons
→ markers
→ evidence path
之所以要有界,是因為這段 Reason 還會流進 needs_human、Control Contract 與 Recovery Path。
不能把整份 Plan 原文塞進一行 Status。
因此訊息限制單行、總長度最多 400,Marker 最多帶幾條截斷後文字。
完整內容則另外寫成:
cortex-planning-artifact-rejection/v1
放在 Coordinator Root 的 Evidence 目錄,保留:
kind
path
content
reasons
markers
work_id
run_id
created_at
內容過長時會截斷並留下原始長度。
Evidence 採原子寫入。
至於 Coordinator 是誰,我們下章分曉。
但如果 Evidence 寫入本身失敗,仍然不能把真正的 Planning Rejection 換成一個 IO Error。
所以這條旁路是 fail-open:
Evidence 寫失敗
→ 留 Log
→ 原本的拒收照常成立
這個取捨很重要。
診斷是為了讓 Failure 可理解。
不能因為診斷工具自己跌倒,就把原本的 Failure 身分洗掉。
否則下一次我看到的會從:
Plan 不接受。
升級成:
寫退件紀錄失敗。
至於 Plan 到底怎麼了?
恭喜,再抽一次。
PR #513 新增 14 條 Regression Tests,完整結果為:
2517 passed
49 subtests passed
policy check exit 0
它驗證了:
三類 reasons 會進入訊息。
blocking-decision 的行號與文字能被看到。
訊息保持單行且有長度上限。
被拒 Artifact 內容會落到 Coordinator Root。
超長內容會留下 Truncated Metadata。
Evidence 寫入失敗不會蓋掉真正的拒收原因。
成功路徑不會平白多寫一份 Rejection Evidence。
這些能支持的主張是:
Planning Artifact 被拒時,Operator 終於能知道為什麼被拒,也能回頭看 Planner 寫了什麼。
它不能支持:
Planner 從此不會產生爛 Plan。
也不能支持:
所有 Blocking Decision 都已經能在 CLI 裡直接回答,自動 Recovery 也已完整打通。
Issue #511 還有後續項目。
今天只是先把最荒謬的一段補起來:
系統明明知道退件理由,卻叫人類靠重跑猜答案。
小pa現在終於可以把一份看得見的 Plan 放到桌上。
小re可以指出哪一段不成立。
小ma可以說哪個 Transition 還不能過。
小bu則已經在旁邊等很久了。
它看著被拆好的十個 Task,舉手:
「所以我先拿哪一個?」
另外九個小bu也同時舉手。
群裡瞬間像宗門發放限量功法。
Plan 已經說清楚了。
下一個問題是,誰來決定這些工作怎麼排、怎麼組、哪個先拿、哪個必須等前一個完成?
下一篇,Planner 把工作拆完,十個 Agent 一起伸手。
Have a nice day.