pnpm + Git 工作树用于多代理开发

当多个 AI 代理需要同时处理同一个 monorepo 时,它们各自都需要一个具有完整功能的 node_modules 的隔离工作副本。 Git 工作树与 pnpm 的 全局虚拟存储 相结合,使之成为可能:每个工作树都有自己的检出和自己的 node_modules,但依赖项通过磁盘上的单个内容寻址存储在所有工作树之间共享。

什么是 Git 工作树?#

通常情况下,一个 Git 仓库只有一个工作目录,并且一次只能关联一个分支。如果你想查看另一个分支,你必须暂存或提交你的更改并切换。 git 工作树 允许你同时检出多个分支,每个分支都在其自己的目录中。 All worktrees share the same repository history and objects. They're just different views into the same repo.

git worktree add ../feature-branch feat/my-feature

这将创建一个新的目录 ../feature-branch,并检出 feat/my-feature,而您的原始工作目录仍保留在其当前分支上。你可以独立在两个目录中工作。

一种常见的模式是使用一个裸仓库(没有自己的工作目录)作为中心,并将所有工作目录创建为工作树:

git clone --bare https://github.com/your-org/your-repo.git your-repo
cd your-repo
git worktree add ./main main
git worktree add ./feature feat/something

为什么需要工作树?#

即使在 AI 代理之前,工作树对于维护一个项目的多个主要版本也是有用的。在我的开发机器上,我使用一个 pnpm 仓库,其中至少有两个工作树:一个在 main 分支上,用于 pnpm v11;另一个在 v10 分支上,用于向后移植和维护版本。 This way, I can fix a bug on v10 without stashing my in-progress v11 work. Both versions are always checked out and ready to go. 过去,在 pnpm 仓库中,2 到 3 个工作树通常就足够了。但是,自从我开始大量使用 AI 代理以来,我需要更多的工作树,才能让我的代理并行处理许多任务。

为什么这对 AI 代理更重要#

借助 AI 编码代理,工作树从方便变成了必不可少。每个代理都需要自己的工作目录来编辑文件、运行构建和执行测试,而不会干扰其他代理。如果没有工作树,这意味着需要多次克隆存储库,每次复制都会复制 git 历史记录。

Worktrees solve the git side. Every agent gets its own isolated checkout while sharing the underlying git objects. 但是每个工作树仍然需要自己的 node_modules,这可能会占用数百兆字节的空间。这就是 pnpm 的 全局虚拟存储 的作用所在:启用它之后,每个工作树的 node_modules 都只包含指向磁盘上单个内容寻址存储的符号链接。这意味着添加新代理程序速度很快,而且几乎不占用额外的磁盘空间。

important

此配置假定工作树和代理共享相同的信任边界。请勿让互不信任的代理程序或用户共用同一个可写的 pnpm 存储。

设置#

1. 创建一个裸仓库#

git clone --bare https://github.com/your-org/your-monorepo.git your-monorepo
cd your-monorepo

2. 为每个分支创建工作树#

# 主开发工作树
git worktree add ./main main

# 代理 A 的功能分支
git worktree add ./feature-auth feat/auth

# 代理 B 的 bug 修复分支
git worktree add ./fix-api fix/api-error

每个工作树都是一个完整的检出,拥有自己的文件,但它们都共享同一个 .git 对象存储。

3. 启用全局虚拟存储#

Add virtualStoreType: global to the pnpm-workspace.yaml in your repository (before v11.23.0 this setting was spelled enableGlobalVirtualStore: true):

pnpm-workspace.yaml
packages:
  - 'packages/*'

virtualStoreType: global

4. 在每个工作树中安装依赖项#

cd main && pnpm install
cd ../feature-auth && pnpm install
cd ../fix-api && pnpm install

第一次执行 pnpm install 会将软件包下载到全局存储中。在其他工作树中进行后续安装几乎是瞬间完成的,因为它们只会创建指向同一存储的符号链接。

工作原理#

如果没有全局虚拟存储,每个工作树都会在 node_modules 中拥有自己的 .pnpm 虚拟存储,其中包含每个包的硬链接或副本。 With virtualStoreType: global, pnpm keeps all package contents in a single shared directory (the global store, which you can find by running pnpm store path), and each worktree's node_modules contains symlinks pointing there:

your-monorepo/(裸 Git 仓库)
├── main/(工作树:main 分支)
│ ├── packages/
│ └── node_modules/
│ ├── lodash → <global-store>/links/@/lodash/...
│ └── express → <global-store>/links/@/express/...
├── feature-auth/(工作树:feat/auth 分支)
│ └── node_modules/
│ ├── lodash → <global-store>/links/@/lodash/... ← 相同目标
│ └── express → <global-store>/links/@/express/...
└── fix-api/ (工作树:fix/api-error 分支)
    └── node_modules/
        ├── lodash → <global-store>/links/@/lodash/... ← 相同目标
        └── express → <global-store>/links/@/express/...

这意味着:

  • Near-zero per-worktree overhead: the local node_modules contains only symlinks to the shared global virtual store. 与 pnpm 的默认行为(将内容寻址存储中的文件硬链接到本地 node_modules/.pnpm 目录)不同,全局虚拟存储意味着根本不会将任何文件复制或硬链接到工作树中。
  • Instant installs for new worktrees: packages are already in the global store.
  • No conflicts: each worktree has its own node_modules tree, so agents can install different dependency versions on different branches without interference.

小技巧#

  • 为代理创建工作树:启动 AI 代理时,为其创建一个专用工作树。该代理程序获得完全隔离,可以编辑文件、运行测试和安装软件包,而不会影响其他代理程序。
  • 清理:当不再需要工作树时,使用 git worktree remove ./feature-auth 将其删除。剩余的工作树虽然成本低廉,但会不断累积。