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
@@ -0,0 +1,231 @@
1
+ #!/bin/bash
2
+ # logging.sh - Structured logging for SpecWeave hooks
3
+ #
4
+ # FEATURES:
5
+ # - Structured JSON logs for machine parsing
6
+ # - Human-readable console output
7
+ # - Request ID tracing across hook chains
8
+ # - Log levels (DEBUG, INFO, WARN, ERROR)
9
+ # - Automatic log rotation
10
+ # - Performance timing
11
+ #
12
+ # USAGE:
13
+ # source logging.sh
14
+ # log_init "hook-name"
15
+ # log_info "Processing request"
16
+ # log_error "Something failed" "error_code=123"
17
+ # log_timing_start "operation"
18
+ # # ... do work ...
19
+ # log_timing_end "operation"
20
+ #
21
+ # v1.0.0 - Initial implementation (2025-12-17)
22
+
23
+ # === Configuration ===
24
+ LOG_DIR="${SPECWEAVE_LOG_DIR:-.specweave/logs/hooks}"
25
+ LOG_LEVEL="${LOG_LEVEL:-INFO}" # DEBUG, INFO, WARN, ERROR
26
+ LOG_FORMAT="${LOG_FORMAT:-json}" # json, text
27
+ LOG_MAX_SIZE_KB="${LOG_MAX_SIZE_KB:-1024}" # 1MB per file
28
+ LOG_ROTATION_COUNT="${LOG_ROTATION_COUNT:-5}"
29
+
30
+ # Log level numeric values
31
+ declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
32
+
33
+ # Request context
34
+ _LOG_REQUEST_ID=""
35
+ _LOG_HOOK_NAME=""
36
+ _LOG_START_TIME=""
37
+ declare -A _LOG_TIMINGS
38
+
39
+ # === Initialization ===
40
+ log_init() {
41
+ local hook_name="$1"
42
+
43
+ mkdir -p "$LOG_DIR" 2>/dev/null || true
44
+
45
+ _LOG_HOOK_NAME="$hook_name"
46
+ _LOG_REQUEST_ID="${REQUEST_ID:-$(generate_request_id)}"
47
+ _LOG_START_TIME=$(get_timestamp_ms)
48
+
49
+ export REQUEST_ID="$_LOG_REQUEST_ID"
50
+ }
51
+
52
+ # === Request ID Generation ===
53
+ generate_request_id() {
54
+ # Format: HHMMSS-RANDOM (compact but unique enough for tracing)
55
+ local time_part
56
+ time_part=$(date +%H%M%S)
57
+ local random_part
58
+ random_part=$(printf '%04x' $((RANDOM % 65536)))
59
+ echo "${time_part}-${random_part}"
60
+ }
61
+
62
+ # === Timestamp ===
63
+ get_timestamp_ms() {
64
+ if command -v gdate &>/dev/null; then
65
+ gdate +%s%3N
66
+ elif date +%s%N &>/dev/null 2>&1; then
67
+ echo $(($(date +%s%N) / 1000000))
68
+ else
69
+ echo "$(($(date +%s) * 1000))"
70
+ fi
71
+ }
72
+
73
+ get_timestamp_iso() {
74
+ date -u +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ"
75
+ }
76
+
77
+ # === Level Check ===
78
+ _should_log() {
79
+ local level="$1"
80
+ local level_num="${LOG_LEVELS[$level]:-1}"
81
+ local threshold_num="${LOG_LEVELS[$LOG_LEVEL]:-1}"
82
+ [[ "$level_num" -ge "$threshold_num" ]]
83
+ }
84
+
85
+ # === Log Rotation ===
86
+ _rotate_log_if_needed() {
87
+ local log_file="$1"
88
+
89
+ [[ ! -f "$log_file" ]] && return
90
+
91
+ local size_kb
92
+ if [[ "$(uname)" == "Darwin" ]]; then
93
+ size_kb=$(($(stat -f %z "$log_file" 2>/dev/null || echo 0) / 1024))
94
+ else
95
+ size_kb=$(($(stat -c %s "$log_file" 2>/dev/null || echo 0) / 1024))
96
+ fi
97
+
98
+ if [[ "$size_kb" -gt "$LOG_MAX_SIZE_KB" ]]; then
99
+ # Rotate logs
100
+ for i in $(seq $((LOG_ROTATION_COUNT - 1)) -1 1); do
101
+ [[ -f "${log_file}.$i" ]] && mv "${log_file}.$i" "${log_file}.$((i + 1))" 2>/dev/null
102
+ done
103
+ mv "$log_file" "${log_file}.1" 2>/dev/null || true
104
+ fi
105
+ }
106
+
107
+ # === Core Logging Function ===
108
+ _log() {
109
+ local level="$1"
110
+ local message="$2"
111
+ shift 2
112
+ local extra_fields=("$@")
113
+
114
+ _should_log "$level" || return 0
115
+
116
+ local timestamp
117
+ timestamp=$(get_timestamp_iso)
118
+
119
+ local log_file="$LOG_DIR/${_LOG_HOOK_NAME:-hooks}.log"
120
+ _rotate_log_if_needed "$log_file"
121
+
122
+ if [[ "$LOG_FORMAT" == "json" ]]; then
123
+ # Build JSON log entry
124
+ local json="{\"ts\":\"$timestamp\",\"level\":\"$level\",\"hook\":\"${_LOG_HOOK_NAME:-unknown}\",\"rid\":\"${_LOG_REQUEST_ID:-none}\",\"msg\":\"$message\""
125
+
126
+ # Add extra fields
127
+ for field in "${extra_fields[@]}"; do
128
+ local key="${field%%=*}"
129
+ local value="${field#*=}"
130
+ # Escape quotes in value
131
+ value="${value//\"/\\\"}"
132
+ json="$json,\"$key\":\"$value\""
133
+ done
134
+
135
+ json="$json}"
136
+ echo "$json" >> "$log_file" 2>/dev/null || true
137
+ else
138
+ # Text format
139
+ local prefix="[$timestamp] [$level] [${_LOG_HOOK_NAME:-unknown}] [${_LOG_REQUEST_ID:-none}]"
140
+ echo "$prefix $message ${extra_fields[*]}" >> "$log_file" 2>/dev/null || true
141
+ fi
142
+
143
+ # Also output to stderr if DEBUG level and debug mode enabled
144
+ if [[ "$level" == "DEBUG" ]] && [[ "${HOOK_DEBUG:-0}" == "1" ]]; then
145
+ echo "[HOOK-$level] $_LOG_HOOK_NAME: $message" >&2
146
+ fi
147
+ }
148
+
149
+ # === Public Logging Functions ===
150
+ log_debug() {
151
+ _log "DEBUG" "$1" "${@:2}"
152
+ }
153
+
154
+ log_info() {
155
+ _log "INFO" "$1" "${@:2}"
156
+ }
157
+
158
+ log_warn() {
159
+ _log "WARN" "$1" "${@:2}"
160
+ }
161
+
162
+ log_error() {
163
+ _log "ERROR" "$1" "${@:2}"
164
+ }
165
+
166
+ # === Performance Timing ===
167
+ log_timing_start() {
168
+ local operation="$1"
169
+ _LOG_TIMINGS[$operation]=$(get_timestamp_ms)
170
+ }
171
+
172
+ log_timing_end() {
173
+ local operation="$1"
174
+ local start_time="${_LOG_TIMINGS[$operation]:-0}"
175
+
176
+ if [[ "$start_time" -gt 0 ]]; then
177
+ local end_time
178
+ end_time=$(get_timestamp_ms)
179
+ local duration_ms=$((end_time - start_time))
180
+
181
+ log_debug "Timing: $operation completed" "duration_ms=$duration_ms"
182
+ unset "_LOG_TIMINGS[$operation]"
183
+
184
+ echo "$duration_ms"
185
+ else
186
+ echo "0"
187
+ fi
188
+ }
189
+
190
+ # === Request Summary ===
191
+ log_request_summary() {
192
+ local status="$1"
193
+ local details="$2"
194
+
195
+ local total_duration=0
196
+ if [[ -n "$_LOG_START_TIME" ]]; then
197
+ local end_time
198
+ end_time=$(get_timestamp_ms)
199
+ total_duration=$((end_time - _LOG_START_TIME))
200
+ fi
201
+
202
+ log_info "Request completed" "status=$status" "total_ms=$total_duration" "details=$details"
203
+ }
204
+
205
+ # === Aggregate Log Stats ===
206
+ get_log_stats() {
207
+ local hook_name="${1:-$_LOG_HOOK_NAME}"
208
+ local log_file="$LOG_DIR/${hook_name}.log"
209
+
210
+ [[ ! -f "$log_file" ]] && echo '{"total":0,"errors":0,"warns":0}' && return
211
+
212
+ local total errors warns
213
+ total=$(wc -l < "$log_file" 2>/dev/null | tr -d ' ')
214
+ errors=$(grep -c '"level":"ERROR"' "$log_file" 2>/dev/null || echo 0)
215
+ warns=$(grep -c '"level":"WARN"' "$log_file" 2>/dev/null || echo 0)
216
+
217
+ echo "{\"total\":$total,\"errors\":$errors,\"warns\":$warns}"
218
+ }
219
+
220
+ # === Correlation ID Propagation ===
221
+ # Use this to pass request ID to child processes
222
+ export_request_context() {
223
+ export REQUEST_ID="$_LOG_REQUEST_ID"
224
+ export HOOK_NAME="$_LOG_HOOK_NAME"
225
+ }
226
+
227
+ # Import context from parent
228
+ import_request_context() {
229
+ _LOG_REQUEST_ID="${REQUEST_ID:-$(generate_request_id)}"
230
+ _LOG_HOOK_NAME="${HOOK_NAME:-unknown}"
231
+ }
@@ -0,0 +1,347 @@
1
+ #!/bin/bash
2
+ # metrics.sh - Hook Metrics Collection and Health Monitoring
3
+ #
4
+ # FEATURES:
5
+ # - Execution time tracking (p50, p95, p99)
6
+ # - Success/failure rate tracking
7
+ # - Request throughput (requests/second)
8
+ # - Concurrent execution tracking
9
+ # - Health score calculation
10
+ # - Metrics aggregation and rollup
11
+ #
12
+ # STORAGE:
13
+ # - Ring buffer for recent metrics (last 1000 entries)
14
+ # - Hourly rollups for historical data
15
+ # - Compact binary-like format for efficiency
16
+ #
17
+ # USAGE:
18
+ # source metrics.sh
19
+ # metrics_init "hook-name"
20
+ # metrics_start_request
21
+ # # ... do work ...
22
+ # metrics_end_request "success" # or "failure", "timeout", "skipped"
23
+ #
24
+ # v1.0.0 - Initial implementation (2025-12-17)
25
+
26
+ set -o pipefail
27
+
28
+ # === Configuration ===
29
+ METRICS_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}/metrics"
30
+ METRICS_BUFFER_SIZE="${METRICS_BUFFER_SIZE:-1000}" # Ring buffer entries
31
+ METRICS_DEBUG="${METRICS_DEBUG:-0}"
32
+
33
+ # Current request context
34
+ _METRICS_HOOK_NAME=""
35
+ _METRICS_START_TIME=""
36
+
37
+ # === Initialization ===
38
+ metrics_init() {
39
+ local hook_name="$1"
40
+ _METRICS_HOOK_NAME="$hook_name"
41
+ mkdir -p "$METRICS_DIR" 2>/dev/null || true
42
+ }
43
+
44
+ # === Timestamp ===
45
+ _metrics_now_ms() {
46
+ if command -v gdate &>/dev/null; then
47
+ gdate +%s%3N
48
+ elif date +%s%N &>/dev/null 2>&1; then
49
+ echo $(($(date +%s%N) / 1000000))
50
+ else
51
+ echo "$(($(date +%s) * 1000))"
52
+ fi
53
+ }
54
+
55
+ _metrics_now_sec() {
56
+ date +%s
57
+ }
58
+
59
+ # === File Paths ===
60
+ _metrics_buffer_file() {
61
+ local hook_name="${1:-$_METRICS_HOOK_NAME}"
62
+ echo "$METRICS_DIR/${hook_name}.buffer"
63
+ }
64
+
65
+ _metrics_stats_file() {
66
+ local hook_name="${1:-$_METRICS_HOOK_NAME}"
67
+ echo "$METRICS_DIR/${hook_name}.stats"
68
+ }
69
+
70
+ _metrics_hourly_file() {
71
+ local hook_name="${1:-$_METRICS_HOOK_NAME}"
72
+ local hour
73
+ hour=$(date +%Y%m%d%H)
74
+ echo "$METRICS_DIR/${hook_name}.hourly.${hour}"
75
+ }
76
+
77
+ # === Request Tracking ===
78
+ metrics_start_request() {
79
+ _METRICS_START_TIME=$(_metrics_now_ms)
80
+ }
81
+
82
+ metrics_end_request() {
83
+ local status="${1:-success}" # success, failure, timeout, skipped
84
+
85
+ [[ -z "$_METRICS_START_TIME" ]] && return
86
+
87
+ local end_time
88
+ end_time=$(_metrics_now_ms)
89
+ local duration_ms=$((_METRICS_START_TIME > 0 ? end_time - _METRICS_START_TIME : 0))
90
+
91
+ # Record to buffer
92
+ _metrics_record_entry "$duration_ms" "$status"
93
+
94
+ # Update running stats
95
+ _metrics_update_stats "$duration_ms" "$status"
96
+
97
+ _METRICS_START_TIME=""
98
+ }
99
+
100
+ # === Buffer Management (Ring Buffer) ===
101
+ _metrics_record_entry() {
102
+ local duration_ms="$1"
103
+ local status="$2"
104
+ local timestamp
105
+ timestamp=$(_metrics_now_sec)
106
+
107
+ local buffer_file
108
+ buffer_file=$(_metrics_buffer_file)
109
+
110
+ # Compact format: timestamp,duration_ms,status_code
111
+ # Status codes: 0=success, 1=failure, 2=timeout, 3=skipped
112
+ local status_code=0
113
+ case "$status" in
114
+ success) status_code=0 ;;
115
+ failure) status_code=1 ;;
116
+ timeout) status_code=2 ;;
117
+ skipped) status_code=3 ;;
118
+ esac
119
+
120
+ echo "${timestamp},${duration_ms},${status_code}" >> "$buffer_file" 2>/dev/null || true
121
+
122
+ # Trim buffer if too large
123
+ if [[ -f "$buffer_file" ]]; then
124
+ local line_count
125
+ line_count=$(wc -l < "$buffer_file" 2>/dev/null | tr -d ' ')
126
+ if [[ "$line_count" -gt "$METRICS_BUFFER_SIZE" ]]; then
127
+ tail -$((METRICS_BUFFER_SIZE / 2)) "$buffer_file" > "${buffer_file}.tmp" 2>/dev/null && \
128
+ mv "${buffer_file}.tmp" "$buffer_file" 2>/dev/null || true
129
+ fi
130
+ fi
131
+ }
132
+
133
+ # === Running Statistics ===
134
+ _metrics_update_stats() {
135
+ local duration_ms="$1"
136
+ local status="$2"
137
+
138
+ local stats_file
139
+ stats_file=$(_metrics_stats_file)
140
+
141
+ # Read current stats
142
+ local total=0 success=0 failure=0 timeout=0 skipped=0
143
+ local sum_duration=0 min_duration=999999 max_duration=0
144
+
145
+ if [[ -f "$stats_file" ]]; then
146
+ # Format: total,success,failure,timeout,skipped,sum_duration,min,max
147
+ IFS=',' read -r total success failure timeout skipped sum_duration min_duration max_duration < "$stats_file" 2>/dev/null || true
148
+ fi
149
+
150
+ # Update counters
151
+ total=$((total + 1))
152
+ case "$status" in
153
+ success) success=$((success + 1)) ;;
154
+ failure) failure=$((failure + 1)) ;;
155
+ timeout) timeout=$((timeout + 1)) ;;
156
+ skipped) skipped=$((skipped + 1)) ;;
157
+ esac
158
+
159
+ # Update duration stats (only for non-skipped)
160
+ if [[ "$status" != "skipped" ]]; then
161
+ sum_duration=$((sum_duration + duration_ms))
162
+ [[ "$duration_ms" -lt "$min_duration" ]] && min_duration=$duration_ms
163
+ [[ "$duration_ms" -gt "$max_duration" ]] && max_duration=$duration_ms
164
+ fi
165
+
166
+ # Write updated stats
167
+ echo "${total},${success},${failure},${timeout},${skipped},${sum_duration},${min_duration},${max_duration}" > "$stats_file" 2>/dev/null || true
168
+ }
169
+
170
+ # === Percentile Calculation ===
171
+ _metrics_calculate_percentile() {
172
+ local hook_name="$1"
173
+ local percentile="$2" # 50, 95, 99
174
+
175
+ local buffer_file
176
+ buffer_file=$(_metrics_buffer_file "$hook_name")
177
+
178
+ [[ ! -f "$buffer_file" ]] && echo "0" && return
179
+
180
+ # Extract durations (second field) and sort
181
+ local sorted_durations
182
+ sorted_durations=$(cut -d',' -f2 "$buffer_file" 2>/dev/null | sort -n)
183
+
184
+ local count
185
+ count=$(echo "$sorted_durations" | wc -l | tr -d ' ')
186
+
187
+ [[ "$count" -eq 0 ]] && echo "0" && return
188
+
189
+ # Calculate index for percentile
190
+ local index=$(( (count * percentile) / 100 ))
191
+ [[ "$index" -lt 1 ]] && index=1
192
+
193
+ # Get value at index
194
+ echo "$sorted_durations" | sed -n "${index}p"
195
+ }
196
+
197
+ # === Public API ===
198
+
199
+ # Get current metrics for a hook
200
+ metrics_get() {
201
+ local hook_name="${1:-$_METRICS_HOOK_NAME}"
202
+ local stats_file
203
+ stats_file=$(_metrics_stats_file "$hook_name")
204
+
205
+ local total=0 success=0 failure=0 timeout=0 skipped=0
206
+ local sum_duration=0 min_duration=0 max_duration=0
207
+
208
+ if [[ -f "$stats_file" ]]; then
209
+ IFS=',' read -r total success failure timeout skipped sum_duration min_duration max_duration < "$stats_file" 2>/dev/null || true
210
+ fi
211
+
212
+ # Calculate rates
213
+ local success_rate=100
214
+ local executed=$((total - skipped))
215
+ if [[ "$executed" -gt 0 ]]; then
216
+ success_rate=$(( (success * 100) / executed ))
217
+ fi
218
+
219
+ # Calculate average duration
220
+ local avg_duration=0
221
+ if [[ "$executed" -gt 0 ]]; then
222
+ avg_duration=$((sum_duration / executed))
223
+ fi
224
+
225
+ # Get percentiles
226
+ local p50 p95 p99
227
+ p50=$(_metrics_calculate_percentile "$hook_name" 50)
228
+ p95=$(_metrics_calculate_percentile "$hook_name" 95)
229
+ p99=$(_metrics_calculate_percentile "$hook_name" 99)
230
+
231
+ echo "{\"hook\":\"$hook_name\",\"total\":$total,\"success\":$success,\"failure\":$failure,\"timeout\":$timeout,\"skipped\":$skipped,\"success_rate\":$success_rate,\"avg_ms\":$avg_duration,\"min_ms\":$min_duration,\"max_ms\":$max_duration,\"p50_ms\":$p50,\"p95_ms\":$p95,\"p99_ms\":$p99}"
232
+ }
233
+
234
+ # Calculate health score (0-100)
235
+ metrics_health_score() {
236
+ local hook_name="${1:-$_METRICS_HOOK_NAME}"
237
+
238
+ local stats_file
239
+ stats_file=$(_metrics_stats_file "$hook_name")
240
+
241
+ [[ ! -f "$stats_file" ]] && echo "100" && return
242
+
243
+ local total success failure timeout skipped sum_duration min_duration max_duration
244
+ IFS=',' read -r total success failure timeout skipped sum_duration min_duration max_duration < "$stats_file" 2>/dev/null || true
245
+
246
+ local executed=$((total - skipped))
247
+ [[ "$executed" -eq 0 ]] && echo "100" && return
248
+
249
+ # Score components:
250
+ # - Success rate: 60% weight
251
+ # - No timeouts: 25% weight
252
+ # - Low latency: 15% weight
253
+
254
+ local success_rate=$(( (success * 100) / executed ))
255
+ local timeout_rate=$(( (timeout * 100) / executed ))
256
+ local avg_duration=$((sum_duration / executed))
257
+
258
+ # Success rate score (0-60)
259
+ local success_score=$((success_rate * 60 / 100))
260
+
261
+ # Timeout score (0-25, penalize timeouts heavily)
262
+ local timeout_score=$((25 - (timeout_rate * 25 / 100)))
263
+ [[ "$timeout_score" -lt 0 ]] && timeout_score=0
264
+
265
+ # Latency score (0-15, based on avg duration)
266
+ # <100ms = 15, 100-500ms = 10, 500-1000ms = 5, >1000ms = 0
267
+ local latency_score=0
268
+ if [[ "$avg_duration" -lt 100 ]]; then
269
+ latency_score=15
270
+ elif [[ "$avg_duration" -lt 500 ]]; then
271
+ latency_score=10
272
+ elif [[ "$avg_duration" -lt 1000 ]]; then
273
+ latency_score=5
274
+ fi
275
+
276
+ local total_score=$((success_score + timeout_score + latency_score))
277
+ echo "$total_score"
278
+ }
279
+
280
+ # Get all hooks metrics summary
281
+ metrics_get_all() {
282
+ local result="["
283
+ local first=true
284
+
285
+ for stats_file in "$METRICS_DIR"/*.stats; do
286
+ [[ ! -f "$stats_file" ]] && continue
287
+
288
+ local hook_name
289
+ hook_name=$(basename "$stats_file" .stats)
290
+
291
+ if [[ "$first" == "true" ]]; then
292
+ first=false
293
+ else
294
+ result="$result,"
295
+ fi
296
+
297
+ result="$result$(metrics_get "$hook_name")"
298
+ done
299
+
300
+ result="$result]"
301
+ echo "$result"
302
+ }
303
+
304
+ # Reset metrics for a hook
305
+ metrics_reset() {
306
+ local hook_name="${1:-$_METRICS_HOOK_NAME}"
307
+
308
+ rm -f "$METRICS_DIR/${hook_name}.buffer" 2>/dev/null || true
309
+ rm -f "$METRICS_DIR/${hook_name}.stats" 2>/dev/null || true
310
+ rm -f "$METRICS_DIR/${hook_name}.hourly."* 2>/dev/null || true
311
+ }
312
+
313
+ # Get system-wide health summary
314
+ metrics_system_health() {
315
+ local total_hooks=0
316
+ local healthy_hooks=0
317
+ local degraded_hooks=0
318
+ local unhealthy_hooks=0
319
+
320
+ for stats_file in "$METRICS_DIR"/*.stats; do
321
+ [[ ! -f "$stats_file" ]] && continue
322
+
323
+ local hook_name
324
+ hook_name=$(basename "$stats_file" .stats)
325
+ local health_score
326
+ health_score=$(metrics_health_score "$hook_name")
327
+
328
+ total_hooks=$((total_hooks + 1))
329
+
330
+ if [[ "$health_score" -ge 80 ]]; then
331
+ healthy_hooks=$((healthy_hooks + 1))
332
+ elif [[ "$health_score" -ge 50 ]]; then
333
+ degraded_hooks=$((degraded_hooks + 1))
334
+ else
335
+ unhealthy_hooks=$((unhealthy_hooks + 1))
336
+ fi
337
+ done
338
+
339
+ local overall_health="healthy"
340
+ if [[ "$unhealthy_hooks" -gt 0 ]]; then
341
+ overall_health="unhealthy"
342
+ elif [[ "$degraded_hooks" -gt 0 ]]; then
343
+ overall_health="degraded"
344
+ fi
345
+
346
+ echo "{\"status\":\"$overall_health\",\"total_hooks\":$total_hooks,\"healthy\":$healthy_hooks,\"degraded\":$degraded_hooks,\"unhealthy\":$unhealthy_hooks}"
347
+ }