关于 implement-spec-in-workflow
将规范 Issue 及其任务票组织成带阻塞关系的任务图,由动态子 Agent 工作流并行或顺序实施,并为每张票创建独立 PR。适合通过分层审查和整体合并完成较大规范变更的团队;流程要求先确认运行器、堆栈模式和既有工作分支。
主要功能
- 01按任务依赖调度可执行票据
- 02为每张票生成独立 PR
- 03支持并行或顺序运行
- 04支持原生堆栈或链式 PR 模式
- 05通过工作流脚本调度子 Agent
- 06根据验证章节提取检查命令
适用场景
- 01按规范 Issue 将功能拆分为可审查的 PR
- 02并行实施依赖图中互不阻塞的任务
- 03在已有分支上继续规范任务并避免重复实现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ javrasya/ skills --skill implement-spec-in-workflow安装后在 Claude Code 中输入/implement-spec-in-workflow确认已加载。
附带文件workflow.template.js
详细介绍
一个下设多个工单的规格问题,会通过子 Agent 组成的动态工作流端到端实现,并形成一个 PR 堆栈——每个工单对应一个 PR。这些工单不是步骤清单,而是具有阻塞关系的任务图,因此始终会有一组可领取的工单组成前沿,运行会根据这组前沿安排任务,而不是按阶段依次推进。之所以采用堆栈,是为了让操作者一次审查一个小型的、以工单为单位的差异,然后通过一次操作合并整个堆栈。
这里稀缺的资源是你的上下文。规格正文、工单正文、研究结果、差异和审查发现都留在工作流内;你只需掌握 issue 编号、十四个插值后的值,以及返回的摘要。除了步骤 2 指定的 ## Validation 部分,不要阅读任何工单正文;不要打开任何源文件,也不要运行搜索——工作流内的发现 Agent 会完成这些事,而且它使用全新的上下文窗口,比你在上下文已满时做得更好。
与此文件同目录的 workflow.template.js 是脚本。它涵盖所有步骤;渲染后即可启动。
需要一个能够运行 agent() 调用脚本的运行器——可以是 Workflow runner、某个框架的 workflow 原语(Claude Code 的 Workflow 工具或同等工具),也可以是 session runner(原 Orca runner;ADR-0011、ADR-0017)。session runner 需要 Node 和已安装的 crew:运行器就是 crew 包(npm install -g @javrasya/crew,或从 javrasya/crew 的检出目录运行 npm install -g <crew checkout>),它会在一个会话主机上将每个 Agent 作为实时会话运行——默认使用 crew 本身,也可以使用 Orca;使用 Orca 还需要在 Orca 终端中启动会话,且其运行时已就绪(参见 crew runner 参考文档)。该脚本是使用四个钩子 agent()、parallel()、phase()、log() 编写的纯 JavaScript;两种运行器使用同一脚本,只有一个渲染值不同,即 RUNNER(步骤 2)。原生堆栈注册还需要 gh-stack 扩展——下文会对此进行检查,绝不自动安装。
步骤
-
堆栈门槛。 运行以下两项探测后,根据结果分别处理;在此之前不要进行任何其他操作:
gh extension list | grep -q gh-stack— 缺失 → 停止。 告知用户运行gh extension install github/gh-stack,然后等待。只有用户明确要求时,才由你自行安装。gh api "repos/<owner>/<repo>/stacks" --silent— 404 → 此仓库未启用堆栈功能(可能受组织推出策略限制,或当前主机尚未开放预览;安装任何东西都无法解决)。请告知用户:GitHub 的原生堆叠 PR 会在每个 PR 上显示堆栈图,并支持一键原子化合并整个堆栈——启用方法见 https://docs.github.com/en/pull-requests/tutorials/roll-out-stacked-prs——或者本次运行可以退化为普通的--base链,并由用户手动从底部向上合并。由用户决定,绝不可静默回退。HTTP 200(即使响应为[])→ 使用原生模式。
运行器。 与堆栈门槛一起,根据两项探测结果确定:
- Crew 探测。
crew --help以 0 退出:crew 已在 PATH 中。 - Orca 探测。
TERM_PROGRAM为Orca且已设置ORCA_TERMINAL_HANDLE,并且orca status --json以 0 退出,且ok: true、result.runtime.state: "ready"和result.runtime.reachable: true,并且 crew 探测通过。其他任何情况——缺少变量、非零退出、stdout 不是 JSON,或状态不符——都视为探测失败。 - Crew 探测失败(因此 Orca 探测也失败):运行将使用 Workflow runner,和不执行此步骤时完全一样,且不会询问任何问题。如果只是缺少 crew——Orca 探测的其他部分均通过——则只说明一次:会话 runner 需要安装 crew,并说明安装方法。
- Crew 探测通过 → 提问,并等待回答。选项按以下顺序排列:crew 上的会话 runner——这是默认选项,因此排在第一并标明默认;每个 Agent 都是一个实时 crew 会话,由操作员进入、查看、输入并在运行控制台中作答,且运行驻留在 crew 守护进程中,在本会话结束后仍会继续;Orca 上的会话 runner,仅当 Orca 探测通过时提供;每个 Agent 都是一个实时 Orca 标签页,且运行驻留在专属的 Orca 终端中;或 Workflow runner,与之前一样,使用主机的工作流原语。由操作员选择。默认选项排在第一并不代表替他们决定:即使他们没有回答,也绝不能替他们选择默认选项。
- 选择了会话 runner,无论使用哪台主机 → 确定 worker 的运行方式。 Claude worker 会以 runner 指定的权限模式(
--permission-mode)启动;如果未指定,则使用 Claude 自身的默认权限模式。Worker 会继承你当前会话的权限模式,就像 Workflow 子 Agent 一样:如果该模式不会提示操作员——auto、acceptEdits、bypassPermissions、dontAsk——就传递该模式,无需提问(auto仍保持为auto)。只有当你的模式为default(手动),或无法确认当前模式时,worker 才会停在权限提示处,需要操作员逐个标签页作答;此时才询问操作员 worker 应如何运行:auto、acceptEdits、bypassPermissions、使用你自己的模式,或不指定标志(由 Claude 自身的默认模式决定)。等待回答;该回答将成为此次启动的--permission-mode参数,或不传该参数。在 crew 上启动时必须指定一个值(crew start有对应条目),因此不要提供“不指定标志”选项:default是手动模式。 - 之前的工作。 提问并等待回答:是否有某个分支已经包含本次运行开始前为此规范完成的工作?操作员指出分支名,或回答没有(ADR-0023)。提供当前检出仓库的分支供其选择(
git for-each-ref --format='%(refname:short)' refs/heads refs/remotes/origin),并在spec/<n>-integration分支旁说明:这是早前某次运行的全栈审查修复,绝不是某个工单的工作。绝不要自行查找此类分支,也绝不要让工作流中的 Agent 查找:有一次,某次已观察到的运行中的发现 Agent 找到了已中止运行的 integration 分支,运行便重新实现了一个工作已在该分支上的工单,并在发布时停下。指定的分支将成为堆栈的第 0 层;随后图 Agent 会逐个说明该分支是否已完成对应工单,而这些工单将由第 0 层 PR 关闭,不会重新构建。 - 运行顺序。 确定 runner 后提问一次并等待回答:并行(默认)会像以往一样,同时构建所有可处理的工单;串行则一次构建一个工单:先派发、实现、通过门禁并发布,再从最新提交切出下一个工单,因此不会有发布操作将变基到其他工单上(ADR-0020)。仅在会话 runner 上提供此选项,无论使用哪台主机。使用 Workflow runner 时,运行采用并行模式,不会询问任何问题;如果操作员要求串行,则在渲染任何内容之前拒绝,并说明原因:串行运行会让 Agent 依次操作同一个文件夹,而 Workflow runner 无法让两个 Agent 共用一个文件夹。他们可以选择并行,或改用会话 runner。
-
解析十四个值。前十个值使用
gh issue view <n> --json title -q .title、git remote -v、git fetch origin和git rev-parse获取——只读取标题和引用,不读取正文。两组配方是唯一例外:每个工单只读取## Validation部分,不读取其他内容。值 说明 __SPEC__参数中的规范问题编号 __REPO__该规范所属仓库的 owner/name__REPO_DIR__该仓库检出目录的绝对路径 __BASE_REF__该堆栈合并到的分支。用户指定的分支即为准。否则,将 git branch --show-current与仓库的默认分支进行比对:若两者相同,则直接使用默认分支;若当前检出位于其他分支上,询问用户该堆栈要合并到哪个分支——默认分支还是当前分支——然后等待回复。检出停留在长期分支上通常意味着工作目标是该分支;错误的基准会导致堆栈中的每个 PR 都基于错误分支,因此绝不猜测。已在功能分支上的既有工作不是此值:它是__START_REF__。__START_REF__步骤 1 中的既有工作:操作者指定的分支;若没有,则为基准引用本身,工作流将其视为“没有第 0 层”。绝不猜测,也绝不由 Agent 查找(ADR-0023)。 __BASE_SHA__本次运行固定的基准(ADR-0030):执行 git fetch origin,然后获取__START_REF__的完整 sha,即git rev-parse --verify 'refs/remotes/origin/<start ref>^{commit}';若指定的既有工作尚未存在于 origin,则使用其本地分支refs/heads/<start ref>。本次运行创建的每个任务都基于此 sha,而不是引用;即使引用在运行过程中发生变化也是如此。若引用无法解析为提交,或 fetch 失败,则拒绝运行:告知操作者是哪个引用,且不渲染任何内容、不启动任何任务__STACK_MODE__步骤 1 中的两个探测均通过时为 native;用户选择备用方案时为chain__RUN_ORDER__步骤 1 中的运行顺序: parallel或sequential;在 Workflow runner 上始终为parallel,渲染为sequential的脚本会拒绝运行__RUNNER__操作者在步骤 1 中选择 session runner 时为 session(无论使用哪台主机),否则为workflow。渲染为orca(之前的值)的脚本仍可运行并恢复相同状态。此值决定 worktree 的措辞以及脚本是否执行回收:使用 session runner 时,隔离 Agent 的 worktree 由 session 主机为其创建,脚本的回收步骤不会移除任何内容——操作者从运行视图或运行控制台进行回收。使用 Workflow runner 时,lane 和 finalize 会通过git worktree remove回收。两种渲染结果之间只有此值不同__NOTES_DIR__~/.claude/spec-notes/<repo-name>-<spec>的绝对路径——持久保存,位于所有检出目录之外,且每个 Agent 均可读取__PER_CHANGE_COMMANDS__规范中每个开放的 ready-for-agent子问题的## Validation部分下### Run per change中的命令(gh api repos/<repo>/issues/<n>/sub_issues --paginate);跳过已关闭及ready-for-human的问题。一条命令是某行中第一个反引号括起的内容;没有此类内容的行,或属于说明文字的行(absent: …、not applicable: …、Recipe measured …、Needs: …、Deferred repo gate: …,以及Label:后的同类文字)均不算命令。每条命令仅保留一次;仅空白字符不同的命令视为相同;保留首次出现的写法;渲染为 JSON 字符串数组(JSON.stringify),不加引号包裹,若没有任务包含命令则写为[]。Crew 使用相同方式提取(crew 的arm.mjs中的recipeCommandSets)__AT_REVIEW_COMMANDS__同上,但取自 ### Run at review__TICKET_RECIPES__按上述规则,列出这些任务各自的命令,渲染为 JSON 对象,以问题编号映射至 {"perChange": [...], "atReview": [...]};每个列表中的命令均按上述规则去重,不加引号包裹;若没有此类任务则写为{}。脚本运行任务自身的列表,因此某角色运行的每条命令都已由基线测量;仅当任务缺少此处的记录时,才使用调度器的副本。Crew 使用相同方式渲染(arm.mjs中的ticketRecipes)__PR_GUIDE__vendored pr技能的绝对路径,即此技能所在仓库中的skills/engineering/pr/SKILL.md:先解析此 SKILL.md 的真实路径(由于已安装的技能是符号链接,因此使用realpath),再从其所在文件夹取../pr/SKILL.md。发布器会读取此文件以编写每个任务 PR 的正文。若路径不存在,则拒绝运行:不渲染任何内容,也不启动任何任务。Crew 使用相同方式解析(arm.mjs中的prGuidePath) -
渲染脚本:将
workflow.template.js复制到<notes-dir>/workflow.js,并替换其中的十四个占位符。只替换,不要重写——发布泳道和前沿调度至关重要。在 crew 上跳过此步骤:crew start会根据步骤 4 中提供的值,以及它以相同方式解析出的值(仓库、仓库路径、笔记目录、固定的基准版本和两组配方),自行渲染模板。 -
等运行器完成步骤 1 后,使用渲染后的路径启动它。运行期间不要亲自执行其中任何工作。
-
工作流运行器。 它会在后台运行,并在返回时通知你;不要轮询它。
-
crew 上的会话运行器。 使用一条命令完成配置并启动:将
crew start表单中的每一项都作为标志传入——步骤 1 中的权限模式、步骤 2 中的__BASE_REF__、__START_REF__、__STACK_MODE__和__RUN_ORDER__、作为运行框架的claude,以及你所用模型的名称作为模型;并将标准输入设为/dev/null:crew start <spec> --harness claude --model <your model id> --base "<base ref>" [--start-ref "<prior work branch>"] --stack-mode <native|chain> --run-order <parallel|sequential> --permission-mode <mode> </dev/null仅当操作员指定了之前的工作时才使用
--start-ref;省略时,任务栈从基准版本开始。在没有终端的情况下,crew 不会显示表单:它只根据标志完成配置;如果某一项没有对应标志,就会指出该项并拒绝执行,而不会询问。每次crew start都会启动一次新的运行,绝不会复用之前启动的运行,无论之前的运行是否仍在进行:每次运行都有自己的文件夹<notes-dir>/runs/<spec>-<UTC time>-<random>,其中包含workflow.js、研究笔记、基准版本的pre-existing-failures.json,以及基准测量因阻塞项而停止后生成的停止记录baseline-blockers.json和baseline-at-review-blockers.json(恢复运行时会先读取这些文件以检查阻塞项),还有状态目录orca-run/。不会创建验证文件,也不会起草任何内容:每个工单都带有自己的验证配方;如果任一工单缺少包含### Run per change小节的## Validation部分,crew start会在显示表单之前拒绝执行,且不会进行任何配置,并指出这些工单——先对 spec 运行preflight以写入这些内容,然后重新启动。它会按上文所述,将运行的基准版本固定为__BASE_SHA__;如果切出的 ref 无法解析为提交,就会拒绝执行,且不会进行任何配置。它会在该目录中渲染脚本,并以名为implement-spec #<spec>: <spec title>的 crew 会话启动运行器,打印脚本路径、每个工单切出的 ref 和 sha、会话 ID,以及进入该运行的控制台命令crew view "<run folder>/orca-run"。若返回非零退出码,表示未进行任何配置:将其消息告知操作员,然后停止。之后等待,并严格按照下文 Orca 的方式报告;此处<notes-dir>/orca-run应理解为crew start打印的状态目录;如果它指向运行器标签页或某个节点的标签页,在 crew 上则是一个 crew 会话(halted.json中的terminal是运行器的会话 ID,每个节点的tab是其工作会话的 ID),从运行控制台crew view "<run folder>/orca-run"进入:按下运行器行或某个节点行上的 Enter 即可进入相应会话。(crew console是守护进程原始会话的调试列表:绝不要让操作员前往那里。) -
Orca 上的会话运行器。 首先清除上一次运行的结束信号,因为笔记目录会跨运行保留——每次
--resume和对同一 spec 的重新配置都会基于上一次运行的文件启动,而新运行器要等到节点加载后才会替换这些文件,远在你开始等待之后:rm -f "<notes-dir>/orca-run/summary.json" "<notes-dir>/orca-run/runner.pid" "<notes-dir>/orca-run/halted.json"然后,将运行器作为独立的 Orca 终端启动,标题使用 spec 名称,以便查看其日志,并确保运行在当前会话结束后仍继续:
orca terminal create --title "implement-spec #<spec>: <spec title>" --command "crew run --host orca \"<notes-dir>/workflow.js\" [--permission-mode <mode>]" --json不要追加
; exit:脚本结束后,运行器会留在其标签页的运行视图中,直到操作员退出该视图;随后,标签页会保留它打印的内容。保留result.terminal.handle;如果result.terminal.surface不是visible,请告知操作员该运行没有可见的标签页。然后等待
<notes-dir>/orca-run/summary.json出现,或运行器退出——绝不要等待终端退出,因为它现在不会退出;也不要根据标签页是否打开来判断,因为标签页的生命周期比运行器更长。运行器启动时会将进程 ID 写入状态目录中的runner.pid,并在脚本结束时、等待操作员查看运行视图之前写入summary.json,因此summary.json出现就表示运行已结束;而runner.pid中记录的进程已不存在,则表示运行器已退出。暂停的运行(某个工单失败,或需要由操作员决定的事项;ADR-0016)尚未结束,不会写入summary.json:运行器会改为写入halted.json,内容为{ at, runId, terminal, nodes: [{ node, title, reason, questions?, tab }] },每当暂停的节点发生变化时都会更新at;运行离开暂停状态后则会删除该文件。使用一条命令等待,最长一小时;如果你的执行环境支持,请将其作为后台命令运行;其中<seen>是上次报告暂停状态时的at值,第一次运行时留空:timeout 3600 sh -c 'd="<notes-dir>/orca-run"; seen="<seen>"; until [ -f "$d/summary.json" ]; do a=; [ -f "$d/halted.json" ] && a=$(node -p "require(process.argv[1]).at" "$d/halted.json" 2>/dev/null); [ -n "$a" ] && [ "$a" != "$seen" ] && break; [ -f "$d/runner.pid" ] && ! node -e "process.kill(+process.argv[1],0)" "$(cat "$d/runner.pid")" 2>/dev/null && break; sleep 15; done'node探测是存活检查,因为它能识别 Windows 进程 ID,而 Git Bash 的kill -0可能无法做到。等待返回后,按以下顺序检查。存在summary.json:运行已结束(运行器可能在两次检查之间退出,因此先检查这个)。否则,如果存在halted.json,且其at不是<seen>:运行已暂停,或暂停的节点发生了变化。告知操作员哪些节点需要他们处理以及原因——每个节点的title和reason、有的话还有它的questions,以及它的tab——并说明在运行器的选项卡(terminal)中按r可恢复所有暂停的节点;若选中某个节点所在的行,则只恢复该节点;然后将<seen>设为该at,再次发出相同的等待命令。在 crew 上,要在运行控制台中进入运行器的会话,并按r。暂停不会结束等待:只有summary.json出现或运行器退出才会结束。否则,如果存在runner.pid,且相同的探测失败:在 Orca 上,运行器已退出;在 crew 上,先恢复(见下文),只有确定没有运行器重新启动时,才报告它已退出。否则,等待时间已过一小时:如果runner.pid仍不存在,运行器从未启动——说明这一点,并指明操作员需要查看的选项卡;如果探测仍然成功,则再次发出相同的等待命令。运行期间不要读取终端日志。Orca 中断服务(例如更新时重启 Orca)不会导致任何 Agent 失败:运行器会等待 Orca 恢复;超过 10 分钟后,它会暂停运行,并在 Orca 恢复后自行继续,因此存活的运行器若没有summary.json,可能只是暂停了;继续等待。crew 中断服务时情况不同:crew 会连同其所有会话一起关闭,包括运行器的会话,所以在 crew 上,已退出且没有summary.json的运行器,可能是守护进程在运行仍可恢复时退出了(例如崩溃、被终止或重启)。守护进程只有在启动后才会恢复此类运行,因此要启动一个守护进程:运行crew daemon start,然后最多等待两分钟,让runner.pid再次指向存活进程(恢复后的运行器会写入一个新的文件;使用相同的node探测)——timeout 120 sh -c 'd="<notes-dir>/orca-run"; until [ -f "$d/summary.json" ] || { [ -f "$d/runner.pid" ] && node -e "process.kill(+process.argv[1],0)" "$(cat "$d/runner.pid")" 2>/dev/null; }; do sleep 5; done'。运行器存活,或summary.json已出现,都表示运行已恢复:告知操作员 crew 已恢复该运行,并再次发出相同的等待命令。只有两者都没有时,运行器才算已退出:按下文说明进行报告。summary.json出现后,读取它:ok: true表示工作流的返回值位于result中;ok: false表示运行器的error和worktrees_kept,后者是因其 Agent 退出或从未启动而保留的工作树。运行器不会回收任何内容,也不会询问:每个 Agent 的会话和工作树都会保留,直到操作员回收——在 Orca 上,可从运行视图回收(在运行器的选项卡中按Ctrl+R,或使用独立视图);在 crew 上,可从运行控制台回收(crew view);参见 crew runner 参考文档。告知操作员这一点;不要等待回收。运行器已退出且没有summary.json,说明它在写入文件前就退出了——可能是被终止、内存不足或崩溃;说明这一点,并指引操作员查看<notes-dir>/orca-run/runner.log,其中记录了运行器打印的每一行;以及旁边的journal.jsonl,其中记录了每个 Agent 的启动、结果或失败及其原因。
-
-
报告返回结果,并注明此次运行使用的 runner——Workflow runner,或带有其主机(crew 或 Orca)及会话或终端标题的 session runner:从栈底到栈顶列出每个 PR 的 URL 和状态、运行结束时所处模式的合并说明;如果运行已暂停,则报告其
reason,以及每张尚未发布的工单的状态和详情,并列出状态为needs decision的工单留下的任何问题;列出交由人工处理的工单及原因、unblock 会话清除的阻碍项(如果运行在 Unblock 阶段暂停,则列出仍待清除的阻碍项)、运行保留的内容及原因——对于 session runner,列出每个 Agent 的会话(在 Orca 上为其标签页)和工作树,这些资源会一直保留,直到操作员从运行视图或运行控制台中回收它们;同时列出结果中的worktrees_kept,即因 Agent 已终止或从未启动而保留的工作树;对于 Workflow runner,列出结果中的worktrees_kept及保留原因——以及备注目录。工作流的返回值已经是摘要,请直接传递,不要根据 PR 重新推导。
要稍后重新查看一个 session-runner 运行,无论其已完成、暂停还是终止,且不启动任何 runner:crew 托管的运行可通过 crew ls 列出,并在运行控制台中通过 crew view 打开;Orca 托管的运行可通过 orca-runs 打开,这是一个独立的运行视图,会列出这台机器上所有由 Orca 托管的运行,并回收某次运行的 Agent,以及恢复 runner 已终止的运行。
运行失败后要继续迭代,请编辑 <notes-dir>/workflow.js(在 crew 上为 <run folder>/workflow.js,即 crew start 打印的脚本),然后使用 runner 的恢复句柄重新启动——Workflow runner 使用 resumeFromRunId,session runner 使用 --resume(在 Orca 上,使用步骤 4 中相同的启动命令,并添加 --resume;在 crew 上,用 crew run "<run folder>/workflow.js" --resume [--permission-mode <mode>] 替代 crew start,后者会重新生成脚本——先清除 summary.json、halted.json 和 runner.pid,因为失败运行留下的这些文件仍然存在——并按相同方式等待新的 summary.json),这样便会从 <notes-dir>/orca-run/journal.jsonl 的日志中重放(在 crew 上为 <run folder>/orca-run/journal.jsonl):未更改的 agent() 调用前缀会从缓存中返回,只有从已编辑的调用开始才会重新运行。
工作流的作用
- 图谱——一个 Agent 阅读规格说明和每张工单,然后返回任务图:阻塞边、哪些工单需要人工处理,以及阻塞项——这台机器缺少、需要由人员一次性提供的东西(凭据、签名身份或配置文件、已设置好的设备或服务),每项都附有证据和检查命令。阻塞项不等于需要人工处理的工单:需要该阻塞项的工单仍由自动化处理(ADR-0021)。它绝不会寻找带有先前工作的分支:这是操作人员指定的
__START_REF__(ADR-0023);如果指定了分支,图谱 Agent 唯一需要做的相关工作,就是针对每张工单,根据证据说明该分支是否已完成工单;这类工单称为已涵盖——不会为其运行 Agent,不会阻塞任何内容,且第 0 层 PR 会关闭它。阻塞边首先取自 GitHub 原生依赖关系(blocked_by,每张工单调用一次 API);只有在 API 未返回任何结果时,才从“Blocked by”部分的正文中读取——依赖关系已通过原生方式记录的工单,无需读取正文。 - 探索——并行研究 Agent 在仓库外的 notes 目录中写入 Markdown。笔记有长度上限,且引用而不摘录——笔记替后续 Agent 省去搜索,而非阅读;后续 Agent 只阅读主题与其工单相关的笔记,不会阅读全部笔记。探索 Agent 也会返回阻塞项。除图谱要求探索的内容外,脚本还总会启动基线 Agent(ADR-0030),作为 Explore 下的独立节点:它在自己的工作树中运行设置钩子,切换到
__BASE_SHA__,运行每条__PER_CHANGE_COMMANDS__命令一次,并返回每条命令的退出代码及失败项(测试需附其 ID;其他检查需附工具、规则、文件、5 到 6 行的代码片段和消息,绝不能只给行号);如果工具支持取消选择测试,还要返回屏蔽命令。submit会拒绝不符合其架构的记录;脚本将接受的记录写入 notes 目录中的pre-existing-failures.json(在 crew 上则写入运行文件夹),绝不提交到仓库。记录含有屏蔽命令时,每个运行每次变更配方的角色——实现者、修复者、门禁审查者、发布者——都会在原命令的位置收到屏蔽命令;包括门禁交叉检查添加的命令。就绪检查对任一形式的检查都计数。每个角色也会收到该记录的路径;若某个角色的工作旨在修复一项被屏蔽的失败,就必须运行未屏蔽命令并说明这一点。基线检查成功前,不会派发任何任务;恢复运行时会保留结果,而不会重新测量。Unblock 完成后,第二个基线节点baseline:at-review会在后台以相同方式测量每条__AT_REVIEW_COMMANDS__命令中不属于每次变更命令集的命令:派发和每张工单都照常继续,只有全栈审查会等待它成功。其记录随后会与每次变更的记录一同写入pre-existing-failures.json,其屏蔽命令也会一并写入;恢复运行时,会保留两个节点中已成功的那个。此处的阻塞项按每次变更测量中的方式处理,但在所有工单都已发布后、审查开始前,运行会暂停,并在 Workflow runner 上将其记录写入baseline-at-review-blockers.json。工具无法启动某条命令,或命令退出状态并非检查失败且无法从其输出读取任何内容,都属于无法运行的情况;正在运行但失败的命令则属于既存失败。无法运行的情况属于环境故障,屏蔽会掩盖它,因此基线 Agent 会按发现阶段使用的格式,将其列为阻塞项(缺少什么、证据、检查命令)。在 crew 主机上,它会调用needs_you:对应行会显示“需要你”,在此期间绝不会收到提醒;操作人员进入其会话并与其一起修复环境。命令能够运行后,它会继续测量并提交记录;在此之前派发仍会暂停,之后运行按其他基线结果的方式继续。没有该工具时,它会将阻塞项作为节点的decisions_needed返回,这会使节点暂停;恢复运行时会继续使用原会话。Workflow runner 的会话中无人参与,因此它会在此处暂停,列出每项阻塞项的具体内容、证据和检查命令,并将暂停记录到 notes 目录中的baseline-blockers.json;下一条基线提示词会注明这次暂停,因此恢复运行时,runner 会再次调用基线节点,先检查每项阻塞项。解除阻塞——如果发现阶段找到了任何阻塞项,在它们全部清除前,运行不会构建任何内容:在会话运行器上,它会开启一个需人工参与的会话,从开始起便标记为needs you,操作人员从运行控制台进入该会话。Agent 会指导操作人员逐项检查并验证阻塞项,但绝不自行安装、签名或配置任何内容;等待期间也绝不会收到提醒、被计为卡住或失败。所有检查通过后,会话结束;若操作人员放弃解决某项阻塞项,运行就会暂停;恢复运行时会继续使用同一会话。Workflow runner 的会话中无人参与,因此它会在此处暂停并列出阻塞项。 - 设置——只有在操作人员启动运行时指定了先前工作,才会执行此步骤:该分支成为第 0 层,也就是堆栈底部,并拥有自己的草稿 PR;其正文会明确说明其中包含本次运行从未纳入门禁的既有工作,并为图谱 Agent 发现已在该分支上完成的每张工单添加
Closes #<n>。首先从 origin 镜像其本地引用——这是堆栈中运行继承而非创建的唯一分支;之后每次执行gh stack link都会按该本地名称推送它(参见堆栈)。 - 派发——每张工单对应一个派发器,它是路由器,而非规划器:它只读取一次工单,快速浏览代码结构(文件列表、grep 命中;约 5 次工具调用),并回答两个问题——要切分成多少个切片,以及每个切片负责哪些验收标准。它不读取规格说明、研究笔记正文或实现,也不会寻找遗漏或偏差:工单由人工根据规格说明拆分而来,可信且按原样处理。每份切片简报少于 3K 个字符,包含四个部分——负责的验收标准(逐字照录)、预期文件、要阅读的笔记、不在范围内的内容;绝不包含设计方案。曾观察到一个派发器被要求撰写“自包含”的简报后,在首次调用工具前思考了 40 分钟,花费 $17 制定计划,而实现者打开代码后又重做了这份计划。通常的判断是只分一个切片——派发器倾向于不拆分,因为拆分不足可以自我纠正(超出范围的内容会作为剩余工作返回并重新切分),而拆分过细则会让每个切片都付出新 Agent 的启动成本,且无法纠正。派发器还会生成后续修复 Agent 会用到的
ticket_brief,而非工单本身;并从工单的## Validation部分逐字复制验证配方——### Run per change放入validation,### Run at review放入review_validation,绝不改写(ADR-0029)。派发器是本次运行所有工作的唯一入口——它还有第二个入口,用于将审查发现切分为修复切片,因此运行中任何地方都不会有 Agent 接到未经拆分的任务(ADR-0004)。 - 实现——切片在工单分支上依次运行,每个切片都由一个独立上下文的 Agent 在自己的工作树中处理(依顺序执行时,使用下文所述运行的单一链式工作树)。工单以其阻塞项所在分支的尖端为基础切出;若位于堆栈底部,则以固定的
__BASE_SHA__为基础切出,绝不基于运行过程中会移动的引用;只要依赖关系允许,工单就会尽早启动。实现者会阅读工单以了解验收标准的措辞和简报指定的笔记,绝不阅读规格说明;它会在代码面前自行设计变更。它负责维护验收约定:工单的标准、规格说明中与这些标准相关的决策,以及规格说明本身新建或修改的任何 ADR——现有 ADR 仅作指导,绝非验收标准(ADR-0008)。如果运行代码后发现工单内容缺失、与自身矛盾或与规格说明矛盾,只要修复仍处于工单留白范围内,实现者就自行决定,并在decided中返回决定,由发布者写入 PR;如果修复会破坏工单、规格说明或 ADR 明确要求保留的内容,则停止处理该验收标准,不留下相关代码,并在decisions_needed中返回问题,注明约束和证据——绝不放入unmet。依并行顺序执行时,工作树按每个 Agent分配,而非按工单分配;每个 Agent 返回时都会标明自己的路径,以便之后由执行通道回收(参见CONTEXT.md中的 Worktree reclaim)。依顺序执行时,所有代码 Agent 会依次使用运行唯一的链式工作树<runId>-chain,并接续前一个 Agent 留下的构建缓存(ADR-0020);doctor 仍会使用自己的工作树。链式 Agent 返回结果前,runner 的遗留内容检查会将链式工作树的git status --porcelain与该 Agent 被告知的现有内容进行比较:任何额外内容都会被退回一次,由该 Agent 在自己的会话中提交或删除;无论最终保留什么内容,都会告知之后所有链式 Agent,明确标示为绝不能提交的文件。每个切片都会针对其最终提交运行工单的每次变更命令,为每条命令返回一条结果{command, passed, exit_code},并在validated_sha中返回通过验证的提交 sha。退出代码为 0 即为通过,无需判断;非 0 只有在切片依据运行的基线记录判定其中所有失败均为既存失败时才算通过——这称为豁免检查(ADR-0030)。迭代过程中,切片会运行构建工具支持的最小范围,并在最后一次修改后运行一次(ADR-0009);每项检查都按原样在前台运行,绝不放入后台、绝不轮询,任何 Agent 都不会清理构建缓存。超出上下文容量(约 70 次工具调用)的切片会提交已完成的工作并返回剩余工作——派发器会将其重新切分给新的 Agent,最多不超过MAX_DISPATCH_ROUNDS次。未完成的工作始终是剩余工作,而非遗漏——切片未编写的必需测试会返回派发环节,而不会交给门禁或人工处理;只有需要由人工解决的矛盾才会通过decisions_needed向后传递,且会结束该工单的切片轮次:相关验收标准绝不会再次派发,即使某个切片也将它们列入了unmet。待定决策,或达到轮次上限后仍为未满足的剩余工作,都不会进入门禁或发布:它们会暂停运行(参见步骤 10)。 - 门禁——每张工单都会在自己的、尚未发布的分支上相对于其切出时的基准进行审查,然后才成为 PR。审查者的首要任务是检查就绪状态:该分支在实现者(或最后一位修复者)指定的 sha 上已通过验证,因此审查者会运行
git rev-parse HEAD;若 sha 匹配且它未做任何编辑,就沿用该结果,只有工作树不同才重新运行(ADR-0009)——豁免检查除外;这类检查必须重新运行并由审查者自行判断(ADR-0030)。如果结果不通过,就作为剩余工作退回派发环节(占用一次MAX_DISPATCH_ROUNDS),而非作为发现项交给修复者。审查者还会将提示词中的每次变更命令与工单的### Run per change进行比较,并在missing_validation中返回提示词遗漏的命令;脚本会将这些命令逐字且各添加一次到工单配方中,并以相同方式将分支退回派发环节,因此下一轮会运行完整配方;遗漏一行只会消耗一轮,而不会使运行失败。这绝不是发现项:修复者无法更改配方,因此审查者每轮都会再次提出它。之后,审查与修复交替进行,直到某轮审查不再返回阻塞项(次要问题可通过);每轮都会使用新的审查者,最多执行GATE_MAX_ROUNDS轮;达到上限时,系统会带着尚未解决的问题发布,并在日志和摘要中标明,而不会让所有下游工单停滞。可构成阻塞的问题范围很窄:未满足的验收标准、未通过的验证命令,或正确性错误(panic、崩溃、发布版本中静默数据丢失)。在门禁阶段,ADR 适配性、架构和风格都属于minor——这些内容已在规格设计时解决,而逐张工单重新证明它们曾让一次观察到的运行把大部分门禁轮次都耗在这上面(ADR-0008)。第 1 轮审查整个差异;之后的轮次会验证声称已修复的问题以及修复者改动的行,因此轮次数衡量的是修复过程,而非全新发现。每轮中的修复阶段都必须经过派发器:它根据ticket_brief和指定文件的结构切分阻塞性发现项——绝不读取工单本身——并为新的修复切片撰写简报;每个切片负责指定的发现项、运行工单的每次变更命令,并逐项返回判断结果。随后脚本会将判断结果与发现项进行核对;没有判断结果的项目会被记录,并交给下一位审查者明确检查。异常退出的审查者绝不会被计为审查通过。 - 发布通道——串行通道将图谱转换为链:每次恰好只有一个 Agent 会将自己的工单分支变基到当前堆栈尖端,并以该尖端为基础打开草稿 PR,在正文中添加
Closes #<ticket>。是否移动过尖端只根据 sha 判断,绝不根据名称:堆栈底部的工单以固定的__BASE_SHA__为基础切出,因此发布者会运行git rev-parse origin/<start ref>;如果当前结果不再是固定的 sha,就会先从固定 sha 将工单重新应用到该分支,再以它为基础打开 PR(ADR-0030)。依顺序执行时,运行中的尖端不会在工单处理期间移动,因此发布者绝不会将工单重新应用到其他工单上,脚本也会拒绝尖端已移动的发布(ADR-0020);只有底部检查仍可能触发变基。变基会生成未经验证的工作树,因此发布者会在其上运行一次工单的每次变更命令;如果尖端未移动,则按 sha 沿用门禁结果,但豁免检查必须由发布者重新运行并自行判断:绝不沿用豁免检查的结果(ADR-0030)。无论哪种情况,PR 正文中都会包含一行来源信息:Validated green at <sha> by <role>;每项豁免检查都列在它下方,格式为Waived: <command> exited <code>(没有豁免检查时不添加任何内容),让人工和全栈审查看清楚验证通过的是哪个工作树以及由谁验证。直到此时,分支才仅存在于共享克隆中,因此变基改写的提交从未离开该克隆,且执行通道的推送会在 origin 上创建分支,而非覆盖它——即仅发布一次:分支在 origin 上恰好到达一次,origin 上的引用绝不会被改写,任何 Agent 都不会被要求强制推送。发布者会先检查 origin(git ls-remote --exit-code --heads):如果已经存在ticket/<n>,那就是先前运行创建的分支,必须停止并交由操作人员决策,绝不快进覆盖它。如果某分支相对于其切出时的尖端没有新增提交,就没有内容可发布:发布者会说明这一点,将该工单记录为已涵盖而非暂停运行;尖端保持不变,其依赖工单继续运行,最终阶段会将该工单标记为仍未关闭。若两个 Agent 同时发布,它们会基于同一个尖端;发布通道的存在正是为了防止这种情况。该通道还会回收工单的工作树:工单进入通道后,其他内容都不会再访问其分支,因此发布者会删除该工单的 Agent 所标明的所有工作树,以及上一位发布者自己的工作树(发布者无法在其内部删除该工作树)。它会在变基之前执行回收,因为变基会使每个切片工作树的 HEAD 脱离其构建所基于的分支;回收时唯一的保护检查,就是确认该 HEAD 仍位于该分支上。该通道还会随着堆栈增长持续注册堆栈——打开 PR 后,它会按从底部到顶部的顺序对整个堆栈运行gh stack link,因此从第 2 层开始就有真实的堆栈映射,而不是等运行结束后才创建(ADR-0006)。每个 PR 正文开头都会写明Layer k of N planned,所有层都会一直保持草稿状态,直到最终阶段:它们共同表明本次运行仍在添加内容。 - 前沿——没有阶段屏障。每张工单都是一个备忘化承诺,等待其依赖工单发布;阻塞项的 PR 一旦存在,该工单就会立即启动,因此运行耗时取决于最长的依赖链,而非各阶段耗时之和。这就是并行执行顺序。依顺序执行时(ADR-0020),前沿会一次处理一张工单:在可执行的工单中,运行会先选择规格说明自身映射中的排序位置(图谱的
map_position)最靠前者,其次选择工单编号最小者;该工单发布后才会派发下一张,因此处理工单期间尖端不会移动,发布者绝不会执行变基,而且除了 Explore 之外,任意时刻都不会有两个 Agent 同时运行。 - 审查——一个 Agent 审查整个堆栈(
base...tip),并在堆栈尖端上运行一次所有已发布工单的### Run at review命令去重并集——这是各工单角色都不会运行的完整测试套件;若 at-review 基线中有某项命令的屏蔽形式,也会将其告知 Agent。Agent 会为每项检查返回{command, passed, exit_code},并像其他角色一样依据记录判断失败:passed: false的检查属于阻塞性发现项,豁免检查不属于发现项;其发现项会像其他工作一样经过派发器处理,修复会作为堆栈顶部的单个集成 PR提交,不会改动下方的任何内容。集成修复者会跨工单修改代码,因此它们的每次变更配方是所有已发布工单的### Run per change命令去重并集,加上 at-review 命令并集——绝不会比任何工单的配方范围更窄;只要有任何工单指定了命令,配方就绝不会为空。另有一个独立、低成本的 Agent 负责打开该 PR,但不修改代码;PR 正文会列出最后一位集成修复者返回的所有豁免检查,以及审查返回但最后一位修复者未能运行通过的所有检查,格式为Waived: <command> exited <exit_code>(无论如何,运行结果都会将审查的结果记为review_waived)。发布是该阶段唯一不可逆的操作,因此不应由最后运行的修复者负责。门禁无法取代这次审查:逐张工单审查在结构上无法看到工单之间的交互——对同一个辅助函数的两种实现、彼此矛盾的抽象,或某张工单依赖但另一张悄然更改的约定。 - 最终阶段——在原生模式下,一次
gh stack link调用会协调该通道一直维护的注册信息:它会添加集成 PR(该 PR 在通道外发布,因此此前从未被链接),并修复通道内任何失败的调用。随后,所有已发布的 PR 都会被标记为可审查——草稿会阻止堆栈合并;而在此步骤之前,草稿正是告诉操作人员运行仍在添加层的信号。只有所有自动化工单都已实现、通过门禁并发布后,才会进入最终阶段(ADR-0016)。工单失败、仍为未满足状态或需要决策,都会暂停运行:从那时起,不会启动任何工单;正在运行的工单也不会再调用新 Agent,除非其派发结果恰好只有一个切片;该切片会完成其处理、门禁和发布。暂停的运行不会执行全栈审查或最终阶段,GitHub 上也不会留下已发布 PR 以外的任何内容;它会先等待仍在运行的 at-review 基线结束,因此没有会话会在运行结束后继续存在,恢复运行时会重放该结果,而不是重新测量;它会返回{ halted: true, reason, tickets: [{ ticket, state, detail }], published, ... },并将每张未发布工单标记为failed、unmet、needs decision、stopped(在处理过程中遇到暂停)或not started,等待恢复运行。完整运行会在顶部 PR 中添加Closes #<spec>,因此一次合并整个堆栈就会同时关闭所有内容。如果没有集成 PR 可列出这些检查,最终阶段就会将全栈审查中的豁免检查添加到顶部 PR 正文,格式为Waived: <command> exited <exit_code>,确保无论如何它们都会显示在 GitHub 上。完整状态采用失败即关闭原则:延后处理的工单、无人修复的门禁发现项、没有判断结果的集成发现项,或从未运行的全栈审查,都会让规格说明保持未关闭——已发布不等于已完成;此时最终阶段不会在任何位置添加关闭规格说明的标记,而会在顶部 PR 和规格说明工单中评论仍未解决的内容。工作流绝不负责合并——合并由操作人员自行决定,并从顶部执行一次。 - 清理——最终阶段只回收通道未回收的内容:最后一位发布者自己的工作树,以及异常退出的回收者仍持有的任何工作树;规则相同。未回收的内容会连同原因一起返回为
worktrees_kept:HEAD 不在其分支上的工作树、文件锁,或异常退出且未报告路径的 Agent 所用工作树(这种工作树绝不删除,只会列出)。在会话运行器上,脚本无论在通道内还是此处都不会回收任何内容:每个 Agent 在整个运行期间都会保留自己的标签页和工作树,运行结束时 runner 不会询问任何问题,操作人员可在运行视图中按需自行回收(ADR-0012,crew runner 参考文档)。顺序运行的链式工作树属于整个运行,而非任何单个 Agent:回收 Agent 时会保留该工作树;只有在没有任何保留中的运行 Agent,且没有 Agent 正在其中运行时,才会由 Reclaim All 最后删除它。若运行暂停时链式工作树已被回收,恢复运行需要它时,就会根据运行的基准重新创建(ADR-0020)。暂停的运行不会回收任何内容:它的工作树会留给恢复运行继续使用。分支、PR 和用户自己的检出目录都保持原样。
技术栈
这条流水线逐个处理工单,构建一条普通分支链——每个 PR 都基于下方的分支——并在过程中将其注册为 原生 GitHub 堆栈,因此从第二层开始,操作人员就能在每个 PR 上看到堆栈映射,并从最上方的 PR 通过 一次原子操作合并整个堆栈(或使用 PUT .../pulls/<top>/merge-async)。
本次运行只使用 gh stack 命令中的 gh stack link:它无状态、会协调状态,且推送时不会强制覆盖——但它会在一次原子推送中按本地引用推送其列出的所有分支,本节后续内容都以此为基础。运行调用它时遵循三条规则(ADR-0006):每次都重新列出整个堆栈,而不是使用堆栈编号快捷方式,以免运行在不同 Agent 之间携带状态;绝不指定尚未有 PR 的分支,因为 link 会在运行控制范围之外创建 PR;并且绝不传入 --open,以免将草稿 PR 标记为可合并并破坏进行中的状态信号。第四条规则(ADR-0007)用于避免推送被拒绝:link 调用列出的每个分支,都必须先将其本地引用与 origin 镜像同步。按照构造,流水线自己的分支本地引用与 origin 一致;第 0 层则不然——它在运行开始前就已到达 origin,而先前运行留下的过期本地引用会遮蔽它;继承的分支也可能在运行期间于 origin 上发生变化。运行不会改写已发布的层(只发布一次),因此以 origin 为准,镜像操作只能移除过期的遮蔽;唯一不会动的是已被工作树检出的分支。堆栈至少需要两层,因此不会注册单 PR 堆栈——这是正确行为,并非失败。link 失败不会导致发布失败,但运行会区分两种失败:临时失败会在下一次发布重新列出分支时自动修复;而推送被非快进拒绝则是确定性问题——它说明某个本地引用仍与 origin 不一致,因此会记录该问题并将其写入简报,而不是继续等待。只有硬退出码 9 会锁定状态,使运行降级为普通分支链;简报会在专属字段中说明这一点,而不是放在脚注里。其余扩展功能不适用于并行运行,因此仍禁止使用——submit 会强制推送所有分支;init、add、sync、rebase 和 checkout 会在每个工作树中各自保存状态(.git/gh-stack 通过 --git-dir 解析),或改写堆栈已发布的历史。
操作人员在合并前应了解以下事项,简报也会重申:
- 每一层都必须单独通过检查:必需的检查、必需的评审和 CODEOWNERS 都会针对堆栈的基础分支,逐一应用于堆栈中的每个 PR——只批准最上方的 PR 并不足够。
- CI 会在每一层运行,且每层都仿佛以基础分支为目标,因此堆栈越深,CI 用量越大。
- 堆栈合并失败后,必须重新读取基础分支的实际状态——GitHub 官方文档对于合并失败是否会完全回滚说法不一,因此不要想当然。
- 在分支链模式下,不存在堆栈对象:需要手动从下往上合并,并使用合并提交;每次合并后删除对应的头部分支,让 GitHub 重新指定下一个 PR 的目标——压缩合并和变基合并会改写 SHA,导致每个子 PR 都出现虚假的差异。
驳回台账
门控循环的风险不在于收敛缓慢,而在于来回反复:修复者认为某条发现有误,于是保留代码;下一位审查者再次提出同一问题,两者就这样围绕同一条发现来回争论,直到达到上限——每一轮都要耗费两个 Agent,却毫无改动。
因此,驳回是一种一等结果。每个修复切片都必须为其负责的每条发现返回一个判定:fixed 或 rejected;驳回时还必须给出具体且可核查的理由。每条驳回都会累积到台账中,并带入之后的所有轮次。后续审查者只有证伪所述理由,才能重新提出已驳回的发现——明确指出哪部分是错的,以及为什么。否则,遇到任何有争议的判断时,循环都将无法终止。
只有确保每条发现都有某种判定,台账才能发挥作用。因此,工作流会在脚本中而非提示词中将判定与发现核对:先按位置和问题精确匹配,否则只接受某个位置唯一对应一条判定的情况。没有判定的发现绝不会被默认视为已修复:系统会记录下来并告知下一位审查者,由其在分支上检查。若通过询问 Agent 未完成哪些工作来进行核对,就会重新引入 ADR-0004 要消除的问题。
每轮都更换新的审查者,才能让“干净”成为一种判定,而不是某位审查者失去耐心后的结果。
审查
门控审查和最终审查都会运行仓库内的 code-review 技能。该技能会针对两个维度,并行调用子 Agent,审查相对于固定基准点的差异:规范(是否遵循此仓库中记录的规范)和规格(是否实现了发起问题所要求的内容)。
两者的区别仅在于固定基准点不同:门控审查将工单分支的切出基准设为固定基准点,并将工单设为规格,因此工单会依据自身的验收标准进行评判;最终审查则将基准引用设为固定基准点,并将规格问题设为规格,对整个堆栈进行审查。
门控审查还会覆盖技能原有的严重级别:只有未满足的标准、失败的验证命令或正确性 Bug 才能阻止通过。规范维度仍会运行并报告结果,但级别为 minor,因此可以通过;这样,ADR 偏差仍会显示在 PR 中,却不会导致多一轮审查(ADR-0008)。整个堆栈的审查则会让两个维度都保持完整权重。
归属
九步流程——将规格和工单组织成任务图、由实现在各自工作树中的子 Agent 执行、将成果发布到共享工作线上、先审查再就绪——遵循 Anthropic 的 implement-spec 技能。本技能新增的内容包括:将流程转化为一个确定性脚本(前沿调度、串行发布通道)、将每张工单各自对应一个 PR 的堆叠式输出注册为原生 GitHub 堆栈、按工单执行带驳回台账的门控循环、通过人工审核排除工单,以及让协调器保持空上下文的规范。