specweave 0.28.59 → 0.28.62

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 (38) hide show
  1. package/bin/specweave.js +36 -0
  2. package/dist/src/cli/commands/init.d.ts.map +1 -1
  3. package/dist/src/cli/commands/init.js +5 -0
  4. package/dist/src/cli/commands/init.js.map +1 -1
  5. package/dist/src/cli/commands/jobs.d.ts +20 -0
  6. package/dist/src/cli/commands/jobs.d.ts.map +1 -0
  7. package/dist/src/cli/commands/jobs.js +448 -0
  8. package/dist/src/cli/commands/jobs.js.map +1 -0
  9. package/dist/src/cli/helpers/init/ado-repo-cloning.d.ts +32 -0
  10. package/dist/src/cli/helpers/init/ado-repo-cloning.d.ts.map +1 -0
  11. package/dist/src/cli/helpers/init/ado-repo-cloning.js +174 -0
  12. package/dist/src/cli/helpers/init/ado-repo-cloning.js.map +1 -0
  13. package/dist/src/cli/helpers/selection-strategy.d.ts +28 -0
  14. package/dist/src/cli/helpers/selection-strategy.d.ts.map +1 -1
  15. package/dist/src/cli/helpers/selection-strategy.js +48 -0
  16. package/dist/src/cli/helpers/selection-strategy.js.map +1 -1
  17. package/dist/src/cli/workers/import-worker.js +116 -2
  18. package/dist/src/cli/workers/import-worker.js.map +1 -1
  19. package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts +15 -0
  20. package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts.map +1 -1
  21. package/dist/src/core/repo-structure/providers/azure-devops-provider.js +56 -0
  22. package/dist/src/core/repo-structure/providers/azure-devops-provider.js.map +1 -1
  23. package/package.json +1 -1
  24. package/plugins/specweave/commands/specweave-jobs.md +7 -7
  25. package/plugins/specweave/hooks/v2/detectors/lifecycle-detector.sh +85 -0
  26. package/plugins/specweave/hooks/v2/detectors/us-completion-detector.sh +148 -0
  27. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +58 -16
  28. package/plugins/specweave/hooks/v2/handlers/ac-validation-handler.sh +4 -0
  29. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +4 -0
  30. package/plugins/specweave/hooks/v2/handlers/living-docs-handler.sh +4 -0
  31. package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +179 -0
  32. package/plugins/specweave/hooks/v2/handlers/status-line-handler.sh +165 -0
  33. package/plugins/specweave/hooks/v2/handlers/status-update.sh +7 -0
  34. package/plugins/specweave/hooks/v2/queue/dequeue.sh +4 -0
  35. package/plugins/specweave/hooks/v2/queue/enqueue.sh +50 -12
  36. package/plugins/specweave/hooks/v2/queue/processor.sh +74 -12
  37. package/plugins/specweave-ado/commands/specweave-ado-clone-repos.md +379 -0
  38. package/plugins/specweave-release/commands/specweave-release-npm.md +51 -14
