specweave 0.32.2 → 0.32.5

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 (244) hide show
  1. package/CLAUDE.md +51 -9
  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-multiproject.js +1 -1
  26. package/dist/src/cli/commands/init-multiproject.js.map +1 -1
  27. package/dist/src/cli/commands/init.d.ts.map +1 -1
  28. package/dist/src/cli/commands/init.js +111 -69
  29. package/dist/src/cli/commands/init.js.map +1 -1
  30. package/dist/src/cli/commands/migrate-to-multiproject.js +2 -2
  31. package/dist/src/cli/commands/migrate-to-multiproject.js.map +1 -1
  32. package/dist/src/cli/helpers/init/external-import.d.ts +3 -0
  33. package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
  34. package/dist/src/cli/helpers/init/external-import.js +17 -4
  35. package/dist/src/cli/helpers/init/external-import.js.map +1 -1
  36. package/dist/src/cli/helpers/init/index.d.ts +1 -0
  37. package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
  38. package/dist/src/cli/helpers/init/index.js +2 -0
  39. package/dist/src/cli/helpers/init/index.js.map +1 -1
  40. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +70 -0
  41. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -1
  42. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +214 -4
  43. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -1
  44. package/dist/src/cli/helpers/init/living-docs-preflight.d.ts +4 -0
  45. package/dist/src/cli/helpers/init/living-docs-preflight.d.ts.map +1 -1
  46. package/dist/src/cli/helpers/init/living-docs-preflight.js +34 -3
  47. package/dist/src/cli/helpers/init/living-docs-preflight.js.map +1 -1
  48. package/dist/src/cli/helpers/init/testing-config.d.ts +3 -0
  49. package/dist/src/cli/helpers/init/testing-config.d.ts.map +1 -1
  50. package/dist/src/cli/helpers/init/testing-config.js +9 -2
  51. package/dist/src/cli/helpers/init/testing-config.js.map +1 -1
  52. package/dist/src/cli/helpers/init/translation-config.d.ts +3 -0
  53. package/dist/src/cli/helpers/init/translation-config.d.ts.map +1 -1
  54. package/dist/src/cli/helpers/init/translation-config.js +21 -4
  55. package/dist/src/cli/helpers/init/translation-config.js.map +1 -1
  56. package/dist/src/cli/helpers/init/wizard-navigation.d.ts +45 -0
  57. package/dist/src/cli/helpers/init/wizard-navigation.d.ts.map +1 -0
  58. package/dist/src/cli/helpers/init/wizard-navigation.js +97 -0
  59. package/dist/src/cli/helpers/init/wizard-navigation.js.map +1 -0
  60. package/dist/src/core/increment/increment-archiver.d.ts +25 -4
  61. package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
  62. package/dist/src/core/increment/increment-archiver.js +64 -20
  63. package/dist/src/core/increment/increment-archiver.js.map +1 -1
  64. package/dist/src/core/increment/increment-utils.d.ts +65 -0
  65. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  66. package/dist/src/core/increment/increment-utils.js +114 -0
  67. package/dist/src/core/increment/increment-utils.js.map +1 -1
  68. package/dist/src/core/living-docs/cross-project-sync.d.ts +97 -0
  69. package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -0
  70. package/dist/src/core/living-docs/cross-project-sync.js +135 -0
  71. package/dist/src/core/living-docs/cross-project-sync.js.map +1 -0
  72. package/dist/src/core/living-docs/external-sync-orchestrator.d.ts +106 -0
  73. package/dist/src/core/living-docs/external-sync-orchestrator.d.ts.map +1 -0
  74. package/dist/src/core/living-docs/external-sync-orchestrator.js +146 -0
  75. package/dist/src/core/living-docs/external-sync-orchestrator.js.map +1 -0
  76. package/dist/src/core/living-docs/feature-archiver.d.ts +4 -0
  77. package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
  78. package/dist/src/core/living-docs/feature-archiver.js +32 -10
  79. package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
  80. package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -1
  81. package/dist/src/core/living-docs/feature-id-manager.js +7 -3
  82. package/dist/src/core/living-docs/feature-id-manager.js.map +1 -1
  83. package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts +38 -0
  84. package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts.map +1 -0
  85. package/dist/src/core/living-docs/governance/ecosystem-detector.js +325 -0
  86. package/dist/src/core/living-docs/governance/ecosystem-detector.js.map +1 -0
  87. package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts +74 -0
  88. package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts.map +1 -0
  89. package/dist/src/core/living-docs/governance/frontend-standards-parser.js +366 -0
  90. package/dist/src/core/living-docs/governance/frontend-standards-parser.js.map +1 -0
  91. package/dist/src/core/living-docs/governance/go-standards-parser.d.ts +64 -0
  92. package/dist/src/core/living-docs/governance/go-standards-parser.d.ts.map +1 -0
  93. package/dist/src/core/living-docs/governance/go-standards-parser.js +229 -0
  94. package/dist/src/core/living-docs/governance/go-standards-parser.js.map +1 -0
  95. package/dist/src/core/living-docs/governance/index.d.ts +50 -0
  96. package/dist/src/core/living-docs/governance/index.d.ts.map +1 -0
  97. package/dist/src/core/living-docs/governance/index.js +56 -0
  98. package/dist/src/core/living-docs/governance/index.js.map +1 -0
  99. package/dist/src/core/living-docs/governance/java-standards-parser.d.ts +89 -0
  100. package/dist/src/core/living-docs/governance/java-standards-parser.d.ts.map +1 -0
  101. package/dist/src/core/living-docs/governance/java-standards-parser.js +356 -0
  102. package/dist/src/core/living-docs/governance/java-standards-parser.js.map +1 -0
  103. package/dist/src/core/living-docs/governance/python-standards-parser.d.ts +83 -0
  104. package/dist/src/core/living-docs/governance/python-standards-parser.d.ts.map +1 -0
  105. package/dist/src/core/living-docs/governance/python-standards-parser.js +347 -0
  106. package/dist/src/core/living-docs/governance/python-standards-parser.js.map +1 -0
  107. package/dist/src/core/living-docs/governance/standards-generator.d.ts +38 -0
  108. package/dist/src/core/living-docs/governance/standards-generator.d.ts.map +1 -0
  109. package/dist/src/core/living-docs/governance/standards-generator.js +476 -0
  110. package/dist/src/core/living-docs/governance/standards-generator.js.map +1 -0
  111. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
  112. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +54 -2
  113. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
  114. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts +5 -1
  115. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
  116. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +358 -30
  117. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
  118. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +44 -0
  119. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
  120. package/dist/src/core/living-docs/living-docs-sync.d.ts +7 -3
  121. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  122. package/dist/src/core/living-docs/living-docs-sync.js +94 -10
  123. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  124. package/dist/src/core/living-docs/module-analyzer.d.ts +22 -0
  125. package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
  126. package/dist/src/core/living-docs/module-analyzer.js +123 -19
  127. package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
  128. package/dist/src/core/living-docs/sync-helpers/generators.d.ts +8 -1
  129. package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -1
  130. package/dist/src/core/living-docs/sync-helpers/generators.js +18 -1
  131. package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -1
  132. package/dist/src/core/living-docs/sync-helpers/index.d.ts +1 -1
  133. package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -1
  134. package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -1
  135. package/dist/src/core/living-docs/sync-helpers/parsers.d.ts +3 -1
  136. package/dist/src/core/living-docs/sync-helpers/parsers.d.ts.map +1 -1
  137. package/dist/src/core/living-docs/sync-helpers/parsers.js +24 -2
  138. package/dist/src/core/living-docs/sync-helpers/parsers.js.map +1 -1
  139. package/dist/src/core/living-docs/types.d.ts +6 -0
  140. package/dist/src/core/living-docs/types.d.ts.map +1 -1
  141. package/dist/src/core/living-docs/validators/index.d.ts +7 -0
  142. package/dist/src/core/living-docs/validators/index.d.ts.map +1 -0
  143. package/dist/src/core/living-docs/validators/index.js +7 -0
  144. package/dist/src/core/living-docs/validators/index.js.map +1 -0
  145. package/dist/src/core/living-docs/validators/project-validator.d.ts +92 -0
  146. package/dist/src/core/living-docs/validators/project-validator.d.ts.map +1 -0
  147. package/dist/src/core/living-docs/validators/project-validator.js +142 -0
  148. package/dist/src/core/living-docs/validators/project-validator.js.map +1 -0
  149. package/dist/src/core/llm/provider-factory.js +2 -2
  150. package/dist/src/core/llm/provider-factory.js.map +1 -1
  151. package/dist/src/core/llm/providers/anthropic-provider.js +1 -1
  152. package/dist/src/core/llm/providers/bedrock-provider.d.ts.map +1 -1
  153. package/dist/src/core/llm/providers/bedrock-provider.js +8 -4
  154. package/dist/src/core/llm/providers/bedrock-provider.js.map +1 -1
  155. package/dist/src/core/project/project-manager.d.ts.map +1 -1
  156. package/dist/src/core/project/project-manager.js +19 -17
  157. package/dist/src/core/project/project-manager.js.map +1 -1
  158. package/dist/src/core/types/config.d.ts +4 -2
  159. package/dist/src/core/types/config.d.ts.map +1 -1
  160. package/dist/src/core/types/config.js.map +1 -1
  161. package/dist/src/core/types/increment-metadata.d.ts +34 -0
  162. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  163. package/dist/src/importers/jira-importer.d.ts +14 -0
  164. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  165. package/dist/src/importers/jira-importer.js +75 -0
  166. package/dist/src/importers/jira-importer.js.map +1 -1
  167. package/dist/src/integrations/jira/jira-token-provider.d.ts +93 -0
  168. package/dist/src/integrations/jira/jira-token-provider.d.ts.map +1 -0
  169. package/dist/src/integrations/jira/jira-token-provider.js +160 -0
  170. package/dist/src/integrations/jira/jira-token-provider.js.map +1 -0
  171. package/dist/src/sync/ado-reconciler.d.ts +92 -0
  172. package/dist/src/sync/ado-reconciler.d.ts.map +1 -0
  173. package/dist/src/sync/ado-reconciler.js +335 -0
  174. package/dist/src/sync/ado-reconciler.js.map +1 -0
  175. package/dist/src/sync/jira-reconciler.d.ts +106 -0
  176. package/dist/src/sync/jira-reconciler.d.ts.map +1 -0
  177. package/dist/src/sync/jira-reconciler.js +405 -0
  178. package/dist/src/sync/jira-reconciler.js.map +1 -0
  179. package/dist/src/types/model-selection.d.ts +6 -4
  180. package/dist/src/types/model-selection.d.ts.map +1 -1
  181. package/dist/src/types/model-selection.js +3 -1
  182. package/dist/src/types/model-selection.js.map +1 -1
  183. package/dist/src/utils/cross-cutting-detector.d.ts +66 -0
  184. package/dist/src/utils/cross-cutting-detector.d.ts.map +1 -0
  185. package/dist/src/utils/cross-cutting-detector.js +179 -0
  186. package/dist/src/utils/cross-cutting-detector.js.map +1 -0
  187. package/dist/src/utils/external-tool-drift-detector.d.ts +1 -1
  188. package/dist/src/utils/external-tool-drift-detector.d.ts.map +1 -1
  189. package/dist/src/utils/external-tool-drift-detector.js +5 -4
  190. package/dist/src/utils/external-tool-drift-detector.js.map +1 -1
  191. package/dist/src/utils/feature-id-derivation.d.ts +8 -3
  192. package/dist/src/utils/feature-id-derivation.d.ts.map +1 -1
  193. package/dist/src/utils/feature-id-derivation.js +14 -6
  194. package/dist/src/utils/feature-id-derivation.js.map +1 -1
  195. package/dist/src/utils/model-selection.d.ts +3 -4
  196. package/dist/src/utils/model-selection.d.ts.map +1 -1
  197. package/dist/src/utils/model-selection.js +3 -4
  198. package/dist/src/utils/model-selection.js.map +1 -1
  199. package/dist/src/utils/project-detection.d.ts +12 -8
  200. package/dist/src/utils/project-detection.d.ts.map +1 -1
  201. package/dist/src/utils/project-detection.js +13 -19
  202. package/dist/src/utils/project-detection.js.map +1 -1
  203. package/package.json +1 -1
  204. package/plugins/specweave/agents/code-standards-detective/AGENT.md +48 -0
  205. package/plugins/specweave/commands/specweave-costs.md +4 -4
  206. package/plugins/specweave/commands/specweave-do.md +9 -9
  207. package/plugins/specweave/commands/specweave-done.md +13 -0
  208. package/plugins/specweave/commands/specweave-status.md +64 -0
  209. package/plugins/specweave/commands/specweave-validate.md +27 -1
  210. package/plugins/specweave/hooks/hooks.json +11 -1
  211. package/plugins/specweave/hooks/spec-project-validator.sh +81 -25
  212. package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +135 -0
  213. package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +34 -0
  214. package/plugins/specweave/scripts/read-costs.sh +3 -3
  215. package/plugins/specweave/skills/code-standards-analyzer/SKILL.md +58 -6
  216. package/plugins/specweave/skills/increment-planner/SKILL.md +135 -29
  217. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +4 -2
  218. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +2 -1
  219. package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +1 -1
  220. package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +1 -1
  221. package/plugins/specweave/skills/spec-generator/SKILL.md +78 -7
  222. package/plugins/specweave-ado/commands/cleanup-duplicates.md +212 -0
  223. package/plugins/specweave-ado/commands/reconcile.md +120 -0
  224. package/plugins/specweave-ado/lib/ado-duplicate-detector.js +279 -0
  225. package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +407 -0
  226. package/plugins/specweave-github/agents/github-manager/AGENT.md +2 -2
  227. package/plugins/specweave-infrastructure/skills/hetzner-provisioner/README.md +1 -1
  228. package/plugins/specweave-jira/agents/jira-manager/AGENT.md +1 -1
  229. package/plugins/specweave-jira/agents/jira-multi-project-mapper/AGENT.md +530 -0
  230. package/plugins/specweave-jira/agents/jira-sync-judge/AGENT.md +438 -0
  231. package/plugins/specweave-jira/commands/cleanup-duplicates.md +219 -0
  232. package/plugins/specweave-jira/commands/close.md +297 -0
  233. package/plugins/specweave-jira/commands/create.md +198 -0
  234. package/plugins/specweave-jira/commands/reconcile.md +123 -0
  235. package/plugins/specweave-jira/commands/status.md +215 -0
  236. package/plugins/specweave-jira/lib/jira-duplicate-detector.js +296 -0
  237. package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +434 -0
  238. package/plugins/specweave-jira/lib/jira-permission-gate.js +160 -0
  239. package/plugins/specweave-jira/lib/jira-permission-gate.ts +276 -0
  240. package/plugins/specweave-jira/lib/jira-profile-resolver.js +222 -0
  241. package/plugins/specweave-jira/lib/jira-profile-resolver.ts +427 -0
  242. package/plugins/specweave-jira/reference/jira-specweave-mapping.md +16 -11
  243. package/plugins/specweave-release/commands/specweave-release-npm.md +140 -14
  244. package/plugins/specweave/commands/specweave-switch-project.md +0 -168
