Read OSS

Spec Kit 架构解析:一个 CLI 如何编排 AI 驱动的开发流程

中级

前置知识

  • Python 打包基础知识(pyproject.toml、wheel、entry points)
  • 熟悉 CLI 框架(typer 或 click)
  • 对 AI 编程助手有基本了解(Claude、Copilot、Gemini 等)

Spec Kit 架构解析:一个 CLI 如何编排 AI 驱动的开发流程

每款 AI 编程助手都有自己的配置目录、命令格式和接收指令的方式。如果你想同时为 Claude Code、Copilot、Gemini CLI、Cursor 以及二十多款其他 agent 提供结构化的工作流指引,就会面临格式各异、文件爆炸的问题。GitHub 的 Spec Kit 用一个 CLI 解决了这一难题——specify 命令将一套共享的工作流模板转换为面向 25 款以上助手的专属指令。本文将梳理这背后的架构设计。

什么是 Spec Kit 和规范驱动开发?

规范驱动开发(Spec-Driven Development,SDD)颠覆了规范与代码之间的传统关系。传统模式下,规范只是搭建实现的脚手架,用完即弃;而 SDD 将规范本身视为核心产出物,代码是规范的具体表达,而非相反。

Spec Kit 通过四个阶段的工作流将这一理念落地:

flowchart LR
    A["/speckit.specify"] --> B["/speckit.plan"]
    B --> C["/speckit.tasks"]
    C --> D["/speckit.implement"]
    A -.->|"clarify"| E["/speckit.clarify"]
    E -.-> A

每个阶段都是一条由 AI agent 执行的斜杠命令。specify 命令将自然语言描述的功能需求转化为结构化规范;plan 命令将规范转化为技术实现方案;tasks 命令将方案拆解为有序的任务列表;implement 命令则逐一执行这些任务。AI agent 既是这些命令的读取者,也是执行者——markdown 文件本身就是程序,LLM 就是运行时。

specify CLI 工具本身并不执行这些工作流,它的职责是引导项目初始化——为开发者所使用的 AI 助手生成正确格式的配置文件。你可以把它理解为一个编译器,从同一份源码生成面向 25 款以上指令集架构的目标产物。

目录结构与模块职责

代码仓库在 Python CLI 包与其分发内容之间有清晰的职责划分:

