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.
Files changed (111) hide show
  1. package/CLAUDE.md +140 -1196
  2. package/bin/specweave.js +23 -0
  3. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +3 -0
  4. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-client-v2.js +39 -0
  6. package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
  7. package/dist/src/cli/commands/set-sync-target.d.ts +41 -0
  8. package/dist/src/cli/commands/set-sync-target.d.ts.map +1 -0
  9. package/dist/src/cli/commands/set-sync-target.js +126 -0
  10. package/dist/src/cli/commands/set-sync-target.js.map +1 -0
  11. package/dist/src/cli/helpers/issue-tracker/github-multi-repo.d.ts.map +1 -1
  12. package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js +5 -8
  13. package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js.map +1 -1
  14. package/dist/src/core/hooks/HookScanner.d.ts +32 -0
  15. package/dist/src/core/hooks/HookScanner.d.ts.map +1 -1
  16. package/dist/src/core/hooks/HookScanner.js +125 -1
  17. package/dist/src/core/hooks/HookScanner.js.map +1 -1
  18. package/dist/src/core/hooks/types.d.ts +10 -1
  19. package/dist/src/core/hooks/types.d.ts.map +1 -1
  20. package/dist/src/core/increment/metadata-manager.d.ts +67 -1
  21. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  22. package/dist/src/core/increment/metadata-manager.js +93 -0
  23. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  24. package/dist/src/core/project/index.d.ts +21 -0
  25. package/dist/src/core/project/index.d.ts.map +1 -0
  26. package/dist/src/core/project/index.js +22 -0
  27. package/dist/src/core/project/index.js.map +1 -0
  28. package/dist/src/core/project/project-service.d.ts +122 -0
  29. package/dist/src/core/project/project-service.d.ts.map +1 -0
  30. package/dist/src/core/project/project-service.js +334 -0
  31. package/dist/src/core/project/project-service.js.map +1 -0
  32. package/dist/src/core/sync/external-tool-resolver.d.ts +171 -0
  33. package/dist/src/core/sync/external-tool-resolver.d.ts.map +1 -0
  34. package/dist/src/core/sync/external-tool-resolver.js +569 -0
  35. package/dist/src/core/sync/external-tool-resolver.js.map +1 -0
  36. package/dist/src/core/types/increment-metadata.d.ts +92 -0
  37. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  38. package/dist/src/hooks/processor.d.ts +7 -3
  39. package/dist/src/hooks/processor.d.ts.map +1 -1
  40. package/dist/src/hooks/processor.js +11 -5
  41. package/dist/src/hooks/processor.js.map +1 -1
  42. package/package.json +1 -1
  43. package/plugins/specweave/.claude-plugin/plugin.json +1 -1
  44. package/plugins/specweave/commands/check-hooks.md +43 -0
  45. package/plugins/specweave/hooks/hooks.json +78 -0
  46. package/plugins/specweave/hooks/lib/circuit-breaker.sh +381 -0
  47. package/plugins/specweave/hooks/lib/logging.sh +231 -0
  48. package/plugins/specweave/hooks/lib/metrics.sh +347 -0
  49. package/plugins/specweave/hooks/lib/semaphore.sh +216 -0
  50. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +156 -22
  51. package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +96 -0
  52. package/plugins/specweave/hooks/v2/queue/processor.sh +13 -5
  53. package/plugins/specweave/lib/hooks/project-bridge.js +76 -0
  54. package/plugins/specweave/lib/hooks/update-tasks-md.js +0 -0
  55. package/plugins/specweave/lib/hooks/us-completion-orchestrator.js +0 -0
  56. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +67 -1
  57. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +93 -0
  58. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
  59. package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +92 -0
  60. package/plugins/specweave/scripts/hook-health.sh +441 -0
  61. package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
  62. package/plugins/specweave-alternatives/.claude-plugin/plugin.json +1 -1
  63. package/plugins/specweave-backend/.claude-plugin/plugin.json +1 -1
  64. package/plugins/specweave-confluent/.claude-plugin/plugin.json +1 -1
  65. package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +1 -1
  66. package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
  67. package/plugins/specweave-docs/.claude-plugin/plugin.json +1 -1
  68. package/plugins/specweave-figma/.claude-plugin/plugin.json +1 -1
  69. package/plugins/specweave-frontend/.claude-plugin/plugin.json +1 -1
  70. package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
  71. package/plugins/specweave-github/lib/github-client-v2.js +39 -0
  72. package/plugins/specweave-github/lib/github-client-v2.ts +44 -0
  73. package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
  74. package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
  75. package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
  76. package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
  77. package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +1 -1
  78. package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
  79. package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -1
  80. package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
  81. package/plugins/specweave-payments/.claude-plugin/plugin.json +1 -1
  82. package/plugins/specweave-plugin-dev/.claude-plugin/plugin.json +1 -1
  83. package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
  84. package/plugins/specweave-testing/.claude-plugin/plugin.json +1 -1
  85. package/plugins/specweave-ui/.claude-plugin/plugin.json +1 -1
  86. package/plugins/specweave/hooks/docs-changed.sh +0 -87
  87. package/plugins/specweave/hooks/hooks.json.bak +0 -147
  88. package/plugins/specweave/hooks/human-input-required.sh +0 -83
  89. package/plugins/specweave/hooks/post-edit-write-consolidated.sh +0 -428
  90. package/plugins/specweave/hooks/post-first-increment.sh +0 -61
  91. package/plugins/specweave/hooks/post-increment-change.sh +0 -103
  92. package/plugins/specweave/hooks/post-increment-completion.sh +0 -513
  93. package/plugins/specweave/hooks/post-increment-planning.sh +0 -1204
  94. package/plugins/specweave/hooks/post-increment-status-change.sh +0 -243
  95. package/plugins/specweave/hooks/post-metadata-change.sh +0 -246
  96. package/plugins/specweave/hooks/post-spec-update.sh +0 -158
  97. package/plugins/specweave/hooks/post-task-completion.sh +0 -557
  98. package/plugins/specweave/hooks/post-task-edit.sh +0 -47
  99. package/plugins/specweave/hooks/post-user-story-complete.sh +0 -230
  100. package/plugins/specweave/hooks/pre-command-deduplication.sh +0 -68
  101. package/plugins/specweave/hooks/pre-edit-write-consolidated.sh +0 -225
  102. package/plugins/specweave/hooks/pre-implementation.sh +0 -75
  103. package/plugins/specweave/hooks/pre-increment-start.sh +0 -173
  104. package/plugins/specweave/hooks/pre-task-completion-edit.sh +0 -355
  105. package/plugins/specweave/hooks/pre-task-completion.sh +0 -269
  106. package/plugins/specweave/hooks/pre-tool-use.sh +0 -137
  107. package/plugins/specweave/hooks/session-start-reconcile.sh +0 -139
  108. package/plugins/specweave/hooks/shared/bulk-operation-detector.sh +0 -167
  109. package/plugins/specweave/hooks/test-pretooluse-env.sh +0 -72
  110. package/plugins/specweave/hooks/validate-increment-completion.sh +0 -113
  111. 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
+ }