specweave 0.32.2 → 0.32.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CLAUDE.md +39 -0
  2. package/bin/specweave.js +34 -0
  3. package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts +100 -0
  4. package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -0
  5. package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +291 -0
  6. package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -0
  7. package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts +103 -0
  8. package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -0
  9. package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +310 -0
  10. package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -0
  11. package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts +126 -0
  12. package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts.map +1 -0
  13. package/dist/plugins/specweave-jira/lib/jira-permission-gate.js +207 -0
  14. package/dist/plugins/specweave-jira/lib/jira-permission-gate.js.map +1 -0
  15. package/dist/src/adapters/codex/README.md +1 -1
  16. package/dist/src/adapters/codex/adapter.js +1 -1
  17. package/dist/src/cli/commands/archive.d.ts +2 -0
  18. package/dist/src/cli/commands/archive.d.ts.map +1 -1
  19. package/dist/src/cli/commands/archive.js +33 -0
  20. package/dist/src/cli/commands/archive.js.map +1 -1
  21. package/dist/src/cli/commands/context.d.ts +92 -0
  22. package/dist/src/cli/commands/context.d.ts.map +1 -0
  23. package/dist/src/cli/commands/context.js +205 -0
  24. package/dist/src/cli/commands/context.js.map +1 -0
  25. package/dist/src/cli/commands/init.d.ts.map +1 -1
  26. package/dist/src/cli/commands/init.js +111 -69
  27. package/dist/src/cli/commands/init.js.map +1 -1
  28. package/dist/src/cli/helpers/init/external-import.d.ts +3 -0
  29. package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
  30. package/dist/src/cli/helpers/init/external-import.js +17 -4
  31. package/dist/src/cli/helpers/init/external-import.js.map +1 -1
  32. package/dist/src/cli/helpers/init/index.d.ts +1 -0
  33. package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
  34. package/dist/src/cli/helpers/init/index.js +2 -0
  35. package/dist/src/cli/helpers/init/index.js.map +1 -1
  36. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +70 -0
  37. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -1
  38. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +214 -4
  39. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -1
  40. package/dist/src/cli/helpers/init/living-docs-preflight.d.ts +4 -0
  41. package/dist/src/cli/helpers/init/living-docs-preflight.d.ts.map +1 -1
  42. package/dist/src/cli/helpers/init/living-docs-preflight.js +34 -3
  43. package/dist/src/cli/helpers/init/living-docs-preflight.js.map +1 -1
  44. package/dist/src/cli/helpers/init/testing-config.d.ts +3 -0
  45. package/dist/src/cli/helpers/init/testing-config.d.ts.map +1 -1
  46. package/dist/src/cli/helpers/init/testing-config.js +9 -2
  47. package/dist/src/cli/helpers/init/testing-config.js.map +1 -1
  48. package/dist/src/cli/helpers/init/translation-config.d.ts +3 -0
  49. package/dist/src/cli/helpers/init/translation-config.d.ts.map +1 -1
  50. package/dist/src/cli/helpers/init/translation-config.js +21 -4
  51. package/dist/src/cli/helpers/init/translation-config.js.map +1 -1
  52. package/dist/src/cli/helpers/init/wizard-navigation.d.ts +45 -0
  53. package/dist/src/cli/helpers/init/wizard-navigation.d.ts.map +1 -0
  54. package/dist/src/cli/helpers/init/wizard-navigation.js +97 -0
  55. package/dist/src/cli/helpers/init/wizard-navigation.js.map +1 -0
  56. package/dist/src/core/increment/increment-archiver.d.ts +25 -4
  57. package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
  58. package/dist/src/core/increment/increment-archiver.js +64 -20
  59. package/dist/src/core/increment/increment-archiver.js.map +1 -1
  60. package/dist/src/core/increment/increment-utils.d.ts +65 -0
  61. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  62. package/dist/src/core/increment/increment-utils.js +114 -0
  63. package/dist/src/core/increment/increment-utils.js.map +1 -1
  64. package/dist/src/core/living-docs/feature-archiver.d.ts +4 -0
  65. package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
  66. package/dist/src/core/living-docs/feature-archiver.js +32 -10
  67. package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
  68. package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -1
  69. package/dist/src/core/living-docs/feature-id-manager.js +7 -3
  70. package/dist/src/core/living-docs/feature-id-manager.js.map +1 -1
  71. package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts +38 -0
  72. package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts.map +1 -0
  73. package/dist/src/core/living-docs/governance/ecosystem-detector.js +325 -0
  74. package/dist/src/core/living-docs/governance/ecosystem-detector.js.map +1 -0
  75. package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts +74 -0
  76. package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts.map +1 -0
  77. package/dist/src/core/living-docs/governance/frontend-standards-parser.js +366 -0
  78. package/dist/src/core/living-docs/governance/frontend-standards-parser.js.map +1 -0
  79. package/dist/src/core/living-docs/governance/go-standards-parser.d.ts +64 -0
  80. package/dist/src/core/living-docs/governance/go-standards-parser.d.ts.map +1 -0
  81. package/dist/src/core/living-docs/governance/go-standards-parser.js +229 -0
  82. package/dist/src/core/living-docs/governance/go-standards-parser.js.map +1 -0
  83. package/dist/src/core/living-docs/governance/index.d.ts +50 -0
  84. package/dist/src/core/living-docs/governance/index.d.ts.map +1 -0
  85. package/dist/src/core/living-docs/governance/index.js +56 -0
  86. package/dist/src/core/living-docs/governance/index.js.map +1 -0
  87. package/dist/src/core/living-docs/governance/java-standards-parser.d.ts +89 -0
  88. package/dist/src/core/living-docs/governance/java-standards-parser.d.ts.map +1 -0
  89. package/dist/src/core/living-docs/governance/java-standards-parser.js +356 -0
  90. package/dist/src/core/living-docs/governance/java-standards-parser.js.map +1 -0
  91. package/dist/src/core/living-docs/governance/python-standards-parser.d.ts +83 -0
  92. package/dist/src/core/living-docs/governance/python-standards-parser.d.ts.map +1 -0
  93. package/dist/src/core/living-docs/governance/python-standards-parser.js +347 -0
  94. package/dist/src/core/living-docs/governance/python-standards-parser.js.map +1 -0
  95. package/dist/src/core/living-docs/governance/standards-generator.d.ts +38 -0
  96. package/dist/src/core/living-docs/governance/standards-generator.d.ts.map +1 -0
  97. package/dist/src/core/living-docs/governance/standards-generator.js +476 -0
  98. package/dist/src/core/living-docs/governance/standards-generator.js.map +1 -0
  99. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
  100. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +54 -2
  101. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
  102. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts +5 -1
  103. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
  104. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +358 -30
  105. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
  106. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +44 -0
  107. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
  108. package/dist/src/core/living-docs/living-docs-sync.d.ts +6 -3
  109. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  110. package/dist/src/core/living-docs/living-docs-sync.js +17 -8
  111. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  112. package/dist/src/core/living-docs/module-analyzer.d.ts +22 -0
  113. package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
  114. package/dist/src/core/living-docs/module-analyzer.js +123 -19
  115. package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
  116. package/dist/src/core/llm/provider-factory.js +2 -2
  117. package/dist/src/core/llm/provider-factory.js.map +1 -1
  118. package/dist/src/core/llm/providers/anthropic-provider.js +1 -1
  119. package/dist/src/core/llm/providers/bedrock-provider.d.ts.map +1 -1
  120. package/dist/src/core/llm/providers/bedrock-provider.js +8 -4
  121. package/dist/src/core/llm/providers/bedrock-provider.js.map +1 -1
  122. package/dist/src/importers/jira-importer.d.ts +14 -0
  123. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  124. package/dist/src/importers/jira-importer.js +75 -0
  125. package/dist/src/importers/jira-importer.js.map +1 -1
  126. package/dist/src/integrations/jira/jira-token-provider.d.ts +93 -0
  127. package/dist/src/integrations/jira/jira-token-provider.d.ts.map +1 -0
  128. package/dist/src/integrations/jira/jira-token-provider.js +160 -0
  129. package/dist/src/integrations/jira/jira-token-provider.js.map +1 -0
  130. package/dist/src/sync/ado-reconciler.d.ts +92 -0
  131. package/dist/src/sync/ado-reconciler.d.ts.map +1 -0
  132. package/dist/src/sync/ado-reconciler.js +335 -0
  133. package/dist/src/sync/ado-reconciler.js.map +1 -0
  134. package/dist/src/sync/jira-reconciler.d.ts +106 -0
  135. package/dist/src/sync/jira-reconciler.d.ts.map +1 -0
  136. package/dist/src/sync/jira-reconciler.js +405 -0
  137. package/dist/src/sync/jira-reconciler.js.map +1 -0
  138. package/dist/src/types/model-selection.d.ts +6 -4
  139. package/dist/src/types/model-selection.d.ts.map +1 -1
  140. package/dist/src/types/model-selection.js +3 -1
  141. package/dist/src/types/model-selection.js.map +1 -1
  142. package/dist/src/utils/external-tool-drift-detector.d.ts +1 -1
  143. package/dist/src/utils/external-tool-drift-detector.d.ts.map +1 -1
  144. package/dist/src/utils/external-tool-drift-detector.js +5 -4
  145. package/dist/src/utils/external-tool-drift-detector.js.map +1 -1
  146. package/dist/src/utils/feature-id-derivation.d.ts +8 -3
  147. package/dist/src/utils/feature-id-derivation.d.ts.map +1 -1
  148. package/dist/src/utils/feature-id-derivation.js +14 -6
  149. package/dist/src/utils/feature-id-derivation.js.map +1 -1
  150. package/dist/src/utils/model-selection.d.ts +3 -4
  151. package/dist/src/utils/model-selection.d.ts.map +1 -1
  152. package/dist/src/utils/model-selection.js +3 -4
  153. package/dist/src/utils/model-selection.js.map +1 -1
  154. package/package.json +1 -1
  155. package/plugins/specweave/agents/code-standards-detective/AGENT.md +48 -0
  156. package/plugins/specweave/commands/specweave-costs.md +4 -4
  157. package/plugins/specweave/commands/specweave-do.md +9 -9
  158. package/plugins/specweave/commands/specweave-done.md +13 -0
  159. package/plugins/specweave/commands/specweave-validate.md +27 -1
  160. package/plugins/specweave/hooks/hooks.json +10 -0
  161. package/plugins/specweave/hooks/spec-project-validator.sh +80 -25
  162. package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +135 -0
  163. package/plugins/specweave/scripts/read-costs.sh +3 -3
  164. package/plugins/specweave/skills/code-standards-analyzer/SKILL.md +58 -6
  165. package/plugins/specweave/skills/increment-planner/SKILL.md +56 -25
  166. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +4 -2
  167. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +2 -1
  168. package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +1 -1
  169. package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +1 -1
  170. package/plugins/specweave-ado/commands/cleanup-duplicates.md +212 -0
  171. package/plugins/specweave-ado/commands/reconcile.md +120 -0
  172. package/plugins/specweave-ado/lib/ado-duplicate-detector.js +279 -0
  173. package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +407 -0
  174. package/plugins/specweave-github/agents/github-manager/AGENT.md +2 -2
  175. package/plugins/specweave-infrastructure/skills/hetzner-provisioner/README.md +1 -1
  176. package/plugins/specweave-jira/agents/jira-manager/AGENT.md +1 -1
  177. package/plugins/specweave-jira/agents/jira-multi-project-mapper/AGENT.md +530 -0
  178. package/plugins/specweave-jira/agents/jira-sync-judge/AGENT.md +438 -0
  179. package/plugins/specweave-jira/commands/cleanup-duplicates.md +219 -0
  180. package/plugins/specweave-jira/commands/close.md +297 -0
  181. package/plugins/specweave-jira/commands/create.md +198 -0
  182. package/plugins/specweave-jira/commands/reconcile.md +123 -0
  183. package/plugins/specweave-jira/commands/status.md +215 -0
  184. package/plugins/specweave-jira/lib/jira-duplicate-detector.js +296 -0
  185. package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +434 -0
  186. package/plugins/specweave-jira/lib/jira-permission-gate.js +160 -0
  187. package/plugins/specweave-jira/lib/jira-permission-gate.ts +276 -0
  188. package/plugins/specweave-jira/lib/jira-profile-resolver.js +222 -0
  189. package/plugins/specweave-jira/lib/jira-profile-resolver.ts +427 -0
  190. package/plugins/specweave-jira/reference/jira-specweave-mapping.md +16 -11
  191. package/plugins/specweave-release/commands/specweave-release-npm.md +140 -14
