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.
Files changed (36) hide show
  1. package/README.md +95 -378
  2. package/dist/src/cli/commands/init.d.ts.map +1 -1
  3. package/dist/src/cli/commands/init.js +8 -26
  4. package/dist/src/cli/commands/init.js.map +1 -1
  5. package/dist/src/core/living-docs/cross-project-sync.d.ts +2 -0
  6. package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
  7. package/dist/src/core/living-docs/cross-project-sync.js +14 -4
  8. package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
  9. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  10. package/dist/src/core/living-docs/living-docs-sync.js +6 -2
  11. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  12. package/package.json +3 -3
  13. package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
  14. package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
  15. package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
  16. package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
  17. package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
  18. package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
  19. package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
  20. package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
  21. package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
  22. package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
  23. package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
  24. package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
  25. package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
  26. package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
  27. package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
  28. package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
  29. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  30. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1262 -0
  31. package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
  32. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  33. package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
  34. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +134 -0
  35. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1254 -0
  36. 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
+ };