tmux-agent 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.
Files changed (161) hide show
  1. package/.codex/skills/speckit/SKILL.md +173 -0
  2. package/.codex/skills/speckit/assets/templates/checklist-template.md +49 -0
  3. package/.codex/skills/speckit/assets/templates/notes-entrypoints-template.md +11 -0
  4. package/.codex/skills/speckit/assets/templates/notes-questions-template.md +7 -0
  5. package/.codex/skills/speckit/assets/templates/notes-readme-template.md +36 -0
  6. package/.codex/skills/speckit/assets/templates/notes-session-template.md +21 -0
  7. package/.codex/skills/speckit/assets/templates/plan-template.md +126 -0
  8. package/.codex/skills/speckit/assets/templates/spec-template.md +135 -0
  9. package/.codex/skills/speckit/assets/templates/tasks-template.md +269 -0
  10. package/.codex/skills/speckit/references/acceptance.md +183 -0
  11. package/.codex/skills/speckit/references/analyze.md +186 -0
  12. package/.codex/skills/speckit/references/checklist.md +302 -0
  13. package/.codex/skills/speckit/references/clarify-auto.md +69 -0
  14. package/.codex/skills/speckit/references/clarify-detailed.md +78 -0
  15. package/.codex/skills/speckit/references/clarify.md +189 -0
  16. package/.codex/skills/speckit/references/constitution.md +90 -0
  17. package/.codex/skills/speckit/references/group.md +89 -0
  18. package/.codex/skills/speckit/references/implement-task.md +115 -0
  19. package/.codex/skills/speckit/references/implement.md +129 -0
  20. package/.codex/skills/speckit/references/notes.md +82 -0
  21. package/.codex/skills/speckit/references/plan-deep.md +87 -0
  22. package/.codex/skills/speckit/references/plan-from-questions.md +115 -0
  23. package/.codex/skills/speckit/references/plan-from-review.md +89 -0
  24. package/.codex/skills/speckit/references/plan.md +97 -0
  25. package/.codex/skills/speckit/references/review-plan.md +156 -0
  26. package/.codex/skills/speckit/references/specify.md +246 -0
  27. package/.codex/skills/speckit/references/tasks.md +155 -0
  28. package/.codex/skills/speckit/references/taskstoissues.md +33 -0
  29. package/.codex/skills/speckit/scripts/bash/check-prerequisites.sh +206 -0
  30. package/.codex/skills/speckit/scripts/bash/common.sh +191 -0
  31. package/.codex/skills/speckit/scripts/bash/create-new-feature.sh +259 -0
  32. package/.codex/skills/speckit/scripts/bash/extract-coded-points.sh +322 -0
  33. package/.codex/skills/speckit/scripts/bash/extract-spec-ids.sh +238 -0
  34. package/.codex/skills/speckit/scripts/bash/extract-tasks.sh +295 -0
  35. package/.codex/skills/speckit/scripts/bash/extract-user-stories.sh +312 -0
  36. package/.codex/skills/speckit/scripts/bash/setup-notes.sh +182 -0
  37. package/.codex/skills/speckit/scripts/bash/setup-plan.sh +110 -0
  38. package/.codex/skills/speckit/scripts/bash/show-todo-tasks.sh +257 -0
  39. package/.codex/skills/speckit/scripts/bash/spec-group-checklist.sh +402 -0
  40. package/.codex/skills/speckit/scripts/bash/spec-group-members.sh +215 -0
  41. package/.codex/skills/speckit/scripts/bash/spec-registry-graph.sh +399 -0
  42. package/.specify/memory/constitution.md +67 -0
  43. package/.specify/templates/agent-file-template.md +28 -0
  44. package/.specify/templates/checklist-template.md +49 -0
  45. package/.specify/templates/plan-template.md +126 -0
  46. package/.specify/templates/spec-template.md +135 -0
  47. package/.specify/templates/tasks-template.md +269 -0
  48. package/README.md +128 -0
  49. package/README.zh-CN.md +127 -0
  50. package/bun.lock +269 -0
  51. package/dist/cli/commands/codex/forkHome.js +88 -0
  52. package/dist/cli/commands/codex/send.js +55 -0
  53. package/dist/cli/commands/codex/sessionInfo.js +42 -0
  54. package/dist/cli/commands/codex/spawn.js +68 -0
  55. package/dist/cli/commands/find.js +26 -0
  56. package/dist/cli/commands/paneKill.js +33 -0
  57. package/dist/cli/commands/paneSpawn.js +40 -0
  58. package/dist/cli/commands/paneTitle.js +33 -0
  59. package/dist/cli/commands/read.js +34 -0
  60. package/dist/cli/commands/send.js +51 -0
  61. package/dist/cli/commands/snapshot.js +19 -0
  62. package/dist/cli/commands/ui/select.js +41 -0
  63. package/dist/cli/commands/windowKill.js +25 -0
  64. package/dist/cli/commands/windowLs.js +15 -0
  65. package/dist/cli/commands/windowNew.js +28 -0
  66. package/dist/cli/commands/windowRename.js +25 -0
  67. package/dist/cli/index.js +365 -0
  68. package/dist/cli/parse.js +39 -0
  69. package/dist/lib/codex/forkHome.js +101 -0
  70. package/dist/lib/codex/isCodexPane.js +55 -0
  71. package/dist/lib/codex/send.js +58 -0
  72. package/dist/lib/codex/sessionInfo.js +449 -0
  73. package/dist/lib/codex/spawn.js +246 -0
  74. package/dist/lib/contracts/types.js +2 -0
  75. package/dist/lib/fs/safeRm.js +32 -0
  76. package/dist/lib/io/readStdin.js +14 -0
  77. package/dist/lib/os/process.js +55 -0
  78. package/dist/lib/output/format.js +95 -0
  79. package/dist/lib/proc/lsof.js +42 -0
  80. package/dist/lib/proc/ps.js +60 -0
  81. package/dist/lib/targeting/errors.js +13 -0
  82. package/dist/lib/targeting/resolvePaneTarget.js +91 -0
  83. package/dist/lib/targeting/resolveWindowTarget.js +40 -0
  84. package/dist/lib/targeting/scope.js +58 -0
  85. package/dist/lib/tmux/capturePane.js +20 -0
  86. package/dist/lib/tmux/exec.js +66 -0
  87. package/dist/lib/tmux/paneOps.js +29 -0
  88. package/dist/lib/tmux/paste.js +23 -0
  89. package/dist/lib/tmux/sendKeys.js +47 -0
  90. package/dist/lib/tmux/session.js +29 -0
  91. package/dist/lib/tmux/snapshotPanes.js +46 -0
  92. package/dist/lib/tmux/snapshotWindows.js +24 -0
  93. package/dist/lib/tmux/windowOps.js +32 -0
  94. package/dist/lib/ui/popupSelect.js +432 -0
  95. package/dist/lib/ui/popupSupport.js +76 -0
  96. package/package.json +23 -0
  97. package/src/cli/commands/codex/forkHome.ts +141 -0
  98. package/src/cli/commands/codex/send.ts +83 -0
  99. package/src/cli/commands/codex/sessionInfo.ts +59 -0
  100. package/src/cli/commands/codex/spawn.ts +90 -0
  101. package/src/cli/commands/find.ts +40 -0
  102. package/src/cli/commands/paneKill.ts +49 -0
  103. package/src/cli/commands/paneSpawn.ts +53 -0
  104. package/src/cli/commands/paneTitle.ts +50 -0
  105. package/src/cli/commands/read.ts +48 -0
  106. package/src/cli/commands/send.ts +71 -0
  107. package/src/cli/commands/snapshot.ts +28 -0
  108. package/src/cli/commands/ui/select.ts +49 -0
  109. package/src/cli/commands/windowKill.ts +35 -0
  110. package/src/cli/commands/windowLs.ts +20 -0
  111. package/src/cli/commands/windowNew.ts +40 -0
  112. package/src/cli/commands/windowRename.ts +36 -0
  113. package/src/cli/index.ts +430 -0
  114. package/src/lib/codex/forkHome.ts +148 -0
  115. package/src/lib/codex/isCodexPane.ts +56 -0
  116. package/src/lib/codex/send.ts +84 -0
  117. package/src/lib/codex/sessionInfo.ts +521 -0
  118. package/src/lib/codex/spawn.ts +305 -0
  119. package/src/lib/contracts/types.ts +30 -0
  120. package/src/lib/fs/safeRm.ts +32 -0
  121. package/src/lib/io/readStdin.ts +11 -0
  122. package/src/lib/output/format.ts +105 -0
  123. package/src/lib/proc/lsof.ts +44 -0
  124. package/src/lib/proc/ps.ts +70 -0
  125. package/src/lib/targeting/errors.ts +25 -0
  126. package/src/lib/targeting/resolvePaneTarget.ts +106 -0
  127. package/src/lib/targeting/resolveWindowTarget.ts +45 -0
  128. package/src/lib/targeting/scope.ts +76 -0
  129. package/src/lib/tmux/capturePane.ts +21 -0
  130. package/src/lib/tmux/exec.ts +90 -0
  131. package/src/lib/tmux/paneOps.ts +35 -0
  132. package/src/lib/tmux/paste.ts +20 -0
  133. package/src/lib/tmux/sendKeys.ts +72 -0
  134. package/src/lib/tmux/session.ts +27 -0
  135. package/src/lib/tmux/snapshotPanes.ts +52 -0
  136. package/src/lib/tmux/snapshotWindows.ts +23 -0
  137. package/src/lib/tmux/windowOps.ts +43 -0
  138. package/src/lib/ui/popupSelect.ts +561 -0
  139. package/src/lib/ui/popupSupport.ts +84 -0
  140. package/tests/e2e/codexForkHome.test.ts +146 -0
  141. package/tests/e2e/codexSessionInfo.test.ts +112 -0
  142. package/tests/e2e/codexTuiSend.test.ts +68 -0
  143. package/tests/integration/codexSpawn.test.ts +113 -0
  144. package/tests/integration/paneOps.test.ts +60 -0
  145. package/tests/integration/sendRead.test.ts +52 -0
  146. package/tests/integration/snapshot.test.ts +39 -0
  147. package/tests/integration/tmuxHarness.ts +39 -0
  148. package/tests/integration/windowOps.test.ts +60 -0
  149. package/tests/unit/codexSend.test.ts +105 -0
  150. package/tests/unit/codexSessionInfo.test.ts +88 -0
  151. package/tests/unit/codexSpawn.test.ts +34 -0
  152. package/tests/unit/keys.test.ts +30 -0
  153. package/tests/unit/outputFormat.test.ts +52 -0
  154. package/tests/unit/popupSelect.test.ts +77 -0
  155. package/tests/unit/popupSupport.test.ts +109 -0
  156. package/tests/unit/resolvePaneTarget.test.ts +43 -0
  157. package/tests/unit/resolveWindowTarget.test.ts +36 -0
  158. package/tests/unit/safeRm.test.ts +41 -0
  159. package/tests/unit/scope.test.ts +57 -0
  160. package/tsconfig.json +14 -0
  161. package/vitest.config.ts +16 -0
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env bash
2
+ # Common functions and variables for all scripts
3
+
4
+ find_repo_root_by_marker() {
5
+ local dir="$1"
6
+ while [[ -n "$dir" && "$dir" != "/" ]]; do
7
+ if [[ -d "$dir/.specify" || -d "$dir/specs" ]]; then
8
+ echo "$dir"
9
+ return 0
10
+ fi
11
+ dir="$(dirname "$dir")"
12
+ done
13
+ return 1
14
+ }
15
+
16
+ get_repo_root() {
17
+ local repo_root=""
18
+
19
+ if repo_root="$(find_repo_root_by_marker "${PWD:-}")"; then
20
+ echo "$repo_root"
21
+ return 0
22
+ fi
23
+
24
+ local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ if repo_root="$(find_repo_root_by_marker "$script_dir")"; then
26
+ echo "$repo_root"
27
+ return 0
28
+ fi
29
+
30
+ # Fallback: if we're inside a Git repo, use the repo root.
31
+ if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then
32
+ git rev-parse --show-toplevel
33
+ return 0
34
+ fi
35
+
36
+ # Final fallback: use the current working directory so we never default to "/specs".
37
+ local pwd="${PWD:-}"
38
+ if [[ -n "$pwd" ]]; then
39
+ echo "$pwd"
40
+ return 0
41
+ fi
42
+
43
+ pwd
44
+ }
45
+
46
+ # Get current feature id (historical name: branch), without any VCS dependence
47
+ get_current_branch() {
48
+ # First check if SPECIFY_FEATURE environment variable is set
49
+ if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
50
+ # Allow shorthand: if user sets SPECIFY_FEATURE to a bare numeric prefix (e.g., "026"),
51
+ # resolve it to the unique specs/<NNN-*> directory if possible.
52
+ if [[ "${SPECIFY_FEATURE}" =~ ^[0-9]{3}$ ]]; then
53
+ local repo_root=""
54
+ repo_root="$(get_repo_root 2>/dev/null || true)"
55
+ local specs_dir="$repo_root/specs"
56
+ if [[ -d "$specs_dir" ]]; then
57
+ local matches=()
58
+ for dir in "$specs_dir"/"${SPECIFY_FEATURE}"-*; do
59
+ if [[ -d "$dir" ]]; then
60
+ matches+=("$(basename "$dir")")
61
+ fi
62
+ done
63
+ if [[ ${#matches[@]} -eq 1 ]]; then
64
+ echo "${matches[0]}"
65
+ return
66
+ fi
67
+ fi
68
+ fi
69
+
70
+ echo "$SPECIFY_FEATURE"
71
+ return
72
+ fi
73
+
74
+ # Then infer from current working directory if we're inside specs/<NNN-*>
75
+ if [[ -n "${PWD:-}" ]] && [[ "${PWD:-}" =~ /specs/([0-9]{3}-[^/]+)(/|$) ]]; then
76
+ echo "${BASH_REMATCH[1]}"
77
+ return
78
+ fi
79
+
80
+ # Finally, fall back to the latest feature directory by numeric prefix
81
+ local repo_root=""
82
+ repo_root="$(get_repo_root 2>/dev/null || true)"
83
+ local specs_dir="$repo_root/specs"
84
+
85
+ if [[ -d "$specs_dir" ]]; then
86
+ local latest_feature=""
87
+ local highest=0
88
+
89
+ for dir in "$specs_dir"/*; do
90
+ if [[ -d "$dir" ]]; then
91
+ local dirname=$(basename "$dir")
92
+ if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
93
+ local number=${BASH_REMATCH[1]}
94
+ number=$((10#$number))
95
+ if [[ "$number" -gt "$highest" ]]; then
96
+ highest=$number
97
+ latest_feature=$dirname
98
+ fi
99
+ fi
100
+ fi
101
+ done
102
+
103
+ if [[ -n "$latest_feature" ]]; then
104
+ echo "$latest_feature"
105
+ return
106
+ fi
107
+ fi
108
+
109
+ echo ""
110
+ }
111
+
112
+ check_feature_branch() {
113
+ local feature_id="$1"
114
+
115
+ if [[ -z "$feature_id" || ! "$feature_id" =~ ^[0-9]{3}- ]]; then
116
+ echo "ERROR: Unable to determine current feature id (expected: 001-feature-name)" >&2
117
+ echo "Hint: set SPECIFY_FEATURE=\"001-feature-name\" (or \"001\" if unique), or pass --feature 001/001-feature-name." >&2
118
+ return 1
119
+ fi
120
+
121
+ return 0
122
+ }
123
+
124
+ get_feature_dir() { echo "$1/specs/$2"; }
125
+
126
+ # Find feature directory by numeric prefix instead of exact branch match
127
+ # This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
128
+ find_feature_dir_by_prefix() {
129
+ local repo_root="$1"
130
+ local branch_name="$2"
131
+ local specs_dir="$repo_root/specs"
132
+
133
+ # Extract numeric prefix from branch (e.g., "004" from "004-whatever")
134
+ if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
135
+ # If branch doesn't have numeric prefix, fall back to exact match
136
+ echo "$specs_dir/$branch_name"
137
+ return
138
+ fi
139
+
140
+ local prefix="${BASH_REMATCH[1]}"
141
+
142
+ # Search for directories in specs/ that start with this prefix
143
+ local matches=()
144
+ if [[ -d "$specs_dir" ]]; then
145
+ for dir in "$specs_dir"/"$prefix"-*; do
146
+ if [[ -d "$dir" ]]; then
147
+ matches+=("$(basename "$dir")")
148
+ fi
149
+ done
150
+ fi
151
+
152
+ # Handle results
153
+ if [[ ${#matches[@]} -eq 0 ]]; then
154
+ # No match found - return the branch name path (will fail later with clear error)
155
+ echo "$specs_dir/$branch_name"
156
+ elif [[ ${#matches[@]} -eq 1 ]]; then
157
+ # Exactly one match - perfect!
158
+ echo "$specs_dir/${matches[0]}"
159
+ else
160
+ # Multiple matches - this shouldn't happen with proper naming convention
161
+ echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
162
+ echo "Please ensure only one spec directory exists per numeric prefix." >&2
163
+ echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
164
+ fi
165
+ }
166
+
167
+ get_feature_paths() {
168
+ local repo_root
169
+ repo_root="$(get_repo_root)"
170
+ local current_branch
171
+ current_branch="$(get_current_branch)"
172
+
173
+ # Use prefix-based lookup to support multiple branches per spec
174
+ local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
175
+
176
+ cat <<EOF
177
+ REPO_ROOT='$repo_root'
178
+ CURRENT_BRANCH='$current_branch'
179
+ FEATURE_DIR='$feature_dir'
180
+ FEATURE_SPEC='$feature_dir/spec.md'
181
+ IMPL_PLAN='$feature_dir/plan.md'
182
+ TASKS='$feature_dir/tasks.md'
183
+ RESEARCH='$feature_dir/research.md'
184
+ DATA_MODEL='$feature_dir/data-model.md'
185
+ QUICKSTART='$feature_dir/quickstart.md'
186
+ CONTRACTS_DIR='$feature_dir/contracts'
187
+ EOF
188
+ }
189
+
190
+ check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
191
+ check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
@@ -0,0 +1,259 @@
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 start_dir="$1"
73
+ local dir="$start_dir"
74
+ while [ "$dir" != "/" ]; do
75
+ if [ -d "$dir/.specify" ] || [ -d "$dir/specs" ]; then
76
+ echo "$dir"
77
+ return 0
78
+ fi
79
+ dir="$(dirname "$dir")"
80
+ done
81
+
82
+ # Fallback: if we're in a git repo, use the repo root.
83
+ if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then
84
+ git rev-parse --show-toplevel
85
+ return 0
86
+ fi
87
+
88
+ # Final fallback: use the provided directory so we never default to "/specs".
89
+ if [ -n "$start_dir" ] && [ "$start_dir" != "/" ]; then
90
+ echo "$start_dir"
91
+ return 0
92
+ fi
93
+
94
+ return 1
95
+ }
96
+
97
+ # Function to get highest number from specs directory
98
+ get_highest_from_specs() {
99
+ local specs_dir="$1"
100
+ local highest=0
101
+
102
+ if [ -d "$specs_dir" ]; then
103
+ for dir in "$specs_dir"/*; do
104
+ [ -d "$dir" ] || continue
105
+ dirname=$(basename "$dir")
106
+ number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
107
+ number=$((10#$number))
108
+ if [ "$number" -gt "$highest" ]; then
109
+ highest=$number
110
+ fi
111
+ done
112
+ fi
113
+
114
+ echo "$highest"
115
+ }
116
+
117
+ # Function to clean and format a branch name
118
+ clean_branch_name() {
119
+ local name="$1"
120
+ echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
121
+ }
122
+
123
+ # Resolve repository root by searching for the `.specify` marker.
124
+ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
125
+ SKILL_DIR="$(CDPATH="" cd "$SCRIPT_DIR/../.." && pwd)"
126
+
127
+ REPO_ROOT="$(find_repo_root "${PWD:-$SCRIPT_DIR}")"
128
+ if [ -z "$REPO_ROOT" ]; then
129
+ REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
130
+ if [ -z "$REPO_ROOT" ]; then
131
+ echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
132
+ exit 1
133
+ fi
134
+ fi
135
+
136
+ cd "$REPO_ROOT"
137
+
138
+ SPECS_DIR="$REPO_ROOT/specs"
139
+ mkdir -p "$SPECS_DIR"
140
+
141
+ # Function to generate branch name with stop word filtering and length filtering
142
+ generate_branch_name() {
143
+ local description="$1"
144
+
145
+ # Common stop words to filter out
146
+ 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)$"
147
+
148
+ # Convert to lowercase and split into words
149
+ local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
150
+
151
+ # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
152
+ local meaningful_words=()
153
+ for word in $clean_name; do
154
+ # Skip empty words
155
+ [ -z "$word" ] && continue
156
+
157
+ # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
158
+ if ! echo "$word" | grep -qiE "$stop_words"; then
159
+ if [ ${#word} -ge 3 ]; then
160
+ meaningful_words+=("$word")
161
+ elif echo "$description" | grep -q "\b${word^^}\b"; then
162
+ # Keep short words if they appear as uppercase in original (likely acronyms)
163
+ meaningful_words+=("$word")
164
+ fi
165
+ fi
166
+ done
167
+
168
+ # If we have meaningful words, use first 3-4 of them
169
+ if [ ${#meaningful_words[@]} -gt 0 ]; then
170
+ local max_words=3
171
+ if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
172
+
173
+ local result=""
174
+ local count=0
175
+ for word in "${meaningful_words[@]}"; do
176
+ if [ $count -ge $max_words ]; then break; fi
177
+ if [ -n "$result" ]; then result="$result-"; fi
178
+ result="$result$word"
179
+ count=$((count + 1))
180
+ done
181
+ echo "$result"
182
+ else
183
+ # Fallback to original logic if no meaningful words found
184
+ local cleaned=$(clean_branch_name "$description")
185
+ echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
186
+ fi
187
+ }
188
+
189
+ # Generate branch name
190
+ if [ -n "$SHORT_NAME" ]; then
191
+ # Use provided short name, just clean it up
192
+ BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
193
+ else
194
+ # Generate from description with smart filtering
195
+ BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
196
+ fi
197
+
198
+ # Determine branch number
199
+ if [ -z "$BRANCH_NUMBER" ]; then
200
+ HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
201
+ BRANCH_NUMBER=$((HIGHEST + 1))
202
+ fi
203
+
204
+ # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
205
+ FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
206
+
207
+ # Guard: prevent duplicate numeric prefixes (especially when --number is used)
208
+ if compgen -G "$SPECS_DIR/${FEATURE_NUM}-*" >/dev/null; then
209
+ >&2 echo "[specify] Error: Spec id prefix '${FEATURE_NUM}' already exists under ${SPECS_DIR}"
210
+ >&2 echo "[specify] Existing:"
211
+ ls -1 "$SPECS_DIR/${FEATURE_NUM}-"* 2>/dev/null | sed 's|.*/|[specify] - |' >&2 || true
212
+ >&2 echo "[specify] Hint: omit --number to auto-allocate, or pick an unused number."
213
+ exit 1
214
+ fi
215
+
216
+ BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
217
+
218
+ # Validate and truncate if necessary (keep feature ids at a reasonable length)
219
+ MAX_FEATURE_ID_LENGTH=244
220
+ if [ ${#BRANCH_NAME} -gt $MAX_FEATURE_ID_LENGTH ]; then
221
+ # Calculate how much we need to trim from suffix
222
+ # Account for: feature number (3) + hyphen (1) = 4 chars
223
+ MAX_SUFFIX_LENGTH=$((MAX_FEATURE_ID_LENGTH - 4))
224
+
225
+ # Truncate suffix at word boundary if possible
226
+ TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
227
+ # Remove trailing hyphen if truncation created one
228
+ TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
229
+
230
+ ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
231
+ BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
232
+
233
+ >&2 echo "[specify] Warning: Feature id exceeded the configured length limit"
234
+ >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
235
+ >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
236
+ fi
237
+
238
+ >&2 echo "[specify] Note: This workflow manages specs directories only (no VCS operations)."
239
+ >&2 echo "[specify] To let other scripts target this feature in your current shell, set:"
240
+ >&2 echo "[specify] export SPECIFY_FEATURE=\"$BRANCH_NAME\""
241
+
242
+ FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
243
+ mkdir -p "$FEATURE_DIR"
244
+
245
+ TEMPLATE="$SKILL_DIR/assets/templates/spec-template.md"
246
+ SPEC_FILE="$FEATURE_DIR/spec.md"
247
+ if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
248
+
249
+ # Set the SPECIFY_FEATURE environment variable for the current session
250
+ export SPECIFY_FEATURE="$BRANCH_NAME"
251
+
252
+ if $JSON_MODE; then
253
+ printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
254
+ else
255
+ echo "BRANCH_NAME: $BRANCH_NAME"
256
+ echo "SPEC_FILE: $SPEC_FILE"
257
+ echo "FEATURE_NUM: $FEATURE_NUM"
258
+ echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
259
+ fi