gsd-core 修复解读:3381 修复 —— /gsd-verify-work --ws 通过 SDK 解析工作流阶段

原创2026-09-26 23:13:31572 阅读

gsd-core 修复解读:#3381 修复 —— /gsd-verify-work --ws 通过 SDK 解析工作流阶段

本篇文章基于 .changeset/archived/fix-3381-init-verify-work-ws.md(type: Fixed,PR #3386)展开,剖析 gsd-core 中 /gsd-verify-work --ws <name> 的缺陷根因、修复实现与回归验证。读完本文,你将理解工作流(workstream)模式下阶段验证的目录作用域问题,掌握 init.verify-work、MVP 模式查询与阶段目标查询如何接收到选中工作流,从而不再错误回退到根目录 .planning/。

一、变更速览:一条 changeset 背后的问题

该 changeset 全文只有一句话,却指向一个真实且影响面明确的缺陷:

/gsd-verify-work --ws <name> now resolves workstream phases through the SDK — init.verify-work, MVP-mode lookup, and phase-goal lookup all receive the selected workstream instead of falling back to root .planning/.

拆解这句话可以得到三个事实:

  1. 修复对象是命令 /gsd-verify-work --ws <name>,即在指定工作流(workstream)中对某个阶段执行验证;
  2. 修复方式是"通过 SDK 解析工作流阶段"——即由 gsd-core 的查询命令(query init.verify-work 等)承担阶段解析,而不是让工作流脚本自行猜测路径;
  3. 修复涉及三个查询点:init.verify-work(初始化阶段上下文)、MVP 模式查询(phase.mvp-mode)、阶段目标查询(roadmap.get-phase);修复前它们会回退到根目录 .planning/,修复后都接收到用户选中的工作流。

这条修复之所以成立,需要先理解两个背景:/gsd-verify-work 命令本身,以及 gsd-core 的工作流(workstream)目录作用域模型。

二、背景:/gsd-verify-work 与工作流作用域

2.1 verify-work 命令是做什么的

命令契约定义在 commands/gsd/verify-work.md 中,其 frontmatter 声明:

name: gsd:verify-work
description: Validate built features through conversational UAT
argument-hint: "[phase number, e.g., '4'] [--ws <name>]"
requires: [execute-phase, phase]

它的职责是"通过会话式 UAT 验证已构建的功能":一次一个测试、纯文本回答、不搞审问式提问;发现问题后自动诊断、规划修复并为执行做准备。输出为 {phase_num}-UAT.md 测试结果追踪文件,若发现问题则产出已诊断的差距与可直接交给 /gsd:execute-phase 的修复计划。命令本身可带两个参数:阶段号(如 4)与 --ws <name> 工作流名。

2.2 工作流模式的目录作用域

gsd-core 在项目根 .planning/ 之外支持多工作流布局。从 src/init.cts 的相关注释与实现可以看到:

  • 每个工作流拥有自己的规划根:.planning/workstreams/<ws>/,其中 phases/、ROADMAP.md、STATE.md、REQUIREMENTS.md 都是工作流作用域的(workstream-scoped);
  • 例外是 PROJECT.md,它是跨工作流共享的("PROJECT.md is shared across workstreams",见 src/init.cts 中 cmdInitCompleteMilestone 相关注释);
  • planningDir(cwd) 是"工作流感知"的:当存在激活的工作流时,它解析到该工作流的规划目录,而不是根 .planning/。

这里的关键机制是 GSD_WORKSTREAM 环境变量。从 src/init.cts 的注释可以确认:显式传入 --ws 会设置 GSD_WORKSTREAM,从而满足工作流模式的检查;而没有激活工作流又没有 --ws 时,planningDir(cwd) 会解析到根 .planning/——这正是"静默报告一个过期的根里程碑"的危险路径。

三、缺陷根因:--ws 未被 SDK 查询消费

修复前的缺陷链条如下:

  1. 用户在 /gsd-verify-work --ws <name> 中显式指定了工作流;
  2. 但工作流脚本(gsd-core/workflows/verify-work.md)没有从 $ARGUMENTS 中提取 --ws 并转发给下游 SDK 查询;
  3. 于是 GSD_WORKSTREAM 从未被设置,planningDir(cwd) 继续解析到根 .planning/;
  4. 结果是:init.verify-work 的阶段查找、phase.mvp-mode 的 MVP 模式判定、roadmap.get-phase 的阶段目标读取,全部落在错误的目录上——要么找不到工作流内的阶段(阶段在 .planning/workstreams/<ws>/phases/ 下),要么读到根目录的同名阶段/目标,产生"张冠李戴"的验证上下文。

从 src/init-command-router.cts 的注释也能印证这条边界:--ws 在发布的工作流中指向独立的 query init.verify-work 查询接口(seam),并在到达 init verify-work 命令族之前被剥离。也就是说,--ws 的消费点本来就在查询层,工作流若不在调用查询时把它带上去,它就彻底丢失。

四、修复的工程实现:三层收口

修复将 --ws 从"用户参数"一路收口到"SDK 查询参数",共三层。

4.1 工作流层:从 $ARGUMENTS 提取 --ws 并派生阶段参数

gsd-core/workflows/verify-work.md 的 initialize 步骤现在包含以下解析逻辑:

GSD_WS=""
echo "$ARGUMENTS" | grep -qE -- '--ws[[:space:]]+[A-Za-z0-9._-]+' && GSD_WS=$(echo "$ARGUMENTS" | grep -oE -- '--ws[[:space:]]+[A-Za-z0-9._-]+')
PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)

