get-shit-done 配置自愈迁移实战:顶层branching_strategy不再触发 “unknown config key” 误报
【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done
这篇技术指南围绕 get-shit-done(GSD)修复记录.changeset/sunny-pandas-dance.md(PR #3527)展开,剖析.planning/config.json读取管线中“旧式顶层键branching_strategy”被误报为未知配置键的根因、自愈式磁盘迁移的修复设计,以及 CJS 与 SDK 双层配置模型的一致性保障。读完你将掌握 GSD 配置加载(loadConfig)的完整调用链、遗留键规范化(normalizeLegacyKeys)与去重告警机制,并能在自己的项目中复现验证与正确书写branching_strategy配置。
问题背景:branching_strategy的规范位置与遗留形态
在 GSD 中,项目级配置存放于项目根目录的.planning/config.json。Git 分支创建策略的规范位置是嵌套键git.branching_strategy,其取值与含义参见 docs/CONFIGURATION.md:
| 取值 | 行为 |
|---|---|
none(默认) | 永不创建分支,适合单人开发、简单项目 |
phase | 在execute-phase开始时为一个 Phase 创建分支,粒度小、可回滚 |
milestone | 在首次execute-phase时为整个 Milestone 创建分支,在complete-milestone时合并,适合发布分支 / 按版本提 PR |
该键决定分支模板是否启用(如phase_branch_template: "gsd/phase-{phase}-{slug}"),因此其能否被正确读取直接决定分支行为。一个常见的历史遗留写法是把该键放在顶层:
{ "branching_strategy": "phase", "git": { "base_branch": "main" } }而规范写法为:
{ "git": { "branching_strategy": "phase", "base_branch": "main" } }问题恰恰出在:loadConfig确实在积极读取这个遗留顶层键,却同时会向用户打印“该键会被忽略”的警告——一句与事实相悖的误报。这正是 PR #3527 修复的核心缺陷(对应 issue #3523)。
缺陷根因:KNOWN_TOP_LEVEL允许清单由“点号路径首段”推导
GSD 的 CJS 侧配置加载入口是 get-shit-done/bin/lib/core.cjs 中的loadConfig(cwd)。它承担两类工作:
- 读取键值:通过
get('branching_strategy', { section: 'git', field: 'branching_strategy' })(core.cjs)同时支持顶层键与git.*嵌套回退——即遗留顶层值会被真正使用,并非“被忽略”; - 告警未知顶层键:从配置模式清单
VALID_CONFIG_KEYS(真实来源是 sdk/shared/config-schema.manifest.json 中的git.branching_strategy等点号路径)推导出KNOWN_TOP_LEVEL允许集合,再对parsed的顶层键逐一比对。
根因在于推导方式本身:
const KNOWN_TOP_LEVEL = new Set([ ...[...VALID_CONFIG_KEYS].map(k => k.split('.')[0]), // ... ]);VALID_CONFIG_KEYS中记录的是点号嵌套路径(如workflow.research、git.branching_strategy),经split('.')[0]取首段后得到的是workflow、git这样的段容器名,而永远无法得到branching_strategy这个键本身。于是当一个遗留配置文件把branching_strategy写在顶层时,它既不在容器名集合中,也不在后续补充的弃用键名单里,被判为“未知配置键”,触发:
gsd-tools: warning: unknown config key(s) in .planning/config.json: branching_strategy — these will be ignored这句话是误导性的——该值实际上被core.cjs中第 455 行的嵌套回退逻辑读取并生效了。正如仓库内漂移记录 docs/agents/cjs-sdk-seam.md 所记载:此前 #3055 曾出现“顶层branching_strategy被静默丢弃而回退到none”的严重缺陷(阶段提交错误地落在了操作者当前分支而非gsd/phase-{N}),#3116 先在 SDK 侧mergeDefaults()补上了遗留键规范化;而 CJS 路径在 #3527 之前仍残留着这一不正确的告警。
修复方案:自愈式磁盘迁移(self-healing on-disk migration)
PR #3527 没有选择“只静默告警名单”或“只允许键存在”,而是复用了仓库中既有的multiRepo → planning.sub_repos迁移先例,实施自愈迁移(option 3):
- 首次
loadConfig时,把顶层branching_strategy的取值**接枝(graft)**进git.branching_strategy; - 同时删除过时的顶层条目;
- 通过写回逻辑将结果持久化到磁盘,此后磁盘上的配置文件即为规范形态。
这一归一化逻辑被收敛到共享的 Configuration Module(见 docs/adr/3524-cjs-sdk-hard-seam.md):normalizeLegacyKeys(parsed) → { parsed, normalizations[] }一次调用即可覆盖depth → granularity、multiRepo → planning.sub_repos、sub_repos → planning.sub_repos、branching_strategy → git.branching_strategy四类遗留键迁移(core.cjs 内联注释)。由于该模块的migrateOnDisk是异步的,而loadConfig是同步函数,写回在调用点以既有的platformWriteSync模式同步完成,且只写磁盘文件本身、绝不把合并后的默认值污染回磁盘(core.cjs)。
与此同时,KNOWN_TOP_LEVEL的“弃用但仍接受”名单中也加入了'branching_strategy'(与depth、multiRepo并列,见 core.cjs)作为安全网——即使首次读取尚未发生写回,告警也永远不会触发。
迁移过程中的覆盖语义与 SDK 的mergeDefaults()保持严格一致:当规范嵌套值git.branching_strategy已存在时,嵌套值胜出,遗留顶层值不得覆盖它;冗余的顶层键仍会被移除。这样既完成了形态自愈,又不改变用户已表达的配置意图。
去重保护:同一警告一进程内最多出现一次
修复还顺带处理了告警的“二次发射”问题。loadConfig在一次 CLI 调用中可能被执行多次——例如单次init phase-op N会先为子命令初始化、再为 git 配置解析各调用一次。若不加防护,同一未知键警告会重复打印。
解决方式是模块级去重集合_warnedUnknownConfigKeys(core.cjs):
const _warnedUnknownConfigKeys = new Set(); // 告警发射点(第 407-419 行附近) const unknownKeys = Object.keys(parsed).filter(k => !KNOWN_TOP_LEVEL.has(k)); if (unknownKeys.length > 0) { const warnKey = unknownKeys.join(','); if (!_warnedUnknownConfigKeys.has(warnKey)) { _warnedUnknownConfigKeys.add(warnKey); process.stderr.write( `gsd-tools: warning: unknown config key(s) in .planning/config.json: ${unknownKeys.join(', ')} — these will be ignored\n` ); } }它以“未知键组合”为去重键,保证同一进程内同一组未知键最多告警一次——对真正未知的键依然如实提示,不会因去重而掩盖问题。
契约测试:CJS 与 SDK 对遗留形态保持一致
该修复的价值不止于消除一条告警,更在于堵住了双层实现之间的漂移。SDK 侧 sdk/src/config.ts 中,项目配置读取在mergeDefaults()前先调用normalizeLegacyKeys(parsed),再由canonicalMergeDefaults完成深合并,早已正确处理顶层遗留键(#3116);CJS 侧则通过共享 Configuration Module 取得同等行为。
回归测试 tests/bug-3523-cjs-loadconfig-branching-strategy-warning.test.cjs 从四个维度锁定了修复:
- 无告警:以
resolve-model planner(内部调用loadConfig的最小 CJS 入口)触发读取,断言 stderr 为空; - 值仍被透出:触发迁移写回后,用
config-get git.branching_strategy能读到milestone等正确取值; - 磁盘迁移生效:断言迁移后磁盘上
config.json中git.branching_strategy已存在、顶层键已删除;嵌套值存在时不被顶层覆盖(nested wins);且GSD_WORKSTREAM=alpha的工作流加载场景下,根配置同样能自愈并持久化; - CJS↔SDK 契约一致性:使用 SDK
mergeDefaults已能处理的同一遗留 fixture,断言 CJS 侧产出相同的branching_strategy值。
其中“去重”断言使用了一个确属未知的哨兵键__gsd3523_dedup_sentinel__,验证未知键警告出现且仅出现一次——不会因loadConfig被多次调用而翻倍,也不会因修复而变为零次。
验证方法与配置实操
在本地项目中复现与验收该修复非常简单:
# 1. 写入一个遗留形态的配置 cat > .planning/config.json <<'EOF' { "branching_strategy": "milestone", "git": { "base_branch": "main" } } EOF # 2. 触发 loadConfig(resolve-model 是最小入口),stderr 应无告警 node get-shit-done/bin/gsd-tools.cjs resolve-model planner # 3. 读取规范键,应输出 milestone node get-shit-done/bin/gsd-tools.cjs config-get git.branching_strategy # 4. 检查磁盘:.planning/config.json 已被自愈为规范形态 cat .planning/config.json首次读取后磁盘文件应已迁移为:
{ "git": { "base_branch": "main", "branching_strategy": "milestone" } }此后无论使用gsd config-set git.branching_strategy <none|phase|milestone>写入,还是直接编辑config.json,都不再产生误导性告警。若你同时在使用多工作流(GSD_WORKSTREAM=alpha)模式,根配置文件同样会在工作流读取过程中被自愈,参见上述测试第三条。
小结
PR #3527 表面上只修掉了一条告警,实际上完成了三件事:
- 修正事实错误:杜绝“键正在被读取却提示将被忽略”的自相矛盾输出;
- 建立自愈路径:以
multiRepo → planning.sub_repos为先例,把遗留顶层branching_strategy在首次读取时自动迁移为规范git.branching_strategy并持久化; - 统一双实现语义:CJS
loadConfig与 SDKmergeDefaults通过共享 Configuration Module 与契约测试(bug-3523 测试)对齐遗留形态的处理结果,嵌套值优先、冗余键清理、告警去重的行为保持一致。
对使用或扩展 GSD 的开发者而言,这篇变更记录是理解.planning/config.json迁移管线的最佳切片:从 config-schema.cjs(模式清单适配器)、configuration 模块 到 loadConfig 的同步写回,再到 配置文档 中git.branching_strategy的枚举语义,整条链路环环相扣——这也是“配置不仅被正确读取,还会自我修复到规范形态”的设计体现。
【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考