@@ -0,0 +1,279 @@
1
+ import { consoleLogger } from "../../../src/utils/logger.js";
2
+ class AdoDuplicateDetector {
3
+ constructor(options = {}) {
4
+ this.org = options.org || process.env.AZURE_DEVOPS_ORG || "";
5
+ this.project = options.project || process.env.AZURE_DEVOPS_PROJECT || "";
6
+ this.pat = options.pat || process.env.AZURE_DEVOPS_PAT || process.env.AZURE_DEVOPS_TOKEN || "";
7
+ this.logger = options.logger || consoleLogger;
8
+ }
9
+ /**
10
+ * Phase 1: Check if work item exists before creating
11
+ */
12
+ async checkBeforeCreate(titlePattern, incrementId) {
13
+ try {
14
+ const items = await this.searchWorkItems(titlePattern);
15
+ if (items.length > 0) {
16
+ return {
17
+ found: true,
18
+ existingItem: items[0],
19
+ count: items.length
20
+ };
21
+ }
22
+ return { found: false, count: 0 };
23
+ } catch (error) {
24
+ this.logger.log(`\u26A0\uFE0F Detection check failed: ${error.message}`);
25
+ return { found: false, count: 0 };
26
+ }
27
+ }
28
+ /**
29
+ * Phase 2: Verify count after creation
30
+ */
31
+ async verifyAfterCreate(titlePattern, expectedCount = 1) {
32
+ try {
33
+ const items = await this.searchWorkItems(titlePattern);
34
+ if (items.length > expectedCount) {
35
+ const sorted = items.sort(
36
+ (a, b) => a.id - b.id
37
+ // Sort by ID (lowest = oldest)
38
+ );
39
+ return {
40
+ success: false,
41
+ expectedCount,
42
+ actualCount: items.length,
43
+ duplicates: sorted.slice(expectedCount)
44
+ // All items after expected count
45
+ };
46
+ }
47
+ return {
48
+ success: true,
49
+ expectedCount,
50
+ actualCount: items.length,
51
+ duplicates: []
52
+ };
53
+ } catch (error) {
54
+ this.logger.log(`\u26A0\uFE0F Verification check failed: ${error.message}`);
55
+ return {
56
+ success: true,
57
+ // Assume success on error
58
+ expectedCount,
59
+ actualCount: expectedCount,
60
+ duplicates: []
61
+ };
62
+ }
63
+ }
64
+ /**
65
+ * Phase 3: Auto-close duplicates
66
+ */
67
+ async closeDuplicates(duplicates, keepItemId) {
68
+ const result = {
69
+ closedCount: 0,
70
+ keptCount: 1,
71
+ errors: []
72
+ };
73
+ for (const item of duplicates) {
74
+ try {
75
+ await this.closeWorkItem(item.id, keepItemId);
76
+ result.closedCount++;
77
+ this.logger.log(` \u2705 Closed #${item.id} (duplicate of #${keepItemId})`);
78
+ } catch (error) {
79
+ result.errors.push(`#${item.id}: ${error.message}`);
80
+ this.logger.log(` \u274C Failed to close #${item.id}: ${error.message}`);
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+ /**
86
+ * Full cleanup: Find and close all duplicates for a feature
87
+ */
88
+ async cleanupFeatureDuplicates(featureId, dryRun = false) {
89
+ const searchPattern = `[${featureId}]`;
90
+ const items = await this.searchWorkItems(searchPattern);
91
+ this.logger.log(`
92
+ \u{1F50D} Scanning for duplicates in Feature ${featureId}...`);
93
+ this.logger.log(` Found ${items.length} total work items`);
94
+ const groups = this.groupByTitle(items);
95
+ const duplicateGroups = groups.filter((g) => g.duplicates.length > 0);
96
+ if (duplicateGroups.length === 0) {
97
+ this.logger.log(` \u2705 No duplicates found!`);
98
+ return {
99
+ groups: [],
100
+ totalItems: items.length,
101
+ duplicateCount: 0,
102
+ closedCount: 0
103
+ };
104
+ }
105
+ this.logger.log(` Detected ${duplicateGroups.length} duplicate groups:
106
+ `);
107
+ for (let i = 0; i < duplicateGroups.length; i++) {
108
+ const group = duplicateGroups[i];
109
+ this.logger.log(` \u{1F4CB} Group ${i + 1}: "${group.title.substring(0, 50)}..."`);
110
+ this.logger.log(` - #${group.keepItem.id} (KEEP) - Created ${group.keepItem.createdDate.split("T")[0]}`);
111
+ for (const dup of group.duplicates) {
112
+ this.logger.log(` - #${dup.id} (CLOSE) - Created ${dup.createdDate.split("T")[0]} - DUPLICATE`);
113
+ }
114
+ this.logger.log("");
115
+ }
116
+ const totalDuplicates = duplicateGroups.reduce((sum, g) => sum + g.duplicates.length, 0);
117
+ if (dryRun) {
118
+ this.logger.log(`
119
+ \u2705 Dry run complete!`);
120
+ this.logger.log(` Total work items: ${items.length}`);
121
+ this.logger.log(` Duplicate groups: ${duplicateGroups.length}`);
122
+ this.logger.log(` Work items to close: ${totalDuplicates}`);
123
+ this.logger.log(`
124
+ \u26A0\uFE0F This was a DRY RUN - no changes made.`);
125
+ return {
126
+ groups: duplicateGroups,
127
+ totalItems: items.length,
128
+ duplicateCount: totalDuplicates,
129
+ closedCount: 0
130
+ };
131
+ }
132
+ let closedCount = 0;
133
+ this.logger.log(`\u{1F5D1}\uFE0F Closing duplicates...`);
134
+ for (const group of duplicateGroups) {
135
+ const result = await this.closeDuplicates(group.duplicates, group.keepItem.id);
136
+ closedCount += result.closedCount;
137
+ }
138
+ this.logger.log(`
139
+ \u2705 Cleanup complete!`);
140
+ this.logger.log(` Closed: ${closedCount} duplicates`);
141
+ this.logger.log(` Kept: ${duplicateGroups.length} original work items`);
142
+ return {
143
+ groups: duplicateGroups,
144
+ totalItems: items.length,
145
+ duplicateCount: totalDuplicates,
146
+ closedCount
147
+ };
148
+ }
149
+ /**
150
+ * Group work items by title
151
+ */
152
+ groupByTitle(items) {
153
+ const titleMap = /* @__PURE__ */ new Map();
154
+ for (const item of items) {
155
+ const existing = titleMap.get(item.title) || [];
156
+ existing.push(item);
157
+ titleMap.set(item.title, existing);
158
+ }
159
+ const groups = [];
160
+ for (const [title, groupItems] of titleMap) {
161
+ const sorted = groupItems.sort((a, b) => a.id - b.id);
162
+ groups.push({
163
+ title,
164
+ items: sorted,
165
+ keepItem: sorted[0],
166
+ duplicates: sorted.slice(1)
167
+ });
168
+ }
169
+ return groups;
170
+ }
171
+ /**
172
+ * Search for work items using WIQL
173
+ */
174
+ async searchWorkItems(titlePattern) {
175
+ if (!this.org || !this.pat) {
176
+ throw new Error("ADO credentials not configured");
177
+ }
178
+ const wiql = {
179
+ query: `SELECT [System.Id], [System.Title], [System.State], [System.CreatedDate]
180
+ FROM WorkItems
181
+ WHERE [System.TeamProject] = '${this.project}'
182
+ AND [System.Title] CONTAINS '${titlePattern}'
183
+ ORDER BY [System.Id] ASC`
184
+ };
185
+ const auth = Buffer.from(`:${this.pat}`).toString("base64");
186
+ const url = `https://dev.azure.com/${this.org}/${this.project}/_apis/wit/wiql?api-version=7.1`;
187
+ const response = await fetch(url, {
188
+ method: "POST",
189
+ headers: {
190
+ Authorization: `Basic ${auth}`,
191
+ "Content-Type": "application/json"
192
+ },
193
+ body: JSON.stringify(wiql)
194
+ });
195
+ if (!response.ok) {
196
+ throw new Error(`WIQL query failed: ${response.status}`);
197
+ }
198
+ const data = await response.json();
199
+ const workItemIds = data.workItems?.map((wi) => wi.id) || [];
200
+ if (workItemIds.length === 0) {
201
+ return [];
202
+ }
203
+ return this.getWorkItemDetails(workItemIds);
204
+ }
205
+ /**
206
+ * Get work item details by IDs
207
+ */
208
+ async getWorkItemDetails(ids) {
209
+ if (ids.length === 0) return [];
210
+ const auth = Buffer.from(`:${this.pat}`).toString("base64");
211
+ const url = `https://dev.azure.com/${this.org}/_apis/wit/workitems?ids=${ids.join(",")}&api-version=7.1`;
212
+ const response = await fetch(url, {
213
+ headers: {
214
+ Authorization: `Basic ${auth}`,
215
+ "Content-Type": "application/json"
216
+ }
217
+ });
218
+ if (!response.ok) {
219
+ throw new Error(`Failed to get work items: ${response.status}`);
220
+ }
221
+ const data = await response.json();
222
+ return (data.value || []).map((wi) => ({
223
+ id: wi.id,
224
+ title: wi.fields["System.Title"],
225
+ state: wi.fields["System.State"],
226
+ createdDate: wi.fields["System.CreatedDate"],
227
+ url: wi._links?.html?.href
228
+ }));
229
+ }
230
+ /**
231
+ * Close a work item with duplicate comment
232
+ */
233
+ async closeWorkItem(workItemId, originalId) {
234
+ const auth = Buffer.from(`:${this.pat}`).toString("base64");
235
+ const url = `https://dev.azure.com/${this.org}/_apis/wit/workitems/${workItemId}?api-version=7.1`;
236
+ const comment = `## Duplicate of #${originalId}
237
+
238
+ This work item was automatically closed by SpecWeave cleanup because it is a duplicate.
239
+
240
+ The original work item (#${originalId}) contains the same content and should be used for tracking instead.
241
+
242
+ ---
243
+ \u{1F916} Auto-closed by SpecWeave Duplicate Cleanup`;
244
+ const operations = [
245
+ {
246
+ op: "add",
247
+ path: "/fields/System.State",
248
+ value: "Closed"
249
+ },
250
+ {
251
+ op: "add",
252
+ path: "/fields/System.History",
253
+ value: comment
254
+ }
255
+ ];
256
+ const response = await fetch(url, {
257
+ method: "PATCH",
258
+ headers: {
259
+ Authorization: `Basic ${auth}`,
260
+ "Content-Type": "application/json-patch+json"
261
+ },
262
+ body: JSON.stringify(operations)
263
+ });
264
+ if (!response.ok) {
265
+ const error = await response.text();
266
+ throw new Error(`Failed to close work item: ${response.status} - ${error}`);
267
+ }
268
+ }
269
+ }
270
+ async function cleanupAdoDuplicates(featureId, dryRun = false) {
271
+ const detector = new AdoDuplicateDetector();
272
+ return detector.cleanupFeatureDuplicates(featureId, dryRun);
273
+ }
274
+ var ado_duplicate_detector_default = AdoDuplicateDetector;
275
+ export {
276
+ AdoDuplicateDetector,
277
+ cleanupAdoDuplicates,
278
+ ado_duplicate_detector_default as default
279
+ };
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Azure DevOps Duplicate Detector (v0.33.0)
3
+ *
4
+ * Implements 3-phase duplicate protection for ADO work items:
5
+ * 1. Detection: Check before create
6
+ * 2. Verification: Count check after create
7
+ * 3. Reflection: Auto-close duplicates
8
+ *
9
+ * Mirrors the GitHub DuplicateDetector pattern for consistency.
10
+ */
11
+
12
+ import { Logger, consoleLogger } from '../../../src/utils/logger.js';
13
+
14
+ export interface AdoWorkItem {
15
+ id: number;
16
+ title: string;
17
+ state: string;
18
+ createdDate: string;
19
+ url?: string;
20
+ }
21
+
22
+ export interface DuplicateGroup {
23
+ title: string;
24
+ items: AdoWorkItem[];
25
+ keepItem: AdoWorkItem;
26
+ duplicates: AdoWorkItem[];
27
+ }
28
+
29
+ export interface DetectionResult {
30
+ found: boolean;
31
+ existingItem?: AdoWorkItem;
32
+ count: number;
33
+ }
34
+
35
+ export interface VerificationResult {
36
+ success: boolean;
37
+ expectedCount: number;
38
+ actualCount: number;
39
+ duplicates: AdoWorkItem[];
40
+ }
41
+
42
+ export interface CleanupResult {
43
+ closedCount: number;
44
+ keptCount: number;
45
+ errors: string[];
46
+ }
47
+
48
+ export class AdoDuplicateDetector {
49
+ private org: string;
50
+ private project: string;
51
+ private pat: string;
52
+ private logger: Logger;
53
+
54
+ constructor(options: {
55
+ org?: string;
56
+ project?: string;
57
+ pat?: string;
58
+ logger?: Logger;
59
+ } = {}) {
60
+ this.org = options.org || process.env.AZURE_DEVOPS_ORG || '';
61
+ this.project = options.project || process.env.AZURE_DEVOPS_PROJECT || '';
62
+ this.pat = options.pat || process.env.AZURE_DEVOPS_PAT || process.env.AZURE_DEVOPS_TOKEN || '';
63
+ this.logger = options.logger || consoleLogger;
64
+ }
65
+
66
+ /**
67
+ * Phase 1: Check if work item exists before creating
68
+ */
69
+ async checkBeforeCreate(
70
+ titlePattern: string,
71
+ incrementId?: string
72
+ ): Promise<DetectionResult> {
73
+ try {
74
+ const items = await this.searchWorkItems(titlePattern);
75
+
76
+ if (items.length > 0) {
77
+ return {
78
+ found: true,
79
+ existingItem: items[0],
80
+ count: items.length,
81
+ };
82
+ }
83
+
84
+ return { found: false, count: 0 };
85
+ } catch (error: any) {
86
+ this.logger.log(`⚠️ Detection check failed: ${error.message}`);
87
+ // Graceful degradation - continue anyway
88
+ return { found: false, count: 0 };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Phase 2: Verify count after creation
94
+ */
95
+ async verifyAfterCreate(
96
+ titlePattern: string,
97
+ expectedCount: number = 1
98
+ ): Promise<VerificationResult> {
99
+ try {
100
+ const items = await this.searchWorkItems(titlePattern);
101
+
102
+ if (items.length > expectedCount) {
103
+ // Duplicates detected!
104
+ const sorted = items.sort(
105
+ (a, b) => a.id - b.id // Sort by ID (lowest = oldest)
106
+ );
107
+
108
+ return {
109
+ success: false,
110
+ expectedCount,
111
+ actualCount: items.length,
112
+ duplicates: sorted.slice(expectedCount), // All items after expected count
113
+ };
114
+ }
115
+
116
+ return {
117
+ success: true,
118
+ expectedCount,
119
+ actualCount: items.length,
120
+ duplicates: [],
121
+ };
122
+ } catch (error: any) {
123
+ this.logger.log(`⚠️ Verification check failed: ${error.message}`);
124
+ return {
125
+ success: true, // Assume success on error
126
+ expectedCount,
127
+ actualCount: expectedCount,
128
+ duplicates: [],
129
+ };
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Phase 3: Auto-close duplicates
135
+ */
136
+ async closeDuplicates(
137
+ duplicates: AdoWorkItem[],
138
+ keepItemId: number
139
+ ): Promise<CleanupResult> {
140
+ const result: CleanupResult = {
141
+ closedCount: 0,
142
+ keptCount: 1,
143
+ errors: [],
144
+ };
145
+
146
+ for (const item of duplicates) {
147
+ try {
148
+ await this.closeWorkItem(item.id, keepItemId);
149
+ result.closedCount++;
150
+ this.logger.log(` ✅ Closed #${item.id} (duplicate of #${keepItemId})`);
151
+ } catch (error: any) {
152
+ result.errors.push(`#${item.id}: ${error.message}`);
153
+ this.logger.log(` ❌ Failed to close #${item.id}: ${error.message}`);
154
+ }
155
+ }
156
+
157
+ return result;
158
+ }
159
+
160
+ /**
161
+ * Full cleanup: Find and close all duplicates for a feature
162
+ */
163
+ async cleanupFeatureDuplicates(
164
+ featureId: string,
165
+ dryRun: boolean = false
166
+ ): Promise<{
167
+ groups: DuplicateGroup[];
168
+ totalItems: number;
169
+ duplicateCount: number;
170
+ closedCount: number;
171
+ }> {
172
+ // 1. Search for all work items with feature ID
173
+ const searchPattern = `[${featureId}]`;
174
+ const items = await this.searchWorkItems(searchPattern);
175
+
176
+ this.logger.log(`\n🔍 Scanning for duplicates in Feature ${featureId}...`);
177
+ this.logger.log(` Found ${items.length} total work items`);
178
+
179
+ // 2. Group by title
180
+ const groups = this.groupByTitle(items);
181
+ const duplicateGroups = groups.filter(g => g.duplicates.length > 0);
182
+
183
+ if (duplicateGroups.length === 0) {
184
+ this.logger.log(` ✅ No duplicates found!`);
185
+ return {
186
+ groups: [],
187
+ totalItems: items.length,
188
+ duplicateCount: 0,
189
+ closedCount: 0,
190
+ };
191
+ }
192
+
193
+ this.logger.log(` Detected ${duplicateGroups.length} duplicate groups:\n`);
194
+
195
+ // 3. Display groups
196
+ for (let i = 0; i < duplicateGroups.length; i++) {
197
+ const group = duplicateGroups[i];
198
+ this.logger.log(` 📋 Group ${i + 1}: "${group.title.substring(0, 50)}..."`);
199
+ this.logger.log(` - #${group.keepItem.id} (KEEP) - Created ${group.keepItem.createdDate.split('T')[0]}`);
200
+ for (const dup of group.duplicates) {
201
+ this.logger.log(` - #${dup.id} (CLOSE) - Created ${dup.createdDate.split('T')[0]} - DUPLICATE`);
202
+ }
203
+ this.logger.log('');
204
+ }
205
+
206
+ const totalDuplicates = duplicateGroups.reduce((sum, g) => sum + g.duplicates.length, 0);
207
+
208
+ if (dryRun) {
209
+ this.logger.log(`\n✅ Dry run complete!`);
210
+ this.logger.log(` Total work items: ${items.length}`);
211
+ this.logger.log(` Duplicate groups: ${duplicateGroups.length}`);
212
+ this.logger.log(` Work items to close: ${totalDuplicates}`);
213
+ this.logger.log(`\n⚠️ This was a DRY RUN - no changes made.`);
214
+
215
+ return {
216
+ groups: duplicateGroups,
217
+ totalItems: items.length,
218
+ duplicateCount: totalDuplicates,
219
+ closedCount: 0,
220
+ };
221
+ }
222
+
223
+ // 4. Close duplicates
224
+ let closedCount = 0;
225
+ this.logger.log(`🗑️ Closing duplicates...`);
226
+
227
+ for (const group of duplicateGroups) {
228
+ const result = await this.closeDuplicates(group.duplicates, group.keepItem.id);
229
+ closedCount += result.closedCount;
230
+ }
231
+
232
+ this.logger.log(`\n✅ Cleanup complete!`);
233
+ this.logger.log(` Closed: ${closedCount} duplicates`);
234
+ this.logger.log(` Kept: ${duplicateGroups.length} original work items`);
235
+
236
+ return {
237
+ groups: duplicateGroups,
238
+ totalItems: items.length,
239
+ duplicateCount: totalDuplicates,
240
+ closedCount,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Group work items by title
246
+ */
247
+ private groupByTitle(items: AdoWorkItem[]): DuplicateGroup[] {
248
+ const titleMap = new Map<string, AdoWorkItem[]>();
249
+
250
+ for (const item of items) {
251
+ const existing = titleMap.get(item.title) || [];
252
+ existing.push(item);
253
+ titleMap.set(item.title, existing);
254
+ }
255
+
256
+ const groups: DuplicateGroup[] = [];
257
+
258
+ for (const [title, groupItems] of titleMap) {
259
+ // Sort by ID (lowest = oldest)
260
+ const sorted = groupItems.sort((a, b) => a.id - b.id);
261
+
262
+ groups.push({
263
+ title,
264
+ items: sorted,
265
+ keepItem: sorted[0],
266
+ duplicates: sorted.slice(1),
267
+ });
268
+ }
269
+
270
+ return groups;
271
+ }
272
+
273
+ /**
274
+ * Search for work items using WIQL
275
+ */
276
+ private async searchWorkItems(titlePattern: string): Promise<AdoWorkItem[]> {
277
+ if (!this.org || !this.pat) {
278
+ throw new Error('ADO credentials not configured');
279
+ }
280
+
281
+ const wiql = {
282
+ query: `SELECT [System.Id], [System.Title], [System.State], [System.CreatedDate]
283
+ FROM WorkItems
284
+ WHERE [System.TeamProject] = '${this.project}'
285
+ AND [System.Title] CONTAINS '${titlePattern}'
286
+ ORDER BY [System.Id] ASC`,
287
+ };
288
+
289
+ const auth = Buffer.from(`:${this.pat}`).toString('base64');
290
+ const url = `https://dev.azure.com/${this.org}/${this.project}/_apis/wit/wiql?api-version=7.1`;
291
+
292
+ const response = await fetch(url, {
293
+ method: 'POST',
294
+ headers: {
295
+ Authorization: `Basic ${auth}`,
296
+ 'Content-Type': 'application/json',
297
+ },
298
+ body: JSON.stringify(wiql),
299
+ });
300
+
301
+ if (!response.ok) {
302
+ throw new Error(`WIQL query failed: ${response.status}`);
303
+ }
304
+
305
+ const data = await response.json();
306
+ const workItemIds = data.workItems?.map((wi: any) => wi.id) || [];
307
+
308
+ if (workItemIds.length === 0) {
309
+ return [];
310
+ }
311
+
312
+ // Fetch full work item details
313
+ return this.getWorkItemDetails(workItemIds);
314
+ }
315
+
316
+ /**
317
+ * Get work item details by IDs
318
+ */
319
+ private async getWorkItemDetails(ids: number[]): Promise<AdoWorkItem[]> {
320
+ if (ids.length === 0) return [];
321
+
322
+ const auth = Buffer.from(`:${this.pat}`).toString('base64');
323
+ const url = `https://dev.azure.com/${this.org}/_apis/wit/workitems?ids=${ids.join(',')}&api-version=7.1`;
324
+
325
+ const response = await fetch(url, {
326
+ headers: {
327
+ Authorization: `Basic ${auth}`,
328
+ 'Content-Type': 'application/json',
329
+ },
330
+ });
331
+
332
+ if (!response.ok) {
333
+ throw new Error(`Failed to get work items: ${response.status}`);
334
+ }
335
+
336
+ const data = await response.json();
337
+ return (data.value || []).map((wi: any) => ({
338
+ id: wi.id,
339
+ title: wi.fields['System.Title'],
340
+ state: wi.fields['System.State'],
341
+ createdDate: wi.fields['System.CreatedDate'],
342
+ url: wi._links?.html?.href,
343
+ }));
344
+ }
345
+
346
+ /**
347
+ * Close a work item with duplicate comment
348
+ */
349
+ private async closeWorkItem(workItemId: number, originalId: number): Promise<void> {
350
+ const auth = Buffer.from(`:${this.pat}`).toString('base64');
351
+ const url = `https://dev.azure.com/${this.org}/_apis/wit/workitems/${workItemId}?api-version=7.1`;
352
+
353
+ const comment = `## Duplicate of #${originalId}
354
+
355
+ This work item was automatically closed by SpecWeave cleanup because it is a duplicate.
356
+
357
+ The original work item (#${originalId}) contains the same content and should be used for tracking instead.
358
+
359
+ ---
360
+ 🤖 Auto-closed by SpecWeave Duplicate Cleanup`;
361
+
362
+ const operations = [
363
+ {
364
+ op: 'add',
365
+ path: '/fields/System.State',
366
+ value: 'Closed',
367
+ },
368
+ {
369
+ op: 'add',
370
+ path: '/fields/System.History',
371
+ value: comment,
372
+ },
373
+ ];
374
+
375
+ const response = await fetch(url, {
376
+ method: 'PATCH',
377
+ headers: {
378
+ Authorization: `Basic ${auth}`,
379
+ 'Content-Type': 'application/json-patch+json',
380
+ },
381
+ body: JSON.stringify(operations),
382
+ });
383
+
384
+ if (!response.ok) {
385
+ const error = await response.text();
386
+ throw new Error(`Failed to close work item: ${response.status} - ${error}`);
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Convenience function for quick duplicate cleanup
393
+ */
394
+ export async function cleanupAdoDuplicates(
395
+ featureId: string,
396
+ dryRun: boolean = false
397
+ ): Promise<{
398
+ groups: DuplicateGroup[];
399
+ totalItems: number;
400
+ duplicateCount: number;
401
+ closedCount: number;
402
+ }> {
403
+ const detector = new AdoDuplicateDetector();
404
+ return detector.cleanupFeatureDuplicates(featureId, dryRun);
405
+ }
406
+
407
+ export default AdoDuplicateDetector;
@@ -780,8 +780,8 @@ You can now:
780
780
  ---
781
781
 
782
782
  **Agent Type**: Specialized
783
- **Model**: Sonnet (Claude 3.5 Sonnet) - Best for API operations and structured tasks
783
+ **Model**: Opus (Claude Opus 4.5) - Best for API operations and structured tasks
784
784
  **Context**: Separate context window (doesn't pollute main conversation)
785
785
  **Version**: 1.0.0
786
786
  **Plugin**: specweave-github
787
- **Last Updated**: 2025-10-30
787
+ **Last Updated**: 2025-12-07
@@ -1,6 +1,6 @@
1
1
  **Name:** hetzner-provisioner
2
2
  **Type:** Infrastructure / DevOps
3
- **Model:** Claude Sonnet 4.5 (balanced for IaC generation)
3
+ **Model:** Claude Opus 4.5 (best-in-class for IaC generation)
4
4
  **Status:** Planned
5
5
 
6
6
  ---
@@ -415,7 +415,7 @@ done
415
415
  3. Format Jira epic payload (project key, summary, description, labels)
416
416
  4. POST to Jira API → Create epic
417
417
  5. Parse response → Extract epic key (PROJ-123)
418
- 6. Save to metadata.json: `external_ids.jira.epic = PROJ-123`
418
+ 6. Save to metadata.json: `external_sync.jira.issueKey = PROJ-123`
419
419
  7. Display: "Created Jira Epic: PROJ-123"
420
420
 
421
421
  ### Example 2: Sync Progress