specweave 0.33.2 → 0.33.3

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 (56) hide show
  1. package/CLAUDE.md +56 -0
  2. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +120 -0
  3. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -0
  4. package/dist/plugins/specweave-ado/lib/per-us-sync.js +276 -0
  5. package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -0
  6. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +4 -1
  7. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
  8. package/dist/plugins/specweave-github/lib/github-client-v2.js +13 -3
  9. package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
  10. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +97 -0
  11. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/per-us-sync.js +274 -0
  13. package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -0
  14. package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts +113 -0
  15. package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts.map +1 -0
  16. package/dist/plugins/specweave-jira/lib/per-us-sync.js +254 -0
  17. package/dist/plugins/specweave-jira/lib/per-us-sync.js.map +1 -0
  18. package/dist/src/core/config/config-manager.d.ts.map +1 -1
  19. package/dist/src/core/config/config-manager.js +58 -0
  20. package/dist/src/core/config/config-manager.js.map +1 -1
  21. package/dist/src/core/config/types.d.ts +80 -0
  22. package/dist/src/core/config/types.d.ts.map +1 -1
  23. package/dist/src/core/config/types.js.map +1 -1
  24. package/dist/src/core/living-docs/cross-project-sync.d.ts +87 -15
  25. package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
  26. package/dist/src/core/living-docs/cross-project-sync.js +147 -28
  27. package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
  28. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  29. package/dist/src/core/living-docs/living-docs-sync.js +26 -22
  30. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  31. package/dist/src/core/living-docs/types.d.ts +24 -3
  32. package/dist/src/core/living-docs/types.d.ts.map +1 -1
  33. package/dist/src/core/types/config.d.ts +79 -0
  34. package/dist/src/core/types/config.d.ts.map +1 -1
  35. package/dist/src/core/types/config.js.map +1 -1
  36. package/dist/src/sync/sync-coordinator.d.ts +20 -0
  37. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  38. package/dist/src/sync/sync-coordinator.js +258 -33
  39. package/dist/src/sync/sync-coordinator.js.map +1 -1
  40. package/dist/src/utils/project-resolver.d.ts +156 -0
  41. package/dist/src/utils/project-resolver.d.ts.map +1 -0
  42. package/dist/src/utils/project-resolver.js +587 -0
  43. package/dist/src/utils/project-resolver.js.map +1 -0
  44. package/package.json +1 -1
  45. package/plugins/specweave/hooks/hooks.json +10 -0
  46. package/plugins/specweave/hooks/user-prompt-submit.sh +105 -3
  47. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +281 -0
  48. package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +29 -0
  49. package/plugins/specweave-ado/lib/per-us-sync.js +247 -0
  50. package/plugins/specweave-ado/lib/per-us-sync.ts +410 -0
  51. package/plugins/specweave-github/lib/github-client-v2.js +10 -3
  52. package/plugins/specweave-github/lib/github-client-v2.ts +15 -3
  53. package/plugins/specweave-github/lib/per-us-sync.js +241 -0
  54. package/plugins/specweave-github/lib/per-us-sync.ts +375 -0
  55. package/plugins/specweave-jira/lib/per-us-sync.js +224 -0
  56. package/plugins/specweave-jira/lib/per-us-sync.ts +366 -0
@@ -204,7 +204,91 @@ fi
204
204
  # DISCIPLINE VALIDATION: Warn about WIP limits (configurable, not hard block!)
205
205
  # ==============================================================================
206
206
 
