Agentflow · Control Plane

如何靠確定性 hook,
讓 AI 開發變得誠實

Agentflow 不靠「叫模型乖乖照做」。它把 AI 的每一輪工作,綁在 git 與檔案的客觀證據上,由宿主端一個零 AI 的裁判程式強制檢查 —— 模型說的話一律不算數,只有指令輸出跟檔案內容算數。

notebook = 唯一真相 exit 2 擋下並要求修正一次 exit 0 放行

「一個宣稱只有在指令輸出或檔案內容證實後,才成為事實。」

這句設計信條被 round-linter.js 跟 completion-context.js 用機器強制執行。模型不能打字說「我審過了」就過關 —— 審查報告必須是一個真實存在的有界檔案,並引用那個確切的 implementation commit hash。整套架構都是這一句話的實作。

The Round Lifecycle

一輪工作的資料流

從你打 godev 到裁判放行,每個階段誰負責、跑哪支程式,都在下面。左側鋼軌上流動的信號,就是這一輪的證據在往下走。

1

啟動 Owner

你打 godev。這只是啟動詞,載入 SKILL.md 的小規則集;streams / delegation / looper 等進階規則按需才載入,不佔 context。

2

初始化 agf.js

新專案建立三件套:notebook(devlog.md)、設定檔(ag.json)、三條 .gitignore 條目,並安裝宿主端 Stop-hook。不會幫你 git init。

node agf.js init
3

有界快照 resume-intake.js

一個有界的本地命令,回傳 STATUS、最後一個未解決的 Ask、branch、changed paths,並分辨「正常的 owner 輸入」與「別的 session 併發亂改」。它不 fetch、不讀封存、不寫檔 —— 成本可控的冷啟動。

resume-intake.js --repo … --notebook … --host claude
4

選路線 & 做事 Coordinator

四選一:direct(宿主自己做清楚、可逆的小事)、selected_advisors(重要未知找具名顧問)、full_pipeline(昂貴/跨信任邊界的事)、blocked。過程中的材料性轉折寫成 RUN 事件當復原麵包屑,每約 10 分鐘寫一個 WIP 進度檢查點。

notebook-write.js append-run / append-wip
5

收尾 Reply notebook-write.js

完成一輪時寫 # ← Reply / A-NNN:含條列式 SUMMARY、完整 FINAL REPORT、批次化的問題(每題附建議預設值)。寫入帶檔案鎖、原子替換,先跑一次自我 preflight 才落地。

terminal-preflight.js <doc> --context-stdin
⚖

Stop-hook 裁判 Host · Zero-AI

模型一結束回合,宿主自動跑 stop-hook.js。它讀真實時鐘與 git push 狀態,對 notebook + 客觀事實跑 round-linter,然後只回兩種結果:

exit 2 · 擋下 某條檢查 fail(例如 cross_check:改了程式卻沒有綁 Ask 的審查決定)。回合保持開啟,要求修正一次。
exit 0 · 放行 全部通過,或回合仍在進行中(Reply 還沒寫,cross_check 只是非阻擋警告)。

三個保命設計:Fail-open — 裁判自己 crash 就放行,永不弄死 session;Loop guard — stop_hook_active 為真時直接放行,一次修正不會變無限迴圈;最多擋一次。

Deterministic Evidence

「要不要外部審查」是算出來的,不是問模型

review_decision() 純粹用 git 的 changed paths 推導。這是「不信模型宣稱」最核心的一段 —— 換誰來跑、結果都一樣。

Git 看到的變更裁判判定意義
動到 source / test / config / 使用者文件 required 必須有外部審查報告,PASS 三項:Outcome、Minimality、Conformance,並引用確切 commit。
只動到 agentflow 自己的記帳檔 not-requested notebook / 封存 / 設定以外沒有實作變更,免審。
第一次 init,全 repo 只有 agentflow 記錄 + .gitignore bootstrap 開機記帳特例,直接放行。
Ask 內含 skip-review: 理由 owner-waived Owner 明示放棄最終審查,理由入帳。只跳過這一關,其他 gate 照跑。
Five Pillars

撐起整套的五根支柱

30,000 行零依賴 Node 測試碼,全繞著這五件事。

01 / STATE

Notebook = 持久對話與復原入口

一份持久文件,同時是對話、狀態與崩潰復原面。session 死掉、換機器、換 agent,下次 godev 就從這裡讀回全部脈絡。

devlog.mdnotebook-write.js
02 / REFEREE

宿主端裁判

註冊成 Claude Code / Codex 的 Stop hook,零 AI、確定性,在模型講完話那一刻執行。exit 2 擋、exit 0 放。強制,不是建議。

stop-hook.jsround-linter.js
03 / EVIDENCE

事實從 git 推導

