get-shit-done 硬接缝工程实践:PR 3577 出界模块清理、配置校验移植与单一默认值源落地解析
导读:本文围绕 get-shit-done 仓库中
.changeset/3577-adr-violations-and-validation-port.md这份「Fixed」类型变更记录展开,深度还原一次发生在 CJS 工具层(gsd-tools)与 TypeScript SDK(gsd-sdk)硬接缝(Hard Seam)边界上的系统性修复。你将看到:一个因违反 ADR 出界(out-of-seam)清单而在 64 GiB Docker 主机上 fork 出数百个 node 进程的无限递归事故是如何被定位与根治的;config-ensure-section如何通过「目录重绑定」而非 CJS 回退来恢复旧契约;configNewProject默认值为何必须取自规范化 Manifest;以及configSet的枚举/形状校验、init 处理器--tdd、STATE.md 自动修剪、GSDError 语义等一批 SDK 侧行为为何需要与 CJS 逐项对齐。
背景:ADR-3524 与「CJS↔SDK 硬接缝」的单一来源政策
get-shit-done(gsd)采用双运行时架构:
- CJS 工具层:
get-shit-done/bin/lib/*.cjs与bin/gsd-tools.cjs,命令路由、同步 fs/exec; - SDK 层:
sdk/src/**/*.ts,异步 I/O、gsd-sdk query <canonical>统一查询入口。
为了让两个运行时共享的每个模块都“只有一个手写真相源”,仓库用 ADR 记录了决策 docs/adr/3524-cjs-sdk-hard-seam.md,其配套 PRD 在 docs/prd/3524-cjs-sdk-hard-seam.md。核心规则(ADR §1)是:
- 每个 Shared Module 有恰一个手写真相源(行为模块在
sdk/src/<module>/的 TypeScript,纯数据模块在sdk/shared/<module>.manifest.json); - CJS 侧只允许机械生成的产物(
bin/lib/<module>.generated.cjs),绝不手工编辑; - 每个模块配 freshness 检查(
sdk/scripts/check-<module>-fresh.mjs)与漂移 lint; - 手写成对复制(hand-synced pair)被禁止,由 scripts/lint-shared-module-handsync.cjs 在 PR 阶段拦截。
更重要的是 ADR §3「出界模块(Out-of-seam Modules)」:部分模块按决策保持 CJS-only,因为“没有 SDK 对应物”:
graphify、gsd2-import、schema-detect、fallow-runner、intel、drift、installer-migrations——这些模块在 CJS 侧保留进程内实现。
PRD 的 Phase 5(docs/prd/3524-cjs-sdk-hard-seam.md L160)明确写到:“CJS-only Module handlers(…intel、drift…)keep their in-process CJS implementations. They are not in the canonical family registry and do not route through the SDK runtime bridge.” 而 PR #3577 要解决的,正是 Phase 6 实施期间出现的一批 违反该政策的绑定(violation) 以及 SDK 侧缺失的行为移植(validation port)。
事故还原:verify.codebase-drift 的双向桥接无限递归
事故链路
变更记录的第一条披露了当时最严重的问题:verify.codebase-drift 与八个 intel.* 动词被错误地绑定进了 SDK catalog / manifest,直接违反 ADR §3。
错误绑定带来的是一段灾难性的调用链:
- SDK 侧存在一个
verifyCodebaseDrift桩(stub); - 这个桩通过
execFileSync回执到gsd-tools verify codebase-drift; - 而 CJS 侧的
verify命令路由(command router)此时已按 Phase 5 切到 SDK runtime bridge,于是又把调用桥接回 SDK; SDK → CLI → SDK形成闭环,在远端 64 GiB Docker 主机上 fork 出数百个 node 进程,直到被人工 kill。
修复方式
- 把所有错误绑定的条目从 SDK catalog 中移除;
- CJS 路由器与
gsd-tools.cjs本来就有针对这些动词的直连 CJS 分发路径,现在它成为唯一路径。
当前仓库源码完整保留了这条决策的“现场注释”,可作为最直接的源码级证据。在 sdk/src/query/verify.ts 顶部有明确说明:
verify.codebase-drifthandler intentionally NOT exported from the SDK.drift(bin/lib/drift.cjs) is out-of-seam, CJS-only per ADR/PRD … Previous Phase 6 stub (whichexecFileSync'd back togsd-tools) created an infinite SDK→CLI→SDK recursion … observed forking hundreds of node processes on a 64 GiB host. The router now dispatchesverify codebase-driftdirect toverify.cmdVerifyCodebaseDrift, which is the canonical implementation.
同类型的不绑定声明还出现在:
- sdk/src/query/command-family-handlers.ts:
verifyCodebaseDrift不被 import; - sdk/src/query/command-family-handlers.ts:
verify族 handlers 中'verify.codebase-drift'被有意省略,注释指向 ADR/PRD 3524 §3 / L160; - sdk/src/query/command-manifest.verify.ts:
verify.codebase-drift刻意不进入 manifest。
需要说明的是,仓库中仍保留着 intel.* 的 SDK 侧实现文件 sdk/src/query/intel.ts(标注为从 bin/lib/intel.cjs 移植而来),本次变更的核心是把这些动词从“会经过 SDK 路由/catalog 绑定”的集合中移除,让它们只走 CJS 进程内分发——避免任何经 runtime bridge 绕一圈又回到 CJS 的路径存在。这也是该变更所在 changeset 命名为 “ADR violations and validation port” 的原因:前者清理绑定违例,后者把 CJS 已实现、SDK 缺失的校验逐项移植过去。
从这条事故中提炼的工程原则
- 禁止“自我回环”桥接:任何 adapter 如果通过子进程调用一个最终会路由回自己的命令,就是无限递归的温床。SDK↔CJS 之间的桥只允许“每侧一个入口、单方向完成”。
- 出界即不绑定:CJS-only 模块不应出现在 SDK catalog / manifest / family handlers 的任何注册表里,只留物理文件与测试引用是不够的——注册表才是“会路由它”的判定依据。
- 源码注释即决策日志:
verify.ts与command-family-handlers.ts里保留的注释,让后续维护者无需回溯 git log 即可知道“为什么不 export / 为什么省略”,这正是仓库工程文档化的做法。
config-ensure-section:用 catalog 重绑定替代 CJS 回退
第二个修复涉及一个“旧契约回归”。历史上 CLI 的无参调用(例如 gsd-tools config-ensure-section 或初始化流程)会生成一份完整默认值 config.json(对应旧 CJS 的 ensureConfigFile → buildNewProjectConfig 链路)。Phase 6 最初把 catalog 里的 'config-ensure-section' 条目绑到了新的 configEnsureSection 处理器上——但该处理器是单节确保语义,必须要求 args[0]=sectionName。结果所有 CLI 调用方都调用的是无参形式,全部被破坏。
修复方案是在 catalog 里把该条目重绑定到 configNewProject,而不是试图给 CJS 侧打补丁回退:
configNewProject的无参分支产出的形状与旧ensureConfigFile → buildNewProjectConfig链条完全一致;- SDK 路径恢复“无参 → 全量默认 config.json 初始化”契约。
在 sdk/src/query/config-mutation.ts 可以确认 configNewProject 的幂等语义:如果 .planning/config.json 已存在,直接返回 { created: false, reason: 'already_exists' },不会覆盖已有配置;只有缺失时才基于默认值构造并原子写入(返回 { created: true, path: '.planning/config.json' })。
实现细节还展示了它与旧行为对齐的努力(见 config-mutation.ts L588-L622 的注释):
- 顶层中
resolve_model_ids、context_window、mode、planning、graphify被从 init 输出中省略——它们要么有独立的解析路径,要么属于用户另行配置的 opt-in 特性; git.base_branch也被省略:保留null会抑制origin/HEAD自动探测,从而破坏 ship-ready 预检;- 运行时通过环境变量(
BRAVE_API_KEY/FIRECRAWL_API_KEY/EXA_API_KEY或~/.gsd/下的 key 文件)探测三个搜索服务是否可用,并把 manifest 中的false默认值覆盖为探测结果。
与这条修复配套的分支 changeset 是 .changeset/3577-config-ensure-section-parity.md,说明仓库内部对“单节确保”与“全量初始化”两个语义做了显式区隔记录。
configNewProject 默认值:从硬编码漂移到 Manifest 单一来源
漂移的代价
修复前,configNewProject 里有一份硬编码重复的 defaults 对象。这份副本与 Manifest 脱节后,产生了大量“两边行为不一致”的隐性缺陷——变更记录明确点名了缺失的键族:
workflow.*下的ai_integration_phase、tdd_mode、human_verify_mode、pattern_mapper、plan_bounce*、auto_prune_state、subagent_timeout、security_*、post_planning_gaps;git.create_tag、claude_md_path;planning.*、graphify.*、mode、resolve_model_ids、context_window。
修复:派生自 Configuration Module Manifest
修复把硬编码副本替换为从 Configuration Module 的 canonical manifest 派生:默认值定义在 sdk/shared/config-defaults.manifest.json,由 sdk/src/configuration/index.ts 导出为 CONFIG_DEFAULTS,再被 configNewProject 消费(见 config-mutation.ts 中的 import { CONFIG_DEFAULTS } from '../configuration/index.js')。
这份 Manifest 本身就是仓库“数据即真相源”的样本,值得展开看几个关键默认值(节选):
| 路径 | 默认值 | 说明 |
|---|---|---|
model_profile |
balanced |
默认模型档位 |
context_window |
200000 |
上下文窗口(CJS 起源的顶层键) |
mode |
interactive |
运行模式(CJS 起源顶层键) |
git.branching_strategy |
none |
分支策略(旧 CJS 顶层 branching_strategy 归一化后所在) |
git.create_tag |
true |
打标签开关 |
workflow.tdd_mode |
false |
TDD 模式 |
workflow.human_verify_mode |
end-of-phase |
人工验证时机 |
workflow.subagent_timeout |
300000 |
子代理超时(毫秒) |
workflow.auto_prune_state |
false |
STATE.md 自动修剪开关 |
workflow.security_enforcement |
true |
安全门强制 |
planning.sub_repos |
[] |
子仓库列表 |
brave_search / firecrawl / exa_search |
false |
运行时按 API key 覆盖 |
Manifest 顶部的 _comment 字段还记录了归一化决策:例如 CJS 扁平键 plan_checker 与 canonical 嵌套 workflow.plan_check 的差异已解决(canonical 名为 workflow.plan_check),安全相关键(security_enforcement、security_asvs_level、security_block_on)与 post_planning_gaps 的 canonical 位置在 workflow.*。生产代码在落盘前会剥离 _comment 元数据(config-mutation.ts L585-L586)。
这次修复的价值在于它关闭了 ADR 所要预防的 DEFECT.PORT-DRIFT.cjs-sdk 缺陷族——当某键只在 CJS 或只在 SDK 生效时,两个运行时的行为就无法再由黄金测试锁住。
configSet 枚举与形状校验移植:把 CJS 已经强制的规则带到 SDK
第三个移植批次把 CJS cmdConfigSet 强制执行的枚举/形状校验器补齐到 SDK 的 configSet。逐项实现在 sdk/src/query/config-mutation.ts 都有对应代码与 CJS 行号注释,整理如下:
| 配置键 | 校验规则 | 合法值 |
|---|---|---|
workflow.drift_action |
枚举 | warn、auto-remap |
workflow.drift_threshold |
正整数 | >= 1 的整数 |
workflow.human_verify_mode |
枚举 | mid-flight、end-of-phase |
statusline.context_position |
枚举 | front、end |
code_quality.fallow.scope |
枚举 | phase、repo |
code_quality.fallow.profile |
枚举 | minimal、standard、strict |
review.default_reviewers |
数组形状 | slug 必须匹配 ^[a-zA-Z0-9_-]+$,归一化为小写唯一,并以归一化值落盘 |
review.default_reviewers 是最复杂的形状校验:拒绝空数组、拒绝非字符串元素、拒绝含非法字符的 slug,去重后小写归一化,并且 parsedValue 被原地改写,持久化的是归一化形式(config-mutation.ts L378-L420),保证 CJS 与 SDK 写入磁盘的值一致。
同一函数中还保留了一批其他布尔/枚举守卫,可作为“SDK 行为对齐 CJS”的完整参考:
git.create_tag、workflow.post_planning_gaps拒绝非布尔值(防config-set git.create_tag maybe之类静默写脏,Bug #3086);context只允许dev/research/review;ship.pr_body_sections走专门的结构校验validateShipPrBodySections(字段白名单、source selector 格式、模板 token 白名单等);- 未知键先查
CONFIG_KEY_SUGGESTIONS纠错映射,再做 LCP 建议(isValidConfigKey,config-mutation.tsL163-L196)。
此外还带上了两个工程细节:写入采用临时文件 + rename 的原子写(失败回退直接写,atomicWriteConfig),并通过 acquireStateLock/releaseStateLock 保护读-改-写(D6),响应值用 maskIfSecret 掩码密钥类键(不回显明文凭据,但磁盘上的值不掩码)。
init 处理器:--tdd 与 workflow.subagent_timeout
initExecutePhase 与 initPlanPhase 两个 init 处理器补齐了两处与 CJS 的参数契约:
- 解析
--tdd布尔覆盖,与 CJS 路由器里的parseNamedArgs(args, [], ['validate', 'tdd'])以及 CJS handler 的options.tdd || config.tdd_mode || false对齐; initMapCodebase读取subagent_timeout:从 canonical 位置workflow.subagent_timeout取值,并使用 Manifest 规定的300000默认值,取代原先的 undefined 兜底。
这一批改动说明 Phase 6 的移植不是机械照抄 handler,而是连参数解析的接受集合(--tdd、--validate)与默认值来源都要逐一与 CJS 对齐,否则同一个命令在两条路径上会有不同的行为下限。
roadmap.analyze 按阶段暴露 mode
此前只有 roadmapGetPhase 会解析状态/计划文本中的 **Mode:** 字段。本次让 roadmap.analyze 也提取同样的字段并按阶段暴露,使消费方无论走哪个查询 handler 都能读到 MVP-mode 标记,避免两个查询对同一事实给出不一致的视图。
这是一个典型的“同一概念双实现造成分歧”的微观案例:ADR-3524 的动机(#1535…#3523 的一长串双端漂移 Bug)本质上都是同构的——某个事实只在某一侧被解析/被暴露。
phaseComplete:配置开启时的 STATE.md 自动修剪
SDK 的 phaseComplete 补上了 CJS cmdPhaseComplete 中 workflow.auto_prune_state === true 的行为分支:以 statePrune(['--keep-recent', '3', '--silent'], …) 的形态调用状态修剪(对应 issue #2087)。
它解决的问题是真实的日常漂移:完成 Phase N 之后,STATE.md 里 [Phase 1..N-3] 的陈旧决策应该被移除,而不是永久残留。若 SDK 侧不移植该分支,则:
- 通过
gsd-sdk走完成的用户永远不会触发修剪; - 状态文档随阶段推进无限膨胀,与 CJS 侧的状态文档生命周期策略(可参考 docs/STATE-MD-LIFECYCLE.md 所在的状态文档治理体系)产生分歧。
这类“默认关闭但开启后必须一致”的配置开关,正是黄金测试最难覆盖、却最容易在跨运行时移植时被漏掉的类型。
initRemoveWorkspace:用抛出的 GSDError 表达失败
错误语义的移植同样重要。此前 SDK 的 initRemoveWorkspace 在“未提供名字”与“工作区不存在”两个分支返回 { data: { error } }——但 CLI 输出路径把“返回 data”一律当作成功处理,导致命令错误码为 0、消息也没进 stderr。
修复后两个分支改为抛出 GSDError(..., ErrorClassification.Validation),从而:
- CLI 返回非零退出码;
- 消息写入 stderr;
- 与 CJS 路径的错误形态一致,可被上层脚本可靠判断。
仓库中 GSDError 与 ErrorClassification 定义于 sdk/src/errors.ts(config-mutation.ts 即从该处 import)。这个案例的普适教训是:跨层移植时,“值返回”与“异常抛掷”的选择必须按 CLI 契约而非内部便利来定——一个被当成功的错误返回,比没有实现更隐蔽。
frontmatterGet:支持 --field <name> 解析
最后一个移植点很“小”但很典型。CLI 实际调用 frontmatter get <file> --field phase 时,传给 handler 的 args 是 [file, '--field', 'phase'];而旧 handler 把 args[1] 当作字段名,于是读到的是字面量字符串 --field,拿不到真正的字段名 phase。
修复让 frontmatterGet 同时支持两种形式:
- 带标志形式:
--field <name>; - 位置参数形式:
args[1]直接作为字段名。
这个案例说明跨运行时移植时连 argv 形态假设都要对齐——SDK handler 不能假设它只会收到“纯净”的位置参数,因为 CJS 侧调用方可能已经在传 --flag value 混合形态。
治理闭环:黄金测试与出界注册表的“事后验证”
本次变更并非孤立补丁,而是整个 ADR-3524 六阶段治理的一部分。观察 sdk/src/golden/golden.integration.test.ts,其中仍保留 verify.codebase-drift 的黄金比对用例(对比 gsd-tools verify codebase-drift 与 SDK registry 分发的 JSON 输出),同时仓库还配套了 .changeset/3577-docker-test-fixup.md(Docker 测试环境修复)等同行 changeset。
这一组合体现了 get-shit-done 仓库的三层防线模型:
- 注册表层(catalog / manifest / family handlers):决定“哪个命令会路由到哪一侧”,本次出界清理就发生在这里;
- 行为层(枚举校验、默认值来源、错误语义、argv 解析):决定“同一命令在两边的可观测行为是否一致”,本次 validation port 覆盖这里;
- 验证层(golden parity 测试 + changeset):在 CI 和变更记录两个层面锁住边界,让违反 ADR §3 的绑定和
PORT-DRIFT缺陷无法再次混入。
小结:一次变更,两场战役
把 PR #3577 放回 ADR-3524 的坐标系看,它其实同时打了两场战役:
- 清理违例:把
verify.codebase-drift、intel.*等出界动词从 SDK catalog / manifest 移除,恢复 ADR §3 / PRD L160 定义的“CJS-only 直接分发”唯一路径,并借源码注释为后来者留下事故现场; - 移植缺口:把 CJS 侧已强制的默认值单一来源(Manifest)、枚举/形状校验、
--tdd解析、STATE.md 自动修剪、GSDError 错误语义、frontmatter --fieldargv 解析逐项搬到 SDK,关闭DEFECT.PORT-DRIFT.cjs-sdk缺陷族。
对任何维护“双运行时、双语言、共享契约”代码库的团队,这条变更记录可提炼为可直接复用的操作清单:先查注册表里有没有不该被路由的模块,再查两边对同一输入是否给出相同的校验与失败语义,最后用黄金对比测试把差异钉死在 CI 里——而不是等 64 GiB 主机上的数百个 node 进程替你做压测。
关键文件索引
- ADR: docs/adr/3524-cjs-sdk-hard-seam.md
- PRD: docs/prd/3524-cjs-sdk-hard-seam.md
- 默认值真相源: sdk/shared/config-defaults.manifest.json
- 配置读写/校验实现: sdk/src/query/config-mutation.ts
- 出界决策现场注释: sdk/src/query/verify.ts、sdk/src/query/command-family-handlers.ts、sdk/src/query/command-manifest.verify.ts
- 黄金 parity 验证: sdk/src/golden/golden.integration.test.ts
- 本文主体变更记录: .changeset/3577-adr-violations-and-validation-port.md
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 StartedRust0629
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