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,276 @@
1
+ /**
2
+ * JIRA Permission Gate
3
+ *
4
+ * Validates that sync permissions are enabled before allowing JIRA write operations.
5
+ * This ensures manual JIRA commands respect the same permission settings as the
6
+ * sync-coordinator.
7
+ *
8
+ * Usage:
9
+ * ```typescript
10
+ * const gate = await createJiraPermissionGate();
11
+ * const result = gate.checkWritePermission();
12
+ * if (!result.allowed) {
13
+ * console.log(result.reason);
14
+ * return;
15
+ * }
16
+ * ```
17
+ *
18
+ * @module jira-permission-gate
19
+ */
20
+
21
+ import { promises as fs } from 'node:fs';
22
+ import * as path from 'node:path';
23
+
24
+ /**
25
+ * Permission check result
26
+ */
27
+ export interface PermissionCheckResult {
28
+ /**
29
+ * Whether the operation is allowed
30
+ */
31
+ allowed: boolean;
32
+
33
+ /**
34
+ * Human-readable reason for the decision
35
+ */
36
+ reason: string;
37
+
38
+ /**
39
+ * Suggested action if permission denied
40
+ */
41
+ suggestedAction?: string;
42
+
43
+ /**
44
+ * Which setting controls this permission
45
+ */
46
+ settingPath?: string;
47
+ }
48
+
49
+ /**
50
+ * Sync settings from config.json
51
+ */
52
+ export interface SyncSettings {
53
+ canUpsertInternalItems: boolean;
54
+ canUpdateExternalItems: boolean;
55
+ canUpdateStatus: boolean;
56
+ }
57
+
58
+ /**
59
+ * Default settings (all disabled for safety)
60
+ */
61
+ export const DEFAULT_SYNC_SETTINGS: SyncSettings = {
62
+ canUpsertInternalItems: false,
63
+ canUpdateExternalItems: false,
64
+ canUpdateStatus: false,
65
+ };
66
+
67
+ /**
68
+ * JIRA Permission Gate
69
+ *
70
+ * Checks permission settings before allowing JIRA write operations.
71
+ */
72
+ export class JiraPermissionGate {
73
+ private settings: SyncSettings;
74
+ private configPath: string;
75
+
76
+ constructor(settings: SyncSettings, configPath: string) {
77
+ this.settings = settings;
78
+ this.configPath = configPath;
79
+ }
80
+
81
+ /**
82
+ * Check if write operations (create/update issues) are allowed
83
+ *
84
+ * Requires: canUpdateExternalItems = true
85
+ */
86
+ checkWritePermission(): PermissionCheckResult {
87
+ if (this.settings.canUpdateExternalItems) {
88
+ return {
89
+ allowed: true,
90
+ reason: 'Write operations permitted (canUpdateExternalItems=true)',
91
+ };
92
+ }
93
+
94
+ return {
95
+ allowed: false,
96
+ reason: 'Permission denied: JIRA updates are disabled.',
97
+ suggestedAction: `Enable sync.settings.canUpdateExternalItems in ${this.configPath}`,
98
+ settingPath: 'sync.settings.canUpdateExternalItems',
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Check if status updates (transitions) are allowed
104
+ *
105
+ * Requires: canUpdateStatus = true
106
+ */
107
+ checkStatusPermission(): PermissionCheckResult {
108
+ if (this.settings.canUpdateStatus) {
109
+ return {
110
+ allowed: true,
111
+ reason: 'Status updates permitted (canUpdateStatus=true)',
112
+ };
113
+ }
114
+
115
+ return {
116
+ allowed: false,
117
+ reason: 'Permission denied: JIRA status transitions are disabled.',
118
+ suggestedAction: `Enable sync.settings.canUpdateStatus in ${this.configPath}`,
119
+ settingPath: 'sync.settings.canUpdateStatus',
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Check if internal item creation is allowed
125
+ *
126
+ * Requires: canUpsertInternalItems = true
127
+ */
128
+ checkCreateInternalPermission(): PermissionCheckResult {
129
+ if (this.settings.canUpsertInternalItems) {
130
+ return {
131
+ allowed: true,
132
+ reason: 'Internal item creation permitted (canUpsertInternalItems=true)',
133
+ };
134
+ }
135
+
136
+ return {
137
+ allowed: false,
138
+ reason: 'Permission denied: Creating internal items is disabled.',
139
+ suggestedAction: `Enable sync.settings.canUpsertInternalItems in ${this.configPath}`,
140
+ settingPath: 'sync.settings.canUpsertInternalItems',
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Check if close operation is allowed (requires both write AND status)
146
+ *
147
+ * Requires: canUpdateExternalItems = true AND canUpdateStatus = true
148
+ */
149
+ checkClosePermission(): PermissionCheckResult {
150
+ const writeCheck = this.checkWritePermission();
151
+ const statusCheck = this.checkStatusPermission();
152
+
153
+ if (writeCheck.allowed && statusCheck.allowed) {
154
+ return {
155
+ allowed: true,
156
+ reason: 'Close operations permitted (canUpdateExternalItems=true, canUpdateStatus=true)',
157
+ };
158
+ }
159
+
160
+ const missingPermissions: string[] = [];
161
+ if (!writeCheck.allowed) {
162
+ missingPermissions.push('canUpdateExternalItems');
163
+ }
164
+ if (!statusCheck.allowed) {
165
+ missingPermissions.push('canUpdateStatus');
166
+ }
167
+
168
+ return {
169
+ allowed: false,
170
+ reason: `Permission denied: Closing JIRA issues requires ${missingPermissions.join(' and ')}.`,
171
+ suggestedAction: `Enable ${missingPermissions.map(p => `sync.settings.${p}`).join(' and ')} in ${this.configPath}`,
172
+ settingPath: missingPermissions.map(p => `sync.settings.${p}`).join(', '),
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Get current settings
178
+ */
179
+ getSettings(): SyncSettings {
180
+ return { ...this.settings };
181
+ }
182
+
183
+ /**
184
+ * Get human-readable permission summary
185
+ */
186
+ getPermissionSummary(): string {
187
+ const parts: string[] = [];
188
+
189
+ if (this.settings.canUpdateExternalItems) {
190
+ parts.push('create/update JIRA issues');
191
+ }
192
+ if (this.settings.canUpdateStatus) {
193
+ parts.push('transition issue status');
194
+ }
195
+ if (this.settings.canUpsertInternalItems) {
196
+ parts.push('create internal items');
197
+ }
198
+
199
+ if (parts.length === 0) {
200
+ return 'All JIRA write operations disabled (read-only mode)';
201
+ }
202
+
203
+ return `Allowed: ${parts.join(', ')}`;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Create a JiraPermissionGate from config.json
209
+ *
210
+ * @param projectRoot - Project root directory (defaults to cwd)
211
+ * @returns JiraPermissionGate instance
212
+ */
213
+ export async function createJiraPermissionGate(
214
+ projectRoot: string = process.cwd()
215
+ ): Promise<JiraPermissionGate> {
216
+ const configPath = path.join(projectRoot, '.specweave', 'config.json');
217
+
218
+ try {
219
+ const content = await fs.readFile(configPath, 'utf-8');
220
+ const config = JSON.parse(content);
221
+
222
+ const settings: SyncSettings = {
223
+ canUpsertInternalItems: config?.sync?.settings?.canUpsertInternalItems ?? false,
224
+ canUpdateExternalItems: config?.sync?.settings?.canUpdateExternalItems ?? false,
225
+ canUpdateStatus: config?.sync?.settings?.canUpdateStatus ?? false,
226
+ };
227
+
228
+ return new JiraPermissionGate(settings, configPath);
229
+ } catch {
230
+ // Return gate with default (disabled) settings if config not found
231
+ return new JiraPermissionGate(DEFAULT_SYNC_SETTINGS, configPath);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Quick check: Are JIRA write operations allowed?
237
+ *
238
+ * Convenience function for simple permission checks.
239
+ *
240
+ * @param projectRoot - Project root directory
241
+ * @returns Permission check result
242
+ */
243
+ export async function canWriteToJira(
244
+ projectRoot: string = process.cwd()
245
+ ): Promise<PermissionCheckResult> {
246
+ const gate = await createJiraPermissionGate(projectRoot);
247
+ return gate.checkWritePermission();
248
+ }
249
+
250
+ /**
251
+ * Quick check: Are JIRA status updates allowed?
252
+ *
253
+ * @param projectRoot - Project root directory
254
+ * @returns Permission check result
255
+ */
256
+ export async function canUpdateJiraStatus(
257
+ projectRoot: string = process.cwd()
258
+ ): Promise<PermissionCheckResult> {
259
+ const gate = await createJiraPermissionGate(projectRoot);
260
+ return gate.checkStatusPermission();
261
+ }
262
+
263
+ /**
264
+ * Quick check: Can JIRA issues be closed?
265
+ *
266
+ * @param projectRoot - Project root directory
267
+ * @returns Permission check result
268
+ */
269
+ export async function canCloseJiraIssue(
270
+ projectRoot: string = process.cwd()
271
+ ): Promise<PermissionCheckResult> {
272
+ const gate = await createJiraPermissionGate(projectRoot);
273
+ return gate.checkClosePermission();
274
+ }
275
+
276
+ export default JiraPermissionGate;
@@ -0,0 +1,222 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ class JiraProfileResolver {
4
+ constructor(projectRoot = process.cwd()) {
5
+ this.projectRoot = projectRoot;
6
+ this.configPath = path.join(projectRoot, ".specweave", "config.json");
7
+ this.incrementsPath = path.join(projectRoot, ".specweave", "increments");
8
+ }
9
+ /**
10
+ * Resolve the JIRA profile for an increment
11
+ *
12
+ * Priority:
13
+ * 1. Increment's metadata.json -> external_sync.jira.profile
14
+ * 2. Config.json -> sync.defaultProfile (v0.31.0+)
15
+ * 3. Config.json -> sync.activeProfile (legacy fallback)
16
+ *
17
+ * @param incrementId - Increment ID (e.g., "0093-my-feature")
18
+ * @returns Profile resolution result
19
+ */
20
+ async resolveProfile(incrementId) {
21
+ const config = await this.loadConfig();
22
+ if (!config) {
23
+ return {
24
+ success: false,
25
+ error: "Failed to load .specweave/config.json",
26
+ incrementId
27
+ };
28
+ }
29
+ const incrementProfile = await this.getIncrementProfile(incrementId);
30
+ const globalDefaultProfile = config.sync?.defaultProfile ?? config.sync?.activeProfile;
31
+ const profileName = incrementProfile || globalDefaultProfile;
32
+ if (!profileName) {
33
+ return {
34
+ success: false,
35
+ error: "No JIRA profile configured. Set sync.defaultProfile in config.json or external_sync.jira.profile in increment metadata.",
36
+ incrementId
37
+ };
38
+ }
39
+ const profiles = config.sync?.profiles || {};
40
+ const profileConfig = profiles[profileName];
41
+ if (!profileConfig) {
42
+ return {
43
+ success: false,
44
+ error: `Profile "${profileName}" not found in config.sync.profiles`,
45
+ incrementId
46
+ };
47
+ }
48
+ if (profileConfig.provider !== "jira") {
49
+ return {
50
+ success: false,
51
+ error: `Profile "${profileName}" is not a JIRA profile (provider: ${profileConfig.provider})`,
52
+ incrementId
53
+ };
54
+ }
55
+ if (!profileConfig.config?.domain || !profileConfig.config?.projectKey) {
56
+ return {
57
+ success: false,
58
+ error: `Profile "${profileName}" missing required fields (domain, projectKey)`,
59
+ incrementId
60
+ };
61
+ }
62
+ return {
63
+ success: true,
64
+ profile: {
65
+ profileName,
66
+ domain: profileConfig.config.domain,
67
+ projectKey: profileConfig.config.projectKey,
68
+ displayName: profileConfig.displayName,
69
+ boardId: profileConfig.config.boardId,
70
+ boardName: profileConfig.config.boardName,
71
+ instanceType: profileConfig.config.instanceType ?? "cloud"
72
+ },
73
+ source: incrementProfile ? "increment" : "global",
74
+ incrementId
75
+ };
76
+ }
77
+ /**
78
+ * Get increment's stored JIRA profile name
79
+ *
80
+ * @param incrementId - Increment ID
81
+ * @returns Profile name or null if not set
82
+ */
83
+ async getIncrementProfile(incrementId) {
84
+ const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
85
+ try {
86
+ const content = await fs.readFile(metadataPath, "utf-8");
87
+ const metadata = JSON.parse(content);
88
+ return metadata?.external_sync?.jira?.profile || metadata?.external_ids?.jira?.profile || null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Get increment's stored JIRA issue key
95
+ *
96
+ * @param incrementId - Increment ID
97
+ * @returns Issue key (e.g., PROJ-123) or null if not linked
98
+ */
99
+ async getIncrementIssueKey(incrementId) {
100
+ const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
101
+ try {
102
+ const content = await fs.readFile(metadataPath, "utf-8");
103
+ const metadata = JSON.parse(content);
104
+ return metadata?.external_sync?.jira?.issueKey || metadata?.external_ids?.jira?.epic || metadata?.external_ids?.jira?.issueKey || null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ /**
110
+ * Set increment's JIRA profile
111
+ *
112
+ * Stores the profile name in the increment's metadata.json
113
+ *
114
+ * @param incrementId - Increment ID
115
+ * @param profileName - Profile name to set
116
+ */
117
+ async setIncrementProfile(incrementId, profileName) {
118
+ const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
119
+ let metadata = {};
120
+ try {
121
+ const content = await fs.readFile(metadataPath, "utf-8");
122
+ metadata = JSON.parse(content);
123
+ } catch {
124
+ }
125
+ if (!metadata.external_sync) {
126
+ metadata.external_sync = {};
127
+ }
128
+ if (!metadata.external_sync.jira) {
129
+ metadata.external_sync.jira = {};
130
+ }
131
+ metadata.external_sync.jira.profile = profileName;
132
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
133
+ }
134
+ /**
135
+ * Set increment's JIRA issue key
136
+ *
137
+ * @param incrementId - Increment ID
138
+ * @param issueKey - JIRA issue key (e.g., PROJ-123)
139
+ */
140
+ async setIncrementIssueKey(incrementId, issueKey) {
141
+ const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
142
+ let metadata = {};
143
+ try {
144
+ const content = await fs.readFile(metadataPath, "utf-8");
145
+ metadata = JSON.parse(content);
146
+ } catch {
147
+ }
148
+ if (!metadata.external_sync) {
149
+ metadata.external_sync = {};
150
+ }
151
+ if (!metadata.external_sync.jira) {
152
+ metadata.external_sync.jira = {};
153
+ }
154
+ const jiraSync = metadata.external_sync.jira;
155
+ jiraSync.issueKey = issueKey;
156
+ jiraSync.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
157
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
158
+ }
159
+ /**
160
+ * List available JIRA profiles
161
+ *
162
+ * @returns Array of profile names
163
+ */
164
+ async listJiraProfiles() {
165
+ const config = await this.loadConfig();
166
+ if (!config?.sync?.profiles) {
167
+ return [];
168
+ }
169
+ const profiles = config.sync.profiles;
170
+ return Object.entries(profiles).filter(([_, p]) => p.provider === "jira").map(([name]) => name);
171
+ }
172
+ /**
173
+ * Get profile details by name
174
+ *
175
+ * @param profileName - Profile name
176
+ * @returns Profile config or null
177
+ */
178
+ async getProfileByName(profileName) {
179
+ const config = await this.loadConfig();
180
+ if (!config?.sync?.profiles) {
181
+ return null;
182
+ }
183
+ const profileConfig = config.sync.profiles[profileName];
184
+ if (!profileConfig || profileConfig.provider !== "jira") {
185
+ return null;
186
+ }
187
+ return {
188
+ profileName,
189
+ domain: profileConfig.config.domain,
190
+ projectKey: profileConfig.config.projectKey,
191
+ displayName: profileConfig.displayName,
192
+ boardId: profileConfig.config.boardId,
193
+ boardName: profileConfig.config.boardName,
194
+ instanceType: profileConfig.config.instanceType ?? "cloud"
195
+ };
196
+ }
197
+ /**
198
+ * Load config.json
199
+ */
200
+ async loadConfig() {
201
+ try {
202
+ const content = await fs.readFile(this.configPath, "utf-8");
203
+ return JSON.parse(content);
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ }
209
+ async function createJiraProfileResolver(projectRoot = process.cwd()) {
210
+ return new JiraProfileResolver(projectRoot);
211
+ }
212
+ async function resolveJiraProfile(incrementId, projectRoot = process.cwd()) {
213
+ const resolver = new JiraProfileResolver(projectRoot);
214
+ return resolver.resolveProfile(incrementId);
215
+ }
216
+ var jira_profile_resolver_default = JiraProfileResolver;
217
+ export {
218
+ JiraProfileResolver,
219
+ createJiraProfileResolver,
220
+ jira_profile_resolver_default as default,
221
+ resolveJiraProfile
222
+ };