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.
- package/CLAUDE.md +56 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +120 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.js +276 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +4 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +13 -3
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +97 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.js +274 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts +113 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.js +254 -0
- package/dist/plugins/specweave-jira/lib/per-us-sync.js.map +1 -0
- package/dist/src/core/config/config-manager.d.ts.map +1 -1
- package/dist/src/core/config/config-manager.js +58 -0
- package/dist/src/core/config/config-manager.js.map +1 -1
- package/dist/src/core/config/types.d.ts +80 -0
- package/dist/src/core/config/types.d.ts.map +1 -1
- package/dist/src/core/config/types.js.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.d.ts +87 -15
- package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.js +147 -28
- package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +26 -22
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/living-docs/types.d.ts +24 -3
- package/dist/src/core/living-docs/types.d.ts.map +1 -1
- package/dist/src/core/types/config.d.ts +79 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts +20 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +258 -33
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/utils/project-resolver.d.ts +156 -0
- package/dist/src/utils/project-resolver.d.ts.map +1 -0
- package/dist/src/utils/project-resolver.js +587 -0
- package/dist/src/utils/project-resolver.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/hooks/hooks.json +10 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +105 -3
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +281 -0
- package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +29 -0
- package/plugins/specweave-ado/lib/per-us-sync.js +247 -0
- package/plugins/specweave-ado/lib/per-us-sync.ts +410 -0
- package/plugins/specweave-github/lib/github-client-v2.js +10 -3
- package/plugins/specweave-github/lib/github-client-v2.ts +15 -3
- package/plugins/specweave-github/lib/per-us-sync.js +241 -0
- package/plugins/specweave-github/lib/per-us-sync.ts +375 -0
- package/plugins/specweave-jira/lib/per-us-sync.js +224 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|