specweave 0.30.0 → 0.30.2

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 (92) hide show
  1. package/dist/plugins/specweave-ado/lib/ado-permission-gate.d.ts +113 -0
  2. package/dist/plugins/specweave-ado/lib/ado-permission-gate.d.ts.map +1 -0
  3. package/dist/plugins/specweave-ado/lib/ado-permission-gate.js +169 -0
  4. package/dist/plugins/specweave-ado/lib/ado-permission-gate.js.map +1 -0
  5. package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts +137 -0
  6. package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts.map +1 -0
  7. package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js +200 -0
  8. package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js.map +1 -0
  9. package/dist/src/cli/commands/sync-scheduled.d.ts.map +1 -1
  10. package/dist/src/cli/commands/sync-scheduled.js +1 -0
  11. package/dist/src/cli/commands/sync-scheduled.js.map +1 -1
  12. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts +3 -1
  13. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts.map +1 -1
  14. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js +9 -7
  15. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js.map +1 -1
  16. package/dist/src/core/background/job-launcher.d.ts +5 -0
  17. package/dist/src/core/background/job-launcher.d.ts.map +1 -1
  18. package/dist/src/core/background/job-launcher.js +14 -3
  19. package/dist/src/core/background/job-launcher.js.map +1 -1
  20. package/dist/src/core/errors/index.d.ts +174 -0
  21. package/dist/src/core/errors/index.d.ts.map +1 -0
  22. package/dist/src/core/errors/index.js +238 -0
  23. package/dist/src/core/errors/index.js.map +1 -0
  24. package/dist/src/core/scheduler/session-sync-executor.d.ts +3 -0
  25. package/dist/src/core/scheduler/session-sync-executor.d.ts.map +1 -1
  26. package/dist/src/core/scheduler/session-sync-executor.js +27 -2
  27. package/dist/src/core/scheduler/session-sync-executor.js.map +1 -1
  28. package/dist/src/core/specs/spec-metadata-manager.d.ts +5 -1
  29. package/dist/src/core/specs/spec-metadata-manager.d.ts.map +1 -1
  30. package/dist/src/core/specs/spec-metadata-manager.js +4 -2
  31. package/dist/src/core/specs/spec-metadata-manager.js.map +1 -1
  32. package/dist/src/importers/item-converter.d.ts +5 -0
  33. package/dist/src/importers/item-converter.d.ts.map +1 -1
  34. package/dist/src/importers/item-converter.js +15 -4
  35. package/dist/src/importers/item-converter.js.map +1 -1
  36. package/dist/src/integrations/ado/ado-client-factory.d.ts +102 -0
  37. package/dist/src/integrations/ado/ado-client-factory.d.ts.map +1 -0
  38. package/dist/src/integrations/ado/ado-client-factory.js +115 -0
  39. package/dist/src/integrations/ado/ado-client-factory.js.map +1 -0
  40. package/dist/src/integrations/ado/ado-client.d.ts +24 -1
  41. package/dist/src/integrations/ado/ado-client.d.ts.map +1 -1
  42. package/dist/src/integrations/ado/ado-client.js +48 -17
  43. package/dist/src/integrations/ado/ado-client.js.map +1 -1
  44. package/dist/src/integrations/ado/ado-pat-provider.d.ts +45 -0
  45. package/dist/src/integrations/ado/ado-pat-provider.d.ts.map +1 -0
  46. package/dist/src/integrations/ado/ado-pat-provider.js +70 -0
  47. package/dist/src/integrations/ado/ado-pat-provider.js.map +1 -0
  48. package/dist/src/integrations/jira/jira-client.d.ts +5 -0
  49. package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
  50. package/dist/src/integrations/jira/jira-client.js +24 -13
  51. package/dist/src/integrations/jira/jira-client.js.map +1 -1
  52. package/dist/src/integrations/jira/jira-incremental-mapper.d.ts +5 -0
  53. package/dist/src/integrations/jira/jira-incremental-mapper.d.ts.map +1 -1
  54. package/dist/src/integrations/jira/jira-incremental-mapper.js +13 -2
  55. package/dist/src/integrations/jira/jira-incremental-mapper.js.map +1 -1
  56. package/dist/src/integrations/jira/jira-mapper.d.ts +5 -0
  57. package/dist/src/integrations/jira/jira-mapper.d.ts.map +1 -1
  58. package/dist/src/integrations/jira/jira-mapper.js +14 -3
  59. package/dist/src/integrations/jira/jira-mapper.js.map +1 -1
  60. package/dist/src/sync/sync-coordinator.d.ts +11 -0
  61. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  62. package/dist/src/sync/sync-coordinator.js +80 -3
  63. package/dist/src/sync/sync-coordinator.js.map +1 -1
  64. package/dist/src/testing/test-generator.d.ts +5 -0
  65. package/dist/src/testing/test-generator.d.ts.map +1 -1
  66. package/dist/src/testing/test-generator.js +17 -6
  67. package/dist/src/testing/test-generator.js.map +1 -1
  68. package/dist/src/utils/fs-native.d.ts +5 -2
  69. package/dist/src/utils/fs-native.d.ts.map +1 -1
  70. package/dist/src/utils/fs-native.js +6 -2
  71. package/dist/src/utils/fs-native.js.map +1 -1
  72. package/dist/src/utils/logger.d.ts +5 -1
  73. package/dist/src/utils/logger.d.ts.map +1 -1
  74. package/dist/src/utils/logger.js +6 -3
  75. package/dist/src/utils/logger.js.map +1 -1
  76. package/package.json +1 -1
  77. package/plugins/specweave/lib/vendor/utils/fs-native.d.ts +5 -2
  78. package/plugins/specweave/lib/vendor/utils/fs-native.js +6 -2
  79. package/plugins/specweave/lib/vendor/utils/fs-native.js.map +1 -1
  80. package/plugins/specweave/lib/vendor/utils/logger.d.ts +5 -1
  81. package/plugins/specweave/lib/vendor/utils/logger.js +6 -3
  82. package/plugins/specweave/lib/vendor/utils/logger.js.map +1 -1
  83. package/plugins/specweave-ado/agents/ado-manager/AGENT.md +62 -4
  84. package/plugins/specweave-ado/commands/specweave-ado-close-workitem.md +197 -12
  85. package/plugins/specweave-ado/commands/specweave-ado-create-workitem.md +148 -24
  86. package/plugins/specweave-ado/commands/specweave-ado-sync.md +170 -77
  87. package/plugins/specweave-ado/lib/ado-permission-gate.js +127 -0
  88. package/plugins/specweave-ado/lib/ado-permission-gate.ts +231 -0
  89. package/plugins/specweave-ado/lib/ado-profile-resolver.js +153 -0
  90. package/plugins/specweave-ado/lib/ado-profile-resolver.ts +323 -0
  91. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +84 -0
  92. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +126 -0
