GSD Core 帮助直通机制解析:让 `gsd-sdk query <subcommand> --help` 抵达 handler 的 CLI 修复与反幻觉不变式
GSD Core 帮助直通机制解析:让 gsd-sdk query <subcommand> --help 抵达 handler 的 CLI 修复与反幻觉不变式
gsd-core(Git. Ship. Done - Core)是一个以 CLI 为操作主轴的工程工作流引擎,其命令表面横跨 gsd-tools.cjs 调度器、各 IDE/终端宿主扩展与 SDK 查询层。本文围绕归档变更记录 .changeset/archived/help-passthrough.md(PR #3026 / issue #3019)展开,剖析"查询类子命令的 --help 无法到达 handler"这一经典 CLI 缺陷的两层修复方案,说明 gsd-tools.cjs 如何在渲染顶层 usage 的同时守住 #1818 确立的"反幻觉不变式"——绝不因一个帮助标志就执行破坏性命令。读完你将掌握:--help 在多层 argv 解析与短路线分发中的正确放行策略、顶层 usage 的结构化输出约定,以及如何在源码与测试中验证该类行为。
一、问题:--help 被"收割"后永远到不了 handler
原变更记录精确描述了缺陷的成因链路:
The query argv parser harvested
--helpas a global flag andmain()short-circuited dispatch — there was no path to discover what arguments a query subcommand accepts.
也就是说,当用户(或 Agent)执行 gsd-sdk query <subcommand> --help 时:
- query argv 解析器把
--help当作"全局标志"提前收割,将其从子命令参数中剥离; main()随后发现 argv 中存在帮助标志便短路整个分发流程,直接打印顶层 usage 并退出;- 结果是:
query下辖子命令各自真正接受的参数(--tag、--older-than、--phase等)根本没有机会被 handler 暴露出来,用户也无从发现。
这不仅是一个可用性缺陷,还带出了两个隐藏问题:
- 子命令参数不可发现:顶层 usage 只枚举命令名,不包含每个子命令的参数要求;真正可靠的发现方式是"不带参数调用该命令,让错误信息列出必填参数"。
- Agent 幻觉放大效应:GSD 的 CLI 面向大量 AI Agent 调用场景,Agent 一旦"幻觉"出一个不存在的 flag,旧行为只会返回一个同样无用的报错,无法引导其回到正确的命令表面。
在当时的实现里,SDK 查询层(sdk/src/cli.ts)与 gsd-tools.cjs 回退路径各管一段,--help 的"短路"发生在 main() 中,早于任何 handler/fallback 的渲染机会——这就是"没有路径去发现 query 子命令参数"的技术本质。
二、两层修复设计:放行 + 兜底渲染
修复被明确拆成两层,每一层解决链路中的一段:
第一层:parser 放行 --help 进入 queryArgv
原记录写道:
The parser now leaves
--helpinqueryArgvso the handler/fallback can render contextual help.
关键改动是:当 argv 中还存在可分发的子命令时,解析器不再把 --help 当作全局标志提前剥离,而是原样保留在 queryArgv 里,让它随参数一路传递到 handler 或 fallback,由真正了解该子命令上下文的那一层来渲染帮助。
这一层的配套约束(见 tests/dispatcher.test.cjs 的回归测试注释)是:仅当没有可分发的子命令时,才响应全局帮助标志。换言之,帮助标志的优先级被压低了一级——子命令的存在优先于全局 flag 的收割。
第二层:gsd-tools.cjs fallback 渲染顶层 usage
原记录继续写道:
The
gsd-tools.cjsfallback now renders top-level usage on--help(instead of erroring), preserving #1818's anti-hallucination invariant by NOT executing the destructive command.
在 gsd-tools.cjs 的调度器 gsd-core/bin/gsd-tools.cjs 中,这一层的实现清晰可见:
// #3019: a `--help` / `-h` flag in argv must render the top-level usage
// and exit 0 — not error out with "Unknown flag". The previous shape
// erred on agent-hallucinated flags, but it also blocked humans from
// discovering the command surface via subcommand help requests routed
// through this dispatcher. Rendering top-level usage on --help is strictly
// better UX than the old short-circuit that printed unrelated usage text.
const HELP_FLAGS = new Set(['-h', '--help', '-?', '--h', '--usage']);
if (args.some((a) => HELP_FLAGS.has(a))) {
process.stdout.write(TOP_LEVEL_USAGE + '\n');
return;
}
行为从"遇到 --help 报 Unknown flag --help 并以非零退出"改为"渲染顶层 usage 并以 0 退出"。与修复前相比,这同时满足了两件事:
- 可发现性(#3019):任何路由到该调度器的子命令帮助请求,都能得到一份完整、结构化的命令清单;
- 安全性(#1818):
--help只触发 usage 渲染,绝不进入实际命令的执行分支。
被渲染的 TOP_LEVEL_USAGE(定义于 gsd-core/bin/gsd-tools.cjs)包含三个固定段落:
Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>]
[--project-dir <path>] [--ws <name>] [--json-errors] [--exit-contract=<v>]
Commands: agent, agent-skills, assumption-delta, ..., worktree
Global flags:
--raw Emit raw output without post-processing
--pick <field> Extract a single field from JSON output (dot/bracket notation)
--cwd <path> Override working directory for project-root resolution
--project-dir <path> Explicit project root; skips the ancestor walk-up entirely
--ws <name> Override active workstream (or set GSD_WORKSTREAM)
--json-errors Emit structured JSON error objects on stderr (or set GSD_JSON_ERRORS=1)
--exit-contract=<v> Exit-code contract version: v1 (default) or v2 (or set GSD_EXIT_CONTRACT)
For command-specific argument requirements, invoke the command without args
(e.g. `gsd-tools phase add`) — the resulting error lists what is required.
注意最后一段"发现提示":它明确告诉调用者,想要某个子命令的参数清单,就"不带参数调用"该命令,错误信息会列出必填项。这正好补上了顶层 usage 不枚举子命令参数的空档,是 #3019 修复刻意保留的发现路径。
三、反幻觉不变式:#1818 的延续
#1818 是这次修复必须守护的既有约束。其原始语义记录在 tests/command-routing-hub.test.cjs 的测试注释中:
Original #1818 invariant: gsd-tools must NOT silently ignore --help/-h and proceed with a destructive command — that turned AI-agent hallucinations into accidental data loss (e.g.
phases clear --helpdeleting phase dirs because the flag was dropped).
也就是说,历史上存在过一个极端危险的行为:--help 被静默丢弃后,命令继续执行——phases clear --help 这种"看起来只是在问帮助"的调用,实际可能把 phase 目录全部删除,AI Agent 的幻觉由此演变成真实数据丢失。
#3019 的修复在改变"响应形态"(从报错退出改为渲染 usage 后正常退出)的同时,保留了不变式的内核。测试对两者同时断言(tests/command-routing-hub.test.cjs):
- 破坏性命令未执行:以
phases clear --help为例,测试先创建哨兵 phase 目录与PLAN.md,调用后断言目录与文件依然存在——clear确实没有运行; - 输出为顶层 usage:
generate-slug hello --help的输出必须不同于正常调用产生的 slug,证明generate-slug未执行;phase complete --help、state load --help同理。
与 --help 形成鲜明对照的是 --version:gsd-core/bin/gsd-tools.cjs 中 --version/-v 被放入 NEVER_VALID_FLAGS,遇到即报 Unknown flag 并给出引导。注释点明了区别对待的理由:帮助标志有真实的发现用途,而版本标志在 Agent 调用中纯属幻觉高发区,静默忽略它可能让破坏性操作在无提示下继续,因此版本标志永远不合法。
四、当前仓库中的落地形态:query 前缀与 SDK 退役
query 作为元前缀被调度器直接接受
本仓库中,query 前缀的处理已经收敛进 gsd-tools.cjs 的 main()(gsd-core/bin/gsd-tools.cjs):
let command = args[0];
// Accept `query` as a meta-prefix for canonical dotted/spaced commands.
// Workflows may call `node gsd-tools.cjs query <command>` directly.
if (command === 'query') {
args.shift();
command = args[0];
}
当调用 node gsd-tools.cjs query phase --help 时,query 被剥离,command 变为 phase,而 --help 仍留在 args 中,随后命中 HELP_FLAGS 分支 渲染顶层 usage——这正是回归测试 gsd-sdk query phase --help 所验证的 fallback 路径。此外,调度器还接受点分规范形式(如 state.update,见 gsd-core/bin/gsd-tools.cjs),仅按第一个点拆分并重组 args,保证 query 前缀、点分形式、空格形式三者行为一致。
sdk/ 已退役,查询入口统一收口
需要说明的是,当前仓库中独立的 sdk/ 源码树已按 .changeset/archived/191-retire-sdk-package-seam.md 退役:gsd-sdk shim/bin 发布路径被删除,共享清单移至 get-shit-done/bin/shared 以兼容 runtime/install。配套地,.changeset/archived/195-workflow-gsd-tools-query.md 记录了工作流 Markdown 中所有 gsd-sdk query 引用被替换为 gsd-tools.cjs query 的迁移。因此,在本文所述修复的当下语境里,"SDK 查询层"的职责实际由 gsd-tools.cjs 的 query 元前缀 + 统一调度器承担,--help 的放行逻辑也顺理成章地落到该调度器的 HELP_FLAGS 分支上。
历史回归测试对此保留了端到端验证(tests/dispatcher.test.cjs):当构建产物 sdk/dist/cli.js 存在时,测试会真实 spawn gsd-sdk query phase --help,断言三件事——exit code 为 0、stdout 含 Usage: gsd-tools 与 Commands:、stderr 不含 Unexpected token 或 not valid JSON。最后一条对应 #3026 的衍生缺陷:SDK fallback 曾对纯文本帮助输出做 JSON.parse,导致 Unexpected token 'U' 崩溃;产物缺席时测试以 t.skip 显式标注跳过,而非静默通过。
宿主扩展复用同一帮助表面
pi/gsd.cjs(pi.dev 宿主扩展,见 pi/gsd.cjs)体现了 GSD 跨宿主的一致策略:命令分发采用 SUBPROCESS-REUSE 模式,即把命令转发给 gsd-tools.cjs 作为子进程执行,而不是在扩展进程内重建命令路由中枢。pi/gsd.cjs 维护的 PI_COMMAND_FAMILIES(pi/gsd.cjs)是一份与 gsd-tools.cjs 顶层命令族对齐的手工精选清单,用于前缀过滤与提示;真正渲染帮助、解析参数的工作始终收敛于同一调度器。这意味着:无论从 gsd-tools.cjs 直接调用、经 query 前缀调用,还是经 pi 扩展间接调用,--help 的响应形态都保持一致,不会因宿主不同而出现"某个入口能查帮助、另一个入口报错"的分裂。
五、可复现的验证路径
以下命令可直接在当前仓库中验证 #3019 修复后的行为(在仓库根目录执行):
# 1. 顶层 --help:渲染 usage,exit 0
node gsd-core/bin/gsd-tools.cjs --help
# 2. -h 短形式:同样渲染 usage,exit 0
node gsd-core/bin/gsd-tools.cjs -h
# 3. 子命令位置携带 --help:渲染 usage,绝不执行子命令
node gsd-core/bin/gsd-tools.cjs phase add --help
node gsd-core/bin/gsd-tools.cjs state load --help
# 4. query 元前缀 + 子命令 + --help(SDK 时代的经典调用形态)
node gsd-core/bin/gsd-tools.cjs query phase --help
# 5. 反例:--version 仍被拒绝
node gsd-core/bin/gsd-tools.cjs --version
对应自动化断言集中在两处:
- tests/dispatcher.test.cjs(原
tests/bug-3019-help-passthrough.test.cjs,已并入 consolidation epic):覆盖无参调用、--help、-h、<subcommand> --help四种形态,并结构化检查 usage 的Usage:/Commands:/ 参数发现提示三个段落; - tests/command-routing-hub.test.cjs:以哨兵文件证明
phases clear --help、generate-slug hello --help、phase complete --help、state load --help均不进入执行分支。
六、边界与同类标志的处理
围绕 --help 的放行,调度器的其他 argv 处理保证了行为的一致性与安全性:
run-with-timeout拦截前置(gsd-core/bin/gsd-tools.cjs):query前缀被剥离后才检查run-with-timeout,因为被包装命令的 argv 不透明,可能自带--raw/--cwd/--pick,拦截必须早于这些全局 flag 的解析;--pick与--raw仍为全局 flag(gsd-core/bin/gsd-tools.cjs):--pick <field>在命令执行后捕获 stdout 并做 JSON 字段提取,缺失字段按"无法回答"处理而非降级为空答案;--raw则跳过后处理;SKIP_ROOT_RESOLUTION白名单(gsd-core/bin/gsd-tools.cjs):generate-slug、current-timestamp、user-story、runtime-identity等纯工具命令跳过.planning/项目根解析,保证帮助/纯计算类调用不依赖工程目录上下文。
这些机制共同说明:帮助直通并不是"看到 --help 就放行一切",而是在保持破坏性命令永不因帮助标志而执行的前提下,为命令表面的发现路径让路。
七、设计要点小结
从 .changeset/archived/help-passthrough.md 的这次修复中,可以提炼出几条对 CLI 设计具有普适意义的结论:
- 帮助标志的优先级应当低于"待分发命令"的存在性——先判定有无可分发的子命令,再决定
--help是就地响应还是随 argv 传递; - 兜底渲染优于报错——当某一层无法提供上下文帮助时,渲染一份结构化的顶层 usage(含参数发现提示)远好于
Unknown flag式的死胡同; - 反幻觉不变式是硬约束——任何"提升帮助可用性"的改动,都不得让破坏性命令在帮助标志下获得执行机会,且必须用哨兵文件类测试显式断言"未执行";
- 帮助表面需要跨入口收敛——SDK 查询层、
query元前缀、宿主扩展最终都汇入同一个调度器,帮助行为才不会因调用路径不同而漂移。
对于需要阅读完整实现的读者,建议依次对照:调度器与 TOP_LEVEL_USAGE、HELP_FLAGS 与 NEVER_VALID_FLAGS 分支、#3019 回归测试、#1818 不变式测试、pi 宿主扩展的分发策略,即可完整还原本次修复的因果链与落点。