get-shit-done SDK:修复 GSDTools 原生查询丢失 workstream 路由(3591)解析
本文基于当前仓库的 changeset 3591-gsdtools-native-workstream.md 展开,剖析 get-shit-done(GSDE,GSD)SDK 中一个隐蔽的路由缺陷:createGSDToolsRuntime 接受 workstream 选项,但其传递给 QueryNativeDirectAdapter 的 dispatch 闭包曾静默丢弃该参数,导致原生(native)查询处理器即使实例配置了 workstream,也始终命中根 .planning/ 目录树。读懂本篇后,你将掌握 GSDTools 运行时桥接的双通道分发架构(native 直连 vs 子进程回退)、workstream 在两条通道中各自的传递机制,以及该修复如何通过构造缝隙(constructor seam)间谍测试验证。
问题背景:一个被静默丢弃的参数
GSD 的 SDK 查询层支持「workstream(工作流)」概念:当 GSDTools 实例以 workstream 创建时,所有 planning 路径类查询应路由到 .planning/workstreams/<name>/ 而非项目根部的 .planning/。这对应仓库中 ADR 记录的两条 seam:worktree workstream seam 模块 与 planning-path projection 模块。
缺陷的形态非常典型——「参数在签名中存在,却在某条代码路径上丢失」:
createGSDToolsRuntime的选项对象包含workstream?: string;- 子进程通道(
QuerySubprocessAdapter)一直正确转发它(以--ws <name>命令行参数注入); - 而 native 通道交给
QueryNativeDirectAdapter的闭包调用的是registry.dispatch(command, args, projectDir),第四个参数workstream被丢掉。
后果是:只要查询走 native 直连路径(由 shouldUseNativeQuery() 决定),无论 workstream 是否设置,planning 查询都落在根 .planning/ 树上——数据读错位置且不报任何错,属于「静默错误路由」,这也是此类缺陷最危险的地方。
运行时装配:createGSDToolsRuntime 的四条腿
修复后的完整装配逻辑见 sdk/src/query-gsd-tools-runtime.ts。该工厂函数接收如下选项:
export function createGSDToolsRuntime(opts: {
projectDir: string;
gsdToolsPath: string;
timeoutMs: number;
workstream?: string;
eventStream?: GSDEventStream;
sessionId?: string;
shouldUseNativeQuery: () => boolean;
execJsonFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<unknown>;
execRawFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<string>;
strictSdk?: boolean;
allowFallbackToSubprocess?: boolean;
onDispatchEvent?: RuntimeBridgeOptions['onDispatchEvent'];
}): GSDToolsRuntime
返回一个包含 bridge: QueryRuntimeBridge 的运行时对象。内部装配分四个部分:
- Registry:
createRegistry(opts.eventStream, opts.sessionId)创建查询命令注册表,所有 planning/state/workspace 查询处理器都注册在此(见 sdk/src/query/registry.ts)。 - 子进程适配器(sdk/src/query-subprocess-adapter.ts):用
execFile以cwd: projectDir执行gsdToolsPath指向的 CLI,workstream在此以 CLI 参数形式注入。 - native 直连适配器(sdk/src/query-native-direct-adapter.ts):进程内直接调用 registry dispatch,带超时策略。
- 传输与策略层:
GSDTransport+QueryExecutionPolicy+QueryNativeHotpathAdapter最终汇入QueryRuntimeBridge,对外暴露dispatchHotpath等统一入口。
修复核心:闭包补齐第 4 个参数
修复只有一行代码,但附带了完整的问题注释,位于 sdk/src/query-gsd-tools-runtime.ts#L44-L54:
const nativeDirectAdapter = new QueryNativeDirectAdapter({
timeoutMs: opts.timeoutMs,
// #3591: forward opts.workstream to the registry so native dispatch
// routes planning-path queries to .planning/workstreams/<name>/
// instead of the root .planning tree. createGSDToolsRuntime accepts
// workstream and the QuerySubprocessAdapter already forwards it
// (line 38); the native dispatch closure was the only seam that
// dropped it, silently routing GSDTools-native queries to root.
dispatch: (registryCommand, registryArgs) =>
registry.dispatch(registryCommand, registryArgs, opts.projectDir, opts.workstream),
...nativeErrorFactory,
});
registry 侧的签名与第 4 个可选参数一致,见 sdk/src/query/registry.ts#L118:
async dispatch(command: string, args: string[], projectDir: string, workstream?: string): Promise<QueryResult>
对比两条通道的 workstream 传递机制
| 通道 | 传递方式 | 证据位置 |
|---|---|---|
| 子进程 | 拼进 CLI 参数:workstream ? ['--ws', name] : [] |
query-subprocess-adapter.ts#L91-L94 |
| native 直连 | 作为 registry.dispatch 的第 4 个实参 |
query-gsd-tools-runtime.ts#L52 |
子进程通道的细节值得注意:commandArgs 将 --ws <name> 追加在所有用户参数之后([gsdToolsPath, command, ...args, ...wsArgs]),这样 CLI 端解析 workstream 标志时不会被位置参数干扰。子进程适配器还处理了 @file: 间接寻址(stdout 输出 @file:<path> 时读文件取真实 JSON)、10MB maxBuffer、ETIMEDOUT 与 killed 的超时分类,这些与 workstream 正交,但体现了该适配器的健壮性设计(query-subprocess-adapter.ts#L125-L145)。
native 通道侧,QueryNativeDirectAdapter 本身对 workstream 无感知——它只知道 dispatch: (command, args) => Promise<QueryResult> 这个闭包,workstream 的语义完全由外层闭包捕获(闭包变量 opts.workstream)。这种「适配器窄接口 + 闭包捕获上下文」的设计,正是本次 bug 能潜伏的原因:类型层面 dispatch 回调只声明两个参数,第 4 个参数的缺失不产生任何编译错误。
workstream 如何在 registry 内生效
registry 收到 workstream 后,planning 路径类查询(state、workspace 等)据此把基准目录从 .planning/ 切换到 .planning/workstreams/<name>/。这一投影逻辑分散在查询模块的 helpers 与 workstream 相关文件中,例如 sdk/src/query/helpers.ts 与 sdk/src/query/workstream.ts。从源码结构看,修复前 native 通道传入的 workstream 恒为 undefined,所有走该通道的 planning 查询因此退化为根树投影;修复后与子进程通道行为对齐。相关的行为约束还有独立的 changeset 3589-planning-paths-workstream-validation.md 与 ADR 0006-planning-path-projection-module.md。
测试如何钉死这个回归
配套测试 sdk/src/bug-3591-gsdtools-runtime-workstream.test.ts 用三层结构覆盖了该修复:
- 闭包转发(主断言):通过
vi.spyOn替换QueryNativeDirectAdapter构造函数,捕获传入的dispatch闭包;同时 mockcreateRegistry返回带dispatchspy 的真实 registry。调用捕获的闭包后断言:
expect(dispatchSpy).toHaveBeenCalledWith(
'__bug-3591-unknown-cmd__',
['x'],
'/tmp/3591-proj',
'frontend-ws',
);
即 opts.workstream === 'frontend-ws' 必须原样成为 registry.dispatch 的第 4 个实参。
- 向后兼容:省略
workstream选项时,第 4 个槽位必须显式传undefined而非省略调用形式本身,保证签名契约稳定:
expect(dispatchSpy).toHaveBeenCalledWith(
'__bug-3591-unknown-cmd-2__', [], '/tmp/3591-proj', undefined,
);
- 端到端:在真实 registry 上注册一个探针处理器
__bug-3591-probe__,经runtime.bridge.dispatchHotpath(...)走完整的 hotpath 分发,断言处理器第三参收到'frontend-ws':
expect(seen[0]?.workstream).toBe('frontend-ws');
文件头部的注释完整记录了 bug 成因与修复手法,是阅读该仓库回归测试模式的典型样本——「捕获构造缝隙里的回调 → 用真实 registry 的 spy 记录实参 → 断言参数四元组」。
适用前提与边界
- 该修复适用于当前仓库 SDK(TypeScript 源码位于 sdk/,测试以 vitest 运行,见 vitest.config.ts);changeset 类型为
Fixed,关联 issue 3591(changeset 目录约定见 .changeset/README.md)。 workstream是可选参数:不设置时行为与根.planning/树完全一致,旧调用方不受影响。- 注意两条通道的等价性依赖
shouldUseNativeQuery()的判定:native 走进程内闭包,子进程走--wsCLI 参数,二者最终都应落到同一个 workstream 投影结果。若未来扩展 registry dispatch 签名,此闭包与 bug-3591 测试 中的四元组断言需同步更新。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00