mcpField
S
squad-run

squad-run

github.com/steloitAgent 工作流039 次安装14 周前更新
代码仓库

关于 squad-run

squad-run 是编排 squad 任务实施的技能,涵盖规划、实现、审查和测试阶段。它为多 Agent 工作流定义任务级路由、审批门和状态转换。

主要功能

  1. 01按任务 ID 自动触发
  2. 02按级别执行流水线
  3. 03路由 Worker 与 Reviewer Agent
  4. 04人工审批与驳回门
  5. 05依据判定转换状态
  6. 06经同意门控的观察事件

适用场景

  1. 01让 squad 任务经过完整流水线
  2. 02继续处理中任务的下一步
  3. 03暂停以审查计划或实现
  4. 04将驳回的工作退回修改

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/steloit/squad-skills --skill squad-run

安装后在 Claude Code 中输入/squad-run确认已加载。

详细介绍

自动触发规则

在以下情况下,务必调用此技能(无需等待 /squad-run):

  1. 用户提到 Squad 任务 ID 并要求实现:

    • “实现任务 #NNN”/“构建任务 NNN”/“做 NNN”/“运行 NNN”
    • 任何同时提到任务编号和实现/构建/处理/执行的消息
  2. Claude 提议实现某个具体的 Squad 任务,且用户确认:

    • 模式:Claude 说“要我实现任务 #NNN [标题] 吗?”→ 用户回复“是”“好”“开始”“去做”
    • 此确认必须自动触发 /squad-run <ID>,不得手动实现
  3. 有任务正在进行时,用户说“下一个任务”/“继续”:

    • 先获取看板上下文,确定下一个待办任务,然后运行它

自动触发时:提取任务 ID 并调用 /squad-run <ID> — 切勿手动实现代码,然后再更新 Squad 状态。

共享上下文:阅读 ../squad/shared.md,了解流水线级别、状态转换、API 端点、错误处理和 Agent 上下文流。 安全原则:阅读 ../squad/principles.md — 必须遵守,不可省略。 架构:阅读 ../squad/schema.md,了解完整的数据库架构、列说明和 JSON 字段格式。

命令

在 Codex 环境中,可以直接以斜杠命令文本调用此技能,例如 $squad-run <ID> 或 $squad-run <ID> --auto。

/squad-run step <ID> — 单步执行

仅执行下一个流水线步骤,然后退出。逻辑与 /squad-run 相同,但不循环。

/squad-run <ID> [--auto] — 运行完整流水线

默认:在计划审查和实现审查的审批环节暂停,等待用户确认。 --auto:全自动运行(仍会触发断路器)。

编排循环(按级别)
Per-step contract (every agent step): record → gate → commit → side-effects.
The agent records its verdict (approved/reject | pass/fail + comment) by POSTing the matching
verdict endpoint; it does NOT move status. The orchestrator then: (1) reads the recorded
verdict, (2) gates (default mode: the card SITS in its review state until the user signal;
`--auto` skips the human gate only), (3) commits the move via the generic `PATCH /api/task/:id`
to the verdict-correct next status with `current_agent:null` and `actor:"Orchestrator"` (re-issuing a move that is already
applied is a safe no-op), (4) runs side-effects (git commit + note) AFTER the move.

L1 Quick:
  todo → orchestrator PATCH {status:impl, current_agent:Worker, actor:Orchestrator} → Worker@impl implements
       → orchestrator PATCH {status:done, current_agent:null, actor:Orchestrator} → side-effects (commit + note)

L2 Standard:
  todo → [orchestrator: todo→plan, current_agent=Worker, actor:Orchestrator] → Worker@plan
       → orchestrator PATCH {status:impl, current_agent:null, actor:Orchestrator} (skip plan_review)
  impl → Worker@impl (implements + writes tests) → orchestrator PATCH {status:impl_review, current_agent:null, actor:Orchestrator}
  impl_review → Reviewer@impl_review returns verdict → orchestrator records → [user confirm] →
                orchestrator PATCH {status: done | impl, current_agent:null, actor:Orchestrator} → side-effects after done

L3 Full:
  todo → [orchestrator: todo→plan, current_agent=Worker, actor:Orchestrator] → Worker@plan
       → orchestrator PATCH {status:plan_review, current_agent:null, actor:Orchestrator}
  plan_review → Reviewer@plan_review returns verdict → orchestrator records → [user confirm] →
                orchestrator PATCH {status: impl (approve) | plan (reject), current_agent:null, actor:Orchestrator}
                (reject re-dispatches Worker@plan with <review_feedback>; plan→plan re-entry is idempotent)
  impl → Worker@impl (implements + writes tests) → orchestrator PATCH {status:impl_review, current_agent:null, actor:Orchestrator}
  impl_review → Reviewer@impl_review returns verdict → orchestrator records → [user confirm] →
                orchestrator PATCH {status: test (approve) | impl (reject), current_agent:null, actor:Orchestrator}
  test → Worker@test returns verdict → orchestrator records → [gate] →
         orchestrator PATCH {status: done (pass) | impl (fail), current_agent:null, actor:Orchestrator} → side-effects after done

Circuit breaker: plan_review_count > 3 OR impl_review_count > 3 → stop, ask user
  (counts are incremented when a verdict is recorded, not by the move PATCH)

先读取任务的 level 字段,以确定要执行哪些步骤。

模型路由(按提供商)

根据提供商从 ../squad/models.json 中解析真实模型名称:

  • 如果设置了 SQUAD_MODEL_PROVIDER 环境变量,则使用其值(claude 或 codex)
  • 否则,如果存在 CODEX_* 环境变量,则使用 codex
  • 否则,如果存在 CLAUDE_* 环境变量,则使用 claude
  • 否则,如果存在 .claude/,则使用 claude
  • 否则,如果存在 .codex/,则使用 codex
  • 否则,使用 models.json 中的 default_provider

对于 Codex,路由器应优先选择 models.json 中能力较强的条目,用于完整的 squad-run 流水线。

首先按照 ../squad/shared.md → 模型解析,解析 MODEL_PROVIDER 以及 read_model/read_effort 辅助函数,然后查询每个 Agent 的模型:

