agentme 0.1.1

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.
Files changed (49) hide show
  1. package/.github/agents/speckit.analyze.agent.md +184 -0
  2. package/.github/agents/speckit.checklist.agent.md +295 -0
  3. package/.github/agents/speckit.clarify.agent.md +181 -0
  4. package/.github/agents/speckit.constitution.agent.md +84 -0
  5. package/.github/agents/speckit.implement.agent.md +198 -0
  6. package/.github/agents/speckit.plan.agent.md +90 -0
  7. package/.github/agents/speckit.specify.agent.md +237 -0
  8. package/.github/agents/speckit.tasks.agent.md +200 -0
  9. package/.github/agents/speckit.taskstoissues.agent.md +30 -0
  10. package/.github/prompts/speckit.analyze.prompt.md +3 -0
  11. package/.github/prompts/speckit.checklist.prompt.md +3 -0
  12. package/.github/prompts/speckit.clarify.prompt.md +3 -0
  13. package/.github/prompts/speckit.constitution.prompt.md +3 -0
  14. package/.github/prompts/speckit.implement.prompt.md +3 -0
  15. package/.github/prompts/speckit.plan.prompt.md +3 -0
  16. package/.github/prompts/speckit.specify.prompt.md +3 -0
  17. package/.github/prompts/speckit.tasks.prompt.md +3 -0
  18. package/.github/prompts/speckit.taskstoissues.prompt.md +3 -0
  19. package/.specify/memory/constitution.md +119 -0
  20. package/.specify/scripts/bash/check-prerequisites.sh +190 -0
  21. package/.specify/scripts/bash/common.sh +253 -0
  22. package/.specify/scripts/bash/create-new-feature.sh +333 -0
  23. package/.specify/scripts/bash/setup-plan.sh +73 -0
  24. package/.specify/scripts/bash/update-agent-context.sh +808 -0
  25. package/.specify/templates/agent-file-template.md +28 -0
  26. package/.specify/templates/checklist-template.md +40 -0
  27. package/.specify/templates/constitution-template.md +50 -0
  28. package/.specify/templates/plan-template.md +110 -0
  29. package/.specify/templates/spec-template.md +115 -0
  30. package/.specify/templates/tasks-template.md +251 -0
  31. package/.vscode/settings.json +14 -0
  32. package/.xdrs/agentme/edrs/application/003-javascript-project-tooling.md +89 -0
  33. package/.xdrs/agentme/edrs/application/010-golang-project-tooling.md +141 -0
  34. package/.xdrs/agentme/edrs/application/skills/001-create-javascript-project/SKILL.md +360 -0
  35. package/.xdrs/agentme/edrs/application/skills/003-create-golang-project/SKILL.md +311 -0
  36. package/.xdrs/agentme/edrs/devops/005-monorepo-structure.md +104 -0
  37. package/.xdrs/agentme/edrs/devops/006-github-pipelines.md +170 -0
  38. package/.xdrs/agentme/edrs/devops/008-common-targets.md +207 -0
  39. package/.xdrs/agentme/edrs/devops/skills/002-monorepo-setup/SKILL.md +270 -0
  40. package/.xdrs/agentme/edrs/index.md +41 -0
  41. package/.xdrs/agentme/edrs/observability/011-service-health-check-endpoint.md +78 -0
  42. package/.xdrs/agentme/edrs/principles/002-coding-best-practices.md +110 -0
  43. package/.xdrs/agentme/edrs/principles/004-unit-test-requirements.md +97 -0
  44. package/.xdrs/agentme/edrs/principles/007-project-quality-standards.md +156 -0
  45. package/.xdrs/agentme/edrs/principles/009-error-handling.md +327 -0
  46. package/.xdrs/index.md +32 -0
  47. package/README.md +119 -0
  48. package/bin/npmdata.js +3 -0
  49. package/package.json +102 -0
