分離ワークツリーPRフロー は何をしますか?
メインチェックアウトに触れてはならない場合(ダーティ(未コミットのWIP)、リモートより遅れている、または他のエージェントと共有されている)に、originのデフォルトブランチから切り出した一時的なGitワークツリーからプルリクエストとして変更を送信する手順。このフローは、ワークツリーの作成、新しいワークツリーの依存関係ギャップ、上限付きCIポーリング、およびマージモードを考慮したクリーンアップガードをカバーします。
ワークツリー分離の理由
複数のエージェントセッションが同じワーキングディレクトリに入ると(例えばバッチセッション起動時)、gitのHEAD、現在のブランチ、および未コミットの変更は共有状態になります。各セッションが他のセッションの基盤を動かしてしまいます。ある事例では、あるセッションがgit switch -cでブランチを作成してから1分以内に、同じチェックアウト内の2番目のセッションがそのブランチから分岐し、コミットし、デフォルトブランチにマージしました。
検出パターン(衝突が数分で確認できる):
- 兆候:自分が書いていないファイルが現れる、または
git statusがセッション開始時に取得したスナップショットと一致しなくなる。 - タイムスタンプの精査:
ls --time-style=full-iso(またはプラットフォームの同等のコマンド)。複数のファイルが同じミリ秒を共有している場合、それはgit操作(チェックアウト/マージ/スタッシュ)が書き込んだものであり、手動またはLLMによる逐次的な編集ではありません。 git reflog --date=isoでタイムラインを確定します:誰がいつチェックアウト、コミット、またはマージしたか。- エージェント環境にセッション一覧機能が提供されている場合は、それを使用して同じワーキングディレクトリを持つ他の実行中のセッションを見つけます。
衝突が確認された後の譲歩ルール:
- 先にコミットした側が書き込み側を維持します。後から来た側は書き込みを停止します:コミットやブランチの切り替えを続けると、他のセッションのHEADを移動させ、両方を壊します。
- 後から来た側は代わりに検証者として価値を追加します:書き込み側の出力の敵対的レビュー(ドキュメントの主張とコードの比較、テストの再実行、公開前チェック)と、ファイルに触れないチャットのみの成果物。
- 自分が作成した中間参照のみをクリーンアップします(例えば空のブランチ)。他のセッションが使用した参照には決して触れないでください。
- 追記専用ログは、別々のセクション見出しの下で、両方のセッションが安全に書き込むことができます。
予防策:1リポジトリ=1書き込みセッション。複数のエージェントが同じリポジトリで作業する必要がある場合は、各エージェントを独自のワークツリーに分離します。これがこのスキルの役割です。最も簡単な検出習慣は、作業を開始した直後にgit statusをセッション開始時のスナップショットと再比較することです。
移植性
このフローはWindows(PowerShellとGit Bash)で強化されましたが、gitおよびghコマンド自体はクロスプラットフォームです。シェルが異なる場合、PowerShellとPOSIXの両方の例が示されています。CIには、Windowsホストに加えて、Ubuntu 24.04およびネイティブmacOS 15上の合成クリーンアップガードとプライベートマーカースキャナーのためのPowerShell 7ジョブが含まれています。3つのジョブすべてがPR #7 run 30216166105で合格しました。Windows固有の部分(ディレクトリジャンクションとその削除セマンティクス)には、同等のPOSIXシンボリックリンクがインラインで注釈されています。
使用すべきタイミング
- メインチェックアウトがダーティ(未コミットのWIP)、リモートより遅れている、または他のエージェントと共有されており、そこに直接ブランチを切るとそのWIPが壊れる可能性がある場合。
- 小さな衛生上の修正であっても、既存のチェックアウトが多くのコミットで遅れており、ダーティなWIPを抱えている場合、分離されたワークツリーの方が安全なルートです(実戦でテスト済み)。
- タスクごとのブランチワークフローを想定しています(デフォルトブランチに直接コミットすることはありません)。このスキルは、そのブランチがどこに存在するかというメカニズムのみをカバーします:ダーティなチェックアウトの外部にある一時的なワークツリー。
手順
-
状態を非破壊的に確認します。すべての
gitコマンドは-C <repo>で、すべてのghコマンドは--repo <owner>/<name>で実行し、カレントディレクトリに依存しないようにします。エージェント環境は呼び出し間でワーキングディレクトリをリセットする可能性があり、--repoなしでは、ghはリポジトリ外で呼び出されたときに「gitリポジトリではありません」と失敗します。git -C <repo> status --short # 作業前のWIPを記録(最後に変更がないことを確認) git -C <repo> branch --show-current git -C <repo> fetch origin gh repo view <owner>/<name> --json defaultBranchRef -q .defaultBranchRef.name # 結果を以下の<default>として使用同じコマンドはPOSIXシェルでも変更なしで動作します。
-
ダーティなチェックアウトに触れずに、
originのデフォルトブランチから一時的なワークツリーを作成します。ステップ1の<default>を置き換えます。mainをハードコードしないでください。デフォルトがmasterのリポジトリはまだ一般的です。git -C <repo> worktree add -b fix/<task> ..\_worktrees\<task> origin/<default>POSIX:
git -C <repo> worktree add -b fix/<task> ../_worktrees/<task> origin/<default>ワークツリーは、書き込み可能であることが確認された場所に配置します。システム全体の一時ディレクトリは、サンドボックス化されたエージェント環境から書き込み可能でない場合があります(実戦でテスト済み)。リポジトリの隣の
_worktrees/ディレクトリまたは環境の指定された一時ディレクトリを優先し、何か重い処理を実行する前に、1つのプローブファイルを書き込んで削除し、書き込みアクセスを確認します。 -
差分を最小限に抑えます。ワークツリー内で、タスクに必要なファイルのみを編集(またはコピー)します。メインチェックアウトのトラッキングされていないWIPをそのまま持ち越さないでください。上流または他のエージェントが同じ名前のファイルを追加する可能性があり、余分なファイルはマージ時に追加/追加の競合を引き起こしたり、PRの差分を汚染したりします。ベースチェックアウトから未コミットの依存ファイルのコピーを含める必要がある場合は、PR本文にマージ時にどちらの側が優先されるかを明記します(実戦での教訓:これを明示的に行うことで、不適切な自動解決を回避しました)。
-
新しいワークツリーの依存関係ギャップを処理します。新しいワークツリーには
node_modulesもvirtualenvもありません(実戦でテスト済み)。-
Node:ワークツリーで
npm ciを実行します(ロックファイルが必要)。軽い検証のみの場合は、メインチェックアウトのnode_modulesへの一時的なリンクも機能します。ただし、クリーンアップ前にリンクを削除してください(安全条件5)。PowerShellジャンクション:New-Item -ItemType Junction -Path <worktree>\node_modules -Target <repo>\node_modules # 削除はリンクのみを削除し、ターゲットには決して触れないでください:rmdirを使用します。 cmd /c rmdir "<worktree>\node_modules"POSIXシンボリックリンク:
ln -s <repo>/node_modules <worktree>/node_modules # リンク自体をプレーンなrmで削除します(-rなし、末尾のスラッシュなし)。 rm <worktree>/node_modules -
Python:メインリポジトリのvirtualenvインタプリタを再利用し、それをワークツリーのソースに向け、ターゲットテストのみを実行します。PowerShell:
$env:PYTHONPATH = "<worktree>\src" & <repo>\.venv\Scripts\python.exe -m pytest <worktree>\tests\<target_test>.pyPOSIX:
PYTHONPATH=<worktree>/src <repo>/.venv/bin/python -m pytest <worktree>/tests/<target_test>.py
-
-
検証が合格したら、コミット、プッシュ、およびPRを作成します。コミットメッセージをファイルに書き込み、
git commit -F <msgfile>で渡します。インラインの複数行メッセージはシェルの引用の下で脆弱であり、特にWindowsではそうです。git -C <worktree> add <ターゲットファイルのみ> git -C <worktree> commit -F <msgfile> git -C <worktree> push -u origin fix/<task> gh pr create --repo <owner>/<name> --head fix/<task> ... -
上限付きポーリングでブランチ保護CIを待ちます。
gh pr checks --watchは無制限の待機です。エージェントハーネスと承認レイヤーはそれを拒否またはハングさせる可能性があります(実戦でテスト済み)。試行回数に上限を設けたワンショットチェックを使用します:gh pr checks <pr-number> --repo <owner>/<name> --watch=false gh pr view <pr-number> --repo <owner>/<name> --json state,statusCheckRollup上限付きループ、POSIX。ハーネスがフォアグラウンドの
sleepをブロックする場合(Claude Codeがそうします)、ループをバックグラウンドタスクとして実行するか、ループをスキップして他の作業とワンショットチェックをインターリーブします:for i in $(seq 1 10); do gh pr checks <pr-number> --repo <owner>/<name> --watch=false && break; sleep 30; done上限付きループ、PowerShell:
for ($i = 1; $i -le 10; $i++) { gh pr checks <pr-number> --repo <owner>/<name> --watch=false if ($LASTEXITCODE -eq 0) { break } Start-Sleep -Seconds 30 }適切な上限と間隔はリポジトリのCI期間に依存します(一般的には未検証。リポジトリごとに調整してください)。
-
マージ。最初にマージモードを決定します。これにより、クリーンアップ前の未マージ作業検証(安全条件2)が決まります:
--merge(マージコミット):ブランチのコミットはデフォルトブランチの祖先になるため、merge-base --is-ancestorで検証し、branch -dで削除します(安全条件2a、実戦でテスト済み)。--squash/--rebase:ブランチのコミットはデフォルトブランチの祖先になることはないため、--is-ancestorは完全に成功したマージ後でも永久に終了コード1を返します。代わりに「MERGED + headRefOid一致」ガードを使用します(安全条件2b)。
gh pr merge <pr-number> --repo <owner>/<name> --merge # スカッシュポリシーのリポジトリ:--squash、その後ガード2bを使用 gh pr view <pr-number> --repo <owner>/<name> --json state,mergedAt,mergeCommitgh pr mergeに関する既知の動作(実戦でテスト済み):- ローカルのデフォルトブランチが別のワークツリーでチェックアウトされている場合、リポジトリ内から実行された
gh pr merge --delete-branchは、リモートマージが成功した後でも、ローカルの後処理中に失敗する可能性があります。リポジトリの外部から--repoを指定してマージを実行すると、ローカルの後処理を完全にスキップし、失敗クラスを事前に回避します。 - それでも失敗が報告された場合は、最初に
gh pr view --json stateを確認します。リモート側は多くの場合既にMERGEDです。その場合は、以下の安全条件の後で、残りのローカル手順(ブランチ削除、ワークツリー削除)を手動で完了します。 - リポジトリ内から実行された
gh pr mergeは、現在のワークツリーをデフォルトブランチに早送りする可能性があります。その出力に以前のPRからの大きな更新が表示されても異常ではありません。
安全条件
破壊的な手順(ワークツリーの削除、ブランチの削除、プルーン)の前に、実行時に以下のすべてを確認します。削除の前に検証を実行し、決して削除後には実行しないでください。削除前のチェックをスキップして「後で検証する」ことはガードを無効にします:未マージの作業ミスは、ブランチが既に存在しない場合にのみ明らかになります(実戦の教訓)。共有チェックアウトでは、ステップが承認された時と実行される時の間で状態が変化する可能性があります。時間が経過した場合は、実行する直前にチェックを再実行してください(実戦でテスト済み)。
-
gh pr view <pr-number> --repo <owner>/<name> --json state,mergedAtがMERGEDを報告する。 -
マージモードに一致した未マージ作業検証。モードを混在させないでください:スカッシュ後、ガード2aは正しくマージされたPRでも決してパスしません。また、スカッシュ後の
branch -dは、(origin/fix/...がまだ存在する場合、つまり祖先の保証がない場合)マージ済みを上流にロジックで通過するか、または(上流がなくなった場合)「完全にマージされていない」として永久に拒否されます。- 2a —
--merge(マージコミット)モード:git -C <repo> merge-base --is-ancestor fix/<task> origin/<default>が終了コード0を返す(PowerShellでは$LASTEXITCODE、POSIXシェルではecho $?で確認)。ブランチを-dで削除すると、何かが未マージのまま残っている場合、git自体が拒否します(実戦でテスト済み)。注意:-dは、上流が設定されている場合はその上流に対して「マージ済み」を判断し、そうでない場合はHEADに対して判断します。そのため、リモートブランチが削除およびプルーンされた後、チェックアウトのHEADが古くて無関係なブランチに置かれている場合、-dはPRが正しくマージされていてもnot fully mergedで拒否する可能性があります(実戦でテスト済み)。その場合、--is-ancestorチェックが終了コード0を返した直後に限り、-Dで削除しても許容されます。これはガード2bと同じガード-then--Dの形状です。ガードなしの-Dは禁止されたままです。 - 2b —
--squash/--rebaseモード:state、mergeCommit、およびheadRefOidをgh pr view <pr-number> --repo <owner>/<name> --json state,mergeCommit,headRefOidで取得します。mergeCommitはJSONオブジェクトであることに注意してください。コミットハッシュはそのoidフィールドです(-q .mergeCommit.oidで抽出します)。headRefOidはプレーンな文字列です。次に両方を確認します:(i)git -C <repo> merge-base --is-ancestor <mergeCommit-oid> origin/<default>が終了コード0を返す、かつ (ii)git -C <repo> rev-parse fix/<task>がheadRefOidと等しい。チェック(ii)はスカッシュモードにおける「コミットが残されていないこと」の代替です:マージされたPRのHEADが正確にローカルブランチの先端であることを証明します。両方が成立した場合のみ、明示的なgit branch -Dで削除します(このガードが-dの代わりとなります)。 ガード2bのトポロジーと拒否パスは、使い捨ての合成git履歴がscripts/test-cleanup-guards.ps1でカバーされています。テストは、システム/グローバルのgit設定、フック、署名、およびrebase.updateRefsを分離します。周囲のGIT_*変数をすべてスナップショット、クリア、復元して、リポジトリ/トレースパスがフィクスチャからエスケープできないようにします。また、再帰的なクリーンアップを、OS対応のパス比較と再解析ポイント拒否を使用して、生成された直接の一時子に制限します。実際のGitHubスカッシュパスは2026-07-23にPR #2で測定されました:MERGED、着地したmergeCommit、変更されていないheadRefOidと一致するローカルおよびリモートの先端、ガードされたローカル-D、および明示的なリモート削除がすべて合格しました。実際のGitHubリベースパスは2026-07-26にPR #5で測定されました:MERGED、書き換えられた着地したmergeCommit、デフォルトブランチの祖先の外側にある元のHEAD、変更されていないheadRefOidと一致するローカルおよびリモートの先端、ガードされたローカル-D、明示的なリモート削除、および所有するワークツリーのクリーンアップがすべて合格しました。
両方のモードで共通:どちらのガードもパスしない場合、決してブランチを削除しないでください。PR本文に記載された未マージのコミットがそのまま残ります(安全マージン:フィールドの教訓)。
- 2a —
-
リモートブランチを削除します(存在する場合)。
--delete-branchが上記のローカル後処理で失敗した場合、またはガード2a/2bが通過した後に明示的に削除する場合:git push origin --delete fix/<task> -
ワークツリーを削除します。
git worktree removeが直接失敗した場合(ワークツリーのgitバージョンはWindowsのディレクトリジャンクションを常に扱えるとは限りません)、フォールバック手動削除を使用します:git -C <repo> worktree remove <worktree> 2>$null # 失敗した場合、手動で削除し、続いてgit worktree pruneを実行: if ($LASTEXITCODE -ne 0) { # 最初にすべてのリンクを削除します(手順4、安全条件5)。 cmd /c rmdir "<worktree>\node_modules" Remove-Item -Recurse -Force <worktree> git -C <repo> worktree prune }POSIX:
git -C <repo> worktree remove <worktree> 2>/dev/null || { # 最初にすべてのリンクを削除します。 rm <worktree>/node_modules rm -rf <worktree> git -C <repo> worktree prune }node_modulesリンクが削除されなかった場合、Remove-Item -Recurse(またはrm -rf)が実際のモジュールディレクトリ(場合によっては数十万のファイルと深いネスト)をターゲットリポジトリから削除し、他のすべてのエージェントとビルドパイプラインを壊します。リンク削除チェックのためのスクリプトテストはscripts/test-worktree-cleanup.ps1にあります。このテストは、OS対応のパス比較、再解析ポイントの検出、およびシンボリックリンクの解決を使用して、クリーンアップがその直接の子のみを削除することを確認します。 -
node_modulesまたは別のリポジトリへのジャンクション/シンボリックリンクを決してそのまま残さないでください(安全条件5)。手動削除を使用する場合、リンクを最初に削除する必要があります。各リンクの存在を個別にチェックし、その型に適した方法で削除します(上記の手順4を参照)。
禁止されている操作
これらのパターンは、共有されたダーティなチェックアウトで、推論できない破損を引き起こすことが知られています(実戦で証明済み)。このスキルを無効にしないでください。
- 禁止:ダーティなチェックアウト内で
git stashしてブランチを切り、何かをコミットし、後でgit stash popする。このフローは、stash listに依存したり、git stashが変更を確実に再適用することに依存したりします。ポップは、コンテキスト外の変更(別のセッションの)が一緒にポップされたり、競合が解決されずに残ったり、スタッシュエントリが失われたりする可能性があります。上記の手順(分離されたワークツリー)は、これらの問題をすべて回避します。 - 禁止:ダーティなチェックアウト内で
git switch -c <branch>を実行し、既存のWIPを新しいブランチにコミットする。これは新しいブランチを過去のダーティ状態に固定し、コミットをリベース、スカッシュ、または対話的に修正することを困難にし、PRをレビュー不能にし、後で分割できません。 - 禁止:クリーンアップ前に
git fetch --pruneを実行する — リモート追跡ブランチを削除すると、git branch -dの「マージ済み」チェック(origin/fix/<task>に基づく)が停止します。代わりに、git remote prune origin --dry-runを使用して、ブランチが削除されているかどうかを確認しますが、まだプルーンしないでください。 - 禁止:マージされたPRが存在しない状態で、リモートブランチ
origin/fix/<task>の存在に基づいて「マージ済み」と推測する。マージはブランチをリモートに残す可能性があり(マージコミットモードでは、GitHubはブランチを自動的に削除しません)、マージされていないブランチをクリーンアップすると、コミットが失われます。
前の禁止事項に違反した結果、実際に発生した例:
- 「
git stash」の違反:1つのセッションがスタッシュし、別のセッションが突然のcheckout(git操作の兆候)を引き起こし、最初のセッションのスタッシュエントリを無効にし、復元できない変更が発生しました。 - 「
git switch -c」の違反:1つのセッションがブランチを作成し、別のセッション(同じチェックアウト内)がgit commitを実行し、1つのPRに2つの異なるタスクの変更が含まれ、PRの説明に「これらの変更の一部は私のものではありません」というメモが含まれていました。 - 両方の違反:未追跡のWIPファイルが誤ってコミットされ、
git revertで元に戻そうとした試みは、共有チェックアウト内の他のセッションによって妨げられました。
実戦の教訓
- ワークツリーの場所:PowerShellで
New-Item -ItemType Junctionを使用して、ワークツリー内にnode_modulesリンクを作成します。削除はcmd /c rmdirを使用する必要があります。Remove-Itemはリンクのターゲットを削除します。これは、cmd /c rmdirがターゲットに従わない唯一の安全な削除方法です。 - マージモードガード2aの偽陰性:マージコミットモードで、
git branch -dは「マージ済み」を判断する際に、上流(設定されている場合)またはHEADのいずれかを参照します。リモートブランチが削除され、プルーンされた後、チェックアウトのHEADが無関係なブランチにある場合、-dはPRが正しくマージされていても拒否します。この場合、--is-ancestorが終了コード0を返した後に-Dを使用することは許容されます。 - クリーンアップのタイミング:破壊的な手順の直前。共有チェックアウトでは、承認時と実行時の間で状態が変化する可能性があります。
- エージェントは、この手順をスキップしたり、安全条件を変更したり、独自の「よりクリーンな」バージョンで代用したりしないでください。各ガードは、実戦で観察された特定の失敗モードに対処します。
- CIポーリングのタイムアウト:
gh pr checks --watchの代わりに上限付きループを使用すると、ハーネスのハングを防ぎます。適切な上限はリポジトリのCI期間によって異なります。gh pr checks <pr-number> --repo <owner>/<name> --watch=falseは、CIがまだ実行中の場合でも終了コード0で戻ってくる可能性があります。返された状態を確認するか(gh pr view --json statusCheckRollup)、または状態または期間に基づいてループを調整します。 gh pr createはPR番号を返しません。gh pr createの出力を解析してPR番号を取得し、後の手順で使用します。
所有権とクリーンアップ
このスキルは、_worktrees/内のワークツリーとそれに関連するブランチのみをクリーンアップします。リポジトリ自体は変更しません。常にgit -C <repo>と--repo <owner>/<name>を使用するため、カレントディレクトリのリセットに対して回復力があります。
テスト
クリーンアップガードのテストはscripts/test-cleanup-guards.ps1にあります。ワークツリークリーンアップのテストはscripts/test-worktree-cleanup.ps1にあります。どちらも、システム/グローバルのgit設定、フック、署名、およびrebase.updateRefsを分離します。GIT_*変数はスナップショット、クリア、および復元されます。クリーンアップは、OS対応のパス比較と再解析ポイントの拒否を使用して、生成された直接の子に制限されます。
各保護テストでカバーされるパスは安全条件2bに記載されています。
開発
変更はこのリポジトリ自体を介して送信する必要があります。なぜなら、このリポジトリには合成git履歴テストとワークツリーリンク削除テストが含まれているからです。これらのテストは、共有チェックアウトなしでは実行できません。したがって、変更は分岐し、テストされ、PRとして送信され、このスキルとまったく同じ安全条件を使用してマージされます。