specweave 0.28.61 → 0.28.63

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 (47) hide show
  1. package/CLAUDE.md +23 -0
  2. package/README.md +23 -1
  3. package/dist/src/cli/helpers/init/ado-repo-cloning.d.ts.map +1 -1
  4. package/dist/src/cli/helpers/init/ado-repo-cloning.js +47 -84
  5. package/dist/src/cli/helpers/init/ado-repo-cloning.js.map +1 -1
  6. package/dist/src/cli/workers/clone-worker.d.ts +18 -0
  7. package/dist/src/cli/workers/clone-worker.d.ts.map +1 -0
  8. package/dist/src/cli/workers/clone-worker.js +191 -0
  9. package/dist/src/cli/workers/clone-worker.js.map +1 -0
  10. package/dist/src/core/background/index.d.ts +2 -1
  11. package/dist/src/core/background/index.d.ts.map +1 -1
  12. package/dist/src/core/background/index.js +1 -1
  13. package/dist/src/core/background/index.js.map +1 -1
  14. package/dist/src/core/background/job-launcher.d.ts +20 -0
  15. package/dist/src/core/background/job-launcher.d.ts.map +1 -1
  16. package/dist/src/core/background/job-launcher.js +88 -4
  17. package/dist/src/core/background/job-launcher.js.map +1 -1
  18. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  19. package/dist/src/core/increment/metadata-manager.js +11 -0
  20. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  21. package/dist/src/core/types/increment-metadata.d.ts +33 -2
  22. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  23. package/dist/src/core/types/increment-metadata.js +32 -5
  24. package/dist/src/core/types/increment-metadata.js.map +1 -1
  25. package/package.json +1 -1
  26. package/plugins/specweave/commands/specweave-done.md +30 -1
  27. package/plugins/specweave/commands/specweave-jobs.md +7 -7
  28. package/plugins/specweave/commands/specweave-next.md +66 -14
  29. package/plugins/specweave/hooks/hooks.json +12 -0
  30. package/plugins/specweave/hooks/v2/detectors/lifecycle-detector.sh +85 -0
  31. package/plugins/specweave/hooks/v2/detectors/us-completion-detector.sh +148 -0
  32. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +73 -15
  33. package/plugins/specweave/hooks/v2/guards/completion-guard.sh +81 -0
  34. package/plugins/specweave/hooks/v2/handlers/ac-validation-handler.sh +4 -0
  35. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +23 -2
  36. package/plugins/specweave/hooks/v2/handlers/living-docs-handler.sh +24 -3
  37. package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +193 -0
  38. package/plugins/specweave/hooks/v2/handlers/status-line-handler.sh +165 -0
  39. package/plugins/specweave/hooks/v2/handlers/status-update.sh +12 -1
  40. package/plugins/specweave/hooks/v2/queue/dequeue.sh +4 -0
  41. package/plugins/specweave/hooks/v2/queue/enqueue.sh +50 -12
  42. package/plugins/specweave/hooks/v2/queue/processor.sh +141 -12
  43. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +11 -0
  44. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
  45. package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +33 -2
  46. package/plugins/specweave/lib/vendor/core/types/increment-metadata.js +32 -5
  47. package/plugins/specweave/lib/vendor/core/types/increment-metadata.js.map +1 -1
@@ -1,8 +1,12 @@
1
1
  #!/bin/bash
2
2
  # living-docs-handler.sh - Sync increment to living docs
3
3
  # Called async by processor, non-blocking, error-tolerant
4
+ #
5
+ # IMPORTANT: Never crash Claude, always exit 0
4
6
  set +e
5
7
 
8
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
9
+
6
10
  INC_ID="${1:-}"
7
11
  [[ -z "$INC_ID" ]] && exit 0
8
12
 
@@ -16,11 +20,28 @@ done
16
20
  # Throttle: max once per minute per increment
17
21
  THROTTLE_FILE="$PROJECT_ROOT/.specweave/state/.living-docs-$INC_ID"
18
22
  if [[ -f "$THROTTLE_FILE" ]]; then
19
- AGE=$(($(date +%s) - $(stat -f %m "$THROTTLE_FILE" 2>/dev/null || echo 0)))
23
+ if [[ "$(uname)" == "Darwin" ]]; then
24
+ AGE=$(($(date +%s) - $(stat -f %m "$THROTTLE_FILE" 2>/dev/null || echo 0)))
25
+ else
26
+ AGE=$(($(date +%s) - $(stat -c %Y "$THROTTLE_FILE" 2>/dev/null || echo 0)))
27
+ fi
20
28
  [[ $AGE -lt 60 ]] && exit 0
