【免费下载链接】gsd-core
Git. Ship. Done - Core
本篇指南以 gsd-core 仓库中的 ADR-0002 决策记录为主线,系统讲解commands/gsd/*.md命令文件契约的五条结构规则与第六条仓库级工作流可达性规则,并结合 lint-command-contract.cjs、command-contract-helpers.cjs 与 command-contract.test.cjs 的源码实现,说明这套校验如何被纳入lint:ci流水线,以及遇到"不可达工作流"报错时应如何处置。读完本文,你将掌握命令文件的正确定义方式、<execution_context>的规范写法,以及 CI 门禁的底层判定逻辑与修复路径。
背景:为什么需要命令契约校验
在 ADR-0002 之前,gsd-core 对commands/gsd/*.md的契约校验是零散且不一致的:
tests/skill-frontmatter-contract.test.cjs(源自enh-2790-skill-consolidation,合并史诗 #1969)只检查特定命令的存在性与 frontmatter;tests/docs-update.test.cjs(源自bug-3135-capture-backlog-workflow)只检查execution_context中的 @-引用能否在磁盘上解析;- 没有任何一条检查同时覆盖所有命令的
allowed-tools合法性、name:命名约定与description:非空。
这意味着任何触碰命令文件的 PR 都可能悄悄破坏契约而没有任何测试拦截。add-backlog.md的缺口(#3135)就是典型案例:在针对性回归测试写出之前,对应的工作流文件在整个合并周期内一直缺失。
同时,65 个命令文件中有 40 个存在冗余的 prose @-引用——同一个路径既出现在<execution_context>(真正加载文件的地方)中,又重复出现在<process>正文里。这种惰性副本每次调用命令都额外消耗约 900 tokens,并且形成了漂移缝隙:prose 引用可能在可执行的execution_context引用之外独立过期。另外debug.md与thread.md两个最大命令将完整实现内联在命令文件中而非委托给工作流文件,导致约 4,400 tokens 的实现细节在每次会话中都随技能索引描述被急切加载。
决策:双层验证体系
ADR-0002 的决策是将命令契约收敛为单一验证缝(validation seam),在两层执行:
- 快速 lint 脚本scripts/lint-command-contract.cjs——作为 CI 预测试步骤在毫秒级内跑完;
- 行为回归测试tests/command-contract.test.cjs——针对真实文件系统验证完整契约。
契约本身定义了什么是一个合法的commands/gsd/*.md文件,见 ADR-0002 决策记录。
五条逐文件规则:命令文件结构契约
lint 脚本对每个命令文件执行五条逐文件检查(源码见 lint-command-contract.cjs 的 check 函数):
name:字段必须存在、非空,且匹配gsd:*或gsd-*前缀(ns- 命令使用gsd-);description:字段必须存在且非空;allowed-tools:块必须存在、非空,且所有条目来自规范工具集;<execution_context>块内的每个 @-引用都必须解析为磁盘上真实存在的文件;<execution_context>块内的 @-引用必须独占一行(不得有行尾 prose)。
规范工具集(CANONICAL_TOOLS)
第三条规则中的"规范工具集"定义在 command-contract-helpers.cjs 中,作为单一事实来源供 lint 脚本与测试套件共享。新增一个规范工具只需改这里,两个消费方会自动同步:
const CANONICAL_TOOLS = new Set([ 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'Task', 'Agent', 'Skill', 'SlashCommand', 'AskUserQuestion', 'WebFetch', 'WebSearch', 'TodoWrite', 'mcp__context7__resolve-library-id', 'mcp__context7__query-docs', 'mcp__context7__*', ]);校验时还有一条通配豁免:任何以mcp__context7__开头的工具,只要CANONICAL_TOOLS包含mcp__context7__*即视为合法(见 check 函数第 3 条规则)。
一个符合契约的命令文件长什么样
仓库中的 commands/gsd/add-tests.md 是符合契约的完整示例。frontmatter 部分:
--- name: gsd:add-tests description: Generate tests for a completed phase based on UAT criteria and implementation argument-hint: "<phase> [additional instructions]" allowed-tools: - Read - Write - Edit - Bash - Glob - Grep - Agent - AskUserQuestion argument-instructions: | Parse the argument as a phase number (integer, decimal, or letter-suffix), plus optional free-text instructions. Example: /gsd:add-tests 12 Example: /gsd:add-tests 12 focus on edge cases in the pricing module requires: [phase] ---<execution_context>块则只声明一个独占一行的 @-引用:
<execution_context> @~/.claude/gsd-core/workflows/add-tests.md </execution_context>注意这里的关键转变:<execution_context>块现在是命令加载声明的唯一权威来源。ADR-0002 从 40 个命令文件中删除了<process>正文里重复的 prose @-引用(每次调用约回收 900 tokens),这些惰性副本不再被允许存在。
frontmatter 解析的 CRLF 容错
command-contract-helpers.cjs 的 parseFrontmatter 特意按/\r?\n/切分行,以兼容 Windows checkout(autocrlf=true)留下的行尾\r——若不做容错,lines.indexOf('---', 1)会因匹配到'---\r'而失败,导致整个 frontmatter 被解析为空、所有字段都被误报为缺失。它还支持 YAML 列表块的累积解析(续行以-开头的行追加到前一个键名下)。
@-引用提取与规范化
executionContextRefs 用正则匹配<execution_context>或<execution_context_extended>块,逐行处理:行以@开头才进入候选,取第一个空白分隔的 token,trailingProse标志当行内 token 之后还有内容时置位(对应第五条规则)。token 会剥掉~/、$HOME/、.claude/、gsd-core/等前缀做规范化,再拼接GSD_ROOT检查磁盘存在性(对应第四条规则)。
第六条规则:工作流可达性(仓库级检查)
第六条与前五条性质不同:它是一个仓库级可达性图,每次运行计算一次而非逐文件检查(源码见 checkWorkflowReachability)。
可达性语义
- 种子(loader):
commands/**、agents/**、skills/**下的每个 markdown 文件。docs/与测试夹具故意不算 loader——文档或测试里提一句路径并不等于任何运行时真的会加载它; - 遍历:从种子出发,沿引用边在
gsd-core/**(不止gsd-core/workflows/,因为references/或templates/文件也可能点名某个工作流路径)内做传递闭包; - 判定:任何
gsd-core/workflows/*.md文件若遍历结束后仍未被标记,即报告为 orphan(孤儿)。
关键设计是只从 loader 播种,绝不从工作流自身内容播种。一个只引用自己的工作流,或两个互相引用的工作流,都必须被判为不可达——因为没有任何命令、agent 或技能 loader 会真正打开它们。互引自洽的"岛"从内部看是连通的,但对外不可达,依然要被报告。
三种引用形态
workflowPathRefs 识别三种引用形状:
| 形态 | 示例 | 适用场景 |
|---|---|---|
| 急切 @-包含 | @~/.claude/gsd-core/workflows/x.md | 每次调用都内联。保留给命令始终需要的工作流 |
| 惰性路径 | `~/.claude/gsd-core/workflows/x.md` | 在使用点按需读取。flag 门控或条件工作流的默认选择 |
| 父级相对子文件 | execute-phase/steps/x.md | 既有工作流目录下的子文件 |
惰性形态是默认首选:急切 @-包含会在该命令的每次调用中都被内联进上下文,包括那些根本用不到该工作流的路径。渐进式披露拆分(#717)存在的目的就是把这份成本移出公共路径。
该解析器还有几处防御性设计:遍历段(..)被丢弃而不是上报(解析器永远只报告workflows/之下的路径);.md扩展名用负向前瞻(?![A-Za-z0-9_])锚定,杜绝.mdx、.md5被静默截断成看似合法的.md路径;结果去重且保持首见顺序。
可达性闭包算法
unreachableWorkflows 实现 BFS 闭包:把 loader 内容里解析出的所有工作流引用入队,出队时若未被访问则标记并递归入队该文件自身的引用,visited集合保证在有环(含自环/互环)情况下遍历必然终止。测试对以下场景逐一验证(见 command-contract.test.cjs):急切包含可达、惰性路径可达、父级相对可达、深度三层传递可达、自引用不可达、互引用岛不可达、环终止、docs-only 提及不可达、悬空引用到达不了任何东西、空工作流集、无 loader 则全部不可达、CRLF 容错、152 个文件中精确报告唯一孤儿等。
lint 脚本如何接入 CI
package.json 的 lint:ci 脚本 将node scripts/lint-command-contract.cjs串入 lint 流水线,位于测试套件之前运行:
npm run lint:cilint 脚本退出码语义:0 表示干净,1 表示有违规并输出诊断。它同时支持--root <dir>参数指向任意仓库根(测试套件正是用它驱动临时夹具目录做端到端验证)。正常输出形如:
ok lint-command-contract: 65 command files checked, 0 violations ok lint-command-contract: 151 workflow files, 151 reachable, 0 unreachable违规时向 stderr 输出逐文件、逐条违规明细,并提示查阅契约规范文档;不可达工作流则逐个列出gsd-core/<path>并说明"每个文件都会随所有运行时安装,却没有任何命令/agent/技能 loader 引用(直接或传递)——要么把它接上 loader,要么删除它"。
行为回归测试:全表面契约守护
tests/command-contract.test.cjs 是整条命令表面的权威行为契约测试,取代了enh-2790与bug-3135中的分散覆盖。它针对真实文件系统逐文件断言五条逐文件规则(name:、description:、allowed-tools:、@-引用可解析、@-引用独占一行),并包含三组重点回归:
- #3561 —— /gsd-map-codebase --fast 路由到可加载工作流:断言 commands/gsd/map-codebase.md 的
--fast路由行确实点名了一个可解析的workflows/scan.md,同时完整 map 路径不得急切加载scan.md(只有workflows/map-codebase.md一个 @-引用); - #3560 —— 真实孤儿会 fail 构建:用临时目录搭建最小夹具树(含一个合法命令文件加一个被急切加载的 live.md),分别验证"干净夹具通过""植入孤儿失败且诊断输出点名孤儿路径""仅 docs/ 提及的孤儿仍然失败""仅传递可达的孤儿通过"四种情形;
- #3560 —— 已删除的工作流不得残留:断言
discovery-phase.md、plan-milestone-gaps.md已从gsd-core/workflows/物理删除,且所有 install-tree 夹具清单不再包含它们,多语言 INVENTORY 文档也不再提及。
该文件还折入了原bug-3168-task-to-agent-rename的检查:命令、工作流、agent 的allowed-tools/toolsfrontmatter 中禁用Task(dispatcher 工具是Agent),工作流正文中不允许出现Task(调度调用(TaskCreate等任务跟踪器命名除外)。
接到"不可达工作流"报错怎么办
当npm run lint:ci(或直接node scripts/lint-command-contract.cjs)报告不可达工作流时,诊断形如:
ERROR lint-command-contract: 1 unreachable workflow file(s) gsd-core/workflows/scan.md ships to every runtime install tree, but no command, agent, or skill references it处理指引见 docs/how-to/resolve-unreachable-workflow-findings.md,只有两种正确解法:
解法一:接线(Wire it)——工作流是活的,缺的是引用
当某个命令、flag 或 agent 在文档上声明会使用该工作流时,选择此方案。在派发该工作流的 loader 中补一条引用,优先用惰性路径:
- If it is `--fast`: strip the flag, then read and execute `~/.claude/gsd-core/workflows/scan.md` (passing remaining args).若 loader 是commands/gsd/*.md,随后重新生成技能表面:
npm run gen:plugin-skills这正是scan.md的正确归宿——/gsd-map-codebase --fast是一个已发布、有文档的 flag,其路由行却点名了一个不可解析的路径。删除文件等于删掉一个活功能的唯一实现(#3561)。
解法二:删除(Delete it)——工作流确实已死
当没有东西应该引用它时(典型场景:命令已删除、工作流遗留),选择此方案。删除前先核实"声称的调用者"真实存在:discovery-phase工作流头部声称"由 plan-phase.md 的 mandatory_discovery 步骤调用",但该步骤并不存在;docs/INVENTORY.md声称它是/gsd-new-project的替代入口,new-project.md却从未引用过它。声称的调用者不是调用者。(discovery-phase在 #3560 中被删除。)
删除是五步清扫,任何一步缺失都会让树不一致:
- 移除
docs/INVENTORY.md中的行——全部五个语言版本(docs/INVENTORY.md及docs/ja-JP/、docs/ko-KR/、docs/zh-CN/、docs/pt-BR/),并检查各文件底部的说明性注释是否也点名了该工作流; - 重新生成 inventory 清单——先构建再生成,顺序反了会静默丢模块:
npm run build:lib && node scripts/gen-inventory-manifest.cjs --write - 重新生成 golden install-tree 夹具(19 个运行时都会列出每个已发布的工作流):
npm run gen:install-tree - 清扫点名该文件的测试——allowlist 与内容断言两类都要。两个陷阱:#3560 都踩过。按裸文件名键控的 allowlist 匹配不到全路径搜索(
tests/planner-language-regression.test.cjs中discovery-phase的陈旧条目就是路径式清扫漏掉的);断言文件存在或内容的测试会钉死它(tests/phase.test.cjs要求plan-milestone-gaps存在并检查其mkdir模式,删除文件在远端 runner 上变成四个红测试)。注意 lint-removed-but-needed.cjs 两类都抓不到——它扫描.github/workflows/、gsd-core/、docs/和package.json,不含tests/,需要自行搜索tests/; - 添加一个
Removed类型的 changeset 片段,并记得Removed类型要求伴随docs/变更——第 1 步已经满足。
最后确认树一致:
npm run lint:ci什么不算引用
可达性检查刻意只扫描commands/、agents/、skills/与gsd-core/**:
| 位置 | 算吗 | 原因 |
|---|---|---|
docs/** | 否 | 文档是对系统的断言,不是 loader。scan.md被docs/INVENTORY.md记录的同时完全不可达 |
tests/fixtures/install-tree/*.json | 否 | 发布清单证明文件会发布——这正是被报告的问题,而非反驳 |
| changeset 片段 | 否 | 历史记录,不是加载路径 |
如果企图通过在某个方便的地方加一条提及来让检查通过,那正是这套作用域要防的失败模式——文件仍然是死的,门禁却变绿了。
检查通过≠真的被用到:结构性可达的边界
第六条规则证明的是结构性可达:某个 loader 点名了该文件。它无法证明文件在任一条已执行路径上被真正读取。对照表:
| 现象 | 含义 |
|---|---|
151 workflow files, 151 reachable, 0 unreachable | 每个已发布工作流都被至少一个 loader 点名。不代表每个都被使用 |
| 仅从另一个不可达工作流可达的工作流 | 正确报告——可达性只从 loader 播种,不可达文件不能把可达性授予任何其他文件 |
| 只互相引用的两个工作流 | 两者都被报告。互引用岛不满足任何东西 |
| 引用自己的工作流 | 被报告。自引用不是 loader |
| 仅出现在围栏代码块内的路径 | 计为引用。这是刻意的高估:检查会 fail 构建,正确树上的一次误报比漏掉孤儿更糟,所以歧义引用朝"可达"倾斜 |
被 loader 点名却从未实际执行的工作流不在本检查范围内——那是本结构性检查不回答的语义问题。
关联能力:契约漂移的相邻防线
命令契约校验与仓库内其他漂移门禁互补。契约校验规则 4 负责 @-引用存在性("include miss"),而 scripts/check-contract-drift.cjs 负责另一方向的契约漂移检测;lint-allowed-tools-parity.cjs 也在lint:ci中与命令契约校验相邻运行,进一步约束工具清单的一致性。若想深入了解契约的完整规范,可直接阅读 ADR-0002 决策记录;若要理解 @-引用解析的更广上下文(含<required_reading>门禁标签与 agent 契约注册表),可从 command-contract-helpers.cjs 的readTagViolations与contractViolations实现入手继续探索。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
get-shit-done 命令契约校验(ADR-0002):以双层校验与前端字段规范锁定 65 个 gsd 斜杠命令的质量基线
get shit done 命令契约校验(ADR 0002):以双层校验与前端字段规范锁定 65 个 gsd 斜杠命令的质量基线 导读 本篇文章围绕 get s
人工智能AI 应用提示工程开发工具工作流自动化AI Agentget-shit-done ADR-0002 深度解析:用 lint + 回归测试两层机制集中校验 commands/gsd 命令契约
get shit done ADR 0002 深度解析:用 lint + 回归测试两层机制集中校验 commands/gsd 命令契约 本文以 get shit
人工智能AI 应用提示工程开发工具工作流自动化AI Agentoh-my-pi 嵌入式 Shell 内建命令体系:pi-builtins 双层架构、Host 契约与逐命令特性门控
oh my pi 嵌入式 Shell 内建命令体系:pi builtins 双层架构、Host 契约与逐命令特性门控 crates/pi builtins 是
人工智能AI Agent代码智能体工具调用CLIMCP Clients
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考