@ouro.bot/cli 0.0.1-alpha.0 → 0.1.0-alpha.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/AdoptionSpecialist.ouro/agent.json +20 -0
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +22 -0
- package/AdoptionSpecialist.ouro/psyche/identities/basilisk.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/jafar.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/jormungandr.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/kaa.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/medusa.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/nagini.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/ouroboros.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/python.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/quetzalcoatl.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/sir-hiss.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/the-serpent.md +31 -0
- package/AdoptionSpecialist.ouro/psyche/identities/the-snake.md +31 -0
- package/README.md +224 -6
- package/dist/heart/agent-entry.js +17 -0
- package/dist/heart/api-error.js +34 -0
- package/dist/heart/config.js +296 -0
- package/dist/heart/core.js +515 -0
- package/dist/heart/daemon/daemon-cli.js +675 -0
- package/dist/heart/daemon/daemon-entry.js +74 -0
- package/dist/heart/daemon/daemon.js +313 -0
- package/dist/heart/daemon/hatch-flow.js +285 -0
- package/dist/heart/daemon/hatch-specialist.js +107 -0
- package/dist/heart/daemon/health-monitor.js +79 -0
- package/dist/heart/daemon/log-tailer.js +146 -0
- package/dist/heart/daemon/message-router.js +98 -0
- package/dist/heart/daemon/os-cron.js +260 -0
- package/dist/heart/daemon/ouro-bot-entry.js +23 -0
- package/dist/heart/daemon/ouro-bot-wrapper.js +90 -0
- package/dist/heart/daemon/ouro-entry.js +23 -0
- package/dist/heart/daemon/ouro-uti.js +212 -0
- package/dist/heart/daemon/process-manager.js +237 -0
- package/dist/heart/daemon/runtime-logging.js +98 -0
- package/dist/heart/daemon/subagent-installer.js +125 -0
- package/dist/heart/daemon/task-scheduler.js +240 -0
- package/dist/heart/harness.js +26 -0
- package/dist/heart/identity.js +281 -0
- package/dist/heart/kicks.js +144 -0
- package/dist/heart/primitives.js +4 -0
- package/dist/heart/providers/anthropic.js +329 -0
- package/dist/heart/providers/azure.js +66 -0
- package/dist/heart/providers/minimax.js +53 -0
- package/dist/heart/providers/openai-codex.js +162 -0
- package/dist/heart/streaming.js +412 -0
- package/dist/heart/turn-coordinator.js +62 -0
- package/dist/inner-worker-entry.js +4 -0
- package/dist/mind/associative-recall.js +197 -0
- package/dist/mind/bundle-manifest.js +118 -0
- package/dist/mind/context.js +302 -0
- package/dist/mind/first-impressions.js +43 -0
- package/dist/mind/format.js +56 -0
- package/dist/mind/friends/channel.js +41 -0
- package/dist/mind/friends/resolver.js +84 -0
- package/dist/mind/friends/store-file.js +171 -0
- package/dist/mind/friends/store.js +4 -0
- package/dist/mind/friends/tokens.js +26 -0
- package/dist/mind/friends/types.js +21 -0
- package/dist/mind/memory.js +388 -0
- package/dist/mind/pending.js +93 -0
- package/dist/mind/phrases.js +43 -0
- package/dist/mind/prompt-refresh.js +20 -0
- package/dist/mind/prompt.js +352 -0
- package/dist/mind/token-estimate.js +119 -0
- package/dist/nerves/cli-logging.js +31 -0
- package/dist/nerves/coverage/audit-rules.js +81 -0
- package/dist/nerves/coverage/audit.js +200 -0
- package/dist/nerves/coverage/cli-main.js +5 -0
- package/dist/nerves/coverage/cli.js +51 -0
- package/dist/nerves/coverage/contract.js +23 -0
- package/dist/nerves/coverage/file-completeness.js +56 -0
- package/dist/nerves/coverage/run-artifacts.js +77 -0
- package/dist/nerves/coverage/source-scanner.js +34 -0
- package/dist/nerves/index.js +152 -0
- package/dist/nerves/runtime.js +38 -0
- package/dist/repertoire/ado-client.js +211 -0
- package/dist/repertoire/ado-context.js +73 -0
- package/dist/repertoire/ado-semantic.js +841 -0
- package/dist/repertoire/ado-templates.js +146 -0
- package/dist/repertoire/coding/index.js +36 -0
- package/dist/repertoire/coding/manager.js +489 -0
- package/dist/repertoire/coding/monitor.js +60 -0
- package/dist/repertoire/coding/reporter.js +45 -0
- package/dist/repertoire/coding/spawner.js +102 -0
- package/dist/repertoire/coding/tools.js +167 -0
- package/dist/repertoire/coding/types.js +2 -0
- package/dist/repertoire/data/ado-endpoints.json +122 -0
- package/dist/repertoire/data/graph-endpoints.json +212 -0
- package/dist/repertoire/github-client.js +64 -0
- package/dist/repertoire/graph-client.js +118 -0
- package/dist/repertoire/skills.js +156 -0
- package/dist/repertoire/tasks/board.js +122 -0
- package/dist/repertoire/tasks/index.js +210 -0
- package/dist/repertoire/tasks/lifecycle.js +80 -0
- package/dist/repertoire/tasks/middleware.js +65 -0
- package/dist/repertoire/tasks/parser.js +173 -0
- package/dist/repertoire/tasks/scanner.js +132 -0
- package/dist/repertoire/tasks/transitions.js +145 -0
- package/dist/repertoire/tasks/types.js +2 -0
- package/dist/repertoire/tools-base.js +714 -0
- package/dist/repertoire/tools-github.js +53 -0
- package/dist/repertoire/tools-teams.js +308 -0
- package/dist/repertoire/tools.js +199 -0
- package/dist/senses/cli-entry.js +15 -0
- package/dist/senses/cli.js +604 -0
- package/dist/senses/commands.js +98 -0
- package/dist/senses/inner-dialog-worker.js +61 -0
- package/dist/senses/inner-dialog.js +231 -0
- package/dist/senses/session-lock.js +119 -0
- package/dist/senses/teams-entry.js +15 -0
- package/dist/senses/teams.js +696 -0
- package/dist/senses/trust-gate.js +150 -0
- package/package.json +34 -11
- package/subagents/README.md +73 -0
- package/subagents/work-doer.md +233 -0
- package/subagents/work-merger.md +624 -0
- package/subagents/work-planner.md +373 -0
- package/bin/ouro.js +0 -6
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Semantic ADO tools -- enriched, single-call operations over the ADO API.
|
|
3
|
+
// Phase 3: ado_backlog_list (query), mutation tools (3D).
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.adoSemanticToolDefinitions = void 0;
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
const ado_client_1 = require("./ado-client");
|
|
8
|
+
const ado_context_1 = require("./ado-context");
|
|
9
|
+
const ado_templates_1 = require("./ado-templates");
|
|
10
|
+
// Fields fetched for enriched work items
|
|
11
|
+
const ENRICHED_FIELDS = [
|
|
12
|
+
"System.Id",
|
|
13
|
+
"System.Title",
|
|
14
|
+
"System.WorkItemType",
|
|
15
|
+
"System.State",
|
|
16
|
+
"System.AssignedTo",
|
|
17
|
+
"System.AreaPath",
|
|
18
|
+
"System.IterationPath",
|
|
19
|
+
"System.Parent",
|
|
20
|
+
].join(",");
|
|
21
|
+
function buildWiqlQuery(project, filters) {
|
|
22
|
+
const conditions = [`[System.TeamProject] = '${project}'`];
|
|
23
|
+
if (filters.areaPath) {
|
|
24
|
+
conditions.push(`[System.AreaPath] UNDER '${filters.areaPath}'`);
|
|
25
|
+
}
|
|
26
|
+
if (filters.iteration) {
|
|
27
|
+
conditions.push(`[System.IterationPath] = '${filters.iteration}'`);
|
|
28
|
+
}
|
|
29
|
+
if (filters.workItemType) {
|
|
30
|
+
conditions.push(`[System.WorkItemType] = '${filters.workItemType}'`);
|
|
31
|
+
}
|
|
32
|
+
if (filters.state) {
|
|
33
|
+
conditions.push(`[System.State] = '${filters.state}'`);
|
|
34
|
+
}
|
|
35
|
+
if (filters.assignee) {
|
|
36
|
+
conditions.push(`[System.AssignedTo] Contains '${filters.assignee}'`);
|
|
37
|
+
}
|
|
38
|
+
return `SELECT [System.Id] FROM WorkItems WHERE ${conditions.join(" AND ")} ORDER BY [System.ChangedDate] DESC`;
|
|
39
|
+
}
|
|
40
|
+
// Valid parent-child relationships for common process templates
|
|
41
|
+
const VALID_PARENT_CHILD = {
|
|
42
|
+
"Epic": ["Feature", "User Story", "Issue", "Product Backlog Item"],
|
|
43
|
+
"Feature": ["User Story", "Product Backlog Item", "Bug", "Task"],
|
|
44
|
+
"User Story": ["Task", "Bug"],
|
|
45
|
+
"Product Backlog Item": ["Task", "Bug"],
|
|
46
|
+
"Issue": ["Task"],
|
|
47
|
+
"Bug": ["Task"],
|
|
48
|
+
};
|
|
49
|
+
function buildCreatePatch(args) {
|
|
50
|
+
const ops = [
|
|
51
|
+
{ op: "add", path: "/fields/System.Title", value: args.title },
|
|
52
|
+
];
|
|
53
|
+
if (args.description) {
|
|
54
|
+
ops.push({ op: "add", path: "/fields/System.Description", value: args.description });
|
|
55
|
+
}
|
|
56
|
+
if (args.areaPath) {
|
|
57
|
+
ops.push({ op: "add", path: "/fields/System.AreaPath", value: args.areaPath });
|
|
58
|
+
}
|
|
59
|
+
if (args.iterationPath) {
|
|
60
|
+
ops.push({ op: "add", path: "/fields/System.IterationPath", value: args.iterationPath });
|
|
61
|
+
}
|
|
62
|
+
if (args.parentId) {
|
|
63
|
+
ops.push({
|
|
64
|
+
op: "add",
|
|
65
|
+
path: "/relations/-",
|
|
66
|
+
value: {
|
|
67
|
+
rel: "System.LinkTypes.Hierarchy-Reverse",
|
|
68
|
+
url: args.parentId, // Will be resolved to full URL by ADO
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return ops;
|
|
73
|
+
}
|
|
74
|
+
function buildReparentPatch(newParentId) {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
op: "add",
|
|
78
|
+
path: "/relations/-",
|
|
79
|
+
value: {
|
|
80
|
+
rel: "System.LinkTypes.Hierarchy-Reverse",
|
|
81
|
+
url: newParentId,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
async function checkAuth(ctx) {
|
|
87
|
+
if (!ctx?.adoToken) {
|
|
88
|
+
return "AUTH_REQUIRED:ado -- I need access to Azure DevOps. Please sign in when prompted.";
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// Authority pre-flight checks removed (AuthorityChecker eliminated).
|
|
93
|
+
// All writes proceed optimistically; 403s are handled at the API response level.
|
|
94
|
+
function buildPreviewOps(operation, args) {
|
|
95
|
+
switch (operation) {
|
|
96
|
+
case "create_epic":
|
|
97
|
+
return buildCreatePatch({ ...args, workItemType: "Epic" });
|
|
98
|
+
case "create_issue":
|
|
99
|
+
return buildCreatePatch({ ...args, workItemType: "Issue" });
|
|
100
|
+
case "move_items": {
|
|
101
|
+
const ids = (args.workItemIds || "").split(",").map(s => s.trim()).filter(Boolean);
|
|
102
|
+
/* v8 ignore next -- defensive fallback for missing arg @preserve */
|
|
103
|
+
return ids.map(() => buildReparentPatch(args.newParentId || "")).flat();
|
|
104
|
+
}
|
|
105
|
+
default:
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function formatWorkItems(items) {
|
|
110
|
+
return items.map(wi => ({
|
|
111
|
+
id: wi.id,
|
|
112
|
+
title: wi.fields["System.Title"] ?? "",
|
|
113
|
+
type: wi.fields["System.WorkItemType"] ?? "",
|
|
114
|
+
state: wi.fields["System.State"] ?? "",
|
|
115
|
+
assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? null,
|
|
116
|
+
areaPath: wi.fields["System.AreaPath"] ?? "",
|
|
117
|
+
iteration: wi.fields["System.IterationPath"] ?? "",
|
|
118
|
+
parent: wi.fields["System.Parent"] ?? null,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
function formatMarkdown(items, maxLen) {
|
|
122
|
+
const lines = [];
|
|
123
|
+
for (const item of items) {
|
|
124
|
+
const assignee = item.assignedTo || "Unassigned";
|
|
125
|
+
const parent = item.parent ? ` (parent: #${item.parent})` : "";
|
|
126
|
+
const line = `- **#${item.id}** ${item.title} [${item.type}] _${item.state}_ | ${assignee}${parent}`;
|
|
127
|
+
// Check if adding this line would exceed limit
|
|
128
|
+
const current = lines.join("\n");
|
|
129
|
+
/* v8 ignore next 4 -- defensive overflow guard; all callers use Infinity maxLen @preserve */
|
|
130
|
+
if (current.length + line.length + 1 > maxLen - 50) {
|
|
131
|
+
lines.push(`_...and ${items.length - lines.length} more items_`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
lines.push(line);
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function formatPlainText(items) {
|
|
139
|
+
return items.map(item => {
|
|
140
|
+
const assignee = item.assignedTo || "Unassigned";
|
|
141
|
+
const parent = item.parent ? ` parent:#${item.parent}` : "";
|
|
142
|
+
return `#${item.id} ${item.type.padEnd(14)} ${item.state.padEnd(10)} ${assignee.padEnd(15)} ${item.title}${parent}`;
|
|
143
|
+
}).join("\n");
|
|
144
|
+
}
|
|
145
|
+
function formatForChannel(items, ctx, org, project) {
|
|
146
|
+
const channel = ctx?.context?.channel;
|
|
147
|
+
if (!channel) {
|
|
148
|
+
return JSON.stringify({ items, organization: org, project });
|
|
149
|
+
}
|
|
150
|
+
if (channel.supportsMarkdown) {
|
|
151
|
+
return formatMarkdown(items, channel.maxMessageLength);
|
|
152
|
+
}
|
|
153
|
+
return formatPlainText(items);
|
|
154
|
+
}
|
|
155
|
+
// Top-level types that are expected to have no parent
|
|
156
|
+
const TOP_LEVEL_TYPES = new Set(["Epic"]);
|
|
157
|
+
// Shared helper: fetch all work items with enriched fields
|
|
158
|
+
async function fetchAllItems(token, organization, project) {
|
|
159
|
+
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '${project}' ORDER BY [System.ChangedDate] DESC`;
|
|
160
|
+
const wiqlResult = await (0, ado_client_1.adoRequest)(token, "POST", organization, `/${project}/_apis/wit/wiql`, JSON.stringify({ query: wiql }));
|
|
161
|
+
let wiqlData;
|
|
162
|
+
try {
|
|
163
|
+
wiqlData = JSON.parse(wiqlResult);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { error: wiqlResult };
|
|
167
|
+
}
|
|
168
|
+
if (!wiqlData.workItems || wiqlData.workItems.length === 0) {
|
|
169
|
+
return { items: [] };
|
|
170
|
+
}
|
|
171
|
+
const ids = wiqlData.workItems.slice(0, 200).map(wi => wi.id);
|
|
172
|
+
const batchResult = await (0, ado_client_1.adoRequest)(token, "GET", organization, `/${project}/_apis/wit/workitems?ids=${ids.join(",")}&fields=${ENRICHED_FIELDS}`);
|
|
173
|
+
let batchData;
|
|
174
|
+
try {
|
|
175
|
+
batchData = JSON.parse(batchResult);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return { error: batchResult };
|
|
179
|
+
}
|
|
180
|
+
/* v8 ignore next -- defensive fallback for missing API field @preserve */
|
|
181
|
+
return { items: batchData.value ?? [] };
|
|
182
|
+
}
|
|
183
|
+
// Detect cycles in parent/child graph using DFS
|
|
184
|
+
function detectCycles(items) {
|
|
185
|
+
const parentMap = new Map();
|
|
186
|
+
for (const item of items) {
|
|
187
|
+
const parent = item.fields["System.Parent"];
|
|
188
|
+
if (parent != null) {
|
|
189
|
+
parentMap.set(item.id, parent);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const cycles = [];
|
|
193
|
+
const visited = new Set();
|
|
194
|
+
for (const itemId of parentMap.keys()) {
|
|
195
|
+
if (visited.has(itemId))
|
|
196
|
+
continue;
|
|
197
|
+
const path = [];
|
|
198
|
+
const pathSet = new Set();
|
|
199
|
+
let current = itemId;
|
|
200
|
+
while (current !== undefined && !visited.has(current)) {
|
|
201
|
+
if (pathSet.has(current)) {
|
|
202
|
+
// Found a cycle -- extract just the cycle portion
|
|
203
|
+
const cycleStart = path.indexOf(current);
|
|
204
|
+
cycles.push(path.slice(cycleStart));
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
path.push(current);
|
|
208
|
+
pathSet.add(current);
|
|
209
|
+
current = parentMap.get(current);
|
|
210
|
+
}
|
|
211
|
+
// Mark all nodes in path as visited
|
|
212
|
+
for (const node of path) {
|
|
213
|
+
visited.add(node);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return cycles;
|
|
217
|
+
}
|
|
218
|
+
exports.adoSemanticToolDefinitions = [
|
|
219
|
+
{
|
|
220
|
+
tool: {
|
|
221
|
+
type: "function",
|
|
222
|
+
function: {
|
|
223
|
+
name: "ado_backlog_list",
|
|
224
|
+
description: "Query the backlog and return enriched work items with hierarchy, type, parent, assignee, area path, and iteration. Supports filtering. Use this instead of raw WIQL queries.",
|
|
225
|
+
parameters: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
organization: { type: "string", description: "ADO organization (optional -- omit to discover)" },
|
|
229
|
+
project: { type: "string", description: "ADO project (optional -- omit to discover)" },
|
|
230
|
+
areaPath: { type: "string", description: "Filter by area path (e.g. 'Platform\\\\Team A')" },
|
|
231
|
+
iteration: { type: "string", description: "Filter by iteration path (e.g. 'Sprint 2')" },
|
|
232
|
+
workItemType: { type: "string", description: "Filter by work item type (e.g. 'Bug', 'Epic', 'User Story')" },
|
|
233
|
+
state: { type: "string", description: "Filter by state (e.g. 'Active', 'New', 'Closed')" },
|
|
234
|
+
assignee: { type: "string", description: "Filter by assigned person name" },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
handler: async (args, ctx) => {
|
|
240
|
+
if (!ctx?.adoToken) {
|
|
241
|
+
return "AUTH_REQUIRED:ado -- I need access to Azure DevOps. Please sign in when prompted.";
|
|
242
|
+
}
|
|
243
|
+
(0, runtime_1.emitNervesEvent)({
|
|
244
|
+
component: "repertoire",
|
|
245
|
+
event: "repertoire.ado_semantic_call",
|
|
246
|
+
message: "ado semantic tool invoked",
|
|
247
|
+
meta: {},
|
|
248
|
+
});
|
|
249
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
250
|
+
if (!adoCtx.ok) {
|
|
251
|
+
return adoCtx.error;
|
|
252
|
+
}
|
|
253
|
+
const { organization, project } = adoCtx;
|
|
254
|
+
const wiql = buildWiqlQuery(project, args);
|
|
255
|
+
// Step 1: WIQL query to get IDs
|
|
256
|
+
const wiqlResult = await (0, ado_client_1.adoRequest)(ctx.adoToken, "POST", organization, `/${project}/_apis/wit/wiql`, JSON.stringify({ query: wiql }));
|
|
257
|
+
// Check for API errors (non-JSON responses)
|
|
258
|
+
let wiqlData;
|
|
259
|
+
try {
|
|
260
|
+
wiqlData = JSON.parse(wiqlResult);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return wiqlResult; // Pass through error message
|
|
264
|
+
}
|
|
265
|
+
if (!wiqlData.workItems || wiqlData.workItems.length === 0) {
|
|
266
|
+
return "No work items found matching the query.";
|
|
267
|
+
}
|
|
268
|
+
// Step 2: Batch fetch enriched details (max 200)
|
|
269
|
+
const ids = wiqlData.workItems.slice(0, 200).map(wi => wi.id);
|
|
270
|
+
const batchResult = await (0, ado_client_1.adoRequest)(ctx.adoToken, "GET", organization, `/${project}/_apis/wit/workitems?ids=${ids.join(",")}&fields=${ENRICHED_FIELDS}`);
|
|
271
|
+
let batchData;
|
|
272
|
+
try {
|
|
273
|
+
batchData = JSON.parse(batchResult);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return batchResult;
|
|
277
|
+
}
|
|
278
|
+
const items = formatWorkItems(batchData.value ?? []);
|
|
279
|
+
return formatForChannel(items, ctx, organization, project);
|
|
280
|
+
},
|
|
281
|
+
integration: "ado",
|
|
282
|
+
},
|
|
283
|
+
// -- ado_create_epic --
|
|
284
|
+
{
|
|
285
|
+
tool: {
|
|
286
|
+
type: "function",
|
|
287
|
+
function: {
|
|
288
|
+
name: "ado_create_epic",
|
|
289
|
+
description: "Create an epic in Azure DevOps with title, description, area path, and optional parent.",
|
|
290
|
+
parameters: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
294
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
295
|
+
title: { type: "string", description: "Epic title" },
|
|
296
|
+
description: { type: "string", description: "Epic description" },
|
|
297
|
+
areaPath: { type: "string", description: "Area path" },
|
|
298
|
+
parentId: { type: "string", description: "Parent work item ID (optional)" },
|
|
299
|
+
},
|
|
300
|
+
required: ["title"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
handler: async (args, ctx) => {
|
|
305
|
+
const authErr = await checkAuth(ctx);
|
|
306
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
307
|
+
if (authErr)
|
|
308
|
+
return authErr;
|
|
309
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
310
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
311
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
312
|
+
if (!adoCtx.ok)
|
|
313
|
+
return adoCtx.error;
|
|
314
|
+
const ops = buildCreatePatch(args);
|
|
315
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
316
|
+
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, "POST", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems/$Epic`, JSON.stringify(ops));
|
|
317
|
+
return result;
|
|
318
|
+
},
|
|
319
|
+
integration: "ado",
|
|
320
|
+
confirmationRequired: true,
|
|
321
|
+
},
|
|
322
|
+
// -- ado_create_issue --
|
|
323
|
+
{
|
|
324
|
+
tool: {
|
|
325
|
+
type: "function",
|
|
326
|
+
function: {
|
|
327
|
+
name: "ado_create_issue",
|
|
328
|
+
description: "Create an issue or user story in Azure DevOps with title, description, area path, and parent epic.",
|
|
329
|
+
parameters: {
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: {
|
|
332
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
333
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
334
|
+
title: { type: "string", description: "Issue/story title" },
|
|
335
|
+
description: { type: "string", description: "Description" },
|
|
336
|
+
areaPath: { type: "string", description: "Area path" },
|
|
337
|
+
parentId: { type: "string", description: "Parent work item ID" },
|
|
338
|
+
workItemType: { type: "string", description: "Work item type (default: Issue)" },
|
|
339
|
+
},
|
|
340
|
+
required: ["title"],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
handler: async (args, ctx) => {
|
|
345
|
+
const authErr = await checkAuth(ctx);
|
|
346
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
347
|
+
if (authErr)
|
|
348
|
+
return authErr;
|
|
349
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
350
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
351
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
352
|
+
if (!adoCtx.ok)
|
|
353
|
+
return adoCtx.error;
|
|
354
|
+
const wiType = args.workItemType || "Issue";
|
|
355
|
+
const ops = buildCreatePatch(args);
|
|
356
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
357
|
+
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, "POST", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems/$${wiType}`, JSON.stringify(ops));
|
|
358
|
+
return result;
|
|
359
|
+
},
|
|
360
|
+
integration: "ado",
|
|
361
|
+
confirmationRequired: true,
|
|
362
|
+
},
|
|
363
|
+
// -- ado_move_items --
|
|
364
|
+
{
|
|
365
|
+
tool: {
|
|
366
|
+
type: "function",
|
|
367
|
+
function: {
|
|
368
|
+
name: "ado_move_items",
|
|
369
|
+
description: "Reparent work items -- move them to a new parent epic or feature.",
|
|
370
|
+
parameters: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
374
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
375
|
+
workItemIds: { type: "string", description: "Comma-separated work item IDs to move" },
|
|
376
|
+
newParentId: { type: "string", description: "New parent work item ID" },
|
|
377
|
+
},
|
|
378
|
+
required: ["workItemIds", "newParentId"],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
handler: async (args, ctx) => {
|
|
383
|
+
const authErr = await checkAuth(ctx);
|
|
384
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
385
|
+
if (authErr)
|
|
386
|
+
return authErr;
|
|
387
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
388
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
389
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
390
|
+
if (!adoCtx.ok)
|
|
391
|
+
return adoCtx.error;
|
|
392
|
+
const ids = args.workItemIds.split(",").map(s => s.trim()).filter(Boolean);
|
|
393
|
+
const ops = buildReparentPatch(args.newParentId);
|
|
394
|
+
const moved = [];
|
|
395
|
+
const errors = [];
|
|
396
|
+
for (const id of ids) {
|
|
397
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
398
|
+
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, "PATCH", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems/${id}`, JSON.stringify(ops));
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(result);
|
|
401
|
+
if (parsed.id) {
|
|
402
|
+
moved.push(parsed.id);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
errors.push({ id, error: result });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
errors.push({ id, error: result });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return JSON.stringify({ moved, errors, organization: adoCtx.organization, project: adoCtx.project });
|
|
413
|
+
},
|
|
414
|
+
integration: "ado",
|
|
415
|
+
confirmationRequired: true,
|
|
416
|
+
},
|
|
417
|
+
// -- ado_restructure_backlog --
|
|
418
|
+
{
|
|
419
|
+
tool: {
|
|
420
|
+
type: "function",
|
|
421
|
+
function: {
|
|
422
|
+
name: "ado_restructure_backlog",
|
|
423
|
+
description: "Bulk restructure: reparent multiple work items in a single logical operation.",
|
|
424
|
+
parameters: {
|
|
425
|
+
type: "object",
|
|
426
|
+
properties: {
|
|
427
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
428
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
429
|
+
operations: { type: "string", description: "JSON array of { workItemId, newParentId } objects" },
|
|
430
|
+
},
|
|
431
|
+
required: ["operations"],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
handler: async (args, ctx) => {
|
|
436
|
+
const authErr = await checkAuth(ctx);
|
|
437
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
438
|
+
if (authErr)
|
|
439
|
+
return authErr;
|
|
440
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
441
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
442
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
443
|
+
if (!adoCtx.ok)
|
|
444
|
+
return adoCtx.error;
|
|
445
|
+
let operations;
|
|
446
|
+
try {
|
|
447
|
+
operations = JSON.parse(args.operations);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return "error: operations must be a valid JSON array";
|
|
451
|
+
}
|
|
452
|
+
const results = [];
|
|
453
|
+
for (const op of operations) {
|
|
454
|
+
const ops = buildReparentPatch(String(op.newParentId));
|
|
455
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
456
|
+
const result = await (0, ado_client_1.adoRequest)(ctx.adoToken, "PATCH", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems/${op.workItemId}`, JSON.stringify(ops));
|
|
457
|
+
try {
|
|
458
|
+
const parsed = JSON.parse(result);
|
|
459
|
+
if (parsed.id) {
|
|
460
|
+
results.push({ workItemId: op.workItemId, success: true });
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
results.push({ workItemId: op.workItemId, success: false, error: result });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
results.push({ workItemId: op.workItemId, success: false, error: result });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return JSON.stringify({ results, organization: adoCtx.organization, project: adoCtx.project });
|
|
471
|
+
},
|
|
472
|
+
integration: "ado",
|
|
473
|
+
confirmationRequired: true,
|
|
474
|
+
},
|
|
475
|
+
// -- ado_validate_structure --
|
|
476
|
+
{
|
|
477
|
+
tool: {
|
|
478
|
+
type: "function",
|
|
479
|
+
function: {
|
|
480
|
+
name: "ado_validate_structure",
|
|
481
|
+
description: "Validate parent/child type rules without making changes. Checks if a work item type can be a child of a given parent.",
|
|
482
|
+
parameters: {
|
|
483
|
+
type: "object",
|
|
484
|
+
properties: {
|
|
485
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
486
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
487
|
+
parentId: { type: "string", description: "Parent work item ID" },
|
|
488
|
+
childType: { type: "string", description: "Proposed child work item type" },
|
|
489
|
+
},
|
|
490
|
+
required: ["parentId", "childType"],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
handler: async (args, ctx) => {
|
|
495
|
+
const authErr = await checkAuth(ctx);
|
|
496
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
497
|
+
if (authErr)
|
|
498
|
+
return authErr;
|
|
499
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
500
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
501
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
502
|
+
if (!adoCtx.ok)
|
|
503
|
+
return adoCtx.error;
|
|
504
|
+
// Fetch parent work item to get its type
|
|
505
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
506
|
+
const parentResult = await (0, ado_client_1.adoRequest)(ctx.adoToken, "GET", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems?ids=${args.parentId}&fields=System.WorkItemType`);
|
|
507
|
+
let parentData;
|
|
508
|
+
try {
|
|
509
|
+
parentData = JSON.parse(parentResult);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return parentResult;
|
|
513
|
+
}
|
|
514
|
+
if (!parentData.value || parentData.value.length === 0) {
|
|
515
|
+
return `error: parent work item ${args.parentId} not found`;
|
|
516
|
+
}
|
|
517
|
+
const parentType = parentData.value[0].fields["System.WorkItemType"];
|
|
518
|
+
const allowedChildren = VALID_PARENT_CHILD[parentType] || [];
|
|
519
|
+
const violations = [];
|
|
520
|
+
if (!allowedChildren.includes(args.childType)) {
|
|
521
|
+
violations.push(`${args.childType} cannot be a child of ${parentType}. Allowed children: ${allowedChildren.join(", ") || "none"}`);
|
|
522
|
+
}
|
|
523
|
+
return JSON.stringify({
|
|
524
|
+
valid: violations.length === 0,
|
|
525
|
+
parentType,
|
|
526
|
+
childType: args.childType,
|
|
527
|
+
violations,
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
integration: "ado",
|
|
531
|
+
},
|
|
532
|
+
// -- ado_preview_changes --
|
|
533
|
+
{
|
|
534
|
+
tool: {
|
|
535
|
+
type: "function",
|
|
536
|
+
function: {
|
|
537
|
+
name: "ado_preview_changes",
|
|
538
|
+
description: "Dry-run: preview what a mutation would do without executing it. Returns structured diff of operations.",
|
|
539
|
+
parameters: {
|
|
540
|
+
type: "object",
|
|
541
|
+
properties: {
|
|
542
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
543
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
544
|
+
operation: { type: "string", description: "Operation to preview: create_epic, create_issue, move_items" },
|
|
545
|
+
title: { type: "string", description: "Title (for create operations)" },
|
|
546
|
+
description: { type: "string", description: "Description (for create operations)" },
|
|
547
|
+
areaPath: { type: "string", description: "Area path" },
|
|
548
|
+
parentId: { type: "string", description: "Parent ID" },
|
|
549
|
+
workItemIds: { type: "string", description: "Comma-separated IDs (for move operations)" },
|
|
550
|
+
newParentId: { type: "string", description: "New parent ID (for move operations)" },
|
|
551
|
+
},
|
|
552
|
+
required: ["operation"],
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
handler: async (args, ctx) => {
|
|
557
|
+
const authErr = await checkAuth(ctx);
|
|
558
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
559
|
+
if (authErr)
|
|
560
|
+
return authErr;
|
|
561
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
562
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
563
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
564
|
+
if (!adoCtx.ok)
|
|
565
|
+
return adoCtx.error;
|
|
566
|
+
const operations = buildPreviewOps(args.operation, args);
|
|
567
|
+
if (operations === null) {
|
|
568
|
+
return `Unknown operation: ${args.operation}. Supported: create_epic, create_issue, move_items`;
|
|
569
|
+
}
|
|
570
|
+
return JSON.stringify({
|
|
571
|
+
preview: true,
|
|
572
|
+
operation: args.operation,
|
|
573
|
+
organization: adoCtx.organization,
|
|
574
|
+
project: adoCtx.project,
|
|
575
|
+
operations,
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
integration: "ado",
|
|
579
|
+
},
|
|
580
|
+
// -- ado_batch_update --
|
|
581
|
+
{
|
|
582
|
+
tool: {
|
|
583
|
+
type: "function",
|
|
584
|
+
function: {
|
|
585
|
+
name: "ado_batch_update",
|
|
586
|
+
description: "Execute multiple ADO operations in a single batch. Supports create, update, and reparent operations. Returns per-item results.",
|
|
587
|
+
parameters: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: {
|
|
590
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
591
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
592
|
+
operations: {
|
|
593
|
+
type: "string",
|
|
594
|
+
description: "JSON array of operations. Each: { type: 'create'|'update'|'reparent', workItemId?: number, workItemType?: string, fields?: Record<string, string>, newParentId?: number }",
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
required: ["operations"],
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
handler: async (args, ctx) => {
|
|
602
|
+
const authErr = await checkAuth(ctx);
|
|
603
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
604
|
+
if (authErr)
|
|
605
|
+
return authErr;
|
|
606
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
607
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
608
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
609
|
+
if (!adoCtx.ok)
|
|
610
|
+
return adoCtx.error;
|
|
611
|
+
let operations;
|
|
612
|
+
try {
|
|
613
|
+
operations = JSON.parse(args.operations);
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
return "error: operations must be a valid JSON array";
|
|
617
|
+
}
|
|
618
|
+
const results = [];
|
|
619
|
+
for (let i = 0; i < operations.length; i++) {
|
|
620
|
+
const op = operations[i];
|
|
621
|
+
try {
|
|
622
|
+
let apiResult;
|
|
623
|
+
if (op.type === "create") {
|
|
624
|
+
/* v8 ignore next -- defensive fallback for missing fields @preserve */
|
|
625
|
+
const patchOps = Object.entries(op.fields || {}).map(([key, value]) => ({
|
|
626
|
+
op: "add",
|
|
627
|
+
path: `/fields/${key}`,
|
|
628
|
+
value,
|
|
629
|
+
}));
|
|
630
|
+
/* v8 ignore next -- defensive fallback for missing workItemType @preserve */
|
|
631
|
+
const wiType = op.workItemType || "Task";
|
|
632
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
633
|
+
apiResult = await (0, ado_client_1.adoRequest)(ctx.adoToken, "POST", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems/$${wiType}`, JSON.stringify(patchOps));
|
|
634
|
+
}
|
|
635
|
+
else if (op.type === "update") {
|
|
636
|
+
/* v8 ignore next -- defensive fallback for missing fields @preserve */
|
|
637
|
+
const patchOps = Object.entries(op.fields || {}).map(([key, value]) => ({
|
|
638
|
+
op: "replace",
|
|
639
|
+
path: `/fields/${key}`,
|
|
640
|
+
value,
|
|
641
|
+
}));
|
|
642
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
643
|
+
apiResult = await (0, ado_client_1.adoRequest)(ctx.adoToken, "PATCH", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems/${op.workItemId}`, JSON.stringify(patchOps));
|
|
644
|
+
}
|
|
645
|
+
else if (op.type === "reparent") {
|
|
646
|
+
const patchOps = buildReparentPatch(String(op.newParentId));
|
|
647
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
648
|
+
apiResult = await (0, ado_client_1.adoRequest)(ctx.adoToken, "PATCH", adoCtx.organization, `/${adoCtx.project}/_apis/wit/workitems/${op.workItemId}`, JSON.stringify(patchOps));
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
results.push({ index: i, type: op.type, success: false, error: `Unknown operation type: ${op.type}` });
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const parsed = JSON.parse(apiResult);
|
|
656
|
+
if (parsed.id) {
|
|
657
|
+
results.push({ index: i, type: op.type, success: true, id: parsed.id });
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
results.push({ index: i, type: op.type, success: false, error: apiResult });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
results.push({ index: i, type: op.type, success: false, error: apiResult });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
results.push({ index: i, type: op.type, success: false, error: err instanceof Error ? err.message : String(err) });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return JSON.stringify({ results, organization: adoCtx.organization, project: adoCtx.project });
|
|
672
|
+
},
|
|
673
|
+
integration: "ado",
|
|
674
|
+
confirmationRequired: true,
|
|
675
|
+
},
|
|
676
|
+
// -- ado_detect_orphans --
|
|
677
|
+
{
|
|
678
|
+
tool: {
|
|
679
|
+
type: "function",
|
|
680
|
+
function: {
|
|
681
|
+
name: "ado_detect_orphans",
|
|
682
|
+
description: "Find work items that have no parent but should have one (e.g. Tasks with no parent). Top-level types like Epic are excluded.",
|
|
683
|
+
parameters: {
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {
|
|
686
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
687
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
handler: async (args, ctx) => {
|
|
693
|
+
const authErr = await checkAuth(ctx);
|
|
694
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
695
|
+
if (authErr)
|
|
696
|
+
return authErr;
|
|
697
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
698
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
699
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
700
|
+
if (!adoCtx.ok)
|
|
701
|
+
return adoCtx.error;
|
|
702
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
703
|
+
const fetched = await fetchAllItems(ctx.adoToken, adoCtx.organization, adoCtx.project);
|
|
704
|
+
if ("error" in fetched)
|
|
705
|
+
return fetched.error;
|
|
706
|
+
const orphans = fetched.items
|
|
707
|
+
.filter(wi => {
|
|
708
|
+
const parent = wi.fields["System.Parent"];
|
|
709
|
+
/* v8 ignore next -- defensive fallback for missing ADO field @preserve */
|
|
710
|
+
const type = wi.fields["System.WorkItemType"] ?? "";
|
|
711
|
+
return parent == null && !TOP_LEVEL_TYPES.has(type);
|
|
712
|
+
})
|
|
713
|
+
.map(wi => ({
|
|
714
|
+
id: wi.id,
|
|
715
|
+
/* v8 ignore next -- defensive fallback for missing ADO field @preserve */
|
|
716
|
+
title: wi.fields["System.Title"] ?? "",
|
|
717
|
+
/* v8 ignore next -- defensive fallback for missing ADO field @preserve */
|
|
718
|
+
type: wi.fields["System.WorkItemType"] ?? "",
|
|
719
|
+
/* v8 ignore next -- defensive fallback for missing ADO field @preserve */
|
|
720
|
+
state: wi.fields["System.State"] ?? "",
|
|
721
|
+
}));
|
|
722
|
+
return JSON.stringify({ orphans, organization: adoCtx.organization, project: adoCtx.project });
|
|
723
|
+
},
|
|
724
|
+
integration: "ado",
|
|
725
|
+
},
|
|
726
|
+
// -- ado_detect_cycles --
|
|
727
|
+
{
|
|
728
|
+
tool: {
|
|
729
|
+
type: "function",
|
|
730
|
+
function: {
|
|
731
|
+
name: "ado_detect_cycles",
|
|
732
|
+
description: "Detect circular parent/child relationships in work items. Returns any cycles found.",
|
|
733
|
+
parameters: {
|
|
734
|
+
type: "object",
|
|
735
|
+
properties: {
|
|
736
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
737
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
handler: async (args, ctx) => {
|
|
743
|
+
const authErr = await checkAuth(ctx);
|
|
744
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
745
|
+
if (authErr)
|
|
746
|
+
return authErr;
|
|
747
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
748
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
749
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
750
|
+
if (!adoCtx.ok)
|
|
751
|
+
return adoCtx.error;
|
|
752
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
753
|
+
const fetched = await fetchAllItems(ctx.adoToken, adoCtx.organization, adoCtx.project);
|
|
754
|
+
if ("error" in fetched)
|
|
755
|
+
return fetched.error;
|
|
756
|
+
const cycles = detectCycles(fetched.items);
|
|
757
|
+
return JSON.stringify({ cycles, organization: adoCtx.organization, project: adoCtx.project });
|
|
758
|
+
},
|
|
759
|
+
integration: "ado",
|
|
760
|
+
},
|
|
761
|
+
// -- ado_validate_parent_type_rules --
|
|
762
|
+
{
|
|
763
|
+
tool: {
|
|
764
|
+
type: "function",
|
|
765
|
+
function: {
|
|
766
|
+
name: "ado_validate_parent_type_rules",
|
|
767
|
+
description: "Check all work items have valid parent types per the project's process template rules.",
|
|
768
|
+
parameters: {
|
|
769
|
+
type: "object",
|
|
770
|
+
properties: {
|
|
771
|
+
organization: { type: "string", description: "ADO organization (optional)" },
|
|
772
|
+
project: { type: "string", description: "ADO project (optional)" },
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
handler: async (args, ctx) => {
|
|
778
|
+
const authErr = await checkAuth(ctx);
|
|
779
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
780
|
+
if (authErr)
|
|
781
|
+
return authErr;
|
|
782
|
+
/* v8 ignore next -- non-null assertions guarded by checkAuth; error return tested via ado_backlog_list @preserve */
|
|
783
|
+
const adoCtx = await (0, ado_context_1.resolveAdoContext)(ctx.adoToken, ctx.context, { organization: args.organization, project: args.project });
|
|
784
|
+
/* v8 ignore next -- error return tested via ado_backlog_list @preserve */
|
|
785
|
+
if (!adoCtx.ok)
|
|
786
|
+
return adoCtx.error;
|
|
787
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
788
|
+
const fetched = await fetchAllItems(ctx.adoToken, adoCtx.organization, adoCtx.project);
|
|
789
|
+
if ("error" in fetched)
|
|
790
|
+
return fetched.error;
|
|
791
|
+
// Fetch process template to derive hierarchy rules
|
|
792
|
+
/* v8 ignore next -- non-null assertion guarded by checkAuth above @preserve */
|
|
793
|
+
const template = await (0, ado_templates_1.fetchProcessTemplate)(ctx.adoToken, adoCtx.organization, adoCtx.project);
|
|
794
|
+
if (!template) {
|
|
795
|
+
return JSON.stringify({
|
|
796
|
+
violations: [],
|
|
797
|
+
message: "Could not fetch process template -- type rule validation skipped.",
|
|
798
|
+
organization: adoCtx.organization,
|
|
799
|
+
project: adoCtx.project,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
const rules = (0, ado_templates_1.deriveHierarchyRules)(template.templateName, template.workItemTypes);
|
|
803
|
+
// Build ID->type lookup
|
|
804
|
+
const typeMap = new Map();
|
|
805
|
+
for (const wi of fetched.items) {
|
|
806
|
+
/* v8 ignore next -- defensive fallback for missing ADO field @preserve */
|
|
807
|
+
typeMap.set(wi.id, wi.fields["System.WorkItemType"] ?? "");
|
|
808
|
+
}
|
|
809
|
+
// Check each item's parent/child type relationship
|
|
810
|
+
const violations = [];
|
|
811
|
+
for (const wi of fetched.items) {
|
|
812
|
+
const parentId = wi.fields["System.Parent"];
|
|
813
|
+
if (parentId == null)
|
|
814
|
+
continue;
|
|
815
|
+
/* v8 ignore next -- defensive fallback for missing ADO field @preserve */
|
|
816
|
+
const childType = wi.fields["System.WorkItemType"] ?? "";
|
|
817
|
+
const parentType = typeMap.get(parentId);
|
|
818
|
+
/* v8 ignore next -- parent not in fetched set @preserve */
|
|
819
|
+
if (!parentType)
|
|
820
|
+
continue;
|
|
821
|
+
const allowedChildren = rules[parentType];
|
|
822
|
+
/* v8 ignore next -- unknown parent type or leaf @preserve */
|
|
823
|
+
if (!allowedChildren || allowedChildren.length === 0)
|
|
824
|
+
continue;
|
|
825
|
+
if (!allowedChildren.includes(childType)) {
|
|
826
|
+
violations.push({
|
|
827
|
+
childId: wi.id,
|
|
828
|
+
childType,
|
|
829
|
+
/* v8 ignore next -- defensive fallback for missing ADO field @preserve */
|
|
830
|
+
childTitle: wi.fields["System.Title"] ?? "",
|
|
831
|
+
parentId,
|
|
832
|
+
parentType,
|
|
833
|
+
reason: `${childType} cannot be a child of ${parentType}. Allowed: ${allowedChildren.join(", ")}.`,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return JSON.stringify({ violations, organization: adoCtx.organization, project: adoCtx.project });
|
|
838
|
+
},
|
|
839
|
+
integration: "ado",
|
|
840
|
+
},
|
|
841
|
+
];
|