specpro-cli 0.1.0__py3-none-any.whl
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.
- specpro_cli/__init__.py +16 -0
- specpro_cli/assets/commands/specpro.analyze.md +1102 -0
- specpro_cli/assets/commands/specpro.checklist.md +335 -0
- specpro_cli/assets/commands/specpro.clarify.md +581 -0
- specpro_cli/assets/commands/specpro.constitution.md +488 -0
- specpro_cli/assets/commands/specpro.feature.md +115 -0
- specpro_cli/assets/commands/specpro.implement.md +1881 -0
- specpro_cli/assets/commands/specpro.manual-test.md +206 -0
- specpro_cli/assets/commands/specpro.plan.md +3284 -0
- specpro_cli/assets/commands/specpro.qc.md +1489 -0
- specpro_cli/assets/commands/specpro.scenarios.md +154 -0
- specpro_cli/assets/commands/specpro.specify.md +1449 -0
- specpro_cli/assets/commands/specpro.status.md +863 -0
- specpro_cli/assets/commands/specpro.tasks.md +1207 -0
- specpro_cli/assets/commands/specpro.test-implement.md +462 -0
- specpro_cli/assets/commands/specpro.test-plan.md +383 -0
- specpro_cli/assets/commands/specpro.user-manual.md +178 -0
- specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
- specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
- specpro_cli/assets/scripts/bash/common.sh +88 -0
- specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
- specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
- specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
- specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
- specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
- specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
- specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
- specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
- specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
- specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
- specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
- specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
- specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
- specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
- specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
- specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
- specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
- specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
- specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
- specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
- specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
- specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
- specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
- specpro_cli/assets/templates/agent-context-template.md +49 -0
- specpro_cli/assets/templates/assumptions-template.md +248 -0
- specpro_cli/assets/templates/checklist-template.md +40 -0
- specpro_cli/assets/templates/clarifications-template.md +155 -0
- specpro_cli/assets/templates/constitution-template.md +50 -0
- specpro_cli/assets/templates/feature-spec-template.md +66 -0
- specpro_cli/assets/templates/plan-overview-template.md +150 -0
- specpro_cli/assets/templates/plan-template.md +387 -0
- specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
- specpro_cli/assets/templates/requirements-template.md +356 -0
- specpro_cli/assets/templates/spec-template.md +267 -0
- specpro_cli/assets/templates/tasks-template.md +252 -0
- specpro_cli/assets/templates/test-tasks-template.md +174 -0
- specpro_cli/cli/__init__.py +5 -0
- specpro_cli/cli/cmd_init.py +416 -0
- specpro_cli/cli/cmd_remove.py +122 -0
- specpro_cli/cli/entry.py +181 -0
- specpro_cli/integrations/__init__.py +36 -0
- specpro_cli/integrations/base.py +601 -0
- specpro_cli/integrations/claude/__init__.py +101 -0
- specpro_cli/integrations/copilot/__init__.py +153 -0
- specpro_cli/integrations/cursor_agent/__init__.py +51 -0
- specpro_cli/integrations/gemini/__init__.py +44 -0
- specpro_cli/integrations/opencode/__init__.py +48 -0
- specpro_cli/integrations/qodercli/__init__.py +54 -0
- specpro_cli/integrations/registry.py +88 -0
- specpro_cli/packaged/__init__.py +5 -0
- specpro_cli/packaged/sync.py +106 -0
- specpro_cli-0.1.0.dist-info/METADATA +117 -0
- specpro_cli-0.1.0.dist-info/RECORD +76 -0
- specpro_cli-0.1.0.dist-info/WHEEL +4 -0
- specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
- specpro_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# NOT LEGACY — this script is ON the standard workflow path.
|
|
5
|
+
# ============================================================================
|
|
6
|
+
#
|
|
7
|
+
# ⚠️ **This header used to read "LEGACY SCRIPT - NO LONGER USED IN STANDARD
|
|
8
|
+
# WORKFLOW", and that statement was false** (`T144` / `ISS-50`). What the old text
|
|
9
|
+
# described was this script's ORIGIN, not its behaviour: the filename and the
|
|
10
|
+
# branch-numbering code below come from the speckit era, when every feature got its own
|
|
11
|
+
# `specs/<NNN>-<short-name>/` subdirectory.
|
|
12
|
+
#
|
|
13
|
+
# What it actually does today is the CURRENT layout, and that is what its callers
|
|
14
|
+
# depend on:
|
|
15
|
+
# - it writes `specs/spec.md` **directly**; no branch subdirectory is created
|
|
16
|
+
# - branch names are still computed and reported, but they direct NO directory
|
|
17
|
+
# - `commands/specpro.specify.md` calls it to obtain `SPEC_FILE`
|
|
18
|
+
#
|
|
19
|
+
# ⇒ **"kept for backwards compatibility" was the wrong frame.** The `-` numbering feature
|
|
20
|
+
# is the legacy part, and it is inert. ⚠️ **Before deleting this file, check its callers**
|
|
21
|
+
# — `grep -rn 'create-new-feature' commands/` — because deleting it breaks the standard
|
|
22
|
+
# workflow, which is the opposite of what its old header invited.
|
|
23
|
+
#
|
|
24
|
+
# ⚠️ The PowerShell twin (`scripts/powershell/create-new-feature.ps1`) had already
|
|
25
|
+
# recorded this discrepancy; the shell side had not. Two twins, one corrected.
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
set -e
|
|
29
|
+
|
|
30
|
+
JSON_MODE=false
|
|
31
|
+
SHORT_NAME=""
|
|
32
|
+
BRANCH_NUMBER=""
|
|
33
|
+
ARGS=()
|
|
34
|
+
i=1
|
|
35
|
+
while [ $i -le $# ]; do
|
|
36
|
+
arg="${!i}"
|
|
37
|
+
case "$arg" in
|
|
38
|
+
--json)
|
|
39
|
+
JSON_MODE=true
|
|
40
|
+
;;
|
|
41
|
+
--short-name)
|
|
42
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
43
|
+
echo 'Error: --short-name requires a value' >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
i=$((i + 1))
|
|
47
|
+
next_arg="${!i}"
|
|
48
|
+
# Check if the next argument is another option (starts with --)
|
|
49
|
+
if [[ "$next_arg" == --* ]]; then
|
|
50
|
+
echo 'Error: --short-name requires a value' >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
SHORT_NAME="$next_arg"
|
|
54
|
+
;;
|
|
55
|
+
--number)
|
|
56
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
57
|
+
echo 'Error: --number requires a value' >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
i=$((i + 1))
|
|
61
|
+
next_arg="${!i}"
|
|
62
|
+
if [[ "$next_arg" == --* ]]; then
|
|
63
|
+
echo 'Error: --number requires a value' >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
BRANCH_NUMBER="$next_arg"
|
|
67
|
+
;;
|
|
68
|
+
--help|-h)
|
|
69
|
+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
|
70
|
+
echo ""
|
|
71
|
+
echo "Options:"
|
|
72
|
+
echo " --json Output in JSON format"
|
|
73
|
+
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
|
74
|
+
echo " --number N Specify branch number manually (overrides auto-detection)"
|
|
75
|
+
echo " --help, -h Show this help message"
|
|
76
|
+
echo ""
|
|
77
|
+
echo "Examples:"
|
|
78
|
+
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
|
79
|
+
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
|
80
|
+
exit 0
|
|
81
|
+
;;
|
|
82
|
+
*)
|
|
83
|
+
ARGS+=("$arg")
|
|
84
|
+
;;
|
|
85
|
+
esac
|
|
86
|
+
i=$((i + 1))
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
FEATURE_DESCRIPTION="${ARGS[*]}"
|
|
90
|
+
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|
91
|
+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Function to find the repository root by searching for existing project markers
|
|
96
|
+
find_repo_root() {
|
|
97
|
+
local dir="$1"
|
|
98
|
+
while [ "$dir" != "/" ]; do
|
|
99
|
+
if [ -d "$dir/.git" ] || [ -d "$dir/.specpro" ]; then
|
|
100
|
+
echo "$dir"
|
|
101
|
+
return 0
|
|
102
|
+
fi
|
|
103
|
+
dir="$(dirname "$dir")"
|
|
104
|
+
done
|
|
105
|
+
return 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Function to get highest number from specs directory
|
|
109
|
+
get_highest_from_specs() {
|
|
110
|
+
local specs_dir="$1"
|
|
111
|
+
local highest=0
|
|
112
|
+
|
|
113
|
+
if [ -d "$specs_dir" ]; then
|
|
114
|
+
for dir in "$specs_dir"/*; do
|
|
115
|
+
[ -d "$dir" ] || continue
|
|
116
|
+
dirname=$(basename "$dir")
|
|
117
|
+
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
|
118
|
+
number=$((10#$number))
|
|
119
|
+
if [ "$number" -gt "$highest" ]; then
|
|
120
|
+
highest=$number
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
echo "$highest"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Function to get highest number from git branches
|
|
129
|
+
get_highest_from_branches() {
|
|
130
|
+
local highest=0
|
|
131
|
+
|
|
132
|
+
# Get all branches (local and remote)
|
|
133
|
+
branches=$(git branch -a 2>/dev/null || echo "")
|
|
134
|
+
|
|
135
|
+
if [ -n "$branches" ]; then
|
|
136
|
+
while IFS= read -r branch; do
|
|
137
|
+
# Clean branch name: remove leading markers and remote prefixes
|
|
138
|
+
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
|
139
|
+
|
|
140
|
+
# Extract feature number if branch matches pattern ###-*
|
|
141
|
+
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
|
142
|
+
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
|
143
|
+
number=$((10#$number))
|
|
144
|
+
if [ "$number" -gt "$highest" ]; then
|
|
145
|
+
highest=$number
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
148
|
+
done <<< "$branches"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
echo "$highest"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Function to check existing branches (local and remote) and return next available number
|
|
155
|
+
check_existing_branches() {
|
|
156
|
+
local specs_dir="$1"
|
|
157
|
+
|
|
158
|
+
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
|
159
|
+
git fetch --all --prune 2>/dev/null || true
|
|
160
|
+
|
|
161
|
+
# Get highest number from ALL branches (not just matching short name)
|
|
162
|
+
local highest_branch=$(get_highest_from_branches)
|
|
163
|
+
|
|
164
|
+
# Get highest number from ALL specs (not just matching short name)
|
|
165
|
+
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
|
166
|
+
|
|
167
|
+
# Take the maximum of both
|
|
168
|
+
local max_num=$highest_branch
|
|
169
|
+
if [ "$highest_spec" -gt "$max_num" ]; then
|
|
170
|
+
max_num=$highest_spec
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# Return next number
|
|
174
|
+
echo $((max_num + 1))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Function to clean and format a branch name
|
|
178
|
+
clean_branch_name() {
|
|
179
|
+
local name="$1"
|
|
180
|
+
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Resolve repository root. Prefer git information when available, but fall back
|
|
184
|
+
# to searching for repository markers so the workflow still functions in repositories that
|
|
185
|
+
# were initialised with --no-git.
|
|
186
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
187
|
+
|
|
188
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
189
|
+
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
190
|
+
HAS_GIT=true
|
|
191
|
+
else
|
|
192
|
+
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
|
193
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
194
|
+
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
|
195
|
+
exit 1
|
|
196
|
+
fi
|
|
197
|
+
HAS_GIT=false
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
cd "$REPO_ROOT"
|
|
201
|
+
|
|
202
|
+
SPECS_DIR="$REPO_ROOT/specs"
|
|
203
|
+
mkdir -p "$SPECS_DIR"
|
|
204
|
+
|
|
205
|
+
# Function to generate branch name with stop word filtering and length filtering
|
|
206
|
+
generate_branch_name() {
|
|
207
|
+
local description="$1"
|
|
208
|
+
|
|
209
|
+
# Common stop words to filter out
|
|
210
|
+
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)$"
|
|
211
|
+
|
|
212
|
+
# Convert to lowercase and split into words
|
|
213
|
+
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
|
214
|
+
|
|
215
|
+
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
|
216
|
+
local meaningful_words=()
|
|
217
|
+
for word in $clean_name; do
|
|
218
|
+
# Skip empty words
|
|
219
|
+
[ -z "$word" ] && continue
|
|
220
|
+
|
|
221
|
+
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
|
222
|
+
if ! echo "$word" | grep -qiE "$stop_words"; then
|
|
223
|
+
if [ ${#word} -ge 3 ]; then
|
|
224
|
+
meaningful_words+=("$word")
|
|
225
|
+
# `${word^^}` is a Bash 4.0+ expansion. On macOS's stock /bin/bash (3.2)
|
|
226
|
+
# it raises `bad substitution` — and because it sits in a `grep` condition
|
|
227
|
+
# inside an `elif`, the failure is SILENT: the branch simply never fires,
|
|
228
|
+
# the acronym word is dropped, and the script still exits 0 with a
|
|
229
|
+
# plausible-looking but wrong branch name. `tr` is the 3.2 equivalent.
|
|
230
|
+
elif echo "$description" | grep -q "\b$(echo "$word" | tr '[:lower:]' '[:upper:]')\b"; then
|
|
231
|
+
# Keep short words if they appear as uppercase in original (likely acronyms)
|
|
232
|
+
meaningful_words+=("$word")
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
done
|
|
236
|
+
|
|
237
|
+
# If we have meaningful words, use first 3-4 of them
|
|
238
|
+
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
|
239
|
+
local max_words=3
|
|
240
|
+
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
|
241
|
+
|
|
242
|
+
local result=""
|
|
243
|
+
local count=0
|
|
244
|
+
for word in "${meaningful_words[@]}"; do
|
|
245
|
+
if [ $count -ge $max_words ]; then break; fi
|
|
246
|
+
if [ -n "$result" ]; then result="$result-"; fi
|
|
247
|
+
result="$result$word"
|
|
248
|
+
count=$((count + 1))
|
|
249
|
+
done
|
|
250
|
+
echo "$result"
|
|
251
|
+
else
|
|
252
|
+
# Fallback to original logic if no meaningful words found
|
|
253
|
+
local cleaned=$(clean_branch_name "$description")
|
|
254
|
+
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
|
255
|
+
fi
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Generate branch name
|
|
259
|
+
if [ -n "$SHORT_NAME" ]; then
|
|
260
|
+
# Use provided short name, just clean it up
|
|
261
|
+
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
|
262
|
+
else
|
|
263
|
+
# Generate from description with smart filtering
|
|
264
|
+
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# Determine branch number
|
|
268
|
+
if [ -z "$BRANCH_NUMBER" ]; then
|
|
269
|
+
if [ "$HAS_GIT" = true ]; then
|
|
270
|
+
# Check existing branches on remotes
|
|
271
|
+
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
|
272
|
+
else
|
|
273
|
+
# Fall back to local directory check
|
|
274
|
+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
|
275
|
+
BRANCH_NUMBER=$((HIGHEST + 1))
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
|
280
|
+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
|
281
|
+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
|
282
|
+
|
|
283
|
+
# GitHub enforces a 244-byte limit on branch names
|
|
284
|
+
# Validate and truncate if necessary
|
|
285
|
+
MAX_BRANCH_LENGTH=244
|
|
286
|
+
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
|
287
|
+
# Calculate how much we need to trim from suffix
|
|
288
|
+
# Account for: feature number (3) + hyphen (1) = 4 chars
|
|
289
|
+
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
|
|
290
|
+
|
|
291
|
+
# Truncate suffix at word boundary if possible
|
|
292
|
+
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
|
293
|
+
# Remove trailing hyphen if truncation created one
|
|
294
|
+
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
|
295
|
+
|
|
296
|
+
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
|
297
|
+
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
|
298
|
+
|
|
299
|
+
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
|
300
|
+
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
|
301
|
+
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# No longer creating Git branches for specifications
|
|
305
|
+
# Each application has one spec.md that evolves over time
|
|
306
|
+
FEATURE_DIR="$SPECS_DIR"
|
|
307
|
+
mkdir -p "$FEATURE_DIR"
|
|
308
|
+
|
|
309
|
+
SPEC_FILE="$FEATURE_DIR/spec.md"
|
|
310
|
+
|
|
311
|
+
# Create spec.md from template if it doesn't exist
|
|
312
|
+
if [[ ! -f "$SPEC_FILE" ]]; then
|
|
313
|
+
TEMPLATE="$REPO_ROOT/.specpro/templates/spec-template.md"
|
|
314
|
+
if [[ -f "$TEMPLATE" ]]; then
|
|
315
|
+
cp "$TEMPLATE" "$SPEC_FILE"
|
|
316
|
+
>&2 echo "[specify] Created initial specification: $SPEC_FILE"
|
|
317
|
+
else
|
|
318
|
+
touch "$SPEC_FILE"
|
|
319
|
+
>&2 echo "[specify] Created empty specification: $SPEC_FILE"
|
|
320
|
+
fi
|
|
321
|
+
else
|
|
322
|
+
>&2 echo "[specify] Specification already exists: $SPEC_FILE"
|
|
323
|
+
>&2 echo "[specify] Use /specpro-specify with update intent to evolve the existing spec"
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# Set the SPECIFY_FEATURE environment variable for the current session
|
|
327
|
+
export SPECIFY_FEATURE="$BRANCH_NAME"
|
|
328
|
+
|
|
329
|
+
if $JSON_MODE; then
|
|
330
|
+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
|
331
|
+
else
|
|
332
|
+
echo "BRANCH_NAME: $BRANCH_NAME"
|
|
333
|
+
echo "SPEC_FILE: $SPEC_FILE"
|
|
334
|
+
echo "FEATURE_NUM: $FEATURE_NUM"
|
|
335
|
+
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
|
336
|
+
fi
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# QC write-back — the ONE write path for `/specpro-qc`
|
|
4
|
+
#
|
|
5
|
+
# Why this exists (T186 / ISS-143, settled 2026-09-19): these bodies used to live
|
|
6
|
+
# inside `commands/specpro.qc.md` as bash fences. That put an EXECUTABLE
|
|
7
|
+
# IMPLEMENTATION in a Markdown document, which the landing policy (`plan.md` ->
|
|
8
|
+
# 落点政策 §1) forbids: only verifiers reach a script, and an implementation that
|
|
9
|
+
# lives in prose can only be verified by EXTRACTING it — an extraction that
|
|
10
|
+
# depends on the Markdown's shape and returns an empty string, silently, the
|
|
11
|
+
# moment that shape changes. T164 verified its own guard exactly that way.
|
|
12
|
+
#
|
|
13
|
+
# ⚠️⚠️ **SCOPE NARROWED 2026-09-19 (`ISS-186` / `T220`) — two subcommands were
|
|
14
|
+
# REMOVED, and their removal was a DECISION, not a cleanup.**
|
|
15
|
+
#
|
|
16
|
+
# * `implementation-details` rewrote `spec.md` prose. Gone.
|
|
17
|
+
# * `require-owner` authorised repairing another command's artifact. Gone.
|
|
18
|
+
#
|
|
19
|
+
# **Why**: the 2026-09-19 ruling made `/specpro-qc` a checker that **does not repair**
|
|
20
|
+
# (`FR-058`). Its write surface is exactly two artifacts — its own checklist, and the
|
|
21
|
+
# ledger entries it routes. With no repair there is nothing for an authorisation guard
|
|
22
|
+
# to guard, so the guard went with the path it protected.
|
|
23
|
+
#
|
|
24
|
+
# ⚠️ **What was tried first, and rejected** (`ISS-38` / `ISS-163` / `T203`): letting a
|
|
25
|
+
# CALLER name the artifact it owned. The distinction was real — delegation does not
|
|
26
|
+
# widen a write face, and the three callers were never equivalent (`specify` owns the
|
|
27
|
+
# whole of `spec.md`; `plan` and `tasks` own only its Lifecycle fields). But it made a
|
|
28
|
+
# boundary violation **declarable** rather than **impossible**, and a command that
|
|
29
|
+
# repairs what it judged has spent the verdict (Principle III). **The ruling removed
|
|
30
|
+
# the repair instead of licensing it.**
|
|
31
|
+
#
|
|
32
|
+
# ⚠️ **What the move must not lose.** `apply_fix` is still the ONE write-back path, and
|
|
33
|
+
# its value is entirely in three guards that were added the hard way (T164):
|
|
34
|
+
#
|
|
35
|
+
# 1. **The destination is a REQUIRED argument.** A defaulted parameter once
|
|
36
|
+
# delivered the CHECKLIST's content into `spec.md` — observed: a 202-byte
|
|
37
|
+
# spec.md replaced by the 100-byte checklist template. An omitted target
|
|
38
|
+
# ABORTS; it does not guess.
|
|
39
|
+
# 2. **A non-zero transformer status aborts** — the producer failed, so its
|
|
40
|
+
# buffer says nothing about the artifact.
|
|
41
|
+
# 3. **An empty result aborts.** Every producer's output derives from an input
|
|
42
|
+
# it had to READ, so a zero-byte result means that read failed.
|
|
43
|
+
#
|
|
44
|
+
# …and then exactly ONE `mv`. Two `mv`s would make "which write happened" a
|
|
45
|
+
# function of the path taken rather than of the code.
|
|
46
|
+
#
|
|
47
|
+
# Usage:
|
|
48
|
+
# scripts/bash/qc-auto-fix.sh apply-fix <tmp> <gen-status> <dest>
|
|
49
|
+
# Exit: 0 = the write happened; 1 = aborted, nothing written
|
|
50
|
+
#
|
|
51
|
+
# ⚠️ NOT `set -e`: the guards below READ the exit status of the transformer they
|
|
52
|
+
# were handed, and `set -e` would end the script before the reading happens
|
|
53
|
+
# (宪法 2.2.2, second carve-out).
|
|
54
|
+
#
|
|
55
|
+
# ⚠️ **The FILENAME is now narrower than the file, and that was considered.** It is
|
|
56
|
+
# kept deliberately: the PowerShell twin and the parity suite both address it by this
|
|
57
|
+
# name, and a rename would be a second change riding along with a substantive one —
|
|
58
|
+
# the exact shape this repository keeps finding. Recorded here so the next reader knows
|
|
59
|
+
# the name was weighed rather than overlooked.
|
|
60
|
+
|
|
61
|
+
set -uo pipefail
|
|
62
|
+
|
|
63
|
+
usage() {
|
|
64
|
+
# ⚠️ The usage names the COMMAND, not this file: the twin prints the same text, and
|
|
65
|
+
# a usage line that spells its own filename makes the pair disagree on stderr for
|
|
66
|
+
# every invocation that reaches it (FR-008).
|
|
67
|
+
cat >&2 <<'USAGE'
|
|
68
|
+
usage: qc-auto-fix apply-fix <tmp> <gen-status> <dest>
|
|
69
|
+
USAGE
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# =======================================================================================
|
|
73
|
+
# apply_fix — the ONE write-back path
|
|
74
|
+
# =======================================================================================
|
|
75
|
+
apply_fix() {
|
|
76
|
+
local tmp=$1 gen_status=$2 target="${3:-}"
|
|
77
|
+
|
|
78
|
+
# ⚠️ Guarded BEFORE the destination test and with its own message: "no source" and
|
|
79
|
+
# "nowhere to write it" are different mistakes, and a shared message would send the
|
|
80
|
+
# reader to look at the argument they did pass.
|
|
81
|
+
if [ -z "$tmp" ]; then
|
|
82
|
+
echo " ✗ fix aborted: no result to write — apply_fix needs a source" >&2
|
|
83
|
+
return 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
if [ -z "$target" ]; then
|
|
87
|
+
echo " ✗ fix aborted: no destination given — apply_fix does not guess" >&2
|
|
88
|
+
rm -f "$tmp"; return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if [ "$gen_status" -ne 0 ]; then
|
|
92
|
+
echo " ✗ fix aborted: the transformer exited $gen_status — $target left untouched" >&2
|
|
93
|
+
rm -f "$tmp"; return 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# An empty result is a failure, not a fix: every caller's output is derived
|
|
97
|
+
# from an input it had to READ, so a zero-byte result means that read failed.
|
|
98
|
+
if [ ! -s "$tmp" ]; then
|
|
99
|
+
echo " ✗ fix aborted: the result is empty — $target left untouched" >&2
|
|
100
|
+
rm -f "$tmp"; return 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
mv "$tmp" "$target"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# =======================================================================================
|
|
107
|
+
|
|
108
|
+
# =======================================================================================
|
|
109
|
+
# dispatch — one subcommand, and that is the whole surface now
|
|
110
|
+
# =======================================================================================
|
|
111
|
+
case "${1:-}" in
|
|
112
|
+
apply-fix)
|
|
113
|
+
shift
|
|
114
|
+
tmp=${1:-}; gen_status=${2:-0}; dest=${3:-}
|
|
115
|
+
if [ -z "$tmp" ]; then usage; exit 1; fi
|
|
116
|
+
apply_fix "$tmp" "$gen_status" "$dest"
|
|
117
|
+
;;
|
|
118
|
+
*)
|
|
119
|
+
usage; exit 1
|
|
120
|
+
;;
|
|
121
|
+
esac
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
eval $(get_feature_paths)
|
|
32
|
+
|
|
33
|
+
# Ensure the specs directory exists
|
|
34
|
+
mkdir -p "$FEATURE_DIR"
|
|
35
|
+
|
|
36
|
+
# Copy plan template if it exists
|
|
37
|
+
TEMPLATE="$REPO_ROOT/.specpro/templates/plan-template.md"
|
|
38
|
+
if [[ -f "$IMPL_PLAN" ]]; then
|
|
39
|
+
echo "Using existing plan at $IMPL_PLAN"
|
|
40
|
+
elif [[ -f "$TEMPLATE" ]]; then
|
|
41
|
+
cp "$TEMPLATE" "$IMPL_PLAN"
|
|
42
|
+
echo "Created plan from template at $IMPL_PLAN"
|
|
43
|
+
else
|
|
44
|
+
echo "Warning: Plan template not found at $TEMPLATE"
|
|
45
|
+
# Create a basic plan file if template doesn't exist
|
|
46
|
+
touch "$IMPL_PLAN"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Output results
|
|
50
|
+
if $JSON_MODE; then
|
|
51
|
+
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
|
52
|
+
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
|
|
53
|
+
else
|
|
54
|
+
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
|
55
|
+
echo "IMPL_PLAN: $IMPL_PLAN"
|
|
56
|
+
echo "SPECS_DIR: $FEATURE_DIR"
|
|
57
|
+
echo "BRANCH: $CURRENT_BRANCH"
|
|
58
|
+
echo "HAS_GIT: $HAS_GIT"
|
|
59
|
+
fi
|
|
60
|
+
|