207
- if echo "$PROMPT" | grep -q "/specweave:increment"; then
207
+ # ==============================================================================
208
+ # PROJECT CONTEXT + WIP LIMITS FOR /specweave:increment (v0.34.0)
209
+ # ==============================================================================
210
+ # CRITICAL: Inject project/board context BEFORE Claude generates spec.md
211
+ # This ensures Claude knows available projects and uses correct IDs
212
+ # ALSO: Check WIP limits in same block to avoid duplicate command detection
213
+
214
+ if echo "$PROMPT" | grep -qE "^/specweave:increment"; then
215
+ # Get project context (uses specweave CLI if available)
216
+ PROJECT_CONTEXT=""
217
+
218
+ if command -v specweave >/dev/null 2>&1; then
219
+ # Use CLI for accurate project/board detection
220
+ CONTEXT_JSON=$(specweave context projects 2>/dev/null || echo '{}')
221
+
222
+ # Validate JSON before parsing (defensive coding)
223
+ if [[ -n "$CONTEXT_JSON" ]] && [[ "$CONTEXT_JSON" != "{}" ]]; then
224
+ if command -v jq >/dev/null 2>&1; then
225
+ # Verify JSON is parseable before extracting fields
226
+ if ! echo "$CONTEXT_JSON" | jq empty 2>/dev/null; then
227
+ CONTEXT_JSON='{}' # Invalid JSON - reset to empty
228
+ fi
229
+ fi
230
+ fi
231
+
232
+ if [[ -n "$CONTEXT_JSON" ]] && [[ "$CONTEXT_JSON" != "{}" ]]; then
233
+ # Parse JSON with jq
234
+ if command -v jq >/dev/null 2>&1; then
235
+ LEVEL=$(echo "$CONTEXT_JSON" | jq -r '.level // 1')
236
+ PROJECTS=$(echo "$CONTEXT_JSON" | jq -r '.projects | map(.id) | join(", ")' 2>/dev/null || echo "")
237
+
238
+ if [[ "$LEVEL" == "2" ]]; then
239
+ # 2-level structure: include boards
240
+ BOARDS_JSON=$(echo "$CONTEXT_JSON" | jq -r '.boardsByProject // {}' 2>/dev/null)
241
+ if [[ -n "$BOARDS_JSON" ]] && [[ "$BOARDS_JSON" != "{}" ]]; then
242
+ PROJECT_CONTEXT="\\n\\nšŸ“¦ PROJECT CONTEXT (2-LEVEL STRUCTURE)\\n\\n"
243
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}āš ļø MANDATORY: Each User Story MUST have both:\\n"
244
+ PROJECT_CONTEXT="${PROJECT_CONTEXT} - **Project**: <project_id>\\n"
245
+ PROJECT_CONTEXT="${PROJECT_CONTEXT} - **Board**: <board_id>\\n\\n"
246
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}Available projects: ${PROJECTS}\\n"
247
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}Boards by project:\\n"
248
+
249
+ # Format boards
250
+ for proj in $(echo "$CONTEXT_JSON" | jq -r '.projects[].id' 2>/dev/null); do
251
+ PROJ_BOARDS=$(echo "$CONTEXT_JSON" | jq -r ".boardsByProject[\"$proj\"] | map(.id) | join(\", \")" 2>/dev/null || echo "")
252
+ [[ -n "$PROJ_BOARDS" ]] && PROJECT_CONTEXT="${PROJECT_CONTEXT} - ${proj}: ${PROJ_BOARDS}\\n"
253
+ done
254
+
255
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}\\nāŒ FORBIDDEN: Comma-separated values (e.g., **Project**: fe, be)\\n"
256
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}āœ… REQUIRED: One project + one board per User Story"
257
+ fi
258
+ elif [[ -n "$PROJECTS" ]]; then
259
+ # 1-level structure: projects only
260
+ PROJECT_COUNT=$(echo "$CONTEXT_JSON" | jq '.projects | length' 2>/dev/null || echo "0")
261
+
262
+ if [[ "$PROJECT_COUNT" -gt 1 ]]; then
263
+ PROJECT_CONTEXT="\\n\\nšŸ“¦ PROJECT CONTEXT (MULTI-PROJECT)\\n\\n"
264
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}āš ļø MANDATORY: Each User Story MUST have:\\n"
265
+ PROJECT_CONTEXT="${PROJECT_CONTEXT} - **Project**: <project_id>\\n\\n"
266
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}Available projects: ${PROJECTS}\\n"
267
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}\\nāŒ FORBIDDEN: Comma-separated values\\n"
268
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}āœ… REQUIRED: One project per User Story"
269
+ elif [[ "$PROJECT_COUNT" -eq 1 ]]; then
270
+ # Single project: auto-select
271
+ SINGLE_PROJECT=$(echo "$CONTEXT_JSON" | jq -r '.projects[0].id' 2>/dev/null)
272
+ PROJECT_CONTEXT="\\n\\nšŸ“¦ PROJECT CONTEXT\\n"
273
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}Single project detected: ${SINGLE_PROJECT} (auto-selected)"
274
+ fi
275
+ fi
276
+ fi
277
+ fi
278
+ else
279
+ # Fallback: Check for multi-project folders
280
+ if [[ -d "$SPECWEAVE_DIR/docs/internal/specs" ]]; then
281
+ PROJ_COUNT=$(find "$SPECWEAVE_DIR/docs/internal/specs" -maxdepth 1 -type d | wc -l)
282
+ if [[ "$PROJ_COUNT" -gt 2 ]]; then
283
+ PROJ_LIST=$(ls -1 "$SPECWEAVE_DIR/docs/internal/specs" 2>/dev/null | grep -v "_" | tr '\n' ', ' | sed 's/,$//')
284
+ PROJECT_CONTEXT="\\n\\nšŸ“¦ PROJECT CONTEXT (MULTI-PROJECT)\\n"
285
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}āš ļø MANDATORY: Each User Story MUST have **Project**: field\\n"
286
+ PROJECT_CONTEXT="${PROJECT_CONTEXT}Available folders: ${PROJ_LIST}"
287
+ fi
288
+ fi
289
+ fi
290
+
291
+ # WIP LIMITS CHECK (inside same block - no duplicate command detection)
208
292
  # Read limits from config.json (respect user's settings!)
