Skip to content

Commit 44b1275

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 5a80b81 commit 44b1275

File tree

10 files changed

+252
-509
lines changed

10 files changed

+252
-509
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
*)
@@ -399,125 +368,32 @@ if [ "$DRY_RUN" != true ]; then
399368
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
400369
fi
401370

402-
# Create matching feature branches in nested independent git repositories
403-
NESTED_REPOS_JSON=""
404-
if [ "$HAS_GIT" = true ]; then
405-
scan_depth="${SCAN_DEPTH:-2}"
406-
nested_repos=$(find_nested_git_repos "$REPO_ROOT" "$scan_depth")
407-
408-
# Filter by --repos if specified: only branch repos in the requested list
409-
if [ -n "$NESTED_REPOS_FILTER" ] && [ -n "$nested_repos" ]; then
410-
IFS=',' read -ra requested_repos <<< "$NESTED_REPOS_FILTER"
411-
filtered=""
412-
while IFS= read -r nested_path; do
413-
[ -z "$nested_path" ] && continue
414-
nested_path="${nested_path%/}"
415-
rel_path="${nested_path#"$REPO_ROOT/"}"
416-
rel_path="${rel_path%/}"
417-
for req in "${requested_repos[@]}"; do
418-
req="$(echo "$req" | xargs)"
419-
req="${req%/}"
420-
if [ "$rel_path" = "$req" ]; then
421-
[ -n "$filtered" ] && filtered+=$'\n'
422-
filtered+="$nested_path"
423-
break
424-
fi
425-
done
426-
done <<< "$nested_repos"
427-
nested_repos="$filtered"
428-
fi
429-
430-
if [ -n "$nested_repos" ]; then
431-
NESTED_REPOS_JSON="["
432-
first=true
433-
while IFS= read -r nested_path; do
434-
[ -z "$nested_path" ] && continue
435-
# Normalize: remove trailing slash
436-
nested_path="${nested_path%/}"
437-
# Compute relative path for output
438-
rel_path="${nested_path#"$REPO_ROOT/"}"
439-
rel_path="${rel_path%/}"
440-
status="skipped"
441-
442-
if [ "$DRY_RUN" = true ]; then
443-
status="dry_run"
444-
else
445-
# Attempt to create the branch in the nested repo
446-
if git -C "$nested_path" checkout -q -b "$BRANCH_NAME" 2>/dev/null; then
447-
status="created"
448-
else
449-
# Check if the branch already exists
450-
if git -C "$nested_path" branch --list "$BRANCH_NAME" 2>/dev/null | grep -q .; then
451-
if [ "$ALLOW_EXISTING" = true ]; then
452-
current_nested="$(git -C "$nested_path" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
453-
if [ "$current_nested" = "$BRANCH_NAME" ]; then
454-
status="existing"
455-
elif git -C "$nested_path" checkout -q "$BRANCH_NAME" 2>/dev/null; then
456-
status="existing"
457-
else
458-
status="failed"
459-
>&2 echo "[specify] Warning: Failed to switch nested repo '$rel_path' to branch '$BRANCH_NAME'"
460-
fi
461-
else
462-
status="existing"
463-
fi
464-
else
465-
status="failed"
466-
>&2 echo "[specify] Warning: Failed to create branch '$BRANCH_NAME' in nested repo '$rel_path'"
467-
fi
468-
fi
469-
fi
470-
471-
if [ "$first" = true ]; then
472-
first=false
473-
else
474-
NESTED_REPOS_JSON+=","
475-
fi
476-
NESTED_REPOS_JSON+="{\"path\":\"$(json_escape "$rel_path")\",\"status\":\"$status\"}"
477-
done <<< "$nested_repos"
478-
NESTED_REPOS_JSON+="]"
479-
fi
480-
fi
481-
482371
if $JSON_MODE; then
483-
# Build the nested repos portion for JSON output
484-
nested_json_field=""
485-
if [ -n "$NESTED_REPOS_JSON" ]; then
486-
nested_json_field="$NESTED_REPOS_JSON"
487-
else
488-
nested_json_field="[]"
489-
fi
490-
491372
if command -v jq >/dev/null 2>&1; then
492373
if [ "$DRY_RUN" = true ]; then
493374
jq -cn \
494375
--arg branch_name "$BRANCH_NAME" \
495376
--arg spec_file "$SPEC_FILE" \
496377
--arg feature_num "$FEATURE_NUM" \
497-
--argjson nested_repos "$nested_json_field" \
498-
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true,NESTED_REPOS:$nested_repos}'
378+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
499379
else
500380
jq -cn \
501381
--arg branch_name "$BRANCH_NAME" \
502382
--arg spec_file "$SPEC_FILE" \
503383
--arg feature_num "$FEATURE_NUM" \
504-
--argjson nested_repos "$nested_json_field" \
505-
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,NESTED_REPOS:$nested_repos}'
384+
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
506385
fi
507386
else
508387
if [ "$DRY_RUN" = true ]; then
509-
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"
388+
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")"
510389
else
511-
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"
390+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
512391
fi
513392
fi
514393
else
515394
echo "BRANCH_NAME: $BRANCH_NAME"
516395
echo "SPEC_FILE: $SPEC_FILE"
517396
echo "FEATURE_NUM: $FEATURE_NUM"
518-
if [ -n "$NESTED_REPOS_JSON" ] && [ "$NESTED_REPOS_JSON" != "[]" ]; then
519-
echo "NESTED_REPOS: $NESTED_REPOS_JSON"
520-
fi
521397
if [ "$DRY_RUN" != true ]; then
522398
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
523399
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)