worktree-hygiene について
エージェント、スクリプト、人間のセッションが共有する Git worktree とブランチを安全に管理するスキルです。削除、所有権の引き継ぎ、並行編集、着地済みブランチの整理に必要な確認手順を定めます。
主な機能
- 01worktree の安全な削除確認
- 02明示的な所有権の引き継ぎ
- 03複数参加者間のファイル管理
- 04着地済みブランチの整理スクリプト
活用シーン
- 01完了した worktree の安全な削除
- 02別のエージェントへの worktree 引き継ぎ
- 03共有ツリーでの編集衝突の回避
- 04着地済みブランチの整理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vivswan/ skills --skill worktree-hygieneインストール後、Claude Code で/worktree-hygieneを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSE.mdREADME.md.codex-plugin/plugin.jsonagents/openai.yamlreferences/retire-branch.mdscripts/retire-branch.mtsscripts/retire/judge.mtsread.mts詳細
Worktree の衛生管理
worktree は、複数の主体で共有される変更可能な状態です。削除は破壊的な操作であり、引き継ぎは所有権の移転です。また、すべての worktree はメインリポジトリの
.gitを共有しています。以下の各ルールは、本番環境で発生した破壊的な障害を防ぐためのものです。
以下でいう「主体」とは、エージェント、サブエージェント、人間のセッション、またはそれらのいずれかが起動したプロセスなど、worktree に書き込みを行うものを指します。
適用する場面
- git worktree を削除するとき、またはブランチが取り込まれた後に整理するとき(その worktree、ローカル参照、必要に応じてリモート参照)
- worktree またはブランチを別の主体に引き継ぐとき、またはそれを所有する主体を再開するとき
- 複数の主体(エージェント、セッション、スクリプト)が同じリポジトリまたは worktree を共有するとき
- 他の worktree がある状態でブランチをチェックアウトしたり、
git configに書き込んだりするとき
Worktree の削除
git worktree remove を実行する前に、次の3点をこの順番で、直前に確認してください。
-
最新のステータスコードでクリーンであること。 最新の
git -C <tree> status --porcelain -uallを実行し、ステータスコードを確認してください。以前の報告にある変更件数を信用してはいけません。件数では新規ファイルと削除処理中の削除を区別できませんが、コードなら区別できます。-uallは、未追跡ファイルを暗黙に非表示にするstatus.showUntrackedFiles=no設定を上書きします。正確なパスが破棄可能だと明確に特定されていないエントリは、すべて削除を止める要因になります。削除処理中の worktree では、削除対象のファイルが
Dとして表示され、連携している主体のキープアライブマーカー(後述)は単独の??になります。そのように特定されていない??、M、Dのエントリは実作業です。- クリーンな tree を削除しても、追跡対象のものは失われません。そのコミットは参照上に残ります。
- 無視対象ファイルは通常の status では見えませんが、tree とともに失われます。ローカルデータベースや取得データなど、重要な無視対象の成果物が worktree にある可能性がある場合は、まず
git -C <tree> status --porcelain -uall --ignoredで一覧表示してください。--ignoredだけではstatus.showUntrackedFiles=no設定を上書きしないため、ここでも-uallを付けてください。
-
永続的な参照上にあること。 DETACHED worktree の固有のコミットは、その worktree 専用の HEAD からしか到達できない場合があります。worktree を削除すると、reflog も含めてその HEAD が削除されます。
git -C <tree> symbolic-ref -q HEADがゼロ以外で終了する場合、detached 状態です。削除する前に、git -C <tree> rev-parse HEADの結果にブランチまたはタグを付けるか、そのコミットが永続的な参照から到達可能であることを証明してください。 -
稼働中の書き込み主体がいないこと。 作業ディレクトリが worktree 内にあるプロセスを確認してください(
lsof -a -p <pid> -d cwdまたはlsof +D <tree>)。実行中の一覧にいない主体自身は書き込みをしていませんが、その主体が起動したプロセス(テストの連鎖、インストールなど)はまだ動作している可能性があります。先に終了させるか、完了するまで待ってください。git worktree list --porcelainに理由とともにlockedと表示されるロック済み tree や、別の主体が使用している可能性のある tree は、所有者と使用目的がわからないまま削除してはいけません。
削除後:
- 必ず
git worktree removeで削除し、単独のrm -rfは使わないでください。 手動で削除すると、共有 git ディレクトリ内に worktree の管理エントリが残ります。その結果、Git はそのブランチがチェックアウト中だと扱い続け、エントリが整理されるまでブランチの削除を拒否します。git worktree prune --expire nowは、古くなったエントリをただちに消去します。オプションなしのpruneはgc.worktreePruneExpire(デフォルトでは3か月)に従うため、削除直後の tree のエントリが残り、ブランチの削除が引き続きブロックされることがあります。Git は最終的には自動でエントリを期限切れにしますが、そのタイミングを当てにしてはいけません。- prune の前にアンカーを設定してください。prune は対象となった各エントリの専用 HEAD と reflog を削除し、対象は自分のものだけでなく、見つからないロックされていないすべての tree です。
git worktree list --porcelainは prunable な各エントリを示し、記録されたHEADの SHA を出力します。そのため、detached 状態のものがあれば、先にブランチまたはタグを付けてください(ルール2)。
- 削除しても、残ったプロセスは終了しません。 完了した主体が起動した停止状態のテスト処理は、ディレクトリが削除された後も何時間も残ることがあります。削除したパスを作業ディレクトリとするプロセスがないか再確認し、あれば終了させてください。
ブランチも削除する
ブランチ上のツリーを削除しても、そのブランチは残ります。作業がメインラインに確実に保存されたと確認できた場合にのみ、ブランチを削除してください。scripts/retire-branch.mts(以下の「取り込み済みブランチの廃止」)は、その確認と削除を、ゲートで保護された1回の実行で行います。いずれの場合も、次の2つのルールが適用されます。
-
git branch -dを決して信用しないでください。 このコマンドが確認するのは、ブランチに設定されたアップストリーム(設定されていない場合はHEAD)へのマージだけです。通常、アップストリームはメインラインではなく、そのブランチ自身のorigin/<topic>です。そのため、未取り込みの作業でも成功する場合があり、取り込み済みの作業でも拒否される場合があります。どちらの結果も、何かを証明するものではありません。 -
サーバー側の参照を削除するのは、検証済みのshaに固定したリースを使う場合に限ってください。検証後、削除前にpushが行われた場合、削除してしまうのではなく拒否されます。
git push <remote> --force-with-lease=refs/heads/<topic>:<verified-sha> :refs/heads/<topic>
自動削除によって、稼働中のワークスペースが破壊されることがあります
分離されたワークツリーを自動でクリーンアップするハーネスは、アクターがクリーンなツリーの状態で処理を完了すると、そのワークツリーを削除します。何も書き込まれる前にアクターが「待機する」ために停止し、そのツリーで他のアクターがまだ作業していると、作業中のディレクトリが削除されてしまいます。
-
アクターが自身の分離ワークツリー内で他のアクターと連携する場合は、開始時にただちにツリーを変更済みにしてください。 ワークツリーのルートに未追跡のマーカーファイルを1つ置くだけで十分です(例:
.orchestrator-keepalive)。マーカーが
git status --porcelain -uallで実際に??として表示されることを確認してください。ここでも-uallを指定してください。そうしないと、status.showUntrackedFiles=noの設定によって、この確認に必要な証拠が隠されることがあります。無視ルールでマーカーが隠れる場合があります。隠れたマーカーでは何も保護できないため、その場合は別の名前を選んでください。マーカーはステージングもコミットもしません(削除ルール1では、これを唯一の削除可能な
??として指定しています)。最終シグナルまたは引き継ぎの前に削除してください。 -
分離ワークツリー内でクリーンな状態のまま停止したアクターを再開する前に、ワークツリーがまだ存在することを確認してください。 存在しない場合は、新しく起動してください。
取り込み済みブランチの廃止
scripts/retire-branch.mtsは、廃止処理を1つのプログラムとして実行するため、ゲートに失敗した場合、破壊的な処理には到達しません。メインラインを実行が所有する参照にfetchし、ブランチが取り込み済みであることを証明し、ブランチのワークツリーに対して上記の削除ルールを適用してから、ワークツリーを削除し、検証済みのshaに固定されたローカル参照を削除します。
まずCIの判定を確認してください。/watch-githubスキルが判定結果を返します。次にリハーサルを行います。--executeを指定しない場合、すべてのゲートを実行しますが、何も削除しません。
bun "<skill-dir>/scripts/retire-branch.mts" --branch feature/thing
[fetch] origin/main -> 4f2a9c... (the remote-tracking ref was already current)
[branch] refs/heads/feature/thing is at 91bd0e...
[landed] by three-way merge: feature/thing merges into 4f2a9c without changing its tree
[worktree] /repo/.worktrees/thing is clean and idle
[worktree] would remove /repo/.worktrees/thing (add --execute)
[delete] would delete refs/heads/feature/thing at 91bd0e... (add --execute)
REHEARSED: every gate passed for feature/thing; nothing was destroyed
その後、本番で実行します。
bun "<skill-dir>/scripts/retire-branch.mts" --branch feature/thing --execute
| 終了コード | 意味 | 対応 |
|---|---|---|
| 0 | すべてのゲートに合格(--executeを指定した場合は、すべての手順を実行) | 完了 |
| 1 | 証拠に基づいてゲートが拒否:未取り込み、ワークツリーに作業が残っている、参照が移動した | 拒否の内容を確認してください。何が妨げになったかが示されています |
| 2 | 使用方法のエラー | 呼び出し方を修正してください |
| 3 | 測定の破損:解決できない参照、gitの失敗、実行できなかったプローブ、浅いクローン | 問いに対する回答は得られていません。ゲートを外して再試行しないでください |
bun "<skill-dir>/scripts/retire-branch.mts" --help:
usage: retire-branch.mts --branch <name> [options]
--branch <name> the branch to retire (required)
--repo-root <path> repository to operate in (default: cwd)
--remote <name> remote to fetch the mainline from (default: origin)
--mainline <name> branch the work must have landed on (default: main)
--original-tip <sha> pre-rebase tip to also prove landed
--no-writer-check skip the lsof live-writer probe (hosts without a usable lsof)
--execute actually remove the worktree and delete the branch
prints: one [gate] line per step; without --execute the destructive steps read 'would ... (add --execute)' and
the run ends 'REHEARSED: every gate passed for <branch>; nothing was destroyed'
exits: 0 every requested step passed; 1 a gate refused on the evidence;
2 usage error; 3 broken measurement (the question was never answered)
--original-tipは、取り込み前にリベースされたブランチに使用します。OLD tipも取り込み済みであることを証明する必要があります。--no-writer-checkは、lsofで確認できないホスト向けのオプションです。この場合、実行時にその旨が明示されます。
squashマージまたはrebaseマージでは、ブランチのshaがメインラインに残らないため、祖先関係の確認は拒否されます。このスクリプトは、ブランチを先端にマージします(git merge-tree --write-tree、git 2.38以降)。先端のツリーが返されれば、メインラインにない内容をブランチが持っていないことを意味します。同じファイルに対する後続のメインライン編集も含まれます。どのゲートを通過したかは、[landed] by ancestryまたは[landed] by three-way mergeと表示されます。各ゲートの理由については、references/retire-branch.mdを参照してください。
ワークツリーの引き継ぎ
所有権は明示的に移譲されるものであり、推測によって移譲されることはありません。ワークツリーの所有者は常に1つだけです。単一のアクター、またはワークツリー内にファイルごとの重複しない担当範囲を付与した調整役アクター(下記の「ファイルの所有権」を参照)のいずれかです。引き継ぎは、名前を付けて明示するイベント(停止と権限付与)であり、沈黙から推測するものではありません。
- まず前任者を停止します。 完了済みまたはアイドル状態のエージェントにメッセージを送ると、再開します。引き継ぎ後に送る確認やお礼のメッセージによって前任者が起動し、後任者が所有するワークツリーへの書き込みを再開してしまいます(稼働中の書き込み元による上書き)。順序は常に、まずアクターを停止し(Claude Code では TaskStop)、その後の別れの挨拶は不要です。
- 編集前に、後任者が稼働中の書き込み元がいないか確認します。まずツリー内を cwd とするプロセスを確認し(削除ルール3と同じ lsof チェック)、次に変更の激しいファイルのハッシュを取得し、待機してからもう一度取得します(
shasum <file>; sleep 5; shasum <file>)。これらの確認で得られるのは証拠であり、確証ではありません。書き込み元は編集の合間にアイドル状態になったり、別のファイルを書き込んだりする可能性があります。所有権の不変条件は、上記の明示的な停止と権限付与です。 - 削除したツリーのブランチは、回収した人に渡ります。 アクターを停止してそのワークツリーを削除すると、そのブランチは回収者にのみ移譲されます。そのブランチの修正作業は、新しいワークツリー内の新しいアクターに割り当てます。
- 解放したアクターを決して復活させないでください。 停止済みのアクターにメッセージを送ると、存在しなくなったディレクトリで再開してしまいます。ワークツリーを削除した後は、そのアクターに二度とメッセージを送ってはいけません。
並行するアクター間のファイルの所有権
複数のアクターが1つのワークツリーに書き込む場合、または複数のラウンドでアクターが同じファイル群を扱う場合:
- すべてのラウンドを通じて、各ファイルの所有者は必ず1つです。 各アクターに、対象ファイルを明示的に列挙した許可リストを割り当てます。
- 後続ラウンドのリストは、それ以前のラウンドが対象としたすべてのファイルをファイルリストに基づいて差し引いて作成します。「完了したように見えるか」では判断しません。
- 前のラウンドのアクターがまだ書き込み中である可能性がある間は、次のラウンドを開始してはいけません。
前のラウンドの所有権が残っているファイルを再割り当てすると、完全一致の編集セマンティクスによって完全な破損を防げる場合でも、書き込みの競合や作業の重複が発生します。
1つのブランチにつき1つのワークツリー
Git はデフォルトでは、すでにいずれかのワークツリーでチェックアウトされているブランチのチェックアウトを拒否します(「already used by worktree」)。そのため、1つのブランチを保持できるツリーは最大1つです。他の誰かがそのブランチを取得する前に、保持者が解放しなければなりません。
git -C <holding-tree> checkout --detach # or check out another branch there,
# or remove the worktree (rules above)
git checkout <branch> # only now succeeds elsewhere
この拒否はガードであり、ロックではありません。git worktree add --force と git checkout --ignore-other-worktrees を使えば回避できます。稼働中のツリーが保持するブランチを取得するために、これらを決して使わないでください。1つのブランチを2つのツリーでチェックアウトすることは、同じ ref に対する書き込み元が2つになることを意味します。
ガードに逆らうのではなく、それを前提に計画します。ブランチをチェックアウトした状態で行う必要のある操作(リベース、マージ、スタックツール)は、そのブランチを保持するツリーが解放または削除された後にのみ実行します。所有者が稼働中の間は実行しません。
Worktree はメインリポジトリの .git を共有します
リンクされた各 worktree は、小さなプライベート Git ディレクトリ(HEAD、インデックス、進行中のリベースまたは bisect の状態、refs/worktree/*)を保持します。それ以外はすべて、メインリポジトリの Git ディレクトリ($GIT_COMMON_DIR)を通じて共有されます。設定、リモート一覧、フック一式、ブランチとタグの保存場所は共通です。複数の実行主体が同時に動作する場合、次のような影響があります。
-
git configの書き込みはリポジトリ全体に反映されます。 ある実行主体がrerereを有効にしたり、remote.pushDefaultを設定したり、branch.<name>.*セクションを書き換えたりすると、実行中であっても、すべての兄弟 worktree の動作が同時に変わります。worktree ごとの設定が使えるのは、extensions.worktreeConfigが有効で、書き込み先がconfig.worktreeの場合に限られます。そうでなければ、すべての書き込みが共有されます。 -
ID は共有されます。 あるツリーで
user.emailまたはuser.nameを書き込むと、すべての兄弟ツリーの次のコミットにその値が記録されます。ID はコマンドごとに設定(git -c user.email=...)するか、実行主体に限定した環境変数で設定してください。他の実行主体が動いている間に、共有設定へ書き込まないでください。 -
ブランチとタグは共有されます。 あるツリーでブランチを更新、削除したり、タグを移動したりすると、その変更はすべてのツリーに即座に反映されます。ある実行主体が、あなたが削除したばかりの ref を対象にリベースまたはマージを始めようとすると、原因を特定できないエラーが発生します。そのため、ref の変更を調整するか、兄弟ツリーが稼働していないタイミングで実施してください。
例外はツリーごとの ref です。HEAD、
FETCH_HEADなどの疑似 ref、refs/worktree/*、refs/bisect/*、refs/rewritten/*が該当します。 -
フックはデフォルトで共有されます。 ある worktree でフックをインストールまたは編集すると、すべての兄弟 worktree で次に実行されるコミットに反映されます。例外は、worktree ごとの
core.hooksPathを使う場合、またはツリーごとに解決される相対パスをフックのパスに指定する場合です。そうでなければ、共有されるものとして扱ってください。