MODEL_WORKER=$(read_model worker)
MODEL_REVIEWER=$(read_model reviewer)
EFFORT_WORKER=$(read_effort worker)
EFFORT_REVIEWER=$(read_effort reviewer)
实现
# 1. Read current task state (status + level + card_type).
# Stateless: re-read (status, level) every loop; NEVER cache status across a gate.
TASK=$(api GET /task/$ID?fields=status,level,card_type)
STATUS=$(echo "$TASK" | jq -r '.status')
LEVEL=$(echo "$TASK" | jq -r '.level')
CARD_TYPE=$(echo "$TASK" | jq -r '.card_type // "task"')

# 1a. Pipeline-exclusion: an epic is a CONTAINER, not runnable. Refuse and list its children.
#     (See ../squad/shared.md → Task Relationships & Epics.)
if [ "$CARD_TYPE" = "epic" ]; then
  REL=$(api GET /task/$ID/relationships)
  KIDS=$(echo "$REL" | jq -r '(.children // []) | if length == 0 then "(no children yet)" else (.[] | "  #\(.id) \(.title) [\(.status)]") end')
  PROG=$(echo "$REL" | jq -r '"\(.children_progress.done)/\(.children_progress.total)"')
  echo "Epic #$ID is a container — run its children ($PROG done):"
  echo "$KIDS"
  exit 0   # abort before dispatch
fi

# 1a-bis. Cancelled is a TERMINAL status — not runnable. Refuse before dispatch; reopen to run.
#         (See ../squad/shared.md → Move Protocol: cancelled is reachable from any status and is
#          left only via POST /task/:id/reopen — cancelled → todo.)
if [ "$STATUS" = "cancelled" ]; then
  echo "Task #$ID is cancelled (terminal) — reopen to run."
  exit 0   # abort before dispatch
fi

# 1a-ter. Done is a TERMINAL status — not runnable. Refuse before dispatch; reopen to re-run.
#         A `done` card is terminal regardless of HOW it got there — a gated pipeline finalize OR an
#         administrative POST /task/:id/complete both land on the same `done`, so one branch covers both.
#         (See ../squad/shared.md → Move Protocol: done has no forward transition; left only via reopen.)
if [ "$STATUS" = "done" ]; then
  echo "Task #$ID is done (terminal) — reopen to re-run."
  exit 0   # abort before dispatch
fi

# 1b. Sub-task readiness nudge (SOFT — NOT a block; the dep hard-block in ⓪ʙ takes precedence).
#     A `task` with incomplete .children → warn "usually run those first".
#     Default mode: AskUserQuestion confirm/cancel. --auto: proceed + log an Orchestrator activity note.
REL=$(api GET /task/$ID/relationships)
OPEN_KIDS=$(echo "$REL" | jq -r '[(.children // [])[] | select(.status != "done" and .status != "cancelled")] | length')
if [ "$OPEN_KIDS" -gt 0 ]; then
  echo "Task #$ID has $OPEN_KIDS open sub-task(s) — usually run those first (nudge, not a block)."
  # --auto: proceed and log:
  #   POST /api/task/$ID/activity {actor:"Orchestrator", model:"system",
  #     message:"--auto proceeded past $OPEN_KIDS open sub-task(s)"}
  # default: AskUserQuestion confirm/cancel before dispatch.
fi

# The plan dispatch's level-aware exit status — the orchestrator moves plan → $PLAN_NEXT after
# Worker@plan finishes: L3 → plan_review, L2 → impl. (L1 never reaches the plan column.)
if [ "$LEVEL" = "3" ]; then PLAN_NEXT=plan_review; else PLAN_NEXT=impl; fi

# 2. Pipeline-entry / dispatch (see Agent Dispatch below)
#    When STATUS == todo:
#      L1  → ONE PATCH {"status":"impl","current_agent":"Worker","actor":"Orchestrator"}, then dispatch Worker@impl.
#      L2/L3 → ONE PATCH {"status":"plan","current_agent":"Worker","actor":"Orchestrator"} (the todo→plan entry
#              move), then dispatch Worker@plan. The card is in the PLAN column before the
#              Worker begins — step ② below is this same single level-aware entry PATCH.
#    When STATUS == plan (fresh entry OR Reviewer-reject re-entry via plan_review→plan):
#      dispatch Worker@plan WITHOUT re-moving status. plan→plan is not a legal transition
#      and MUST NOT be attempted (idempotent re-entry). Set current_agent:"Worker" only.
#    All other statuses: dispatch the column's agent (see Agent Dispatch table).
# 3. After agent: append one activity event via POST /activity (see schema.md for format)
# 4. The agent records its verdict; orchestrator READS it → GATE (default) → COMMIT move
#    (generic PATCH, current_agent:null) → SIDE-EFFECTS (git commit + note, only after a done commit).
#    See "Per-Step Transition Contract" below for the full ordering + mapping.
# 5. Re-read state, loop until done or circuit breaker
观察门控接缝(发出任何 user_steering 事件前须征得同意)

在编排器发出任何 user_steering 观察事件之前,必须通过同意门控 — ../squad/scripts/observe.py(只读;参见 ../squad/shared.md → 观察与同意)。在运行开始时仅解析一次,并缓存整个运行期间的退出代码(observe.py 无状态 — 每次调用都会执行一次 GET /consent):

# Resolve once per run; cache the decision. 0 = emit, non-zero = skip (no parsing).
python3 ../squad/scripts/observe.py gate >/dev/null 2>&1; OBSERVE_OK=$?
# … later, per correction, reuse the cached decision — do NOT re-resolve:
#   [ "$OBSERVE_OK" = 0 ] && <emit user_steering>   # else skip

本地紧急开关(DO_NOT_TRACK/SQUAD_OBSERVE_DISABLED/CI)会直接关闭门控,不发起网络请求;否则会读取服务器端的同意状态,并在发生任何错误时默认拒绝。运行过程中通过网页取消选择的操作会在下一次运行时生效;任何滞后的事件都会被服务器端的同意门控以 403 拒绝。事件发出本身就是观察捕获;此门控是其调用的接缝。

每步状态转换约定(每次状态变更均由编排器负责)