目录 职责
src/specify_cli/ Python 包——CLI 命令、集成系统、extension/preset 管理器
templates/commands/ 9 个斜杠命令模板(带 YAML frontmatter 的 markdown)
templates/*.md 文档模板(spec、plan、tasks、constitution、checklist)
scripts/bash/scripts/powershell/ 命令模板调用的 shell 脚本
extensions/git/ 内置 git extension——5 个命令 + 18 个生命周期钩子
presets/lean/ 内置 "lean" preset——轻量版工作流模板
tests/ pytest 测试套件,结构与 integration 子包保持镜像
docs/ DocFX 文档站点源文件

src/specify_cli/ 下的 Python 包模块结构简洁:

graph TD
    INIT["__init__.py<br/>(4,143 lines — CLI + TUI + orchestration)"]
    INT["integrations/<br/>__init__.py + base.py + 27 agent subpackages"]
    MAN["integrations/manifest.py<br/>(SHA-256 file tracking)"]
    EXT["extensions.py<br/>(ExtensionManifest, Registry, Manager)"]
    PRE["presets.py<br/>(PresetManifest, Registry, Manager)"]
    AGT["agents.py<br/>(CommandRegistrar — bridges extensions to agents)"]

    INIT --> INT
    INIT --> EXT
    INIT --> PRE
    EXT --> AGT
    PRE --> AGT
    INT --> MAN

整个项目以 __init__.py 为核心向外辐射。integrations 包负责 agent 抽象层;extensions 和 presets 是两套并行的插件系统,均通过 agents.py 作为桥接层,输出各 agent 专属的格式。

巨型 __init__.py——为什么这样设计

src/specify_cli/__init__.py 共 4,143 行,是整个项目的重力中心,包含:

  • main() 入口和 Typer app 配置
  • 所有 CLI 命令:initcheckversionextensionpresetintegration
  • 进度显示的 TUI 组件 StepTracker
  • 交互选择组件 select_with_arrows()
  • 共享基础设施安装函数(_install_shared_infra_locate_core_pack
  • 适配 wheel 安装和源码检出两种场景的资源路径解析逻辑

这种单文件的设计是一种有意为之的取舍。对于通过 uv tool install 安装的 CLI 工具而言,启动速度至关重要。单个模块意味着 import 时的文件系统查找次数更少。真正的拆分发生在这个文件的周围:integration 类层次结构位于 integrations/base.py,文件清单追踪位于 integrations/manifest.py,extension 和 preset 系统则各有独立模块。

入口本身非常简洁——__init__.py#L4139-L4143

def main():
    app()

if __name__ == "__main__":
    main()

app 对象是一个 Typer 实例,使用了自定义的 BannerGroup,会在帮助信息输出前显示 ASCII banner——__init__.py#L301-L307

app = typer.Typer(
    name="specify",
    help="Setup tool for Specify spec-driven development projects",
    add_completion=False,
    invoke_without_command=True,
    cls=BannerGroup,
)

提示: 初次探索代码库时,在 __init__.py 中搜索 @app.command() 装饰器,即可快速定位所有 CLI 命令。大约有十几个,每个对应一个独立的子命令,如 initextension addpreset list 等。

模块依赖图与引导链

当用户执行 specify init 时,import 链会触发一系列级联操作,在第一行命令逻辑执行之前,25 款以上的 integration 已全部完成注册。流程如下:

flowchart TD
    A["specify (entry point)"] --> B["specify_cli:main()"]
    B --> C["__init__.py module loads"]
    C --> D["_build_agent_config()"]
    D --> E["from .integrations import INTEGRATION_REGISTRY"]
    E --> F["integrations/__init__.py loads"]
    F --> G["_register_builtins()"]
    G --> H["Imports 27 agent subpackages"]
    H --> I["_register() for each → populates INTEGRATION_REGISTRY"]
    I --> J["AGENT_CONFIG dict built from registry"]

关键函数是 _build_agent_config(),在模块级别调用:

def _build_agent_config() -> dict[str, dict[str, Any]]:
    """Derive AGENT_CONFIG from INTEGRATION_REGISTRY."""
    from .integrations import INTEGRATION_REGISTRY
    config: dict[str, dict[str, Any]] = {}
    for key, integration in INTEGRATION_REGISTRY.items():
        if integration.config:
            config[key] = dict(integration.config)
    return config

AGENT_CONFIG = _build_agent_config()

这意味着 INTEGRATION_REGISTRY 必须在 __init__.py 加载完成之前全部填充完毕。这一工作由 _register_builtins() 完成——它按字母顺序 import 每个 integration 子包并逐一注册。integrations/__init__.py 的最后一行无条件触发它:

_register_builtins()

这种"import 时即注册"的模式确保了注册表始终完整一致,不存在部分初始化的风险。代价是:若新增的 integration 存在语法错误,整个 CLI 将无法启动。

agents.py 模块则采用了类似的懒加载初始化方式。其 CommandRegistrar 类在首次访问时从注册表构建 AGENT_CONFIGS,并通过 try/except 处理模块加载期间可能出现的循环导入问题——agents.py#L30-L57

离线打包与分发

企业环境往往在安装阶段无法访问外网。Spec Kit 通过 Hatch 的 force-include 机制解决这一问题,将所有运行时资源直接打包进 Python wheel。

相关配置位于 pyproject.toml#L28-L45

[tool.hatch.build.targets.wheel.force-include]
"templates/agent-file-template.md" = "specify_cli/core_pack/templates/agent-file-template.md"
"templates/commands" = "specify_cli/core_pack/commands"
"scripts/bash" = "specify_cli/core_pack/scripts/bash"
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
"extensions/git" = "specify_cli/core_pack/extensions/git"
"presets/lean" = "specify_cli/core_pack/presets/lean"

构建时,Hatch 将仓库中的 templates/scripts/extensions/presets/ 目录复制到 wheel 内的 specify_cli/core_pack/ 下。运行时,_locate_core_pack() 负责判断使用哪条路径:

def _locate_core_pack() -> Path | None:
    candidate = Path(__file__).parent / "core_pack"
    if candidate.is_dir():
        return candidate
    return None
flowchart TD
    A["_locate_core_pack()"] --> B{"core_pack/ exists?"}
    B -->|"Yes (wheel install)"| C["Use specify_cli/core_pack/"]
    B -->|"No (source checkout)"| D["Fallback to repo root: templates/, scripts/"]

所有需要访问资源的函数——_install_shared_infra()_locate_bundled_extension()_locate_bundled_preset()——都遵循这种双路径模式。回退到仓库相对路径的机制意味着,从源码运行(uv run specify init)的开发者无需先构建 wheel,就能获得一致的行为体验。

提示: 遇到资源路径问题时,检查 specify_cli 安装目录下是否存在 core_pack/。若不存在,说明你在以源码模式运行,这在开发阶段完全正常,但资源路径的解析方式会有所不同。

下一步

至此,我们已梳理了整体架构——单体 CLI 核心、integration 注册表的引导机制,以及离线打包方案。接下来,我们将深入系统中最关键的命令。在第二篇中,我们将完整追踪 specify init 的执行路径:从 17 个 CLI 参数,经过交互式 TUI,到将空目录转化为完整 SDD 项目脚手架的 8 步编排流水线。