@@ -0,0 +1,179 @@
1
+ #!/bin/bash
2
+ # living-specs-handler.sh - Update living SPECS on lifecycle events
3
+ # Events: increment.created, increment.done, increment.archived, increment.reopened
4
+ #
5
+ # This handler updates the specs/ folder structure when increment lifecycle changes.
6
+ # It is called by the event processor, NOT directly by post-tool-use.
7
+ #
8
+ # IMPORTANT: This script must be fast (<100ms) and never crash Claude
9
+ set +e
10
+
11
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
12
+
13
+ EVENT="${1:-}"
14
+ INC_ID="${2:-}"
15
+
16
+ [[ -z "$EVENT" ]] && exit 0
17
+ [[ -z "$INC_ID" ]] && exit 0
18
+
19
+ # Find project root
20
+ PROJECT_ROOT="$PWD"
21
+ while [[ "$PROJECT_ROOT" != "/" ]] && [[ ! -d "$PROJECT_ROOT/.specweave" ]]; do
22
+ PROJECT_ROOT=$(dirname "$PROJECT_ROOT")
23
+ done
24
+ [[ ! -d "$PROJECT_ROOT/.specweave" ]] && exit 0
25
+
26
+ # Throttle: max once per 60 seconds per increment
27
+ STATE_DIR="$PROJECT_ROOT/.specweave/state"
28
+ THROTTLE_FILE="$STATE_DIR/.living-specs-$INC_ID"
29
+ mkdir -p "$STATE_DIR" 2>/dev/null
30
+
31
+ if [[ -f "$THROTTLE_FILE" ]]; then
32
+ if [[ "$(uname)" == "Darwin" ]]; then
33
+ AGE=$(($(date +%s) - $(stat -f %m "$THROTTLE_FILE" 2>/dev/null || echo 0)))
34
+ else
35
+ AGE=$(($(date +%s) - $(stat -c %Y "$THROTTLE_FILE" 2>/dev/null || echo 0)))
36
+ fi
37
+ [[ $AGE -lt 60 ]] && exit 0
38
+ fi
39
+ touch "$THROTTLE_FILE"
40
+
41
+ # Find the sync script
42
+ SYNC_SCRIPT=""
43
+ for path in \
44
+ "$PROJECT_ROOT/plugins/specweave/lib/hooks/sync-living-docs.js" \
45
+ "$PROJECT_ROOT/dist/plugins/specweave/lib/hooks/sync-living-docs.js" \
46
+ "${CLAUDE_PLUGIN_ROOT:-}/lib/hooks/sync-living-docs.js"; do
47
+ [[ -f "$path" ]] && { SYNC_SCRIPT="$path"; break; }
48
+ done
49
+
50
+ # Log event (silent, async)
51
+ LOG_FILE="$PROJECT_ROOT/.specweave/logs/hooks.log"
52
+ mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
53
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] living-specs-handler: $EVENT $INC_ID" >> "$LOG_FILE" 2>/dev/null
54
+
55
+ # Get increment paths
56
+ INC_DIR="$PROJECT_ROOT/.specweave/increments/$INC_ID"
57
+ ARCHIVE_DIR="$PROJECT_ROOT/.specweave/increments/_archive/$INC_ID"
58
+ SPEC_FILE="$INC_DIR/spec.md"
59
+ ARCHIVE_SPEC="$ARCHIVE_DIR/spec.md"
60
+
61
+ # Extract feature ID from spec.md (fast grep)
62
+ get_feature_id() {
63
+ local spec="$1"
64
+ [[ -f "$spec" ]] && grep -E "^(epic|feature_id):" "$spec" 2>/dev/null | head -1 | sed 's/.*:[[:space:]]*//' | tr -d '"'"'"
65
+ }
66
+
67
+ case "$EVENT" in
68
+ increment.created)
69
+ # Create spec entry in living docs via Node.js script
70
+ if [[ -n "$SYNC_SCRIPT" ]] && [[ -f "$SPEC_FILE" ]]; then
71
+ FEATURE_ID=$(get_feature_id "$SPEC_FILE")
72
+ cd "$PROJECT_ROOT" || exit 0
73
+ if [[ -n "$FEATURE_ID" ]]; then
74
+ FEATURE_ID="$FEATURE_ID" timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
75
+ else
76
+ timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
77
+ fi
78
+ fi
79
+ ;;
80
+
81
+ increment.done)
82
+ # Update status to complete in living docs
83
+ if [[ -n "$SYNC_SCRIPT" ]] && [[ -f "$SPEC_FILE" ]]; then
84
+ FEATURE_ID=$(get_feature_id "$SPEC_FILE")
85
+ cd "$PROJECT_ROOT" || exit 0
86
+
87
+ # Mark as complete in FEATURE.md if it exists
88
+ if [[ -n "$FEATURE_ID" ]]; then
89
+ SPECS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
90
+ # Find FEATURE.md for this feature
91
+ FEATURE_FILE=$(find "$SPECS_DIR" -path "*/$FEATURE_ID/FEATURE.md" 2>/dev/null | head -1)
92
+
93
+ if [[ -f "$FEATURE_FILE" ]]; then
94
+ # Update status in FEATURE.md (in-progress -> complete)
95
+ sed -i.bak 's/status: in-progress/status: complete/g' "$FEATURE_FILE" 2>/dev/null
96
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
97
+
98
+ # Update increment row status
99
+ sed -i.bak "s/\[$INC_ID\].*in-progress/[$INC_ID](..\/..\/..\/..\/increments\/$INC_ID\/spec.md) | complete/g" "$FEATURE_FILE" 2>/dev/null
100
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
101
+ fi
102
+
103
+ # Also run full sync for completeness
104
+ FEATURE_ID="$FEATURE_ID" timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
105
+ else
106
+ timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
107
+ fi
108
+ fi
109
+ ;;
110
+
111
+ increment.archived)
112
+ # Move spec entry to _archive section in living docs
113
+ if [[ -f "$ARCHIVE_SPEC" ]]; then
114
+ FEATURE_ID=$(get_feature_id "$ARCHIVE_SPEC")
115
+
116
+ if [[ -n "$FEATURE_ID" ]]; then
117
+ SPECS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
118
+ ACTIVE_FEATURE=$(find "$SPECS_DIR" -path "*/$FEATURE_ID/FEATURE.md" -not -path "*/_archive/*" 2>/dev/null | head -1)
119
+ ARCHIVE_SPECS="$SPECS_DIR/specweave/_archive"
120
+
121
+ if [[ -f "$ACTIVE_FEATURE" ]]; then
122
+ # Check if all increments for this feature are archived
123
+ FEATURE_DIR=$(dirname "$ACTIVE_FEATURE")
124
+ ACTIVE_INC_COUNT=$(ls -1 "$FEATURE_DIR"/*.md 2>/dev/null | grep -v FEATURE.md | grep -v README.md | wc -l | tr -d ' ')
125
+
126
+ if [[ "$ACTIVE_INC_COUNT" -eq 0 ]]; then
127
+ # Move entire feature folder to archive
128
+ mkdir -p "$ARCHIVE_SPECS" 2>/dev/null
129
+ mv "$FEATURE_DIR" "$ARCHIVE_SPECS/" 2>/dev/null
130
+ else
131
+ # Just update status in FEATURE.md
132
+ sed -i.bak "s/\[$INC_ID\].*|.*$/[$INC_ID](..\/..\/..\/..\/increments\/_archive\/$INC_ID\/spec.md) | archived/g" "$ACTIVE_FEATURE" 2>/dev/null
133
+ rm -f "${ACTIVE_FEATURE}.bak" 2>/dev/null
134
+ fi
135
+ fi
136
+ fi
137
+ fi
138
+ ;;
139
+
140
+ increment.reopened)
141
+ # Restore from archive section in living docs
142
+ if [[ -f "$SPEC_FILE" ]]; then
143
+ FEATURE_ID=$(get_feature_id "$SPEC_FILE")
144
+
145
+ if [[ -n "$FEATURE_ID" ]]; then
146
+ SPECS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
147
+ ARCHIVE_FEATURE="$SPECS_DIR/specweave/_archive/$FEATURE_ID"
148
+ ACTIVE_SPECS="$SPECS_DIR/specweave"
149
+
150
+ # Check if feature was archived, restore it
151
+ if [[ -d "$ARCHIVE_FEATURE" ]]; then
152
+ mv "$ARCHIVE_FEATURE" "$ACTIVE_SPECS/" 2>/dev/null
153
+
154
+ # Update status back to in-progress
155
+ FEATURE_FILE="$ACTIVE_SPECS/$FEATURE_ID/FEATURE.md"
156
+ if [[ -f "$FEATURE_FILE" ]]; then
157
+ sed -i.bak 's/status: archived/status: in-progress/g' "$FEATURE_FILE" 2>/dev/null
158
+ sed -i.bak 's/status: complete/status: in-progress/g' "$FEATURE_FILE" 2>/dev/null
159
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
160
+ fi
161
+ else
162
+ # Feature still exists, just update increment status
163
+ FEATURE_FILE=$(find "$SPECS_DIR" -path "*/$FEATURE_ID/FEATURE.md" -not -path "*/_archive/*" 2>/dev/null | head -1)
164
+ if [[ -f "$FEATURE_FILE" ]]; then
165
+ sed -i.bak "s/\[$INC_ID\].*archived/[$INC_ID](..\/..\/..\/..\/increments\/$INC_ID\/spec.md) | in-progress/g" "$FEATURE_FILE" 2>/dev/null
166
+ sed -i.bak "s/\[$INC_ID\].*complete/[$INC_ID](..\/..\/..\/..\/increments\/$INC_ID\/spec.md) | in-progress/g" "$FEATURE_FILE" 2>/dev/null
167
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
168
+ fi
169
+ fi
170
+
171
+ # Run full sync to restore any missing content
172
+ cd "$PROJECT_ROOT" || exit 0
173
+ FEATURE_ID="$FEATURE_ID" timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
174
+ fi
175
+ fi
176
+ ;;
177
+ esac
178
+
179
+ exit 0
@@ -0,0 +1,165 @@
1
+ #!/bin/bash
2
+ # status-line-handler.sh - Event-driven status line updates
3
+ # Events: user-story.completed, user-story.reopened, increment.done, increment.archived, increment.reopened
4
+ #
5
+ # This handler updates the status line ONLY when meaningful events occur:
6
+ # - User story completed (all ACs + tasks done for that US)
7
+ # - User story reopened (US tasks/ACs unchecked)
8
+ # - Increment lifecycle changes (done, archived, reopened)
9
+ #
10
+ # It does NOT update on every task.md edit - that would cause:
11
+ # - Race conditions (multiple rapid updates)
12
+ # - Performance issues (too frequent writes)
13
+ # - Status line flickering
14
+ #
15
+ # IMPORTANT: This script must be fast (<20ms) and never crash Claude
16
+ set +e
17
+
18
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
19
+
20
+ EVENT="${1:-}"
21
+ EVENT_DATA="${2:-}"
22
+
23
+ [[ -z "$EVENT" ]] && exit 0
24
+ [[ -z "$EVENT_DATA" ]] && exit 0
25
+
26
+ # Find project root
27
+ PROJECT_ROOT="$PWD"
28
+ while [[ "$PROJECT_ROOT" != "/" ]] && [[ ! -d "$PROJECT_ROOT/.specweave" ]]; do
29
+ PROJECT_ROOT=$(dirname "$PROJECT_ROOT")
30
+ done
31
+ [[ ! -d "$PROJECT_ROOT/.specweave" ]] && exit 0
32
+
33
+ STATE_DIR="$PROJECT_ROOT/.specweave/state"
34
+ CACHE_FILE="$STATE_DIR/status-line.json"
35
+ mkdir -p "$STATE_DIR" 2>/dev/null
36
+
37
+ # Log event (silent, async)
38
+ LOG_FILE="$PROJECT_ROOT/.specweave/logs/hooks.log"
39
+ mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
40
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] status-line-handler: $EVENT $EVENT_DATA" >> "$LOG_FILE" 2>/dev/null
41
+
42
+ # Parse event data
43
+ # Format: INC_ID:US_ID (for user-story events) or just INC_ID (for lifecycle events)
44
+ if [[ "$EVENT_DATA" == *":"* ]]; then
45
+ INC_ID="${EVENT_DATA%%:*}"
46
+ US_ID="${EVENT_DATA##*:}"
47
+ else
48
+ INC_ID="$EVENT_DATA"
49
+ US_ID=""
50
+ fi
51
+
52
+ # Handle events
53
+ case "$EVENT" in
54
+ user-story.completed)
55
+ # User story completed - update status line with US progress
56
+ # This is the RIGHT time to update (not on every checkbox click)
57
+ ;;
58
+
59
+ user-story.reopened)
60
+ # User story reopened - update status line
61
+ ;;
62
+
63
+ increment.done)
64
+ # Increment completed - update status line to show completion
65
+ ;;
66
+
67
+ increment.archived)
68
+ # Increment archived - clear from status line, find next active
69
+ INC_ID="" # Force finding a new active increment
70
+ ;;
71
+
72
+ increment.reopened)
73
+ # Increment reopened - show it as active again
74
+ ;;
75
+
76
+ *)
77
+ # Unknown event - ignore
78
+ exit 0
79
+ ;;
80
+ esac
81
+
82
+ # Find active increment if not determined or was archived
83
+ if [[ -z "$INC_ID" ]] || [[ "$EVENT" == "increment.archived" ]]; then
84
+ INC_ID=""
85
+ for meta in "$PROJECT_ROOT/.specweave/increments"/[0-9]*/metadata.json; do
86
+ [[ -f "$meta" ]] || continue
87
+ STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$meta" | grep -o '"[^"]*"$' | tr -d '"')
88
+ [[ "$STATUS" == "active" || "$STATUS" == "planning" ]] && {
89
+ INC_ID=$(basename "$(dirname "$meta")")
90
+ break
91
+ }
92
+ done
93
+ fi
94
+
95
+ # If no active increment, write null status
96
+ if [[ -z "$INC_ID" ]]; then
97
+ cat > "$CACHE_FILE" << 'EOF'
98
+ {"current":null,"event":"increment.archived","ts":"__TS__"}
99
+ EOF
100
+ sed -i.bak "s/__TS__/$(date +%s)/" "$CACHE_FILE" 2>/dev/null
101
+ rm -f "${CACHE_FILE}.bak" 2>/dev/null
102
+ exit 0
103
+ fi
104
+
105
+ # Get increment paths
106
+ TASKS_FILE="$PROJECT_ROOT/.specweave/increments/$INC_ID/tasks.md"
107
+ SPEC_FILE="$PROJECT_ROOT/.specweave/increments/$INC_ID/spec.md"
108
+ META_FILE="$PROJECT_ROOT/.specweave/increments/$INC_ID/metadata.json"
109
+
110
+ # Check files exist
111
+ [[ ! -f "$TASKS_FILE" ]] && exit 0
112
+
113
+ # Count tasks (pure bash, fast)
114
+ TOTAL=$(grep -c "^###\? T-" "$TASKS_FILE" 2>/dev/null || echo 0)
115
+ DONE=$(grep -c "\[x\]" "$TASKS_FILE" 2>/dev/null || echo 0)
116
+ PCT=0; [[ $TOTAL -gt 0 ]] && PCT=$((DONE * 100 / TOTAL))
117
+
118
+ # Count user stories completed
119
+ US_TOTAL=0
120
+ US_DONE=0
121
+ if [[ -f "$SPEC_FILE" ]]; then
122
+ # Count user story headers (## US-XXX or similar)
123
+ US_TOTAL=$(grep -c "^##.*US-" "$SPEC_FILE" 2>/dev/null || echo 0)
124
+
125
+ # Count by checking if all ACs for each US are checked
126
+ # This is a simplified count - the actual detection is done by us-completion-detector.sh
127
+ US_STATE_FILE="$STATE_DIR/.us-completion-$INC_ID"
128
+ if [[ -f "$US_STATE_FILE" ]]; then
129
+ US_DONE=$(grep -c "=yes$" "$US_STATE_FILE" 2>/dev/null || echo 0)
130
+ fi
131
+ fi
132
+
133
+ # Get increment status
134
+ INC_STATUS="active"
135
+ if [[ -f "$META_FILE" ]]; then
136
+ INC_STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$META_FILE" | grep -o '"[^"]*"$' | tr -d '"')
137
+ fi
138
+
139
+ # Build detailed status JSON with event info
140
+ TMP_FILE="$CACHE_FILE.tmp.$$"
141
+ cat > "$TMP_FILE" << EOF
142
+ {
143
+ "current": {
144
+ "id": "$INC_ID",
145
+ "completed": $DONE,
146
+ "total": $TOTAL,
147
+ "percentage": $PCT,
148
+ "status": "$INC_STATUS",
149
+ "userStories": {
150
+ "completed": $US_DONE,
151
+ "total": $US_TOTAL
152
+ }
153
+ },
154
+ "lastEvent": {
155
+ "type": "$EVENT",
156
+ "data": "$EVENT_DATA"
157
+ },
158
+ "ts": "$(date +%s)"
159
+ }
160
+ EOF
161
+
162
+ # Atomic write (mv is atomic on same filesystem)
163
+ mv "$TMP_FILE" "$CACHE_FILE" 2>/dev/null
164
+
165
+ exit 0
@@ -1,8 +1,15 @@
1
1
  #!/bin/bash
2
2
  # status-update.sh - Fast status line update (synchronous)
3
3
  # Goal: <20ms execution, pure bash, no external processes
4
+ #
5
+ # NOTE: This is the LEGACY handler. New EDA architecture uses
6
+ # status-line-handler.sh which is EVENT-DRIVEN.
7
+ #
8
+ # IMPORTANT: Never crash Claude, always exit 0
4
9
  set +e
5
10
 
11
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
12
+
6
13
  INC_ID="${1:-}"
7
14
 
8
15
  # Find project root
@@ -2,8 +2,12 @@
2
2
  # dequeue.sh - Get and remove next event from queue
3
3
  # Usage: dequeue.sh [--peek]
4
4
  # Returns JSON event or empty if queue is empty
5
+ #
6
+ # IMPORTANT: Never crash Claude, always exit 0
5
7
  set +e
6
8
 
9
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
10
+
7
11
  PEEK=false
8
12
  [[ "$1" == "--peek" ]] && PEEK=true
9
13
 
@@ -1,9 +1,15 @@
1
1
  #!/bin/bash
2
- # enqueue.sh - Add event to queue with deduplication
2
+ # enqueue.sh - Add event to queue with deduplication and coalescing
3
3
  # Usage: enqueue.sh <event_type> <event_data>
4
- # Events are deduplicated by type+data hash within 5 second window
4
+ #
5
+ # Events are coalesced (deduplicated) by type+data hash within 10 second window.
6
+ # Events have priorities: lifecycle=1 (highest), user-story=2, other=3 (lowest)
7
+ #
8
+ # IMPORTANT: This script must be fast (<5ms) and never crash
5
9
  set +e
6
10
 
11
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
12
+
7
13
  EVENT_TYPE="${1:-unknown}"
8
14
  EVENT_DATA="${2:-}"
9
15
 
@@ -17,25 +23,57 @@ done
17
23
  QUEUE_DIR="$PROJECT_ROOT/.specweave/state/event-queue"
18
24
  mkdir -p "$QUEUE_DIR" 2>/dev/null || exit 0
19
25
 
20
- # Deduplication: hash event type + data
21
- HASH=$(echo "${EVENT_TYPE}:${EVENT_DATA}" | md5 | cut -c1-8)
26
+ # Assign event priority
27
+ # Priority 1: Lifecycle events (most important)
28
+ # Priority 2: User story events
29
+ # Priority 3: Other events
30
+ PRIORITY=3
31
+ case "$EVENT_TYPE" in
32
+ increment.created|increment.done|increment.archived|increment.reopened)
33
+ PRIORITY=1
34
+ ;;
35
+ user-story.completed|user-story.reopened)
36
+ PRIORITY=2
37
+ ;;
38
+ esac
39
+
40
+ # Coalescing: hash event type + data for deduplication
41
+ # Cross-platform md5 (works on macOS and Linux)
42
+ if command -v md5 >/dev/null 2>&1; then
43
+ HASH=$(echo "${EVENT_TYPE}:${EVENT_DATA}" | md5 | cut -c1-8)
44
+ elif command -v md5sum >/dev/null 2>&1; then
45
+ HASH=$(echo "${EVENT_TYPE}:${EVENT_DATA}" | md5sum | cut -c1-8)
46
+ else
47
+ # Fallback: simple hash from type and data
48
+ HASH=$(printf "%s" "${EVENT_TYPE}:${EVENT_DATA}" | cksum | cut -d' ' -f1)
49
+ fi
50
+
22
51
  DEDUP_FILE="$QUEUE_DIR/.dedup-$HASH"
23
- DEDUP_TTL=5
52
+ DEDUP_TTL=10 # Increased from 5s to 10s for better coalescing
24
53
 
25
- # Check if duplicate (within TTL)
54
+ # Coalescing check: skip if same event within TTL
26
55
  if [[ -f "$DEDUP_FILE" ]]; then
27
- AGE=$(($(date +%s) - $(stat -f %m "$DEDUP_FILE" 2>/dev/null || echo 0)))
56
+ if [[ "$(uname)" == "Darwin" ]]; then
57
+ AGE=$(($(date +%s) - $(stat -f %m "$DEDUP_FILE" 2>/dev/null || echo 0)))
58
+ else
59
+ AGE=$(($(date +%s) - $(stat -c %Y "$DEDUP_FILE" 2>/dev/null || echo 0)))
60
+ fi
28
61
  [[ $AGE -lt $DEDUP_TTL ]] && exit 0
29
62
  fi
30
63
  touch "$DEDUP_FILE"
31
64
 
32
65
  # Enqueue event (atomic write)
33
- TIMESTAMP=$(date +%s%N)
34
- EVENT_FILE="$QUEUE_DIR/${TIMESTAMP}-${EVENT_TYPE}.event"
35
- cat > "$EVENT_FILE" << EOF
36
- {"type":"$EVENT_TYPE","data":"$EVENT_DATA","ts":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
66
+ # Filename includes priority for priority-ordered processing
67
+ TIMESTAMP=$(date +%s%N 2>/dev/null || date +%s)
68
+ EVENT_FILE="$QUEUE_DIR/${PRIORITY}-${TIMESTAMP}-${EVENT_TYPE}.event"
69
+
70
+ # Create event file atomically
71
+ TMP_FILE="$EVENT_FILE.tmp.$$"
72
+ cat > "$TMP_FILE" << EOF
73
+ {"type":"$EVENT_TYPE","data":"$EVENT_DATA","priority":$PRIORITY,"ts":"$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)"}
37
74
  EOF
75
+ mv "$TMP_FILE" "$EVENT_FILE" 2>/dev/null
38
76
 
39
- # Cleanup old dedup files (>30s)
77
+ # Cleanup old dedup files (>30s) - non-blocking
40
78
  find "$QUEUE_DIR" -name ".dedup-*" -mmin +1 -delete 2>/dev/null &
41
79
  exit 0
@@ -1,10 +1,21 @@
1
1
  #!/bin/bash
2
- # processor.sh - Background event processor
3
- # Processes queued events asynchronously, routes to handlers
2
+ # processor.sh - Background event processor with EDA routing
3
+ # Processes queued events asynchronously, routes to specialized handlers
4
+ #
4
5
  # Usage: processor.sh [--daemon]
5
- # Self-terminates after 30s of idle
6
+ #
7
+ # Event routing:
8
+ # - increment.created/done/archived/reopened -> living-specs-handler
9
+ # - user-story.completed/reopened -> status-line-handler
10
+ # - task.updated/spec.updated -> living-docs-handler (legacy)
11
+ #
12
+ # Self-terminates after 60s of idle
13
+ #
14
+ # IMPORTANT: This script uses flock for safe concurrent access
6
15
  set +e
7
16
 
17
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
18
+
8
19
  DAEMON_MODE=false
9
20
  [[ "$1" == "--daemon" ]] && DAEMON_MODE=true
10
21
 
@@ -19,23 +30,44 @@ QUEUE_DIR="$PROJECT_ROOT/.specweave/state/event-queue"
19
30
  HANDLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../handlers" && pwd)"
20
31
  LOG_FILE="$PROJECT_ROOT/.specweave/logs/processor.log"
21
32
  PID_FILE="$PROJECT_ROOT/.specweave/state/.processor.pid"
22
- IDLE_TIMEOUT=30
33
+ LOCK_FILE="$PROJECT_ROOT/.specweave/state/.processor.lock"
34
+ IDLE_TIMEOUT=60 # Increased from 30s to 60s
23
35
  IDLE_COUNT=0
36
+ HANDLER_TIMEOUT=30 # Max time per handler call
24
37
 
25
38
  mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
39
+ mkdir -p "$QUEUE_DIR" 2>/dev/null
40
+
41
+ # Acquire exclusive lock using flock
42
+ exec 200>"$LOCK_FILE"
43
+ if ! flock -n 200 2>/dev/null; then
44
+ # Another processor is running - exit silently
45
+ exit 0
46
+ fi
26
47
 
27
- # Check if already running
48
+ # Double-check PID file for extra safety
28
49
  if [[ -f "$PID_FILE" ]]; then
29
50
  OLD_PID=$(cat "$PID_FILE" 2>/dev/null)
30
- if kill -0 "$OLD_PID" 2>/dev/null; then
51
+ if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then
31
52
  exit 0 # Already running
32
53
  fi
33
54
  fi
34
55
  echo $$ > "$PID_FILE"
35
- trap 'rm -f "$PID_FILE"' EXIT
56
+ trap 'rm -f "$PID_FILE"; flock -u 200 2>/dev/null' EXIT
36
57
 
37
58
  log() { echo "[$(date +%H:%M:%S)] $1" >> "$LOG_FILE" 2>/dev/null; }
38
- log "Processor started (PID: $$)"
59
+ log "Processor started (PID: $$, IDLE_TIMEOUT: ${IDLE_TIMEOUT}s)"
60
+
61
+ # Run handler with timeout (prevents stuck handlers from blocking queue)
62
+ run_handler() {
63
+ local handler="$1"
64
+ local event_type="$2"
65
+ local event_data="$3"
66
+
67
+ if [[ -x "$handler" ]]; then
68
+ timeout "$HANDLER_TIMEOUT" bash "$handler" "$event_type" "$event_data" 2>/dev/null || true
69
+ fi
70
+ }
39
71
 
40
72
  process_event() {
41
73
  local EVENT_JSON="$1"
@@ -45,12 +77,38 @@ process_event() {
45
77
  log "Processing: $EVENT_TYPE ($EVENT_DATA)"
46
78
 
47
79
  case "$EVENT_TYPE" in
80
+ # ========================================
81
+ # EDA Event Routing (new architecture)
82
+ # ========================================
83
+
84
+ # Lifecycle events -> living-specs-handler
85
+ increment.created|increment.done|increment.archived|increment.reopened)
86
+ run_handler "$HANDLER_DIR/living-specs-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
87
+ # Also update status line on lifecycle changes
88
+ run_handler "$HANDLER_DIR/status-line-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
89
+ ;;
90
+
91
+ # User story events -> status-line-handler
92
+ user-story.completed|user-story.reopened)
93
+ run_handler "$HANDLER_DIR/status-line-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
94
+ ;;
95
+
96
+ # ========================================
97
+ # Legacy event routing (backward compat)
98
+ # ========================================
48
99
  task.updated|spec.updated)
49
- bash "$HANDLER_DIR/living-docs-handler.sh" "$EVENT_DATA" 2>/dev/null
50
- bash "$HANDLER_DIR/ac-validation-handler.sh" "$EVENT_DATA" 2>/dev/null
100
+ # Legacy: don't update status line on every task edit
101
+ # That causes race conditions and flickering
102
+ run_handler "$HANDLER_DIR/living-docs-handler.sh" "" "$EVENT_DATA"
103
+ run_handler "$HANDLER_DIR/ac-validation-handler.sh" "" "$EVENT_DATA"
51
104
  ;;
105
+
52
106
  metadata.changed)
53
- bash "$HANDLER_DIR/github-sync-handler.sh" "$EVENT_DATA" 2>/dev/null
107
+ run_handler "$HANDLER_DIR/github-sync-handler.sh" "" "$EVENT_DATA"
108
+ ;;
109
+
110
+ *)
111
+ log "Unknown event type: $EVENT_TYPE"
54
112
  ;;
55
113
  esac
56
114
  }
@@ -64,9 +122,13 @@ while true; do
64
122
  process_event "$EVENT"
65
123
  else
66
124
  IDLE_COUNT=$((IDLE_COUNT + 1))
67
- [[ $IDLE_COUNT -ge $IDLE_TIMEOUT ]] && { log "Idle timeout, exiting"; exit 0; }
125
+ if [[ $IDLE_COUNT -ge $IDLE_TIMEOUT ]]; then
126
+ log "Idle timeout (${IDLE_TIMEOUT}s), exiting"
127
+ exit 0
128
+ fi
68
129
  sleep 1
69
130
  fi
70
131
 
132
+ # In non-daemon mode, exit after 3s of idle (quick processing)
71
133
  [[ "$DAEMON_MODE" == "false" ]] && [[ $IDLE_COUNT -ge 3 ]] && exit 0
72
134
  done