specweave 1.0.22 → 1.0.24
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/README.md +95 -378
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +8 -26
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.d.ts +2 -0
- package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.js +14 -4
- package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +6 -2
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/package.json +3 -3
- 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.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/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1262 -0
- package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
- package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +134 -0
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1254 -0
- package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Post Living Docs Update Hook - Azure DevOps Sync
|
|
5
|
+
# ============================================================================
|
|
6
|
+
#
|
|
7
|
+
# Triggered after living docs are updated to sync with Azure DevOps.
|
|
8
|
+
# CRITICAL: External tool status ALWAYS wins in conflicts!
|
|
9
|
+
#
|
|
10
|
+
# Triggers:
|
|
11
|
+
# 1. After /specweave:done (increment completion)
|
|
12
|
+
# 2. After /specweave:sync-docs update
|
|
13
|
+
# 3. After manual spec edits
|
|
14
|
+
# 4. After webhook from ADO
|
|
15
|
+
#
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
set -e
|
|
19
|
+
|
|
20
|
+
# Configuration
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
23
|
+
LIVING_DOCS_DIR="$PROJECT_ROOT/.specweave/docs/internal/specs"
|
|
24
|
+
LOG_FILE="$PROJECT_ROOT/.specweave/logs/ado-sync.log"
|
|
25
|
+
DEBUG=${DEBUG:-0}
|
|
26
|
+
|
|
27
|
+
# Ensure log directory exists
|
|
28
|
+
mkdir -p "$(dirname "$LOG_FILE")"
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# Logging
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
log() {
|
|
35
|
+
local level=$1
|
|
36
|
+
shift
|
|
37
|
+
local message="$@"
|
|
38
|
+
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
39
|
+
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
|
40
|
+
[ "$DEBUG" -eq 1 ] && echo "[$level] $message" >&2
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
log_info() {
|
|
44
|
+
log "INFO" "$@"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log_error() {
|
|
48
|
+
log "ERROR" "$@"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
log_debug() {
|
|
52
|
+
[ "$DEBUG" -eq 1 ] && log "DEBUG" "$@"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# External Tool Detection
|
|
57
|
+
# ============================================================================
|
|
58
|
+
|
|
59
|
+
detect_external_tool() {
|
|
60
|
+
local spec_path=$1
|
|
61
|
+
|
|
62
|
+
# Check for external links in spec metadata
|
|
63
|
+
if grep -q "externalLinks:" "$spec_path"; then
|
|
64
|
+
if grep -q "ado:" "$spec_path"; then
|
|
65
|
+
echo "ado"
|
|
66
|
+
elif grep -q "jira:" "$spec_path"; then
|
|
67
|
+
echo "jira"
|
|
68
|
+
elif grep -q "github:" "$spec_path"; then
|
|
69
|
+
echo "github"
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# Status Mapping
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
map_ado_status_to_local() {
|
|
79
|
+
local ado_status=$1
|
|
80
|
+
|
|
81
|
+
case "$ado_status" in
|
|
82
|
+
"New")
|
|
83
|
+
echo "draft"
|
|
84
|
+
;;
|
|
85
|
+
"Active")
|
|
86
|
+
echo "in-progress"
|
|
87
|
+
;;
|
|
88
|
+
"Resolved")
|
|
89
|
+
echo "implemented"
|
|
90
|
+
;;
|
|
91
|
+
"Closed")
|
|
92
|
+
echo "complete"
|
|
93
|
+
;;
|
|
94
|
+
"In Review"|"In QA")
|
|
95
|
+
echo "in-qa"
|
|
96
|
+
;;
|
|
97
|
+
*)
|
|
98
|
+
echo "unknown"
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
map_local_status_to_ado() {
|
|
104
|
+
local local_status=$1
|
|
105
|
+
|
|
106
|
+
case "$local_status" in
|
|
107
|
+
"draft")
|
|
108
|
+
echo "New"
|
|
109
|
+
;;
|
|
110
|
+
"in-progress")
|
|
111
|
+
echo "Active"
|
|
112
|
+
;;
|
|
113
|
+
"implemented")
|
|
114
|
+
echo "Resolved"
|
|
115
|
+
;;
|
|
116
|
+
"complete")
|
|
117
|
+
echo "Closed"
|
|
118
|
+
;;
|
|
119
|
+
"in-qa")
|
|
120
|
+
echo "In Review"
|
|
121
|
+
;;
|
|
122
|
+
*)
|
|
123
|
+
echo "Active"
|
|
124
|
+
;;
|
|
125
|
+
esac
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# ============================================================================
|
|
129
|
+
# ADO API Functions
|
|
130
|
+
# ============================================================================
|
|
131
|
+
|
|
132
|
+
get_ado_work_item_status() {
|
|
133
|
+
local work_item_id=$1
|
|
134
|
+
local org="${AZURE_DEVOPS_ORG}"
|
|
135
|
+
local project="${AZURE_DEVOPS_PROJECT}"
|
|
136
|
+
local pat="${AZURE_DEVOPS_PAT}"
|
|
137
|
+
|
|
138
|
+
if [ -z "$org" ] || [ -z "$pat" ]; then
|
|
139
|
+
log_error "ADO credentials not configured"
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
local api_url="https://dev.azure.com/${org}/${project}/_apis/wit/workitems/${work_item_id}?api-version=7.0"
|
|
144
|
+
|
|
145
|
+
log_debug "Fetching ADO work item $work_item_id status"
|
|
146
|
+
|
|
147
|
+
local response=$(curl -s -u ":${pat}" \
|
|
148
|
+
-H "Content-Type: application/json" \
|
|
149
|
+
"$api_url")
|
|
150
|
+
|
|
151
|
+
if [ $? -ne 0 ]; then
|
|
152
|
+
log_error "Failed to fetch ADO work item status"
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# Extract status from response
|
|
157
|
+
local status=$(echo "$response" | jq -r '.fields["System.State"]')
|
|
158
|
+
|
|
159
|
+
if [ "$status" = "null" ] || [ -z "$status" ]; then
|
|
160
|
+
log_error "Could not extract status from ADO response"
|
|
161
|
+
return 1
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
echo "$status"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
update_ado_work_item() {
|
|
168
|
+
local work_item_id=$1
|
|
169
|
+
local spec_content=$2
|
|
170
|
+
local org="${AZURE_DEVOPS_ORG}"
|
|
171
|
+
local project="${AZURE_DEVOPS_PROJECT}"
|
|
172
|
+
local pat="${AZURE_DEVOPS_PAT}"
|
|
173
|
+
|
|
174
|
+
if [ -z "$org" ] || [ -z "$pat" ]; then
|
|
175
|
+
log_error "ADO credentials not configured"
|
|
176
|
+
return 1
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Extract current status from spec
|
|
180
|
+
local local_status=$(echo "$spec_content" | grep "^status:" | cut -d: -f2 | tr -d ' ')
|
|
181
|
+
local ado_status=$(map_local_status_to_ado "$local_status")
|
|
182
|
+
|
|
183
|
+
local api_url="https://dev.azure.com/${org}/${project}/_apis/wit/workitems/${work_item_id}?api-version=7.0"
|
|
184
|
+
|
|
185
|
+
# Create update payload
|
|
186
|
+
local payload=$(cat <<EOF
|
|
187
|
+
[
|
|
188
|
+
{
|
|
189
|
+
"op": "add",
|
|
190
|
+
"path": "/fields/System.State",
|
|
191
|
+
"value": "$ado_status"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"op": "add",
|
|
195
|
+
"path": "/fields/System.History",
|
|
196
|
+
"value": "Updated from SpecWeave living docs"
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
EOF
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
log_debug "Updating ADO work item $work_item_id with status: $ado_status"
|
|
203
|
+
|
|
204
|
+
curl -s -X PATCH \
|
|
205
|
+
-u ":${pat}" \
|
|
206
|
+
-H "Content-Type: application/json-patch+json" \
|
|
207
|
+
-d "$payload" \
|
|
208
|
+
"$api_url" > /dev/null
|
|
209
|
+
|
|
210
|
+
if [ $? -ne 0 ]; then
|
|
211
|
+
log_error "Failed to update ADO work item"
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
log_info "Updated ADO work item $work_item_id"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# ============================================================================
|
|
219
|
+
# Conflict Resolution - CRITICAL: External Wins!
|
|
220
|
+
# ============================================================================
|
|
221
|
+
|
|
222
|
+
resolve_status_conflict() {
|
|
223
|
+
local spec_path=$1
|
|
224
|
+
local local_status=$2
|
|
225
|
+
local external_status=$3
|
|
226
|
+
|
|
227
|
+
local mapped_external=$(map_ado_status_to_local "$external_status")
|
|
228
|
+
|
|
229
|
+
if [ "$local_status" != "$mapped_external" ]; then
|
|
230
|
+
log_info "Status conflict detected:"
|
|
231
|
+
log_info " Local: $local_status"
|
|
232
|
+
log_info " External: $external_status (mapped: $mapped_external)"
|
|
233
|
+
log_info " Resolution: EXTERNAL WINS - applying $mapped_external"
|
|
234
|
+
|
|
235
|
+
# Update local spec with external status
|
|
236
|
+
sed -i.bak "s/^status: .*/status: $mapped_external/" "$spec_path"
|
|
237
|
+
|
|
238
|
+
# Add sync metadata
|
|
239
|
+
local timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
240
|
+
|
|
241
|
+
# Check if syncedAt exists, update or add
|
|
242
|
+
if grep -q "syncedAt:" "$spec_path"; then
|
|
243
|
+
sed -i.bak "s/syncedAt: .*/syncedAt: \"$timestamp\"/" "$spec_path"
|
|
244
|
+
else
|
|
245
|
+
# Add after externalLinks section
|
|
246
|
+
sed -i.bak "/externalLinks:/a\\
|
|
247
|
+
syncedAt: \"$timestamp\"" "$spec_path"
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# Clean up backup files
|
|
251
|
+
rm -f "${spec_path}.bak"
|
|
252
|
+
|
|
253
|
+
log_info "Local spec updated with external status: $mapped_external"
|
|
254
|
+
return 0
|
|
255
|
+
else
|
|
256
|
+
log_debug "No status conflict - local and external match: $local_status"
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ============================================================================
|
|
262
|
+
# Main Sync Function
|
|
263
|
+
# ============================================================================
|
|
264
|
+
|
|
265
|
+
sync_spec_with_ado() {
|
|
266
|
+
local spec_path=$1
|
|
267
|
+
|
|
268
|
+
if [ ! -f "$spec_path" ]; then
|
|
269
|
+
log_error "Spec file not found: $spec_path"
|
|
270
|
+
return 1
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
local spec_name=$(basename "$spec_path")
|
|
274
|
+
log_info "Syncing spec: $spec_name"
|
|
275
|
+
|
|
276
|
+
# Read spec content
|
|
277
|
+
local spec_content=$(cat "$spec_path")
|
|
278
|
+
|
|
279
|
+
# Extract ADO work item ID from metadata
|
|
280
|
+
local work_item_id=$(echo "$spec_content" | grep -A5 "externalLinks:" | grep -A3 "ado:" | grep "featureId:" | cut -d: -f2 | tr -d ' ')
|
|
281
|
+
|
|
282
|
+
if [ -z "$work_item_id" ]; then
|
|
283
|
+
log_debug "No ADO work item linked to spec, skipping sync"
|
|
284
|
+
return 0
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
log_info "Found ADO work item ID: $work_item_id"
|
|
288
|
+
|
|
289
|
+
# Step 1: Push updates to ADO (content changes)
|
|
290
|
+
update_ado_work_item "$work_item_id" "$spec_content"
|
|
291
|
+
|
|
292
|
+
# Step 2: CRITICAL - Pull status from ADO (external wins!)
|
|
293
|
+
local external_status=$(get_ado_work_item_status "$work_item_id")
|
|
294
|
+
|
|
295
|
+
if [ -z "$external_status" ]; then
|
|
296
|
+
log_error "Could not fetch ADO status"
|
|
297
|
+
return 1
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
log_info "ADO status: $external_status"
|
|
301
|
+
|
|
302
|
+
# Step 3: Extract local status
|
|
303
|
+
local local_status=$(echo "$spec_content" | grep "^status:" | cut -d: -f2 | tr -d ' ')
|
|
304
|
+
|
|
305
|
+
log_info "Local status: $local_status"
|
|
306
|
+
|
|
307
|
+
# Step 4: Resolve conflicts - EXTERNAL WINS
|
|
308
|
+
resolve_status_conflict "$spec_path" "$local_status" "$external_status"
|
|
309
|
+
|
|
310
|
+
log_info "Sync completed for $spec_name"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# ============================================================================
|
|
314
|
+
# Entry Point
|
|
315
|
+
# ============================================================================
|
|
316
|
+
|
|
317
|
+
main() {
|
|
318
|
+
log_info "=== Post Living Docs Update Hook Started ==="
|
|
319
|
+
|
|
320
|
+
# Get the spec path from arguments or environment
|
|
321
|
+
local spec_path="${1:-$SPECWEAVE_UPDATED_SPEC}"
|
|
322
|
+
|
|
323
|
+
if [ -z "$spec_path" ]; then
|
|
324
|
+
log_error "No spec path provided"
|
|
325
|
+
exit 1
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# Detect external tool
|
|
329
|
+
local tool=$(detect_external_tool "$spec_path")
|
|
330
|
+
|
|
331
|
+
if [ "$tool" != "ado" ]; then
|
|
332
|
+
log_debug "Not an ADO-linked spec, skipping"
|
|
333
|
+
exit 0
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
log_info "Detected ADO integration for spec"
|
|
337
|
+
|
|
338
|
+
# Perform sync
|
|
339
|
+
sync_spec_with_ado "$spec_path"
|
|
340
|
+
|
|
341
|
+
local exit_code=$?
|
|
342
|
+
|
|
343
|
+
if [ $exit_code -eq 0 ]; then
|
|
344
|
+
log_info "=== Sync completed successfully ==="
|
|
345
|
+
else
|
|
346
|
+
log_error "=== Sync failed with exit code: $exit_code ==="
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
exit $exit_code
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Run main function
|
|
353
|
+
main "$@"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# SpecWeave Azure DevOps Sync Hook
|
|
4
|
+
# Runs after task completion to sync progress to Azure DevOps Work Items
|
|
5
|
+
#
|
|
6
|
+
# This hook is part of the specweave-ado plugin and handles:
|
|
7
|
+
# - Syncing task completion state to Azure DevOps work items
|
|
8
|
+
# - Updating ADO work item 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 .ado.item field
|
|
14
|
+
# - Azure DevOps PAT 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)] [ADO] 🔗 Azure DevOps 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)] [ADO] ℹ️ No active increment, skipping ADO 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)] [ADO] ℹ️ No metadata.json for $CURRENT_INCREMENT, skipping ADO 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 ADO work item link
|
|
85
|
+
if ! command -v jq &> /dev/null; then
|
|
86
|
+
echo "[$(date)] [ADO] ⚠️ jq not found, skipping ADO sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
87
|
+
cat <<EOF
|
|
88
|
+
{
|
|
89
|
+
"continue": true
|
|
90
|
+
}
|
|
91
|
+
EOF
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
ADO_ITEM=$(jq -r '.ado.item // empty' "$METADATA_FILE" 2>/dev/null)
|
|
96
|
+
|
|
97
|
+
if [ -z "$ADO_ITEM" ]; then
|
|
98
|
+
echo "[$(date)] [ADO] ℹ️ No Azure DevOps work item 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)] [ADO] ⚠️ Node.js not found, skipping ADO 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 ADO sync script
|
|
119
|
+
if [ ! -f "dist/commands/ado-sync.js" ]; then
|
|
120
|
+
echo "[$(date)] [ADO] ⚠️ ado-sync.js not found, skipping ADO 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
|
+
# AZURE DEVOPS SYNC LOGIC
|
|
131
|
+
# ============================================================================
|
|
132
|
+
|
|
133
|
+
echo "[$(date)] [ADO] 🔄 Syncing to Azure DevOps work item $ADO_ITEM" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
134
|
+
|
|
135
|
+
# Run ADO sync command (non-blocking)
|
|
136
|
+
node dist/commands/ado-sync.js "$CURRENT_INCREMENT" 2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
|
|
137
|
+
echo "[$(date)] [ADO] ⚠️ Failed to sync to Azure DevOps (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
echo "[$(date)] [ADO] ✅ Azure DevOps sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
141
|
+
|
|
142
|
+
# ============================================================================
|
|
143
|
+
# SPEC COMMIT SYNC (NEW!)
|
|
144
|
+
# ============================================================================
|
|
145
|
+
|
|
146
|
+
echo "[$(date)] [ADO] 🔗 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)] [ADO] 🚀 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 ado \
|
|
155
|
+
2>&1 | tee -a "$DEBUG_LOG" >/dev/null || {
|
|
156
|
+
echo "[$(date)] [ADO] ⚠️ Spec commit sync failed (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
echo "[$(date)] [ADO] ✅ Spec commit sync complete" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
160
|
+
else
|
|
161
|
+
echo "[$(date)] [ADO] ℹ️ 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
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
};
|