Skip to content

Commit e3f1d30

Browse files
sakitACopilot
andcommitted
feat: move nested repo branching to plan-aware, task-driven workflow
Instead of automatically creating branches in all nested repos during /speckit.specify, this redesign defers branching to the plantasksimplement workflow: - Specify phase: creates root branch + spec only (no nested repo work) - Plan phase: setup-plan.sh discovers nested repos (NESTED_REPOS in JSON) and the AI agent identifies affected modules from the spec - Tasks phase: generates a Phase 1 setup task for creating feature branches in only the affected nested repos identified by the plan - Implement phase: executes the branch-creation task via git commands Changes: - Revert create-new-feature.sh/.ps1: remove --repos, --scan-depth, and NESTED_REPOS output (specify phase is clean again) - Add nested repo discovery to setup-plan.sh/.ps1 with --scan-depth flag - Update plan.md template: add step for AI to identify affected repos - Update plan-template.md: add Affected Nested Repositories section - Update tasks.md command: guidance for generating branch-creation task - Update tasks-template.md: Phase 1 example for nested repo branching - Update specify.md: remove nested repo guidance, point to plan+tasks - Rewrite tests: 13 tests covering discovery, depth config, setup-plan output, and verification that specify phase doesn't branch nested repos Resolves the concern that at specify time the spec doesn't exist yet, so we cannot know which repos are affected. The AI agent now makes this determination during planning based on spec analysis. Refs: #2120 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 54cc59e commit e3f1d30

File tree

10 files changed

+249
-498
lines changed

10 files changed

+249
-498
lines changed

scripts/bash/create-new-feature.sh

Lines changed: 5 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ ALLOW_EXISTING=false
88
SHORT_NAME=""
99
BRANCH_NUMBER=""
1010
USE_TIMESTAMP=false
11-
NESTED_REPOS_FILTER=""
12-
SCAN_DEPTH=""
1311
ARGS=()
1412
i=1
1513
while [ $i -le $# ]; do
@@ -54,34 +52,8 @@ while [ $i -le $# ]; do
5452
--timestamp)
5553
USE_TIMESTAMP=true
5654
;;
57-
--repos)
58-
if [ $((i + 1)) -gt $# ]; then
59-
echo 'Error: --repos requires a value (comma-separated list of repo paths)' >&2
60-
exit 1
61-
fi
62-
i=$((i + 1))
63-
next_arg="${!i}"
64-
if [[ "$next_arg" == --* ]]; then
65-
echo 'Error: --repos requires a value (comma-separated list of repo paths)' >&2
66-
exit 1
67-
fi
68-
NESTED_REPOS_FILTER="$next_arg"
69-
;;
70-
--scan-depth)
71-
if [ $((i + 1)) -gt $# ]; then
72-
echo 'Error: --scan-depth requires a value' >&2
73-
exit 1
74-
fi
75-
i=$((i + 1))
76-
next_arg="${!i}"
77-
if [[ "$next_arg" == --* ]]; then
78-
echo 'Error: --scan-depth requires a value' >&2
79-
exit 1
80-
fi
81-
SCAN_DEPTH="$next_arg"
82-
;;
8355
--help|-h)
84-
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] [--repos <paths>] [--scan-depth N] <feature_description>"
56+
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
8557
echo ""
8658
echo "Options:"
8759
echo " --json Output in JSON format"
@@ -90,15 +62,12 @@ while [ $i -le $# ]; do
9062
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
9163
echo " --number N Specify branch number manually (overrides auto-detection)"
9264
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
93-
echo " --repos <paths> Comma-separated list of nested repo relative paths to branch (default: all discovered)"
94-
echo " --scan-depth N Max directory depth to scan for nested repos (default: 2)"
9565
echo " --help, -h Show this help message"
9666
echo ""
9767
echo "Examples:"
9868
echo " $0 'Add user authentication system' --short-name 'user-auth'"
9969
echo " $0 'Implement OAuth2 integration for API' --number 5"
10070
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
101-
echo " $0 --repos 'components/api,components/auth' 'Add API auth support'"
10271
exit 0
10372
;;
10473
*)
@@ -412,125 +381,32 @@ if [ "$DRY_RUN" != true ]; then
412381
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
413382
fi
414383