编排器(而非 Agent)负责发起每次状态转换。每个 Agent 步骤都严格按以下顺序执行:

  1. 记录 — Agent 执行工作、填写输出字段,并通过 POST 对应的判定端点记录判定结果(approved/reject | pass/fail + comment)(Reviewer@plan_review → /plan-review,Reviewer@impl_review → /review,Worker@test → /test-result)。Agent 不会更改状态。
  2. 读取 — 编排器读取当前阶段由服务器生成的 last_plan_review_status / last_review_status / last_test_status 字段。下一状态根据判定结果在本地计算(见下表),绝不使用 POST 响应中的 newStatus。
  3. 门控(默认模式)— 在状态变更之前运行 AskUserQuestion,供用户接受或拒绝。--auto 会跳过人工提示,但仍会发起状态变更。
  4. 提交 — 编排器发起唯一一个经过验证的通用 PATCH /api/task/:id,将状态设为下一状态,并设置 current_agent:null 和 actor:"Orchestrator"。
  5. 副作用 — 只有在提交 done 状态变更之后,才执行 git commit 并添加提交备注。

读取判定结果 — 当前审查阶段由服务器生成的状态:

# The orchestrator reads the server-derived verdict for the current review stage:
#   plan_review → last_plan_review_status · impl_review → last_review_status · test → last_test_status
case "$STATUS" in
  plan_review) VFIELD=last_plan_review_status ;;
  impl_review) VFIELD=last_review_status ;;
  test)        VFIELD=last_test_status ;;
esac
VERDICT=$(api GET /task/$ID?fields=$VFIELD \
  | VFIELD="$VFIELD" python3 -c "import sys,json,os; print(json.load(sys.stdin).get(os.environ['VFIELD']) or '')")

判定结果 → 下一状态(在本地计算;与 getTransitions 一致)。每一行都通过唯一一个经过验证的通用 PATCH {status:<next>, current_agent:null, actor:"Orchestrator"} 发起。判定结果是从派生字段读取的字面值——审查阶段为 approved / changes_requested,测试阶段为 pass / fail:

Agent @ 状态判定结果通用 PATCH 状态变更
Worker @ plan(完成)L3 → plan_review · L2 → impl
Reviewer @ plan_reviewapproved / changes_requestedimpl / plan
Worker @ impl(完成)impl_review(L2/L3)
Reviewer @ impl_reviewapproved / changes_requested(L2 → done · L3 → test)/ impl
Worker @ testpass / faildone / impl
done finalize—done
L3 审批快照(Reviewer → test)

当 Reviewer 的判定为 approved,且计算出的移动是 L3 impl_review → test 转换时,紧接着发出通用移动 PATCH 后,立即捕获 APPROVAL_TREE——Reviewer 批准的完整工作树状态(已跟踪修改 + 未跟踪文件)的确定性内容哈希。这仅适用于 L3:L1/L2 没有 test 阶段,而 L2 的 approved 分支会直接移动到 done,因此绝不能捕获。

# Right after the impl_review→test move PATCH. L3-only: L2 approved→done never captures.
# Non-destructive content-hash of the WHOLE tree (tracked + untracked) via a throwaway
# temp index — the real index / working tree / stash list are never touched.
if [ "$LEVEL" = "3" ]; then
  TMPIDX=$(mktemp) && rm -f "$TMPIDX"          # mktemp pre-creates an empty file; git reads it
                                               # as a corrupt index ("index file smaller than
                                               # expected") — REMOVE it before use so git creates
                                               # a fresh temp index at that path.
  GIT_INDEX_FILE="$TMPIDX" git add -A          # stage tracked edits + untracked into the temp index
  APPROVAL_TREE=$(GIT_INDEX_FILE="$TMPIDX" git write-tree)   # deterministic tree SHA of the whole tree
  rm -f "$TMPIDX"                              # clean up the throwaway index
fi

APPROVAL_TREE 现在表示 Reviewer 批准的确切差异——树的字节完全相同 ⇒ 哈希相同;任何内容更改(包括新增未跟踪文件)⇒ 哈希不同。

拒绝循环(plan_review→plan、impl_review→impl、test→impl)会重新调度该列的 Agent;plan→plan 重新进入时只设置 current_agent(不执行非法的状态移动)。重新发出已应用的移动是安全的空操作。

人工关卡覆盖写入(默认模式)。 在第 3 步关卡中,人工可以拒绝——即使 Agent 已记录 approved 也可以(派生的 $VERDICT 仍为 approved,因此上表会计算出前进移动)。人工要求退回的操作不是终端回滚备注:它会在移动之前作为持久、可追溯的覆盖记录在服务端。人工拒绝时(仅默认模式——--auto 绝不会拒绝),记录覆盖,然后重新读取现已翻转的 $VERDICT,并继续使用同一个判定→移动表——此时会计算出流水线允许的后退移动(plan_review→plan / impl_review→impl / test→impl):

