specweave 0.33.3 → 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 (106) hide show
  1. package/CLAUDE.md +85 -19
  2. package/dist/src/cli/cleanup-zombies.js +8 -5
  3. package/dist/src/cli/cleanup-zombies.js.map +1 -1
  4. package/dist/src/cli/commands/jobs.js +19 -2
  5. package/dist/src/cli/commands/jobs.js.map +1 -1
  6. package/dist/src/cli/commands/living-docs.js +1 -1
  7. package/dist/src/cli/commands/living-docs.js.map +1 -1
  8. package/dist/src/cli/helpers/init/external-import-grouping.d.ts.map +1 -1
  9. package/dist/src/cli/helpers/init/external-import-grouping.js +11 -7
  10. package/dist/src/cli/helpers/init/external-import-grouping.js.map +1 -1
  11. package/dist/src/cli/workers/clone-worker.js +22 -5
  12. package/dist/src/cli/workers/clone-worker.js.map +1 -1
  13. package/dist/src/config/types.d.ts +203 -1208
  14. package/dist/src/config/types.d.ts.map +1 -1
  15. package/dist/src/core/background/job-dependency.d.ts.map +1 -1
  16. package/dist/src/core/background/job-dependency.js +1 -0
  17. package/dist/src/core/background/job-dependency.js.map +1 -1
  18. package/dist/src/core/background/job-launcher.js +2 -2
  19. package/dist/src/core/background/job-launcher.js.map +1 -1
  20. package/dist/src/core/background/job-manager.d.ts +8 -0
  21. package/dist/src/core/background/job-manager.d.ts.map +1 -1
  22. package/dist/src/core/background/job-manager.js +19 -1
  23. package/dist/src/core/background/job-manager.js.map +1 -1
  24. package/dist/src/core/background/types.d.ts +9 -1
  25. package/dist/src/core/background/types.d.ts.map +1 -1
  26. package/dist/src/core/background/types.js +8 -1
  27. package/dist/src/core/background/types.js.map +1 -1
  28. package/dist/src/importers/external-importer.d.ts +26 -5
  29. package/dist/src/importers/external-importer.d.ts.map +1 -1
  30. package/dist/src/importers/item-converter.d.ts.map +1 -1
  31. package/dist/src/importers/item-converter.js +18 -1
  32. package/dist/src/importers/item-converter.js.map +1 -1
  33. package/dist/src/importers/jira-importer.d.ts +10 -0
  34. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  35. package/dist/src/importers/jira-importer.js +70 -6
  36. package/dist/src/importers/jira-importer.js.map +1 -1
  37. package/dist/src/init/architecture/types.d.ts +33 -140
  38. package/dist/src/init/architecture/types.d.ts.map +1 -1
  39. package/dist/src/init/compliance/types.d.ts +30 -27
  40. package/dist/src/init/compliance/types.d.ts.map +1 -1
  41. package/dist/src/init/repo/types.d.ts +11 -34
  42. package/dist/src/init/repo/types.d.ts.map +1 -1
  43. package/dist/src/init/research/src/config/types.d.ts +15 -82
  44. package/dist/src/init/research/src/config/types.d.ts.map +1 -1
  45. package/dist/src/init/research/types.d.ts +38 -93
  46. package/dist/src/init/research/types.d.ts.map +1 -1
  47. package/dist/src/init/team/types.d.ts +4 -42
  48. package/dist/src/init/team/types.d.ts.map +1 -1
  49. package/dist/src/living-docs/smart-doc-organizer.js +1 -1
  50. package/dist/src/living-docs/smart-doc-organizer.js.map +1 -1
  51. package/dist/src/sync/closure-metrics.d.ts +102 -0
  52. package/dist/src/sync/closure-metrics.d.ts.map +1 -0
  53. package/dist/src/sync/closure-metrics.js +267 -0
  54. package/dist/src/sync/closure-metrics.js.map +1 -0
  55. package/dist/src/sync/sync-coordinator.d.ts +29 -0
  56. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  57. package/dist/src/sync/sync-coordinator.js +153 -16
  58. package/dist/src/sync/sync-coordinator.js.map +1 -1
  59. package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
  60. package/dist/src/utils/docs-preview/config-generator.js +4 -0
  61. package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
  62. package/dist/src/utils/notification-constants.d.ts +87 -0
  63. package/dist/src/utils/notification-constants.d.ts.map +1 -0
  64. package/dist/src/utils/notification-constants.js +131 -0
  65. package/dist/src/utils/notification-constants.js.map +1 -0
  66. package/dist/src/utils/notification-manager.d.ts +24 -0
  67. package/dist/src/utils/notification-manager.d.ts.map +1 -1
  68. package/dist/src/utils/notification-manager.js +29 -0
  69. package/dist/src/utils/notification-manager.js.map +1 -1
  70. package/dist/src/utils/platform-utils.d.ts +13 -3
  71. package/dist/src/utils/platform-utils.d.ts.map +1 -1
  72. package/dist/src/utils/platform-utils.js +17 -6
  73. package/dist/src/utils/platform-utils.js.map +1 -1
  74. package/package.json +1 -1
  75. package/plugins/specweave/commands/specweave-increment.md +46 -0
  76. package/plugins/specweave/commands/specweave-jobs.md +153 -8
  77. package/plugins/specweave/commands/specweave-judge-llm.md +296 -0
  78. package/plugins/specweave/commands/specweave-organize-docs.md +2 -2
  79. package/plugins/specweave/hooks/hooks.json +10 -0
  80. package/plugins/specweave/hooks/spec-project-validator.sh +24 -2
  81. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
  82. package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
  83. package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
  84. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +87 -0
  85. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.test.sh +302 -0
  86. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +72 -18
  87. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +406 -0
  88. package/plugins/specweave/scripts/session-watchdog.sh +288 -134
  89. package/plugins/specweave/skills/increment-planner/SKILL.md +48 -18
  90. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +27 -14
  91. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +16 -5
  92. package/plugins/specweave/skills/spec-generator/SKILL.md +74 -15
  93. package/plugins/specweave-docs/commands/build.md +4 -4
  94. package/plugins/specweave-docs/commands/generate.md +1 -1
  95. package/plugins/specweave-docs/commands/health.md +1 -1
  96. package/plugins/specweave-docs/commands/init.md +1 -1
  97. package/plugins/specweave-docs/commands/organize.md +2 -2
  98. package/plugins/specweave-docs/commands/validate.md +1 -1
  99. package/plugins/specweave-docs/commands/view.md +391 -0
  100. package/plugins/specweave-docs/skills/preview/SKILL.md +56 -17
  101. package/src/templates/AGENTS.md.template +24 -28
  102. package/src/templates/CLAUDE.md.template +12 -8
  103. package/plugins/specweave/commands/specweave-judge.md +0 -276
  104. package/plugins/specweave-docs/commands/preview.md +0 -274
  105. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -738
  106. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1107
