specweave 0.22.2 → 0.22.3
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 +162 -13
- package/dist/src/cli/commands/repair-status-desync.d.ts +69 -0
- package/dist/src/cli/commands/repair-status-desync.d.ts.map +1 -0
- package/dist/src/cli/commands/repair-status-desync.js +221 -0
- package/dist/src/cli/commands/repair-status-desync.js.map +1 -0
- package/dist/src/cli/commands/validate-status-sync.d.ts +52 -0
- package/dist/src/cli/commands/validate-status-sync.d.ts.map +1 -0
- package/dist/src/cli/commands/validate-status-sync.js +176 -0
- package/dist/src/cli/commands/validate-status-sync.js.map +1 -0
- package/dist/src/cli/update-status-line.d.ts +16 -0
- package/dist/src/cli/update-status-line.d.ts.map +1 -0
- package/dist/src/cli/update-status-line.js +44 -0
- package/dist/src/cli/update-status-line.js.map +1 -0
- package/dist/src/core/increment/completion-validator.d.ts +56 -0
- package/dist/src/core/increment/completion-validator.d.ts.map +1 -0
- package/dist/src/core/increment/completion-validator.js +102 -0
- package/dist/src/core/increment/completion-validator.js.map +1 -0
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +10 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/spec-frontmatter-updater.d.ts +78 -0
- package/dist/src/core/increment/spec-frontmatter-updater.d.ts.map +1 -0
- package/dist/src/core/increment/spec-frontmatter-updater.js +152 -0
- package/dist/src/core/increment/spec-frontmatter-updater.js.map +1 -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 +2 -1
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/status-line/status-line-manager.d.ts +3 -2
- package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-manager.js +40 -17
- package/dist/src/core/status-line/status-line-manager.js.map +1 -1
- package/dist/src/core/status-line/status-line-updater.d.ts +67 -0
- package/dist/src/core/status-line/status-line-updater.d.ts.map +1 -0
- package/dist/src/core/status-line/status-line-updater.js +203 -0
- package/dist/src/core/status-line/status-line-updater.js.map +1 -0
- package/dist/src/core/status-line/types.d.ts +19 -5
- package/dist/src/core/status-line/types.d.ts.map +1 -1
- package/dist/src/core/status-line/types.js +3 -3
- package/dist/src/core/status-line/types.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-done.md +60 -4
- package/plugins/specweave/commands/specweave-reopen.md +29 -2
- package/plugins/specweave/commands/specweave-sync-docs.md +71 -4
- package/plugins/specweave/commands/specweave-update-status.md +151 -0
- package/plugins/specweave/hooks/lib/update-status-line.sh +34 -6
- package/plugins/specweave/hooks/user-prompt-submit.sh +21 -0
- package/plugins/specweave/hooks/validate-increment-completion.sh +113 -0
- package/plugins/specweave-ado/lib/ado-multi-project-sync.js +0 -1
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +21 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -170
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -3222
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specweave:update-status
|
|
3
|
+
description: Force-update status line cache with latest increment status
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Update Status Line
|
|
7
|
+
|
|
8
|
+
**Purpose**: Force-refresh status line cache when it appears stale or after making changes.
|
|
9
|
+
|
|
10
|
+
**Use When**:
|
|
11
|
+
- Status line doesn't reflect recent changes
|
|
12
|
+
- After completing tasks (want to see updated progress immediately)
|
|
13
|
+
- After status transitions (active → completed)
|
|
14
|
+
- Before checking progress with `/specweave:progress`
|
|
15
|
+
- When status line shows outdated information
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
1. **Scan All Increments**
|
|
22
|
+
- Reads all spec.md files in `.specweave/increments/`
|
|
23
|
+
- Identifies open increments (status = active/planning/in-progress)
|
|
24
|
+
- Extracts creation dates from YAML frontmatter
|
|
25
|
+
|
|
26
|
+
2. **Select Current Increment**
|
|
27
|
+
- Sorts by creation date (oldest first)
|
|
28
|
+
- Takes first as current active increment
|
|
29
|
+
- Counts total open increments
|
|
30
|
+
|
|
31
|
+
3. **Parse Task Progress**
|
|
32
|
+
- Reads tasks.md from current increment
|
|
33
|
+
- Uses TaskCounter for accurate counting (no overcounting)
|
|
34
|
+
- Calculates: completed, total, percentage
|
|
35
|
+
|
|
36
|
+
4. **Update Cache**
|
|
37
|
+
- Writes `.specweave/state/status-line.json`
|
|
38
|
+
- Atomic write (temp file → rename)
|
|
39
|
+
- Runs SYNCHRONOUSLY (user waits ~50-100ms)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Force-update status line (no arguments)
|
|
47
|
+
/specweave:update-status
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Output**:
|
|
51
|
+
```
|
|
52
|
+
✓ Status line cache updated
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Example
|
|
58
|
+
|
|
59
|
+
**Before Update** (stale cache):
|
|
60
|
+
```
|
|
61
|
+
Status Line: [0043] ████░░░░ 5/8 tasks (2 open)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**After Update** (fresh cache):
|
|
65
|
+
```
|
|
66
|
+
Status Line: [0043] ████████ 8/8 tasks (1 open) ✓
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Integration with Other Commands
|
|
72
|
+
|
|
73
|
+
This command is automatically called by:
|
|
74
|
+
|
|
75
|
+
1. **`/specweave:progress`**
|
|
76
|
+
- Ensures status line is fresh before showing progress
|
|
77
|
+
- User always sees accurate completion percentages
|
|
78
|
+
|
|
79
|
+
2. **`/specweave:done`**
|
|
80
|
+
- Refreshes status line before PM validation
|
|
81
|
+
- Ensures final progress is accurate
|
|
82
|
+
|
|
83
|
+
3. **`/specweave:status`**
|
|
84
|
+
- Updates cache before showing increment list
|
|
85
|
+
- Shows current increment status
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Technical Details
|
|
90
|
+
|
|
91
|
+
**Performance**: ~50-100ms (synchronous execution)
|
|
92
|
+
|
|
93
|
+
**Cache Location**: `.specweave/state/status-line.json`
|
|
94
|
+
|
|
95
|
+
**Cache Format**:
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"current": {
|
|
99
|
+
"id": "0043-spec-md-desync-fix",
|
|
100
|
+
"name": "0043-spec-md-desync-fix",
|
|
101
|
+
"completed": 8,
|
|
102
|
+
"total": 8,
|
|
103
|
+
"percentage": 100
|
|
104
|
+
},
|
|
105
|
+
"openCount": 1,
|
|
106
|
+
"lastUpdate": "2025-11-18T15:30:00Z"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Source of Truth**:
|
|
111
|
+
- Increment status: `spec.md` YAML frontmatter (NOT metadata.json)
|
|
112
|
+
- Task progress: `tasks.md` (TaskCounter for accuracy)
|
|
113
|
+
|
|
114
|
+
**Atomicity**: Uses temp file → rename pattern to prevent cache corruption
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Troubleshooting
|
|
119
|
+
|
|
120
|
+
**Status line still stale after update?**
|
|
121
|
+
1. Check if `.specweave/state/status-line.json` was modified
|
|
122
|
+
2. Verify spec.md has correct status in YAML frontmatter
|
|
123
|
+
3. Check tasks.md has proper task format (## T-001, etc.)
|
|
124
|
+
|
|
125
|
+
**Error: "Failed to update status line"**
|
|
126
|
+
- Ensure `.specweave/increments/` exists
|
|
127
|
+
- Check file permissions on `.specweave/state/`
|
|
128
|
+
- Verify spec.md has valid YAML frontmatter
|
|
129
|
+
|
|
130
|
+
**Cache shows wrong increment?**
|
|
131
|
+
- Status line shows oldest active increment by default
|
|
132
|
+
- Complete older increments first
|
|
133
|
+
- Or check `created` date in spec.md frontmatter
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Implementation
|
|
138
|
+
|
|
139
|
+
**CLI**: `src/cli/update-status-line.ts`
|
|
140
|
+
**Core Logic**: `src/core/status-line/status-line-updater.ts`
|
|
141
|
+
**Hook (async)**: `plugins/specweave/hooks/lib/update-status-line.sh`
|
|
142
|
+
|
|
143
|
+
**Difference from Hook**:
|
|
144
|
+
- **Command**: SYNCHRONOUS (user waits, sees immediate result)
|
|
145
|
+
- **Hook**: ASYNCHRONOUS (background refresh, no blocking)
|
|
146
|
+
|
|
147
|
+
Both use same logic, different execution model.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
**This command ensures status line is ALWAYS fresh when you need it!** ✓
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# update-status-line.sh (
|
|
3
|
+
# update-status-line.sh (Enhanced with AC Metrics)
|
|
4
4
|
#
|
|
5
5
|
# Updates status line cache with current increment progress.
|
|
6
|
-
# Shows: [increment-name] ████░░░░ X/Y tasks (Z open)
|
|
6
|
+
# Shows: [increment-name] ████░░░░ X/Y tasks | A/B ACs (Z open)
|
|
7
7
|
#
|
|
8
8
|
# Logic:
|
|
9
9
|
# 1. Scan all spec.md for status=active/planning (SOURCE OF TRUTH!)
|
|
10
10
|
# 2. Take first (oldest) as current increment
|
|
11
11
|
# 3. Count all active/planning as openCount
|
|
12
|
-
# 4. Parse current increment's tasks.md for progress
|
|
13
|
-
# 5.
|
|
12
|
+
# 4. Parse current increment's tasks.md for task progress
|
|
13
|
+
# 5. Parse current increment's spec.md for AC progress
|
|
14
|
+
# 6. Write to cache
|
|
14
15
|
#
|
|
15
16
|
# Performance: 50-100ms (runs async, user doesn't wait)
|
|
16
17
|
|
|
@@ -116,19 +117,44 @@ if [[ -f "$TASKS_FILE" ]]; then
|
|
|
116
117
|
fi
|
|
117
118
|
fi
|
|
118
119
|
|
|
120
|
+
# Step 4b: Parse spec.md for AC (Acceptance Criteria) progress
|
|
121
|
+
SPEC_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/spec.md"
|
|
122
|
+
TOTAL_ACS=0
|
|
123
|
+
COMPLETED_ACS=0
|
|
124
|
+
OPEN_ACS=0
|
|
125
|
+
|
|
126
|
+
if [[ -f "$SPEC_FILE" ]]; then
|
|
127
|
+
# Count total ACs: both checked and unchecked
|
|
128
|
+
# Pattern: - [ ] **AC- OR - [x] **AC-
|
|
129
|
+
TOTAL_ACS=$(grep -cE '^- \[(x| )\] \*\*AC-' "$SPEC_FILE" 2>/dev/null || echo 0)
|
|
130
|
+
TOTAL_ACS=$(echo "$TOTAL_ACS" | tr -d '\n\r ' || echo 0)
|
|
131
|
+
|
|
132
|
+
# Count completed ACs (checked)
|
|
133
|
+
# Pattern: - [x] **AC-
|
|
134
|
+
COMPLETED_ACS=$(grep -cE '^- \[x\] \*\*AC-' "$SPEC_FILE" 2>/dev/null || echo 0)
|
|
135
|
+
COMPLETED_ACS=$(echo "$COMPLETED_ACS" | tr -d '\n\r ' || echo 0)
|
|
136
|
+
|
|
137
|
+
# Count open ACs (unchecked)
|
|
138
|
+
# Pattern: - [ ] **AC-
|
|
139
|
+
OPEN_ACS=$(grep -cE '^- \[ \] \*\*AC-' "$SPEC_FILE" 2>/dev/null || echo 0)
|
|
140
|
+
OPEN_ACS=$(echo "$OPEN_ACS" | tr -d '\n\r ' || echo 0)
|
|
141
|
+
fi
|
|
142
|
+
|
|
119
143
|
# Step 5: Extract increment ID and name
|
|
120
144
|
# Format: XXXX-name where XXXX is 4-digit prefix (brackets added by manager)
|
|
121
145
|
INCREMENT_ID=$(echo "$CURRENT_INCREMENT" | grep -oE '^[0-9]{4}')
|
|
122
146
|
INCREMENT_NAME_ONLY=$(echo "$CURRENT_INCREMENT" | sed 's/^[0-9]\{4\}-//')
|
|
123
147
|
INCREMENT_NAME="$INCREMENT_ID-$INCREMENT_NAME_ONLY"
|
|
124
148
|
|
|
125
|
-
# Step 6: Write cache
|
|
149
|
+
# Step 6: Write cache (now includes AC metrics)
|
|
126
150
|
jq -n \
|
|
127
151
|
--arg id "$CURRENT_INCREMENT" \
|
|
128
152
|
--arg name "$INCREMENT_NAME" \
|
|
129
153
|
--argjson completed "$COMPLETED_TASKS" \
|
|
130
154
|
--argjson total "$TOTAL_TASKS" \
|
|
131
155
|
--argjson percentage "$PERCENTAGE" \
|
|
156
|
+
--argjson acsCompleted "$COMPLETED_ACS" \
|
|
157
|
+
--argjson acsTotal "$TOTAL_ACS" \
|
|
132
158
|
--argjson openCount "$OPEN_COUNT" \
|
|
133
159
|
'{
|
|
134
160
|
current: {
|
|
@@ -136,7 +162,9 @@ jq -n \
|
|
|
136
162
|
name: $name,
|
|
137
163
|
completed: $completed,
|
|
138
164
|
total: $total,
|
|
139
|
-
percentage: $percentage
|
|
165
|
+
percentage: $percentage,
|
|
166
|
+
acsCompleted: $acsCompleted,
|
|
167
|
+
acsTotal: $acsTotal
|
|
140
168
|
},
|
|
141
169
|
openCount: $openCount,
|
|
142
170
|
lastUpdate: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
|
|
@@ -342,6 +342,27 @@ if echo "$PROMPT" | grep -qiE "(add|create|implement|build|develop)" && ! echo "
|
|
|
342
342
|
fi
|
|
343
343
|
fi
|
|
344
344
|
|
|
345
|
+
# ==============================================================================
|
|
346
|
+
# STATUS LINE REFRESH: Ensure cache is fresh before showing context
|
|
347
|
+
# ==============================================================================
|
|
348
|
+
# Performance: ~50-100ms (acceptable for UX)
|
|
349
|
+
# Frequency: Every user prompt (high coverage)
|
|
350
|
+
# Benefit: Catches ALL edge cases (manual edits, resume, direct changes)
|
|
351
|
+
#
|
|
352
|
+
# Why here? This hook runs on EVERY user prompt, ensuring status line
|
|
353
|
+
# is ALWAYS up-to-date before showing context to the user.
|
|
354
|
+
#
|
|
355
|
+
# Prevents desync scenarios:
|
|
356
|
+
# - Manual spec.md edits (status: planning → active)
|
|
357
|
+
# - /specweave:resume (status: paused → active)
|
|
358
|
+
# - Direct metadata changes (without hook triggers)
|
|
359
|
+
# - File system operations bypassing hooks
|
|
360
|
+
#
|
|
361
|
+
# Background execution: Runs async, doesn't block user prompt
|
|
362
|
+
|
|
363
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
364
|
+
bash "$HOOK_DIR/lib/update-status-line.sh" 2>/dev/null || true
|
|
365
|
+
|
|
345
366
|
# ==============================================================================
|
|
346
367
|
# OUTPUT: Approve with context or no context
|
|
347
368
|
# ==============================================================================
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Pre-commit hook: Validate Increment Completion
|
|
4
|
+
#
|
|
5
|
+
# Prevents commits where increment status="completed" but:
|
|
6
|
+
# - Acceptance criteria are still open (- [ ] **AC-...)
|
|
7
|
+
# - Tasks are still pending (**Status**: [ ] pending)
|
|
8
|
+
#
|
|
9
|
+
# This prevents false completion and data integrity violations.
|
|
10
|
+
#
|
|
11
|
+
# Exit code 0: All completed increments are valid (allow commit)
|
|
12
|
+
# Exit code 1: Invalid completion detected (block commit)
|
|
13
|
+
|
|
14
|
+
# ANSI colors
|
|
15
|
+
RED='\033[0;31m'
|
|
16
|
+
YELLOW='\033[1;33m'
|
|
17
|
+
GREEN='\033[0;32m'
|
|
18
|
+
NC='\033[0m' # No Color
|
|
19
|
+
|
|
20
|
+
# Check if in SpecWeave project
|
|
21
|
+
if [ ! -d ".specweave/increments" ]; then
|
|
22
|
+
# Not a SpecWeave project, skip validation
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Track if any validation failures found
|
|
27
|
+
has_failures=false
|
|
28
|
+
|
|
29
|
+
# Check all increments
|
|
30
|
+
for increment_dir in .specweave/increments/*/; do
|
|
31
|
+
# Skip if not a directory
|
|
32
|
+
[ ! -d "$increment_dir" ] && continue
|
|
33
|
+
|
|
34
|
+
increment_id=$(basename "$increment_dir")
|
|
35
|
+
spec_file="$increment_dir/spec.md"
|
|
36
|
+
tasks_file="$increment_dir/tasks.md"
|
|
37
|
+
metadata_file="$increment_dir/metadata.json"
|
|
38
|
+
|
|
39
|
+
# Skip if required files don't exist
|
|
40
|
+
[ ! -f "$spec_file" ] && continue
|
|
41
|
+
[ ! -f "$tasks_file" ] && continue
|
|
42
|
+
|
|
43
|
+
# Get status from spec.md frontmatter
|
|
44
|
+
spec_status=$(grep -m1 "^status:" "$spec_file" 2>/dev/null | cut -d: -f2 | tr -d ' ')
|
|
45
|
+
|
|
46
|
+
# Get status from metadata.json (if exists)
|
|
47
|
+
metadata_status=""
|
|
48
|
+
if [ -f "$metadata_file" ]; then
|
|
49
|
+
metadata_status=$(grep -m1 '"status"' "$metadata_file" 2>/dev/null | cut -d'"' -f4)
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Only validate if status is "completed"
|
|
53
|
+
if [ "$spec_status" = "completed" ] || [ "$metadata_status" = "completed" ]; then
|
|
54
|
+
# Count open acceptance criteria (- [ ] **AC-)
|
|
55
|
+
open_acs=$(grep -c "^- \[ \] \*\*AC-" "$spec_file" 2>/dev/null || echo 0)
|
|
56
|
+
|
|
57
|
+
# Count pending tasks (**Status**: [ ] pending)
|
|
58
|
+
pending_tasks=$(grep -ic "\*\*Status\*\*:\s*\[\s*\]\s*pending" "$tasks_file" 2>/dev/null || echo 0)
|
|
59
|
+
|
|
60
|
+
# Validate
|
|
61
|
+
if [ "$open_acs" -gt 0 ] || [ "$pending_tasks" -gt 0 ]; then
|
|
62
|
+
has_failures=true
|
|
63
|
+
|
|
64
|
+
echo -e "${RED}❌ COMMIT BLOCKED: Invalid completion detected${NC}"
|
|
65
|
+
echo -e "${YELLOW}Increment: $increment_id${NC}"
|
|
66
|
+
echo -e "Status: $spec_status (spec.md) / $metadata_status (metadata.json)"
|
|
67
|
+
echo ""
|
|
68
|
+
|
|
69
|
+
if [ "$open_acs" -gt 0 ]; then
|
|
70
|
+
echo -e "${RED} • $open_acs acceptance criteria still open${NC}"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if [ "$pending_tasks" -gt 0 ]; then
|
|
74
|
+
echo -e "${RED} • $pending_tasks tasks still pending${NC}"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
echo ""
|
|
78
|
+
echo -e "${YELLOW}Fix options:${NC}"
|
|
79
|
+
echo " 1. Complete the open work before committing"
|
|
80
|
+
echo " 2. Change status to 'active' or 'paused' in both spec.md and metadata.json"
|
|
81
|
+
echo ""
|
|
82
|
+
echo -e "${YELLOW}To see details:${NC}"
|
|
83
|
+
echo " cat $spec_file | grep '- \[ \] \*\*AC-'"
|
|
84
|
+
echo " cat $tasks_file | grep -i 'Status.*pending'"
|
|
85
|
+
echo ""
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
# Exit with failure if any validation failures
|
|
91
|
+
if [ "$has_failures" = true ]; then
|
|
92
|
+
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
93
|
+
echo -e "${RED}COMMIT BLOCKED: Cannot commit increments with invalid completion${NC}"
|
|
94
|
+
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
95
|
+
echo ""
|
|
96
|
+
echo -e "${YELLOW}Why this matters:${NC}"
|
|
97
|
+
echo " • Prevents false completion (status='completed' with open work)"
|
|
98
|
+
echo " • Ensures data integrity (metadata matches reality)"
|
|
99
|
+
echo " • Stops misleading status line and GitHub sync"
|
|
100
|
+
echo ""
|
|
101
|
+
echo -e "${YELLOW}To fix:${NC}"
|
|
102
|
+
echo " 1. Complete all open ACs and pending tasks, OR"
|
|
103
|
+
echo " 2. Change status to 'active'/'paused' in spec.md and metadata.json"
|
|
104
|
+
echo ""
|
|
105
|
+
echo -e "${YELLOW}To bypass (NOT RECOMMENDED):${NC}"
|
|
106
|
+
echo " git commit --no-verify"
|
|
107
|
+
echo ""
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# All validations passed
|
|
112
|
+
echo -e "${GREEN}✅ Completion validation passed${NC}"
|
|
113
|
+
exit 0
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specweave-github:cleanup-duplicates
|
|
3
3
|
description: Clean up duplicate GitHub issues for an Epic. Finds issues with duplicate titles and closes all except the first created issue.
|
|
4
|
+
justification: |
|
|
5
|
+
CRITICAL INCIDENT RESPONSE TOOL - DO NOT DELETE!
|
|
6
|
+
|
|
7
|
+
Why This Command Exists:
|
|
8
|
+
- Prevention systems (deduplication, GitHub self-healing) work for single-process execution
|
|
9
|
+
- Multiple parallel Claude Code instances bypass all prevention (file-based cache, no distributed locking)
|
|
10
|
+
- GitHub API race conditions: Time gap between "check exists" and "create issue" allows duplicates
|
|
11
|
+
- Historical duplicates from pre-v0.14.1 users (before prevention was added)
|
|
12
|
+
|
|
13
|
+
Evidence of Need:
|
|
14
|
+
- 2025-11-13: 123 duplicate GitHub issues incident (cleaned to 29 unique)
|
|
15
|
+
- Parallel execution creates race conditions that prevention CANNOT solve
|
|
16
|
+
- Industry standard: Prevention + Detection + Cleanup (defense in depth)
|
|
17
|
+
|
|
18
|
+
When to Delete:
|
|
19
|
+
- ONLY if distributed locking implemented (Redis/file locks)
|
|
20
|
+
- AND parallel execution tested (100+ concurrent syncs with zero duplicates)
|
|
21
|
+
- AND zero duplicates for 6+ months in production
|
|
22
|
+
- AND all users migrated to prevention-enabled versions
|
|
23
|
+
|
|
24
|
+
See: .specweave/increments/0043-spec-md-desync-fix/reports/ULTRATHINK-CLEANUP-DUPLICATES-NECESSITY-2025-11-18.md
|
|
4
25
|
---
|
|
5
26
|
|
|
6
27
|
# Clean Up Duplicate GitHub Issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { EnhancedContentBuilder } from "../../../
|
|
2
|
-
import { SpecIncrementMapper } from "../../../
|
|
3
|
-
import { parseSpecContent } from "../../../
|
|
1
|
+
import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
|
|
2
|
+
import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
|
|
3
|
+
import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as fs from "fs/promises";
|
|
6
6
|
async function syncSpecToJiraWithEnhancedContent(options) {
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { AdoClientV2 } from "./ado-client-v2.js";
|
|
2
|
-
import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
|
|
3
|
-
import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
|
|
4
|
-
import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import fs from "fs/promises";
|
|
7
|
-
async function syncSpecToAdoWithEnhancedContent(options) {
|
|
8
|
-
const { specPath, organization, project, dryRun = false, verbose = false } = options;
|
|
9
|
-
try {
|
|
10
|
-
const baseSpec = await parseSpecContent(specPath);
|
|
11
|
-
if (!baseSpec) {
|
|
12
|
-
return {
|
|
13
|
-
success: false,
|
|
14
|
-
action: "error",
|
|
15
|
-
error: "Failed to parse spec content"
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
if (verbose) {
|
|
19
|
-
console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
|
|
20
|
-
}
|
|
21
|
-
const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
|
|
22
|
-
const rootDir = await findSpecWeaveRoot(specPath);
|
|
23
|
-
const mapper = new SpecIncrementMapper(rootDir);
|
|
24
|
-
const mapping = await mapper.mapSpecToIncrements(specId);
|
|
25
|
-
if (verbose) {
|
|
26
|
-
console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
|
|
27
|
-
}
|
|
28
|
-
const taskMapping = buildTaskMapping(mapping.increments, organization, project);
|
|
29
|
-
const architectureDocs = await findArchitectureDocs(rootDir, specId);
|
|
30
|
-
const enhancedSpec = {
|
|
31
|
-
...baseSpec,
|
|
32
|
-
summary: baseSpec.description,
|
|
33
|
-
taskMapping,
|
|
34
|
-
architectureDocs
|
|
35
|
-
};
|
|
36
|
-
const builder = new EnhancedContentBuilder();
|
|
37
|
-
const description = builder.buildExternalDescription(enhancedSpec);
|
|
38
|
-
if (verbose) {
|
|
39
|
-
console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
|
|
40
|
-
}
|
|
41
|
-
if (dryRun) {
|
|
42
|
-
console.log("\u{1F50D} DRY RUN - Would create/update feature with:");
|
|
43
|
-
console.log(` Title: ${baseSpec.title}`);
|
|
44
|
-
console.log(` Description length: ${description.length}`);
|
|
45
|
-
return {
|
|
46
|
-
success: true,
|
|
47
|
-
action: "no-change",
|
|
48
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
if (!organization || !project) {
|
|
52
|
-
return {
|
|
53
|
-
success: false,
|
|
54
|
-
action: "error",
|
|
55
|
-
error: "Azure DevOps organization/project not specified"
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
const profile = {
|
|
59
|
-
provider: "ado",
|
|
60
|
-
displayName: `${organization}/${project}`,
|
|
61
|
-
config: {
|
|
62
|
-
organization,
|
|
63
|
-
project
|
|
64
|
-
},
|
|
65
|
-
timeRange: { default: "1M", max: "6M" }
|
|
66
|
-
};
|
|
67
|
-
const pat = process.env.AZURE_DEVOPS_PAT || "";
|
|
68
|
-
const client = new AdoClientV2(profile, pat);
|
|
69
|
-
const existingFeature = await findExistingFeature(client, baseSpec.identifier.compact);
|
|
70
|
-
let result;
|
|
71
|
-
if (existingFeature) {
|
|
72
|
-
await client.updateWorkItem(existingFeature.id, {
|
|
73
|
-
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
74
|
-
description
|
|
75
|
-
});
|
|
76
|
-
result = {
|
|
77
|
-
success: true,
|
|
78
|
-
action: "updated",
|
|
79
|
-
featureId: existingFeature.id,
|
|
80
|
-
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${existingFeature.id}`,
|
|
81
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
82
|
-
};
|
|
83
|
-
} else {
|
|
84
|
-
const feature = await client.createEpic({
|
|
85
|
-
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
86
|
-
description,
|
|
87
|
-
tags: ["spec", "external-tool-sync"]
|
|
88
|
-
});
|
|
89
|
-
result = {
|
|
90
|
-
success: true,
|
|
91
|
-
action: "created",
|
|
92
|
-
featureId: feature.id,
|
|
93
|
-
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${feature.id}`,
|
|
94
|
-
tasksLinked: taskMapping?.tasks.length || 0
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
if (verbose) {
|
|
98
|
-
console.log(`\u2705 ${result.action === "created" ? "Created" : "Updated"} feature #${result.featureId}`);
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
} catch (error) {
|
|
102
|
-
return {
|
|
103
|
-
success: false,
|
|
104
|
-
action: "error",
|
|
105
|
-
error: error.message
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
async function findSpecWeaveRoot(specPath) {
|
|
110
|
-
let currentDir = path.dirname(specPath);
|
|
111
|
-
while (true) {
|
|
112
|
-
const specweaveDir = path.join(currentDir, ".specweave");
|
|
113
|
-
try {
|
|
114
|
-
await fs.access(specweaveDir);
|
|
115
|
-
return currentDir;
|
|
116
|
-
} catch {
|
|
117
|
-
const parentDir = path.dirname(currentDir);
|
|
118
|
-
if (parentDir === currentDir) {
|
|
119
|
-
throw new Error(".specweave directory not found");
|
|
120
|
-
}
|
|
121
|
-
currentDir = parentDir;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function buildTaskMapping(increments, organization, project) {
|
|
126
|
-
if (increments.length === 0) return void 0;
|
|
127
|
-
const firstIncrement = increments[0];
|
|
128
|
-
const tasks = firstIncrement.tasks.map((task) => ({
|
|
129
|
-
id: task.id,
|
|
130
|
-
title: task.title,
|
|
131
|
-
userStories: task.userStories
|
|
132
|
-
}));
|
|
133
|
-
return {
|
|
134
|
-
incrementId: firstIncrement.id,
|
|
135
|
-
tasks,
|
|
136
|
-
tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/repo?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
async function findArchitectureDocs(rootDir, specId) {
|
|
140
|
-
const docs = [];
|
|
141
|
-
const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
|
|
142
|
-
try {
|
|
143
|
-
const adrDir = path.join(archDir, "adr");
|
|
144
|
-
try {
|
|
145
|
-
const adrs = await fs.readdir(adrDir);
|
|
146
|
-
const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
|
|
147
|
-
for (const adr of relatedAdrs) {
|
|
148
|
-
docs.push({
|
|
149
|
-
type: "adr",
|
|
150
|
-
path: path.join(adrDir, adr),
|
|
151
|
-
title: adr.replace(".md", "").replace(/-/g, " ")
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
155
|
-
}
|
|
156
|
-
} catch {
|
|
157
|
-
}
|
|
158
|
-
return docs;
|
|
159
|
-
}
|
|
160
|
-
async function findExistingFeature(client, specId) {
|
|
161
|
-
try {
|
|
162
|
-
const features = await client.queryWorkItems(`[System.Title] Contains '[${specId}]' AND [System.WorkItemType] = 'Feature'`);
|
|
163
|
-
return features[0] || null;
|
|
164
|
-
} catch {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
export {
|
|
169
|
-
syncSpecToAdoWithEnhancedContent
|
|
170
|
-
};
|