@@ -0,0 +1,253 @@
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
+ return 1
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
138
+ if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
139
+ echo "ERROR: Failed to resolve feature directory" >&2
140
+ return 1
141
+ fi
142
+
143
+ # Use printf '%q' to safely quote values, preventing shell injection
144
+ # via crafted branch names or paths containing special characters
145
+ printf 'REPO_ROOT=%q\n' "$repo_root"
146
+ printf 'CURRENT_BRANCH=%q\n' "$current_branch"
147
+ printf 'HAS_GIT=%q\n' "$has_git_repo"
148
+ printf 'FEATURE_DIR=%q\n' "$feature_dir"
149
+ printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
150
+ printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
151
+ printf 'TASKS=%q\n' "$feature_dir/tasks.md"
152
+ printf 'RESEARCH=%q\n' "$feature_dir/research.md"
153
+ printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
154
+ printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
155
+ printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
156
+ }
157
+
158
+ # Check if jq is available for safe JSON construction
159
+ has_jq() {
160
+ command -v jq >/dev/null 2>&1
161
+ }
162
+
163
+ # Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
164
+ # Handles backslash, double-quote, and control characters (newline, tab, carriage return).
165
+ json_escape() {
166
+ local s="$1"
167
+ s="${s//\\/\\\\}"
168
+ s="${s//\"/\\\"}"
169
+ s="${s//$'\n'/\\n}"
170
+ s="${s//$'\t'/\\t}"
171
+ s="${s//$'\r'/\\r}"
172
+ printf '%s' "$s"
173
+ }
174
+
175
+ check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
176
+ check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
177
+
178
+ # Resolve a template name to a file path using the priority stack:
179
+ # 1. .specify/templates/overrides/
180
+ # 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
181
+ # 3. .specify/extensions/<ext-id>/templates/
182
+ # 4. .specify/templates/ (core)
183
+ resolve_template() {
184
+ local template_name="$1"
185
+ local repo_root="$2"
186
+ local base="$repo_root/.specify/templates"
187
+
188
+ # Priority 1: Project overrides
189
+ local override="$base/overrides/${template_name}.md"
190
+ [ -f "$override" ] && echo "$override" && return 0
191
+
192
+ # Priority 2: Installed presets (sorted by priority from .registry)
193
+ local presets_dir="$repo_root/.specify/presets"
194
+ if [ -d "$presets_dir" ]; then
195
+ local registry_file="$presets_dir/.registry"
196
+ if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
197
+ # Read preset IDs sorted by priority (lower number = higher precedence)
198
+ local sorted_presets
199
+ sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
200
+ import json, sys, os
201
+ try:
202
+ with open(os.environ['SPECKIT_REGISTRY']) as f:
203
+ data = json.load(f)
204
+ presets = data.get('presets', {})
205
+ for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
206
+ print(pid)
207
+ except Exception:
208
+ sys.exit(1)
209
+ " 2>/dev/null)
210
+ if [ $? -eq 0 ] && [ -n "$sorted_presets" ]; then
211
+ while IFS= read -r preset_id; do
212
+ local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
213
+ [ -f "$candidate" ] && echo "$candidate" && return 0
214
+ done <<< "$sorted_presets"
215
+ else
216
+ # python3 returned empty list — fall through to directory scan
217
+ for preset in "$presets_dir"/*/; do
218
+ [ -d "$preset" ] || continue
219
+ local candidate="$preset/templates/${template_name}.md"
220
+ [ -f "$candidate" ] && echo "$candidate" && return 0
221
+ done
222
+ fi
223
+ else
224
+ # Fallback: alphabetical directory order (no python3 available)
225
+ for preset in "$presets_dir"/*/; do
226
+ [ -d "$preset" ] || continue
227
+ local candidate="$preset/templates/${template_name}.md"
228
+ [ -f "$candidate" ] && echo "$candidate" && return 0
229
+ done
230
+ fi
231
+ fi
232
+
233
+ # Priority 3: Extension-provided templates
234
+ local ext_dir="$repo_root/.specify/extensions"
235
+ if [ -d "$ext_dir" ]; then
236
+ for ext in "$ext_dir"/*/; do
237
+ [ -d "$ext" ] || continue
238
+ # Skip hidden directories (e.g. .backup, .cache)
239
+ case "$(basename "$ext")" in .*) continue;; esac
240
+ local candidate="$ext/templates/${template_name}.md"
241
+ [ -f "$candidate" ] && echo "$candidate" && return 0
242
+ done
243
+ fi
244
+
245
+ # Priority 4: Core templates
246
+ local core="$base/${template_name}.md"
247
+ [ -f "$core" ] && echo "$core" && return 0
248
+
249
+ # Return success with empty output so callers using set -e don't abort;
250
+ # callers check [ -n "$TEMPLATE" ] to detect "not found".
251
+ return 0
252
+ }
253
+
@@ -0,0 +1,333 @@
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
+ # Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
71
+ FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
72
+ if [ -z "$FEATURE_DESCRIPTION" ]; then
73
+ echo "Error: Feature description cannot be empty or contain only whitespace" >&2
74
+ exit 1
75
+ fi
76
+
77
+ # Function to find the repository root by searching for existing project markers
78
+ find_repo_root() {
79
+ local dir="$1"
80
+ while [ "$dir" != "/" ]; do
81
+ if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
82
+ echo "$dir"
83
+ return 0
84
+ fi
85
+ dir="$(dirname "$dir")"
86
+ done
87
+ return 1
88
+ }
89
+
90
+ # Function to get highest number from specs directory
91
+ get_highest_from_specs() {
92
+ local specs_dir="$1"
93
+ local highest=0
94
+
95
+ if [ -d "$specs_dir" ]; then
96
+ for dir in "$specs_dir"/*; do
97
+ [ -d "$dir" ] || continue
98
+ dirname=$(basename "$dir")
99
+ number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
100
+ number=$((10#$number))
101
+ if [ "$number" -gt "$highest" ]; then
102
+ highest=$number
103
+ fi
104
+ done
105
+ fi
106
+
107
+ echo "$highest"
108
+ }
109
+
110
+ # Function to get highest number from git branches
111
+ get_highest_from_branches() {
112
+ local highest=0
113
+
114
+ # Get all branches (local and remote)
115
+ branches=$(git branch -a 2>/dev/null || echo "")
116
+
117
+ if [ -n "$branches" ]; then
118
+ while IFS= read -r branch; do
119
+ # Clean branch name: remove leading markers and remote prefixes
120
+ clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
121
+
122
+ # Extract feature number if branch matches pattern ###-*
123
+ if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
124
+ number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
125
+ number=$((10#$number))
126
+ if [ "$number" -gt "$highest" ]; then
127
+ highest=$number
128
+ fi
129
+ fi
130
+ done <<< "$branches"
131
+ fi
132
+
133
+ echo "$highest"
134
+ }
135
+
136
+ # Function to check existing branches (local and remote) and return next available number
137
+ check_existing_branches() {
138
+ local specs_dir="$1"
139
+
140
+ # Fetch all remotes to get latest branch info (suppress errors if no remotes)
141
+ git fetch --all --prune 2>/dev/null || true
142
+
143
+ # Get highest number from ALL branches (not just matching short name)
144
+ local highest_branch=$(get_highest_from_branches)
145
+
146
+ # Get highest number from ALL specs (not just matching short name)
147
+ local highest_spec=$(get_highest_from_specs "$specs_dir")
148
+
149
+ # Take the maximum of both
150
+ local max_num=$highest_branch
151
+ if [ "$highest_spec" -gt "$max_num" ]; then
152
+ max_num=$highest_spec
153
+ fi
154
+
155
+ # Return next number
156
+ echo $((max_num + 1))
157
+ }
158
+
159
+ # Function to clean and format a branch name
160
+ clean_branch_name() {
161
+ local name="$1"
162
+ echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
163
+ }
164
+
165
+ # Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
166
+ json_escape() {
167
+ local s="$1"
168
+ s="${s//\\/\\\\}"
169
+ s="${s//\"/\\\"}"
170
+ s="${s//$'\n'/\\n}"
171
+ s="${s//$'\t'/\\t}"
172
+ s="${s//$'\r'/\\r}"
173
+ printf '%s' "$s"
174
+ }
175
+
176
+ # Resolve repository root. Prefer git information when available, but fall back
177
+ # to searching for repository markers so the workflow still functions in repositories that
178
+ # were initialised with --no-git.
179
+ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
180
+ source "$SCRIPT_DIR/common.sh"
181
+
182
+ if git rev-parse --show-toplevel >/dev/null 2>&1; then
183
+ REPO_ROOT=$(git rev-parse --show-toplevel)
184
+ HAS_GIT=true
185
+ else
186
+ REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
187
+ if [ -z "$REPO_ROOT" ]; then
188
+ echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
189
+ exit 1
190
+ fi
191
+ HAS_GIT=false
192
+ fi
193
+
194
+ cd "$REPO_ROOT"
195
+
196
+ SPECS_DIR="$REPO_ROOT/specs"
197
+ mkdir -p "$SPECS_DIR"
198
+
199
+ # Function to generate branch name with stop word filtering and length filtering
200
+ generate_branch_name() {
201
+ local description="$1"
202
+
203
+ # Common stop words to filter out
204
+ 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)$"
205
+
206
+ # Convert to lowercase and split into words
207
+ local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
208
+
209
+ # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
210
+ local meaningful_words=()
211
+ for word in $clean_name; do
212
+ # Skip empty words
213
+ [ -z "$word" ] && continue
214
+
215
+ # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
216
+ if ! echo "$word" | grep -qiE "$stop_words"; then
217
+ if [ ${#word} -ge 3 ]; then
218
+ meaningful_words+=("$word")
219
+ elif echo "$description" | grep -q "\b${word^^}\b"; then
220
+ # Keep short words if they appear as uppercase in original (likely acronyms)
221
+ meaningful_words+=("$word")
222
+ fi
223
+ fi
224
+ done
225
+
226
+ # If we have meaningful words, use first 3-4 of them
227
+ if [ ${#meaningful_words[@]} -gt 0 ]; then
228
+ local max_words=3
229
+ if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
230
+
231
+ local result=""
232
+ local count=0
233
+ for word in "${meaningful_words[@]}"; do
234
+ if [ $count -ge $max_words ]; then break; fi
235
+ if [ -n "$result" ]; then result="$result-"; fi
236
+ result="$result$word"
237
+ count=$((count + 1))
238
+ done
239
+ echo "$result"
240
+ else
241
+ # Fallback to original logic if no meaningful words found
242
+ local cleaned=$(clean_branch_name "$description")
243
+ echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
244
+ fi
245
+ }
246
+
247
+ # Generate branch name
248
+ if [ -n "$SHORT_NAME" ]; then
249
+ # Use provided short name, just clean it up
250
+ BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
251
+ else
252
+ # Generate from description with smart filtering
253
+ BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
254
+ fi
255
+
256
+ # Determine branch number
257
+ if [ -z "$BRANCH_NUMBER" ]; then
258
+ if [ "$HAS_GIT" = true ]; then
259
+ # Check existing branches on remotes
260
+ BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
261
+ else
262
+ # Fall back to local directory check
263
+ HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
264
+ BRANCH_NUMBER=$((HIGHEST + 1))
265
+ fi
266
+ fi
267
+
268
+ # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
269
+ FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
270
+ BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
271
+
272
+ # GitHub enforces a 244-byte limit on branch names
273
+ # Validate and truncate if necessary
274
+ MAX_BRANCH_LENGTH=244
275
+ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
276
+ # Calculate how much we need to trim from suffix
277
+ # Account for: feature number (3) + hyphen (1) = 4 chars
278
+ MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
279
+
280
+ # Truncate suffix at word boundary if possible
281
+ TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
282
+ # Remove trailing hyphen if truncation created one
283
+ TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
284
+
285
+ ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
286
+ BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
287
+
288
+ >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
289
+ >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
290
+ >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
291
+ fi
292
+
293
+ if [ "$HAS_GIT" = true ]; then
294
+ if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
295
+ # Check if branch already exists
296
+ if git branch --list "$BRANCH_NAME" | grep -q .; then
297
+ >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
298
+ exit 1
299
+ else
300
+ >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
301
+ exit 1
302
+ fi
303
+ fi
304
+ else
305
+ >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
306
+ fi
307
+
308
+ FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
309
+ mkdir -p "$FEATURE_DIR"
310
+
311
+ TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT")
312
+ SPEC_FILE="$FEATURE_DIR/spec.md"
313
+ if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
314
+
315
+ # Inform the user how to persist the feature variable in their own shell
316
+ printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
317
+
318
+ if $JSON_MODE; then
319
+ if command -v jq >/dev/null 2>&1; then
320
+ jq -cn \
321
+ --arg branch_name "$BRANCH_NAME" \
322
+ --arg spec_file "$SPEC_FILE" \
323
+ --arg feature_num "$FEATURE_NUM" \
324
+ '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
325
+ else
326
+ printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
327
+ fi
328
+ else
329
+ echo "BRANCH_NAME: $BRANCH_NAME"
330
+ echo "SPEC_FILE: $SPEC_FILE"
331
+ echo "FEATURE_NUM: $FEATURE_NUM"
332
+ printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
333
+ fi
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ # Parse command line arguments
6
+ JSON_MODE=false
7
+ ARGS=()
8
+
9
+ for arg in "$@"; do
10
+ case "$arg" in
11
+ --json)
12
+ JSON_MODE=true
13
+ ;;
14
+ --help|-h)
15
+ echo "Usage: $0 [--json]"
16
+ echo " --json Output results in JSON format"
17
+ echo " --help Show this help message"
18
+ exit 0
19
+ ;;
20
+ *)
21
+ ARGS+=("$arg")
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
+ # Check if we're on a proper feature branch (only for git repos)
36
+ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
37
+
38
+ # Ensure the feature directory exists
39
+ mkdir -p "$FEATURE_DIR"
40
+
41
+ # Copy plan template if it exists
42
+ TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT")
43
+ if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
44
+ cp "$TEMPLATE" "$IMPL_PLAN"
45
+ echo "Copied plan template to $IMPL_PLAN"
46
+ else
47
+ echo "Warning: Plan template not found"
48
+ # Create a basic plan file if template doesn't exist
49
+ touch "$IMPL_PLAN"
50
+ fi
51
+
52
+ # Output results
53
+ if $JSON_MODE; then
54
+ if has_jq; then
55
+ jq -cn \
56
+ --arg feature_spec "$FEATURE_SPEC" \
57
+ --arg impl_plan "$IMPL_PLAN" \
58
+ --arg specs_dir "$FEATURE_DIR" \
59
+ --arg branch "$CURRENT_BRANCH" \
60
+ --arg has_git "$HAS_GIT" \
61
+ '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
62
+ else
63
+ printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
64
+ "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
65
+ fi
66
+ else
67
+ echo "FEATURE_SPEC: $FEATURE_SPEC"
68
+ echo "IMPL_PLAN: $IMPL_PLAN"
69
+ echo "SPECS_DIR: $FEATURE_DIR"
70
+ echo "BRANCH: $CURRENT_BRANCH"
71
+ echo "HAS_GIT: $HAS_GIT"
72
+ fi
73
+