首页
/ spec-kit Preset 预设系统详解:模板解析栈、组合策略与 `specify preset` 命令全景

spec-kit Preset 预设系统详解:模板解析栈、组合策略与 `specify preset` 命令全景

2026-09-04 15:08:29作者:钟日瑜

在 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 行,包含 PresetManifestPresetRegistryPresetManagerPresetCatalogPresetResolver 五大类)。

搜索可用预设: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 执行,要点有三:

  1. 兼容性检查check_compatibility() 读取 preset.ymlrequires.speckit_version,用 packaging.SpecifierSet 与当前版本比对,不满足时抛出 PresetCompatibilityError 并提示升级命令(见 presets/init.py)。
  2. 命令自动注册:若预设含 type: "command" 条目,命令会被注册到当前激活的 AI 编码代理集成目录中(如 .claude/commands/.gemini/commands/),并按代理类型渲染为 Markdown 或 TOML。
  3. 安装后调和(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.specifyspeckit.planspeckit.tasksspeckit.implementspeckit.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)。

启用 / 禁用:enabledisable

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 控制 searchadd 到哪里寻找预设。多个 catalog 按优先级顺序(数字小者先查)生效,解析顺序为(首个命中即停止):

  1. 环境变量SPECKIT_PRESET_CATALOG_URL 覆盖所有 catalog(等价于只用这一个 catalog);
  2. 项目配置.specify/preset-catalogs.yml
  3. 用户配置~/.specify/preset-catalogs.yml
  4. 内置默认 — 官方 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 需要时实时查栈;命令则用同一栈做替换与组合,但只在激活集成目录中物化一次——代理每次执行命令时不会重新解析栈。

从最高到最低优先级,解析栈为:

  1. Project-local overrides.specify/templates/overrides/
  2. Installed presets — 按 priority 排序(数字小者先查),位于 .specify/presets/‹id›/
  3. Installed extensions — 按 priority 排序,位于 .specify/extensions/‹id›/
  4. 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(脚本)被低优先级内容替换

脚本只支持 replacewrap——在 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 中按条目声明 strategyname 指定组合目标,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)是组合的执行者,其算法值得细读:

  1. 顶层是 replace 就直接返回——低层内容完全无关;
  2. 否则自高优先级向下找到最近的 replace 层作为"有效基座"(base),base 之下的层被忽略;
  3. 从 base 向上一层层应用策略:prependcontent = layer + "\n\n" + contentappendcontent = content + "\n\n" + layerwrap 则把占位符替换进基座内容;
  4. 对命令类型:逐层剥离 frontmatter 防止 YAML 元数据泄漏进正文,最终把最高优先级层的 frontmatter 重新装回;若其缺少 scripts / agent_scripts / argument-hint 键,会从 base 层继承;内部的 strategy 键一律剥除,不会出现在写给代理的命令文件里;
  5. 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)的处理流程:

  1. 只处理 type: "command" 条目;
  2. 若策略非 replace,先检查该预设是否是组合栈的顶层——是则预组合后写入 .composed/ 再注册;不是则先注册原始文件,随后立即执行 reconciliation 修正为正确内容;若没有可组合的基座(如要 wrap 的扩展未安装),警告并跳过该命令而不中止整个安装;
  3. 通过 resolve_active_agent_for_registration() 解析当前激活集成:项目没有 init-options.json(旧布局)时回退到"检测全部代理"的注册模式;文件存在但损坏时失败关闭(不注册任何内容),避免静默扩散;
  4. 注册时按代理格式渲染——Claude 系写 Markdown .md$ARGUMENTS 占位),Gemini/Qwen/Tabnine 写 TOML({{args}}),Copilot 写 .agent.md + 伴生 .prompt.md

更完整的架构图(含注册流程图、agent 格式表、constitution 生命周期)见 presets/ARCHITECTURE.md

Preset 清单规范:preset.yml 的硬性校验

创建预设时(可复制 presets/scaffold/ 脚手架起步),PresetManifestpresets/init.py)会对清单做严格校验,常见报错都源于此:

校验项 规则
schema_version 必须等于 "1.0"(字符串)
必备顶层字段 schema_versionpresetrequiresprovides
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。

相关文档

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384