news 2026/9/7 17:30:58

gstack /autoplan Implementation Tasks Aggregator 原理与实战:多阶段评审任务的跨阶段汇聚管道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gstack /autoplan Implementation Tasks Aggregator 原理与实战:多阶段评审任务的跨阶段汇聚管道

gstack /autoplan Implementation Tasks Aggregator 原理与实战:多阶段评审任务的跨阶段汇聚管道

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

导读

本文详解 gstack 的/autoplan自动评审流水线中,Phase 4「Final Approval Gate」所使用的 Implementation Tasks Aggregator(任务汇聚器):它把 CEO、Design、Eng、DX 四个评审阶段各自写入磁盘的 JSONL 任务清单,聚合、去重、排序后渲染成一个统一的### Implementation Tasks勾选清单。读完本文你将掌握:任务 JSONL 的落盘格式与文件命名规则、聚合脚本每一段 jq 管道的语义(分支/提交窗口过滤、run_id 去重、精确去重与优先级排序)、运行时变量替换与构建期模板占位符的区别,以及那个让聚合器"永远输出零任务"的 #2018 jq 上下文重绑定陷阱及其回归测试。

本文对应的权威文档是 tasks-aggregator.md,它在整个流水线中的触发时机由 autoplan/SKILL.md 的 Section index 与 sections/manifest.json 共同注册:在呈现 Final Approval Gate(Phase 4)时读取,聚合器算出的$AGGREGATED_TASKS会被闸门消息替换进去。全文所有代码与路径均可在当前仓库中逐一验证。


一、聚合器在 /autoplan 流水线中的位置

/autoplan的目标是"一条命令,粗糙计划进、全量评审计划出":它从磁盘读取四个评审 skill(CEO、Design、Eng、DX)并按严格顺序串行执行——CEO → Design → Eng → DX,每一阶段必须完整结束后才进入下一阶段(见 autoplan/SKILL.md 的 "Sequential Execution — MANDATORY")。中间的所有 AskUserQuestion 都基于 6 条决策原则自动裁决,品味类决策(close approaches、borderline scope、codex 分歧)与 User Challenge 被保留到最后的 Final Approval Gate 让用户做最终判断。

每个评审阶段在结束时不仅要产出评审结论,还要为后续实施沉淀一份"可构建的扁平任务列表"。聚合器就是这些阶段产物与最终闸门之间的汇流点:

  • 上游:四个评审 skill 各自写入tasks-<phase>-*.jsonl任务工件;
  • 聚合器:把四份列表合并为一个全局有序、无重复的清单;
  • 下游:Final Approval Gate 输出模板中的### Implementation Tasks (aggregated across phases)小节直接渲染这个清单,供用户在 A) 批准 / B) 覆盖 / C) 盘问 / D) 修订 / E) 拒绝之间做选择。

值得强调的是,聚合器不是用户手动运行的一个命令,而是一段由 Agent 在运行时执行的 bash/jq 指令序列。gstack 以"决策树骨架"方式组织 skill:主 SKILL 只负责编排(读哪个 section、何时读),而 tasks-aggregator.md 是 Phase 4 这一具体步骤的"真源"(source of truth)。SKILL 中的指引明确写着:

Read~/.claude/skills/gstack/autoplan/sections/tasks-aggregator.mdand execute it in full. Do not work from memory — that section is the source of truth for this step.

也就是说,任何人(含 Agent 自身)想理解 Phase 4 之前如何准备任务清单,只需读这一个文件即可。


二、数据源:每个评审阶段写下的 JSONL 任务工件

2.1 工件格式与写入端

在 sections/manifest.json 中,聚合器被声明为id: "tasks-aggregator",对应 Phase 4。而它的上游——每个评审阶段"逐任务写 JSONL"的行为——由解析器{{TASKS_SECTION_EMIT:<phase>}}负责渲染,实现在 scripts/resolvers/tasks-section.ts。该文件头部注释直接说明了两种解析器的分工:

  • {{TASKS_SECTION_EMIT:<phase>}}:单 skill 的任务产出 + JSONL 写入(CEO/Design/Eng/DX 四个评审各自使用);
  • {{TASKS_SECTION_AGGREGATE}}:autoplan 跨阶段聚合,即本主题。

VALID_PHASES集合只接受四个值:ceo-reviewdesign-revieweng-reviewdevex-review,传入其他值会让解析器直接抛错(tasks-section.ts)。

四个评审 skill 的实际写入代码位于各自的review-sections.md中,例如 plan-ceo-review/sections/review-sections.md、plan-eng-review/sections/review-sections.md。每条任务以一行 JSON 追加到文件末尾,字段 schema 在 tasks-section.ts 中有权威定义:

字段含义取值示例
phase来源评审阶段ceo-review/design-review/eng-review/devex-review
run_id本次运行的唯一标识20260814T000000Z-12345(UTC 时间戳 + 进程 PID)
branch评审运行时的当前分支feature/x
commit评审针对的 HEAD 提交(完整 SHA)abc123def...
id任务编号T1T2
priority优先级P1(阻塞发布)/P2(应落在同一分支)/P3(后续 TODO)
component所属组件browse/src/server.ts所属模块名
files涉及文件(JSON 数组字面量)["browse/src/sanitize.ts","browse/src/server.ts"]
effort_human人类工作量估计~2h
effort_ccCC(Claude Code + gstack)时间估计~15min
title任务的祈使句标题Add null check in cookie import
source_finding溯源:哪个 section 的哪条 finding<section name> — <finding 摘要>

写入端的固定约束包括:

  • 文件命名:$TASKS_DIR/tasks-<phase>-$(date +%Y%m%d-%H%M%S).jsonl,例如tasks-ceo-review-20260814-093015.jsonl
  • run_id使用$(date -u +%Y%m%dT%H%M%SZ)-$$$$是写入进程的 PID,保证同秒内多次运行也不撞号;
  • 必须用jq -nc构造每一行,禁止手写echo/printf拼 JSON——因为 title 与 source_finding 可能含引号、换行、反斜杠,只有交给 jq 序列化才安全(tasks-section.ts);
  • 零发现时仍要触碰该文件: > "$TASKS_FILE"),因为"空文件 = 跑过但无 finding"与"没有文件 = 根本没跑"在聚合端是两种语义(tasks-section.ts);
  • jq缺失时跳过 JSONL 写入并向用户告警,但绝不手工伪造 JSONL

这些字段名之所以重要,是因为聚合脚本要按它们逐个做过滤、去重与排序。

2.2 读取端目录约定

TASKS_DIR派生自gstack-slug计算出的项目别名(SLUG):

eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" TASKS_DIR="${HOME}/.gstack/projects/${SLUG:-unknown}"

SLUG无法取得时回退为字面量unknown,避免目录拼接失败。四个评审阶段写入、聚合器读取的正是~/.gstack/projects/<slug>/下同一组文件。


三、聚合主脚本:逐步拆解

下面这段脚本出自 tasks-aggregator.md 的完整原文(也与 tasks-section.ts 中generateTasksSectionAggregate运行时产出的内容一致),先整体给出,再分五步解读:

eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" TASKS_DIR="${HOME}/.gstack/projects/${SLUG:-unknown}" BRANCH=$(git branch --show-current 2>/dev/null || echo unknown) # Commit window: last 5 commits on this branch. Drops stale standalone reviews. COMMITS_RECENT=$(git log --format=%H -n 5 2>/dev/null | tr '\n' '|' | sed 's/|$//') AGGREGATED_TASKS="" if command -v jq >/dev/null 2>&1; then # Collect entries from all 4 phases, scoped to current branch + commit window. # For each phase, keep only the latest run_id. Within the surviving set, # dedupe by (component, sorted(files), title) — exact match only. # Sort by priority (P1 > P2 > P3) then by phase order. ALL_JSONL=$(mktemp -t autoplan-tasks.XXXXXXXX) for phase in ceo-review design-review eng-review devex-review; do # Use find instead of glob expansion — zsh nomatch errors otherwise when # a phase produced no JSONL files. Sorting by name keeps the order stable. while IFS= read -r f; do [ -f "$f" ] || continue # Filter to current branch + recent commits, then keep records for the # latest run_id only. (Single phase may have multiple files if the user # re-ran the review; aggregator takes the newest.) # .commit must be bound BEFORE piping to the split commit array: a # pipe rebinds jq's context, so a bare .commit after it indexes the # ARRAY with a string, every line errors into 2>/dev/null, and the # aggregate is empty forever — the #2018 zero-tasks bug. jq -c --arg branch "$BRANCH" --arg commits "$COMMITS_RECENT" \ '.commit as $c | select(.branch == $branch and ($commits | split("|") | index($c) != null))' \ "$f" 2>/dev/null >> "$ALL_JSONL" || true done < <(find "$TASKS_DIR" -maxdepth 1 -name "tasks-$phase-*.jsonl" 2>/dev/null | sort) # Reduce to latest run_id per phase if [ -s "$ALL_JSONL" ]; then jq -sc --arg phase "$phase" \ '[.[] | select(.phase == $phase)] | (max_by(.run_id) // null) as $latest_run | if $latest_run then map(select(.run_id == $latest_run.run_id)) else [] end | .[]' \ "$ALL_JSONL" > "$ALL_JSONL.phase" 2>/dev/null || true # Replace with reduced version for this phase, accumulating others jq -c --arg phase "$phase" 'select(.phase != $phase)' "$ALL_JSONL" > "$ALL_JSONL.other" 2>/dev/null || true cat "$ALL_JSONL.other" "$ALL_JSONL.phase" > "$ALL_JSONL" rm -f "$ALL_JSONL.phase" "$ALL_JSONL.other" fi done # Exact-match dedup by (component, sorted(files), title). Non-matches kept # separately with a possible-duplicate marker injected by the renderer. AGGREGATED_TASKS=$(jq -s \ 'group_by([.component, (.files | sort), .title]) | map( # Take the highest-priority entry per group; tie-break by phase order sort_by({P1:0,P2:1,P3:2}[.priority] // 99, {"ceo-review":0,"design-review":1,"eng-review":2,"devex-review":3}[.phase] // 99) | .[0] ) | sort_by({P1:0,P2:1,P3:2}[.priority] // 99, {"ceo-review":0,"design-review":1,"eng-review":2,"devex-review":3}[.phase] // 99) | if length == 0 then "_No actionable tasks emitted from any phase._" else map("- [ ] **\(.id) (\(.priority), human: \(.effort_human) / CC: \(.effort_cc)) — \(.component)** — \(.title)\n - Surfaced by: \(.phase) — \(.source_finding)\n - Files: \(.files | join(", "))") | join("\n") end' "$ALL_JSONL" 2>/dev/null | sed 's/^"//;s/"$//;s/\\n/\n/g') rm -f "$ALL_JSONL" else AGGREGATED_TASKS="_jq not installed — install jq to aggregate per-phase task lists. Skipping._" fi

