一个人开三个 Claude Code 窗口,一个改前端,一个写后端,一个更新文档。听起来很高效,但真干起来你会发现:手动把任务描述复制到每个窗口,效率低得要命。更麻烦的是,窗口之间完全不知道对方在干什么,改同一个文件撞车了都不知道。

我试过共享文档、聊天窗口喊话、甚至用 Git 分支当信号——都不好用。最后找到一个反直觉的方案:用文件系统当任务板,让多个 Claude Code 实例自己去抢任务。
在这里插入图片描述

用文件当便利贴,用目录当看板

思路不复杂。Claude Code 能读写文件,多个实例跑在同一台机器上,天然共享磁盘。那就让磁盘当协作媒介。

task-queue/
├── config.yaml        # 全局配置
├── workers.yaml       # 角色定义
├── pending/           # 待领取
├── running/           # 执行中
├── done/              # 已完成
└── failed/            # 已失败

pending 目录放任务,谁有空谁来揭。揭走之后文件挪到 running,干完了挪到 done。跟团队用白板贴便利贴一个道理。

有人可能会问,为什么不直接上 Redis 或消息队列?一个人在自己机器上搞多实例并行,装个 Redis 就为了传任务?太重了。文件系统零依赖,Linux 开箱即用,这就够了。

任务的状态流转就一条线:

投递任务              领取                完成
pending/  ────────►  running/  ────────►  done/
              mv 抢占        mv 归档
                                │
                                │ 失败
                                ▼
                            failed/

         超时未完成
running/ ─────────────► pending/  (被其他实例重新领取)

在这里插入图片描述

抢占靠 mv,原子性靠 rename

两个实例同时看到同一个任务,都想去领——这是经典的竞态条件。怎么保证只有一个成功?

答案在 Linux 的 rename 系统调用里。POSIX 标准明确规定:同一文件系统上的 rename 是原子操作。而 mv 在同卷上底层就是 rename。

# 实例1
mv pending/001-fix-auth.md running/001-fix-auth__frontend.md   # ✅ 成功

# 实例2(几乎同时)
mv pending/001-fix-auth.md running/001-fix-auth__backend.md    # ❌ 失败:No such file or directory

一个成功,一个报文件不存在——正好是我们要的互斥效果。不用加锁,不用数据库事务,一个 mv 搞定。

有个前提:pending 和 running 必须在同一个文件系统上。跨卷 mv 变成复制+删除,两步操作不原子。部署时用 df 查一下挂载点就行。

实在不放心极端情况,可以加一层 flock 文件锁做二次保护。但实测下来 rename 的原子性够可靠,flock 更像是心理安慰。
在这里插入图片描述

任务文件长什么样

任务就是一个 .md 文件,YAML frontmatter 声明元数据,Markdown 正文写具体要求:

---
id: "001"
title: "修复认证模块空指针问题"
type: code-fix
priority: 1
assignee: any
created: "2026-07-02T10:00:00"
timeout: 600
---

## 任务描述

检查 src/auth.c 中的 login() 函数,修复当 username 为 NULL 时的空指针崩溃问题。

## 验收标准

- NULL 输入返回错误码,不崩溃
- 添加对应单元测试

几个字段展开说下:

  • type:任务类型标签,比如 code-fixfrontenddocumentation。这个值必须出现在某个实例的 capabilities 列表里,否则没人能领。
  • assigneeany 表示任意实例可领;写具体 ID 比如 backend,就只有 backend 实例能领。
  • priority:数字越小越优先。紧急任务设 1,普通任务默认 99。
  • timeout:超时秒数。领取后超过这个时间还没完成,其他实例可以把任务从 running 挪回 pending 重新领取——这是防止实例崩溃导致任务卡死的保底机制。
    在这里插入图片描述

实例身份:我是谁,我能干什么

每个 Claude Code 实例需要两样东西:身份标识(我是谁)和能力范围(我能干什么)。

身份标识有三级读取优先级:

1. 环境变量 CLAUDE_WORKER_ID        ← 推荐
       ↓ 未设置
2. 本地文件 ~/.claude/task-queue-myid  ← 适合固定身份的机器
       ↓ 不存在
3. 自动生成 hostname-pid             ← 仅用于临时测试

第三种为什么不推荐?每次脚本调用 PID 都不同,pick.sh 领的任务,done.sh 找不到——文件名里带的 ID 对不上。

推荐做法是在 ~/.bashrc 里定义别名:

alias claude-fe='CLAUDE_WORKER_ID=frontend TASK_QUEUE_DIR=~/task-queue claude'
alias claude-be='CLAUDE_WORKER_ID=backend  TASK_QUEUE_DIR=~/task-queue claude'
alias claude-docs='CLAUDE_WORKER_ID=docs   TASK_QUEUE_DIR=~/task-queue claude'

之后 claude-fe 一敲,身份和能力自动就位。

角色定义集中在 workers.yaml 里:

workers:
  frontend:
    name: "前端工程师"
    capabilities: [code-fix, frontend, unit-test]
    work_dir: /home/user/project
  backend:
    name: "后端工程师"
    capabilities: [code-fix, backend, api-design]
    work_dir: /home/user/project
  docs:
    name: "文档工程师"
    capabilities: [documentation, code-review]
    work_dir: /home/user/project