@@ -0,0 +1,434 @@
1
+ /**
2
+ * JIRA Duplicate Detector (v0.33.0)
3
+ *
4
+ * Implements 3-phase duplicate protection for JIRA issues:
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 JiraIssue {
15
+ key: string;
16
+ summary: string;
17
+ status: string;
18
+ created: string;
19
+ url?: string;
20
+ }
21
+
22
+ export interface DuplicateGroup {
23
+ summary: string;
24
+ issues: JiraIssue[];
25
+ keepIssue: JiraIssue;
26
+ duplicates: JiraIssue[];
27
+ }
28
+
29
+ export interface DetectionResult {
30
+ found: boolean;
31
+ existingIssue?: JiraIssue;
32
+ count: number;
33
+ }
34
+
35
+ export interface VerificationResult {
36
+ success: boolean;
37
+ expectedCount: number;
38
+ actualCount: number;
39
+ duplicates: JiraIssue[];
40
+ }
41
+
42
+ export interface CleanupResult {
43
+ closedCount: number;
44
+ keptCount: number;
45
+ errors: string[];
46
+ }
47
+
48
+ export class JiraDuplicateDetector {
49
+ private domain: string;
50
+ private auth: string;
51
+ private logger: Logger;
52
+
53
+ constructor(options: {
54
+ domain?: string;
55
+ email?: string;
56
+ token?: string;
57
+ logger?: Logger;
58
+ } = {}) {
59
+ this.domain = options.domain || process.env.JIRA_DOMAIN || '';
60
+ const email = options.email || process.env.JIRA_EMAIL || '';
61
+ const token = options.token || process.env.JIRA_API_TOKEN || '';
62
+ this.auth = Buffer.from(`${email}:${token}`).toString('base64');
63
+ this.logger = options.logger || consoleLogger;
64
+ }
65
+
66
+ /**
67
+ * Phase 1: Check if issue exists before creating
68
+ */
69
+ async checkBeforeCreate(
70
+ summaryPattern: string,
71
+ incrementId?: string
72
+ ): Promise<DetectionResult> {
73
+ try {
74
+ const issues = await this.searchIssues(summaryPattern);
75
+
76
+ if (issues.length > 0) {
77
+ return {
78
+ found: true,
79
+ existingIssue: issues[0],
80
+ count: issues.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
+ summaryPattern: string,
97
+ expectedCount: number = 1
98
+ ): Promise<VerificationResult> {
99
+ try {
100
+ const issues = await this.searchIssues(summaryPattern);
101
+
102
+ if (issues.length > expectedCount) {
103
+ // Duplicates detected!
104
+ const sorted = issues.sort(
105
+ (a, b) => new Date(a.created).getTime() - new Date(b.created).getTime()
106
+ );
107
+
108
+ return {
109
+ success: false,
110
+ expectedCount,
111
+ actualCount: issues.length,
112
+ duplicates: sorted.slice(expectedCount), // All issues after expected count
113
+ };
114
+ }
115
+
116
+ return {
117
+ success: true,
118
+ expectedCount,
119
+ actualCount: issues.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: JiraIssue[],
138
+ keepIssueKey: string
139
+ ): Promise<CleanupResult> {
140
+ const result: CleanupResult = {
141
+ closedCount: 0,
142
+ keptCount: 1,
143
+ errors: [],
144
+ };
145
+
146
+ for (const issue of duplicates) {
147
+ try {
148
+ await this.closeIssue(issue.key, keepIssueKey);
149
+ result.closedCount++;
150
+ this.logger.log(` ✅ Closed ${issue.key} (duplicate of ${keepIssueKey})`);
151
+ } catch (error: any) {
152
+ result.errors.push(`${issue.key}: ${error.message}`);
153
+ this.logger.log(` ❌ Failed to close ${issue.key}: ${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
+ totalIssues: number;
169
+ duplicateCount: number;
170
+ closedCount: number;
171
+ }> {
172
+ // 1. Search for all issues with feature ID
173
+ const searchPattern = `[${featureId}]`;
174
+ const issues = await this.searchIssues(searchPattern);
175
+
176
+ this.logger.log(`\n🔍 Scanning for duplicates in Feature ${featureId}...`);
177
+ this.logger.log(` Found ${issues.length} total issues`);
178
+
179
+ // 2. Group by summary
180
+ const groups = this.groupBySummary(issues);
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
+ totalIssues: issues.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.summary.substring(0, 50)}..."`);
199
+ this.logger.log(` - ${group.keepIssue.key} (KEEP) - Created ${group.keepIssue.created.split('T')[0]}`);
200
+ for (const dup of group.duplicates) {
201
+ this.logger.log(` - ${dup.key} (CLOSE) - Created ${dup.created.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 issues: ${issues.length}`);
211
+ this.logger.log(` Duplicate groups: ${duplicateGroups.length}`);
212
+ this.logger.log(` Issues to close: ${totalDuplicates}`);
213
+ this.logger.log(`\n⚠️ This was a DRY RUN - no changes made.`);
214
+
215
+ return {
216
+ groups: duplicateGroups,
217
+ totalIssues: issues.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.keepIssue.key);
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 issues`);
235
+
236
+ return {
237
+ groups: duplicateGroups,
238
+ totalIssues: issues.length,
239
+ duplicateCount: totalDuplicates,
240
+ closedCount,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Group issues by summary
246
+ */
247
+ private groupBySummary(issues: JiraIssue[]): DuplicateGroup[] {
248
+ const summaryMap = new Map<string, JiraIssue[]>();
249
+
250
+ for (const issue of issues) {
251
+ const existing = summaryMap.get(issue.summary) || [];
252
+ existing.push(issue);
253
+ summaryMap.set(issue.summary, existing);
254
+ }
255
+
256
+ const groups: DuplicateGroup[] = [];
257
+
258
+ for (const [summary, groupIssues] of summaryMap) {
259
+ // Sort by created date (oldest first)
260
+ const sorted = groupIssues.sort(
261
+ (a, b) => new Date(a.created).getTime() - new Date(b.created).getTime()
262
+ );
263
+
264
+ groups.push({
265
+ summary,
266
+ issues: sorted,
267
+ keepIssue: sorted[0],
268
+ duplicates: sorted.slice(1),
269
+ });
270
+ }
271
+
272
+ return groups;
273
+ }
274
+
275
+ /**
276
+ * Search for issues using JQL
277
+ */
278
+ private async searchIssues(summaryPattern: string): Promise<JiraIssue[]> {
279
+ if (!this.domain || !this.auth) {
280
+ throw new Error('JIRA credentials not configured');
281
+ }
282
+
283
+ const jql = encodeURIComponent(`summary ~ "${summaryPattern}" ORDER BY created ASC`);
284
+ const url = `https://${this.domain}/rest/api/3/search?jql=${jql}&fields=summary,status,created`;
285
+
286
+ const response = await fetch(url, {
287
+ headers: {
288
+ Authorization: `Basic ${this.auth}`,
289
+ Accept: 'application/json',
290
+ },
291
+ });
292
+
293
+ if (!response.ok) {
294
+ throw new Error(`JQL search failed: ${response.status}`);
295
+ }
296
+
297
+ const data = await response.json();
298
+ return (data.issues || []).map((issue: any) => ({
299
+ key: issue.key,
300
+ summary: issue.fields.summary,
301
+ status: issue.fields.status?.name,
302
+ created: issue.fields.created,
303
+ url: `https://${this.domain}/browse/${issue.key}`,
304
+ }));
305
+ }
306
+
307
+ /**
308
+ * Close an issue with duplicate comment
309
+ */
310
+ private async closeIssue(issueKey: string, originalKey: string): Promise<void> {
311
+ // First, add comment
312
+ await this.addComment(issueKey, originalKey);
313
+
314
+ // Then, transition to closed
315
+ const transitions = await this.getTransitions(issueKey);
316
+
317
+ // Find "Won't Do" or "Done" transition
318
+ const closeTransition = transitions.find(
319
+ (t: any) =>
320
+ t.name === "Won't Do" ||
321
+ t.name === 'Done' ||
322
+ t.name === 'Closed' ||
323
+ t.to?.name === "Won't Do" ||
324
+ t.to?.name === 'Done'
325
+ );
326
+
327
+ if (!closeTransition) {
328
+ throw new Error(`No close transition found. Available: ${transitions.map((t: any) => t.name).join(', ')}`);
329
+ }
330
+
331
+ const url = `https://${this.domain}/rest/api/3/issue/${issueKey}/transitions`;
332
+
333
+ const response = await fetch(url, {
334
+ method: 'POST',
335
+ headers: {
336
+ Authorization: `Basic ${this.auth}`,
337
+ 'Content-Type': 'application/json',
338
+ },
339
+ body: JSON.stringify({
340
+ transition: { id: closeTransition.id },
341
+ }),
342
+ });
343
+
344
+ if (!response.ok) {
345
+ const error = await response.text();
346
+ throw new Error(`Failed to transition issue: ${response.status} - ${error}`);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Get available transitions for an issue
352
+ */
353
+ private async getTransitions(issueKey: string): Promise<any[]> {
354
+ const url = `https://${this.domain}/rest/api/3/issue/${issueKey}/transitions`;
355
+
356
+ const response = await fetch(url, {
357
+ headers: {
358
+ Authorization: `Basic ${this.auth}`,
359
+ Accept: 'application/json',
360
+ },
361
+ });
362
+
363
+ if (!response.ok) {
364
+ throw new Error(`Failed to get transitions: ${response.status}`);
365
+ }
366
+
367
+ const data = await response.json();
368
+ return data.transitions || [];
369
+ }
370
+
371
+ /**
372
+ * Add duplicate comment to issue
373
+ */
374
+ private async addComment(issueKey: string, originalKey: string): Promise<void> {
375
+ const url = `https://${this.domain}/rest/api/3/issue/${issueKey}/comment`;
376
+
377
+ const comment = `h2. Duplicate of ${originalKey}
378
+
379
+ This issue was automatically closed by SpecWeave cleanup because it is a duplicate.
380
+
381
+ The original issue (${originalKey}) contains the same content and should be used for tracking instead.
382
+
383
+ ----
384
+ 🤖 Auto-closed by SpecWeave Duplicate Cleanup`;
385
+
386
+ const response = await fetch(url, {
387
+ method: 'POST',
388
+ headers: {
389
+ Authorization: `Basic ${this.auth}`,
390
+ 'Content-Type': 'application/json',
391
+ },
392
+ body: JSON.stringify({
393
+ body: {
394
+ type: 'doc',
395
+ version: 1,
396
+ content: [
397
+ {
398
+ type: 'paragraph',
399
+ content: [
400
+ {
401
+ type: 'text',
402
+ text: comment,
403
+ },
404
+ ],
405
+ },
406
+ ],
407
+ },
408
+ }),
409
+ });
410
+
411
+ if (!response.ok) {
412
+ // Non-fatal, just log warning
413
+ this.logger.log(` ⚠️ Failed to add comment to ${issueKey}`);
414
+ }
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Convenience function for quick duplicate cleanup
420
+ */
421
+ export async function cleanupJiraDuplicates(
422
+ featureId: string,
423
+ dryRun: boolean = false
424
+ ): Promise<{
425
+ groups: DuplicateGroup[];
426
+ totalIssues: number;
427
+ duplicateCount: number;
428
+ closedCount: number;
429
+ }> {
430
+ const detector = new JiraDuplicateDetector();
431
+ return detector.cleanupFeatureDuplicates(featureId, dryRun);
432
+ }
433
+
434
+ export default JiraDuplicateDetector;
@@ -0,0 +1,160 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ const DEFAULT_SYNC_SETTINGS = {
4
+ canUpsertInternalItems: false,
5
+ canUpdateExternalItems: false,
6
+ canUpdateStatus: false
7
+ };
8
+ class JiraPermissionGate {
9
+ constructor(settings, configPath) {
10
+ this.settings = settings;
11
+ this.configPath = configPath;
12
+ }
13
+ /**
14
+ * Check if write operations (create/update issues) are allowed
15
+ *
16
+ * Requires: canUpdateExternalItems = true
17
+ */
18
+ checkWritePermission() {
19
+ if (this.settings.canUpdateExternalItems) {
20
+ return {
21
+ allowed: true,
22
+ reason: "Write operations permitted (canUpdateExternalItems=true)"
23
+ };
24
+ }
25
+ return {
26
+ allowed: false,
27
+ reason: "Permission denied: JIRA updates are disabled.",
28
+ suggestedAction: `Enable sync.settings.canUpdateExternalItems in ${this.configPath}`,
29
+ settingPath: "sync.settings.canUpdateExternalItems"
30
+ };
31
+ }
32
+ /**
33
+ * Check if status updates (transitions) are allowed
34
+ *
35
+ * Requires: canUpdateStatus = true
36
+ */
37
+ checkStatusPermission() {
38
+ if (this.settings.canUpdateStatus) {
39
+ return {
40
+ allowed: true,
41
+ reason: "Status updates permitted (canUpdateStatus=true)"
42
+ };
43
+ }
44
+ return {
45
+ allowed: false,
46
+ reason: "Permission denied: JIRA status transitions are disabled.",
47
+ suggestedAction: `Enable sync.settings.canUpdateStatus in ${this.configPath}`,
48
+ settingPath: "sync.settings.canUpdateStatus"
49
+ };
50
+ }
51
+ /**
52
+ * Check if internal item creation is allowed
53
+ *
54
+ * Requires: canUpsertInternalItems = true
55
+ */
56
+ checkCreateInternalPermission() {
57
+ if (this.settings.canUpsertInternalItems) {
58
+ return {
59
+ allowed: true,
60
+ reason: "Internal item creation permitted (canUpsertInternalItems=true)"
61
+ };
62
+ }
63
+ return {
64
+ allowed: false,
65
+ reason: "Permission denied: Creating internal items is disabled.",
66
+ suggestedAction: `Enable sync.settings.canUpsertInternalItems in ${this.configPath}`,
67
+ settingPath: "sync.settings.canUpsertInternalItems"
68
+ };
69
+ }
70
+ /**
71
+ * Check if close operation is allowed (requires both write AND status)
72
+ *
73
+ * Requires: canUpdateExternalItems = true AND canUpdateStatus = true
74
+ */
75
+ checkClosePermission() {
76
+ const writeCheck = this.checkWritePermission();
77
+ const statusCheck = this.checkStatusPermission();
78
+ if (writeCheck.allowed && statusCheck.allowed) {
79
+ return {
80
+ allowed: true,
81
+ reason: "Close operations permitted (canUpdateExternalItems=true, canUpdateStatus=true)"
82
+ };
83
+ }
84
+ const missingPermissions = [];
85
+ if (!writeCheck.allowed) {
86
+ missingPermissions.push("canUpdateExternalItems");
87
+ }
88
+ if (!statusCheck.allowed) {
89
+ missingPermissions.push("canUpdateStatus");
90
+ }
91
+ return {
92
+ allowed: false,
93
+ reason: `Permission denied: Closing JIRA issues requires ${missingPermissions.join(" and ")}.`,
94
+ suggestedAction: `Enable ${missingPermissions.map((p) => `sync.settings.${p}`).join(" and ")} in ${this.configPath}`,
95
+ settingPath: missingPermissions.map((p) => `sync.settings.${p}`).join(", ")
96
+ };
97
+ }
98
+ /**
99
+ * Get current settings
100
+ */
101
+ getSettings() {
102
+ return { ...this.settings };
103
+ }
104
+ /**
105
+ * Get human-readable permission summary
106
+ */
107
+ getPermissionSummary() {
108
+ const parts = [];
109
+ if (this.settings.canUpdateExternalItems) {
110
+ parts.push("create/update JIRA issues");
111
+ }
112
+ if (this.settings.canUpdateStatus) {
113
+ parts.push("transition issue status");
114
+ }
115
+ if (this.settings.canUpsertInternalItems) {
116
+ parts.push("create internal items");
117
+ }
118
+ if (parts.length === 0) {
119
+ return "All JIRA write operations disabled (read-only mode)";
120
+ }
121
+ return `Allowed: ${parts.join(", ")}`;
122
+ }
123
+ }
124
+ async function createJiraPermissionGate(projectRoot = process.cwd()) {
125
+ const configPath = path.join(projectRoot, ".specweave", "config.json");
126
+ try {
127
+ const content = await fs.readFile(configPath, "utf-8");
128
+ const config = JSON.parse(content);
129
+ const settings = {
130
+ canUpsertInternalItems: config?.sync?.settings?.canUpsertInternalItems ?? false,
131
+ canUpdateExternalItems: config?.sync?.settings?.canUpdateExternalItems ?? false,
132
+ canUpdateStatus: config?.sync?.settings?.canUpdateStatus ?? false
133
+ };
134
+ return new JiraPermissionGate(settings, configPath);
135
+ } catch {
136
+ return new JiraPermissionGate(DEFAULT_SYNC_SETTINGS, configPath);
137
+ }
138
+ }
139
+ async function canWriteToJira(projectRoot = process.cwd()) {
140
+ const gate = await createJiraPermissionGate(projectRoot);
141
+ return gate.checkWritePermission();
142
+ }
143
+ async function canUpdateJiraStatus(projectRoot = process.cwd()) {
144
+ const gate = await createJiraPermissionGate(projectRoot);
145
+ return gate.checkStatusPermission();
146
+ }
147
+ async function canCloseJiraIssue(projectRoot = process.cwd()) {
148
+ const gate = await createJiraPermissionGate(projectRoot);
149
+ return gate.checkClosePermission();
150
+ }
151
+ var jira_permission_gate_default = JiraPermissionGate;
152
+ export {
153
+ DEFAULT_SYNC_SETTINGS,
154
+ JiraPermissionGate,
155
+ canCloseJiraIssue,
156
+ canUpdateJiraStatus,
157
+ canWriteToJira,
158
+ createJiraPermissionGate,
159
+ jira_permission_gate_default as default
160
+ };