specweave 0.26.5 → 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.
Files changed (65) hide show
  1. package/CLAUDE.md +35 -5
  2. package/bin/specweave.js +15 -0
  3. package/dist/plugins/specweave-github/lib/completion-calculator.js +2 -2
  4. package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +28 -1
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.js +191 -19
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  9. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts +3 -0
  10. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
  11. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +25 -2
  12. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
  13. package/dist/src/cli/commands/archive.d.ts +10 -0
  14. package/dist/src/cli/commands/archive.d.ts.map +1 -0
  15. package/dist/src/cli/commands/archive.js +78 -0
  16. package/dist/src/cli/commands/archive.js.map +1 -0
  17. package/dist/src/cli/commands/init.js +2 -2
  18. package/dist/src/cli/commands/init.js.map +1 -1
  19. package/dist/src/cli/helpers/init/initial-increment-generator.d.ts.map +1 -1
  20. package/dist/src/cli/helpers/init/initial-increment-generator.js +48 -8
  21. package/dist/src/cli/helpers/init/initial-increment-generator.js.map +1 -1
  22. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  23. package/dist/src/core/increment/metadata-manager.js +15 -12
  24. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  25. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  26. package/dist/src/core/living-docs/living-docs-sync.js +43 -6
  27. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  28. package/dist/src/init/repo/types.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/plugins/specweave/agents/pm/AGENT.md +13 -7
  31. package/plugins/specweave/commands/sync-diagnostics.md +227 -0
  32. package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
  33. package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
  34. package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
  35. package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
  36. package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
  37. package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
  38. package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
  39. package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
  40. package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
  41. package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
  42. package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
  43. package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
  44. package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
  45. package/plugins/specweave/hooks/user-prompt-submit.sh +20 -8
  46. package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
  47. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +15 -12
  48. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
  49. package/plugins/specweave/skills/increment-planner/SKILL.md +57 -7
  50. package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
  51. package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
  52. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
  53. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  54. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1080 -0
  55. package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
  56. package/plugins/specweave-github/lib/completion-calculator.js +1 -1
  57. package/plugins/specweave-github/lib/completion-calculator.ts +2 -2
  58. package/plugins/specweave-github/lib/github-feature-sync.js +152 -18
  59. package/plugins/specweave-github/lib/github-feature-sync.ts +225 -22
  60. package/plugins/specweave-github/lib/user-story-issue-builder.js +21 -1
  61. package/plugins/specweave-github/lib/user-story-issue-builder.ts +31 -3
  62. package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
  63. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
  64. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +981 -0
  65. package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
@@ -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 GitHubFeatureSync {
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 execFileNoThrow("gh", [
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
- "comment",
396
+ "edit",
337
397
  issueNumber.toString(),
338
- "--body",
339
- this.calculator.buildProgressComment(completion)
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
- ` \u{1F4CA} Progress: ${completion.acsPercentage.toFixed(0)}% ACs, ${completion.tasksPercentage.toFixed(0)}% tasks`
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
  };