specweave 1.0.28 → 1.0.30

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 (34) hide show
  1. package/CLAUDE.md +39 -0
  2. package/package.json +1 -1
  3. package/plugins/specweave/.claude-plugin/plugin.json +1 -1
  4. package/plugins/specweave/commands/check-hooks.md +43 -0
  5. package/plugins/specweave/hooks/lib/circuit-breaker.sh +381 -0
  6. package/plugins/specweave/hooks/lib/logging.sh +231 -0
  7. package/plugins/specweave/hooks/lib/metrics.sh +347 -0
  8. package/plugins/specweave/hooks/lib/semaphore.sh +216 -0
  9. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +156 -22
  10. package/plugins/specweave/scripts/hook-health.sh +441 -0
  11. package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
  12. package/plugins/specweave-alternatives/.claude-plugin/plugin.json +1 -1
  13. package/plugins/specweave-backend/.claude-plugin/plugin.json +1 -1
  14. package/plugins/specweave-confluent/.claude-plugin/plugin.json +1 -1
  15. package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +1 -1
  16. package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
  17. package/plugins/specweave-docs/.claude-plugin/plugin.json +1 -1
  18. package/plugins/specweave-figma/.claude-plugin/plugin.json +1 -1
  19. package/plugins/specweave-frontend/.claude-plugin/plugin.json +1 -1
  20. package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
  21. package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
  22. package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
  23. package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
  24. package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
  25. package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +1 -1
  26. package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
  27. package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -1
  28. package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
  29. package/plugins/specweave-payments/.claude-plugin/plugin.json +1 -1
  30. package/plugins/specweave-plugin-dev/.claude-plugin/plugin.json +1 -1
  31. package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
  32. package/plugins/specweave-testing/.claude-plugin/plugin.json +1 -1
  33. package/plugins/specweave-ui/.claude-plugin/plugin.json +1 -1
  34. /package/plugins/specweave/hooks/{hooks.json.bak → hooks.json} +0 -0
package/CLAUDE.md CHANGED
@@ -1343,6 +1343,45 @@ echo '{"decision": "block", "reason": "Error message here"}'
1343
1343
  exit 2
