specweave 0.26.4 → 0.26.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +154 -4
- package/bin/specweave.js +15 -0
- package/dist/plugins/specweave-github/lib/completion-calculator.js +2 -2
- package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +28 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +191 -19
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +25 -2
- package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
- package/dist/src/cli/commands/archive.d.ts +10 -0
- package/dist/src/cli/commands/archive.d.ts.map +1 -0
- package/dist/src/cli/commands/archive.js +78 -0
- package/dist/src/cli/commands/archive.js.map +1 -0
- package/dist/src/cli/commands/init.js +2 -2
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/init/initial-increment-generator.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/initial-increment-generator.js +48 -8
- package/dist/src/cli/helpers/init/initial-increment-generator.js.map +1 -1
- package/dist/src/core/increment/increment-reopener.d.ts.map +1 -1
- package/dist/src/core/increment/increment-reopener.js +13 -14
- package/dist/src/core/increment/increment-reopener.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +19 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +85 -0
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -0
- package/dist/src/core/increment/status-change-sync-trigger.js +137 -0
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -0
- package/dist/src/core/increment/sync-circuit-breaker.d.ts +64 -0
- package/dist/src/core/increment/sync-circuit-breaker.d.ts.map +1 -0
- package/dist/src/core/increment/sync-circuit-breaker.js +95 -0
- package/dist/src/core/increment/sync-circuit-breaker.js.map +1 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts +12 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +157 -24
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/init/repo/types.d.ts +1 -1
- package/package.json +2 -2
- package/plugins/specweave/agents/pm/AGENT.md +13 -7
- package/plugins/specweave/commands/sync-diagnostics.md +227 -0
- package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
- package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
- package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
- package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
- package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
- package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
- package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
- package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
- package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
- package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
- package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
- package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
- package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +20 -8
- package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +19 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/skills/brownfield-analyzer/SKILL.md +267 -868
- package/plugins/specweave/skills/increment-planner/SKILL.md +379 -1245
- package/plugins/specweave/skills/role-orchestrator/SKILL.md +293 -969
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
- package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-docs/skills/technical-writing/SKILL.md +333 -839
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1080 -0
- package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
- package/plugins/specweave-github/lib/completion-calculator.js +1 -1
- package/plugins/specweave-github/lib/completion-calculator.ts +2 -2
- package/plugins/specweave-github/lib/github-feature-sync.js +152 -18
- package/plugins/specweave-github/lib/github-feature-sync.ts +225 -22
- package/plugins/specweave-github/lib/user-story-issue-builder.js +21 -1
- package/plugins/specweave-github/lib/user-story-issue-builder.ts +31 -3
- package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +981 -0
- package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
- package/plugins/specweave-testing/skills/tdd-expert/SKILL.md +269 -749
- package/plugins/specweave-testing/skills/unit-testing-expert/SKILL.md +318 -810
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# SpecWeave GitHub Sync Hook
|
|
4
|
+
# Runs after task completion to sync progress to GitHub Projects
|
|
5
|
+
#
|
|
6
|
+
# ARCHITECTURE (v0.19.0+): IMMUTABLE DESCRIPTIONS + PROGRESS COMMENTS
|
|
7
|
+
# - User Story files (.specweave/docs/internal/specs/) ↔ GitHub Issues
|
|
8
|
+
# - Issue descriptions created once (IMMUTABLE snapshot)
|
|
9
|
+
# - All updates via progress comments (audit trail)
|
|
10
|
+
#
|
|
11
|
+
# This hook is part of the specweave-github plugin and handles:
|
|
12
|
+
# - Finding which spec user stories the current work belongs to
|
|
13
|
+
# - Syncing progress via GitHub comments (NOT editing issue body)
|
|
14
|
+
# - Creating audit trail of all changes over time
|
|
15
|
+
# - Notifying stakeholders via GitHub notifications
|
|
16
|
+
#
|
|
17
|
+
# Dependencies:
|
|
18
|
+
# - Node.js and TypeScript CLI (dist/cli/commands/sync-spec-content.js)
|
|
19
|
+
# - GitHub CLI (gh) must be installed and authenticated
|
|
20
|
+
# - ProgressCommentBuilder (lib/progress-comment-builder.ts)
|
|
21
|
+
|
|
22
|
+
set -e
|
|
23
|
+
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# PROJECT ROOT DETECTION
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
# Find project root by searching upward for .specweave/ directory
|
|
29
|
+
find_project_root() {
|
|
30
|
+
local dir="$1"
|
|
31
|
+
while [ "$dir" != "/" ]; do
|
|
32
|
+
if [ -d "$dir/.specweave" ]; then
|
|
33
|
+
echo "$dir"
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
dir="$(dirname "$dir")"
|
|
37
|
+
done
|
|
38
|
+
# Fallback: try current directory
|
|
39
|
+
if [ -d "$(pwd)/.specweave" ]; then
|
|
40
|
+
pwd
|
|
41
|
+
else
|
|
42
|
+
echo "$(pwd)"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
|
|
47
|
+
cd "$PROJECT_ROOT" 2>/dev/null || true
|
|
48
|
+
|
|
49
|
+
# ============================================================================
|
|
50
|
+
# CONFIGURATION
|
|
51
|
+
# ============================================================================
|
|
52
|
+
|
|
53
|
+
LOGS_DIR=".specweave/logs"
|
|
54
|
+
DEBUG_LOG="$LOGS_DIR/hooks-debug.log"
|
|
55
|
+
|
|
56
|
+
mkdir -p "$LOGS_DIR" 2>/dev/null || true
|
|
57
|
+
|
|
58
|
+
# ============================================================================
|
|
59
|
+
# PRECONDITIONS CHECK
|
|
60
|
+
# ============================================================================
|
|
61
|
+
|
|
62
|
+
echo "[$(date)] [GitHub] 🔗 GitHub sync hook fired" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
63
|
+
|
|
64
|
+
# Check if Node.js is available
|
|
65
|
+
if ! command -v node &> /dev/null; then
|
|
66
|
+
echo "[$(date)] [GitHub] ⚠️ Node.js not found, skipping GitHub sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
67
|
+
cat <<EOF
|
|
68
|
+
{
|
|
69
|
+
"continue": true
|
|
70
|
+
}
|
|
71
|
+
EOF
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Check if github-spec-content-sync CLI exists
|
|
76
|
+
SYNC_CLI="$PROJECT_ROOT/dist/src/cli/commands/sync-spec-content.js"
|
|
77
|
+
if [ ! -f "$SYNC_CLI" ]; then
|
|
78
|
+
echo "[$(date)] [GitHub] ⚠️ sync-spec-content CLI not found at $SYNC_CLI, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
79
|
+
cat <<EOF
|
|
80
|
+
{
|
|
81
|
+
"continue": true
|
|
82
|
+
}
|
|
83
|
+
EOF
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Check for gh CLI
|
|
88
|
+
if ! command -v gh &> /dev/null; then
|
|
89
|
+
echo "[$(date)] [GitHub] ⚠️ GitHub CLI (gh) not found, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
90
|
+
cat <<EOF
|
|
91
|
+
{
|
|
92
|
+
"continue": true
|
|
93
|
+
}
|
|
94
|
+
EOF
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ============================================================================
|
|
99
|
+
# DETECT ALL SPECS (Multi-Spec Support)
|
|
100
|
+
# ============================================================================
|
|
101
|
+
|
|
102
|
+
# Strategy: Use multi-spec detector to find ALL specs referenced in current increment
|
|
103
|
+
|
|
104
|
+
# 1. Detect current increment (temporary context)
|
|
105
|
+
CURRENT_INCREMENT=$(ls -td .specweave/increments/*/ 2>/dev/null | xargs -n1 basename | grep -v "_backlog" | grep -v "_archive" | grep -v "_working" | head -1)
|
|
106
|
+
|
|
107
|
+
if [ -z "$CURRENT_INCREMENT" ]; then
|
|
108
|
+
echo "[$(date)] [GitHub] ℹ️ No active increment, checking for spec changes..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
109
|
+
# Fall through to sync all changed specs
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# 2. Use TypeScript CLI to detect all specs
|
|
113
|
+
DETECT_CLI="$PROJECT_ROOT/dist/src/cli/commands/detect-specs.js"
|
|
114
|
+
|
|
115
|
+
if [ -f "$DETECT_CLI" ]; then
|
|
116
|
+
echo "[$(date)] [GitHub] 🔍 Detecting all specs in increment $CURRENT_INCREMENT..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
117
|
+
|
|
118
|
+
# Call detect-specs CLI and capture JSON output
|
|
119
|
+
DETECTION_RESULT=$(node "$DETECT_CLI" 2>> "$DEBUG_LOG" || echo "{}")
|
|
120
|
+
|
|
121
|
+
# Extract spec count
|
|
122
|
+
SPEC_COUNT=$(echo "$DETECTION_RESULT" | node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync(0,'utf-8')); console.log(data.specs?.length || 0)")
|
|
123
|
+
|
|
124
|
+
echo "[$(date)] [GitHub] 📋 Detected $SPEC_COUNT spec(s)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
125
|
+
|
|
126
|
+
# Store detection result for later use
|
|
127
|
+
echo "$DETECTION_RESULT" > /tmp/specweave-detected-specs.json
|
|
128
|
+
else
|
|
129
|
+
echo "[$(date)] [GitHub] ⚠️ detect-specs CLI not found at $DETECT_CLI, falling back to git diff" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
130
|
+
SPEC_COUNT=0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# ============================================================================
|
|
134
|
+
# SYNC ALL DETECTED SPECS TO GITHUB (Multi-Spec Support)
|
|
135
|
+
# ============================================================================
|
|
136
|
+
|
|
137
|
+
if [ -f /tmp/specweave-detected-specs.json ] && [ "$SPEC_COUNT" -gt 0 ]; then
|
|
138
|
+
# Multi-spec sync: Loop through all detected specs
|
|
139
|
+
echo "[$(date)] [GitHub] 🔄 Syncing $SPEC_COUNT spec(s) to GitHub..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
140
|
+
|
|
141
|
+
# Extract spec paths using Node.js
|
|
142
|
+
SPEC_PATHS=$(node -e "
|
|
143
|
+
const fs = require('fs');
|
|
144
|
+
const data = JSON.parse(fs.readFileSync('/tmp/specweave-detected-specs.json', 'utf-8'));
|
|
145
|
+
const syncable = data.specs.filter(s => s.syncEnabled && s.project !== '_parent');
|
|
146
|
+
syncable.forEach(s => console.log(s.path));
|
|
147
|
+
" 2>> "$DEBUG_LOG")
|
|
148
|
+
|
|
149
|
+
# Count syncable specs
|
|
150
|
+
SYNCABLE_COUNT=$(echo "$SPEC_PATHS" | grep -v '^$' | wc -l | tr -d ' ')
|
|
151
|
+
|
|
152
|
+
if [ "$SYNCABLE_COUNT" -gt 0 ]; then
|
|
153
|
+
echo "[$(date)] [GitHub] 📋 Syncing $SYNCABLE_COUNT syncable spec(s) (excluding _parent)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
154
|
+
|
|
155
|
+
# Sync each spec
|
|
156
|
+
echo "$SPEC_PATHS" | while read -r SPEC_FILE; do
|
|
157
|
+
if [ -n "$SPEC_FILE" ] && [ -f "$SPEC_FILE" ]; then
|
|
158
|
+
# Extract project and spec ID from path
|
|
159
|
+
SPEC_NAME=$(basename "$SPEC_FILE" .md)
|
|
160
|
+
PROJECT=$(basename "$(dirname "$SPEC_FILE")")
|
|
161
|
+
|
|
162
|
+
echo "[$(date)] [GitHub] 🔄 Syncing $PROJECT/$SPEC_NAME..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
163
|
+
|
|
164
|
+
(cd "$PROJECT_ROOT" && node "$SYNC_CLI" --spec "$SPEC_FILE" --provider github) 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
|
|
165
|
+
echo "[$(date)] [GitHub] ⚠️ Spec sync failed for $PROJECT/$SPEC_NAME (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
echo "[$(date)] [GitHub] ✅ Synced $PROJECT/$SPEC_NAME" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
169
|
+
fi
|
|
170
|
+
done
|
|
171
|
+
|
|
172
|
+
echo "[$(date)] [GitHub] ✅ Multi-spec sync complete ($SYNCABLE_COUNT synced)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
173
|
+
else
|
|
174
|
+
echo "[$(date)] [GitHub] ℹ️ No syncable specs (all specs are _parent or syncEnabled=false)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Cleanup temp file
|
|
178
|
+
rm -f /tmp/specweave-detected-specs.json 2>/dev/null || true
|
|
179
|
+
else
|
|
180
|
+
# Fallback: Sync all modified specs (check git diff)
|
|
181
|
+
echo "[$(date)] [GitHub] 🔄 Checking for modified specs..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
182
|
+
|
|
183
|
+
MODIFIED_SPECS=$(git diff --name-only HEAD .specweave/docs/internal/specs/**/*.md 2>/dev/null || echo "")
|
|
184
|
+
|
|
185
|
+
if [ -n "$MODIFIED_SPECS" ]; then
|
|
186
|
+
echo "[$(date)] [GitHub] 📝 Found modified specs:" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
187
|
+
echo "$MODIFIED_SPECS" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
188
|
+
|
|
189
|
+
# Sync each modified spec
|
|
190
|
+
echo "$MODIFIED_SPECS" | while read -r SPEC_FILE; do
|
|
191
|
+
if [ -n "$SPEC_FILE" ] && [ -f "$SPEC_FILE" ]; then
|
|
192
|
+
echo "[$(date)] [GitHub] 🔄 Syncing $SPEC_FILE..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
193
|
+
(cd "$PROJECT_ROOT" && node "$SYNC_CLI" --spec "$SPEC_FILE" --provider github) 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || true
|
|
194
|
+
fi
|
|
195
|
+
done
|
|
196
|
+
|
|
197
|
+
echo "[$(date)] [GitHub] ✅ Batch spec sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
198
|
+
else
|
|
199
|
+
echo "[$(date)] [GitHub] ℹ️ No modified specs found, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# ============================================================================
|
|
204
|
+
# EPIC GITHUB ISSUE SYNC (DEPRECATED v0.24.0+)
|
|
205
|
+
# ============================================================================
|
|
206
|
+
#
|
|
207
|
+
# ⚠️ DEPRECATED: SpecWeave now syncs ONLY at User Story level.
|
|
208
|
+
#
|
|
209
|
+
# Feature/Epic-level issues are no longer updated.
|
|
210
|
+
# Use /specweave-github:sync instead to sync User Story issues.
|
|
211
|
+
#
|
|
212
|
+
# To re-enable (NOT recommended):
|
|
213
|
+
# export SPECWEAVE_ENABLE_EPIC_SYNC=true
|
|
214
|
+
#
|
|
215
|
+
# @see .specweave/increments/0047-us-task-linkage/reports/GITHUB-TITLE-FORMAT-FIX-PLAN.md
|
|
216
|
+
# ============================================================================
|
|
217
|
+
|
|
218
|
+
if [ "$SPECWEAVE_ENABLE_EPIC_SYNC" = "true" ]; then
|
|
219
|
+
echo "[$(date)] [GitHub] 🔄 Checking for Epic GitHub issue update (DEPRECATED)..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
220
|
+
|
|
221
|
+
# Find active increment ID
|
|
222
|
+
ACTIVE_INCREMENT=$(ls -t .specweave/increments/ | grep -v '^\.' | while read inc; do
|
|
223
|
+
if [ -f ".specweave/increments/$inc/metadata.json" ]; then
|
|
224
|
+
STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' ".specweave/increments/$inc/metadata.json" 2>/dev/null | sed 's/.*"\([^"]*\)".*/\1/' || true)
|
|
225
|
+
if [ "$STATUS" = "active" ]; then
|
|
226
|
+
echo "$inc"
|
|
227
|
+
break
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
done | head -1)
|
|
231
|
+
|
|
232
|
+
if [ -n "$ACTIVE_INCREMENT" ]; then
|
|
233
|
+
echo "[$(date)] [GitHub] 🎯 Active increment: $ACTIVE_INCREMENT" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
234
|
+
|
|
235
|
+
# Run Epic sync script (silently, errors logged to debug log)
|
|
236
|
+
if [ -f "$PROJECT_ROOT/scripts/update-epic-github-issue.sh" ]; then
|
|
237
|
+
echo "[$(date)] [GitHub] 🚀 Updating Epic GitHub issue (DEPRECATED)..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
238
|
+
"$PROJECT_ROOT/scripts/update-epic-github-issue.sh" "$ACTIVE_INCREMENT" >> "$DEBUG_LOG" 2>&1 || true
|
|
239
|
+
echo "[$(date)] [GitHub] ⚠️ Epic sync is deprecated. Use /specweave-github:sync instead." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
240
|
+
else
|
|
241
|
+
echo "[$(date)] [GitHub] ⚠️ Epic sync script not found, skipping" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
242
|
+
fi
|
|
243
|
+
else
|
|
244
|
+
echo "[$(date)] [GitHub] ℹ️ No active increment found, skipping Epic sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
245
|
+
fi
|
|
246
|
+
else
|
|
247
|
+
echo "[$(date)] [GitHub] ℹ️ Epic sync disabled (sync at User Story level only)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# ============================================================================
|
|
251
|
+
# OUTPUT TO CLAUDE
|
|
252
|
+
# ============================================================================
|
|
253
|
+
|
|
254
|
+
cat <<EOF
|
|
255
|
+
{
|
|
256
|
+
"continue": true
|
|
257
|
+
}
|
|
258
|
+
EOF
|
|
@@ -131,7 +131,7 @@ class CompletionCalculator {
|
|
|
131
131
|
const taskId = match[1];
|
|
132
132
|
const taskTitle = match[2].trim();
|
|
133
133
|
const taskBody = match[3];
|
|
134
|
-
const acMatch = taskBody.match(/\*\*AC\*\*:\s*([^\n]+)/);
|
|
134
|
+
const acMatch = taskBody.match(/\*\*(?:Satisfies ACs?|AC)\*\*:\s*([^\n]+)/);
|
|
135
135
|
if (!acMatch) {
|
|
136
136
|
continue;
|
|
137
137
|
}
|
|
@@ -266,8 +266,8 @@ export class CompletionCalculator {
|
|
|
266
266
|
const taskTitle = match[2].trim();
|
|
267
267
|
const taskBody = match[3];
|
|
268
268
|
|
|
269
|
-
// Extract AC list
|
|
270
|
-
const acMatch = taskBody.match(/\*\*AC\*\*:\s*([^\n]+)/);
|
|
269
|
+
// Extract AC list (support both old and new field names)
|
|
270
|
+
const acMatch = taskBody.match(/\*\*(?:Satisfies ACs?|AC)\*\*:\s*([^\n]+)/);
|
|
271
271
|
if (!acMatch) {
|
|
272
272
|
continue; // Skip tasks without AC field
|
|
273
273
|
}
|
|
@@ -6,7 +6,8 @@ import { UserStoryIssueBuilder } from "./user-story-issue-builder.js";
|
|
|
6
6
|
import { CompletionCalculator } from "./completion-calculator.js";
|
|
7
7
|
import { DuplicateDetector } from "./duplicate-detector.js";
|
|
8
8
|
import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
|
|
9
|
-
class
|
|
9
|
+
const _GitHubFeatureSync = class _GitHubFeatureSync {
|
|
10
|
+
// 30 seconds
|
|
10
11
|
constructor(client, specsDir, projectRoot) {
|
|
11
12
|
this.client = client;
|
|
12
13
|
this.specsDir = specsDir;
|
|
@@ -23,6 +24,23 @@ class GitHubFeatureSync {
|
|
|
23
24
|
* 4. Update frontmatter with GitHub issue links
|
|
24
25
|
*/
|
|
25
26
|
async syncFeatureToGitHub(featureId) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const lastSync = _GitHubFeatureSync.syncLocks.get(featureId);
|
|
29
|
+
if (lastSync && now - lastSync < _GitHubFeatureSync.LOCK_DURATION_MS) {
|
|
30
|
+
const secondsRemaining = Math.ceil((_GitHubFeatureSync.LOCK_DURATION_MS - (now - lastSync)) / 1e3);
|
|
31
|
+
console.log(`
|
|
32
|
+
\u23ED\uFE0F Sync already in progress for ${featureId} (or completed ${Math.floor((now - lastSync) / 1e3)}s ago)`);
|
|
33
|
+
console.log(` \u2139\uFE0F Sync will be available in ${secondsRemaining}s to prevent duplicates`);
|
|
34
|
+
console.log(` \u{1F4A1} This prevents race conditions between task completion and status change syncs`);
|
|
35
|
+
return {
|
|
36
|
+
milestoneNumber: 0,
|
|
37
|
+
milestoneUrl: "",
|
|
38
|
+
issuesCreated: 0,
|
|
39
|
+
issuesUpdated: 0,
|
|
40
|
+
userStoriesProcessed: 0
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
_GitHubFeatureSync.syncLocks.set(featureId, now);
|
|
26
44
|
console.log(`
|
|
27
45
|
\u{1F504} Syncing Feature ${featureId} to GitHub...`);
|
|
28
46
|
const featureFolder = await this.findFeatureFolder(featureId);
|
|
@@ -109,8 +127,8 @@ class GitHubFeatureSync {
|
|
|
109
127
|
console.log(` \u{1F6E1}\uFE0F Duplicates detected: ${result.duplicatesFound}, auto-closed: ${result.duplicatesClosed}`);
|
|
110
128
|
}
|
|
111
129
|
await this.updateUserStoryFrontmatter(userStory.filePath, issueNumber);
|
|
130
|
+
await this.updateUserStoryIssue(issueNumber, issueContent, userStory.filePath);
|
|
112
131
|
if (result.wasReused) {
|
|
113
|
-
await this.updateUserStoryIssue(issueNumber, issueContent, userStory.filePath);
|
|
114
132
|
issuesUpdated++;
|
|
115
133
|
} else {
|
|
116
134
|
issuesCreated++;
|
|
@@ -201,10 +219,29 @@ class GitHubFeatureSync {
|
|
|
201
219
|
return folders;
|
|
202
220
|
}
|
|
203
221
|
/**
|
|
204
|
-
* Create GitHub Milestone for Feature
|
|
222
|
+
* Create GitHub Milestone for Feature (with duplicate detection)
|
|
205
223
|
*/
|
|
206
224
|
async createMilestone(featureData) {
|
|
207
225
|
const title = `${featureData.id}: ${featureData.title}`;
|
|
226
|
+
const existingResult = await execFileNoThrow("gh", [
|
|
227
|
+
"api",
|
|
228
|
+
"repos/:owner/:repo/milestones",
|
|
229
|
+
"--jq",
|
|
230
|
+
`.[] | select(.title == "${title}") | {number, html_url}`
|
|
231
|
+
]);
|
|
232
|
+
console.log(` \u{1F50D} Milestone detection: exitCode=${existingResult.exitCode}, stdout length=${existingResult.stdout.length}`);
|
|
233
|
+
if (existingResult.exitCode !== 0) {
|
|
234
|
+
console.log(` \u26A0\uFE0F Detection failed: ${existingResult.stderr}`);
|
|
235
|
+
}
|
|
236
|
+
if (existingResult.exitCode === 0 && existingResult.stdout.trim()) {
|
|
237
|
+
const existing = JSON.parse(existingResult.stdout);
|
|
238
|
+
console.log(` \u267B\uFE0F Reusing existing Milestone #${existing.number}`);
|
|
239
|
+
return {
|
|
240
|
+
number: existing.number,
|
|
241
|
+
url: existing.html_url
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
console.log(` \u2139\uFE0F No existing milestone found, creating new one...`);
|
|
208
245
|
const description = `Feature ${featureData.id}
|
|
209
246
|
|
|
210
247
|
Status: ${featureData.status}
|
|
@@ -270,16 +307,7 @@ Created: ${featureData.created}`;
|
|
|
270
307
|
` \u2705 Created and verified complete: ${completion.acsCompleted}/${completion.acsTotal} ACs, ${completion.tasksCompleted}/${completion.tasksTotal} tasks`
|
|
271
308
|
);
|
|
272
309
|
} else {
|
|
273
|
-
await
|
|
274
|
-
"issue",
|
|
275
|
-
"comment",
|
|
276
|
-
issueNumber.toString(),
|
|
277
|
-
"--body",
|
|
278
|
-
this.calculator.buildProgressComment(completion)
|
|
279
|
-
]);
|
|
280
|
-
console.log(
|
|
281
|
-
` \u{1F4CA} Created: ${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks`
|
|
282
|
-
);
|
|
310
|
+
await this.postProgressCommentIfChanged(issueNumber, completion);
|
|
283
311
|
}
|
|
284
312
|
return issueNumber;
|
|
285
313
|
}
|
|
@@ -331,17 +359,118 @@ Created: ${featureData.created}`;
|
|
|
331
359
|
` \u26A0\uFE0F Reopened: ${completion.blockingAcs.length + completion.blockingTasks.length} items incomplete`
|
|
332
360
|
);
|
|
333
361
|
} else {
|
|
362
|
+
await this.postProgressCommentIfChanged(issueNumber, completion);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
await this.updateStatusLabels(issueNumber, completion);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Update status labels on GitHub issue based on completion state
|
|
369
|
+
*
|
|
370
|
+
* SMART LABEL MANAGEMENT:
|
|
371
|
+
* - Only manages status:* labels (status:not_started, status:in-progress, status:completed)
|
|
372
|
+
* - Preserves all other labels (priority, type, custom labels)
|
|
373
|
+
* - Ensures exactly one status label is present
|
|
374
|
+
*/
|
|
375
|
+
async updateStatusLabels(issueNumber, completion) {
|
|
376
|
+
try {
|
|
377
|
+
const issueData = await this.client.getIssue(issueNumber);
|
|
378
|
+
const currentLabels = issueData.labels || [];
|
|
379
|
+
const statusLabels = currentLabels.filter((label) => label.startsWith("status:"));
|
|
380
|
+
const otherLabels = currentLabels.filter((label) => !label.startsWith("status:"));
|
|
381
|
+
let newStatusLabel;
|
|
382
|
+
if (completion.overallComplete) {
|
|
383
|
+
newStatusLabel = "status:complete";
|
|
384
|
+
} else if (completion.acsPercentage > 0 || completion.tasksPercentage > 0) {
|
|
385
|
+
newStatusLabel = "status:active";
|
|
386
|
+
} else {
|
|
387
|
+
newStatusLabel = "status:not_started";
|
|
388
|
+
}
|
|
389
|
+
const needsUpdate = statusLabels.length !== 1 || !statusLabels.includes(newStatusLabel);
|
|
390
|
+
if (!needsUpdate) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (statusLabels.length > 0) {
|
|
334
394
|
await execFileNoThrow("gh", [
|
|
335
395
|
"issue",
|
|
336
|
-
"
|
|
396
|
+
"edit",
|
|
337
397
|
issueNumber.toString(),
|
|
338
|
-
"--
|
|
339
|
-
|
|
398
|
+
"--remove-label",
|
|
399
|
+
...statusLabels
|
|
340
400
|
]);
|
|
401
|
+
}
|
|
402
|
+
const result = await execFileNoThrow("gh", [
|
|
403
|
+
"issue",
|
|
404
|
+
"edit",
|
|
405
|
+
issueNumber.toString(),
|
|
406
|
+
"--add-label",
|
|
407
|
+
newStatusLabel
|
|
408
|
+
]);
|
|
409
|
+
if (result.exitCode === 0) {
|
|
410
|
+
console.log(` \u{1F3F7}\uFE0F Updated label: ${newStatusLabel}`);
|
|
411
|
+
} else {
|
|
412
|
+
console.warn(` \u26A0\uFE0F Failed to add label ${newStatusLabel}: ${result.stderr}`);
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.warn(` \u26A0\uFE0F Failed to update status labels: ${error.message}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Post progress comment only if it differs from the last comment
|
|
420
|
+
*
|
|
421
|
+
* DEDUPLICATION FIX (2025-11-24):
|
|
422
|
+
* - Prevents posting identical consecutive comments
|
|
423
|
+
* - Fetches last comment from issue
|
|
424
|
+
* - Compares content (ignoring timestamps)
|
|
425
|
+
* - Only posts if progress has changed
|
|
426
|
+
*
|
|
427
|
+
* Root Cause: updateUserStoryIssue() was posting progress comments on EVERY sync,
|
|
428
|
+
* even when progress hadn't changed, causing 4+ duplicate comments.
|
|
429
|
+
*
|
|
430
|
+
* @param issueNumber - GitHub issue number
|
|
431
|
+
* @param completion - Completion status with AC/task metrics
|
|
432
|
+
*/
|
|
433
|
+
async postProgressCommentIfChanged(issueNumber, completion) {
|
|
434
|
+
try {
|
|
435
|
+
const commentsResult = await execFileNoThrow("gh", [
|
|
436
|
+
"api",
|
|
437
|
+
"repos/:owner/:repo/issues/" + issueNumber + "/comments",
|
|
438
|
+
"--jq",
|
|
439
|
+
".[-1] | {body: .body, created_at: .created_at}"
|
|
440
|
+
// Get last comment only
|
|
441
|
+
]);
|
|
442
|
+
let lastCommentBody = "";
|
|
443
|
+
if (commentsResult.exitCode === 0 && commentsResult.stdout.trim()) {
|
|
444
|
+
try {
|
|
445
|
+
const lastComment = JSON.parse(commentsResult.stdout);
|
|
446
|
+
lastCommentBody = lastComment.body || "";
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const newCommentBody = this.calculator.buildProgressComment(completion);
|
|
451
|
+
const normalizeComment = (text) => {
|
|
452
|
+
return text.replace(/🤖 Auto-updated by SpecWeave AC Completion Gate/g, "").replace(/\s+/g, " ").trim();
|
|
453
|
+
};
|
|
454
|
+
const normalizedLast = normalizeComment(lastCommentBody);
|
|
455
|
+
const normalizedNew = normalizeComment(newCommentBody);
|
|
456
|
+
if (normalizedLast === normalizedNew) {
|
|
341
457
|
console.log(
|
|
342
|
-
` \
|
|
458
|
+
` \u23ED\uFE0F Progress unchanged (${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks) - skipping duplicate comment`
|
|
343
459
|
);
|
|
460
|
+
return;
|
|
344
461
|
}
|
|
462
|
+
await execFileNoThrow("gh", [
|
|
463
|
+
"issue",
|
|
464
|
+
"comment",
|
|
465
|
+
issueNumber.toString(),
|
|
466
|
+
"--body",
|
|
467
|
+
newCommentBody
|
|
468
|
+
]);
|
|
469
|
+
console.log(
|
|
470
|
+
` \u{1F4CA} Progress: ${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks (updated)`
|
|
471
|
+
);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error(` \u26A0\uFE0F Failed to check/post progress comment: ${error.message}`);
|
|
345
474
|
}
|
|
346
475
|
}
|
|
347
476
|
/**
|
|
@@ -388,7 +517,12 @@ ${newFrontmatter}---${bodyContent}`;
|
|
|
388
517
|
${newFrontmatter}---${bodyContent}`;
|
|
389
518
|
await writeFile(userStoryPath, newContent, "utf-8");
|
|
390
519
|
}
|
|
391
|
-
}
|
|
520
|
+
};
|
|
521
|
+
// SYNC LOCK: Prevent concurrent syncs of the same feature
|
|
522
|
+
// Maps featureId → last sync timestamp
|
|
523
|
+
_GitHubFeatureSync.syncLocks = /* @__PURE__ */ new Map();
|
|
524
|
+
_GitHubFeatureSync.LOCK_DURATION_MS = 3e4;
|
|
525
|
+
let GitHubFeatureSync = _GitHubFeatureSync;
|
|
392
526
|
export {
|
|
393
527
|
GitHubFeatureSync
|
|
394
528
|
};
|