209
293
  CONFIG_FILE="$SPECWEAVE_DIR/config.json"
210
294
  SOFT_LIMIT=1
@@ -226,13 +310,31 @@ if echo "$PROMPT" | grep -q "/specweave:increment"; then
226
310
 
227
311
  # Above hard cap: strong warning but NOT a block (user decides!)
228
312
  if [[ "$ACTIVE_COUNT" -ge "$HARD_CAP" ]]; then
229
- printf '{"decision":"approve","systemMessage":"āš ļø WIP LIMIT EXCEEDED (%s/%s)\\n\\nYou have %s active increments (configured maximum: %s)\\n\\nActive increments:\\n%s\\n\\n🧠 Research shows 3+ concurrent tasks = 40%% slower + more bugs\\n\\nšŸ’” Options:\\n 1ļøāƒ£ Complete an increment: /specweave:done <id>\\n 2ļøāƒ£ Pause an increment: /specweave:pause <id>\\n 3ļøāƒ£ Increase limit: Edit .specweave/config.json limits.hardCap\\n 4ļøāƒ£ Continue anyway (not recommended)\\n\\nšŸ“ To proceed anyway, just confirm your intent."}\n' "$ACTIVE_COUNT" "$HARD_CAP" "$ACTIVE_COUNT" "$HARD_CAP" "$ACTIVE_LIST"
313
+ WIP_MSG="āš ļø WIP LIMIT EXCEEDED (${ACTIVE_COUNT}/${HARD_CAP})\\n\\nYou have ${ACTIVE_COUNT} active increments (configured maximum: ${HARD_CAP})\\n\\nActive increments:\\n${ACTIVE_LIST}\\n\\n🧠 Research shows 3+ concurrent tasks = 40%% slower + more bugs\\n\\nšŸ’” Options:\\n 1ļøāƒ£ Complete an increment: /specweave:done <id>\\n 2ļøāƒ£ Pause an increment: /specweave:pause <id>\\n 3ļøāƒ£ Increase limit: Edit .specweave/config.json limits.hardCap\\n 4ļøāƒ£ Continue anyway (not recommended)\\n\\nšŸ“ To proceed anyway, just confirm your intent."
314
+ # Prepend project context if available
315
+ if [[ -n "$PROJECT_CONTEXT" ]]; then
316
+ printf '{"decision":"approve","systemMessage":"%s%s"}\n' "$PROJECT_CONTEXT" "$WIP_MSG"
317
+ else
318
+ printf '{"decision":"approve","systemMessage":"%s"}\n' "$WIP_MSG"
319
+ fi
230
320
  exit 0
