本文是「代理軟體工程」系列的第三篇。
第一篇:代理軟體工程 | 別再用人類的尺子量 AI 的活了:代理原生工作估算
第二篇:代理軟體工程 #2 | 重新思考 Code Review
Git 是為人類設計的鋒利刀具。Agent 需要的是安全的電動工具。
引言:版本控制的隱藏假設正在瓦解
2019 年底,Google 工程師 Martin von Zweigbergk 以個人專案的名義啟動了一個版本控制系統。他把命令行工具取名為 jj(因為好打字)。
專案叫 Jujutsu[1],因為和 jj 匹配,好像沒啥實際含義。六年後,這個專案有了 25k 個 star,成了 Google 內部版本控制演進的重要方向,而它的设计理念正在被一個當初沒有預料到的使用者群体重新發現:AI Coding Agent。
時間剛剛邁入 2026 年 3 月,幾件事就同時發生了。
OpenAI 被報導正在構建 GitHub 競品[2],起因是 GitHub 頻繁的服務降級對依賴 AI Agent 持續整合的團隊造成了不可接受的影響。
OpenAI 還開源了 Symphony[3]。一個用 Elixir 實現的 Agent 編排層,核心理念是「讓工程師管理工作而非監督 Agent」,它為每個 Linear issue 建立隔離工作空間、啟動 Codex 自主執行、收集工作證明。
與此同時,一家名為 [JJHub](https://jjhub.tech[4]) 的創業公司宣布基於 Jujutsu 構建 Agent-Native 的開發平台,其核心論點是「AI Agent 讓一個小創業公司在 commit 量上看起來和 Google 一樣」。
我也看到曾經的 Rust 核心維護者 Steve(官方 Rust Book 作者,也為我的 Rust 書作過序),在 2025 年年底也基於 JJ 自己開了創業公司 ersc[5],Steve 也寫了一篇 JJ 教程[6]。
GitHub 自己則在 2026 年 1 月初推出了 Agentic Workflows[7] 技術預覽,試圖把 AI Agent 嵌入現有的 CI/CD 框架中。
所有這些動作指向同一個事實:Git 這個統治了軟體工程二十年的版本控制系統,其核心設計假設正在被 AI Coding Agent 系統性地挑戰。
但挑戰 Git 不是目的。版本控制只是手段,它服務的根本目標是:安全地管理程式碼變更的歷史和並發。 在代理軟體工程的框架下,這個目標沒有變,但實現方式必須根本性地重新思考。
第一部分:Git 的五個隱藏假設
要理解為什麼 Git 在 Agent 時代遇到困難,我們需要先解剖它的設計假設。這些假設在人類編碼時代完全合理,但在 Agent 時代正在一個接一個地瓦解。
假設一:操作者理解上下文
Git 的每一個互動設計都假設操作者,人類開發者,理解自己在做什麼。
git add -p(互動式暫存)假設你能逐行判斷哪些修改應該進入下一個 commit。git rebase -i(互動式變基)假設你能理解 commit 之間的依賴關係並做出正確的編輯決策。git merge產生衝突時假設你能理解雙方修改的語意意圖並做出合併決策。
Agent 不「理解」上下文。它基於統計推理做出決策。它可以執行 git add -A && git commit -m "..." 這種確定性的命令序列,但面對 git rebase -i 的互動式介面,需要在文字編輯器中重排 pick/squash/edit 行時,就變成了一個脆弱的操作,每一步都增加了出錯的機率。
假設二:操作是順序的
Git 的並發模型假設一個工作目錄在同一時間只有一個操作者。git stash 的存在就是證明。當你需要切換上下文時,必須先「暫存」當前狀態。git rebase 遇到衝突時進入一個 "in progress" 狀態,在這個狀態被解決之前,你不能做其他操作。
在 Agent 時代,並發是常態。像 OpenAI 的 Symphony 這樣的編排系統可能同時管理 5-10 個 Agent,每個處理不同的 issue,它們可能需要在同一個程式碼庫上工作。Git 的鎖文件機制(.git/index.lock)和 "in progress" 狀態在這種場景下變成了嚴重的並發瓶頸。
假設三:變更需要顯式暫存
Git 的 staging area(index)是一個為人類認知設計的中間層。它的價值在於讓你能「精選」哪些修改進入下一個 commit,這對編寫精心組織的 commit 歷史很重要。
但 Agent 不需要「精選」。它在一個隔離的工作空間中執行一個明確定義的任務,所有檔案修改都是為了完成這個任務。git add 對 Agent 來說是一個純粹的儀式動作,100% 的情況下是 git add -A。每一次這種儀式動作都消耗工具呼叫的 token 預算,增加上下文噪聲,但不產生任何決策價值。
假設四:分支需要命名
Git 的分支模型要求每個分支有一個名字。这在人類工作流中有意義。你需要和同事溝通「我在 feature/user-registration 分支上」。但 Agent 的工作分支是臨時的、隔離的、一次性的。為每個 Agent 會話取一個分支名是無意義的開銷。
更深層的問題是:Git 的 branch 概念把「分支指標」和「程式碼變更」綁定在了一起。當 Agent amend 一個 commit 時,commit hash 變了,branch pointer 需要更新,任何引用舊 hash 的系統都需要跟著更新。這種引用不穩定性在自動化編排中造成了大量的協調開銷。
假設五:衝突必須立即解決
Git 在遇到合併衝突時會停下來等待人類輸入。git merge 產生衝突標記(<<<<<<<),git rebase 進入 "in progress" 狀態,兩者都要求你在繼續之前解決衝突。
這對人類是合理的。衝突意味著兩個人對同一段程式碼有不同的理解,需要人的判斷來決定正確的合併。但對 Agent 編排系統來說,「停下來等待」是災難性的。一個管理 10 個並行 Agent 的編排器,如果任何一個 Agent 的 rebase 被衝突阻塞,就需要介入處理。而介入本身需要上下文(理解為什麼會衝突),這個上下文可能不在 Agent 編排器的範圍內。
第二部分:Jujutsu 如何系統性地解決這些問題
Jujutsu(jj)不是「更好的 Git CLI」,而是對版本控制資料模型的根本重新設計。它碰巧使用 Git 作為儲存後端(保持相容性),但其使用者模型和 Git 完全不同。它用 Rust 實現,底層使用 gitoxide 庫,所有核心開發者用 jj 開發 jj 自身。
解決方案一:一切皆 Commit,消除儀式性操作
jj 把 Git 中四種不同的「狀態容器」(working directory、staging area、stash、commit)統一為一種:commit。
你的工作副本(@)就是一個 commit。你編輯檔案時,jj 自動把變更快照為這個 commit 的更新。沒有 staging area,沒有 stash,沒有 "dirty working directory" 的概念。
┌─────────────────────────────────────────────────────────────┐ │ Git 的狀態模型:4 種容器,需要手動在她們之間轉移 │ │ │ │ ┌──────────┐ git add ┌─────────┐ git commit ┌────────┐ │ │ │ Working │──────────►│ Staging │────────────►│ Commit │ │ │ │ Directory│ │ Area │ │ │ │ │ └────┬─────┘ └─────────┘ └────────┘ │ │ │ │ │ │ git stash ┌─────────┐ │ │ └────────────►│ Stash │ (另一個臨時容器) │ │ └─────────┘ │ │ │ │ Agent 操作:git add -A && git commit -m "..." │ │ = 2 次工具呼叫,純儀式,零決策價值 │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ jj 的狀態模型:1 種容器,自動快照 │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ @ (working copy commit) │ │ │ │ ← 編輯檔案自動快照為這個 commit 的更新 │ │ │ └──────────────────────────────────────────┘ │ │ │ │ Agent 操作:直接編輯檔案,完成。 │ │ = 0 次額外工具呼叫 │ └─────────────────────────────────────────────────────────────┘
對 Agent 來說這意味著:
工具呼叫從 2 步變成 0 步。 Git 的 git add -A && git commit -m "..." 在 jj 中不需要。修改檔案就是「提交」。Agent 只需要在想表達語意邊界時執行 jj new(開始一個新 change)和 jj describe(描述 change 的意圖)。
永遠不會出現阻塞性錯誤。 "Your local changes to the following files would be overwritten by merge"這種 Git 錯誤在 jj 中不存在,因為沒有「未提交的修改」這個概念。
示例
# jj:編輯檔案後 vim src/auth.rs # 修改檔案——完了。 # jj 自動把當前工作副本快照為 @ commit 的更新 # 不需要 add,不需要 commit jj describe -m "實作登入" # (可選)給這個 change 加個描述
關鍵機制:jj 執行任何命令時(jj status、jj log、jj new 等),都會先自動快照工作副本的所有變更到當前 @ commit 中。所以你的「未儲存修改」實際上每時每刻都是被儲存的。
Git 的心智模型: 檔案修改 ──是一種──→ "未追蹤的髒狀態"(危險,可能遺失) jj 的心智模型: 檔案修改 ──是一種──→ "@ commit 的最新內容"(安全,已快照)
jj 沒有 staging area,但它用 split 來做同樣的事,而且更靈活:
# jj:此時 @ commit 已經包含了所有 3 個檔案的修改(自動快照的) jj split # 互動式地把當前 @ commit 拆分成兩個 commit # 介面讓你選擇哪些檔案/哪些 hunk 進入第一個 commit # 剩餘的自動成為第二個 commit
Git 的思路: 修改 → 挑選哪些 "進入" staging → commit → 剩餘留在 working dir (先選後存) jj 的思路: 修改 → 全部自動進入 @ commit → split 拆分出你想要的部分 (先存後拆) 區別:jj 的方式永遠不會 "丟東西"——所有修改始終在某個 commit 中。 Git 的方式中 working directory 的修改可能因為誤操作遺失。
所以,jj 的心智模型比 git 更加簡單。
核心思想轉變是:Git 要求你在做修改之前決定「這個修改去哪」(working dir? stage? stash?),jj 讓你先做修改,之後再組織。 所有修改永遠安全地存在於某個 commit 中,你可以事後拆分(split)、移動(squash/move)、描述(describe)。這種「先存後整理」的模型天然消除了資料遺失的風險。
對 Agent 來說,這意味著 Agent 只需要知道兩個操作:jj new(開始一個新的邏輯變更)和 jj describe(給它寫描述)。其他一切,諸如儲存、暫存、臨時儲存,版本控制系統自動處理了。
解決方案二:Change ID,穩定的邏輯標識
jj 區分了兩個層次的標識:
Revision:一個不可變的快照(類似 Git 的 commit hash),每次修改內容都產生新的 revision Change:一個穩定的邏輯標識(change ID),無論你怎麼修改一個 change 的內容,其 change ID 不變
┌─────────────────────────────────────────────────────────────┐ │ Git:amend 打斷引用鏈 │ │ │ │ commit abc123 ──"實作登入" │ │ │ │ │ ▼ (agent amend 修改了內容) │ │ commit def456 ──"實作登入 (修復)" ← 新 hash! │ │ │ │ 問題:CI、PR、編排器中引用 abc123 的地方全部失效 │ │ 編排器:"我追蹤的 abc123 去哪了??" │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ jj:Change ID 跨修改保持穩定 │ │ │ │ change kkmpptqz ──"實作登入" │ │ (revision: abc123) │ │ │ │ │ ▼ (agent 修改了內容) │ │ change kkmpptqz ──"實作登入 (修復)" ← 同一個 change ID! │ │ (revision: def456) ← revision 變了,但 change ID 沒變 │ │ │ │ 編排器始終用 kkmpptqz 追蹤,不受 amend 影響 │ └─────────────────────────────────────────────────────────────┘
這對編排系統至關重要。 像 Symphony 這樣的 Agent 編排系統需要追蹤「這個 issue 對應的程式碼變更」。用 Git 的 commit hash,Agent 每次 amend 都會打斷這種關聯。用 jj 的 change ID,無論 Agent 修改多少次,編排器都能透過 change ID 穩定地追蹤。
解決方案三:衝突是一等物件,不阻塞,不中斷
這是 jj 從 Darcs/Pijul 借鑑的最深層創新。
Git 中衝突是一種阻塞狀態。你被卡住了,必須先解決衝突才能繼續。jj 中衝突是資料,一個 commit 可以「含有衝突」,這和它含有一個函式或一個檔案沒有本質區別。
技術實現上,jj 不儲存文字衝突標記(<<<<<<<),而是維護衝突樹的邏輯表示:一系列 diff 的組合。這意味著 rebase 一個含衝突的 commit 不會產生「嵌套衝突標記」的噩夢。
┌─────────────────────────────────────────────────────────────┐ │ Git:衝突阻塞流水線 │ │ │ │ Agent 執行 git rebase main │ │ │ │ │ ▼ │ │ commit 1 ── rebase 成功 ✓ │ │ │ │ │ ▼ │ │ commit 2 ── 衝突!✗ ──── 流水線卡住 │ │ │ 需要互動式解決 │ │ × git rebase --continue │ │ commit 3 ── 無法繼續 Agent 可能不理解衝突上下文 │ │ commit 4 ── 無法繼續 │ │ │ │ 狀態:REBASE IN PROGRESS(中間狀態,危險) │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ jj:衝突是資料,操作永遠完成 │ │ │ │ Agent 執行 jj rebase -d main │ │ │ │ │ ▼ │ │ change 1 ── rebase 成功 ✓ │ │ │ │ │ ▼ │ │ change 2 ── 有衝突,標記為 conflicted ⚠ ── 但操作繼續! │ │ │ │ │ ▼ │ │ change 3 ── rebase 成功 ✓(衝突解決後自動傳播) │ │ │ │ │ ▼ │ │ change 4 ── rebase 成功 ✓ │ │ │ │ 狀態:操作完成。change 2 標記為 conflicted,可以之後解決。 │ │ 沒有中間狀態,沒有阻塞。 │ └─────────────────────────────────────────────────────────────┘
更重要的設計含義:
rebase 永遠「成功」。 沒有 git rebase --continue,沒有 "in progress" 狀態。操作是原子的——它要麼完成(可能產生含衝突的 commit),要麼因為其他原因失敗。
衝突解決可以傳播。 如果你在某個 change 中解決了衝突,jj 會自動把解決方案傳播到所有後代 change。這類似 Git 的 rerere(record/replay resolve),但它是預設行為且更可靠。
衝突可以延後處理。 Agent 完成工作後,編排器可以檢查是否有含衝突的 change,然後決定是讓 Agent 解決、啟動一個專門的衝突解決 Agent、還是升級給人類。衝突不再是「流水線卡住了」,而是「產出中有一個需要額外處理的標記」。
解決方案四:Operation Log,完整的操作審計
Git 有 reflog,但它只記錄 branch tip 的移動,格式晦澀,主要用於災難恢復。
jj 的 operation log 記錄你對倉庫執行的每一個操作。每條記錄是一個原子操作,即使一個 rebase 涉及 10 個 commit,它也是 operation log 中的一條記錄。
┌─────────────────────────────────────────────────────────────┐
│ Git reflog:粗粒度,只記錄 branch tip 移動 │
│ │
│ abc123 HEAD@{0}: commit: fix login │
│ def456 HEAD@{1}: rebase: updating HEAD │
│ 789abc HEAD@{2}: checkout: moving from main to feature │
│ │
│ 問題:一次涉及 10 個 commit 的 rebase 在 reflog 中 │
│ 顯示為 10 條分散的記錄,很難原子回退 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ jj operation log:原子級,每個操作一條記錄 │
│ │
│ @ kmlprxqv rebase 4 commits onto main │
│ ○ zxsewqpw describe change kkmpptqz │
│ ○ tnrqpaxy new empty change │
│ ○ vqrpmzxn fetch from origin │
│ │
│ 撤銷整個 4-commit rebase:jj op restore zxsewqpw │
│ 一條命令,原子回退。 │
└─────────────────────────────────────────────────────────────┘# 查看操作歷史 jj op log # 撤銷最近一次操作(原子的) jj undo # 回退到任意歷史操作的狀態 jj op restore <operation-id>
對 Agent 來說,這是「時間機器」。 Agent 做了一個災難性操作?不需要從 reflog 中人肉找到正確狀態,一個 jj op restore 精確回到之前的時刻。編排系統可以在 Agent 每次操作前記錄 operation ID,失敗時一鍵回滾。
解決方案五:自動 Rebase,多 Agent 並發的基礎
當你修改一個 commit,jj 自動把所有後代 rebase 到新版本上。配合衝突是一等物件的設計,這個自動 rebase 即使產生衝突也不會阻塞。
┌─────────────────────────────────────────────────────────────┐ │ 場景:修改歷史中的 commit B(B → B') │ │ │ │ 修改前: A ← B ← C ← D │ │ │ │ ─── Git ─── │ │ 修改 B 後:A ← B' (手動 amend) │ │ A ← B ← C ← D (C, D 還指向舊 B!) │ │ 你需要: git rebase --onto B' B D │ │ 如果 C 衝突:rebase 卡住,手動解決 │ │ │ │ ─── jj ─── │ │ 修改 B 後:A ← B' ← C' ← D' (自動 rebase,一步完成) │ │ 如果 C 衝突:C' 標記為 conflicted,但 D' 仍然生成 │ │ 無需手動操作,無中間狀態 │ └─────────────────────────────────────────────────────────────┘
這是 jj 最核心也最反直覺的設計。
Git 在產生衝突時會產生嵌套衝突標記——<<<<<<< 裡面套 <<<<<<<,基本上不可讀。我相信這是每个人類開發者最頭疼的地方吧。
jj 對衝突的理解完全不同。它不在檔案裡插入文字標記,而是在資料模型層面記錄「這個檔案有多個版本,她們之間有分歧」。
內部表示是一個衝突樹(conflict tree),不是文字,而是結構化資料。
jj 內部對一個衝突檔案的表示(概念模型,不是實際格式):
file: src/auth.rs
conflict:
base: "fn login(user: &str, pass: &str) -> bool { ... }" # 共同祖先
side_1: "fn login(user: &str, pass: &str) -> bool { # 一方的修改
validate_credentials(user, pass)
}"
side_2: "fn login(user: &str, pass: &str) -> Result<...> { # 另一方的修改
let valid = check_password(user, pass)?;
}"當你 checkout 或查看這個檔案時,jj 渲染成類似 Git 的衝突標記格式給你看。但這只是展示層,底層儲存的是結構化的三方資料,不是文字標記。
儲存層:結構化的衝突樹(base + side_1 + side_2)
│
▼ (當你打開檔案時,渲染為人類可讀格式)
展示層:類似 Git 的 <<<<<<< 標記(但這只是視圖,不是資料)這個區別至關重要。因為結構化資料可以被程式精確操作,而文字標記只能被人類閱讀。
自動 Rebase 時衝突怎麼處理
情況一:rebase 沒有衝突
最簡單的情況。兩個 change 修改了不同的檔案或同一檔案的不同區域:
修改前:
main: A ← B ← C
↑
你修改了 B 變成 B'(比如改了 src/auth.rs)
自動 rebase:
C 依賴 B。B 變成了 B'。jj 自動把 C rebase 到 B' 上。
結果:A ← B' ← C'
如果 C 修改的是 src/config.rs(和 B' 修改的 src/auth.rs 沒有重疊)
→ C' 乾淨地套用,沒有衝突。自動完成,你什麼都不需要做。情況二:rebase 產生衝突,jj 怎麼「成功」
關鍵來了。假設 B 和 C 都修改了同一個檔案的同一個區域:
修改前:
A ← B ← C ← D
↑
你修改了 B 變成 B'(改了 src/auth.rs 第 10-20 行)
但 C 也修改了 src/auth.rs 第 10-20 行
Git 的行為:
git rebase --onto B' B D
→ 處理 C 時發現衝突
→ 停!在 src/auth.rs 中插入 <<<<<<< 標記
→ 進入 "rebase in progress" 狀態
→ 等你解決衝突並執行 git rebase --continue
→ D 還沒被處理,掛起
jj 的行為:
jj 修改 B → B',自動 rebase C 和 D
→ 處理 C 時發現衝突
→ 不停!建立 C',其中 src/auth.rs 被記錄為 "衝突狀態":
conflict:
base: B 版本的 src/auth.rs 第 10-20 行
side_1: B' 版本的第 10-20 行(你的修改)
side_2: C 版本的第 10-20 行(C 的修改)
→ C' 是一個 "含有衝突的 commit"——但它是一個合法的 commit!
→ 繼續處理 D → D' rebase 到 C' 上
→ 如果 D 沒碰 src/auth.rs 第 10-20 行,D' 是乾淨的
→ 如果 D 也碰了,D' 也被標記為含有衝突
→ 操作完成。整個 rebase 鏈沒有中斷。┌──────────────────────────────────────────────────────────────┐ │ Git rebase 遇到衝突 │ │ │ │ A ← B' ← C ← D │ │ ↑ │ │ 衝突!停在這裡 │ │ 狀態:REBASE IN PROGRESS │ │ C 變成了一個檔案裡插著 <<<<<<< 標記的半成品 │ │ D 還沒被處理 │ │ 你必須:解決衝突 → git add → git rebase --continue │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ jj rebase 遇到衝突 │ │ │ │ A ← B' ← C' ← D' │ │ ⚠ ✓ │ │ 衝突被記錄 D' 乾淨(如果它不碰衝突區域) │ │ 在 C' 中 │ │ 但 C' 是一個 │ │ 合法的 commit │ │ │ │ 狀態:操作完成。沒有 "in progress"。 │ │ C' 被標記為 conflicted,你可以之後解決。 │ └──────────────────────────────────────────────────────────────┘
情況三:怎麼解決衝突
當你決定要解決 C' 中的衝突時:
bash
# 查看哪些 changes 有衝突
jj log
# 輸出中會標記:
# ⚠ kkmpptqz (conflict) "feature: 實作註冊"
# 切換到衝突的 change
jj edit kkmpptqz
# 此時你打開 src/auth.rs,看到的是渲染出來的衝突標記:
# <<<<<<< Side #1 (你的修改 B')
# fn login(...) -> bool {
# validate_credentials(user, pass)
# ||||||| Base (共同祖先 B)
# fn login(...) -> bool {
# check(user, pass)
# ======= Side #2 (C 的修改)
# fn login(...) -> Result<...> {
# let valid = check_password(user, pass)?;
# >>>>>>>
# 你(或 Agent)編輯檔案,刪除標記,寫出正確的合併結果
# 儲存檔案
# jj 自動檢測到衝突標記消失了 → 這個 change 不再是 conflicted
# 而且——關鍵——D' 會被自動 rebase 到解決了衝突的 C' 上
# 如果 D' 之前因為 C' 的衝突而連帶 conflicted,
# 現在衝突解決後 D' 可能也自動變乾淨了情況四:衝突解決的傳播,這才是殺手級特性
解決衝突前:
A ← B' ← C'(⚠衝突) ← D'(⚠連帶衝突) ← E'(⚠連帶衝突)
解決 C' 的衝突後:A ← B' ← C''(✓乾淨) ← D''(?) ← E''(?)
jj 自動把衝突解決傳播到 D'' 和 E''。 如果 D 和 E 的衝突完全是由 C 的衝突引起的, D'' 和 E'' 自動變乾淨,不需要你逐個解決。 Git 中你需要對 C, D, E 每一個都手動解決衝突。 jj 中你只需要解決 C 的衝突,D 和 E 可能自動修復。 這就像 Git 的 `rerere`(record/replay resolved conflicts),但 jj 的實現更根本——它不是 "記住你之前解決過類似的衝突然後重放",而是 "衝突解決本身就是一個可以被 rebase 傳播的 diff"。
為什麼「衝突是資料」如此重要
把衝突從「狀態」變成「資料」的本質影響:
┌──────────────────────────────────────────────────────────────┐ │ "衝突是狀態"(Git) │ │ │ │ • 衝突阻塞操作——你必須先解決 │ │ • 衝突不能被 commit——只能存在於 working directory │ │ • 衝突不能被傳遞——每次 rebase 都可能重新產生 │ │ • 衝突讓倉庫進入 "中間狀態"——危險,需要清理 │ │ • 解決衝突的時間視窗:rebase 進行中(有壓力,不能做別的) │ │ │ │ 對 Agent 的影響: │ │ Agent 遇到衝突 → 流水線卡住 → 需要編排器介入 │ │ → 但編排器可能沒有解決衝突的上下文 → 升級給人類 │ │ → 人類需要理解 Agent 在做什麼 → 高成本 │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ "衝突是資料"(jj) │ │ │ │ • 衝突不阻塞操作——先完成 rebase,衝突標記在 commit 中 │ │ • 衝突可以被 commit——它就是 commit 的一部分 │ │ • 衝突解決可以傳播——解決一處,後代自動修復 │ │ • 倉庫永遠處於 "乾淨狀態"——只是某些 commit 含有衝突標記 │ │ • 解決衝突的時間視窗:任何時候(沒有壓力,可以做別的事先) │ │ │ │ 對 Agent 的影響: │ │ Agent 遇到衝突 → 操作完成,衝突被標記 → 流水線繼續 │ │ → 編排器看到 "change X is conflicted" → 決策: │ │ a) 啟動一個衝突解決 Agent(給它衝突的三方資料) │ │ b) 暫時跳過,先處理其他不衝突的任務 │ │ c) 升級給人類(但不緊急,因為沒東西被阻塞) │ │ → 什麼時候解決都行,不影響其他 Agent 的工作 │ └──────────────────────────────────────────────────────────────┘
Agent 解決衝突的可能方式
因為 jj 的衝突是結構化資料(base + side_1 + side_2),Agent 可以獲得比 Git 更豐富的資訊來解決衝突:
# Agent 看到的資訊 # Git:一個檔案裡混著 <<<<<<< 標記的文字 # Agent 需要從文字中解析出 "哪是 ours,哪是 theirs,哪是 base" # 容易解析錯,尤其是嵌套衝突 # jj:結構化的三方資料 # base: 原始版本(共同祖先) # side_1: 第一方修改 # side_2: 第二方修改 # Agent 可以精確獲取三方各自的完整內容 # 一個合理的 Agent 衝突解決策略: # 1. 提取 base, side_1, side_2 的完整內容 # 2. 理解 side_1 的意圖(從 change description 或關聯的 Contract) # 3. 理解 side_2 的意圖(同上) # 4. 生成合併結果 # 5. 驗證合併結果滿足兩方的 Contract
如果我們進一步把 Contract (某種 Spec 的約束) 和 change 綁定,衝突解決就變得更有意義了:
change kkmpptqz (side_1) → contract-042 "實作用戶註冊"
change rrsnntyz (side_2) → contract-057 "修復登入驗證"
衝突發生在 src/auth.rs
衝突解決 Agent 可以獲取兩個 Contract 的意圖:
- contract-042 要求:註冊時用 bcrypt-12 哈希密碼
- contract-057 要求:登入時驗證密碼並返回 JWT
→ Agent 有足夠的語意資訊來判斷如何合併:
兩者修改了不同的功能(註冊 vs 登入),
合併結果應該同時包含註冊邏輯和修復後的登入邏輯。這就是衝突是資料 + Spec-Change 綁定的協同效應,衝突不再是「兩段不知為何打架的程式碼」,而是「兩個已知意圖的變更在同一區域的重疊」,有了意圖資訊,解決方案就有了判斷依據。
解決方案六:workspace,多 Agent 並發的優勢
Git worktree 是多 Agent 並發工作的一個重要場景。jj 也有自己的方案,而且比 Git worktree 更乾淨。
Git worktree 的核心場景:你想同時在同一個倉庫的不同分支上工作,但不想來回 stash/checkout。
jj 對應的概念叫 workspace。但它的模型比 Git worktree 更簡潔,因為 jj 沒有 index/staging area,每個 workspace 的狀態就是一個 @ commit,沒有別的東西。
# jj:建立多個 workspace jj workspace add ../feature-login jj workspace add ../bugfix-auth # 現在你有三個目錄: # ./repo ← 預設 workspace,@ = 某個 change # ../feature-login ← workspace "feature-login",@ = 另一個 change # ../bugfix-auth ← workspace "bugfix-auth",@ = 又一個 change # 她們共享同一個 jj 倉庫資料 # 每個 workspace 有自己獨立的 @ (working copy commit) # 查看所有 workspace jj workspace list # 輸出: # default: kkmpptqz (當前 change) # feature-login: rrsnntyz (另一個 change) # bugfix-auth: ppmmlkqz (又一個 change) # 關鍵區別 ┌──────────────────────────────────────────────────────────────┐ │ Git Worktree │ │ │ │ 每個 worktree 有: │ │ ├── 獨立的 working directory (檔案) │ │ ├── 獨立的 index / staging area (暫存狀態) │ │ ├── 獨立的 HEAD (指向哪個 commit/branch) │ │ └── 共享的 .git/objects (物件庫) │ │ │ │ 限制: │ │ • 每個 worktree 必須指向不同的 branch │ │ (不能兩個 worktree checkout 同一個 branch) │ │ • 每個 worktree 有自己的 index,狀態複雜度 ×N │ │ • 如果一個 worktree 處於 "rebase in progress", │ │ 它會鎖住相關引用,影響其他 worktree │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ jj Workspace │ │ │ │ 每個 workspace 有: │ │ ├── 獨立的 working directory (檔案) │ │ ├── 獨立的 @ (working copy commit) │ │ └── 共享的 repo 資料 (所有 changes + operation log) │ │ │ │ 沒有: │ │ • 沒有 index(不需要) │ │ • 沒有 branch 指向限制(workspace 指向 change,不是 branch) │ │ • 沒有 "in progress" 狀態(衝突是資料,不阻塞) │ └──────────────────────────────────────────────────────────────┘ jj workspace 的每個例項只有 **一層狀態**: 每個 jj Workspace 的狀態: @ commit(當前 working copy change) 沒有 staging area,沒有 "in progress" 狀態。 編排器只需要知道 "這個 workspace 的 @ 指向哪個 change"。
這就是「一切皆 commit」在 workspace 場景下的體現:每個 workspace 的完整狀態就是一個 change ID。編排器追蹤 N 個 Agent 的狀態,就是追蹤 N 個 change ID,而不是 N × (working dir + index + HEAD + 可能的中間狀態) 的笛卡爾積。
對 Agent 編排的意義
┌──────────────────────────────────────────────────────────────┐ │ 編排器管理 10 個 Agent workspace 的複雜度 │ │ │ │ Git Worktree: │ │ • 10 個 branch 要命名和管理 │ │ • 10 個 staging area 可能處於不一致狀態 │ │ • 任何一個可能卡在 "rebase in progress" │ │ • merge 衝突阻塞流水線 │ │ • 崩潰恢復需要判斷每個 worktree 處於什麼狀態 │ │ 狀態空間:O(N × M) N=worktree 數,M=可能的中間狀態數 │ │ │ │ jj Workspace: │ │ • 10 個匿名 change,不需要命名 │ │ • 沒有 staging area │ │ • 沒有 "in progress" 狀態 │ │ • 衝突標記但不阻塞 │ │ • 崩潰恢復:jj op restore 或直接繼續(修改都在 @ 中) │ │ 狀態空間:O(N) 每個 workspace 就是一個 change ID │ └──────────────────────────────────────────────────────────────┘
所以 jj workspace 不只是「Git worktree 的替代品」,它透過消除 staging area 和中間狀態,把每個工作空間的狀態複雜度從 多維 壓縮到了 一維(一個 change ID)。對於需要管理多個並行 Agent 的編排系統來說,這是數量級的簡化。
第三部分:當 Spec 遇到版本控制,Spec-Change 綁定
回顧:Spec 在代理軟體工程中的角色
在本系列的前一篇文章中,我們建立了代理軟體工程的核心框架:Spec 是控制面,Code 是資料面,Agent 是執行面。 人類的核心工作是編寫 Spec(規格說明),Agent 負責將 Spec 轉化為程式碼實現,驗證系統確認程式碼滿足 Spec。
在這個框架中,我們把給 Agent 執行的任務級 Spec 稱為 Task Contract(任務合約)。一個結構化的文件,包含四個核心要素:
意圖(Intent):做什麼,以及為什麼 已定決策(Decisions):已經確定的技術選擇,消除 Agent 的決策空間 邊界(Boundaries):允許修改哪些檔案、禁止做哪些事 完成條件(Completion Criteria):BDD 格式的驗收標準,確定性的通過/失敗檢查
Contract 的概念來源於我們對 Code Review 危機的分析:當 AI 程式碼生產速度遠超人類審查能力時,解決方案不是「更快地審查程式碼」,而是把人類的審查點上移到意圖層——人類審查 Spec/Contract 是否正確定義了意圖和驗收標準,機器驗證程式碼是否滿足 Contract。
當前的斷裂:Spec 和版本控制是兩個世界
在今天的工具鏈中,Spec(無論是 GitHub Issue、Jira Ticket 還是我們定義的 Contract)和版本控制系統之間的關聯是脆弱的、約定式的:
┌─────────────────────────────────────────────────────────────┐ │ 當前的鬆散關聯 │ │ │ │ Spec/Contract │ Version Control │ │ (外部系統) │ (Git/jj) │ │ │ │ │ "實作用戶註冊" ·····靠 commit message 中的····→ abc123 │ │ Issue #42 "fixes #42" 字串關聯 commit │ │ │ │ 問題: │ │ 1. 關聯靠約定(忘了寫 #42 就斷了) │ │ 2. commit hash 不穩定(amend 就斷了) │ │ 3. 一個 Spec 對應多少 commits?不知道 │ │ 4. 一個 commit 滿足了 Spec 的哪些驗收標準?不知道 │ │ 5. Spec 變更和 Code 變更沒有關聯的版本歷史 │ └─────────────────────────────────────────────────────────────┘
這種斷裂在人類手動開發時可以接受——人類在心裡維護著「我正在為 Issue #42 寫程式碼」的關聯。但在 Agent 編排系統中,這種關聯必須是形式化的、可查詢的、不會因為 amend 而斷裂的。
Spec-Change 綁定:jj 提供的機會
jj 的兩個特性為解決這個問題提供了基礎:
Stable Change ID 解決了 "amend 打斷關聯" 的問題。無論 Agent 修改多少次,change ID 不變,編排系統可以始終用 change ID 追蹤「這個 Contract 對應的程式碼變更」。
Git 外部的元資料儲存 解決了 "關聯靠約定" 的問題。jj 已經在 Git 倉庫外儲存了 bookmarks 等元資料。Contract 綁定可以作为另一種元資料,成為版本控制的一等公民。
┌─────────────────────────────────────────────────────────────┐ │ 理想的 Spec-Change 綁定 │ │ │ │ Contract "用戶註冊" │ │ ├── intent: "實作註冊 API endpoint" │ │ ├── decisions: [bcrypt-12, POST /api/v1/auth/register] │ │ ├── boundaries: [ONLY src/auth/**, DO NOT add deps] │ │ ├── criteria: [註冊成功→201, 重複信箱→409, ...] │ │ │ │ │ ├── bound_changes: ← 形式化的綁定,不是 commit message │ │ │ ├── kkmpptqz "實作註冊 endpoint" │ │ │ ├── rrsnntyz "新增註冊測試" │ │ │ └── ppmmlkqz "修復信箱校驗" │ │ │ │ │ └── verification: ← 驗證狀態關聯到 Contract 級別 │ │ ├── kkmpptqz: L1=PASS, L2=PASS, L3=PASS │ │ ├── rrsnntyz: L1=PASS │ │ └── aggregate: all_criteria_met = true │ │ │ │ 可查詢: │ │ "Contract 042 的所有 changes" → [kkmpptqz, rrsnntyz, ...] │ │ "change kkmpptqz 屬於哪個 Contract" → Contract 042 │ │ "Contract 042 的驗收標準是否全部通過" → true │ └─────────────────────────────────────────────────────────────┘
這個模型讓 Spec 和版本控制從「兩個鬆散關聯的世界」變成了「一個統一的追溯鏈」:
人類定義 Spec/Contract
│
▼
Agent 生成 Changes(關聯到 Contract)
│
▼
驗證系統檢查 Changes 是否滿足 Contract 的 Criteria
│
▼
Contract Acceptance(人類審查 Contract 定義是否正確 + 驗證是否全部通過)Spec 層級與版本控制的對應
在我們的 Spec 設想中,Spec 分為三層:
L0 組織級 Spec(安全策略、架構原則),變化極慢 L1 專案級 Spec(技術棧、模組邊界、API 契約),變化緩慢 L2 任務級 Spec / Contract(具體任務的意圖、約束、驗收標準),每任務一個
這三層和版本控制的對應關係是:
┌─────────────────────────────────────────────────────────────┐ │ Spec 層級與版本控制的對映 │ │ │ │ L0 組織級 Spec ──→ 規則檔案(rules/security-baseline.md) │ │ (安全策略等) 版本化在倉庫中,極少變更 │ │ Agent 透過規則路由按需載入 │ │ │ │ L1 專案級 Spec ──→ 專案配置檔案(specs/project.yaml) │ │ (技術棧、契約) 版本化在倉庫中,Sprint 級別變更 │ │ Agent 啟動時載入 │ │ │ │ L2 Task Contract → 與 jj changes 形式化綁定 │ │ (任務意圖/驗收) Contract 變更觸發 changes 重新生成 │ │ 驗證結果關聯到 Contract 級別 │ └─────────────────────────────────────────────────────────────┘
關鍵洞察:L0 和 L1 的 Spec 本身就應該版本化在程式碼倉庫中。她們是程式碼的「憲法」,變更 L0/L1 Spec 應該像修憲一樣慎重,並且有完整的變更歷史。L2 Contract 則是「工單」級別的,它的生命週期和對應的 changes 一起開始、一起結束。
第四部分:Agent-Native VCS 還缺什麼
jj 解決了 Git 在 Agent 場景下的大部分摩擦點。但從代理軟體工程的完整框架來看,還有幾個維度是當前 jj 尚未覆蓋的。
原語一:驗證狀態作為 Change 屬性
當前的版本控制系統不知道一個 change 是否通過了測試。CI/CD 是外部系統,驗證結果存在 GitHub Actions 或 Jenkins 裡,不在版本控制系統中。
在 Agent-Native VCS 中,驗證狀態應該是 change 的內在屬性:
Change kkmpptqz:
revision: abc123
description: "實作註冊 endpoint"
contract: contract-042
verification:
L1_type_check: PASS (2s)
L2_contract: PASS (5s)
L3_bdd: PASS (30s)
L4_adversarial: PENDING
status: verified_to_L3這讓「這個變更加以合併嗎?」成為版本控制系統可以直接回答的問題,不需要去查外部 CI 系統。合併的門控從「人類點 approve」變成「驗證引擎確認所有層通過」。
原語二:Agent 身份與許可權
Git 的 author/committer 欄位用 name + email 標識。在多 Agent 並發工作的場景下,我們需要更豐富的身份資訊:
Change kkmpptqz: agent: claude-code-session-7a3b role: implementer contract: contract-042 permissions: workspace-write, files=[src/auth/**] orchestrator: symphony-run-019
這讓審計系統可以回答:「這段程式碼是哪個 Agent 在什麼許可權下生成的?」在合規場景中,這比 "Author: bot@ci.example.com" 有價值得多。
原語三:快照時間線而非 DAG
Git 的 commit DAG(有向無環圖)是為人類理解 branch/merge 歷史設計的。但當 10 個 Agent 並行工作時,DAG 變成一團不可讀的義大利麵條。
Agent-Native 的歷史可視化應該是按時間線排列的快照序列,可以按 Contract、按 Agent、按模組過濾。想象一個時間軸,每個 Agent 是一條泳道,Contract 是顏色編碼,你可以 scrub 到任何時刻看到程式碼庫在那個瞬間的狀態。
jj 的 operation log 已經提供了部分基礎。它按時間記錄所有操作。缺的是和 Contract/Agent 身份的關聯,以及可視化層。
第五部分:從 Code Review 到 Contract Acceptance
版本控制的變革不僅僅是工具層面的,它連帶著改變了整個協作模型。
傳統模型:Branch → PR → Code Review → Merge
GitHub 圍繞 Pull Request 構建了整個協作流程。開發者建立 branch,推送程式碼,開 PR,同事 review 程式碼 diff,approve 後合併。
這個流程在 Agent 時代面臨三個根本問題:
1. Review 的物件錯了。 在我們之前的《重新思考 Code Review》文章中詳細分析過,當 AI 以機器速度生成程式碼時,人類逐行審查程式碼 diff 是不可擴充套件的。Faros AI 的資料顯示 PR 合併率暴漲 97.8% 而 Review 時間暴漲 91.1%。
2. Branch 的粒度錯了。 一個 Agent 可能在幾分鐘內完成一個 Contract,建立 branch → push → 開 PR → 等 review → merge 的流程比實際工作時間還長。
3. Merge 的門控錯了。 PR 的 approve 按鈕是一個人類判斷,「這段程式碼看起來對」。但驗證應該是確定性的,「這段程式碼通過了所有驗證層」。
Agent-Native 模型:Contract → Changes → Verification → Acceptance
┌──────────────────────────────────────────────────────────────┐ │ 傳統 GitHub 流程 │ │ │ │ Issue ──→ Branch ──→ Code ──→ PR ──→ Code Review ──→ Merge │ │ (模糊) (命名) (手寫) (開 PR) (人讀 diff) (approve) │ │ │ │ 人類參與:全流程 │ │ 門控:人類主觀判斷 │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ Agent-Native 流程 │ │ │ │ Contract → Changes → Verification → Acceptance │ │ (結構化任務契約) (匿名 jj) (確定性驗證) (Contract 級) │ │ │ │ 人類參與:Contract 定義 + 最終驗收 │ │ 門控:驗證引擎確認所有 Criteria 通過 │ └──────────────────────────────────────────────────────────────┘
在這個模型中:
Contract 取代 Issue + PR description。 Contract 是結構化的、包含驗收標準的、可自動化驗證的。它不是「給人看的任務備忘錄」,而是「給 Agent 消費的可執行規格 + 給驗證系統執行的檢查清單」。
Changes 取代 Branch。 jj 的匿名 changes + stable change ID 意味著不需要 branch 名稱,編排系統透過 change ID 追蹤。
Verification Pipeline 取代 Code Review。 從型別檢查到合約驗證到 BDD 測試到對抗性 Agent 審查,多層確定性驗證提供了比人類 Code Review 更全面、更快速、更可擴充套件的品質保證。
Contract Acceptance 取代 PR Approve。 人類審查的不是程式碼 diff,而是:「這個 Contract 的驗收標準是否明確定義?驗證結果是否表明 Contract 被正確滿足?」
第六部分:實踐指南 - 如何在現有工具鏈中漸進採用
理想很美好,但大多數團隊不會一夜之間拋棄 Git 和 GitHub。以下是一個漸進式的採用路徑。
Level 0:在 Git 倉庫中 colocate jj(零風險)
jj 支持和 Git 倉庫共存(colocate 模式),這意味著你可以在任何現有 Git 倉庫中開始使用 jj,不影響團隊中其他人繼續使用 Git。
# 在現有 Git 倉庫中初始化 jj(兩者共存) cd your-repo jj git init --colocate # 現在可以同時使用 jj 和 git # jj 操作會同步到 Git,git 操作也對 jj 可見
對 Agent 的即時價值:
jj op log提供了 Agent 操作的完整審計自動快照意味著 Agent 不會「遺失」未提交的修改 jj op restore在 Agent 犯錯時精確回退
Level 1:Agent 工作空間使用 jj
在 Claude Code / Codex CLI 的工作空間中,用 jj 替代 git 作為 Agent 的版本控制工具:
# Agent 的工具集簡化 # 不需要:git add, git stash, git rebase --continue # 只需要:jj new, jj describe, jj bookmark set, jj git push
關鍵收益: Agent 的版本控制工具呼叫量減少 30-50%(消除了 git add、git stash、git rebase --continue 等儀式性操作),每次節省的工具呼叫都是更多可用於實際工作的上下文預算。
Level 2:用 Change ID 追蹤任務
在 Agent 編排系統中,用 jj 的 change ID 替代 Git 的 commit hash 來追蹤變更。change ID 跨 amend 穩定,編排器無需在 Agent 每次修改後重新綁定引用。
Level 3:利用衝突即資料的特性
在多 Agent 並發場景中,不再把衝突視為「錯誤」,而是視為「需要額外處理的狀態」:Agent 完成後檢查是否有衝突標記的 changes。簡單衝突自動解決,複雜衝突升級給人類。流水線永遠不會被「卡住」。
第七部分:對比總覽
Git vs jj vs 理想 Agent-Native VCS
關鍵結論:jj 已經解決了 80% 的 Agent-VCS 摩擦。透過「一切皆 commit」、change ID、衝突即資料、自動 rebase。剩下的 20% 是需要和代理軟體工程上層(Spec/Contract、驗證管道、Agent 編排)聯動的部分。
結語:版本控制反映了它服務的工作方式
回顧版本控制的歷史,每一代系統都反映了它所處時代的工作方式:
CVS/SVN 時代:中央伺服器,順序提交。反映了「一個團隊、一個程式碼庫、一個整合經理」的瀑布式開發。
Git 時代:分散式,分支廉價。反映了「分散式團隊、並行開發、Pull Request 協作」的敏捷開發。
Agent 時代:需要什麼?自動快照、穩定標識、衝突即資料、原子操作、完整審計。反映了「多 Agent 並發、Spec/Contract 驅動、確定性驗證、機器速度」的代理軟體工程。
Git 不是「錯」了。它為人類開發者完美服務了二十年。但它的核心假設——操作者理解上下文、操作是順序的、變更需要顯式暫存、衝突必須立即解決,在 Agent 時代成了摩擦力而不是功能。
Jujutsu 提供了一条務實的演進路徑:保持 Git 相容性(不破壞現有生態),同時引入 Agent-Native 的資料模型和操作語意。 它不需要你拋棄 GitHub,不需要團隊全員切換,不需要重建 CI/CD,但它給了 Agent 一個安全的、確定性的、不會被「卡住」的版本控制介面。
而當我們把版本控制和 Spec/Contract 系統聯動起來。透過 Change ID 實現 Contract-Change 綁定、透過驗證狀態整合實現自動化門控、透過 Agent 身份實現完整審計,版本控制就從一個孤立的「程式碼儲存層」升級為代理軟體工程的核心基礎設施。
Spec 是控制面,Code 是資料面,Agent 是執行面,而版本控制是承載資料面的物理層。 當物理層從「為人類設計的鋒利刀具」變成「為 Agent 設計的安全電動工具」時,整個上層系統的設計空間都會被打開。
Jujutsu: https://github.com/jj-vcs/jj
[2]OpenAI 被報導正在構建 GitHub 競品:https://www.reddit.com/r/technology/comments/1rk8bfa/openai_is_developing_alternative_to_microsofts/
[3]Symphony: https://github.com/openai/symphony
[4]JJHub: https://jjhub.tech/
[5]ersc: https://ersc.io/
[6]JJ 教程:https://steveklabnik.github.io/jujutsu-tutorial/introduction/introduction.html
[7]Agentic Workflows: https://github.com/github/gh-aw