specweave 1.0.356 → 1.0.357

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 (136) hide show
  1. package/bin/specweave.js +21 -1
  2. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -1
  3. package/dist/plugins/specweave-github/lib/duplicate-detector.js +18 -2
  4. package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.js +10 -0
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  8. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +17 -0
  9. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
  10. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +85 -0
  11. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
  12. package/dist/src/adapters/agents-md-generator.js +1 -1
  13. package/dist/src/adapters/agents-md-generator.js.map +1 -1
  14. package/dist/src/adapters/claude-md-generator.js +2 -2
  15. package/dist/src/adapters/claude-md-generator.js.map +1 -1
  16. package/dist/src/cli/commands/check-discipline.d.ts.map +1 -1
  17. package/dist/src/cli/commands/check-discipline.js +2 -1
  18. package/dist/src/cli/commands/check-discipline.js.map +1 -1
  19. package/dist/src/cli/commands/create-increment.d.ts.map +1 -1
  20. package/dist/src/cli/commands/create-increment.js +4 -1
  21. package/dist/src/cli/commands/create-increment.js.map +1 -1
  22. package/dist/src/cli/commands/health.d.ts +34 -0
  23. package/dist/src/cli/commands/health.d.ts.map +1 -0
  24. package/dist/src/cli/commands/health.js +349 -0
  25. package/dist/src/cli/commands/health.js.map +1 -0
  26. package/dist/src/cli/commands/migrate-to-umbrella.d.ts.map +1 -1
  27. package/dist/src/cli/commands/migrate-to-umbrella.js +38 -0
  28. package/dist/src/cli/commands/migrate-to-umbrella.js.map +1 -1
  29. package/dist/src/cli/commands/save.js +1 -1
  30. package/dist/src/cli/commands/save.js.map +1 -1
  31. package/dist/src/cli/commands/sync-living-docs.d.ts.map +1 -1
  32. package/dist/src/cli/commands/sync-living-docs.js +2 -1
  33. package/dist/src/cli/commands/sync-living-docs.js.map +1 -1
  34. package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
  35. package/dist/src/cli/commands/sync-progress.js +208 -8
  36. package/dist/src/cli/commands/sync-progress.js.map +1 -1
  37. package/dist/src/core/ac-progress-sync.d.ts.map +1 -1
  38. package/dist/src/core/ac-progress-sync.js +13 -4
  39. package/dist/src/core/ac-progress-sync.js.map +1 -1
  40. package/dist/src/core/config/types.d.ts +33 -0
  41. package/dist/src/core/config/types.d.ts.map +1 -1
  42. package/dist/src/core/config/types.js.map +1 -1
  43. package/dist/src/core/doctor/checkers/increments-checker.js +2 -2
  44. package/dist/src/core/doctor/checkers/increments-checker.js.map +1 -1
  45. package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts.map +1 -1
  46. package/dist/src/core/hooks/LifecycleHookDispatcher.js +6 -1
  47. package/dist/src/core/hooks/LifecycleHookDispatcher.js.map +1 -1
  48. package/dist/src/core/increment/completion-validator.d.ts.map +1 -1
  49. package/dist/src/core/increment/completion-validator.js +8 -7
  50. package/dist/src/core/increment/completion-validator.js.map +1 -1
  51. package/dist/src/core/increment/increment-reopener.d.ts.map +1 -1
  52. package/dist/src/core/increment/increment-reopener.js +5 -4
  53. package/dist/src/core/increment/increment-reopener.js.map +1 -1
  54. package/dist/src/core/increment/metadata-manager.d.ts +5 -6
  55. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  56. package/dist/src/core/increment/metadata-manager.js +12 -13
  57. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  58. package/dist/src/core/increment/spec-frontmatter-updater.d.ts.map +1 -1
  59. package/dist/src/core/increment/spec-frontmatter-updater.js +3 -2
  60. package/dist/src/core/increment/spec-frontmatter-updater.js.map +1 -1
  61. package/dist/src/core/increment/status-auto-transition.d.ts.map +1 -1
  62. package/dist/src/core/increment/status-auto-transition.js +5 -4
  63. package/dist/src/core/increment/status-auto-transition.js.map +1 -1
  64. package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
  65. package/dist/src/core/increment/status-change-sync-trigger.js +3 -2
  66. package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
  67. package/dist/src/core/increment/status-commands.d.ts.map +1 -1
  68. package/dist/src/core/increment/status-commands.js +4 -3
  69. package/dist/src/core/increment/status-commands.js.map +1 -1
  70. package/dist/src/core/increment/template-creator.d.ts.map +1 -1
  71. package/dist/src/core/increment/template-creator.js +49 -5
  72. package/dist/src/core/increment/template-creator.js.map +1 -1
  73. package/dist/src/core/lazy-loading/llm-plugin-detector.js +1 -1
  74. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  75. package/dist/src/core/living-docs/living-docs-sync.js +16 -6
  76. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  77. package/dist/src/core/migration/consolidation-engine.d.ts +59 -0
  78. package/dist/src/core/migration/consolidation-engine.d.ts.map +1 -0
  79. package/dist/src/core/migration/consolidation-engine.js +177 -0
  80. package/dist/src/core/migration/consolidation-engine.js.map +1 -0
  81. package/dist/src/core/migration/types.d.ts +2 -0
  82. package/dist/src/core/migration/types.d.ts.map +1 -1
  83. package/dist/src/core/project/project-resolution.d.ts.map +1 -1
  84. package/dist/src/core/project/project-resolution.js +20 -2
  85. package/dist/src/core/project/project-resolution.js.map +1 -1
  86. package/dist/src/generators/spec/spec-parser.js +2 -2
  87. package/dist/src/generators/spec/spec-parser.js.map +1 -1
  88. package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
  89. package/dist/src/integrations/jira/jira-client.js +83 -30
  90. package/dist/src/integrations/jira/jira-client.js.map +1 -1
  91. package/dist/src/sync/external-issue-auto-creator.d.ts +6 -2
  92. package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
  93. package/dist/src/sync/external-issue-auto-creator.js +75 -19
  94. package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
  95. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  96. package/dist/src/sync/sync-coordinator.js +20 -4
  97. package/dist/src/sync/sync-coordinator.js.map +1 -1
  98. package/dist/src/sync/sync-target-resolver.d.ts +36 -0
  99. package/dist/src/sync/sync-target-resolver.d.ts.map +1 -0
  100. package/dist/src/sync/sync-target-resolver.js +72 -0
  101. package/dist/src/sync/sync-target-resolver.js.map +1 -0
  102. package/dist/src/utils/external-tool-drift-detector.js +4 -4
  103. package/dist/src/utils/external-tool-drift-detector.js.map +1 -1
  104. package/dist/src/utils/find-project-root.d.ts +26 -0
  105. package/dist/src/utils/find-project-root.d.ts.map +1 -1
  106. package/dist/src/utils/find-project-root.js +78 -0
  107. package/dist/src/utils/find-project-root.js.map +1 -1
  108. package/dist/src/utils/multi-project-detector.js +1 -1
  109. package/dist/src/utils/multi-project-detector.js.map +1 -1
  110. package/package.json +1 -1
  111. package/plugins/specweave/PLUGIN.md +1 -0
  112. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +5 -6
  113. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +12 -13
  114. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
  115. package/plugins/specweave/lib/vendor/core/increment/status-auto-transition.js +5 -4
  116. package/plugins/specweave/lib/vendor/core/increment/status-auto-transition.js.map +1 -1
  117. package/plugins/specweave/skills/brainstorm/SKILL.md +619 -0
  118. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
  119. package/plugins/specweave-docs/PLUGIN.md +1 -1
  120. package/plugins/specweave-github/lib/duplicate-detector.js +21 -1
  121. package/plugins/specweave-github/lib/duplicate-detector.ts +18 -2
  122. package/plugins/specweave-github/lib/github-feature-sync.js +17 -0
  123. package/plugins/specweave-github/lib/github-feature-sync.ts +11 -0
  124. package/plugins/specweave-jira/lib/jira-status-sync.js +75 -0
  125. package/plugins/specweave-jira/lib/jira-status-sync.ts +91 -1
  126. package/src/templates/CLAUDE.md.template +3 -1
  127. package/plugins/specweave/hooks/.specweave/logs/auto-iterations.log +0 -1
  128. package/plugins/specweave/hooks/.specweave/logs/auto-stop-reasons.log +0 -1
  129. package/plugins/specweave/skills/.specweave/logs/reflect/auto-reflect.log +0 -15
  130. package/plugins/specweave/skills/.specweave/logs/reflect/reflect.log +0 -3
  131. package/plugins/specweave/skills/.specweave/logs/stop-auto.log +0 -1
  132. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +0 -180
  133. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -1266
  134. package/plugins/specweave-github/lib/enhanced-github-sync.js +0 -249
  135. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +0 -150
  136. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1260
