specweave 1.0.299 → 1.0.301
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/bin/specweave.js +25 -1
- package/dist/src/cli/commands/auto.js +1 -0
- package/dist/src/cli/commands/auto.js.map +1 -1
- package/dist/src/cli/commands/scan-plugins.d.ts +12 -0
- package/dist/src/cli/commands/scan-plugins.d.ts.map +1 -0
- package/dist/src/cli/commands/scan-plugins.js +80 -0
- package/dist/src/cli/commands/scan-plugins.js.map +1 -0
- package/dist/src/core/doctor/checkers/installation-health-checker.js +6 -6
- package/dist/src/core/doctor/checkers/installation-health-checker.js.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +8 -27
- package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
- package/dist/src/core/lazy-loading/llm-plugin-detector.js +12 -90
- package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
- package/dist/src/core/skill-security/index.d.ts +9 -0
- package/dist/src/core/skill-security/index.d.ts.map +1 -0
- package/dist/src/core/skill-security/index.js +5 -0
- package/dist/src/core/skill-security/index.js.map +1 -0
- package/dist/src/core/skill-security/parser.d.ts +27 -0
- package/dist/src/core/skill-security/parser.d.ts.map +1 -0
- package/dist/src/core/skill-security/parser.js +55 -0
- package/dist/src/core/skill-security/parser.js.map +1 -0
- package/dist/src/core/skill-security/reporter.d.ts +21 -0
- package/dist/src/core/skill-security/reporter.d.ts.map +1 -0
- package/dist/src/core/skill-security/reporter.js +121 -0
- package/dist/src/core/skill-security/reporter.js.map +1 -0
- package/dist/src/core/skill-security/rules.d.ts +25 -0
- package/dist/src/core/skill-security/rules.d.ts.map +1 -0
- package/dist/src/core/skill-security/rules.js +137 -0
- package/dist/src/core/skill-security/rules.js.map +1 -0
- package/dist/src/core/skill-security/scanner.d.ts +41 -0
- package/dist/src/core/skill-security/scanner.d.ts.map +1 -0
- package/dist/src/core/skill-security/scanner.js +78 -0
- package/dist/src/core/skill-security/scanner.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/hooks/lib/score-increment.sh +87 -0
- package/plugins/specweave/hooks/stop-auto-v5.sh +55 -9
- package/plugins/specweave/hooks/tests/test-auto-context-integration.sh +126 -0
- package/plugins/specweave/hooks/tests/test-stop-auto-enriched.sh +128 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +99 -150
- package/plugins/specweave/scripts/setup-auto.sh +58 -4
- package/plugins/specweave/scripts/tests/test-setup-auto-selection.sh +74 -0
- package/plugins/specweave/scripts/tests/test-setup-auto-usergoal.sh +83 -0
- package/plugins/specweave/skills/auto/SKILL.md +3 -1
- package/plugins/specweave/skills/do/SKILL.md +11 -0
- package/plugins/specweave/skills/increment/SKILL.md +8 -2
- package/plugins/specweave/skills/team-lead/SKILL.md +69 -5
- package/plugins/specweave-jira/skills/jira-mapper/SKILL.md +13 -14
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +74 -4
- package/plugins/specweave-jira/skills/jira-sync/SKILL.md +18 -27
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SKILL.md self-scan scanner.
|
|
3
|
+
* Applies detection rules against SKILL.md content and bash code blocks.
|
|
4
|
+
*/
|
|
5
|
+
import { SKILL_SECURITY_RULES } from './rules.js';
|
|
6
|
+
import { extractBashBlocks } from './parser.js';
|
|
7
|
+
/**
|
|
8
|
+
* Scan SKILL.md content using structured detection rules.
|
|
9
|
+
* Rules marked `codeBlockOnly: true` only apply inside bash code blocks.
|
|
10
|
+
* Other rules apply to the full markdown content.
|
|
11
|
+
*/
|
|
12
|
+
export function scanSkillMd(content, rules = SKILL_SECURITY_RULES) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
const allLines = content.split('\n');
|
|
15
|
+
// Build a set of line ranges for bash code blocks (1-based)
|
|
16
|
+
const bashBlocks = extractBashBlocks(content);
|
|
17
|
+
const bashBlockLines = new Set();
|
|
18
|
+
for (const block of bashBlocks) {
|
|
19
|
+
const blockLineCount = block.content.split('\n').length;
|
|
20
|
+
for (let offset = 0; offset < blockLineCount; offset++) {
|
|
21
|
+
bashBlockLines.add(block.startLine + 1 + offset); // +1 because startLine is the fence
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Separate rules by scope
|
|
25
|
+
const codeOnlyRules = rules.filter(r => r.codeBlockOnly);
|
|
26
|
+
const fullContentRules = rules.filter(r => !r.codeBlockOnly);
|
|
27
|
+
// Apply full-content rules to every line
|
|
28
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
29
|
+
const line = allLines[i];
|
|
30
|
+
const lineNum = i + 1;
|
|
31
|
+
for (const rule of fullContentRules) {
|
|
32
|
+
if (rule.pattern.test(line)) {
|
|
33
|
+
const match = line.match(rule.pattern);
|
|
34
|
+
findings.push({
|
|
35
|
+
ruleId: rule.id,
|
|
36
|
+
category: rule.category,
|
|
37
|
+
severity: rule.severity,
|
|
38
|
+
message: rule.message,
|
|
39
|
+
suggestedFix: rule.suggestedFix,
|
|
40
|
+
line: lineNum,
|
|
41
|
+
matchedText: match?.[0] ?? line.trim().slice(0, 60),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Apply code-block-only rules to lines inside bash blocks
|
|
47
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
48
|
+
const lineNum = i + 1;
|
|
49
|
+
if (!bashBlockLines.has(lineNum))
|
|
50
|
+
continue;
|
|
51
|
+
const line = allLines[i];
|
|
52
|
+
for (const rule of codeOnlyRules) {
|
|
53
|
+
if (rule.pattern.test(line)) {
|
|
54
|
+
const match = line.match(rule.pattern);
|
|
55
|
+
findings.push({
|
|
56
|
+
ruleId: rule.id,
|
|
57
|
+
category: rule.category,
|
|
58
|
+
severity: rule.severity,
|
|
59
|
+
message: rule.message,
|
|
60
|
+
suggestedFix: rule.suggestedFix,
|
|
61
|
+
line: lineNum,
|
|
62
|
+
matchedText: match?.[0] ?? line.trim().slice(0, 60),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Sort findings by line number
|
|
68
|
+
findings.sort((a, b) => a.line - b.line);
|
|
69
|
+
const hasCriticalOrHigh = findings.some(f => f.severity === 'critical' || f.severity === 'high');
|
|
70
|
+
const hasAny = findings.length > 0;
|
|
71
|
+
const exitCode = hasCriticalOrHigh ? 2 : hasAny ? 1 : 0;
|
|
72
|
+
return {
|
|
73
|
+
exitCode,
|
|
74
|
+
passed: !hasCriticalOrHigh,
|
|
75
|
+
findings,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../../../../src/core/skill-security/scanner.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specweave",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.301",
|
|
4
4
|
"description": "Spec-driven development framework for AI coding agents. Works with Claude Code, Codex, Antigravity, Cursor, Copilot & more. 100+ skills, 49 CLI commands, verified skill certification, autonomous execution, and living documentation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# score-increment.sh — Score an increment against a text query using keyword overlap.
|
|
3
|
+
#
|
|
4
|
+
# Usage: score-increment.sh <increment_dir> <query>
|
|
5
|
+
# Output: integer 0-100 (higher = better match)
|
|
6
|
+
#
|
|
7
|
+
# Corpus: increment directory name + metadata title + spec.md overview + task titles
|
|
8
|
+
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
INCREMENT_DIR="$1"
|
|
12
|
+
QUERY="$2"
|
|
13
|
+
|
|
14
|
+
if [ -z "$INCREMENT_DIR" ] || [ -z "$QUERY" ]; then
|
|
15
|
+
echo "0"
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [ ! -d "$INCREMENT_DIR" ]; then
|
|
20
|
+
echo "0"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Build corpus from multiple sources
|
|
25
|
+
CORPUS=""
|
|
26
|
+
|
|
27
|
+
# 1. Directory name (e.g. "0257-auto-context-aware-selection" → words)
|
|
28
|
+
DIR_NAME=$(basename "$INCREMENT_DIR")
|
|
29
|
+
CORPUS="$CORPUS ${DIR_NAME//-/ }"
|
|
30
|
+
|
|
31
|
+
# 2. Title from metadata.json
|
|
32
|
+
META_FILE="$INCREMENT_DIR/metadata.json"
|
|
33
|
+
if [ -f "$META_FILE" ]; then
|
|
34
|
+
TITLE=$(jq -r '.title // .name // ""' "$META_FILE" 2>/dev/null || echo "")
|
|
35
|
+
CORPUS="$CORPUS $TITLE"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# 3. First 500 chars of spec.md (overview/problem statement)
|
|
39
|
+
SPEC_FILE="$INCREMENT_DIR/spec.md"
|
|
40
|
+
if [ -f "$SPEC_FILE" ]; then
|
|
41
|
+
OVERVIEW=$(head -c 500 "$SPEC_FILE" 2>/dev/null || echo "")
|
|
42
|
+
CORPUS="$CORPUS $OVERVIEW"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# 4. Task titles from tasks.md (lines starting with ### T-)
|
|
46
|
+
TASKS_FILE="$INCREMENT_DIR/tasks.md"
|
|
47
|
+
if [ -f "$TASKS_FILE" ]; then
|
|
48
|
+
TASK_TITLES=$(grep '^### T-' "$TASKS_FILE" 2>/dev/null | sed 's/^### T-[0-9]*: //' || echo "")
|
|
49
|
+
CORPUS="$CORPUS $TASK_TITLES"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Normalize corpus: lowercase, non-alphanumeric → space
|
|
53
|
+
CORPUS_LC=$(echo "$CORPUS" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' ' ')
|
|
54
|
+
|
|
55
|
+
# Extract unique query keywords: lowercase, length > 2, skip common stopwords
|
|
56
|
+
STOPWORDS="the and for with from that this are was were has have been will not"
|
|
57
|
+
KEYWORDS=$(echo "$QUERY" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '\n' | \
|
|
58
|
+
awk 'length > 2' | \
|
|
59
|
+
grep -vxF -e "the" -e "and" -e "for" -e "with" -e "from" -e "that" -e "this" \
|
|
60
|
+
-e "are" -e "was" -e "were" -e "has" -e "have" -e "been" -e "will" \
|
|
61
|
+
-e "not" -e "into" -e "use" -e "its" | \
|
|
62
|
+
sort -u)
|
|
63
|
+
|
|
64
|
+
if [ -z "$KEYWORDS" ]; then
|
|
65
|
+
echo "0"
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Count matches
|
|
70
|
+
TOTAL=0
|
|
71
|
+
MATCHED=0
|
|
72
|
+
while IFS= read -r keyword; do
|
|
73
|
+
[ -z "$keyword" ] && continue
|
|
74
|
+
TOTAL=$((TOTAL + 1))
|
|
75
|
+
if echo "$CORPUS_LC" | grep -qw "$keyword" 2>/dev/null; then
|
|
76
|
+
MATCHED=$((MATCHED + 1))
|
|
77
|
+
fi
|
|
78
|
+
done <<< "$KEYWORDS"
|
|
79
|
+
|
|
80
|
+
if [ "$TOTAL" -eq 0 ]; then
|
|
81
|
+
echo "0"
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Score = matched_keywords / total_keywords * 100
|
|
86
|
+
SCORE=$(( MATCHED * 100 / TOTAL ))
|
|
87
|
+
echo "$SCORE"
|
|
@@ -52,10 +52,22 @@ count_pending_tasks() {
|
|
|
52
52
|
local f="$1"; [ ! -f "$f" ] && echo "0" && return
|
|
53
53
|
local c; c=$(grep -c '\[ \]' "$f" 2>/dev/null) || true; echo "${c:-0}"
|
|
54
54
|
}
|
|
55
|
+
count_completed_tasks() {
|
|
56
|
+
local f="$1"; [ ! -f "$f" ] && echo "0" && return
|
|
57
|
+
local c; c=$(grep -c '\[x\]' "$f" 2>/dev/null) || true; echo "${c:-0}"
|
|
58
|
+
}
|
|
55
59
|
count_open_acs() {
|
|
56
60
|
local f="$1"; [ ! -f "$f" ] && echo "0" && return
|
|
57
61
|
local c; c=$(grep -c '\[ \]' "$f" 2>/dev/null) || true; echo "${c:-0}"
|
|
58
62
|
}
|
|
63
|
+
get_next_task_title() {
|
|
64
|
+
local f="$1"; [ ! -f "$f" ] && echo "" && return
|
|
65
|
+
local lnum; lnum=$(grep -n '\[ \]' "$f" 2>/dev/null | head -1 | cut -d: -f1)
|
|
66
|
+
[ -z "$lnum" ] && echo "" && return
|
|
67
|
+
# Search backwards from [ ] line to find ### T-NNN: Title heading
|
|
68
|
+
local title; title=$(head -n "$lnum" "$f" | grep '### T-' | tail -1 | sed 's/^### T-[0-9]*: //')
|
|
69
|
+
echo "${title}" | head -c 80
|
|
70
|
+
}
|
|
59
71
|
silent_approve() {
|
|
60
72
|
local reason="$1" rc="${2:-session_inactive}" ctx="${3:-"{}"}"
|
|
61
73
|
log "APPROVE: $reason"
|
|
@@ -143,8 +155,12 @@ if [ -f "$DEDUP_PREV" ]; then
|
|
|
143
155
|
fi
|
|
144
156
|
echo "$NOW" > "$DEDUP_PREV" 2>/dev/null
|
|
145
157
|
|
|
146
|
-
# 7.
|
|
158
|
+
# 7. Read userGoal from session marker
|
|
159
|
+
USER_GOAL=$(jq -r '.userGoal // ""' "$SESSION" 2>/dev/null || echo "")
|
|
160
|
+
|
|
161
|
+
# 8. Scan active increments (enriched: next task, progress fraction)
|
|
147
162
|
TP=0; TAC=0; IC=0; ILIST=""
|
|
163
|
+
_SCORE_SCRIPT="$SCRIPT_DIR/lib/score-increment.sh"
|
|
148
164
|
for meta in $(find "$INC_DIR" -maxdepth 2 -name "metadata.json" 2>/dev/null); do
|
|
149
165
|
st=$(jq -r '.status // "unknown"' "$meta" 2>/dev/null || echo "unknown")
|
|
150
166
|
[ "$st" != "active" ] && [ "$st" != "in-progress" ] && continue
|
|
@@ -152,11 +168,23 @@ for meta in $(find "$INC_DIR" -maxdepth 2 -name "metadata.json" 2>/dev/null); do
|
|
|
152
168
|
p=$(count_pending_tasks "$d/tasks.md"); a=$(count_open_acs "$d/spec.md")
|
|
153
169
|
if [ "$p" -gt 0 ] || [ "$a" -gt 0 ]; then
|
|
154
170
|
TP=$((TP + p)); TAC=$((TAC + a)); IC=$((IC + 1))
|
|
155
|
-
|
|
171
|
+
# Extract next pending task title (first [ ] line, strip markdown)
|
|
172
|
+
_next_task=$(grep -m1 '\[ \]' "$d/tasks.md" 2>/dev/null | sed 's/.*\] //' | head -c 80 || echo "")
|
|
173
|
+
# Count completed tasks for progress fraction
|
|
174
|
+
_done=$(grep -c '\[x\]' "$d/tasks.md" 2>/dev/null) || _done=0
|
|
175
|
+
_total=$((_done + p))
|
|
176
|
+
# Score against userGoal if set and scoring script available
|
|
177
|
+
_score=""
|
|
178
|
+
if [ -n "$USER_GOAL" ] && [ -f "$_SCORE_SCRIPT" ]; then
|
|
179
|
+
_score=$(bash "$_SCORE_SCRIPT" "$d" "$USER_GOAL" 2>/dev/null || echo "0")
|
|
180
|
+
fi
|
|
181
|
+
# Extended ILIST format: id|pending|acs|next_task|done|total|score
|
|
182
|
+
_entry="$id|$p|$a|$_next_task|$_done|$_total|${_score:-0}"
|
|
183
|
+
[ -z "$ILIST" ] && ILIST="$_entry" || ILIST="$ILIST,$_entry"
|
|
156
184
|
fi
|
|
157
185
|
done
|
|
158
186
|
|
|
159
|
-
#
|
|
187
|
+
# 9. All complete → approve
|
|
160
188
|
if [ "$IC" -eq 0 ]; then
|
|
161
189
|
rm -f "$SESSION" "$DEDUP_PREV" "$TURN_FILE" 2>/dev/null
|
|
162
190
|
loud_approve "All work complete" "all_complete" \
|
|
@@ -164,19 +192,37 @@ if [ "$IC" -eq 0 ]; then
|
|
|
164
192
|
"All tasks and ACs complete. Run /sw:done to close the increment."
|
|
165
193
|
fi
|
|
166
194
|
|
|
167
|
-
#
|
|
195
|
+
# 10. Work remains → block with enriched context message
|
|
196
|
+
# Sort entries by score descending (highest-relevance first) when userGoal is set
|
|
197
|
+
SORTED_ILIST="$ILIST"
|
|
198
|
+
if [ -n "$USER_GOAL" ] && [ "$IC" -gt 1 ]; then
|
|
199
|
+
# Sort by score field (7th field) descending
|
|
200
|
+
SORTED_ILIST=$(echo "$ILIST" | tr ',' '\n' | sort -t'|' -k7 -nr | tr '\n' ',')
|
|
201
|
+
SORTED_ILIST="${SORTED_ILIST%,}" # trim trailing comma
|
|
202
|
+
fi
|
|
203
|
+
|
|
168
204
|
DETAILS=""
|
|
169
|
-
|
|
205
|
+
_BEST_ID=""
|
|
206
|
+
IFS=',' read -ra ENTRIES <<< "$SORTED_ILIST"
|
|
170
207
|
for entry in "${ENTRIES[@]}"; do
|
|
171
|
-
IFS='|' read -r eid ep ea <<< "$entry"
|
|
172
|
-
|
|
208
|
+
IFS='|' read -r eid ep ea enext edone etotal escore <<< "$entry"
|
|
209
|
+
[ -z "$_BEST_ID" ] && _BEST_ID="$eid"
|
|
210
|
+
_progress="${edone:-0}/${etotal:-0} tasks"
|
|
211
|
+
_next_info=""
|
|
212
|
+
[ -n "$enext" ] && _next_info=" | Next: $enext"
|
|
213
|
+
DETAILS="${DETAILS}\n ▸ ${eid}: ${_progress}${_next_info}"
|
|
173
214
|
done
|
|
174
215
|
|
|
175
|
-
|
|
216
|
+
# Build enriched block message
|
|
217
|
+
BMSG=""
|
|
218
|
+
[ -n "$USER_GOAL" ] && BMSG="Goal: ${USER_GOAL}\n"
|
|
219
|
+
BMSG="${BMSG}Auto Mode: ${IC} increment(s) need work${DETAILS}"
|
|
220
|
+
BMSG="${BMSG}\nTurn $TURN/$MAX_TURNS | Continue: /sw:do ${_BEST_ID}"
|
|
176
221
|
BMSG=$(echo -e "$BMSG")
|
|
177
222
|
|
|
178
223
|
block "Work remaining: $TP tasks, $TAC ACs" "work_remaining" \
|
|
179
224
|
"$(jq -n --argjson p "$TP" --argjson a "$TAC" --argjson i "$IC" \
|
|
180
225
|
--argjson t "$TURN" --argjson mt "$MAX_TURNS" --arg il "$ILIST" \
|
|
181
|
-
|
|
226
|
+
--arg goal "$USER_GOAL" --arg best "$_BEST_ID" \
|
|
227
|
+
'{pendingTasks:$p,openAcs:$a,incompleteIncrements:$i,increments:$il,turn:{current:$t,max:$mt},userGoal:$goal,recommended:$best}')" \
|
|
182
228
|
"$BMSG"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Integration test: scored selection + enriched feedback (T-012, AC-US1-01, AC-US2-01, AC-US3-01)
|
|
3
|
+
# Creates a mock .specweave/ project, scores increments, and verifies enrichment helpers.
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
SCORE_SCRIPT="$SCRIPT_DIR/../lib/score-increment.sh"
|
|
9
|
+
STOP_HOOK="$SCRIPT_DIR/../stop-auto-v5.sh"
|
|
10
|
+
TMPDIR_ROOT=$(mktemp -d)
|
|
11
|
+
trap 'rm -rf "$TMPDIR_ROOT"' EXIT
|
|
12
|
+
|
|
13
|
+
PASS=0; FAIL=0
|
|
14
|
+
|
|
15
|
+
assert_eq() {
|
|
16
|
+
if [ "$1" = "$2" ]; then
|
|
17
|
+
echo " ✓ $3"; PASS=$((PASS+1))
|
|
18
|
+
else
|
|
19
|
+
echo " ✗ $3 (expected '$2', got '$1')"; FAIL=$((FAIL+1))
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
assert_gt() {
|
|
24
|
+
if [ "$1" -gt "$2" ]; then
|
|
25
|
+
echo " ✓ $3 ($1 > $2)"; PASS=$((PASS+1))
|
|
26
|
+
else
|
|
27
|
+
echo " ✗ $3 (expected $1 > $2)"; FAIL=$((FAIL+1))
|
|
28
|
+
fi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
assert_contains() {
|
|
32
|
+
if echo "$1" | grep -q "$2"; then
|
|
33
|
+
echo " ✓ $3"; PASS=$((PASS+1))
|
|
34
|
+
else
|
|
35
|
+
echo " ✗ $3 (expected '$1' to contain '$2')"; FAIL=$((FAIL+1))
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
echo "Integration: scored selection + enriched feedback"
|
|
40
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
41
|
+
|
|
42
|
+
# Setup mock .specweave/ structure
|
|
43
|
+
SW="$TMPDIR_ROOT/.specweave"
|
|
44
|
+
INC_DIR="$SW/increments"
|
|
45
|
+
STATE_DIR="$SW/state"
|
|
46
|
+
LOGS_DIR="$SW/logs"
|
|
47
|
+
mkdir -p "$INC_DIR/0100-user-auth" "$INC_DIR/0101-deploy-pipeline" "$STATE_DIR" "$LOGS_DIR"
|
|
48
|
+
|
|
49
|
+
# Create increment 0100: auth
|
|
50
|
+
echo '{"title":"User Authentication Feature","status":"active","lastActivity":"2026-02-18T10:00:00Z"}' \
|
|
51
|
+
> "$INC_DIR/0100-user-auth/metadata.json"
|
|
52
|
+
cat > "$INC_DIR/0100-user-auth/spec.md" << 'EOF'
|
|
53
|
+
# User Authentication Feature
|
|
54
|
+
|
|
55
|
+
Implement login, signup, and OAuth for user authentication.
|
|
56
|
+
EOF
|
|
57
|
+
cat > "$INC_DIR/0100-user-auth/tasks.md" << 'EOF'
|
|
58
|
+
### T-001: Add login endpoint
|
|
59
|
+
**Status**: [x] completed
|
|
60
|
+
### T-002: Add signup endpoint
|
|
61
|
+
**Status**: [x] completed
|
|
62
|
+
### T-003: OAuth integration
|
|
63
|
+
**Status**: [ ] pending
|
|
64
|
+
### T-004: Session management
|
|
65
|
+
**Status**: [ ] pending
|
|
66
|
+
EOF
|
|
67
|
+
|
|
68
|
+
# Create increment 0101: deploy
|
|
69
|
+
echo '{"title":"CI/CD Deploy Pipeline","status":"active","lastActivity":"2026-02-17T10:00:00Z"}' \
|
|
70
|
+
> "$INC_DIR/0101-deploy-pipeline/metadata.json"
|
|
71
|
+
cat > "$INC_DIR/0101-deploy-pipeline/spec.md" << 'EOF'
|
|
72
|
+
# CI/CD Deploy Pipeline
|
|
73
|
+
|
|
74
|
+
Setup continuous integration and deployment pipeline with Docker and GitHub Actions.
|
|
75
|
+
EOF
|
|
76
|
+
cat > "$INC_DIR/0101-deploy-pipeline/tasks.md" << 'EOF'
|
|
77
|
+
### T-001: Create Dockerfile
|
|
78
|
+
**Status**: [x] completed
|
|
79
|
+
### T-002: Configure GitHub Actions
|
|
80
|
+
**Status**: [ ] pending
|
|
81
|
+
### T-003: Set up staging environment
|
|
82
|
+
**Status**: [ ] pending
|
|
83
|
+
EOF
|
|
84
|
+
|
|
85
|
+
# TC-015: Scored selection — auth scores higher for "fix authentication"
|
|
86
|
+
s_auth=$(bash "$SCORE_SCRIPT" "$INC_DIR/0100-user-auth" "fix authentication" 2>/dev/null)
|
|
87
|
+
s_deploy=$(bash "$SCORE_SCRIPT" "$INC_DIR/0101-deploy-pipeline" "fix authentication" 2>/dev/null)
|
|
88
|
+
assert_gt "$s_auth" "$s_deploy" "TC-015a: auth scores higher than deploy for auth prompt"
|
|
89
|
+
|
|
90
|
+
# TC-015b: Deploy scores higher for "deploy pipeline docker"
|
|
91
|
+
s_auth2=$(bash "$SCORE_SCRIPT" "$INC_DIR/0100-user-auth" "deploy pipeline docker" 2>/dev/null)
|
|
92
|
+
s_deploy2=$(bash "$SCORE_SCRIPT" "$INC_DIR/0101-deploy-pipeline" "deploy pipeline docker" 2>/dev/null)
|
|
93
|
+
assert_gt "$s_deploy2" "$s_auth2" "TC-015b: deploy scores higher for deploy prompt"
|
|
94
|
+
|
|
95
|
+
# TC-015c: userGoal wiring — verify logic matches setup-auto.sh
|
|
96
|
+
AUTO_MODE_FILE="$STATE_DIR/auto-mode.json"
|
|
97
|
+
PROMPT="fix authentication"
|
|
98
|
+
if [ -f "$AUTO_MODE_FILE" ]; then
|
|
99
|
+
if [ -n "$PROMPT" ]; then
|
|
100
|
+
_UPDATED=$(jq --arg g "$PROMPT" '.userGoal = $g' "$AUTO_MODE_FILE" 2>/dev/null)
|
|
101
|
+
else
|
|
102
|
+
_UPDATED=$(jq '.userGoal = null' "$AUTO_MODE_FILE" 2>/dev/null)
|
|
103
|
+
fi
|
|
104
|
+
[ -n "$_UPDATED" ] && echo "$_UPDATED" > "$AUTO_MODE_FILE"
|
|
105
|
+
elif [ -n "$PROMPT" ]; then
|
|
106
|
+
jq -n --arg g "$PROMPT" '{"active":false,"userGoal":$g}' > "$AUTO_MODE_FILE"
|
|
107
|
+
fi
|
|
108
|
+
goal=$(jq -r '.userGoal' "$AUTO_MODE_FILE")
|
|
109
|
+
assert_eq "$goal" "fix authentication" "TC-015c: userGoal written to auto-mode.json"
|
|
110
|
+
|
|
111
|
+
# TC-015d: Stop hook enrichment — source hook, test helpers
|
|
112
|
+
export PROJECT_ROOT="$TMPDIR_ROOT"
|
|
113
|
+
export __STOP_AUTO_V5_SOURCED=1
|
|
114
|
+
source "$STOP_HOOK" 2>/dev/null
|
|
115
|
+
|
|
116
|
+
next_auth=$(get_next_task_title "$INC_DIR/0100-user-auth/tasks.md")
|
|
117
|
+
assert_eq "$next_auth" "OAuth integration" "TC-015d: enriched next task from auth increment"
|
|
118
|
+
|
|
119
|
+
done_auth=$(count_completed_tasks "$INC_DIR/0100-user-auth/tasks.md")
|
|
120
|
+
pending_auth=$(count_pending_tasks "$INC_DIR/0100-user-auth/tasks.md")
|
|
121
|
+
assert_eq "$done_auth" "2" "TC-015e: auth increment done count = 2"
|
|
122
|
+
assert_eq "$pending_auth" "2" "TC-015f: auth increment pending count = 2"
|
|
123
|
+
|
|
124
|
+
echo ""
|
|
125
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
126
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for enriched stop hook feedback (AC-US3-01, AC-US3-02, AC-US3-03, AC-US4-01, AC-US4-03)
|
|
3
|
+
# Sources stop-auto-v5.sh to load helper functions only.
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
STOP_HOOK="$SCRIPT_DIR/../stop-auto-v5.sh"
|
|
9
|
+
TMPDIR_ROOT=$(mktemp -d)
|
|
10
|
+
trap 'rm -rf "$TMPDIR_ROOT"' EXIT
|
|
11
|
+
|
|
12
|
+
PASS=0; FAIL=0
|
|
13
|
+
|
|
14
|
+
assert_eq() {
|
|
15
|
+
if [ "$1" = "$2" ]; then
|
|
16
|
+
echo " ✓ $3"; PASS=$((PASS+1))
|
|
17
|
+
else
|
|
18
|
+
echo " ✗ $3 (expected '$2', got '$1')"; FAIL=$((FAIL+1))
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
assert_ge() {
|
|
23
|
+
if [ "$1" -ge "$2" ]; then
|
|
24
|
+
echo " ✓ $3 ($1 >= $2)"; PASS=$((PASS+1))
|
|
25
|
+
else
|
|
26
|
+
echo " ✗ $3 (expected >= $2, got $1)"; FAIL=$((FAIL+1))
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Set up minimal .specweave structure so sourcing succeeds
|
|
31
|
+
export PROJECT_ROOT="$TMPDIR_ROOT"
|
|
32
|
+
mkdir -p "$TMPDIR_ROOT/.specweave/logs" "$TMPDIR_ROOT/.specweave/state" "$TMPDIR_ROOT/.specweave/increments"
|
|
33
|
+
|
|
34
|
+
# Source only (loads helper functions, skips main execution)
|
|
35
|
+
export __STOP_AUTO_V5_SOURCED=1
|
|
36
|
+
source "$STOP_HOOK" 2>/dev/null
|
|
37
|
+
|
|
38
|
+
echo "stop-auto-v5.sh enrichment function tests"
|
|
39
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
40
|
+
|
|
41
|
+
# Create a tasks.md with known counts
|
|
42
|
+
TASKS_FILE="$TMPDIR_ROOT/tasks.md"
|
|
43
|
+
cat > "$TASKS_FILE" << 'EOF'
|
|
44
|
+
### T-001: Setup database
|
|
45
|
+
**User Story**: US-001 | **Status**: [x] completed
|
|
46
|
+
|
|
47
|
+
### T-002: Implement login
|
|
48
|
+
**User Story**: US-001 | **Status**: [x] completed
|
|
49
|
+
|
|
50
|
+
### T-003: Add unit tests
|
|
51
|
+
**User Story**: US-001 | **Status**: [x] completed
|
|
52
|
+
|
|
53
|
+
### T-004: Write integration tests
|
|
54
|
+
**User Story**: US-001 | **Status**: [x] completed
|
|
55
|
+
|
|
56
|
+
### T-005: Deploy to staging
|
|
57
|
+
**User Story**: US-001 | **Status**: [x] completed
|
|
58
|
+
|
|
59
|
+
### T-006: Configure auth middleware
|
|
60
|
+
**User Story**: US-001 | **Status**: [ ] pending
|
|
61
|
+
|
|
62
|
+
### T-007: Add error handling
|
|
63
|
+
**User Story**: US-001 | **Status**: [ ] pending
|
|
64
|
+
|
|
65
|
+
### T-008: Final review and cleanup
|
|
66
|
+
**User Story**: US-001 | **Status**: [ ] pending
|
|
67
|
+
EOF
|
|
68
|
+
|
|
69
|
+
# TC-010: Block message includes next task title (AC-US3-01)
|
|
70
|
+
result=$(get_next_task_title "$TASKS_FILE")
|
|
71
|
+
assert_eq "$result" "Configure auth middleware" "TC-010: get_next_task_title returns first pending task"
|
|
72
|
+
|
|
73
|
+
# TC-011: count_completed_tasks counts [x] entries
|
|
74
|
+
done_count=$(count_completed_tasks "$TASKS_FILE")
|
|
75
|
+
assert_eq "$done_count" "5" "TC-011: count_completed_tasks = 5"
|
|
76
|
+
|
|
77
|
+
# TC-012: count_pending_tasks counts [ ] entries (AC-US3-03)
|
|
78
|
+
pending_count=$(count_pending_tasks "$TASKS_FILE")
|
|
79
|
+
assert_eq "$pending_count" "3" "TC-012: count_pending_tasks = 3"
|
|
80
|
+
|
|
81
|
+
# TC-012b: Progress fraction calculation
|
|
82
|
+
total=$((done_count + pending_count))
|
|
83
|
+
assert_eq "$total" "8" "TC-012b: total = done + pending = 8"
|
|
84
|
+
|
|
85
|
+
# TC-013: Empty tasks file → zeros
|
|
86
|
+
EMPTY_FILE="$TMPDIR_ROOT/empty.md"
|
|
87
|
+
touch "$EMPTY_FILE"
|
|
88
|
+
done_empty=$(count_completed_tasks "$EMPTY_FILE")
|
|
89
|
+
pending_empty=$(count_pending_tasks "$EMPTY_FILE")
|
|
90
|
+
assert_eq "$done_empty" "0" "TC-013: empty file → 0 completed"
|
|
91
|
+
assert_eq "$pending_empty" "0" "TC-013b: empty file → 0 pending"
|
|
92
|
+
|
|
93
|
+
# TC-013c: get_next_task_title on empty file → empty string
|
|
94
|
+
next_empty=$(get_next_task_title "$EMPTY_FILE")
|
|
95
|
+
assert_eq "$next_empty" "" "TC-013c: empty file → no next title"
|
|
96
|
+
|
|
97
|
+
# TC-014: Missing file → zeros
|
|
98
|
+
done_missing=$(count_completed_tasks "/nonexistent/tasks.md")
|
|
99
|
+
assert_eq "$done_missing" "0" "TC-014: missing file → 0 completed"
|
|
100
|
+
|
|
101
|
+
# TC-015: get_next_task_title with all completed → empty
|
|
102
|
+
ALL_DONE_FILE="$TMPDIR_ROOT/all-done.md"
|
|
103
|
+
cat > "$ALL_DONE_FILE" << 'EOF'
|
|
104
|
+
### T-001: Done task one
|
|
105
|
+
**Status**: [x] completed
|
|
106
|
+
|
|
107
|
+
### T-002: Done task two
|
|
108
|
+
**Status**: [x] completed
|
|
109
|
+
EOF
|
|
110
|
+
next_done=$(get_next_task_title "$ALL_DONE_FILE")
|
|
111
|
+
assert_eq "$next_done" "" "TC-015: all done → no next title"
|
|
112
|
+
|
|
113
|
+
# TC-016: get_next_task_title with task without title pattern
|
|
114
|
+
MIXED_FILE="$TMPDIR_ROOT/mixed.md"
|
|
115
|
+
cat > "$MIXED_FILE" << 'EOF'
|
|
116
|
+
### T-001: First Task Title
|
|
117
|
+
**Status**: [x] completed
|
|
118
|
+
### T-002: Second Task Title
|
|
119
|
+
**Status**: [ ] pending
|
|
120
|
+
### T-003: Third Task Title
|
|
121
|
+
**Status**: [ ] pending
|
|
122
|
+
EOF
|
|
123
|
+
next_mixed=$(get_next_task_title "$MIXED_FILE")
|
|
124
|
+
assert_eq "$next_mixed" "Second Task Title" "TC-016: returns first pending task title"
|
|
125
|
+
|
|
126
|
+
echo ""
|
|
127
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
128
|
+
[ "$FAIL" -eq 0 ] || exit 1
|