415-
# Create matching feature branches in nested independent git repositories
416-
NESTED_REPOS_JSON=""
417-
if [ "$HAS_GIT" = true ]; then
418-
scan_depth="${SCAN_DEPTH:-2}"
419-
nested_repos=$(find_nested_git_repos "$REPO_ROOT" "$scan_depth")
420-
421-
# Filter by --repos if specified: only branch repos in the requested list
422-
if [ -n "$NESTED_REPOS_FILTER" ] && [ -n "$nested_repos" ]; then
423-
IFS=',' read -ra requested_repos <<< "$NESTED_REPOS_FILTER"
424-
filtered=""
425-
while IFS= read -r nested_path; do
426-
[ -z "$nested_path" ] && continue
427-
nested_path="${nested_path%/}"
428-
rel_path="${nested_path#"$REPO_ROOT/"}"
429-
rel_path="${rel_path%/}"
430-
for req in "${requested_repos[@]}"; do
431-
req="$(echo "$req" | xargs)"
432-
req="${req%/}"
433-
if [ "$rel_path" = "$req" ]; then
434-
[ -n "$filtered" ] && filtered+=$'\n'
435-
filtered+="$nested_path"
436-
break
437-
fi
438-
done
439-
done <<< "$nested_repos"
440-
nested_repos="$filtered"
441-
fi
442-
443-
if [ -n "$nested_repos" ]; then
444-
NESTED_REPOS_JSON="["
445-
first=true
446-
while IFS= read -r nested_path; do
447-
[ -z "$nested_path" ] && continue
448-
# Normalize: remove trailing slash
449-
nested_path="${nested_path%/}"
450-
# Compute relative path for output
451-
rel_path="${nested_path#"$REPO_ROOT/"}"
452-
rel_path="${rel_path%/}"
453-
status="skipped"
454-
455-
if [ "$DRY_RUN" = true ]; then
456-
status="dry_run"
457-
else
458-
# Attempt to create the branch in the nested repo
459-
if git -C "$nested_path" checkout -q -b "$BRANCH_NAME" 2>/dev/null; then
460-
status="created"
461-
else
462-
# Check if the branch already exists
463-
if git -C "$nested_path" branch --list "$BRANCH_NAME" 2>/dev/null | grep -q .; then
464-
if [ "$ALLOW_EXISTING" = true ]; then
465-
current_nested="$(git -C "$nested_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
466-
if [ "$current_nested" = "$BRANCH_NAME" ]; then
467-
status="existing"
468-
elif git -C "$nested_path" checkout -q "$BRANCH_NAME" 2>/dev/null; then
469-
status="existing"
470-
else
471-
status="failed"
472-
>&2 echo "[specify] Warning: Failed to switch nested repo '$rel_path' to branch '$BRANCH_NAME'"
473-
fi
474-
else
475-
status="existing"
476-
fi
477-
else
478-
status="failed"
479-
>&2 echo "[specify] Warning: Failed to create branch '$BRANCH_NAME' in nested repo '$rel_path'"
480-
fi
481-
fi
482-
fi
483-
484-
if [ "$first" = true ]; then
485-
first=false
486-
else
487-
NESTED_REPOS_JSON+=","
488-
fi
489-
NESTED_REPOS_JSON+="{\"path\":\"$(json_escape "$rel_path")\",\"status\":\"$status\"}"
490-
done <<< "$nested_repos"
491-
NESTED_REPOS_JSON+="]"
492-
fi
493-
fi
494-
495384
if $JSON_MODE; then
496-
# Build the nested repos portion for JSON output
497-
nested_json_field=""
498-
if [ -n "$NESTED_REPOS_JSON" ]; then
499-
nested_json_field="$NESTED_REPOS_JSON"
500-
else
501-
nested_json_field="[]"
502-
fi
503-
504385
if command -v jq >/dev/null 2>&1; then
505386
if [ "$DRY_RUN" = true ]; then
506387
jq -cn \
507388
--arg branch_name "$BRANCH_NAME" \
508389
--arg spec_file "$SPEC_FILE" \
509390
--arg feature_num "$FEATURE_NUM" \
510-
--argjson nested_repos "$nested_json_field" \
511-
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true,NESTED_REPOS:$nested_repos}'
391+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
512392
else
513393
jq -cn \
514394
--arg branch_name "$BRANCH_NAME" \
515395
--arg spec_file "$SPEC_FILE" \
516396
--arg feature_num "$FEATURE_NUM" \
517-
--argjson nested_repos "$nested_json_field" \
518-
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,NESTED_REPOS:$nested_repos}'
397+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
519398
fi
520399
else
521400
if [ "$DRY_RUN" = true ]; then
522-
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true,"NESTED_REPOS":%s}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" "$nested_json_field"
401+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
523402
else
524-
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","NESTED_REPOS":%s}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" "$nested_json_field"
403+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
525404
fi
526405
fi
527406
else
528407
echo "BRANCH_NAME: $BRANCH_NAME"
529408
echo "SPEC_FILE: $SPEC_FILE"
530409
echo "FEATURE_NUM: $FEATURE_NUM"
531-
if [ -n "$NESTED_REPOS_JSON" ] && [ "$NESTED_REPOS_JSON" != "[]" ]; then
532-
echo "NESTED_REPOS: $NESTED_REPOS_JSON"
533-
fi
534410
if [ "$DRY_RUN" != true ]; then
535411
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
536412
fi