changed paths、push 狀態、tracker、真實時鐘,全由宿主蒐集。模型的宣稱只是待驗證的假設,不是事實。

completion-context.jssuite-evidence.js
04 / REVIEW

比例原則審查

按改了幾行、有無跨信任邊界、廣度,確定性地選 narrow / targeted / full。外部 worker 在隔離的無 remote git clone 裡跑,碰不到真 repo。

cross-check-plan.jsexternal-runner.js
05 / EVOLUTION

事故驅動的規則

每條 — I-NNN 規則都對應一次真實失敗(46KB 事故簿)。改規則前必須先讀它的事故記錄。系統靠把踩過的坑編碼成守衛而長大。

incidents-log.mdSKILL.md
ELI5 · 白話版

用畫畫來理解這兩支

如果上面太技術,先看這個五歲版。想像小明畫了一張圖,想貼到冰箱上 —— 家裡的規矩是:貼上去之前,要有人檢查過。

故事:一張圖要貼冰箱之前

1️⃣ 小明只是在角落寫了名字(沒真的畫東西)→ 不用檢查,直接貼 ✅
2️⃣ 小明真的畫了新東西 → 要檢查!這時候 —
    🧑‍🏫 cross-check-plan(安排的人) 決定「要檢查多仔細」:塗掉一個點 → 瞄一眼;畫了一塊 → 看那一塊;整張重畫 → 全部看。
    🏃 external-runner(跑腿的人) 把圖印一張影印本,拿到隔壁房間關起門,交給另一個老師看(不能偷改、不能帶走),再把「通過/不通過」拿回來。
3️⃣ 老師說「通過」→ 才能貼冰箱 🧊

Q · 檢查多仔細

我要自己設定嗎?

不用 —— 自動算的。 cross-check-plan 用公式看你改了幾檔、幾行、有沒有動到危險的地方,自己算出瞄一眼 / 看一塊 / 全看。你不用打字說「我要 narrow」。

你唯一會出手的時候(可選):在指令寫 stronger(查嚴一點)或 skip-review: 理由(這次授權跳過)。用哪個等級的模型則在 ag.json 設一次就好。

Q · 派給別人

「別人」在程式碼裡是誰?

是另一個 AI,不是人。 程式去開一個新的子程序,跑另一個 AI 的命令列工具 —— 自己審自己會偏心,所以要換一個。

你今天親眼看過:先 spawn("codex") → 配額用完;再 spawn("claude") → 跑完了。那兩個就是被「派」出去的別人。

child_process.spawn("codex", ["exec", "<審查指令>"], { cwd: 隔離clone })
//                    ↑ 另一個 AI       ↑ 要它做的事      ↑ 在影印本資料夾裡跑
整條總結:你幾乎不用設定 —— 深淺自動算、模型設一次;你只要正常改程式、收尾,這條鏈就自己把「另一個 AI」叫來審,審完核對 commit 才放行。先決定看多仔細(cross-check-plan),再送去給人看(external-runner):一個是安排,一個是跑腿。
Review Pipeline · 審查流水線內部

「派外部 AI 審」是怎麼運作的

當裁判判定一輪 required,這條流水線就啟動。它由兩支腳本組成:一支確定性地決定審多深,一支在隔離環境把審核派出去。

cross-check-plan.js — 確定性選審查範圍

吃一份變更事實(改了哪些檔、幾行、有無行為變更 / 跨信任邊界 / 廣度),用固定規則吐出 narrow / targeted / full。同一份事實,永遠同一個答案 —— 不問模型意見。

條件(讀 git 事實)審查強度審核員要做的事
跨信任邊界 · 或廣泛變更 · 或 ≥10 檔 · 或 ≥500 行 full 重跑完整測試套件 + 指名的高風險檢查
非行為變更 · 且純文件 · 且 ≤5 檔 · 且 ≤160 行 narrow 只讀 diff + 指名的契約檢查,不重跑無關全套
其餘(一般行為 / 混合變更) targeted 檢查行為邊界 + 重跑重點測試(全套證據由 coordinator 供應)
stronger / skip-review: 理由 升一級 / 跳過 owner 明示提高強度,或授權跳過最終審查
三級都逃不掉的一條:審核員必須回報「剛好一個 Outcome / Minimality / Conformance,各 PASS 或 BLOCKING」。這正是裁判(cross_check gate)之後要逐項核對的欄位。實測中我們的 CHANGELOG 一行純文件 → 判 narrow,分毫不差。

external-runner.js — 在隔離環境派審

每個外部審核都跑在一個拋棄式、無 remote 的 git clone 裡。worker 只能產出它宣告的報告檔;判斷權永遠留在 coordinator 手上。

