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.
- package/bin/specweave.js +36 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +5 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/jobs.d.ts +20 -0
- package/dist/src/cli/commands/jobs.d.ts.map +1 -0
- package/dist/src/cli/commands/jobs.js +448 -0
- package/dist/src/cli/commands/jobs.js.map +1 -0
- package/dist/src/cli/helpers/init/ado-repo-cloning.d.ts +32 -0
- package/dist/src/cli/helpers/init/ado-repo-cloning.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/ado-repo-cloning.js +174 -0
- package/dist/src/cli/helpers/init/ado-repo-cloning.js.map +1 -0
- package/dist/src/cli/helpers/selection-strategy.d.ts +28 -0
- package/dist/src/cli/helpers/selection-strategy.d.ts.map +1 -1
- package/dist/src/cli/helpers/selection-strategy.js +48 -0
- package/dist/src/cli/helpers/selection-strategy.js.map +1 -1
- package/dist/src/cli/workers/import-worker.js +116 -2
- package/dist/src/cli/workers/import-worker.js.map +1 -1
- package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts +15 -0
- package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts.map +1 -1
- package/dist/src/core/repo-structure/providers/azure-devops-provider.js +56 -0
- package/dist/src/core/repo-structure/providers/azure-devops-provider.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-jobs.md +7 -7
- package/plugins/specweave/hooks/v2/detectors/lifecycle-detector.sh +85 -0
- package/plugins/specweave/hooks/v2/detectors/us-completion-detector.sh +148 -0
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +58 -16
- package/plugins/specweave/hooks/v2/handlers/ac-validation-handler.sh +4 -0
- package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +4 -0
- package/plugins/specweave/hooks/v2/handlers/living-docs-handler.sh +4 -0
- package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +179 -0
- package/plugins/specweave/hooks/v2/handlers/status-line-handler.sh +165 -0
- package/plugins/specweave/hooks/v2/handlers/status-update.sh +7 -0
- package/plugins/specweave/hooks/v2/queue/dequeue.sh +4 -0
- package/plugins/specweave/hooks/v2/queue/enqueue.sh +50 -12
- package/plugins/specweave/hooks/v2/queue/processor.sh +74 -12
- package/plugins/specweave-ado/commands/specweave-ado-clone-repos.md +379 -0
- 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
|
-
#
|
|
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
|
-
#
|
|
21
|
-
|
|
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=
|
|
52
|
+
DEDUP_TTL=10 # Increased from 5s to 10s for better coalescing
|
|
24
53
|
|
|
25
|
-
#
|
|
54
|
+
# Coalescing check: skip if same event within TTL
|
|
26
55
|
if [[ -f "$DEDUP_FILE" ]]; then
|
|
27
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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 ]]
|
|
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
|