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
|
@@ -1,37 +1,70 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# fail-fast-wrapper.sh - HARD TIMEOUT wrapper for all hooks
|
|
2
|
+
# fail-fast-wrapper.sh - HARD TIMEOUT wrapper for all hooks with proper concurrency control
|
|
3
3
|
# If ANY hook takes longer than HOOK_TIMEOUT, it gets KILLED.
|
|
4
4
|
#
|
|
5
5
|
# Usage: bash fail-fast-wrapper.sh <hook-script> [args...]
|
|
6
6
|
#
|
|
7
7
|
# Environment:
|
|
8
|
-
# HOOK_TIMEOUT
|
|
9
|
-
# HOOK_DEBUG
|
|
8
|
+
# HOOK_TIMEOUT - max seconds (default: 5)
|
|
9
|
+
# HOOK_DEBUG - set to 1 for verbose logging
|
|
10
|
+
# HOOK_MAX_CONCURRENT - max concurrent hooks (default: 15)
|
|
11
|
+
# HOOK_ACQUIRE_TIMEOUT_MS - semaphore acquire timeout (default: 3000)
|
|
10
12
|
#
|
|
11
13
|
# Exit behavior:
|
|
12
14
|
# - Returns hook output on success
|
|
13
15
|
# - Returns safe JSON on timeout ({"continue":true} or {"decision":"approve"})
|
|
14
16
|
# - NEVER hangs - timeout is enforced with SIGKILL
|
|
15
17
|
#
|
|
16
|
-
#
|
|
17
|
-
# -
|
|
18
|
-
# -
|
|
19
|
-
# -
|
|
18
|
+
# CONCURRENCY CONTROL (v1.0.30):
|
|
19
|
+
# - Semaphore-based concurrency limiting (NOT process storm detection)
|
|
20
|
+
# - Proper circuit breaker with CLOSED/OPEN/HALF_OPEN states
|
|
21
|
+
# - Structured logging with request tracing
|
|
22
|
+
# - Metrics collection for observability
|
|
20
23
|
#
|
|
21
24
|
# v0.33.0 - Enhanced with crash prevention integration
|
|
25
|
+
# v1.0.30 - Complete rewrite with proper concurrency primitives
|
|
22
26
|
|
|
23
27
|
set -o pipefail
|
|
24
28
|
|
|
25
29
|
# === Configuration ===
|
|
26
30
|
HOOK_TIMEOUT="${HOOK_TIMEOUT:-5}" # 5 seconds - more than enough for any hook
|
|
27
31
|
HOOK_DEBUG="${HOOK_DEBUG:-0}"
|
|
32
|
+
HOOK_MAX_CONCURRENT="${HOOK_MAX_CONCURRENT:-15}" # Max concurrent hooks
|
|
33
|
+
HOOK_ACQUIRE_TIMEOUT_MS="${HOOK_ACQUIRE_TIMEOUT_MS:-3000}" # 3 seconds to acquire semaphore
|
|
28
34
|
LOG_FILE="${HOME}/.claude/hook-failures.log"
|
|
29
35
|
|
|
30
|
-
# ===
|
|
36
|
+
# === Library paths ===
|
|
31
37
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
-
|
|
38
|
+
LIB_DIR="${SCRIPT_DIR}/../lib"
|
|
33
39
|
|
|
34
|
-
# Source
|
|
40
|
+
# === Source libraries (fail gracefully if missing) ===
|
|
41
|
+
SEMAPHORE_LOADED=false
|
|
42
|
+
CIRCUIT_BREAKER_LOADED=false
|
|
43
|
+
LOGGING_LOADED=false
|
|
44
|
+
METRICS_LOADED=false
|
|
45
|
+
|
|
46
|
+
# Set state dir for all libraries
|
|
47
|
+
export SPECWEAVE_STATE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}"
|
|
48
|
+
export SPECWEAVE_LOG_DIR="${SPECWEAVE_LOG_DIR:-.specweave/logs/hooks}"
|
|
49
|
+
|
|
50
|
+
if [[ -f "$LIB_DIR/semaphore.sh" ]]; then
|
|
51
|
+
source "$LIB_DIR/semaphore.sh" 2>/dev/null && SEMAPHORE_LOADED=true
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ -f "$LIB_DIR/circuit-breaker.sh" ]]; then
|
|
55
|
+
source "$LIB_DIR/circuit-breaker.sh" 2>/dev/null && CIRCUIT_BREAKER_LOADED=true
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ -f "$LIB_DIR/logging.sh" ]]; then
|
|
59
|
+
source "$LIB_DIR/logging.sh" 2>/dev/null && LOGGING_LOADED=true
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [[ -f "$LIB_DIR/metrics.sh" ]]; then
|
|
63
|
+
source "$LIB_DIR/metrics.sh" 2>/dev/null && METRICS_LOADED=true
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Legacy crash prevention (fallback)
|
|
67
|
+
CRASH_PREVENTION="${LIB_DIR}/crash-prevention.sh"
|
|
35
68
|
if [[ -f "$CRASH_PREVENTION" ]]; then
|
|
36
69
|
source "$CRASH_PREVENTION" 2>/dev/null || true
|
|
37
70
|
fi
|
|
@@ -44,7 +77,7 @@ log_debug() {
|
|
|
44
77
|
log_failure() {
|
|
45
78
|
local msg="$1"
|
|
46
79
|
mkdir -p "$(dirname "$LOG_FILE")"
|
|
47
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HOOK
|
|
80
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HOOK FAILURE: $msg" >> "$LOG_FILE"
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
# === Safe JSON output based on hook type ===
|
|
@@ -58,6 +91,12 @@ get_safe_output() {
|
|
|
58
91
|
fi
|
|
59
92
|
}
|
|
60
93
|
|
|
94
|
+
# === Get hook name from script path ===
|
|
95
|
+
get_hook_name() {
|
|
96
|
+
local script="$1"
|
|
97
|
+
basename "$script" .sh | tr '/' '-'
|
|
98
|
+
}
|
|
99
|
+
|
|
61
100
|
# === Read stdin with timeout ===
|
|
62
101
|
# Critical: stdin can block forever if not handled properly
|
|
63
102
|
read_stdin_with_timeout() {
|
|
@@ -92,18 +131,68 @@ main() {
|
|
|
92
131
|
exit 0
|
|
93
132
|
fi
|
|
94
133
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
134
|
+
local hook_name
|
|
135
|
+
hook_name=$(get_hook_name "$script")
|
|
136
|
+
|
|
137
|
+
# === Initialize libraries ===
|
|
138
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
139
|
+
log_init "$hook_name"
|
|
140
|
+
log_debug "Starting hook execution" "script=$script"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
144
|
+
metrics_init "$hook_name"
|
|
145
|
+
metrics_start_request
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# === CIRCUIT BREAKER CHECK ===
|
|
149
|
+
# If circuit is open for this hook, fail fast
|
|
150
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
151
|
+
if ! cb_allow_request "$hook_name"; then
|
|
152
|
+
local cb_status
|
|
153
|
+
cb_status=$(cb_get_status "$hook_name")
|
|
154
|
+
log_debug "Circuit breaker OPEN for $hook_name: $cb_status"
|
|
155
|
+
|
|
156
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
157
|
+
log_warn "Circuit breaker open - failing fast" "hook=$hook_name"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
161
|
+
metrics_end_request "skipped"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
get_safe_output "$script"
|
|
165
|
+
exit 0
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# === SEMAPHORE: Acquire concurrency slot ===
|
|
170
|
+
local semaphore_acquired=false
|
|
171
|
+
if [[ "$SEMAPHORE_LOADED" == "true" ]]; then
|
|
172
|
+
if acquire_semaphore "hooks" "$HOOK_MAX_CONCURRENT" "$HOOK_ACQUIRE_TIMEOUT_MS"; then
|
|
173
|
+
semaphore_acquired=true
|
|
174
|
+
log_debug "Acquired semaphore slot for $hook_name"
|
|
175
|
+
else
|
|
176
|
+
# Could not acquire semaphore in time - graceful degradation
|
|
177
|
+
log_debug "Semaphore timeout for $hook_name - graceful skip"
|
|
178
|
+
|
|
179
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
180
|
+
log_warn "Semaphore acquisition timeout - graceful degradation" "hook=$hook_name" "max_concurrent=$HOOK_MAX_CONCURRENT"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
184
|
+
metrics_end_request "skipped"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# DON'T record as failure - this is graceful degradation, not an error
|
|
102
188
|
get_safe_output "$script"
|
|
103
189
|
exit 0
|
|
104
190
|
fi
|
|
105
191
|
fi
|
|
106
192
|
|
|
193
|
+
# Ensure semaphore is released on exit
|
|
194
|
+
trap 'release_semaphore 2>/dev/null || true' EXIT INT TERM
|
|
195
|
+
|
|
107
196
|
log_debug "Executing: $script (timeout: ${HOOK_TIMEOUT}s)"
|
|
108
197
|
|
|
109
198
|
# Read stdin first (with its own timeout)
|
|
@@ -111,11 +200,8 @@ main() {
|
|
|
111
200
|
stdin_content=$(read_stdin_with_timeout)
|
|
112
201
|
|
|
113
202
|
# Execute the hook with hard timeout
|
|
114
|
-
# Using timeout with --kill-after to ensure SIGKILL if SIGTERM doesn't work
|
|
115
203
|
local output
|
|
116
204
|
local exit_code
|
|
117
|
-
|
|
118
|
-
# Create temp file for output (avoid subshell issues)
|
|
119
205
|
local tmp_out
|
|
120
206
|
tmp_out=$(mktemp)
|
|
121
207
|
|
|
@@ -156,12 +242,30 @@ main() {
|
|
|
156
242
|
output=$(cat "$tmp_out" 2>/dev/null)
|
|
157
243
|
rm -f "$tmp_out"
|
|
158
244
|
|
|
245
|
+
# Release semaphore immediately after execution
|
|
246
|
+
if [[ "$semaphore_acquired" == "true" ]]; then
|
|
247
|
+
release_semaphore
|
|
248
|
+
fi
|
|
249
|
+
|
|
159
250
|
# Handle timeout (exit code 124 or 137)
|
|
160
251
|
if [[ $exit_code -eq 124 ]] || [[ $exit_code -eq 137 ]]; then
|
|
161
252
|
log_failure "$script - killed after ${HOOK_TIMEOUT}s"
|
|
162
253
|
log_debug "TIMEOUT: $script killed after ${HOOK_TIMEOUT}s"
|
|
163
254
|
|
|
164
|
-
|
|
255
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
256
|
+
log_error "Hook timeout - killed after ${HOOK_TIMEOUT}s" "hook=$hook_name"
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
260
|
+
metrics_end_request "timeout"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# Record failure for circuit breaker
|
|
264
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
265
|
+
cb_record_failure "$hook_name"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
# Clean up potential zombie processes (legacy)
|
|
165
269
|
if type kill_zombie_heredocs &>/dev/null; then
|
|
166
270
|
kill_zombie_heredocs 2>/dev/null || true
|
|
167
271
|
fi
|
|
@@ -170,6 +274,36 @@ main() {
|
|
|
170
274
|
exit 0
|
|
171
275
|
fi
|
|
172
276
|
|
|
277
|
+
# Handle hook errors (non-zero exit, excluding block exit code 2)
|
|
278
|
+
if [[ $exit_code -ne 0 ]] && [[ $exit_code -ne 2 ]]; then
|
|
279
|
+
log_debug "Hook error: $script exited with $exit_code"
|
|
280
|
+
|
|
281
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
282
|
+
log_warn "Hook exited with error" "hook=$hook_name" "exit_code=$exit_code"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
286
|
+
metrics_end_request "failure"
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
290
|
+
cb_record_failure "$hook_name"
|
|
291
|
+
fi
|
|
292
|
+
else
|
|
293
|
+
# Success!
|
|
294
|
+
if [[ "$METRICS_LOADED" == "true" ]]; then
|
|
295
|
+
metrics_end_request "success"
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
|
|
299
|
+
cb_record_success "$hook_name"
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
if [[ "$LOGGING_LOADED" == "true" ]]; then
|
|
303
|
+
log_debug "Hook completed successfully" "hook=$hook_name"
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
|
|
173
307
|
# Return output or safe default
|
|
174
308
|
if [[ -n "$output" ]]; then
|
|
175
309
|
echo "$output"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# project-bridge-handler.sh - Bridge increment events to project-level EDA
|
|
3
|
+
#
|
|
4
|
+
# This handler connects the increment-level EDA (hooks v2) to the
|
|
5
|
+
# project-level EDA (ProjectRegistry + Adapters).
|
|
6
|
+
#
|
|
7
|
+
# Events bridged:
|
|
8
|
+
# - increment.created -> May create project labels in GitHub
|
|
9
|
+
# - increment.done -> Triggers project sync to all external tools
|
|
10
|
+
# - increment.archived -> Updates project sync status
|
|
11
|
+
# - increment.reopened -> Triggers project sync
|
|
12
|
+
#
|
|
13
|
+
# Architecture:
|
|
14
|
+
# [Increment Event] -> [This Handler] -> [project-bridge.js] -> [ProjectService]
|
|
15
|
+
# |
|
|
16
|
+
# v
|
|
17
|
+
# [ProjectEventBus]
|
|
18
|
+
# |
|
|
19
|
+
# +----------------+----------------+
|
|
20
|
+
# v v v
|
|
21
|
+
# [GitHub] [ADO] [JIRA]
|
|
22
|
+
# Adapter Adapter Adapter
|
|
23
|
+
#
|
|
24
|
+
# IMPORTANT: Never crash Claude, always exit 0
|
|
25
|
+
set +e
|
|
26
|
+
|
|
27
|
+
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
|
|
28
|
+
|
|
29
|
+
EVENT_TYPE="${1:-}"
|
|
30
|
+
EVENT_DATA="${2:-}"
|
|
31
|
+
|
|
32
|
+
[[ -z "$EVENT_TYPE" ]] && exit 0
|
|
33
|
+
[[ -z "$EVENT_DATA" ]] && exit 0
|
|
34
|
+
|
|
35
|
+
# Find project root
|
|
36
|
+
PROJECT_ROOT="$PWD"
|
|
37
|
+
while [[ "$PROJECT_ROOT" != "/" ]] && [[ ! -d "$PROJECT_ROOT/.specweave" ]]; do
|
|
38
|
+
PROJECT_ROOT=$(dirname "$PROJECT_ROOT")
|
|
39
|
+
done
|
|
40
|
+
[[ ! -d "$PROJECT_ROOT/.specweave" ]] && exit 0
|
|
41
|
+
|
|
42
|
+
# Throttle: max once per 2 minutes per increment for project sync
|
|
43
|
+
# (More aggressive throttle since this triggers external API calls)
|
|
44
|
+
INC_ID="${EVENT_DATA%%:*}" # Extract increment ID from INC_ID:US_ID format
|
|
45
|
+
STATE_DIR="$PROJECT_ROOT/.specweave/state"
|
|
46
|
+
THROTTLE_FILE="$STATE_DIR/.project-bridge-$INC_ID"
|
|
47
|
+
THROTTLE_WINDOW=120 # 2 minutes
|
|
48
|
+
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
49
|
+
|
|
50
|
+
if [[ -f "$THROTTLE_FILE" ]]; then
|
|
51
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
52
|
+
AGE=$(($(date +%s) - $(stat -f %m "$THROTTLE_FILE" 2>/dev/null || echo 0)))
|
|
53
|
+
else
|
|
54
|
+
AGE=$(($(date +%s) - $(stat -c %Y "$THROTTLE_FILE" 2>/dev/null || echo 0)))
|
|
55
|
+
fi
|
|
56
|
+
[[ $AGE -lt $THROTTLE_WINDOW ]] && exit 0
|
|
57
|
+
fi
|
|
58
|
+
touch "$THROTTLE_FILE"
|
|
59
|
+
|
|
60
|
+
# Log event
|
|
61
|
+
LOG_FILE="$PROJECT_ROOT/.specweave/logs/hooks.log"
|
|
62
|
+
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
|
|
63
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] project-bridge-handler: $EVENT_TYPE $EVENT_DATA" >> "$LOG_FILE" 2>/dev/null
|
|
64
|
+
|
|
65
|
+
# Find the bridge script
|
|
66
|
+
BRIDGE_SCRIPT=""
|
|
67
|
+
for path in \
|
|
68
|
+
"$PROJECT_ROOT/dist/plugins/specweave/lib/hooks/project-bridge.js" \
|
|
69
|
+
"$PROJECT_ROOT/plugins/specweave/lib/hooks/project-bridge.js" \
|
|
70
|
+
"${CLAUDE_PLUGIN_ROOT:-}/lib/hooks/project-bridge.js"; do
|
|
71
|
+
[[ -f "$path" ]] && { BRIDGE_SCRIPT="$path"; break; }
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
# Cross-platform timeout wrapper
|
|
75
|
+
run_with_timeout() {
|
|
76
|
+
local timeout_secs="$1"
|
|
77
|
+
shift
|
|
78
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
79
|
+
timeout "$timeout_secs" "$@" 2>/dev/null || true
|
|
80
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
81
|
+
gtimeout "$timeout_secs" "$@" 2>/dev/null || true
|
|
82
|
+
else
|
|
83
|
+
"$@" 2>/dev/null || true
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Run bridge script if available
|
|
88
|
+
if [[ -n "$BRIDGE_SCRIPT" ]]; then
|
|
89
|
+
cd "$PROJECT_ROOT" || exit 0
|
|
90
|
+
# Run with 60s timeout (external API calls can be slow)
|
|
91
|
+
run_with_timeout 60 node "$BRIDGE_SCRIPT" "$EVENT_TYPE" "$EVENT_DATA" >> "$LOG_FILE" 2>&1 &
|
|
92
|
+
else
|
|
93
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] project-bridge-handler: No bridge script found, skipping" >> "$LOG_FILE" 2>/dev/null
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
exit 0
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Usage: processor.sh [--daemon]
|
|
6
6
|
#
|
|
7
|
-
# Event routing:
|
|
8
|
-
# - increment.created/done/archived/reopened -> living-specs-handler
|
|
9
|
-
# - user-story.completed/reopened -> status-line-handler
|
|
7
|
+
# Event routing (EDA v2):
|
|
8
|
+
# - increment.created/done/archived/reopened -> living-specs-handler + status-line-handler + project-bridge-handler
|
|
9
|
+
# - user-story.completed/reopened -> status-line-handler + project-bridge-handler
|
|
10
10
|
# - task.updated/spec.updated -> living-docs-handler (legacy)
|
|
11
|
+
# - metadata.changed -> github-sync-handler
|
|
12
|
+
#
|
|
13
|
+
# The project-bridge-handler connects increment events to project-level EDA,
|
|
14
|
+
# enabling automatic sync to GitHub, ADO, and JIRA via ProjectService.
|
|
11
15
|
#
|
|
12
16
|
# Self-terminates after 60s of idle
|
|
13
17
|
#
|
|
@@ -148,16 +152,20 @@ process_event() {
|
|
|
148
152
|
# EDA Event Routing (new architecture)
|
|
149
153
|
# ========================================
|
|
150
154
|
|
|
151
|
-
# Lifecycle events -> living-specs-handler
|
|
155
|
+
# Lifecycle events -> living-specs-handler + status-line-handler + project-bridge-handler
|
|
152
156
|
increment.created|increment.done|increment.archived|increment.reopened)
|
|
153
157
|
run_handler "$HANDLER_DIR/living-specs-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
154
158
|
# Also update status line on lifecycle changes
|
|
155
159
|
run_handler "$HANDLER_DIR/status-line-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
160
|
+
# Bridge to project-level EDA (triggers sync to GitHub/ADO/JIRA)
|
|
161
|
+
run_handler "$HANDLER_DIR/project-bridge-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
156
162
|
;;
|
|
157
163
|
|
|
158
|
-
# User story events -> status-line-handler
|
|
164
|
+
# User story events -> status-line-handler + project-bridge-handler
|
|
159
165
|
user-story.completed|user-story.reopened)
|
|
160
166
|
run_handler "$HANDLER_DIR/status-line-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
167
|
+
# Bridge to project-level EDA (may trigger issue updates)
|
|
168
|
+
run_handler "$HANDLER_DIR/project-bridge-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
161
169
|
;;
|
|
162
170
|
|
|
163
171
|
# ========================================
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* project-bridge.js - Bridge increment events to project-level EDA
|
|
4
|
+
*
|
|
5
|
+
* Called by project-bridge-handler.sh to connect increment-level
|
|
6
|
+
* events to the ProjectService and trigger project sync.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node project-bridge.js <event-type> <event-data>
|
|
9
|
+
*
|
|
10
|
+
* Event types:
|
|
11
|
+
* - increment.created - New increment created
|
|
12
|
+
* - increment.done - Increment completed
|
|
13
|
+
* - increment.archived - Increment archived
|
|
14
|
+
* - increment.reopened - Increment reopened
|
|
15
|
+
* - user-story.completed - User story completed
|
|
16
|
+
* - user-story.reopened - User story reopened
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ProjectService } from '../../../../dist/src/core/project/project-service.js';
|
|
20
|
+
import { consoleLogger } from '../vendor/utils/logger.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Bridge increment event to project-level EDA
|
|
24
|
+
*/
|
|
25
|
+
async function bridgeIncrementEvent(eventType, eventData) {
|
|
26
|
+
try {
|
|
27
|
+
console.log(`[project-bridge] Processing: ${eventType} ${eventData}`);
|
|
28
|
+
|
|
29
|
+
const projectRoot = process.cwd();
|
|
30
|
+
const service = ProjectService.getInstance(projectRoot, consoleLogger);
|
|
31
|
+
|
|
32
|
+
// Initialize adapters (no-op if already initialized)
|
|
33
|
+
await service.initialize();
|
|
34
|
+
|
|
35
|
+
// Emit the increment event to project-level EDA
|
|
36
|
+
await service.emitIncrementEvent(eventType, eventData);
|
|
37
|
+
|
|
38
|
+
console.log(`[project-bridge] Successfully bridged: ${eventType}`);
|
|
39
|
+
return { success: true };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`[project-bridge] Error: ${error.message}`);
|
|
42
|
+
return { success: false, error: error.message };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Main entry point
|
|
47
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
48
|
+
|
|
49
|
+
if (isMainModule) {
|
|
50
|
+
const eventType = process.argv[2];
|
|
51
|
+
const eventData = process.argv[3];
|
|
52
|
+
|
|
53
|
+
if (!eventType || !eventData) {
|
|
54
|
+
console.error('Usage: node project-bridge.js <event-type> <event-data>');
|
|
55
|
+
console.error('Example: node project-bridge.js increment.done 0145-project-registry');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
bridgeIncrementEvent(eventType, eventData)
|
|
60
|
+
.then((result) => {
|
|
61
|
+
if (result.success) {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
} else {
|
|
64
|
+
// Non-blocking error - don't fail the hook
|
|
65
|
+
console.error(`[project-bridge] Bridge failed: ${result.error}`);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
console.error('[project-bridge] Fatal error:', error);
|
|
71
|
+
// Non-blocking - don't fail the hook
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { bridgeIncrementEvent };
|
|
File without changes
|
|
File without changes
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles CRUD operations for increment metadata (status, type, timestamps).
|
|
5
5
|
* Part of increment 0007: Smart Status Management
|
|
6
6
|
*/
|
|
7
|
-
import { IncrementMetadata, IncrementMetadataExtended, IncrementStatus, IncrementType } from '../types/increment-metadata.js';
|
|
7
|
+
import { IncrementMetadata, IncrementMetadataExtended, IncrementMetadataV2, IncrementStatus, IncrementType, SyncTarget } from '../types/increment-metadata.js';
|
|
8
8
|
import { Logger } from '../../utils/logger.js';
|
|
9
9
|
/**
|
|
10
10
|
* Error thrown when metadata operations fail
|
|
@@ -163,5 +163,71 @@ export declare class MetadataManager {
|
|
|
163
163
|
* Get human-readable status transition error message
|
|
164
164
|
*/
|
|
165
165
|
static getTransitionError(from: IncrementStatus, to: IncrementStatus): string;
|
|
166
|
+
/**
|
|
167
|
+
* Set the external tool sync target for an increment
|
|
168
|
+
*
|
|
169
|
+
* This explicitly specifies which sync profile the increment uses.
|
|
170
|
+
* The sync target provides audit trail and deterministic sync behavior.
|
|
171
|
+
*
|
|
172
|
+
* @param incrementId - Increment ID
|
|
173
|
+
* @param syncTarget - Sync target configuration
|
|
174
|
+
* @param rootDir - Optional root directory
|
|
175
|
+
* @returns Updated metadata
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* MetadataManager.setSyncTarget('0142-feature', {
|
|
180
|
+
* profileId: 'github-frontend',
|
|
181
|
+
* provider: 'github',
|
|
182
|
+
* derivedFrom: 'project-mapping',
|
|
183
|
+
* setAt: new Date().toISOString(),
|
|
184
|
+
* sourceProjectId: 'frontend-app'
|
|
185
|
+
* });
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
static setSyncTarget(incrementId: string, syncTarget: SyncTarget, rootDir?: string): IncrementMetadataV2;
|
|
189
|
+
/**
|
|
190
|
+
* Get the sync target for an increment
|
|
191
|
+
*
|
|
192
|
+
* @param incrementId - Increment ID
|
|
193
|
+
* @param rootDir - Optional root directory
|
|
194
|
+
* @returns Sync target or undefined if not set
|
|
195
|
+
*/
|
|
196
|
+
static getSyncTarget(incrementId: string, rootDir?: string): SyncTarget | undefined;
|
|
197
|
+
/**
|
|
198
|
+
* Clear the sync target for an increment
|
|
199
|
+
*
|
|
200
|
+
* Use this when the external tool configuration changes and
|
|
201
|
+
* the increment needs to be re-resolved.
|
|
202
|
+
*
|
|
203
|
+
* @param incrementId - Increment ID
|
|
204
|
+
* @param rootDir - Optional root directory
|
|
205
|
+
* @returns Updated metadata
|
|
206
|
+
*/
|
|
207
|
+
static clearSyncTarget(incrementId: string, rootDir?: string): IncrementMetadataV2;
|
|
208
|
+
/**
|
|
209
|
+
* Check if increment has a sync target configured
|
|
210
|
+
*
|
|
211
|
+
* @param incrementId - Increment ID
|
|
212
|
+
* @param rootDir - Optional root directory
|
|
213
|
+
* @returns true if sync target is set
|
|
214
|
+
*/
|
|
215
|
+
static hasSyncTarget(incrementId: string, rootDir?: string): boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Get all increments with a specific sync provider
|
|
218
|
+
*
|
|
219
|
+
* Useful for bulk operations on all GitHub/JIRA/ADO synced increments.
|
|
220
|
+
*
|
|
221
|
+
* @param provider - Provider type ('github', 'jira', 'ado')
|
|
222
|
+
* @returns Array of increments with that provider configured
|
|
223
|
+
*/
|
|
224
|
+
static getByProvider(provider: 'github' | 'jira' | 'ado'): IncrementMetadataV2[];
|
|
225
|
+
/**
|
|
226
|
+
* Get all increments with a specific sync profile
|
|
227
|
+
*
|
|
228
|
+
* @param profileId - Profile ID from config.sync.profiles
|
|
229
|
+
* @returns Array of increments using that profile
|
|
230
|
+
*/
|
|
231
|
+
static getByProfile(profileId: string): IncrementMetadataV2[];
|
|
166
232
|
}
|
|
167
233
|
//# sourceMappingURL=metadata-manager.d.ts.map
|
|
@@ -619,6 +619,99 @@ export class MetadataManager {
|
|
|
619
619
|
}
|
|
620
620
|
return `Invalid transition: ${from} → ${to}`;
|
|
621
621
|
}
|
|
622
|
+
// ==========================================================================
|
|
623
|
+
// Sync Target Methods (v1.0.31+ - ADR-0211)
|
|
624
|
+
// ==========================================================================
|
|
625
|
+
/**
|
|
626
|
+
* Set the external tool sync target for an increment
|
|
627
|
+
*
|
|
628
|
+
* This explicitly specifies which sync profile the increment uses.
|
|
629
|
+
* The sync target provides audit trail and deterministic sync behavior.
|
|
630
|
+
*
|
|
631
|
+
* @param incrementId - Increment ID
|
|
632
|
+
* @param syncTarget - Sync target configuration
|
|
633
|
+
* @param rootDir - Optional root directory
|
|
634
|
+
* @returns Updated metadata
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```typescript
|
|
638
|
+
* MetadataManager.setSyncTarget('0142-feature', {
|
|
639
|
+
* profileId: 'github-frontend',
|
|
640
|
+
* provider: 'github',
|
|
641
|
+
* derivedFrom: 'project-mapping',
|
|
642
|
+
* setAt: new Date().toISOString(),
|
|
643
|
+
* sourceProjectId: 'frontend-app'
|
|
644
|
+
* });
|
|
645
|
+
* ```
|
|
646
|
+
*/
|
|
647
|
+
static setSyncTarget(incrementId, syncTarget, rootDir) {
|
|
648
|
+
const metadata = this.read(incrementId, rootDir);
|
|
649
|
+
metadata.syncTarget = syncTarget;
|
|
650
|
+
metadata.lastActivity = new Date().toISOString();
|
|
651
|
+
this.write(incrementId, metadata, rootDir);
|
|
652
|
+
this.logger.debug(`Set sync target for ${incrementId}: ${syncTarget.profileId} (${syncTarget.provider})`);
|
|
653
|
+
return metadata;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Get the sync target for an increment
|
|
657
|
+
*
|
|
658
|
+
* @param incrementId - Increment ID
|
|
659
|
+
* @param rootDir - Optional root directory
|
|
660
|
+
* @returns Sync target or undefined if not set
|
|
661
|
+
*/
|
|
662
|
+
static getSyncTarget(incrementId, rootDir) {
|
|
663
|
+
const metadata = this.read(incrementId, rootDir);
|
|
664
|
+
return metadata.syncTarget;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Clear the sync target for an increment
|
|
668
|
+
*
|
|
669
|
+
* Use this when the external tool configuration changes and
|
|
670
|
+
* the increment needs to be re-resolved.
|
|
671
|
+
*
|
|
672
|
+
* @param incrementId - Increment ID
|
|
673
|
+
* @param rootDir - Optional root directory
|
|
674
|
+
* @returns Updated metadata
|
|
675
|
+
*/
|
|
676
|
+
static clearSyncTarget(incrementId, rootDir) {
|
|
677
|
+
const metadata = this.read(incrementId, rootDir);
|
|
678
|
+
delete metadata.syncTarget;
|
|
679
|
+
metadata.lastActivity = new Date().toISOString();
|
|
680
|
+
this.write(incrementId, metadata, rootDir);
|
|
681
|
+
this.logger.debug(`Cleared sync target for ${incrementId}`);
|
|
682
|
+
return metadata;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Check if increment has a sync target configured
|
|
686
|
+
*
|
|
687
|
+
* @param incrementId - Increment ID
|
|
688
|
+
* @param rootDir - Optional root directory
|
|
689
|
+
* @returns true if sync target is set
|
|
690
|
+
*/
|
|
691
|
+
static hasSyncTarget(incrementId, rootDir) {
|
|
692
|
+
const metadata = this.read(incrementId, rootDir);
|
|
693
|
+
return !!metadata.syncTarget;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Get all increments with a specific sync provider
|
|
697
|
+
*
|
|
698
|
+
* Useful for bulk operations on all GitHub/JIRA/ADO synced increments.
|
|
699
|
+
*
|
|
700
|
+
* @param provider - Provider type ('github', 'jira', 'ado')
|
|
701
|
+
* @returns Array of increments with that provider configured
|
|
702
|
+
*/
|
|
703
|
+
static getByProvider(provider) {
|
|
704
|
+
return this.getAll().filter(m => m.syncTarget?.provider === provider);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Get all increments with a specific sync profile
|
|
708
|
+
*
|
|
709
|
+
* @param profileId - Profile ID from config.sync.profiles
|
|
710
|
+
* @returns Array of increments using that profile
|
|
711
|
+
*/
|
|
712
|
+
static getByProfile(profileId) {
|
|
713
|
+
return this.getAll().filter(m => m.syncTarget?.profileId === profileId);
|
|
714
|
+
}
|
|
622
715
|
}
|
|
623
716
|
/**
|
|
624
717
|
* Logger instance (injectable for testing)
|