INIT=$(gsd_run query init.verify-work "${PHASE_ARG}" ${GSD_WS})

要点:

  • GSD_WS 默认置空;只有当 $ARGUMENTS 中出现 --ws <name> 形态时才提取整对 token(--ws 连同工作流名)作为 GSD_WS;
  • PHASE_ARG 通过 sed 删除 --ws <name> 后经 xargs 去空白得到,保证阶段号参数纯净;
  • 随后 query init.verify-work "${PHASE_ARG}" ${GSD_WS} 把工作流名原样转发给 SDK 查询(GSD_WS 展开后即 --ws <name> 两个 token,未加引号以正确分词)。

值得注意的细节:--ws 值的工作流 slug 字符类被刻意收窄为 [A-Za-z0-9._-]+(而非宽泛的 [^[:space:]]+),因为工作流 slug 由这些字符构成,收窄可以避免误吞后续参数,也保证了 GSD_WS 能可靠到达每一个工作流敏感的查询。

4.2 SDK 查询层:init.verify-work 接收工作流并解析阶段

query init.verify-work 由 src/init.cts 的 cmdInitVerifyWork 实现。收到带工作流作用域的 cwd 后,它通过共享原语解析阶段:

const config = loadConfig(cwd);
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
phaseInfo = applyRoadmapFallback(phaseInfo, roadmapPhase, (rp) => { /* 合成回退对象 */ });
  • guardedFindPhase(src/init.cts)封装 findPhaseInternal,并附加 #2056 外来前缀防护:当查询携带了项目代码前缀而阶段不匹配时返回 null;
  • guardedGetRoadmapPhase 是对路线图阶段读取的同等防护封装;
  • applyRoadmapFallback(src/init.cts)是归档/未命中回退:若磁盘阶段已归档而路线图仍有记录,则置空;若磁盘未命中而路线图命中,则用路线图合成阶段信息(phase_number、phase_name、phase_slug、空 plans/summaries 等)。该共享回退同样被 execute-phase、plan-phase、code-review、review 等命令复用,保证行为一致。

