specweave 0.33.4 → 0.33.5

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 (66) hide show
  1. package/CLAUDE.md +20 -12
  2. package/dist/src/cli/commands/jobs.js +19 -2
  3. package/dist/src/cli/commands/jobs.js.map +1 -1
  4. package/dist/src/cli/commands/living-docs.js +1 -1
  5. package/dist/src/cli/commands/living-docs.js.map +1 -1
  6. package/dist/src/cli/helpers/init/external-import-grouping.d.ts.map +1 -1
  7. package/dist/src/cli/helpers/init/external-import-grouping.js +11 -7
  8. package/dist/src/cli/helpers/init/external-import-grouping.js.map +1 -1
  9. package/dist/src/cli/workers/clone-worker.js +22 -5
  10. package/dist/src/cli/workers/clone-worker.js.map +1 -1
  11. package/dist/src/core/background/job-dependency.d.ts.map +1 -1
  12. package/dist/src/core/background/job-dependency.js +1 -0
  13. package/dist/src/core/background/job-dependency.js.map +1 -1
  14. package/dist/src/core/background/job-launcher.js +2 -2
  15. package/dist/src/core/background/job-launcher.js.map +1 -1
  16. package/dist/src/core/background/job-manager.d.ts +8 -0
  17. package/dist/src/core/background/job-manager.d.ts.map +1 -1
  18. package/dist/src/core/background/job-manager.js +19 -1
  19. package/dist/src/core/background/job-manager.js.map +1 -1
  20. package/dist/src/core/background/types.d.ts +9 -1
  21. package/dist/src/core/background/types.d.ts.map +1 -1
  22. package/dist/src/core/background/types.js +8 -1
  23. package/dist/src/core/background/types.js.map +1 -1
  24. package/dist/src/importers/external-importer.d.ts +26 -5
  25. package/dist/src/importers/external-importer.d.ts.map +1 -1
  26. package/dist/src/importers/item-converter.d.ts.map +1 -1
  27. package/dist/src/importers/item-converter.js +18 -1
  28. package/dist/src/importers/item-converter.js.map +1 -1
  29. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  30. package/dist/src/importers/jira-importer.js +15 -1
  31. package/dist/src/importers/jira-importer.js.map +1 -1
  32. package/dist/src/living-docs/smart-doc-organizer.js +1 -1
  33. package/dist/src/living-docs/smart-doc-organizer.js.map +1 -1
  34. package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
  35. package/dist/src/utils/docs-preview/config-generator.js +4 -0
  36. package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
  37. package/dist/src/utils/notification-constants.d.ts +8 -6
  38. package/dist/src/utils/notification-constants.d.ts.map +1 -1
  39. package/dist/src/utils/notification-constants.js +8 -6
  40. package/dist/src/utils/notification-constants.js.map +1 -1
  41. package/dist/src/utils/notification-manager.d.ts +24 -0
  42. package/dist/src/utils/notification-manager.d.ts.map +1 -1
  43. package/dist/src/utils/notification-manager.js +29 -0
  44. package/dist/src/utils/notification-manager.js.map +1 -1
  45. package/package.json +1 -1
  46. package/plugins/specweave/commands/specweave-judge-llm.md +296 -0
  47. package/plugins/specweave/commands/specweave-organize-docs.md +2 -2
  48. package/plugins/specweave/hooks/hooks.json +10 -0
  49. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +87 -0
  50. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.test.sh +302 -0
  51. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +72 -18
  52. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +406 -0
  53. package/plugins/specweave/scripts/session-watchdog.sh +10 -4
  54. package/plugins/specweave/skills/increment-planner/SKILL.md +1 -1
  55. package/plugins/specweave-docs/commands/build.md +4 -4
  56. package/plugins/specweave-docs/commands/generate.md +1 -1
  57. package/plugins/specweave-docs/commands/health.md +1 -1
  58. package/plugins/specweave-docs/commands/init.md +1 -1
  59. package/plugins/specweave-docs/commands/organize.md +2 -2
  60. package/plugins/specweave-docs/commands/validate.md +1 -1
  61. package/plugins/specweave-docs/commands/view.md +391 -0
  62. package/plugins/specweave-docs/skills/preview/SKILL.md +56 -17
  63. package/src/templates/AGENTS.md.template +24 -28
  64. package/src/templates/CLAUDE.md.template +12 -8
  65. package/plugins/specweave/commands/specweave-judge.md +0 -276
  66. package/plugins/specweave-docs/commands/preview.md +0 -274
