Isolated Worktree PR Flow
Procédure pour soumettre une modification sous forme de pull request depuis un
worktree git temporaire créé à partir de la branche par défaut d'origin,
lorsque le checkout principal ne doit pas être touché — car il est sale (travail
en cours non commité), en retard par rapport au dépôt distant, ou partagé avec
d'autres agents. Le flux couvre la création du worktree, les lacunes de
dépendances du nouveau worktree, l'interrogation CI avec limitation, et les
gardes de nettoyage adaptées au mode de fusion.
Pourquoi l'isolation par Worktree
Lorsque plusieurs sessions d'agents entrent dans le même répertoire de travail
(par exemple via un lancement de session par lots), HEAD de git, la branche
courante et les modifications non commitées sont un état partagé — chaque
session déplace le terrain sous l'autre. Dans un incident mesuré, en moins
d'une minute après qu'une session ait créé une branche avec git switch -c, une
deuxième session dans le même checkout a créé une branche à partir de celle-ci,
a commité et fusionné sur la branche par défaut.
Schéma de détection (confirme une collision en quelques minutes) :
- Signes : des fichiers que vous n'avez pas écrits apparaissent, ou
git statusne correspond plus à l'instantané que vous avez pris au début de votre session. - Examen des horodatages :
ls --time-style=full-iso(ou l'équivalent de votre plateforme). Plusieurs fichiers partageant la même milliseconde signifie qu'une opération git (checkout / merge / stash) les a écrits, pas des éditions manuelles ou séquentielles par LLM. git reflog --date=isoétablit la chronologie : qui a fait un checkout, commité ou fusionné, et quand.- Si votre environnement d'agent fournit une fonctionnalité de liste de sessions, utilisez-la pour trouver d'autres sessions en cours avec le même répertoire de travail.
Règles de cession une fois une collision confirmée :
- Le côté qui a commité en premier reste le rédacteur. Le retardataire cesse d'écrire : continuer à commiter ou changer de branche déplace le HEAD de l'autre session et les brise toutes deux.
- Le retardataire apporte plutôt de la valeur en tant que vérificateur : relecture contradictoire de la sortie du rédacteur (revendications du doc vs code, réexécution des tests, vérifications pré-publication) et livrables uniquement par chat qui ne touchent à aucun fichier.
- Nettoyez uniquement les références intermédiaires que vous avez vous-même créées (par exemple une branche vide) ; ne touchez jamais aux références que l'autre session a utilisées.
- Les journaux en mode ajout seulement sont sûrs pour les deux sessions en écriture, sous des en-têtes de section séparés.
Prévention : un dépôt = une session en écriture. Lorsque plusieurs agents
doivent
travailler sur le même dépôt, isolez chacun dans son propre worktree — c'est ce
que fait cette compétence. L'habitude de détection la plus simple est de
re-comparer git status avec votre instantané de début de session juste après
avoir commencé le travail.
Portabilité
Ce flux a été durci sur Windows (PowerShell plus Git Bash), mais les commandes
git et gh elles-mêmes sont multiplateformes. Là où les shells diffèrent, des
exemples PowerShell et POSIX sont donnés. Le CI inclut des jobs PowerShell 7
pour les gardes de nettoyage synthétiques et le scanner de marqueurs privés sur
Ubuntu 24.04 et macOS 15 natif en plus des hôtes Windows. Les trois jobs ont
réussi dans PR #7 run 30216166105.
Les éléments spécifiques à Windows — les jonctions de répertoire et leur
sémantique de suppression — ont des équivalents de liens symboliques POSIX
notés en ligne.
Quand l'utiliser
- Le checkout principal est sale (travail en cours non commité), en retard par rapport au dépôt distant, ou partagé avec d'autres agents, et créer une branche directement dedans pourrait casser ce travail en cours.
- Même pour une petite correction d'hygiène : si le checkout existant est en retard de nombreux commits et contient du travail en cours sale, un worktree isolé est la voie la plus sûre (testé sur le terrain).
- Un flux de travail par branche par tâche est supposé (ne jamais commiter directement sur la branche par défaut). Cette compétence couvre uniquement la mécanique de l'endroit où cette branche réside : un worktree temporaire en dehors du checkout sale.
Procédure
-
Vérifier l'état, de manière non destructive. Exécutez chaque commande
gitavec-C <repo>et chaque commandeghavec--repo <propriétaire>/<nom>, de sorte que rien ne dépende du répertoire courant — les environnements d'agents peuvent réinitialiser le répertoire de travail entre les appels, et sans--repo,ghéchoue avec « not a git repository » lorsqu'il est invoqué en dehors du dépôt.git -C <repo> status --short # enregistrer le WIP pré-travail (vérifié inchangé à la fin) git -C <repo> branch --show-current git -C <repo> fetch origin gh repo view <propriétaire>/<nom> --json defaultBranchRef -q .defaultBranchRef.name # utiliser le résultat comme <default> ci-dessousLes mêmes commandes fonctionnent inchangées dans les shells POSIX.
-
Créer un worktree temporaire à partir de la branche par défaut d'
origin, sans toucher au checkout sale. Remplacez<default>de l'étape 1 — ne codez pas en durmain; les dépôts dont la valeur par défaut estmastersont encore courants.git -C <repo> worktree add -b fix/<tâche> ..\_worktrees\<tâche> origin/<default>POSIX :
git -C <repo> worktree add -b fix/<tâche> ../_worktrees/<tâche> origin/<default>Placez le worktree à un endroit dont l'écriture est confirmée. Les répertoires temporaires système ne sont parfois pas accessibles en écriture depuis des environnements d'agents sandboxés (testé sur le terrain). Préférez un répertoire
_worktrees/à côté du dépôt ou le répertoire temporaire désigné de votre environnement, et avant d'exécuter quoi que ce soit de lourd, écrivez un fichier de test et supprimez-le pour confirmer l'accès en écriture. -
Garder le diff minimal. Modifiez (ou copiez) uniquement les fichiers dont la tâche a besoin, à l'intérieur du worktree. Ne transportez pas en bloc le WIP non suivi du checkout principal — en amont ou d'autres agents peuvent ajouter des fichiers avec les mêmes noms, et les fichiers supplémentaires provoquent des conflits add/add au moment de la fusion ou polluent le diff de la PR. Si vous devez inclure une copie d'un fichier de dépendance non commité du checkout de base, indiquez dans le corps de la PR quel côté gagne lors de la fusion (leçon de terrain : le faire explicitement a évité une mauvaise auto-résolution).
-
Gérer les lacunes de dépendances d'un nouveau worktree. Un nouveau worktree n'a pas de
node_modulesni de virtualenv (testé sur le terrain).-
Node : exécutez
npm cidans le worktree (nécessite un fichier de verrouillage). Pour une vérification légère uniquement, un lien temporaire versnode_modulesdu checkout principal fonctionne aussi — mais supprimez le lien avant le nettoyage (condition de sécurité 5). Jonction PowerShell :New-Item -ItemType Junction -Path <worktree>\node_modules -Target <repo>\node_modules # La suppression doit supprimer uniquement le lien, jamais la cible : utilisez rmdir. cmd /c rmdir "<worktree>\node_modules"Lien symbolique POSIX :
ln -s <repo>/node_modules <worktree>/node_modules # Supprimez le lien lui-même avec rm simple (pas de -r, pas de barre oblique finale). rm <worktree>/node_modules -
Python : réutilisez l'interpréteur du virtualenv du dépôt principal et pointez-le vers les sources du worktree, en exécutant uniquement les tests cibles. PowerShell :
$env:PYTHONPATH = "<worktree>\src" & <repo>\.venv\Scripts\python.exe -m pytest <worktree>\tests\<test_cible>.pyPOSIX :
PYTHONPATH=<worktree>/src <repo>/.venv/bin/python -m pytest <worktree>/tests/<test_cible>.py
-
-
Lorsque la vérification réussit : commiter, pousser et créer la PR. Écrivez le message de commit dans un fichier et passez-le avec
git commit -F <fichier_msg>— les messages multi-lignes en ligne sont fragiles sous les guillemets du shell, surtout sur Windows.git -C <worktree> add <fichiers cibles uniquement> git -C <worktree> commit -F <fichier_msg> git -C <worktree> push -u origin fix/<tâche> gh pr create --repo <propriétaire>/<nom> --head fix/<tâche> ... -
Attendre le CI de protection de branche avec interrogation limitée.
gh pr checks --watchest une attente illimitée — les harnais d'agents et les couches d'approbation peuvent le rejeter ou se bloquer (testé sur le terrain). Utilisez des vérifications ponctuelles avec un nombre maximum de tentatives :gh pr checks <num-pr> --repo <propriétaire>/<nom> --watch=false gh pr view <num-pr> --repo <propriétaire>/<nom> --json state,statusCheckRollupBoucle limitée, POSIX. Là où le harnais bloque
sleepau premier plan (Claude Code le fait), exécutez la boucle comme une tâche d'arrière-plan, ou sautez la boucle et entrelacez les vérifications ponctuelles avec d'autres morceaux de travail :for i in $(seq 1 10); do gh pr checks <num-pr> --repo <propriétaire>/<nom> --watch=false && break; sleep 30; doneBoucle limitée, PowerShell :
for ($i = 1; $i -le 10; $i++) { gh pr checks <num-pr> --repo <propriétaire>/<nom> --watch=false if ($LASTEXITCODE -eq 0) { break } Start-Sleep -Seconds 30 }La limite et l'intervalle corrects dépendent de la durée du CI du dépôt (non vérifié en général — ajuster par dépôt).
-
Fusionner — et décider du mode de fusion en premier, car il détermine la vérification pré-nettoyage du travail non fusionné (condition de sécurité 2) :
--merge(commit de fusion) : les commits de la branche deviennent des ancêtres de la branche par défaut, donc vérifiez avecmerge-base --is-ancestoret supprimez avecbranch -d(condition de sécurité 2a ; testé sur le terrain).--squash/--rebase: les commits de la branche ne deviennent jamais des ancêtres de la branche par défaut, donc--is-ancestorsur la branche termine avec 1 pour toujours, même après une fusion entièrement réussie. Utilisez plutôt la garde « MERGED + correspondance headRefOid » (condition de sécurité 2b).
gh pr merge <num-pr> --repo <propriétaire>/<nom> --merge # dépôts avec politique squash : --squash, puis utiliser la garde 2b gh pr view <num-pr> --repo <propriétaire>/<nom> --json state,mergedAt,mergeCommitComportements connus autour de
gh pr merge(testé sur le terrain) :- Si la branche par défaut locale est extraite dans un autre worktree,
gh pr merge --delete-branchexécuté depuis l'intérieur du dépôt peut échouer lors de son post-traitement local même si la fusion distante a réussi. Exécuter la fusion avec--repodepuis l'extérieur du dépôt saute entièrement le post-traitement local, évitant la classe d'échec en amont. - Si un échec est quand même signalé, vérifiez d'abord
gh pr view --json state— le côté distant est souvent déjàMERGED. Si c'est le cas, terminez les étapes locales restantes (suppression de branche, retrait du worktree) manuellement après les conditions de sécurité ci-dessous. gh pr mergeexécuté depuis l'intérieur du dépôt peut avancer rapidement le worktree actuel vers la branche par défaut ; voir des mises à jour importantes de PR antérieures dans sa sortie n'est pas une anomalie.
Conditions de sécurité
Avant toute étape destructive (retrait du worktree, suppression de branche, prune), vérifiez tous les éléments suivants au moment de l'exécution — exécutez la vérification AVANT la suppression, jamais après. Sauter une vérification pré-suppression et « vérifier après » neutralise la garde : une erreur de travail non fusionné ne ferait surface qu'une fois la branche déjà partie (leçon de terrain). Dans les checkouts partagés, l'état peut dériver entre le moment où une étape a été approuvée et celui où elle s'exécute ; si du temps s'est écoulé, réexécutez les vérifications immédiatement avant l'exécution (testé sur le terrain).
-
gh pr view <num-pr> --repo <propriétaire>/<nom> --json state,mergedAtsignaleMERGED. -
Vérification du travail non fusionné adaptée au mode de fusion. Ne mélangez pas les modes : après un squash, la garde 2a ne passe jamais même pour une PR correctement fusionnée ; et
branch -daprès un squash soit passe à travers sur la logique « fusionné vers upstream » (siorigin/fix/...existe encore — ce qui n'est pas une garantie d'ancêtre), soit est refusé pour toujours comme « pas entièrement fusionné » (si l'upstream est parti).- 2a — Mode
--merge(commit de fusion) :git -C <repo> merge-base --is-ancestor fix/<tâche> origin/<default>termine avec 0 (vérifiez$LASTEXITCODEdans PowerShell,echo $?dans les shells POSIX). Supprimez la branche avec-d, afin que git lui-même refuse encore si quoi que ce soit a été laissé non fusionné (testé sur le terrain). Mise en garde :-djuge « fusionné » par rapport à l'upstream de la branche s'il est défini, sinon par rapport à HEAD — donc après que la branche distante a été supprimée et nettoyée, et avec HEAD du checkout sur une branche plus ancienne et non liée,-dpeut refuser avec « not fully merged » même si la PR a fusionné correctement (testé sur le terrain). Dans ce cas, supprimer avec-Dn'est acceptable qu'immédiatement après que la vérification--is-ancestora terminé avec 0 — la même forme garde-puis--Dque 2b ; un-Dsans la garde reste interdit. - 2b — Mode
--squash/--rebase: récupérezstate,mergeCommit, etheadRefOidviagh pr view <num-pr> --repo <propriétaire>/<nom> --json state,mergeCommit,headRefOid. Notez quemergeCommitest un objet JSON — le hachage du commit est son champoid(-q .mergeCommit.oidl'extrait) ;headRefOidest une chaîne simple. Confirmez ensuite les deux : (i)git -C <repo> merge-base --is-ancestor <oid-mergeCommit> origin/<default>termine avec 0, et (ii)git -C <repo> rev-parse fix/<tâche>égaleheadRefOid. La vérification (ii) est le remplacement en mode squash pour « aucun commit laissé derrière » : elle prouve que la tête de la PR fusionnée est exactement le sommet de votre branche locale. Seulement lorsque les deux tiennent, supprimez avec ungit branch -Dexplicite (cette garde remplace-d). La topologie et les chemins de rejet de la garde 2b sont couverts par des historiques Git synthétiques jetables dansscripts/test-cleanup-guards.ps1. Le test isole la configuration Git système/globale, les hooks, la signature etrebase.updateRefs; capture, efface et restaure chaque variableGIT_*ambiante afin que les chemins du dépôt/de trace ne puissent pas s'échapper du fixture ; et contraint le nettoyage récursif à son enfant temporaire direct généré avec une comparaison de chemin sensible à l'OS et un rejet des points de réanalyse. Le chemin squash GitHub en direct a été mesuré le 2026-07-23 avec PR #2 :MERGED,mergeCommitatterri, pointes locales et distantes inchangées correspondant àheadRefOid,-Dlocal gardé et suppression distante explicite ont tous réussi. Le chemin rebase GitHub en direct a été mesuré le 2026-07-26 avec PR #5 :MERGED, unmergeCommitatterri réécrit, la tête d'origine en dehors de l'ascendance de la branche par défaut, pointes locales et distantes inchangées correspondant àheadRefOid,-Dlocal gardé, suppression distante explicite et retrait du worktree propriétaire ont tous fonctionné.
- 2a — Mode