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.
Files changed (49) hide show
  1. package/bin/specweave.js +25 -1
  2. package/dist/src/cli/commands/auto.js +1 -0
  3. package/dist/src/cli/commands/auto.js.map +1 -1
  4. package/dist/src/cli/commands/scan-plugins.d.ts +12 -0
  5. package/dist/src/cli/commands/scan-plugins.d.ts.map +1 -0
  6. package/dist/src/cli/commands/scan-plugins.js +80 -0
  7. package/dist/src/cli/commands/scan-plugins.js.map +1 -0
  8. package/dist/src/core/doctor/checkers/installation-health-checker.js +6 -6
  9. package/dist/src/core/doctor/checkers/installation-health-checker.js.map +1 -1
  10. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts +8 -27
  11. package/dist/src/core/lazy-loading/llm-plugin-detector.d.ts.map +1 -1
  12. package/dist/src/core/lazy-loading/llm-plugin-detector.js +12 -90
  13. package/dist/src/core/lazy-loading/llm-plugin-detector.js.map +1 -1
  14. package/dist/src/core/skill-security/index.d.ts +9 -0
  15. package/dist/src/core/skill-security/index.d.ts.map +1 -0
  16. package/dist/src/core/skill-security/index.js +5 -0
  17. package/dist/src/core/skill-security/index.js.map +1 -0
  18. package/dist/src/core/skill-security/parser.d.ts +27 -0
  19. package/dist/src/core/skill-security/parser.d.ts.map +1 -0
  20. package/dist/src/core/skill-security/parser.js +55 -0
  21. package/dist/src/core/skill-security/parser.js.map +1 -0
  22. package/dist/src/core/skill-security/reporter.d.ts +21 -0
  23. package/dist/src/core/skill-security/reporter.d.ts.map +1 -0
  24. package/dist/src/core/skill-security/reporter.js +121 -0
  25. package/dist/src/core/skill-security/reporter.js.map +1 -0
  26. package/dist/src/core/skill-security/rules.d.ts +25 -0
  27. package/dist/src/core/skill-security/rules.d.ts.map +1 -0
  28. package/dist/src/core/skill-security/rules.js +137 -0
  29. package/dist/src/core/skill-security/rules.js.map +1 -0
  30. package/dist/src/core/skill-security/scanner.d.ts +41 -0
  31. package/dist/src/core/skill-security/scanner.d.ts.map +1 -0
  32. package/dist/src/core/skill-security/scanner.js +78 -0
  33. package/dist/src/core/skill-security/scanner.js.map +1 -0
  34. package/package.json +1 -1
  35. package/plugins/specweave/hooks/lib/score-increment.sh +87 -0
  36. package/plugins/specweave/hooks/stop-auto-v5.sh +55 -9
  37. package/plugins/specweave/hooks/tests/test-auto-context-integration.sh +126 -0
  38. package/plugins/specweave/hooks/tests/test-stop-auto-enriched.sh +128 -0
  39. package/plugins/specweave/hooks/user-prompt-submit.sh +99 -150
  40. package/plugins/specweave/scripts/setup-auto.sh +58 -4
  41. package/plugins/specweave/scripts/tests/test-setup-auto-selection.sh +74 -0
  42. package/plugins/specweave/scripts/tests/test-setup-auto-usergoal.sh +83 -0
  43. package/plugins/specweave/skills/auto/SKILL.md +3 -1
  44. package/plugins/specweave/skills/do/SKILL.md +11 -0
  45. package/plugins/specweave/skills/increment/SKILL.md +8 -2
  46. package/plugins/specweave/skills/team-lead/SKILL.md +69 -5
  47. package/plugins/specweave-jira/skills/jira-mapper/SKILL.md +13 -14
  48. package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +74 -4
  49. 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.299",
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. Scan active increments
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
- [ -z "$ILIST" ] && ILIST="$id|$p|$a" || ILIST="$ILIST,$id|$p|$a"
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
- # 8. All complete → approve
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
- # 9. Work remains → block with concise message
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
- IFS=',' read -ra ENTRIES <<< "$ILIST"
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
- DETAILS="${DETAILS}\n - ${eid}: ${ep} tasks pending, ${ea} ACs open"
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
- BMSG="Auto Mode: ${IC} increment(s) need work${DETAILS}\nTurn $TURN/$MAX_TURNS | Continue: /sw:do | Complete: /sw:done"
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
- '{pendingTasks:$p,openAcs:$a,incompleteIncrements:$i,increments:$il,turn:{current:$t,max:$mt}}')" \
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