Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Codebuddy CLI](https://www.codebuddy.ai/cli), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Pi Coding Agent](https://pi.dev)
- [uv](https://docs.astral.sh/uv/) for package management (recommended) or [pipx](https://pipx.pypa.io/) for persistent installation
- [Python 3.11+](https://www.python.org/downloads/)
- [Git](https://git-scm.com/downloads)
- [Git](https://git-scm.com/downloads) _(optional — required only when the git extension is enabled)_

## Installation

Expand Down
2 changes: 1 addition & 1 deletion docs/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ rm -rf .venv dist build *.egg-info
|---------|-----|
| `ModuleNotFoundError: typer` | Run `uv pip install -e .` |
| Scripts not executable (Linux) | Re-run init or `chmod +x scripts/*.sh` |
| Git step skipped | You passed `--no-git` or Git not installed |
| Git step skipped | Git not installed on your system |
| Wrong script type downloaded | Pass `--script sh` or `--script ps` explicitly |
| TLS errors on corporate network | Configure your environment's certificate store or proxy. The `--skip-tls` flag is deprecated and has no effect. |

Expand Down
13 changes: 2 additions & 11 deletions docs/reference/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ specify init [<project_name>]
| `--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 |
| `--no-git` | Skip git repository initialization |
| `--ignore-agent-tools` | Skip checks for AI coding agent CLI tools |
| `--preset <id>` | Install a preset during initialization |
| `--branch-numbering` | Branch numbering strategy: `sequential` (default) or `timestamp` |

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

> [!NOTE]
> The git extension is currently enabled by default during `specify init`.
> Starting in `v0.10.0`, it will require explicit opt-in. To add it after init, run `specify extension add git`.
> 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.

Expand All @@ -45,14 +42,8 @@ specify init --here --force --integration copilot
# Use PowerShell scripts (Windows/cross-platform)
specify init my-project --integration copilot --script ps

# Skip git initialization
specify init my-project --integration copilot --no-git

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

# Use timestamp-based branch numbering (useful for distributed teams)
specify init my-project --integration copilot --branch-numbering timestamp
```

### Environment Variables
Expand All @@ -67,7 +58,7 @@ specify init my-project --integration copilot --branch-numbering timestamp
specify check
```

Checks that required tools are available on your system: `git` and any CLI-based AI coding agents. IDE-based agents are skipped since they don't require a CLI tool.
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.

Expand Down
50 changes: 8 additions & 42 deletions docs/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,58 +257,28 @@ rm speckit.old-command-name.md
# Restart your IDE
```

### Scenario 4: "I'm working on a project without Git"
### Scenario 4: "I don't want the git extension"

If you initialized your project with `--no-git`, you can still upgrade:
The git extension is now opt-in, so upgrades do not install it unless you add it explicitly.

```bash
# Manually back up files you customized
cp .specify/memory/constitution.md /tmp/constitution-backup.md
cp .specify/memory/constitution.md .specify/memory/constitution.backup.md

# Run upgrade
specify init --here --force --integration copilot --no-git
specify init --here --force --integration copilot

# Restore customizations
mv /tmp/constitution-backup.md .specify/memory/constitution.md
```

The `--no-git` flag skips git initialization but doesn't affect file updates.

---

## Using `--no-git` Flag

The `--no-git` flag tells Spec Kit to **skip git repository initialization**. This is useful when:

- You manage version control differently (Mercurial, SVN, etc.)
- Your project is part of a larger monorepo with existing git setup
- You're experimenting and don't want version control yet

**During initial setup:**

```bash
specify init my-project --integration copilot --no-git
mv .specify/memory/constitution.backup.md .specify/memory/constitution.md
```

**During upgrade:**
If you later decide you want the git extension's commands and hooks, install it explicitly:

```bash
specify init --here --force --integration copilot --no-git
specify extension add git
```

### What `--no-git` does NOT do

❌ Does NOT prevent file updates
❌ Does NOT skip slash command installation
❌ Does NOT affect template merging

It **only** skips running `git init` and creating the initial commit.

### Working without Git

If you use `--no-git`, you'll need to manage feature directories manually:

**Set the `SPECIFY_FEATURE` environment variable** before using planning commands:
Projects that do not use Git can still work with Spec Kit by setting `SPECIFY_FEATURE` manually before planning commands:

```bash
# Bash/Zsh
Expand All @@ -318,10 +288,6 @@ export SPECIFY_FEATURE="001-my-feature"
$env:SPECIFY_FEATURE = "001-my-feature"
```

This tells Spec Kit which feature directory to use when creating specs, plans, and tasks.

**Why this matters:** Without git, Spec Kit can't detect your current branch name to determine the active feature. The environment variable provides that context manually.

---

## Troubleshooting
Expand Down
3 changes: 0 additions & 3 deletions scripts/bash/check-prerequisites.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,6 @@ if $PATHS_ONLY; then
exit 0
fi

# Validate branch name
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1

# Validate required directories and files
if [[ ! -d "$FEATURE_DIR" ]]; then
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
Expand Down
186 changes: 17 additions & 169 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ find_specify_root() {
return 1
}

# Get repository root, prioritizing .specify directory over git
# This prevents using a parent git repo when spec-kit is initialized in a subdirectory
# Get repository root, prioritizing .specify directory
# This prevents using a parent repository when spec-kit is initialized in a subdirectory
get_repo_root() {
# First, look for .specify directory (spec-kit's own marker)
local specify_root
Expand All @@ -34,123 +34,24 @@ get_repo_root() {
return
fi

# Fallback to git if no .specify found
if git rev-parse --show-toplevel >/dev/null 2>&1; then
git rev-parse --show-toplevel
return
fi

# Final fallback to script location for non-git repos
# Final fallback to script location
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$script_dir/../../.." && pwd)
}

# Get current branch, with fallback for non-git repositories
# Get current feature name from explicit state only.
# Returns the feature identifier or errors if none is set.
# Feature state is set by the git extension (via SPECIFY_FEATURE) or by
# the specify command (via .specify/feature.json read in get_feature_paths).
get_current_branch() {
# First check if SPECIFY_FEATURE environment variable is set
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
echo "$SPECIFY_FEATURE"
return
fi

# Then check git if available at the spec-kit root (not parent)
local repo_root=$(get_repo_root)
if has_git; then
git -C "$repo_root" rev-parse --abbrev-ref HEAD
return
fi

# For non-git repos, try to find the latest feature directory
local specs_dir="$repo_root/specs"

if [[ -d "$specs_dir" ]]; then
local latest_feature=""
local highest=0
local latest_timestamp=""

for dir in "$specs_dir"/*; do
if [[ -d "$dir" ]]; then
local dirname=$(basename "$dir")
if [[ "$dirname" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
# Timestamp-based branch: compare lexicographically
local ts="${BASH_REMATCH[1]}"
if [[ "$ts" > "$latest_timestamp" ]]; then
latest_timestamp="$ts"
latest_feature=$dirname
fi
elif [[ "$dirname" =~ ^([0-9]{3,})- ]]; then
local number=${BASH_REMATCH[1]}
number=$((10#$number))
if [[ "$number" -gt "$highest" ]]; then
highest=$number
# Only update if no timestamp branch found yet
if [[ -z "$latest_timestamp" ]]; then
latest_feature=$dirname
fi
fi
fi
fi
done

if [[ -n "$latest_feature" ]]; then
echo "$latest_feature"
return
fi
fi

echo "main" # Final fallback
}

# Check if we have git available at the spec-kit root level
# Returns true only if git is installed and the repo root is inside a git work tree
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
has_git() {
# First check if git command is available (before calling get_repo_root which may use git)
command -v git >/dev/null 2>&1 || return 1
local repo_root=$(get_repo_root)
# Check if .git exists (directory or file for worktrees/submodules)
[ -e "$repo_root/.git" ] || return 1
# Verify it's actually a valid git work tree
git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1
}

# Strip a single optional path segment (e.g. gitflow "feat/004-name" -> "004-name").
# Only when the full name is exactly two slash-free segments; otherwise returns the raw name.
spec_kit_effective_branch_name() {
local raw="$1"
if [[ "$raw" =~ ^([^/]+)/([^/]+)$ ]]; then
printf '%s\n' "${BASH_REMATCH[2]}"
else
printf '%s\n' "$raw"
fi
}

check_feature_branch() {
local raw="$1"
local has_git_repo="$2"

# For non-git repos, we can't enforce branch naming but still provide output
if [[ "$has_git_repo" != "true" ]]; then
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
return 0
fi

local branch
branch=$(spec_kit_effective_branch_name "$raw")

# Accept sequential prefix (3+ digits) but exclude malformed timestamps
# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
local is_sequential=false
if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then
is_sequential=true
fi
if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then
echo "ERROR: Not on a feature branch. Current branch: $raw" >&2
echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2
return 1
fi

return 0
# No explicit feature set — caller must handle this via feature.json
# in get_feature_paths(). Return empty to signal "unknown".
echo ""
}

# Safely read .specify/feature.json's "feature_directory" value.
Expand Down Expand Up @@ -186,7 +87,7 @@ read_feature_json_feature_directory() {
}

# Returns 0 when .specify/feature.json lists feature_directory that exists as a directory
# and matches the resolved active FEATURE_DIR (so __SPECKIT_COMMAND_PLAN__ can skip git branch pattern checks).
# and matches the resolved active FEATURE_DIR.
# Delegates parsing to read_feature_json_feature_directory, which is safe under `set -e`.
feature_json_matches_feature_dir() {
local repo_root="$1"
Expand All @@ -206,92 +107,39 @@ feature_json_matches_feature_dir() {
[[ "$norm_json" == "$norm_active" ]]
}

# Find feature directory by numeric prefix instead of exact branch match
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
find_feature_dir_by_prefix() {
local repo_root="$1"
local branch_name
branch_name=$(spec_kit_effective_branch_name "$2")
local specs_dir="$repo_root/specs"

# Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches)
local prefix=""
if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
prefix="${BASH_REMATCH[1]}"
elif [[ "$branch_name" =~ ^([0-9]{3,})- ]]; then
prefix="${BASH_REMATCH[1]}"
else
# If branch doesn't have a recognized prefix, fall back to exact match
echo "$specs_dir/$branch_name"
return
fi

# Search for directories in specs/ that start with this prefix
local matches=()
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/"$prefix"-*; do
if [[ -d "$dir" ]]; then
matches+=("$(basename "$dir")")
fi
done
fi

# Handle results
if [[ ${#matches[@]} -eq 0 ]]; then
# No match found - return the branch name path (will fail later with clear error)
echo "$specs_dir/$branch_name"
elif [[ ${#matches[@]} -eq 1 ]]; then
# Exactly one match - perfect!
echo "$specs_dir/${matches[0]}"
else
# Multiple matches - this shouldn't happen with proper naming convention
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
echo "Please ensure only one spec directory exists per prefix." >&2
return 1
fi
}

get_feature_paths() {
local repo_root=$(get_repo_root)
local current_branch=$(get_current_branch)
local has_git_repo="false"

if has_git; then
has_git_repo="true"
fi

# Resolve feature directory. Priority:
# 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
# 2. .specify/feature.json "feature_directory" key (persisted by __SPECKIT_COMMAND_SPECIFY__)
# 3. Branch-name-based prefix lookup (legacy fallback)
# 2. .specify/feature.json "feature_directory" key (persisted by specify command)
# 3. Error — no feature context available
local feature_dir
if [[ -n "${SPECIFY_FEATURE_DIRECTORY:-}" ]]; then
feature_dir="$SPECIFY_FEATURE_DIRECTORY"
# Normalize relative paths to absolute under repo root
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
elif [[ -f "$repo_root/.specify/feature.json" ]]; then
# Shared, set -e-safe parser: jq -> python3 -> grep/sed. Returns empty on
# missing/unparseable/unset so we fall through to the branch-prefix lookup.
local _fd
_fd=$(read_feature_json_feature_directory "$repo_root")
if [[ -n "$_fd" ]]; then
feature_dir="$_fd"
# Normalize relative paths to absolute under repo root
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
elif ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
echo "ERROR: Failed to resolve feature directory" >&2
else
echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE or ensure .specify/feature.json contains feature_directory." >&2
return 1
Comment on lines +131 to 132
fi
elif ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
echo "ERROR: Failed to resolve feature directory" >&2
else
echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE or run the specify command to create .specify/feature.json." >&2
return 1
Comment on lines +135 to 136
fi

# Use printf '%q' to safely quote values, preventing shell injection
# via crafted branch names or paths containing special characters
printf 'REPO_ROOT=%q\n' "$repo_root"
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
printf 'HAS_GIT=%q\n' "$has_git_repo"
printf 'FEATURE_DIR=%q\n' "$feature_dir"
printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
Expand Down
Loading
Loading