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
@@ -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 - max seconds (default: 5)
9
- # HOOK_DEBUG - set to 1 for verbose logging
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
- # CRASH PREVENTION:
17
- # - Integrates with crash-prevention.sh for process storm detection
18
- # - Auto-kills zombie processes on timeout
19
- # - Records failures for circuit breaker
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
- # === Crash Prevention Integration ===
36
+ # === Library paths ===
31
37
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
- CRASH_PREVENTION="${SCRIPT_DIR}/../lib/crash-prevention.sh"
38
+ LIB_DIR="${SCRIPT_DIR}/../lib"
33
39
 
34
- # Source crash prevention if available (non-blocking)
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 TIMEOUT: $msg" >> "$LOG_FILE"
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
- # === CRASH PREVENTION: Process Storm Detection ===
96
- # If too many hooks are running, skip this one to prevent cascade
97
- if type detect_process_storm &>/dev/null; then
98
- local storm_status
99
- storm_status=$(detect_process_storm 25)
100
- if [[ "$storm_status" == STORM* ]]; then
101
- log_failure "$script - BLOCKED due to process storm: $storm_status"
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
- # === CRASH PREVENTION: Clean up potential zombie processes ===
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
@@ -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)