chord-cli 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  <a href="LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-blue"></a>
7
7
  <img alt="Node 24 or later" src="https://img.shields.io/badge/node-%E2%89%A5%2024-339933?logo=nodedotjs&logoColor=white">
8
8
  <img alt="Windows and Linux" src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue">
9
+ <a href="README.zh-CN.md"><img alt="中文说明" src="https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-2F7CF6"></a>
9
10
  </p>
10
11
 
11
12
  One repository, as many agents as you care to start. Each agent works in its own worktree; a shared board records who holds which file; an agent that wants a file another agent holds asks first and waits for the answer; when the work is committed, one call rebases the branch and fast-forwards main. Idle agents are woken the moment a message arrives, so the agents negotiate among themselves while you keep handing out work in plain language.
@@ -22,11 +23,24 @@ One repository, as many agents as you care to start. Each agent works in its own
22
23
  - One repository, N agents, one worktree each: parallel work without stepping on each other
23
24
  - A file-level claims board: register before editing, talk to the holder first, and a hook turns away any write that was never claimed
24
25
  - Asynchronous messaging between agents: idle ones are woken, busy ones hear at their next tool call
25
- - `finish` in one step: rebase, fast-forward main, release the claims, tell the others; a conflict stops for a human instead of guessing
26
+ - `finish` in one step: rebase, fast-forward main, release the claims, tell the others; on a conflict nothing changes and the agent is told to rebase, resolve it and finish again, asking you only if it cannot
26
27
  - Agents learn the rules from chord itself, at start-up and on every prompt; you give tasks, not instructions about chord
27
28
  - Agent-agnostic by design: adding a tool means one launch recipe, not a new coordinator
28
29
  - `chord ls` for the whole picture, `messages` and `events` to replay what happened, `stop` and `clean` to wind down
29
30
 
