get-shit-done 多运行时安装命名空间归一化:彻底修复 Agent 正文中的 `/gsd:<cmd>` 冒号引用泄漏(3677)
get-shit-done(GSD)是一个面向 Claude Code、OpenCode、Gemini、Codex 等多种 AI 编程助手的 meta-prompting、上下文工程与规范驱动开发系统。其安装器 bin/install.js 需要把仓库里以"冒号形式"(/gsd:<cmd>)书写的斜杠命令引用,转换成各运行时实际可路由的形态。本篇文章围绕修复条目 .changeset/fix-3677-agent-colon-namespace-leak.md(type: Fixed,PR #3680),完整解析该缺陷的成因、运行时分类策略、纯函数修复设计、底层转换器的实现细节与配套回归测试,帮助你理解并复现这套"多运行时正文归一化"机制。
背景:一套仓库源码、多套运行时命名约定的冲突
GSD 安装器负责把单份上游仓库,展开为面向十余种运行时的本地产物。不同运行时对"斜杠命令 / Skill / Agent 名称"的注册方式并不一致,这直接导致仓库源码中的正文引用与最终安装产物之间存在命名形态差异:
- 仓库中的 Agent、Workflow、文档正文统一使用冒号形式书写,例如
agents/gsd-code-reviewer.md中描述 "Spawned by/gsd:code-review",agents/gsd-code-fixer.md中写有 "Spawned by/gsd:code-review --fixworkflow"; - 但自 #2808 起,以
name:字段注册 Agent/Skill 的运行时(Claude Code、Qwen、Hermes)使用连字符形式gsd-<cmd>作为规范名称,使自动补全展示与Skill(skill="gsd-<cmd>")调用保持一致; - 连字符/冒号双向转换器集中在 scripts/fix-slash-commands.cjs,其文件头注释说明了方向约定:默认方向把废弃的
/gsd-<cmd>还原为仓库正文使用的冒号形式;反向方向transformContentToHyphen在安装期把/gsd:<cmd>/gsd:<cmd>改写成gsd-<cmd>。
当正文中遗留的 /gsd:<cmd> 冒号引用被安装进一个只注册了 gsd-<cmd>(连字符)名称的运行时后,这些引用将指向不存在的斜杠命令——这就是命名空间泄漏的本质:文本看起来有效,运行时却无法路由。
问题全貌:三个"泄漏面"都需要覆盖
GSD 的多运行时展开涉及三类正文载体,它们在安装管线中各自存在独立的转换路径,因此在 #3677 之前,冒号引用的泄漏需要分三个表面分别修复:
- SKILL.md 正文:由 #3583 提出、#3629 落地,在
convertClaudeCommandToClaudeSkill内对 Skill 正文调用transformContentToHyphen(见 bin/install.js),使安装后的 SKILL.md 正文与连字符name:匹配; - 运行时发射(runtime emissions):由 #3584 提出、#3606 落地,覆盖运行时自身生成的引用文本;
- Agent 正文(本次 #3677 修复的目标):安装器把
agents/gsd-*.md逐个拷贝为各运行时的 Agent 定义,此前缺少与上述两者一致的归一化步骤。
正如该 changeset 所述,三个修复共同构成"三面覆盖"(three-surface coverage),缺一不可。
根因拆解:为什么只有 Claude / Qwen / Hermes 会中招
要理解修复为什么采用"显式白名单",需要先弄清各运行时在安装管线中的不同行为。从 bin/install.js 顶部对 HYPHEN_NAME_AGENT_RUNTIMES 的解释看,运行时被划分为三类:
| 类别 | 运行时 | 行为 |
|---|---|---|
连字符 name: + 原样拷贝正文 |
claude、qwen、hermes |
注册 gsd-<cmd> 名称,但正文几乎原样拷贝:Claude 不做任何命名空间转换,Qwen/Hermes 只做品牌词替换(把 CLAUDE.md→QWEN.md、Claude Code→Qwen Code、.claude/→.qwen/ 等),冒号引用因此泄漏 |
| 自我转换运行时 | codex、copilot、antigravity、cursor、windsurf、augment、trae、codebuddy、cline、opencode、kilo |
各自的 convertClaudeAgentToXAgent() 转换器已经自行处理命名空间问题,归一化层不应重复改写 |
| 刻意使用冒号 | gemini |
有意的冒号命名空间,必须原样保留 |
代理安装循环中的实际代码印证了这一点:Qwen/Hermes 分支只做品牌替换(bin/install.js),随后立即进入 #3677 的归一化步骤,最后才写盘(bin/install.js)。
修复设计:一个纯函数谓词 + 一个门控辅助函数
#3677 的核心接缝由两个从 bin/install.js 导出的函数构成,二者都是纯函数,不依赖文件系统状态,因此可以被测试直接加载、无需真跑安装流程。
谓词:该运行时是否需要连字符归一化
const HYPHEN_NAME_AGENT_RUNTIMES = new Set(['claude', 'qwen', 'hermes']);
function shouldNormalizeHyphenNamespaceInAgentBody(runtime) {
if (typeof runtime !== 'string' || runtime === '') return false;
return HYPHEN_NAME_AGENT_RUNTIMES.has(runtime);
}
代码注释明确说明这里选择显式白名单而非黑名单:对未知/未来的运行时默认"不重写"——理由是宁可让旧引用泄漏,也不能把一个命名空间行为尚未验证的运行时正文改坏(better to leak than to mangle)。这体现了命名空间转换类逻辑必须"保守默认"的设计原则。
门控辅助函数:命中白名单才委托给底层转换器
function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
if (!shouldNormalizeHyphenNamespaceInAgentBody(runtime)) return content;
return transformContentToHyphen(content, cmdNames);
}
cmdNames 由调用方通过模块顶部的 readGsdCommandNames() 预读一次(bin/install.js),避免每个 Agent 重复执行 readdirSync 与正则构造。
底层转换器:脚本即库的双向归一化器
被委托的 transformContentToHyphen 位于 scripts/fix-slash-commands.cjs,这个文件既是可单独运行的修复脚本,也是安装器复用的库。它通过若干安全设计确保转换不会误伤正文:
- 命令名册来自真实文件:
readCmdNames()读取commands/gsd/目录下的.md文件名作为已知命令集合(scripts/fix-slash-commands.cjs); - 最长优先匹配:
buildColonPattern将命令按长度降序排序后拼进正则(scripts/fix-slash-commands.cjs),避免plan-phase抢先吃掉plan-phase-x之类的长命令; - 双向词边界保护:左侧使用负向环视
(?<![a-zA-Z0-9_-]),右侧使用(?=[^a-zA-Z0-9_-]|$),保证mygsd:foo不被命中、/gsd:plan-phase-extra不会误判为plan-phase; - 只改写已知命令:
gsd-sdk、gsd-tools这类 CLI 二进制标识符不属于斜杠命令名册,一律保持原样,防止把可执行文件引用改坏; - 空名册短路:当命令列表为空或非法时返回
null,调用方直接 no-op,避免退化成(?=)的宽泛匹配导致意外改写(scripts/fix-slash-commands.cjs)。
匹配结果同时支持 /gsd:execute-phase 与不带前导斜杠的裸写 gsd:execute-phase(后者同样会被重写为 gsd-execute-phase),因为冒号并不是正则匹配的一部分。
挂接点:为什么放在"所有运行时转换之后"
#3677 归一化调用在 Agent 安装循环中的位置经过精心设计——位于所有运行时专属转换完成之后、writeFileSync 之前(bin/install.js)。以 Qwen/Hermes 为例,安装流程对每个 agents/gsd-*.md 依次执行:
- 读取源文件并替换
~/.claude/等硬编码路径; - 写入安装归属说明(attribution);
- 执行运行时专属转换:OpenCode/Kilo 改 frontmatter,Gemini 走
convertClaudeToGeminiAgent,Codex/Copilot/Cursor 等走各自的 agent 转换器,Qwen/Hermes 做品牌词替换; - 调用
normalizeAgentBodyForRuntime(bin/install.js); - 写盘并进入
verifyInstalled校验。
放在最后一步之后的意义在于:先让各运行时的转换器完成自己的改写,再由这一层只对"未处理命名空间"的运行时兜底,避免与自我转换运行时产生双重改写。与之对应的,在通用负载拷贝函数 copyWithPathReplacement 中,同样存在对 .md 正文的统一归一化调用(源注释标注为 #3683),从而让 SKILL/Workflow 等负载与 Agent 正文保持同一套行为。
回归测试解剖:五组测试锁定行为契约
对应的回归测试位于 tests/bug-3677-agent-colon-namespace-leak.test.cjs,共分五组,覆盖从"函数存在性"到"真实源码效果"的完整契约:
- A 组(导出接缝):断言
shouldNormalizeHyphenNamespaceInAgentBody与normalizeAgentBodyForRuntime从bin/install.js导出,保证测试能直接命中安装器内部逻辑; - B 组(谓词分桶):对白名单运行时
claude/qwen/hermes断言返回true,对 11 个自我转换运行时与gemini断言返回false,并单独断言未知运行时bogus-runtime-id默认返回false(保守策略)。测试注释同时给出约束:任何出现在runtime-artifact-layout.cjs的运行时必须恰好落入两个集合之一; - C 组(门控行为):用一段含
/gsd:execute-phase、/gsd:verify-work与gsd-sdk query commit的样例正文,验证 claude/qwen/hermes 都被改写为连字符形式、gemini的冒号被保留、copilot在本层返回原样; - D 组(底层转换器健全性):确认只改写已注册命令,未知命令与
gsd-sdk保持不动; - E 组(真实源码效能 + 幂等性):从 PR #3681 移植而来(源码注明感谢 johnzilla),逐个对
agents/gsd-*.md真实文件执行转换并断言无任何冒号残留;同时验证幂等性——对已经是连字符形式的输入重复执行转换是 no-op,这一点对反复重装(re-install 会再次运行转换)至关重要,防止双重改写破坏正文。
给使用者的实际影响与验证方式
对最终用户而言,本修复的意义在于:完成一次 gsd update 全量重装后,Claude Code / Qwen / Hermes 安装目录下 Agent 正文中的 /gsd:<cmd> 都会被规范化为可路由的 /gsd-<cmd> 形式;而 Gemini 用户与 11 个自我转换运行时的安装产物则与修复前完全一致,不会出现任何意外的正文变化。
如果需要在当前仓库验证该修复的行为契约,可直接运行上述 tests/bug-3677-agent-colon-namespace-leak.test.cjs(基于 Node 内置 node:test,测试文件自身会设置 GSD_TEST_MODE=1 以跳过安装器主流程)。结合 scripts/fix-slash-commands.cjs 的通读,你可以把"命名空间形态变更"这类跨运行时风险,沉淀为"白名单谓词 + 纯函数门控 + 词边界正则 + 幂等性测试"这一套可复用的修复范式,应用到自己的多目标产物安装器设计中。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00