gsd-core 修复解读:3381 修复 —— /gsd-verify-work --ws 通过 SDK 解析工作流阶段
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/.
拆解这句话可以得到三个事实:
- 修复对象是命令
/gsd-verify-work --ws <name>,即在指定工作流(workstream)中对某个阶段执行验证; - 修复方式是"通过 SDK 解析工作流阶段"——即由 gsd-core 的查询命令(
query init.verify-work等)承担阶段解析,而不是让工作流脚本自行猜测路径; - 修复涉及三个查询点:
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 查询消费
修复前的缺陷链条如下:
- 用户在
/gsd-verify-work --ws <name>中显式指定了工作流; - 但工作流脚本(
gsd-core/workflows/verify-work.md)没有从$ARGUMENTS中提取--ws并转发给下游 SDK 查询; - 于是
GSD_WORKSTREAM从未被设置,planningDir(cwd)继续解析到根.planning/; - 结果是:
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');
这份断言同时锁死了三类东西:
- 解析逻辑:
GSD_WS初始化、--ws检测与提取、PHASE_ARG的派生(sed删除--ws <name>对); - 转发对象:三个工作流敏感查询必须无一遗漏地收到
GSD_WS; - 语义承诺:注释明确写明了每条断言的动机——
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>。