首页
/ Spec Kit 在 Monorepo 中的多项目管理:目录作用域解析、SPECIFY_INIT_DIR 与非交互式定位

Spec Kit 在 Monorepo 中的多项目管理:目录作用域解析、SPECIFY_INIT_DIR 与非交互式定位

2026-09-05 15:39:38作者:郦嵘贵Just

Spec Kit 项目是**目录作用域(directory-scoped)**的:任何包含 .specify/ 目录的目录就是一个独立的 Spec Kit 项目。因此一个 monorepo 根下可以并存多个互相独立的 Spec Kit 项目,各自拥有独立的 .specify/specs/、constitution 与功能(feature)编号。本篇指南讲解 monorepo 布局如何组织、如何在成员项目间切换与从仓库根定位目标项目,并结合 spec-kit 仓库中的根解析实现(scripts/bash/common.shsrc/specify_cli/_project.py)与测试用例(tests/test_init_dir.pytests/test_init_dir_cli.py)说明其“严格校验、绝不回退”的设计原理。

目录作用域与 monorepo 布局

Spec Kit 判定“项目在哪里”的依据不是 Git 仓库根,而是离你当前目录最近的 .specify/。根解析逻辑已经优先选择最近的 .specify/,而不是 Git toplevel——这一点从 Bash 侧的 get_repo_root 实现可以直接看到:

# Get repository root, prioritizing .specify directory
# This prevents using a parent repository when spec-kit is initialized in a subdirectory
get_repo_root() {
    # Explicit project override wins (see resolve_specify_init_dir).
    if [[ -n "${SPECIFY_INIT_DIR:-}" ]]; then
        resolve_specify_init_dir
        return
    fi

    # First, look for .specify directory (spec-kit's own marker)
    local specify_root
    if specify_root=$(find_specify_root); then
        echo "$specify_root"
        return
    fi

    # Final fallback to script location
    ...
}

其中 find_specify_root 从当前目录逐级向上查找第一个包含 .specify/ 的目录(到文件系统根为止),这正是“nearest wins”语义的落地。由于该优先级高于 Git toplevel,在成员项目目录内执行的命令会解析到该成员项目,而不是仓库根。

推荐的标准布局如下(一个 Git 仓库位于根目录,各成员项目分散在 apps/packages/ 下):

my-monorepo/
├── .git/                     # one Git repository at the root
├── apps/
│   ├── web/
│   │   └── .specify/         # Spec Kit project "web"
│   │       └── memory/constitution.md
│   └── api/
│       └── .specify/         # Spec Kit project "api"
│           └── memory/constitution.md
└── packages/
    └── ui/
        └── .specify/         # Spec Kit project "ui"

每个成员项目独立初始化

specify init apps/web --integration claude
specify init apps/api --integration claude

每个项目保留自己的 specs/ 目录,并独立编号功能:apps/web/specs/001-…apps/api/specs/001-… 互不干扰。这是目录作用域的直接推论——功能编号状态(如 .specify/feature.json 中记录的活动功能指针)都落在各自项目的 .specify/ 下,天然隔离。

在成员项目内工作:默认工作流不变

在成员项目内工作时,工作流与单项目完全一致:进入项目目录,然后在 Agent 中运行斜杠命令。根解析会自动找到最近的 .specify/

cd apps/web
# then run /speckit.specify, /speckit.plan, … in your agent

斜杠命令(/speckit.specify/speckit.plan/speckit.tasks 等)内部会调用随项目分发的脚本(Bash/PowerShell/Python 三套并行实现),这些脚本统一通过 get_repo_root 确定项目根,因此无需任何额外配置。

从仓库根定位成员项目:SPECIFY_INIT_DIR

对于非交互式或 CI 场景,如果不想 cd 进子目录,可以设置 SPECIFY_INIT_DIR 指向成员项目根(即包含 .specify/ 的那个目录)。相对路径基于当前目录解析:

# operate on apps/web from the monorepo root (no cd required)
export SPECIFY_INIT_DIR=apps/web

严格校验:绝不回退到当前目录

