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):
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_modulescontains 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_modulestree, so agents can install different dependency versions on different branches without interference.
小技巧#
- 为代理创建工作树:启动 AI 代理时,为其创建一个专用工作树。该代理程序获得完全隔离,可以编辑文件、运行测试和安装软件包,而不会影响其他代理程序。
- 清理:当不再需要工作树时,使用
git worktree remove ./feature-auth将其删除。剩余的工作树虽然成本低廉,但会不断累积。