spec-kit Preset 预设系统详解:模板解析栈、组合策略与 `specify preset` 命令全景
在 Spec-Driven Development 工作流中,Spec Kit 的 Preset(预设)机制允许你在不修改任何核心文件的前提下,替换模板、重写命令、统一团队术语,甚至对整个工作流做本地化改造。本文基于仓库内的官方参考文档与 src/specify_cli/presets/ 源码,系统讲解 specify preset 的完整命令族(search / add / remove / list / info / resolve / enable / disable / set-priority / catalog),深入剖析"优先级解析栈 + replace/prepend/append/wrap 组合策略"的底层实现,并给出可直接复现的实操示例,帮助你在团队中落地组织级规范、审计模板来源、调试多预设叠加时的文件胜出规则。
Preset 是什么:不改工具,只换内容
Preset 是对 Spec Kit 工作行为进行定制的最小单元——它以"模板、命令、脚本"三类文件为载体,覆盖 Spec Kit 生成的 artifacts(specs、plans、tasks、checklists、constitutions)以及指导 LLM 生成这些产物的命令。多个 Preset 可以同时安装,并按优先级叠加(stacking),互不冲突。
两类被覆盖对象的作用不同(见 presets/README.md):
- Templates(模板):定义"生成什么"——spec、plan、constitution 等 artifact 的骨架结构;
- Commands(命令):定义"LLM 如何生成"——逐步指导 AI 编码代理执行 SDD 流程的指令。
一个关键设计是:模板解析发生在运行时。安装时预设文件只是被拷贝进 .specify/presets/<id>/,Spec Kit 每次需要模板时都会重新遍历解析栈,而不是把模板合并到某个单一位置。未安装任何预设时,行为与"没有 Preset 系统"时完全一致——始终回落到核心模板。
Preset 全生命周期命令
所有 specify preset 子命令都要求项目已经通过 specify init 初始化。命令实现位于 src/specify_cli/presets/_commands.py,业务逻辑集中在 src/specify_cli/presets/init.py(约 5900 行,包含 PresetManifest、PresetRegistry、PresetManager、PresetCatalog、PresetResolver 五大类)。
搜索可用预设:specify preset search
specify preset search [query]
| 选项 | 说明 |
|---|---|
--tag |
按标签过滤 |
--author |
按作者过滤 |
不带 query 时列出所有可用预设。搜索会在所有激活的 catalog 中进行——因此 catalog 的管理直接决定了你能"看见"和"装到"什么,后文会展开。
安装预设:specify preset add
specify preset add [<preset_id>]
| 选项 | 说明 |
|---|---|
--dev <path> |
从本地目录安装(开发调试用) |
--from <url> |
从自定义 URL 安装,而不是从 catalog |
--priority <N> |
解析优先级(默认 10;数字越小优先级越高) |
安装行为由 PresetManager 执行,要点有三:
- 兼容性检查:
check_compatibility()读取preset.yml的requires.speckit_version,用packaging.SpecifierSet与当前版本比对,不满足时抛出PresetCompatibilityError并提示升级命令(见 presets/init.py)。 - 命令自动注册:若预设含
type: "command"条目,命令会被注册到当前激活的 AI 编码代理集成目录中(如.claude/commands/、.gemini/commands/),并按代理类型渲染为 Markdown 或 TOML。 - 安装后调和(reconciliation):安装与移除之后,系统会基于当前解析栈重新计算受影响命令名下的有效内容并写回;移除时甚至可能更新该预设曾写入的非活跃代理目录,以恢复幸存的命令/skill 层。非活跃集成不会收到这些命令文件,直到你通过
specify integration use <key>(或switch <key>)切换——切换时会为新的激活集成重新 scaffold 所有已启用预设。
官方 catalog 中的内置预设示例(presets/catalog.json)包含 lean(精简核心工作流命令)与 constitution-sync(为把"物化模板"当作受审产物的团队恢复安装期 constitution 物化)。以 lean 的清单 presets/lean/preset.yml 为例,它通过 5 个 type: "command" 条目覆盖 speckit.specify、speckit.plan、speckit.tasks、speckit.implement、speckit.constitution 五个核心命令,要求 speckit_version: ">=0.6.0"。
移除、列出、查询
# 移除:删除文件、注销其命令、清理注册表条目
specify preset remove <preset_id>
# 列出:显示版本、描述、模板数量与当前状态
specify preset list
# 详细信息:模板、元数据、标签
specify preset info <preset_id>
# 追踪某个名字最终解析到哪个文件
specify preset resolve <name>
specify preset list 的输出按解析/胜出顺序打印:优先级最高的预设(priority 数字最小)排在最前,priority 相同时按预设 id 字母序决胜——这与命令组合、模板解析使用的顺序完全一致,因此列表第一项就是重叠文件的最终胜者。这一语义在源码中由 PresetRegistry.list_by_priority() 实现:默认跳过 enabled: false 的预设,默认 priority 归一化为 10,排序键为 (priority, pack_id)(见 presets/init.py)。
启用 / 禁用:enable 与 disable
specify preset enable <preset_id>
specify preset disable <preset_id>
禁用不等于移除。两者的差别是排查行为问题时最常踩的坑:
| 操作 | 文件解析 | 已注册命令 | 文件/注册表 |
|---|---|---|---|
disable |
该预设被跳过,模板/脚本不再命中 | 保留在 AI 代理中(直到移除) | 保留 |
remove |
彻底消失 | 从所有代理目录注销并删除文件 | 全部清除 |
因此:如果只想临时对比"有/没有该预设"的模板输出,用 disable;如果希望命令层面的改动立即失效,必须 remove。随时可用 enable 重新启用。
调整优先级:set-priority
specify preset set-priority <preset_id> <priority>
数字越小越优先。当多个预设提供同名文件时,priority 数字最小者胜出。
Catalog:预设的发现与安装来源
Catalog 控制 search 和 add 到哪里寻找预设。多个 catalog 按优先级顺序(数字小者先查)生效,解析顺序为(首个命中即停止):
- 环境变量 —
SPECKIT_PRESET_CATALOG_URL覆盖所有 catalog(等价于只用这一个 catalog); - 项目配置 —
.specify/preset-catalogs.yml; - 用户配置 —
~/.specify/preset-catalogs.yml; - 内置默认 — 官方 catalog + 社区 catalog。
源码印证(PresetCatalog.get_active_catalogs(),见 presets/init.py):
- 环境变量路径返回单个
priority=1, install_allowed=True的自定义条目,且对非默认 URL 打印一次性警告:"Only use catalogs from sources you trust." - 项目配置整体替换默认栈(注意:不是与默认栈合并);
- 无配置时的内置栈为:
default(priority 1,允许安装)+community(priority 2,仅发现、不允许安装)。
Catalog 管理命令
# 列出所有激活 catalog(含优先级与安装权限)
specify preset catalog list
# 添加 catalog
specify preset catalog add <url>
# 按名称移除
specify preset catalog remove <name>
catalog add 的完整参数:
| 选项 | 说明 |
|---|---|
--name <name> |
必填,catalog 唯一名称 |
--priority <N> |
优先级(默认 10;数字小者优先) |
--install-allowed / --no-install-allowed |
是否允许从该 catalog 安装预设(默认仅发现) |
--description <text> |
可选描述 |
添加操作会写入项目的 .specify/preset-catalogs.yml,示例:
catalogs:
- name: "my-org-presets"
url: "https://example.com/preset-catalog.json"
priority: 5
install_allowed: true
description: "Our approved presets"
从源码结构看,每个 catalog 条目是 PresetCatalogEntry(url, name, priority, install_allowed, description) 数据类(presets/init.py),catalog JSON 按 URL 做 SHA256 哈希生成独立缓存文件(presets/init.py),缓存有效期为 1 小时。此外,对 GitHub 域名的请求会自动附带 GH_TOKEN / GITHUB_TOKEN(适用于私有仓库托管的 catalog 或预设 ZIP),非 GitHub URL 一律不带凭据。
文件解析:优先级栈与组合策略
四层解析栈
Preset 可以提供三类文件:command 文件、template 文件(如 plan-template.md)、script 文件。每个文件名字在栈中独立求值,所以不同文件完全可以来自不同层。模板与脚本在 Spec Kit 需要时实时查栈;命令则用同一栈做替换与组合,但只在激活集成目录中物化一次——代理每次执行命令时不会重新解析栈。
从最高到最低优先级,解析栈为:
- Project-local overrides —
.specify/templates/overrides/ - Installed presets — 按 priority 排序(数字小者先查),位于
.specify/presets/‹id›/ - Installed extensions — 按 priority 排序,位于
.specify/extensions/‹id›/ - Spec Kit core —
.specify/templates/
flowchart TB
subgraph stack [" "]
direction TB
A["最高优先级<br/><br/>1. Project-local overrides<br/>.specify/templates/overrides/"]
B["2. Presets — 按 priority<br/>.specify/presets/‹id›/"]
C["3. Extensions — 按 priority<br/>.specify/extensions/‹id›/"]
D["4. Spec Kit core<br/>.specify/templates/<br/>最低优先级"]
end
A --> B --> C --> D
各层内文件按类型组织在子目录中:
| 类型 | 子目录 | 覆盖路径 |
|---|---|---|
| Templates | templates/ |
.specify/templates/overrides/ |
| Commands | commands/ |
.specify/templates/overrides/ |
| Scripts | scripts/ |
.specify/templates/overrides/scripts/ |
一次典型的运行时解析(请求 plan-template.md):
flowchart TB
A["请求文件:<br/>plan-template.md"] --> B{"存在项目本地 override?"}
B -- 是 --> Z["✓ 使用此文件"]
B -- 否 --> C{"Preset: compliance<br/>(priority 5)"}
C -- 是 --> Z
C -- 否 --> D{"Preset: team-workflow<br/>(priority 10)"}
D -- 是 --> Z
D -- 否 --> E{"Extension 文件?"}
E -- 是 --> Z
E -- 否 --> F["Spec Kit core"]
F --> Z
实操示例:
specify preset add compliance --priority 5
specify preset add team-workflow --priority 10
对于两者都提供的文件,compliance(5 < 10)胜出;只有一方提供的文件用那一方的;都没有的用 core 默认。
四种组合策略
默认策略是 replace:栈中第一个命中的文件整体生效。模板与命令还支持组合策略,让预设"增强"而非"替换"低优先级内容:
| 策略 | 行为 | 模板 | 命令 | 脚本 |
|---|---|---|---|---|
replace(默认) |
完全替换低优先级内容 | ✓ | ✓ | ✓ |
prepend |
放在低优先级内容之前(空行分隔) | ✓ | ✓ | — |
append |
放在低优先级内容之后(空行分隔) | ✓ | ✓ | — |
wrap |
内容中的占位符 {CORE_TEMPLATE}(模板/命令)或 $CORE_SCRIPT(脚本)被低优先级内容替换 |
✓ | ✓ | ✓ |
脚本只支持 replace 与 wrap——在 presets/init.py 中定义:
VALID_PRESET_TEMPLATE_TYPES = {"template", "command", "script"}
VALID_PRESET_STRATEGIES = {"replace", "prepend", "append", "wrap"}
# Scripts only support replace and wrap (prepend/append don't make semantic sense for executable code)
VALID_SCRIPT_STRATEGIES = {"replace", "wrap"}
在 preset.yml 中按条目声明 strategy(name 指定组合目标,file 指向实际内容文件,二者可以不同):
provides:
templates:
- type: "template"
name: "spec-template"
file: "templates/spec-addendum.md"
strategy: "append" # 在 core 模板之后追加内容
组合是递归链式的:例如一个 prepend 的 security 预设加一个 append 的 compliance 预设,最终产物是"security 头 + core 内容 + compliance 尾"。
源码透视:resolve_content() 如何组合多层
PresetResolver.resolve_content()(presets/init.py)是组合的执行者,其算法值得细读:
- 顶层是 replace 就直接返回——低层内容完全无关;
- 否则自高优先级向下找到最近的 replace 层作为"有效基座"(base),base 之下的层被忽略;
- 从 base 向上一层层应用策略:
prepend为content = layer + "\n\n" + content,append为content = content + "\n\n" + layer,wrap则把占位符替换进基座内容; - 对命令类型:逐层剥离 frontmatter 防止 YAML 元数据泄漏进正文,最终把最高优先级层的 frontmatter 重新装回;若其缺少
scripts/agent_scripts/argument-hint键,会从 base 层继承;内部的strategy键一律剥除,不会出现在写给代理的命令文件里; wrap层若缺少占位符会抛出PresetValidationError,错误信息明确指出缺失的是{CORE_TEMPLATE}还是$CORE_SCRIPT。
而 resolve()(presets/init.py)在栈内查找单个文件时还有两个容易忽略的细节:
- manifest 优先于约定路径:只要预设的
preset.yml声明了某(name, type)条目,就必须解析其file:字段;声明了但文件缺失时不回落到约定路径templates/<name>.md,以免掩盖拼写错误或捡到未声明的散落文件; - 第 5 层兜底:项目
.specify/templates/未命中时,还会查 wheel 安装内的 core_pack 或源码检出仓库根下的templates/,保证wrap策略总能定位到真正的{CORE_TEMPLATE}来源。resolve_core()则是跳过预设层(tier 2)的特化变体,防止 wrap 时误把另一个预设的 wrap 产物当成 core。
调试利器:specify preset resolve
specify preset resolve spec-template
specify preset resolve speckit.specify
该命令调用 collect_all_layers() 打印完整解析栈(presets/_commands.py):
- 显示最高优先级层的路径与来源(
top layer from: ...); - 检测到组合时,额外输出 Composition chain——从有效 base 层向上逐层列出
[base]、[prepend]、[append]、[wrap]标签与各层来源路径; - 若组合无法产出结果(没有任何 replace 基座),给出黄色警告;
- 名字中不含点的按 template 解析,含点的按 command 解析(如
speckit.git.feature)。
多个预设提供同名文件时,这条命令就是判断"到底谁赢"的唯一权威手段。
命令注册与集成目录
模板是运行时解析的,命令则不同:命令覆盖在安装时物化。PresetManager._register_commands()(presets/init.py)的处理流程:
- 只处理
type: "command"条目; - 若策略非 replace,先检查该预设是否是组合栈的顶层——是则预组合后写入
.composed/再注册;不是则先注册原始文件,随后立即执行 reconciliation 修正为正确内容;若没有可组合的基座(如要 wrap 的扩展未安装),警告并跳过该命令而不中止整个安装; - 通过
resolve_active_agent_for_registration()解析当前激活集成:项目没有init-options.json(旧布局)时回退到"检测全部代理"的注册模式;文件存在但损坏时失败关闭(不注册任何内容),避免静默扩散; - 注册时按代理格式渲染——Claude 系写 Markdown
.md($ARGUMENTS占位),Gemini/Qwen/Tabnine 写 TOML({{args}}),Copilot 写.agent.md+ 伴生.prompt.md。
更完整的架构图(含注册流程图、agent 格式表、constitution 生命周期)见 presets/ARCHITECTURE.md。
Preset 清单规范:preset.yml 的硬性校验
创建预设时(可复制 presets/scaffold/ 脚手架起步),PresetManifest(presets/init.py)会对清单做严格校验,常见报错都源于此:
| 校验项 | 规则 |
|---|---|
schema_version |
必须等于 "1.0"(字符串) |
| 必备顶层字段 | schema_version、preset、requires、provides |
preset.id |
小写字母/数字/连字符(正则 ^[a-z0-9-]+$) |
preset.version |
合法语义化版本,且必须是带引号的字符串(version: 1.0 会被 YAML 解析成 float 而报错) |
requires.speckit_version |
非空字符串,作为 PEP 440 specifier 使用 |
provides.templates |
至少一条;每条必须含 type / name / file |
type 取值 |
template / command / script |
strategy 取值 |
replace / prepend / append / wrap(脚本仅 replace / wrap);自动转小写持久化 |
| 命令名 | 点分小写(^[a-z0-9.-]+$,如 speckit.specify);其他类型不含点 |
file 路径 |
必须是预设目录内的相对路径,禁止 .. 上跳 |
| 重复声明 | 同一 (name, type) 不得重复,否则后续条目将永远不可达 |
安装本地开发版验证:
# 1. 复制 scaffold 并编辑 preset.yml
# 2. 本地安装
specify preset add --dev ./my-preset
# 3. 验证解析结果
specify preset resolve spec-template
常见疑问(FAQ)
可以同时使用多个预设吗?
可以。预设按 priority 叠加——每个文件独立地从"提供它且优先级最高"的来源解析。用 specify preset set-priority 控制顺序。
如何确认某个名字实际生效的是哪个文件?
运行 specify preset resolve <name> 追踪解析栈,查看胜出文件;组合场景下还会打印完整的 Composition chain。
disable 和 remove 到底差在哪?
disable 保留安装、仅将其排除在模板/脚本解析之外,之前注册的命令仍留在 AI 代理中,直到你真正移除该预设——所以命令层面的变更需要 remove 才会失效。disable 适合临时对照"有/无预设"的模板或脚本输出差异,随时 enable 恢复。remove 则是完全卸载:删除文件、注销命令、清除注册表条目。
预设由谁维护? 绝大多数预设由其作者独立创建和维护。Spec Kit 维护者不审查、不审计、不背书、不支持预设代码本身(对 community catalog 仅验证条目完整性与格式正确)。安装前请自行审查预设源码,风险自担;具体问题请联系其作者或在对应仓库提 issue。
相关文档
- 用户指南与快速上手:presets/README.md
- 内部架构(解析/注册/catalog 流程图、模块结构):presets/ARCHITECTURE.md
- 预设上架指南:presets/PUBLISHING.md
- 参考文档(本文主体来源):docs/reference/presets.md
- 核心实现:src/specify_cli/presets/init.py(
PresetResolver/PresetManager/PresetCatalog)与 src/specify_cli/presets/_commands.py
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 StartedRust0622
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