231
321
  fi
232
322
 
233
323
  # At soft limit: mild warning, approve
234
324
  if [[ "$ACTIVE_COUNT" -ge "$SOFT_LIMIT" ]]; then
235
- printf '{"decision":"approve","systemMessage":"āš ļø WIP LIMIT REACHED (%s/%s)\\n\\nYou have %s active increment(s) (recommended limit: %s)\\n\\nActive increments:\\n%s\\n\\n🧠 Focus Principle: Fewer active increments = maximum productivity\\n\\nšŸ’” Consider:\\n 1ļøāƒ£ Complete current work (recommended)\\n 2ļøāƒ£ Pause current work (/specweave:pause)\\n 3ļøāƒ£ Continue anyway\\n\\nāš ļø Emergency hotfix/bug? Use --type=hotfix or --type=bug"}\n' "$ACTIVE_COUNT" "$SOFT_LIMIT" "$ACTIVE_COUNT" "$SOFT_LIMIT" "$ACTIVE_LIST"
325
+ WIP_MSG="āš ļø WIP LIMIT REACHED (${ACTIVE_COUNT}/${SOFT_LIMIT})\\n\\nYou have ${ACTIVE_COUNT} active increment(s) (recommended limit: ${SOFT_LIMIT})\\n\\nActive increments:\\n${ACTIVE_LIST}\\n\\n🧠 Focus Principle: Fewer active increments = maximum productivity\\n\\nšŸ’” Consider:\\n 1ļøāƒ£ Complete current work (recommended)\\n 2ļøāƒ£ Pause current work (/specweave:pause)\\n 3ļøāƒ£ Continue anyway\\n\\nāš ļø Emergency hotfix/bug? Use --type=hotfix or --type=bug"
326
+ # Prepend project context if available
327
+ if [[ -n "$PROJECT_CONTEXT" ]]; then
328
+ printf '{"decision":"approve","systemMessage":"%s%s"}\n' "$PROJECT_CONTEXT" "$WIP_MSG"
329
+ else
330
+ printf '{"decision":"approve","systemMessage":"%s"}\n' "$WIP_MSG"
331
+ fi
332
+ exit 0
333
+ fi
334
+
335
+ # No WIP limit warning, but we may have project context to inject
336
+ if [[ -n "$PROJECT_CONTEXT" ]]; then
337
+ printf '{"decision":"approve","systemMessage":"%s"}\n' "$PROJECT_CONTEXT"
236
338
  exit 0
237
339
  fi
238
340
  fi