角色写死在 skill 代码里不好吗?不好。角色是配置,不是逻辑。今天三个实例,明天可能五个,改 yaml 比改代码靠谱。

领取任务时,pick.sh 拿自己的 capabilities 去匹配任务的 type。type 是 frontend 的任务,只有 capabilities 里含 frontend 的实例能领。assignee 是 any 的任务,所有匹配 type 的实例都能抢;assignee 指定了具体 ID,只有那个实例能领。
在这里插入图片描述

核心脚本拆解

整个 skill 七个 bash 脚本,逻辑不复杂,但每个都有讲究。

common.sh——公共函数库,所有脚本都 source 它:

get_worker_id()       # 三级优先级读取身份
get_my_capabilities() # 从 workers.yaml 读取能力列表
get_my_work_dir()     # 读取工作目录
now_epoch()           # 当前时间戳(秒)
now_iso()             # ISO 格式时间戳

capabilities 的解析踩过坑。最初用 gsub(/[[:space:]]/, "") 清空格,结果连换行符也清了,“documentation” 和 “code-review” 粘成了 “documentationcode-review”。后来改成 split 按逗号拆分,只清空格和 tab,不动换行,问题解决。

pick.sh——核心领取逻辑,155 行,做了这几件事:

  1. 扫描 pending/ 目录的所有 .md 文件
  2. 扫描 running/ 目录中超时的文件(当前时间 - 修改时间 > timeout)
  3. 对每个候选任务检查 assignee 和 type 是否匹配
  4. 按 priority 排序,选最小的
  5. 尝试 mv 抢占——成功就 claimed,失败就 lost

超时任务的处理有个细节:先 mv 回 pending 原始文件名,再走正常抢占流程。这样保证了文件名的一致性。

done.sh / fail.sh——找到自己 running/ 里的任务,追加执行记录,mv 到对应目录。追加的记录包括完成时间、实例 ID、结果摘要。

status.sh——输出 JSON,包含各目录计数和本实例当前运行的任务。

list.sh——表格形式展示 pending 任务,标注哪些本实例能领。

init.sh——一键创建目录结构、配置文件和示例任务。
在这里插入图片描述

定时轮询:有空就干,没空等着

CronCreate 可以注册定时任务,让 Claude Code 每隔几分钟自动检查队列。但有几个限制你得知道:

第一,只在 REPL 空闲时触发。你正在和 Claude 对话的时候,轮询不会执行。所以是"有空就干,没空等着"——不是后台线程。

第二,recurring 任务 7 天自动过期。到期得重新 /task-queue start

第三,轮询绑定当前会话,Claude Code 退出就没了。

推荐策略:自动轮询兜底 + 手动 pick 应急。日常靠轮询自动领任务,紧急任务或轮询没触发时手动 /task-queue pick

轮询间隔我设的 300 秒(5分钟)。太短了频繁扫描没意义,太长了任务响应慢。5 分钟是个平衡点——你喝口水回来,新任务大概率已经被领走了。
在这里插入图片描述

实测数据

跑了两轮测试。

场景1:并发抢占

两个实例(frontend 和 backend)同时抢一个 type: code-fix, assignee: any 的任务。

尝试次数 frontend 结果 backend 结果
1 claimed lost
2 lost claimed
3 claimed lost

每次都只有一个成功,符合预期。

场景2:超时重领

backend 领了一个任务后"消失"(不再操作),超过 timeout 后 frontend 重新领取成功。

时间线 事件
T+0 backend 领取任务
T+600 超时阈值到达
T+602 frontend pick 检测到超时,mv 回 pending,再 mv 到 running

踩坑记录

  1. gsub(/[[:space:]]/, "") 会清掉换行符——改成 split + 逐项清理。
  2. local best_fileset -u 下报 unbound variable——改成 local best_file=""
  3. 临时 ID(hostname-pid)每次调用 PID 不同——文档里明确标注"仅用于测试,生产必须用固定身份"。
    在这里插入图片描述

什么时候用,什么时候别用

适合的场景:

  • 一个人开多个 Claude Code 窗口,按功能分工并行开发
  • 长期项目积累待办事项,各实例有空就领
  • CI/CD 管道发现 bug,自动写文件到 pending/ 入队
  • 多个文件需要 code review,分发给不同实例并行审

不适合的场景:

  • 需要毫秒级响应的实时调度——文件系统轮询做不到
  • 多人共用一台机器的严格互斥——加 flock 可以缓解,但本质上是单机方案
  • 任务间有复杂依赖关系——当前不支持 DAG 调度,得手动控制顺序
  • 需要跨机器协调——本方案仅限同一台机器的本地文件系统

边界清醒比方案完美更重要。这个方案解决的是"一个人多窗口并行"的问题,不是分布式任务调度的问题。

后续可以做什么

任务依赖是最自然的扩展——在 frontmatter 里加 depends_on: ["001"],pick 时检查前置任务是否在 done/ 里。优先级动态调整也有用,紧急任务插队。结果通知可以接 SendMessage 或系统通知。Web 可视化用一个简单 HTML 页面展示队列状态就行。跨机器支持用 Git 仓库当共享存储,push/pull 同步状态。

这些都不急。先把单机多实例跑顺了,再考虑扩展。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