Skip to content
Open
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
52 changes: 52 additions & 0 deletions scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,58 @@ json_escape() {
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }

# Discover nested independent git repositories under REPO_ROOT.
# Searches up to $max_depth directory levels deep for subdirectories containing
# .git (directory or file, covering worktrees/submodules). Excludes the root
# repo itself and common non-project directories.
# Usage: find_nested_git_repos [repo_root] [max_depth]
# repo_root — defaults to $(get_repo_root)
# max_depth — defaults to 2
# Outputs one absolute path per line.
find_nested_git_repos() {
local repo_root="${1:-$(get_repo_root)}"
local max_depth="${2:-2}"
# Directories to skip during traversal
local -a skip_dirs=(".specify" ".git" "node_modules" "vendor" ".venv" "venv"
"__pycache__" ".gradle" "build" "dist" "target" ".idea"
".vscode" "specs")

# Run in a subshell to avoid leaking helper functions into global scope
(
_should_skip() {
local name="$1"
local skip
for skip in "${skip_dirs[@]}"; do
[ "$name" = "$skip" ] && return 0
done
return 1
}

_scan_dir() {
local dir="$1"
local current_depth="$2"
local child
local child_name
for child in "$dir"/*/; do
[ -d "$child" ] || continue
child="${child%/}"
child_name="$(basename "$child")"
_should_skip "$child_name" && continue

if [ -e "$child/.git" ]; then
if git -C "$child" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "$child"
fi
elif [ "$current_depth" -lt "$max_depth" ]; then
_scan_dir "$child" $((current_depth + 1))
fi
done
}

_scan_dir "$repo_root" 1
)
}

# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
Expand Down
65 changes: 58 additions & 7 deletions scripts/bash/setup-plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,47 @@ set -e

# Parse command line arguments
JSON_MODE=false
SCAN_DEPTH=""
ARGS=()

for arg in "$@"; do
case "$arg" in
--json)
JSON_MODE=true
;;
--scan-depth)
# Next argument is the depth value — handled below
SCAN_DEPTH="__NEXT__"
;;
--help|-h)
echo "Usage: $0 [--json]"
echo " --json Output results in JSON format"
echo " --help Show this help message"
echo "Usage: $0 [--json] [--scan-depth N]"
echo " --json Output results in JSON format"
echo " --scan-depth N Max directory depth for nested repo discovery (default: 2)"
echo " --help Show this help message"
exit 0
;;
*)
ARGS+=("$arg")
if [ "$SCAN_DEPTH" = "__NEXT__" ]; then
SCAN_DEPTH="$arg"
else
ARGS+=("$arg")
fi
;;
esac
done
# Validate --scan-depth argument
if [ "$SCAN_DEPTH" = "__NEXT__" ]; then
echo "ERROR: --scan-depth requires a positive integer value" >&2
exit 1
fi
if [ -n "$SCAN_DEPTH" ]; then
case "$SCAN_DEPTH" in
''|*[!0-9]*|0)
echo "ERROR: --scan-depth must be a positive integer, got '$SCAN_DEPTH'" >&2
exit 1
;;
esac
fi

# Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down Expand Up @@ -49,6 +72,30 @@ else
touch "$IMPL_PLAN"
fi

# Discover nested independent git repositories (for AI agent to analyze)
NESTED_REPOS_JSON="[]"
if [ "$HAS_GIT" = true ]; then
scan_depth="${SCAN_DEPTH:-2}"
nested_repos=$(find_nested_git_repos "$REPO_ROOT" "$scan_depth")
if [ -n "$nested_repos" ]; then
NESTED_REPOS_JSON="["
first=true
while IFS= read -r nested_path; do
[ -z "$nested_path" ] && continue
nested_path="${nested_path%/}"
rel_path="${nested_path#"$REPO_ROOT/"}"
rel_path="${rel_path%/}"
if [ "$first" = true ]; then
first=false
else
NESTED_REPOS_JSON+=","
fi
NESTED_REPOS_JSON+="{\"path\":\"$(json_escape "$rel_path")\"}"
done <<< "$nested_repos"
NESTED_REPOS_JSON+="]"
fi
fi

# Output results
if $JSON_MODE; then
if has_jq; then
Expand All @@ -58,16 +105,20 @@ if $JSON_MODE; then
--arg specs_dir "$FEATURE_DIR" \
--arg branch "$CURRENT_BRANCH" \
--arg has_git "$HAS_GIT" \
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
--argjson nested_repos "$NESTED_REPOS_JSON" \
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git,NESTED_REPOS:$nested_repos}'
else
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s","NESTED_REPOS":%s}\n' \
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")" "$NESTED_REPOS_JSON"
fi
else
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "SPECS_DIR: $FEATURE_DIR"
echo "BRANCH: $CURRENT_BRANCH"
echo "HAS_GIT: $HAS_GIT"
if [ "$NESTED_REPOS_JSON" != "[]" ]; then
echo "NESTED_REPOS: $NESTED_REPOS_JSON"
fi
fi

40 changes: 40 additions & 0 deletions scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,46 @@ function Test-DirHasFiles {
}
}

# Discover nested independent git repositories under RepoRoot.
# Searches up to 2 directory levels deep for subdirectories containing .git
# (directory or file, covering worktrees/submodules). Excludes the root repo
# itself and common non-project directories.
# Returns an array of absolute paths. Scan depth is configurable (default 2).
function Find-NestedGitRepos {
param(
[string]$RepoRoot = (Get-RepoRoot),
[int]$MaxDepth = 2
Comment on lines +202 to +210
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above Find-NestedGitRepos says it "Searches up to 2 directory levels deep", but the implementation accepts a configurable -MaxDepth parameter. Please update the comment to reflect that scan depth is configurable (default 2) so the documentation matches behavior.

Copilot uses AI. Check for mistakes.
)

$skipDirs = @('.specify', '.git', 'node_modules', 'vendor', '.venv', 'venv',
'__pycache__', '.gradle', 'build', 'dist', 'target', '.idea',
'.vscode', 'specs')

function ScanDir {
param([string]$Dir, [int]$CurrentDepth)
$found = @()
$children = Get-ChildItem -Path $Dir -Directory -ErrorAction SilentlyContinue |
Where-Object { $skipDirs -notcontains $_.Name }

foreach ($child in $children) {
$gitMarker = Join-Path $child.FullName '.git'
if (Test-Path -LiteralPath $gitMarker) {
try {
$null = git -C $child.FullName rev-parse --is-inside-work-tree 2>$null
if ($LASTEXITCODE -eq 0) {
$found += $child.FullName
}
} catch { }
} elseif ($CurrentDepth -lt $MaxDepth) {
$found += ScanDir -Dir $child.FullName -CurrentDepth ($CurrentDepth + 1)
}
}
return $found
}

return ScanDir -Dir $RepoRoot -CurrentDepth 1
}

# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
Expand Down
26 changes: 23 additions & 3 deletions scripts/powershell/setup-plan.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
[CmdletBinding()]
param(
[switch]$Json,
[int]$ScanDepth = 0,
[switch]$Help
)

$ErrorActionPreference = 'Stop'

# Show help if requested
if ($Help) {
Write-Output "Usage: ./setup-plan.ps1 [-Json] [-Help]"
Write-Output " -Json Output results in JSON format"
Write-Output " -Help Show this help message"
Write-Output "Usage: ./setup-plan.ps1 [-Json] [-ScanDepth N] [-Help]"
Write-Output " -Json Output results in JSON format"
Write-Output " -ScanDepth N Max directory depth for nested repo discovery (default: 2)"
Write-Output " -Help Show this help message"
exit 0
}

Expand Down Expand Up @@ -42,6 +44,17 @@ if ($template -and (Test-Path $template)) {
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
}

# Discover nested independent git repositories (for AI agent to analyze)
$nestedReposResult = @()
if ($paths.HAS_GIT -eq 'true' -or $paths.HAS_GIT -eq $true) {
$effectiveDepth = if ($ScanDepth -gt 0) { $ScanDepth } else { 2 }
$nestedRepos = Find-NestedGitRepos -RepoRoot $paths.REPO_ROOT -MaxDepth $effectiveDepth
foreach ($nestedPath in $nestedRepos) {
$relPath = $nestedPath.Substring($paths.REPO_ROOT.Length).TrimStart('\', '/')
$nestedReposResult += [PSCustomObject]@{ path = $relPath }
}
}

# Output results
if ($Json) {
$result = [PSCustomObject]@{
Expand All @@ -50,6 +63,7 @@ if ($Json) {
SPECS_DIR = $paths.FEATURE_DIR
BRANCH = $paths.CURRENT_BRANCH
HAS_GIT = $paths.HAS_GIT
NESTED_REPOS = $nestedReposResult
}
$result | ConvertTo-Json -Compress
} else {
Expand All @@ -58,4 +72,10 @@ if ($Json) {
Write-Output "SPECS_DIR: $($paths.FEATURE_DIR)"
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
Write-Output "HAS_GIT: $($paths.HAS_GIT)"
if ($nestedReposResult.Count -gt 0) {
Write-Output "NESTED_REPOS:"
foreach ($nr in $nestedReposResult) {
Write-Output " $($nr.path)"
}
}
}
14 changes: 10 additions & 4 deletions templates/commands/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,17 @@ You **MUST** consider the user input before proceeding (if not empty).

## Outline

1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH, and NESTED_REPOS. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").

2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied).

3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
3. **Identify affected nested repositories**: If NESTED_REPOS is non-empty:
- Read the feature spec (FEATURE_SPEC)
- For each nested repo in NESTED_REPOS, determine whether this feature requires changes in that repo based on the spec's requirements, user stories, and technical scope
- Document the affected repos in the plan's **Project Structure** section under a subsection called "Affected Nested Repositories", listing each repo path and a brief reason why it's affected
- This information will be used by `/speckit.tasks` to generate a setup task for creating feature branches in the affected repos

4. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
- Fill Constitution Check section from constitution
- Evaluate gates (ERROR if violations unjustified)
Expand All @@ -73,9 +79,9 @@ You **MUST** consider the user input before proceeding (if not empty).
- Phase 1: Update agent context by running the agent script
- Re-evaluate Constitution Check post-design

4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
5. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, generated artifacts, and affected nested repos (if any).

5. **Check for extension hooks**: After reporting, check if `.specify/extensions.yml` exists in the project root.
6. **Check for extension hooks**: After reporting, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_plan` key
- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally
- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default.
Expand Down
1 change: 1 addition & 0 deletions templates/commands/specify.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Given that feature description, do this:
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
- **Note**: Nested git repository branching is handled during the `/speckit.plan` and `/speckit.tasks` phases, not here. The plan identifies affected repos and tasks generates a setup task for branch creation.

3. Load `templates/spec-template.md` to understand required sections.

Expand Down
1 change: 1 addition & 0 deletions templates/commands/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ You **MUST** consider the user input before proceeding (if not empty).
3. **Execute task generation workflow**:
- Load plan.md and extract tech stack, libraries, project structure
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
- If plan.md contains an "Affected Nested Repositories" section: extract the repo paths and reasons. Generate a setup task (in Phase 1) to create the feature branch in each affected nested repo using `git -C "<repo_path>" checkout -b "<BRANCH_NAME>"`. The branch name comes from the current feature branch.
- If data-model.md exists: Extract entities and map to user stories
- If contracts/ exists: Map interface contracts to user stories
- If research.md exists: Extract decisions for setup tasks
Expand Down
15 changes: 15 additions & 0 deletions templates/plan-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ ios/ or android/
**Structure Decision**: [Document the selected structure and reference the real
directories captured above]

### Affected Nested Repositories
<!--
OPTIONAL: Only include this section if the project has nested independent git
repositories (reported by setup-plan's NESTED_REPOS output). List each nested
repo that this feature needs to modify and why. The /speckit.tasks command uses
this section to generate a setup task for creating feature branches.

If there are no nested repos, delete this section entirely.
-->

| Repo Path | Reason |
|-----------|--------|
| [e.g., components/auth] | [e.g., New OAuth2 provider needs auth module changes] |
| [e.g., components/api] | [e.g., New REST endpoints for OAuth2 flow] |

## Complexity Tracking

> **Fill ONLY if Constitution Check has violations that must be justified**
Expand Down
8 changes: 8 additions & 0 deletions templates/tasks-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ description: "Task list template for feature implementation"
- [ ] T002 Initialize [language] project with [framework] dependencies
- [ ] T003 [P] Configure linting and formatting tools

<!--
If plan.md contains "Affected Nested Repositories", add a task here to create
the feature branch in each affected nested repo. Example:
- [ ] T00X [P] Create feature branch in nested repos: `git -C components/auth checkout -b <BRANCH>` and `git -C components/api checkout -b <BRANCH>`
This must be one of the first tasks so that subsequent work can target the
correct branch in each nested repo.
-->

---

## Phase 2: Foundational (Blocking Prerequisites)
Expand Down
Loading