Files
Pascal THUET 490566847c feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver (#3186)
* 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)
2026-07-01 15:55:18 -05:00

6.0 KiB

Core Commands

The core specify commands handle project initialization, system checks, and version information.

Initialize a Project

specify init [<project_name>]
Option Description
--integration <key> AI coding agent integration to use (e.g. copilot, claude, gemini). See the Integrations reference for all available keys
--integration-options Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")
--script sh|ps Script type: sh (bash/zsh) or ps (PowerShell)
--here Initialize in the current directory instead of creating a new one
--force Force merge/overwrite when initializing in an existing directory
--ignore-agent-tools Skip checks for AI coding agent CLI tools
--preset <id> Install a preset during initialization

Creates a new Spec Kit project with the necessary directory structure, templates, scripts, and AI coding agent integration files.

Note

Git repository initialization and branching are managed by the git extension, which is not installed by default. Run specify extension add git after init to enable git workflows.

Use <project_name> to create a new directory, or --here (or .) to initialize in the current directory. If the directory already has files, use --force to merge without confirmation.

When --integration is omitted, interactive terminals prompt you to choose an integration. Non-interactive sessions, such as CI or piped runs, default to GitHub Copilot; pass --integration <key> to choose a different integration explicitly.

Examples

# Create a new project with an integration
specify init my-project --integration copilot

# Initialize in the current directory
specify init --here --integration copilot

# Force merge into a non-empty directory
specify init --here --force --integration copilot

# Use PowerShell scripts (Windows/cross-platform)
specify init my-project --integration copilot --script ps

# Install a preset during initialization
specify init my-project --integration copilot --preset compliance

Environment Variables

Variable Description
SPECIFY_INIT_DIR Target a member project from outside its directory (e.g. a monorepo root) without cd, for non-interactive / CI use. Set it to the project root — the directory containing .specify/ (relative paths resolve against the current directory). The path must exist and contain .specify/, otherwise the command errors and does not fall back to the current directory. Resolved once in the core root helper (get_repo_root in Bash, Get-RepoRoot in PowerShell), so it is honored by the core feature scripts (/speckit.plan, /speckit.tasks, …) and the Git extension's feature-branch creation, which inherit it. The specify CLI applies the same validation rules to every project-scoped subcommand (specify integration …, specify extension …, specify workflow …, specify preset …, and the rest that operate on a .specify/ project), so those can target a member project too. When unset, Bash/PowerShell helpers keep their existing upward search; the specify CLI keeps its project-scoped resolver cwd-only unless a command explicitly defines broader detection (for example, bundle commands).
SPECIFY_FEATURE_DIRECTORY Override the active feature directory within the resolved project (takes precedence over .specify/feature.json). Relative paths resolve under the project root. Combine with SPECIFY_INIT_DIR to pick both the project and the feature non-interactively.
SPECIFY_FEATURE Override feature detection for non-Git repositories. Set to the feature directory name (e.g., 001-photo-albums) to work on a specific feature when not using Git branches. Must be set in the context of the agent prior to using /speckit.plan or follow-up commands.

Two resolution axes. SPECIFY_INIT_DIR selects the project (which directory contains .specify/); SPECIFY_FEATURE_DIRECTORY / .specify/feature.json select the feature within that project. They are independent — project first, then feature.

Symlinked project roots. SPECIFY_INIT_DIR relocates where the project is, not how a command treats symlinks: each command keeps its existing cwd-path stance. Commands that traverse and write project files through broad input paths (bundle, workflow run <file>) refuse a symlinked .specify/ to preserve write confinement. Other project-scoped commands keep their existing behavior when SPECIFY_INIT_DIR points at a project root, which may include following a symlinked .specify/.

Check Installed Tools

specify check

Checks that CLI-based AI coding agents are available on your system. IDE-based agents are skipped since they don't require a CLI tool.

This command stays offline. If a command behaves like an older Spec Kit version or an expected CLI feature is missing, run specify self check to check whether your local CLI is behind the latest release.

Version Information

specify version

Displays the Spec Kit CLI version, Python version, platform, and architecture.

To inspect local CLI capabilities without checking the network:

specify version --features
specify version --features --json

The JSON form is intended for scripts and coding agents that need to choose a workflow based on the installed CLI's supported features.

A quick version check is also available via:

specify --version
specify -V