@arvorco/relentless 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/relentless.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -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 -->
|
|
@@ -0,0 +1,166 @@
|
|
|
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-tasks Require tasks.md to exist (for implementation phase)
|
|
13
|
+
# --include-tasks Include tasks.md in AVAILABLE_DOCS list
|
|
14
|
+
# --paths-only Only output path variables (no validation)
|
|
15
|
+
# --help, -h Show help message
|
|
16
|
+
#
|
|
17
|
+
# OUTPUTS:
|
|
18
|
+
# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
|
|
19
|
+
# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
|
|
20
|
+
# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
|
|
21
|
+
|
|
22
|
+
set -e
|
|
23
|
+
|
|
24
|
+
# Parse command line arguments
|
|
25
|
+
JSON_MODE=false
|
|
26
|
+
REQUIRE_TASKS=false
|
|
27
|
+
INCLUDE_TASKS=false
|
|
28
|
+
PATHS_ONLY=false
|
|
29
|
+
|
|
30
|
+
for arg in "$@"; do
|
|
31
|
+
case "$arg" in
|
|
32
|
+
--json)
|
|
33
|
+
JSON_MODE=true
|
|
34
|
+
;;
|
|
35
|
+
--require-tasks)
|
|
36
|
+
REQUIRE_TASKS=true
|
|
37
|
+
;;
|
|
38
|
+
--include-tasks)
|
|
39
|
+
INCLUDE_TASKS=true
|
|
40
|
+
;;
|
|
41
|
+
--paths-only)
|
|
42
|
+
PATHS_ONLY=true
|
|
43
|
+
;;
|
|
44
|
+
--help|-h)
|
|
45
|
+
cat << 'EOF'
|
|
46
|
+
Usage: check-prerequisites.sh [OPTIONS]
|
|
47
|
+
|
|
48
|
+
Consolidated prerequisite checking for Spec-Driven Development workflow.
|
|
49
|
+
|
|
50
|
+
OPTIONS:
|
|
51
|
+
--json Output in JSON format
|
|
52
|
+
--require-tasks Require tasks.md to exist (for implementation phase)
|
|
53
|
+
--include-tasks Include tasks.md in AVAILABLE_DOCS list
|
|
54
|
+
--paths-only Only output path variables (no prerequisite validation)
|
|
55
|
+
--help, -h Show this help message
|
|
56
|
+
|
|
57
|
+
EXAMPLES:
|
|
58
|
+
# Check task prerequisites (plan.md required)
|
|
59
|
+
./check-prerequisites.sh --json
|
|
60
|
+
|
|
61
|
+
# Check implementation prerequisites (plan.md + tasks.md required)
|
|
62
|
+
./check-prerequisites.sh --json --require-tasks --include-tasks
|
|
63
|
+
|
|
64
|
+
# Get feature paths only (no validation)
|
|
65
|
+
./check-prerequisites.sh --paths-only
|
|
66
|
+
|
|
67
|
+
EOF
|
|
68
|
+
exit 0
|
|
69
|
+
;;
|
|
70
|
+
*)
|
|
71
|
+
echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
|
|
72
|
+
exit 1
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
# Source common functions
|
|
78
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
79
|
+
source "$SCRIPT_DIR/common.sh"
|
|
80
|
+
|
|
81
|
+
# Get feature paths and validate branch
|
|
82
|
+
eval $(get_feature_paths)
|
|
83
|
+
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
|
84
|
+
|
|
85
|
+
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
|
86
|
+
if $PATHS_ONLY; then
|
|
87
|
+
if $JSON_MODE; then
|
|
88
|
+
# Minimal JSON paths payload (no validation performed)
|
|
89
|
+
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
|
90
|
+
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
|
|
91
|
+
else
|
|
92
|
+
echo "REPO_ROOT: $REPO_ROOT"
|
|
93
|
+
echo "BRANCH: $CURRENT_BRANCH"
|
|
94
|
+
echo "FEATURE_DIR: $FEATURE_DIR"
|
|
95
|
+
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
|
96
|
+
echo "IMPL_PLAN: $IMPL_PLAN"
|
|
97
|
+
echo "TASKS: $TASKS"
|
|
98
|
+
fi
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Validate required directories and files
|
|
103
|
+
if [[ ! -d "$FEATURE_DIR" ]]; then
|
|
104
|
+
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
|
|
105
|
+
echo "Run /relentless.specify first to create the feature structure." >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [[ ! -f "$IMPL_PLAN" ]]; then
|
|
110
|
+
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
|
|
111
|
+
echo "Run /relentless.plan first to create the implementation plan." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Check for tasks.md if required
|
|
116
|
+
if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
|
|
117
|
+
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
|
|
118
|
+
echo "Run /relentless.tasks first to create the task list." >&2
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Build list of available documents
|
|
123
|
+
docs=()
|
|
124
|
+
|
|
125
|
+
# Always check these optional docs
|
|
126
|
+
[[ -f "$RESEARCH" ]] && docs+=("research.md")
|
|
127
|
+
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
|
|
128
|
+
|
|
129
|
+
# Check contracts directory (only if it exists and has files)
|
|
130
|
+
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
|
|
131
|
+
docs+=("contracts/")
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
|
|
135
|
+
|
|
136
|
+
# Include tasks.md if requested and it exists
|
|
137
|
+
if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
|
|
138
|
+
docs+=("tasks.md")
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Output results
|
|
142
|
+
if $JSON_MODE; then
|
|
143
|
+
# Build JSON array of documents
|
|
144
|
+
if [[ ${#docs[@]} -eq 0 ]]; then
|
|
145
|
+
json_docs="[]"
|
|
146
|
+
else
|
|
147
|
+
json_docs=$(printf '"%s",' "${docs[@]}")
|
|
148
|
+
json_docs="[${json_docs%,}]"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
|
|
152
|
+
else
|
|
153
|
+
# Text output
|
|
154
|
+
echo "FEATURE_DIR:$FEATURE_DIR"
|
|
155
|
+
echo "AVAILABLE_DOCS:"
|
|
156
|
+
|
|
157
|
+
# Show status of each potential document
|
|
158
|
+
check_file "$RESEARCH" "research.md"
|
|
159
|
+
check_file "$DATA_MODEL" "data-model.md"
|
|
160
|
+
check_dir "$CONTRACTS_DIR" "contracts/"
|
|
161
|
+
check_file "$QUICKSTART" "quickstart.md"
|
|
162
|
+
|
|
163
|
+
if $INCLUDE_TASKS; then
|
|
164
|
+
check_file "$TASKS" "tasks.md"
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Common functions and variables for all scripts
|
|
3
|
+
|
|
4
|
+
# Get repository root, with fallback for non-git repositories
|
|
5
|
+
get_repo_root() {
|
|
6
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
7
|
+
git rev-parse --show-toplevel
|
|
8
|
+
else
|
|
9
|
+
# Fall back to script location for non-git repos
|
|
10
|
+
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
(cd "$script_dir/../../.." && pwd)
|
|
12
|
+
fi
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Get current branch, with fallback for non-git repositories
|
|
16
|
+
get_current_branch() {
|
|
17
|
+
# First check if SPECIFY_FEATURE environment variable is set
|
|
18
|
+
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
|
|
19
|
+
echo "$SPECIFY_FEATURE"
|
|
20
|
+
return
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Then check git if available
|
|
24
|
+
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
|
|
25
|
+
git rev-parse --abbrev-ref HEAD
|
|
26
|
+
return
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# For non-git repos, try to find the latest feature directory
|
|
30
|
+
local repo_root=$(get_repo_root)
|
|
31
|
+
local specs_dir="$repo_root/specs"
|
|
32
|
+
|
|
33
|
+
if [[ -d "$specs_dir" ]]; then
|
|
34
|
+
local latest_feature=""
|
|
35
|
+
local highest=0
|
|
36
|
+
|
|
37
|
+
for dir in "$specs_dir"/*; do
|
|
38
|
+
if [[ -d "$dir" ]]; then
|
|
39
|
+
local dirname=$(basename "$dir")
|
|
40
|
+
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
|
|
41
|
+
local number=${BASH_REMATCH[1]}
|
|
42
|
+
number=$((10#$number))
|
|
43
|
+
if [[ "$number" -gt "$highest" ]]; then
|
|
44
|
+
highest=$number
|
|
45
|
+
latest_feature=$dirname
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
if [[ -n "$latest_feature" ]]; then
|
|
52
|
+
echo "$latest_feature"
|
|
53
|
+
return
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "main" # Final fallback
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Check if we have git available
|
|
61
|
+
has_git() {
|
|
62
|
+
git rev-parse --show-toplevel >/dev/null 2>&1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
check_feature_branch() {
|
|
66
|
+
local branch="$1"
|
|
67
|
+
local has_git_repo="$2"
|
|
68
|
+
|
|
69
|
+
# For non-git repos, we can't enforce branch naming but still provide output
|
|
70
|
+
if [[ "$has_git_repo" != "true" ]]; then
|
|
71
|
+
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
|
|
76
|
+
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
|
77
|
+
echo "Feature branches should be named like: 001-feature-name" >&2
|
|
78
|
+
return 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get_feature_dir() { echo "$1/specs/$2"; }
|
|
85
|
+
|
|
86
|
+
# Find feature directory by numeric prefix instead of exact branch match
|
|
87
|
+
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
|
|
88
|
+
find_feature_dir_by_prefix() {
|
|
89
|
+
local repo_root="$1"
|
|
90
|
+
local branch_name="$2"
|
|
91
|
+
local specs_dir="$repo_root/specs"
|
|
92
|
+
|
|
93
|
+
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
|
|
94
|
+
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
|
|
95
|
+
# If branch doesn't have numeric prefix, fall back to exact match
|
|
96
|
+
echo "$specs_dir/$branch_name"
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
local prefix="${BASH_REMATCH[1]}"
|
|
101
|
+
|
|
102
|
+
# Search for directories in specs/ that start with this prefix
|
|
103
|
+
local matches=()
|
|
104
|
+
if [[ -d "$specs_dir" ]]; then
|
|
105
|
+
for dir in "$specs_dir"/"$prefix"-*; do
|
|
106
|
+
if [[ -d "$dir" ]]; then
|
|
107
|
+
matches+=("$(basename "$dir")")
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Handle results
|
|
113
|
+
if [[ ${#matches[@]} -eq 0 ]]; then
|
|
114
|
+
# No match found - return the branch name path (will fail later with clear error)
|
|
115
|
+
echo "$specs_dir/$branch_name"
|
|
116
|
+
elif [[ ${#matches[@]} -eq 1 ]]; then
|
|
117
|
+
# Exactly one match - perfect!
|
|
118
|
+
echo "$specs_dir/${matches[0]}"
|
|
119
|
+
else
|
|
120
|
+
# Multiple matches - this shouldn't happen with proper naming convention
|
|
121
|
+
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
|
122
|
+
echo "Please ensure only one spec directory exists per numeric prefix." >&2
|
|
123
|
+
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
|
|
124
|
+
fi
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get_feature_paths() {
|
|
128
|
+
local repo_root=$(get_repo_root)
|
|
129
|
+
local current_branch=$(get_current_branch)
|
|
130
|
+
local has_git_repo="false"
|
|
131
|
+
|
|
132
|
+
if has_git; then
|
|
133
|
+
has_git_repo="true"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Use prefix-based lookup to support multiple branches per spec
|
|
137
|
+
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
|
|
138
|
+
|
|
139
|
+
cat <<EOF
|
|
140
|
+
REPO_ROOT='$repo_root'
|
|
141
|
+
CURRENT_BRANCH='$current_branch'
|
|
142
|
+
HAS_GIT='$has_git_repo'
|
|
143
|
+
FEATURE_DIR='$feature_dir'
|
|
144
|
+
FEATURE_SPEC='$feature_dir/spec.md'
|
|
145
|
+
IMPL_PLAN='$feature_dir/plan.md'
|
|
146
|
+
TASKS='$feature_dir/tasks.md'
|
|
147
|
+
RESEARCH='$feature_dir/research.md'
|
|
148
|
+
DATA_MODEL='$feature_dir/data-model.md'
|
|
149
|
+
QUICKSTART='$feature_dir/quickstart.md'
|
|
150
|
+
CONTRACTS_DIR='$feature_dir/contracts'
|
|
151
|
+
EOF
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
|
155
|
+
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
|
156
|
+
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
JSON_MODE=false
|
|
6
|
+
SHORT_NAME=""
|
|
7
|
+
BRANCH_NUMBER=""
|
|
8
|
+
ARGS=()
|
|
9
|
+
i=1
|
|
10
|
+
while [ $i -le $# ]; do
|
|
11
|
+
arg="${!i}"
|
|
12
|
+
case "$arg" in
|
|
13
|
+
--json)
|
|
14
|
+
JSON_MODE=true
|
|
15
|
+
;;
|
|
16
|
+
--short-name)
|
|
17
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
18
|
+
echo 'Error: --short-name requires a value' >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
i=$((i + 1))
|
|
22
|
+
next_arg="${!i}"
|
|
23
|
+
# Check if the next argument is another option (starts with --)
|
|
24
|
+
if [[ "$next_arg" == --* ]]; then
|
|
25
|
+
echo 'Error: --short-name requires a value' >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
SHORT_NAME="$next_arg"
|
|
29
|
+
;;
|
|
30
|
+
--number)
|
|
31
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
32
|
+
echo 'Error: --number requires a value' >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
i=$((i + 1))
|
|
36
|
+
next_arg="${!i}"
|
|
37
|
+
if [[ "$next_arg" == --* ]]; then
|
|
38
|
+
echo 'Error: --number requires a value' >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
BRANCH_NUMBER="$next_arg"
|
|
42
|
+
;;
|
|
43
|
+
--help|-h)
|
|
44
|
+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
|
45
|
+
echo ""
|
|
46
|
+
echo "Options:"
|
|
47
|
+
echo " --json Output in JSON format"
|
|
48
|
+
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
|
49
|
+
echo " --number N Specify branch number manually (overrides auto-detection)"
|
|
50
|
+
echo " --help, -h Show this help message"
|
|
51
|
+
echo ""
|
|
52
|
+
echo "Examples:"
|
|
53
|
+
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
|
54
|
+
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
|
55
|
+
exit 0
|
|
56
|
+
;;
|
|
57
|
+
*)
|
|
58
|
+
ARGS+=("$arg")
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
i=$((i + 1))
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
FEATURE_DESCRIPTION="${ARGS[*]}"
|
|
65
|
+
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|
66
|
+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Function to find the repository root by searching for existing project markers
|
|
71
|
+
find_repo_root() {
|
|
72
|
+
local dir="$1"
|
|
73
|
+
while [ "$dir" != "/" ]; do
|
|
74
|
+
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
|
|
75
|
+
echo "$dir"
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
dir="$(dirname "$dir")"
|
|
79
|
+
done
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Function to get highest number from specs directory
|
|
84
|
+
get_highest_from_specs() {
|
|
85
|
+
local specs_dir="$1"
|
|
86
|
+
local highest=0
|
|
87
|
+
|
|
88
|
+
if [ -d "$specs_dir" ]; then
|
|
89
|
+
for dir in "$specs_dir"/*; do
|
|
90
|
+
[ -d "$dir" ] || continue
|
|
91
|
+
dirname=$(basename "$dir")
|
|
92
|
+
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
|
93
|
+
number=$((10#$number))
|
|
94
|
+
if [ "$number" -gt "$highest" ]; then
|
|
95
|
+
highest=$number
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
echo "$highest"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Function to get highest number from git branches
|
|
104
|
+
get_highest_from_branches() {
|
|
105
|
+
local highest=0
|
|
106
|
+
|
|
107
|
+
# Get all branches (local and remote)
|
|
108
|
+
branches=$(git branch -a 2>/dev/null || echo "")
|
|
109
|
+
|
|
110
|
+
if [ -n "$branches" ]; then
|
|
111
|
+
while IFS= read -r branch; do
|
|
112
|
+
# Clean branch name: remove leading markers and remote prefixes
|
|
113
|
+
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
|
114
|
+
|
|
115
|
+
# Extract feature number if branch matches pattern ###-*
|
|
116
|
+
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
|
117
|
+
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
|
118
|
+
number=$((10#$number))
|
|
119
|
+
if [ "$number" -gt "$highest" ]; then
|
|
120
|
+
highest=$number
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
done <<< "$branches"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
echo "$highest"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Function to check existing branches (local and remote) and return next available number
|
|
130
|
+
check_existing_branches() {
|
|
131
|
+
local specs_dir="$1"
|
|
132
|
+
|
|
133
|
+
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
|
134
|
+
git fetch --all --prune 2>/dev/null || true
|
|
135
|
+
|
|
136
|
+
# Get highest number from ALL branches (not just matching short name)
|
|
137
|
+
local highest_branch=$(get_highest_from_branches)
|
|
138
|
+
|
|
139
|
+
# Get highest number from ALL specs (not just matching short name)
|
|
140
|
+
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
|
141
|
+
|
|
142
|
+
# Take the maximum of both
|
|
143
|
+
local max_num=$highest_branch
|
|
144
|
+
if [ "$highest_spec" -gt "$max_num" ]; then
|
|
145
|
+
max_num=$highest_spec
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Return next number
|
|
149
|
+
echo $((max_num + 1))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Function to clean and format a branch name
|
|
153
|
+
clean_branch_name() {
|
|
154
|
+
local name="$1"
|
|
155
|
+
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Resolve repository root. Prefer git information when available, but fall back
|
|
159
|
+
# to searching for repository markers so the workflow still functions in repositories that
|
|
160
|
+
# were initialised with --no-git.
|
|
161
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
162
|
+
|
|
163
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
164
|
+
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
165
|
+
HAS_GIT=true
|
|
166
|
+
else
|
|
167
|
+
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
|
168
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
169
|
+
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
|
170
|
+
exit 1
|
|
171
|
+
fi
|
|
172
|
+
HAS_GIT=false
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
cd "$REPO_ROOT"
|
|
176
|
+
|
|
177
|
+
SPECS_DIR="$REPO_ROOT/specs"
|
|
178
|
+
mkdir -p "$SPECS_DIR"
|
|
179
|
+
|
|
180
|
+
# Function to generate branch name with stop word filtering and length filtering
|
|
181
|
+
generate_branch_name() {
|
|
182
|
+
local description="$1"
|
|
183
|
+
|
|
184
|
+
# Common stop words to filter out
|
|
185
|
+
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)$"
|
|
186
|
+
|
|
187
|
+
# Convert to lowercase and split into words
|
|
188
|
+
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
|
189
|
+
|
|
190
|
+
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
|
191
|
+
local meaningful_words=()
|
|
192
|
+
for word in $clean_name; do
|
|
193
|
+
# Skip empty words
|
|
194
|
+
[ -z "$word" ] && continue
|
|
195
|
+
|
|
196
|
+
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
|
197
|
+
if ! echo "$word" | grep -qiE "$stop_words"; then
|
|
198
|
+
if [ ${#word} -ge 3 ]; then
|
|
199
|
+
meaningful_words+=("$word")
|
|
200
|
+
elif echo "$description" | grep -q "\b${word^^}\b"; then
|
|
201
|
+
# Keep short words if they appear as uppercase in original (likely acronyms)
|
|
202
|
+
meaningful_words+=("$word")
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
done
|
|
206
|
+
|
|
207
|
+
# If we have meaningful words, use first 3-4 of them
|
|
208
|
+
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
|
209
|
+
local max_words=3
|
|
210
|
+
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
|
211
|
+
|
|
212
|
+
local result=""
|
|
213
|
+
local count=0
|
|
214
|
+
for word in "${meaningful_words[@]}"; do
|
|
215
|
+
if [ $count -ge $max_words ]; then break; fi
|
|
216
|
+
if [ -n "$result" ]; then result="$result-"; fi
|
|
217
|
+
result="$result$word"
|
|
218
|
+
count=$((count + 1))
|
|
219
|
+
done
|
|
220
|
+
echo "$result"
|
|
221
|
+
else
|
|
222
|
+
# Fallback to original logic if no meaningful words found
|
|
223
|
+
local cleaned=$(clean_branch_name "$description")
|
|
224
|
+
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
|
225
|
+
fi
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Generate branch name
|
|
229
|
+
if [ -n "$SHORT_NAME" ]; then
|
|
230
|
+
# Use provided short name, just clean it up
|
|
231
|
+
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
|
232
|
+
else
|
|
233
|
+
# Generate from description with smart filtering
|
|
234
|
+
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# Determine branch number
|
|
238
|
+
if [ -z "$BRANCH_NUMBER" ]; then
|
|
239
|
+
if [ "$HAS_GIT" = true ]; then
|
|
240
|
+
# Check existing branches on remotes
|
|
241
|
+
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
|
242
|
+
else
|
|
243
|
+
# Fall back to local directory check
|
|
244
|
+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
|
245
|
+
BRANCH_NUMBER=$((HIGHEST + 1))
|
|
246
|
+
fi
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
|
250
|
+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
|
251
|
+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
|
252
|
+
|
|
253
|
+
# GitHub enforces a 244-byte limit on branch names
|
|
254
|
+
# Validate and truncate if necessary
|
|
255
|
+
MAX_BRANCH_LENGTH=244
|
|
256
|
+
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
|
257
|
+
# Calculate how much we need to trim from suffix
|
|
258
|
+
# Account for: feature number (3) + hyphen (1) = 4 chars
|
|
259
|
+
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
|
|
260
|
+
|
|
261
|
+
# Truncate suffix at word boundary if possible
|
|
262
|
+
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
|
263
|
+
# Remove trailing hyphen if truncation created one
|
|
264
|
+
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
|
265
|
+
|
|
266
|
+
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
|
267
|
+
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
|
268
|
+
|
|
269
|
+
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
|
270
|
+
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
|
271
|
+
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
if [ "$HAS_GIT" = true ]; then
|
|
275
|
+
git checkout -b "$BRANCH_NAME"
|
|
276
|
+
else
|
|
277
|
+
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
|
281
|
+
mkdir -p "$FEATURE_DIR"
|
|
282
|
+
|
|
283
|
+
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
|
|
284
|
+
SPEC_FILE="$FEATURE_DIR/spec.md"
|
|
285
|
+
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
|
286
|
+
|
|
287
|
+
# Set the SPECIFY_FEATURE environment variable for the current session
|
|
288
|
+
export SPECIFY_FEATURE="$BRANCH_NAME"
|
|
289
|
+
|
|
290
|
+
if $JSON_MODE; then
|
|
291
|
+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
|
292
|
+
else
|
|
293
|
+
echo "BRANCH_NAME: $BRANCH_NAME"
|
|
294
|
+
echo "SPEC_FILE: $SPEC_FILE"
|
|
295
|
+
echo "FEATURE_NUM: $FEATURE_NUM"
|
|
296
|
+
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
|
297
|
+
fi
|