1344
1344
  ```
1345
1345
 
1346
+ ### Hook Concurrency System (v1.0.30+)
1347
+
1348
+ **All hooks use proper concurrency primitives via `fail-fast-wrapper.sh`:**
1349
+
1350
+ **Semaphore** (`hooks/lib/semaphore.sh`):
1351
+ - Limits concurrent hooks to `HOOK_MAX_CONCURRENT` (default: 15)
1352
+ - Graceful degradation when slots unavailable (returns safe default)
1353
+ - Auto-cleanup of stale locks (>30s old)
1354
+
1355
+ **Circuit Breaker** (`hooks/lib/circuit-breaker.sh`):
1356
+ - Per-hook circuit breakers (not global!)
1357
+ - States: CLOSED → (5 failures) → OPEN → (30s) → HALF_OPEN → (3 successes) → CLOSED
1358
+ - Prevents cascade failures from broken hooks
1359
+
1360
+ **Metrics** (`hooks/lib/metrics.sh`):
1361
+ - Tracks success/failure/timeout/skipped per hook
1362
+ - Calculates latency percentiles (p50, p95, p99)
1363
+ - Health score (0-100) per hook
1364
+
1365
+ **Configuration:**
1366
+ ```bash
1367
+ HOOK_MAX_CONCURRENT=15 # Max concurrent hooks
1368
+ HOOK_TIMEOUT=5 # Hook execution timeout (seconds)
1369
+ HOOK_DEBUG=1 # Enable debug logging
1370
+ HOOK_ACQUIRE_TIMEOUT_MS=3000 # Semaphore acquire timeout
1371
+ ```
1372
+
1373
+ **Health Dashboard:**
1374
+ ```bash
1375
+ bash plugins/specweave/scripts/hook-health.sh # Full dashboard
1376
+ bash plugins/specweave/scripts/hook-health.sh --status # Quick status
1377
+ bash plugins/specweave/scripts/hook-health.sh --reset # Reset circuit breakers
1378
+ ```
1379
+
1380
+ **Root Cause of Process Storm (Fixed in v1.0.30):**
1381
+ The old system detected "process storms" (>25 concurrent processes) and blocked ALL hooks.
1382
+ This caused cascading failures where even safe hooks were blocked.
1383
+ The new system uses proper semaphore-based concurrency limiting with graceful degradation.
1384
+
1346
1385
  ---
1347
1386
 
1348
1387
  ## Quick Reference
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specweave",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Spec-driven development framework for Claude Code. AI-native workflow with living documentation, intelligent agents, and multilingual support (9 languages). Enterprise-grade traceability with permanent specs and temporary increments.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sw",
3
3
  "description": "SpecWeave framework. Provides increment planning (PM, Architect, Tech Lead agents), specification generation, TDD workflow, living docs sync, and brownfield support. Essential for all SpecWeave projects.",
4
- "version": "1.0.0-rc.1",
4
+ "version": "1.0.0",
5
5
  "author": {
6
6
  "name": "SpecWeave Team",
7
7
  "url": "https://spec-weave.com"
@@ -185,7 +185,50 @@ Run with auto-fix: `/sw:check-hooks --fix`
185
185
  ### "Permission denied"
186
186
  Check hook file permissions: `chmod +x plugins/*/hooks/*.sh`
187
187
 
188
+ ## Quick Health Dashboard
189
+
190
+ For a quick visual dashboard of hook health, run:
191
+
192
+ ```bash
193
+ bash plugins/specweave/scripts/hook-health.sh
194
+ ```
195
+
196
+ Or use specific views:
197
+ ```bash
198
+ bash plugins/specweave/scripts/hook-health.sh --status # Quick status
199
+ bash plugins/specweave/scripts/hook-health.sh --metrics # Detailed metrics
200
+ bash plugins/specweave/scripts/hook-health.sh --reset # Reset circuit breakers
201
+ bash plugins/specweave/scripts/hook-health.sh --clean # Clean stale state
202
+ ```
203
+
204
+ ## Concurrency System (v1.0.30+)
205
+
206
+ The hook system uses proper concurrency primitives:
207
+
208
+ ### Semaphore
209
+ - Limits concurrent hook execution (default: 15)
210
+ - Graceful degradation when slots unavailable
211
+ - Automatic cleanup of stale locks
212
+
213
+ ### Circuit Breaker
214
+ - Per-hook circuit breakers with 3 states:
215
+ - CLOSED: Normal operation
216
+ - OPEN: Too many failures, fail fast
217
+ - HALF_OPEN: Testing recovery
218
+
219
+ ### Metrics
220
+ - Success/failure/timeout tracking
221
+ - Latency percentiles (p50, p95, p99)
222
+ - Health score calculation
223
+
224
+ ### Configuration
225
+ Environment variables:
226
+ - `HOOK_MAX_CONCURRENT` - Max concurrent hooks (default: 15)
227
+ - `HOOK_TIMEOUT` - Hook execution timeout in seconds (default: 5)
228
+ - `HOOK_DEBUG` - Enable debug logging (1 = enabled)
229
+
188
230
  ## See Also
189
231
 
190
232
  - Hook Health Check Architecture: `.specweave/increments/0037-project-specific-tasks/architecture/HOOK-HEALTH-CHECK-ARCHITECTURE.md`
191
233
  - Hook Development Guide: `.specweave/docs/public/guides/hook-development.md`
234
+ - Concurrency Libraries: `plugins/specweave/hooks/lib/`
@@ -0,0 +1,381 @@
1
+ #!/bin/bash
2
+ # circuit-breaker.sh - Proper Circuit Breaker Pattern for SpecWeave Hooks
3
+ #
4
+ # PROBLEM SOLVED:
5
+ # The old circuit breaker was too simple - it just counted failures and blocked everything.
6
+ # This implementation follows the proper circuit breaker pattern with three states:
7
+ # - CLOSED: Normal operation, requests flow through
8
+ # - OPEN: Too many failures, requests are rejected immediately (fail-fast)
9
+ # - HALF-OPEN: Testing if system recovered, allows limited requests
10
+ #
11
+ # DESIGN:
12
+ # - Failure threshold before opening (default: 5 failures in 60s window)
13
+ # - Recovery timeout before half-open (default: 30s)
14
+ # - Success threshold to close (default: 3 successes in half-open)
15
+ # - Per-hook circuit breakers (not global)
16
+ # - Sliding window for failure counting
17
+ # - Automatic state transitions
18
+ #
19
+ # USAGE:
20
+ # source circuit-breaker.sh
21
+ #
22
+ # # Before executing hook:
23
+ # if cb_allow_request "hook-name"; then
24
+ # # Execute hook
25
+ # if hook_succeeded; then
26
+ # cb_record_success "hook-name"
27
+ # else
28
+ # cb_record_failure "hook-name"
29
+ # fi
30
+ # else
31
+ # # Circuit is open, return safe default
32
+ # fi
33
+ #
34
+ # v1.0.0 - Initial implementation (2025-12-17)
35
+
36
+ set -o pipefail
37
+
38
+ # === Configuration ===
39
+ CB_STATE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}/circuit-breakers"
40
+ CB_FAILURE_THRESHOLD="${CB_FAILURE_THRESHOLD:-5}" # Failures before OPEN
41
+ CB_FAILURE_WINDOW_SEC="${CB_FAILURE_WINDOW_SEC:-60}" # Sliding window for counting failures
42
+ CB_RECOVERY_TIMEOUT_SEC="${CB_RECOVERY_TIMEOUT_SEC:-30}" # Time in OPEN before HALF-OPEN
43
+ CB_SUCCESS_THRESHOLD="${CB_SUCCESS_THRESHOLD:-3}" # Successes in HALF-OPEN to CLOSE
44
+ CB_DEBUG="${CB_DEBUG:-0}"
45
+
46
+ # States
47
+ CB_STATE_CLOSED="CLOSED"
48
+ CB_STATE_OPEN="OPEN"
49
+ CB_STATE_HALF_OPEN="HALF_OPEN"
50
+
51
+ # === Initialization ===
52
+ _cb_init() {
53
+ mkdir -p "$CB_STATE_DIR" 2>/dev/null || true
54
+ }
55
+
56
+ # === Logging ===
57
+ _cb_log() {
58
+ [[ "$CB_DEBUG" == "1" ]] && echo "[CB $(date +%H:%M:%S)] $*" >&2
59
+ }
60
+
61
+ # === State File Paths ===
62
+ _cb_state_file() {
63
+ local name="$1"
64
+ echo "$CB_STATE_DIR/${name}.state"
65
+ }
66
+
67
+ _cb_failures_file() {
68
+ local name="$1"
69
+ echo "$CB_STATE_DIR/${name}.failures"
70
+ }
71
+
72
+ _cb_successes_file() {
73
+ local name="$1"
74
+ echo "$CB_STATE_DIR/${name}.successes"
75
+ }
76
+
77
+ # === Read/Write State ===
78
+ _cb_read_state() {
79
+ local name="$1"
80
+ local state_file
81
+ state_file=$(_cb_state_file "$name")
82
+
83
+ if [[ -f "$state_file" ]]; then
84
+ cat "$state_file" 2>/dev/null || echo "$CB_STATE_CLOSED"
85
+ else
86
+ echo "$CB_STATE_CLOSED"
87
+ fi
88
+ }
89
+
90
+ _cb_write_state() {
91
+ local name="$1"
92
+ local state="$2"
93
+ local state_file
94
+ state_file=$(_cb_state_file "$name")
95
+
96
+ _cb_init
97
+ echo "$state" > "$state_file" 2>/dev/null || true
98
+ _cb_log "State transition for $name: -> $state"
99
+ }
100
+
101
+ # === Timestamp helpers ===
102
+ _cb_now() {
103
+ date +%s
104
+ }
105
+
106
+ _cb_file_mtime() {
107
+ local file="$1"
108
+ if [[ "$(uname)" == "Darwin" ]]; then
109
+ stat -f %m "$file" 2>/dev/null || echo "0"
110
+ else
111
+ stat -c %Y "$file" 2>/dev/null || echo "0"
112
+ fi
113
+ }
114
+
115
+ # === Failure Tracking (Sliding Window) ===
116
+ _cb_count_recent_failures() {
117
+ local name="$1"
118
+ local failures_file
119
+ failures_file=$(_cb_failures_file "$name")
120
+
121
+ [[ ! -f "$failures_file" ]] && echo "0" && return
122
+
123
+ local now
124
+ now=$(_cb_now)
125
+ local window_start=$((now - CB_FAILURE_WINDOW_SEC))
126
+ local count=0
127
+
128
+ # Read failure timestamps and count those within window
129
+ while IFS= read -r timestamp; do
130
+ [[ -z "$timestamp" ]] && continue
131
+ if [[ "$timestamp" -ge "$window_start" ]]; then
132
+ count=$((count + 1))
133
+ fi
134
+ done < "$failures_file"
135
+
136
+ echo "$count"
137
+ }
138
+
139
+ _cb_add_failure() {
140
+ local name="$1"
141
+ local failures_file
142
+ failures_file=$(_cb_failures_file "$name")
143
+
144
+ _cb_init
145
+ local now
146
+ now=$(_cb_now)
147
+
148
+ # Append timestamp
149
+ echo "$now" >> "$failures_file" 2>/dev/null || true
150
+
151
+ # Cleanup old entries (keep only last 100)
152
+ if [[ -f "$failures_file" ]]; then
153
+ tail -100 "$failures_file" > "${failures_file}.tmp" 2>/dev/null && \
154
+ mv "${failures_file}.tmp" "$failures_file" 2>/dev/null || true
155
+ fi
156
+ }
157
+
158
+ _cb_clear_failures() {
159
+ local name="$1"
160
+ local failures_file
161
+ failures_file=$(_cb_failures_file "$name")
162
+ rm -f "$failures_file" 2>/dev/null || true
163
+ }
164
+
165
+ # === Success Tracking (Half-Open State) ===
166
+ _cb_count_successes() {
167
+ local name="$1"
168
+ local successes_file
169
+ successes_file=$(_cb_successes_file "$name")
170
+
171
+ [[ ! -f "$successes_file" ]] && echo "0" && return
172
+
173
+ wc -l < "$successes_file" 2>/dev/null | tr -d ' '
174
+ }
175
+
176
+ _cb_add_success() {
177
+ local name="$1"
178
+ local successes_file
179
+ successes_file=$(_cb_successes_file "$name")
180
+
181
+ _cb_init
182
+ echo "$(_cb_now)" >> "$successes_file" 2>/dev/null || true
183
+ }
184
+
185
+ _cb_clear_successes() {
186
+ local name="$1"
187
+ local successes_file
188
+ successes_file=$(_cb_successes_file "$name")
189
+ rm -f "$successes_file" 2>/dev/null || true
190
+ }
191
+
192
+ # === State Transitions ===
193
+ _cb_check_should_open() {
194
+ local name="$1"
195
+ local failure_count
196
+ failure_count=$(_cb_count_recent_failures "$name")
197
+
198
+ if [[ "$failure_count" -ge "$CB_FAILURE_THRESHOLD" ]]; then
199
+ _cb_log "$name: Failure threshold reached ($failure_count >= $CB_FAILURE_THRESHOLD)"
200
+ return 0 # Should open
201
+ fi
202
+ return 1 # Should not open
203
+ }
204
+
205
+ _cb_check_should_half_open() {
206
+ local name="$1"
207
+ local state_file
208
+ state_file=$(_cb_state_file "$name")
209
+
210
+ [[ ! -f "$state_file" ]] && return 1
211
+
212
+ local state_mtime
213
+ state_mtime=$(_cb_file_mtime "$state_file")
214
+ local now
215
+ now=$(_cb_now)
216
+ local age=$((now - state_mtime))
217
+
218
+ if [[ "$age" -ge "$CB_RECOVERY_TIMEOUT_SEC" ]]; then
219
+ _cb_log "$name: Recovery timeout reached (${age}s >= ${CB_RECOVERY_TIMEOUT_SEC}s)"
220
+ return 0 # Should transition to half-open
221
+ fi
222
+ return 1
223
+ }
224
+
225
+ _cb_check_should_close() {
226
+ local name="$1"
227
+ local success_count
228
+ success_count=$(_cb_count_successes "$name")
229
+
230
+ if [[ "$success_count" -ge "$CB_SUCCESS_THRESHOLD" ]]; then
231
+ _cb_log "$name: Success threshold reached ($success_count >= $CB_SUCCESS_THRESHOLD)"
232
+ return 0 # Should close
233
+ fi
234
+ return 1
235
+ }
236
+
237
+ # === Public API ===
238
+
239
+ # Check if request should be allowed
240
+ # Returns 0 if allowed, 1 if circuit is open
241
+ cb_allow_request() {
242
+ local name="${1:-default}"
243
+
244
+ _cb_init
245
+
246
+ local state
247
+ state=$(_cb_read_state "$name")
248
+
249
+ case "$state" in
250
+ "$CB_STATE_CLOSED")
251
+ _cb_log "$name: CLOSED - allowing request"
252
+ return 0
253
+ ;;
254
+
255
+ "$CB_STATE_OPEN")
256
+ # Check if we should transition to half-open
257
+ if _cb_check_should_half_open "$name"; then
258
+ _cb_write_state "$name" "$CB_STATE_HALF_OPEN"
259
+ _cb_clear_successes "$name"
260
+ _cb_log "$name: OPEN -> HALF_OPEN - allowing test request"
261
+ return 0
262
+ fi
263
+ _cb_log "$name: OPEN - rejecting request (fail-fast)"
264
+ return 1
265
+ ;;
266
+
267
+ "$CB_STATE_HALF_OPEN")
268
+ _cb_log "$name: HALF_OPEN - allowing test request"
269
+ return 0
270
+ ;;
271
+
272
+ *)
273
+ # Unknown state, default to closed
274
+ _cb_write_state "$name" "$CB_STATE_CLOSED"
275
+ return 0
276
+ ;;
277
+ esac
278
+ }
279
+
280
+ # Record successful request
281
+ cb_record_success() {
282
+ local name="${1:-default}"
283
+
284
+ local state
285
+ state=$(_cb_read_state "$name")
286
+
287
+ case "$state" in
288
+ "$CB_STATE_CLOSED")
289
+ # Clear any old failures on success
290
+ # (helps prevent lingering failures from keeping count high)
291
+ ;;
292
+
293
+ "$CB_STATE_HALF_OPEN")
294
+ _cb_add_success "$name"
295
+ if _cb_check_should_close "$name"; then
296
+ _cb_write_state "$name" "$CB_STATE_CLOSED"
297
+ _cb_clear_failures "$name"
298
+ _cb_clear_successes "$name"
299
+ _cb_log "$name: HALF_OPEN -> CLOSED (recovered)"
300
+ fi
301
+ ;;
302
+ esac
303
+ }
304
+
305
+ # Record failed request
306
+ cb_record_failure() {
307
+ local name="${1:-default}"
308
+
309
+ local state
310
+ state=$(_cb_read_state "$name")
311
+
312
+ _cb_add_failure "$name"
313
+
314
+ case "$state" in
315
+ "$CB_STATE_CLOSED")
316
+ if _cb_check_should_open "$name"; then
317
+ _cb_write_state "$name" "$CB_STATE_OPEN"
318
+ _cb_log "$name: CLOSED -> OPEN (too many failures)"
319
+ fi
320
+ ;;
321
+
322
+ "$CB_STATE_HALF_OPEN")
323
+ # Any failure in half-open immediately opens circuit
324
+ _cb_write_state "$name" "$CB_STATE_OPEN"
325
+ _cb_clear_successes "$name"
326
+ _cb_log "$name: HALF_OPEN -> OPEN (failed during recovery)"
327
+ ;;
328
+ esac
329
+ }
330
+
331
+ # Get circuit breaker status
332
+ cb_get_status() {
333
+ local name="${1:-default}"
334
+
335
+ _cb_init
336
+
337
+ local state
338
+ state=$(_cb_read_state "$name")
339
+ local failures
340
+ failures=$(_cb_count_recent_failures "$name")
341
+ local successes
342
+ successes=$(_cb_count_successes "$name")
343
+
344
+ echo "{\"name\":\"$name\",\"state\":\"$state\",\"failures\":$failures,\"successes\":$successes,\"failure_threshold\":$CB_FAILURE_THRESHOLD,\"recovery_timeout_sec\":$CB_RECOVERY_TIMEOUT_SEC}"
345
+ }
346
+
347
+ # Force reset circuit breaker
348
+ cb_reset() {
349
+ local name="${1:-default}"
350
+
351
+ _cb_write_state "$name" "$CB_STATE_CLOSED"
352
+ _cb_clear_failures "$name"
353
+ _cb_clear_successes "$name"
354
+ _cb_log "$name: Force reset to CLOSED"
355
+ }
356
+
357
+ # List all circuit breakers
358
+ cb_list_all() {
359
+ _cb_init
360
+
361
+ local result="["
362
+ local first=true
363
+
364
+ for state_file in "$CB_STATE_DIR"/*.state; do
365
+ [[ ! -f "$state_file" ]] && continue
366
+
367
+ local name
368
+ name=$(basename "$state_file" .state)
369
+
370
+ if [[ "$first" == "true" ]]; then
371
+ first=false
372
+ else
373
+ result="$result,"
374
+ fi
375
+
376
+ result="$result$(cb_get_status "$name")"
377
+ done
378
+
379
+ result="$result]"
380
+ echo "$result"
381
+ }