gsd-core init.* 阶段查询支持 `--phase` 参数:让 `gsd-sdk query init.*` 不再把 flag 当成阶段名

原创2026-09-25 19:24:49572 阅读

gsd-core init.* 阶段查询支持 --phase 参数:让 gsd-sdk query init.* 不再把 flag 当成阶段名

本篇技术指南聚焦 gsd-core 中 gsd-sdk query init.*(即 init.* 命令族)的阶段性查询参数修复:从 changeSet sdk-init-phase-flags.md 记录的 PR #3389 修复出发,深入解析 --phase <N> 与 --phase=N 两种 flag 形式的底层实现(normalizePhaseAlias)、受影响子命令范围、严格参数校验(ADR-3473)语境,以及 init.test.cjs 中 #3865 测试组的验证方式。读完本文,你将掌握在 init.execute-phase、init.plan-phase、init.verify-work、init.code-review 等查询命令中正确使用 --phase 参数,并理解其与位置参数形式的等价关系与边界行为。

一、背景:flag 形式查询曾把 --phase 当作字面阶段名

归档的 changeSet 记录了一次典型的"参数歧义"缺陷修复:

---
type: Fixed
pr: 3389
---
**`gsd-sdk query init.*` phase-scoped handlers now accept `--phase <N>` and `--phase=N`**
- flag-style init queries no longer search for a phase literally named `--phase`.

在修复之前,以 flag 形式调用阶段作用域的 init 查询(例如 gsd-tools query init.execute-phase --phase 03),底层会把 --phase 当作位置参数位上的阶段名去搜索——也就是说,它不是在"读取 --phase 的值 03",而是在磁盘上查找一个名字就叫 --phase 的阶段目录。其后果是静默的:查询不会报错,而是返回 phase_found: false、plan_count: 0。源码注释中记录了一起真实事故——某阶段磁盘上明明有 7 份计划,查询结果却是 0("the reported incident: 7 plans read as 0",见 init-command-router.cts 第 133-135 行附近注释)。

这一缺陷在源码与测试中以 issue #3865 追踪(changeSet 以 PR #3389 记录)。它之所以危险,正是因为"静默错误":工作流层(如 execute-phase.md、plan-phase.md)拿到 phase_found:false 后可能走错误分支,而用户从 JSON 输出上很难第一时间察觉查询参数没有生效。

二、核心修复:normalizePhaseAlias 统一入口

修复的实现位于 src/init-command-router.cts 的 normalizePhaseAlias 函数(第 82-112 行)。该路由器是 manifest-backed 的 init 子命令路由器:所有 init.* 子命令都有 SDK 等价物,并通过 executeForCjs(同步桥)派发,CJS 回退仅在 GSD_WORKSTREAM 激活或 SDK 构建缺失时保留。

normalizePhaseAlias 的核心思想是:把 flag 形式规约(normalize)成位置参数形式,再放入"调用方自己读取的 args[2] 槽位",从而使下游 handler 和严格 flag 校验看到的 argv 与位置参数形式完全一致。函数对输入分四种情况处理:

输入(args[2] 位置) 处理结果 说明
--phase <N> 将 args[2], args[3] 折叠为单个值 <N> 经典双 token 形式,等价于位置参数 <N>
--phase=<N> 截取 = 后的值替换 args[2] 单 token 内联值形式
--phase(无值) usage error,直接报错退出 明确提示 --phase requires a value: use --phase <N> (or the positional form <N>),绝不静默
其他以 -- 开头的 flag 规约为 undefined 等价于"未给位置参数"的输入(见下节)
普通 token 原样作为阶段名 位置参数形式,行为不变

其中两个关键细节值得注意:

  1. 无值 --phase 是硬错误而非软失败:函数在报错后还会 throw,作为 fail-closed 的兜底(第 91-92 行、第 103-104 行注释),确保即使 error() 回调异常返回,也不会继续执行后面的 splice 逻辑把坏值写进 argv。
  2. 其他 flag 形状的 args[2] 会规约为 undefined:例如 init execute-phase --tdd 这种写法,--tdd 不再被当作阶段名传给下游,而是走"无位置参数"路径——execute-phase/plan-phase/verify-work 会给出 "phase required" 用法错误,基于 find 的查询返回 phase_found:false,todos 则丢弃区域过滤。

isFlagToken 谓词(command-arg-projection.cts 第 114-116 行,tok.startsWith('--'))在这里被复用为 flag 形状判定,与严格参数解析器保持同一个判定标准。

三、受影响的 init.* 阶段作用域子命令

在 command-aliases.cts 的 INIT_COMMAND_ALIASES(第 276-492 行区域)中,INIT_SUBCOMMANDS 共登记了 27 个 init 子命令。其中读取 args[2] 位置槽位作为阶段 token 的"阶段作用域查询",都通过 normalizePhaseAlias 获得 --phase 支持:

子命令 路由处理(见 init-command-router.cts) 额外 flag
execute-phase normalizePhaseAlias + 严格解析(第 136-149 行) --validate、--tdd(布尔)、--wave [value]
plan-phase normalizePhaseAlias + 严格解析(第 150-171 行) --granularity、--prd、--ingest、--research-phase(值)、--validate、--tdd、--reviews、--chunked(布尔)
verify-work normalizePhaseAlias + 仅位置校验(第 233-237 行) 无(--ws 在到达此路由器前已被上层剥离)
phase-op normalizePhaseAlias + 仅位置校验(第 238-242 行) 无
code-review normalizePhaseAlias + 严格解析(第 243-247 行) --fix(布尔)
review normalizePhaseAlias + 仅位置校验(第 248-252 行) 无
discuss-phase-assumptions normalizePhaseAlias + 严格解析(第 253-257 行) --auto(布尔)
todos normalizePhaseAlias + 仅位置校验(第 258-262 行) 无

