GSD 的 SDK-first 架构缝重构:规划路径投影、工作流清单、STATE.md 变换与 CJS 命令路由的收敛实战
本文基于 get-shit-done(GSD,面向 Claude Code / OpenCode / Gemini CLI / Codex CLI 的元提示词、上下文工程与规格驱动开发系统)仓库中 changeset 3312-sdk-first-architecture-seams.md(对应 PR #3316,“Refactor SDK-first architecture seams”)展开,逐条剖析该重构覆盖的四条“架构缝(seam)”:规划路径投影、工作流清单、STATE.md 进度投影以及 CJS 命令路由。读完后你将理解 GSD 如何用“单一来源纯投影 + 生成式 CJS 桥接 + 薄适配器”的模式消除 CJS 与 SDK 双实现之间的行为漂移(drift),并能在本仓库中定位每一处实现与对应的测试证据。
1. changeset 原文解读:一次“收口”式重构
changeset 文件本身极简,但信息密度很高:
type: Changed
pr: 3316
正文一句话概括了全部改动范围:
Tighten SDK-first architecture seams across planning path projection, workstream inventory, STATE.md transforms, and CJS command routing. Shared CJS/SDK helpers now reduce drift, and STATE.md progress projection preserves curated wider aggregates without hiding real disk-derived progress.
拆开看,它声明了三类事实:
- 重构覆盖四条缝:规划路径投影(planning path projection)、工作流清单(workstream inventory)、STATE.md 变换(transforms)、CJS 命令路由(command routing);
- 手段:引入 CJS 与 SDK 共享的 helper,压低两侧实现漂移;
- 关键行为语义:STATE.md 的进度投影要“保住人工策展的更宽聚合值(curated wider aggregates),同时又不掩盖磁盘扫描得到的真实进度”。
这条改动是 GSD “SDK-first”架构策略的具体落地。该策略的总体边界定义在 ADR 0005-sdk-architecture-seam-map.md 中:SDK 被显式建模为“带缝的模块组合 + 薄调用点适配器”,任何行为变化都应收敛到所有权的 seam 模块内,否则即视为设计违规。本次 changeset 涉及的两条缝另有专门 ADR:0004-worktree-workstream-seam-module.md(规划/工作树/工作流路径状态策略)与 0006-planning-path-projection-module.md(规划路径投影策略)。
2. 规划路径投影缝:planning-workspace.cjs 集中持有路径推导
changeset 说的“planning path projection”,在当前仓库中由 planning-workspace.cjs 独家持有。模块头注释直接声明了所有权边界:
/**
* Planning Workspace — .planning path resolution + active workstream routing.
*
* This module owns the planning workspace seam:
* - planningDir/planningRoot/planningPaths
* - active workstream pointer policy (session-scoped > shared)
* - pointer storage adapters (session/shared/memory)
*/
2.1 三个路径函数:唯一的 .planning 布局真相源
planningDir(cwd, ws, project)(L60-L77)负责把“工作目录 + 工作流 + 项目”三个维度投影成磁盘路径,并内置了两条防御规则:
- 未显式传参时回退读取环境变量
GSD_PROJECT/GSD_WORKSTREAM; - 用
BAD_SEGMENT = /[/\\]|\.\./拒绝项目/工作流名中的路径分隔符与..穿越分量,命中即抛GSD_PROJECT contains invalid path characters之类的错误。
planningPaths(cwd, ws)(L83-L94)则把目录一次性展开为固定键集合,供所有命令共用:
| 键 | 相对 .planning 的路径 |
说明 |
|---|---|---|
state |
STATE.md |
状态文档(frontmatter + 正文) |
roadmap |
ROADMAP.md |
阶段路线图 |
project |
PROJECT.md |
项目文档 |
config |
config.json |
工作流配置 |
phases |
phases/ |
阶段目录 |
requirements |
REQUIREMENTS.md |
需求文档 |
“路径投影”之所以重要,是因为 GSD 的 CJS 命令与 SDK 查询处理器过去都各自拼路径;收敛到这一处后,workstreams/<ws> 的布局(path.join(base, 'workstreams', ws))变成单点事实,双端不可能再算出不一致的目录。
2.2 active workstream 指针:session 优先于 shared 的适配器策略
工作流“当前激活”指针的存储被抽象成三类适配器(L169-L242):
- shared(
createSharedPointerAdapter):写入.planning/active-workstream,全终端共享; - session(
createSessionScopedPointerAdapter):依据 11 个会话环境变量(GSD_SESSION_KEY、CLAUDE_SESSION_ID、OPENCODE_SESSION_ID、CODEX_THREAD_ID、TMUX_PANE等)派生 sessionKey,落到os.tmpdir()/gsd-workstream-sessions/<sha1(planningRoot)>/<sessionKey>,并回退到控制 TTY token——这让并行的多个 AI 会话各自持有独立激活工作流,互不踩踏; - memory(
createMemoryPointerAdapter):纯内存值,用于测试注入。
选择逻辑在 pickActiveWorkstreamAdapter:只要探测到会话标识就走 session 适配器,否则回落 shared。读取侧还有两层自清洁:activeWorkstream.get() 会校验指针名字符合命名策略(workstream-name-policy.cjs)且 .planning/workstreams/<name> 目录真实存在,任一不满足即清空指针并返回 null。
同模块的 withPlanningLock(L248-L307)展示了“缝内细节”的典型深度:用 wx 标志原子创建 .lock,对 PLANNING_LOCK_RETRY_ERRNOS(EPERM/EBUSY/EAGAIN/EINTR/EINVAL/EIO/ENOENT/ESTALE——覆盖 Docker overlay-fs、NFS、Windows/macOS 杀软场景)做重试而非传播,超过 30 秒的陈旧锁会被直接摘除,整个超时窗口 10 秒。这些跨平台容错被封闭在缝内,调用方无感知。
值得一提的是模块尾部的 findContextMdIn(L391-L401):它把“CONTEXT.md 或 NN-CONTEXT.md 前缀形式”这一判据从 init.cjs、roadmap.cjs、core.cjs、gap-checker.cjs 等 5 个调用点的重复实现中提取为规范谓词(注释明确引用了 issue #3739)。这是 changeset 所述“Shared CJS/SDK helpers now reduce drift”的典型例证——去重本身就是收敛漂移的手段。
3. 工作流清单缝:纯投影 builder + 生成式 CJS,双端共享一份逻辑
这是四条缝中“共享 helper 降漂移”体现得最彻底的一条,分为三层。
3.1 CJS 侧:I/O 编排与投影彻底分离
workstream-inventory.cjs 的文件头注释写明了分工:
Pure projection logic lives in workstream-inventory-builder.generated.cjs. This module handles I/O orchestration only.
inspectWorkstream(L58-L87)只做三件事:读 phases/ 子目录、经 plan-scan.cjs 统计每个阶段的 plan/summary 文件数、从 STATE.md 抽取 Status / Current Phase / Last Activity 字段投影(readStateProjection),然后把这份“预收集的输入”交给纯函数 buildWorkstreamInventory。listWorkstreamInventories(L89-L116)则定义了 flat/workstream 两种运行模式:当 .planning/workstreams/ 不存在时返回 mode: 'flat' 与提示 No workstreams — operating in flat mode。命令处理器只消费这份 inventory,不再各自重扫目录——这正是 0004 ADR 要求的路径-状态策略集中化。
3.2 SDK 侧:唯一的逻辑真相源
纯投影逻辑的源头在 sdk/src/workstream-inventory/builder.ts。文件头注释强调 “No I/O. No async”——调用方负责收集 BuilderInputs,模块只做无状态变换。其输出类型是完整可校验的清单结构:
export interface WorkstreamInventory {
name: string;
path: string;
active: boolean;
files: { roadmap: boolean; state: boolean; requirements: boolean };
status: string;
current_phase: string | null;
last_activity: string | null;
phases: WorkstreamPhaseInventory[];
phase_count: number;
completed_phases: number;
roadmap_phase_count: number;
total_plans: number;
completed_plans: number;
progress_percent: number;
}
其中每个 WorkstreamPhaseInventory 携带 directory、status: 'complete' | 'in_progress' | 'pending'、plan_count、summary_count,即清单投影中阶段状态判定与进度百分比的唯一算法。
3.3 生成 + 新鲜度门禁:漂移在 CI 阶段即被拦截
SDK 是 TypeScript/ESM(sdk/package.json 中 type: module、构建产物 dist/index.js),而安装到各 AI 运行时下的 GSD 命令是 CJS。两侧共享同一逻辑的方式是:以 SDK 的 builder.ts 为源,生成 CJS 版本 workstream-inventory-builder.generated.cjs。
sdk/package.json 的 scripts 里成对出现:
"gen:workstream-inventory-builder": "npm run build && node scripts/gen-workstream-inventory-builder.mjs",
"check:workstream-inventory-builder-fresh": "npm run build && node scripts/check-workstream-inventory-builder-fresh.mjs"
gen:* 负责重新生成,check:* 负责断言生成物与源一致——一旦有人在 CJS 侧手改生成文件或忘记重新生成,门禁即失败。仓库根 package.json 通过 check:workstream-inventory-builder-fresh 把该检查挂到了主仓流程中,测试 workstream-inventory-builder-generator.test.cjs 进一步验证生成链路,planning-workspace.test.cjs 覆盖路径/指针/锁行为。这种“单一源 + 投影生成 + 新鲜度检查”的组合,就是 changeset 中 “Shared CJS/SDK helpers now reduce drift” 的机制性答案:漂移不再依赖人工同步,而是被编译期/CI 检查强制为零。
4. STATE.md 变换缝:保住策展聚合,又不吞掉磁盘真实进度
changeset 最后一句——“preserves curated wider aggregates without hiding real disk-derived progress”——对应 state.cjs(约 1940 行)中针对 issue #3242 Bug A 的两处关键变换。
4.1 readModifyWriteStateMd 的 resync 开关
STATE.md 的 frontmatter 含 progress.* 计数字段,由 syncStateFrontmatter 基于磁盘扫描重建。但“全量重建”会与“人工跨里程碑策展的聚合值”冲突(比如一个跨 3 个里程碑的总计划数,局部磁盘扫描只能看到更窄的子集)。解法在 readModifyWriteStateMd(L1034-L1058)的 resync 选项:
- 默认
resync: true:transform 后从磁盘整体重建 frontmatter; - 传入
resync: false(如state update单字段正文更新):变换前先对 frontmatter 做快照,syncStateFrontmatter运行后把快照中的progress块原样写回。注释明确写道“only restore keys that were present in the snapshot”——只恢复快照里已存在的键,因此syncStateFrontmatter合法派生的新字段(如 status、current_phase)不受影响。
写路径整体在 acquireStateLock / releaseStateLock 锁内完成,避免读改写交错。
4.2 state json 的读侧对称保护
cmdStateJson(L1081-L1087 附近)在输出 JSON 时始终“从正文 + 磁盘重建”,以防 SUMMARY 文件新增后 percent/completed_plans 陈旧(#1589);随后调用 shouldPreserveExistingProgress(existingFm.progress, built.progress) 做判定:只有当既有 frontmatter 的聚合值宽于(wider than)磁盘扫描结果时才保留策展值,否则让磁盘真实进度覆盖。这就是 changeset 语义的完整实现:“保住更宽的策展聚合”与“不掩盖磁盘真实进度”是一对互补规则,而非单向保留。
state update 侧同样区分了字段:仅当更新 Progress、Total Plans in Phase、Total Phases 这类会直接投影进 progress.* 的字段时才置 shouldResync = true 触发重建(L195-L212),其余正文字段更新走 resync: false 路径,不碰策展计数器。相关回归由 bug-3242-state-update-progress-trample.test.cjs 与 bug-2642-state-update-progress-trample.test.cjs 守护。
5. CJS 命令路由缝:共享 family 路由与后续演进
changeset 的第四条缝是“CJS command routing”。其共享 helper 是 cjs-command-router-adapter.cjs(全文件 39 行),导出单一函数 routeCjsCommandFamily:
function routeCjsCommandFamily({
args, subcommands, handlers, defaultSubcommand,
unsupported = {}, unknownMessage, error,
}) {
const subcommand = args[1] || defaultSubcommand;
if (subcommand && unsupported[subcommand]) { error(unsupported[subcommand]); return; }
const handler = subcommand ? handlers[subcommand] : null;
if (handler) { handler(); return; }
const available = subcommands.filter(s => !unsupported[s]);
error(unknownMessage(subcommand, available));
}
它把“取子命令 → 拒绝 unsupported → 分发 handler → 兜底 unknown 错误信息(附可用列表)”这一每个命令家族都要写一遍的样板,收敛为 CJS 各路由(phase/roadmap/state/validate/verify 等 *-command-router.cjs)共享的一个实现——错误信息的构造权仍交给调用方的 unknownMessage 回调,保持输出文案可控,只收敛控制流。
从源码结构看,该缝的演进并未止步于此:当前仓库已出现 command-routing-hub.cjs,以纯结果({ ok: true, data } | { ok: false, errorKind, message })方式进一步集中“SDK vs CJS 模式决策、错误分类法、no-throw 契约”,且定义了一个封闭的 ERROR_KINDS 枚举(UnknownCommand / InvalidArgs / HandlerRefusal / HandlerFailure / SdkLoadFailed / SdkDispatchFailed,L31-L42)。其头部注释列出的不变量值得注意:Hub 绝不向 stdout/stderr 打印、绝不 process.exit、绝不抛出、且 SDK 模式下不做 CJS 静默回退(SDK 崩溃即返回 SdkDispatchFailed)。这延续了 #3316 确立的方向:路由层的决策逻辑持续从各家族向中心收敛,同时保持结果可判定、错误分类可枚举。测试侧由 command-routing-hub.test.cjs、command-contract.test.cjs 以及 cjs-sdk-bridge-seam-contracts.test.cjs 等覆盖。
6. 如何在仓库中验证这套重构
结合 changeset 与源码,验证路径如下(均为只读查看,无需修改仓库):
- 路径投影单点化:查看 planning-workspace.cjs 的
planningDir/planningPaths与findContextMdIn,对照测试 planning-workspace.test.cjs; - 共享清单逻辑:从 SDK 源 sdk/src/workstream-inventory/builder.ts 出发,对照 CJS 侧 workstream-inventory.cjs 与生成物 workstream-inventory-builder.generated.cjs,运行
npm run check:workstream-inventory-builder-fresh(根 package.json)确认新鲜度; - STATE.md 投影语义:定位 state.cjs 中
readModifyWriteStateMd的resync分支与cmdStateJson的shouldPreserveExistingProgress调用,回归测试见 tests/bug-3242-state-update-progress-trample.test.cjs; - 路由收敛:通读 cjs-command-router-adapter.cjs 全文件,再比较 command-routing-hub.cjs 的不变量清单与 tests/command-routing-hub.test.cjs。
7. 适用前提与小结
需要说明的边界:本文以仓库当前 HEAD(1.50.0-canary.0)的实现为准;changeset #3316 本身只承诺“收紧四条缝、引入共享 helper、改进 STATE.md 进度投影语义”,而 command-routing-hub.cjs 等中心路由是后续提交引入的进一步演进,阅读时应区分“changeset 直接内容”与“仓库现状”。SDK 侧要求 Node >=22.0.0(见 sdk/package.json),CJS 命令在受支持运行时的安装布局下生效。
小结:这次“SDK-first 架构缝”重构的方法论可以概括为三步——路径与清单的推导集中到 seam 模块,纯投影逻辑以 SDK 为唯一源并经生成物下沉到 CJS 且以新鲜度检查锁死漂移,STATE.md 的写/读两侧用对称的保留规则调和“策展值”与“磁盘值”。对维护多实现(CJS + SDK)的系统而言,这套“单一源 + 投影生成 + 行为回归测试”的组合是比口头约定更有效的一致性工程。
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