在拿到阶段信息后,cmdInitVerifyWork 继续组装验证上下文:

  • phase_dir、phase_number、phase_name、has_verification;
  • state_path / roadmap_path:经由工作流感知的 planningDir(cwd) 解析(STATE.md、ROADMAP.md),供 verify-work.md 的 plan_gap_closure 步骤读取,而不是硬编码根 .planning/ 字面量(对应 #2376 的修复);
  • phase_completion:内含 buildPhaseCompletionProjection 的结果(implementation_complete、verification_status、verification_passed、phase_complete、verification_next_action、verification_next_command 等),以及 UAT 状态(uat_passed、uat_blockers、ready_to_transition);
  • ui_phase_active 与 section_manifest:供 UI 验证与"mvp-uat-framing"小节门控使用。

正因为 GSD_WS(即 --ws <name>)被传到了这条查询链路,GSD_WORKSTREAM 得以生效,planningDir(cwd) 解析到 .planning/workstreams/<ws>/,findPhaseInternal 才能在正确的作用域下找到阶段目录——修复前这一步恰恰回退到了根 .planning/。

4.3 作用域查询:MVP 模式与阶段目标

除了初始化查询,工作流还把工作流转发给另外两个查询:

# MVP 模式检测:集中式 phase.mvp-mode 解析器
MVP_MODE=$(gsd_run query phase.mvp-mode "${phase_number}" ${GSD_WS} --pick active)

以及回归测试断言中的阶段目标查询:

gsd_run query roadmap.get-phase "${phase_number}" ${GSD_WS} --pick goal
  • phase.mvp-mode 判定"当前阶段是否为 MVP 模式"。修复前它读取根 .planning/ 下路线图中的 **Mode:** mvp 标记,工作流内阶段会得到错误答案;修复后工作流被转发,模式判定按工作流作用域进行。工作流注释还说明:verify-work 没有 --mvp 命令行开关,模式从已规划阶段继承,因此查询省略 --cli-flag,走 roadmap → config → false 的回落链。
  • roadmap.get-phase 提取阶段目标(goal)。其解析依据位于 src/roadmap-parser.cts,通过 **Goal:** 区块正则提取目标文本。该查询在 verify-work 的 verify_phase_goal 步骤中被消费——验证器(gsd-verifier)需要拿到正确的阶段目标与需求 ID 才能核对实现是否达标。

三个查询全部拿到 GSD_WS,正是 changeset 中"init.verify-work、MVP-mode lookup、phase-goal lookup all receive the selected workstream"的落地形态。

五、验证结果如何被路由:verification 状态机

阶段解析正确之后,验证结果本身由 src/verification.cts 统一裁决。该模块是验证状态路由的唯一事实源,定义在 VERIFICATION_ROUTING_TABLE(src/verification.cts):

status 语义 推荐下一步
passed 验证通过 继续
gaps_found 发现差距 运行 plan-phase <N> --gaps 规划修复,重新执行后再发布
human_needed 需要人工验证 完成 *-UAT.md 中的手工测试后重跑 verify-work
stale 覆盖的源文件在验证后发生了变化 重跑 execute-phase(在验证门处恢复并重新运行验证器,刷新 VERIFICATION.md 与摘要)
missing 无验证报告 运行 execute-phase 是安全的(不会重跑已有 SUMMARY.md 的计划)
unparseable 报告 frontmatter 不是合法 YAML 直接修复报告中的语法错误(重跑 execute-phase 无法修复)
unknown 非标准的自定义状态 若是手工标记则无需处理,否则运行 execute-phase 重新生成验证

其中 passed/gaps_found/human_needed 是验证器(gsd-verifier agent)真正会写出的值(VERIFIER_STATUSES);stale/missing/unparseable/unknown 是内部构造的哨兵状态。next_command 统一经过 formatGsdSlash 投影为当前运行时的命令形态(如 /gsd-execute-phase、$gsd-execute-phase),避免硬编码废弃的 /gsd: 冒号形式。

readVerificationStatus 只读取 *-VERIFICATION.md frontmatter 中的 status(解析器锚定在文件字节 0,正文里的 status: 不会误读),并支持 #4155 引入的覆盖输入指纹(covered_files + covered_digest)作为比 mtime 更强的过期判定依据。这些路由结果最终通过 cmdInitVerifyWork 的 phase_completion 暴露给工作流,驱动 UAT 会话与差距闭环。

六、回归测试:--ws 转发链路被锁死

修复不是只改脚本了事,还配了回归测试。当前测试位于 tests/verify-work-auto-transition.test.cjs,标题即 bug #3381: verify-work forwards workstream context,测试注释说明它由 tests/bug-3381-verify-work-workstream.test.cjs 折叠而来(合并史诗 #1969)。

该测试用正则逐条断言工作流源码中必须存在的转发契约:

assert.match(workflow, /GSD_WS=""/, 'verify-work must initialize GSD_WS');
assert.match(workflow, /grep -qE -- '--ws[[:space:]]+[A-Za-z0-9._-]+'/, 'verify-work must detect --ws in $ARGUMENTS');
assert.match(workflow, /grep -oE -- '--ws[[:space:]]+[A-Za-z0-9._-]+'/, 'verify-work must extract the --ws flag pair from $ARGUMENTS');
assert.match(workflow, /PHASE_ARG=\$\(echo "\$ARGUMENTS" \| sed -E 's\/--ws[[:space:]]+[A-Za-z0-9._-]+\/\/g' \| xargs\)/, 'verify-work must derive PHASE_ARG after removing --ws');
assert.match(workflow, /gsd_run query init\.verify-work "\$\{PHASE_ARG\}" \$\{GSD_WS\}/, 'init.verify-work must receive GSD_WS so phase_dir resolves in workstreams');
assert.match(workflow, /gsd_run query phase\.mvp-mode "\$\{phase_number\}" \$\{GSD_WS\} --pick active/, 'phase.mvp-mode must receive GSD_WS so roadmap mode is workstream-scoped');
assert.match(workflow, /gsd_run query roadmap\.get-phase "\$\{phase_number\}" \$\{GSD_WS\} --pick goal/, 'roadmap.get-phase must receive GSD_WS so goals are workstream-scoped');

这份断言同时锁死了三类东西:

  1. 解析逻辑:GSD_WS 初始化、--ws 检测与提取、PHASE_ARG 的派生(sed 删除 --ws <name> 对);
  2. 转发对象:三个工作流敏感查询必须无一遗漏地收到 GSD_WS;
  3. 语义承诺:注释明确写明了每条断言的动机——init.verify-work 收到 GSD_WS 才能让 phase_dir 在工作流内正确解析;phase.mvp-mode 收到它路线图模式才是工作流作用域;roadmap.get-phase 收到它目标才是工作流作用域。

任何未来的重构若删掉其中一条转发,测试就会立刻失败,从而防止缺陷 #3381 以"回归"形式复活。

七、使用方式与适用前提

修复后的命令形态与使用方式如下:

# 在根项目上验证阶段 4
/gsd-verify-work 4

# 在指定工作流 my-ws 中验证阶段 4(本次修复的核心场景)
/gsd-verify-work 4 --ws my-ws

工作流 slug 由 [A-Za-z0-9._-] 字符构成,--ws 与工作流名之间需以空白分隔。适用前提与限制:

  • 该命令依赖 gsd-core 的 SDK 查询能力(gsd_run query ...),需要已完成 gsd-core 安装并在 $ARGUMENTS 中携带阶段号或存在活跃会话;
  • 工作流模式要求显式工作流:无活跃工作流且未传 --ws 时,planningDir(cwd) 解析到根 .planning/ 是旧行为,请务必显式指定;
  • 本修复保证的是"工作流内阶段解析正确",验证的裁决仍遵循 src/verification.cts 的状态机(passed/gaps_found/human_needed 等),UAT 输出仍是 {phase_num}-UAT.md;
  • 差距闭环路径不变:发现问题后 plan_gap_closure 步骤使用 {state_path}、{roadmap_path}(二者已由工作流感知的 planningDir(cwd) 解析)拉起 gsd-planner --gaps 生成带 gap_closure: true 与 gap_ids 的计划,供后续 verify-work 恢复时对账。

结语

fix-3381-init-verify-work-ws.md 是一条"小而完整"的修复:它把一个命令参数(--ws)通过工作流脚本的提取、SDK 查询的转发、GSD_WORKSTREAM 的作用域生效,最终传导到三个查询点(init.verify-work、phase.mvp-mode、roadmap.get-phase),彻底消除了"用户明明指定了工作流、SDK 却读根 .planning/"的静默错误。若你的项目使用 gsd-core 的多工作流模式并在工作流内执行阶段验证,请确认版本包含此修复(PR #3386),并在调用时始终显式传递 --ws <name>。

登录后查看全文
gsd-core