步骤 1:建立运行上下文与提交窗口

脚本开头确定四个关键变量:

  1. SLUG:由gstack-slug输出,定位项目级工件目录~/.gstack/projects/<slug>
  2. TASKS_DIR:上述目录,所有 JSONL 的读写点;
  3. BRANCH:当前分支名,git branch --show-current失败时回退为unknown
  4. COMMITS_RECENT当前分支最近 5 个提交的完整 SHA,用|连接成单串。它的作用是丢弃"过期的独立评审"——如果你在别的分支或很久之前跑过评审,其任务不会混入本次闸门。

步骤 2:按阶段收集 + 分支/提交窗口过滤

外层for phase in ceo-review design-review eng-review devex-review固定按 CEO → Design → Eng → DX 的顺序遍历。内层用find而非 shell 通配符展开来枚举tasks-$phase-*.jsonl——注释明确说明这是为了兼容 zsh:阶段没有产出任何 JSONL 时,通配符展开会触发nomatch报错中断脚本;find+sort则既无此问题又保证枚举顺序稳定。

对每个文件的过滤是整段脚本的心智核心:

jq -c --arg branch "$BRANCH" --arg commits "$COMMITS_RECENT" \ '.commit as $c | select(.branch == $branch and ($commits | split("|") | index($c) != null))' \ "$f" 2>/dev/null >> "$ALL_JSONL" || true

语义是:仅保留branch等于当前分支、且commit命中最近 5 提交窗口的记录。2>/dev/null|| true是刻意的容错设计——单个文件损坏不应杀死整个聚合,坏行被丢弃即可。

步骤 3:每个阶段只保留最新一次运行(run_id 收敛)

同一阶段可能因用户重跑评审而存在多个文件(每次运行一个时间戳文件名、一个run_id)。聚合原则是"取最新一次运行",否则会重复计数:

'[.[] | select(.phase == $phase)] | (max_by(.run_id) // null) as $latest_run | if $latest_run then map(select(.run_id == $latest_run.run_id)) else [] end | .[]'

max_by(.run_id)基于YYYYMMDDTHHMMSSZ-PID字符串做字典序比较,天然等价于时间排序;// null防御空输入;随后用select保留该次运行的全部记录。临时文件.phase/.othercat重组技巧,实现了"只缩当前阶段、保留其余阶段累积结果"的原地归并。

步骤 4:跨阶段精确去重 + 双键排序