@@ -16,9 +16,9 @@ description: Two-way sync between SpecWeave increment and Azure DevOps work item
16
16
  ## Options
17
17
 
18
18
  - `--direction <mode>`: Sync direction (default: `two-way`)
19
- - `two-way`: SpecWeave ADO (default - recommended)
20
- - `to-ado`: SpecWeave ADO only (push progress)
21
- - `from-ado`: ADO SpecWeave only (pull updates)
19
+ - `two-way`: SpecWeave <-> ADO (default - recommended)
20
+ - `to-ado`: SpecWeave -> ADO only (push progress)
21
+ - `from-ado`: ADO -> SpecWeave only (pull updates)
22
22
 
23
23
  ## Examples
24
24
 
@@ -37,112 +37,205 @@ description: Two-way sync between SpecWeave increment and Azure DevOps work item
37
37
 
38
38
  ## Command Behavior
39
39
 
40
- When user runs this command, invoke `ado-manager` agent to perform two-way sync:
41
-
42
- ### Phase 1: Pull FROM ADO (default behavior)
43
- 1. Fetch work item state from ADO API
44
- 2. Detect changes in ADO:
45
- - State changes (New → Active → Resolved → Closed)
46
- - Priority changes
47
- - Iteration/sprint changes
48
- - Comments from team members
49
- - Field updates
50
- 3. Apply ADO changes to SpecWeave increment:
51
- - Update increment status to match ADO state
52
- - Update priority in metadata
53
- - Import team comments to increment notes
54
- - Update iteration tracking
55
-
56
- ### Phase 2: Push TO ADO (default behavior)
57
- 1. Read tasks.md from increment
58
- 2. Calculate completion percentage
59
- 3. Identify recently completed tasks
60
- 4. Format progress update comment
61
- 5. POST comment to ADO work item
62
- 6. Update work item state if needed (New → Active → Resolved)
63
- 7. Update custom fields (completion %, current task, etc.)
64
-
65
- **Agent Invocation**:
40
+ When user runs this command, Claude should:
41
+
42
+ ### 1. Check Permission Gate (MANDATORY FIRST STEP)
43
+
44
+ **Before ANY ADO write operations**, check permissions:
45
+
46
+ ```typescript
47
+ // Read .specweave/config.json
48
+ const config = JSON.parse(await fs.readFile('.specweave/config.json', 'utf-8'));
49
+ const canUpdateExternal = config?.sync?.settings?.canUpdateExternalItems ?? false;
50
+ const canUpdateStatus = config?.sync?.settings?.canUpdateStatus ?? false;
51
+
52
+ // Permission check based on direction
53
+ if (direction === 'to-ado' || direction === 'two-way') {
54
+ if (!canUpdateExternal) {
55
+ console.log(`
56
+ Permission Denied: ADO Write Operations Disabled
57
+
58
+ Cannot push changes to ADO (sync.settings.canUpdateExternalItems = false).
59
+
60
+ Options:
61
+ 1. Enable writes: Set canUpdateExternalItems to true in config.json
62
+ 2. Pull-only mode: /specweave-ado:sync ${incrementId} --direction from-ado
63
+ `);
64
+ return;
65
+ }
66
+ }
67
+ ```
68
+
69
+ For `--direction from-ado` (pull-only), permission check is skipped as it's read-only.
70
+
71
+ ### 2. Resolve ADO Profile
72
+
73
+ Use the increment's stored profile or fall back to global activeProfile:
74
+
75
+ ```typescript
76
+ // Load increment metadata
77
+ const metadataPath = `.specweave/increments/${incrementId}/metadata.json`;
78
+ const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf-8'));
79
+
80
+ // Priority: increment profile > global activeProfile
81
+ let profileName = metadata?.external_sync?.ado?.profile;
82
+ if (!profileName) {
83
+ profileName = config?.sync?.activeProfile;
84
+ }
85
+
86
+ // Validate profile exists
87
+ const profileConfig = config?.sync?.profiles?.[profileName];
88
+ if (!profileConfig || profileConfig.provider !== 'ado') {
89
+ console.log(`❌ ADO profile "${profileName}" not found`);
90
+ console.log('Available ADO profiles:', Object.entries(config?.sync?.profiles || {})
91
+ .filter(([_, p]) => p.provider === 'ado')
92
+ .map(([name]) => name)
93
+ .join(', '));
94
+ return;
95
+ }
96
+
97
+ const { organization, project } = profileConfig.config;
98
+ console.log(`Using ADO profile: ${profileName}`);
99
+ console.log(` Organization: ${organization}`);
100
+ console.log(` Project: ${project}`);
101
+ ```
102
+
103
+ ### 3. Invoke ADO Manager Agent
104
+
66
105
  ```
67
106
  Use Task tool with subagent_type: "specweave-ado:ado-manager:ado-manager"
68
107
 
69
- Prompt: "Two-way sync for increment 0005-payment-integration with ADO.
108
+ Prompt: "{direction} sync for increment {increment-id} with ADO.
109
+
110
+ IMPORTANT:
111
+ - Permission verified: canUpdateExternalItems={canUpdateExternal}
112
+ - Using profile: {profileName} (org: {organization}, project: {project})
70
113
 
71
114
  Phase 1 - Pull FROM ADO:
72
- 1. Fetch work item #12345 from ADO API
115
+ 1. Fetch work item #{workItemId} from ADO API
73
116
  2. Detect changes: state, priority, iteration, comments
74
117
  3. Apply ADO changes to increment metadata
75
118
  4. Import team comments to increment notes
76
119
 
77
- Phase 2 - Push TO ADO:
78
- 1. Read .specweave/increments/0005/tasks.md
120
+ Phase 2 - Push TO ADO (if direction allows):
121
+ 1. Read .specweave/increments/{increment-id}/tasks.md
79
122
  2. Calculate: X/Y tasks complete (Z%)
80
123
  3. Identify: Recently completed tasks
81
124
  4. Format comment with progress update
82
- 5. Load work item ID from increment-metadata.json
83
- 6. POST comment to ADO API
125
+ 5. Load work item ID from metadata.json
126
+ 6. POST comment to ADO API using org: {organization}, project: {project}
84
127
  7. Update work item state/fields
85
128
 
86
- Display: Two-way sync summary"
129
+ Display: Sync summary with profile used"
130
+ ```
131
+
132
+ ### 4. Display Result
133
+
134
+ ```
135
+ Sync Summary for increment {increment-id}
136
+
137
+ Profile: {profileName}
138
+ Organization: {organization}
139
+ Project: {project}
140
+
141
+ Direction: {direction}
142
+
143
+ FROM ADO:
144
+ State: Active -> Resolved
145
+ Priority: 2 -> 1
146
+ Comments: 3 new
147
+
148
+ TO ADO:
149
+ Progress: 60% (6/10 tasks)
150
+ Posted comment #98765
151
+
152
+ Work Item: https://dev.azure.com/{organization}/{project}/_workitems/edit/{workItemId}
87
153
  ```