@@ -622,9 +622,26 @@ Created: ${featureData.created}`;
622
622
  * - Prevents premature closure on creation
623
623
  */
624
624
  async createUserStoryIssue(issueContent, milestoneTitle, userStoryPath) {
625
+ const repoSlug = `${this.client.getOwner()}/${this.client.getRepo()}`;
626
+ for (const label of issueContent.labels) {
627
+ await execFileNoThrow("gh", [
628
+ "label",
629
+ "create",
630
+ label,
631
+ "--repo",
632
+ repoSlug,
633
+ "--color",
634
+ "ededed",
635
+ "--description",
636
+ "SpecWeave auto-label",
637
+ "--force"
638
+ ], { env: this.getGhEnv() });
639
+ }
625
640
  const result = await execFileNoThrow("gh", [
626
641
  "issue",
627
642
  "create",
643
+ "--repo",
644
+ repoSlug,
628
645
  "--title",
629
646
  issueContent.title,
630
647
  "--body",
@@ -907,10 +907,21 @@ export class GitHubFeatureSync {
907
907
  milestoneTitle: string,
908
908
  userStoryPath: string
909
909
  ): Promise<number> {
910
+ // Step 0: Ensure all required labels exist in the target repo
911
+ const repoSlug = `${this.client.getOwner()}/${this.client.getRepo()}`;
912
+ for (const label of issueContent.labels) {
913
+ await execFileNoThrow('gh', [
914
+ 'label', 'create', label, '--repo', repoSlug,
915
+ '--color', 'ededed', '--description', 'SpecWeave auto-label', '--force'
916
+ ], { env: this.getGhEnv() });
917
+ }
918
+
910
919
  // Step 1: Create issue (always open initially - gh CLI limitation)
911
920
  const result = await execFileNoThrow('gh', [
912
921
  'issue',
913
922
  'create',
923
+ '--repo',
924
+ repoSlug,
914
925
  '--title',
915
926
  issueContent.title,
916
927
  '--body',
@@ -92,6 +92,81 @@ _Synced from SpecWeave_`;
92
92
  body
