fonzarely/regine-photos-archiverpublic⑂ Fork 0
⑂ 3d48bcd
Commits
⬇ Clone ▾
git clone https://git.rickub.com/fonzarely/regine-photos-archiver.git
git clone ssh://git@rickub.com/fonzarely/regine-photos-archiver.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

init

Fabien Champigny committed 2026-09-07T16:49:27+02:00 Browse files
3d48bcd
added .gitignore +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+.claude/
new file mode 100644
@@ -0,0 +1 @@
1+.claude/
added .specify/.gitignore +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+# Machine-local Spec Kit state — not meant to be shared.
2+# Managed by the Specify CLI; safe to edit (your changes are preserved on refresh).
3+
4+# Local pointer to the current feature directory. Rewritten every time you
5+# switch features, so it is per-checkout state rather than something to share.
6+feature.json
7+
8+# Per-machine extension config overrides.
9+extensions/*/local-config.yml
new file mode 100644
@@ -0,0 +1,9 @@
1+# Machine-local Spec Kit state — not meant to be shared.
2+# Managed by the Specify CLI; safe to edit (your changes are preserved on refresh).
3+
4+# Local pointer to the current feature directory. Rewritten every time you
5+# switch features, so it is per-checkout state rather than something to share.
6+feature.json
7+
8+# Per-machine extension config overrides.
9+extensions/*/local-config.yml
added .specify/init-options.json +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+{
2+ "ai": "claude",
3+ "ai_skills": true,
4+ "feature_numbering": "sequential",
5+ "here": false,
6+ "integration": "claude",
7+ "script": "sh",
8+ "speckit_version": "1.0.4"
9+}
new file mode 100644
@@ -0,0 +1,9 @@
1+{
2+ "ai": "claude",
3+ "ai_skills": true,
4+ "feature_numbering": "sequential",
5+ "here": false,
6+ "integration": "claude",
7+ "script": "sh",
8+ "speckit_version": "1.0.4"
9+}
added .specify/integration.json +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+{
2+ "version": "1.0.4",
3+ "integration_state_schema": 1,
4+ "installed_integrations": [
5+ "claude"
6+ ],
7+ "integration_settings": {
8+ "claude": {
9+ "script": "sh",
10+ "invoke_separator": "-"
11+ }
12+ },
13+ "integration": "claude",
14+ "default_integration": "claude"
15+}
new file mode 100644
@@ -0,0 +1,15 @@
1+{
2+ "version": "1.0.4",
3+ "integration_state_schema": 1,
4+ "installed_integrations": [
5+ "claude"
6+ ],
7+ "integration_settings": {
8+ "claude": {
9+ "script": "sh",
10+ "invoke_separator": "-"
11+ }
12+ },
13+ "integration": "claude",
14+ "default_integration": "claude"
15+}
added .specify/integrations/claude.manifest.json +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+{
2+ "integration": "claude",
3+ "version": "1.0.4",
4+ "installed_at": "2026-09-07T14:46:33.242231+00:00",
5+ "files": {
6+ ".claude/skills/speckit-analyze/SKILL.md": "72a6e6ff794e3099debe70e492433b94e6c8da57e49e03e8711b506ecfc3e608",
7+ ".claude/skills/speckit-clarify/SKILL.md": "f4b3f2c95087ac2343c0b67faff67f7223d34213ca1816aa25908db5b9aff0ac",
8+ ".claude/skills/speckit-constitution/SKILL.md": "a93047917a5fefeefff7aa35991109ce8f9938c4890e52206408b511bf756eb5",
9+ ".claude/skills/speckit-implement/SKILL.md": "51bd89322e0258ae377ea66a6af41a159b0e1a05304d5a17ea0f9a9baa6640a9",
10+ ".claude/skills/speckit-converge/SKILL.md": "6eca60f035306017d43afefd3a7a23ae448484b584080aa404ff65e3e3fdec0a",
11+ ".claude/skills/speckit-plan/SKILL.md": "2fe3f96886e96284965c8586d14df5d226c1e796b3eb39d110c9d8231abe6417",
12+ ".claude/skills/speckit-checklist/SKILL.md": "34c8c681f5472f2790d65ac29ca01e23f4f32ab6f06c9dca1b38fc89e2cfba86",
13+ ".claude/skills/speckit-specify/SKILL.md": "42fe016b9183bb8fa7ce7c65e04ea8d382f7f2abfc94849aeead999247675886",
14+ ".claude/skills/speckit-tasks/SKILL.md": "597853362a0a770fe967c5d56136e2db38de1aa7b97b89552db667b70ac493c1",
15+ ".claude/skills/speckit-taskstoissues/SKILL.md": "76a6ec1fcc2d4f2f4ae4d1a70e59eca034fd9a3f97c5454094b023ed3da65151"
16+ }
17+}
new file mode 100644
@@ -0,0 +1,17 @@
1+{
2+ "integration": "claude",
3+ "version": "1.0.4",
4+ "installed_at": "2026-09-07T14:46:33.242231+00:00",
5+ "files": {
6+ ".claude/skills/speckit-analyze/SKILL.md": "72a6e6ff794e3099debe70e492433b94e6c8da57e49e03e8711b506ecfc3e608",
7+ ".claude/skills/speckit-clarify/SKILL.md": "f4b3f2c95087ac2343c0b67faff67f7223d34213ca1816aa25908db5b9aff0ac",
8+ ".claude/skills/speckit-constitution/SKILL.md": "a93047917a5fefeefff7aa35991109ce8f9938c4890e52206408b511bf756eb5",
9+ ".claude/skills/speckit-implement/SKILL.md": "51bd89322e0258ae377ea66a6af41a159b0e1a05304d5a17ea0f9a9baa6640a9",
10+ ".claude/skills/speckit-converge/SKILL.md": "6eca60f035306017d43afefd3a7a23ae448484b584080aa404ff65e3e3fdec0a",
11+ ".claude/skills/speckit-plan/SKILL.md": "2fe3f96886e96284965c8586d14df5d226c1e796b3eb39d110c9d8231abe6417",
12+ ".claude/skills/speckit-checklist/SKILL.md": "34c8c681f5472f2790d65ac29ca01e23f4f32ab6f06c9dca1b38fc89e2cfba86",
13+ ".claude/skills/speckit-specify/SKILL.md": "42fe016b9183bb8fa7ce7c65e04ea8d382f7f2abfc94849aeead999247675886",
14+ ".claude/skills/speckit-tasks/SKILL.md": "597853362a0a770fe967c5d56136e2db38de1aa7b97b89552db667b70ac493c1",
15+ ".claude/skills/speckit-taskstoissues/SKILL.md": "76a6ec1fcc2d4f2f4ae4d1a70e59eca034fd9a3f97c5454094b023ed3da65151"
16+ }
17+}
added .specify/integrations/speckit.manifest.json +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+{
2+ "integration": "speckit",
3+ "version": "1.0.4",
4+ "installed_at": "2026-09-07T14:46:33.263099+00:00",
5+ "files": {
6+ ".specify/scripts/bash/common.sh": "170e91ece502b88d83c715e427473843e0b358f550e86e3ff524546df405a9ca",
7+ ".specify/scripts/bash/setup-plan.sh": "f417e1b8de7a48fa9d5fea3aabea8fe62149fa11c4b41b3103de06efe73dec8d",
8+ ".specify/scripts/bash/setup-tasks.sh": "4a33dd1e6c32ddc7d570f4b538572c54069192573eed0ec654d9fb826e31a4fe",
9+ ".specify/scripts/bash/check-prerequisites.sh": "daa377146db4fb69912f42611a7b91c5d55873b3e3e27ed33bd8d67505826344",
10+ ".specify/scripts/bash/resolve-template.sh": "829e227096abc8bf0889889ec9f792f503ca5d395b7836a8a7eb739ee75214e7",
11+ ".specify/scripts/bash/create-new-feature.sh": "fe99ea8da184380056ce8512ca5d37f67fb499ee4234835c15dcf7c4fb75451c",
12+ ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3",
13+ ".specify/templates/checklist-template.md": "856532b3cb66171c662cc16f16b31a5856e4655a8666aad1e545bbfc7f603ca1",
14+ ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842",
15+ ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e",
16+ ".specify/templates/plan-template.md": "7e637502d41eccf0ca672496636365691fdca62ef37b27ec07fcb412dbfa90d4",
17+ ".specify/.gitignore": "8c908410d177a1ef3d0dee16d7ad55f2ac3333df3104c4d4adee1c9b82f1dbc1"
18+ }
19+}
new file mode 100644
@@ -0,0 +1,19 @@
1+{
2+ "integration": "speckit",
3+ "version": "1.0.4",
4+ "installed_at": "2026-09-07T14:46:33.263099+00:00",
5+ "files": {
6+ ".specify/scripts/bash/common.sh": "170e91ece502b88d83c715e427473843e0b358f550e86e3ff524546df405a9ca",
7+ ".specify/scripts/bash/setup-plan.sh": "f417e1b8de7a48fa9d5fea3aabea8fe62149fa11c4b41b3103de06efe73dec8d",
8+ ".specify/scripts/bash/setup-tasks.sh": "4a33dd1e6c32ddc7d570f4b538572c54069192573eed0ec654d9fb826e31a4fe",
9+ ".specify/scripts/bash/check-prerequisites.sh": "daa377146db4fb69912f42611a7b91c5d55873b3e3e27ed33bd8d67505826344",
10+ ".specify/scripts/bash/resolve-template.sh": "829e227096abc8bf0889889ec9f792f503ca5d395b7836a8a7eb739ee75214e7",
11+ ".specify/scripts/bash/create-new-feature.sh": "fe99ea8da184380056ce8512ca5d37f67fb499ee4234835c15dcf7c4fb75451c",
12+ ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3",
13+ ".specify/templates/checklist-template.md": "856532b3cb66171c662cc16f16b31a5856e4655a8666aad1e545bbfc7f603ca1",
14+ ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842",
15+ ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e",
16+ ".specify/templates/plan-template.md": "7e637502d41eccf0ca672496636365691fdca62ef37b27ec07fcb412dbfa90d4",
17+ ".specify/.gitignore": "8c908410d177a1ef3d0dee16d7ad55f2ac3333df3104c4d4adee1c9b82f1dbc1"
18+ }
19+}
added .specify/memory/.constitution-template.json +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+{
2+ "sha256": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3",
3+ "source": "core"
4+}
new file mode 100644
@@ -0,0 +1,4 @@
1+{
2+ "sha256": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3",
3+ "source": "core"
4+}
added .specify/memory/constitution.md +50 -0
new file mode 100644
@@ -0,0 +1,50 @@
1+# [PROJECT_NAME] Constitution
2+<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
3+
4+## Core Principles
5+
6+### [PRINCIPLE_1_NAME]
7+<!-- Example: I. Library-First -->
8+[PRINCIPLE_1_DESCRIPTION]
9+<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
10+
11+### [PRINCIPLE_2_NAME]
12+<!-- Example: II. CLI Interface -->
13+[PRINCIPLE_2_DESCRIPTION]
14+<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
15+
16+### [PRINCIPLE_3_NAME]
17+<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
18+[PRINCIPLE_3_DESCRIPTION]
19+<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
20+
21+### [PRINCIPLE_4_NAME]
22+<!-- Example: IV. Integration Testing -->
23+[PRINCIPLE_4_DESCRIPTION]
24+<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
25+
26+### [PRINCIPLE_5_NAME]
27+<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
28+[PRINCIPLE_5_DESCRIPTION]
29+<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
30+
31+## [SECTION_2_NAME]
32+<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
33+
34+[SECTION_2_CONTENT]
35+<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
36+
37+## [SECTION_3_NAME]
38+<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
39+
40+[SECTION_3_CONTENT]
41+<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
42+
43+## Governance
44+<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
45+
46+[GOVERNANCE_RULES]
47+<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
48+
49+**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
50+<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
new file mode 100644
@@ -0,0 +1,50 @@
1+# [PROJECT_NAME] Constitution
2+<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
3+
4+## Core Principles
5+
6+### [PRINCIPLE_1_NAME]
7+<!-- Example: I. Library-First -->
8+[PRINCIPLE_1_DESCRIPTION]
9+<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
10+
11+### [PRINCIPLE_2_NAME]
12+<!-- Example: II. CLI Interface -->
13+[PRINCIPLE_2_DESCRIPTION]
14+<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
15+
16+### [PRINCIPLE_3_NAME]
17+<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
18+[PRINCIPLE_3_DESCRIPTION]
19+<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
20+
21+### [PRINCIPLE_4_NAME]
22+<!-- Example: IV. Integration Testing -->
23+[PRINCIPLE_4_DESCRIPTION]
24+<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
25+
26+### [PRINCIPLE_5_NAME]
27+<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
28+[PRINCIPLE_5_DESCRIPTION]
29+<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
30+
31+## [SECTION_2_NAME]
32+<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
33+
34+[SECTION_2_CONTENT]
35+<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
36+
37+## [SECTION_3_NAME]
38+<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
39+
40+[SECTION_3_CONTENT]
41+<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
42+
43+## Governance
44+<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
45+
46+[GOVERNANCE_RULES]
47+<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
48+
49+**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
50+<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
added .specify/scripts/bash/check-prerequisites.sh +243 -0
new file mode 100755
@@ -0,0 +1,243 @@
1+#!/usr/bin/env bash
2+
3+# Consolidated prerequisite checking script
4+#
5+# This script provides unified prerequisite checking for Spec-Driven Development workflow.
6+# It replaces the functionality previously spread across multiple scripts.
7+#
8+# Usage: ./check-prerequisites.sh [OPTIONS]
9+#
10+# OPTIONS:
11+# --json Output in JSON format
12+# --require-spec Require spec.md to exist (for analysis phase)
13+# --require-tasks Require tasks.md to exist (for implementation phase)
14+# --include-tasks Include tasks.md in AVAILABLE_DOCS list
15+# --paths-only Only output path variables (no validation)
16+# --template NAME Include composed template content in JSON output
17+# --help, -h Show help message
18+#
19+# OUTPUTS:
20+# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
21+# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
22+# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
23+
24+set -e
25+
26+# Parse command line arguments
27+JSON_MODE=false
28+REQUIRE_SPEC=false
29+REQUIRE_TASKS=false
30+INCLUDE_TASKS=false
31+PATHS_ONLY=false
32+TEMPLATE_NAME=""
33+
34+while [[ $# -gt 0 ]]; do
35+ case "$1" in
36+ --json)
37+ JSON_MODE=true
38+ ;;
39+ --require-spec)
40+ REQUIRE_SPEC=true
41+ ;;
42+ --require-tasks)
43+ REQUIRE_TASKS=true
44+ ;;
45+ --include-tasks)
46+ INCLUDE_TASKS=true
47+ ;;
48+ --paths-only)
49+ PATHS_ONLY=true
50+ ;;
51+ --template)
52+ shift
53+ if [[ $# -eq 0 ]]; then
54+ echo "ERROR: --template requires a template name" >&2
55+ exit 1
56+ fi
57+ TEMPLATE_NAME="$1"
58+ ;;
59+ --help|-h)
60+ cat << 'EOF'
61+Usage: check-prerequisites.sh [OPTIONS]
62+
63+Consolidated prerequisite checking for Spec-Driven Development workflow.
64+
65+OPTIONS:
66+ --json Output in JSON format
67+ --require-spec Require spec.md to exist (for analysis phase)
68+ --require-tasks Require tasks.md to exist (for implementation phase)
69+ --include-tasks Include tasks.md in AVAILABLE_DOCS list
70+ --paths-only Only output path variables (no prerequisite validation)
71+ --template NAME Include composed template content in JSON output
72+ --help, -h Show this help message
73+
74+EXAMPLES:
75+ # Check task prerequisites (plan.md required)
76+ ./check-prerequisites.sh --json
77+
78+ # Check implementation prerequisites (plan.md + tasks.md required)
79+ ./check-prerequisites.sh --json --require-tasks --include-tasks
80+
81+ # Get feature paths only (no validation)
82+ ./check-prerequisites.sh --paths-only
83+
84+EOF
85+ exit 0
86+ ;;
87+ *)
88+ echo "ERROR: Unknown option '$1'. Use --help for usage information." >&2
89+ exit 1
90+ ;;
91+ esac
92+ shift
93+done
94+
95+# Source common functions
96+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
97+source "$SCRIPT_DIR/common.sh"
98+
99+# Get feature paths.
100+# In --paths-only mode this is pure resolution, so pass --no-persist to opt out
101+# of the feature.json write side effect (issue #3025).
102+if $PATHS_ONLY; then
103+ _paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
104+else
105+ _paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
106+fi
107+eval "$_paths_output"
108+unset _paths_output
109+
110+# If paths-only mode, output paths and exit (no validation)
111+if $PATHS_ONLY; then
112+ if $JSON_MODE; then
113+ # Minimal JSON paths payload (no validation performed)
114+ if has_jq; then
115+ jq -cn \
116+ --arg repo_root "$REPO_ROOT" \
117+ --arg branch "$CURRENT_BRANCH" \
118+ --arg feature_dir "$FEATURE_DIR" \
119+ --arg feature_spec "$FEATURE_SPEC" \
120+ --arg impl_plan "$IMPL_PLAN" \
121+ --arg tasks "$TASKS" \
122+ '{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
123+ else
124+ printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
125+ "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
126+ fi
127+ else
128+ echo "REPO_ROOT: $REPO_ROOT"
129+ echo "BRANCH: $CURRENT_BRANCH"
130+ echo "FEATURE_DIR: $FEATURE_DIR"
131+ echo "FEATURE_SPEC: $FEATURE_SPEC"
132+ echo "IMPL_PLAN: $IMPL_PLAN"
133+ echo "TASKS: $TASKS"
134+ fi
135+ exit 0
136+fi
137+
138+# Validate required directories and files
139+if [[ ! -d "$FEATURE_DIR" ]]; then
140+ echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
141+ echo "Run /speckit-specify first to create the feature structure." >&2
142+ exit 1
143+fi
144+
145+if [[ ! -f "$IMPL_PLAN" ]]; then
146+ echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
147+ echo "Run /speckit-plan first to create the implementation plan." >&2
148+ exit 1
149+fi
150+
151+# Check for spec.md if required
152+if $REQUIRE_SPEC && [[ ! -f "$FEATURE_SPEC" ]]; then
153+ echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
154+ echo "Run /speckit-specify first to create the feature specification." >&2
155+ exit 1
156+fi
157+
158+# Check for tasks.md if required
159+if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
160+ echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
161+ echo "Run /speckit-tasks first to create the task list." >&2
162+ exit 1
163+fi
164+
165+# Build list of available documents
166+docs=()
167+
168+# Always check these optional docs
169+[[ -f "$RESEARCH" ]] && docs+=("research.md")
170+[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
171+
172+# Check contracts directory (only if it exists and has files)
173+if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
174+ docs+=("contracts/")
175+fi
176+
177+[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
178+
179+# Include tasks.md if requested and it exists
180+if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
181+ docs+=("tasks.md")
182+fi
183+
184+TEMPLATE_CONTENT=""
185+if [[ -n "$TEMPLATE_NAME" ]]; then
186+ if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
187+ TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}"
188+ else
189+ echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2
190+ exit 1
191+ fi
192+fi
193+
194+# Output results
195+if $JSON_MODE; then
196+ # Build JSON array of documents
197+ if has_jq; then
198+ if [[ ${#docs[@]} -eq 0 ]]; then
199+ json_docs="[]"
200+ else
201+ json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
202+ fi
203+ if [[ -n "$TEMPLATE_NAME" ]]; then
204+ jq -cn \
205+ --arg feature_dir "$FEATURE_DIR" \
206+ --argjson docs "$json_docs" \
207+ --arg template_content "$TEMPLATE_CONTENT" \
208+ '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TEMPLATE_CONTENT:$template_content}'
209+ else
210+ jq -cn \
211+ --arg feature_dir "$FEATURE_DIR" \
212+ --argjson docs "$json_docs" \
213+ '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
214+ fi
215+ else
216+ if [[ ${#docs[@]} -eq 0 ]]; then
217+ json_docs="[]"
218+ else
219+ json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
220+ json_docs="[${json_docs%,}]"
221+ fi
222+ if [[ -n "$TEMPLATE_NAME" ]]; then
223+ printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TEMPLATE_CONTENT":"%s"}\n' \
224+ "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "$TEMPLATE_CONTENT")"
225+ else
226+ printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
227+ fi
228+ fi
229+else
230+ # Text output
231+ echo "FEATURE_DIR:$FEATURE_DIR"
232+ echo "AVAILABLE_DOCS:"
233+
234+ # Show status of each potential document
235+ check_file "$RESEARCH" "research.md"
236+ check_file "$DATA_MODEL" "data-model.md"
237+ check_dir "$CONTRACTS_DIR" "contracts/"
238+ check_file "$QUICKSTART" "quickstart.md"
239+
240+ if $INCLUDE_TASKS; then
241+ check_file "$TASKS" "tasks.md"
242+ fi
243+fi
new file mode 100755
@@ -0,0 +1,243 @@
1+#!/usr/bin/env bash
2+
3+# Consolidated prerequisite checking script
4+#
5+# This script provides unified prerequisite checking for Spec-Driven Development workflow.
6+# It replaces the functionality previously spread across multiple scripts.
7+#
8+# Usage: ./check-prerequisites.sh [OPTIONS]
9+#
10+# OPTIONS:
11+# --json Output in JSON format
12+# --require-spec Require spec.md to exist (for analysis phase)
13+# --require-tasks Require tasks.md to exist (for implementation phase)
14+# --include-tasks Include tasks.md in AVAILABLE_DOCS list
15+# --paths-only Only output path variables (no validation)
16+# --template NAME Include composed template content in JSON output
17+# --help, -h Show help message
18+#
19+# OUTPUTS:
20+# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
21+# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
22+# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
23+
24+set -e
25+
26+# Parse command line arguments
27+JSON_MODE=false
28+REQUIRE_SPEC=false
29+REQUIRE_TASKS=false
30+INCLUDE_TASKS=false
31+PATHS_ONLY=false
32+TEMPLATE_NAME=""
33+
34+while [[ $# -gt 0 ]]; do
35+ case "$1" in
36+ --json)
37+ JSON_MODE=true
38+ ;;
39+ --require-spec)
40+ REQUIRE_SPEC=true
41+ ;;
42+ --require-tasks)
43+ REQUIRE_TASKS=true
44+ ;;
45+ --include-tasks)
46+ INCLUDE_TASKS=true
47+ ;;
48+ --paths-only)
49+ PATHS_ONLY=true
50+ ;;
51+ --template)
52+ shift
53+ if [[ $# -eq 0 ]]; then
54+ echo "ERROR: --template requires a template name" >&2
55+ exit 1
56+ fi
57+ TEMPLATE_NAME="$1"
58+ ;;
59+ --help|-h)
60+ cat << 'EOF'
61+Usage: check-prerequisites.sh [OPTIONS]
62+
63+Consolidated prerequisite checking for Spec-Driven Development workflow.
64+
65+OPTIONS:
66+ --json Output in JSON format
67+ --require-spec Require spec.md to exist (for analysis phase)
68+ --require-tasks Require tasks.md to exist (for implementation phase)
69+ --include-tasks Include tasks.md in AVAILABLE_DOCS list
70+ --paths-only Only output path variables (no prerequisite validation)
71+ --template NAME Include composed template content in JSON output
72+ --help, -h Show this help message
73+
74+EXAMPLES:
75+ # Check task prerequisites (plan.md required)
76+ ./check-prerequisites.sh --json
77+
78+ # Check implementation prerequisites (plan.md + tasks.md required)
79+ ./check-prerequisites.sh --json --require-tasks --include-tasks
80+
81+ # Get feature paths only (no validation)
82+ ./check-prerequisites.sh --paths-only
83+
84+EOF
85+ exit 0
86+ ;;
87+ *)
88+ echo "ERROR: Unknown option '$1'. Use --help for usage information." >&2
89+ exit 1
90+ ;;
91+ esac
92+ shift
93+done
94+
95+# Source common functions
96+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
97+source "$SCRIPT_DIR/common.sh"
98+
99+# Get feature paths.
100+# In --paths-only mode this is pure resolution, so pass --no-persist to opt out
101+# of the feature.json write side effect (issue #3025).
102+if $PATHS_ONLY; then
103+ _paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
104+else
105+ _paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
106+fi
107+eval "$_paths_output"
108+unset _paths_output
109+
110+# If paths-only mode, output paths and exit (no validation)
111+if $PATHS_ONLY; then
112+ if $JSON_MODE; then
113+ # Minimal JSON paths payload (no validation performed)
114+ if has_jq; then
115+ jq -cn \
116+ --arg repo_root "$REPO_ROOT" \
117+ --arg branch "$CURRENT_BRANCH" \
118+ --arg feature_dir "$FEATURE_DIR" \
119+ --arg feature_spec "$FEATURE_SPEC" \
120+ --arg impl_plan "$IMPL_PLAN" \
121+ --arg tasks "$TASKS" \
122+ '{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
123+ else
124+ printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
125+ "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
126+ fi
127+ else
128+ echo "REPO_ROOT: $REPO_ROOT"
129+ echo "BRANCH: $CURRENT_BRANCH"
130+ echo "FEATURE_DIR: $FEATURE_DIR"
131+ echo "FEATURE_SPEC: $FEATURE_SPEC"
132+ echo "IMPL_PLAN: $IMPL_PLAN"
133+ echo "TASKS: $TASKS"
134+ fi
135+ exit 0
136+fi
137+
138+# Validate required directories and files
139+if [[ ! -d "$FEATURE_DIR" ]]; then
140+ echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
141+ echo "Run /speckit-specify first to create the feature structure." >&2
142+ exit 1
143+fi
144+
145+if [[ ! -f "$IMPL_PLAN" ]]; then
146+ echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
147+ echo "Run /speckit-plan first to create the implementation plan." >&2
148+ exit 1
149+fi
150+
151+# Check for spec.md if required
152+if $REQUIRE_SPEC && [[ ! -f "$FEATURE_SPEC" ]]; then
153+ echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
154+ echo "Run /speckit-specify first to create the feature specification." >&2
155+ exit 1
156+fi
157+
158+# Check for tasks.md if required
159+if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
160+ echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
161+ echo "Run /speckit-tasks first to create the task list." >&2
162+ exit 1
163+fi
164+
165+# Build list of available documents
166+docs=()
167+
168+# Always check these optional docs
169+[[ -f "$RESEARCH" ]] && docs+=("research.md")
170+[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
171+
172+# Check contracts directory (only if it exists and has files)
173+if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
174+ docs+=("contracts/")
175+fi
176+
177+[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
178+
179+# Include tasks.md if requested and it exists
180+if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
181+ docs+=("tasks.md")
182+fi
183+
184+TEMPLATE_CONTENT=""
185+if [[ -n "$TEMPLATE_NAME" ]]; then
186+ if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
187+ TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}"
188+ else
189+ echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2
190+ exit 1
191+ fi
192+fi
193+
194+# Output results
195+if $JSON_MODE; then
196+ # Build JSON array of documents
197+ if has_jq; then
198+ if [[ ${#docs[@]} -eq 0 ]]; then
199+ json_docs="[]"
200+ else
201+ json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
202+ fi
203+ if [[ -n "$TEMPLATE_NAME" ]]; then
204+ jq -cn \
205+ --arg feature_dir "$FEATURE_DIR" \
206+ --argjson docs "$json_docs" \
207+ --arg template_content "$TEMPLATE_CONTENT" \
208+ '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TEMPLATE_CONTENT:$template_content}'
209+ else
210+ jq -cn \
211+ --arg feature_dir "$FEATURE_DIR" \
212+ --argjson docs "$json_docs" \
213+ '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
214+ fi
215+ else
216+ if [[ ${#docs[@]} -eq 0 ]]; then
217+ json_docs="[]"
218+ else
219+ json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
220+ json_docs="[${json_docs%,}]"
221+ fi
222+ if [[ -n "$TEMPLATE_NAME" ]]; then
223+ printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TEMPLATE_CONTENT":"%s"}\n' \
224+ "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "$TEMPLATE_CONTENT")"
225+ else
226+ printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
227+ fi
228+ fi
229+else
230+ # Text output
231+ echo "FEATURE_DIR:$FEATURE_DIR"
232+ echo "AVAILABLE_DOCS:"
233+
234+ # Show status of each potential document
235+ check_file "$RESEARCH" "research.md"
236+ check_file "$DATA_MODEL" "data-model.md"
237+ check_dir "$CONTRACTS_DIR" "contracts/"
238+ check_file "$QUICKSTART" "quickstart.md"
239+
240+ if $INCLUDE_TASKS; then
241+ check_file "$TASKS" "tasks.md"
242+ fi
243+fi
added .specify/scripts/bash/common.sh +926 -0
new file mode 100755
@@ -0,0 +1,926 @@
1+#!/usr/bin/env bash
2+# Common functions and variables for all scripts
3+
4+# Find repository root by searching upward for .specify directory
5+# This is the primary marker for spec-kit projects
6+find_specify_root() {
7+ local dir="${1:-$(pwd)}"
8+ # Normalize to absolute path to prevent infinite loop with relative paths
9+ # Use -- to handle paths starting with - (e.g., -P, -L)
10+ dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1
11+ local prev_dir=""
12+ while true; do
13+ if [ -d "$dir/.specify" ]; then
14+ echo "$dir"
15+ return 0
16+ fi
17+ # Stop if we've reached filesystem root or dirname stops changing
18+ if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then
19+ break
20+ fi
21+ prev_dir="$dir"
22+ dir="$(dirname "$dir")"
23+ done
24+ return 1
25+}
26+
27+# Resolve an explicit SPECIFY_INIT_DIR project override (the directory that
28+# *contains* .specify/), for non-interactive / CI use — e.g. running a Spec Kit
29+# command against a member project from a monorepo root without cd.
30+#
31+# Precondition: SPECIFY_INIT_DIR is non-empty. Echoes the validated absolute
32+# project root, or prints an error and returns 1. Strict by design: the path
33+# must exist and contain .specify/, with no silent fallback to cwd or the
34+# script-location default (which would silently write to the wrong project).
35+#
36+# This is the single resolver: bundled extensions inherit it by sourcing core
37+# (e.g. the git extension's create-new-feature-branch) rather than duplicating it.
38+resolve_specify_init_dir() {
39+ local init_root
40+ # Normalize: relative paths resolve against $(pwd); a trailing slash collapses.
41+ # CDPATH="" so a relative value cannot be resolved against the caller's CDPATH
42+ # (which would also echo to stdout and corrupt the captured path).
43+ if ! init_root="$(CDPATH="" cd -- "$SPECIFY_INIT_DIR" 2>/dev/null && pwd)"; then
44+ echo "ERROR: SPECIFY_INIT_DIR does not point to an existing directory: $SPECIFY_INIT_DIR" >&2
45+ return 1
46+ fi
47+ if [[ ! -d "$init_root/.specify" ]]; then
48+ echo "ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): $init_root" >&2
49+ return 1
50+ fi
51+ printf '%s\n' "$init_root"
52+}
53+
54+# Get repository root, prioritizing .specify directory
55+# This prevents using a parent repository when spec-kit is initialized in a subdirectory
56+get_repo_root() {
57+ # Explicit project override wins (see resolve_specify_init_dir).
58+ if [[ -n "${SPECIFY_INIT_DIR:-}" ]]; then
59+ resolve_specify_init_dir
60+ return
61+ fi
62+
63+ # First, look for .specify directory (spec-kit's own marker)
64+ local specify_root
65+ if specify_root=$(find_specify_root); then
66+ echo "$specify_root"
67+ return
68+ fi
69+
70+ # Final fallback to script location
71+ local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
72+ (cd "$script_dir/../../.." && pwd)
73+}
74+
75+# Get current feature name from explicit state only.
76+# Returns the feature identifier or empty string if none is set.
77+# Feature state is set by SPECIFY_FEATURE (from create-new-feature or
78+# the git extension) or implicitly via .specify/feature.json.
79+get_current_branch() {
80+ if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
81+ echo "$SPECIFY_FEATURE"
82+ return
83+ fi
84+
85+ # No explicit feature set — caller must handle this via feature.json
86+ # in get_feature_paths(). Return empty to signal "unknown".
87+ echo ""
88+}
89+
90+# Safely read .specify/feature.json's "feature_directory" value.
91+# Prints the raw value (possibly relative) to stdout, or empty string if the file
92+# is missing, unparseable, or does not contain the key. Always returns 0 so callers
93+# under `set -e` cannot be aborted by parser failure.
94+# Parser order mirrors the historical get_feature_paths behavior: jq -> python3 -> grep/sed.
95+read_feature_json_feature_directory() {
96+ local repo_root="$1"
97+ local fj="$repo_root/.specify/feature.json"
98+ [[ -f "$fj" ]] || { printf '%s' ''; return 0; }
99+
100+ # Try parsers in order (jq -> python3 -> grep/sed), falling through on
101+ # failure. Selection is by *parse success*, not mere availability: on
102+ # Windows `python3` commonly resolves to the Microsoft Store App Execution
103+ # Alias stub, which passes `command -v` but fails at runtime (exit 49), so
104+ # an availability-gated `elif` would pick python3, swallow its failure, and
105+ # never reach the grep/sed fallback -- leaving feature.json unreadable even
106+ # though it is valid (issue #3304).
107+ local _fd=''
108+ if command -v jq >/dev/null 2>&1; then
109+ if ! _fd=$(jq -r '.feature_directory // empty' "$fj" 2>/dev/null); then
110+ _fd=''
111+ fi
112+ fi
113+ if [[ -z "$_fd" ]] && command -v python3 >/dev/null 2>&1; then
114+ # Use Python so pretty-printed/multi-line JSON still parses correctly.
115+ if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then
116+ _fd=''
117+ fi
118+ fi
119+ if [[ -z "$_fd" ]]; then
120+ # Last-resort single-line grep/sed fallback. The `|| true` guards against
121+ # grep returning 1 (no match) aborting under `set -e` / `pipefail`.
122+ _fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \
123+ | head -n 1 \
124+ | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
125+ fi
126+
127+ printf '%s' "$_fd"
128+ return 0
129+}
130+
131+# Persist a feature_directory value to .specify/feature.json.
132+# Writes only when the file is missing or the value differs from what's stored.
133+# Accepts the raw (possibly relative) path — callers should pass the original
134+# user-supplied value, not the normalized absolute path.
135+_persist_feature_json() {
136+ local repo_root="$1"
137+ local feature_dir_value="$2"
138+ local fj="$repo_root/.specify/feature.json"
139+
140+ # Strip repo_root prefix if the value is absolute and under repo_root
141+ if [[ "$feature_dir_value" == "$repo_root/"* ]]; then
142+ feature_dir_value="${feature_dir_value#"$repo_root/"}"
143+ fi
144+
145+ # Read current value (if any) and skip write when unchanged
146+ local current_val
147+ current_val=$(read_feature_json_feature_directory "$repo_root")
148+ if [[ "$current_val" == "$feature_dir_value" ]]; then
149+ return 0
150+ fi
151+
152+ # Ensure .specify/ directory exists
153+ mkdir -p "$repo_root/.specify"
154+
155+ # Write feature.json — prefer jq for safe JSON, fall back to printf
156+ if command -v jq >/dev/null 2>&1; then
157+ jq -cn --arg fd "$feature_dir_value" '{feature_directory:$fd}' > "$fj"
158+ else
159+ printf '{"feature_directory":"%s"}\n' "$(json_escape "$feature_dir_value")" > "$fj"
160+ fi
161+}
162+
163+get_feature_paths() {
164+ # Read-only callers (e.g. check-prerequisites.sh --paths-only) pass
165+ # --no-persist so pure path resolution never writes .specify/feature.json,
166+ # which would dirty the working tree or overwrite a pinned value (issue #3025).
167+ local no_persist=false
168+ if [[ "${1:-}" == "--no-persist" ]]; then
169+ no_persist=true
170+ shift
171+ fi
172+
173+ # Split decl/assignment so a SPECIFY_INIT_DIR validation failure in
174+ # get_repo_root propagates as a hard error instead of being masked by `local`.
175+ local repo_root
176+ repo_root=$(get_repo_root) || return 1
177+ local current_branch
178+ current_branch=$(get_current_branch)
179+
180+ # Resolve feature directory. Priority:
181+ # 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
182+ # 2. .specify/feature.json "feature_directory" key (persisted by specify command)
183+ # 3. Error — no feature context available
184+ local feature_dir
185+ if [[ -n "${SPECIFY_FEATURE_DIRECTORY:-}" ]]; then
186+ feature_dir="$SPECIFY_FEATURE_DIRECTORY"
187+ # Normalize relative paths to absolute under repo root
188+ [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
189+ # Persist to feature.json so future sessions without the env var still
190+ # work — unless the caller opted out for read-only resolution (#3025).
191+ if [[ "$no_persist" != true ]]; then
192+ _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
193+ fi
194+ elif [[ -f "$repo_root/.specify/feature.json" ]]; then
195+ local _fd
196+ _fd=$(read_feature_json_feature_directory "$repo_root")
197+ if [[ -n "$_fd" ]]; then
198+ feature_dir="$_fd"
199+ # Normalize relative paths to absolute under repo root
200+ [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
201+ else
202+ echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or ensure .specify/feature.json contains feature_directory." >&2
203+ return 1
204+ fi
205+ else
206+ echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or run the specify command to create .specify/feature.json." >&2
207+ return 1
208+ fi
209+
210+ # When no branch context exists (no SPECIFY_FEATURE, feature resolved via
211+ # SPECIFY_FEATURE_DIRECTORY or feature.json), fall back to the feature
212+ # directory basename so CURRENT_BRANCH is a usable identifier rather than
213+ # an empty, misleading value (issue #3026).
214+ if [[ -z "$current_branch" ]]; then
215+ local feature_dir_trimmed="${feature_dir%/}"
216+ current_branch="${feature_dir_trimmed##*/}"
217+ fi
218+
219+ # Use printf '%q' to safely quote values, preventing shell injection
220+ # via crafted branch names or paths containing special characters
221+ printf 'REPO_ROOT=%q\n' "$repo_root"
222+ printf 'CURRENT_BRANCH=%q\n' "$current_branch"
223+ printf 'FEATURE_DIR=%q\n' "$feature_dir"
224+ printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
225+ printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
226+ printf 'TASKS=%q\n' "$feature_dir/tasks.md"
227+ printf 'RESEARCH=%q\n' "$feature_dir/research.md"
228+ printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
229+ printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
230+ printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
231+}
232+
233+# Check if jq is available for safe JSON construction
234+has_jq() {
235+ command -v jq >/dev/null 2>&1
236+}
237+
238+get_invoke_separator() {
239+ local repo_root="${1:-$(get_repo_root)}"
240+ if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
241+ printf '%s\n' "$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
242+ return 0
243+ fi
244+
245+ local integration_json="$repo_root/.specify/integration.json"
246+ local separator="."
247+ local parsed=0
248+
249+ if [[ -f "$integration_json" ]]; then
250+ # Try parsers in order (jq -> python3 -> awk), falling through on
251+ # failure. Selection is by *parse success*, not mere availability: on
252+ # Windows `python3` commonly resolves to the Microsoft Store App
253+ # Execution Alias stub, which passes `command -v` but fails at runtime
254+ # (exit 49). An availability-gated branch would pick python3, swallow
255+ # its failure, and — because this function historically had no text
256+ # fallback — silently return "." even for `-`-separator integrations
257+ # (e.g. forge, cline), yielding wrong command hints (issue #3304).
258+ if command -v jq >/dev/null 2>&1; then
259+ local jq_separator
260+ if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then
261+ case "$jq_separator" in
262+ "."|"-") separator="$jq_separator"; parsed=1 ;;
263+ esac
264+ fi
265+ fi
266+
267+ if [[ "$parsed" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then
268+ local py_separator
269+ if py_separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null
270+import json
271+import sys
272+
273+try:
274+ with open(sys.argv[1], encoding="utf-8") as fh:
275+ state = json.load(fh)
276+ key = state.get("default_integration") or state.get("integration") or ""
277+ settings = state.get("integration_settings")
278+ separator = "."
279+ if isinstance(key, str) and isinstance(settings, dict):
280+ entry = settings.get(key)
281+ if isinstance(entry, dict) and entry.get("invoke_separator") in {".", "-"}:
282+ separator = entry["invoke_separator"]
283+ print(separator)
284+except Exception:
285+ sys.exit(1)
286+PY
287+); then
288+ case "$py_separator" in
289+ "."|"-") separator="$py_separator"; parsed=1 ;;
290+ esac
291+ fi
292+ fi
293+
294+ if [[ "$parsed" -eq 0 ]]; then
295+ # Last-resort text fallback for environments with neither jq nor a
296+ # working python3 (e.g. stock Windows + Git Bash). Reads the active
297+ # integration key (default_integration, else integration) and its
298+ # invoke_separator from within the integration_settings object.
299+ # Handles both pretty-printed (the written form) and compact JSON.
300+ # Accumulate all lines into one buffer in END rather than using
301+ # gawk-only whole-file slurp (RS="^$"), so this stays portable to
302+ # the BSD awk on macOS.
303+ local awk_separator
304+ awk_separator=$(awk '
305+ function keyval(d, name, v) {
306+ if (match(d, "\"" name "\"[ \t\r\n]*:[ \t\r\n]*\"[^\"]*\"")) {
307+ v=substr(d,RSTART,RLENGTH); sub(/^.*:[ \t\r\n]*"/,"",v); sub(/"$/,"",v); return v
308+ }
309+ return ""
310+ }
311+ { doc = doc $0 "\n" }
312+ END {
313+ key=keyval(doc,"default_integration"); if (key=="") key=keyval(doc,"integration")
314+ sep="."
315+ if (key!="") {
316+ settings=doc
317+ if (match(doc, /"integration_settings"[ \t\r\n]*:[ \t\r\n]*[{]/)) {
318+ settings=substr(doc, RSTART+RLENGTH-1)
319+ }
320+ if (match(settings, "\"" key "\"[ \t\r\n]*:[ \t\r\n]*[{]")) {
321+ start=RSTART+RLENGTH-1
322+ depth=0
323+ obj=""
324+ for (i=start; i<=length(settings); i++) {
325+ c=substr(settings,i,1)
326+ obj=obj c
327+ if (c=="{") depth++
328+ else if (c=="}") { depth--; if (depth==0) break }
329+ }
330+ if (match(obj, /"invoke_separator"[ \t\r\n]*:[ \t\r\n]*"[-.]"/)) {
331+ tok=substr(obj,RSTART,RLENGTH); s=substr(tok,length(tok)-1,1)
332+ if (s=="." || s=="-") sep=s
333+ }
334+ }
335+ }
336+ print sep
337+ }
338+ ' "$integration_json" 2>/dev/null)
339+ case "$awk_separator" in
340+ "."|"-") separator="$awk_separator" ;;
341+ esac
342+ fi
343+ fi
344+
345+ _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
346+ _SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
347+ printf '%s\n' "$separator"
348+}
349+
350+format_speckit_command() {
351+ local command_name="$1"
352+ local repo_root="${2:-$(get_repo_root)}"
353+ local separator
354+ if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
355+ separator="$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
356+ else
357+ separator=$(get_invoke_separator "$repo_root")
358+ _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
359+ _SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
360+ fi
361+
362+ command_name="${command_name#/}"
363+ command_name="${command_name#speckit.}"
364+ command_name="${command_name#speckit-}"
365+ command_name="${command_name//./$separator}"
366+
367+ printf '/speckit%s%s\n' "$separator" "$command_name"
368+}
369+
370+# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
371+# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).
372+json_escape() {
373+ local s="$1"
374+ s="${s//\\/\\\\}"
375+ s="${s//\"/\\\"}"
376+ s="${s//$'\n'/\\n}"
377+ s="${s//$'\t'/\\t}"
378+ s="${s//$'\r'/\\r}"
379+ s="${s//$'\b'/\\b}"
380+ s="${s//$'\f'/\\f}"
381+ # Escape any remaining U+0001-U+001F control characters as \uXXXX.
382+ # (U+0000/NUL cannot appear in bash strings and is excluded.)
383+ # LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes,
384+ # so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.
385+ local LC_ALL=C
386+ local i char code
387+ for (( i=0; i<${#s}; i++ )); do
388+ char="${s:$i:1}"
389+ printf -v code '%d' "'$char" 2>/dev/null || code=256
390+ if (( code >= 1 && code <= 31 )); then
391+ printf '\\u%04x' "$code"
392+ else
393+ printf '%s' "$char"
394+ fi
395+ done
396+}
397+
398+check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
399+check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
400+
401+_python3_command() {
402+ if command -v python3 >/dev/null 2>&1 &&
403+ python3 -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
404+ printf '%s\n' "python3"
405+ elif command -v python >/dev/null 2>&1 &&
406+ python -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
407+ printf '%s\n' "python"
408+ elif command -v py >/dev/null 2>&1 &&
409+ py -3 -c 'import sys' >/dev/null 2>&1; then
410+ printf '%s\n' "py -3"
411+ else
412+ return 1
413+ fi
414+}
415+
416+_sorted_extension_ids() {
417+ local ext_dir="$1"
418+ local python_spec
419+ if python_spec=$(_python3_command); then
420+ local -a python_cmd
421+ read -r -a python_cmd <<< "$python_spec"
422+ local py_stderr sorted_ids
423+ py_stderr=$(mktemp)
424+ if sorted_ids=$(SPECKIT_EXTENSIONS="$ext_dir" "${python_cmd[@]}" -c "
425+import json, os, re, sys
426+from pathlib import Path
427+
428+root = Path(os.environ['SPECKIT_EXTENSIONS'])
429+registered = {}
430+registry = root / '.registry'
431+if os.path.lexists(registry):
432+ if not registry.is_file():
433+ print('registry_invalid: not a regular file', file=sys.stderr)
434+ sys.exit(1)
435+ try:
436+ data = json.loads(registry.read_text(encoding='utf-8'))
437+ except Exception as exc:
438+ print('registry_invalid: ' + str(exc), file=sys.stderr)
439+ sys.exit(1)
440+ if not isinstance(data, dict):
441+ print('registry_invalid: root must be a mapping', file=sys.stderr)
442+ sys.exit(1)
443+ raw_extensions = data.get('extensions', {})
444+ if not isinstance(raw_extensions, dict):
445+ print('registry_invalid: extensions must be a mapping', file=sys.stderr)
446+ sys.exit(1)
447+ registered = raw_extensions
448+
449+def priority(value):
450+ if isinstance(value, bool):
451+ return 10
452+ try:
453+ parsed = int(value)
454+ return parsed if parsed >= 1 else 10
455+ except (TypeError, ValueError, OverflowError):
456+ return 10
457+
458+ranked = []
459+for ext_id, meta in registered.items():
460+ if isinstance(ext_id, str) and re.fullmatch(r'[a-z0-9-]+', ext_id) and isinstance(meta, dict) and bool(meta.get('enabled', True)):
461+ ranked.append((priority(meta.get('priority')), ext_id))
462+for path in root.iterdir():
463+ if path.is_dir() and re.fullmatch(r'[a-z0-9-]+', path.name) and path.name not in registered:
464+ ranked.append((10, path.name))
465+for _, ext_id in sorted(ranked):
466+ print(ext_id)
467+" 2>"$py_stderr"); then
468+ rm -f "$py_stderr"
469+ printf '%s\n' "$sorted_ids"
470+ return 0
471+ else
472+ echo "Error: invalid extension registry $ext_dir/.registry" >&2
473+ rm -f "$py_stderr"
474+ return 1
475+ fi
476+ fi
477+
478+ if [ -e "$ext_dir/.registry" ] || [ -L "$ext_dir/.registry" ]; then
479+ if [ ! -f "$ext_dir/.registry" ] || [ ! -r "$ext_dir/.registry" ]; then
480+ echo "Error: invalid extension registry $ext_dir/.registry" >&2
481+ return 1
482+ fi
483+ echo "Error: Python 3 is required to honor the extension registry" >&2
484+ return 2
485+ fi
486+
487+ local ext extension_id
488+ for ext in "$ext_dir"/*/; do
489+ [ -d "$ext" ] || continue
490+ extension_id=$(basename "$ext")
491+ case "$extension_id" in *[!a-z0-9-]*) continue ;; esac
492+ printf '%s\n' "$extension_id"
493+ done
494+}
495+
496+# Resolve a template name to a file path using the priority stack:
497+# 1. .specify/templates/overrides/
498+# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
499+# 3. .specify/extensions/<ext-id>/templates/
500+# 4. .specify/templates/ (core)
501+resolve_template() {
502+ local template_name="$1"
503+ local repo_root="$2"
504+ local base="$repo_root/.specify/templates"
505+
506+ case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac
507+
508+ # Priority 1: Project overrides
509+ local override="$base/overrides/${template_name}.md"
510+ [ -f "$override" ] && echo "$override" && return 0
511+
512+ # Priority 2: Installed presets (sorted by priority from .registry)
513+ local presets_dir="$repo_root/.specify/presets"
514+ if [ -d "$presets_dir" ]; then
515+ local registry_file="$presets_dir/.registry"
516+ local python_spec=""
517+ local -a python_cmd=()
518+ if python_spec=$(_python3_command); then
519+ read -r -a python_cmd <<< "$python_spec"
520+ fi
521+ if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then
522+ # Read preset IDs sorted by priority (lower number = higher precedence).
523+ # The python3 call is wrapped in an if-condition so that set -e does not
524+ # abort the function when python3 exits non-zero (e.g. invalid JSON).
525+ local sorted_presets=""
526+ if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c "
527+import json, re, sys, os
528+try:
529+ with open(os.environ['SPECKIT_REGISTRY'], encoding='utf-8') as f:
530+ data = json.load(f)
531+ presets = data.get('presets', {})
532+ def priority(meta):
533+ if not isinstance(meta, dict) or isinstance(meta.get('priority'), bool):
534+ return 10
535+ try:
536+ value = int(meta.get('priority', 10))
537+ return value if value >= 1 else 10
538+ except (TypeError, ValueError, OverflowError):
539+ return 10
540+ for pid, meta in sorted(presets.items(), key=lambda x: (priority(x[1]), x[0])):
541+ if isinstance(meta, dict) and bool(meta.get('enabled', True)) and re.fullmatch(r'[a-z0-9-]+', pid):
542+ print(pid)
543+except Exception:
544+ sys.exit(1)
545+" 2>/dev/null); then
546+ if [ -n "$sorted_presets" ]; then
547+ # python3 succeeded and returned preset IDs — search in priority order
548+ while IFS= read -r preset_id; do
549+ local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
550+ [ -f "$candidate" ] && echo "$candidate" && return 0
551+ candidate="$presets_dir/$preset_id/${template_name}.md"
552+ [ -f "$candidate" ] && echo "$candidate" && return 0
553+ done <<< "$sorted_presets"
554+ fi
555+ # python3 succeeded but registry has no presets — nothing to search
556+ else
557+ # python3 failed (missing, or registry parse error) — fall back to unordered directory scan
558+ for preset in "$presets_dir"/*/; do
559+ [ -d "$preset" ] || continue
560+ local candidate="$preset/templates/${template_name}.md"
561+ [ -f "$candidate" ] && echo "$candidate" && return 0
562+ candidate="$preset/${template_name}.md"
563+ [ -f "$candidate" ] && echo "$candidate" && return 0
564+ done
565+ fi
566+ else
567+ # Fallback: alphabetical directory order (no python3 available)
568+ for preset in "$presets_dir"/*/; do
569+ [ -d "$preset" ] || continue
570+ local candidate="$preset/templates/${template_name}.md"
571+ [ -f "$candidate" ] && echo "$candidate" && return 0
572+ candidate="$preset/${template_name}.md"
573+ [ -f "$candidate" ] && echo "$candidate" && return 0
574+ done
575+ fi
576+ fi
577+
578+ # Priority 3: Extension-provided templates
579+ local ext_dir="$repo_root/.specify/extensions"
580+ if [ -d "$ext_dir" ]; then
581+ local sorted_extensions=""
582+ if ! sorted_extensions=$(_sorted_extension_ids "$ext_dir"); then
583+ return 2
584+ fi
585+ while IFS= read -r extension_id; do
586+ [ -n "$extension_id" ] || continue
587+ local ext="$ext_dir/$extension_id"
588+ local candidate="$ext/templates/${template_name}.md"
589+ [ -f "$candidate" ] || candidate="$ext/${template_name}.md"
590+ [ -f "$candidate" ] && echo "$candidate" && return 0
591+ done <<< "$sorted_extensions"
592+ fi
593+
594+ # Priority 4: Core templates
595+ local core="$base/${template_name}.md"
596+ [ -f "$core" ] && echo "$core" && return 0
597+
598+ # Template not found in any location.
599+ # Return 1 so callers can distinguish "not found" from "found".
600+ # Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true
601+ return 1
602+}
603+
604+# Resolve a template name to composed content using composition strategies.
605+# Reads strategy metadata from preset manifests and composes content
606+# from multiple layers using prepend, append, or wrap strategies.
607+#
608+# Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT")
609+# Returns composed content string on stdout; exit code 1 if not found.
610+resolve_template_content() {
611+ local template_name="$1"
612+ local repo_root="$2"
613+ local base="$repo_root/.specify/templates"
614+
615+ case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac
616+
617+ # Collect all layers (highest priority first)
618+ local -a layer_paths=()
619+ local -a layer_strategies=()
620+
621+ # Priority 1: Project overrides (always "replace")
622+ local override="$base/overrides/${template_name}.md"
623+ if [ -f "$override" ]; then
624+ if ! cat "$override"; then
625+ echo "Error: failed to read template layer $override" >&2
626+ return 2
627+ fi
628+ return 0
629+ fi
630+
631+ local effective_base_found=false
632+
633+ # Priority 2: Installed presets (sorted by priority from .registry)
634+ local presets_dir="$repo_root/.specify/presets"
635+ if [ -d "$presets_dir" ]; then
636+ local registry_file="$presets_dir/.registry"
637+ local sorted_presets=""
638+ local registry_parsed=false
639+ local python_spec=""
640+ local -a python_cmd=()
641+ if python_spec=$(_python3_command); then
642+ read -r -a python_cmd <<< "$python_spec"
643+ fi
644+ if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then
645+ if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c "
646+import json, re, sys, os
647+try:
648+ with open(os.environ['SPECKIT_REGISTRY'], encoding='utf-8') as f:
649+ data = json.load(f)
650+ presets = data.get('presets', {})
651+ def priority(meta):
652+ if not isinstance(meta, dict) or isinstance(meta.get('priority'), bool):
653+ return 10
654+ try:
655+ value = int(meta.get('priority', 10))
656+ return value if value >= 1 else 10
657+ except (TypeError, ValueError, OverflowError):
658+ return 10
659+ for pid, meta in sorted(presets.items(), key=lambda x: (priority(x[1]), x[0])):
660+ if isinstance(meta, dict) and bool(meta.get('enabled', True)) and re.fullmatch(r'[a-z0-9-]+', pid):
661+ print(pid)
662+except Exception:
663+ sys.exit(1)
664+" 2>/dev/null); then
665+ registry_parsed=true
666+ fi
667+ fi
668+ if [ "$registry_parsed" = false ]; then
669+ for preset in "$presets_dir"/*/; do
670+ [ -d "$preset" ] || continue
671+ local fallback_id
672+ fallback_id=$(basename "$preset")
673+ case "$fallback_id" in *[!a-z0-9-]*) continue ;; esac
674+ sorted_presets+="${sorted_presets:+$'\n'}$fallback_id"
675+ done
676+ fi
677+
678+ if [ -n "$sorted_presets" ]; then
679+ while IFS= read -r preset_id; do
680+ local strategy="replace"
681+ local manifest_file=""
682+ local manifest="$presets_dir/$preset_id/preset.yml"
683+ local manifest_declared=false
684+ if [ -f "$manifest" ]; then
685+ if [ "${#python_cmd[@]}" -eq 0 ]; then
686+ echo "Error: Python 3 and PyYAML are required to resolve preset template composition" >&2
687+ return 2
688+ fi
689+ local result
690+ local py_stderr
691+ local parse_status
692+ py_stderr=$(mktemp)
693+ if result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "${python_cmd[@]}" -c "
694+import sys, os
695+try:
696+ import yaml
697+except ImportError:
698+ print('yaml_missing', file=sys.stderr)
699+ sys.exit(2)
700+try:
701+ with open(os.environ['SPECKIT_MANIFEST'], encoding='utf-8') as f:
702+ data = yaml.safe_load(f)
703+ if not isinstance(data, dict):
704+ raise ValueError('manifest root must be a mapping')
705+ if 'provides' not in data:
706+ raise ValueError('manifest missing provides section')
707+ provides = data['provides']
708+ if not isinstance(provides, dict):
709+ raise ValueError('manifest provides must be a mapping')
710+ if 'templates' not in provides:
711+ raise ValueError('manifest provides missing templates')
712+ templates = provides['templates']
713+ if not isinstance(templates, list):
714+ raise ValueError('manifest templates must be a list')
715+ if not templates:
716+ raise ValueError('manifest must provide at least one template')
717+ valid_types = ('template', 'command', 'script')
718+ valid_strategies = ('replace', 'prepend', 'append', 'wrap')
719+ for t in templates:
720+ if not isinstance(t, dict):
721+ raise ValueError('manifest template entries must be mappings')
722+ if 'type' not in t or 'name' not in t or 'file' not in t:
723+ raise ValueError('manifest template entry missing type, name, or file')
724+ for field in ('type', 'name', 'file'):
725+ if not isinstance(t[field], str):
726+ raise ValueError('manifest template ' + field + ' must be a string')
727+ if t['type'] not in valid_types:
728+ raise ValueError('invalid manifest template type')
729+ strategy = t.get('strategy', 'replace')
730+ if not isinstance(strategy, str):
731+ raise ValueError('manifest template strategy must be a string')
732+ strategy = strategy.lower()
733+ if strategy not in valid_strategies:
734+ raise ValueError('invalid manifest template strategy')
735+ if t['type'] == 'script' and strategy not in ('replace', 'wrap'):
736+ raise ValueError('invalid manifest script strategy')
737+ for t in templates:
738+ if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template':
739+ file_value = t.get('file', '')
740+ strategy = t.get('strategy', 'replace')
741+ print('found\t' + strategy + '\t' + file_value)
742+ sys.exit(0)
743+ print('absent\treplace\t')
744+except Exception as exc:
745+ print(f'manifest_invalid: {exc}', file=sys.stderr)
746+ sys.exit(3)
747+" 2>"$py_stderr"); then
748+ parse_status=0
749+ else
750+ parse_status=$?
751+ fi
752+ if [ "$parse_status" -ne 0 ]; then
753+ if [ "$parse_status" -eq 2 ]; then
754+ echo "Error: PyYAML is required to resolve preset template composition" >&2
755+ else
756+ echo "Error: invalid preset manifest $manifest" >&2
757+ fi
758+ rm -f "$py_stderr"
759+ return 2
760+ fi
761+ if [ -n "$result" ]; then
762+ local declaration
763+ IFS=$'\t' read -r declaration strategy manifest_file <<< "$result"
764+ [ "$declaration" = "found" ] && manifest_declared=true
765+ strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
766+ fi
767+ rm -f "$py_stderr"
768+ fi
769+
770+ local candidate=""
771+ if [ -n "$manifest_file" ]; then
772+ case "$manifest_file" in
773+ /*|*../*|../*) manifest_file="" ;;
774+ esac
775+ fi
776+ if [ -n "$manifest_file" ]; then
777+ local mf="$presets_dir/$preset_id/$manifest_file"
778+ [ -f "$mf" ] && candidate="$mf"
779+ fi
780+ if [ -z "$candidate" ] && [ "$manifest_declared" = false ]; then
781+ local cf="$presets_dir/$preset_id/templates/${template_name}.md"
782+ [ -f "$cf" ] && candidate="$cf"
783+ if [ -z "$candidate" ]; then
784+ cf="$presets_dir/$preset_id/${template_name}.md"
785+ [ -f "$cf" ] && candidate="$cf"
786+ fi
787+ fi
788+ if [ -n "$candidate" ]; then
789+ layer_paths+=("$candidate")
790+ layer_strategies+=("$strategy")
791+ if [ "$strategy" = "replace" ]; then
792+ effective_base_found=true
793+ break
794+ fi
795+ fi
796+ done <<< "$sorted_presets"
797+ fi
798+ fi
799+
800+ # Priority 3: Extension-provided templates (always "replace")
801+ local ext_dir="$repo_root/.specify/extensions"
802+ if [ "$effective_base_found" = false ] && [ -d "$ext_dir" ]; then
803+ local sorted_extensions=""
804+ if ! sorted_extensions=$(_sorted_extension_ids "$ext_dir"); then
805+ return 2
806+ fi
807+ while IFS= read -r extension_id; do
808+ [ -n "$extension_id" ] || continue
809+ local ext="$ext_dir/$extension_id"
810+ local candidate="$ext/templates/${template_name}.md"
811+ [ -f "$candidate" ] || candidate="$ext/${template_name}.md"
812+ if [ -f "$candidate" ]; then
813+ layer_paths+=("$candidate")
814+ layer_strategies+=("replace")
815+ effective_base_found=true
816+ break
817+ fi
818+ done <<< "$sorted_extensions"
819+ fi
820+
821+ # Priority 4: Core templates (always "replace")
822+ local core="$base/${template_name}.md"
823+ if [ "$effective_base_found" = false ] && [ -f "$core" ]; then
824+ layer_paths+=("$core")
825+ layer_strategies+=("replace")
826+ fi
827+
828+ local count=${#layer_paths[@]}
829+ [ "$count" -eq 0 ] && return 1
830+
831+ # Check if any layer uses a non-replace strategy
832+ local has_composition=false
833+ for s in "${layer_strategies[@]}"; do
834+ [ "$s" != "replace" ] && has_composition=true && break
835+ done
836+
837+ # If the top (highest-priority) layer is replace, it wins entirely —
838+ # lower layers are irrelevant regardless of their strategies.
839+ if [ "${layer_strategies[0]}" = "replace" ]; then
840+ if ! cat "${layer_paths[0]}"; then
841+ echo "Error: failed to read template layer ${layer_paths[0]}" >&2
842+ return 2
843+ fi
844+ return 0
845+ fi
846+
847+ if [ "$has_composition" = false ]; then
848+ if ! cat "${layer_paths[0]}"; then
849+ echo "Error: failed to read template layer ${layer_paths[0]}" >&2
850+ return 2
851+ fi
852+ return 0
853+ fi
854+
855+ # Find the effective base: scan from highest priority (index 0) downward
856+ # to find the nearest replace layer. Only compose layers above that base.
857+ local base_idx=-1
858+ local i
859+ for (( i=0; i<count; i++ )); do
860+ if [ "${layer_strategies[$i]}" = "replace" ]; then
861+ base_idx=$i
862+ break
863+ fi
864+ done
865+
866+ if [ $base_idx -lt 0 ]; then
867+ echo "Error: template '$template_name' has composing layers but no replace base" >&2
868+ return 2
869+ fi
870+
871+ # Read the base content; compose layers above the base (higher priority)
872+ local content
873+ if ! content=$(cat "${layer_paths[$base_idx]}"; status=$?; printf x; exit "$status"); then
874+ echo "Error: failed to read template layer ${layer_paths[$base_idx]}" >&2
875+ return 2
876+ fi
877+ content="${content%x}"
878+
879+ for (( i=base_idx-1; i>=0; i-- )); do
880+ local path="${layer_paths[$i]}"
881+ local strat="${layer_strategies[$i]}"
882+ local layer_content
883+ # Preserve trailing newlines
884+ if ! layer_content=$(cat "$path"; status=$?; printf x; exit "$status"); then
885+ echo "Error: failed to read template layer $path" >&2
886+ return 2
887+ fi
888+ layer_content="${layer_content%x}"
889+
890+ case "$strat" in
891+ replace) content="$layer_content" ;;
892+ prepend)
893+ content=$(printf '%s\n\n%s' "$layer_content" "$content"; printf x)
894+ content="${content%x}"
895+ ;;
896+ append)
897+ content=$(printf '%s\n\n%s' "$content" "$layer_content"; printf x)
898+ content="${content%x}"
899+ ;;
900+ wrap)
901+ case "$layer_content" in
902+ *'{CORE_TEMPLATE}'*) ;;
903+ *) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 2 ;;
904+ esac
905+ # Consume the wrapper left to right instead of rewriting it in
906+ # place. Rewriting re-scanned the string just modified, so base
907+ # content holding a literal {CORE_TEMPLATE} reintroduced the
908+ # token every pass and the loop never terminated. Advancing over
909+ # ``rest`` bounds the work by the tokens in the original wrapper
910+ # and leaves inserted content untouched, matching the single-pass
911+ # semantics of .Replace()/.replace() in the PowerShell and Python
912+ # ports.
913+ local wrapped="" rest="$layer_content"
914+ while [[ "$rest" == *'{CORE_TEMPLATE}'* ]]; do
915+ wrapped="${wrapped}${rest%%\{CORE_TEMPLATE\}*}${content}"
916+ rest="${rest#*\{CORE_TEMPLATE\}}"
917+ done
918+ content="${wrapped}${rest}"
919+ ;;
920+ *) echo "Error: unknown strategy '$strat'" >&2; return 2 ;;
921+ esac
922+ done
923+
924+ printf '%s' "$content"
925+ return 0
926+}
new file mode 100755
@@ -0,0 +1,926 @@
1+#!/usr/bin/env bash
2+# Common functions and variables for all scripts
3+
4+# Find repository root by searching upward for .specify directory
5+# This is the primary marker for spec-kit projects
6+find_specify_root() {
7+ local dir="${1:-$(pwd)}"
8+ # Normalize to absolute path to prevent infinite loop with relative paths
9+ # Use -- to handle paths starting with - (e.g., -P, -L)
10+ dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1
11+ local prev_dir=""
12+ while true; do
13+ if [ -d "$dir/.specify" ]; then
14+ echo "$dir"
15+ return 0
16+ fi
17+ # Stop if we've reached filesystem root or dirname stops changing
18+ if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then
19+ break
20+ fi
21+ prev_dir="$dir"
22+ dir="$(dirname "$dir")"
23+ done
24+ return 1
25+}
26+
27+# Resolve an explicit SPECIFY_INIT_DIR project override (the directory that
28+# *contains* .specify/), for non-interactive / CI use — e.g. running a Spec Kit
29+# command against a member project from a monorepo root without cd.
30+#
31+# Precondition: SPECIFY_INIT_DIR is non-empty. Echoes the validated absolute
32+# project root, or prints an error and returns 1. Strict by design: the path
33+# must exist and contain .specify/, with no silent fallback to cwd or the
34+# script-location default (which would silently write to the wrong project).
35+#
36+# This is the single resolver: bundled extensions inherit it by sourcing core
37+# (e.g. the git extension's create-new-feature-branch) rather than duplicating it.
38+resolve_specify_init_dir() {
39+ local init_root
40+ # Normalize: relative paths resolve against $(pwd); a trailing slash collapses.
41+ # CDPATH="" so a relative value cannot be resolved against the caller's CDPATH
42+ # (which would also echo to stdout and corrupt the captured path).
43+ if ! init_root="$(CDPATH="" cd -- "$SPECIFY_INIT_DIR" 2>/dev/null && pwd)"; then
44+ echo "ERROR: SPECIFY_INIT_DIR does not point to an existing directory: $SPECIFY_INIT_DIR" >&2
45+ return 1
46+ fi
47+ if [[ ! -d "$init_root/.specify" ]]; then
48+ echo "ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): $init_root" >&2
49+ return 1
50+ fi
51+ printf '%s\n' "$init_root"
52+}
53+
54+# Get repository root, prioritizing .specify directory
55+# This prevents using a parent repository when spec-kit is initialized in a subdirectory
56+get_repo_root() {
57+ # Explicit project override wins (see resolve_specify_init_dir).
58+ if [[ -n "${SPECIFY_INIT_DIR:-}" ]]; then
59+ resolve_specify_init_dir
60+ return
61+ fi
62+
63+ # First, look for .specify directory (spec-kit's own marker)
64+ local specify_root
65+ if specify_root=$(find_specify_root); then
66+ echo "$specify_root"
67+ return
68+ fi
69+
70+ # Final fallback to script location
71+ local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
72+ (cd "$script_dir/../../.." && pwd)
73+}
74+
75+# Get current feature name from explicit state only.
76+# Returns the feature identifier or empty string if none is set.
77+# Feature state is set by SPECIFY_FEATURE (from create-new-feature or
78+# the git extension) or implicitly via .specify/feature.json.
79+get_current_branch() {
80+ if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
81+ echo "$SPECIFY_FEATURE"
82+ return
83+ fi
84+
85+ # No explicit feature set — caller must handle this via feature.json
86+ # in get_feature_paths(). Return empty to signal "unknown".
87+ echo ""
88+}
89+
90+# Safely read .specify/feature.json's "feature_directory" value.
91+# Prints the raw value (possibly relative) to stdout, or empty string if the file
92+# is missing, unparseable, or does not contain the key. Always returns 0 so callers
93+# under `set -e` cannot be aborted by parser failure.
94+# Parser order mirrors the historical get_feature_paths behavior: jq -> python3 -> grep/sed.
95+read_feature_json_feature_directory() {
96+ local repo_root="$1"
97+ local fj="$repo_root/.specify/feature.json"
98+ [[ -f "$fj" ]] || { printf '%s' ''; return 0; }
99+
100+ # Try parsers in order (jq -> python3 -> grep/sed), falling through on
101+ # failure. Selection is by *parse success*, not mere availability: on
102+ # Windows `python3` commonly resolves to the Microsoft Store App Execution
103+ # Alias stub, which passes `command -v` but fails at runtime (exit 49), so
104+ # an availability-gated `elif` would pick python3, swallow its failure, and
105+ # never reach the grep/sed fallback -- leaving feature.json unreadable even
106+ # though it is valid (issue #3304).
107+ local _fd=''
108+ if command -v jq >/dev/null 2>&1; then
109+ if ! _fd=$(jq -r '.feature_directory // empty' "$fj" 2>/dev/null); then
110+ _fd=''
111+ fi
112+ fi
113+ if [[ -z "$_fd" ]] && command -v python3 >/dev/null 2>&1; then
114+ # Use Python so pretty-printed/multi-line JSON still parses correctly.
115+ if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then
116+ _fd=''
117+ fi
118+ fi
119+ if [[ -z "$_fd" ]]; then
120+ # Last-resort single-line grep/sed fallback. The `|| true` guards against
121+ # grep returning 1 (no match) aborting under `set -e` / `pipefail`.
122+ _fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \
123+ | head -n 1 \
124+ | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
125+ fi
126+
127+ printf '%s' "$_fd"
128+ return 0
129+}
130+
131+# Persist a feature_directory value to .specify/feature.json.
132+# Writes only when the file is missing or the value differs from what's stored.
133+# Accepts the raw (possibly relative) path — callers should pass the original
134+# user-supplied value, not the normalized absolute path.
135+_persist_feature_json() {
136+ local repo_root="$1"
137+ local feature_dir_value="$2"
138+ local fj="$repo_root/.specify/feature.json"
139+
140+ # Strip repo_root prefix if the value is absolute and under repo_root
141+ if [[ "$feature_dir_value" == "$repo_root/"* ]]; then
142+ feature_dir_value="${feature_dir_value#"$repo_root/"}"
143+ fi
144+
145+ # Read current value (if any) and skip write when unchanged
146+ local current_val
147+ current_val=$(read_feature_json_feature_directory "$repo_root")
148+ if [[ "$current_val" == "$feature_dir_value" ]]; then
149+ return 0
150+ fi
151+
152+ # Ensure .specify/ directory exists
153+ mkdir -p "$repo_root/.specify"
154+
155+ # Write feature.json — prefer jq for safe JSON, fall back to printf
156+ if command -v jq >/dev/null 2>&1; then
157+ jq -cn --arg fd "$feature_dir_value" '{feature_directory:$fd}' > "$fj"
158+ else
159+ printf '{"feature_directory":"%s"}\n' "$(json_escape "$feature_dir_value")" > "$fj"
160+ fi
161+}
162+
163+get_feature_paths() {
164+ # Read-only callers (e.g. check-prerequisites.sh --paths-only) pass
165+ # --no-persist so pure path resolution never writes .specify/feature.json,
166+ # which would dirty the working tree or overwrite a pinned value (issue #3025).
167+ local no_persist=false
168+ if [[ "${1:-}" == "--no-persist" ]]; then
169+ no_persist=true
170+ shift
171+ fi
172+
173+ # Split decl/assignment so a SPECIFY_INIT_DIR validation failure in
174+ # get_repo_root propagates as a hard error instead of being masked by `local`.
175+ local repo_root
176+ repo_root=$(get_repo_root) || return 1
177+ local current_branch
178+ current_branch=$(get_current_branch)
179+
180+ # Resolve feature directory. Priority:
181+ # 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override)
182+ # 2. .specify/feature.json "feature_directory" key (persisted by specify command)
183+ # 3. Error — no feature context available
184+ local feature_dir
185+ if [[ -n "${SPECIFY_FEATURE_DIRECTORY:-}" ]]; then
186+ feature_dir="$SPECIFY_FEATURE_DIRECTORY"
187+ # Normalize relative paths to absolute under repo root
188+ [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
189+ # Persist to feature.json so future sessions without the env var still
190+ # work — unless the caller opted out for read-only resolution (#3025).
191+ if [[ "$no_persist" != true ]]; then
192+ _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
193+ fi
194+ elif [[ -f "$repo_root/.specify/feature.json" ]]; then
195+ local _fd
196+ _fd=$(read_feature_json_feature_directory "$repo_root")
197+ if [[ -n "$_fd" ]]; then
198+ feature_dir="$_fd"
199+ # Normalize relative paths to absolute under repo root
200+ [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
201+ else
202+ echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or ensure .specify/feature.json contains feature_directory." >&2
203+ return 1
204+ fi
205+ else
206+ echo "ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or run the specify command to create .specify/feature.json." >&2
207+ return 1
208+ fi
209+
210+ # When no branch context exists (no SPECIFY_FEATURE, feature resolved via
211+ # SPECIFY_FEATURE_DIRECTORY or feature.json), fall back to the feature
212+ # directory basename so CURRENT_BRANCH is a usable identifier rather than
213+ # an empty, misleading value (issue #3026).
214+ if [[ -z "$current_branch" ]]; then
215+ local feature_dir_trimmed="${feature_dir%/}"
216+ current_branch="${feature_dir_trimmed##*/}"
217+ fi
218+
219+ # Use printf '%q' to safely quote values, preventing shell injection
220+ # via crafted branch names or paths containing special characters
221+ printf 'REPO_ROOT=%q\n' "$repo_root"
222+ printf 'CURRENT_BRANCH=%q\n' "$current_branch"
223+ printf 'FEATURE_DIR=%q\n' "$feature_dir"
224+ printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
225+ printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
226+ printf 'TASKS=%q\n' "$feature_dir/tasks.md"
227+ printf 'RESEARCH=%q\n' "$feature_dir/research.md"
228+ printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
229+ printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
230+ printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
231+}
232+
233+# Check if jq is available for safe JSON construction
234+has_jq() {
235+ command -v jq >/dev/null 2>&1
236+}
237+
238+get_invoke_separator() {
239+ local repo_root="${1:-$(get_repo_root)}"
240+ if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
241+ printf '%s\n' "$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
242+ return 0
243+ fi
244+
245+ local integration_json="$repo_root/.specify/integration.json"
246+ local separator="."
247+ local parsed=0
248+
249+ if [[ -f "$integration_json" ]]; then
250+ # Try parsers in order (jq -> python3 -> awk), falling through on
251+ # failure. Selection is by *parse success*, not mere availability: on
252+ # Windows `python3` commonly resolves to the Microsoft Store App
253+ # Execution Alias stub, which passes `command -v` but fails at runtime
254+ # (exit 49). An availability-gated branch would pick python3, swallow
255+ # its failure, and — because this function historically had no text
256+ # fallback — silently return "." even for `-`-separator integrations
257+ # (e.g. forge, cline), yielding wrong command hints (issue #3304).
258+ if command -v jq >/dev/null 2>&1; then
259+ local jq_separator
260+ if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then
261+ case "$jq_separator" in
262+ "."|"-") separator="$jq_separator"; parsed=1 ;;
263+ esac
264+ fi
265+ fi
266+
267+ if [[ "$parsed" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then
268+ local py_separator
269+ if py_separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null
270+import json
271+import sys
272+
273+try:
274+ with open(sys.argv[1], encoding="utf-8") as fh:
275+ state = json.load(fh)
276+ key = state.get("default_integration") or state.get("integration") or ""
277+ settings = state.get("integration_settings")
278+ separator = "."
279+ if isinstance(key, str) and isinstance(settings, dict):
280+ entry = settings.get(key)
281+ if isinstance(entry, dict) and entry.get("invoke_separator") in {".", "-"}:
282+ separator = entry["invoke_separator"]
283+ print(separator)
284+except Exception:
285+ sys.exit(1)
286+PY
287+); then
288+ case "$py_separator" in
289+ "."|"-") separator="$py_separator"; parsed=1 ;;
290+ esac
291+ fi
292+ fi
293+
294+ if [[ "$parsed" -eq 0 ]]; then
295+ # Last-resort text fallback for environments with neither jq nor a
296+ # working python3 (e.g. stock Windows + Git Bash). Reads the active
297+ # integration key (default_integration, else integration) and its
298+ # invoke_separator from within the integration_settings object.
299+ # Handles both pretty-printed (the written form) and compact JSON.
300+ # Accumulate all lines into one buffer in END rather than using
301+ # gawk-only whole-file slurp (RS="^$"), so this stays portable to
302+ # the BSD awk on macOS.
303+ local awk_separator
304+ awk_separator=$(awk '
305+ function keyval(d, name, v) {
306+ if (match(d, "\"" name "\"[ \t\r\n]*:[ \t\r\n]*\"[^\"]*\"")) {
307+ v=substr(d,RSTART,RLENGTH); sub(/^.*:[ \t\r\n]*"/,"",v); sub(/"$/,"",v); return v
308+ }
309+ return ""
310+ }
311+ { doc = doc $0 "\n" }
312+ END {
313+ key=keyval(doc,"default_integration"); if (key=="") key=keyval(doc,"integration")
314+ sep="."
315+ if (key!="") {
316+ settings=doc
317+ if (match(doc, /"integration_settings"[ \t\r\n]*:[ \t\r\n]*[{]/)) {
318+ settings=substr(doc, RSTART+RLENGTH-1)
319+ }
320+ if (match(settings, "\"" key "\"[ \t\r\n]*:[ \t\r\n]*[{]")) {
321+ start=RSTART+RLENGTH-1
322+ depth=0
323+ obj=""
324+ for (i=start; i<=length(settings); i++) {
325+ c=substr(settings,i,1)
326+ obj=obj c
327+ if (c=="{") depth++
328+ else if (c=="}") { depth--; if (depth==0) break }
329+ }
330+ if (match(obj, /"invoke_separator"[ \t\r\n]*:[ \t\r\n]*"[-.]"/)) {
331+ tok=substr(obj,RSTART,RLENGTH); s=substr(tok,length(tok)-1,1)
332+ if (s=="." || s=="-") sep=s
333+ }
334+ }
335+ }
336+ print sep
337+ }
338+ ' "$integration_json" 2>/dev/null)
339+ case "$awk_separator" in
340+ "."|"-") separator="$awk_separator" ;;
341+ esac
342+ fi
343+ fi
344+
345+ _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
346+ _SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
347+ printf '%s\n' "$separator"
348+}
349+
350+format_speckit_command() {
351+ local command_name="$1"
352+ local repo_root="${2:-$(get_repo_root)}"
353+ local separator
354+ if [[ "${_SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT:-}" == "$repo_root" && -n "${_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE:-}" ]]; then
355+ separator="$_SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE"
356+ else
357+ separator=$(get_invoke_separator "$repo_root")
358+ _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root"
359+ _SPECIFY_INVOKE_SEPARATOR_CACHE_VALUE="$separator"
360+ fi
361+
362+ command_name="${command_name#/}"
363+ command_name="${command_name#speckit.}"
364+ command_name="${command_name#speckit-}"
365+ command_name="${command_name//./$separator}"
366+
367+ printf '/speckit%s%s\n' "$separator" "$command_name"
368+}
369+
370+# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
371+# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).
372+json_escape() {
373+ local s="$1"
374+ s="${s//\\/\\\\}"
375+ s="${s//\"/\\\"}"
376+ s="${s//$'\n'/\\n}"
377+ s="${s//$'\t'/\\t}"
378+ s="${s//$'\r'/\\r}"
379+ s="${s//$'\b'/\\b}"
380+ s="${s//$'\f'/\\f}"
381+ # Escape any remaining U+0001-U+001F control characters as \uXXXX.
382+ # (U+0000/NUL cannot appear in bash strings and is excluded.)
383+ # LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes,
384+ # so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.
385+ local LC_ALL=C
386+ local i char code
387+ for (( i=0; i<${#s}; i++ )); do
388+ char="${s:$i:1}"
389+ printf -v code '%d' "'$char" 2>/dev/null || code=256
390+ if (( code >= 1 && code <= 31 )); then
391+ printf '\\u%04x' "$code"
392+ else
393+ printf '%s' "$char"
394+ fi
395+ done
396+}
397+
398+check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
399+check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
400+
401+_python3_command() {
402+ if command -v python3 >/dev/null 2>&1 &&
403+ python3 -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
404+ printf '%s\n' "python3"
405+ elif command -v python >/dev/null 2>&1 &&
406+ python -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then
407+ printf '%s\n' "python"
408+ elif command -v py >/dev/null 2>&1 &&
409+ py -3 -c 'import sys' >/dev/null 2>&1; then
410+ printf '%s\n' "py -3"
411+ else
412+ return 1
413+ fi
414+}
415+
416+_sorted_extension_ids() {
417+ local ext_dir="$1"
418+ local python_spec
419+ if python_spec=$(_python3_command); then
420+ local -a python_cmd
421+ read -r -a python_cmd <<< "$python_spec"
422+ local py_stderr sorted_ids
423+ py_stderr=$(mktemp)
424+ if sorted_ids=$(SPECKIT_EXTENSIONS="$ext_dir" "${python_cmd[@]}" -c "
425+import json, os, re, sys
426+from pathlib import Path
427+
428+root = Path(os.environ['SPECKIT_EXTENSIONS'])
429+registered = {}
430+registry = root / '.registry'
431+if os.path.lexists(registry):
432+ if not registry.is_file():
433+ print('registry_invalid: not a regular file', file=sys.stderr)
434+ sys.exit(1)
435+ try:
436+ data = json.loads(registry.read_text(encoding='utf-8'))
437+ except Exception as exc:
438+ print('registry_invalid: ' + str(exc), file=sys.stderr)
439+ sys.exit(1)
440+ if not isinstance(data, dict):
441+ print('registry_invalid: root must be a mapping', file=sys.stderr)
442+ sys.exit(1)
443+ raw_extensions = data.get('extensions', {})
444+ if not isinstance(raw_extensions, dict):
445+ print('registry_invalid: extensions must be a mapping', file=sys.stderr)
446+ sys.exit(1)
447+ registered = raw_extensions
448+
449+def priority(value):
450+ if isinstance(value, bool):
451+ return 10
452+ try:
453+ parsed = int(value)
454+ return parsed if parsed >= 1 else 10
455+ except (TypeError, ValueError, OverflowError):
456+ return 10
457+
458+ranked = []
459+for ext_id, meta in registered.items():
460+ if isinstance(ext_id, str) and re.fullmatch(r'[a-z0-9-]+', ext_id) and isinstance(meta, dict) and bool(meta.get('enabled', True)):
461+ ranked.append((priority(meta.get('priority')), ext_id))
462+for path in root.iterdir():
463+ if path.is_dir() and re.fullmatch(r'[a-z0-9-]+', path.name) and path.name not in registered:
464+ ranked.append((10, path.name))
465+for _, ext_id in sorted(ranked):
466+ print(ext_id)
467+" 2>"$py_stderr"); then
468+ rm -f "$py_stderr"
469+ printf '%s\n' "$sorted_ids"
470+ return 0
471+ else
472+ echo "Error: invalid extension registry $ext_dir/.registry" >&2
473+ rm -f "$py_stderr"
474+ return 1
475+ fi
476+ fi
477+
478+ if [ -e "$ext_dir/.registry" ] || [ -L "$ext_dir/.registry" ]; then
479+ if [ ! -f "$ext_dir/.registry" ] || [ ! -r "$ext_dir/.registry" ]; then
480+ echo "Error: invalid extension registry $ext_dir/.registry" >&2
481+ return 1
482+ fi
483+ echo "Error: Python 3 is required to honor the extension registry" >&2
484+ return 2
485+ fi
486+
487+ local ext extension_id
488+ for ext in "$ext_dir"/*/; do
489+ [ -d "$ext" ] || continue
490+ extension_id=$(basename "$ext")
491+ case "$extension_id" in *[!a-z0-9-]*) continue ;; esac
492+ printf '%s\n' "$extension_id"
493+ done
494+}
495+
496+# Resolve a template name to a file path using the priority stack:
497+# 1. .specify/templates/overrides/
498+# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
499+# 3. .specify/extensions/<ext-id>/templates/
500+# 4. .specify/templates/ (core)
501+resolve_template() {
502+ local template_name="$1"
503+ local repo_root="$2"
504+ local base="$repo_root/.specify/templates"
505+
506+ case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac
507+
508+ # Priority 1: Project overrides
509+ local override="$base/overrides/${template_name}.md"
510+ [ -f "$override" ] && echo "$override" && return 0
511+
512+ # Priority 2: Installed presets (sorted by priority from .registry)
513+ local presets_dir="$repo_root/.specify/presets"
514+ if [ -d "$presets_dir" ]; then
515+ local registry_file="$presets_dir/.registry"
516+ local python_spec=""
517+ local -a python_cmd=()
518+ if python_spec=$(_python3_command); then
519+ read -r -a python_cmd <<< "$python_spec"
520+ fi
521+ if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then
522+ # Read preset IDs sorted by priority (lower number = higher precedence).
523+ # The python3 call is wrapped in an if-condition so that set -e does not
524+ # abort the function when python3 exits non-zero (e.g. invalid JSON).
525+ local sorted_presets=""
526+ if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c "
527+import json, re, sys, os
528+try:
529+ with open(os.environ['SPECKIT_REGISTRY'], encoding='utf-8') as f:
530+ data = json.load(f)
531+ presets = data.get('presets', {})
532+ def priority(meta):
533+ if not isinstance(meta, dict) or isinstance(meta.get('priority'), bool):
534+ return 10
535+ try:
536+ value = int(meta.get('priority', 10))
537+ return value if value >= 1 else 10
538+ except (TypeError, ValueError, OverflowError):
539+ return 10
540+ for pid, meta in sorted(presets.items(), key=lambda x: (priority(x[1]), x[0])):
541+ if isinstance(meta, dict) and bool(meta.get('enabled', True)) and re.fullmatch(r'[a-z0-9-]+', pid):
542+ print(pid)
543+except Exception:
544+ sys.exit(1)
545+" 2>/dev/null); then
546+ if [ -n "$sorted_presets" ]; then
547+ # python3 succeeded and returned preset IDs — search in priority order
548+ while IFS= read -r preset_id; do
549+ local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
550+ [ -f "$candidate" ] && echo "$candidate" && return 0
551+ candidate="$presets_dir/$preset_id/${template_name}.md"
552+ [ -f "$candidate" ] && echo "$candidate" && return 0
553+ done <<< "$sorted_presets"
554+ fi
555+ # python3 succeeded but registry has no presets — nothing to search
556+ else
557+ # python3 failed (missing, or registry parse error) — fall back to unordered directory scan
558+ for preset in "$presets_dir"/*/; do
559+ [ -d "$preset" ] || continue
560+ local candidate="$preset/templates/${template_name}.md"
561+ [ -f "$candidate" ] && echo "$candidate" && return 0
562+ candidate="$preset/${template_name}.md"
563+ [ -f "$candidate" ] && echo "$candidate" && return 0
564+ done
565+ fi
566+ else
567+ # Fallback: alphabetical directory order (no python3 available)
568+ for preset in "$presets_dir"/*/; do
569+ [ -d "$preset" ] || continue
570+ local candidate="$preset/templates/${template_name}.md"
571+ [ -f "$candidate" ] && echo "$candidate" && return 0
572+ candidate="$preset/${template_name}.md"
573+ [ -f "$candidate" ] && echo "$candidate" && return 0
574+ done
575+ fi
576+ fi
577+
578+ # Priority 3: Extension-provided templates
579+ local ext_dir="$repo_root/.specify/extensions"
580+ if [ -d "$ext_dir" ]; then
581+ local sorted_extensions=""
582+ if ! sorted_extensions=$(_sorted_extension_ids "$ext_dir"); then
583+ return 2
584+ fi
585+ while IFS= read -r extension_id; do
586+ [ -n "$extension_id" ] || continue
587+ local ext="$ext_dir/$extension_id"
588+ local candidate="$ext/templates/${template_name}.md"
589+ [ -f "$candidate" ] || candidate="$ext/${template_name}.md"
590+ [ -f "$candidate" ] && echo "$candidate" && return 0
591+ done <<< "$sorted_extensions"
592+ fi
593+
594+ # Priority 4: Core templates
595+ local core="$base/${template_name}.md"
596+ [ -f "$core" ] && echo "$core" && return 0
597+
598+ # Template not found in any location.
599+ # Return 1 so callers can distinguish "not found" from "found".
600+ # Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true
601+ return 1
602+}
603+
604+# Resolve a template name to composed content using composition strategies.
605+# Reads strategy metadata from preset manifests and composes content
606+# from multiple layers using prepend, append, or wrap strategies.
607+#
608+# Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT")
609+# Returns composed content string on stdout; exit code 1 if not found.
610+resolve_template_content() {
611+ local template_name="$1"
612+ local repo_root="$2"
613+ local base="$repo_root/.specify/templates"
614+
615+ case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac
616+
617+ # Collect all layers (highest priority first)
618+ local -a layer_paths=()
619+ local -a layer_strategies=()
620+
621+ # Priority 1: Project overrides (always "replace")
622+ local override="$base/overrides/${template_name}.md"
623+ if [ -f "$override" ]; then
624+ if ! cat "$override"; then
625+ echo "Error: failed to read template layer $override" >&2
626+ return 2
627+ fi
628+ return 0
629+ fi
630+
631+ local effective_base_found=false
632+
633+ # Priority 2: Installed presets (sorted by priority from .registry)
634+ local presets_dir="$repo_root/.specify/presets"
635+ if [ -d "$presets_dir" ]; then
636+ local registry_file="$presets_dir/.registry"
637+ local sorted_presets=""
638+ local registry_parsed=false
639+ local python_spec=""
640+ local -a python_cmd=()
641+ if python_spec=$(_python3_command); then
642+ read -r -a python_cmd <<< "$python_spec"
643+ fi
644+ if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then
645+ if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c "
646+import json, re, sys, os
647+try:
648+ with open(os.environ['SPECKIT_REGISTRY'], encoding='utf-8') as f:
649+ data = json.load(f)
650+ presets = data.get('presets', {})
651+ def priority(meta):
652+ if not isinstance(meta, dict) or isinstance(meta.get('priority'), bool):
653+ return 10
654+ try:
655+ value = int(meta.get('priority', 10))
656+ return value if value >= 1 else 10
657+ except (TypeError, ValueError, OverflowError):
658+ return 10
659+ for pid, meta in sorted(presets.items(), key=lambda x: (priority(x[1]), x[0])):
660+ if isinstance(meta, dict) and bool(meta.get('enabled', True)) and re.fullmatch(r'[a-z0-9-]+', pid):
661+ print(pid)
662+except Exception:
663+ sys.exit(1)
664+" 2>/dev/null); then
665+ registry_parsed=true
666+ fi
667+ fi
668+ if [ "$registry_parsed" = false ]; then
669+ for preset in "$presets_dir"/*/; do
670+ [ -d "$preset" ] || continue
671+ local fallback_id
672+ fallback_id=$(basename "$preset")
673+ case "$fallback_id" in *[!a-z0-9-]*) continue ;; esac
674+ sorted_presets+="${sorted_presets:+$'\n'}$fallback_id"
675+ done
676+ fi
677+
678+ if [ -n "$sorted_presets" ]; then
679+ while IFS= read -r preset_id; do
680+ local strategy="replace"
681+ local manifest_file=""
682+ local manifest="$presets_dir/$preset_id/preset.yml"
683+ local manifest_declared=false
684+ if [ -f "$manifest" ]; then
685+ if [ "${#python_cmd[@]}" -eq 0 ]; then
686+ echo "Error: Python 3 and PyYAML are required to resolve preset template composition" >&2
687+ return 2
688+ fi
689+ local result
690+ local py_stderr
691+ local parse_status
692+ py_stderr=$(mktemp)
693+ if result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "${python_cmd[@]}" -c "
694+import sys, os
695+try:
696+ import yaml
697+except ImportError:
698+ print('yaml_missing', file=sys.stderr)
699+ sys.exit(2)
700+try:
701+ with open(os.environ['SPECKIT_MANIFEST'], encoding='utf-8') as f:
702+ data = yaml.safe_load(f)
703+ if not isinstance(data, dict):
704+ raise ValueError('manifest root must be a mapping')
705+ if 'provides' not in data:
706+ raise ValueError('manifest missing provides section')
707+ provides = data['provides']
708+ if not isinstance(provides, dict):
709+ raise ValueError('manifest provides must be a mapping')
710+ if 'templates' not in provides:
711+ raise ValueError('manifest provides missing templates')
712+ templates = provides['templates']
713+ if not isinstance(templates, list):
714+ raise ValueError('manifest templates must be a list')
715+ if not templates:
716+ raise ValueError('manifest must provide at least one template')
717+ valid_types = ('template', 'command', 'script')
718+ valid_strategies = ('replace', 'prepend', 'append', 'wrap')
719+ for t in templates:
720+ if not isinstance(t, dict):
721+ raise ValueError('manifest template entries must be mappings')
722+ if 'type' not in t or 'name' not in t or 'file' not in t:
723+ raise ValueError('manifest template entry missing type, name, or file')
724+ for field in ('type', 'name', 'file'):
725+ if not isinstance(t[field], str):
726+ raise ValueError('manifest template ' + field + ' must be a string')
727+ if t['type'] not in valid_types:
728+ raise ValueError('invalid manifest template type')
729+ strategy = t.get('strategy', 'replace')
730+ if not isinstance(strategy, str):
731+ raise ValueError('manifest template strategy must be a string')
732+ strategy = strategy.lower()
733+ if strategy not in valid_strategies:
734+ raise ValueError('invalid manifest template strategy')
735+ if t['type'] == 'script' and strategy not in ('replace', 'wrap'):
736+ raise ValueError('invalid manifest script strategy')
737+ for t in templates:
738+ if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template':
739+ file_value = t.get('file', '')
740+ strategy = t.get('strategy', 'replace')
741+ print('found\t' + strategy + '\t' + file_value)
742+ sys.exit(0)
743+ print('absent\treplace\t')
744+except Exception as exc:
745+ print(f'manifest_invalid: {exc}', file=sys.stderr)
746+ sys.exit(3)
747+" 2>"$py_stderr"); then
748+ parse_status=0
749+ else
750+ parse_status=$?
751+ fi
752+ if [ "$parse_status" -ne 0 ]; then
753+ if [ "$parse_status" -eq 2 ]; then
754+ echo "Error: PyYAML is required to resolve preset template composition" >&2
755+ else
756+ echo "Error: invalid preset manifest $manifest" >&2
757+ fi
758+ rm -f "$py_stderr"
759+ return 2
760+ fi
761+ if [ -n "$result" ]; then
762+ local declaration
763+ IFS=$'\t' read -r declaration strategy manifest_file <<< "$result"
764+ [ "$declaration" = "found" ] && manifest_declared=true
765+ strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
766+ fi
767+ rm -f "$py_stderr"
768+ fi
769+
770+ local candidate=""
771+ if [ -n "$manifest_file" ]; then
772+ case "$manifest_file" in
773+ /*|*../*|../*) manifest_file="" ;;
774+ esac
775+ fi
776+ if [ -n "$manifest_file" ]; then
777+ local mf="$presets_dir/$preset_id/$manifest_file"
778+ [ -f "$mf" ] && candidate="$mf"
779+ fi
780+ if [ -z "$candidate" ] && [ "$manifest_declared" = false ]; then
781+ local cf="$presets_dir/$preset_id/templates/${template_name}.md"
782+ [ -f "$cf" ] && candidate="$cf"
783+ if [ -z "$candidate" ]; then
784+ cf="$presets_dir/$preset_id/${template_name}.md"
785+ [ -f "$cf" ] && candidate="$cf"
786+ fi
787+ fi
788+ if [ -n "$candidate" ]; then
789+ layer_paths+=("$candidate")
790+ layer_strategies+=("$strategy")
791+ if [ "$strategy" = "replace" ]; then
792+ effective_base_found=true
793+ break
794+ fi
795+ fi
796+ done <<< "$sorted_presets"
797+ fi
798+ fi
799+
800+ # Priority 3: Extension-provided templates (always "replace")
801+ local ext_dir="$repo_root/.specify/extensions"
802+ if [ "$effective_base_found" = false ] && [ -d "$ext_dir" ]; then
803+ local sorted_extensions=""
804+ if ! sorted_extensions=$(_sorted_extension_ids "$ext_dir"); then
805+ return 2
806+ fi
807+ while IFS= read -r extension_id; do
808+ [ -n "$extension_id" ] || continue
809+ local ext="$ext_dir/$extension_id"
810+ local candidate="$ext/templates/${template_name}.md"
811+ [ -f "$candidate" ] || candidate="$ext/${template_name}.md"
812+ if [ -f "$candidate" ]; then
813+ layer_paths+=("$candidate")
814+ layer_strategies+=("replace")
815+ effective_base_found=true
816+ break
817+ fi
818+ done <<< "$sorted_extensions"
819+ fi
820+
821+ # Priority 4: Core templates (always "replace")
822+ local core="$base/${template_name}.md"
823+ if [ "$effective_base_found" = false ] && [ -f "$core" ]; then
824+ layer_paths+=("$core")
825+ layer_strategies+=("replace")
826+ fi
827+
828+ local count=${#layer_paths[@]}
829+ [ "$count" -eq 0 ] && return 1
830+
831+ # Check if any layer uses a non-replace strategy
832+ local has_composition=false
833+ for s in "${layer_strategies[@]}"; do
834+ [ "$s" != "replace" ] && has_composition=true && break
835+ done
836+
837+ # If the top (highest-priority) layer is replace, it wins entirely —
838+ # lower layers are irrelevant regardless of their strategies.
839+ if [ "${layer_strategies[0]}" = "replace" ]; then
840+ if ! cat "${layer_paths[0]}"; then
841+ echo "Error: failed to read template layer ${layer_paths[0]}" >&2
842+ return 2
843+ fi
844+ return 0
845+ fi
846+
847+ if [ "$has_composition" = false ]; then
848+ if ! cat "${layer_paths[0]}"; then
849+ echo "Error: failed to read template layer ${layer_paths[0]}" >&2
850+ return 2
851+ fi
852+ return 0
853+ fi
854+
855+ # Find the effective base: scan from highest priority (index 0) downward
856+ # to find the nearest replace layer. Only compose layers above that base.
857+ local base_idx=-1
858+ local i
859+ for (( i=0; i<count; i++ )); do
860+ if [ "${layer_strategies[$i]}" = "replace" ]; then
861+ base_idx=$i
862+ break
863+ fi
864+ done
865+
866+ if [ $base_idx -lt 0 ]; then
867+ echo "Error: template '$template_name' has composing layers but no replace base" >&2
868+ return 2
869+ fi
870+
871+ # Read the base content; compose layers above the base (higher priority)
872+ local content
873+ if ! content=$(cat "${layer_paths[$base_idx]}"; status=$?; printf x; exit "$status"); then
874+ echo "Error: failed to read template layer ${layer_paths[$base_idx]}" >&2
875+ return 2
876+ fi
877+ content="${content%x}"
878+
879+ for (( i=base_idx-1; i>=0; i-- )); do
880+ local path="${layer_paths[$i]}"
881+ local strat="${layer_strategies[$i]}"
882+ local layer_content
883+ # Preserve trailing newlines
884+ if ! layer_content=$(cat "$path"; status=$?; printf x; exit "$status"); then
885+ echo "Error: failed to read template layer $path" >&2
886+ return 2
887+ fi
888+ layer_content="${layer_content%x}"
889+
890+ case "$strat" in
891+ replace) content="$layer_content" ;;
892+ prepend)
893+ content=$(printf '%s\n\n%s' "$layer_content" "$content"; printf x)
894+ content="${content%x}"
895+ ;;
896+ append)
897+ content=$(printf '%s\n\n%s' "$content" "$layer_content"; printf x)
898+ content="${content%x}"
899+ ;;
900+ wrap)
901+ case "$layer_content" in
902+ *'{CORE_TEMPLATE}'*) ;;
903+ *) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 2 ;;
904+ esac
905+ # Consume the wrapper left to right instead of rewriting it in
906+ # place. Rewriting re-scanned the string just modified, so base
907+ # content holding a literal {CORE_TEMPLATE} reintroduced the
908+ # token every pass and the loop never terminated. Advancing over
909+ # ``rest`` bounds the work by the tokens in the original wrapper
910+ # and leaves inserted content untouched, matching the single-pass
911+ # semantics of .Replace()/.replace() in the PowerShell and Python
912+ # ports.
913+ local wrapped="" rest="$layer_content"
914+ while [[ "$rest" == *'{CORE_TEMPLATE}'* ]]; do
915+ wrapped="${wrapped}${rest%%\{CORE_TEMPLATE\}*}${content}"
916+ rest="${rest#*\{CORE_TEMPLATE\}}"
917+ done
918+ content="${wrapped}${rest}"
919+ ;;
920+ *) echo "Error: unknown strategy '$strat'" >&2; return 2 ;;
921+ esac
922+ done
923+
924+ printf '%s' "$content"
925+ return 0
926+}
added .specify/scripts/bash/create-new-feature.sh +407 -0
new file mode 100755
@@ -0,0 +1,407 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+JSON_MODE=false
6+DRY_RUN=false
7+ALLOW_EXISTING=false
8+SHORT_NAME=""
9+BRANCH_NUMBER=""
10+USE_TIMESTAMP=false
11+NUMBER_EXPLICIT=false
12+ARGS=()
13+i=1
14+while [ $i -le $# ]; do
15+ arg="${!i}"
16+ case "$arg" in
17+ --json)
18+ JSON_MODE=true
19+ ;;
20+ --dry-run)
21+ DRY_RUN=true
22+ ;;
23+ --allow-existing-branch)
24+ ALLOW_EXISTING=true
25+ ;;
26+ --short-name)
27+ if [ $((i + 1)) -gt $# ]; then
28+ echo 'Error: --short-name requires a value' >&2
29+ exit 1
30+ fi
31+ i=$((i + 1))
32+ next_arg="${!i}"
33+ # Check if the next argument is another option (starts with --)
34+ if [[ "$next_arg" == --* ]]; then
35+ echo 'Error: --short-name requires a value' >&2
36+ exit 1
37+ fi
38+ SHORT_NAME="$next_arg"
39+ ;;
40+ --number)
41+ if [ $((i + 1)) -gt $# ]; then
42+ echo 'Error: --number requires a value' >&2
43+ exit 1
44+ fi
45+ i=$((i + 1))
46+ next_arg="${!i}"
47+ if [[ "$next_arg" == --* ]]; then
48+ echo 'Error: --number requires a value' >&2
49+ exit 1
50+ fi
51+ BRANCH_NUMBER="$next_arg"
52+ if [ -n "$BRANCH_NUMBER" ]; then
53+ NUMBER_EXPLICIT=true
54+ fi
55+ ;;
56+ --timestamp)
57+ USE_TIMESTAMP=true
58+ ;;
59+ --help|-h)
60+ echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
61+ echo ""
62+ echo "Options:"
63+ echo " --json Output in JSON format"
64+ echo " --dry-run Compute feature name and paths without creating directories or files"
65+ echo " --allow-existing-branch Reuse an existing feature directory if it already exists"
66+ echo " --short-name <name> Provide a custom short name (2-4 words) for the feature"
67+ echo " --number N Prefer a feature number (auto-corrected if its specs prefix exists)"
68+ echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
69+ echo " --help, -h Show this help message"
70+ echo ""
71+ echo "Examples:"
72+ echo " $0 'Add user authentication system' --short-name 'user-auth'"
73+ echo " $0 'Implement OAuth2 integration for API' --number 5"
74+ echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
75+ exit 0
76+ ;;
77+ *)
78+ ARGS+=("$arg")
79+ ;;
80+ esac
81+ i=$((i + 1))
82+done
83+
84+FEATURE_DESCRIPTION="${ARGS[*]}"
85+if [ -z "$FEATURE_DESCRIPTION" ]; then
86+ echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
87+ exit 1
88+fi
89+
90+# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
91+FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
92+if [ -z "$FEATURE_DESCRIPTION" ]; then
93+ echo "Error: Feature description cannot be empty or contain only whitespace" >&2
94+ exit 1
95+fi
96+
97+MAX_FEATURE_NUMBER=9223372036854775807
98+MAX_BRANCH_LENGTH=244
99+
100+is_feature_number_in_range() {
101+ local value="$1"
102+ local normalized="${value#"${value%%[!0]*}"}"
103+ [ -n "$normalized" ] || normalized=0
104+ [ ${#normalized} -lt ${#MAX_FEATURE_NUMBER} ] && return 0
105+ [ ${#normalized} -gt ${#MAX_FEATURE_NUMBER} ] && return 1
106+ # Equal-length digit strings must be compared without arithmetic overflow.
107+ # shellcheck disable=SC2071
108+ [[ "$normalized" < "$MAX_FEATURE_NUMBER" || "$normalized" == "$MAX_FEATURE_NUMBER" ]]
109+}
110+
111+# Function to get highest number from specs directory
112+get_highest_from_specs() {
113+ local specs_dir="$1"
114+ local highest=0
115+
116+ if [ -d "$specs_dir" ]; then
117+ for dir in "$specs_dir"/*; do
118+ [ -d "$dir" ] || continue
119+ dirname=$(basename "$dir")
120+ # Match sequential prefixes (>=3 digits), but skip timestamp dirs.
121+ if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
122+ number=$(echo "$dirname" | grep -Eo '^[0-9]+')
123+ if is_feature_number_in_range "$number"; then
124+ number=$((10#$number))
125+ if [ "$number" -gt "$highest" ]; then
126+ highest=$number
127+ fi
128+ fi
129+ fi
130+ done
131+ fi
132+
133+ echo "$highest"
134+}
135+
136+# Return success when a spec directory owns the given numeric prefix.
137+spec_prefix_exists() {
138+ local specs_dir="$1"
139+ local feature_num="$2"
140+
141+ for spec_path in "$specs_dir/${feature_num}-"*; do
142+ [ -d "$spec_path" ] && return 0
143+ done
144+ return 1
145+}
146+
147+# Function to clean and format a branch name
148+clean_branch_name() {
149+ local name="$1"
150+ echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
151+}
152+
153+# Fit a feature prefix and suffix within GitHub's branch-name limit.
154+fit_branch_name() {
155+ local feature_num="$1"
156+ local branch_suffix="$2"
157+ local branch_name="${feature_num}-${branch_suffix}"
158+
159+ if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then
160+ local prefix_length=$(( ${#feature_num} + 1 ))
161+ local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length))
162+ local truncated_suffix
163+ truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//')
164+ branch_name="${feature_num}-${truncated_suffix}"
165+ fi
166+
167+ printf '%s' "$branch_name"
168+}
169+
170+# Quote a value for POSIX shell reuse, byte-identical to Python's shlex.quote
171+# so the persistence hints match the Python variant exactly (printf %q output
172+# differs between bash versions and from shlex.quote for spaces/metachars).
173+shell_quote() {
174+ local value="$1" LC_ALL=C
175+ if [[ "$value" =~ ^[A-Za-z0-9_@%+=:,./-]+$ ]]; then
176+ printf '%s' "$value"
177+ else
178+ local q="'\"'\"'"
179+ printf "'%s'" "${value//\'/$q}"
180+ fi
181+}
182+
183+# Resolve repository root using common.sh functions which prioritize .specify
184+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
185+source "$SCRIPT_DIR/common.sh"
186+
187+REPO_ROOT=$(get_repo_root) || exit 1
188+
189+cd "$REPO_ROOT"
190+
191+SPECS_DIR="$REPO_ROOT/specs"
192+if [ "$DRY_RUN" != true ]; then
193+ mkdir -p "$SPECS_DIR"
194+fi
195+
196+# Function to generate branch name with stop word filtering and length filtering
197+generate_branch_name() {
198+ local description="$1"
199+
200+ # Common stop words to filter out
201+ local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
202+
203+ # Convert to lowercase and split into words
204+ local clean_name=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
205+
206+ # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
207+ local meaningful_words=()
208+ for word in $clean_name; do
209+ # Skip empty words
210+ [ -z "$word" ] && continue
211+
212+ # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
213+ if ! echo "$word" | grep -qiE "$stop_words"; then
214+ if [ ${#word} -ge 3 ]; then
215+ meaningful_words+=("$word")
216+ # Keep short words that appear as an uppercase acronym in the original.
217+ # Uppercase via tr and match with grep -w (both portable) rather than
218+ # bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX).
219+ elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then
220+ meaningful_words+=("$word")
221+ fi
222+ fi
223+ done
224+
225+ # If we have meaningful words, use first 3-4 of them
226+ if [ ${#meaningful_words[@]} -gt 0 ]; then
227+ local max_words=3
228+ if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
229+
230+ local result=""
231+ local count=0
232+ for word in "${meaningful_words[@]}"; do
233+ if [ $count -ge $max_words ]; then break; fi
234+ if [ -n "$result" ]; then result="$result-"; fi
235+ result="$result$word"
236+ count=$((count + 1))
237+ done
238+ echo "$result"
239+ else
240+ # Fallback to original logic if no meaningful words found
241+ local cleaned=$(clean_branch_name "$description")
242+ echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
243+ fi
244+}
245+
246+# Generate branch name
247+if [ -n "$SHORT_NAME" ]; then
248+ # Use provided short name, just clean it up
249+ BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
250+else
251+ # Generate from description with smart filtering
252+ BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
253+fi
254+
255+# Warn if --number and --timestamp are both specified
256+if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
257+ >&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
258+ BRANCH_NUMBER=""
259+fi
260+
261+# Determine branch prefix
262+if [ "$USE_TIMESTAMP" = true ]; then
263+ FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
264+ BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
265+else
266+ if [ -n "$BRANCH_NUMBER" ] && [[ ! "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then
267+ echo "Error: --number must be an unsigned integer, got '$BRANCH_NUMBER'" >&2
268+ exit 1
269+ fi
270+
271+ # Bash arithmetic is signed 64-bit; reject digit strings that would wrap.
272+ if [ -n "$BRANCH_NUMBER" ] && ! is_feature_number_in_range "$BRANCH_NUMBER"; then
273+ echo "Error: --number must be between 0 and $MAX_FEATURE_NUMBER, got '$BRANCH_NUMBER'" >&2
274+ exit 1
275+ fi
276+
277+ # Determine branch number from existing feature directories
278+ if [ -z "$BRANCH_NUMBER" ]; then
279+ HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
280+ if [ "$HIGHEST" -eq "$MAX_FEATURE_NUMBER" ]; then
281+ echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
282+ exit 1
283+ fi
284+ BRANCH_NUMBER=$((HIGHEST + 1))
285+ fi
286+
287+ # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
288+ FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
289+
290+ # Treat an explicit number as a preference when its prefix is already used
291+ # by a feature directory. Auto-detected numbers are already conflict-free.
292+ if [ "$NUMBER_EXPLICIT" = true ]; then
293+ SPEC_CONFLICT=false
294+ REQUESTED_BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
295+ REQUESTED_DIR="$SPECS_DIR/$REQUESTED_BRANCH_NAME"
296+ if [ "$ALLOW_EXISTING" != true ] || [ ! -d "$REQUESTED_DIR" ]; then
297+ spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" && SPEC_CONFLICT=true
298+ fi
299+
300+ if [ "$SPEC_CONFLICT" = true ]; then
301+ REQUESTED_NUM="$FEATURE_NUM"
302+ HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
303+ BRANCH_NUMBER=$HIGHEST
304+ while true; do
305+ if [ "$BRANCH_NUMBER" -eq "$MAX_FEATURE_NUMBER" ]; then
306+ echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
307+ exit 1
308+ fi
309+ BRANCH_NUMBER=$((BRANCH_NUMBER + 1))
310+ FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
311+ spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" || break
312+ done
313+ >&2 echo "[specify] Warning: --number $REQUESTED_NUM conflicts with an existing spec directory; using $FEATURE_NUM instead"
314+ fi
315+ fi
316+
317+fi
318+
319+# GitHub enforces a 244-byte limit on branch names
320+# Validate and truncate if necessary
321+ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
322+BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
323+if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then
324+ >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
325+ >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
326+ >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
327+fi
328+
329+FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
330+SPEC_FILE="$FEATURE_DIR/spec.md"
331+
332+if [ "$DRY_RUN" != true ]; then
333+ if [ -d "$FEATURE_DIR" ] && [ "$ALLOW_EXISTING" != true ]; then
334+ if [ "$USE_TIMESTAMP" = true ]; then
335+ >&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Rerun to get a new timestamp or use a different --short-name."
336+ else
337+ >&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Please use a different feature name or specify a different number with --number."
338+ fi
339+ exit 1
340+ fi
341+
342+ NEEDS_SPEC=false
343+ SPEC_TEMPLATE_FOUND=false
344+ SPEC_TEMPLATE_CONTENT=""
345+ if [ ! -f "$SPEC_FILE" ]; then
346+ NEEDS_SPEC=true
347+ if SPEC_TEMPLATE_CONTENT=$(resolve_template_content "spec-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
348+ SPEC_TEMPLATE_CONTENT="${SPEC_TEMPLATE_CONTENT%x}"
349+ SPEC_TEMPLATE_FOUND=true
350+ else
351+ resolve_status=$?
352+ if [ "$resolve_status" -ne 1 ]; then
353+ exit "$resolve_status"
354+ fi
355+ fi
356+ fi
357+
358+ mkdir -p "$FEATURE_DIR"
359+
360+ if [ "$NEEDS_SPEC" = true ]; then
361+ if [ "$SPEC_TEMPLATE_FOUND" = true ]; then
362+ printf '%s' "$SPEC_TEMPLATE_CONTENT" > "$SPEC_FILE"
363+ else
364+ echo "Warning: Spec template not found; created empty spec file" >&2
365+ touch "$SPEC_FILE"
366+ fi
367+ fi
368+
369+ # Persist to .specify/feature.json so downstream commands can find the feature
370+ _persist_feature_json "$REPO_ROOT" "$FEATURE_DIR"
371+
372+ # Inform the user how to set feature state in their own shell
373+ printf '# To persist: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" >&2
374+ printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" >&2
375+fi
376+
377+if $JSON_MODE; then
378+ if command -v jq >/dev/null 2>&1; then
379+ if [ "$DRY_RUN" = true ]; then
380+ jq -cn \
381+ --arg branch_name "$BRANCH_NAME" \
382+ --arg spec_file "$SPEC_FILE" \
383+ --arg feature_num "$FEATURE_NUM" \
384+ '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
385+ else
386+ jq -cn \
387+ --arg branch_name "$BRANCH_NAME" \
388+ --arg spec_file "$SPEC_FILE" \
389+ --arg feature_num "$FEATURE_NUM" \
390+ '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
391+ fi
392+ else
393+ if [ "$DRY_RUN" = true ]; then
394+ 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")"
395+ else
396+ printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
397+ fi
398+ fi
399+else
400+ echo "BRANCH_NAME: $BRANCH_NAME"
401+ echo "SPEC_FILE: $SPEC_FILE"
402+ echo "FEATURE_NUM: $FEATURE_NUM"
403+ if [ "$DRY_RUN" != true ]; then
404+ printf '# To persist in your shell: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")"
405+ printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")"
406+ fi
407+fi
new file mode 100755
@@ -0,0 +1,407 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+JSON_MODE=false
6+DRY_RUN=false
7+ALLOW_EXISTING=false
8+SHORT_NAME=""
9+BRANCH_NUMBER=""
10+USE_TIMESTAMP=false
11+NUMBER_EXPLICIT=false
12+ARGS=()
13+i=1
14+while [ $i -le $# ]; do
15+ arg="${!i}"
16+ case "$arg" in
17+ --json)
18+ JSON_MODE=true
19+ ;;
20+ --dry-run)
21+ DRY_RUN=true
22+ ;;
23+ --allow-existing-branch)
24+ ALLOW_EXISTING=true
25+ ;;
26+ --short-name)
27+ if [ $((i + 1)) -gt $# ]; then
28+ echo 'Error: --short-name requires a value' >&2
29+ exit 1
30+ fi
31+ i=$((i + 1))
32+ next_arg="${!i}"
33+ # Check if the next argument is another option (starts with --)
34+ if [[ "$next_arg" == --* ]]; then
35+ echo 'Error: --short-name requires a value' >&2
36+ exit 1
37+ fi
38+ SHORT_NAME="$next_arg"
39+ ;;
40+ --number)
41+ if [ $((i + 1)) -gt $# ]; then
42+ echo 'Error: --number requires a value' >&2
43+ exit 1
44+ fi
45+ i=$((i + 1))
46+ next_arg="${!i}"
47+ if [[ "$next_arg" == --* ]]; then
48+ echo 'Error: --number requires a value' >&2
49+ exit 1
50+ fi
51+ BRANCH_NUMBER="$next_arg"
52+ if [ -n "$BRANCH_NUMBER" ]; then
53+ NUMBER_EXPLICIT=true
54+ fi
55+ ;;
56+ --timestamp)
57+ USE_TIMESTAMP=true
58+ ;;
59+ --help|-h)
60+ echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
61+ echo ""
62+ echo "Options:"
63+ echo " --json Output in JSON format"
64+ echo " --dry-run Compute feature name and paths without creating directories or files"
65+ echo " --allow-existing-branch Reuse an existing feature directory if it already exists"
66+ echo " --short-name <name> Provide a custom short name (2-4 words) for the feature"
67+ echo " --number N Prefer a feature number (auto-corrected if its specs prefix exists)"
68+ echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
69+ echo " --help, -h Show this help message"
70+ echo ""
71+ echo "Examples:"
72+ echo " $0 'Add user authentication system' --short-name 'user-auth'"
73+ echo " $0 'Implement OAuth2 integration for API' --number 5"
74+ echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
75+ exit 0
76+ ;;
77+ *)
78+ ARGS+=("$arg")
79+ ;;
80+ esac
81+ i=$((i + 1))
82+done
83+
84+FEATURE_DESCRIPTION="${ARGS[*]}"
85+if [ -z "$FEATURE_DESCRIPTION" ]; then
86+ echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
87+ exit 1
88+fi
89+
90+# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
91+FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
92+if [ -z "$FEATURE_DESCRIPTION" ]; then
93+ echo "Error: Feature description cannot be empty or contain only whitespace" >&2
94+ exit 1
95+fi
96+
97+MAX_FEATURE_NUMBER=9223372036854775807
98+MAX_BRANCH_LENGTH=244
99+
100+is_feature_number_in_range() {
101+ local value="$1"
102+ local normalized="${value#"${value%%[!0]*}"}"
103+ [ -n "$normalized" ] || normalized=0
104+ [ ${#normalized} -lt ${#MAX_FEATURE_NUMBER} ] && return 0
105+ [ ${#normalized} -gt ${#MAX_FEATURE_NUMBER} ] && return 1
106+ # Equal-length digit strings must be compared without arithmetic overflow.
107+ # shellcheck disable=SC2071
108+ [[ "$normalized" < "$MAX_FEATURE_NUMBER" || "$normalized" == "$MAX_FEATURE_NUMBER" ]]
109+}
110+
111+# Function to get highest number from specs directory
112+get_highest_from_specs() {
113+ local specs_dir="$1"
114+ local highest=0
115+
116+ if [ -d "$specs_dir" ]; then
117+ for dir in "$specs_dir"/*; do
118+ [ -d "$dir" ] || continue
119+ dirname=$(basename "$dir")
120+ # Match sequential prefixes (>=3 digits), but skip timestamp dirs.
121+ if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
122+ number=$(echo "$dirname" | grep -Eo '^[0-9]+')
123+ if is_feature_number_in_range "$number"; then
124+ number=$((10#$number))
125+ if [ "$number" -gt "$highest" ]; then
126+ highest=$number
127+ fi
128+ fi
129+ fi
130+ done
131+ fi
132+
133+ echo "$highest"
134+}
135+
136+# Return success when a spec directory owns the given numeric prefix.
137+spec_prefix_exists() {
138+ local specs_dir="$1"
139+ local feature_num="$2"
140+
141+ for spec_path in "$specs_dir/${feature_num}-"*; do
142+ [ -d "$spec_path" ] && return 0
143+ done
144+ return 1
145+}
146+
147+# Function to clean and format a branch name
148+clean_branch_name() {
149+ local name="$1"
150+ echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
151+}
152+
153+# Fit a feature prefix and suffix within GitHub's branch-name limit.
154+fit_branch_name() {
155+ local feature_num="$1"
156+ local branch_suffix="$2"
157+ local branch_name="${feature_num}-${branch_suffix}"
158+
159+ if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then
160+ local prefix_length=$(( ${#feature_num} + 1 ))
161+ local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length))
162+ local truncated_suffix
163+ truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//')
164+ branch_name="${feature_num}-${truncated_suffix}"
165+ fi
166+
167+ printf '%s' "$branch_name"
168+}
169+
170+# Quote a value for POSIX shell reuse, byte-identical to Python's shlex.quote
171+# so the persistence hints match the Python variant exactly (printf %q output
172+# differs between bash versions and from shlex.quote for spaces/metachars).
173+shell_quote() {
174+ local value="$1" LC_ALL=C
175+ if [[ "$value" =~ ^[A-Za-z0-9_@%+=:,./-]+$ ]]; then
176+ printf '%s' "$value"
177+ else
178+ local q="'\"'\"'"
179+ printf "'%s'" "${value//\'/$q}"
180+ fi
181+}
182+
183+# Resolve repository root using common.sh functions which prioritize .specify
184+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
185+source "$SCRIPT_DIR/common.sh"
186+
187+REPO_ROOT=$(get_repo_root) || exit 1
188+
189+cd "$REPO_ROOT"
190+
191+SPECS_DIR="$REPO_ROOT/specs"
192+if [ "$DRY_RUN" != true ]; then
193+ mkdir -p "$SPECS_DIR"
194+fi
195+
196+# Function to generate branch name with stop word filtering and length filtering
197+generate_branch_name() {
198+ local description="$1"
199+
200+ # Common stop words to filter out
201+ local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
202+
203+ # Convert to lowercase and split into words
204+ local clean_name=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
205+
206+ # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
207+ local meaningful_words=()
208+ for word in $clean_name; do
209+ # Skip empty words
210+ [ -z "$word" ] && continue
211+
212+ # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
213+ if ! echo "$word" | grep -qiE "$stop_words"; then
214+ if [ ${#word} -ge 3 ]; then
215+ meaningful_words+=("$word")
216+ # Keep short words that appear as an uppercase acronym in the original.
217+ # Uppercase via tr and match with grep -w (both portable) rather than
218+ # bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX).
219+ elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then
220+ meaningful_words+=("$word")
221+ fi
222+ fi
223+ done
224+
225+ # If we have meaningful words, use first 3-4 of them
226+ if [ ${#meaningful_words[@]} -gt 0 ]; then
227+ local max_words=3
228+ if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
229+
230+ local result=""
231+ local count=0
232+ for word in "${meaningful_words[@]}"; do
233+ if [ $count -ge $max_words ]; then break; fi
234+ if [ -n "$result" ]; then result="$result-"; fi
235+ result="$result$word"
236+ count=$((count + 1))
237+ done
238+ echo "$result"
239+ else
240+ # Fallback to original logic if no meaningful words found
241+ local cleaned=$(clean_branch_name "$description")
242+ echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
243+ fi
244+}
245+
246+# Generate branch name
247+if [ -n "$SHORT_NAME" ]; then
248+ # Use provided short name, just clean it up
249+ BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
250+else
251+ # Generate from description with smart filtering
252+ BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
253+fi
254+
255+# Warn if --number and --timestamp are both specified
256+if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
257+ >&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
258+ BRANCH_NUMBER=""
259+fi
260+
261+# Determine branch prefix
262+if [ "$USE_TIMESTAMP" = true ]; then
263+ FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
264+ BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
265+else
266+ if [ -n "$BRANCH_NUMBER" ] && [[ ! "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then
267+ echo "Error: --number must be an unsigned integer, got '$BRANCH_NUMBER'" >&2
268+ exit 1
269+ fi
270+
271+ # Bash arithmetic is signed 64-bit; reject digit strings that would wrap.
272+ if [ -n "$BRANCH_NUMBER" ] && ! is_feature_number_in_range "$BRANCH_NUMBER"; then
273+ echo "Error: --number must be between 0 and $MAX_FEATURE_NUMBER, got '$BRANCH_NUMBER'" >&2
274+ exit 1
275+ fi
276+
277+ # Determine branch number from existing feature directories
278+ if [ -z "$BRANCH_NUMBER" ]; then
279+ HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
280+ if [ "$HIGHEST" -eq "$MAX_FEATURE_NUMBER" ]; then
281+ echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
282+ exit 1
283+ fi
284+ BRANCH_NUMBER=$((HIGHEST + 1))
285+ fi
286+
287+ # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
288+ FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
289+
290+ # Treat an explicit number as a preference when its prefix is already used
291+ # by a feature directory. Auto-detected numbers are already conflict-free.
292+ if [ "$NUMBER_EXPLICIT" = true ]; then
293+ SPEC_CONFLICT=false
294+ REQUESTED_BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
295+ REQUESTED_DIR="$SPECS_DIR/$REQUESTED_BRANCH_NAME"
296+ if [ "$ALLOW_EXISTING" != true ] || [ ! -d "$REQUESTED_DIR" ]; then
297+ spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" && SPEC_CONFLICT=true
298+ fi
299+
300+ if [ "$SPEC_CONFLICT" = true ]; then
301+ REQUESTED_NUM="$FEATURE_NUM"
302+ HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
303+ BRANCH_NUMBER=$HIGHEST
304+ while true; do
305+ if [ "$BRANCH_NUMBER" -eq "$MAX_FEATURE_NUMBER" ]; then
306+ echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
307+ exit 1
308+ fi
309+ BRANCH_NUMBER=$((BRANCH_NUMBER + 1))
310+ FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
311+ spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" || break
312+ done
313+ >&2 echo "[specify] Warning: --number $REQUESTED_NUM conflicts with an existing spec directory; using $FEATURE_NUM instead"
314+ fi
315+ fi
316+
317+fi
318+
319+# GitHub enforces a 244-byte limit on branch names
320+# Validate and truncate if necessary
321+ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
322+BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
323+if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then
324+ >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
325+ >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
326+ >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
327+fi
328+
329+FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
330+SPEC_FILE="$FEATURE_DIR/spec.md"
331+
332+if [ "$DRY_RUN" != true ]; then
333+ if [ -d "$FEATURE_DIR" ] && [ "$ALLOW_EXISTING" != true ]; then
334+ if [ "$USE_TIMESTAMP" = true ]; then
335+ >&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Rerun to get a new timestamp or use a different --short-name."
336+ else
337+ >&2 echo "Error: Feature directory '$FEATURE_DIR' already exists. Please use a different feature name or specify a different number with --number."
338+ fi
339+ exit 1
340+ fi
341+
342+ NEEDS_SPEC=false
343+ SPEC_TEMPLATE_FOUND=false
344+ SPEC_TEMPLATE_CONTENT=""
345+ if [ ! -f "$SPEC_FILE" ]; then
346+ NEEDS_SPEC=true
347+ if SPEC_TEMPLATE_CONTENT=$(resolve_template_content "spec-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
348+ SPEC_TEMPLATE_CONTENT="${SPEC_TEMPLATE_CONTENT%x}"
349+ SPEC_TEMPLATE_FOUND=true
350+ else
351+ resolve_status=$?
352+ if [ "$resolve_status" -ne 1 ]; then
353+ exit "$resolve_status"
354+ fi
355+ fi
356+ fi
357+
358+ mkdir -p "$FEATURE_DIR"
359+
360+ if [ "$NEEDS_SPEC" = true ]; then
361+ if [ "$SPEC_TEMPLATE_FOUND" = true ]; then
362+ printf '%s' "$SPEC_TEMPLATE_CONTENT" > "$SPEC_FILE"
363+ else
364+ echo "Warning: Spec template not found; created empty spec file" >&2
365+ touch "$SPEC_FILE"
366+ fi
367+ fi
368+
369+ # Persist to .specify/feature.json so downstream commands can find the feature
370+ _persist_feature_json "$REPO_ROOT" "$FEATURE_DIR"
371+
372+ # Inform the user how to set feature state in their own shell
373+ printf '# To persist: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" >&2
374+ printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" >&2
375+fi
376+
377+if $JSON_MODE; then
378+ if command -v jq >/dev/null 2>&1; then
379+ if [ "$DRY_RUN" = true ]; then
380+ jq -cn \
381+ --arg branch_name "$BRANCH_NAME" \
382+ --arg spec_file "$SPEC_FILE" \
383+ --arg feature_num "$FEATURE_NUM" \
384+ '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
385+ else
386+ jq -cn \
387+ --arg branch_name "$BRANCH_NAME" \
388+ --arg spec_file "$SPEC_FILE" \
389+ --arg feature_num "$FEATURE_NUM" \
390+ '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
391+ fi
392+ else
393+ if [ "$DRY_RUN" = true ]; then
394+ 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")"
395+ else
396+ printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
397+ fi
398+ fi
399+else
400+ echo "BRANCH_NAME: $BRANCH_NAME"
401+ echo "SPEC_FILE: $SPEC_FILE"
402+ echo "FEATURE_NUM: $FEATURE_NUM"
403+ if [ "$DRY_RUN" != true ]; then
404+ printf '# To persist in your shell: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")"
405+ printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")"
406+ fi
407+fi
added .specify/scripts/bash/resolve-template.sh +57 -0
new file mode 100755
@@ -0,0 +1,57 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
6+source "$SCRIPT_DIR/common.sh"
7+
8+JSON_MODE=false
9+TEMPLATE_NAME=""
10+
11+for arg in "$@"; do
12+ case "$arg" in
13+ --json) JSON_MODE=true ;;
14+ --help|-h)
15+ echo "Usage: $0 <template-name> [--json]"
16+ exit 0
17+ ;;
18+ -*)
19+ echo "ERROR: Unknown option '$arg'" >&2
20+ exit 1
21+ ;;
22+ *)
23+ if [[ -n "$TEMPLATE_NAME" ]]; then
24+ echo "ERROR: Unexpected argument '$arg'" >&2
25+ exit 1
26+ fi
27+ TEMPLATE_NAME="$arg"
28+ ;;
29+ esac
30+done
31+
32+if [[ -z "$TEMPLATE_NAME" ]]; then
33+ echo "ERROR: Template name is required" >&2
34+ exit 1
35+fi
36+
37+REPO_ROOT=$(get_repo_root)
38+if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
39+ TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}"
40+else
41+ echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2
42+ exit 1
43+fi
44+
45+if $JSON_MODE; then
46+ if has_jq; then
47+ jq -cn \
48+ --arg template_name "$TEMPLATE_NAME" \
49+ --arg template_content "$TEMPLATE_CONTENT" \
50+ '{TEMPLATE_NAME:$template_name,TEMPLATE_CONTENT:$template_content}'
51+ else
52+ printf '{"TEMPLATE_NAME":"%s","TEMPLATE_CONTENT":"%s"}\n' \
53+ "$(json_escape "$TEMPLATE_NAME")" "$(json_escape "$TEMPLATE_CONTENT")"
54+ fi
55+else
56+ printf '%s' "$TEMPLATE_CONTENT"
57+fi
new file mode 100755
@@ -0,0 +1,57 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
6+source "$SCRIPT_DIR/common.sh"
7+
8+JSON_MODE=false
9+TEMPLATE_NAME=""
10+
11+for arg in "$@"; do
12+ case "$arg" in
13+ --json) JSON_MODE=true ;;
14+ --help|-h)
15+ echo "Usage: $0 <template-name> [--json]"
16+ exit 0
17+ ;;
18+ -*)
19+ echo "ERROR: Unknown option '$arg'" >&2
20+ exit 1
21+ ;;
22+ *)
23+ if [[ -n "$TEMPLATE_NAME" ]]; then
24+ echo "ERROR: Unexpected argument '$arg'" >&2
25+ exit 1
26+ fi
27+ TEMPLATE_NAME="$arg"
28+ ;;
29+ esac
30+done
31+
32+if [[ -z "$TEMPLATE_NAME" ]]; then
33+ echo "ERROR: Template name is required" >&2
34+ exit 1
35+fi
36+
37+REPO_ROOT=$(get_repo_root)
38+if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
39+ TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}"
40+else
41+ echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2
42+ exit 1
43+fi
44+
45+if $JSON_MODE; then
46+ if has_jq; then
47+ jq -cn \
48+ --arg template_name "$TEMPLATE_NAME" \
49+ --arg template_content "$TEMPLATE_CONTENT" \
50+ '{TEMPLATE_NAME:$template_name,TEMPLATE_CONTENT:$template_content}'
51+ else
52+ printf '{"TEMPLATE_NAME":"%s","TEMPLATE_CONTENT":"%s"}\n' \
53+ "$(json_escape "$TEMPLATE_NAME")" "$(json_escape "$TEMPLATE_CONTENT")"
54+ fi
55+else
56+ printf '%s' "$TEMPLATE_CONTENT"
57+fi
added .specify/scripts/bash/setup-plan.sh +85 -0
new file mode 100755
@@ -0,0 +1,85 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+# Parse command line arguments
6+JSON_MODE=false
7+
8+for arg in "$@"; do
9+ case "$arg" in
10+ --json)
11+ JSON_MODE=true
12+ ;;
13+ --help|-h)
14+ echo "Usage: $0 [--json]"
15+ echo " --json Output results in JSON format"
16+ echo " --help Show this help message"
17+ exit 0
18+ ;;
19+ *)
20+ echo "ERROR: Unknown option '$arg'" >&2
21+ exit 1
22+ ;;
23+ esac
24+done
25+
26+# Get script directory and load common functions
27+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28+source "$SCRIPT_DIR/common.sh"
29+
30+# Get all paths and variables from common functions
31+_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
32+eval "$_paths_output"
33+unset _paths_output
34+
35+# Ensure the feature directory exists
36+mkdir -p "$FEATURE_DIR"
37+
38+# Copy plan template if plan doesn't already exist
39+if [[ -f "$IMPL_PLAN" ]]; then
40+ if $JSON_MODE; then
41+ echo "Plan already exists at $IMPL_PLAN, skipping template copy" >&2
42+ else
43+ echo "Plan already exists at $IMPL_PLAN, skipping template copy"
44+ fi
45+else
46+ if resolve_template_content "plan-template" "$REPO_ROOT" > "$IMPL_PLAN"; then
47+ if $JSON_MODE; then
48+ echo "Copied plan template to $IMPL_PLAN" >&2
49+ else
50+ echo "Copied plan template to $IMPL_PLAN"
51+ fi
52+ else
53+ resolve_status=$?
54+ rm -f "$IMPL_PLAN"
55+ if [ "$resolve_status" -ne 1 ]; then
56+ exit "$resolve_status"
57+ fi
58+ if $JSON_MODE; then
59+ echo "Warning: Plan template not found" >&2
60+ else
61+ echo "Warning: Plan template not found"
62+ fi
63+ touch "$IMPL_PLAN"
64+ fi
65+fi
66+
67+# Output results
68+if $JSON_MODE; then
69+ if has_jq; then
70+ jq -cn \
71+ --arg feature_spec "$FEATURE_SPEC" \
72+ --arg impl_plan "$IMPL_PLAN" \
73+ --arg specs_dir "$FEATURE_DIR" \
74+ --arg branch "$CURRENT_BRANCH" \
75+ '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch}'
76+ else
77+ printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
78+ "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")"
79+ fi
80+else
81+ echo "FEATURE_SPEC: $FEATURE_SPEC"
82+ echo "IMPL_PLAN: $IMPL_PLAN"
83+ echo "SPECS_DIR: $FEATURE_DIR"
84+ echo "BRANCH: $CURRENT_BRANCH"
85+fi
new file mode 100755
@@ -0,0 +1,85 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+# Parse command line arguments
6+JSON_MODE=false
7+
8+for arg in "$@"; do
9+ case "$arg" in
10+ --json)
11+ JSON_MODE=true
12+ ;;
13+ --help|-h)
14+ echo "Usage: $0 [--json]"
15+ echo " --json Output results in JSON format"
16+ echo " --help Show this help message"
17+ exit 0
18+ ;;
19+ *)
20+ echo "ERROR: Unknown option '$arg'" >&2
21+ exit 1
22+ ;;
23+ esac
24+done
25+
26+# Get script directory and load common functions
27+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28+source "$SCRIPT_DIR/common.sh"
29+
30+# Get all paths and variables from common functions
31+_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
32+eval "$_paths_output"
33+unset _paths_output
34+
35+# Ensure the feature directory exists
36+mkdir -p "$FEATURE_DIR"
37+
38+# Copy plan template if plan doesn't already exist
39+if [[ -f "$IMPL_PLAN" ]]; then
40+ if $JSON_MODE; then
41+ echo "Plan already exists at $IMPL_PLAN, skipping template copy" >&2
42+ else
43+ echo "Plan already exists at $IMPL_PLAN, skipping template copy"
44+ fi
45+else
46+ if resolve_template_content "plan-template" "$REPO_ROOT" > "$IMPL_PLAN"; then
47+ if $JSON_MODE; then
48+ echo "Copied plan template to $IMPL_PLAN" >&2
49+ else
50+ echo "Copied plan template to $IMPL_PLAN"
51+ fi
52+ else
53+ resolve_status=$?
54+ rm -f "$IMPL_PLAN"
55+ if [ "$resolve_status" -ne 1 ]; then
56+ exit "$resolve_status"
57+ fi
58+ if $JSON_MODE; then
59+ echo "Warning: Plan template not found" >&2
60+ else
61+ echo "Warning: Plan template not found"
62+ fi
63+ touch "$IMPL_PLAN"
64+ fi
65+fi
66+
67+# Output results
68+if $JSON_MODE; then
69+ if has_jq; then
70+ jq -cn \
71+ --arg feature_spec "$FEATURE_SPEC" \
72+ --arg impl_plan "$IMPL_PLAN" \
73+ --arg specs_dir "$FEATURE_DIR" \
74+ --arg branch "$CURRENT_BRANCH" \
75+ '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch}'
76+ else
77+ printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
78+ "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")"
79+ fi
80+else
81+ echo "FEATURE_SPEC: $FEATURE_SPEC"
82+ echo "IMPL_PLAN: $IMPL_PLAN"
83+ echo "SPECS_DIR: $FEATURE_DIR"
84+ echo "BRANCH: $CURRENT_BRANCH"
85+fi
added .specify/scripts/bash/setup-tasks.sh +94 -0
new file mode 100755
@@ -0,0 +1,94 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+# Parse command line arguments
6+JSON_MODE=false
7+
8+for arg in "$@"; do
9+ case "$arg" in
10+ --json) JSON_MODE=true ;;
11+ --help|-h)
12+ echo "Usage: $0 [--json]"
13+ echo " --json Output results in JSON format"
14+ echo " --help Show this help message"
15+ exit 0
16+ ;;
17+ *) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;;
18+ esac
19+done
20+
21+# Source common functions
22+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23+source "$SCRIPT_DIR/common.sh"
24+
25+# Get feature paths
26+_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
27+eval "$_paths_output"
28+unset _paths_output
29+
30+# Validate required files
31+if [[ ! -f "$IMPL_PLAN" ]]; then
32+ echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
33+ echo "Run /speckit-plan first to create the implementation plan." >&2
34+ exit 1
35+fi
36+
37+if [[ ! -f "$FEATURE_SPEC" ]]; then
38+ echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
39+ echo "Run /speckit-specify first to create the feature structure." >&2
40+ exit 1
41+fi
42+
43+# Build available docs list
44+docs=()
45+[[ -f "$RESEARCH" ]] && docs+=("research.md")
46+[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
47+if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
48+ docs+=("contracts/")
49+fi
50+[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
51+
52+# Resolve tasks template through override stack
53+TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true
54+if TASKS_TEMPLATE_CONTENT=$(resolve_template_content "tasks-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
55+ TASKS_TEMPLATE_CONTENT="${TASKS_TEMPLATE_CONTENT%x}"
56+else
57+ echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2
58+ echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/overrides/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the core .specify/templates/tasks-template.md template." >&2
59+ exit 1
60+fi
61+
62+# Output results
63+if $JSON_MODE; then
64+ if has_jq; then
65+ if [[ ${#docs[@]} -eq 0 ]]; then
66+ json_docs="[]"
67+ else
68+ json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
69+ fi
70+ jq -cn \
71+ --arg feature_dir "$FEATURE_DIR" \
72+ --argjson docs "$json_docs" \
73+ --arg tasks_template "${TASKS_TEMPLATE:-}" \
74+ --arg tasks_template_content "$TASKS_TEMPLATE_CONTENT" \
75+ '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template,TASKS_TEMPLATE_CONTENT:$tasks_template_content}'
76+ else
77+ if [[ ${#docs[@]} -eq 0 ]]; then
78+ json_docs="[]"
79+ else
80+ json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
81+ json_docs="[${json_docs%,}]"
82+ fi
83+ printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TASKS_TEMPLATE":"%s","TASKS_TEMPLATE_CONTENT":"%s"}\n' \
84+ "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")" "$(json_escape "$TASKS_TEMPLATE_CONTENT")"
85+ fi
86+else
87+ echo "FEATURE_DIR: $FEATURE_DIR"
88+ echo "TASKS_TEMPLATE: ${TASKS_TEMPLATE:-not found}"
89+ echo "AVAILABLE_DOCS:"
90+ check_file "$RESEARCH" "research.md"
91+ check_file "$DATA_MODEL" "data-model.md"
92+ check_dir "$CONTRACTS_DIR" "contracts/"
93+ check_file "$QUICKSTART" "quickstart.md"
94+fi
new file mode 100755
@@ -0,0 +1,94 @@
1+#!/usr/bin/env bash
2+
3+set -e
4+
5+# Parse command line arguments
6+JSON_MODE=false
7+
8+for arg in "$@"; do
9+ case "$arg" in
10+ --json) JSON_MODE=true ;;
11+ --help|-h)
12+ echo "Usage: $0 [--json]"
13+ echo " --json Output results in JSON format"
14+ echo " --help Show this help message"
15+ exit 0
16+ ;;
17+ *) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;;
18+ esac
19+done
20+
21+# Source common functions
22+SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23+source "$SCRIPT_DIR/common.sh"
24+
25+# Get feature paths
26+_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
27+eval "$_paths_output"
28+unset _paths_output
29+
30+# Validate required files
31+if [[ ! -f "$IMPL_PLAN" ]]; then
32+ echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
33+ echo "Run /speckit-plan first to create the implementation plan." >&2
34+ exit 1
35+fi
36+
37+if [[ ! -f "$FEATURE_SPEC" ]]; then
38+ echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
39+ echo "Run /speckit-specify first to create the feature structure." >&2
40+ exit 1
41+fi
42+
43+# Build available docs list
44+docs=()
45+[[ -f "$RESEARCH" ]] && docs+=("research.md")
46+[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
47+if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
48+ docs+=("contracts/")
49+fi
50+[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
51+
52+# Resolve tasks template through override stack
53+TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true
54+if TASKS_TEMPLATE_CONTENT=$(resolve_template_content "tasks-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then
55+ TASKS_TEMPLATE_CONTENT="${TASKS_TEMPLATE_CONTENT%x}"
56+else
57+ echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2
58+ echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/overrides/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the core .specify/templates/tasks-template.md template." >&2
59+ exit 1
60+fi
61+
62+# Output results
63+if $JSON_MODE; then
64+ if has_jq; then
65+ if [[ ${#docs[@]} -eq 0 ]]; then
66+ json_docs="[]"
67+ else
68+ json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
69+ fi
70+ jq -cn \
71+ --arg feature_dir "$FEATURE_DIR" \
72+ --argjson docs "$json_docs" \
73+ --arg tasks_template "${TASKS_TEMPLATE:-}" \
74+ --arg tasks_template_content "$TASKS_TEMPLATE_CONTENT" \
75+ '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template,TASKS_TEMPLATE_CONTENT:$tasks_template_content}'
76+ else
77+ if [[ ${#docs[@]} -eq 0 ]]; then
78+ json_docs="[]"
79+ else
80+ json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
81+ json_docs="[${json_docs%,}]"
82+ fi
83+ printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TASKS_TEMPLATE":"%s","TASKS_TEMPLATE_CONTENT":"%s"}\n' \
84+ "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")" "$(json_escape "$TASKS_TEMPLATE_CONTENT")"
85+ fi
86+else
87+ echo "FEATURE_DIR: $FEATURE_DIR"
88+ echo "TASKS_TEMPLATE: ${TASKS_TEMPLATE:-not found}"
89+ echo "AVAILABLE_DOCS:"
90+ check_file "$RESEARCH" "research.md"
91+ check_file "$DATA_MODEL" "data-model.md"
92+ check_dir "$CONTRACTS_DIR" "contracts/"
93+ check_file "$QUICKSTART" "quickstart.md"
94+fi
added .specify/templates/checklist-template.md +45 -0
new file mode 100644
@@ -0,0 +1,45 @@
1+# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
2+
3+**Purpose**: [Brief description of what this checklist covers]
4+**Created**: [DATE]
5+**Feature**: [Link to spec.md or relevant documentation]
6+
7+**Note**: This custom checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
8+**Review Ownership**: This checklist is a reviewer-owned requirements-quality review artifact. Mark an item `[x]` only when the reviewer determines the requirements-quality criterion is satisfied.
9+**Marker Semantics**: `[x]` means the criterion has been reviewed and satisfied for requirements quality. It does not mean implementation work is complete.
10+
11+<!--
12+ ============================================================================
13+ IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
14+
15+ The /speckit-checklist command MUST replace these with actual items based on:
16+ - User's specific checklist request
17+ - Feature requirements from spec.md
18+ - Technical context from plan.md
19+ - Implementation details from tasks.md
20+
21+ DO NOT keep these sample items in the generated checklist file.
22+ ============================================================================
23+-->
24+
25+## [Category 1]
26+
27+- [ ] CHK001 First checklist item with clear action
28+- [ ] CHK002 Second checklist item
29+- [ ] CHK003 Third checklist item
30+
31+## [Category 2]
32+
33+- [ ] CHK004 Another category item
34+- [ ] CHK005 Item with specific criteria
35+- [ ] CHK006 Final item in this category
36+
37+## Notes
38+
39+- Mark items `[x]` only after review confirms the requirement-quality criterion is satisfied
40+- Leave items unchecked when they still require clarification, correction, or reviewer evaluation
41+- `/speckit-implement` reads checklist checkbox state as a gate and must not modify markers
42+- `checklists/requirements.md` has a separate built-in lifecycle maintained by `/speckit-specify` and `/speckit-clarify`
43+- Add comments or findings inline
44+- Link to relevant resources or documentation
45+- Items are numbered sequentially for easy reference
new file mode 100644
@@ -0,0 +1,45 @@
1+# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
2+
3+**Purpose**: [Brief description of what this checklist covers]
4+**Created**: [DATE]
5+**Feature**: [Link to spec.md or relevant documentation]
6+
7+**Note**: This custom checklist is generated by the `/speckit-checklist` command based on feature context and requirements.
8+**Review Ownership**: This checklist is a reviewer-owned requirements-quality review artifact. Mark an item `[x]` only when the reviewer determines the requirements-quality criterion is satisfied.
9+**Marker Semantics**: `[x]` means the criterion has been reviewed and satisfied for requirements quality. It does not mean implementation work is complete.
10+
11+<!--
12+ ============================================================================
13+ IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
14+
15+ The /speckit-checklist command MUST replace these with actual items based on:
16+ - User's specific checklist request
17+ - Feature requirements from spec.md
18+ - Technical context from plan.md
19+ - Implementation details from tasks.md
20+
21+ DO NOT keep these sample items in the generated checklist file.
22+ ============================================================================
23+-->
24+
25+## [Category 1]
26+
27+- [ ] CHK001 First checklist item with clear action
28+- [ ] CHK002 Second checklist item
29+- [ ] CHK003 Third checklist item
30+
31+## [Category 2]
32+
33+- [ ] CHK004 Another category item
34+- [ ] CHK005 Item with specific criteria
35+- [ ] CHK006 Final item in this category
36+
37+## Notes
38+
39+- Mark items `[x]` only after review confirms the requirement-quality criterion is satisfied
40+- Leave items unchecked when they still require clarification, correction, or reviewer evaluation
41+- `/speckit-implement` reads checklist checkbox state as a gate and must not modify markers
42+- `checklists/requirements.md` has a separate built-in lifecycle maintained by `/speckit-specify` and `/speckit-clarify`
43+- Add comments or findings inline
44+- Link to relevant resources or documentation
45+- Items are numbered sequentially for easy reference
added .specify/templates/constitution-template.md +50 -0
new file mode 100644
@@ -0,0 +1,50 @@
1+# [PROJECT_NAME] Constitution
2+<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
3+
4+## Core Principles
5+
6+### [PRINCIPLE_1_NAME]
7+<!-- Example: I. Library-First -->
8+[PRINCIPLE_1_DESCRIPTION]
9+<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
10+
11+### [PRINCIPLE_2_NAME]
12+<!-- Example: II. CLI Interface -->
13+[PRINCIPLE_2_DESCRIPTION]
14+<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
15+
16+### [PRINCIPLE_3_NAME]
17+<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
18+[PRINCIPLE_3_DESCRIPTION]
19+<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
20+
21+### [PRINCIPLE_4_NAME]
22+<!-- Example: IV. Integration Testing -->
23+[PRINCIPLE_4_DESCRIPTION]
24+<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
25+
26+### [PRINCIPLE_5_NAME]
27+<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
28+[PRINCIPLE_5_DESCRIPTION]
29+<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
30+
31+## [SECTION_2_NAME]
32+<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
33+
34+[SECTION_2_CONTENT]
35+<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
36+
37+## [SECTION_3_NAME]
38+<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
39+
40+[SECTION_3_CONTENT]
41+<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
42+
43+## Governance
44+<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
45+
46+[GOVERNANCE_RULES]
47+<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
48+
49+**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
50+<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
new file mode 100644
@@ -0,0 +1,50 @@
1+# [PROJECT_NAME] Constitution
2+<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
3+
4+## Core Principles
5+
6+### [PRINCIPLE_1_NAME]
7+<!-- Example: I. Library-First -->
8+[PRINCIPLE_1_DESCRIPTION]
9+<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
10+
11+### [PRINCIPLE_2_NAME]
12+<!-- Example: II. CLI Interface -->
13+[PRINCIPLE_2_DESCRIPTION]
14+<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
15+
16+### [PRINCIPLE_3_NAME]
17+<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
18+[PRINCIPLE_3_DESCRIPTION]
19+<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
20+
21+### [PRINCIPLE_4_NAME]
22+<!-- Example: IV. Integration Testing -->
23+[PRINCIPLE_4_DESCRIPTION]
24+<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
25+
26+### [PRINCIPLE_5_NAME]
27+<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
28+[PRINCIPLE_5_DESCRIPTION]
29+<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
30+
31+## [SECTION_2_NAME]
32+<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
33+
34+[SECTION_2_CONTENT]
35+<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
36+
37+## [SECTION_3_NAME]
38+<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
39+
40+[SECTION_3_CONTENT]
41+<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
42+
43+## Governance
44+<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
45+
46+[GOVERNANCE_RULES]
47+<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
48+
49+**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
50+<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
added .specify/templates/plan-template.md +113 -0
new file mode 100644
@@ -0,0 +1,113 @@
1+# Implementation Plan: [FEATURE]
2+
3+**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
4+
5+**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
6+
7+**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow.
8+
9+## Summary
10+
11+[Extract from feature spec: primary requirement + technical approach from research]
12+
13+## Technical Context
14+
15+<!--
16+ ACTION REQUIRED: Replace the content in this section with the technical details
17+ for the project. The structure here is presented in advisory capacity to guide
18+ the iteration process.
19+-->
20+
21+**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
22+
23+**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
24+
25+**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
26+
27+**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
28+
29+**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
30+
31+**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
32+
33+**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
34+
35+**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
36+
37+**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
38+
39+## Constitution Check
40+
41+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
42+
43+[Gates determined based on constitution file]
44+
45+## Project Structure
46+
47+### Documentation (this feature)
48+
49+```text
50+specs/[###-feature]/
51+├── plan.md # This file (/speckit-plan command output)
52+├── research.md # Phase 0 output (/speckit-plan command)
53+├── data-model.md # Phase 1 output (/speckit-plan command)
54+├── quickstart.md # Phase 1 output (/speckit-plan command)
55+├── contracts/ # Phase 1 output (/speckit-plan command)
56+└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
57+```
58+
59+### Source Code (repository root)
60+<!--
61+ ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
62+ for this feature. Delete unused options and expand the chosen structure with
63+ real paths (e.g., apps/admin, packages/something). The delivered plan must
64+ not include Option labels.
65+-->
66+
67+```text
68+# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
69+src/
70+├── models/
71+├── services/
72+├── cli/
73+└── lib/
74+
75+tests/
76+├── contract/
77+├── integration/
78+└── unit/
79+
80+# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
81+backend/
82+├── src/
83+│ ├── models/
84+│ ├── services/
85+│ └── api/
86+└── tests/
87+
88+frontend/
89+├── src/
90+│ ├── components/
91+│ ├── pages/
92+│ └── services/
93+└── tests/
94+
95+# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
96+api/
97+└── [same as backend above]
98+
99+ios/ or android/
100+└── [platform-specific structure: feature modules, UI flows, platform tests]
101+```
102+
103+**Structure Decision**: [Document the selected structure and reference the real
104+directories captured above]
105+
106+## Complexity Tracking
107+
108+> **Fill ONLY if Constitution Check has violations that must be justified**
109+
110+| Violation | Why Needed | Simpler Alternative Rejected Because |
111+|-----------|------------|-------------------------------------|
112+| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
113+| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
new file mode 100644
@@ -0,0 +1,113 @@
1+# Implementation Plan: [FEATURE]
2+
3+**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
4+
5+**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
6+
7+**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow.
8+
9+## Summary
10+
11+[Extract from feature spec: primary requirement + technical approach from research]
12+
13+## Technical Context
14+
15+<!--
16+ ACTION REQUIRED: Replace the content in this section with the technical details
17+ for the project. The structure here is presented in advisory capacity to guide
18+ the iteration process.
19+-->
20+
21+**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
22+
23+**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
24+
25+**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
26+
27+**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
28+
29+**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
30+
31+**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
32+
33+**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
34+
35+**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
36+
37+**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
38+
39+## Constitution Check
40+
41+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
42+
43+[Gates determined based on constitution file]
44+
45+## Project Structure
46+
47+### Documentation (this feature)
48+
49+```text
50+specs/[###-feature]/
51+├── plan.md # This file (/speckit-plan command output)
52+├── research.md # Phase 0 output (/speckit-plan command)
53+├── data-model.md # Phase 1 output (/speckit-plan command)
54+├── quickstart.md # Phase 1 output (/speckit-plan command)
55+├── contracts/ # Phase 1 output (/speckit-plan command)
56+└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
57+```
58+
59+### Source Code (repository root)
60+<!--
61+ ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
62+ for this feature. Delete unused options and expand the chosen structure with
63+ real paths (e.g., apps/admin, packages/something). The delivered plan must
64+ not include Option labels.
65+-->
66+
67+```text
68+# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
69+src/
70+├── models/
71+├── services/
72+├── cli/
73+└── lib/
74+
75+tests/
76+├── contract/
77+├── integration/
78+└── unit/
79+
80+# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
81+backend/
82+├── src/
83+│ ├── models/
84+│ ├── services/
85+│ └── api/
86+└── tests/
87+
88+frontend/
89+├── src/
90+│ ├── components/
91+│ ├── pages/
92+│ └── services/
93+└── tests/
94+
95+# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
96+api/
97+└── [same as backend above]
98+
99+ios/ or android/
100+└── [platform-specific structure: feature modules, UI flows, platform tests]
101+```
102+
103+**Structure Decision**: [Document the selected structure and reference the real
104+directories captured above]
105+
106+## Complexity Tracking
107+
108+> **Fill ONLY if Constitution Check has violations that must be justified**
109+
110+| Violation | Why Needed | Simpler Alternative Rejected Because |
111+|-----------|------------|-------------------------------------|
112+| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
113+| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
added .specify/templates/spec-template.md +131 -0
new file mode 100644
@@ -0,0 +1,131 @@
1+# Feature Specification: [FEATURE NAME]
2+
3+**Feature Branch**: `[###-feature-name]`
4+
5+**Created**: [DATE]
6+
7+**Status**: Draft
8+
9+**Input**: User description: "$ARGUMENTS"
10+
11+## User Scenarios & Testing *(mandatory)*
12+
13+<!--
14+ IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
15+ Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
16+ you should still have a viable MVP (Minimum Viable Product) that delivers value.
17+
18+ Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
19+ Think of each story as a standalone slice of functionality that can be:
20+ - Developed independently
21+ - Tested independently
22+ - Deployed independently
23+ - Demonstrated to users independently
24+-->
25+
26+### User Story 1 - [Brief Title] (Priority: P1)
27+
28+[Describe this user journey in plain language]
29+
30+**Why this priority**: [Explain the value and why it has this priority level]
31+
32+**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
33+
34+**Acceptance Scenarios**:
35+
36+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
37+2. **Given** [initial state], **When** [action], **Then** [expected outcome]
38+
39+---
40+
41+### User Story 2 - [Brief Title] (Priority: P2)
42+
43+[Describe this user journey in plain language]
44+
45+**Why this priority**: [Explain the value and why it has this priority level]
46+
47+**Independent Test**: [Describe how this can be tested independently]
48+
49+**Acceptance Scenarios**:
50+
51+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
52+
53+---
54+
55+### User Story 3 - [Brief Title] (Priority: P3)
56+
57+[Describe this user journey in plain language]
58+
59+**Why this priority**: [Explain the value and why it has this priority level]
60+
61+**Independent Test**: [Describe how this can be tested independently]
62+
63+**Acceptance Scenarios**:
64+
65+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
66+
67+---
68+
69+[Add more user stories as needed, each with an assigned priority]
70+
71+### Edge Cases
72+
73+<!--
74+ ACTION REQUIRED: The content in this section represents placeholders.
75+ Fill them out with the right edge cases.
76+-->
77+
78+- What happens when [boundary condition]?
79+- How does system handle [error scenario]?
80+
81+## Requirements *(mandatory)*
82+
83+<!--
84+ ACTION REQUIRED: The content in this section represents placeholders.
85+ Fill them out with the right functional requirements.
86+-->
87+
88+### Functional Requirements
89+
90+- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
91+- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
92+- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
93+- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
94+- **FR-005**: System MUST [behavior, e.g., "log all security events"]
95+
96+*Example of marking unclear requirements:*
97+
98+- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
99+- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
100+
101+### Key Entities *(include if feature involves data)*
102+
103+- **[Entity 1]**: [What it represents, key attributes without implementation]
104+- **[Entity 2]**: [What it represents, relationships to other entities]
105+
106+## Success Criteria *(mandatory)*
107+
108+<!--
109+ ACTION REQUIRED: Define measurable success criteria.
110+ These must be technology-agnostic and measurable.
111+-->
112+
113+### Measurable Outcomes
114+
115+- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
116+- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
117+- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
118+- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
119+
120+## Assumptions
121+
122+<!--
123+ ACTION REQUIRED: The content in this section represents placeholders.
124+ Fill them out with the right assumptions based on reasonable defaults
125+ chosen when the feature description did not specify certain details.
126+-->
127+
128+- [Assumption about target users, e.g., "Users have stable internet connectivity"]
129+- [Assumption about scope boundaries, e.g., "Mobile support is out of scope for v1"]
130+- [Assumption about data/environment, e.g., "Existing authentication system will be reused"]
131+- [Dependency on existing system/service, e.g., "Requires access to the existing user profile API"]
new file mode 100644
@@ -0,0 +1,131 @@
1+# Feature Specification: [FEATURE NAME]
2+
3+**Feature Branch**: `[###-feature-name]`
4+
5+**Created**: [DATE]
6+
7+**Status**: Draft
8+
9+**Input**: User description: "$ARGUMENTS"
10+
11+## User Scenarios & Testing *(mandatory)*
12+
13+<!--
14+ IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
15+ Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
16+ you should still have a viable MVP (Minimum Viable Product) that delivers value.
17+
18+ Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
19+ Think of each story as a standalone slice of functionality that can be:
20+ - Developed independently
21+ - Tested independently
22+ - Deployed independently
23+ - Demonstrated to users independently
24+-->
25+
26+### User Story 1 - [Brief Title] (Priority: P1)
27+
28+[Describe this user journey in plain language]
29+
30+**Why this priority**: [Explain the value and why it has this priority level]
31+
32+**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
33+
34+**Acceptance Scenarios**:
35+
36+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
37+2. **Given** [initial state], **When** [action], **Then** [expected outcome]
38+
39+---
40+
41+### User Story 2 - [Brief Title] (Priority: P2)
42+
43+[Describe this user journey in plain language]
44+
45+**Why this priority**: [Explain the value and why it has this priority level]
46+
47+**Independent Test**: [Describe how this can be tested independently]
48+
49+**Acceptance Scenarios**:
50+
51+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
52+
53+---
54+
55+### User Story 3 - [Brief Title] (Priority: P3)
56+
57+[Describe this user journey in plain language]
58+
59+**Why this priority**: [Explain the value and why it has this priority level]
60+
61+**Independent Test**: [Describe how this can be tested independently]
62+
63+**Acceptance Scenarios**:
64+
65+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
66+
67+---
68+
69+[Add more user stories as needed, each with an assigned priority]
70+
71+### Edge Cases
72+
73+<!--
74+ ACTION REQUIRED: The content in this section represents placeholders.
75+ Fill them out with the right edge cases.
76+-->
77+
78+- What happens when [boundary condition]?
79+- How does system handle [error scenario]?
80+
81+## Requirements *(mandatory)*
82+
83+<!--
84+ ACTION REQUIRED: The content in this section represents placeholders.
85+ Fill them out with the right functional requirements.
86+-->
87+
88+### Functional Requirements
89+
90+- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
91+- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
92+- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
93+- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
94+- **FR-005**: System MUST [behavior, e.g., "log all security events"]
95+
96+*Example of marking unclear requirements:*
97+
98+- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
99+- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
100+
101+### Key Entities *(include if feature involves data)*
102+
103+- **[Entity 1]**: [What it represents, key attributes without implementation]
104+- **[Entity 2]**: [What it represents, relationships to other entities]
105+
106+## Success Criteria *(mandatory)*
107+
108+<!--
109+ ACTION REQUIRED: Define measurable success criteria.
110+ These must be technology-agnostic and measurable.
111+-->
112+
113+### Measurable Outcomes
114+
115+- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
116+- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
117+- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
118+- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
119+
120+## Assumptions
121+
122+<!--
123+ ACTION REQUIRED: The content in this section represents placeholders.
124+ Fill them out with the right assumptions based on reasonable defaults
125+ chosen when the feature description did not specify certain details.
126+-->
127+
128+- [Assumption about target users, e.g., "Users have stable internet connectivity"]
129+- [Assumption about scope boundaries, e.g., "Mobile support is out of scope for v1"]
130+- [Assumption about data/environment, e.g., "Existing authentication system will be reused"]
131+- [Dependency on existing system/service, e.g., "Requires access to the existing user profile API"]
added .specify/templates/tasks-template.md +252 -0
new file mode 100644
@@ -0,0 +1,252 @@
1+---
2+
3+description: "Task list template for feature implementation"
4+---
5+
6+# Tasks: [FEATURE NAME]
7+
8+**Input**: Design documents from `/specs/[###-feature-name]/`
9+
10+**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
11+
12+**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
13+
14+**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
15+
16+## Format: `[ID] [P?] [Story] Description`
17+
18+- **[P]**: Can run in parallel (different files, no dependencies)
19+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
20+- Include exact file paths in descriptions
21+
22+## Path Conventions
23+
24+- **Single project**: `src/`, `tests/` at repository root
25+- **Web app**: `backend/src/`, `frontend/src/`
26+- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
27+- Paths shown below assume single project - adjust based on plan.md structure
28+
29+<!--
30+ ============================================================================
31+ IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
32+
33+ The /speckit-tasks command MUST replace these with actual tasks based on:
34+ - User stories from spec.md (with their priorities P1, P2, P3...)
35+ - Feature requirements from plan.md
36+ - Entities from data-model.md
37+ - Endpoints from contracts/
38+
39+ Tasks MUST be organized by user story so each story can be:
40+ - Implemented independently
41+ - Tested independently
42+ - Delivered as an MVP increment
43+
44+ DO NOT keep these sample tasks in the generated tasks.md file.
45+ ============================================================================
46+-->
47+
48+## Phase 1: Setup (Shared Infrastructure)
49+
50+**Purpose**: Project initialization and basic structure
51+
52+- [ ] T001 Create project structure per implementation plan
53+- [ ] T002 Initialize [language] project with [framework] dependencies
54+- [ ] T003 [P] Configure linting and formatting tools
55+
56+---
57+
58+## Phase 2: Foundational (Blocking Prerequisites)
59+
60+**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
61+
62+**⚠️ CRITICAL**: No user story work can begin until this phase is complete
63+
64+Examples of foundational tasks (adjust based on your project):
65+
66+- [ ] T004 Setup database schema and migrations framework
67+- [ ] T005 [P] Implement authentication/authorization framework
68+- [ ] T006 [P] Setup API routing and middleware structure
69+- [ ] T007 Create base models/entities that all stories depend on
70+- [ ] T008 Configure error handling and logging infrastructure
71+- [ ] T009 Setup environment configuration management
72+
73+**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
74+
75+---
76+
77+## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
78+
79+**Goal**: [Brief description of what this story delivers]
80+
81+**Independent Test**: [How to verify this story works on its own]
82+
83+### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
84+
85+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
86+
87+- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
88+- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
89+
90+### Implementation for User Story 1
91+
92+- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
93+- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
94+- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
95+- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
96+- [ ] T016 [US1] Add validation and error handling
97+- [ ] T017 [US1] Add logging for user story 1 operations
98+
99+**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
100+
101+---
102+
103+## Phase 4: User Story 2 - [Title] (Priority: P2)
104+
105+**Goal**: [Brief description of what this story delivers]
106+
107+**Independent Test**: [How to verify this story works on its own]
108+
109+### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
110+
111+- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
112+- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
113+
114+### Implementation for User Story 2
115+
116+- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
117+- [ ] T021 [US2] Implement [Service] in src/services/[service].py
118+- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
119+- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
120+
121+**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
122+
123+---
124+
125+## Phase 5: User Story 3 - [Title] (Priority: P3)
126+
127+**Goal**: [Brief description of what this story delivers]
128+
129+**Independent Test**: [How to verify this story works on its own]
130+
131+### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
132+
133+- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
134+- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
135+
136+### Implementation for User Story 3
137+
138+- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
139+- [ ] T027 [US3] Implement [Service] in src/services/[service].py
140+- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
141+
142+**Checkpoint**: All user stories should now be independently functional
143+
144+---
145+
146+[Add more user story phases as needed, following the same pattern]
147+
148+---
149+
150+## Phase N: Polish & Cross-Cutting Concerns
151+
152+**Purpose**: Improvements that affect multiple user stories
153+
154+- [ ] TXXX [P] Documentation updates in docs/
155+- [ ] TXXX Code cleanup and refactoring
156+- [ ] TXXX Performance optimization across all stories
157+- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
158+- [ ] TXXX Security hardening
159+- [ ] TXXX Run quickstart.md validation
160+
161+---
162+
163+## Dependencies & Execution Order
164+
165+### Phase Dependencies
166+
167+- **Setup (Phase 1)**: No dependencies - can start immediately
168+- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
169+- **User Stories (Phase 3+)**: All depend on Foundational phase completion
170+ - User stories can then proceed in parallel (if staffed)
171+ - Or sequentially in priority order (P1 → P2 → P3)
172+- **Polish (Final Phase)**: Depends on all desired user stories being complete
173+
174+### User Story Dependencies
175+
176+- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
177+- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
178+- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
179+
180+### Within Each User Story
181+
182+- Tests (if included) MUST be written and FAIL before implementation
183+- Models before services
184+- Services before endpoints
185+- Core implementation before integration
186+- Story complete before moving to next priority
187+
188+### Parallel Opportunities
189+
190+- All Setup tasks marked [P] can run in parallel
191+- All Foundational tasks marked [P] can run in parallel (within Phase 2)
192+- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
193+- All tests for a user story marked [P] can run in parallel
194+- Models within a story marked [P] can run in parallel
195+- Different user stories can be worked on in parallel by different team members
196+
197+---
198+
199+## Parallel Example: User Story 1
200+
201+```bash
202+# Launch all tests for User Story 1 together (if tests requested):
203+Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
204+Task: "Integration test for [user journey] in tests/integration/test_[name].py"
205+
206+# Launch all models for User Story 1 together:
207+Task: "Create [Entity1] model in src/models/[entity1].py"
208+Task: "Create [Entity2] model in src/models/[entity2].py"
209+```
210+
211+---
212+
213+## Implementation Strategy
214+
215+### MVP First (User Story 1 Only)
216+
217+1. Complete Phase 1: Setup
218+2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
219+3. Complete Phase 3: User Story 1
220+4. **STOP and VALIDATE**: Test User Story 1 independently
221+5. Deploy/demo if ready
222+
223+### Incremental Delivery
224+
225+1. Complete Setup + Foundational → Foundation ready
226+2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
227+3. Add User Story 2 → Test independently → Deploy/Demo
228+4. Add User Story 3 → Test independently → Deploy/Demo
229+5. Each story adds value without breaking previous stories
230+
231+### Parallel Team Strategy
232+
233+With multiple developers:
234+
235+1. Team completes Setup + Foundational together
236+2. Once Foundational is done:
237+ - Developer A: User Story 1
238+ - Developer B: User Story 2
239+ - Developer C: User Story 3
240+3. Stories complete and integrate independently
241+
242+---
243+
244+## Notes
245+
246+- [P] tasks = different files, no dependencies
247+- [Story] label maps task to specific user story for traceability
248+- Each user story should be independently completable and testable
249+- Verify tests fail before implementing
250+- Commit after each task or logical group
251+- Stop at any checkpoint to validate story independently
252+- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
new file mode 100644
@@ -0,0 +1,252 @@
1+---
2+
3+description: "Task list template for feature implementation"
4+---
5+
6+# Tasks: [FEATURE NAME]
7+
8+**Input**: Design documents from `/specs/[###-feature-name]/`
9+
10+**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
11+
12+**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
13+
14+**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
15+
16+## Format: `[ID] [P?] [Story] Description`
17+
18+- **[P]**: Can run in parallel (different files, no dependencies)
19+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
20+- Include exact file paths in descriptions
21+
22+## Path Conventions
23+
24+- **Single project**: `src/`, `tests/` at repository root
25+- **Web app**: `backend/src/`, `frontend/src/`
26+- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
27+- Paths shown below assume single project - adjust based on plan.md structure
28+
29+<!--
30+ ============================================================================
31+ IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
32+
33+ The /speckit-tasks command MUST replace these with actual tasks based on:
34+ - User stories from spec.md (with their priorities P1, P2, P3...)
35+ - Feature requirements from plan.md
36+ - Entities from data-model.md
37+ - Endpoints from contracts/
38+
39+ Tasks MUST be organized by user story so each story can be:
40+ - Implemented independently
41+ - Tested independently
42+ - Delivered as an MVP increment
43+
44+ DO NOT keep these sample tasks in the generated tasks.md file.
45+ ============================================================================
46+-->
47+
48+## Phase 1: Setup (Shared Infrastructure)
49+
50+**Purpose**: Project initialization and basic structure
51+
52+- [ ] T001 Create project structure per implementation plan
53+- [ ] T002 Initialize [language] project with [framework] dependencies
54+- [ ] T003 [P] Configure linting and formatting tools
55+
56+---
57+
58+## Phase 2: Foundational (Blocking Prerequisites)
59+
60+**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
61+
62+**⚠️ CRITICAL**: No user story work can begin until this phase is complete
63+
64+Examples of foundational tasks (adjust based on your project):
65+
66+- [ ] T004 Setup database schema and migrations framework
67+- [ ] T005 [P] Implement authentication/authorization framework
68+- [ ] T006 [P] Setup API routing and middleware structure
69+- [ ] T007 Create base models/entities that all stories depend on
70+- [ ] T008 Configure error handling and logging infrastructure
71+- [ ] T009 Setup environment configuration management
72+
73+**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
74+
75+---
76+
77+## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
78+
79+**Goal**: [Brief description of what this story delivers]
80+
81+**Independent Test**: [How to verify this story works on its own]
82+
83+### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
84+
85+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
86+
87+- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
88+- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
89+
90+### Implementation for User Story 1
91+
92+- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
93+- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
94+- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
95+- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
96+- [ ] T016 [US1] Add validation and error handling
97+- [ ] T017 [US1] Add logging for user story 1 operations
98+
99+**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
100+
101+---
102+
103+## Phase 4: User Story 2 - [Title] (Priority: P2)
104+
105+**Goal**: [Brief description of what this story delivers]
106+
107+**Independent Test**: [How to verify this story works on its own]
108+
109+### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
110+
111+- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
112+- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
113+
114+### Implementation for User Story 2
115+
116+- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
117+- [ ] T021 [US2] Implement [Service] in src/services/[service].py
118+- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
119+- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
120+
121+**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
122+
123+---
124+
125+## Phase 5: User Story 3 - [Title] (Priority: P3)
126+
127+**Goal**: [Brief description of what this story delivers]
128+
129+**Independent Test**: [How to verify this story works on its own]
130+
131+### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
132+
133+- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
134+- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
135+
136+### Implementation for User Story 3
137+
138+- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
139+- [ ] T027 [US3] Implement [Service] in src/services/[service].py
140+- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
141+
142+**Checkpoint**: All user stories should now be independently functional
143+
144+---
145+
146+[Add more user story phases as needed, following the same pattern]
147+
148+---
149+
150+## Phase N: Polish & Cross-Cutting Concerns
151+
152+**Purpose**: Improvements that affect multiple user stories
153+
154+- [ ] TXXX [P] Documentation updates in docs/
155+- [ ] TXXX Code cleanup and refactoring
156+- [ ] TXXX Performance optimization across all stories
157+- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
158+- [ ] TXXX Security hardening
159+- [ ] TXXX Run quickstart.md validation
160+
161+---
162+
163+## Dependencies & Execution Order
164+
165+### Phase Dependencies
166+
167+- **Setup (Phase 1)**: No dependencies - can start immediately
168+- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
169+- **User Stories (Phase 3+)**: All depend on Foundational phase completion
170+ - User stories can then proceed in parallel (if staffed)
171+ - Or sequentially in priority order (P1 → P2 → P3)
172+- **Polish (Final Phase)**: Depends on all desired user stories being complete
173+
174+### User Story Dependencies
175+
176+- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
177+- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
178+- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
179+
180+### Within Each User Story
181+
182+- Tests (if included) MUST be written and FAIL before implementation
183+- Models before services
184+- Services before endpoints
185+- Core implementation before integration
186+- Story complete before moving to next priority
187+
188+### Parallel Opportunities
189+
190+- All Setup tasks marked [P] can run in parallel
191+- All Foundational tasks marked [P] can run in parallel (within Phase 2)
192+- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
193+- All tests for a user story marked [P] can run in parallel
194+- Models within a story marked [P] can run in parallel
195+- Different user stories can be worked on in parallel by different team members
196+
197+---
198+
199+## Parallel Example: User Story 1
200+
201+```bash
202+# Launch all tests for User Story 1 together (if tests requested):
203+Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
204+Task: "Integration test for [user journey] in tests/integration/test_[name].py"
205+
206+# Launch all models for User Story 1 together:
207+Task: "Create [Entity1] model in src/models/[entity1].py"
208+Task: "Create [Entity2] model in src/models/[entity2].py"
209+```
210+
211+---
212+
213+## Implementation Strategy
214+
215+### MVP First (User Story 1 Only)
216+
217+1. Complete Phase 1: Setup
218+2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
219+3. Complete Phase 3: User Story 1
220+4. **STOP and VALIDATE**: Test User Story 1 independently
221+5. Deploy/demo if ready
222+
223+### Incremental Delivery
224+
225+1. Complete Setup + Foundational → Foundation ready
226+2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
227+3. Add User Story 2 → Test independently → Deploy/Demo
228+4. Add User Story 3 → Test independently → Deploy/Demo
229+5. Each story adds value without breaking previous stories
230+
231+### Parallel Team Strategy
232+
233+With multiple developers:
234+
235+1. Team completes Setup + Foundational together
236+2. Once Foundational is done:
237+ - Developer A: User Story 1
238+ - Developer B: User Story 2
239+ - Developer C: User Story 3
240+3. Stories complete and integrate independently
241+
242+---
243+
244+## Notes
245+
246+- [P] tasks = different files, no dependencies
247+- [Story] label maps task to specific user story for traceability
248+- Each user story should be independently completable and testable
249+- Verify tests fail before implementing
250+- Commit after each task or logical group
251+- Stop at any checkpoint to validate story independently
252+- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
added .specify/workflows/speckit/workflow.yml +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+schema_version: "1.0"
2+workflow:
3+ id: "speckit"
4+ name: "Full SDD Cycle"
5+ version: "1.0.0"
6+ author: "GitHub"
7+ description: "Runs specify → plan → tasks → implement with review gates"
8+
9+requires:
10+ # 0.8.5 is the first release with engine-side resolution of the
11+ # ``integration: "auto"`` default. Older versions would treat "auto"
12+ # as a literal integration key and fail at dispatch.
13+ speckit_version: ">=0.8.5"
14+ integrations:
15+ # The four commands below (specify, plan, tasks, implement) are core
16+ # spec-kit commands provided by every integration. The list here is an
17+ # advisory, non-exhaustive compatibility hint following the documented
18+ # ``any: [...]`` schema -- it is NOT a closed set. The workflow runs
19+ # against any integration the project was initialized with, including
20+ # ones not listed below, as long as that integration provides the four
21+ # core commands referenced in ``steps``.
22+ any:
23+ - "alquimia"
24+ - "claude"
25+ - "copilot"
26+ - "gemini"
27+ - "opencode"
28+
29+inputs:
30+ spec:
31+ type: string
32+ required: true
33+ prompt: "Describe what you want to build"
34+ integration:
35+ type: string
36+ default: "auto"
37+ prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)"
38+ scope:
39+ type: string
40+ default: "full"
41+ enum: ["full", "backend-only", "frontend-only"]
42+
43+steps:
44+ - id: specify
45+ command: speckit.specify
46+ integration: "{{ inputs.integration }}"
47+ input:
48+ args: "{{ inputs.spec }}"
49+
50+ - id: review-spec
51+ type: gate
52+ message: "Review the generated spec before planning."
53+ options: [approve, reject]
54+ on_reject: abort
55+
56+ - id: plan
57+ command: speckit.plan
58+ integration: "{{ inputs.integration }}"
59+ input:
60+ args: "{{ inputs.spec }}"
61+
62+ - id: review-plan
63+ type: gate
64+ message: "Review the plan before generating tasks."
65+ options: [approve, reject]
66+ on_reject: abort
67+
68+ - id: tasks
69+ command: speckit.tasks
70+ integration: "{{ inputs.integration }}"
71+ input:
72+ args: "{{ inputs.spec }}"
73+
74+ - id: implement
75+ command: speckit.implement
76+ integration: "{{ inputs.integration }}"
77+ input:
78+ args: "{{ inputs.spec }}"
new file mode 100644
@@ -0,0 +1,78 @@
1+schema_version: "1.0"
2+workflow:
3+ id: "speckit"
4+ name: "Full SDD Cycle"
5+ version: "1.0.0"
6+ author: "GitHub"
7+ description: "Runs specify → plan → tasks → implement with review gates"
8+
9+requires:
10+ # 0.8.5 is the first release with engine-side resolution of the
11+ # ``integration: "auto"`` default. Older versions would treat "auto"
12+ # as a literal integration key and fail at dispatch.
13+ speckit_version: ">=0.8.5"
14+ integrations:
15+ # The four commands below (specify, plan, tasks, implement) are core
16+ # spec-kit commands provided by every integration. The list here is an
17+ # advisory, non-exhaustive compatibility hint following the documented
18+ # ``any: [...]`` schema -- it is NOT a closed set. The workflow runs
19+ # against any integration the project was initialized with, including
20+ # ones not listed below, as long as that integration provides the four
21+ # core commands referenced in ``steps``.
22+ any:
23+ - "alquimia"
24+ - "claude"
25+ - "copilot"
26+ - "gemini"
27+ - "opencode"
28+
29+inputs:
30+ spec:
31+ type: string
32+ required: true
33+ prompt: "Describe what you want to build"
34+ integration:
35+ type: string
36+ default: "auto"
37+ prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)"
38+ scope:
39+ type: string
40+ default: "full"
41+ enum: ["full", "backend-only", "frontend-only"]
42+
43+steps:
44+ - id: specify
45+ command: speckit.specify
46+ integration: "{{ inputs.integration }}"
47+ input:
48+ args: "{{ inputs.spec }}"
49+
50+ - id: review-spec
51+ type: gate
52+ message: "Review the generated spec before planning."
53+ options: [approve, reject]
54+ on_reject: abort
55+
56+ - id: plan
57+ command: speckit.plan
58+ integration: "{{ inputs.integration }}"
59+ input:
60+ args: "{{ inputs.spec }}"
61+
62+ - id: review-plan
63+ type: gate
64+ message: "Review the plan before generating tasks."
65+ options: [approve, reject]
66+ on_reject: abort
67+
68+ - id: tasks
69+ command: speckit.tasks
70+ integration: "{{ inputs.integration }}"
71+ input:
72+ args: "{{ inputs.spec }}"
73+
74+ - id: implement
75+ command: speckit.implement
76+ integration: "{{ inputs.integration }}"
77+ input:
78+ args: "{{ inputs.spec }}"
added .specify/workflows/workflow-registry.json +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+{
2+ "schema_version": "1.0",
3+ "workflows": {
4+ "speckit": {
5+ "name": "Full SDD Cycle",
6+ "version": "1.0.0",
7+ "description": "Runs specify \u2192 plan \u2192 tasks \u2192 implement with review gates",
8+ "source": "bundled",
9+ "installed_at": "2026-09-07T14:46:33.281392+00:00",
10+ "updated_at": "2026-09-07T14:46:33.281399+00:00"
11+ }
12+ }
13+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,13 @@
1+{
2+ "schema_version": "1.0",
3+ "workflows": {
4+ "speckit": {
5+ "name": "Full SDD Cycle",
6+ "version": "1.0.0",
7+ "description": "Runs specify \u2192 plan \u2192 tasks \u2192 implement with review gates",
8+ "source": "bundled",
9+ "installed_at": "2026-09-07T14:46:33.281392+00:00",
10+ "updated_at": "2026-09-07T14:46:33.281399+00:00"
11+ }
12+ }
13+}
\ No newline at end of file\ No newline at end of file