88
154
 
89
155
  ---
90
156
 
157
+ ## Permission Check Matrix
158
+
159
+ | Direction | canUpdateExternalItems | Result |
160
+ |-----------|------------------------|--------|
161
+ | from-ado | any | Allowed (read-only) |
162
+ | to-ado | false | Denied |
163
+ | to-ado | true | Allowed |
164
+ | two-way | false | Denied |
165
+ | two-way | true | Allowed |
166
+
167
+ ---
168
+
169
+ ## Profile Resolution
170
+
171
+ The command resolves the ADO profile in this order:
172
+
173
+ 1. **Increment profile** (metadata.json -> external_sync.ado.profile)
174
+ 2. **Global profile** (config.json -> sync.activeProfile)
175
+
176
+ This allows:
177
+ - Different increments to sync to different ADO projects
178
+ - No manual `activeProfile` switching needed
179
+ - Automatic project targeting
180
+
181
+ ---
182
+
91
183
  ## Example Output
92
184
 
93
185
  ### Two-way Sync (Default)
94
186
 
95
187
  ```
96
- 🔄 Two-way sync for increment 0005...
188
+ User: /specweave-ado:sync 0005-payment-integration
97
189
 
98
- ✓ Azure DevOps work item: #12345
99
- Sync direction: Two-way (push & pull)
190
+ Claude:
191
+ Checking permissions...
192
+ canUpdateExternalItems: true
100
193
 
101
- Detecting changes (both directions)...
194
+ Resolving ADO profile...
195
+ Using: ado-nova-x-sandbox (from increment)
196
+ Organization: nova-systems
197
+ Project: Nova X Sandbox
198
+
199
+ Syncing...
102
200
 
103
201
  FROM ADO:
104
- Work item state changed: Active Resolved
105
- Iteration updated: Sprint 23 Sprint 24
106
- Priority changed: 2 1
107
- 3 new comments from team
108
-
109
- FROM SpecWeave:
110
- 2 new tasks completed (T-005, T-006)
111
- Progress: 40% → 60% (6/10 tasks)
112
- Current task: T-007
113
-
114
- Syncing TO ADO...
115
- ✓ Posted progress comment (ID: 98765)
116
- Updated completion: 60%
117
- ✓ Updated current task field: T-007
118
-
119
- Syncing FROM ADO...
120
- ✓ Updated increment status: active → completed
121
- ✓ Updated priority: P2 → P1
122
- ✓ Updated iteration tracking: Sprint 24
123
- ✓ Imported 3 team comments to increment notes
124
-
125
- ✅ Bidirectional Sync Complete!
126
-
127
- SpecWeave ↔ ADO synchronized
128
- • Pushed: Progress (60%), 2 task updates
129
- • Pulled: State (Resolved), priority (P1), iteration, 3 comments
130
-
131
- ADO Work Item: https://dev.azure.com/myorg/MyProject/_workitems/edit/12345
132
- Last synced: just now
133
- Next sync: Automatic (hook-based) or manual when ready
202
+ State changed: Active -> Resolved
203
+ Iteration updated: Sprint 23 -> Sprint 24
204
+ Priority changed: 2 -> 1
205
+ 3 new comments from team
206
+
207
+ TO ADO:
208
+ Progress: 60% complete (6/10 tasks)
209
+ Posted comment (ID: 98765)
210
+ Updated completion field: 60%
211
+
212
+ Sync Complete!
213
+ Profile: ado-nova-x-sandbox
214
+ Work Item: https://dev.azure.com/nova-systems/Nova%20X%20Sandbox/_workitems/edit/12345
134
215
  ```