1 · clonegit clone --no-local <repo> 到拋棄式目錄,跟原 repo 物件分離
2 · 斷網拔掉所有 remote —— worker push 不出去、也拉不到外面
3 · 驗獨立確認 git dir 在內、無 alternates;不獨立就拒絕執行
4 · 跑 worker在 clone 裡跑審核員(codex / claude),關 stdin、有界輸出、逾時 + 停滯看門狗、process group 清理
5 · 回事實回傳退出碼、輸出、clone 變更身份、以及下面這段誠實聲明
acceptance: { accepted: false,
  reason: "Process exit and captured output
           do not prove coordinator acceptance." }
assurance:  { os_level_confinement: "not_proven",
              remote_provider_cancellation: "not_proven" }
它對自己的能耐很誠實:exit 0 ≠ 通過 —— worker 的退出碼與輸出證明不了 coordinator 的接受。這個 clone 是「碰撞解法,不是 OS 沙盒」,它主動記下自己的限制(絕對路徑寫入、繼承的憑證、網路、供應商端無法保證取消)。worker 只寫宣告的報告檔,coordinator 匯入前逐一比對 clone 的每個變更 vs 凍結白名單。實測回傳的 independent: true / remotes: [] / accepted: false,就是這裡吐出來的。
Live Test · 實測

沙盒示範:派審嘗試與 commit 核對

拿 archify(一個獨立 git repo 的真程式專案)做派審嘗試與檢查器測試;外部審查沒有完整成功,詳見下方限制。全程在拋棄式沙盒 clone 裡,真正的 archify repo 一根寒毛都沒動。下面每個數字都是實際腳本輸出。

步驟用什麼真實結果
① 改動 + 收尾git改 CHANGELOG 一行 → commit A 6a9ee2ab…688c
② 決定審查範圍cross-check-plan.js判 narrow,吐出凍結的審核義務
③ 派外部 AI 審(隔離)external-runner.jsindependent: true、remotes: []、accepted: false
④ 生報告檔外部模型 → fallback見下方「真實插曲」
⑤ 核對 commit → 放行lint_cross_check(裁判本尊)PASS / FAIL,見下方

③ 隔離是真的

external-runner.js 把 archify git clone --no-local 到一個獨立目錄,拔光所有 remote,並標記 acceptance.accepted = false —— 「程序退出與輸出,證明不了 coordinator 的接受」。這會減少一般 Git 誤推的風險,但不是 OS 或網路沙盒;不能據此保證 worker 無法存取原 repo 或向外傳輸。

④ 真實插曲(特別有教育意義):我真的把審核派出去了,結果撞到兩道現實的牆 —— codex 回 You've hit your usage limit(配額用完);claude -p(fallback)繼承了本機 ~/.claude 的整套 hooks/權限設定,吐不出乾淨報告。這正是 delegation 規則寫的「遇到 session-limit → 停用該 profile → 換另一個家族」。為了演完第 ⑤ 步,審查報告內容改由手寫成符合規格的 stand-in —— 但判它的函式是 100% agentflow 真碼。

⑤ 拱心石:同一份報告,commit 一改就翻臉

報告始終只認證 commit A。做一個真的 commit B(模擬「審完之後又改一行」),把兩種情況丟給真正的 lint_cross_check:

CASE 1 · PASS
出貨 = A · 報告認證 = A
6a9ee2ab…688c
被審的 = 出貨的 → 放行。
CASE 2 · FAIL
出貨 = B · 報告認證 = A
21fb7ef7…700a ≠ 6a9ee2ab…688c
審完後又改了一行 → 新 commit 沒人看過 → 擋下。
CASE 2 → {
  "id": "cross_check",
  "status": "fail",
  "detail": "external review report commit does not
             match the round's final implementation commit"
}

差一個 commit 就當作沒審過 —— 這不是誰講的,是那個 fail 親口說的。「被審的」與「出貨的」必須是位元組相同的同一個 commit。

Fit Check

為什麼它跟這個 vault 不合

工具沒壞,是用錯場景

第三支柱假設 repo 是乾淨的程式碼專案。但 seafan 是一個 Obsidian vault:

  • 專案 CLAUDE.md 明寫「不執行任何 git 指令」,而 agentflow 預設要 commit / push。
  • 工作樹有數千個 untracked 檔案(P.A.R.A/、PAI MEMORY/、wiki/、一堆 .canvas)。
  • 一旦要「完成一輪」,review_decision() 會把整個髒工作樹當本輪變更,強制一次涵蓋全 vault 的 full 審查 —— 對「開發一個功能」毫無意義。

結論:這套「file-logged + git evidence」的開發紀律,是為乾淨程式碼 repo 設計的。若要用,應該在底下真正的程式子專案(例如 archify/,正是本次實測的所在)重新 init,而不是整個 vault root。