SPECIFY_INIT_DIR 的路径必须存在且包含 .specify/。校验失败时命令直接报错,绝不回退到当前目录或 Git toplevel——这是刻意设计:一个拼写错误绝不会把 spec 写到错误的项目里。

  • 路径不存在:按你输入的原样报告;
  • 路径存在但不是 Spec Kit 项目:报告其解析后的绝对路径。
# SPECIFY_INIT_DIR=apps/wbe  (typo: no such directory)
ERROR: SPECIFY_INIT_DIR does not point to an existing directory: apps/wbe

# SPECIFY_INIT_DIR=apps  (exists, but has no .specify/ of its own)
ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): /home/you/my-monorepo/apps

这段“错误文案 + 硬退出”的行为在三套实现中是刻意对齐的。Bash 侧见 resolve_specify_init_dir

resolve_specify_init_dir() {
    local init_root
    # Normalize: relative paths resolve against $(pwd); a trailing slash collapses.
    # CDPATH="" so a relative value cannot be resolved against the caller's CDPATH
    if ! init_root="$(CDPATH="" cd -- "$SPECIFY_INIT_DIR" 2>/dev/null && pwd)"; then
        echo "ERROR: SPECIFY_INIT_DIR does not point to an existing directory: $SPECIFY_INIT_DIR" >&2
        return 1
    fi
    if [[ ! -d "$init_root/.specify" ]]; then
        echo "ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): $init_root" >&2
        return 1
    fi
    printf '%s\n' "$init_root"
}

Python CLI 侧由 _resolve_init_dir_override 实现,docstring 明确写道它“应用与 shell 解析器相同的校验规则”,并在校验失败时 raise typer.Exit(1) 硬退出;它还特别说明了一个刻意的实现差异:Python 侧通过 Path.resolve() 规范化符号链接(物理路径),而 shell 侧 cd -- "$X" && pwd 保留逻辑路径,两者对非符号链接路径结果一致。

该行为有相当规模的回归测试覆盖:tests/test_init_dir.py 验证 shell 侧的“不存在路径报错不回退”(test_nonexistent_path_errors_no_fallback)、“无 .specify/ 报错不回退”(test_path_without_specify_errors_no_fallback)、相对路径按 cwd 规范化(test_relative_path_normalized_against_cwd)、尾斜杠容错(test_trailing_slash_tolerated)、对 cwd 内项目的优先级覆盖(test_precedence_over_cwd_project)以及未设置时保持原有 cwd 向上查找行为(test_unset_preserves_cwd_walk)等;PowerShell 侧有平行的 test_ps_* 系列用例。

两个定位轴:项目 × 功能

SPECIFY_INIT_DIR 选择项目SPECIFY_FEATURE_DIRECTORY 选择项目内的功能。二者独立、可以组合:同时设置即可非交互式地确定“哪个项目 + 哪个功能”。完整的变量契约见 docs/reference/core.md 的 Environment Variables 一节,其中明确了“Two resolution axes”模型:先项目(SPECIFY_INIT_DIR),后功能(SPECIFY_FEATURE_DIRECTORY.specify/feature.json)。

从源码看,功能目录解析优先级也印证了这一模型——scripts/bash/common.sh 中先取项目根(可被 SPECIFY_INIT_DIR 重定向),再按顺序解析:

  1. SPECIFY_FEATURE_DIRECTORY 环境变量(显式覆盖,相对路径会被规范化到项目根下,并持久化进 feature.json);
  2. .specify/feature.jsonfeature_directory 键(由 specify 命令持久化);
  3. 两者皆无则报错,提示设置 SPECIFY_FEATURE_DIRECTORY 或先运行 specify 命令。

CLI 子命令同样遵守该变量

specify CLI 的项目级子命令遵守同一变量与同一套校验规则(存在 + 含 .specify/,不回退 cwd),因此也能在不做 cd 的情况下从根目录操作成员项目:

export SPECIFY_INIT_DIR=apps/web
specify workflow list          # lists apps/web's workflows
specify integration status     # reports apps/web's integration