135
216
 
136
- ### One-Way Sync (to-ado)
217
+ ### Permission Denied
137
218
 
138
219
  ```
139
- Pushed to ADO Work Item #12345
220
+ User: /specweave-ado:sync 0005 --direction to-ado
221
+
222
+ Claude:
223
+ Checking permissions...
224
+ canUpdateExternalItems: false
140
225
 
141
- Progress: 60% complete (6/10 tasks)
226
+ Permission Denied: ADO Write Operations Disabled
142
227
 
143
- Recently Completed:
144
- - T-005: Add payment tests
145
- - T-006: Update documentation
228
+ Cannot push changes to ADO.
146
229
 
147
- URL: https://dev.azure.com/myorg/MyProject/_workitems/edit/12345
230
+ Options:
231
+ 1. Enable writes: Set sync.settings.canUpdateExternalItems = true
232
+ 2. Pull-only: /specweave-ado:sync 0005 --direction from-ado
148
233
  ```
234
+
235
+ ---
236
+
237
+ ## Related
238
+
239
+ - `/specweave-ado:create-workitem` - Create new ADO work item
240
+ - `/specweave-ado:status` - Check sync status (read-only, always allowed)
241
+ - `/specweave-ado:close-workitem` - Close work item when complete
@@ -0,0 +1,127 @@
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 AdoPermissionGate {
9
+ constructor(settings, configPath) {
10
+ this.settings = settings;
11
+ this.configPath = configPath;
12
+ }
13
+ /**
14
+ * Check if write operations (create/update work items) 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: External tool 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 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: Status updates 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
+ * Get current settings
72
+ */
73
+ getSettings() {
74
+ return { ...this.settings };
75
+ }
76
+ /**
77
+ * Get human-readable permission summary
78
+ */
79
+ getPermissionSummary() {
80
+ const parts = [];
81
+ if (this.settings.canUpdateExternalItems) {
82
+ parts.push("create/update ADO items");
83
+ }
84
+ if (this.settings.canUpdateStatus) {
85
+ parts.push("update status");
86
+ }
87
+ if (this.settings.canUpsertInternalItems) {
88
+ parts.push("create internal items");
89
+ }
90
+ if (parts.length === 0) {
91
+ return "All ADO write operations disabled (read-only mode)";
92
+ }
93
+ return `Allowed: ${parts.join(", ")}`;
94
+ }
95
+ }
96
+ async function createAdoPermissionGate(projectRoot = process.cwd()) {
97
+ const configPath = path.join(projectRoot, ".specweave", "config.json");
98
+ try {
99
+ const content = await fs.readFile(configPath, "utf-8");
100
+ const config = JSON.parse(content);
101
+ const settings = {
102
+ canUpsertInternalItems: config?.sync?.settings?.canUpsertInternalItems ?? false,
103
+ canUpdateExternalItems: config?.sync?.settings?.canUpdateExternalItems ?? false,
104
+ canUpdateStatus: config?.sync?.settings?.canUpdateStatus ?? false
105
+ };
106
+ return new AdoPermissionGate(settings, configPath);
107
+ } catch {
108
+ return new AdoPermissionGate(DEFAULT_SYNC_SETTINGS, configPath);
109
+ }
110
+ }
111
+ async function canWriteToAdo(projectRoot = process.cwd()) {
112
+ const gate = await createAdoPermissionGate(projectRoot);
113
+ return gate.checkWritePermission();
114
+ }
115
+ async function canUpdateAdoStatus(projectRoot = process.cwd()) {
116
+ const gate = await createAdoPermissionGate(projectRoot);
117
+ return gate.checkStatusPermission();
118
+ }
119
+ var ado_permission_gate_default = AdoPermissionGate;
120
+ export {
121
+ AdoPermissionGate,
122
+ DEFAULT_SYNC_SETTINGS,
123
+ canUpdateAdoStatus,
124
+ canWriteToAdo,
125
+ createAdoPermissionGate,
126
+ ado_permission_gate_default as default
127
+ };
@@ -0,0 +1,231 @@
1
+ /**
2
+ * ADO Permission Gate
3
+ *
4
+ * Validates that sync permissions are enabled before allowing ADO write operations.
5
+ * This ensures manual ADO commands respect the same permission settings as the
6
+ * sync-coordinator.
7
+ *
8
+ * Usage:
9
+ * ```typescript
10
+ * const gate = await createAdoPermissionGate();
11
+ * const result = gate.checkWritePermission();
12
+ * if (!result.allowed) {
13
+ * console.log(result.reason);
14
+ * return;
15
+ * }
16
+ * ```
17
+ *
18
+ * @module ado-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
+ * ADO Permission Gate
69
+ *
70
+ * Checks permission settings before allowing ADO write operations.
71
+ */
72
+ export class AdoPermissionGate {
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 work items) 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: External tool 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 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: Status updates 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
+ * Get current settings
146
+ */
147
+ getSettings(): SyncSettings {
148
+ return { ...this.settings };
149
+ }
150
+
151
+ /**
152
+ * Get human-readable permission summary
153
+ */
154
+ getPermissionSummary(): string {
155
+ const parts: string[] = [];
156
+
157
+ if (this.settings.canUpdateExternalItems) {
158
+ parts.push('create/update ADO items');
159
+ }
160
+ if (this.settings.canUpdateStatus) {
161
+ parts.push('update status');
162
+ }
163
+ if (this.settings.canUpsertInternalItems) {
164
+ parts.push('create internal items');
165
+ }
166
+
167
+ if (parts.length === 0) {
168
+ return 'All ADO write operations disabled (read-only mode)';
169
+ }
170
+
171
+ return `Allowed: ${parts.join(', ')}`;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Create an AdoPermissionGate from config.json
177
+ *
178
+ * @param projectRoot - Project root directory (defaults to cwd)
179
+ * @returns AdoPermissionGate instance
180
+ */
181
+ export async function createAdoPermissionGate(
182
+ projectRoot: string = process.cwd()
183
+ ): Promise<AdoPermissionGate> {
184
+ const configPath = path.join(projectRoot, '.specweave', 'config.json');
185
+
186
+ try {
187
+ const content = await fs.readFile(configPath, 'utf-8');
188
+ const config = JSON.parse(content);
189
+
190
+ const settings: SyncSettings = {
191
+ canUpsertInternalItems: config?.sync?.settings?.canUpsertInternalItems ?? false,
192
+ canUpdateExternalItems: config?.sync?.settings?.canUpdateExternalItems ?? false,
193
+ canUpdateStatus: config?.sync?.settings?.canUpdateStatus ?? false,
194
+ };
195
+
196
+ return new AdoPermissionGate(settings, configPath);
197
+ } catch {
198
+ // Return gate with default (disabled) settings if config not found
199
+ return new AdoPermissionGate(DEFAULT_SYNC_SETTINGS, configPath);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Quick check: Are ADO write operations allowed?
205
+ *
206
+ * Convenience function for simple permission checks.
207
+ *
208
+ * @param projectRoot - Project root directory
209
+ * @returns Permission check result
210
+ */
211
+ export async function canWriteToAdo(
212
+ projectRoot: string = process.cwd()
213
+ ): Promise<PermissionCheckResult> {
214
+ const gate = await createAdoPermissionGate(projectRoot);
215
+ return gate.checkWritePermission();
216
+ }
217
+
218
+ /**
219
+ * Quick check: Are ADO status updates allowed?
220
+ *
221
+ * @param projectRoot - Project root directory
222
+ * @returns Permission check result
223
+ */
224
+ export async function canUpdateAdoStatus(
225
+ projectRoot: string = process.cwd()
226
+ ): Promise<PermissionCheckResult> {
227
+ const gate = await createAdoPermissionGate(projectRoot);
228
+ return gate.checkStatusPermission();
229
+ }
230
+
231
+ export default AdoPermissionGate;