首页
/ AutoGPT Orchestrate Skill:用 tmux 与 git worktree 编排 Claude Code 并行代理舰队的元监督系统

AutoGPT Orchestrate Skill:用 tmux 与 git worktree 编排 Claude Code 并行代理舰队的元监督系统

2026-09-06 23:42:12作者:田桥桑Industrious

本文基于 AutoGPT 仓库中的 orchestrate 技能定义 及其 scripts 目录 下的十个 Shell 脚本撰写。它讲解如何用「一个 tmux 会话 + N 个窗口 + N 个 git worktree」的模型并行驱动多个 Claude Code 代理:从空闲 worktree 的发现与分配、代理的生成与任务下发、机械层(零 token)的闲置重启与对话框批准、到基于 checkpoint 与 CI/Review 状态的严格完成校验。读完后,你将能够复现这套「LLM 监督 + 脚本机械执行」的双层代理编排架构,并理解其中状态机、原子状态写入、会话恢复与假完成检测等关键实现细节。

一、系统总览:一个会话,N 个窗口,一个机械保姆

orchestrate 是一个可被用户直接调用的 Claude Code 技能(user-invocable: true),其定位是元代理监督者:当前这个 Claude 会话本身就是监督者,负责阅读各代理窗口(pane)的输出、检查 CI、对停滞的代理下达针对性指导。其核心拓扑是:

Orchestrating Claude(本会话 = 监督者)
  └── 读取 pane 输出、检查 CI、干预
        run-loop.sh(独立 tmux 窗口,每 30s 一轮)
          └── 仅做机械动作:闲置重启、对话框批准、状态流转

这套模型的关键设计原则是:监督者不另开窗口。技能文档明确警告——另开一个监督 Claude 窗口会丢失上下文、难以观察,且会叠加上下文压缩问题;因此监督者必须留在当前对话中,以每 2~3 分钟一次的轮询节奏主动监控。

与之配合的是机械层 run-loop.sh,它「零 token」运行,只处理无需判断力的事务:重启已崩溃的代理、在对话框上按 Enter、在完成且验证通过后流转状态。从源码看,run-loop.sh 在启动时会先把自身所在的 scripts 目录复制到 $HOME/.claude/orchestrator/scripts 这一稳定位置(run-loop.sh#L22-L27),原因是 recycle-agent.sh 回收 worktree 时可能把当前分支切换回 spare/N,从而清掉仅存在于当前分支上的技能脚本副本。

脚本工具箱

技能共提供 10 个脚本,职责划分清晰:

脚本 用途
find-spare.sh [REPO_ROOT] 列出空闲 worktree,每行输出 PATH BRANCH
spawn-agent.sh SESSION PATH SPARE NEW_BRANCH OBJECTIVE [PR] [STEPS...] 建窗口 + 切分支 + 启动 claude + 下发任务,stdout 只输出 SESSION:WIN
recycle-agent.sh WINDOW PATH SPARE_BRANCH 杀掉窗口 + 还原 spare 分支
run-loop.sh 机械保姆:闲置重启 + 对话框批准 + 完成信号流转 + 健康检查
verify-complete.sh WINDOW 验证 PR 真正完成:checkpoint ✓ + 0 未解决线程 + CI 绿 + 无新鲜 CHANGES_REQUESTED
notify.sh MESSAGE 经 Discord webhook(DISCORD_WEBHOOK_URL 或状态文件 .discord_webhook)、macOS 通知中心、stdout 三路发通知
capacity.sh [REPO_ROOT] 打印可用与占用中的 worktree
status.sh 打印舰队状态 + 各 pane 实时前台命令
poll-cycle.sh 一次监控循环——分类 pane、跟踪 checkpoint、返回 JSON 动作数组
classify-pane.sh WINDOW 对单个 pane 做状态分类

二、worktree 的发现与回收:spare/N 分支模型

整套系统以 git worktree 为代理的物理隔离单元。每个 worktree 平时停留在一个 spare/N 分支上表示「空闲」,被占用时切出 feat/xxx 任务分支,完成后回收回 spare/N

find-spare.sh 的实现非常精炼:直接解析 git worktree list --porcelain 输出,用 awk 提取 worktreebranch 字段,再只保留分支名匹配 refs/heads/spare/[0-9]+$ 的行(find-spare.sh#L18-L24):

git -C "$REPO_ROOT" worktree list --porcelain \
  | awk '
      /^worktree / { path = substr($0, 10) }
      /^branch /   { branch = substr($0, 8); print path " " branch }
    ' \
  | { grep -E " refs/heads/spare/[0-9]+$" || true; } \
  | sed 's|refs/heads/||'

因此「空闲 worktree」的判定标准就是当前分支是 spare/N——这也正是后面「受保护 worktree」规则依赖的锚点。

回收则由 recycle-agent.sh 完成,其步骤(recycle-agent.sh#L23-L30):

tmux kill-window -t "$WINDOW" 2>/dev/null || true   # 杀窗口(可能已不存在)
git -C "$WORKTREE_PATH" rebase --abort 2>/dev/null || true
git -C "$WORKTREE_PATH" merge --abort 2>/dev/null || true
git -C "$WORKTREE_PATH" reset --hard HEAD 2>/dev/null
git -C "$WORKTREE_PATH" clean -fd 2>/dev/null
git -C "$WORKTREE_PATH" checkout "$SPARE_BRANCH"   # 还原到 spare/N

先中止可能进行中的 rebase/merge、硬重置并清掉未跟踪文件,最后切回 spare 分支——保证 worktree 以干净状态重新进入「空闲」池。

受保护 worktree 规则:技能文档特别指出 AutoGPT1(分支 dx/orchestrate-skill)承载了 orchestrate 技能脚本本身,若被当作 spare 使用,recycle-agent.sh 把它切回 spare/1 会清掉 .claude/skills/,使后续所有 spawn-agent.sh 调用失效。规则是:用 find-spare.sh 选 worktree 时必须跳过当前分支为受保护分支的 worktree;若误在其中生成了代理,绝不允许对它跑 recycle-agent.sh,任务结束后要手动恢复分支。

三、生成代理:spawn-agent.sh 的完整动作链

spawn-agent.sh 是整个系统里最复杂的脚本,一次调用完成六件事:

  1. 生成稳定会话 IDSESSION_ID=$(uuidgen ...)L35),该 UUID 传给 claude --session-id,使会话崩溃后始终能用 claude --resume $SESSION_ID 精确恢复完整上下文;
  2. 创建/切换到任务分支git checkout -b "$NEW_BRANCH",失败则退回 checkout "$NEW_BRANCH"L38-L39);
  3. 开命名 tmux 窗口tmux new-window -t "$SESSION" -n "$WORKTREE_NAME" -P -F '#{window_index}',捕获窗口号组成 WINDOW="SESSION:IDX"
  4. 写入初始 agent 记录:用 jq 把包含 windowworktree_pathspare_branchsession_idstate: "running" 等字段的记录追加到状态文件(L47-L74),若提供了 PR_NUMBERSTEPS,再打一层补丁写入 pr_number/steps——因此调用方不得在脚本返回后再重复追加记录;
  5. 启动 claude 并等待就绪tmux send-keys ... "cd <path> && claude --permission-mode bypassPermissions --session-id '<ID>'" EnterL91);随后 _wait_idle 最长等 60 秒,判定条件是三行 pane 尾部出现 且无 spinner 字符(✳✽✢✶· 等),期间遇到 Enter to confirm 对话框会自动 Down Enter 确认(L95-L120);
  6. 下发任务文本:把 objective 与完成协议拼在一起发送——「每完成一步输出 CHECKPOINT:<step-name>,全部完成后单独一行输出 ORCHESTRATOR:DONE」。

