Community写作与编辑github.com

h8nc4y/isolated-worktree-pr-flow

适用于 Claude Code 和 Codex 的代理技能:当主检出处于脏状态、落后于远程或与其他代理共享时,从 origin 的默认分支的临时 git 工作树创建 PR,并包含合并模式感知的清理保护。

isolated-worktree-pr-flow 是什么?

该技能定义了一个严格的流程,用于在 git 工作树中创建和管理 Pull Request,当主仓库检出无法被干扰时(例如有未提交的更改、落后于远程,或被多个代理会话共享)。它解决了在协作的 LLM 代理环境中常见的状态冲突问题,通过强制使用隔离的工作树来防止一个代理的操作破坏另一个代理的工作。该技能提供跨平台(Windows PowerShell 和 POSIX)的详细程序,涵盖从创建临时工作树和处理依赖项间隙,到有界限的 CI 轮询和基于合并模式(合并提交、压缩或变基)的智能清理保护。包含全面的安全条件(S1-S7),以确保仅在 PR 被确认合并且工作被验证已合并后,才删除分支和工作树,从而防止数据丢失。

兼容平台✓Claude Code✓Codex CLI~Cursor
npx skills add h8nc4y/isolated-worktree-pr-flow

Installed? Explore more 写作与编辑 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

隔离工作树 PR 流程

从临时 git 工作树(从 origin 的默认分支切出)提交更改作为 Pull Request 的过程,当主检出不能被触碰时——因为它处于脏状态(未提交的 WIP)、落后于远程,或与其他代理共享。该流程涵盖工作树创建、新工作树依赖项间隙、有界限的 CI 轮询以及合并模式感知的清理保护。

为什么要工作树隔离

当多个代理会话进入同一个工作目录时(例如通过批量会话启动),git 的 HEAD、当前分支和未提交的更改成为共享状态——每个会话都会改变其它会话的基础。在一个已测量的某次事件中,在一个会话使用 git switch -c 创建分支的一分钟内,第二个会话在同一检出中从该分支派生、提交并合并到默认分支。

检测模式(在几分钟内确认冲突):

  1. 迹象:出现了你未编写的文件,或 git status 不再匹配你的会话启动时所做的快照。
  2. 时间戳检查:ls --time-style=full-iso(或你平台的等效命令)。多个文件共享相同的毫秒数意味着 git 操作(checkout / merge / stash)写了它们,而不是手动或 LLM 顺序编辑。
  3. git reflog --date=iso 确定时间线:谁 checkout、提交或合并,以及何时。
  4. 如果你的代理环境提供了会话列表功能,请使用它来查找具有相同工作目录的其他正在运行的会话。

确认冲突后让步规则:

  • 先提交的一方继续担任写入者。后来者停止写入:继续提交或切换分支会移动另一个会话的 HEAD 并破坏双方。
  • 后来者改为作为验证者增加价值:对抗性审查写入者的输出(文档声明 vs 代码、重新运行测试、发布前检查)以及不接触任何文件的仅聊天可交付物。
  • 只清理你自己创建的中间引用(例如一个空分支);永远不要触碰其他会话使用过的引用。
  • 追加式日志对于两个会话都是安全的,可以在不同的章节标题下写入。

预防:一个仓库 = 一个写入会话。当多个代理必须在同一个仓库上工作时,将每个代理隔离在其自己的工作树中——这正是此技能所做的。最便宜的检测习惯是,在你开始工作后立即重新将 git status 与会话启动快照进行比较。

可移植性

该流程在 Windows(PowerShell 加上 Git Bash)上进行了强化,但 git 和 gh 命令本身是跨平台的。在 shell 不同的地方,给出了 PowerShell 和 POSIX 示例。CI 包括了用于在 Ubuntu 24.04 和原生 macOS 15 以及 Windows 主机上进行合成清理保护和私有标记扫描器的 PowerShell 7 作业。这三个作业在 PR #7 run 30216166105 中全部通过。特定于 Windows 的部分——目录交接点及其删除语义——在行内注明了 POSIX 符号链接等效项。

何时使用

  • 主检出是脏的(未提交的 WIP)、落后于远程,或与其他代理共享,并且直接在其中切分支可能会破坏该 WIP。
  • 即使对于小的维护修复:如果现有检出落后许多提交并带有脏的 WIP,隔离的工作树是更安全的途径(经过现场测试)。
  • 假定采用每个任务一个分支的工作流(永远不要直接提交到默认分支)。此技能仅涵盖该分支所在位置的机制:在脏检出之外的一个临时工作树。

