这篇能做出什么
做完之后,你会有一个叫 agents 的 tmux 会话:里面固定有几路 Claude Code 各自在独立的 git worktree 里干活,一路一个任务域。你用一条命令把任务文件“派”给某一路,用另一条命令一次性扫出所有路的最近输出,所有终端输出同时落盘成日志文件。关掉终端、断网、第二天回来接着看,活还在跑(只要机器没重启)。
具体一点,最终你会拥有:
up.sh:一条命令拉起 4 路(数量可改)并行的编码会话,每路一个分支、一个目录、一份日志。dispatch.sh:把tasks/03-export-csv.md这类任务卡派给指定的一路,而不是手打一长串 prompt。peek.sh:一条命令打印所有路最近 15 行输出,相当于一个粗糙但够用的监控面板。logs/alpha.log这类文件:每路的完整终端留痕,可以grep、可以归档。- 一套判断标准:什么时候该
/compact,什么时候该 Ctrl-C 重开,什么时候该把一路拆成两路。
关键思路只有一句:把上下文外置到文件里,tmux 只负责“多个终端 + 可寻址的输入输出”这件事。任务卡是上下文,日志是记忆,会话本身随时可以牺牲掉重开。
前置条件清单
动手前确认这些:
- macOS 或 Linux,装了
tmux(tmux -V有输出即可;参数细节以官方手册当前版本为准)。 - 已安装并登录 Claude Code CLI,终端里直接敲
claude能进交互界面。版本差异较大,涉及具体命令行参数时以官方文档当前版本为准。 - 一个 git 仓库,并且工作区是干净的(
git status无未提交改动)。并行会话靠 worktree 隔离,脏工作区会让git worktree add的基线变模糊。 - 仓库根目录能跑通测试命令(
npm test、pytest、go test ./...之类),后面任务卡的“完成标准”要写它。 - 会用三个 tmux 动作就够:
Ctrl-b d脱离、tmux attach -t agents回来、Ctrl-b [进复制模式滚屏。 - 建议开
set -g mouse on,鼠标滚轮直接翻历史,省很多事。
目录约定(后面脚本都用这套,想改就改脚本里的变量):
```text
~/work/myproj/ # 主仓库
~/work/wt-alpha/ # agent alpha 的 worktree
~/work/wt-beta/ # agent beta 的 worktree
~/work/logs/ # 各路日志
~/work/tasks/ # 任务卡
~/work/bin/ # up.sh / dispatch.sh / peek.sh
```
分步骤
第 1 步:先把单路跑通,再谈并行
别一上来就写脚本。先手动来一遍:
```bash
tmux new-session -s smoke -c ~/work/myproj
在 tmux 里
claude
```
在里面让它读一个文件、改一行、跑一次测试。确认三件事:TUI 显示正常不折行、能正常滚屏回看、Ctrl-b d 之后 tmux attach -t smoke 能回来。顺手留意一下它“空闲等待输入”时界面长什么样——第 5 步的脚本要靠这个特征判断它忙不忙。
确认没问题后 tmux kill-session -t smoke。
小技巧:如果 TUI 在 tmux 里显示错乱,先确认不是窗口太窄导致的折行,用 tmux set -g default-size 200x50 或 resize-window -x 200 -y 50 调宽(不同 tmux 版本参数略有差异,tmux -V 后查官方手册)。
第 2 步:一路一个 worktree
多路并行最常见的失败不是智能体不够聪明,是两路同时改了同一个文件,合并时互相打脸。用 worktree 把分支和目录一起隔离:
```bash
cd ~/work/myproj
git worktree add -b agent/alpha ../wt-alpha
git worktree add -b agent/beta ../wt-beta
git worktree add -b agent/gamma ../wt-gamma
git worktree add -b agent/delta ../wt-delta
git worktree list
```
规矩定死:alpha 只在 wt-alpha 里改,beta 只在 wt-beta 里改,谁也不许跨目录。合并时一路一路来:git checkout main && git merge agent/alpha。
注意 worktree 共享同一个 .git 目录,所以不要让两路同时往同一个分支提交,也不要在两路里同时跑会改写索引的 git 操作。
第 3 步:写任务卡,而不是写 prompt
send-keys 最怕的就是把几百字的 prompt 塞进终端——多行文本会被 tmux 逐行“回车”,变成一堆半截命令。正确做法是把任务写成文件,终端里只发一句“去读那个文件”。
tasks/03-export-csv.md 模板:
```markdown
任务:给订单导出加编码选项
背景
- 相关代码:src/export/、src/order/
- 禁止修改:src/core/(其他 agent 正在改)
要求
1. 导出接口新增 encoding 参数,默认 utf-8,保持向后兼容
2. 补一条单测覆盖 gbk 分支
完成标准
- [ ]
npm test -- export全绿 - [ ] 未修改 src/core/
交付
在仓库根目录写 HANDOFF.md:改了哪些文件、遗留问题、下一步建议。
```
最后那个 HANDOFF.md 很关键。它让“这一路的上下文”在会话被清空后依然存在,是后面规避上下文上限的基础。
第 4 步:一键拉起多路会话
~/work/bin/up.sh:
```bash
#!/usr/bin/env bash
一键拉起多路并行 Claude Code 会话
set -euo pipefail
SESSION=agents
REPO_ROOT="$HOME/work/myproj"
LOG_DIR="$HOME/work/logs"
AGENTS=(alpha beta gamma delta)
mkdir -p "$LOG_DIR"
已有同名会话就先清掉,避免 target 重名
tmux kill-session -t "$SESSION" 2>/dev/null || true
0 号窗口留给监控脚本
tmux new-session -d -s "$SESSION" -n monitor -c "$REPO_ROOT"
for name in "${AGENTS[@]}"; do
wt="$HOME/work/wt-$name"
if [[ ! -d "$wt" ]]; then
git -C "$REPO_ROOT" worktree add -b "agent/$name" "$wt"
fi
tmux new-window -t "$SESSION" -n "$name" -c "$wt"
所有输出同时落盘;>> 追加写入,重复执行也不会丢历史
tmux pipe-pane -o -t "$SESSION:$name" \
"sed -u 's/\x1b\[[0-9;]*m//g' >> $LOG_DIR/$name.log"
-l 表示后面是字面量,避免 tmux 解释特殊字符
tmux send-keys -t "$SESSION:$name" -l "claude"
tmux send-keys -t "$SESSION:$name" Enter
sleep 3 # 等 TUI 起来,见常见坑第 5 条
done
tmux select-window -t "$SESSION:monitor"
tmux attach -t "$SESSION"
```
执行 chmod +x ~/work/bin/up.sh && ~/work/bin/up.sh。
几个细节值得解释:
sed -u是去掉终端颜色控制码,让日志用grep、less看得下去;-u保证不缓冲,实时写入。想看原始带色输出就把 sed 去掉。pipe-pane -o是“开关式”参数:管道已存在时会关闭它。所以重复跑 up.sh 前先 kill 会话,逻辑最干净。- 窗口名不要互为前缀(比如
a和ab),老版本 tmux 匹配窗口名是前缀匹配,会打错窗口。实在想用短名,目标写成"$SESSION:=$name"做精确匹配。
第一次在新目录里跑,Claude Code 可能会问“是否信任此目录”之类的确认。所以建议先手动把每个窗口过一遍首启确认,等 up.sh 全自动时就不会卡在确认框上。
第 5 步:send-keys 派活脚本
~/work/bin/dispatch.sh:
```bash
#!/usr/bin/env bash
用法: ./dispatch.sh alpha tasks/03-export-csv.md
set -euo pipefail
SESSION="${SESSION:-agents}"
TARGET="$1"
TASK_FILE="$2"
if [[ ! -f "$TASK_FILE" ]]; then
echo "任务文件不存在: $TASK_FILE" >&2
exit 1
fi
1) 确认目标窗口存在
if ! tmux list-windows -t "$SESSION" -F '#{window_name}' | grep -qx "$TARGET"; then
echo "没有这个窗口: $TARGET" >&2
tmux list-windows -t "$SESSION" -F '#{window_index} #{window_name}' >&2
exit 1
fi
2) 确认它空闲:最后几行里没有“正在运行”的标志
这里的匹配串要按你实际看到的界面改,先手动观察一次
recent=$(tmux capture-pane -pJ -t "$SESSION:$TARGET" -S -6 | sed '/^$/d')
3) 派活:只发一句短指令,让它自己读文件
MSG="读 $TASK_FILE,按里面的要求执行。完成后写 HANDOFF.md。"
tmux send-keys -t "$SESSION:$TARGET" -l "$MSG"
tmux send-keys -t "$SESSION:$TARGET" Enter
echo "已派发给 $TARGET: $MSG"
```
用法:
```bash
cd ~/work
./bin/dispatch.sh alpha tasks/03-export-csv.md
./bin/dispatch.sh beta tasks/04-fix-pagination.md
```
为什么坚持“只发一句短指令”:send-keys 的输入是终端按键流,不是 API 调用。带换行的长文本会被当成多次回车,特殊字符(;、$、!)在某些 shell 配置下还会被处理。加 -l 只能保证字面量,保证不了多行语义。任务卡 + 一句话,是最稳的组合。
第 6 步:输出留痕与回看
三种回看方式,按粒度选:
```bash
1) 实时盯某一路(跟看直播)
tmux attach -t agents \; select-window -t agents:alpha
2) 抓快照:含历史缓冲区的最后 200 行
tmux capture-pane -pJ -t agents:alpha -S -200 | tail -n 60
3) 翻归档日志:完整、可 grep、可对比
tail -n 80 ~/work/logs/alpha.log
grep -n "error\|Error\|FAIL" ~/work/logs/*.log
```
capture-pane 的参数值得记牢:-p 打到标准输出,-S -200 从历史缓冲区往上 200 行开始取(-S - 是取全部历史),-J 把被窗口宽度折断的行拼回一行——不加 -J,日志里全是硬折行的碎句子。
~/work/bin/peek.sh 一次性扫全部:
```bash
#!/usr/bin/env bash
一眼看完所有路在干什么
set -euo pipefail
SESSION="${SESSION:-agents}"
for w in $(tmux list-windows -t "$SESSION" -F '#{window_name}'); do
echo "===== $w ====="
tmux capture-pane -pJ -t "$SESSION:$w" -S -18 | sed '/^[[:space:]]*$/d' | tail -n 12
echo
done
```
把它挂到 monitor 窗口里循环跑,就是一个极简看板:
```bash
tmux send-keys -t agents:monitor -l \
"while true; do clear; ~/work/bin/peek.sh; sleep 20; done"
tmux send-keys -t agents:monitor Enter
```
第 7 步:上下文上限规避——什么时候该拆
每一路会话都有独立的AI 词典:上下文窗口">上下文窗口,互不共享。这意味着两件事:并行确实能扩大总吞吐;但任何一路自己聊久了,仍然会撞上限。撞上限的典型症状是:
- 开始重复问同样的问题,或反复读已经读过的文件;
- 忘记任务卡里写死的约束(“不要改 src/core”),开始越界改;
- 回答变得笼统,不再引用具体文件路径和函数名;
/compact之后反而更差,因为它把细节压掉了。
对应的处置顺序,从轻到重:
1. 同一任务内的中期整理:任务做到一半,让它把当前进展写进 HANDOFF.md,然后 /compact 压缩对话(命令名以官方文档当前版本为准)。适合“还是一个任务,只是聊太长了”。
2. 换任务就清空:这一路的下一个任务跟上一个无关,直接 /clear 或 Ctrl-C 后重开 claude。别在一个会话里攒三个不相关的任务。
3. 拆会话:出现下面任一情况,就把一路拆成两路、两个 worktree、两张任务卡——
- 任务要跨三个以上互不相干的目录;
- 需要同时保留的决策点多到一屏写不下;
- 你发现自己在对话里反复重申同一批背景(说明背景该进任务卡了)。
拆的时候不需要“迁移上下文”,只需要把已知信息写进新的任务卡,然后 ./bin/dispatch.sh epsilon tasks/05-xxx.md。这就是把上下文外置的好处:会话是可抛弃的。
反方向的判断同样重要:并行路数不是越多越好。四路同时跑测试,本机 CPU 和磁盘先扛不住;同一个账号的请求频率也可能撞限额,表现是一路开始报错、重试、卡住。看到这种信号就减到两三路,或者错峰派活。
第 8 步:收工与合并
每路跑完,先看 HANDOFF.md,再合并:
```bash
cd ~/work/myproj
git checkout main
git merge --no-ff agent/alpha
npm test
```
一次只合一路,合完跑测试,绿了再合下一路。四路一起合,出问题你不知道是谁的锅。
常见坑与排错
1. 多行 prompt 被拆成多条命令。 表现是终端里出现一堆半截指令。原因:send-keys 发的是按键流,文本里的换行等于回车。解法:任务写文件,终端只发一句话。
2. 忘了发 Enter。 加了 -l 之后,文本只是“打进去”,不会提交。必须单独一句 tmux send-keys -t <target> Enter。这两句要连着写,中间不要插别的。
3. 特殊字符被解释。 文本里带 ;、$、!、反引号时,命令变成了别的东西。一律加 -l;确实要发控制键时用键名(Enter、Escape、C-c),不要用字符串模拟。
4. 智能体正在跑,你插话插歪了。 它正在执行时发指令,输入可能被当成排队消息,也可能直接打断。稳妥做法:先 peek.sh 看一眼,要打断就先发 Escape 或 C-c,等回到空闲提示符再派活。
5. 脚本发 claude 时 TUI 还没起来。 表现是命令字母被吞、或落到 shell 提示符上。解法:sleep 3 起步,或者写个轮询函数,反复 capture-pane 直到看见空闲提示符特征再返回。这个特征每个版本界面不同,先手动观察一次。
6. 回看时行数不够 / 全是碎行。 用 -S -200 甚至 -S - 取历史,加 -J 拼回折行。注意 capture-pane 只能拿到 tmux 的 scrollback 缓冲区,超出部分靠 pipe-pane 的日志文件补。
7. 日志文件不更新或重复。 pipe-pane -o 是开关语义,重复执行可能把管道关掉。排查:tmux display-message -p -t agents:alpha '#{pane_pipe}' 看是否为 1。想重开就先 kill 会话再跑 up.sh。日志用 >> 追加,重启不会丢历史。
8. worktree 里改不动 git。 多半是两路同时操作同一分支,或者主仓库有未提交改动导致 worktree 状态混乱。规矩:一路一分支,主仓库保持干净。
9. 会话凭空消失。 tmux server 挂了或机器重启,会话全没。所以任务卡和日志必须落盘——这也是本方案把“记忆”放在文件而不是会话里的原因。恢复流程:up.sh 重新拉起,dispatch.sh 重派没完成的任务卡。
10. 并行路数一多就集体变慢。 先怀疑资源(CPU、磁盘、测试端口冲突),再怀疑额度限制。减路数、错峰、把重任务放到单独一路,通常就恢复了。
下一步建议
- 加桌面通知:在任务卡里要求“完成后写 HANDOFF.md”,然后用一个
inotifywait(Linux)或fswatch(macOS)小脚本监听HANDOFF.md出现,调系统通知。比盯着屏幕划算。 - 让 peek 更有信息量:把
peek.sh的输出接到一个静态 HTML 里定时刷新,浏览器标签页就是看板。 - 把任务卡变成队列:
tasks/里放01-*.md、02-*.md,写个循环:某一路空了就取下一张。这已经是很轻量的自建调度器了。 - 加一道自动体检:每路跑完自动
git diff --stat,超过预期文件数就报警——防止智能体“顺手重构”了你没让它碰的模块。 - 固化自己的拆会话阈值:跑上一周,记录每次是哪一路先撞上下文上限、当时在做什么任务。你的阈值会比任何通用建议都准。
