funador/claude-code-merge-queue
GitHub: funador/claude-code-merge-queue
为并行运行多个 Claude Code agent 而设计的本地零成本合并队列,通过串行化发布操作避免推送冲突和资源竞争。
Stars: 293 | Forks: 0
# Claude Code 合并队列 🚦
Claude Code 已经隔离了你的 agent —— `--worktree`(或者子 agent 上的 `isolation:
"worktree"`)原生地为每个 session 提供了独立的 git worktree,无需
任何设置。这部分已经解决了。Claude Code Merge Queue 解决的是之后的问题:当
四个相互隔离的 agent 试图*同时*进行发布(land)、构建和测试时会发生
什么。
- 🏁 所有人都推送到同一个分支,有人会在竞争中失败,
被拒绝的推送变成了 rebase,这有时又会变成*另一次*
被拒绝的推送。
- 🔥 完整的构建非常消耗资源。四个构建同时运行会把你的笔记本电脑
变成取暖器。
- 🎲 如果你的测试命中了共享的数据库,并发运行会相互抢占彼此的
重置操作。失败看起来像是偶发故障。它们其实并不是偶发故障。它们只是诚实地反映了冲突。
这些都不是技术不行。当多个快速且自信的进程在没有流量控制的情况下共享一个可变的东西时,就会发生这种情况。
要求 agent 去“请协调一下”解决不了问题。一个 agent(或者一个匆忙的
队友)会在最不应该的时刻,精确地一次性打破既定规范,并且毫无
恶意。
**所以,不要讲客套。让冲突变得不可能发生。** 🚦
**为并行 Claude Code agent 设计的本地、零成本合并队列。**
## ⚡ 快速开始
```
npm install --save-dev claude-code-merge-queue # or: pnpm add -D / yarn add -D / bun add -d
npx claude-code-merge-queue init
```
这完成了整个设置,而不仅仅是生成配置文件:
- **`claude-code-merge-queue.config.mjs`** — 从你当前的分支自动检测 `integrationBranch`,从 package.json 自动检测 `checkCommand`
(`check:push` / `check` / `ci` / `test`,匹配到的第一个生效)。
- **`CLAUDE.md`**(如果你已经有了,会追加到其中)—— 这是让整个
过程完全自动化的部分。Claude Code 在每次 session 中都会自动读取它,它会指示 agent 在测试通过后自行完成发布(land),而无需被
提醒。请参阅下文的“无人值守部分”。
- **`.claude/settings.json`** —— 接入 `WorktreeCreate` hook(如果是新建,或者合并到你现有的设置中,且不会触碰已有的任何其他
配置)。
- **`.husky/pre-push`** —— 如果你已经有 Husky,则会创建或追加到其中。如果没有,`init` 会明确提示你,而不是静默地写入
未追踪且不与团队共享的 `.git/hooks/pre-push` 中。
- **`package.json`** 脚本 —— 添加 `land`、`sync`、`promote`、`preview` 和
`preview:restore`,跳过任何你已经自己定义过的脚本。
- **`claude-code-merge-queue-preflight.mjs`** + `preland`/`presync` 脚本 —— 一个自包含的安全网,npm 会在 `land`/`sync` 之前自动运行。如果
这个工具自己的名称/bin 再次发生更改(它已经改过一次 —— `lanekeeper` →
`claude-code-merge-queue`),并且某条开发线(lane)还没有 rebase 过那个节点,它的
`package.json` 依然会调用那个不存在的旧名称 —— 这是一个赤裸裸且
令人困惑的 `sh: lanekeeper: command not found`。这个脚本会用实际的诊断信息捕获那个
情况(“这个分支相对于
origin/<branch> 已经过时 —— 请先 rebase”)。它故意采用了普通的
JS 编写,对 `claude-code-merge-queue` 本身零依赖,因此即使在未来重命名后它也能继续
工作。
**提交它写入的所有内容**,然后你就可以开始运行了。只需两步,不需要设置
指南。
如果 `init` 无法检测到 `checkCommand`(在 package.json 中没有匹配的脚本),那么在设置完成之前,每次推送都会被**拦截** —— 请参阅下方的 🧰 包含哪些功能。这是
刻意为之的。
从这里开始:执行 `claude --worktree
` 启动一条隔离的开发线 —
Claude Code Merge Queue 的 hook 会接管后续操作,CLAUDE.md 会告诉 agent 剩下该怎么做。
当你真正想要发布(ship)时,你只需要出现并运行 `claude-code-merge-queue promote` 即可。🚀
## 🆚 与 GitHub Merge Queue 的对比
GitHub 已经提供了一个 merge queue。它会让你付出两项代价,而本项目不会:
| | GitHub Merge Queue | Claude Code Merge Queue |
|---|---|---|
| 私有仓库 | **仅限 Enterprise Cloud** | 任何套餐,任何仓库 |
| 每次发布的成本 | GitHub Actions 分钟数,每次队列尝试都会消耗 | $0 —— 在你自己的机器上运行 |
| 必要条件 | 需要发起 Pull Request | 无需任何条件 —— 直接 rebase + push |
理念相同 —— 将发布操作串行化,合并前进行测试,保持历史记录整洁 —— 区别在于在本地运行,而不是在别人计费的云上。💸
## 🧰 包含哪些功能
| 命令 | 功能描述 |
|---|---|
| `claude-code-merge-queue hook worktree-create` | 一个 Claude Code 的 `WorktreeCreate` hook。将 Claude Code Merge Queue 编号的开发线接入 Claude 的*原生* worktree 创建机制 —— 而非重新发明轮子。 |
| `claude-code-merge-queue build-lock -- ` | 串行化运行你的 ``(你的构建命令),在机器范围内跨所有开发线执行。 |
| `claude-code-merge-queue land` | 通过 FIFO 队列将你的开发线 rebase 并推送到 integration 分支上,确保两条开发线不会同时处于推送过程中。Agent 会自行运行此命令 —— 见下文。 |
| `claude-code-merge-queue sync` | 快进更新你的主检出(checkout),让开发服务器能看到刚刚合并的代码 —— 并且如果 lockfile 发生变化,它会重新安装依赖,确保每条开发线 symlink 的 `node_modules` 永远不会过期。 |
| `claude-code-merge-queue promote` | 将 integration 分支发布到生产环境。**仅限人工操作** —— 绝不出现在 agent 的指令中,绝不自动化。 |
| `claude-code-merge-queue preview` | 将开发线实时的工作树(包括未提交的更改)即时镜像到主检出上,这样你无需构建即可查看结果。 |
| `claude-code-merge-queue port` | 打印某条开发线的 dev-server 端口,该端口根据其自身的目录名派生。 |
| `claude-code-merge-queue prune` | 按需移除已经合并的兄弟开发线 worktree —— `land` 已经会自动执行此操作,这主要用于“现在立即清理”,而不是等待下一条开发线合并代码。 |
此外还有 🔒 一个 pre-push hook,使得 `land` 成为强制性步骤:直接对 integration 分支执行 `git push` 会被直接拒绝,没有商量的余地。不是 lint
警告。也不是 Slack 提醒。直接拒绝 —— 并告诉你实际应该运行的命令。相同的 hook 还会在允许任何代码合并之前运行你实际的检查(`checkCommand` —
lint/typecheck/test/build);如果没有设置 checkCommand,默认情况下**每一次推送都会失败**,而不是
静默地合并未经验证的代码。
以上每一个拦截点都有真实且经过深思熟虑的绕过方式 —— 见下文的“紧急逃生舱” —— 但你需要明确指出你要推送的具体分支名称,而不是使用一个通用的 flag。
以及 🧪 一个已文档化的扩展点(`src/lib/ephemeral.ts` +
`examples/ephemeral-tmp-dir.example.ts`),用于解决每个设置指南都会忽略的问题:如果你的测试命中了共享资源 —— 数据库、队列或任何有状态的组件 —— 并发的开发线需要拥有各自的临时副本,并且崩溃的运行产生的副本需要能够自行清理,且没有人会注意到它曾经崩溃过。
## ⚙️ 配置
所有配置都集中在一个文件中 —— 请参阅
[`examples/claude-code-merge-queue.config.mjs`](examples/claude-code-merge-queue.config.mjs) 查看包含注释的每个
字段说明。简短版本如下:
```
export default {
branchPrefix: "lane/", // lane/1, lane/2, ...
worktreeSuffix: "-lane-", // ../your-repo-lane-1
portBase: 3000, // lane n gets portBase + n
integrationBranch: "main", // where agents land — see below
productionBranch: null, // set this for a two-stage model — see below
protectedBranches: [], // extra branches beyond the two above; most repos need none
regenerableFiles: [], // files a build tool rewrites — never block a rebase on these
symlinks: [".env", ".env.local", "node_modules"],
buildOutputDirs: ["dist", "build", ".next"], // preview never copies these onto your checkout
checkCommand: "npm run check", // what actually gates a landing — see below
checksRequired: true, // false = deliberately run with none; see below
};
```
这里没有任何内容是针对特定框架或分支模型硬编码的。🧩 格式错误的
配置(分支名为空、端口为负数、`productionBranch` 等于
`integrationBranch`……)在加载任何命令时会立即抛出错误并列出所有问题 —— 而不是在三步之后发生神秘的失败。
## 🚨 紧急逃生舱
每一个被拦截的推送 —— integration 分支、`productionBranch` 以及任何在 `protectedBranches` 中的分支 —— 都有真正的解决途径。只需一个环境变量,无需提示,也无需记住二次验证:
```
CLAUDE_CODE_MERGE_QUEUE_EMERGENCY_PUSH=1 git push origin HEAD:main
```
这是唯一一个坦白说是基于约定,而不是硬性保证的地方:
它能阻止失误和误推,但拦不住自己设置该变量的恶意 agent。
## 🙌 无人值守部分
在这个 pipeline 的任何环节,测试是审查者,而不是人类。
- **`checkCommand` 决定了能否合并。** 任何代码如果不通过此项检查都无法到达 `integrationBranch` —— 这是大多数代码变更会经历的唯一的正确性检查。
- **`claude-code-merge-queue promote` 是一个发布决定,而不是代码
审查。** 它的意思是“这项已经测试通过的工作现在应该发布了”,而不是“我
看过 diff 了”。你在生产分支上的 CI 是第二个
自动化检查点 —— 仍然不是人工检查。
- **当有漏网之鱼混入其中时,修复方法是添加测试,而不是增加一个
审查者。** 每一次失误都会转化为永久的护栏,而不是一次性的
补救。
这并不适用于所有团队 —— 如果你想在每一次变更发布前都有人工进行
审查,那么这个流程是刻意省略了该步骤的。
## 🔁 大部分设计背后的核心理念
构建队列、发布队列和临时资源模式都以**相同的方式**实现了
崩溃安全:
1. 声明占用资源。
2. 用你的进程 ID 标记该声明。
3. 让活跃度检测 —— 而不是超时 —— 来决定声明何时失效。
`queue-lock.ts` 为构建和发布队列实现了这一点;`ephemeral.ts`
为测试资源实现了这一点。对任何处于声明过程中的进程执行 `kill -9`,下一个
进程就会注意到该 PID 已死并重新认领它。
`WorktreeCreate` hook 是这一理念在一次性脚本(没有可检查活跃度的进程)上的延伸应用:声明(占用权)本身就是
worktree,而 `git worktree add` 在路径已被占用时失败则是保障
原子性的防线。
没有过时的锁,没有“重启你的笔记本电脑试试”,也没有神奇的超时数字。✅
## 🔍 了解局限性
敏锐的读者在发问之前应该已经了解以下几点:
- **单台机器,而不是集群。** FIFO 队列存在于本地临时存储中 ——
它无法跨多台笔记本电脑协调。两台机器同时进行发布只会
收到普通的 git non-fast-forward 拒绝(安全的,不会造成数据损坏 —— 失败者会重新拉取并重试,就像今天任何没有使用队列的团队一样)。
- **这不是安全边界。** 这里所有的护栏都是为了阻止失误和
规范偏移,而不是真正具有对抗性的 agent。拥有 Shell 访问权限意味着总是可以执行 `git push --no-verify`、删除 hook 或故意编辑配置 —— 没有任何仅限本地运行的工具能阻止这一切。
- **保证运行了检查 —— 但不保证检查是优秀的。** 它强制要求存在 `checkCommand` 并且通过,但无法分辨那是一个真正的
测试套件还是仅仅执行了 `echo ok`。“测试即审查者”的真实程度仅取决于
测试内容本身。
- **`WorktreeCreate` hook 是这个技术栈中最年轻的部分** —— Claude Code 于 2026 年 2 月推出了它。失去它会优雅降级:手动回退到 `git worktree add`,你依然可以保留构建队列、发布队列、预览以及临时资源等功能,这些全都不依赖于它。
- **缓慢的 `checkCommand` 是真正的吞吐量天花板,而不是免费午餐。** FIFO 锁会在整个持续期间保持锁定 —— 每次只能合并一个,且是机器范围内的。一个耗时 3 到 4 分钟的测试套件会将你的速度限制在每小时远低于 20 次合并,这还是在完全没有队列等待的情况下。
- **遇到 Rebase 冲突会中止,绝不盲目猜测。** 遇到任何冲突都会执行 `git rebase --abort`,工作树保持干净。通常这里的“你”是 agent,而不是人类 —— CLAUDE.md 会指示它解决冲突并重新运行 `land`,就像处理任何其他 bug 一样;`checkCommand` 依然会捕获错误的解决方案。
- **自动修剪会通过 `lsof` 检查是否有活跃的 Claude Code session。** 仅仅是一个已合并的分支是不够的 —— 一个全新的、零提交的开发线也会*轻易地*被视为“已合并”,因此修剪操作也拒绝触碰具有活跃 Claude Code 进程的 worktree这被刻意设定得比“任何进程”更严格:一个孤立的 MCP server 或游离的构建进程可能会比创建它的 session 存活得更久,否则会把废弃的开发线永远卡住(已确认在运行中发生)。缺少 `lsof` 会触发安全关闭机制 —— 将活跃度视为未知,绝不移除。
- **`WorktreeCreate` hook 需要宿主项目自身真正的安装。** 它通过 `npx claude-code-merge-queue hook worktree-create` 运行(原始 hook 不像 `npm run` 脚本那样在 PATH 中拥有 `node_modules/.bin`),如果 npx 无法在本地解析到该包,它会静默回退到一个临时的、未锁定版本的副本 —— 这种情况曾在生产环境中发生过,并掩盖了两次开发线合并期间存在的损坏的安装。该 hook 现在拒绝从 npx 的临时缓存中运行,因此损坏的安装会立即抛出错误,而不是在一个不匹配的替代版本上勉强运行。
## 🧬 项目起源
这是从为了在一个真实的生产代码库上运行多个并行 Claude Code agent 而不相互干扰所构建的工具中提取和归纳出的形态。隐去了名称;但机制
保持不变。
## 📄 许可证
MIT。Fork 它,重命名它,或者对配置的形状提出异议 —— 这正是它的意义所在。标签:AI代理, Git工作流, MITM代理, SOC Prime, TypeScript, 任务调度, 合并队列, 安全插件, 开发工具, 暗色界面, 自动化攻击