步骤

  1. 非破坏性地检查状态。使用 -C <repo> 运行每个 git 命令,并使用 --repo <owner>/<name> 运行每个 gh 命令,以便不依赖当前目录——代理环境可能在调用之间重置工作目录,如果没有 --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 shell 中无需修改即可工作。

  2. 从 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/ 目录或你环境的指定临时目录,并且在运行任何繁重操作之前,写入一个探测文件并删除它以确认写访问权限。

  3. 保持差异最小。在工作树内,只编辑(或复制进)任务所需的文件。不要将主检出的未跟踪 WIP 全部带过来——上游或其他代理可能添加同名文件,多余的文件会在合并时导致添加/添加冲突或污染 PR 差异。如果你必须从基础检出包含一个未提交的依赖文件的副本,请在 PR 正文中说明合并时哪一方获胜(现场教训:明确地这样做避免了一个糟糕的自动解决)。

  4. 处理新工作树依赖项间隙。一个新的工作树没有 node_modules 也没有 virtualenv(经过现场测试)。

    • Node: 在工作树中运行 npm ci(需要一个 lockfile)。如果仅用于轻量验证,临时链接到主检出的 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>.py
      

      POSIX:

      PYTHONPATH=<worktree>/src <repo>/.venv/bin/python -m pytest <worktree>/tests/<target_test>.py
      
  5. 当验证通过时:提交、推送并创建 PR。将提交消息写入一个文件并使用 git commit -F <msgfile> 传递它——内联多行消息在 shell 引用下很脆弱,尤其是在 Windows 上。

    git -C <worktree> add <target files only>
    git -C <worktree> commit -F <msgfile>
    git -C <worktree> push -u origin fix/<task>
    gh pr create --repo <owner>/<name> --head fix/<task> ...
    
  6. 使用有界限的轮询等待分支保护 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 持续时间(通常未经验证——按仓库调优)。

  7. 合并——并且首先决定合并模式,因为它决定了清理前未合并工作验证(安全条件 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 策略仓库:--squash,然后使用保护 2b
    gh pr view <pr-number> --repo <owner>/<name> --json state,mergedAt,mergeCommit
    

    关于 gh pr merge 的已知行为(经过现场测试):

    • 如果本地默认分支在另一个工作树中被检出,从仓库内部运行的 gh pr merge --delete-branch 可能在本地后处理期间失败,即使远程合并成功。使用 --repo 从仓库外部运行合并会完全跳过本地后处理,从一开始就避免了那类失败。
    • 如果仍然报告失败,首先检查 gh pr view --json state——远程端通常已经是 MERGED。如果是这样,在下面的安全条件之后手动完成剩余的本地步骤(分支删除、工作树移除)。
    • 从仓库内部运行的 gh pr merge 可能会快进当前工作树到默认分支;在其输出中看到来自较早 PR 的大量更新并非异常。

安全条件

在任何破坏性步骤(工作树移除、分支删除、prune)之前,在执行时检查以下所有项——在删除之前运行验证,而不是之后。跳过删除前检查和“之后验证”会破坏保护:一个未合并工作的错误只会在分支已经消失时才会显现(现场教训)。在共享检出中,状态可能在步骤被批准和运行时之间漂移;如果时间已经过去,在执行前立即重新运行检查(经过现场测试)。

  1. gh pr view <pr-number> --repo <owner>/<name> --json state,mergedAt 报告 MERGED。

  2. 匹配合并模式的未合并工作验证。不要混合模式:在 squash 之后,保护 2a 对于正确合并的 PR 也永远不会通过;并且 squash 之后的 branch -d 要么通过合并到上游逻辑滑过(如果 origin/fix/... 仍然存在——这不是祖先保证),要么永远被拒绝为“未完全合并”(如果上游已消失)。

    • 2a — --merge(合并提交)模式: git -C <repo> merge-base --is-ancestor fix/<task> origin/<default> 退出 0(在 PowerShell 中检查 $LASTEXITCODE,在 POSIX shell 中检查 echo $?)。使用 -d 删除分支,这样 git 本身仍然拒绝是否有任何东西未合并(经过现场测试)。注意:-d 根据分支的上游(如果已设置)来判断“是否合并”,否则根据 HEAD——因此在远程分支被删除和修剪后,并且检出的 HEAD 位于旧的、不相关的分支上时,-d 可能拒绝并显示 not fully merged,即使 PR 已正确合并(经过现场测试)。在这种情况下,只有在 --is-ancestor 检查退出 0 之后立即使用 -D 删除才是可接受的——与 2b 中相同的保护-然后--D 形状;没有保护的 -D 仍然被禁止。
    • 2b — --squash / --rebase 模式:通过 gh pr view <pr-number> --repo <owner>/<name> --json state,mergeCommit,headRefOid 获取 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) 是 squash 模式对“没有留下提交”的替代:它证明合并的 PR 头正好是你的本地分支顶端。仅当两者都成立时,才使用显式的 git branch -D 删除(此保护替代了 -d)。 保护 2b 的拓扑和拒绝路径由 scripts/test-cleanup-guards.ps1 中的一次性合成 Git 历史覆盖。该测试隔离了系统/全局 Git 配置、钩子、签名和 rebase.updateRefs;快照、清除并恢复每个环境变量 GIT_*,以便仓库/跟踪路径无法逃脱夹具;并通过操作系统感知的路径比较和重解析点拒绝,将递归清理限制在其生成的直接临时子目录中。实时的 GitHub squash 路径于 2026-07-23 使用 PR #2 测量:MERGED、已落地的 mergeCommit、匹配 headRefOid 的本地和远程顶端、受保护的本地 -D 以及显式的远程删除全部通过。实时的 GitHub rebase 路径于 2026-07-26 使用 PR #5 测量:MERGED、一个重写的已落地 mergeCommit、默认分支祖先之外的原始头、匹配 headRefOid 的本地和远程顶端、受保护的本地 -D、显式的远程删除以及拥有的工作树清理全部通过。
  3. 删除远程分支(无论合并模式如何;如果在合并时 --delete-branch 未能执行此操作,或者你使用了 2b 的 guard-then--D 路径并且远程分支仍然存在)。PowerShell:

    git -C <repo> push origin --delete fix/<task>
    # 或 gh api 等效项。
    

    POSIX:

    git -C <repo> push origin --delete fix/<task>
    

    可接受的替代方案 GitHub CLI:

    gh api repos/<owner>/<name>/git/refs/heads/fix/<task> -X DELETE
    
  4. 使用 git -C <repo> worktree remove <worktree_path> 移除工作树。这不是 rm -rf:git 验证没有未提交的更改,并且有多个额外的安全检查。如果由于缺少 unix 域套接字路径(Windows)或 reparse 点(如果工作树包含像 node_modules 这样的目录交接点)而失败,请在 worktree remove 之前检查、报告,并且如果不存在 GIT 相关文件,则可以删除。在任何情况下都不要在工作树之外使用 rm -rf:在移除之前验证工作树路径是预期的、唯一的,并且不匹配 或任何其他已知路径。

    为什么不能使用 rm -rf:如果一个代理对工作树路径的假设是错误的(例如,由于路径转义或假设由先前代理放置的工作树位置),直接删除可能会删除仓库本身。git worktree remove 是唯一安全的路径,因为它应用 git 的内部验证,拒绝破坏仓库的 HEAD 或索引(安全条件 4)。

  5. 如果创建了到主检出 node_modules 或其他目录的链接,请在工作树自身内移除该链接(在 git 工作树移除之前)。使用 rmdir(Windows 上的交接点)或 rm(POSIX 上的符号链接)以确保只删除链接本身,而不是目标(安全条件 5;经过现场测试)。如果工作树包含一个虚拟的 desktop.ini 或其他仅限 UI 的隐藏文件,这些文件不影响 git 并且可以在工作树清理之前删除。

  6. 运行 git -C <repo> worktree prune 以清理 git worktree list 中的数据。这对于确保将来的 git worktree add 使用相同的父级路径没有冲突是必要的。

  7. 验证没有留下任何引用。

    git -C <repo> branch -a | Select-String 'fix/<task>'            # 应该没有输出
    git -C <repo> worktree list | Select-String '<task>'            # 应该没有输出
    if (Test-Path <worktree_path>) { Write-Warning '工作树目录仍然存在' }
    

    POSIX:

    git -C <repo> branch -a | grep 'fix/<task>'
    git -C <repo> worktree list | grep '<task>'
    [ -d <worktree_path> ] && echo '工作树目录仍然存在' >&2
    

在拼接步骤 1-7 之后,PR 已合并,并且本地和远程分支及工作树都被清理了。PR 的 gh pr view <number> --json state,mergedAt 仍然返回 MERGED;该记录是永久的且是只读的。

合并后人工检查

对于人工和 LLM 代理:

  • PR 差异是否反映了目标变更?如果工作树从脏检出复制了文件,PR 可能包含无关或意外的更改。
  • 如果在步骤 4 中链接了 node_modules,请验证链接在工作树移除之前被移除。如果工作树是从脏检出创建的,并且包含主仓库的 WIP 副本,则 PR 可能包含不应提交的更改。
  • 删除上游引用的显式 git push(步骤 3)是 GitHub 上标准 PR 清理的一部分;gh pr merge --delete-branch 已经这样做了,但如果你从仓库外部运行合并,这可能是必要的(参见已知行为)。
  • 最终视觉检查 gh pr view <pr-number> --repo <owner>/<name> --json state,mergedAt,mergeCommit 以确认请求已关闭且已合并。

主检出清理(可选)

合并后,原始(脏)检出的 WIP 仍然存在并且是脏的。如果该 WIP 不再相关,删除它是一个 git 命令(处理 f...)。

相关技能