93
93
  });
94
94
  }
95
+ /**
96
+ * Post AC progress comment with proper ADF formatting and dedup.
97
+ *
98
+ * Builds native ADF with:
99
+ * - Bold header showing completion percentage
100
+ * - Bullet list with checkmark/cross emojis per AC
101
+ * - Fingerprint marker to prevent duplicate comments
102
+ *
103
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
104
+ * @param acStates - Array of AC states with id, description, completed
105
+ * @returns true if comment was posted, false if skipped (duplicate)
106
+ */
107
+ async postProgressComment(issueKey, acStates) {
108
+ const total = acStates.length;
109
+ const completed = acStates.filter((ac) => ac.completed).length;
110
+ const percentage = Math.round(completed / total * 100);
111
+ const fingerprint = `sw-progress:${completed}/${total}`;
112
+ try {
113
+ const commentsResp = await this.client.get(`/issue/${issueKey}/comment`, {
114
+ params: { orderBy: "-created", maxResults: 1 }
115
+ });
116
+ const lastComment = commentsResp.data?.comments?.[0];
117
+ if (lastComment) {
118
+ const lastText = extractAdfText(lastComment.body);
119
+ if (lastText.includes(fingerprint)) {
120
+ return false;
121
+ }
122
+ }
123
+ } catch {
124
+ }
125
+ const listItems = acStates.map((ac) => ({
126
+ type: "listItem",
127
+ content: [{
128
+ type: "paragraph",
129
+ content: [
130
+ { type: "text", text: `${ac.completed ? "\u2705" : "\u274C"} ${ac.id}: ${ac.description}` }
131
+ ]
132
+ }]
133
+ }));
134
+ const body = {
135
+ type: "doc",
136
+ version: 1,
137
+ content: [
138
+ {
139
+ type: "heading",
140
+ attrs: { level: 3 },
141
+ content: [{ type: "text", text: `Progress: ${completed}/${total} ACs (${percentage}%)` }]
142
+ },
143
+ {
144
+ type: "bulletList",
145
+ content: listItems
146
+ },
147
+ {
148
+ type: "paragraph",
149
+ content: [
150
+ { type: "text", text: `${fingerprint} | Synced from SpecWeave`, marks: [{ type: "em" }] }
151
+ ]
152
+ }
153
+ ]
154
+ };
155
+ await this.client.post(`/issue/${issueKey}/comment`, { body });
156
+ return true;
157
+ }
158
+ }
159
+ function extractAdfText(adf) {
160
+ if (!adf) return "";
161
+ if (typeof adf === "string") return adf;
162
+ let text = "";
163
+ if (adf.text) text += adf.text;
164
+ if (Array.isArray(adf.content)) {
165
+ for (const child of adf.content) {
166
+ text += extractAdfText(child);
167
+ }
168
+ }
169
+ return text;
95
170
  }