31
+ ## 📊 Benchmark
32
+
33
+ On [CooperBench](https://github.com/cooperbench/CooperBench) two agents each take one feature in the same repository, and a pair passes when the merged result passes the tests of both features. The gpt-6-luna figures cover the pairs where both sides finished.
34
+
35
+ | chord vs | gpt-6.1-sol | gpt-6-luna |
36
+ |---|---|---|
37
+ | solo (one agent, both features) | 76% vs 74% | 63% vs 51% |
38
+ | coop + git (messages and a shared git remote) | 76% vs 73% | 63% vs 47% |
39
+ | team (a lead hands out the work) | 76% vs 69% | **63% vs 34%** |
40
+ | coop (messages only) | 76% vs 11% | 63% vs 11% |
41
+
42
+ On the stronger model chord keeps pace with the best baselines. On the weaker one it is the only two-agent setup that does not fall below a single agent, and it beats the team setting by 20 points. chord's agents got only the feature text, while the coop + git and team prompts spell out how to split and merge the work.
43
+
30
44
  ## 🤖 Supported agents
31
45
 
32
46
  | agent | status |
@@ -100,13 +114,16 @@ chord stop api # let it finish its turn, then quit
100
114
  chord clean # remove the workspaces and branches of agents that have left
101
115
  ```
102
116
 
103
- `--name` fixes an agent's name, workspace and branch, so launching the same name later returns to the same worktree (add `--resume` or `-c` to pick up the conversation too). Run chord in a repository root and it offers to move the repository into a child directory first.
117
+ `--name` fixes an agent's name, workspace and branch, so launching the same name later returns to the same worktree. `chord resume <agent>` does that and also reopens the agent's own conversation, for Claude and Codex alike. Run chord in a repository root and it offers to move the repository into a child directory first.
118
+
119
+ Agents start in the project folder, the same directory you run plain `claude` or `codex` in, so that stays their project too: one `/resume` list, one CLAUDE.md, one trust answer. Each agent edits only its own worktree under `chord/agents/<name>/`; chord refuses edits (editor tools, apply_patch) to the main checkout and to other agents' worktrees, and tells the agent off after a shell command that ran there.
104
120
 
105
121
  ## 🧰 Commands
106
122
 
107
123
  | command | what it does |
108
124
  |---|---|
109
125
  | `chord <agent> [--name x] [tool args...]` | start an agent: `claude` or `codex` |
126
+ | `chord resume <agent> [tool args...]` | start an agent that has left again, in its own worktree, reopening its own conversation |
110
127
  | `chord ls [--all]` | agents and claims of this project, or of every project |
111
128
  | `chord messages [agent] [-n N]` | message history |
112
129
  | `chord events [agent] [-n N]` | claim, release, merge and conflict history |
@@ -118,17 +135,19 @@ chord clean # remove the workspaces and branches of agents that hav
118
135
  `<project>/chord/config.json`:
119
136
 
120
137
  ```json
121
- { "setup": "npm ci" }
138
+ { "setup": "npm ci", "test": "npm test" }
122
139
  ```
123
140
 
124
141
  `setup` runs once in every new worktree before the agent starts, with the `CHORD_*` variables in its environment, so a fresh worktree arrives with its dependencies in place. A failed setup abandons the launch and removes the worktree.
125
142
 
143
+ `test`, when set, runs in the agent's worktree each time `finish` has rebased the branch onto main, before main moves. If it fails nothing is merged and the agent gets the end of the output: a clean rebase can still break what another agent merged. Other agents' `finish` waits while it runs.
144
+
126
145
  A repository's own Claude configuration (`.claude/settings*.json`, `.mcp.json`) reaches an agent once you have trusted that repository in Claude Code yourself; the MCP servers you approved there are the ones that start.
127
146
 
128
147
  ## 🔌 How chord works with your tools
129
148
 
130
149
  - **Waking an agent** means typing a short note into its terminal. chord does this only when the input box is empty, the keyboard has been quiet for a few seconds, and the terminal has echoed the note; a dialogue never receives the Enter.
131
- - **Trust**: chord records the project's agents folder as trusted in `~/.claude.json` once, so Claude does not ask about each agent's workspace. Codex asks you to review chord's hooks once, and again whenever their definition changes; answer "trust all". chord says so at launch if a hook has been switched off.
150
+ - **Trust**: agents run in the project folder, so Claude asks about it once, as it does for plain `claude`; chord writes nothing to `~/.claude.json`. Codex asks you to review chord's hooks once, and again whenever their definition changes; answer "trust all". chord says so at launch if a hook has been switched off.
132
151
  - **Branches and worktrees.** Each agent adds a `chord/<name>` branch and a worktree; `chord clean` removes them when the agent has left and keeps anything not yet on main.
133
152
  - **Permissions.** chord's own tools are pre-approved for Claude, so an agent working on its own is never parked at a prompt for them; your usual shell and edit approvals apply as before. Codex runs with full access. On Windows chord refuses a sandboxed launch, because that sandbox cannot run the hooks; on Linux the hooks run inside the sandbox, but Codex then asks you to approve each chord tool call.
134
153
  - **The terminal title** reads `chord <agent> · <task>`, so each window says which agent it is.
@@ -0,0 +1,169 @@
1
+ # chord
2
+
3
+ **把编码 agent 当成一个集群来跑。**
4
+
5
+ <p align="center">
6
+ <a href="LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-blue"></a>
7
+ <img alt="Node 24 or later" src="https://img.shields.io/badge/node-%E2%89%A5%2024-339933?logo=nodedotjs&logoColor=white">
8
+ <img alt="Windows and Linux" src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue">
9
+ <a href="README.md"><img alt="English README" src="https://img.shields.io/badge/README-English-2F7CF6"></a>
10
+ </p>
11
+
12
+ 一个仓库,想开几个 agent 就开几个。每个 agent 在自己的 worktree 里干活;一块共享的看板记着谁占着哪个文件;想改别人占着的文件,先问,等对方回话;活干完提交了,一个调用把分支 rebase 到 main 并快进。空闲的 agent 一有消息就被叫醒,所以 agent 之间自己协商,你只管用大白话继续派活。
13
+
14
+ <p align="center">
15
+ <picture>
16
+ <source media="(prefers-color-scheme: dark)" srcset="docs/demo-dark.svg">
17
+ <img alt="两个 agent 各在自己的终端里占用文件,通过消息商量一个共用文件,然后 finish 合进 main;chord ls 显示它们的状态" src="docs/demo-light.svg" width="100%">
18
+ </picture>
19
+ </p>
20
+
21
+ ## ✨ 特点
22
+
23
+ - 一个仓库、N 个 agent、每人一个 worktree:并行干活,互不踩脚
24
+ - 文件级的占用看板:改之前先登记,先占的人先说话,没登记过的写入被 hook 挡回去
25
+ - agent 之间的异步消息:空闲的被叫醒,忙着的在下一次工具调用时听到
26
+ - `finish` 一步到位:rebase、快进 main、释放占用、通知其他人;遇到冲突什么都不动,让 agent 自己 rebase、解决、再 finish,解决不了才来问你
27
+ - 规则由 chord 自己教给 agent,启动时讲一遍,每条 prompt 再提醒一次;你只派任务,不用解释 chord
28
+ - 设计上不绑定某一家 agent:接一个新工具只是加一条启动配方,不用新写协调器
29
+ - `chord ls` 看全局,`messages` 和 `events` 回放发生过什么,`stop` 和 `clean` 收尾
30
+
31
+ ## 📊 基准测试
32
+
33
+ 在 [CooperBench](https://github.com/cooperbench/CooperBench) 上,两个 agent 在同一个仓库里各做一个功能,合并后的结果通过两个功能的测试才算这一对通过。gpt-6-luna 的数字只算两边都做完的那些对。
34
+
35
+ | chord 对比 | gpt-6.1-sol | gpt-6-luna |
36
+ |---|---|---|
37
+ | solo(一个 agent 做两个功能) | 76% vs 74% | 63% vs 51% |
38
+ | coop + git(消息加一个共享的 git 远端) | 76% vs 73% | 63% vs 47% |
39
+ | team(一个主管派活) | 76% vs 69% | **63% vs 34%** |
40
+ | coop(只有消息) | 76% vs 11% | 63% vs 11% |
41
+
42
+ 在强模型上 chord 和最好的基线持平。在弱模型上它是唯一不掉到单 agent 以下的双 agent 方案,比 team 高 20 个点。chord 的 agent 只拿到功能描述,而 coop + git 和 team 的 prompt 里把怎么分工、怎么合并都写明了。
43
+
44
+ ## 🤖 支持的 agent
45
+
46
+ | agent | 状态 |
47
+ |---|---|
48
+ | Claude Code | 支持 |
49
+ | Codex CLI | 支持 |
50
+ | OpenCode | 计划中 |
51
+ | Gemini CLI | 计划中 |
52
+ | Cursor CLI | 计划中 |
53
+ | GitHub Copilot CLI | 计划中 |
54
+ | Qwen Code | 计划中 |
55
+ | DeepSeek Harness | 计划中 |
56
+
57
+ 接一个新 agent 需要它的工具提供三样东西:启动时注入 hook 和 MCP server 的办法、hook 事件的名字和载荷、一个空闲/忙碌信号。
58
+
59
+ ## 🧩 工作原理
60
+
61
+ <p align="center">
62
+ <picture>
63
+ <source media="(prefers-color-scheme: dark)" srcset="docs/architecture-dark.svg">
64
+ <img alt="你把任务交给一群 agent,每人一个 worktree;agent 共享一块占用和消息的看板,做完 finish 进 main" src="docs/architecture-light.svg" width="100%">
65
+ </picture>
66
+ </p>
67
+
68
+ 1. **隔离。** 每个 agent 在自己的分支、自己的 worktree 里干活。主 checkout 只通过 `finish` 变化。
69
+ 2. **协调。** 一块共享看板存着占用、消息和事件。agent 用 chord 给的工具登记、占用、对话;hook 在每次写入前查占用,并在它干活时把新消息递过去。
70
+ 3. **合并。** `finish` 把分支 rebase 到 main 再快进。rebase 干净就合进去;其他任何情况都停在原地等你。
71
+
72
+ ## 📦 安装
73
+
74
+ 机器上有 Node 24 和 git:
75
+
76
+ ```
77
+ npm install -g chord-cli
78
+ ```
79
+
80
+ Linux 上加 `--allow-scripts=node-pty`,并装好 python3、make 和 g++:node-pty 不带 Linux 的二进制,安装时现场编译。
81
+
82
+ 或者从源码:
83
+
84
+ ```
85
+ git clone https://github.com/Cedr1cSun/chord-cli && cd chord-cli
86
+ npm install && npm link
87
+ ```
88
+
89
+ ## 🚀 快速开始
90
+
91
+ 一个项目是一个文件夹,仓库放在它的子目录里:
92
+
93
+ ```
94
+ my-project/
95
+ my-repo/ 主 checkout
96
+ chord/ chord 自己的文件,在仓库之外
97
+ ```
98
+
99
+ 在项目文件夹里,一个终端开一个 agent:
100
+
101
+ ```
102
+ chord claude # 开一个 agent
103
+ chord codex --name api # 再开一个,起个名字
104
+ chord claude "fix the flaky test in tests/auth" # 工具名后面的参数原样交给工具
105
+ ```
106
+
107
+ 在这些终端里像平时一样派任务。另开一个终端:
108
+
109
+ ```
110
+ chord ls # agent、各自在做什么、未读消息、分支状态、文件占用
111
+ chord messages api # 某个 agent 说过什么、收到过什么
112
+ chord events # 占用、释放、合并、冲突
113
+ chord stop api # 让它做完这一轮再退出
114
+ chord clean # 删掉已经离开的 agent 的工作区和分支
115
+ ```
116
+
117
+ `--name` 固定 agent 的名字、工作区和分支,以后用同一个名字启动就回到同一个 worktree。`chord resume <agent>` 也是这样,还会接着打开这个 agent 自己的对话,Claude 和 Codex 都一样。在仓库根目录里跑 chord,它会先提议把仓库移进一个子目录。
118
+
119
+ agent 在项目文件夹里启动,也就是你平时直接敲 `claude` 或 `codex` 的那个目录,所以那仍然是它们的项目:同一份 `/resume` 列表,同一个 CLAUDE.md,同一次信任确认。每个 agent 只改 `chord/agents/<名字>/` 下自己的 worktree;chord 拒绝对主 checkout 和其他 agent 的 worktree 的编辑(编辑器工具、apply_patch),shell 命令在那里跑过之后也会说一声。
120
+
121
+ ## 🧰 命令
122
+
123
+ | 命令 | 作用 |
124
+ |---|---|
125
+ | `chord <agent> [--name x] [tool args...]` | 开一个 agent:`claude` 或 `codex` |
126
+ | `chord resume <agent> [tool args...]` | 把离开的 agent 再开起来,回到它的 worktree,接着它自己的对话 |
127
+ | `chord ls [--all]` | 本项目的 agent 和占用,或所有项目的 |
128
+ | `chord messages [agent] [-n N]` | 消息记录 |
129
+ | `chord events [agent] [-n N]` | 占用、释放、合并、冲突的记录 |
130
+ | `chord stop <agent>` | 等 agent 这一轮结束、输入框为空时退出它 |
131
+ | `chord clean [--force] [agent...]` | 删掉离线 agent 的工作区、worktree 和分支;还没到 main 上的工作留着,除非 `--force` |
132
+
133
+ ## ⚙️ 配置
134
+
135
+ `<项目>/chord/config.json`:
136
+
137
+ ```json
138
+ { "setup": "npm ci", "test": "npm test" }
139
+ ```
140
+
141
+ `setup` 在每个新建的 worktree 里、agent 启动前跑一次,环境里带着 `CHORD_*` 变量,这样新 worktree 一来就装好了依赖。setup 失败则放弃这次启动并删掉 worktree。
142
+
143
+ `test` 设置了的话,每次 `finish` 把分支 rebase 到 main 之后、main 移动之前,在 agent 的 worktree 里跑一遍。失败就什么都不合,agent 拿到输出的末尾:rebase 干净也可能弄坏别人刚合进去的东西。它跑的时候其他 agent 的 `finish` 等着。
144
+
145
+ 仓库自带的 Claude 配置(`.claude/settings*.json`、`.mcp.json`)在你自己在 Claude Code 里信任过这个仓库之后才会到达 agent;你在那里批准过的 MCP server 才会启动。
146
+
147
+ ## 🔌 chord 怎么和你的工具相处
148
+
149
+ - **叫醒 agent** 就是往它的终端里敲一句短话。chord 只在输入框为空、键盘安静了几秒、终端回显了这句话之后才这么做;对话框永远收不到那个回车。
150
+ - **信任**:agent 在项目文件夹里跑,所以 Claude 像平时敲 `claude` 一样只问一次;chord 不往 `~/.claude.json` 里写任何东西。Codex 会让你审核一次 chord 的 hook,hook 定义变了会再问一次;选 "trust all"。有 hook 被关掉时 chord 会在启动时告诉你。
151
+ - **分支和 worktree。** 每个 agent 添一个 `chord/<名字>` 分支和一个 worktree;`chord clean` 在 agent 离开后删掉它们,还没到 main 上的东西留着。
152
+ - **权限。** chord 自己的工具对 Claude 预先放行,所以独自干活的 agent 不会卡在它们的确认框上;你平时的 shell 和编辑审批照旧。Codex 以完全访问运行。Windows 上 chord 拒绝带沙箱启动,因为那个沙箱起不来 hook;Linux 上 hook 在沙箱里运行,但 Codex 会让你逐个批准 chord 的工具调用。
153
+ - **终端标题**是 `chord <agent> · <任务>`,每个窗口都说得出自己是谁。
154
+ - **hook 把关的是编辑器工具。** 从 shell 改的文件在每条命令之后按名字报出来,并附上该做的占用。
155
+
156
+ ## 🗺️ 范围
157
+
158
+ - 在 Windows 11 和 Linux 上运行。
159
+ - 每个项目一个仓库。
160
+ - 项目路径最长约 190 个字符。
161
+ - 仓库里有 CRLF 文件和一条从未重新规范化过的 `* text` 属性时,每个 worktree 都会出现假改动;chord 会报出文件名和把仓库修好的命令。
162
+
163
+ ## 🧪 测试
164
+
165
+ `npm test` 在临时文件夹里的真实 git 仓库上跑单元测试。协调本身用真实工具做过端到端验证,从两个 agent 商量一个文件到二十个 agent 合进同一个 main,还有 Python、Rust 和 JavaScript 的开源仓库。
166
+
167
+ ## 📄 许可
168
+
169
+ [MIT](LICENSE)
package/dist/clean.js CHANGED
@@ -9,12 +9,15 @@ const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8',
9
9
  * main, or whose worktree has uncommitted work, is kept unless `force`; an online agent is always kept. Directories
10
10
  * under chord/agents/ with no database row (a launch that died before the tool started) are treated the same way. */
11
11
  export function clean(project, d, names = [], force = false) {
12
- db.sweep(d);
12
+ db.sweep(d, undefined, undefined, 2500); // as in ls: a dead wrapper with a reused pid must not count as online
13
13
  const main = git(project.repo, 'rev-parse', '--abbrev-ref', 'HEAD');
14
14
  const agents = db.listAgents(d).filter(a => (names.length ? names.includes(a.name) : a.status === 'offline'));
15
15
  const result = { removed: [], kept: [] };
16
16
  const kept = (name, why) => result.kept.push({ name, why });
17
+ const agentsDir = path.join(project.dir, 'chord', 'agents');
17
18
  const remove = (a, hasRow) => {
19
+ // by name, never by the stored paths: a row pointing elsewhere must not take that folder or worktree with it
20
+ const dir = path.join(agentsDir, a.name), wt = path.join(dir, project.repoName);
18
21
  try {
19
22
  if (a.status !== 'offline') {
20
23
  kept(a.name, 'still online');
@@ -31,37 +34,37 @@ export function clean(project, d, names = [], force = false) {
31
34
  merged = false;
32
35
  }
33
36
  if (!merged) {
34
- kept(a.name, `${branch} has commits that are not on ${main}; use --force to discard them`);
37
+ kept(a.name, `unmerged commits on ${branch}`);
35
38
  return;
36
39
  }
37
40
  }
38
41
  // work that was never committed (an agent stopped half-way) is just as much work as an unmerged commit
39
- if (!force && fs.existsSync(path.join(a.worktree, '.git'))) {
42
+ if (!force && fs.existsSync(path.join(wt, '.git'))) {
40
43
  let dirty = '';
41
44
  try {
42
- dirty = git(a.worktree, 'status', '--porcelain');
45
+ dirty = git(wt, 'status', '--porcelain');
43
46
  }
44
47
  catch { /* a broken worktree holds nothing to keep */ }
45
48
  if (dirty) {
46
- kept(a.name, `${a.worktree} has uncommitted changes; use --force to discard them`);
49
+ kept(a.name, `uncommitted changes in ${path.relative(project.dir, wt).split(path.sep).join('/')}`);
47
50
  return;
48
51
  }
49
52
  }
50
53
  // The directory goes first. On Windows a directory some process is using (a shell left open in it) cannot be
51
54
  // removed; then the branch and the database row stay too, so the next clean sees the agent whole.
52
- if (fs.existsSync(a.worktree)) {
55
+ if (fs.existsSync(wt)) {
53
56
  try {
54
- git(project.repo, 'worktree', 'remove', '--force', a.worktree);
57
+ git(project.repo, 'worktree', 'remove', '--force', wt);
55
58
  }
56
59
  catch { /* fall through to rm */ }
57
60
  }
58
61
  try {
59
- fs.rmSync(a.cwd, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
62
+ fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
60
63
  }
61
64
  catch { /* checked below */ }
62
65
  git(project.repo, 'worktree', 'prune');
63
- if (fs.existsSync(a.cwd)) {
64
- kept(a.name, `${a.cwd} could not be removed, probably because a process is using it; close it and run clean again`);
66
+ if (fs.existsSync(dir)) {
67
+ kept(a.name, `${dir} could not be removed, probably because a process is using it; close it and run clean again`);
65
68
  return;
66
69
  }
67
70
  if (hasBranch)
@@ -80,7 +83,6 @@ export function clean(project, d, names = [], force = false) {
80
83
  for (const a of agents)
81
84
  remove(a, true);
82
85
  // directories with no row: a launch that died before the tool registered, or a database that was deleted
83
- const agentsDir = path.join(project.dir, 'chord', 'agents');
84
86
  const known = new Set(db.listAgents(d).map(a => a.name.toLowerCase()));
85
87
  let orphans = [];
86
88
  try {
@@ -88,6 +90,6 @@ export function clean(project, d, names = [], force = false) {
88
90
  }
89
91
  catch { /* no agents directory yet */ }
90
92
  for (const n of orphans)
91
- remove({ name: n, status: 'offline', cwd: path.join(agentsDir, n), worktree: path.join(agentsDir, n, project.repoName) }, false);
93
+ remove({ name: n, status: 'offline' }, false);
92
94
  return result;
93
95
  }