所有阶段汇聚进ALL_JSONL后,最终渲染前做一次全局去重与排序:

  • 去重键(component, sorted(files), title)三元组的完全相等匹配。filessort再比较,使文件书写顺序不同但集合相同的任务仍被视为重复(tasks-section.ts 注释还提到:未命中的"疑似重复"会由渲染器另行标记 possible-duplicate,而非静默丢弃);
  • 组内择优sort_by({P1:0,P2:1,P3:2}[.priority] // 99, {...}[.phase] // 99)取每组第一条,即同组冲突时高优先级(P1)获胜,再平局则按阶段顺序 ceo → design → eng → devex 取先者;缺失的优先级/阶段字段映射到99表示"最低优先";
  • 最终排序:同上双键——先优先级(P1 > P2 > P3),再阶段顺序。

步骤 5:渲染 Markdown 勾选清单

排序后的记录被映射为每条一行、符合"gstack 风格"的待办项:

- [ ] **T1 (P1, human: ~1h / CC: ~15min) — some-component** — Add null check - Surfaced by: ceo-review — demo finding - Files: browse/src/sanitize.ts, browse/src/server.ts

渲染模板为map("- [ ] **\(.id) (\(.priority), human: \(.effort_human) / CC: \(.effort_cc)) — \(.component)** — \(.title)\n - Surfaced by: \(.phase) — \(.source_finding)\n - Files: \(.files | join(", "))")。外层sed 's/^"//;s/"$//;s/\\n/\n/g'负责去掉 jq 输出字符串的外层引号并把\n还原为真实换行。没有记录时输出占位文案_No actionable tasks emitted from any phase._


四、运行时替换,而非构建期模板

阅读 tasks-aggregator.md 时要特别区分两个层面:

  • 构建期(gen-skill-docs)tasks-aggregator.mdtasks-aggregator.md.tmpl渲染而来,而后者只有一行占位符{{TASKS_SECTION_AGGREGATE}}(见 tasks-aggregator.md.tmpl),由generateTasksSectionAggregate解析器填充成你看到的完整脚本文本。文档头部注释AUTO-GENERATED from tasks-aggregator.md.tmpl — do not edit directly也印证了这一点:手工改.md会被下一次bun run gen:skill-docs覆盖;
  • 运行时(Agent 执行)$AGGREGATED_TASKS不是模板占位符。文档明确写道:

This is NOT a template placeholder — the agent does the substitution at runtime, not gen-skill-docs at build time.

即:Agent 按脚本实际执行并把 bash 变量$AGGREGATED_TASKS的内容在打印闸门消息前手工替换进 Final Approval Gate 模板中的### Implementation Tasks (aggregated across phases)小节(该小节模板见 autoplan/SKILL.md)。如果把它当成构建期占位符处理,四个阶段的任务就永远不会出现在用户的最终报告里。


五、空结果与降级路径

5.1 两种"空"必须区分

  • 无文件(本轮没有任何评审 skill 运行):渲染_No per-phase task lists found in $TASKS_DIR for branch $BRANCH. Each review skill writes its own; if you ran one of them but no list appears here, check that jq is installed and the tasks-<phase>-*.jsonl files exist._
  • 有文件但零任务:所有阶段都跑了、确认无 finding,聚合器输出_No actionable tasks emitted from any phase._

区分二者的关键在于 2.1 节提到的写入约定:空文件意味着"跑过、没发现",无文件意味着"压根没跑"。这也解释了为什么脚本在if [ -s "$ALL_JSONL" ]后才做 run_id 收敛——空文件不该触发任何统计。

5.2 jq 缺失时的降级

脚本首先检查command -v jq。jq 不可用时AGGREGATED_TASKS被置为字符串_jq not installed — install jq to aggregate per-phase task lists. Skipping._,闸门消息仍可完整呈现,只是任务清单段落变成安装提示,流水线不因此中断。


六、#2018 零任务 Bug:一个 jq 上下文陷阱的真实教训

聚合脚本注释中专门内嵌了一段 bug 史,值得单独展开,因为它是理解 jq 语义与整个过滤链正确性的一把钥匙(tasks-aggregator.md):

.commit must be bound BEFORE piping to the split commit array: a pipe rebinds jq's context, so a bare .commit after it indexes the ARRAY with a string, every line errors into 2>/dev/null, and the aggregate is empty forever — the #2018 zero-tasks bug.

错误形态曾经是:

select(.branch == $branch and ($commits | split("|") | index(.commit) != null))

根因:在 jq 中,|管道会把求值上下文重绑定到左侧表达式的结果上。这里$commits | split("|")之后的上下文是字符串数组,此时再写裸.commit,实际是对数组做字符串索引["abc123","def456"]["commit"],每一行都抛Cannot index array with string "commit";错误被2>/dev/null吞掉、|| true吞掉退出码,最终结果是聚合永远为空,且与"确实没有任务"在现象上无法区分——一个"死亡但外表正常"的功能。

正确形态是先用as把值绑定到变量,再做管道:

.commit as $c | select(.branch == $branch and ($commits | split("|") | index($c) != null))

.commit as $c在管道之前执行,$c通过变量作用域穿透后续管道,不再受上下文重绑定影响。

回归保障落在 test/tasks-section-jq.test.ts:该测试从解析器源码中正则提取真实下发的 jq 程序(而非复制一份近似实现),再对着 fixture JSONL 用真实jq二进制执行,验证:同分支且在窗口内能匹配、异分支/窗口外提交被滤除、空输入不伪造输出;最后一条测试直接断言源码中不允许再次出现split("|") | ... | index(.commit)这种会重绑定上下文的写法。任何重引入该缺陷形态的改动都会让这些测试变红,这正是文档注释the #2018 zero-tasks bug被逐字保留在产物脚本中的原因——它同时是给未来维护者的现场注释与给回归测试的显式契约。


七、实战排查清单

结合上游写入端与聚合脚本,当"评审跑了但闸门里没有任务清单"时,按下面顺序自查:

  1. jq 是否安装:聚合脚本输出是否为_jq not installed ...文案;
  2. 文件是否存在ls ~/.gstack/projects/<slug>/tasks-<phase>-*.jsonl。完全不存在 = 对应评审阶段本轮没跑(也可能没有 UI/DX 范围而合法跳过 Phase 2 / Phase 3.5);
  3. 分支与提交窗口:任务记录的branch/commit是否属于当前分支最近 5 个提交。文件存在但记录全部被窗口滤除时,聚合结果同样为空——这正是该过滤想表达的语义:只信本次评审
  4. 文件是否为空wc -l为 0 表示该阶段跑过但零 finding,属正常结果而非故障;
  5. 是否重跑过评审:同一阶段多个文件时,聚合只认run_id最大(最新)的那次;
  6. 内容是否含特殊字符:检查 JSONL 行是否由jq -nc生成而非手写拼串,title/source_finding 中的引号或换行是否被正确转义。

八、小结:从"评审输出"到"可勾选实施清单"的最后一步

Implementation Tasks Aggregator 是/autoplan自动化闭环中承上启下的枢纽:上游吞掉四个评审阶段(ceo-phase、design-phase、eng-phase、dx-phase)产出的 JSONL 工件,下游吐给 Final Approval Gate 一份已按当前分支与最近 5 提交过滤、按 run_id 收敛到最新、按 (component, files, title) 精确去重、按 P1→P3 与 CEO→DX 排序的 markdown 勾选清单。它把多模型、多阶段的评审噪音压缩成"批准、覆盖、盘问、修订、拒绝"五个选项前的那一屏事实,而其正确性由文档内嵌注释、运行时替换约定与 #2018 回归测试三重保障。若想深入其实现细节,最直接的阅读路径是:先读 tasks-aggregator.md 了解行为契约,再到 tasks-section.ts 看两种解析器如何产出脚本,最后在 tasks-section-jq.test.ts 里看真实 jq 程序的回归演练。

【免费下载链接】gstackUse Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA项目地址: https://gitcode.com/GitHub_Trending/gs/gstack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 17:29:29

单臂路由技术:原理、优化与企业级部署指南

1. 单臂路由技术解析&#xff1a;从理论到实践的全方位指南在中小型网络部署中&#xff0c;我们经常会遇到这样的场景&#xff1a;一台核心交换机需要同时处理多个VLAN间的通信需求&#xff0c;但预算有限无法购置三层交换机。这时候单臂路由&#xff08;Router-on-a-Stick&…

作者头像 李华
网站建设 2026/9/7 17:29:00

Windows 11下Ryzen AI MAX+ 395显存分配优化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:27:34

从单体到微服务再到事件驱动:架构演进实战路径与避坑指南

做了十来年架构&#xff0c;从最开始一个人维护一套SSH单体应用&#xff0c;到后来带团队把系统拆成几十个微服务&#xff0c;再到近两年开始把核心链路逐步迁到事件驱动架构&#xff0c;这条演进路线其实踩了非常多的坑。很多时候网上讲架构演进都是拿现成的结论讲&#xff0c…

作者头像 李华
网站建设 2026/9/7 17:27:05

2026年AI写小说软件推荐:存稿管理与断更应对榜(5款)

断更是网文作者的噩梦&#xff1a;要么是灵感枯竭写不出来&#xff0c;要么是现实事务打断节奏&#xff0c;要么是稿子写到一半丢了。AI写小说软件在应对断更上能做三件事&#xff1a;帮作者积累存稿、在断更后快速恢复状态、在稿件安全上兜底。本文依据各产品官方公开资料&…

作者头像 李华