第 6 步严格采用「文本与 Enter 分开发送 + sleep 0.3」的拆分模式(L124-L126),原因在注释中写得很清楚:合并发送时 Enter 可能在字符串尚未完全缓冲进 Claude 输入区时触发,消息会卡成未发送的 [Pasted text +N lines]。这个模式被上升为全局规则:长消息必须拆分,单字符短消息(yDown、空 Enter)才可以合并

四、checkpoint 协议与 pane 状态机

协议约定

代理每完成一个必需步骤,就在 pane 中输出一行 CHECKPOINT:<step-name>。必需步骤在生成时以参数传入(如 pr-address pr-test)。run-loop.sh 不会在所有必需 checkpoint 都被发现之前回收窗口;verify-complete.sh 失败时代理会被自动 re-brief。

poll-cycle.sh:状态机的核心

poll-cycle.sh 每轮对每个 agent 执行一次完整分类并原子更新状态文件,其要点(结合源码):

  • 安全防御:先对 window 值做正则校验 ^[a-zA-Z0-9_.-]+:[a-zA-Z0-9_.-]+(\.[0-9]+)?$,防止 tmux target 注入(L88-L92);对状态文件先做 jq -e '.' 预校验,被 SIGKILL 截断的损坏文件会输出 [] 而不是在 set -e 下崩溃(L40-L44);
  • checkpoint 解析:捕获最近 500 行滚动缓冲区(tmux capture-pane -S -500),grep 出所有 CHECKPOINT:[a-zA-Z0-9_-]+sort -u 后与状态中已有 checkpoint 求并集(L113-L130);
  • 卡死检测:对处于 running 的 agent,取 pane 尾部 20 行做 md5(跨平台兼容 md5sum/md5),与上一轮的 last_output_hash 比较;哈希连续稳定超过 idle_threshold_seconds(默认 300 秒)即判定 stuck 并触发 kick(L129-L136);
  • 升级机制revision_count 累计 kick 次数,达到 3 次即置为 escalated,不再自动重启,转人工处理——对应技能文档「Escalate after 3 kicks」规则(L162-L166L187-L195);
  • 完成信号:pane 中出现 ORCHESTRATOR:DONE 时,状态置为 pending_evaluation、动作记为 complete——但 run-loop 不做验证只记日志,真正的评估交给监督者的后台轮询(L144-L150);
  • 原子写:所有状态更新遵循「jq 写 .tmpmv」的模式,且用 trap 在退出时清理残留 .tmpL30L252-L255)。

