本稿は「エージェント・ソフトウェア・エンジニアリング」シリーズの第 3 回です。
第 1 回:エージェント・ソフトウェア・エンジニアリング|もはや人間の物差しで AI の仕事を測るな:エージェントネイティブな作業見積もり
第 2 回:エージェント・ソフトウェア・エンジニアリング #2|コードレビューの再考
Git は人間のために設計された鋭利な刃物だ。エージェントに必要なのは、安全な電動工具である。
序論:バージョン管理の隠れた前提が崩れ去ろうとしている
2019 年末、Google エンジニアの Martin von Zweigbergk 氏が個人プロジェクトとしてバージョン管理システムを立ち上げました。コマンドラインツールの名前は jj(タイピングしやすいから)。
プロジェクト名は Jujutsu。jj と響きが合うだけで、実質的な意味はないようです。それから 6 年、このプロジェクトはスター数 2.5 万を突破し、Google 社内におけるバージョン管理進化の重要な方向性となりました。そして今、その設計思想が、当初は想定されていなかったあるユーザー層によって再発見されようとしています。それが「AI コーディング・エージェント」です。
時はまさに 2026 年 3 月。いくつかの出来事が同時に起きました。
まず、OpenAI が GitHub の競合を構築中であるとの報道がありました。発端は、GitHub の頻繁なサービス低下が、AI エージェントによる継続的インテグレーションに依存するチームに許容しがたい影響を与えたことでした。
さらに OpenAI は、Elixir で実装されたエージェントオーケストレーション層である Symphony をオープンソース化しました。その中核理念は「エンジニアがエージェントを監督するのではなく、仕事を管理する」こと。各 Linear イシューに対して隔離されたワークスペースを作成し、Codex を起動して自律的に実行させ、作業の証明を集約します。
同じ頃、JJHub というスタートアップ企業が、Jujutsu をベースにしたエージェントネイティブな開発プラットフォームの構築を発表しました。その中核となる主張は「AI エージェントにより、小規模なスタートアップでもコミット量において Google と同等に見えるようになる」というものです。
かつての Rust コアメンテナであり、公式 Rust Book の著者(私の Rust 本の序文も書いてくれました)である Steve も、2025 年末に JJ をベースに ersc というスタートアップを立ち上げ、JJ のチュートリアルも執筆しています。
一方 GitHub 自身も 2026 年 1 月初旬、既存の CI/CD フレームワークに AI エージェントを組み込む試みとして Agentic Workflows の技術プレビューを公開しました。
これら全ての動きが指し示している事実は一つです。20 年間ソフトウェアエンジニアリングを支配してきたバージョン管理システム「Git」の中核的な設計上の前提が、AI コーディング・エージェントによって体系的に挑戦されているという事実です。
しかし、Git への挑戦が目的なのではありません。バージョン管理は手段に過ぎず、それが奉仕する根本的な目標は「コード変更の履歴と並行処理をいかに安全に管理するか」ということです。エージェント・ソフトウェア・エンジニアリングの枠組みにおいて、この目標は変わりませんが、その実現方法は根本からの再考を迫られています。
第 1 部:Git が持つ 5 つの隠れた前提
なぜ Git がエージェントの時代に困難に直面するのかを理解するには、その設計上の前提を解剖する必要があります。これらの前提は人間がコーディングする時代には完全に合理的でしたが、エージェントの時代には次々と崩れ去ろうとしています。
前提その 1:操作者が文脈を理解していること
Git の対話設計はすべて、操作者、つまり人間開発者が「自分が何をしているか」を理解していることを前提としています。
git add -p(対話型ステージング)は、どの変更を次のコミットに入れるべきかを行単位で判断できると仮定しています。git rebase -i(対話型リベース)は、コミット間の依存関係と、正しい編集判断ができると仮定しています。git mergeで競合が起きた際、双方の変更のセマンティックな意図を理解し、マージの判断ができると仮定しています。
エージェントは文脈を「理解」しません。統計的推論に基づいて判断を下します。git add -A && git commit -m "..." のような決定論的なコマンド列の実行はできますが、git rebase -i のような対話型インターフェース、つまりテキストエディタ上で pick/squash/edit 行を並べ替えるような操作は、脆弱な作業となり、一歩ごとにエラーの確率を高めてしまいます。
前提その 2:操作は順序的であること
Git の並行モデルは、ある作業ディレクトリには同時に一人の操作者しか存在しないと仮定しています。その証左が git stash の存在です。コンテキストを切り替える際、まず現在の状態を「一時退避」する必要があります。git rebase は競合に遭遇すると「in progress(進行中)」状態に入り、この状態が解決されるまで他の操作ができません。
エージェント時代において、並行処理は日常です。OpenAI の Symphony のようなオーケストレーションシステムは、5〜10 のエージェントを同時に管理し、それぞれが異なるイシューを処理し、同じコードベースで作業する可能性があります。Git のロックファイル機構(.git/index.lock)や「in progress」状態は、こうしたシナリオにおいて深刻な並行処理のボトルネックとなります。
前提その 3:変更は明示的にステージする必要があること
Git のステージングエリア(index)は、人間の認知のために設計された中間層です。その価値は、どの変更を次のコミットに入れるか「選び抜く」ことができる点にあり、よく整理されたコミット履歴を書く上で重要です。
しかし、エージェントに「選び抜き」は不要です。隔離されたワークスペースで明確に定義されたタスクを実行しており、ファイルの変更はすべてそのタスク完了のためのものです。エージェントにとって git add は純粋な儀式的動作であり、100% の確率で git add -A です。この儀式的動作のたびにツール呼び出しのトークン予算を消費し、コンテキストノイズを増やすだけで、意思決定の価値は何も生み出しません。
前提その 4:ブランチには名前が必要なこと
Git のブランチモデルでは、ブランチごとに名前を付ける必要があります。これは人間のワークフローでは意味があります。同僚に「今 feature/user-registration ブランチで作業中だ」と伝える必要があるからです。しかし、エージェントの作業ブランチは一時的で、隔離され、使い捨てのものです。エージェントのセッションごとにブランチ名を付けるのは無意味なオーバーヘッドです。
より深い問題は、Git の branch という概念が「ブランチポインタ」と「コード変更」を結びつけている点にあります。エージェントがコミットを amend(修正)すると、コミットハッシュが変わり、ブランチポインタの更新が必要になります。古いハッシュを参照しているシステムはすべて更新を迫られます。この参照の不安定性が、自動化されたオーケストレーションにおいて大量の調整コストを生んでいます。
前提その 5:競合は即座に解決されなければならないこと
Git はマージ競合に遭遇すると停止し、人間の入力を待ちます。git merge は競合マーカー(<<<<<<<)を出力し、git rebase は「in progress」状態に入ります。どちらも続行する前に競合の解決を要求します。
人間にとっては合理的です。競合とは二人の人間が同じコードに対して異なる理解を持っていることを意味し、正しいマージを決定するには人間の判断が必要だからです。しかし、エージェントオーケストレーションシステムにとって「停止して待つ」ことは破滅的です。10 の並行エージェントを管理するオーケストレーターが、もし一つのエージェントのリベースでも競合でブロックされた場合、介入を余儀なくされます。しかし、その介入自体にコンテキスト(なぜ競合したのかの理解)が必要であり、そのコンテキストはエージェントオーケストレーターの範囲外にあるかもしれません。
第 2 部:Jujutsu がこれらの問題を体系的にどう解決するか
Jujutsu(jj)は「より良い Git CLI」ではなく、バージョン管理のデータモデルそのものの根本的な再設計です。ストレージバックエンドには Git を使用し(互換性維持)、ユーザーモデルは Git とは全く異なります。Rust で実装され、底层には gitoxide ライブラリを使用し、コア開発者全員が jj で jj 自身を開発しています。
解決策 1:すべてをコミットとし、儀式的操作を排除する
jj は Git における 4 種類の「状態コンテナ」(ワーキングディレクトリ、ステージングエリア、スタッシュ、コミット)を、コミットという 1 つに統一しました。
あなたの作業用コピー(@)も一つのコミットです。ファイルを編集すると、jj は自動的にその変更をこの commit の更新としてスナップショットします。ステージングエリアも、スタッシュも、「汚れたワーキングディレクトリ」という概念もありません。
┌─────────────────────────────────────────────────────────────┐
│ Git の状態モデル:4 種類のコンテナ。手動で移動が必要 │
│ │
│ ┌──────────┐ git add ┌─────────┐ git commit ┌────────┐ │
│ │ Working │──────────►│ Staging │────────────►│ Commit │ │
│ │ Directory│ │ Area │ │ │ │
│ └────┬─────┘ └─────────┘ └────────┘ │
│ │ │
│ │ git stash ┌─────────┐ │
│ └────────────►│ Stash │ (別の一時コンテナ) │
│ └─────────┘ │
│ │
│ エージェント操作:git add -A && git commit -m "..." │
│ = ツール呼び出し 2 回。純粋な儀式。意思決定の価値ゼロ。 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ jj の状態モデル:1 種類のコンテナ。自動スナップショット │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ @ (作業用コピーコミット) │ │
│ │ ← ファイル編集時に自動でこのコミットの更新としてスナップ │
│ └──────────────────────────────────────────┘ │
│ │
│ エージェント操作:ファイルを編集して完了。 │
│ = 追加のツール呼び出し 0 回 │
└─────────────────────────────────────────────────────────────┘
エージェントにとって、これは以下の意味を持ちます。
ツール呼び出しが 2 ステップから 0 ステップになる。 Git の git add -A && git commit -m "..." は jj では不要です。ファイルの修正がそのまま「コミット」になります。エージェントが意味的な境界を表現したい場合にのみ jj new(新しい変更の開始)と jj describe(変更の意図の説明)を実行すれば済みます。
ブロックされるエラーが二度と発生しない。 「マージによって上書きされてしまうローカルの変更があります」といった Git のエラーは jj には存在しません。「未コミットの修正」という概念がないからです。
例:
# jj: ファイル編集後
vim src/auth.rs # ファイルを修正。これでおしまい。
# jj は現在の作業用コピーを @ コミットの更新として自動スナップショット
# add も commit も不要
jj describe -m "ログインの実装" # (任意)この変更の説明を追加
重要な仕組み: jj はコマンド(jj status、jj log、jj new など)を実行する際、常に作業用コピーのすべての変更を現在の @ コミットに自動的にスナップショットします。つまり、「保存されていない変更」は、実質的に常時保存されています。
Git のメンタルモデル:
ファイル修正 ──一種の──→ "未追跡の汚れた状態"(危険、消失の恐れ)
jj のメンタルモデル:
ファイル修正 ──一種の──→ "@ コミットの最新の内容"(安全、スナップショット済み)
jj にはステージングエリアはありませんが、より柔軟な split で同様のことを実現します。
# jj:この時点で @ コミットには 3 つのファイルすべての修正が含まれている(自動スナップショット済み)
jj split # 現在の @ コミットを対話的に 2 つのコミットに分割
# どのファイル/どのハングルを 1 つ目のコミットに入れるかを選択させるインターフェース
# 残りは自動的に 2 つ目のコミットになる
Git の発想:
修正 → どの変更をステージングに"入れるか"選択 → コミット → 残りは作業用ディレクトリへ
(選んでから保存)
jj の発想:
修正 → すべて自動で @ コミットに入る → split で必要な部分を分割
(保存してから分割)
違い:jj の方式なら絶対に"物を失う"ことがない。すべての修正は常にいずれかのコミット内にある。
Git の場合、作業用ディレクトリの変更は誤操作で失われる可能性がある。
つまり、jj のメンタルモデルは Git よりもはるかにシンプルです。
中核となる考え方の転換はこうです。Git は修正を行う前に「この修正をどこに持っていくか」(作業用?ステージ?スタッシュ?)を決めるよう求めますが、jj はまず修正を行い、後で整理することを可能にします。 すべての変更はあるコミットの中に安全に存在し、後から分割(split)したり、移動(squash/move)したり、記述(describe)したりできます。この「保存して後から整理」というモデルは、データ消失のリスクを本質的に排除しています。
エージェントにとって、これは 2 つの操作だけ覚えていれば良いことを意味します。jj new(新しい論理的変更を開始)と jj describe(その説明を書く)です。保存、ステージ、一時退避など、その他すべてはバージョン管理システムが自動的に処理します。
解決策 2:Change ID、安定した論理識別子
jj は 2 つの階層の識別子を区別します。
- Revision(リビジョン):不変のスナップショット(Git のコミットハッシュに類似)。内容を変更するたびに新しいリビジョンが生成されます。
- Change(チェンジ):安定した論理識別子(Change ID)。チェンジの内容を何度修正しても、Change ID は変わりません。
┌─────────────────────────────────────────────────────────────┐
│ Git:amend すると参照チェーンが切断される │
│ │
│ commit abc123 ──"ログインの実装" │
│ │ │
│ ▼ (エージェントが amend して内容を変更) │
│ commit def456 ──"ログインの実装 (修正)" ← 新しいハッシュ! │
│ │
│ 問題:CI、PR、オーケストレーター内で abc123 を参照している場所がすべて無効に
│ オーケストレーター:「追跡していた abc123 はどこへ???」 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ jj:Change ID は修正をまたいで安定している │
│ │
│ change kkmpptqz ──"ログインの実装" │
│ (revision: abc123) │
│ │ │
│ ▼ (エージェントが内容を変更) │
│ change kkmpptqz ──"ログインの実装 (修正)" ← 同じ Change ID!│
│ (revision: def456) ← リビジョンは変わったが Change ID は不変 │
│ │
│ オーケストレーターは amend の影響を受けず kkmpptqz で追跡可能 │
└─────────────────────────────────────────────────────────────┘
これはオーケストレーションシステムにとって極めて重要です。 Symphony のようなエージェントオーケストレーションシステムは、「このイシューに対応するコード変更」を追跡する必要があります。Git のコミットハッシュを使うと、エージェントが amend するたびにその関連が切れてしまいます。jj の Change ID を使えば、エージェントが何回修正しようとも、オーケストレーターは Change ID を通じて安定的に追跡可能です。
解決策 3:競合は一等のオブジェクト。ブロックも中断もしない
これは jj が Darcs/Pijul から受け継いだ最も深遠なイノベーションです。
Git において競合はブロッキング状態です。立ち往生し、継続するには競合を解決する必要があります。jj において競合はデータです。コミットは「競合を含んだ」状態になれます。これは関数やファイルを含んでいるのと本質的に変わりません。
技術的には、jj はテキストの競合マーカー(<<<<<<<)を保存するのではなく、競合ツリーの論理的な表現(一連の diff の組み合わせ)を維持します。つまり、競合を含むコミットをリベースしても、「ネストされた競合マーカー」という悪夢に陥ることはありません。
┌─────────────────────────────────────────────────────────────┐
│ Git:競合がパイプラインをブロックする │
│ │
│ エージェントが git rebase main を実行 │
│ │ │
│ ▼ │
│ commit 1 ── リベース成功 ✓ │
│ │ │
│ ▼ │
│ commit 2 ── 競合!✗ ──── パイプライン停止 │
│ │ 対話的な解決が必要 │
│ × git rebase --continue │
│ commit 3 ── 続行不能 エージェントは競合の文脈を理解できない │
│ commit 4 ── 続行不能 │
│ │
│ 状態:REBASE IN PROGRESS(中間状態、危険) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ jj:競合はデータ、操作は常に完了する │
│ │
│ エージェントが jj rebase -d main を実行 │
│ │ │
│ ▼ │
│ change 1 ── リベース成功 ✓ │
│ │ │
│ ▼ │
│ change 2 ── 競合あり、conflicted とマーク ⚠ ── しかし操作続行!│
│ │ │
│ ▼ │
│ change 3 ── リベース成功 ✓(競合解決後に自動伝播) │
│ │ │
│ ▼ │
│ change 4 ── リベース成功 ✓ │
│ │
│ 状態:操作完了。change 2 は conflicted とマークされ、後で解決可能。│
│ 中間状態なし、ブロックなし。 │
└─────────────────────────────────────────────────────────────┘
より重要な設計的意味合いは以下の通りです。
リベースは常に「成功」する。 git rebase --continue も「in progress」状態もありません。操作はアトミックです。完了する(競合を含むコミットを生成する)か、他の理由で失敗するかのどちらかです。
競合解決は伝播する。 あるチェンジで競合を解決すると、jj はその解決策をすべての子孫チェンジに自動的に伝播させます。これは Git の rerere(record/replay resolve)に似ていますが、デフォルトの動作であり、より信頼性が高いものです。
競合解決は先送りできる。 エージェントが作業を完了した後、オーケストレーターは競合を含むチェンジがあるか確認し、エージェントに解決させるか、競合解決専門のエージェントを起動するか、人間にエスカレーションするかを決定できます。競合は「パイプラインの停止」ではなく、「追加処理が必要なマークのついた成果物」になります。
解決策 4:Operation Log、完全な操作監査
Git には reflog がありますが、記録されるのはブランチの先端の移動のみで、形式も難解であり、主に災害復旧用です。
jj の operation log は、リポジトリに対して実行したすべての操作を記録します。各レコードは 1 つのアトミックな操作です。10 のコミットを含むリベースでさえ、operation log 上では 1 つのレコードになります。
┌─────────────────────────────────────────────────────────────┐
│ Git reflog:粒度が粗く、ブランチの先端の移動のみを記録 │
│ │
│ abc123 HEAD@{0}: commit: fix login │
│ def456 HEAD@{1}: rebase: updating HEAD │
│ 789abc HEAD@{2}: checkout: moving from main to feature │
│ │
│ 問題:10 のコミットを含むリベースが reflog 上では │
│ 10 個に分散したレコードとして表示され、アトミックな戻し方が困難 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ jj operation log:アトミック、各操作が 1 レコード │
│ │
│ @ kmlprxqv rebase 4 commits onto main │
│ ○ zxsewqpw describe change kkmpptqz │
│ ○ tnrqpaxy new empty change │
│ ○ vqrpmzxn fetch from origin │
│ │
│ 4 コミットのリベース全体を元に戻す:jj op restore zxsewqpw │
│ 1 コマンドでアトミックに戻す。 │
└─────────────────────────────────────────────────────────────┘
# 操作履歴の表示
jj op log
# 直近の操作を元に戻す(アトミック)
jj undo
# 任意の過去の操作の状態に戻す
jj op restore <operation-id>
エージェントにとって、これは「タイムマシン」です。 エージェントが破滅的な操作をしてしまった場合、reflog から手動で正しい状態を探す必要はありません。jj op restore で正確に過去のある時点に戻せます。オーケストレーションシステムは、エージェントの各操作の前に operation ID を記録し、失敗時にワンクリックでロールバックできます。
解決策 5:自動リベース、マルチエージェント並行処理の基盤
コミットを変更すると、jj はすべての子孫コミットを新しいバージョンに自動的にリベースします。競合を一等のデータとする設計と組み合わせることで、この自動リベースは競合が発生してもブロックされません。
┌─────────────────────────────────────────────────────────────┐
│ シナリオ:履歴内のコミット B を修正(B → B') │
│ │
│ 修正前: A ← B ← C ← D │
│ │
│ ─── Git ─── │
│ 修正後: A ← B' (手動 amend) │
│ A ← B ← C ← D (C, D はまだ古い B を参照!) │
│ 必要: git rebase --onto B' B D │
│ C で競合:リベース停止、手動解決 │
│ │
│ ─── jj ─── │
│ 修正後: A ← B' ← C' ← D' (自動リベース、1 ステップで完了) │
│ 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 のような競合マーカー形式にレンダリングして表示します。しかし、これは表示層の話であり、底层に保存されているのは構造化された 3 方データであり、単なるテキストマーカーではありません。
保存層:構造化された競合ツリー(base + side_1 + side_2)
│
▼ (ファイルを開く際に人間が読める形式にレンダリング)
表示層:Git 風の <<<<<<< マーカー(これは単なるビューでデータではない)
この違いは極めて重要です。構造化データはプログラムで正確に操作できますが、テキストマーカーは人間に読ませるためだけのものだからです。
自動リベース時の競合処理
ケース 1:リベースで競合がない場合
最も単純なケースです。2 つのチェンジが異なるファイル、または同じファイルの異なる領域を修正している場合です。
修正前:
main: A ← B ← C
↑
B を B'に修正(例:src/auth.rs を変更)
自動リベース:
C は B に依存。B が B'になる。jj は C を自動的に B'にリベース。
結果:A ← B' ← C'
C が src/config.rs を修正しており(B'が修正した src/auth.rs と重複なし)
→ C'はクリーンに適用、競合なし。自動完了。何もする必要なし。
ケース 2:リベースで競合が発生。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'と修正、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'は"競合を含むコミット"。しかし、これは有効なコミット!
→ D の処理へ継続 → D'は C'にリベース
→ D が src/auth.rs の 10-20 行目に触れていなければ、D'はクリーン
→ D も触れていれば、D'も競合を含むとマーク
→ 操作完了。リベースチェーン全体が中断されない。
┌──────────────────────────────────────────────────────────────┐
│ 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'は │
│ 有効なコミット │
│ │
│ 状態:操作完了。「in progress」なし。 │
│ C'は conflicted とマーク、後で解決可能。 │
└──────────────────────────────────────────────────────────────┘
ケース 3:競合の解決方法
C'の競合を解決することにした場合:
# どの 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)?;
# >>>>>>>
# あなた(またはエージェント)がファイルを編集し、マーカーを消して正しいマージ結果を書く
# ファイルを保存
# jj は競合マーカーが消えたことを自動検出 → この change はもはや conflicted ではない
# しかも重要なのは、D'が競合解決済みの C'に自動的にリベースされる点
# もし D'が C'の競合が原因で連鎖的に conflicted になっていても、
# 競合解決後は D'も自動的にクリーンになる
ケース 4:競合解決の伝播、これがキラー機能
競合解決前: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 の実装はより根本的だ。
"以前解決した類似の競合を記憶して再生する"のではなく、
"競合解決そのものがリベースで伝播可能な diff である"という考え方。
なぜ「競合はデータ」であることがそれほど重要なのか
競合を「状態」から「データ」に変えることの本質的な影響:
┌──────────────────────────────────────────────────────────────┐
│ "競合は状態" (Git) │
│ │
│ • 競合が操作をブロック ── 先に解決する必要がある │
│ • 競合はコミットできない ── 作業用ディレクトリにしか存在できない │
│ • 競合は伝播しない ── リベースのたびに再発する可能性がある │
│ • 競合によりリポジトリが"中間状態"に ── 危険で、クリーンが必要 │
│ • 競合解決の時間枠:リベース中(プレッシャーがあり、他はできない)│
│ │
│ エージェントへの影響: │
│ エージェントが競合に遭遇 → パイプライン停止 → オーケストレーターの介入が必要│
│ → しかしオーケストレーターに解決の文脈がない → 人間にエスカレーション │
│ → 人間はエージェントの作業内容を理解する必要がある → コストが高い │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ "競合はデータ" (jj) │
│ │
│ • 競合は操作をブロックしない ── リベースを完了し、競合はコミット内にマーク│
│ • 競合もコミット可能 ── コミットの一部である │
│ • 競合解決は伝播可能 ── 一箇所解決で子孫が自動修復 │
│ • リポジトリは常に"クリーン状態" ── 一部のコミットに競合マークがあるだけ│
│ • 競合解決の時間枠:いつでも(プレッシャーなし、他を先にできる) │
│ │
│ エージェントへの影響: │
│ エージェントが競合に遭遇 → 操作完了、競合マーク → パイプライン継続│
│ → オーケストレーターが "change X is conflicted" を検出 → 判断: │
│ a) 競合解決エージェントを起動(3 方データを提供) │
│ b) 一旦スキップし、競合のない他のタスクを処理 │
│ c) 人間にエスカレーション(緊急ではない、何もブロックされていない)│
│ → いつ解決してもよく、他のエージェントの作業には影響しない │
└──────────────────────────────────────────────────────────────┘
エージェントによる競合解決の可能性
jj の競合は構造化データ(base + side_1 + side_2)であるため、エージェントは Git よりも豊富な情報を得て競合を解決できます。
# エージェントが見る情報
# Git:<<<<<<< マーカーが混在したテキストファイル
# エージェントは "どれが ours で、どれが theirs で、どれが base か" をテキストから解析
# 特にネストされた競合の場合、解析を誤る可能性あり
# jj:構造化された 3 方データ
# base: 元バージョン(共通の祖先)
# side_1: 第 1 者の修正
# side_2: 第 2 者の修正
# エージェントは 3 方それぞれの完全な内容を正確に取得可能
# 合理的なエージェントの競合解決戦略:
# 1. base, side_1, side_2 の全内容を抽出
# 2. side_1 の意図を理解(変更の説明または関連する Contract から)
# 3. side_2 の意図を理解(同様)
# 4. マージ結果を生成
# 5. マージ結果が両方の Contract を満たすか検証
さらに Contract(何らかの仕様制約)を change に紐付けることで、競合解決はより根拠に基づいたものになります。
change kkmpptqz (side_1) → contract-042 "ユーザー登録の実装"
change rrsnntyz (side_2) → contract-057 "ログイン認証の修正"
競合は src/auth.rs で発生
競合解決エージェントは 2 つの Contract の意図を取得可能:
- contract-042 の要件:登録時は bcrypt-12 でパスワードをハッシュ化
- contract-057 の要件:ログイン時はパスワードを検証し JWT を返す
→ エージェントはマージ方法を判断するための十分なセマンティック情報を持つ:
両者は異なる機能(登録 vs ログイン)を変更しており、
マージ結果には登録ロジックと修正済みのログインロジックの両方が含まれるべき。
これが「競合はデータ」+「Spec-Change の紐付け」の相乗効果です。競合は「なぜ争っているか分からない 2 つのコード」ではなく、「既知の意図を持つ 2 つの変更が同一領域で重なったもの」となり、意図情報があれば解決策の判断基準が得られます。
解決策 6:workspace、マルチエージェント並行処理の強み
Git worktree はマルチエージェントで並行作業する際の重要なシナリオです。jj にも独自の仕組みがありますが、Git worktree よりもクリーンです。
Git worktree の中核シナリオ:スタッシュや checkout を行ったり来たりせずに、同じリポジトリの異なるブランチで同時に作業したい場合です。
jj における対応概念はworkspaceです。しかしそのモデルは Git worktree よりも単純です。jj には index/staging area がないため、各 workspace の状態は @ コミットというだけで、他には何もありません。
# jj:複数の workspace を作成
jj workspace add ../feature-login
jj workspace add ../bugfix-auth
# 3 つのディレクトリができる:
# ./repo ← デフォルトの workspace、@ = ある change
# ../feature-login ← workspace "feature-login"、@ = 別の change
# ../bugfix-auth ← workspace "bugfix-auth"、@ = さらに別の change
# これらは同じ jj リポジトリデータを共有
# 各 workspace は独立した @ (作業用コピーコミット) を持つ
# すべての workspace を表示
jj workspace list
# 出力:
# default: kkmpptqz (現在の change)
# feature-login: rrsnntyz (別の change)
# bugfix-auth: ppmmlkqz (さらに別の change)
# 決定的な違い
┌──────────────────────────────────────────────────────────────┐
│ Git Worktree │
│ │
│ 各 worktree が持つ: │
│ ├── 独立した作業用ディレクトリ (ファイル) │
│ ├── 独立した index / staging area (一時保存状態) │
│ ├── 独立した HEAD (どのコミット/ブランチを指すか) │
│ └── 共有の .git/objects (オブジェクト庫) │
│ │
│ 制限: │
│ • 各 worktree は異なるブランチを指す必要がある │
│ (2 つの worktree が同じブランチをチェックアウトできない) │
│ • 各 worktree は独自の index を持ち、状態の複雑さが N 倍 │
│ • 1 つの worktree が "rebase in progress" 状態だと、 │
│ 関連する参照をロックし、他の worktree に影響を与える │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ jj Workspace │
│ │
│ 各 workspace が持つ: │
│ ├── 独立した作業用ディレクトリ (ファイル) │
│ ├── 独立した @ (作業用コピーコミット) │
│ └── 共有のリポジトリデータ (すべての changes + operation log)│
│ │
│ 持たない: │
│ • index 不要(不要) │
│ • ブランチの指し示し制限なし(workspace はブランチではなく change を指す)│
│ • "in progress" 状態なし(競合はデータでブロックしない) │
└──────────────────────────────────────────────────────────────┘
jj workspace の各インスタンスが持つのは **1 層の状態** のみ:
各 jj Workspace の状態:
@ コミット(現在の作業用コピー change)
staging area なし、"in progress" 状態なし。
オーケストレーターが知っておくべきは "この workspace の @ がどの change を指しているか" のみ。
これが「すべてがコミット」ということが workspace シナリオでどう現れるかです。各 workspace の完全な状態は 1 つの change ID です。オーケストレーターが N 個のエージェントの状態を追跡するとは、N × (作業用ディレクトリ + index + HEAD + あり得る中間状態) というデカルト積ではなく、N 個の change ID を追跡するだけなのです。
エージェントオーケストレーションへの意味
┌──────────────────────────────────────────────────────────────┐
│ 10 のエージェント workspace を管理する際の複雑さ │
│ │
│ Git Worktree: │
│ • 10 のブランチに名前を付け管理する必要がある │
│ • 10 の staging area が不整合な状態にある可能性 │
│ • いずれかが "rebase in progress" で止まる可能性 │
│ • マージ競合がパイプラインをブロック │
│ • 崩壊からの回復には各 worktree がどの状態か判断が必要 │
│ 状態空間:O(N × M) N=worktree 数, M=あり得る中間状態の数 │
│ │
│ jj Workspace: │
│ • 10 の匿名 change、名前不要 │
│ • staging area なし │
│ • "in progress" 状態なし │
│ • 競合はマークされるがブロックしない │
│ • 崩壊からの回復:jj op restore または継続(変更はすべて @ にある)│
│ 状態空間:O(N) 各 workspace は 1 つの change ID のみ │
└──────────────────────────────────────────────────────────────┘
つまり jj workspace は「Git worktree の代わり」であるだけでなく、staging area と中間状態を排除することで、各作業空間の状態の複雑さを多次元から1 次元(1 つの change ID)へと圧縮しています。複数の並行エージェントを管理する必要があるオーケストレーションシステムにとって、これは桁違いの簡素化です。
第 3 部:Spec がバージョン管理に出会うとき、Spec-Change の紐付け
再考:エージェント・ソフトウェア・エンジニアリングにおける Spec の役割
本シリーズの前回の記事で、エージェント・ソフトウェア・エンジニアリングの核となる枠組みを確立しました。Spec は制御面、Code はデータ面、Agent は実行面です。人間の主たる仕事は Spec(仕様書)を書くことであり、エージェントは Spec をコード実装に変換し、検証システムがコードが Spec を満たしているかを確認します。
この枠組みにおいて、エージェントに実行させるタスクレベルの Spec をTask Contract(タスク契約)と呼びます。これは構造化されたドキュメントで、4 つの核となる要素を含みます。
- 意図(Intent):何をするのか、そしてなぜか。
- 決定済み事項(Decisions):すでに決定された技術的選択。エージェントの判断余地を排除する。
- 境界(Boundaries):どのファイルを修正してよいか、何をしてはならないか。
- 完了条件(Completion Criteria):BDD 形式の受け入れ基準。決定的な合格/不合格のチェック。
Contract という概念は、私たちがコードレビューの危機を分析した結果から生まれました。AI によるコード生成速度が人間のレビュー能力を遥かに上回る場合、解決策は「より速くコードをレビューする」ことではなく、人間のレビューを意図レイヤーに引き上げることです。つまり、人間は Spec/Contract によって意図と受け入れ基準が正しく定義されているかをレビューし、機械はコードが Contract を満たしているかを検証するのです。
現在の断絶:Spec とバージョン管理は別世界
今日のツールチェーンでは、Spec(GitHub の Issue、Jira のチケット、あるいは私たちの定義する Contract であれ)とバージョン管理システムの間の関連は脆く、慣習的なものです。
┌─────────────────────────────────────────────────────────────┐
│ 現在の緩い関連付け │
│ │
│ Spec/Contract │ Version Control │
│ (外部システム) │ (Git/jj) │
│ │ │
│ "ユーザー登録の実装" ······commit message 内の····→ abc123 │
│ Issue #42 "fixes #42" という文字列関連 commit │
│ │
│ 問題: │
│ 1. 関連が慣習に依存(#42 を書き忘れると切れる) │
│ 2. commit hash が不安定(amend すると切れる) │
│ 3. 1 つの Spec にいくつの commits 対応するか不明 │
│ 4. 1 つの commit が Spec のどの受け入れ基準を満たしたか不明 │
│ 5. Spec の変更と Code の変更に関連するバージョン履歴がない │
└─────────────────────────────────────────────────────────────┘
この断絶は人間の手動作業では許容できます。人間は心の中で「自分は今 Issue #42 のためにコードを書いている」という関連を維持しているからです。しかし、エージェントオーケストレーションシステムでは、この関連は形式的で、照会可能で、amend しても切れないものでなければなりません。
Spec-Change の紐付け:jj がもたらす機会
jj の 2 つの特性が、この問題解決の基盤を提供します。
Stable Change IDは「amend による関連の断絶」を解決します。エージェントが何回修正しても change ID は変わらないため、オーケストレーションシステムは常に change ID を使って「この Contract に対応するコード変更」を追跡できます。
Git 外部のメタデータ保存は「慣習に依存する関連」を解決します。jj はすでに Git リポジトリの外部に bookmarks などのメタデータを保存しています。Contract の紐付けも別種のメタデータとして、バージョン管理の一等の市民にすることができます。
┌─────────────────────────────────────────────────────────────┐
│ 理想的な Spec-Change の紐付け │
│ │
│ Contract "ユーザー登録" │
│ ├── intent: "登録 API エンドポイントの実装" │
│ ├── decisions: [bcrypt-12, POST /api/v1/auth/register] │
│ ├── boundaries: [ONLY src/auth/**, DO NOT add deps] │
│ ├── criteria: [登録成功→201, 重複メール→409, ...] │
│ │ │
│ ├── bound_changes: ← コミットメッセージではなく形式的な紐付け│
│ │ ├── kkmpptqz "登録エンドポイントの実装" │
│ │ ├── 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 とバージョン管理は「2 つの緩く関連した世界」から「1 つの統一されたトレーサビリティチェーン」へと進化します。
人間が Spec/Contract を定義
│
▼
エージェントが Changes を生成(Contract に紐付け)
│
▼
検証システムが Changes が Contract の Criteria を満たすかチェック
│
▼
Contract の承認(人間が Contract 定義の正当性をレビュー + 検証がすべて通過したか確認)
Spec の階層とバージョン管理の対応
私たちの Spec の構想では、Spec は 3 層に分かれています。
- L0 組織レベル Spec(セキュリティポリシー、アーキテクチャ原則)。変化は極めて遅い。
- L1 プロジェクトレベル Spec(技術スタック、モジュール境界、API 契約)。変化は遅い。
- L2 タスクレベル Spec / Contract(具体的なタスクの意図、制約、受け入れ基準)。タスクごとに 1 つ。
この 3 層とバージョン管理の対応関係は以下の通りです。
┌─────────────────────────────────────────────────────────────┐
│ Spec レイヤーとバージョン管理の対応 │
│ │
│ L0 組織レベル Spec ──→ ルールファイル(rules/security-baseline.md)│
│ (セキュリティポリシー等) リポジトリ内でバージョン管理、稀に変更 │
│ エージェントがルールルーティングで必要に応じて読み込み │
│ │
│ L1 プロジェクトレベル Spec ──→ プロジェクト設定ファイル(specs/project.yaml)│
│ (技術スタック、契約) リポジトリ内でバージョン管理、スプリント単位で変更│
│ エージェント起動時に読み込み │
│ │
│ L2 タスク Contract → jj changes と形式的に紐付け │
│ (タスクの意図/受け入れ) Contract の変更で changes を再生成 │
│ 検証結果を Contract レベルで関連付ける │
└─────────────────────────────────────────────────────────────┘
重要な洞察:L0 と L1 の Spec 自体もコードリポジトリ内でバージョン管理されるべきです。これらはコードの「憲法」であり、L0/L1 Spec の変更は憲法改正のように慎重に行われ、完全な変更履歴を持つ必要があります。L2 Contract は「作業指示書」レベルのもので、そのライフサイクルは対応する changes と共に始まり、共に終わります。
第 4 部:エージェントネイティブ VCS に足りないもの
jj はエージェントシナリオにおける Git の摩擦点のほとんどを解決しました。しかし、エージェント・ソフトウェア・エンジニアリングの完全な枠組みから見れば、現在の jj がカバーしていないいくつかの次元があります。
プリミティブ 1:検証状態を Change の属性として
現在のバージョン管理システムは、ある change がテストに合格したかどうかを知りません。CI/CD は外部システムであり、検証結果は GitHub Actions や Jenkins などに存在し、バージョン管理システム内にはありません。
エージェントネイティブ VCS では、検証状態は change の内在的な属性であるべきです。
Change kkmpptqz:
revision: abc123
description: "登録エンドポイントの実装"
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 ボタン」から「検証エンジンが全レイヤーの通過を確認」に変わります。
プリミティブ 2:エージェントのアイデンティティと権限
Git の author/committer フィールドは name + email で識別します。マルチエージェントが並行作業するシナリオでは、より豊富なアイデンティティ情報が必要です。
Change kkmpptqz:
agent: claude-code-session-7a3b
role: implementer
contract: contract-042
permissions: workspace-write, files=[src/auth/**]
orchestrator: symphony-run-019
これにより、監査システムは「このコードはどのエージェントが、どのような権限で生成したのか?」と答えることができます。コンプライアンスの観点からは、"Author: bot@ci.example.com"よりもはるかに価値があります。
プリミティブ 3:DAG ではなくスナップショットの時系列
Git のコミット DAG(有向非巡回グラフ)は、人間がブランチ/マージの履歴を理解するために設計されています。しかし、10 のエージェントが並行作業する場合、DAG は読めないスパゲッティのようなものになります。
エージェントネイティブな履歴の可視化は、時系列に並べられたスナップショットのシーケンスであるべきです。Contract ごと、エージェントごと、モジュールごとにフィルタリング可能です。タイムラインをイメージし、各エージェントをレーンとし、Contract を色分けし、任意の時点にスクラブして、その瞬間のコードベースの状態を見ることができます。
jj の operation log はすでにその基盤の一部を提供しています。すべての操作を時系列で記録します。欠けているのは Contract/エージェントアイデンティティとの関連付けと、可視化レイヤーです。
第 5 部:コードレビューから Contract の承認へ
バージョン管理の変革はツールレベルにとどまらず、コラボレーションモデルそのものを変えます。
従来モデル:Branch → PR → コードレビュー → マージ
GitHub はプルリクエスト(PR)中核に据えたコラボレーションフローを構築しました。開発者はブランチを作成し、コードをプッシュし、PR を開き、同僚がコード差分をレビューし、approve 後にマージします。
このフローはエージェント時代において 3 つの根本的な問題に直面しています。
1. レビューの対象が間違っている。 以前の記事『コードレビューの再考』で詳しく分析しましたが、AI がマシン並みの速度でコードを生成する時代、人間が 1 行ずつコード差分をレビューすることは拡張可能ではありません。Faros AI のデータでは、PR のマージ率は 97.8% 急増した一方、レビュー時間は 91.1% 急増しています。
2. ブランチの粒度が間違っている。 エージェントは数分で 1 つの Contract を完了させる可能性があります。ブランチ作成→プッシュ→PR 作成→レビュー待ち→マージというプロセスは、実際の作業時間よりも長くなってしまいます。
3. マージのゲートが間違っている。 PR の approve ボタンは人間の判断、「このコードは正しそう」という主観に基づいています。しかし、検証は決定的であるべきです。「このコードはすべての検証レイヤーに合格した」という事実です。
エージェントネイティブモデル:Contract → Changes → 検証 → 承認
┌──────────────────────────────────────────────────────────────┐
│ 従来の GitHub プロセス │
│ │
│ Issue ──→ Branch ──→ Code ──→ PR ──→ コードレビュー ──→ マージ │
│ (曖昧) (名前付き) (手書き) (PR 作成) (人間が diff 読解) (approve)│
│ │
│ 人間の関与:全プロセス │
│ ゲート:人間の主観的判断 │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ エージェントネイティブプロセス │
│ │
│ Contract → Changes → Verification → Acceptance │
│ (構造化タスク契約) (匿名 jj) (決定的検証) (Contract レベル) │
│ │
│ 人間の関与:Contract 定義 + 最終承認 │
│ ゲート:検証エンジンが全 Criteria の通過を確認 │
└──────────────────────────────────────────────────────────────┘
このモデルでは:
Contract が Issue + PR 説明に取って代わる。 Contract は構造化され、受け入れ基準を含み、自動検証可能です。これは「人間用のタスクメモ」ではなく、「エージェントが消費する実行可能仕様+検証システムが実行するチェックリスト」です。
Changes が Branch に取って代わる。 jj の匿名 changes と stable change ID により、ブランチ名は不要です。オーケストレーションシステムは change ID で追跡します。
Verification Pipeline がコードレビューに取って代わる。 型チェックから契約検証、BDD テスト、対立的エージェントによるレビューまで、多層的な決定的検証が、人間のコードレビューよりも包括的で、高速で、拡張可能な品質保証を提供します。
Contract Acceptance が PR の approve に取って代わる。 人間がレビューするのはコード差分ではなく、「この Contract の受け入れ基準は正しく定義されているか?検証結果は Contract が正しく満たされたことを示しているか?」です。
第 6 部:実践ガイド - 既存ツールチェーンでの段階的導入法
理想は素晴らしいですが、多くのチームが一夜にして Git や GitHub を捨てるわけではありません。以下は段階的な導入パスです。
Level 0:Git リポジトリ内で jj を併用(ゼロリスク)
jj は Git リポジトリとの共存(colocate モード)をサポートしているため、既存の Git リポジトリで jj の使用を開始でき、チームの他のメンバーが Git を使い続けるのにも影響しません。
# 既存の Git リポジトリで jj を初期化(両者共存)
cd your-repo
jj git init --colocate
# これで jj と git の両方が使用可能
# jj の操作は Git に同期され、git の操作も jj から可視
エージェントへの即時の価値:
jj op logにより、エージェント操作の完全な監査が可能- 自動スナップショットにより、エージェントが「未コミットの修正」を失うことがない
jj op restoreで、エージェントがミスを犯した際に正確に巻き戻せる
Level 1:エージェントの作業用スペースで jj を使用
Claude Code や Codex CLI の作業用スペースで、エージェントのバージョン管理ツールとして git の代わりに jj を使用します。
# エージェントのツールセットを簡素化
# 不要:git add, git stash, git rebase --continue
# 必要:jj new, jj describe, jj bookmark set, jj git push
主なメリット: エージェントのバージョン管理ツール呼び出し回数が 30-50% 削減されます(git add、git stash、git rebase --continue などの儀式的操作が不要になるため)。節約されたツール呼び出しの分だけ、実際の作業により多くのコンテキスト予算を割くことができます。
Level 2:Change ID を使ったタスクの追跡
エージェントオーケストレーションシステム内で、変更の追跡に Git のコミットハッシュの代わりに jj の change ID を使用します。change ID は amend をまたいでも安定しているため、オーケストレーターはエージェントが修正するたびに参照を再結合する必要がなくなります。
Level 3:競合をデータとする特性の活用
マルチエージェントの並行シナリオにおいて、競合を「エラー」ではなく「追加処理が必要な状態」として扱います。エージェントの完了後に競合マークのついた changes がないかチェックします。単純な競合は自動解決し、複雑な競合は人間にエスカレーションします。パイプラインが「停止」することはありません。
第 7 部:対比の総覧
Git vs jj vs 理想的なエージェントネイティブ VCS
| 次元 | Git | Jujutsu (jj) | 理想的なエージェントネイティブ VCS |
|---|---|---|---|
| データモデル | 4 種のコンテナ | 1 種のコンテナ(コミット) | 1 種のコンテナ + Spec メタデータ |
| 変更の識別子 | コミットハッシュ(修正のたびに変化) | Change ID(安定) | Change ID + Contract 紐付け |
| 競合モデル | ブロッキング状態 | 一等のデータオブジェクト | 一等オブジェクト + 自動解決戦略 |
| 操作監査 | reflog(粗粒度) | operation log(アトミック) | operation log + エージェントアイデンティティ |
| ブランチモデル | 名前付きブランチ | 匿名 changes + ブックマーク | 匿名 changes + Contract 関連 |
| 自動リベース | 手動 | デフォルトで自動 | 自動 + 競合伝播 |
| Spec 統合 | なし(コミットメッセージの慣習に依存) | なし | Contract-Change の形式的紐付け |
| 検証統合 | なし(外部 CI に依存) | なし | 検証状態が change の属性 |
| エージェント親和性 | 低(多くの儀式的操作) | 高(自動スナップショット、ブロックなし) | 極めて高い(ネイティブなアイデンティティ/権限) |
| ストレージバックエンド | Git のみ | Git(拡張可能) | 拡張可能(Git / クラウド) |
| エコシステム互換 | ネイティブ | Git 互換 | Git 互換 + ネイティブプロトコル |
重要な結論:jj はエージェント-VCS 間の摩擦の 80% をすでに解決している。「すべてをコミットに」「Change ID」「競合はデータ」「自動リベース」によって。残りの 20% は、エージェント・ソフトウェア・エンジニアリングの上位レイヤー(Spec/Contract、検証パイプライン、エージェントオーケストレーション)との連携部分です。
結び:バージョン管理はそれが奉仕する働き方を反映する
バージョン管理の歴史を振り返ると、各世代のシステムはその時代の働き方を反映しています。
CVS/SVN 時代: 中央サーバー、順序的なコミット。「1 つのチーム、1 つのコードベース、1 人の統合マネージャー」というウォーターフォール開発を反映。
Git 時代: 分散型、安価なブランチ。「分散チーム、並行開発、プルリクエストによるコラボレーション」というアジャイル開発を反映。
エージェント時代: 何が必要か?自動スナップショット、安定した識別子、競合はデータ、アトミックな操作、完全な監査。「マルチエージェント並行処理、Spec/Contract 駆動、決定的検証、マシン速度」を反映するエージェント・ソフトウェア・エンジニアリングです。
Git が「間違い」だったわけではありません。人間開発者のために 20 年間完璧に機能しました。しかし、その中核的な前提、つまり「操作者が文脈を理解している」「操作は順序的」「変更は明示的なステージングが必要」「競合は即座に解決されなければならない」は、エージェント時代において機能ではなく摩擦となっています。
Jujutsu は現実的な進化の道筋を提供します。Git 互換性を維持し(既存のエコシステムを破壊せず)、エージェントネイティブなデータモデルと操作セマンティクスを導入するという道筋です。GitHub を捨てる必要も、チーム全員での切り替えも、CI/CD の再構築も不要ですが、エージェントに対して安全で、決定的で、「停止」することのないバージョン管理インターフェースを与えてくれます。
そして、バージョン管理を Spec/Contract システムと連動させるとき、Change ID による Contract-Change の紐付け、検証状態の統合による自動ゲートキーピング、エージェントアイデンティティによる完全な監査を通じて、バージョン管理は孤立した「コード保存レイヤー」から、エージェント・ソフトウェア・エンジニアリングの中核インフラへとアップグレードされます。
Spec は制御面、Code はデータ面、Agent は実行面、そしてバージョン管理はデータ面を担う物理レイヤーです。 物理レイヤーが「人間のために設計された鋭利な刃物」から「エージェントのために設計された安全な電動工具」へと変わるとき、上位システム全体の設計空間が拓かれるのです。
参考文献
[1] 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