tests/test_init_dir_cli.py 专门验证这一表面:从非项目 cwd 重定向到兄弟项目(test_override_redirects_to_sibling_from_nonproject_cwd)、空字符串按未设置处理(test_empty_override_treated_as_unset)、非法值对 bundle/workflow run 等子命令同样硬报错不回退,以及符号链接场景下的行为约束。

SPECIFY_INIT_DIR 如何到达你的 Agent

SPECIFY_INIT_DIR 是由斜杠命令调用的 shell 脚本读取的(Bash 的 get_repo_root、PowerShell 的 Get-RepoRoot),只有当运行这些脚本的 shell 环境中存在该变量时才生效。这带来两类截然不同的使用场景:

  • 脚本化 / CI 运行:在驱动命令的同一个 shell 里 export,行为可靠;
  • 交互式 Agent:已导出的变量能否到达 Agent 所调用的 shell 工具,取决于具体 Agent。建议在启动 Agent 之前导出 SPECIFY_INIT_DIR,并验证一次(例如运行 /speckit.specify,确认新功能目录落在目标项目的 specs/ 下)。

Monorepo 中的 Git:项目作用域 vs 共享分支命名空间

Spec Kit 的项目文件作用域限定在解析出的项目根,但 Git 操作仍然在所在的 Git 工作树中执行。在根目录有一个 Git 仓库、项目位于子目录的 monorepo 中,创建功能分支会在共享的根仓库中创建或切换分支。spec 目录仍位于所选成员项目之下,而 Git 分支命名空间被整个 monorepo 共享。应在仓库根管理分支与提交;若希望各项目拥有隔离的分支命名空间,则为每个成员项目单独初始化 Git。

这一“文件作用域与 Git 作用域解耦”的特性源自 spec-kit 的设计:.specify/ 是项目标记,但分支操作继承当前工作树的 Git 仓库。git 扩展的功能分支创建脚本(extensions/git/scripts/bash/create-new-feature-branch.sh)也读取 SPECIFY_INIT_DIR,因此它同样支持“文件写在成员项目、分支落在共享仓库”的组合——tests/test_init_dir.py 中的 test_git_ext_create_feature_numbers_from_target 即验证了 git 扩展会按目标项目的 specs/ 独立编号功能。

由此产生的实际约束值得明确:所有成员项目共享一个分支命名空间,001-… 这类分支名在多项目并行开发时可能冲突;如果团队按项目并行开发,应自行规划分支前缀,或采用每项目独立 Git 仓库的隔离模式。

Constitution:每个项目一份,无内置继承

每个成员项目拥有自己的 .specify/memory/constitution.md/speckit.constitution 只编辑当前解析出的项目的本地文件。从源码看,constitution 路径完全由项目根派生,没有任何跨项目查找逻辑;因此 Spec Kit 不提供内置的 base/inheritance 机制

  • 如果你希望一份 constitution 引用 monorepo 其他位置的共享规则,需要自己维护这层“接线”(例如在文档中互相引用或复制);
  • 否则,为每个项目复制或同步共享的工程规则。

要点小结

  • 项目 = 目录.specify/ 是项目标记,根解析优先最近的 .specify/ 而非 Git toplevel,monorepo 下多项目天然隔离(specs/、constitution、feature 编号各自独立);
  • specify init <dir> 逐项目初始化,每个项目独立编号功能;
  • SPECIFY_INIT_DIR 提供“项目轴”非交互式定位:相对路径按 cwd 解析,必须存在且含 .specify/,否则硬报错、不回退——拼写错误永远不会污染错误的项目;
  • SPECIFY_FEATURE_DIRECTORY 提供“功能轴”,两轴组合可完全非交互式地定位“项目 + 功能”;
  • Git 作用域独立于项目作用域:单 Git 仓库意味着共享分支命名空间,需要隔离时按项目初始化 Git;
  • 实现证据集中在 scripts/bash/common.shsrc/specify_cli/_project.py 及其测试 tests/test_init_dir.pytests/test_init_dir_cli.py,变量完整契约见 docs/reference/core.md
登录后查看全文
热门项目推荐
相关项目推荐