其中 execute-phase 路由还额外说明:--wave 属于 optionalValueFlags(值归属于 workflow 层消费,CLI 层只关心 token 出现与否),这是 ADR-3473 Bucket-A 修正后的独立机制,与 --phase 的规约逻辑互不干扰。

这一设计也与 phase list-plans 保持一致——后者本来就同时接受位置参数与 --phase 两种形式(见 init-command-router.cts 第 128-130 行注释)。修复的实质是把这一"双形式等价"契约推广到整个 init 命令族。

四、为什么必须走"规约"而非"直读":严格参数校验语境

修复采用"先规约再解析"而非"直接读取 --phase 值",根因在于 gsd-core 在 ADR-3473 §8.4("failure is a value") 之后推行严格参数校验:parseNamedArgs 不再宽容地接受未声明的 flag 或游离位置参数,而是返回带类型的 Result;parseNamedArgsOrExit(command-arg-projection.cts 第 231-242 行)在失败时调用 error() 并以 ERROR_REASON.USAGE 退出。

如果直接在 handler 里读取 --phase 的值而不做规约,就会遇到两个问题:

  1. 裸 --phase 60 留下的 60 会成为"被拒绝的游离位置参数"——严格校验会直接报 usage error,而不是按用户意图解析阶段号;
  2. 若绕过校验(旧版宽容解析路径),则会回到"把 --phase 当阶段名"的静默错误——这正是 pre-#3884 时期的问题:--phase 01-stub 会静默传下去当阶段名搜索(见 adr857-core-without-capabilities.test.cjs 第 285-288 行注释)。

因此 normalizePhaseAlias 在解析器之前完成规约:把 --phase <N> / --phase=N 折叠成调用方槽位 args[2] 中的位置值,让后面的 parseNamedArgsOrExit 看到与位置参数形式逐字节相同的 argv。这样既保留了严格校验的 fail-closed 语义,又实现了 flag 形式与位置形式的完全等价。

五、测试验证:#3865 测试组的完整覆盖

修复配套的回归测试位于 tests/init.test.cjs 第 924-986 行,以 #3865 为分组标记,覆盖六个场景:

测试 命令 断言
--phase <N> 形式 init execute-phase --phase 03 phase_found === true 且 plan_count === 1(直指"7 plans read as 0"事故)
--phase=N 形式 init execute-phase --phase=03 phase_found === true
位置参数控制组 init execute-phase 03 phase_found === true(确认旧形式未回归)
plan-phase init plan-phase --phase 03 phase_found === true
verify-work init verify-work --phase 03 phase_found === true
code-review init code-review --phase 03 phase_found === true
无值 --phase init execute-phase --phase success === false(非零退出),且诊断消息必须包含 --phase 字样

最后一条尤为重要:它锁定了"无值的 --phase 必须报错,绝不静默返回 phase_found:false"这一行为契约——即用户少传了值,也要得到明确的用法诊断,而不是一个看似成功实则空结果的 JSON 输出。

六、实操示例:在 SDK 查询与工作流中的用法

修复后,gsd-sdk query init.* 阶段作用域查询的三种合法写法等价:

# 形式一:双 token flag(本次修复新增)
gsd-tools query init.execute-phase --phase 03

# 形式二:单 token 内联值(本次修复新增)
gsd-tools query init.execute-phase --phase=03

# 形式三:位置参数(原有行为,保持不变)
gsd-tools query init.execute-phase 03

同样的规则适用于 init.plan-phase、init.verify-work、init.code-review、init.review、init.phase-op、init.discuss-phase-assumptions、init.todos。

实际工作流中,这些查询常以 --pick 形式被上层使用。例如 resolve-verify-command-path-findings.md 第 33 行展示了从前一阶段继承已验证命令的惯用法:

gsd-tools query init.plan-phase N --pick prior_verify_commands

此时 N 即阶段号,可安全替换为 --phase N 或 --phase=N 形式,行为完全一致——这使脚本化调用(尤其是阶段号来自变量、需要避免位置参数拼接歧义的场景)更加健壮。

七、小结

  • 问题本质:flag 形式的 init 阶段查询曾把字面 --phase 当作阶段名搜索,静默返回 phase_found:false / plan_count:0,曾造成"磁盘 7 份计划被读成 0"的事故。
  • 修复方案:init-command-router.cts 的 normalizePhaseAlias 在严格解析前把 --phase <N> / --phase=N 规约到位置槽位 args[2],使两种形式与位置参数形式严格等价;无值 --phase 报错退出(fail-closed)。
  • 适用范围:8 个阶段作用域 init 子命令全部生效,与 phase list-plans 的双形式契约对齐。
  • 验证保障:init.test.cjs 的 #3865 测试组覆盖两种新形式、位置参数控制组、3 个附加子命令以及无值报错分支,防止回归到"静默空结果"。

如果希望深入底层,可继续阅读 init-command-router.cts(路由器与规约函数)、command-arg-projection.cts(严格参数解析与 isFlagToken)、command-aliases.cts(init 子命令登记表),以及 init.cts(cmdInitExecutePhase、cmdInitPlanPhase、cmdInitVerifyWork 等下游实现)。

登录后查看全文
gsd-core