Spec Kit 在 Monorepo 中的多项目管理:目录作用域解析、SPECIFY_INIT_DIR 与非交互式定位
Spec Kit 项目是**目录作用域(directory-scoped)**的:任何包含 .specify/ 目录的目录就是一个独立的 Spec Kit 项目。因此一个 monorepo 根下可以并存多个互相独立的 Spec Kit 项目,各自拥有独立的 .specify/、specs/、constitution 与功能(feature)编号。本篇指南讲解 monorepo 布局如何组织、如何在成员项目间切换与从仓库根定位目标项目,并结合 spec-kit 仓库中的根解析实现(scripts/bash/common.sh、src/specify_cli/_project.py)与测试用例(tests/test_init_dir.py、tests/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 重定向),再按顺序解析:
SPECIFY_FEATURE_DIRECTORY环境变量(显式覆盖,相对路径会被规范化到项目根下,并持久化进feature.json);.specify/feature.json的feature_directory键(由 specify 命令持久化);- 两者皆无则报错,提示设置
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.sh、src/specify_cli/_project.py 及其测试 tests/test_init_dir.py、tests/test_init_dir_cli.py,变量完整契约见 docs/reference/core.md。
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 StartedRust0623
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