chord-cli 0.1.3 → 0.2.1
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 +26 -8
- package/README.zh-CN.md +170 -0
- package/dist/clean.js +21 -15
- package/dist/cli.js +166 -69
- package/dist/db.js +102 -44
- package/dist/hooks.js +108 -66
- package/dist/launch.js +30 -58
- package/dist/mcp.js +3 -1
- package/dist/project.js +6 -7
- package/dist/pty.js +76 -14
- package/dist/serve.js +6 -7
- package/dist/tools.js +95 -42
- package/dist/trust.js +4 -36
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
<p align="center">
|
|
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
|
-
<img alt="Windows and Linux" src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue">
|
|
8
|
+
<img alt="Windows, macOS and Linux" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%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.
|
|
@@ -27,6 +28,19 @@ One repository, as many agents as you care to start. Each agent works in its own
|
|
|
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.
|
|
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 |
|
|
@@ -53,7 +67,7 @@ A new agent needs three things from its tool: a way to inject hooks and an MCP s
|
|
|
53
67
|
|
|
54
68
|
1. **Isolation.** Every agent works on its own branch in its own worktree. The main checkout changes through `finish` alone.
|
|
55
69
|
2. **Coordination.** A shared board holds claims, messages and events. Agents register, claim and talk through tools chord gives them; hooks check claims before each write and hand new messages to the agent as it works.
|
|
56
|
-
3. **Merging.** `finish` rebases the branch onto main and fast-forwards. A clean rebase merges;
|
|
70
|
+
3. **Merging.** `finish` rebases the branch onto main and fast-forwards. A clean rebase that passes the project's test command merges; on a conflict nothing moves, and the agent resolves it and finishes again.
|
|
57
71
|
|
|
58
72
|
## 📦 Install
|
|
59
73
|
|
|
@@ -63,7 +77,7 @@ With Node 24 and git on the machine:
|
|
|
63
77
|
npm install -g chord-cli
|
|
64
78
|
```
|
|
65
79
|
|
|
66
|
-
On Linux add `--allow-scripts=node-pty`, with python3, make and g++ present: node-pty ships no Linux binary and builds one on install.
|
|
80
|
+
On Linux add `--allow-scripts=node-pty`, with python3, make and g++ present: node-pty ships no Linux binary and builds one on install. macOS needs nothing more: node-pty ships a binary for it.
|
|
67
81
|
|
|
68
82
|
Or from source:
|
|
69
83
|
|
|
@@ -100,18 +114,22 @@ 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
|
|
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 |
|
|
113
130
|
| `chord stop <agent>` | quit an agent once its turn ends and its input box is empty |
|
|
114
131
|
| `chord clean [--force] [agent...]` | remove offline agents' workspaces, worktrees and branches; work not yet on main stays unless `--force` |
|
|
132
|
+
| `chord --version` | the installed version |
|
|
115
133
|
|
|
116
134
|
## ⚙️ Configuration
|
|
117
135
|
|
|
@@ -125,20 +143,20 @@ chord clean # remove the workspaces and branches of agents that hav
|
|
|
125
143
|
|
|
126
144
|
`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.
|
|
127
145
|
|
|
128
|
-
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.
|
|
146
|
+
A repository's own Claude configuration (`.claude/settings*.json`, `.mcp.json`) reaches an agent once you have trusted that repository, or the project folder, in Claude Code yourself; the MCP servers you approved there are the ones that start.
|
|
129
147
|
|
|
130
148
|
## 🔌 How chord works with your tools
|
|
131
149
|
|
|
132
150
|
- **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.
|
|
133
|
-
- **Trust**:
|
|
151
|
+
- **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.
|
|
134
152
|
- **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.
|
|
135
|
-
- **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
|
|
153
|
+
- **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 keeps the sandbox and approval mode from your own configuration. On Windows chord refuses a sandbox given on the command line, because that sandbox cannot run the hooks; on Linux and macOS the hooks run inside the sandbox, but Codex then asks you to approve each chord tool call.
|
|
136
154
|
- **The terminal title** reads `chord <agent> · <task>`, so each window says which agent it is.
|
|
137
155
|
- **Hooks gate the editor tools.** Files changed from the shell are reported by name after each command, with the claim to make.
|
|
138
156
|
|
|
139
157
|
## 🗺️ Scope
|
|
140
158
|
|
|
141
|
-
- Runs on Windows 11 and Linux.
|
|
159
|
+
- Runs on Windows 11, macOS and Linux.
|
|
142
160
|
- One repository per project.
|
|
143
161
|
- Project paths up to about 190 characters.
|
|
144
162
|
- A repository with CRLF files and a `* text` attribute that was never renormalised shows phantom changes in every worktree; chord names the files and the command that puts the repository right.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
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, macOS and Linux" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%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 在同一个仓库里各做一个功能,合并后的结果通过两个功能的测试才算这一对通过。
|
|
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 干净、项目的 test 命令通过就合进去;遇到冲突什么都不动,agent 自己解决再 finish。
|
|
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 的二进制,安装时现场编译。macOS 不需要额外步骤:node-pty 自带 macOS 的二进制。
|
|
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
|
+
| `chord --version` | 安装的版本 |
|
|
133
|
+
|
|
134
|
+
## ⚙️ 配置
|
|
135
|
+
|
|
136
|
+
`<项目>/chord/config.json`:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{ "setup": "npm ci", "test": "npm test" }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`setup` 在每个新建的 worktree 里、agent 启动前跑一次,环境里带着 `CHORD_*` 变量,这样新 worktree 一来就装好了依赖。setup 失败则放弃这次启动并删掉 worktree。
|
|
143
|
+
|
|
144
|
+
`test` 设置了的话,每次 `finish` 把分支 rebase 到 main 之后、main 移动之前,在 agent 的 worktree 里跑一遍。失败就什么都不合,agent 拿到输出的末尾:rebase 干净也可能弄坏别人刚合进去的东西。它跑的时候其他 agent 的 `finish` 等着。
|
|
145
|
+
|
|
146
|
+
仓库自带的 Claude 配置(`.claude/settings*.json`、`.mcp.json`)在你自己在 Claude Code 里信任过这个仓库或项目文件夹之后才会到达 agent;你在那里批准过的 MCP server 才会启动。
|
|
147
|
+
|
|
148
|
+
## 🔌 chord 怎么和你的工具相处
|
|
149
|
+
|
|
150
|
+
- **叫醒 agent** 就是往它的终端里敲一句短话。chord 只在输入框为空、键盘安静了几秒、终端回显了这句话之后才这么做;对话框永远收不到那个回车。
|
|
151
|
+
- **信任**:agent 在项目文件夹里跑,所以 Claude 像平时敲 `claude` 一样只问一次;chord 不往 `~/.claude.json` 里写任何东西。Codex 会让你审核一次 chord 的 hook,hook 定义变了会再问一次;选 "trust all"。有 hook 被关掉时 chord 会在启动时告诉你。
|
|
152
|
+
- **分支和 worktree。** 每个 agent 添一个 `chord/<名字>` 分支和一个 worktree;`chord clean` 在 agent 离开后删掉它们,还没到 main 上的东西留着。
|
|
153
|
+
- **权限。** chord 自己的工具对 Claude 预先放行,所以独自干活的 agent 不会卡在它们的确认框上;你平时的 shell 和编辑审批照旧。Codex 沿用你自己配置里的沙箱和审批模式。Windows 上 chord 拒绝命令行里给出的沙箱,因为那个沙箱起不来 hook;Linux 和 macOS 上沙箱里 hook 照常运行,但 Codex 会让你逐个批准 chord 的工具调用。
|
|
154
|
+
- **终端标题**是 `chord <agent> · <任务>`,每个窗口都说得出自己是谁。
|
|
155
|
+
- **hook 把关的是编辑器工具。** 从 shell 改的文件在每条命令之后按名字报出来,并附上该做的占用。
|
|
156
|
+
|
|
157
|
+
## 🗺️ 范围
|
|
158
|
+
|
|
159
|
+
- 在 Windows 11、macOS 和 Linux 上运行。
|
|
160
|
+
- 每个项目一个仓库。
|
|
161
|
+
- 项目路径最长约 190 个字符。
|
|
162
|
+
- 仓库里有 CRLF 文件和一条从未重新规范化过的 `* text` 属性时,每个 worktree 都会出现假改动;chord 会报出文件名和把仓库修好的命令。
|
|
163
|
+
|
|
164
|
+
## 🧪 测试
|
|
165
|
+
|
|
166
|
+
`npm test` 在临时文件夹里的真实 git 仓库上跑单元测试。协调本身用真实工具做过端到端验证,从两个 agent 商量一个文件到二十个 agent 合进同一个 main,还有 Python、Rust 和 JavaScript 的开源仓库。
|
|
167
|
+
|
|
168
|
+
## 📄 许可
|
|
169
|
+
|
|
170
|
+
[MIT](LICENSE)
|
package/dist/clean.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
// chord clean: remove the workspaces, worktrees and branches of agents that are gone.
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
2
|
import fs from 'node:fs';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import * as db from "./db.js";
|
|
6
5
|
import { setupMarker } from "./launch.js";
|
|
7
|
-
const
|
|
6
|
+
const { git } = db;
|
|
8
7
|
/** Remove offline agents (all of them, or the named ones). An agent whose branch has commits that are not on
|
|
9
8
|
* main, or whose worktree has uncommitted work, is kept unless `force`; an online agent is always kept. Directories
|
|
10
9
|
* under chord/agents/ with no database row (a launch that died before the tool started) are treated the same way. */
|
|
11
10
|
export function clean(project, d, names = [], force = false) {
|
|
12
|
-
db.sweep(d);
|
|
11
|
+
db.sweep(d, undefined, undefined, 2500); // as in ls: a dead wrapper with a reused pid must not count as online
|
|
13
12
|
const main = git(project.repo, 'rev-parse', '--abbrev-ref', 'HEAD');
|
|
14
13
|
const agents = db.listAgents(d).filter(a => (names.length ? names.includes(a.name) : a.status === 'offline'));
|
|
15
14
|
const result = { removed: [], kept: [] };
|
|
16
15
|
const kept = (name, why) => result.kept.push({ name, why });
|
|
16
|
+
const agentsDir = path.join(project.dir, 'chord', 'agents');
|
|
17
17
|
const remove = (a, hasRow) => {
|
|
18
|
+
// by name, never by the stored paths: a row pointing elsewhere must not take that folder or worktree with it
|
|
19
|
+
const dir = path.join(agentsDir, a.name), wt = path.join(dir, project.repoName);
|
|
18
20
|
try {
|
|
19
21
|
if (a.status !== 'offline') {
|
|
20
22
|
kept(a.name, 'still online');
|
|
@@ -31,37 +33,42 @@ export function clean(project, d, names = [], force = false) {
|
|
|
31
33
|
merged = false;
|
|
32
34
|
}
|
|
33
35
|
if (!merged) {
|
|
34
|
-
kept(a.name,
|
|
36
|
+
kept(a.name, `unmerged commits on ${branch}`);
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
// 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(
|
|
41
|
+
if (!force && fs.existsSync(path.join(wt, '.git'))) {
|
|
42
|
+
const rel = path.relative(project.dir, wt).split(path.sep).join('/');
|
|
40
43
|
let dirty = '';
|
|
44
|
+
// a worktree git cannot read (its link broken by a move) may still hold work: not knowing is not clean
|
|
41
45
|
try {
|
|
42
|
-
dirty = git(
|
|
46
|
+
dirty = git(wt, 'status', '--porcelain');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
kept(a.name, `uncommitted changes unknown: git status failed in ${rel}`);
|
|
50
|
+
return;
|
|
43
51
|
}
|
|
44
|
-
catch { /* a broken worktree holds nothing to keep */ }
|
|
45
52
|
if (dirty) {
|
|
46
|
-
kept(a.name,
|
|
53
|
+
kept(a.name, `uncommitted changes in ${rel}`);
|
|
47
54
|
return;
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
// The directory goes first. On Windows a directory some process is using (a shell left open in it) cannot be
|
|
51
58
|
// removed; then the branch and the database row stay too, so the next clean sees the agent whole.
|
|
52
|
-
if (fs.existsSync(
|
|
59
|
+
if (fs.existsSync(wt)) {
|
|
53
60
|
try {
|
|
54
|
-
git(project.repo, 'worktree', 'remove', '--force',
|
|
61
|
+
git(project.repo, 'worktree', 'remove', '--force', wt);
|
|
55
62
|
}
|
|
56
63
|
catch { /* fall through to rm */ }
|
|
57
64
|
}
|
|
58
65
|
try {
|
|
59
|
-
fs.rmSync(
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
60
67
|
}
|
|
61
68
|
catch { /* checked below */ }
|
|
62
69
|
git(project.repo, 'worktree', 'prune');
|
|
63
|
-
if (fs.existsSync(
|
|
64
|
-
kept(a.name, `${
|
|
70
|
+
if (fs.existsSync(dir)) {
|
|
71
|
+
kept(a.name, `${dir} could not be removed, probably because a process is using it; close it and run clean again`);
|
|
65
72
|
return;
|
|
66
73
|
}
|
|
67
74
|
if (hasBranch)
|
|
@@ -80,7 +87,6 @@ export function clean(project, d, names = [], force = false) {
|
|
|
80
87
|
for (const a of agents)
|
|
81
88
|
remove(a, true);
|
|
82
89
|
// 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
90
|
const known = new Set(db.listAgents(d).map(a => a.name.toLowerCase()));
|
|
85
91
|
let orphans = [];
|
|
86
92
|
try {
|
|
@@ -88,6 +94,6 @@ export function clean(project, d, names = [], force = false) {
|
|
|
88
94
|
}
|
|
89
95
|
catch { /* no agents directory yet */ }
|
|
90
96
|
for (const n of orphans)
|
|
91
|
-
remove({ name: n, status: 'offline'
|
|
97
|
+
remove({ name: n, status: 'offline' }, false);
|
|
92
98
|
return result;
|
|
93
99
|
}
|