specweave 0.26.11 â 0.26.13
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/dist/plugins/specweave-github/lib/completion-calculator.d.ts +4 -1
- package/dist/plugins/specweave-github/lib/completion-calculator.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/completion-calculator.js +49 -29
- package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -1
- package/dist/src/core/increment/increment-archiver.d.ts +3 -0
- package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
- package/dist/src/core/increment/increment-archiver.js +35 -4
- package/dist/src/core/increment/increment-archiver.js.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.d.ts +5 -0
- package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.js +66 -18
- package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/specweave-archive.md +10 -1
- package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
- package/plugins/specweave/hooks/hooks.json +10 -0
- package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
- package/plugins/specweave/hooks/lib/update-active-increment.sh +96 -0
- package/plugins/specweave/hooks/lib/update-status-line.sh +153 -189
- package/plugins/specweave/hooks/post-edit-write-consolidated.sh +6 -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-metadata-change.sh +9 -0
- package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
- package/plugins/specweave/hooks/post-task-completion.sh +8 -0
- package/plugins/specweave/hooks/post-task-edit.sh +37 -0
- package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
- package/plugins/specweave/hooks/pre-command-deduplication.sh +43 -53
- 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 +5 -0
- package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
- package/plugins/specweave/hooks/user-prompt-submit.sh +143 -289
- package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
- 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-github/hooks/.specweave/logs/hooks-debug.log +1098 -0
- package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
- package/plugins/specweave-github/lib/completion-calculator.js +34 -16
- package/plugins/specweave-github/lib/completion-calculator.ts +54 -32
- 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 +1008 -0
- package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
- package/src/templates/AGENTS.md.template +301 -2452
- package/src/templates/CLAUDE.md.template +99 -667
|
@@ -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
|
|
@@ -99,8 +99,11 @@ class CompletionCalculator {
|
|
|
99
99
|
* Process:
|
|
100
100
|
* 1. Find increment link in user story's "Implementation" section
|
|
101
101
|
* 2. Read increment's tasks.md
|
|
102
|
-
* 3. Filter tasks that reference this user story
|
|
102
|
+
* 3. Filter tasks that reference this user story (via **User Story** field OR AC-IDs)
|
|
103
103
|
* 4. Extract completion status from **Status**: [x] or [ ]
|
|
104
|
+
*
|
|
105
|
+
* IMPORTANT: Checks **User Story** field FIRST (handles multi-story tasks like "US-001, US-002"),
|
|
106
|
+
* then falls back to AC-based filtering. Also handles "Satisfies ACs: All" correctly.
|
|
104
107
|
*/
|
|
105
108
|
async extractTasks(userStoryContent, userStoryId) {
|
|
106
109
|
const tasks = [];
|
|
@@ -125,33 +128,48 @@ class CompletionCalculator {
|
|
|
125
128
|
return tasks;
|
|
126
129
|
}
|
|
127
130
|
const tasksContent = await readFile(tasksPath, "utf-8");
|
|
131
|
+
const normalizeUsId = (id) => {
|
|
132
|
+
const num = id.replace(/^US-?0*/, "");
|
|
133
|
+
return `US-${num.padStart(3, "0")}`;
|
|
134
|
+
};
|
|
135
|
+
const normalizedCurrentUs = normalizeUsId(userStoryId);
|
|
128
136
|
const taskPattern = /###?\s+(T-\d+):\s*([^\n]+)\n([\s\S]*?)(?=\n###?\s+T-\d+:|$)/g;
|
|
129
137
|
let match;
|
|
130
138
|
while ((match = taskPattern.exec(tasksContent)) !== null) {
|
|
131
139
|
const taskId = match[1];
|
|
132
140
|
const taskTitle = match[2].trim();
|
|
133
141
|
const taskBody = match[3];
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
const userStoryFieldMatch = taskBody.match(/\*\*User Story\*\*:\s*([^\n]+)/);
|
|
143
|
+
let belongsToThisUS = false;
|
|
144
|
+
if (userStoryFieldMatch) {
|
|
145
|
+
const userStoryList = userStoryFieldMatch[1].trim();
|
|
146
|
+
const userStoryIds = userStoryList.split(",").map((us) => us.trim());
|
|
147
|
+
belongsToThisUS = userStoryIds.some((usId) => {
|
|
148
|
+
return normalizeUsId(usId) === normalizedCurrentUs;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (!belongsToThisUS) {
|
|
152
|
+
const acMatch2 = taskBody.match(/\*\*(?:Satisfies ACs?|AC)\*\*:\s*([^\n]+)/);
|
|
153
|
+
if (acMatch2) {
|
|
154
|
+
const acList = acMatch2[1].trim();
|
|
155
|
+
if (acList.toLowerCase() !== "all") {
|
|
156
|
+
const acIds2 = acList.split(",").map((ac) => ac.trim());
|
|
157
|
+
belongsToThisUS = acIds2.some((acId) => {
|
|
158
|
+
const usMatch = acId.match(/AC-([A-Z]+\d+)-/);
|
|
159
|
+
if (!usMatch) return false;
|
|
160
|
+
const extractedUsId = usMatch[1];
|
|
161
|
+
return normalizeUsId(extractedUsId) === normalizedCurrentUs;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
137
165
|
}
|
|
138
|
-
const acList = acMatch[1].trim();
|
|
139
|
-
const acIds = acList.split(",").map((ac) => ac.trim());
|
|
140
|
-
const belongsToThisUS = acIds.some((acId) => {
|
|
141
|
-
const usMatch = acId.match(/AC-([A-Z]+\d+)-/);
|
|
142
|
-
if (!usMatch) return false;
|
|
143
|
-
const extractedUsId = usMatch[1];
|
|
144
|
-
const extractedNum = extractedUsId.replace(/^US/, "");
|
|
145
|
-
const normalizedExtracted = `US-${extractedNum.padStart(3, "0")}`;
|
|
146
|
-
const currentNum = userStoryId.replace(/^US-?/, "");
|
|
147
|
-
const normalizedCurrent = `US-${currentNum.padStart(3, "0")}`;
|
|
148
|
-
return normalizedExtracted === normalizedCurrent;
|
|
149
|
-
});
|
|
150
166
|
if (!belongsToThisUS) {
|
|
151
167
|
continue;
|
|
152
168
|
}
|
|
153
169
|
const statusMatch = taskBody.match(/\*\*Status\*\*:\s*\[([x ])\]/);
|
|
154
170
|
const completed = statusMatch ? statusMatch[1] === "x" : false;
|
|
171
|
+
const acMatch = taskBody.match(/\*\*(?:Satisfies ACs?|AC)\*\*:\s*([^\n]+)/);
|
|
172
|
+
const acIds = acMatch ? acMatch[1].trim().split(",").map((ac) => ac.trim()) : [];
|
|
155
173
|
tasks.push({
|
|
156
174
|
id: taskId,
|
|
157
175
|
title: taskTitle,
|
|
@@ -212,8 +212,11 @@ export class CompletionCalculator {
|
|
|
212
212
|
* Process:
|
|
213
213
|
* 1. Find increment link in user story's "Implementation" section
|
|
214
214
|
* 2. Read increment's tasks.md
|
|
215
|
-
* 3. Filter tasks that reference this user story
|
|
215
|
+
* 3. Filter tasks that reference this user story (via **User Story** field OR AC-IDs)
|
|
216
216
|
* 4. Extract completion status from **Status**: [x] or [ ]
|
|
217
|
+
*
|
|
218
|
+
* IMPORTANT: Checks **User Story** field FIRST (handles multi-story tasks like "US-001, US-002"),
|
|
219
|
+
* then falls back to AC-based filtering. Also handles "Satisfies ACs: All" correctly.
|
|
217
220
|
*/
|
|
218
221
|
private async extractTasks(userStoryContent: string, userStoryId: string): Promise<Task[]> {
|
|
219
222
|
const tasks: Task[] = [];
|
|
@@ -252,12 +255,20 @@ export class CompletionCalculator {
|
|
|
252
255
|
|
|
253
256
|
const tasksContent = await readFile(tasksPath, 'utf-8');
|
|
254
257
|
|
|
255
|
-
//
|
|
258
|
+
// Normalize the current user story ID for comparison
|
|
259
|
+
// Handles: "US-001", "US-1", "US001", "001"
|
|
260
|
+
const normalizeUsId = (id: string): string => {
|
|
261
|
+
const num = id.replace(/^US-?0*/, ''); // Remove "US-", "US", and leading zeros
|
|
262
|
+
return `US-${num.padStart(3, '0')}`; // Normalize to "US-001" format
|
|
263
|
+
};
|
|
264
|
+
const normalizedCurrentUs = normalizeUsId(userStoryId);
|
|
265
|
+
|
|
266
|
+
// Extract tasks that reference this User Story
|
|
256
267
|
// Pattern:
|
|
257
268
|
// ### T-001: Task Title
|
|
258
|
-
// **User Story**:
|
|
259
|
-
// **
|
|
260
|
-
// **
|
|
269
|
+
// **User Story**: US-001, US-002 â Check this FIRST!
|
|
270
|
+
// **Satisfies ACs**: AC-US1-01, AC-US1-02 (or "All")
|
|
271
|
+
// **Status**: [x] completed
|
|
261
272
|
const taskPattern = /###?\s+(T-\d+):\s*([^\n]+)\n([\s\S]*?)(?=\n###?\s+T-\d+:|$)/g;
|
|
262
273
|
let match;
|
|
263
274
|
|
|
@@ -266,34 +277,41 @@ export class CompletionCalculator {
|
|
|
266
277
|
const taskTitle = match[2].trim();
|
|
267
278
|
const taskBody = match[3];
|
|
268
279
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
// PRIORITY 1: Check **User Story** field directly (handles multi-story tasks!)
|
|
281
|
+
// Pattern: **User Story**: US-001, US-002, US-003
|
|
282
|
+
const userStoryFieldMatch = taskBody.match(/\*\*User Story\*\*:\s*([^\n]+)/);
|
|
283
|
+
let belongsToThisUS = false;
|
|
284
|
+
|
|
285
|
+
if (userStoryFieldMatch) {
|
|
286
|
+
const userStoryList = userStoryFieldMatch[1].trim();
|
|
287
|
+
// Split by comma and check if any matches current user story
|
|
288
|
+
const userStoryIds = userStoryList.split(',').map((us) => us.trim());
|
|
289
|
+
belongsToThisUS = userStoryIds.some((usId) => {
|
|
290
|
+
return normalizeUsId(usId) === normalizedCurrentUs;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// PRIORITY 2: Fall back to AC-based filtering (legacy support)
|
|
295
|
+
if (!belongsToThisUS) {
|
|
296
|
+
const acMatch = taskBody.match(/\*\*(?:Satisfies ACs?|AC)\*\*:\s*([^\n]+)/);
|
|
297
|
+
if (acMatch) {
|
|
298
|
+
const acList = acMatch[1].trim();
|
|
299
|
+
|
|
300
|
+
// Handle special "All" case - if "All", include task if User Story field matched
|
|
301
|
+
// (already handled above), otherwise skip AC-based check for "All"
|
|
302
|
+
if (acList.toLowerCase() !== 'all') {
|
|
303
|
+
const acIds = acList.split(',').map((ac) => ac.trim());
|
|
304
|
+
belongsToThisUS = acIds.some((acId) => {
|
|
305
|
+
// Extract US ID from AC-ID: AC-US1-01 â US-001
|
|
306
|
+
const usMatch = acId.match(/AC-([A-Z]+\d+)-/);
|
|
307
|
+
if (!usMatch) return false;
|
|
308
|
+
|
|
309
|
+
const extractedUsId = usMatch[1]; // e.g., "US1" or "US001"
|
|
310
|
+
return normalizeUsId(extractedUsId) === normalizedCurrentUs;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
273
314
|
}
|
|
274
|
-
const acList = acMatch[1].trim();
|
|
275
|
-
|
|
276
|
-
// Check if any AC in this task belongs to current User Story
|
|
277
|
-
// AC-US1-01 â US-001
|
|
278
|
-
// AC-US001-01 â US-001
|
|
279
|
-
const acIds = acList.split(',').map((ac) => ac.trim());
|
|
280
|
-
const belongsToThisUS = acIds.some((acId) => {
|
|
281
|
-
// Extract US ID from AC-ID
|
|
282
|
-
// AC-US1-01 â US1 â US-001
|
|
283
|
-
// AC-US001-01 â US001 â US-001
|
|
284
|
-
const usMatch = acId.match(/AC-([A-Z]+\d+)-/);
|
|
285
|
-
if (!usMatch) return false;
|
|
286
|
-
|
|
287
|
-
// Normalize to US-XXX format (pad with zeros)
|
|
288
|
-
const extractedUsId = usMatch[1]; // e.g., "US1" or "US001"
|
|
289
|
-
const extractedNum = extractedUsId.replace(/^US/, ''); // "1" or "001"
|
|
290
|
-
const normalizedExtracted = `US-${extractedNum.padStart(3, '0')}`; // "US-001"
|
|
291
|
-
|
|
292
|
-
const currentNum = userStoryId.replace(/^US-?/, ''); // "001" or "1"
|
|
293
|
-
const normalizedCurrent = `US-${currentNum.padStart(3, '0')}`; // "US-001"
|
|
294
|
-
|
|
295
|
-
return normalizedExtracted === normalizedCurrent;
|
|
296
|
-
});
|
|
297
315
|
|
|
298
316
|
if (!belongsToThisUS) {
|
|
299
317
|
continue;
|
|
@@ -303,6 +321,10 @@ export class CompletionCalculator {
|
|
|
303
321
|
const statusMatch = taskBody.match(/\*\*Status\*\*:\s*\[([x ])\]/);
|
|
304
322
|
const completed = statusMatch ? statusMatch[1] === 'x' : false;
|
|
305
323
|
|
|
324
|
+
// Extract AC list for userStories field (for backward compatibility)
|
|
325
|
+
const acMatch = taskBody.match(/\*\*(?:Satisfies ACs?|AC)\*\*:\s*([^\n]+)/);
|
|
326
|
+
const acIds = acMatch ? acMatch[1].trim().split(',').map((ac) => ac.trim()) : [];
|
|
327
|
+
|
|
306
328
|
tasks.push({
|
|
307
329
|
id: taskId,
|
|
308
330
|
title: taskTitle,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# SpecWeave JIRA Sync Hook
|
|
4
|
+
# Runs after task completion to sync progress to JIRA Issues
|
|
5
|
+
#
|
|
6
|
+
# This hook is part of the specweave-jira plugin and handles:
|
|
7
|
+
# - Syncing task completion state to JIRA issues
|
|
8
|
+
# - Updating JIRA issue status based on increment progress
|
|
9
|
+
#
|
|
10
|
+
# Dependencies:
|
|
11
|
+
# - Node.js for running sync scripts
|
|
12
|
+
# - jq for JSON parsing
|
|
13
|
+
# - metadata.json must have .jira.issue field
|
|
14
|
+
# - JIRA API credentials in .env
|
|
15
|
+
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
# ============================================================================
|
|
19
|
+
# PROJECT ROOT DETECTION
|
|
20
|
+
# ============================================================================
|
|
21
|
+
|
|
22
|
+
# Find project root by searching upward for .specweave/ directory
|
|
23
|
+
find_project_root() {
|
|
24
|
+
local dir="$1"
|
|
25
|
+
while [ "$dir" != "/" ]; do
|
|
26
|
+
if [ -d "$dir/.specweave" ]; then
|
|
27
|
+
echo "$dir"
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
dir="$(dirname "$dir")"
|
|
31
|
+
done
|
|
32
|
+
# Fallback: try current directory
|
|
33
|
+
if [ -d "$(pwd)/.specweave" ]; then
|
|
34
|
+
pwd
|
|
35
|
+
else
|
|
36
|
+
echo "$(pwd)"
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
|
|
41
|
+
cd "$PROJECT_ROOT" 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
# ============================================================================
|
|
44
|
+
# CONFIGURATION
|
|
45
|
+
# ============================================================================
|
|
46
|
+
|
|
47
|
+
LOGS_DIR=".specweave/logs"
|
|
48
|
+
DEBUG_LOG="$LOGS_DIR/hooks-debug.log"
|
|
49
|
+
|
|
50
|
+
mkdir -p "$LOGS_DIR" 2>/dev/null || true
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# PRECONDITIONS CHECK
|
|
54
|
+
# ============================================================================
|
|
55
|
+
|
|
56
|
+
echo "[$(date)] [JIRA] đ JIRA sync hook fired" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
57
|
+
|
|
58
|
+
# Detect current increment
|
|
59
|
+
CURRENT_INCREMENT=$(ls -td .specweave/increments/*/ 2>/dev/null | xargs -n1 basename | grep -v "_backlog" | grep -v "_archive" | grep -v "_working" | head -1)
|
|
60
|
+
|
|
61
|
+
if [ -z "$CURRENT_INCREMENT" ]; then
|
|
62
|
+
echo "[$(date)] [JIRA] âšī¸ No active increment, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
63
|
+
cat <<EOF
|
|
64
|
+
{
|
|
65
|
+
"continue": true
|
|
66
|
+
}
|
|
67
|
+
EOF
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Check for metadata.json
|
|
72
|
+
METADATA_FILE=".specweave/increments/$CURRENT_INCREMENT/metadata.json"
|
|
73
|
+
|
|
74
|
+
if [ ! -f "$METADATA_FILE" ]; then
|
|
75
|
+
echo "[$(date)] [JIRA] âšī¸ No metadata.json for $CURRENT_INCREMENT, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
76
|
+
cat <<EOF
|
|
77
|
+
{
|
|
78
|
+
"continue": true
|
|
79
|
+
}
|
|
80
|
+
EOF
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Check for JIRA issue link
|
|
85
|
+
if ! command -v jq &> /dev/null; then
|
|
86
|
+
echo "[$(date)] [JIRA] â ī¸ jq not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
87
|
+
cat <<EOF
|
|
88
|
+
{
|
|
89
|
+
"continue": true
|
|
90
|
+
}
|
|
91
|
+
EOF
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
JIRA_ISSUE=$(jq -r '.jira.issue // empty' "$METADATA_FILE" 2>/dev/null)
|
|
96
|
+
|
|
97
|
+
if [ -z "$JIRA_ISSUE" ]; then
|
|
98
|
+
echo "[$(date)] [JIRA] âšī¸ No JIRA issue linked to $CURRENT_INCREMENT, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
99
|
+
cat <<EOF
|
|
100
|
+
{
|
|
101
|
+
"continue": true
|
|
102
|
+
}
|
|
103
|
+
EOF
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Check for Node.js
|
|
108
|
+
if ! command -v node &> /dev/null; then
|
|
109
|
+
echo "[$(date)] [JIRA] â ī¸ Node.js not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
110
|
+
cat <<EOF
|
|
111
|
+
{
|
|
112
|
+
"continue": true
|
|
113
|
+
}
|
|
114
|
+
EOF
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Check for JIRA sync script
|
|
119
|
+
if [ ! -f "dist/commands/jira-sync.js" ]; then
|
|
120
|
+
echo "[$(date)] [JIRA] â ī¸ jira-sync.js not found, skipping JIRA sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
121
|
+
cat <<EOF
|
|
122
|
+
{
|
|
123
|
+
"continue": true
|
|
124
|
+
}
|
|
125
|
+
EOF
|
|
126
|
+
exit 0
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# ============================================================================
|
|
130
|
+
# JIRA SYNC LOGIC
|
|
131
|
+
# ============================================================================
|
|
132
|
+
|
|
133
|
+
echo "[$(date)] [JIRA] đ Syncing to JIRA issue $JIRA_ISSUE" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
134
|
+
|
|
135
|
+
# Run JIRA sync command (non-blocking)
|
|
136
|
+
node dist/commands/jira-sync.js "$CURRENT_INCREMENT" 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
|
|
137
|
+
echo "[$(date)] [JIRA] â ī¸ Failed to sync to JIRA (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
echo "[$(date)] [JIRA] â
JIRA sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
141
|
+
|
|
142
|
+
# ============================================================================
|
|
143
|
+
# SPEC COMMIT SYNC (NEW!)
|
|
144
|
+
# ============================================================================
|
|
145
|
+
|
|
146
|
+
echo "[$(date)] [JIRA] đ Checking for spec commit sync..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
147
|
+
|
|
148
|
+
# Call TypeScript CLI to sync commits
|
|
149
|
+
if command -v node &> /dev/null && [ -f "$PROJECT_ROOT/dist/cli/commands/sync-spec-commits.js" ]; then
|
|
150
|
+
echo "[$(date)] [JIRA] đ Running spec commit sync..." >> "$DEBUG_LOG" 2>/dev/null || true
|
|
151
|
+
|
|
152
|
+
node "$PROJECT_ROOT/dist/cli/commands/sync-spec-commits.js" \
|
|
153
|
+
--increment "$PROJECT_ROOT/.specweave/increments/$CURRENT_INCREMENT" \
|
|
154
|
+
--provider jira \
|
|
155
|
+
2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
|
|
156
|
+
echo "[$(date)] [JIRA] â ī¸ Spec commit sync failed (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
echo "[$(date)] [JIRA] â
Spec commit sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
160
|
+
else
|
|
161
|
+
echo "[$(date)] [JIRA] âšī¸ Spec commit sync not available (node or script not found)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# ============================================================================
|
|
165
|
+
# OUTPUT TO CLAUDE
|
|
166
|
+
# ============================================================================
|
|
167
|
+
|
|
168
|
+
cat <<EOF
|
|
169
|
+
{
|
|
170
|
+
"continue": true
|
|
171
|
+
}
|
|
172
|
+
EOF
|
|
@@ -1,6 +1,6 @@
|
|
|
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";
|
|
1
|
+
import { EnhancedContentBuilder } from "../../../dist/src/core/sync/enhanced-content-builder.js";
|
|
2
|
+
import { SpecIncrementMapper } from "../../../dist/src/core/sync/spec-increment-mapper.js";
|
|
3
|
+
import { parseSpecContent } from "../../../dist/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) {
|