@@ -0,0 +1,281 @@
1
+ #!/bin/bash
2
+ #
3
+ # per-us-project-validator.sh
4
+ #
5
+ # Pre-tool-use hook that validates EACH User Story in spec.md has
6
+ # **Project**: (and **Board**: for 2-level) fields.
7
+ #
8
+ # This is the CRITICAL ENFORCEMENT for increment 0137.
9
+ #
10
+ # Activation:
11
+ # - tool_name: Write (also Edit if full content provided)
12
+ # - file_path matches: .specweave/increments/*/spec.md
13
+ #
14
+ # Rules:
15
+ # - Each ### US-XXX section MUST have **Project**: <value> on next few lines
16
+ # - For 2-level structures, each US MUST also have **Board**: <value>
17
+ # - Project values MUST match configured projects (not generic keywords)
18
+ # - Fallback allowed for existing specs via SPECWEAVE_LEGACY_SPEC=1
19
+ #
20
+ # Returns exit code 1 (block) if validation fails, 0 (allow) otherwise.
21
+ #
22
+ # Bypass:
23
+ # - SPECWEAVE_FORCE_PROJECT=1 - Skip all project validation
24
+ # - SPECWEAVE_LEGACY_SPEC=1 - Allow specs without per-US project (legacy mode)
25
+ #
26
+
27
+ set -e
28
+
29
+ # Source common utilities if available
30
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
+ PLUGIN_ROOT="${SCRIPT_DIR}/../.."
32
+
33
+ # Logging (silent by default)
34
+ log_debug() {
35
+ if [ "$SPECWEAVE_DEBUG" = "1" ]; then
36
+ echo "[per-us-project] $*" >&2
37
+ fi
38
+ }
39
+
40
+ # Check for force bypass
41
+ if [ "$SPECWEAVE_FORCE_PROJECT" = "1" ]; then
42
+ echo '{"decision": "allow", "message": "āš ļø Per-US project validation bypassed (SPECWEAVE_FORCE_PROJECT=1)"}'
43
+ exit 0
44
+ fi
45
+
46
+ # Check for legacy mode bypass
47
+ if [ "$SPECWEAVE_LEGACY_SPEC" = "1" ]; then
48
+ echo '{"decision": "allow", "message": "āš ļø Per-US project validation skipped (SPECWEAVE_LEGACY_SPEC=1 - legacy mode)"}'
49
+ exit 0
50
+ fi
51
+
52
+ # Read tool input from stdin
53
+ INPUT=$(cat)
54
+
55
+ # Extract tool name
56
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
57
+
58
+ # Only validate Write tool calls (Edit doesn't provide full content)
59
+ if [ "$TOOL_NAME" != "Write" ]; then
60
+ echo '{"decision": "allow"}'
61
+ exit 0
62
+ fi
63
+
64
+ # Extract file path
65
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
66
+
67
+ # Only validate spec.md files in increments folder
68
+ if [[ ! "$FILE_PATH" =~ \.specweave/increments/[0-9]{3,4}E?-[^/]+/spec\.md$ ]]; then
69
+ echo '{"decision": "allow"}'
70
+ exit 0
71
+ fi
72
+
73
+ log_debug "Validating per-US project fields for: $FILE_PATH"
74
+
75
+ # Extract file content
76
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
77
+
78
+ # Count User Stories (### US-XXX pattern)
79
+ US_PATTERN='### US-[0-9]+'
80
+ TOTAL_US=$(echo "$CONTENT" | grep -cE "$US_PATTERN" || echo 0)
81
+
82
+ log_debug "Total User Stories found: $TOTAL_US"
83
+
84
+ # If no user stories, allow (might be tasks-only spec or overview)
85
+ if [ "$TOTAL_US" -eq 0 ]; then
86
+ echo '{"decision": "allow"}'
87
+ exit 0
88
+ fi
89
+
90
+ # Extract User Story sections and check for **Project**: field
91
+ # Strategy: For each ### US-XXX, look at next 10 lines for **Project**:
92
+
93
+ MISSING_PROJECT=()
94
+ MISSING_BOARD=()
95
+ MULTI_PROJECT=() # USs with multiple projects (comma-separated)
96
+ MULTI_BOARD=() # USs with multiple boards (comma-separated)
97
+ US_WITH_PROJECT=0
98
+
99
+ # Use awk to extract US sections and check for Project field
100
+ 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)
103
+
104
+ if [ -z "$US_ID" ]; then
105
+ continue
106
+ fi
107
+
108
+ # Get line number of this US heading
109
+ LINE_NUM=$(echo "$CONTENT" | grep -nE "^### $US_ID:" | head -1 | cut -d: -f1)
110
+
111
+ if [ -z "$LINE_NUM" ]; then
112
+ continue
113
+ fi
114
+
115
+ # Extract next 10 lines after heading
116
+ SECTION=$(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)) | head -n 10)
117
+
118
+ # Check for **Project**: field
119
+ PROJECT_LINE=$(echo "$SECTION" | grep -E '^\*\*Project\*\*:\s*\S' | head -1)
120
+
121
+ if [ -n "$PROJECT_LINE" ]; then
122
+ US_WITH_PROJECT=$((US_WITH_PROJECT + 1))
123
+
124
+ # Extract project value
125
+ PROJECT_VALUE=$(echo "$PROJECT_LINE" | sed 's/^\*\*Project\*\*:\s*//' | tr -d '[:space:]')
126
+
127
+ # Check for comma (multiple projects - FORBIDDEN)
128
+ if echo "$PROJECT_VALUE" | grep -q ','; then
129
+ MULTI_PROJECT+=("$US_ID (has: $PROJECT_VALUE)")
130
+ log_debug "$US_ID has MULTIPLE projects (FORBIDDEN): $PROJECT_VALUE"
131
+ else
132
+ log_debug "$US_ID has **Project**: field āœ“ ($PROJECT_VALUE)"
133
+ fi
134
+ else
135
+ MISSING_PROJECT+=("$US_ID")
136
+ log_debug "$US_ID MISSING **Project**: field āœ—"
137
+ fi
138
+
139
+ done < <(echo "$CONTENT" | grep -E "^### US-[0-9]+:")
140
+
141
+ log_debug "User Stories with **Project**: $US_WITH_PROJECT / $TOTAL_US"
142
+
143
+ # Get project root for structure detection
144
+ PROJECT_ROOT="${FILE_PATH%%/.specweave/*}"
145
+ cd "$PROJECT_ROOT" 2>/dev/null || true
146
+
147
+ # Get structure level from config
148
+ STRUCTURE_LEVEL=1
149
+ if command -v specweave >/dev/null 2>&1; then
150
+ CONTEXT_OUTPUT=$(specweave context projects 2>/dev/null || echo '{"level": 1}')
151
+ STRUCTURE_LEVEL=$(echo "$CONTEXT_OUTPUT" | jq -r '.level // 1')
152
+ AVAILABLE_PROJECTS=$(echo "$CONTEXT_OUTPUT" | jq -r '.projects[].id // empty' | tr '\n' ', ' | sed 's/,$//')
153
+ else
154
+ # Fallback: check for 2-level indicators in config
155
+ if [ -f "$PROJECT_ROOT/.specweave/config.json" ]; then
156
+ HAS_AREA_PATH=$(jq -r '.sync.profiles | to_entries | map(select(.value.config.areaPathMapping or .value.config.areaPaths)) | length' "$PROJECT_ROOT/.specweave/config.json" 2>/dev/null || echo 0)
157
+ HAS_BOARD_MAPPING=$(jq -r '.sync.profiles | to_entries | map(select(.value.config.boardMapping.boards | length > 0)) | length' "$PROJECT_ROOT/.specweave/config.json" 2>/dev/null || echo 0)
158
+
159
+ if [ "$HAS_AREA_PATH" -gt 0 ] || [ "$HAS_BOARD_MAPPING" -gt 0 ]; then
160
+ STRUCTURE_LEVEL=2
161
+ fi
162
+ fi
163
+ fi
164
+
165
+ log_debug "Structure level: $STRUCTURE_LEVEL"
166
+
167
+ # For 2-level structures, also check for **Board**: field
168
+ if [ "$STRUCTURE_LEVEL" = "2" ]; then
169
+ while IFS= read -r us_line; do
170
+ US_ID=$(echo "$us_line" | grep -oE 'US-[0-9]+' | head -1)
171
+
172
+ if [ -z "$US_ID" ]; then
173
+ continue
174
+ fi
175
+
176
+ LINE_NUM=$(echo "$CONTENT" | grep -nE "^### $US_ID:" | head -1 | cut -d: -f1)
177
+
178
+ if [ -z "$LINE_NUM" ]; then
179
+ continue
180
+ fi
181
+
182
+ SECTION=$(echo "$CONTENT" | tail -n +$((LINE_NUM + 1)) | head -n 10)
183
+
184
+ # Check for **Board**: field
185
+ BOARD_LINE=$(echo "$SECTION" | grep -E '^\*\*Board\*\*:\s*\S' | head -1)
186
+
187
+ if [ -n "$BOARD_LINE" ]; then
188
+ # Extract board value
189
+ BOARD_VALUE=$(echo "$BOARD_LINE" | sed 's/^\*\*Board\*\*:\s*//' | tr -d '[:space:]')
190
+
191
+ # Check for comma (multiple boards - FORBIDDEN)
192
+ if echo "$BOARD_VALUE" | grep -q ','; then
193
+ MULTI_BOARD+=("$US_ID (has: $BOARD_VALUE)")
194
+ log_debug "$US_ID has MULTIPLE boards (FORBIDDEN): $BOARD_VALUE"
195
+ else
196
+ log_debug "$US_ID has **Board**: field āœ“ ($BOARD_VALUE)"
197
+ fi
198
+ else
199
+ MISSING_BOARD+=("$US_ID")
200
+ log_debug "$US_ID MISSING **Board**: field āœ—"
201
+ fi
202
+
203
+ done < <(echo "$CONTENT" | grep -E "^### US-[0-9]+:")
204
+ fi
205
+
206
+ # Build error message if validation fails
207
+ ERRORS=""
208
+
209
+ if [ ${#MISSING_PROJECT[@]} -gt 0 ]; then
210
+ MISSING_LIST=$(printf ", %s" "${MISSING_PROJECT[@]}")
211
+ MISSING_LIST=${MISSING_LIST:2} # Remove leading ", "
212
+
213
+ ERRORS="$ERRORS\nāŒ User Stories MISSING **Project**: field:\n ${MISSING_LIST}\n"
214
+ ERRORS="$ERRORS\n Each User Story MUST have **Project**: <project_id> after the heading.\n"
215
+ ERRORS="$ERRORS\n Example:\n"
216
+ ERRORS="$ERRORS ### US-001: Login Form\n"
217
+ ERRORS="$ERRORS **Project**: frontend-app\n"
218
+
219
+ if [ -n "$AVAILABLE_PROJECTS" ]; then
220
+ ERRORS="$ERRORS\n Available projects: ${AVAILABLE_PROJECTS}\n"
221
+ fi
222
+ fi
223
+
224
+ if [ ${#MISSING_BOARD[@]} -gt 0 ]; then
225
+ MISSING_LIST=$(printf ", %s" "${MISSING_BOARD[@]}")
226
+ MISSING_LIST=${MISSING_LIST:2}
227
+
228
+ ERRORS="$ERRORS\nāŒ User Stories MISSING **Board**: field (2-level structure):\n ${MISSING_LIST}\n"
229
+ ERRORS="$ERRORS\n For 2-level structures, each US MUST have **Board**: <board_id>\n"
230
+ ERRORS="$ERRORS\n Example:\n"
231
+ ERRORS="$ERRORS ### US-001: Login Form\n"
232
+ ERRORS="$ERRORS **Project**: acme-corp\n"
233
+ ERRORS="$ERRORS **Board**: mobile-team\n"
234
+ fi
235
+
236
+ # Check for multiple projects (1:1 violation)
237
+ if [ ${#MULTI_PROJECT[@]} -gt 0 ]; then
238
+ MULTI_LIST=$(printf ", %s" "${MULTI_PROJECT[@]}")
239
+ MULTI_LIST=${MULTI_LIST:2}
240
+
241
+ ERRORS="$ERRORS\nāŒ User Stories with MULTIPLE projects (FORBIDDEN - 1:1 mapping required):\n ${MULTI_LIST}\n"
242
+ ERRORS="$ERRORS\n Each User Story MUST map to EXACTLY ONE project.\n"
243
+ ERRORS="$ERRORS Split cross-project features into separate USs:\n"
244
+ ERRORS="$ERRORS\n WRONG:\n"
245
+ ERRORS="$ERRORS ### US-001: OAuth Implementation\n"
246
+ ERRORS="$ERRORS **Project**: frontend-app, backend-api ← FORBIDDEN\n"
247
+ ERRORS="$ERRORS\n CORRECT:\n"
248
+ ERRORS="$ERRORS ### US-001: OAuth Login Form\n"
249
+ ERRORS="$ERRORS **Project**: frontend-app\n"
250
+ ERRORS="$ERRORS\n ### US-002: OAuth API Endpoints\n"
251
+ ERRORS="$ERRORS **Project**: backend-api\n"
252
+ fi
253
+
254
+ # Check for multiple boards (1:1 violation)
255
+ if [ ${#MULTI_BOARD[@]} -gt 0 ]; then
256
+ MULTI_LIST=$(printf ", %s" "${MULTI_BOARD[@]}")
257
+ MULTI_LIST=${MULTI_LIST:2}
258
+
259
+ ERRORS="$ERRORS\nāŒ User Stories with MULTIPLE boards (FORBIDDEN - 1:1 mapping required):\n ${MULTI_LIST}\n"
260
+ ERRORS="$ERRORS\n Each User Story MUST map to EXACTLY ONE board.\n"
261
+ ERRORS="$ERRORS Split cross-board features into separate USs.\n"
262
+ fi
263
+
264
+ # If errors found, block the write
265
+ if [ -n "$ERRORS" ]; then
266
+ # Add bypass instructions
267
+ ERRORS="$ERRORS\nšŸ’” To bypass this validation:\n"
268
+ ERRORS="$ERRORS - SPECWEAVE_FORCE_PROJECT=1 (skip all validation)\n"
269
+ ERRORS="$ERRORS - SPECWEAVE_LEGACY_SPEC=1 (allow specs without per-US project)\n"
270
+
271
+ # Escape for JSON
272
+ REASON=$(echo -e "$ERRORS" | jq -Rs .)
273
+
274
+ echo "{\"decision\": \"block\", \"reason\": $REASON}"
275
+ exit 0
276
+ fi
277
+
278
+ # All validations passed
279
+ log_debug "All $TOTAL_US User Stories have required fields āœ“"
280
+ echo '{"decision": "allow"}'
281
+ exit 0
@@ -120,6 +120,35 @@ case "$EVENT" in
120
120
  run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
121
121
  fi
122
122
  fi
123
+
124
+ # ========================================================================
125
+ # CRITICAL FIX (v0.34.0): Close GitHub/JIRA/ADO issues on increment.done
126
+ # ========================================================================
127
+ # Root cause: sync-increment-closure.js was NEVER being called when
128
+ # increments completed. The EDA architecture routes increment.done to
129
+ # this handler, but only sync-living-docs.js was called (updates docs).
130
+ #
131
+ # sync-increment-closure.js contains the actual logic to:
132
+ # - Close GitHub issues linked to User Stories
133
+ # - Close JIRA/ADO issues (with proper gates)
134
+ # - Post completion comments on external issues
135
+ #
136
+ # Without this, GitHub issues stayed OPEN forever after increment closure!
137
+ # See: GitHub Issues #817-#822 (increment 0136) stayed open until manual fix
138
+ CLOSURE_SCRIPT=""
139
+ for path in \
140
+ "$PROJECT_ROOT/plugins/specweave/lib/hooks/sync-increment-closure.js" \
141
+ "$PROJECT_ROOT/dist/plugins/specweave/lib/hooks/sync-increment-closure.js" \
142
+ "${CLAUDE_PLUGIN_ROOT:-}/lib/hooks/sync-increment-closure.js"; do
143
+ [[ -f "$path" ]] && { CLOSURE_SCRIPT="$path"; break; }
144
+ done
145
+
146
+ if [[ -n "$CLOSURE_SCRIPT" ]]; then
147
+ cd "$PROJECT_ROOT" || exit 0
148
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] Calling sync-increment-closure.js for $INC_ID" >> "$LOG_FILE" 2>/dev/null
149
+ # Run closure sync in background (can take 10-30s for multiple issues)
150
+ run_with_timeout 60 node "$CLOSURE_SCRIPT" "$INC_ID" >> "$LOG_FILE" 2>&1 &
151
+ fi
123
152
  ;;
124
153
 
125
154
  increment.archived)