specweave 1.0.29 → 1.0.31
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 +140 -1196
- package/bin/specweave.js +23 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +39 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/src/cli/commands/set-sync-target.d.ts +41 -0
- package/dist/src/cli/commands/set-sync-target.d.ts.map +1 -0
- package/dist/src/cli/commands/set-sync-target.js +126 -0
- package/dist/src/cli/commands/set-sync-target.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js +5 -8
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js.map +1 -1
- package/dist/src/core/hooks/HookScanner.d.ts +32 -0
- package/dist/src/core/hooks/HookScanner.d.ts.map +1 -1
- package/dist/src/core/hooks/HookScanner.js +125 -1
- package/dist/src/core/hooks/HookScanner.js.map +1 -1
- package/dist/src/core/hooks/types.d.ts +10 -1
- package/dist/src/core/hooks/types.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts +67 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +93 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/project/index.d.ts +21 -0
- package/dist/src/core/project/index.d.ts.map +1 -0
- package/dist/src/core/project/index.js +22 -0
- package/dist/src/core/project/index.js.map +1 -0
- package/dist/src/core/project/project-service.d.ts +122 -0
- package/dist/src/core/project/project-service.d.ts.map +1 -0
- package/dist/src/core/project/project-service.js +334 -0
- package/dist/src/core/project/project-service.js.map +1 -0
- package/dist/src/core/sync/external-tool-resolver.d.ts +171 -0
- package/dist/src/core/sync/external-tool-resolver.d.ts.map +1 -0
- package/dist/src/core/sync/external-tool-resolver.js +569 -0
- package/dist/src/core/sync/external-tool-resolver.js.map +1 -0
- package/dist/src/core/types/increment-metadata.d.ts +92 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/hooks/processor.d.ts +7 -3
- package/dist/src/hooks/processor.d.ts.map +1 -1
- package/dist/src/hooks/processor.js +11 -5
- package/dist/src/hooks/processor.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave/commands/check-hooks.md +43 -0
- package/plugins/specweave/hooks/hooks.json +78 -0
- package/plugins/specweave/hooks/lib/circuit-breaker.sh +381 -0
- package/plugins/specweave/hooks/lib/logging.sh +231 -0
- package/plugins/specweave/hooks/lib/metrics.sh +347 -0
- package/plugins/specweave/hooks/lib/semaphore.sh +216 -0
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +156 -22
- package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +96 -0
- package/plugins/specweave/hooks/v2/queue/processor.sh +13 -5
- package/plugins/specweave/lib/hooks/project-bridge.js +76 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.js +0 -0
- package/plugins/specweave/lib/hooks/us-completion-orchestrator.js +0 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +67 -1
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +93 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +92 -0
- package/plugins/specweave/scripts/hook-health.sh +441 -0
- package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-alternatives/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-backend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-confluent/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-figma/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-frontend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-github/lib/github-client-v2.js +39 -0
- package/plugins/specweave-github/lib/github-client-v2.ts +44 -0
- package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-payments/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-plugin-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-testing/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ui/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave/hooks/docs-changed.sh +0 -87
- package/plugins/specweave/hooks/hooks.json.bak +0 -147
- package/plugins/specweave/hooks/human-input-required.sh +0 -83
- package/plugins/specweave/hooks/post-edit-write-consolidated.sh +0 -428
- package/plugins/specweave/hooks/post-first-increment.sh +0 -61
- package/plugins/specweave/hooks/post-increment-change.sh +0 -103
- package/plugins/specweave/hooks/post-increment-completion.sh +0 -513
- package/plugins/specweave/hooks/post-increment-planning.sh +0 -1204
- package/plugins/specweave/hooks/post-increment-status-change.sh +0 -243
- package/plugins/specweave/hooks/post-metadata-change.sh +0 -246
- package/plugins/specweave/hooks/post-spec-update.sh +0 -158
- package/plugins/specweave/hooks/post-task-completion.sh +0 -557
- package/plugins/specweave/hooks/post-task-edit.sh +0 -47
- package/plugins/specweave/hooks/post-user-story-complete.sh +0 -230
- package/plugins/specweave/hooks/pre-command-deduplication.sh +0 -68
- package/plugins/specweave/hooks/pre-edit-write-consolidated.sh +0 -225
- package/plugins/specweave/hooks/pre-implementation.sh +0 -75
- package/plugins/specweave/hooks/pre-increment-start.sh +0 -173
- package/plugins/specweave/hooks/pre-task-completion-edit.sh +0 -355
- package/plugins/specweave/hooks/pre-task-completion.sh +0 -269
- package/plugins/specweave/hooks/pre-tool-use.sh +0 -137
- package/plugins/specweave/hooks/session-start-reconcile.sh +0 -139
- package/plugins/specweave/hooks/shared/bulk-operation-detector.sh +0 -167
- package/plugins/specweave/hooks/test-pretooluse-env.sh +0 -72
- package/plugins/specweave/hooks/validate-increment-completion.sh +0 -113
- package/plugins/specweave/lib/hooks/consolidated-sync.js +0 -288
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|