classify-pane.sh:单窗口四态分类

classify-pane.sh 输出 {"state": "running|idle|waiting_approval|complete", ...} JSON,判定优先级是:

  1. pane 中存在独占一行的 ORCHESTRATOR:DONEcompleteL43);
  2. 尾部 40 行命中审批模式("Do you want to proceed"、"[y/n]"、"Esc to cancel" 等 10 种模式)→ waiting_approvalL48-L70);
  3. pane 前台进程是 shell(zsh/bash/fish…)→ idle,即 claude 已退出(L74-L79);
  4. 其余 → running

run-loop.sh 的机械动作

拿到 poll-cycle 的动作数组后,run-loop 只做三件事(run-loop.sh#L194-L198):

  • kick:仅对 idle 状态(代理进程已退出)生效——先 wait_for_claude_idle 等 pane 稳定,再发送 claude --resume '<session_id>' --permission-mode bypassPermissions(无 session_id 时用 --continue)恢复原会话上下文(L114-L136);
  • approve:按三种模式自动批准——Enter to confirm 对话框发 Down Enter;编号选项对话框(❯ 1. 已在第一项)直接按 Enter;git/npm/pnpm/poetry/pytest/docker/make/cargo/pip/yarn 等白名单前缀或 localhost curl 的 y/n 提示发 yL141-L170)。其余未知对话框一律跳过,交由监督者处理;
  • 轮询间隔自适应退避:基础间隔 30 秒,无任何活动时按 POLL_CURRENT + POLL_CURRENT/2 + 1 递增直至封顶 300 秒;一旦发生 kick/complete/出现 waiting_approval 则立即重置回基础值(L204-L211)。

一个容易误读的细节:run-loop 日志中的 RUNNING 计数匹配正则 running|stuck|waiting_approval|idle,所以全队在等待审批时 RUNNING 也 >0,并不表示代理在干活。怀疑时直接查状态文件:

jq '.agents[] | {window, state, worktree}' ~/.claude/orchestrator-state.json

五、状态文件:单一事实源

状态文件位于 ~/.claude/orchestrator-state.json(可用 ORCHESTRATOR_STATE_FILE 环境变量覆盖),从不提交进 git,由监督者直接用 jq + 原子写维护。完整结构如下:

{
  "active": true,
  "tmux_session": "autogpt1",
  "idle_threshold_seconds": 300,
  "loop_window": "autogpt1:5",
  "repo": "Significant-Gravitas/AutoGPT",
  "discord_webhook": "https://discord.com/api/webhooks/...",
  "last_poll_at": 0,
  "agents": [
    {
      "window": "autogpt1:3",
      "worktree": "AutoGPT6",
      "worktree_path": "/path/to/AutoGPT6",
      "spare_branch": "spare/6",
      "branch": "feat/my-feature",
      "objective": "Implement X and open a PR",
      "pr_number": "12345",
      "session_id": "550e8400-e29b-41d4-a716-446655440000",
      "steps": ["pr-address", "pr-test"],
      "checkpoints": ["pr-address"],
      "state": "running",
      "last_output_hash": "",
      "last_seen_at": 0,
      "spawned_at": 0,
      "idle_since": 0,
      "revision_count": 0,
      "last_rebriefed_at": 0
    }
  ]
}

顶层可选字段:repo(GitHub owner/repo,用于 CI/线程检查,缺省时脚本从 git remote 自动推导);discord_webhook(完成通知,亦读 DISCORD_WEBHOOK_URL 环境变量)。

