import { z } from "zod";
import { AdoClient } from "../../ado-client.js";
const linkTypeMap: Record<string, string> = {
parent: "System.LinkTypes.Hierarchy-Reverse",
child: "System.LinkTypes.Hierarchy-Forward",
related: "System.LinkTypes.Related",
predecessor: "System.LinkTypes.Dependency-Reverse",
successor: "System.LinkTypes.Dependency-Forward",
duplicate: "System.LinkTypes.Duplicate-Forward",
"duplicate-of": "System.LinkTypes.Duplicate-Reverse",
tests: "Microsoft.VSTS.Common.TestedBy-Reverse",
"tested-by": "Microsoft.VSTS.Common.TestedBy-Forward",
};
export const removeWorkItemLinkSchema = z.object({
sourceId: z.number().describe("Source work item ID"),
targetId: z.number().describe("Target work item ID"),
linkType: z
.enum([
"parent",
"child",
"related",
"predecessor",
"successor",
"duplicate",
"duplicate-of",
"tests",
"tested-by",
])
.describe("Type of link to remove"),
});
export const removeWorkItemLinkTool = {
name: "remove_work_item_link",
description: "Remove an existing link between two work items. Specify the source and target work item IDs and the link type to remove. Use this to break relationships that are no longer valid, like removing a child from a parent.",
inputSchema: {
type: "object" as const,
properties: {
sourceId: {
type: "number",
description: "Source work item ID",
},
targetId: {
type: "number",
description: "Target work item ID",
},
linkType: {
type: "string",
enum: [
"parent",
"child",
"related",
"predecessor",
"successor",
"duplicate",
"duplicate-of",
"tests",
"tested-by",
],
description: "Type of link to remove",
},
},
required: ["sourceId", "targetId", "linkType"],
},
};
export interface RemoveLinkResult {
success: boolean;
message: string;
}
export async function removeWorkItemLink(
client: AdoClient,
params: z.infer<typeof removeWorkItemLinkSchema>
): Promise<RemoveLinkResult> {
const validatedParams = removeWorkItemLinkSchema.parse(params);
const witApi = await client.getWorkItemTrackingApi();
// Get the ADO link type reference name
const linkTypeRefName = linkTypeMap[validatedParams.linkType];
if (!linkTypeRefName) {
throw new Error(`Unknown link type: ${validatedParams.linkType}`);
}
// First, get the work item to find the relation index
const workItem = await witApi.getWorkItem(
validatedParams.sourceId,
undefined,
undefined,
4 // WorkItemExpand.Relations
);
if (!workItem.relations) {
throw new Error(`Work item ${validatedParams.sourceId} has no relations`);
}
// Find the relation index that matches the target and link type
const orgUrl = client.getOrgUrl();
const targetUrl = `${orgUrl}/_apis/wit/workItems/${validatedParams.targetId}`;
const relationIndex = workItem.relations.findIndex(
(rel) =>
rel.rel === linkTypeRefName &&
rel.url?.toLowerCase().includes(`/workitems/${validatedParams.targetId}`)
);
if (relationIndex === -1) {
throw new Error(
`No ${validatedParams.linkType} link found between work item ${validatedParams.sourceId} and ${validatedParams.targetId}`
);
}
// Remove the relation using patch
const patchDocument = [
{
op: "remove",
path: `/relations/${relationIndex}`,
},
];
await witApi.updateWorkItem(
undefined, // customHeaders
patchDocument,
validatedParams.sourceId
);
return {
success: true,
message: `Successfully removed ${validatedParams.linkType} link between work item ${validatedParams.sourceId} and ${validatedParams.targetId}`,
};
}