@@ -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
@@ -0,0 +1,406 @@
1
+ #!/bin/bash
2
+ # Comprehensive test suite for per-us-project-validator.sh
3
+ # Tests that each User Story in spec.md has **Project**: (and **Board**: for 2-level)
4
+ #
5
+ # Usage: bash per-us-project-validator.test.sh
6
+ #
7
+ # v0.34.0 - Initial test suite with all US formats
8
+
9
+ set -e
10
+
11
+ GUARD="$(dirname "$0")/per-us-project-validator.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
+ # Test helper: should block
29
+ test_should_block() {
30
+ local name="$1"
31
+ local spec_content="$2"
32
+ TOTAL=$((TOTAL + 1))
33
+
34
+ local file_path="$TEST_DIR/.specweave/increments/0001-test/spec.md"
35
+ mkdir -p "$(dirname "$file_path")"
36
+
37
+ # Build JSON input for the guard
38
+ # Use printf + jq -Rs to properly escape newlines in the content
39
+ local json_input
40
+ json_input=$(printf '%s' "$spec_content" | jq -Rs \
41
+ --arg tool_name "Write" \
42
+ --arg file_path "$file_path" \
43
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path, content: .}}')
44
+
45
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
46
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
47
+
48
+ if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q '"decision".*"block"'; then
49
+ echo -e "${GREEN}✓ BLOCKED${NC}: $name"
50
+ PASS=$((PASS + 1))
51
+ else
52
+ echo -e "${RED}✗ NOT BLOCKED${NC}: $name"
53
+ echo " Exit code: $exit_code"
54
+ echo " Result: $(echo "$result" | head -3)"
55
+ FAIL=$((FAIL + 1))
56
+ fi
57
+ }
58
+
59
+ # Test helper: should allow
60
+ test_should_allow() {
61
+ local name="$1"
62
+ local spec_content="$2"
63
+ TOTAL=$((TOTAL + 1))
64
+
65
+ local file_path="$TEST_DIR/.specweave/increments/0001-test/spec.md"
66
+ mkdir -p "$(dirname "$file_path")"
67
+
68
+ # Build JSON input for the guard
69
+ # Use printf + jq -Rs to properly escape newlines in the content
70
+ local json_input
71
+ json_input=$(printf '%s' "$spec_content" | jq -Rs \
72
+ --arg tool_name "Write" \
73
+ --arg file_path "$file_path" \
74
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path, content: .}}')
75
+
76
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
77
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
78
+
79
+ if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q '"decision".*"allow"'; then
80
+ echo -e "${GREEN}✓ ALLOWED${NC}: $name"
81
+ PASS=$((PASS + 1))
82
+ else
83
+ echo -e "${RED}✗ WRONGLY BLOCKED${NC}: $name"
84
+ echo " Exit code: $exit_code"
85
+ echo " Result: $(echo "$result" | head -5)"
86
+ FAIL=$((FAIL + 1))
87
+ fi
88
+ }
89
+
90
+ echo "========================================"
91
+ echo " PER-US PROJECT VALIDATOR - COMPREHENSIVE TESTS"
92
+ echo "========================================"
93
+ echo ""
94
+ echo "Test directory: $TEST_DIR"
95
+ echo ""
96
+
97
+ echo -e "${YELLOW}=== SIMPLE US FORMAT (### US-001:) ===${NC}"
98
+
99
+ test_should_allow "Simple US with Project field" '---
100
+ increment: 0001-test
101
+ project: my-app
102
+ ---
103
+ # Feature
104
+
105
+ ### US-001: Login Form
106
+ **Project**: frontend-app
107
+
108
+ **As a** user
109
+ **I want** to log in
110
+ **So that** I can access the app
111
+
112
+ **Acceptance Criteria**:
113
+ - [ ] **AC-US1-01**: Form exists
114
+ '
115
+
116
+ test_should_block "Simple US WITHOUT Project field" '---
117
+ increment: 0001-test
118
+ project: my-app
119
+ ---
120
+ # Feature
121
+
122
+ ### US-001: Login Form
123
+
124
+ **As a** user
125
+ **I want** to log in
126
+ **So that** I can access the app
127
+ '
128
+
129
+ echo ""
130
+ echo -e "${YELLOW}=== MULTI-PROJECT US FORMAT (#### US-FE-001:) ===${NC}"
131
+
132
+ test_should_allow "Multi-project US-FE-001 with Project field" '---
133
+ increment: 0001-test
134
+ project: my-app
135
+ multi_project: true
136
+ ---
137
+ # Feature
138
+
139
+ #### US-FE-001: Login Form UI
140
+ **Project**: frontend-app
141
+
142
+ **As a** user
143
+ **I want** to see a login form
144
+
145
+ #### US-BE-001: Auth API
146
+ **Project**: backend-api
147
+
148
+ **As a** frontend
149
+ **I want** auth endpoints
150
+ '
151
+
152
+ test_should_block "Multi-project US-FE-001 WITHOUT Project field" '---
153
+ increment: 0001-test
154
+ project: my-app
155
+ multi_project: true
156
+ ---
157
+ # Feature
158
+
159
+ #### US-FE-001: Login Form UI
160
+
161
+ **As a** user
162
+ **I want** to see a login form
163
+ '
164
+
165
+ echo ""
166
+ echo -e "${YELLOW}=== MIXED FORMAT (### with prefixes) ===${NC}"
167
+
168
+ test_should_allow "Mixed: ### US-FE-001 with Project" '---
169
+ increment: 0001-test
170
+ project: my-app
171
+ ---
172
+ # Feature
173
+
174
+ ### US-FE-001: Frontend Story
175
+ **Project**: frontend
176
+
177
+ ### US-BE-001: Backend Story
178
+ **Project**: backend
179
+ '
180
+
181
+ test_should_block "Mixed: ### US-FE-001 without Project" '---
182
+ increment: 0001-test
183
+ project: my-app
184
+ ---
185
+ # Feature
186
+
187
+ ### US-FE-001: Frontend Story
188
+
189
+ ### US-BE-001: Backend Story
190
+ **Project**: backend
191
+ '
192
+
193
+ echo ""
194
+ echo -e "${YELLOW}=== SHARED PROJECT PREFIX ===${NC}"
195
+
196
+ test_should_allow "US-SHARED-001 with Project" '---
197
+ increment: 0001-test
198
+ project: my-app
199
+ ---
200
+ # Feature
201
+
202
+ #### US-SHARED-001: Shared Types
203
+ **Project**: shared-lib
204
+
205
+ **As a** developer
206
+ **I want** shared types
207
+ '
208
+
209
+ test_should_block "US-SHARED-001 without Project" '---
210
+ increment: 0001-test
211
+ project: my-app
212
+ ---
213
+ # Feature
214
+
215
+ #### US-SHARED-001: Shared Types
216
+
217
+ **As a** developer
218
+ **I want** shared types
219
+ '
220
+
221
+ echo ""
222
+ echo -e "${YELLOW}=== MULTIPLE PROJECTS (1:1 VIOLATION - COMMA SEPARATED) ===${NC}"
223
+
224
+ test_should_block "Multiple projects (comma separated) - FORBIDDEN" '---
225
+ increment: 0001-test
226
+ project: my-app
227
+ ---
228
+ # Feature
229
+
230
+ ### US-001: Cross-cutting Story
231
+ **Project**: frontend-app, backend-api
232
+
233
+ **As a** user
234
+ **I want** everything
235
+ '
236
+
237
+ test_should_block "Multiple projects with spaces" '---
238
+ increment: 0001-test
239
+ project: my-app
240
+ ---
241
+ # Feature
242
+
243
+ ### US-001: Cross-cutting Story
244
+ **Project**: frontend-app,backend-api,shared
245
+
246
+ **As a** user
247
+ **I want** everything
248
+ '
249
+
250
+ echo ""
251
+ echo -e "${YELLOW}=== NO USER STORIES (should allow) ===${NC}"
252
+
253
+ test_should_allow "Spec without any User Stories" '---
254
+ increment: 0001-test
255
+ project: my-app
256
+ ---
257
+ # Feature Overview
258
+
259
+ This is just an overview document.
260
+
261
+ ## Goals
262
+
263
+ - Goal 1
264
+ - Goal 2
265
+ '
266
+
267
+ test_should_allow "Tasks-only spec (no US headings)" '---
268
+ increment: 0001-test
269
+ project: my-app
270
+ ---
271
+ # Implementation Tasks
272
+
273
+ ## T-001: Setup
274
+
275
+ Do setup stuff.
276
+ '
277
+
278
+ echo ""
279
+ echo -e "${YELLOW}=== ALL USER STORIES HAVE PROJECT ===${NC}"
280
+
281
+ test_should_allow "Multiple US, all with Project" '---
282
+ increment: 0001-test
283
+ project: my-app
284
+ ---
285
+ # Feature
286
+
287
+ ### US-001: First Story
288
+ **Project**: app-a
289
+
290
+ ### US-002: Second Story
291
+ **Project**: app-b
292
+
293
+ ### US-003: Third Story
294
+ **Project**: app-c
295
+ '
296
+
297
+ test_should_block "Multiple US, one missing Project" '---
298
+ increment: 0001-test
299
+ project: my-app
300
+ ---
301
+ # Feature
302
+
303
+ ### US-001: First Story
304
+ **Project**: app-a
305
+
306
+ ### US-002: Second Story
307
+
308
+ ### US-003: Third Story
309
+ **Project**: app-c
310
+ '
311
+
312
+ echo ""
313
+ echo -e "${YELLOW}=== NON-WRITE TOOLS ===${NC}"
314
+
315
+ # Test with Edit tool (should allow - no full content)
316
+ TOTAL=$((TOTAL + 1))
317
+ json_input=$(jq -n \
318
+ --arg tool_name "Edit" \
319
+ --arg file_path "$TEST_DIR/.specweave/increments/0001-test/spec.md" \
320
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path}}')
321
+
322
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
323
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
324
+ if [[ "$exit_code" == "0" ]]; then
325
+ echo -e "${GREEN}✓ ALLOWED${NC}: Edit tool (no full content validation)"
326
+ PASS=$((PASS + 1))
327
+ else
328
+ echo -e "${RED}✗ FAILED${NC}: Edit tool should be allowed"
329
+ FAIL=$((FAIL + 1))
330
+ fi
331
+
332
+ echo ""
333
+ echo -e "${YELLOW}=== BYPASS MODES ===${NC}"
334
+
335
+ # Test SPECWEAVE_FORCE_PROJECT bypass
336
+ TOTAL=$((TOTAL + 1))
337
+ spec_content='---
338
+ increment: 0001-test
339
+ ---
340
+ ### US-001: No Project
341
+ **As a** user
342
+ **I want** something
343
+ '
344
+ json_input=$(jq -n \
345
+ --arg tool_name "Write" \
346
+ --arg file_path "$TEST_DIR/.specweave/increments/0001-test/spec.md" \
347
+ --arg content "$spec_content" \
348
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
349
+
350
+ result=$(SPECWEAVE_FORCE_PROJECT=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
351
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
352
+ if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q "bypassed"; then
353
+ echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_FORCE_PROJECT=1"
354
+ PASS=$((PASS + 1))
355
+ else
356
+ echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_FORCE_PROJECT=1"
357
+ FAIL=$((FAIL + 1))
358
+ fi
359
+
360
+ # Test SPECWEAVE_LEGACY_SPEC bypass
361
+ TOTAL=$((TOTAL + 1))
362
+ result=$(SPECWEAVE_LEGACY_SPEC=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
363
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
364
+ if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q "legacy"; then
365
+ echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_LEGACY_SPEC=1"
366
+ PASS=$((PASS + 1))
367
+ else
368
+ echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_LEGACY_SPEC=1"
369
+ FAIL=$((FAIL + 1))
370
+ fi
371
+
372
+ echo ""
373
+ echo -e "${YELLOW}=== FILES OUTSIDE INCREMENTS ===${NC}"
374
+
375
+ # Test file outside increments folder
376
+ TOTAL=$((TOTAL + 1))
377
+ json_input=$(jq -n \
378
+ --arg tool_name "Write" \
379
+ --arg file_path "$TEST_DIR/other/spec.md" \
380
+ --arg content "no project" \
381
+ '{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
382
+
383
+ result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
384
+ exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
385
+ if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q '"decision".*"allow"'; then
386
+ echo -e "${GREEN}✓ ALLOWED${NC}: spec.md outside increments folder"
387
+ PASS=$((PASS + 1))
388
+ else
389
+ echo -e "${RED}✗ FAILED${NC}: Files outside increments should be allowed"
390
+ FAIL=$((FAIL + 1))
391
+ fi
392
+
393
+ echo ""
394
+ echo "========================================"
395
+ echo " RESULTS"
396
+ echo "========================================"
397
+ echo -e "Total: $TOTAL"
398
+ echo -e "${GREEN}Passed: $PASS${NC}"
399
+ if [[ $FAIL -gt 0 ]]; then
400
+ echo -e "${RED}Failed: $FAIL${NC}"
401
+ exit 1
402
+ else
403
+ echo -e "Failed: 0"
404
+ echo ""
405
+ echo -e "${GREEN}ALL TESTS PASSED!${NC}"
406
+ fi