mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
* feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver The shell resolver honors SPECIFY_INIT_DIR (#2892), but the Python CLI did not: it resolved the project as Path.cwd() + a .specify/ check and never read the override. So setup-plan.sh respected it while `specify integration install` ignored it, and you still had to cd into the member project. Route project resolution through a shared _resolve_init_dir_override() that applies the shell resolver's validation rules (relative to cwd, must exist and contain .specify/, hard error, no fallback, same error strings). It's wired into _require_specify_project() — the chokepoint for every project-scoped subcommand (integration/extension/workflow/preset/...) — and the `workflow run <file>` standalone path, which re-applies its symlinked-.specify guard on the override branch too. init is unchanged: it creates .specify/, so the must-pre-exist rule doesn't apply. The resolver canonicalizes symlinks via Path.resolve() while the shell keeps the logical path; they agree for non-symlinked paths (documented in the resolver). Tests in tests/test_init_dir_cli.py mirror the strict cases from test_init_dir.py through the CLI; conftest now strips SPECIFY_* for the whole suite so a stray export can't perturb the now-env-reading resolver. Docs note the CLI applies the same rules. Discussion: github/spec-kit#2834 (Disclosure: I used an AI coding agent to audit the call sites and resolver, draft the change, and run an adversarial code review; reviewed by me.) * fix(cli): honor SPECIFY_INIT_DIR for bundle commands Assisted-by: Codex (model: GPT-5, autonomous) * fix(bundler): refuse symlinked .specify on the SPECIFY_INIT_DIR override path find_project_root refuses a symlinked .specify (following it could read/write outside the tree, and a test pins that), but the SPECIFY_INIT_DIR override added for bundle commands returned early and skipped that guard: _resolve_init_dir_override validates .specify with is_dir(), which follows symlinks. So `specify bundle` accepted via the override a layout the cwd path rejects. Re-check the override result with the same guard, plus a regression test. (Disclosure: found via an AI code review and fixed with an AI coding agent; reviewed by me.) * fix(cli): keep SPECIFY_INIT_DIR strict for bundles Treat an explicit symlinked SPECIFY_INIT_DIR project as a hard bundle error instead of returning no project, which could initialize the current directory. Align the docs with the actual unset resolver behavior. Assisted-by: Codex (model: GPT-5, autonomous) * docs(core): note symlinked .specify handling differs across CLI surfaces A symlinked .specify is followed by integration/extension/workflow (matching the shell resolver) but refused by bundle and workflow run <file> (write confinement). Document the asymmetry so it reads as intentional. (Disclosure: AI-assisted; reviewed by me.) * docs(core): reframe symlinked .specify note around the override invariant Per maintainer feedback on #3186: SPECIFY_INIT_DIR relocates where the project is, not how a surface treats symlinks. Each surface keeps its cwd-path stance (write surfaces refuse a symlinked .specify, read/config surfaces follow it), so the split is one policy relocated, not an inconsistency. * docs: address Copilot review on resolver docstrings - _project.py: the error messages "mirror" the shell wording rather than "match" it (the CLI renders a Rich `Error:` line, the shell a plain `ERROR:`). - find_project_root: document that honoring SPECIFY_INIT_DIR when start is None can raise typer.Exit / BundlerError, so the Path | None signature isn't surprising to direct callers. * docs(bundler): note require_project_root inherits the override raise behavior find_project_root can raise typer.Exit / BundlerError under the SPECIFY_INIT_DIR override (start=None); require_project_root inherits that, so document it alongside its own BundlerError-on-missing-project. * docs: clarify symlinked project root behavior Assisted-by: OpenAI Codex (model: GPT-5, autonomous) * Address SPECIFY_INIT_DIR review feedback Assisted-by: OpenAI Codex (model: GPT-5, autonomous) * Route workflow JSON errors to stderr Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
124 lines
4.9 KiB
Markdown
124 lines
4.9 KiB
Markdown
# Using Spec Kit in a Monorepo
|
|
|
|
A Spec Kit project is **directory-scoped**: the project is whichever directory
|
|
contains `.specify/`. A monorepo can hold several independent Spec Kit projects
|
|
under one repository root, each with its own `.specify/`, `specs/`, constitution,
|
|
and feature numbering.
|
|
|
|
Root resolution already prefers the **nearest** `.specify/` over the Git
|
|
toplevel, so commands run from inside a member project resolve to that project,
|
|
not the repo root.
|
|
|
|
## Layout
|
|
|
|
```text
|
|
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"
|
|
```
|
|
|
|
Initialize each member project independently:
|
|
|
|
```bash
|
|
specify init apps/web --integration claude
|
|
specify init apps/api --integration claude
|
|
```
|
|
|
|
Each project keeps its own `specs/` directory and numbers features
|
|
independently (`apps/web/specs/001-…`, `apps/api/specs/001-…`).
|
|
|
|
## Working inside a member project
|
|
|
|
The default workflow is unchanged: change into the project directory and run the
|
|
slash commands. Root resolution finds the nearest `.specify/`.
|
|
|
|
```bash
|
|
cd apps/web
|
|
# then run /speckit.specify, /speckit.plan, … in your agent
|
|
```
|
|
|
|
## Targeting a member project from the repo root
|
|
|
|
For non-interactive or CI runs where you do not want to `cd`, set
|
|
**`SPECIFY_INIT_DIR`** to the member project root (the directory *containing*
|
|
`.specify/`). Relative paths resolve against the current directory.
|
|
|
|
```bash
|
|
# operate on apps/web from the monorepo root (no cd required)
|
|
export SPECIFY_INIT_DIR=apps/web
|
|
```
|
|
|
|
The path must exist and contain `.specify/`. If it does not, the command
|
|
**errors and does not fall back** to the current directory or the Git toplevel.
|
|
This is deliberate: a typo never writes specs into the wrong project. A
|
|
nonexistent path is reported as you typed it; a path that exists but is not a
|
|
Spec Kit project is reported as its resolved absolute path:
|
|
|
|
```text
|
|
# 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
|
|
```
|
|
|
|
`SPECIFY_INIT_DIR` selects the **project**; `SPECIFY_FEATURE_DIRECTORY` selects
|
|
the **feature** within it. They compose: set both to pick a project and a
|
|
feature non-interactively. See the
|
|
[`SPECIFY_INIT_DIR` reference](../reference/core.md#environment-variables) for
|
|
the full contract and the two-axes model.
|
|
|
|
The `specify` CLI's project-scoped subcommands honor the same variable, so they
|
|
target a member project from the root without `cd` too:
|
|
|
|
```bash
|
|
export SPECIFY_INIT_DIR=apps/web
|
|
specify workflow list # lists apps/web's workflows
|
|
specify integration status # reports apps/web's integration
|
|
```
|
|
|
|
The validation rules are the same: the path must exist and contain `.specify/`,
|
|
with no fallback to the current directory.
|
|
|
|
## How `SPECIFY_INIT_DIR` reaches your agent
|
|
|
|
`SPECIFY_INIT_DIR` is read by the shell scripts that the slash commands invoke
|
|
(`get_repo_root` in Bash, `Get-RepoRoot` in PowerShell). It takes effect only
|
|
when it is present in the environment of the shell that runs those scripts.
|
|
|
|
- **Scripted / CI runs:** export it in the same shell that drives the commands;
|
|
it is reliable there.
|
|
- **Interactive agents:** whether an exported variable reaches the shell tool an
|
|
agent uses is agent-specific. Export `SPECIFY_INIT_DIR` *before* launching the
|
|
agent, and verify once (e.g. run `/speckit.specify` and confirm the new feature
|
|
landed under the intended project's `specs/`).
|
|
|
|
## Git in a monorepo
|
|
|
|
> [!NOTE]
|
|
> Spec Kit project files are scoped to the **resolved project root**, but Git
|
|
> operations still run in the containing Git work tree. In a monorepo with a
|
|
> single Git repository at the root and projects in subdirectories, feature
|
|
> branch creation creates or switches branches in the shared root repository.
|
|
> Spec directories still live under the selected member project, while the Git
|
|
> branch namespace is shared by the whole monorepo. Manage branches and commits
|
|
> at the repository root, or initialize Git per member project if you want
|
|
> isolated per-project branch namespaces.
|
|
|
|
## Constitutions
|
|
|
|
Each member project has its own `.specify/memory/constitution.md` and
|
|
`/speckit.constitution` edits the local project's file. Spec Kit does not provide
|
|
a built-in base/inheritance mechanism; if you want one constitution to reference
|
|
shared rules elsewhere in the monorepo, you need to maintain that wiring yourself.
|
|
Otherwise, duplicate or sync shared engineering rules per project.
|