specweave 1.0.31 → 1.0.33
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-plugin/marketplace.json +1 -1
- package/CLAUDE.md +205 -148
- package/README.md +0 -2
- package/bin/specweave.js +11 -0
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/update-instructions.d.ts +16 -0
- package/dist/src/cli/commands/update-instructions.d.ts.map +1 -0
- package/dist/src/cli/commands/update-instructions.js +134 -0
- package/dist/src/cli/commands/update-instructions.js.map +1 -0
- package/dist/src/cli/helpers/init/directory-structure.d.ts +28 -1
- package/dist/src/cli/helpers/init/directory-structure.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/directory-structure.js +163 -33
- package/dist/src/cli/helpers/init/directory-structure.js.map +1 -1
- package/dist/src/cli/helpers/init/index.d.ts +2 -1
- package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/index.js +3 -1
- package/dist/src/cli/helpers/init/index.js.map +1 -1
- package/dist/src/cli/helpers/init/instruction-file-merger.d.ts +23 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js +243 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -0
- package/dist/src/cli/helpers/init/plugin-installer.js +49 -0
- package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
- package/dist/src/config/types.d.ts +2 -2
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts +26 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts.map +1 -1
- package/dist/src/core/living-docs/external-sync-orchestrator.js +61 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.js.map +1 -1
- package/dist/src/core/living-docs/scaffolding/index.d.ts +12 -0
- package/dist/src/core/living-docs/scaffolding/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/index.js +15 -0
- package/dist/src/core/living-docs/scaffolding/index.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/merger.d.ts +183 -0
- package/dist/src/core/living-docs/scaffolding/merger.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/merger.js +523 -0
- package/dist/src/core/living-docs/scaffolding/merger.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.d.ts +102 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.js +346 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.d.ts +108 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.js +204 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.js.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts +38 -2
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/generators.js +65 -10
- package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -1
- package/dist/src/core/tools/index.d.ts +11 -0
- package/dist/src/core/tools/index.d.ts.map +1 -0
- package/dist/src/core/tools/index.js +10 -0
- package/dist/src/core/tools/index.js.map +1 -0
- package/dist/src/core/tools/tool-event-bus.d.ts +33 -0
- package/dist/src/core/tools/tool-event-bus.d.ts.map +1 -0
- package/dist/src/core/tools/tool-event-bus.js +84 -0
- package/dist/src/core/tools/tool-event-bus.js.map +1 -0
- package/dist/src/core/tools/tool-index-builder.d.ts +27 -0
- package/dist/src/core/tools/tool-index-builder.d.ts.map +1 -0
- package/dist/src/core/tools/tool-index-builder.js +289 -0
- package/dist/src/core/tools/tool-index-builder.js.map +1 -0
- package/dist/src/core/tools/tool-registry.d.ts +51 -0
- package/dist/src/core/tools/tool-registry.d.ts.map +1 -0
- package/dist/src/core/tools/tool-registry.js +224 -0
- package/dist/src/core/tools/tool-registry.js.map +1 -0
- package/dist/src/core/tools/tool-search-engine.d.ts +22 -0
- package/dist/src/core/tools/tool-search-engine.d.ts.map +1 -0
- package/dist/src/core/tools/tool-search-engine.js +174 -0
- package/dist/src/core/tools/tool-search-engine.js.map +1 -0
- package/dist/src/core/tools/types/tool-registry-types.d.ts +112 -0
- package/dist/src/core/tools/types/tool-registry-types.d.ts.map +1 -0
- package/dist/src/core/tools/types/tool-registry-types.js +7 -0
- package/dist/src/core/tools/types/tool-registry-types.js.map +1 -0
- package/dist/src/init/compliance/types.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/hooks.json +3 -13
- package/plugins/specweave/hooks/lib/common-setup.sh +47 -321
- package/plugins/specweave/hooks/lib/migrate-increment-work.sh +5 -5
- package/plugins/specweave/hooks/lib/sync-spec-content.sh +5 -5
- package/plugins/specweave/hooks/universal/dispatcher.mjs +4 -5
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +43 -296
- package/plugins/specweave/hooks/universal/hook-wrapper.sh +3 -1
- package/plugins/specweave/hooks/user-prompt-submit.sh +1 -1
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +2 -2
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -10
- package/plugins/specweave/hooks/v2/guards/completion-guard.sh +12 -29
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +27 -29
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +10 -4
- package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +139 -0
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +4 -2
- package/plugins/specweave/hooks/v2/session-end.sh +3 -1
- package/plugins/specweave/hooks/v2/session-start.sh +3 -1
- package/plugins/specweave/skills/increment-planner/templates/plan.md +14 -0
- package/plugins/specweave/skills/update-instructions/SKILL.md +80 -0
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh +1 -1
- package/plugins/specweave-mobile/README.md +55 -35
- package/plugins/specweave-mobile/agents/mobile-architect/AGENT.md +805 -329
- package/plugins/specweave-mobile/skills/expo-workflow/SKILL.md +226 -9
- package/plugins/specweave-mobile/skills/native-modules/SKILL.md +221 -20
- package/plugins/specweave-mobile/skills/performance-optimization/SKILL.md +186 -14
- package/plugins/specweave-mobile/skills/react-native-setup/SKILL.md +151 -54
- package/plugins/specweave-release/commands/npm.md +61 -17
- package/plugins/specweave-release/hooks/post-task-completion.sh +2 -3
- package/src/templates/AGENTS.md.template +34 -0
- package/src/templates/CLAUDE.md.template +121 -155
- package/plugins/specweave/hooks/config-env-separator.sh +0 -99
- package/plugins/specweave/hooks/github-metadata-guard.sh +0 -73
- package/plugins/specweave/hooks/lib/circuit-breaker.sh +0 -381
- package/plugins/specweave/hooks/lib/crash-prevention.sh +0 -336
- package/plugins/specweave/hooks/lib/logging.sh +0 -231
- package/plugins/specweave/hooks/lib/metrics.sh +0 -347
- package/plugins/specweave/hooks/lib/semaphore.sh +0 -216
- package/plugins/specweave/hooks/project-folder-guard.sh +0 -274
- package/plugins/specweave/hooks/spec-project-validator.sh +0 -210
- package/plugins/specweave/hooks/v2/guards/bash-file-guard.sh +0 -212
- package/plugins/specweave/hooks/v2/guards/bash-file-guard.test.sh +0 -163
- package/plugins/specweave/hooks/v2/guards/features-folder-guard.sh +0 -51
- package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +0 -63
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +0 -335
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +0 -406
|
@@ -1,347 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# semaphore.sh - File-based semaphore for limiting concurrent hook execution
|
|
3
|
-
#
|
|
4
|
-
# PROBLEM SOLVED:
|
|
5
|
-
# Process storms occur when many hooks spawn simultaneously, overwhelming the system.
|
|
6
|
-
# Instead of detecting storms and blocking EVERYTHING (current behavior), this semaphore
|
|
7
|
-
# limits concurrency properly - excess requests wait or timeout gracefully.
|
|
8
|
-
#
|
|
9
|
-
# USAGE:
|
|
10
|
-
# source semaphore.sh
|
|
11
|
-
# if acquire_semaphore "hook-name" 10 5000; then
|
|
12
|
-
# # Do work
|
|
13
|
-
# release_semaphore "hook-name"
|
|
14
|
-
# else
|
|
15
|
-
# # Timeout - return safe default
|
|
16
|
-
# fi
|
|
17
|
-
#
|
|
18
|
-
# DESIGN:
|
|
19
|
-
# - Uses file-based locks (portable, no dependencies)
|
|
20
|
-
# - Configurable max concurrent slots
|
|
21
|
-
# - Configurable timeout with exponential backoff
|
|
22
|
-
# - Auto-cleanup of stale locks (older than 30s)
|
|
23
|
-
# - Request queuing with FIFO ordering
|
|
24
|
-
#
|
|
25
|
-
# v1.0.0 - Initial implementation (2025-12-17)
|
|
26
|
-
|
|
27
|
-
set -o pipefail
|
|
28
|
-
|
|
29
|
-
# === Configuration ===
|
|
30
|
-
SEMAPHORE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}/semaphores"
|
|
31
|
-
SEMAPHORE_MAX_AGE_SECONDS=30 # Stale lock threshold
|
|
32
|
-
SEMAPHORE_DEBUG="${SEMAPHORE_DEBUG:-0}"
|
|
33
|
-
|
|
34
|
-
# === Initialization ===
|
|
35
|
-
_init_semaphore_dir() {
|
|
36
|
-
mkdir -p "$SEMAPHORE_DIR" 2>/dev/null || true
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# === Logging ===
|
|
40
|
-
_sem_log() {
|
|
41
|
-
[[ "$SEMAPHORE_DEBUG" == "1" ]] && echo "[SEM $(date +%H:%M:%S.%3N)] $*" >&2
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
# === Cleanup stale locks ===
|
|
45
|
-
# Locks older than SEMAPHORE_MAX_AGE_SECONDS are considered abandoned
|
|
46
|
-
cleanup_stale_locks() {
|
|
47
|
-
local name="$1"
|
|
48
|
-
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
49
|
-
|
|
50
|
-
[[ ! -d "$lock_dir" ]] && return 0
|
|
51
|
-
|
|
52
|
-
local now
|
|
53
|
-
now=$(date +%s)
|
|
54
|
-
|
|
55
|
-
for lock_file in "$lock_dir"/*.lock; do
|
|
56
|
-
[[ ! -f "$lock_file" ]] && continue
|
|
57
|
-
|
|
58
|
-
local mtime
|
|
59
|
-
if [[ "$(uname)" == "Darwin" ]]; then
|
|
60
|
-
mtime=$(stat -f %m "$lock_file" 2>/dev/null || echo "0")
|
|
61
|
-
else
|
|
62
|
-
mtime=$(stat -c %Y "$lock_file" 2>/dev/null || echo "0")
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
local age=$((now - mtime))
|
|
66
|
-
if [[ "$age" -gt "$SEMAPHORE_MAX_AGE_SECONDS" ]]; then
|
|
67
|
-
_sem_log "Cleaning stale lock: $lock_file (age: ${age}s)"
|
|
68
|
-
rm -f "$lock_file" 2>/dev/null || true
|
|
69
|
-
fi
|
|
70
|
-
done
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
# === Count active slots ===
|
|
74
|
-
count_active_slots() {
|
|
75
|
-
local name="$1"
|
|
76
|
-
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
77
|
-
|
|
78
|
-
[[ ! -d "$lock_dir" ]] && echo "0" && return
|
|
79
|
-
|
|
80
|
-
local count=0
|
|
81
|
-
for lock_file in "$lock_dir"/*.lock; do
|
|
82
|
-
[[ -f "$lock_file" ]] && count=$((count + 1))
|
|
83
|
-
done
|
|
84
|
-
|
|
85
|
-
echo "$count"
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
# === Acquire semaphore slot ===
|
|
89
|
-
# Args: name, max_slots, timeout_ms
|
|
90
|
-
# Returns: 0 if acquired, 1 if timeout
|
|
91
|
-
acquire_semaphore() {
|
|
92
|
-
local name="${1:-default}"
|
|
93
|
-
local max_slots="${2:-10}"
|
|
94
|
-
local timeout_ms="${3:-5000}"
|
|
95
|
-
|
|
96
|
-
_init_semaphore_dir
|
|
97
|
-
|
|
98
|
-
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
99
|
-
mkdir -p "$lock_dir" 2>/dev/null || true
|
|
100
|
-
|
|
101
|
-
# Generate unique slot ID (use random instead of %N which doesn't work on macOS)
|
|
102
|
-
local slot_id="$$-$RANDOM$RANDOM"
|
|
103
|
-
local lock_file="$lock_dir/${slot_id}.lock"
|
|
104
|
-
|
|
105
|
-
# Store slot ID for release
|
|
106
|
-
export _SEMAPHORE_SLOT_ID="$slot_id"
|
|
107
|
-
export _SEMAPHORE_NAME="$name"
|
|
108
|
-
export _SEMAPHORE_LOCK_FILE="$lock_file"
|
|
109
|
-
|
|
110
|
-
local start_time
|
|
111
|
-
# macOS doesn't support %N, use seconds * 1000 as approximation
|
|
112
|
-
if command -v gdate &>/dev/null; then
|
|
113
|
-
start_time=$(gdate +%s%3N)
|
|
114
|
-
else
|
|
115
|
-
start_time=$(($(date +%s) * 1000))
|
|
116
|
-
fi
|
|
117
|
-
|
|
118
|
-
local attempt=0
|
|
119
|
-
local backoff_ms=10 # Start with 10ms backoff
|
|
120
|
-
local max_backoff_ms=200
|
|
121
|
-
|
|
122
|
-
while true; do
|
|
123
|
-
# Cleanup stale locks periodically (every 5 attempts)
|
|
124
|
-
[[ $((attempt % 5)) -eq 0 ]] && cleanup_stale_locks "$name"
|
|
125
|
-
|
|
126
|
-
local current_slots
|
|
127
|
-
current_slots=$(count_active_slots "$name")
|
|
128
|
-
|
|
129
|
-
if [[ "$current_slots" -lt "$max_slots" ]]; then
|
|
130
|
-
# Try to acquire slot atomically
|
|
131
|
-
if (set -o noclobber; echo "$$" > "$lock_file") 2>/dev/null; then
|
|
132
|
-
_sem_log "Acquired slot $slot_id for $name (slots: $((current_slots + 1))/$max_slots)"
|
|
133
|
-
return 0
|
|
134
|
-
fi
|
|
135
|
-
fi
|
|
136
|
-
|
|
137
|
-
# Check timeout
|
|
138
|
-
local now
|
|
139
|
-
if command -v gdate &>/dev/null; then
|
|
140
|
-
now=$(gdate +%s%3N)
|
|
141
|
-
else
|
|
142
|
-
now=$(($(date +%s) * 1000))
|
|
143
|
-
fi
|
|
144
|
-
local elapsed=$((now - start_time))
|
|
145
|
-
|
|
146
|
-
if [[ "$elapsed" -ge "$timeout_ms" ]]; then
|
|
147
|
-
_sem_log "Timeout acquiring $name after ${elapsed}ms (slots: $current_slots/$max_slots)"
|
|
148
|
-
return 1
|
|
149
|
-
fi
|
|
150
|
-
|
|
151
|
-
# Exponential backoff with jitter
|
|
152
|
-
local jitter=$((RANDOM % 20))
|
|
153
|
-
local sleep_ms=$((backoff_ms + jitter))
|
|
154
|
-
|
|
155
|
-
_sem_log "Waiting for slot $name (attempt $attempt, backoff ${sleep_ms}ms, slots: $current_slots/$max_slots)"
|
|
156
|
-
|
|
157
|
-
# Sleep (convert ms to fractional seconds)
|
|
158
|
-
sleep "0.$(printf '%03d' $sleep_ms)" 2>/dev/null || sleep 0.1
|
|
159
|
-
|
|
160
|
-
# Increase backoff (exponential with cap)
|
|
161
|
-
backoff_ms=$((backoff_ms * 2))
|
|
162
|
-
[[ "$backoff_ms" -gt "$max_backoff_ms" ]] && backoff_ms=$max_backoff_ms
|
|
163
|
-
|
|
164
|
-
attempt=$((attempt + 1))
|
|
165
|
-
done
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
# === Release semaphore slot ===
|
|
169
|
-
release_semaphore() {
|
|
170
|
-
local name="${1:-$_SEMAPHORE_NAME}"
|
|
171
|
-
local lock_file="${_SEMAPHORE_LOCK_FILE}"
|
|
172
|
-
|
|
173
|
-
if [[ -n "$lock_file" ]] && [[ -f "$lock_file" ]]; then
|
|
174
|
-
rm -f "$lock_file" 2>/dev/null || true
|
|
175
|
-
_sem_log "Released slot for $name"
|
|
176
|
-
fi
|
|
177
|
-
|
|
178
|
-
unset _SEMAPHORE_SLOT_ID
|
|
179
|
-
unset _SEMAPHORE_NAME
|
|
180
|
-
unset _SEMAPHORE_LOCK_FILE
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
# === Get semaphore status ===
|
|
184
|
-
get_semaphore_status() {
|
|
185
|
-
local name="${1:-default}"
|
|
186
|
-
local max_slots="${2:-10}"
|
|
187
|
-
|
|
188
|
-
_init_semaphore_dir
|
|
189
|
-
cleanup_stale_locks "$name"
|
|
190
|
-
|
|
191
|
-
local active
|
|
192
|
-
active=$(count_active_slots "$name")
|
|
193
|
-
|
|
194
|
-
echo "{\"name\":\"$name\",\"active\":$active,\"max\":$max_slots,\"available\":$((max_slots - active))}"
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
# === Force release all slots (emergency) ===
|
|
198
|
-
force_release_all() {
|
|
199
|
-
local name="${1:-default}"
|
|
200
|
-
local lock_dir="$SEMAPHORE_DIR/$name"
|
|
201
|
-
|
|
202
|
-
if [[ -d "$lock_dir" ]]; then
|
|
203
|
-
rm -f "$lock_dir"/*.lock 2>/dev/null || true
|
|
204
|
-
_sem_log "Force released all slots for $name"
|
|
205
|
-
fi
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
# === Trap handler for automatic cleanup ===
|
|
209
|
-
_semaphore_cleanup_trap() {
|
|
210
|
-
release_semaphore 2>/dev/null || true
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
# Register cleanup trap if sourced
|
|
214
|
-
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
|
|
215
|
-
trap _semaphore_cleanup_trap EXIT INT TERM
|
|
216
|
-
fi
|