96
171
  export {
97
172
  JiraStatusSync
@@ -13,7 +13,7 @@
13
13
 
14
14
  import axios, { AxiosInstance } from 'axios';
15
15
  import { detectDeploymentType, getApiBaseUrl } from './jira-deployment-detector.js';
16
- import { toCommentBody } from './content-format-adapter.js';
16
+ import { toCommentBody, type AdfDocument, type AdfNode } from './content-format-adapter.js';
17
17
 
18
18
  /**
19
19
  * External status representation (JIRA-specific)
@@ -160,4 +160,94 @@ export class JiraStatusSync {
160
160
  body
161
161
  });
162
162
  }
163
+
164
+ /**
165
+ * Post AC progress comment with proper ADF formatting and dedup.
166
+ *
167
+ * Builds native ADF with:
168
+ * - Bold header showing completion percentage
169
+ * - Bullet list with checkmark/cross emojis per AC
170
+ * - Fingerprint marker to prevent duplicate comments
171
+ *
172
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
173
+ * @param acStates - Array of AC states with id, description, completed
174
+ * @returns true if comment was posted, false if skipped (duplicate)
175
+ */
176
+ async postProgressComment(
177
+ issueKey: string,
178
+ acStates: Array<{ id: string; description: string; completed: boolean }>,
179
+ ): Promise<boolean> {
180
+ const total = acStates.length;
181
+ const completed = acStates.filter(ac => ac.completed).length;
182
+ const percentage = Math.round((completed / total) * 100);
183
+ const fingerprint = `sw-progress:${completed}/${total}`;
184
+
185
+ // Dedup: check last comment for same fingerprint
186
+ try {
187
+ const commentsResp = await this.client.get(`/issue/${issueKey}/comment`, {
188
+ params: { orderBy: '-created', maxResults: 1 },
189
+ });
190
+ const lastComment = commentsResp.data?.comments?.[0];
191
+ if (lastComment) {
192
+ const lastText = extractAdfText(lastComment.body);
193
+ if (lastText.includes(fingerprint)) {
194
+ return false; // Already posted for this state
195
+ }
196
+ }
197
+ } catch {
198
+ // If comment fetch fails, proceed with posting
199
+ }
200
+
201
+ // Build ADF body directly — ✅ for done, ❌ for pending
202
+ const listItems: AdfNode[] = acStates.map(ac => ({
203
+ type: 'listItem',
204
+ content: [{
205
+ type: 'paragraph',
206
+ content: [
207
+ { type: 'text', text: `${ac.completed ? '\u2705' : '\u274C'} ${ac.id}: ${ac.description}` },
208
+ ],
209
+ }],
210
+ }));
211
+
212
+ const body: AdfDocument = {
213
+ type: 'doc',
214
+ version: 1,
215
+ content: [
216
+ {
217
+ type: 'heading',
218
+ attrs: { level: 3 },
219
+ content: [{ type: 'text', text: `Progress: ${completed}/${total} ACs (${percentage}%)` }],
220
+ },
221
+ {
222
+ type: 'bulletList',
223
+ content: listItems,
224
+ },
225
+ {
226
+ type: 'paragraph',
227
+ content: [
228
+ { type: 'text', text: `${fingerprint} | Synced from SpecWeave`, marks: [{ type: 'em' }] },
229
+ ],
230
+ },
231
+ ],
232
+ };
233
+
234
+ await this.client.post(`/issue/${issueKey}/comment`, { body });
235
+ return true;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Extract plain text from an ADF document (for dedup comparison).
241
+ */
242
+ function extractAdfText(adf: any): string {
243
+ if (!adf) return '';
244
+ if (typeof adf === 'string') return adf;
245
+ let text = '';
246
+ if (adf.text) text += adf.text;
247
+ if (Array.isArray(adf.content)) {
248
+ for (const child of adf.content) {
249
+ text += extractAdfText(child);
250
+ }
251
+ }
252
+ return text;
163
253
  }
@@ -64,7 +64,9 @@ SpecWeave auto-detects product descriptions and routes to `/sw:increment`:
64
64
 
65
65
  **Signals** (5+ = auto-route): Project name | Features list (3+) | Tech stack | Timeline/MVP | Problem statement | Business model
66
66
 
67
- **Opt-out phrases**: "Just brainstorm first" | "Don't plan yet" | "Quick discussion" | "Let's explore ideas"
67
+ **Opt-out phrases**: "Don't plan yet" | "Quick discussion" | "Let's explore ideas"
68
+
69
+ **Brainstorm routing**: "Just brainstorm first" | "brainstorm" | "ideate" | "what are our options" → routes to `/sw:brainstorm`
68
70
 
69
71
  **NOT opt-out phrases**: "simple" | "quick" | "basic" | "small" — these still require `/sw:increment`
70
72
 
@@ -1 +0,0 @@
1
- {"timestamp":"2026-01-04T16:11:43Z","event":"session_stop","reason":"No auto session active","details":"approve_called:main","success":false,"iteration":0,"increment":"none"}
@@ -1 +0,0 @@
1
- {"timestamp":"2026-01-04T16:11:43Z","sessionId":"unknown","reason":"No auto session active","details":"approve_called:main","success":false,"iteration":0,"increment":"none","testsRun":false,"testsPassed":0,"testsFailed":0}
@@ -1,15 +0,0 @@
1
- {
2
- "ran": false,
3
- "inputSummary": {
4
- "transcriptLines": 266
5
- },
6
- "extracted": {
7
- "skillLearnings": []
8
- },
9
- "written": {
10
- "learningsAdded": 0,
11
- "learningsSkippedDuplicate": 0
12
- },
13
- "durationMs": 1.4448749999999997,
14
- "reason": "CLAUDE.md not found in project"
15
- }
@@ -1,3 +0,0 @@
1
- [2026-02-03T22:03:06Z] [info] Starting reflection (265 lines)
2
- [2026-02-03T22:03:06Z] [info] Reflection started in background
3
- [2026-02-03T22:03:06Z] [info] Reflection completed - no learnings
@@ -1 +0,0 @@
1
- [2026-02-03T22:03:06Z] APPROVE: No increments directory
@@ -1,180 +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
- // Derive repository name from git remote or fall back to project name
134
- let repoName = project;
135
- try {
136
- const { execSync } = require("child_process");
137
- const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
138
- const match = remoteUrl.match(/\/([^/]+?)(?:\.git)?$/);
139
- if (match) repoName = match[1];
140
- } catch {
141
- // Fallback to project name if git is unavailable
142
- }
143
- return {
144
- incrementId: firstIncrement.id,
145
- tasks,
146
- tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/${repoName}?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
147
- };
148
- }
149
- async function findArchitectureDocs(rootDir, specId) {
150
- const docs = [];
151
- const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
152
- try {
153
- const adrDir = path.join(archDir, "adr");
154
- try {
155
- const adrs = await fs.readdir(adrDir);
156
- const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
157
- for (const adr of relatedAdrs) {
158
- docs.push({
159
- type: "adr",
160
- path: path.join(adrDir, adr),
161
- title: adr.replace(".md", "").replace(/-/g, " ")
162
- });
163
- }
164
- } catch {
165
- }
166
- } catch {
167
- }
168
- return docs;
169
- }
170
- async function findExistingFeature(client, specId) {
171
- try {
172
- const features = await client.queryWorkItems(`[System.Title] Contains '[${specId}]' AND [System.WorkItemType] = 'Feature'`);
173
- return features[0] || null;
174
- } catch {
175
- return null;
176
- }
177
- }
178
- export {
179
- syncSpecToAdoWithEnhancedContent
180
- };