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.
- package/dist/plugins/specweave-ado/lib/ado-permission-gate.d.ts +113 -0
- package/dist/plugins/specweave-ado/lib/ado-permission-gate.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-permission-gate.js +169 -0
- package/dist/plugins/specweave-ado/lib/ado-permission-gate.js.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts +137 -0
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js +200 -0
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js.map +1 -0
- package/dist/src/cli/commands/sync-scheduled.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-scheduled.js +1 -0
- package/dist/src/cli/commands/sync-scheduled.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts +3 -1
- package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js +9 -7
- package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js.map +1 -1
- package/dist/src/core/background/job-launcher.d.ts +5 -0
- package/dist/src/core/background/job-launcher.d.ts.map +1 -1
- package/dist/src/core/background/job-launcher.js +14 -3
- package/dist/src/core/background/job-launcher.js.map +1 -1
- package/dist/src/core/errors/index.d.ts +174 -0
- package/dist/src/core/errors/index.d.ts.map +1 -0
- package/dist/src/core/errors/index.js +238 -0
- package/dist/src/core/errors/index.js.map +1 -0
- package/dist/src/core/scheduler/session-sync-executor.d.ts +3 -0
- package/dist/src/core/scheduler/session-sync-executor.d.ts.map +1 -1
- package/dist/src/core/scheduler/session-sync-executor.js +27 -2
- package/dist/src/core/scheduler/session-sync-executor.js.map +1 -1
- package/dist/src/core/specs/spec-metadata-manager.d.ts +5 -1
- package/dist/src/core/specs/spec-metadata-manager.d.ts.map +1 -1
- package/dist/src/core/specs/spec-metadata-manager.js +4 -2
- package/dist/src/core/specs/spec-metadata-manager.js.map +1 -1
- package/dist/src/importers/item-converter.d.ts +5 -0
- package/dist/src/importers/item-converter.d.ts.map +1 -1
- package/dist/src/importers/item-converter.js +15 -4
- package/dist/src/importers/item-converter.js.map +1 -1
- package/dist/src/integrations/ado/ado-client-factory.d.ts +102 -0
- package/dist/src/integrations/ado/ado-client-factory.d.ts.map +1 -0
- package/dist/src/integrations/ado/ado-client-factory.js +115 -0
- package/dist/src/integrations/ado/ado-client-factory.js.map +1 -0
- package/dist/src/integrations/ado/ado-client.d.ts +24 -1
- package/dist/src/integrations/ado/ado-client.d.ts.map +1 -1
- package/dist/src/integrations/ado/ado-client.js +48 -17
- package/dist/src/integrations/ado/ado-client.js.map +1 -1
- package/dist/src/integrations/ado/ado-pat-provider.d.ts +45 -0
- package/dist/src/integrations/ado/ado-pat-provider.d.ts.map +1 -0
- package/dist/src/integrations/ado/ado-pat-provider.js +70 -0
- package/dist/src/integrations/ado/ado-pat-provider.js.map +1 -0
- package/dist/src/integrations/jira/jira-client.d.ts +5 -0
- package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-client.js +24 -13
- package/dist/src/integrations/jira/jira-client.js.map +1 -1
- package/dist/src/integrations/jira/jira-incremental-mapper.d.ts +5 -0
- package/dist/src/integrations/jira/jira-incremental-mapper.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-incremental-mapper.js +13 -2
- package/dist/src/integrations/jira/jira-incremental-mapper.js.map +1 -1
- package/dist/src/integrations/jira/jira-mapper.d.ts +5 -0
- package/dist/src/integrations/jira/jira-mapper.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-mapper.js +14 -3
- package/dist/src/integrations/jira/jira-mapper.js.map +1 -1
- package/dist/src/sync/sync-coordinator.d.ts +11 -0
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +80 -3
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/dist/src/testing/test-generator.d.ts +5 -0
- package/dist/src/testing/test-generator.d.ts.map +1 -1
- package/dist/src/testing/test-generator.js +17 -6
- package/dist/src/testing/test-generator.js.map +1 -1
- package/dist/src/utils/fs-native.d.ts +5 -2
- package/dist/src/utils/fs-native.d.ts.map +1 -1
- package/dist/src/utils/fs-native.js +6 -2
- package/dist/src/utils/fs-native.js.map +1 -1
- package/dist/src/utils/logger.d.ts +5 -1
- package/dist/src/utils/logger.d.ts.map +1 -1
- package/dist/src/utils/logger.js +6 -3
- package/dist/src/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/lib/vendor/utils/fs-native.d.ts +5 -2
- package/plugins/specweave/lib/vendor/utils/fs-native.js +6 -2
- package/plugins/specweave/lib/vendor/utils/fs-native.js.map +1 -1
- package/plugins/specweave/lib/vendor/utils/logger.d.ts +5 -1
- package/plugins/specweave/lib/vendor/utils/logger.js +6 -3
- package/plugins/specweave/lib/vendor/utils/logger.js.map +1 -1
- package/plugins/specweave-ado/agents/ado-manager/AGENT.md +62 -4
- package/plugins/specweave-ado/commands/specweave-ado-close-workitem.md +197 -12
- package/plugins/specweave-ado/commands/specweave-ado-create-workitem.md +148 -24
- package/plugins/specweave-ado/commands/specweave-ado-sync.md +170 -77
- package/plugins/specweave-ado/lib/ado-permission-gate.js +127 -0
- package/plugins/specweave-ado/lib/ado-permission-gate.ts +231 -0
- package/plugins/specweave-ado/lib/ado-profile-resolver.js +153 -0
- package/plugins/specweave-ado/lib/ado-profile-resolver.ts +323 -0
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +84 -0
- 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
|
|
20
|
-
- `to-ado`: SpecWeave
|
|
21
|
-
- `from-ado`: ADO
|
|
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,
|
|
41
|
-
|
|
42
|
-
###
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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: "
|
|
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 #
|
|
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/
|
|
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
|
|
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:
|
|
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
|
-
|
|
188
|
+
User: /specweave-ado:sync 0005-payment-integration
|
|
97
189
|
|
|
98
|
-
|
|
99
|
-
|
|
190
|
+
Claude:
|
|
191
|
+
Checking permissions...
|
|
192
|
+
canUpdateExternalItems: true
|
|
100
193
|
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
###
|
|
217
|
+
### Permission Denied
|
|
137
218
|
|
|
138
219
|
```
|
|
139
|
-
|
|
220
|
+
User: /specweave-ado:sync 0005 --direction to-ado
|
|
221
|
+
|
|
222
|
+
Claude:
|
|
223
|
+
Checking permissions...
|
|
224
|
+
canUpdateExternalItems: false
|
|
140
225
|
|
|
141
|
-
|
|
226
|
+
Permission Denied: ADO Write Operations Disabled
|
|
142
227
|
|
|
143
|
-
|
|
144
|
-
- T-005: Add payment tests
|
|
145
|
-
- T-006: Update documentation
|
|
228
|
+
Cannot push changes to ADO.
|
|
146
229
|
|
|
147
|
-
|
|
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;
|