21
29
  fi
22
30
  touch "$THROTTLE_FILE"
23
31
 
32
+ # Cross-platform timeout wrapper
33
+ run_with_timeout() {
34
+ local timeout_secs="$1"
35
+ shift
36
+ if command -v timeout >/dev/null 2>&1; then
37
+ timeout "$timeout_secs" "$@" 2>/dev/null || true
38
+ elif command -v gtimeout >/dev/null 2>&1; then
39
+ gtimeout "$timeout_secs" "$@" 2>/dev/null || true
40
+ else
41
+ "$@" 2>/dev/null || true
42
+ fi
43
+ }
44
+
24
45
  # Find sync script
25
46
  SYNC_SCRIPT=""
26
47
  for path in \
@@ -39,8 +60,8 @@ FEATURE_ID=""
39
60
  # Run sync (timeout 30s)
40
61
  cd "$PROJECT_ROOT" || exit 0
41
62
  if [[ -n "$FEATURE_ID" ]]; then
42
- FEATURE_ID="$FEATURE_ID" timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1
63
+ FEATURE_ID="$FEATURE_ID" run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1
43
64
  else
44
- timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1
65
+ run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1
45
66
  fi
46
67
  exit 0
@@ -0,0 +1,193 @@
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
+ # Cross-platform timeout wrapper
68
+ # Uses GNU timeout, gtimeout (macOS with coreutils), or fallback
69
+ run_with_timeout() {
70
+ local timeout_secs="$1"
71
+ shift
72
+ if command -v timeout >/dev/null 2>&1; then
73
+ timeout "$timeout_secs" "$@" 2>/dev/null || true
74
+ elif command -v gtimeout >/dev/null 2>&1; then
75
+ gtimeout "$timeout_secs" "$@" 2>/dev/null || true
76
+ else
77
+ "$@" 2>/dev/null || true
78
+ fi
79
+ }
80
+
81
+ case "$EVENT" in
82
+ increment.created)
83
+ # Create spec entry in living docs via Node.js script
84
+ if [[ -n "$SYNC_SCRIPT" ]] && [[ -f "$SPEC_FILE" ]]; then
85
+ FEATURE_ID=$(get_feature_id "$SPEC_FILE")
86
+ cd "$PROJECT_ROOT" || exit 0
87
+ if [[ -n "$FEATURE_ID" ]]; then
88
+ FEATURE_ID="$FEATURE_ID" run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
89
+ else
90
+ run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
91
+ fi
92
+ fi
93
+ ;;
94
+
95
+ increment.done)
96
+ # Update status to complete in living docs
97
+ if [[ -n "$SYNC_SCRIPT" ]] && [[ -f "$SPEC_FILE" ]]; then
98
+ FEATURE_ID=$(get_feature_id "$SPEC_FILE")
99
+ cd "$PROJECT_ROOT" || exit 0
100
+
101
+ # Mark as complete in FEATURE.md if it exists
102
+ if [[ -n "$FEATURE_ID" ]]; then
103
+ SPECS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
104
+ # Find FEATURE.md for this feature
105
+ FEATURE_FILE=$(find "$SPECS_DIR" -path "*/$FEATURE_ID/FEATURE.md" 2>/dev/null | head -1)
106
+
107
+ if [[ -f "$FEATURE_FILE" ]]; then
108
+ # Update status in FEATURE.md (in-progress -> complete)
109
+ sed -i.bak 's/status: in-progress/status: complete/g' "$FEATURE_FILE" 2>/dev/null
110
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
111
+
112
+ # Update increment row status
113
+ sed -i.bak "s/\[$INC_ID\].*in-progress/[$INC_ID](..\/..\/..\/..\/increments\/$INC_ID\/spec.md) | complete/g" "$FEATURE_FILE" 2>/dev/null
114
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
115
+ fi
116
+
117
+ # Also run full sync for completeness
118
+ FEATURE_ID="$FEATURE_ID" run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
119
+ else
120
+ run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
121
+ fi
122
+ fi
123
+ ;;
124
+
125
+ increment.archived)
126
+ # Move spec entry to _archive section in living docs
127
+ if [[ -f "$ARCHIVE_SPEC" ]]; then
128
+ FEATURE_ID=$(get_feature_id "$ARCHIVE_SPEC")
129
+
130
+ if [[ -n "$FEATURE_ID" ]]; then
131
+ SPECS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
132
+ ACTIVE_FEATURE=$(find "$SPECS_DIR" -path "*/$FEATURE_ID/FEATURE.md" -not -path "*/_archive/*" 2>/dev/null | head -1)
133
+ ARCHIVE_SPECS="$SPECS_DIR/specweave/_archive"
134
+
135
+ if [[ -f "$ACTIVE_FEATURE" ]]; then
136
+ # Check if all increments for this feature are archived
137
+ FEATURE_DIR=$(dirname "$ACTIVE_FEATURE")
138
+ ACTIVE_INC_COUNT=$(ls -1 "$FEATURE_DIR"/*.md 2>/dev/null | grep -v FEATURE.md | grep -v README.md | wc -l | tr -d ' ')
139
+
140
+ if [[ "$ACTIVE_INC_COUNT" -eq 0 ]]; then
141
+ # Move entire feature folder to archive
142
+ mkdir -p "$ARCHIVE_SPECS" 2>/dev/null
143
+ mv "$FEATURE_DIR" "$ARCHIVE_SPECS/" 2>/dev/null
144
+ else
145
+ # Just update status in FEATURE.md
146
+ sed -i.bak "s/\[$INC_ID\].*|.*$/[$INC_ID](..\/..\/..\/..\/increments\/_archive\/$INC_ID\/spec.md) | archived/g" "$ACTIVE_FEATURE" 2>/dev/null
147
+ rm -f "${ACTIVE_FEATURE}.bak" 2>/dev/null
148
+ fi
149
+ fi
150
+ fi
151
+ fi
152
+ ;;
153
+
154
+ increment.reopened)
155
+ # Restore from archive section in living docs
156
+ if [[ -f "$SPEC_FILE" ]]; then
157
+ FEATURE_ID=$(get_feature_id "$SPEC_FILE")
158
+
159
+ if [[ -n "$FEATURE_ID" ]]; then
160
+ SPECS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
161
+ ARCHIVE_FEATURE="$SPECS_DIR/specweave/_archive/$FEATURE_ID"
162
+ ACTIVE_SPECS="$SPECS_DIR/specweave"
163
+
164
+ # Check if feature was archived, restore it
165
+ if [[ -d "$ARCHIVE_FEATURE" ]]; then
166
+ mv "$ARCHIVE_FEATURE" "$ACTIVE_SPECS/" 2>/dev/null
167
+
168
+ # Update status back to in-progress
169
+ FEATURE_FILE="$ACTIVE_SPECS/$FEATURE_ID/FEATURE.md"
170
+ if [[ -f "$FEATURE_FILE" ]]; then
171
+ sed -i.bak 's/status: archived/status: in-progress/g' "$FEATURE_FILE" 2>/dev/null
172
+ sed -i.bak 's/status: complete/status: in-progress/g' "$FEATURE_FILE" 2>/dev/null
173
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
174
+ fi
175
+ else
176
+ # Feature still exists, just update increment status
177
+ FEATURE_FILE=$(find "$SPECS_DIR" -path "*/$FEATURE_ID/FEATURE.md" -not -path "*/_archive/*" 2>/dev/null | head -1)
178
+ if [[ -f "$FEATURE_FILE" ]]; then
179
+ sed -i.bak "s/\[$INC_ID\].*archived/[$INC_ID](..\/..\/..\/..\/increments\/$INC_ID\/spec.md) | in-progress/g" "$FEATURE_FILE" 2>/dev/null
180
+ sed -i.bak "s/\[$INC_ID\].*complete/[$INC_ID](..\/..\/..\/..\/increments\/$INC_ID\/spec.md) | in-progress/g" "$FEATURE_FILE" 2>/dev/null
181
+ rm -f "${FEATURE_FILE}.bak" 2>/dev/null
182
+ fi
183
+ fi
184
+
185
+ # Run full sync to restore any missing content
186
+ cd "$PROJECT_ROOT" || exit 0
187
+ FEATURE_ID="$FEATURE_ID" run_with_timeout 30 node "$SYNC_SCRIPT" "$INC_ID" >/dev/null 2>&1 &
188
+ fi
189
+ fi
190
+ ;;
191
+ esac
192
+
193
+ 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
@@ -18,7 +25,11 @@ mkdir -p "$STATE_DIR" 2>/dev/null
18
25
 
19
26
  # TTL check (10 seconds)
20
27
  if [[ -f "$CACHE_FILE" ]]; then
21
- CACHE_AGE=$(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)))
28
+ if [[ "$(uname)" == "Darwin" ]]; then
29
+ CACHE_AGE=$(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0)))
30
+ else
31
+ CACHE_AGE=$(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0)))
32
+ fi
22
33
  [[ $CACHE_AGE -lt 10 ]] && exit 0
23
34
  fi
24
35
 
@@ -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