@@ -40,6 +40,16 @@
40
40
  }
41
41
  ]
42
42
  },
43
+ {
44
+ "matcher": "Write",
45
+ "matcher_content": "\\.specweave/increments/\\d{3,4}E?-[^/]+/spec\\.md",
46
+ "hooks": [
47
+ {
48
+ "type": "command",
49
+ "command": "bash -c 'W=\"${CLAUDE_PLUGIN_ROOT}/hooks/universal/fail-fast-wrapper.sh\"; S=\"${CLAUDE_PLUGIN_ROOT}/hooks/v2/guards/metadata-json-guard.sh\"; [[ -x \"$W\" ]] && exec \"$W\" \"$S\" || (cat >/dev/null && printf \"{\\\"decision\\\":\\\"allow\\\"}\")'"
50
+ }
51
+ ]
52
+ },
43
53
  {
44
54
  "matcher": "Write",
45
55
  "matcher_content": "\\.specweave/increments/\\d{3,4}E?-[^/]+/spec\\.md",
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+ #
3
+ # metadata-json-guard.sh
4
+ #
5
+ # Pre-tool-use hook that ensures metadata.json exists BEFORE spec.md can be created.
6
+ # This prevents increments from being created without proper metadata.
7
+ #
8
+ # ROOT CAUSE: When Claude creates increments via user prompt (not /specweave:increment),
9
+ # metadata.json may be forgotten, causing:
10
+ # - Status tracking broken
11
+ # - WIP limits don't work
12
+ # - External sync fails (GitHub/Jira/ADO)
13
+ # - All increment commands fail
14
+ #
15
+ # SOLUTION: Block spec.md creation if metadata.json doesn't exist in same increment folder.
16
+ # Claude MUST create metadata.json FIRST, then spec.md.
17
+ #
18
+ # Activation:
19
+ # - tool_name: Write
20
+ # - file_path matches: .specweave/increments/*/spec.md
21
+ #
22
+ # Returns exit code 2 (block) if metadata.json missing, 0 (allow) otherwise.
23
+ #
24
+ # Bypass: Set SPECWEAVE_FORCE_METADATA=1 to skip validation
25
+ #
26
+ # v0.34.0 - Initial implementation based on user project bug analysis
27
+
28
+ set -e
29
+
30
+ # Check for force bypass
31
+ if [ "$SPECWEAVE_FORCE_METADATA" = "1" ]; then
32
+ echo '{"decision": "allow", "message": "metadata.json guard bypassed (SPECWEAVE_FORCE_METADATA=1)"}'
33
+ exit 0
34
+ fi
35
+
36
+ # Disable hooks bypass
37
+ if [ "$SPECWEAVE_DISABLE_HOOKS" = "1" ]; then
38
+ echo '{"decision": "allow"}'
39
+ exit 0
40
+ fi
41
+
42
+ # Read tool input from stdin
43
+ INPUT=$(cat)
44
+
45
+ # Extract tool name
46
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .tool_input.tool_name // ""' 2>/dev/null || echo "")
47
+
48
+ # Only validate Write tool calls
49
+ if [ "$TOOL_NAME" != "Write" ]; then
50
+ echo '{"decision": "allow"}'
51
+ exit 0
52
+ fi
53
+
54
+ # Extract file path - handle both formats
55
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
56
+
57
+ # Only validate spec.md files in increments folder
58
+ # Match: 3-4 digits, optional E suffix, kebab-case name, spec.md
59
+ if [[ ! "$FILE_PATH" =~ \.specweave/increments/([0-9]{3,4}E?-[^/]+)/spec\.md$ ]]; then
60
+ echo '{"decision": "allow"}'
61
+ exit 0
62
+ fi
63
+
64
+ # Extract increment folder path
65
+ INCREMENT_DIR=$(dirname "$FILE_PATH")
66
+ INCREMENT_ID="${BASH_REMATCH[1]}"
67
+
68
+ # Check if metadata.json exists in the same increment folder
69
+ METADATA_PATH="${INCREMENT_DIR}/metadata.json"
70
+
71
+ if [ -f "$METADATA_PATH" ]; then
72
+ # metadata.json exists, allow spec.md creation
73
+ echo '{"decision": "allow"}'
74
+ exit 0
75
+ fi
76
+
77
+ # metadata.json doesn't exist - BLOCK spec.md creation
78
+ NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
79
+
80
+ cat << BLOCK_EOF
81
+ {
82
+ "decision": "block",
83
+ "reason": "🚫 BLOCKED: Cannot create spec.md without metadata.json\n\n⚠️ CRITICAL RULE: metadata.json MUST be created FIRST!\n\nWithout metadata.json:\n - ❌ Status tracking broken\n - ❌ WIP limits don't work\n - ❌ External sync fails (GitHub/Jira/ADO)\n - ❌ All increment commands fail\n\n📋 CORRECT WORKFLOW:\n1. Create metadata.json FIRST:\n Write({\n file_path: \"${METADATA_PATH}\",\n content: {\n \"id\": \"${INCREMENT_ID}\",\n \"status\": \"planned\",\n \"type\": \"feature\",\n \"priority\": \"P1\",\n \"created\": \"${NOW}\",\n \"lastActivity\": \"${NOW}\",\n \"testMode\": \"TDD\",\n \"coverageTarget\": 95,\n \"feature_id\": null,\n \"epic_id\": null,\n \"externalLinks\": {}\n }\n })\n\n2. THEN create spec.md\n\n📖 See: CLAUDE.md section '3. metadata.json is MANDATORY'\n\n💡 Bypass: Set SPECWEAVE_FORCE_METADATA=1 to skip this validation"
84
+ }
85
+ BLOCK_EOF
86
+
87
+ exit 2
@@ -0,0 +1,302 @@
1
+ #!/bin/bash
2
+ # Comprehensive test suite for metadata-json-guard.sh
3
+ # Tests that spec.md creation is blocked when metadata.json is missing
4
+ #
5
+ # Usage: bash metadata-json-guard.test.sh
6
+ #
7
+ # v0.34.0 - Initial test suite
8
+
9
+ set -e
10
+
11
+ GUARD="$(dirname "$0")/metadata-json-guard.sh"
12
+ TEST_DIR=$(mktemp -d)
13
+ PASS=0
14
+ FAIL=0
15
+ TOTAL=0
16
+
17
+ # Colors for output
18
+ RED='\033[0;31m'
19
+ GREEN='\033[0;32m'
20
+ YELLOW='\033[1;33m'
21
+ NC='\033[0m' # No Color
22
+
23
+ cleanup() {
24
+ rm -rf "$TEST_DIR"
25
+ }
26
+ trap cleanup EXIT
27
+
28
+ # Create test increment structure
29
+ setup_test_increment() {
30
+ local with_metadata="$1"
31
+ local increment_name="${2:-0001-test-feature}"
32
+
33
+ mkdir -p "$TEST_DIR/.specweave/increments/$increment_name"
34
+
35
+ if [ "$with_metadata" = "true" ]; then
36
+ cat > "$TEST_DIR/.specweave/increments/$increment_name/metadata.json" << 'EOF'
37
+ {
38
+ "id": "0001-test-feature",
39
+ "status": "planned",
40
+ "type": "feature",
41
+ "priority": "P1",
42
+ "created": "2025-12-10T00:00:00Z",
43
+ "lastActivity": "2025-12-10T00:00:00Z",
44
+ "testMode": "TDD",
45
+ "coverageTarget": 95,
46
+ "feature_id": null,
47
+ "epic_id": null,
48
+ "externalLinks": {}
49
+ }
50
+ EOF
51
+ fi
52
+ }
53
+
54
+ # Test helper: should block
55
+ test_should_block() {
56
+ local name="$1"
57
+ local file_path="$2"
58
+ local content="$3"
59
+ TOTAL=$((TOTAL + 1))
60
+
61
+ # Build JSON input for the guard
62
+ local json_input
63
+ json_input=$(jq -n \
64
+ --arg tool_name "Write" \
65
+ --arg file_path "$file_path" \
66
+ --arg content "$content" \
67
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
68
+
69
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
70
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
71
+
72
+ if [[ "$exit_code" == "2" ]]; then
73
+ echo -e "${GREEN}✓ BLOCKED${NC}: $name"
74
+ PASS=$((PASS + 1))
75
+ else
76
+ echo -e "${RED}✗ NOT BLOCKED${NC}: $name"
77
+ echo " File: $file_path"
78
+ echo " Exit code: $exit_code (expected 2)"
79
+ echo " Output: $(echo "$result" | head -5)"
80
+ FAIL=$((FAIL + 1))
81
+ fi
82
+ }
83
+
84
+ # Test helper: should allow
85
+ test_should_allow() {
86
+ local name="$1"
87
+ local file_path="$2"
88
+ local content="$3"
89
+ TOTAL=$((TOTAL + 1))
90
+
91
+ # Build JSON input for the guard
92
+ local json_input
93
+ json_input=$(jq -n \
94
+ --arg tool_name "Write" \
95
+ --arg file_path "$file_path" \
96
+ --arg content "$content" \
97
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
98
+
99
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
100
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
101
+
102
+ if [[ "$exit_code" == "0" ]]; then
103
+ echo -e "${GREEN}✓ ALLOWED${NC}: $name"
104
+ PASS=$((PASS + 1))
105
+ else
106
+ echo -e "${RED}✗ WRONGLY BLOCKED${NC}: $name"
107
+ echo " File: $file_path"
108
+ echo " Exit code: $exit_code (expected 0)"
109
+ FAIL=$((FAIL + 1))
110
+ fi
111
+ }
112
+
113
+ # Test non-Write tools (should always allow)
114
+ test_non_write_tool() {
115
+ local name="$1"
116
+ local tool_name="$2"
117
+ TOTAL=$((TOTAL + 1))
118
+
119
+ local json_input
120
+ json_input=$(jq -n \
121
+ --arg tool_name "$tool_name" \
122
+ --arg file_path "$TEST_DIR/.specweave/increments/0001-test/spec.md" \
123
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path}}')
124
+
125
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
126
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
127
+
128
+ if [[ "$exit_code" == "0" ]]; then
129
+ echo -e "${GREEN}✓ ALLOWED (non-Write)${NC}: $name"
130
+ PASS=$((PASS + 1))
131
+ else
132
+ echo -e "${RED}✗ WRONGLY BLOCKED${NC}: $name"
133
+ FAIL=$((FAIL + 1))
134
+ fi
135
+ }
136
+
137
+ echo "========================================"
138
+ echo " METADATA JSON GUARD - COMPREHENSIVE TESTS"
139
+ echo "========================================"
140
+ echo ""
141
+ echo "Test directory: $TEST_DIR"
142
+ echo ""
143
+
144
+ echo -e "${YELLOW}=== CORE FUNCTIONALITY: BLOCK WHEN METADATA MISSING ===${NC}"
145
+
146
+ # Setup without metadata
147
+ setup_test_increment "false" "0001-no-metadata"
148
+ test_should_block "spec.md without metadata.json (3-digit)" \
149
+ "$TEST_DIR/.specweave/increments/0001-no-metadata/spec.md" \
150
+ "---\nincrement: 0001-no-metadata\n---"
151
+
152
+ setup_test_increment "false" "0012-no-metadata"
153
+ test_should_block "spec.md without metadata.json (4-digit)" \
154
+ "$TEST_DIR/.specweave/increments/0012-no-metadata/spec.md" \
155
+ "---\nincrement: 0012-no-metadata\n---"
156
+
157
+ setup_test_increment "false" "123-short"
158
+ test_should_block "spec.md without metadata.json (3-digit short)" \
159
+ "$TEST_DIR/.specweave/increments/123-short/spec.md" \
160
+ "---\nincrement: 123-short\n---"
161
+
162
+ setup_test_increment "false" "0001E-external"
163
+ test_should_block "spec.md without metadata.json (external E suffix)" \
164
+ "$TEST_DIR/.specweave/increments/0001E-external/spec.md" \
165
+ "---\nincrement: 0001E-external\n---"
166
+
167
+ echo ""
168
+ echo -e "${YELLOW}=== CORE FUNCTIONALITY: ALLOW WHEN METADATA EXISTS ===${NC}"
169
+
170
+ # Setup with metadata
171
+ setup_test_increment "true" "0002-has-metadata"
172
+ test_should_allow "spec.md WITH metadata.json present" \
173
+ "$TEST_DIR/.specweave/increments/0002-has-metadata/spec.md" \
174
+ "---\nincrement: 0002-has-metadata\n---"
175
+
176
+ setup_test_increment "true" "0123E-external-with-meta"
177
+ test_should_allow "spec.md WITH metadata.json (external E suffix)" \
178
+ "$TEST_DIR/.specweave/increments/0123E-external-with-meta/spec.md" \
179
+ "---\nincrement: 0123E-external-with-meta\n---"
180
+
181
+ echo ""
182
+ echo -e "${YELLOW}=== NON-SPEC FILES: SHOULD NOT VALIDATE ===${NC}"
183
+
184
+ # Create increment without metadata for these tests
185
+ setup_test_increment "false" "0003-other-files"
186
+ test_should_allow "plan.md (not spec.md)" \
187
+ "$TEST_DIR/.specweave/increments/0003-other-files/plan.md" \
188
+ "# Plan"
189
+
190
+ test_should_allow "tasks.md (not spec.md)" \
191
+ "$TEST_DIR/.specweave/increments/0003-other-files/tasks.md" \
192
+ "# Tasks"
193
+
194
+ test_should_allow "metadata.json itself" \
195
+ "$TEST_DIR/.specweave/increments/0003-other-files/metadata.json" \
196
+ '{"id": "test"}'
197
+
198
+ test_should_allow "report in subfolder" \
199
+ "$TEST_DIR/.specweave/increments/0003-other-files/reports/COMPLETION.md" \
200
+ "# Report"
201
+
202
+ echo ""
203
+ echo -e "${YELLOW}=== FILES OUTSIDE INCREMENTS: SHOULD NOT VALIDATE ===${NC}"
204
+
205
+ test_should_allow "spec.md outside increments (docs)" \
206
+ "$TEST_DIR/.specweave/docs/internal/spec.md" \
207
+ "# Documentation"
208
+
209
+ test_should_allow "spec.md in root" \
210
+ "$TEST_DIR/spec.md" \
211
+ "# Root spec"
212
+
213
+ test_should_allow "Random file" \
214
+ "$TEST_DIR/random.txt" \
215
+ "Random content"
216
+
217
+ echo ""
218
+ echo -e "${YELLOW}=== NON-WRITE TOOLS: SHOULD ALWAYS ALLOW ===${NC}"
219
+
220
+ test_non_write_tool "Read tool" "Read"
221
+ test_non_write_tool "Edit tool" "Edit"
222
+ test_non_write_tool "Glob tool" "Glob"
223
+ test_non_write_tool "Grep tool" "Grep"
224
+ test_non_write_tool "Bash tool" "Bash"
225
+
226
+ echo ""
227
+ echo -e "${YELLOW}=== BYPASS MODES ===${NC}"
228
+
229
+ # Test SPECWEAVE_FORCE_METADATA bypass
230
+ setup_test_increment "false" "0004-bypass"
231
+ TOTAL=$((TOTAL + 1))
232
+ json_input=$(jq -n \
233
+ --arg tool_name "Write" \
234
+ --arg file_path "$TEST_DIR/.specweave/increments/0004-bypass/spec.md" \
235
+ --arg content "---\nincrement: 0004-bypass\n---" \
236
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
237
+
238
+ result=$(SPECWEAVE_FORCE_METADATA=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
239
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
240
+ if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q "bypassed"; then
241
+ echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_FORCE_METADATA=1 works"
242
+ PASS=$((PASS + 1))
243
+ else
244
+ echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_FORCE_METADATA=1"
245
+ echo " Exit code: $exit_code"
246
+ FAIL=$((FAIL + 1))
247
+ fi
248
+
249
+ # Test SPECWEAVE_DISABLE_HOOKS bypass
250
+ TOTAL=$((TOTAL + 1))
251
+ result=$(SPECWEAVE_DISABLE_HOOKS=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
252
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
253
+ if [[ "$exit_code" == "0" ]]; then
254
+ echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_DISABLE_HOOKS=1 works"
255
+ PASS=$((PASS + 1))
256
+ else
257
+ echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_DISABLE_HOOKS=1"
258
+ FAIL=$((FAIL + 1))
259
+ fi
260
+
261
+ echo ""
262
+ echo -e "${YELLOW}=== EDGE CASES ===${NC}"
263
+
264
+ # Test with empty file path
265
+ TOTAL=$((TOTAL + 1))
266
+ json_input='{"tool_name": "Write", "tool_input": {"file_path": "", "content": "test"}}'
267
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
268
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
269
+ if [[ "$exit_code" == "0" ]]; then
270
+ echo -e "${GREEN}✓ ALLOWED${NC}: Empty file path"
271
+ PASS=$((PASS + 1))
272
+ else
273
+ echo -e "${RED}✗ FAILED${NC}: Empty file path should be allowed"
274
+ FAIL=$((FAIL + 1))
275
+ fi
276
+
277
+ # Test with malformed JSON (should fail gracefully)
278
+ TOTAL=$((TOTAL + 1))
279
+ result=$(echo "not json" | bash "$GUARD" 2>&1; echo "EXIT:$?")
280
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
281
+ if [[ "$exit_code" == "0" ]]; then
282
+ echo -e "${GREEN}✓ ALLOWED${NC}: Malformed JSON handled gracefully"
283
+ PASS=$((PASS + 1))
284
+ else
285
+ echo -e "${RED}✗ FAILED${NC}: Malformed JSON should be allowed (fail-safe)"
286
+ FAIL=$((FAIL + 1))
287
+ fi
288
+
289
+ echo ""
290
+ echo "========================================"
291
+ echo " RESULTS"
292
+ echo "========================================"
293
+ echo -e "Total: $TOTAL"
294
+ echo -e "${GREEN}Passed: $PASS${NC}"
295
+ if [[ $FAIL -gt 0 ]]; then
296
+ echo -e "${RED}Failed: $FAIL${NC}"
297
+ exit 1
298
+ else
299
+ echo -e "Failed: 0"
300
+ echo ""
301
+ echo -e "${GREEN}ALL TESTS PASSED!${NC}"
302
+ fi
@@ -12,9 +12,16 @@
12
12
  # - file_path matches: .specweave/increments/*/spec.md
13
13
  #
14
14
  # Rules:
15
- # - Each ### US-XXX section MUST have **Project**: <value> on next few lines
15
+ # - Each US section MUST have **Project**: <value> on next few lines
16
16
  # - For 2-level structures, each US MUST also have **Board**: <value>
17
17
  # - Project values MUST match configured projects (not generic keywords)
18
+ # - Supports ALL US formats:
19
+ # - ### US-001: (simple)
20
+ # - #### US-001: (4 hashes)
21
+ # - ### US-FE-001: (with project prefix)
22
+ # - #### US-FE-001: (multi-project)
23
+ # - #### US-BE-001: (backend)
24
+ # - #### US-SHARED-001: (shared)
18
25
  # - Fallback allowed for existing specs via SPECWEAVE_LEGACY_SPEC=1
19
26
  #
20
27
  # Returns exit code 1 (block) if validation fails, 0 (allow) otherwise.
@@ -23,6 +30,7 @@
23
30
  # - SPECWEAVE_FORCE_PROJECT=1 - Skip all project validation
24
31
  # - SPECWEAVE_LEGACY_SPEC=1 - Allow specs without per-US project (legacy mode)
25
32
  #
33
+ # v0.34.0 - Fixed to handle all US ID formats (US-001, US-FE-001, etc.)
26
34
 
27
35
  set -e
28
36
 
@@ -75,8 +83,13 @@ log_debug "Validating per-US project fields for: $FILE_PATH"
75
83
  # Extract file content
76
84
  CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
77
85
 
78
- # Count User Stories (### US-XXX pattern)
79
- US_PATTERN='### US-[0-9]+'
86
+ # Count User Stories - support ALL formats:
87
+ # - ### US-001: (simple, 3 hashes)
88
+ # - #### US-001: (4 hashes)
89
+ # - ### US-FE-001: (with project prefix like FE, BE, SHARED)
90
+ # - #### US-FE-001: (multi-project with 4 hashes)
91
+ # Pattern: 3-4 hashes, space, US-, optional prefix (letters+dash), digits, colon
92
+ US_PATTERN='^#{3,4} US-([A-Z]+-)?[0-9]+:'
80
93
  TOTAL_US=$(echo "$CONTENT" | grep -cE "$US_PATTERN" || echo 0)
81
94
 
82
95
  log_debug "Total User Stories found: $TOTAL_US"
@@ -88,7 +101,7 @@ if [ "$TOTAL_US" -eq 0 ]; then
88
101
  fi
89
102
 
90
103
  # Extract User Story sections and check for **Project**: field
91
- # Strategy: For each ### US-XXX, look at next 10 lines for **Project**:
104
+ # Strategy: For each US heading, look at next 10 lines for **Project**:
92
105
 
93
106
  MISSING_PROJECT=()
94
107
  MISSING_BOARD=()
@@ -96,26 +109,46 @@ MULTI_PROJECT=() # USs with multiple projects (comma-separated)
96
109
  MULTI_BOARD=() # USs with multiple boards (comma-separated)
97
110
  US_WITH_PROJECT=0
98
111
 
99
- # Use awk to extract US sections and check for Project field
112
+ # Use grep to find all US headings (both ### and ####, with or without prefix)
100
113
  while IFS= read -r us_line; do
101
- # Extract US ID (e.g., US-001)
102
- US_ID=$(echo "$us_line" | grep -oE 'US-[0-9]+' | head -1)
114
+ # Extract US ID - handles US-001, US-FE-001, US-BE-001, US-SHARED-001, etc.
115
+ US_ID=$(echo "$us_line" | grep -oE 'US-([A-Z]+-)?[0-9]+' | head -1)
103
116
 
104
117
  if [ -z "$US_ID" ]; then
105
118
  continue
106
119
  fi
107
120
 
108
- # Get line number of this US heading
109
- LINE_NUM=$(echo "$CONTENT" | grep -nE "^### $US_ID:" | head -1 | cut -d: -f1)
121
+ # Get line number of this US heading (works with both ### and ####)
122
+ # Escape the US_ID for grep (the dash needs no escape, but be safe)
123
+ LINE_NUM=$(echo "$CONTENT" | grep -nE "^#{3,4} ${US_ID}:" | head -1 | cut -d: -f1)
110
124
 
111
125
  if [ -z "$LINE_NUM" ]; then
126
+ log_debug "Could not find line number for $US_ID"
112
127
  continue
113
128
  fi
114
129
 
115
- # Extract next 10 lines after heading
116
- SECTION=$(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)) | head -n 10)
130
+ # Extract lines after heading UNTIL next US heading, separator (---), or max 15 lines
131
+ # This prevents reading **Project** from a DIFFERENT user story
132
+ SECTION=""
133
+ line_count=0
134
+ while IFS= read -r line; do
135
+ # Stop at next US heading (### US- or #### US-)
136
+ if [[ "$line" =~ ^#{3,4}\ US- ]] && [ "$line_count" -gt 0 ]; then
137
+ break
138
+ fi
139
+ # Stop at separator
140
+ if [[ "$line" =~ ^---$ ]] && [ "$line_count" -gt 0 ]; then
141
+ break
142
+ fi
143
+ # Max 15 lines
144
+ if [ "$line_count" -ge 15 ]; then
145
+ break
146
+ fi
147
+ SECTION+="$line"$'\n'
148
+ line_count=$((line_count + 1))
149
+ done < <(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)))
117
150
 
118
- # Check for **Project**: field
151
+ # Check for **Project**: field within this US section only
119
152
  PROJECT_LINE=$(echo "$SECTION" | grep -E '^\*\*Project\*\*:\s*\S' | head -1)
120
153
 
121
154
  if [ -n "$PROJECT_LINE" ]; then
@@ -136,7 +169,7 @@ while IFS= read -r us_line; do
136
169
  log_debug "$US_ID MISSING **Project**: field ✗"
137
170
  fi
138
171
 
139
- done < <(echo "$CONTENT" | grep -E "^### US-[0-9]+:")
172
+ done < <(echo "$CONTENT" | grep -E "^#{3,4} US-([A-Z]+-)?[0-9]+:")
140
173
 
141
174
  log_debug "User Stories with **Project**: $US_WITH_PROJECT / $TOTAL_US"
142
175
 
@@ -167,21 +200,42 @@ log_debug "Structure level: $STRUCTURE_LEVEL"
167
200
  # For 2-level structures, also check for **Board**: field
168
201
  if [ "$STRUCTURE_LEVEL" = "2" ]; then
169
202
  while IFS= read -r us_line; do
170
- US_ID=$(echo "$us_line" | grep -oE 'US-[0-9]+' | head -1)
203
+ # Extract US ID - handles all formats (US-001, US-FE-001, etc.)
204
+ US_ID=$(echo "$us_line" | grep -oE 'US-([A-Z]+-)?[0-9]+' | head -1)
171
205
 
172
206
  if [ -z "$US_ID" ]; then
173
207
  continue
174
208
  fi
175
209
 
176
- LINE_NUM=$(echo "$CONTENT" | grep -nE "^### $US_ID:" | head -1 | cut -d: -f1)
210
+ # Get line number (works with both ### and ####)
211
+ LINE_NUM=$(echo "$CONTENT" | grep -nE "^#{3,4} ${US_ID}:" | head -1 | cut -d: -f1)
177
212
 
178
213
  if [ -z "$LINE_NUM" ]; then
214
+ log_debug "Could not find line number for $US_ID (board check)"
179
215
  continue
180
216
  fi
181
217
 
182
- SECTION=$(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)) | head -n 10)
218
+ # Extract lines after heading UNTIL next US heading, separator, or max 15 lines
219
+ SECTION=""
220
+ line_count=0
221
+ while IFS= read -r line; do
222
+ # Stop at next US heading
223
+ if [[ "$line" =~ ^#{3,4}\ US- ]] && [ "$line_count" -gt 0 ]; then
224
+ break
225
+ fi
226
+ # Stop at separator
227
+ if [[ "$line" =~ ^---$ ]] && [ "$line_count" -gt 0 ]; then
228
+ break
229
+ fi
230
+ # Max 15 lines
231
+ if [ "$line_count" -ge 15 ]; then
232
+ break
233
+ fi
234
+ SECTION+="$line"$'\n'
235
+ line_count=$((line_count + 1))
236
+ done < <(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)))
183
237
 
184
- # Check for **Board**: field
238
+ # Check for **Board**: field within this US section only
185
239
  BOARD_LINE=$(echo "$SECTION" | grep -E '^\*\*Board\*\*:\s*\S' | head -1)
186
240
 
187
241
  if [ -n "$BOARD_LINE" ]; then
@@ -200,7 +254,7 @@ if [ "$STRUCTURE_LEVEL" = "2" ]; then
200
254
  log_debug "$US_ID MISSING **Board**: field ✗"
201
255
  fi
202
256
 
203
- done < <(echo "$CONTENT" | grep -E "^### US-[0-9]+:")
257
+ done < <(echo "$CONTENT" | grep -E "^#{3,4} US-([A-Z]+-)?[0-9]+:")
204
258
  fi
205
259
 
206
260
  # Build error message if validation fails