scripts/bash/setup-plan.sh

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,36 @@ set -e
44

55
# Parse command line arguments
66
JSON_MODE=false
7+
SCAN_DEPTH=""
78
ARGS=()
89

910
for arg in "$@"; do
1011
case "$arg" in
1112
--json)
1213
JSON_MODE=true
1314
;;
15+
--scan-depth)
16+
# Next argument is the depth value — handled below
17+
SCAN_DEPTH="__NEXT__"
18+
;;
1419
--help|-h)
15-
echo "Usage: $0 [--json]"
16-
echo " --json Output results in JSON format"
17-
echo " --help Show this help message"
20+
echo "Usage: $0 [--json] [--scan-depth N]"
21+
echo " --json Output results in JSON format"
22+
echo " --scan-depth N Max directory depth for nested repo discovery (default: 2)"
23+
echo " --help Show this help message"
1824
exit 0
1925
;;
2026
*)
21-
ARGS+=("$arg")
27+
if [ "$SCAN_DEPTH" = "__NEXT__" ]; then
28+
SCAN_DEPTH="$arg"
29+
else
30+
ARGS+=("$arg")
31+
fi
2232
;;
2333
esac
2434
done
35+
# Reset sentinel if --scan-depth was passed without a value
36+
[ "$SCAN_DEPTH" = "__NEXT__" ] && SCAN_DEPTH=""
2537

2638
# Get script directory and load common functions
2739
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -49,6 +61,30 @@ else
4961
touch "$IMPL_PLAN"
5062
fi
5163

64+
# Discover nested independent git repositories (for AI agent to analyze)
65+
NESTED_REPOS_JSON="[]"
66+
if [ "$HAS_GIT" = true ]; then
67+
scan_depth="${SCAN_DEPTH:-2}"
68+
nested_repos=$(find_nested_git_repos "$REPO_ROOT" "$scan_depth")
69+
if [ -n "$nested_repos" ]; then
70+
NESTED_REPOS_JSON="["
71+
first=true
72+
while IFS= read -r nested_path; do
73+
[ -z "$nested_path" ] && continue
74+
nested_path="${nested_path%/}"
75+
rel_path="${nested_path#"$REPO_ROOT/"}"
76+
rel_path="${rel_path%/}"
77+
if [ "$first" = true ]; then
78+
first=false
79+
else
80+
NESTED_REPOS_JSON+=","
81+
fi
82+
NESTED_REPOS_JSON+="{\"path\":\"$(json_escape "$rel_path")\"}"
83+
done <<< "$nested_repos"
84+
NESTED_REPOS_JSON+="]"
85+
fi
86+
fi
87+
5288
# Output results
5389
if $JSON_MODE; then
5490
if has_jq; then
@@ -58,16 +94,20 @@ if $JSON_MODE; then
5894
--arg specs_dir "$FEATURE_DIR" \
5995
--arg branch "$CURRENT_BRANCH" \
6096
--arg has_git "$HAS_GIT" \
61-
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
97+
--argjson nested_repos "$NESTED_REPOS_JSON" \
98+
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git,NESTED_REPOS:$nested_repos}'
6299
else
63-
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
64-
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
100+
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s","NESTED_REPOS":%s}\n' \
101+
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")" "$NESTED_REPOS_JSON"
65102
fi
66103
else
67104
echo "FEATURE_SPEC: $FEATURE_SPEC"
68105
echo "IMPL_PLAN: $IMPL_PLAN"
69106
echo "SPECS_DIR: $FEATURE_DIR"
70107
echo "BRANCH: $CURRENT_BRANCH"
71108
echo "HAS_GIT: $HAS_GIT"
109+
if [ "$NESTED_REPOS_JSON" != "[]" ]; then
110+
echo "NESTED_REPOS: $NESTED_REPOS_JSON"
111+
fi
72112
fi
73113

0 commit comments

Comments
 (0)