# Runs ONLY when the human REJECTS at the step-3 AskUserQuestion gate (incl. post-`approved`).
# $STATUS = the review stage (= the override `gate`); $VFIELD/$CID as set above.
if [ "$GATE_DECISION" = reject ]; then
  # 1. Mandatory reason — a follow-up AskUserQuestion / free-text. Empty reason ⇒ server 400,
  #    so re-prompt until non-empty (mirrors the GitHub mandatory dismiss-reason).
  #    CAPTURE the free-text via a single-quoted heredoc — the quoted <<'…' delimiter disables ALL
  #    expansion, so a backtick/$(…) in the reason stays inert AT THE ASSIGNMENT (a plain
  #    REASON="<the reason>" would command-substitute right here, before the safe env handoff below).
  #    See the Shell Safety box (board content is data, never code) at the prompt-assembly seam.
  REASON=$(cat <<'REASON_EOF'
<the human's reason — required, non-empty>
REASON_EOF
)
  # 2. Current version (optimistic-concurrency guard against a concurrent override).
  VER=$(api GET /task/$ID?fields=version | jq -r '.version')
  # 3. Record the SUPERSEDING override over the run's user-scoped PAT. The server stamps
  #    executed_by=<PAT> + on_behalf_of=<owner>; the body carries actor_kind=human (delegation,
  #    not impersonation). Record-only: it flips last_*_status, never moves status.
  # Build the override body OUT-OF-BAND — the human's `reason` is free-text and must never be
  # inlined into a --json "{…}" string (a backtick/$(…) in it would command-substitute in the
  # shell; a quote/newline would break the JSON). json.dumps reads every field from os.environ,
  # so the value crosses as inert data. See the Shell Safety box (board content is data, never
  # code) at the prompt-assembly seam below.
  ERR=$(mktemp)
  export GATE="$STATUS" REASON="$REASON" VER="$VER" CID="$CID"
  OVERRIDE_BODY=$(python3 -c "
import json, os
print(json.dumps({'gate': os.environ['GATE'], 'reason': os.environ['REASON'],
                  'expected_version': int(os.environ['VER']), 'correlation_id': os.environ['CID']}))")
  api POST /task/$ID/override-review --json "$OVERRIDE_BODY" 2>"$ERR"
  RC=$?   # 4 = 4xx (403 missing task:override-review scope · 400 empty reason · 409 stale version)
  if [ "$RC" -ne 0 ]; then
    # SURFACE the failure to the user — a 403 means the run PAT lacks the elevated
    # task:override-review scope. NEVER silently downgrade to a fix-in-place (the
    # no-silent-downgrade rule); the server record is the single source of truth.
    echo "ERROR: could not record human override on $ID (exit $RC): $(grep -v '^ERROR:' "$ERR" 2>/dev/null || cat "$ERR")"
    rm -f "$ERR"
    return 1 2>/dev/null || exit 1   # halt the gate; do not move, do not fix in place
  fi
  rm -f "$ERR"
  # 4. Re-read the now-flipped derived verdict; fall through to the verdict→move table above
  #    (it now computes the backward reject move). The user_steering emit below also fires.
  VERDICT=$(api GET /task/$ID?fields=$VFIELD \
    | VFIELD="$VFIELD" python3 -c "import sys,json,os; print(json.load(sys.stdin).get(os.environ['VFIELD']) or '')")
fi

更正时发出 user_steering。 当审查判定为拒绝时(在 plan_review 阶段 Reviewer 判定 changes_requested→plan,在 impl_review 阶段 Reviewer 判定 changes_requested→impl,在 test 阶段 Worker 判定 fail→impl)——或者在默认模式下,人工在 AskUserQuestion 关卡(第 3 步)拒绝时——发出一个抽象化的 user_steering 事件;该事件受运行开始边界处(上文)缓存的 OBSERVE_OK 控制,并采用尽力而为方式(|| true)。枚举值根据 ../squad/shared.md 中的关卡 抽象规则(各关卡映射表)确定;--comment 使用抽象化模式(经过泄漏过滤;命中任何内容时替换为 (redacted))。使用该步骤的 correlation_id,使事件与步骤关联。常规批准不发出任何事件;拒绝循环中的重新调度是一次新的发生(使用新的 correlation_id),而非重复事件。

# After reading $VERDICT (and before/with the reject move). $CID = the step's correlation_id.
if [ "$OBSERVE_OK" = 0 ]; then
  case "$STATUS:$VERDICT" in
    plan_review:changes_requested)
      python3 ../squad/scripts/observe.py emit "$ID" --modality evaluative --valence negative \
        --target planning --severity moderate --attributability violated_constraint \
        --comment "rejected the plan" --correlation-id "$CID" || true ;;
    impl_review:changes_requested)
      python3 ../squad/scripts/observe.py emit "$ID" --modality evaluative --valence negative \
        --target verification --severity moderate --attributability violated_constraint \
        --comment "requested implementation changes" --correlation-id "$CID" || true ;;
    test:fail)
      python3 ../squad/scripts/observe.py emit "$ID" --modality evaluative --valence negative \
        --target verification --severity major --attributability violated_constraint \
        --comment "tests failed" --correlation-id "$CID" || true ;;
  esac
fi
Agent 昵称与身份

流水线运行两个 Agent。每个 Agent 都有一个固定昵称,在所有记录中保持一致——任务卡会变成工作日志;每个字段和每条日志记录都有签名。

昵称角色模型键推理强度(codex)状态触发条件
WorkerWorker Agent(端到端:计划、实现 + 测试、测试运行)workerhighplan、impl、test
ReviewerReview Agent(验证输出并提供反馈)reviewermediumplan_review、impl_review

线上的 actor 标签。 看板的 actor 字段(位于通用任务 PATCH 和活动追加中)由服务端根据固定枚举进行验证,该枚举早于双 Agent 流水线: Planner / Critic / Builder / Shield / Inspector / Ranger / Refiner / Orchestrator / Heartbeat。 未知值(例如 Worker)会导致 400 错误。因此,枚举约束字段的写入会发送列的线上标签;自由字符串字段(current_agent、判定中的 reviewer / tester、签名标头)则直接使用真实昵称 Worker / Reviewer:

列v2 Agent线上 actor(枚举约束写入)
planWorkerPlanner
plan_reviewReviewerCritic
implWorkerBuilder
impl_reviewReviewerInspector
testWorkerRanger

JSON 格式、线上 Actor 标签契约和签名标头规则,请参见 ../squad/schema.md。

Agent 调度

模板文件位于 ../squad/templates/。每列对应一次派发;模板中的 <FOCUS> 占位符用于选择匹配的 ## Focus: 部分。

状态模板FOCUS昵称模型键
plantemplates/worker.mdplanWorkerworker
plan_reviewtemplates/reviewer.mdplan_reviewReviewerreviewer
impltemplates/worker.mdimplWorkerworker
impl_reviewtemplates/reviewer.mdimpl_reviewReviewerreviewer
testtemplates/worker.mdtestWorkerworker

每次派发的最少字段(只获取该次派发所需的字段):

派发必需字段
Worker@plantitle,description,spec,plan_review_comments
Reviewer@plan_reviewtitle,description,spec,plan,decision_log,done_when
Worker@impltitle,description,spec,plan,done_when,plan_review_comments,review_comments
Reviewer@impl_reviewtitle,description,spec,plan,done_when,implementation_notes
Worker@testtitle,implementation_notes

派发流程 — 每次派发都按以下顺序执行:

⓪ Fetch project brief (once per pipeline run, cache for all dispatches)
   PROJECT_DATA = api GET /projects/$PROJECT
   PROJECT_BRIEF = extract .brief field (empty string if null or project not found)
   This is injected into every agent template via <project_brief> placeholder.