关键字段语义(与 spawn-agent.sh#L57-L73 的实际写入一一对应):

  • session_id——传给 claude --session-id 的 UUID,用于 claude --resume UUID 在崩溃或窗口关闭后恢复精确会话;
  • last_rebriefed_at——上次 re-brief 的 Unix 时间戳,强制 5 分钟冷却防止刷屏;
  • spawned_at——代理生成时间,是 verify-complete 第 4 项检查(「CI 运行必须晚于代理生成」)的基准;
  • revision_count / idle_since——卡死与升级计数的累加器。

代理状态机共七个状态:running | idle | stuck | waiting_approval | complete | done | escalated(poll-cycle 内部还有 pending_evaluation 作为完成评估的中间态)。done 的含义是「已验证完成」——窗口仍然开着、会话仍然活着、worktree 仍停在任务分支上,尚未回收,这与下文「窗口永不自动杀死」的原则一致。

状态漂移的恢复

脚本写入的状态文件可能因窗口被关、会话过期或监督者跨对话重启而与真实环境漂移。典型征兆:loop_window 指向已不存在的窗口;agent 状态是 running 但 tmux 窗口已关闭或只剩 shell 提示符;last_seen_at 已是数小时前。恢复步骤:

# 1. 核对真实 tmux 窗口
tmux list-windows -t SESSION -F '#{window_index}: #{window_name} (#{pane_current_command})'

# 2. 与状态文件交叉比对
jq -r '.agents[] | "\(.window) \(.state) \(.worktree)"' ~/.claude/orchestrator-state.json

# 3a. agent 窗口已关 —— 标 idle 让 run-loop 重启它
jq --arg w "SESSION:WIN" '(.agents[] | select(.window==$w)).state = "idle"' \
  ~/.claude/orchestrator-state.json > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json

# 3b. loop_window 已亡 —— 清引用并重启 run-loop.sh
jq '.loop_window = null' ~/.claude/orchestrator-state.json > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json
LOOP_WIN=$(tmux new-window -t "$SESSION" -n "orchestrator" -P -F '#{window_index}')
tmux send-keys -t "${SESSION}:${LOOP_WIN}" "bash $SKILLS_DIR/run-loop.sh" Enter
jq --arg w "${SESSION}:${LOOP_WIN}" '.loop_window = $w' ~/.claude/orchestrator-state.json \
  > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json

# 4. 任何修复后重跑 status.sh 确认一致性再恢复监督

六、生成代理的完整操作序列

监督者(Claude 本身)按下述六步执行,其中第 3 步的意图映射来自技能文档的「Intent → action mapping」表:

用户说的话 动作
"status" / "what's running" status.sh + capacity.sh 并展示
"how many free" / "capacity" capacity.sh
"start N agents on X, Y, Z" 执行下面的生成序列
"add task: …" 同序列但只处理单个任务
"stop" / "shut down" 执行停机序列(见第九节)
"poll" / "check now" poll-cycle.sh 并处理动作
"recycle window X" 直接跑 recycle-agent.sh

步骤 1 —— 解析 tmux 会话

tmux list-sessions -F "#{session_name}: #{session_windows} windows" 2>/dev/null

绝不在 Claude 进程内创建 tmux 会话——它将成为 Claude 进程的子进程,随会话结束而死。没有现成会话时,应提示用户先在终端执行 tmux new-session -d -s autogpt1 再重新调用。

步骤 2 —— 展示可用容量bash $SKILLS_DIR/capacity.sh $(git rev-parse --show-toplevel)capacity.sh 输出两段:find-spare.sh 给出的可用 worktree 列表,以及状态文件中 state != "done" 的在用 worktree(capacity.sh#L15-L43)。

步骤 3 —— 收集任务要素:每个任务需要 objective(做什么)、branch(如 feat/my-feature,未给则从 objective 推导)、pr_number(在已有 PR 上工作时必填,供验证用)、steps(按顺序的 checkpoint 名)。idle_threshold_seconds 仅当用户提及时才询问(默认 300)。永远不要求用户指定 worktree——从 find-spare.sh 自动分配。

步骤 4 —— 每任务生成一个代理

SPARE_LIST=$(bash $SKILLS_DIR/find-spare.sh $(git rev-parse --show-toplevel))
# 对每个任务取下一行 spare:
WORKTREE_PATH=$(echo "$SPARE_LINE" | awk '{print $1}')
SPARE_BRANCH=$(echo "$SPARE_LINE" | awk '{print $2}')

# 带 PR 号与必需步骤:
WINDOW=$(bash $SKILLS_DIR/spawn-agent.sh "$SESSION" "$WORKTREE_PATH" "$SPARE_BRANCH" "$NEW_BRANCH" "$OBJECTIVE" "$PR_NUMBER" "pr-address" "pr-test")
# 无 PR 的新工作:
WINDOW=$(bash $SKILLS_DIR/spawn-agent.sh "$SESSION" "$WORKTREE_PATH" "$SPARE_BRANCH" "$NEW_BRANCH" "$OBJECTIVE")

状态文件不存在时初始化(repo 从 git remote 推导,供 verify-complete.sh 与监督者使用):

REPO=$(git remote get-url origin 2>/dev/null | sed 's|.*github\.com[:/]||; s|\.git$||' || echo "")
jq -n \
  --arg session "$SESSION" \
  --arg repo "$REPO" \
  --argjson threshold 300 \
  '{active:true, tmux_session:$session, idle_threshold_seconds:$threshold,
    repo:$repo, loop_window:null, supervisor_window:null, last_poll_at:0, agents:[]}' \
  > ~/.claude/orchestrator-state.json

可选地挂上 Discord webhook:

jq --arg hook "$DISCORD_WEBHOOK_URL" '.discord_webhook = $hook' ~/.claude/orchestrator-state.json \
  > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json

步骤 5 —— 启动机械保姆

LOOP_WIN=$(tmux new-window -t "$SESSION" -n "orchestrator" -P -F '#{window_index}')
LOOP_WINDOW="${SESSION}:${LOOP_WIN}"
tmux send-keys -t "$LOOP_WINDOW" "bash $SKILLS_DIR/run-loop.sh" Enter
jq --arg w "$LOOP_WINDOW" '.loop_window = $w' ~/.claude/orchestrator-state.json \
  > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json

步骤 6 —— 立即开始监督:见下一节的轮询循环;绝不另开监督窗口。

监督者的自持轮询循环

监督者是反应式的——只在工具完成或用户发消息时才被触发。为此要构造一个不依赖用户的自持轮询:

  1. 每次轮询以 run_in_background: true + 前置 sleep 启动:
    sleep 120 && tmux capture-pane -t autogpt1:0 -p -S -200 | tail -40
    # 对每个活跃窗口类似
    
  2. 后台任务通知后读取 pane 输出并采取动作;
  3. 立即排程下一次后台轮询——这是循环存活的关键;
  4. 全部代理 done/escalated 后停止排程。

技能文档特别强调:永远不要对用户说"我会每 2-3 分钟轮询一次"——没有触发器,这句话什么都不做;必须真正启动后台任务。

每轮轮询的检查清单:

# 1. 读状态
cat ~/.claude/orchestrator-state.json | jq '.agents[] | {window, worktree, branch, state, pr_number, checkpoints}'
# 2. 对每个 running/stuck/idle 代理捕获 pane
tmux capture-pane -t SESSION:WIN -p -S -200 | tail -60

按观察结果决策:

看到什么 动作
Spinner / 工具运行中 什么都不做——代理在干活
空闲 提示符且无 ORCHESTRATOR:DONE 停滞——发送带状态文件中 objective 的针对性 nudge
错误循环卡死 发送带精确错误与解法的定向修复
等待输入 / 提问 tmux send-keys 回答并解除阻塞
CI 红 gh pr checks PR_NUMBER --repo REPO,告诉代理具体哪一项失败
GitHub abuse 限流错误 nudge:"等 60 秒再继续,每次回复间隔 sleep 3"
上下文被压缩 / 代理迷失 发送恢复指令:读状态文件中该 agent 记录 + gh pr view PR_NUMBER --json title,body
输出中出现 ORCHESTRATOR:DONE 用 GraphQL 查真实未解决线程数,>0 则 re-brief;=0 则跑 verify-complete.sh

从状态文件轮询所有窗口,而不是靠记忆

jq -r '.agents[] | select(.state | test("running|idle|stuck|waiting_approval")) | .window' ~/.claude/orchestrator-state.json

凡是在 spawn-agent.sh 之外手工加的窗口,必须立刻补进状态文件。

七、worktree 生命周期与「永不自动杀窗」

spare/N 分支  →  spawn-agent.sh(--session-id UUID)→  窗口 + feat/分支 + claude 运行中
                                                          ↓
                                            CHECKPOINT:<step>(随步骤完成逐个出现)
                                                          ↓
                                                   ORCHESTRATOR:DONE
                                                          ↓
                          verify-complete.sh:checkpoint ✓ + 0 线程 + CI 绿 + 无新鲜 CHANGES_REQUESTED
                                                          ↓
                                        状态 → "done",发通知,窗口保持打开
                                                          ↓
                                        用户/监督者显式要求回收
                                                          ↓
                                    recycle-agent.sh → 回到 spare/N(重新空闲)

窗口永不自动杀死:worktree 停在任务分支、会话保持存活——代理已完成工作,但窗口、git 状态与 Claude 会话全部保留,直到你选择回收。恢复 done 或崩溃会话:

# 按存储的 session ID 恢复(首选——精确会话、完整上下文)
claude --resume SESSION_ID --permission-mode bypassPermissions
# 或恢复该 worktree 目录中最近一次会话
cd /path/to/worktree && claude --continue --permission-mode bypassPermissions

经 tmux 恢复时新建窗口并把命令送进去,然后更新状态文件中的窗口地址:

NEW_WIN=$(tmux new-window -t SESSION -n WORKTREE_NAME -P -F '#{window_index}')
tmux send-keys -t "SESSION:${NEW_WIN}" "cd /path/to/worktree && claude --resume SESSION_ID --permission-mode bypassPermissions" Enter

jq --arg old "SESSION:OLD_WIN" --arg new "SESSION:NEW_WIN" \
  '(.agents[] | select(.window == $old)).window = $new' \
  ~/.claude/orchestrator-state.json > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json

--continue 会恢复包括所有工具调用、文件编辑与上下文在内的完整对话历史,代理从断点精确继续。手工回收的完整命令:

bash ~/.claude/orchestrator/scripts/recycle-agent.sh SESSION:WIN WORKTREE_PATH spare/N
jq --arg w "SESSION:WIN" '.agents |= map(if .window == $w then .state = "recycled" else . end)' \
  ~/.claude/orchestrator-state.json > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json

八、完成校验:verify-complete.sh 的六项检查

verify-complete.sh WINDOW 是「ORCHESTRATOR:DONE」的严格闸门,检查顺序刻意敏感verify-complete.sh#L2-L10):

1. Checkpoints     —— 必需步骤是否都打了 checkpoint
2. CI complete    —— 无 pending(机器人在检查运行后才发评论,必须先等)
3. CI passing     —— 无 fail
4. spawned_at     —— 分支上存在晚于代理生成时刻的 CI 运行(证明真实 push 过)
5. 0 unresolved threads —— 放在 CI 之后,确保机器人评论被算入
6. 无新鲜 CHANGES_REQUESTED —— 同上

源码层面的几处关键设计:

  • fail-closed 原则gh pr view ... --json commits,latestReviews 拉取失败时直接判 NOT COMPLETE,而不是静默放行(L127-L131);
  • CHANGES_REQUESTED 陈旧性规则:一条 CHANGES_REQUESTED 评审只有在提交时间晚于最新 commit 时才阻断验证;使用 committedDate 而非 updatedAt(后者会被机器人评论等任何 PR 活动刷新,制造假阴性);使用 latestReviews 而非 reviews,评审者后来 approve 会自动使旧的 CHANGES_REQUESTED 失效(L117-L177);
  • 无 repo 时的降级:状态文件与 git remote 都推不出 repo 时,只验证 checkpoint 并打印警告后放行(L54-L58)——这是唯一允许跳过 GitHub 检查的分支。

通过 → run-loop 会自动流转、无需人工干预;失败 → 用失败原因 re-brief 代理。绝不手工把状态改成 done 来绕过

re-brief 停滞代理的正确姿势

发任何 nudge 前,先确认 pane 处于空闲 提示符——往仍在处理的 pane 里发文本会产生代理永远看不见的 [Pasted text +N lines]

tmux capture-pane -t SESSION:WIN -p 2>/dev/null | tail -5
# 末行是 spinner(✳✽✢✶·)、"Running…" 或没有 ❯ —— 等 10–15 秒再查

确认空闲后:

OBJ=$(jq -r --arg w SESSION:WIN '.agents[] | select(.window==$w) | .objective' ~/.claude/orchestrator-state.json)
PR=$(jq -r --arg w SESSION:WIN '.agents[] | select(.window==$w) | .pr_number' ~/.claude/orchestrator-state.json)
tmux send-keys -t SESSION:WIN "You appear stalled. Your objective: $OBJ. Check: gh pr view $PR --json title,body,headRefName to reorient."
sleep 0.3
tmux send-keys -t SESSION:WIN Enter

若 agent 记录里设了 image_path,附加一句 "Re-read context at IMAGE_PATH with the Read tool."

代理的自恢复协议

spawn-agent.sh 会自动把以下指令注入每个 objective(见 spawn-agent.sh#L124 的拼装文本):

如果你的上下文被压缩、忘记了该做什么,运行: cat ~/.claude/orchestrator-state.json | jq '.agents[] | select(.window=="SESSION:WIN")' 以及 gh pr view PR_NUMBER --json title,body,headRefName 来重新定向。 每完成一个步骤就单独一行输出 CHECKPOINT:<step-name>

给代理传图片

tmux send-keys 只能传文本,无法往 pane 粘贴原始图片。给代理视觉上下文的标准做法:

  1. 把图片存到稳定路径:cp "$USER_PROVIDED_PATH" /tmp/orchestrator-context-$(date +%s).png
  2. 在 objective 字符串里引用路径,例如: OBJECTIVE="Implement the layout shown in /tmp/orchestrator-context-1234567890.png. Read that image first with the Read tool to understand the design."
  3. 代理用 Read 工具直接读图——Claude Code 代理是多模态的。

命名约定固定为 /tmp/orchestrator-context-<timestamp>.png,这样监督者 re-brief 时知道找哪张图。

九、/pr-test 串行化规则与最终评估

为什么必须串行

/pr-test/pr-test --fix 运行本地 Docker + 集成测试,依赖共享端口、共享数据库和共享构建缓存。两个 /pr-test 同时跑会导致端口冲突与数据库损坏。规则:任意时刻只允许一个 /pr-test,由监督者串行化

监督者拥有测试队列:

  1. 代理们并行做 pr-reviewpr-address 是安全的(只 push 代码、回复 GitHub);
  2. 某个 PR 需要本地测试时,把它加入你的心智队列——不要给代理 pr-test 步骤;
  3. 由你自己顺序执行 /pr-test https://github.com/OWNER/REPO/pull/PR_NUMBER --fix
  4. 通过 tmux send-keys 把结果回喂给相关代理:
    tmux send-keys -t SESSION:WIN "Local tests for PR #N: <paste failure output or 'all passed'>. Fix any failures and push, then output ORCHESTRATOR:DONE."
    sleep 0.3
    tmux send-keys -t SESSION:WIN Enter
    
  5. 等 CI 确认变绿后才可标记代理 done。

多个 PR 同时等待测试时,先测进展最靠前的(pending CI 检查最少);上一个跑完才开下一个。

最终评估:脚本是闸门,人是裁判

verify-complete.sh 只能阻止过早标记,无法判断工作是否真的合格——那是监督者的职责。当 run-loop 把代理标为 pending_evaluation 并通知你时,必须完成以下三步:

第 1 步:亲自跑 /pr-test(必需、串行、用 TodoWrite 排队)

- [ ] /pr-test https://github.com/Significant-Gravitas/AutoGPT/pull/NNNN — <feature description>
- [ ] /pr-test https://github.com/Significant-Gravitas/AutoGPT/pull/MMMM — <feature description>

/pr-test 可能「偷懒」给出含糊输出,此时带完整上下文重跑:

/pr-test https://github.com/OWNER/REPO/pull/PR_NUMBER
Context: This PR implements <objective from state file>. Key files: <list>.
Please verify: <specific behaviors to check>.

结果评估铁律:任何头条功能场景 PARTIAL 都是即时阻断项——不批准、不标 done、不接受 ORCHESTRATOR:DONE

/pr-test 结果 动作
全部头条场景 PASS 进入评估第 2 步
任一头条场景 PARTIAL 立即 re-brief 代理
任一头条场景 FAIL 立即 re-brief 代理

PARTIAL 的含义是「功能只部分工作」——例如 Apply 按钮从未出现、或 AI 没有返回任何 action block:代理完成了目标的一部分而非全部。PARTIAL/FAIL 时的处理:① 不标记 done;② 用具体失败场景 re-brief,例如:

tmux send-keys -t SESSION:WIN "PARTIAL result on /pr-test — S5 (Apply button) never appeared. The AI must output JSON action blocks for the Apply button to render. Fix this before re-running /pr-test."
sleep 0.3
tmux send-keys -t SESSION:WIN Enter

③ 把状态改回 running;④ 等新 ORCHESTRATOR:DONE 后再从头重跑 /pr-test只有全 PASS 才有资格批准——PASS 与 PARTIAL 的混合即为失败。 技能文档记述了一次真实教训:一个 PR 曾在 S5 PARTIAL 的情况下被误批——AI 从未输出 JSON action block 导致 Apply 按钮根本不出现,修复其实已在代理触手可及,只因 PARTIAL 未被当作阻断项而漏过。

第 2 步:自主评估四查:读 PR diff 对照 objective(代码是否真的实现了要求);读已解决线程(是真修复还是无改动直接 resolve);查 CI 运行名(有无不该通过的可疑重试);查 PR 描述(标题、摘要、测试计划是否齐全)。

第 3 步:裁决

  • 全 PASS + 评估良好 → 标 done,告知用户 PR 已就绪,询问是否关窗;
  • 任一 PARTIAL/FAIL → re-brief,状态回 running
  • 即便全 PASS 但评估发现缺口 → re-brief 具体缺口,状态回 running
# 正面评估通过后才标记 done
jq --arg w "SESSION:WIN" '(.agents[] | select(.window == $w)).state = "done"' \
  ~/.claude/orchestrator-state.json > /tmp/orch.tmp && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json

绝不基于脚本输出就标记 done——监督者握有完整 objective 上下文,脚本没有。

十、线程解决完整性:防「假完成」的 GraphQL 稽核

这是全系统最常见的失败模式:代理调用 resolveReviewThread 让未解决计数归零,却根本没修代码——产生能通过 verify-complete.sh 的假「完成」信号。

唯一合法的解决顺序:① 读线程理解诉求 → ② 做真实代码修改 → ③ git commit + git push → ④ 在线程中回复 commit SHA(如 "Fixed in abc1234")→ ⑤ 然后才调用 resolveReviewThread。「Accepted」「Acknowledged」之类的回复不算解决,只有真实 commit 才算。

监督者必须用 GraphQL 亲验线程计数,永远不信代理的"0 unresolved"自报。第一步取总数,第二步分页遍历所有页(大 PR 上 first:100 只会漏掉第一页之后的线程):

# 取总数
TOTAL=$(gh api graphql -f query='{ repository(owner: "OWNER", name: "REPO") { pullRequest(number: PR) { reviewThreads { totalCount } } } }' \
  | jq '.data.repository.pullRequest.reviewThreads.totalCount')

# 分页统计未解决
CURSOR=""; UNRESOLVED=0
while true; do
  AFTER=${CURSOR:+", after: \"$CURSOR\""}
  PAGE=$(gh api graphql -f query="{ repository(owner: \"OWNER\", name: \"REPO\") { pullRequest(number: PR) { reviewThreads(first: 100${AFTER}) { pageInfo { hasNextPage endCursor } nodes { isResolved } } } } }")
  UNRESOLVED=$(( UNRESOLVED + $(echo "$PAGE" | jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false)] | length') ))
  HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
  CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
  [ "$HAS_NEXT" = "false" ] && break
done
echo "Unresolved: $UNRESOLVED"

未解决 >0 即代理未完成任务——带真实计数与规则 re-brief。此外还应稽核假解决:分页收集所有已解决线程,凡最后一条评论不含 "Fixed in""Removed in""Addressed in"(附 commit 链接)的已解决线程都应追查——代理可能虚假解决,或需要解释的假阳性。每条代理 objective 都应包含该禁令原文,并作为 pr-address 工作的第 1 号规则。

GitHub 限流的两种形态

错误 HTTP 状态 成因 恢复
body 含 {"code":"abuse"} 403 次级限流——短时间写操作(评论、mutation)过多 2–3 分钟,60 秒通常不够
API rate limit exceeded 429 主级限流——每小时读调用过多 等到 X-RateLimit-Reset 时间戳

预防:代理回复线程的 API 调用之间必须 sleep 3;未解决线程超过 20 条时提高到 sleep 5。看到 403 abuse:① nudge 代理停止所有 API 写操作、等 2 分钟后以 sleep 3 间隔恢复;② 等待期间不要再 nudge——第二次 nudge 会重置限流时钟。

十一、停机条件与全局规则

何时停止舰队(active = false)

以下检查全部满足才可停:

检查项 验证方式
所有代理为 doneescalated `jq '[.agents[]
所有 PR 有 0 未解决评审线程 每 PR 的 GraphQL isResolved 检查
所有 PR CI 绿,且运行触发于代理最后一次 push 之后 gh run list --branch BRANCH --limit 1 时间戳 > 状态中的 spawned_at
无新鲜 CHANGES_REQUESTED(晚于最新 commit) verify-complete.sh 检查此点——陈旧的前置评审被忽略
无未经人工审阅的 escalated 代理 有则先上报用户

不要仅因代理输出了 ORCHESTRATOR:DONE 就停——那是验证的信号,不是停止的信号。但用户明确说 "stop" / "shut down" / "kill everything" 时即使代理仍在跑也应停:

jq '.active = false' ~/.claude/orchestrator-state.json > /tmp/orch.tmp \
  && mv /tmp/orch.tmp ~/.claude/orchestrator-state.json
LOOP_WINDOW=$(jq -r '.loop_window // ""' ~/.claude/orchestrator-state.json)
[ -n "$LOOP_WINDOW" ] && tmux kill-window -t "$LOOP_WINDOW" 2>/dev/null || true

优雅停机不回收运行中的 worktree——代理可能仍在任务中途;跑 capacity.sh 查看仍在进行的工作。注意 run-loop.sh 每轮开头都会检查 .active,置 false 后它会自行退出(run-loop.sh#L180-L183)。

全局关键规则

技能文档将 16 条铁律收束为:脚本承担重活(不重写其逻辑);从不让用户挑 worktree;从不重启运行中的代理(只对 idle kick);自动确认 settings 对话框(Down+Enter);所有 spawn 一律 --permission-mode bypassPermissions;3 次 kick 后升级;状态写入永远原子化(.tmpmv);不批准 worktree 范围外的破坏性命令;验证不过永不回收;不建 TASK.md 文件(有提交风险,上下文持久化靠状态文件 + gh pr view);re-brief 停滞代理要读状态文件取 objective 并经 tmux 发送;ORCHESTRATOR:DONE 是验证信号而非验收信号;受保护 worktree 绝不作 spare;图片经文件路径传递(/tmp/orchestrator-context-<ts>.png);send-keys 长消息必须拆分;所有窗口从状态文件动态派生(绝不硬编码窗口数);代理重新派任务时立刻更新状态文件记录;无 commit 禁止 GraphQL resolveReviewThread;代理声称 "0 未解决线程" 后必须亲自 GraphQL 复核。

十二、小结:这套编排架构的可借鉴之处

SKILL.md 与配套脚本看,orchestrate 技能给出的是一套可迁移的多代理编排方法论,其核心取舍值得借鉴:

  1. 判断与机械分离:LLM 监督者只做需要上下文与判断的事(解读失败、定向 re-brief、最终评估),脚本层只做确定性事务(重启、批准、checkpoint 解析),且脚本通过稳定路径自拷贝抵御分支切换;
  2. 单一事实源 + 原子写:一切以 JSON 状态文件为准,jq.tmpmv,配合损坏文件预校验与 trap 清理,保证轮询链在异常下不雪崩;
  3. 完成信号与验收信号解耦ORCHESTRATOR:DONE 只触发 pending_evaluation,真正放行必须经过六项顺序敏感的校验与人工级评估,PARTIAL 视同 FAIL;
  4. 对「假完成」的制度化防御:CHANGES_REQUESTED 陈旧性判定、线程解决需附 commit SHA、GraphQL 分页全量复核——全部针对代理可能「刷指标」的失败模式;
  5. 资源隔离与回收闭环:git worktree + spare/N 分支构成可自动回收的执行池,受保护 worktree 规则防止回收动作自我伤害。

适用前提:本机有 tmux、jq、gh CLI 与可写家目录;仓库以 worktree 池方式组织并预置 spare/N 分支;Claude Code 支持 --session-id/--resume/--continue--permission-mode bypassPermissions。技能脚本位于 .claude/skills/orchestrate/scripts/,状态文件默认 ~/.claude/orchestrator-state.json,均可通过 ORCHESTRATOR_STATE_FILE 环境变量重定向。

登录后查看全文
热门项目推荐
相关项目推荐