specweave 1.0.351 → 1.0.352
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/bin/specweave.js +9 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts +5 -0
- package/dist/plugins/specweave-ado/lib/ado-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js +61 -23
- package/dist/plugins/specweave-ado/lib/ado-client-v2.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +3 -2
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js +2 -1
- package/dist/plugins/specweave-ado/lib/ado-profile-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +25 -9
- package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/dist/plugins/specweave-ado/lib/conflict-resolver.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js +14 -1
- package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +10 -7
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +7 -5
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +13 -3
- package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts +24 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +36 -20
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-graphql-client.js +32 -22
- package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +144 -8
- package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts +8 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-spec-sync.js +94 -24
- package/dist/plugins/specweave-github/lib/github-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +1 -0
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/per-us-sync.js +29 -9
- package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts +59 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js +159 -0
- package/dist/plugins/specweave-jira/lib/content-format-adapter.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts +45 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js +92 -0
- package/dist/plugins/specweave-jira/lib/jira-deployment-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +13 -28
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +2 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +19 -7
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts +47 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js +110 -0
- package/dist/plugins/specweave-jira/lib/jira-field-discovery.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts +26 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js +77 -0
- package/dist/plugins/specweave-jira/lib/jira-paginated-search.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/dist/plugins/specweave-jira/lib/jira-spec-commit-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +17 -2
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +103 -33
- package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +4 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +19 -6
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts +29 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js +73 -0
- package/dist/plugins/specweave-jira/lib/metadata-paths.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts +15 -2
- package/dist/plugins/specweave-jira/lib/reorganization-detector.d.ts.map +1 -1
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js +121 -33
- package/dist/plugins/specweave-jira/lib/reorganization-detector.js.map +1 -1
- package/dist/src/cli/commands/sync-progress.d.ts +6 -0
- package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
- package/dist/src/cli/commands/sync-progress.js +37 -0
- package/dist/src/cli/commands/sync-progress.js.map +1 -1
- package/dist/src/cli/commands/sync-task.d.ts +16 -0
- package/dist/src/cli/commands/sync-task.d.ts.map +1 -0
- package/dist/src/cli/commands/sync-task.js +42 -0
- package/dist/src/cli/commands/sync-task.js.map +1 -0
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts +9 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.d.ts.map +1 -1
- package/dist/src/core/hooks/LifecycleHookDispatcher.js +26 -8
- package/dist/src/core/hooks/LifecycleHookDispatcher.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +26 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/status-commands.d.ts.map +1 -1
- package/dist/src/core/increment/status-commands.js +33 -11
- package/dist/src/core/increment/status-commands.js.map +1 -1
- package/dist/src/locales/de/cli.json +245 -77
- package/dist/src/locales/es/cli.json +238 -3
- package/dist/src/locales/fr/cli.json +254 -86
- package/dist/src/locales/ja/cli.json +246 -78
- package/dist/src/locales/ko/cli.json +246 -78
- package/dist/src/locales/pt/cli.json +245 -77
- package/dist/src/locales/zh/cli.json +251 -83
- package/dist/src/sync/github-reconciler.d.ts +30 -0
- package/dist/src/sync/github-reconciler.d.ts.map +1 -1
- package/dist/src/sync/github-reconciler.js +235 -1
- package/dist/src/sync/github-reconciler.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +31 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +26 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.d.ts +30 -0
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js +235 -1
- package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
- package/plugins/specweave/skills/team-lead/agents/security.md +0 -1
- package/plugins/specweave-ado/hooks/post-task-completion.sh +2 -2
- package/plugins/specweave-ado/lib/ado-client-v2.js +51 -21
- package/plugins/specweave-ado/lib/ado-client-v2.ts +62 -23
- package/plugins/specweave-ado/lib/ado-duplicate-detector.js +4 -4
- package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +4 -3
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.js +54 -12
- package/plugins/specweave-ado/lib/ado-hierarchical-sync.ts +88 -18
- package/plugins/specweave-ado/lib/ado-profile-resolver.js +1 -1
- package/plugins/specweave-ado/lib/ado-profile-resolver.ts +3 -1
- package/plugins/specweave-ado/lib/ado-spec-sync.js +22 -9
- package/plugins/specweave-ado/lib/ado-spec-sync.ts +27 -9
- package/plugins/specweave-ado/lib/conflict-resolver.js +17 -1
- package/plugins/specweave-ado/lib/conflict-resolver.ts +17 -1
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +11 -1
- package/plugins/specweave-ado/lib/per-us-sync.js +8 -1
- package/plugins/specweave-ado/lib/per-us-sync.ts +17 -2
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +28 -2
- package/plugins/specweave-github/hooks/post-task-completion.sh +6 -3
- package/plugins/specweave-github/lib/enhanced-github-sync.js +35 -6
- package/plugins/specweave-github/lib/github-board-resolver.js +4 -4
- package/plugins/specweave-github/lib/github-board-resolver.ts +4 -4
- package/plugins/specweave-github/lib/github-client-v2.js +6 -6
- package/plugins/specweave-github/lib/github-client-v2.ts +11 -7
- package/plugins/specweave-github/lib/github-client.js +5 -4
- package/plugins/specweave-github/lib/github-client.ts +7 -5
- package/plugins/specweave-github/lib/github-cross-repo-sync.js +17 -3
- package/plugins/specweave-github/lib/github-cross-repo-sync.ts +16 -3
- package/plugins/specweave-github/lib/github-feature-sync-cli.js +20 -11
- package/plugins/specweave-github/lib/github-feature-sync-cli.ts +42 -20
- package/plugins/specweave-github/lib/github-graphql-client.js +29 -20
- package/plugins/specweave-github/lib/github-graphql-client.ts +34 -22
- package/plugins/specweave-github/lib/github-hierarchical-sync.js +2 -2
- package/plugins/specweave-github/lib/github-hierarchical-sync.ts +2 -2
- package/plugins/specweave-github/lib/github-multi-project-sync.js +23 -7
- package/plugins/specweave-github/lib/github-multi-project-sync.ts +26 -8
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +110 -5
- package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +135 -9
- package/plugins/specweave-github/lib/github-spec-sync.js +85 -24
- package/plugins/specweave-github/lib/github-spec-sync.ts +100 -26
- package/plugins/specweave-github/lib/github-sync-orchestrator.js +2 -1
- package/plugins/specweave-github/lib/github-sync-orchestrator.ts +3 -1
- package/plugins/specweave-github/lib/github-us-auto-closer.js +25 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +43 -0
- package/plugins/specweave-github/lib/per-us-sync.js +26 -11
- package/plugins/specweave-github/lib/per-us-sync.ts +29 -11
- package/plugins/specweave-jira/hooks/post-task-completion.sh +2 -1
- package/plugins/specweave-jira/lib/content-format-adapter.js +116 -0
- package/plugins/specweave-jira/lib/content-format-adapter.ts +189 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +21 -5
- package/plugins/specweave-jira/lib/jira-deployment-detector.js +63 -0
- package/plugins/specweave-jira/lib/jira-deployment-detector.ts +113 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.js +12 -29
- package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +13 -27
- package/plugins/specweave-jira/lib/jira-epic-sync.js +15 -5
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +22 -7
- package/plugins/specweave-jira/lib/jira-field-discovery.js +76 -0
- package/plugins/specweave-jira/lib/jira-field-discovery.ts +139 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.js +10 -0
- package/plugins/specweave-jira/lib/jira-hierarchical-sync.ts +11 -0
- package/plugins/specweave-jira/lib/jira-multi-project-sync.js +19 -9
- package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +25 -14
- package/plugins/specweave-jira/lib/jira-paginated-search.js +55 -0
- package/plugins/specweave-jira/lib/jira-paginated-search.ts +108 -0
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.js +5 -3
- package/plugins/specweave-jira/lib/jira-spec-commit-sync.ts +5 -3
- package/plugins/specweave-jira/lib/jira-spec-sync.js +102 -31
- package/plugins/specweave-jira/lib/jira-spec-sync.ts +123 -45
- package/plugins/specweave-jira/lib/jira-status-sync.js +18 -5
- package/plugins/specweave-jira/lib/jira-status-sync.ts +21 -6
- package/plugins/specweave-jira/lib/metadata-paths.js +38 -0
- package/plugins/specweave-jira/lib/metadata-paths.ts +73 -0
- package/plugins/specweave-jira/lib/reorganization-detector.js +101 -23
- package/plugins/specweave-jira/lib/reorganization-detector.ts +125 -35
- package/plugins/specweave-jira/scripts/refresh-cache.js +1 -1
- package/plugins/specweave-jira/scripts/refresh-cache.ts +2 -2
- package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +3 -5
|
@@ -33,16 +33,15 @@ async function buildHierarchicalWIQL(organization, pat, containers) {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
if (container.filters) {
|
|
37
|
+
const containerFilterClauses = buildFilterClauses(container.filters);
|
|
38
|
+
if (containerFilterClauses.length > 0) {
|
|
39
|
+
parts.push(...containerFilterClauses);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
36
42
|
projectClauses.push(`(${parts.join(" AND ")})`);
|
|
37
43
|
}
|
|
38
44
|
let whereClause = projectClauses.join(" OR ");
|
|
39
|
-
const filters = containers[0]?.filters;
|
|
40
|
-
if (filters) {
|
|
41
|
-
const filterClauses = buildFilterClauses(filters);
|
|
42
|
-
if (filterClauses.length > 0) {
|
|
43
|
-
whereClause = `(${whereClause}) AND ${filterClauses.join(" AND ")}`;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
45
|
return `
|
|
47
46
|
SELECT [System.Id], [System.Title], [System.Description], [System.State],
|
|
48
47
|
[System.CreatedDate], [System.ChangedDate], [System.WorkItemType],
|
|
@@ -91,11 +90,18 @@ function addTimeRangeFilter(wiql, timeRange) {
|
|
|
91
90
|
}
|
|
92
91
|
const { since, until } = calculateTimeRange(timeRange);
|
|
93
92
|
const timeFilter = `[System.CreatedDate] >= '${since}' AND [System.CreatedDate] <= '${until}'`;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
const orderByRegex = /\bORDER\s+BY\b/i;
|
|
94
|
+
const match = wiql.match(orderByRegex);
|
|
95
|
+
if (match && match.index !== void 0) {
|
|
96
|
+
const before = wiql.substring(0, match.index).trimEnd();
|
|
97
|
+
const after = wiql.substring(match.index);
|
|
98
|
+
return `${before} AND ${timeFilter} ${after}`;
|
|
99
|
+
}
|
|
100
|
+
const whereRegex = /\bWHERE\b/i;
|
|
101
|
+
if (whereRegex.test(wiql)) {
|
|
97
102
|
return `${wiql} AND ${timeFilter}`;
|
|
98
103
|
}
|
|
104
|
+
return `${wiql} WHERE ${timeFilter}`;
|
|
99
105
|
}
|
|
100
106
|
function calculateTimeRange(timeRange) {
|
|
101
107
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -179,6 +185,10 @@ async function fetchWorkItemsFiltered(config, pat, timeRange) {
|
|
|
179
185
|
const baseWiql = await buildHierarchicalWIQL(organization, pat, containers);
|
|
180
186
|
const wiql = addTimeRangeFilter(baseWiql, timeRange);
|
|
181
187
|
console.log("\u{1F50D} Fetching work items (FILTERED strategy):", wiql);
|
|
188
|
+
const uniqueProjects = new Set(containers.map((c) => c.id));
|
|
189
|
+
if (uniqueProjects.size > 1) {
|
|
190
|
+
return executeQueryOrgLevel(organization, pat, wiql);
|
|
191
|
+
}
|
|
182
192
|
const project = containers[0].id;
|
|
183
193
|
return executeQuery(organization, project, pat, wiql);
|
|
184
194
|
}
|
|
@@ -208,7 +218,34 @@ async function executeQuery(organization, project, pat, wiql) {
|
|
|
208
218
|
});
|
|
209
219
|
return response.value || [];
|
|
210
220
|
}
|
|
211
|
-
function
|
|
221
|
+
async function executeQueryOrgLevel(organization, pat, wiql) {
|
|
222
|
+
const baseUrl = `https://dev.azure.com/${organization}`;
|
|
223
|
+
const queryUrl = `${baseUrl}/_apis/wit/wiql?api-version=7.1`;
|
|
224
|
+
const queryResult = await makeRequest(queryUrl, pat, "POST", { query: wiql });
|
|
225
|
+
if (!queryResult.workItems || queryResult.workItems.length === 0) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
const ids = queryResult.workItems.map((wi) => wi.id);
|
|
229
|
+
const batchUrl = `${baseUrl}/_apis/wit/workitemsbatch?api-version=7.1`;
|
|
230
|
+
const response = await makeRequest(batchUrl, pat, "POST", {
|
|
231
|
+
ids,
|
|
232
|
+
fields: [
|
|
233
|
+
"System.Id",
|
|
234
|
+
"System.Title",
|
|
235
|
+
"System.Description",
|
|
236
|
+
"System.State",
|
|
237
|
+
"System.CreatedDate",
|
|
238
|
+
"System.ChangedDate",
|
|
239
|
+
"System.WorkItemType",
|
|
240
|
+
"System.AreaPath",
|
|
241
|
+
"System.IterationPath",
|
|
242
|
+
"System.Tags"
|
|
243
|
+
]
|
|
244
|
+
});
|
|
245
|
+
return response.value || [];
|
|
246
|
+
}
|
|
247
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
248
|
+
function makeRequest(url, pat, method = "GET", body, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
212
249
|
return new Promise((resolve, reject) => {
|
|
213
250
|
const { hostname, pathname, search } = new URL(url);
|
|
214
251
|
const authHeader = "Basic " + Buffer.from(`:${pat}`).toString("base64");
|
|
@@ -223,7 +260,8 @@ function makeRequest(url, pat, method = "GET", body) {
|
|
|
223
260
|
hostname,
|
|
224
261
|
path: pathname + search,
|
|
225
262
|
method,
|
|
226
|
-
headers
|
|
263
|
+
headers,
|
|
264
|
+
timeout: timeoutMs
|
|
227
265
|
};
|
|
228
266
|
const req = https.request(options, (res) => {
|
|
229
267
|
let data = "";
|
|
@@ -243,6 +281,10 @@ function makeRequest(url, pat, method = "GET", body) {
|
|
|
243
281
|
}
|
|
244
282
|
});
|
|
245
283
|
});
|
|
284
|
+
req.on("timeout", () => {
|
|
285
|
+
req.destroy();
|
|
286
|
+
reject(new Error(`ADO request timed out after ${timeoutMs}ms: ${method} ${url}`));
|
|
287
|
+
});
|
|
246
288
|
req.on("error", (error) => {
|
|
247
289
|
reject(error);
|
|
248
290
|
});
|
|
@@ -81,6 +81,14 @@ export async function buildHierarchicalWIQL(
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Per-container filters (each container applies its own filters)
|
|
85
|
+
if (container.filters) {
|
|
86
|
+
const containerFilterClauses = buildFilterClauses(container.filters);
|
|
87
|
+
if (containerFilterClauses.length > 0) {
|
|
88
|
+
parts.push(...containerFilterClauses);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
// Combine parts with AND
|
|
85
93
|
projectClauses.push(`(${parts.join(' AND ')})`);
|
|
86
94
|
}
|
|
@@ -88,15 +96,6 @@ export async function buildHierarchicalWIQL(
|
|
|
88
96
|
// Build WHERE clause with project clauses
|
|
89
97
|
let whereClause = projectClauses.join(' OR ');
|
|
90
98
|
|
|
91
|
-
// Add global filters (apply to all projects)
|
|
92
|
-
const filters = containers[0]?.filters;
|
|
93
|
-
if (filters) {
|
|
94
|
-
const filterClauses = buildFilterClauses(filters);
|
|
95
|
-
if (filterClauses.length > 0) {
|
|
96
|
-
whereClause = `(${whereClause}) AND ${filterClauses.join(' AND ')}`;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
99
|
// Build complete WIQL query
|
|
101
100
|
return `
|
|
102
101
|
SELECT [System.Id], [System.Title], [System.Description], [System.State],
|
|
@@ -185,15 +184,29 @@ function addTimeRangeFilter(wiql: string, timeRange: string): string {
|
|
|
185
184
|
|
|
186
185
|
const { since, until } = calculateTimeRange(timeRange as TimeRangePreset);
|
|
187
186
|
|
|
188
|
-
// Add time range to WHERE clause
|
|
189
187
|
const timeFilter = `[System.CreatedDate] >= '${since}' AND [System.CreatedDate] <= '${until}'`;
|
|
190
188
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
// Find the last ORDER BY outside of string literals (case-insensitive)
|
|
190
|
+
// Strategy: find ORDER BY keyword, ensuring it's not inside single quotes
|
|
191
|
+
const orderByRegex = /\bORDER\s+BY\b/i;
|
|
192
|
+
const match = wiql.match(orderByRegex);
|
|
193
|
+
|
|
194
|
+
if (match && match.index !== undefined) {
|
|
195
|
+
// Insert AND timeFilter before ORDER BY
|
|
196
|
+
const before = wiql.substring(0, match.index).trimEnd();
|
|
197
|
+
const after = wiql.substring(match.index);
|
|
198
|
+
return `${before} AND ${timeFilter} ${after}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// No ORDER BY — check if there's a WHERE clause
|
|
202
|
+
const whereRegex = /\bWHERE\b/i;
|
|
203
|
+
if (whereRegex.test(wiql)) {
|
|
204
|
+
// Append to existing WHERE clause
|
|
195
205
|
return `${wiql} AND ${timeFilter}`;
|
|
196
206
|
}
|
|
207
|
+
|
|
208
|
+
// No WHERE clause at all — add one
|
|
209
|
+
return `${wiql} WHERE ${timeFilter}`;
|
|
197
210
|
}
|
|
198
211
|
|
|
199
212
|
/**
|
|
@@ -364,9 +377,15 @@ async function fetchWorkItemsFiltered(
|
|
|
364
377
|
|
|
365
378
|
console.log('🔍 Fetching work items (FILTERED strategy):', wiql);
|
|
366
379
|
|
|
367
|
-
//
|
|
368
|
-
const
|
|
380
|
+
// Detect multi-project: if containers span more than one project, use org-level endpoint
|
|
381
|
+
const uniqueProjects = new Set(containers.map(c => c.id));
|
|
382
|
+
if (uniqueProjects.size > 1) {
|
|
383
|
+
// Cross-project: use org-level WIQL endpoint
|
|
384
|
+
return executeQueryOrgLevel(organization, pat, wiql);
|
|
385
|
+
}
|
|
369
386
|
|
|
387
|
+
// Single-project: use project-scoped endpoint
|
|
388
|
+
const project = containers[0].id;
|
|
370
389
|
return executeQuery(organization, project, pat, wiql);
|
|
371
390
|
}
|
|
372
391
|
|
|
@@ -419,13 +438,58 @@ async function executeQuery(
|
|
|
419
438
|
}
|
|
420
439
|
|
|
421
440
|
/**
|
|
422
|
-
*
|
|
441
|
+
* Execute WIQL query at org level (cross-project)
|
|
442
|
+
*/
|
|
443
|
+
async function executeQueryOrgLevel(
|
|
444
|
+
organization: string,
|
|
445
|
+
pat: string,
|
|
446
|
+
wiql: string
|
|
447
|
+
): Promise<WorkItem[]> {
|
|
448
|
+
const baseUrl = `https://dev.azure.com/${organization}`;
|
|
449
|
+
|
|
450
|
+
// Execute query at org level
|
|
451
|
+
const queryUrl = `${baseUrl}/_apis/wit/wiql?api-version=7.1`;
|
|
452
|
+
const queryResult: any = await makeRequest(queryUrl, pat, 'POST', { query: wiql });
|
|
453
|
+
|
|
454
|
+
if (!queryResult.workItems || queryResult.workItems.length === 0) {
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Get full work item details (batch request at org level)
|
|
459
|
+
const ids = queryResult.workItems.map((wi: any) => wi.id);
|
|
460
|
+
const batchUrl = `${baseUrl}/_apis/wit/workitemsbatch?api-version=7.1`;
|
|
461
|
+
|
|
462
|
+
const response: any = await makeRequest(batchUrl, pat, 'POST', {
|
|
463
|
+
ids,
|
|
464
|
+
fields: [
|
|
465
|
+
'System.Id',
|
|
466
|
+
'System.Title',
|
|
467
|
+
'System.Description',
|
|
468
|
+
'System.State',
|
|
469
|
+
'System.CreatedDate',
|
|
470
|
+
'System.ChangedDate',
|
|
471
|
+
'System.WorkItemType',
|
|
472
|
+
'System.AreaPath',
|
|
473
|
+
'System.IterationPath',
|
|
474
|
+
'System.Tags',
|
|
475
|
+
],
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return response.value || [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Default request timeout in ms (configurable via ado.requestTimeoutMs) */
|
|
482
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Make HTTPS request to ADO API with timeout
|
|
423
486
|
*/
|
|
424
487
|
function makeRequest(
|
|
425
488
|
url: string,
|
|
426
489
|
pat: string,
|
|
427
490
|
method: string = 'GET',
|
|
428
|
-
body?: any
|
|
491
|
+
body?: any,
|
|
492
|
+
timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS
|
|
429
493
|
): Promise<any> {
|
|
430
494
|
return new Promise((resolve, reject) => {
|
|
431
495
|
const { hostname, pathname, search } = new URL(url);
|
|
@@ -446,6 +510,7 @@ function makeRequest(
|
|
|
446
510
|
path: pathname + search,
|
|
447
511
|
method,
|
|
448
512
|
headers,
|
|
513
|
+
timeout: timeoutMs,
|
|
449
514
|
};
|
|
450
515
|
|
|
451
516
|
const req = https.request(options, (res) => {
|
|
@@ -470,6 +535,11 @@ function makeRequest(
|
|
|
470
535
|
});
|
|
471
536
|
});
|
|
472
537
|
|
|
538
|
+
req.on('timeout', () => {
|
|
539
|
+
req.destroy();
|
|
540
|
+
reject(new Error(`ADO request timed out after ${timeoutMs}ms: ${method} ${url}`));
|
|
541
|
+
});
|
|
542
|
+
|
|
473
543
|
req.on('error', (error) => {
|
|
474
544
|
reject(error);
|
|
475
545
|
});
|
|
@@ -26,7 +26,7 @@ class AdoProfileResolver {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
const incrementProfile = await this.getIncrementProfile(incrementId);
|
|
29
|
-
const globalDefaultProfile = config.sync?.defaultProfile;
|
|
29
|
+
const globalDefaultProfile = config.sync?.activeProfile ?? config.sync?.defaultProfile;
|
|
30
30
|
const profileName = incrementProfile || globalDefaultProfile;
|
|
31
31
|
if (!profileName) {
|
|
32
32
|
return {
|
|
@@ -108,6 +108,7 @@ interface ConfigProfile {
|
|
|
108
108
|
* Sync config from config.json
|
|
109
109
|
*/
|
|
110
110
|
interface SyncConfig {
|
|
111
|
+
activeProfile?: string;
|
|
111
112
|
defaultProfile?: string;
|
|
112
113
|
profiles?: Record<string, ConfigProfile>;
|
|
113
114
|
}
|
|
@@ -158,7 +159,8 @@ export class AdoProfileResolver {
|
|
|
158
159
|
const incrementProfile = await this.getIncrementProfile(incrementId);
|
|
159
160
|
|
|
160
161
|
// Determine which profile to use
|
|
161
|
-
|
|
162
|
+
// Read both activeProfile (canonical) and defaultProfile (legacy) with fallback
|
|
163
|
+
const globalDefaultProfile = config.sync?.activeProfile ?? config.sync?.defaultProfile;
|
|
162
164
|
const profileName = incrementProfile || globalDefaultProfile;
|
|
163
165
|
|
|
164
166
|
if (!profileName) {
|
|
@@ -163,7 +163,8 @@ class AdoSpecSync {
|
|
|
163
163
|
value: this.mapPriorityToAdo(spec.metadata.priority)
|
|
164
164
|
}
|
|
165
165
|
];
|
|
166
|
-
const
|
|
166
|
+
const encodedType = encodeURIComponent(workItemType);
|
|
167
|
+
const response = await this.client.post(`/wit/workitems/$${encodedType}?api-version=7.0`, payload);
|
|
167
168
|
const featureData = response.data;
|
|
168
169
|
console.log(` \u2705 Created ADO Feature #${featureData.id}: ${featureData._links.html.href}`);
|
|
169
170
|
return {
|
|
@@ -173,29 +174,41 @@ class AdoSpecSync {
|
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
/**
|
|
176
|
-
* Update existing ADO Feature
|
|
177
|
+
* Update existing ADO Feature (conditional — only writes changed fields)
|
|
177
178
|
*/
|
|
178
179
|
async updateAdoFeature(featureId, spec) {
|
|
179
180
|
const featureTitle = `[${spec.metadata.id.toUpperCase()}] ${spec.metadata.title}`;
|
|
180
181
|
const featureDescription = this.generateFeatureDescription(spec);
|
|
181
|
-
const
|
|
182
|
-
|
|
182
|
+
const current = await this.fetchAdoFeature(featureId);
|
|
183
|
+
const payload = [];
|
|
184
|
+
if (current.fields["System.Title"] !== featureTitle) {
|
|
185
|
+
payload.push({
|
|
183
186
|
op: "replace",
|
|
184
187
|
path: "/fields/System.Title",
|
|
185
188
|
value: featureTitle
|
|
186
|
-
}
|
|
187
|
-
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
console.log(` \u2139\uFE0F Title unchanged, skipping`);
|
|
192
|
+
}
|
|
193
|
+
if (current.fields["System.Description"] !== featureDescription) {
|
|
194
|
+
payload.push({
|
|
188
195
|
op: "replace",
|
|
189
196
|
path: "/fields/System.Description",
|
|
190
197
|
value: featureDescription
|
|
191
|
-
}
|
|
192
|
-
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
console.log(` \u2139\uFE0F Description unchanged, skipping`);
|
|
201
|
+
}
|
|
202
|
+
if (payload.length === 0) {
|
|
203
|
+
console.log(` \u2139\uFE0F No changes detected for ADO Feature #${featureId}`);
|
|
204
|
+
return current;
|
|
205
|
+
}
|
|
193
206
|
const response = await this.client.patch(
|
|
194
207
|
`/wit/workitems/${featureId}?api-version=7.0`,
|
|
195
208
|
payload
|
|
196
209
|
);
|
|
197
210
|
const featureData = response.data;
|
|
198
|
-
console.log(` \u2705 Updated ADO Feature #${featureId}`);
|
|
211
|
+
console.log(` \u2705 Updated ADO Feature #${featureId} (${payload.length} field(s) changed)`);
|
|
199
212
|
return {
|
|
200
213
|
id: featureData.id,
|
|
201
214
|
url: featureData._links.html.href,
|
|
@@ -255,7 +255,8 @@ export class AdoSpecSync {
|
|
|
255
255
|
}
|
|
256
256
|
];
|
|
257
257
|
|
|
258
|
-
const
|
|
258
|
+
const encodedType = encodeURIComponent(workItemType);
|
|
259
|
+
const response = await this.client.post(`/wit/workitems/$${encodedType}?api-version=7.0`, payload);
|
|
259
260
|
const featureData = response.data;
|
|
260
261
|
|
|
261
262
|
console.log(` ✅ Created ADO Feature #${featureData.id}: ${featureData._links.html.href}`);
|
|
@@ -268,24 +269,41 @@ export class AdoSpecSync {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
/**
|
|
271
|
-
* Update existing ADO Feature
|
|
272
|
+
* Update existing ADO Feature (conditional — only writes changed fields)
|
|
272
273
|
*/
|
|
273
274
|
private async updateAdoFeature(featureId: number, spec: SpecContent): Promise<AdoFeature> {
|
|
274
275
|
const featureTitle = `[${spec.metadata.id.toUpperCase()}] ${spec.metadata.title}`;
|
|
275
276
|
const featureDescription = this.generateFeatureDescription(spec);
|
|
276
277
|
|
|
277
|
-
|
|
278
|
-
|
|
278
|
+
// Fetch current values to avoid overwriting ADO-side edits
|
|
279
|
+
const current = await this.fetchAdoFeature(featureId);
|
|
280
|
+
|
|
281
|
+
const payload: any[] = [];
|
|
282
|
+
|
|
283
|
+
if (current.fields['System.Title'] !== featureTitle) {
|
|
284
|
+
payload.push({
|
|
279
285
|
op: 'replace',
|
|
280
286
|
path: '/fields/System.Title',
|
|
281
287
|
value: featureTitle
|
|
282
|
-
}
|
|
283
|
-
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
console.log(` ℹ️ Title unchanged, skipping`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (current.fields['System.Description'] !== featureDescription) {
|
|
294
|
+
payload.push({
|
|
284
295
|
op: 'replace',
|
|
285
296
|
path: '/fields/System.Description',
|
|
286
297
|
value: featureDescription
|
|
287
|
-
}
|
|
288
|
-
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
console.log(` ℹ️ Description unchanged, skipping`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (payload.length === 0) {
|
|
304
|
+
console.log(` ℹ️ No changes detected for ADO Feature #${featureId}`);
|
|
305
|
+
return current;
|
|
306
|
+
}
|
|
289
307
|
|
|
290
308
|
const response = await this.client.patch(
|
|
291
309
|
`/wit/workitems/${featureId}?api-version=7.0`,
|
|
@@ -293,7 +311,7 @@ export class AdoSpecSync {
|
|
|
293
311
|
);
|
|
294
312
|
const featureData = response.data;
|
|
295
313
|
|
|
296
|
-
console.log(` ✅ Updated ADO Feature #${featureId}`);
|
|
314
|
+
console.log(` ✅ Updated ADO Feature #${featureId} (${payload.length} field(s) changed)`);
|
|
297
315
|
|
|
298
316
|
return {
|
|
299
317
|
id: featureData.id,
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import * as yaml from "yaml";
|
|
4
4
|
const STATUS_MAPPING = {
|
|
5
5
|
ado: {
|
|
6
|
+
// Agile process template
|
|
6
7
|
"New": "draft",
|
|
7
8
|
"Active": "in-progress",
|
|
8
9
|
"Resolved": "implemented",
|
|
@@ -10,7 +11,20 @@ const STATUS_MAPPING = {
|
|
|
10
11
|
"In Review": "in-qa",
|
|
11
12
|
"In QA": "in-qa",
|
|
12
13
|
"Blocked": "blocked",
|
|
13
|
-
"Removed": "cancelled"
|
|
14
|
+
"Removed": "cancelled",
|
|
15
|
+
// Scrum process template
|
|
16
|
+
"Approved": "draft",
|
|
17
|
+
"Committed": "in-progress",
|
|
18
|
+
"Done": "complete",
|
|
19
|
+
// CMMI process template
|
|
20
|
+
"Proposed": "draft",
|
|
21
|
+
// 'Active' already mapped above (shared with CMMI)
|
|
22
|
+
// 'Resolved' already mapped above (shared with CMMI)
|
|
23
|
+
// 'Closed' already mapped above (shared with CMMI)
|
|
24
|
+
// Basic process template
|
|
25
|
+
"To Do": "draft",
|
|
26
|
+
"Doing": "in-progress"
|
|
27
|
+
// 'Done' already mapped above (shared with Basic)
|
|
14
28
|
},
|
|
15
29
|
jira: {
|
|
16
30
|
"To Do": "draft",
|
|
@@ -30,6 +44,8 @@ const STATUS_MAPPING = {
|
|
|
30
44
|
}
|
|
31
45
|
};
|
|
32
46
|
const REVERSE_STATUS_MAPPING = {
|
|
47
|
+
// Default reverse mapping uses Agile states (most common)
|
|
48
|
+
// Callers should use process-template-aware mapping when template is known
|
|
33
49
|
ado: {
|
|
34
50
|
"draft": "New",
|
|
35
51
|
"in-progress": "Active",
|
|
@@ -87,6 +87,7 @@ export interface ExternalStatus {
|
|
|
87
87
|
|
|
88
88
|
const STATUS_MAPPING = {
|
|
89
89
|
ado: {
|
|
90
|
+
// Agile process template
|
|
90
91
|
'New': 'draft' as SpecStatus,
|
|
91
92
|
'Active': 'in-progress' as SpecStatus,
|
|
92
93
|
'Resolved': 'implemented' as SpecStatus,
|
|
@@ -94,7 +95,20 @@ const STATUS_MAPPING = {
|
|
|
94
95
|
'In Review': 'in-qa' as SpecStatus,
|
|
95
96
|
'In QA': 'in-qa' as SpecStatus,
|
|
96
97
|
'Blocked': 'blocked' as SpecStatus,
|
|
97
|
-
'Removed': 'cancelled' as SpecStatus
|
|
98
|
+
'Removed': 'cancelled' as SpecStatus,
|
|
99
|
+
// Scrum process template
|
|
100
|
+
'Approved': 'draft' as SpecStatus,
|
|
101
|
+
'Committed': 'in-progress' as SpecStatus,
|
|
102
|
+
'Done': 'complete' as SpecStatus,
|
|
103
|
+
// CMMI process template
|
|
104
|
+
'Proposed': 'draft' as SpecStatus,
|
|
105
|
+
// 'Active' already mapped above (shared with CMMI)
|
|
106
|
+
// 'Resolved' already mapped above (shared with CMMI)
|
|
107
|
+
// 'Closed' already mapped above (shared with CMMI)
|
|
108
|
+
// Basic process template
|
|
109
|
+
'To Do': 'draft' as SpecStatus,
|
|
110
|
+
'Doing': 'in-progress' as SpecStatus,
|
|
111
|
+
// 'Done' already mapped above (shared with Basic)
|
|
98
112
|
},
|
|
99
113
|
jira: {
|
|
100
114
|
'To Do': 'draft' as SpecStatus,
|
|
@@ -115,6 +129,8 @@ const STATUS_MAPPING = {
|
|
|
115
129
|
};
|
|
116
130
|
|
|
117
131
|
const REVERSE_STATUS_MAPPING = {
|
|
132
|
+
// Default reverse mapping uses Agile states (most common)
|
|
133
|
+
// Callers should use process-template-aware mapping when template is known
|
|
118
134
|
ado: {
|
|
119
135
|
'draft': 'New',
|
|
120
136
|
'in-progress': 'Active',
|
|
@@ -130,10 +130,20 @@ function buildTaskMapping(increments, organization, project) {
|
|
|
130
130
|
title: task.title,
|
|
131
131
|
userStories: task.userStories
|
|
132
132
|
}));
|
|
133
|
+
// Derive repository name from git remote or fall back to project name
|
|
134
|
+
let repoName = project;
|
|
135
|
+
try {
|
|
136
|
+
const { execSync } = require("child_process");
|
|
137
|
+
const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
138
|
+
const match = remoteUrl.match(/\/([^/]+?)(?:\.git)?$/);
|
|
139
|
+
if (match) repoName = match[1];
|
|
140
|
+
} catch {
|
|
141
|
+
// Fallback to project name if git is unavailable
|
|
142
|
+
}
|
|
133
143
|
return {
|
|
134
144
|
incrementId: firstIncrement.id,
|
|
135
145
|
tasks,
|
|
136
|
-
tasksUrl: `https://dev.azure.com/${organization}/${project}/_git
|
|
146
|
+
tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/${repoName}?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
|
|
137
147
|
};
|
|
138
148
|
}
|
|
139
149
|
async function findArchitectureDocs(rootDir, specId) {
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { consoleLogger } from "../../../src/utils/logger.js";
|
|
2
|
+
const WORK_ITEM_TYPE_BY_TEMPLATE = {
|
|
3
|
+
agile: "User Story",
|
|
4
|
+
scrum: "Product Backlog Item",
|
|
5
|
+
cmmi: "Requirement",
|
|
6
|
+
basic: "Issue"
|
|
7
|
+
};
|
|
2
8
|
class PerUSAdoSync {
|
|
3
9
|
constructor(adoClient, projectMappings, options = {}) {
|
|
4
10
|
this.adoClient = adoClient;
|
|
5
11
|
this.projectMappings = projectMappings;
|
|
6
12
|
this.logger = options.logger ?? consoleLogger;
|
|
13
|
+
this.workItemType = options.workItemType ?? WORK_ITEM_TYPE_BY_TEMPLATE[options.processTemplate?.toLowerCase() ?? ""] ?? "User Story";
|
|
7
14
|
}
|
|
8
15
|
/**
|
|
9
16
|
* Sync all user stories to their respective ADO projects
|
|
@@ -133,7 +140,7 @@ class PerUSAdoSync {
|
|
|
133
140
|
} else {
|
|
134
141
|
const newItem = await this.adoClient.createWorkItem(
|
|
135
142
|
mapping.project,
|
|
136
|
-
|
|
143
|
+
this.workItemType,
|
|
137
144
|
title,
|
|
138
145
|
description,
|
|
139
146
|
areaPath
|
|
@@ -81,6 +81,16 @@ export interface AdoClient {
|
|
|
81
81
|
getWorkItemUrl(project: string, workItemId: number): string;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Default work item type by process template
|
|
86
|
+
*/
|
|
87
|
+
const WORK_ITEM_TYPE_BY_TEMPLATE: Record<string, string> = {
|
|
88
|
+
agile: 'User Story',
|
|
89
|
+
scrum: 'Product Backlog Item',
|
|
90
|
+
cmmi: 'Requirement',
|
|
91
|
+
basic: 'Issue',
|
|
92
|
+
};
|
|
93
|
+
|
|
84
94
|
/**
|
|
85
95
|
* Per-US ADO Sync
|
|
86
96
|
*
|
|
@@ -91,15 +101,20 @@ export class PerUSAdoSync {
|
|
|
91
101
|
private projectMappings: ProjectMappings;
|
|
92
102
|
private adoClient: AdoClient;
|
|
93
103
|
private logger: Logger;
|
|
104
|
+
private workItemType: string;
|
|
94
105
|
|
|
95
106
|
constructor(
|
|
96
107
|
adoClient: AdoClient,
|
|
97
108
|
projectMappings: ProjectMappings,
|
|
98
|
-
options: { logger?: Logger } = {}
|
|
109
|
+
options: { logger?: Logger; workItemType?: string; processTemplate?: string } = {}
|
|
99
110
|
) {
|
|
100
111
|
this.adoClient = adoClient;
|
|
101
112
|
this.projectMappings = projectMappings;
|
|
102
113
|
this.logger = options.logger ?? consoleLogger;
|
|
114
|
+
// Explicit workItemType takes priority, then process template lookup, then default
|
|
115
|
+
this.workItemType = options.workItemType
|
|
116
|
+
?? WORK_ITEM_TYPE_BY_TEMPLATE[options.processTemplate?.toLowerCase() ?? '']
|
|
117
|
+
?? 'User Story';
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
/**
|
|
@@ -268,7 +283,7 @@ export class PerUSAdoSync {
|
|
|
268
283
|
// Create new work item
|
|
269
284
|
const newItem = await this.adoClient.createWorkItem(
|
|
270
285
|
mapping.project,
|
|
271
|
-
|
|
286
|
+
this.workItemType,
|
|
272
287
|
title,
|
|
273
288
|
description,
|
|
274
289
|
areaPath
|
|
@@ -74,14 +74,38 @@ else
|
|
|
74
74
|
[[ "$GH_ENABLED" != "true" ]] && { log "GitHub sync not enabled"; exit 0; }
|
|
75
75
|
fi
|
|
76
76
|
|
|
77
|
-
# Check auto-create enabled
|
|
77
|
+
# Check auto-create enabled
|
|
78
|
+
# Two flags control auto-creation behavior:
|
|
79
|
+
# - sync.autoSync (boolean): Global auto-sync toggle. When true, enables ALL auto-sync
|
|
80
|
+
# operations including issue creation, progress sync, and status updates.
|
|
81
|
+
# - hooks.post_increment_planning.auto_create_github_issue (boolean): Fine-grained toggle
|
|
82
|
+
# for ONLY the auto-create-issue behavior in post-increment-planning hooks.
|
|
83
|
+
#
|
|
84
|
+
# Precedence: auto_create_github_issue (specific) > autoSync (global)
|
|
85
|
+
# If BOTH are set, auto_create_github_issue takes precedence for issue creation.
|
|
86
|
+
# If ONLY autoSync is true, issue creation is enabled (as part of the global auto-sync).
|
|
78
87
|
AUTO_SYNC=$(jq -r '.sync.autoSync // false' "$CONFIG_PATH" 2>/dev/null)
|
|
79
88
|
AUTO_CREATE=$(jq -r '.hooks.post_increment_planning.auto_create_github_issue // false' "$CONFIG_PATH" 2>/dev/null)
|
|
89
|
+
|
|
90
|
+
# Warn if only one flag is set (likely misconfiguration)
|
|
91
|
+
if [[ "$AUTO_SYNC" == "true" ]] && [[ "$AUTO_CREATE" == "false" ]]; then
|
|
92
|
+
log "Warning: sync.autoSync=true but auto_create_github_issue=false. Issue creation enabled via autoSync. Set auto_create_github_issue=true to be explicit."
|
|
93
|
+
elif [[ "$AUTO_SYNC" == "false" ]] && [[ "$AUTO_CREATE" == "true" ]]; then
|
|
94
|
+
log "Warning: auto_create_github_issue=true but sync.autoSync=false. Only issue creation is enabled; other sync operations are disabled."
|
|
95
|
+
fi
|
|
96
|
+
|
|
80
97
|
if [[ "$AUTO_SYNC" != "true" ]] && [[ "$AUTO_CREATE" != "true" ]]; then
|
|
81
98
|
log "Auto-sync disabled (autoSync=$AUTO_SYNC, auto_create=$AUTO_CREATE)"
|
|
82
99
|
exit 0
|
|
83
100
|
fi
|
|
84
101
|
|
|
102
|
+
# Log which flag triggered activation
|
|
103
|
+
if [[ "$AUTO_CREATE" == "true" ]]; then
|
|
104
|
+
log "Auto-create enabled via hooks.post_increment_planning.auto_create_github_issue"
|
|
105
|
+
else
|
|
106
|
+
log "Auto-create enabled via sync.autoSync (global toggle)"
|
|
107
|
+
fi
|
|
108
|
+
|
|
85
109
|
# Get owner/repo
|
|
86
110
|
OWNER=$(jq -r '.sync.github.owner // ""' "$CONFIG_PATH" 2>/dev/null)
|
|
87
111
|
REPO=$(jq -r '.sync.github.repo // ""' "$CONFIG_PATH" 2>/dev/null)
|
|
@@ -115,7 +139,9 @@ fi
|
|
|
115
139
|
if [[ "${SPECWEAVE_SKIP_DEBOUNCE:-0}" != "1" ]]; then
|
|
116
140
|
DEBOUNCE_FILE="$STATE_DIR/.github-auto-create-pending-$INC_ID"
|
|
117
141
|
if [[ -f "$DEBOUNCE_FILE" ]]; then
|
|
118
|
-
|
|
142
|
+
# POSIX-portable: use perl for mtime (works on macOS and Linux)
|
|
143
|
+
FILE_MTIME=$(perl -e 'print((stat($ARGV[0]))[9])' "$DEBOUNCE_FILE" 2>/dev/null || echo 0)
|
|
144
|
+
SIGNAL_AGE=$(($(date +%s) - FILE_MTIME))
|
|
119
145
|
if (( SIGNAL_AGE < 30 )); then
|
|
120
146
|
log "Debounce: signal age ${SIGNAL_AGE}s < 30s. Deferring."
|
|
121
147
|
exit 0
|
|
@@ -80,10 +80,13 @@ for i in {1..15}; do
|
|
|
80
80
|
break
|
|
81
81
|
fi
|
|
82
82
|
|
|
83
|
-
# Check for stale lock
|
|
83
|
+
# Check for stale lock (POSIX-portable: works on macOS and Linux)
|
|
84
84
|
if [[ -d "$LOCK_FILE" ]]; then
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
# Use find -mmin which works on both macOS and Linux
|
|
86
|
+
LOCK_TIMEOUT_MIN=$(( (LOCK_TIMEOUT + 59) / 60 )) # Convert seconds to minutes (ceil)
|
|
87
|
+
[[ $LOCK_TIMEOUT_MIN -lt 1 ]] && LOCK_TIMEOUT_MIN=1
|
|
88
|
+
STALE=$(find "$LOCK_FILE" -maxdepth 0 -mmin +${LOCK_TIMEOUT_MIN} 2>/dev/null)
|
|
89
|
+
if [[ -n "$STALE" ]]; then
|
|
87
90
|
rmdir "$LOCK_FILE" 2>/dev/null || true
|
|
88
91
|
continue
|
|
89
92
|
fi
|