⓪ʙ Resolve dependencies & review feedback (once per pipeline run, cache for all dispatches)

   **Resolve dependencies via the relationships API** (see `../squad/shared.md` → **Task Relationships & Epics**).
   The `blocks` dependency edges are read from `GET /api/task/:id/relationships` → `.blocked_by` — NOT
   text-parsed from the description. (The `Depends on:` text convention and the old dependencies
   endpoint are both retired — see `../squad/shared.md`.)
   ```bash
   # Read structured blocks edges; .blocked_by = the deps this task is blocked by.
   REL=$(api GET /task/$ID/relationships)
   DEP_IDS=$(echo "$REL" | jq -r '.blocked_by[]?.id')

就绪检查(硬性阻断): 当依赖的状态为 done 或 cancelled(两种终态)时,该依赖即为已解决。 如果任何 .blocked_by[].status 不属于 {done, cancelled},则任务尚未就绪:

  • 默认模式:使用 AskUserQuestion — 展示未完成的依赖,并在继续前确认。
  • --auto 模式:以 "blocked by incomplete dependency #N" 拒绝并中止流水线。 这是硬性阻断,优先于软性子任务提示(下方 ①)。 用作阻塞项的史诗会自动完成 — 当其所有子项都达到终态时,其存储状态会汇总为 done/cancelled,从而自动满足基于状态的就绪检查(无需手动执行 /complete)。派生的史诗 complete 汇总状态仅用于显示;满足依赖条件的是与汇总结果保持同步的存储状态。 下方的 jq 保持不变 — done 已在已解决状态集合 {done, cancelled} 中。
BLOCKERS=$(echo "$REL" | jq -r '.blocked_by[]? | select(.status != "done" and .status != "cancelled") | "#\(.id) (\(.status))"')
if [ -n "$BLOCKERS" ]; then
  # --auto: refuse + abort. default: AskUserQuestion confirm/cancel.
  echo "blocked by incomplete dependency $BLOCKERS"
fi

客户端不检查循环依赖。服务端会在写入时(事务内 CTE)强制确保无环;若 POST /relationships 形成循环,则返回 409。该 409 会在写入路径中(声明技能里)暴露,而不会在此处预先验证。

获取每项依赖的上下文(用于注入缓存):对 DEP_IDS 中的每个 id,获取上下文字段。 若返回 404,则发出警告并跳过该依赖,继续处理。

for DEP_ID in $DEP_IDS; do
  DEP_TASK=$(api GET /task/$DEP_ID?fields=title,status,decision_log,implementation_notes)
  if [ -z "$(echo "$DEP_TASK" | jq -r '.id // empty')" ]; then
    echo "WARNING: dependency #$DEP_ID not found (404), skipping"
    continue
  fi
  # Cache: DEPS[$DEP_ID] = { title, status, decision_log, implementation_notes }
done

构建每次派发的依赖上下文字符串: 对每项缓存的依赖,根据当前派发组装上下文:

  • Worker@plan:decision_log(500 个字符)+ implementation_notes(500 个字符)
  • Worker@impl:implementation_notes(500 个字符)
  • Reviewer@impl_review:decision_log(300 个字符)

截断规则:若字段长度超过限制,则取前 N 个字符 + ...[truncated]。 若依赖状态不属于 {done, cancelled}:在该依赖的内容块前添加 [IN PROGRESS] 警告。 状态为 done 或 cancelled 的依赖均已解决 — 不添加 [IN PROGRESS] 标记。 若没有依赖:DEPS_CONTEXT=""(空字符串 — 清除占位符)。

每项依赖的格式:

### #<DEP_ID>: <title> [<status>]
[IN PROGRESS]

**Decision Log:**
<truncated decision_log>

**Implementation Notes:**
<truncated implementation_notes>

提取重跑时的审查反馈 — 使用一个占位符 <review_feedback>,并按派发类型填充:Worker@plan 重跑时获取最后一条 plan_review_comments;Worker@impl 重跑时获取最后一条 review_comments;其他所有派发均使用空字符串。

# Review feedback for a Worker@plan re-run (source: plan_review_comments)
# — for a Worker@impl re-run, run the SAME extraction against review_comments instead.
REVIEW_FEEDBACK=""
FEEDBACK_JSON=$(echo "$TASK" | jq -r '.plan_review_comments // ""')   # or .review_comments
if [ -n "$FEEDBACK_JSON" ] && [ "$FEEDBACK_JSON" != "null" ]; then
  REVIEW_FEEDBACK=$(echo "$FEEDBACK_JSON" | python3 -c "
import sys, json
data = json.load(sys.stdin)
if isinstance(data, list) and len(data) > 0:
  print(data[-1].get('comment', ''))
")
fi

① 读取任务字段(按每次分派读取字段,以尽量减少令牌用量)

Worker@plan

TASK = api GET /task/$ID?fields=title,description,spec,plan_review_comments

Reviewer@plan_review

TASK = api GET /task/$ID?fields=title,description,spec,plan,decision_log,done_when

Worker@impl

TASK = api GET /task/$ID?fields=title,description,spec,plan,done_when,plan_review_comments,review_comments

Reviewer@impl_review

TASK = api GET /task/$ID?fields=title,description,spec,plan,done_when,implementation_notes

Worker@test

TASK = api GET /task/$ID?fields=title,implementation_notes 对每次分派,只提取上述列出的字段

② 移入对应列 + 将 Agent 标记为活跃 — 一次按层级执行的 PATCH 为本次分派的这一步生成一个全新的关联 ID — 切勿缓存或 在循环中重复使用。每次分派都必须使用新 ID,包括每次驳回后的 再次分派(plan_review→plan、impl_review→impl、test→impl):每次分派都是一个 独立步骤,必须有自己的 ID。

CORRELATION_ID=$(python3 -c 'import uuid;print(uuid.uuid4())')

同一个 $CORRELATION_ID 会同时传入 Agent 模板(步骤 ④ 中的 --set correlation_id=) 和编排器针对本步骤发出的 activity POST(步骤 ⑥), 这样看板就会将步骤的 record-results 写入和 agent_log 合并为同一条时间线 记录。

列状态变更和 current_agent 赋值必须通过一次 PATCH 完成(绝不能分开调用或额外调用)。根据正在进行的分派,选择相应的请求体(current_agent 是自由字符串字段 — 填写实际昵称,而不是 wire 标签): • Worker@plan,来自 todo(L2/L3): { "status": "plan", "current_agent": "Worker", "actor": "Orchestrator" } • Worker@impl,来自 todo(L1): { "status": "impl", "current_agent": "Worker", "actor": "Orchestrator" } • Worker@plan,当前已在 plan(已在上方处理的新进入,或 Reviewer 驳回后 经 plan_review→plan 重新进入): { "current_agent": "Worker" } (不变更状态 — plan→plan 不合法;幂等重新分派) • 已在自身列中的任何分派(Reviewer@plan_review、Worker@impl、 Reviewer@impl_review、Worker@test): { "current_agent": "" } (不变更状态) api PATCH /task/$ID --json <上述请求体>

③ 读取模板文件 Read 工具:../squad/templates/worker.md 或 ../squad/templates/reviewer.md(Agent Dispatch 表)

④ 填写模板占位符 替换以下每个占位符的所有出现位置: → 实际任务 ID → 实际项目名称 → 本次分派的重点(plan | impl | test | plan_review | impl_review) <project_brief> → 步骤 ⓪ 中的项目简介(若未设置则为空字符串) → 任务标题 <description> → 任务描述(用户最初的请求 — 可能早于 spec) <spec> → Refiner spec 的呈现内容:"## Refined Spec\n\n…";若没有 spec,则为 ""(参见下方 SPEC_MD);如果存在 spec,则其优先级高于 description(../squad/shared.md → Spec Precedence) → plan 字段值 <decision_log> → decision_log 字段值 <done_when> → done_when 字段值 <implementation_notes> → implementation_notes 字段值 <dependencies_context> → 步骤 ⓪ʙ 中按分派提供的依赖上下文(若无则为空字符串) <review_feedback> → 本次重新运行的最新评审反馈(首次运行时为空;参见 ⓪ʙ) <correlation_id> → $CORRELATION_ID(步骤 ② 中生成的全新单步 ID) → 当前 UTC 时间(ISO 8601) <MODEL_WORKER> → $MODEL_WORKER <MODEL_REVIEWER> → $MODEL_REVIEWER <EFFORT_WORKER> → $EFFORT_WORKER <EFFORT_REVIEWER> → $EFFORT_REVIEWER Spec 渲染 — 将获取的 spec JSON 渲染为 <spec> 的 markdown。若任务没有 spec(旧版/未经细化),则为空字符串,这样 <spec> 会干净地折叠,## Original Request(<description>)则承载需求。(内联 python,与 REVIEW_FEEDBACK 模式一致 — 无需新增脚本。)

渲染后的 spec 非空时,应将其视为权威内容,并将 ## Original Request(<description>)标注为可能早于 spec 的人工原始请求 — 常设优先级规则(../squad/shared.md → Spec Precedence)适用于同时使用这两个字段的所有调度;不要注入单次提示词的优先级防护规则。

SPEC_MD=$(echo "$TASK" | python3 -c "
import sys, json
d = json.load(sys.stdin); spec = d.get('spec')
if not spec: print('', end=''); sys.exit(0)
out = ['## Refined Spec', '']
if spec.get('goal'): out += ['**Goal:** ' + spec['goal'], '']
reqs = spec.get('requirements') or []
if reqs: out += ['**Requirements:**'] + ['- ' + r for r in reqs] + ['']
qa = spec.get('qa') or []
if qa:
    out += ['**Clarifications (Q&A):**']
    for it in qa:
        out += ['- Q: ' + (it.get('question') or '')]
        out += ['  A: ' + (it['answer'] if it.get('answer') is not None else '(unanswered)')]
print('\n'.join(out).rstrip())
")
# Every Worker@plan / Reviewer / Worker@impl dispatch consumes the spec. Worker@test does
# NOT (mechanical lint/build/test only) → pass SPEC_MD="" for the test dispatch.
🛡️ Shell 安全——看板内容是数据,绝不是代码(防注入)

规则。 绝不要通过内联渲染内容或看板内容(渲染后的提示词、plan、spec、description、decision_log、done_when、提交标题、人为覆盖的 reason、任何 Markdown/代码文本),来拼接受解释器处理的字符串——双引号括起来的 Shell 命令、python3 -c "…" 程序或 --json "{…}" 正文。应通过带外方式传递。

捕获也是一种汇。 将这些内容放入变量也有同样的风险:普通的双引号命令 VAR="<rendered content>" 会在赋值行上对任何反引号/$(…) 执行命令替换——早于任何下游环境变量/json.dumps 处理的保护。对于编排器输出的字面自由文本,请用单引号 heredoc 捕获(VAR=$(cat <<'EOF' … EOF))——带引号的分隔符会禁用所有展开,因此内容是 Shell 字面量(等同于 OWASP 的 argv/stdin/文件字面量方式)。从安全来源捕获——通过 jq -r 管道读取看板、使用 json.load,或读取任何命令的输出(VAR=$(… | jq -r …))——同样不会执行;后续的 "$VAR" 展开也绝不会再次替换其中的内容。只有对自由文本进行字面量双引号捕获才是汇。

原因。 Bash 会在程序运行前,对字面命令文本中的反引号和 $(…) 执行命令替换——因此,内容中的任何此类序列都会在编排器的 Shell 中执行,其输出会替换原内容并拼接到字符串中。内容既遭到破坏,也被执行。

它会静默失败。 不会出现错误:提示词/产物会悄无声息地遭到破坏(已观察到:Agent 提示词被篡改、计划快照格式错误),而替换的命令已经运行。(在 zsh 和 bash 下,echo 处理转义换行的方式也不同——这也是应使用真正的序列化器来构造正文,而不是拼接字符串的另一个原因。)

安全后果(RCE)。 卡片的 plan/spec/description/Markdown 都是不可信输入:其中可能包含恶意或间接提示词注入的 $(…)/反引号,将其内联到 Shell/python3 -c 字符串中会执行该负载。这是 OWASP 操作系统命令注入防护——参数化:将数据与代码分离——而不是关于转义/引用的小技巧。每一次此类插值都是缺陷(一个问题),而非易用性上的小瑕疵。

三种安全的带外模式:

  1. 临时文件 + --json @file——使用 python3 json.dumps(或 jq -n --arg)序列化到文件,再通过路径传递;适用于较大的/多行看板正文。
  2. 环境变量 → os.environ——导出值,在程序内部通过 os.environ[...] 读取;绝不要将其拼接进 -c 文本(下方第⑥步活动中的 BODY)。
  3. stdin/渲染管道——echo "$VAR" | python3 -c …(SPEC_MD/REVIEW_FEEDBACK 构造器)或 render_agent_prompt.py 的管道到变量方式。"$VAR" 展开不会执行——bash 不会再次替换变量值;只有被字面粘贴到命令文本中的内容才有危险。
# ❌ 错误——内容被内联到命令文本中;bash 会先替换任何 $(…)/反引号:
#     api POST /task/$ID/activity --json "{\"message\": \"<rendered-content>\"}"
#     python3 -c "print('<rendered-content>')"
# ❌ 错误——即使是捕获也会执行替换:用普通双引号赋值来捕获自由文本:
#     REASON="<rendered-content>"
# ✅ 正确(捕获)——单引号 heredoc:带引号的 <<'…' 分隔符会禁用所有展开,
#   因此自由文本中的反引号/$(…) 会保留为字面内容:
REASON=$(cat <<'REASON_EOF'
<the human's reason>
REASON_EOF
)
# ✅ 正确(写入)——通过环境传递值;程序将其作为惰性数据读取:
BODY=$(MSG="$REASON" python3 -c "import json, os; print(json.dumps({'message': os.environ['MSG']}))")
api POST /task/$ID/activity --json "$BODY"

推荐的辅助脚本:

PROMPT=$(python3 ../squad/scripts/render_agent_prompt.py \
  --template ../squad/templates/<worker|reviewer>.md \
  --models ../squad/models.json \
  --provider "$MODEL_PROVIDER" \
  --set ID="$ID" \
  --set PROJECT="$PROJECT" \
  --set FOCUS="$STATUS" \
  --set project_brief="$PROJECT_BRIEF" \
  --set title="$TITLE" \
  --set description="$DESCRIPTION" \
  --set spec="$SPEC_MD" \
  --set plan="$PLAN" \
  --set decision_log="$DECISION_LOG" \
  --set done_when="$DONE_WHEN" \
  --set implementation_notes="$IMPLEMENTATION_NOTES" \
  --set dependencies_context="$DEPS_CONTEXT" \
  --set review_feedback="$REVIEW_FEEDBACK" \
  --set correlation_id="$CORRELATION_ID" \
  --set TIMESTAMP="$TIMESTAMP")

如果字段缺失,传入空字符串(--set key="")。 仅当所有未解析的 <...> 令牌都应视为错误时,才使用 --strict。

⑤ 使用已填充的提示词启动 Task 工具 如果 MODEL_PROVIDER 是 codex: Task( subagent_type = "general-purpose", model = "", model_reasoning_effort= "", prompt = )

否则(claude): Task( subagent_type = "general-purpose", model = "", prompt = )

⑥ Task 完成后——为此步骤追加一条归属于 Agent 的活动事件。 actor 是 Agent 刚刚执行的列对应的 WIRE LABEL(参见 Wire Actor Labels — 不是 "Orchestrator"),并标注 Agent 的模型,以及——可选,尽力而为——运行时在 Task 完成时自行报告的每个子 Agent 的 Token 用量(仅当运行时提供时记录,否则省略,绝不为 null/0;绝不由 Orchestrator 估算)。 可直接复制粘贴的具体示例,以及 Agent 事件与 Orchestrator 机器事件的区别:参见本段下方的 "⑥ Agent-attributed step event"。 correlation_id 与步骤 ② 中生成并在步骤 ④ 传给 Agent 模板的 $CORRELATION_ID 相同—— Agent 的 record-results 写入和这条活动事件使用同一个 id(correlation_id:$CORRELATION_ID), 以便看板将它们归入此步骤的同一条时间线记录。


#### ⑥ Agent-attributed step event

The per-step activity event is attributed to the **agent that just ran** (`actor:<wire label for
its column>`, `model:<that agent's resolved model>`) — **not** `actor:"Orchestrator"`. The `actor`
field is enum-bound on the wire, so it carries the column's wire label (Wire Actor Labels table
above); `actor` and `model` are the reliable per-step attribution and are always present. `tokens`
is **OPTIONAL and best-effort**: include it ONLY when the runtime itself reports per-subagent
usage for the just-completed Task, read from that reported usage — never orchestrator-estimated (a
caller sees only the final result, not the subagent's internal LLM/tool calls, so any local
estimate structurally undercounts). Omit it otherwise (never null, never 0); per-agent token stats
are therefore not guaranteed or complete. Single atomic POST, no read-modify-write:

~~~bash
# ⑥ Agent-attributed step event — the AGENT that just ran is the actor (NOT "Orchestrator"),
#    recorded under its column's WIRE LABEL, tagged with its model + (OPTIONAL, best-effort)
#    the runtime's OWN reported per-subagent token usage at Task completion.
#    WIRE_ACTOR  = this step's wire label (plan→Planner · plan_review→Critic · impl→Builder ·
#                  impl_review→Inspector · test→Ranger)
#    AGENT_MODEL = the dispatched agent's resolved model ($MODEL_WORKER / $MODEL_REVIEWER)
#    STEP_MSG    = one-line summary of what the step did
#    STEP_TOKENS = best-effort: your runtime's OWN reported per-subagent usage for the
#                  just-completed Task — read from what the runtime reports, never
#                  orchestrator-estimated. Leave UNSET if the runtime reports nothing — the key
#                  is then omitted (never null/0); per-agent token stats are not guaranteed.
# Shell Safety (see the box at the prompt-assembly seam): every field crosses via the environment
# and is read with os.environ INSIDE the program — board content is data, never inlined into the
# python3 -c "…" text. This is the canonical env→os.environ out-of-band pattern.
BODY=$(WIRE_ACTOR="$WIRE_ACTOR" AGENT_MODEL="$AGENT_MODEL" STEP_MSG="$STEP_MSG" \
  CID="$CORRELATION_ID" STEP_TOKENS="${STEP_TOKENS:-}" python3 -c "
import json, os
b = {'actor': os.environ['WIRE_ACTOR'], 'model': os.environ['AGENT_MODEL'],
     'message': os.environ['STEP_MSG'], 'correlation_id': os.environ['CID']}
tok = os.environ.get('STEP_TOKENS')
if tok: b['tokens'] = int(tok)   # runtime usage; omitted entirely when the runtime reports nothing
print(json.dumps(b))")
api POST /task/$ID/activity --json "$BODY"
~~~

**Agent-step event vs orchestrator machine event.** The step-⑥ event above is *agent-attributed*.
The orchestrator's OWN machine events stay `actor:"Orchestrator", model:"system"`: the status-move
PATCHes (the transition contract) and the **commit note** below. Do not sweep those into agent
attribution.

The Worker RETURNS its output (no self-move). Once the impl dispatch completes — its own template
already ran the project formatter and tests via Command Resolution — the orchestrator issues the
impl→impl_review **commit** move directly:

```bash
api PATCH /task/$ID --json '{"status": "impl_review", "current_agent": null, "actor": "Orchestrator"}'

默认模式:在 plan_review、impl_review 和 test(L3 测试门禁)阶段,契约顺序明确——Agent 记录其裁定 → Orchestrator 读取该裁定 → 门禁(通过 AskUserQuestion 接受/拒绝)→ Orchestrator 使用通用 PATCH(current_agent:null)提交状态流转。门禁 先于状态流转 PATCH;状态流转始终使用通用 PATCH,绝不使用裁定端点。 如果人工在门禁处拒绝——包括 Agent 已记录 approved 之后——必须先记录一条 持久化、可追溯的覆盖记录(POST /task/$ID/override-review,必须提供 reason),以更改派生的裁定, 然后正常读取→状态流转流程会计算向前一阶段退回的操作(即上文所述的人工门禁覆盖写入流程);如果返回 403(PAT 不具备 task:override-review 权限),则将错误告知用户,绝不静默修正。 自动模式(--auto):顺序相同,但在门禁处自动接受(Orchestrator 仍会发出状态流转 PATCH)。

→ 完成状态流转(所有级别)

过时原则(impl_review → test → done)。 Reviewer 的批准仅适用于获批时的差异;在完成提交之前, 对工作树的任何后续更改都会使该批准失效,并需要通过同一个 impl_review 门禁重新审核。

过期批准复查(L3,APPROVAL_TREE 非空时)。 在下方完成提交状态流转 PATCH 之前, 重新捕获整个树的内容哈希,并将其与 Reviewer 批准的快照进行比较——按内容是否相同判断,不要按测试文件与源文件的分类判断:

# Re-capture PRECOMMIT_TREE with the SAME non-destructive primitive (whole tree, tracked + untracked).
if [ "$LEVEL" = "3" ] && [ -n "$APPROVAL_TREE" ]; then
  TMPIDX=$(mktemp) && rm -f "$TMPIDX"
  GIT_INDEX_FILE="$TMPIDX" git add -A
  PRECOMMIT_TREE=$(GIT_INDEX_FILE="$TMPIDX" git write-tree)
  rm -f "$TMPIDX"

  if [ "$PRECOMMIT_TREE" = "$APPROVAL_TREE" ]; then
    : # identical content hash → the approved diff is intact → proceed to the done-commit unchanged.
  else
    # Differs → the approval is STALE (e.g. the test dispatch edited the tree after approval).
    # Re-dispatch the Reviewer through the EXISTING impl_review gate — NOT a new column/status.
    # Move the card back via the standard impl_review re-entry, mint a FRESH per-step
    # correlation_id, follow the existing record→read→gate→commit contract, and respect the
    # EXISTING circuit breaker: impl_review_count > 3 → stop, ask user (no infinite loop). The
    # re-review uses the same reviewer template (FOCUS=impl_review), the same /review verdict
    # endpoint, and the same verdict→move table:
    #   • re-review approved        → capture a FRESH APPROVAL_TREE (re-snapshot with the primitive
    #                                 above), the test stage runs again, and the next test→done attempt
    #                                 re-enters this compare.
    #   • re-review changes_requested → the EXISTING impl_review → impl reject loop fires (route back to
    #                                 the Worker), unchanged.
    echo "stale approval: working tree changed after impl_review approval — re-reviewing"
    # … re-enter impl_review here (existing gate, fresh correlation_id, impl_review_count breaker) …
  fi
fi
# 1. Move to done (single validated generic PATCH) — clears current_agent.
#    Re-issuing when already done is a safe no-op.
api PATCH /task/$ID --json '{"status": "done", "current_agent": null, "actor": "Orchestrator"}'

# 2. Side-effects AFTER the state commit — commit pending changes.
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
  git add -A
  # The task title is board content — never inline it into the commit-message string (a backtick
  # in a title would command-substitute; see the Shell Safety box). Build the message in a
  # variable (expansion is inert) or use `git commit -F <file>`.
  MSG="feat: $TITLE [squad #$ID]"
  git commit -m "$MSG"
fi
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "no-git")

# 3. Record commit hash as an activity event (machine event, actor=Orchestrator/system)
SUBJECT=$(git log -1 --format=%s 2>/dev/null || echo "no-git")
BODY=$(jq -n --arg msg "Committed $COMMIT_HASH: $SUBJECT [squad #$ID]" \
  '{actor: "Orchestrator", model: "system", message: $msg}')
api POST /task/$ID/activity --json "$BODY"

如果尚无提交,则跳过该事件,或记录 message:"Committed (none) [squad #$ID]"。

→ Coach(本次运行的摩擦审查)

卡片变为 done 并完成提交后,按照 ../squad/shared.md → Coach Dispatch 派发 Coach(步骤 ⑤ 中已解析 MODEL_PROVIDER 和辅助脚本)。传入:

  • skill_name = squad-run
  • source_task = $ID
  • run_summary = "squad-run pipeline completed task $ID to done."
  • trajectory = 任务的活动事件(按顺序排列的所有 Agent 步骤)+ implementation_notes + 审查裁定
  • friction_signals = 本轮记录的拒绝循环 / 断路器触发 / Agent 重试;无异常时为 none
/squad-run review <ID> — 代码审查

对状态为 impl_review 的任务触发 Reviewer(与 impl_review 步骤相同)。

评论

支持 Markdown