@taiga-ai/mcp-server 0.9.0 → 0.10.0-rc.405
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +326 -19
- package/package.json +1 -1
- package/src/client.ts +61 -0
- package/src/server.integration.test.ts +665 -2
- package/src/tools/annotations.ts +24 -2
- package/src/tools/backlogs.ts +233 -3
- package/src/tools/builds.ts +145 -15
- package/src/tools/catalogue.test.ts +2 -0
- package/src/tools/instructions.ts +10 -1
- package/src/tools/products.ts +6 -5
- package/src/tools/prompts.ts +11 -3
- package/taiga-ai-mcp-server-0.10.0-rc.405.tgz +0 -0
- package/taiga-ai-mcp-server-0.9.0.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -175,6 +175,29 @@ var TaigaClient = class {
|
|
|
175
175
|
async getBacklogItem(itemId) {
|
|
176
176
|
return this.request("GET", `/backlog/${seg(itemId)}`);
|
|
177
177
|
}
|
|
178
|
+
async createBacklogItem(productId, body) {
|
|
179
|
+
return this.request("POST", `/products/${seg(productId)}/backlog`, {
|
|
180
|
+
...body,
|
|
181
|
+
productId
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** A person's move on the line board: the column dropped into, and what to follow there. */
|
|
185
|
+
async transitionInitiative(itemId, body) {
|
|
186
|
+
return this.request("PUT", `/backlog/${seg(itemId)}/state`, body);
|
|
187
|
+
}
|
|
188
|
+
/** Answer the needs-human flag: plan again, resume the run, or clear a refused start. */
|
|
189
|
+
async resumeInitiative(itemId) {
|
|
190
|
+
return this.request("POST", `/backlog/${seg(itemId)}/resume`);
|
|
191
|
+
}
|
|
192
|
+
async getActivePlan(itemId) {
|
|
193
|
+
return this.request("GET", `/backlog/${seg(itemId)}/plans/active`);
|
|
194
|
+
}
|
|
195
|
+
async approvePlan(planId) {
|
|
196
|
+
return this.request("POST", `/plans/${seg(planId)}/approve`);
|
|
197
|
+
}
|
|
198
|
+
async rejectPlan(planId, body) {
|
|
199
|
+
return this.request("POST", `/plans/${seg(planId)}/reject`, body);
|
|
200
|
+
}
|
|
178
201
|
async listBuildRuns(productId, opts) {
|
|
179
202
|
const params = new URLSearchParams();
|
|
180
203
|
if (opts?.backlogItemId) params.set("backlogItemId", opts.backlogItemId);
|
|
@@ -183,9 +206,24 @@ var TaigaClient = class {
|
|
|
183
206
|
const qs = params.toString();
|
|
184
207
|
return this.request("GET", `/products/${seg(productId)}/builds${qs ? `?${qs}` : ""}`);
|
|
185
208
|
}
|
|
209
|
+
/** The runs of every product the key can see, newest first; `active` narrows to the ones executing now. */
|
|
210
|
+
async listBuildRunActivity(opts) {
|
|
211
|
+
const params = new URLSearchParams();
|
|
212
|
+
if (opts?.active) params.set("active", "true");
|
|
213
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
214
|
+
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
215
|
+
const qs = params.toString();
|
|
216
|
+
return this.request("GET", `/builds${qs ? `?${qs}` : ""}`);
|
|
217
|
+
}
|
|
186
218
|
async getBuildRun(id) {
|
|
187
219
|
return this.request("GET", `/builds/${seg(id)}`);
|
|
188
220
|
}
|
|
221
|
+
async stopBuildRun(id) {
|
|
222
|
+
return this.request("POST", `/builds/${seg(id)}/stop`);
|
|
223
|
+
}
|
|
224
|
+
async resumeBuildRun(id) {
|
|
225
|
+
return this.request("POST", `/builds/${seg(id)}/resume`);
|
|
226
|
+
}
|
|
189
227
|
async listBuildRunSteps(id) {
|
|
190
228
|
return this.request("GET", `/builds/${seg(id)}/steps`);
|
|
191
229
|
}
|
|
@@ -218,8 +256,10 @@ function readConfig() {
|
|
|
218
256
|
//#endregion
|
|
219
257
|
//#region src/tools/annotations.ts
|
|
220
258
|
/**
|
|
221
|
-
* Tool annotations shared by
|
|
222
|
-
* control: the API enforces what a key may do.
|
|
259
|
+
* Tool annotations shared by the tools. Hints for the client, not a
|
|
260
|
+
* control: the API enforces what a key may do. A client that asks the
|
|
261
|
+
* person before a destructive call reads destructiveHint; the server-side
|
|
262
|
+
* approval gate (PRO-548) is separate work.
|
|
223
263
|
*/
|
|
224
264
|
const READ_ONLY = {
|
|
225
265
|
readOnlyHint: true,
|
|
@@ -227,6 +267,24 @@ const READ_ONLY = {
|
|
|
227
267
|
idempotentHint: true,
|
|
228
268
|
openWorldHint: false
|
|
229
269
|
};
|
|
270
|
+
/** A write that adds or edits a row and starts nothing: a created initiative, a moved one. */
|
|
271
|
+
const WRITE = {
|
|
272
|
+
readOnlyHint: false,
|
|
273
|
+
destructiveHint: false,
|
|
274
|
+
idempotentHint: false,
|
|
275
|
+
openWorldHint: false
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* A write that starts or stops paid work, or opens the door to it: queueing
|
|
279
|
+
* an initiative, approving a plan, resuming or stopping a run. Each starts
|
|
280
|
+
* an agent that spends tokens and opens pull requests, or halts one mid-way.
|
|
281
|
+
*/
|
|
282
|
+
const OPERATE = {
|
|
283
|
+
readOnlyHint: false,
|
|
284
|
+
destructiveHint: true,
|
|
285
|
+
idempotentHint: false,
|
|
286
|
+
openWorldHint: false
|
|
287
|
+
};
|
|
230
288
|
//#endregion
|
|
231
289
|
//#region src/tools/inputs.ts
|
|
232
290
|
/** The inputs several tools share, so a product or a page is named the same way everywhere. */
|
|
@@ -466,6 +524,49 @@ const SETTLED_BACKLOG_STATUSES = ["done", "canceled"];
|
|
|
466
524
|
function isSettledOrArchived(item) {
|
|
467
525
|
return item.archivedAt != null || SETTLED_BACKLOG_STATUSES.includes(item.status);
|
|
468
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* The columns a person can drop an initiative into by hand.
|
|
529
|
+
*
|
|
530
|
+
* `plan` is NOT one of them any more. Planning is the first thing the Build
|
|
531
|
+
* station does, not a place you put work, so the only way to start an
|
|
532
|
+
* initiative is to queue it and let its turn come -- which is also what makes
|
|
533
|
+
* "one at a time" true of planning as well as building, since the line holds
|
|
534
|
+
* one active entry per project and nothing else starts work. `build` is not
|
|
535
|
+
* droppable either: you cannot drop work INTO the station, only take it out.
|
|
536
|
+
* `done` and `canceled` ARE (PRO-1325): finishing work by hand, and reopening
|
|
537
|
+
* it, are moves on the board like any other.
|
|
538
|
+
*/
|
|
539
|
+
const USER_DROP_STATES = [
|
|
540
|
+
"backlog",
|
|
541
|
+
"todo",
|
|
542
|
+
"queue",
|
|
543
|
+
"done",
|
|
544
|
+
"canceled"
|
|
545
|
+
];
|
|
546
|
+
/**
|
|
547
|
+
* The moves that take an initiative off the line for good, and so close a
|
|
548
|
+
* pull request it is still waiting on (PRO-2476). Queue is not one of them:
|
|
549
|
+
* a pull request sent back to the queue waits its turn and is adopted
|
|
550
|
+
* (PRO-2468). Neither is Done: marked done by hand, the pull request may
|
|
551
|
+
* still be merged.
|
|
552
|
+
*/
|
|
553
|
+
const PR_CLOSING_STATES = [
|
|
554
|
+
"backlog",
|
|
555
|
+
"todo",
|
|
556
|
+
"canceled"
|
|
557
|
+
];
|
|
558
|
+
/**
|
|
559
|
+
* What `PUT /backlog/:id/state` accepts: the drops, plus `plan`.
|
|
560
|
+
*
|
|
561
|
+
* `plan` is not a column a person drops into any more, but it is still a state
|
|
562
|
+
* the endpoint takes -- it is how the Build station is told to start over
|
|
563
|
+
* ("Plan again"). One endpoint for both because they are the same write: the
|
|
564
|
+
* initiative's state, decided by a person.
|
|
565
|
+
*/
|
|
566
|
+
const INITIATIVE_TRANSITION_STATES = [...USER_DROP_STATES, "plan"];
|
|
567
|
+
/** Longest rejection comment on a plan (PRO-1324): a paragraph of direction,
|
|
568
|
+
* small enough that the prompt section it becomes stays a section. */
|
|
569
|
+
const PLAN_REJECTION_COMMENT_MAX_CHARS = 2e3;
|
|
469
570
|
const TURN_EVENT_TRIGGERS = [
|
|
470
571
|
"defined",
|
|
471
572
|
"plan_started",
|
|
@@ -1494,16 +1595,20 @@ function describeMiss(type, wanted, timelines, product) {
|
|
|
1494
1595
|
* repository filter). Exactly one of the three; a remote linked to several
|
|
1495
1596
|
* products is ambiguous and says so.
|
|
1496
1597
|
*/
|
|
1497
|
-
async function resolveProductRef(client, ref, accepted) {
|
|
1598
|
+
async function resolveProductRef(client, ref, accepted, usage) {
|
|
1498
1599
|
const refusal = (code, message) => ({ refusal: {
|
|
1499
1600
|
code,
|
|
1500
1601
|
message
|
|
1501
1602
|
} });
|
|
1502
|
-
|
|
1603
|
+
const given = [
|
|
1503
1604
|
ref.productId,
|
|
1504
1605
|
ref.slug,
|
|
1505
1606
|
ref.remoteUrl
|
|
1506
|
-
].filter((v) => v !== void 0).length
|
|
1607
|
+
].filter((v) => v !== void 0).length;
|
|
1608
|
+
if (given !== 1) {
|
|
1609
|
+
const how = given === 0 ? usage ?? "taiga_list_products shows what exists." : "Two names could mean two products.";
|
|
1610
|
+
return refusal("VALIDATION_FAILED", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
|
|
1611
|
+
}
|
|
1507
1612
|
if (ref.productId) try {
|
|
1508
1613
|
return { product: (await client.getProduct(ref.productId)).data };
|
|
1509
1614
|
} catch (error) {
|
|
@@ -1842,8 +1947,44 @@ const BACKLOG_SUMMARY_FIELDS = [
|
|
|
1842
1947
|
"createdAt",
|
|
1843
1948
|
"updatedAt"
|
|
1844
1949
|
];
|
|
1950
|
+
/**
|
|
1951
|
+
* What an initiative says about itself after a write: where it is on the
|
|
1952
|
+
* line, whose turn it is and what the line is waiting for. Not the brief,
|
|
1953
|
+
* the criteria or the technical context the caller already has.
|
|
1954
|
+
*/
|
|
1955
|
+
const ITEM_OUTCOME_FIELDS = [
|
|
1956
|
+
"id",
|
|
1957
|
+
"productId",
|
|
1958
|
+
"title",
|
|
1959
|
+
"status",
|
|
1960
|
+
"state",
|
|
1961
|
+
"planningStatus",
|
|
1962
|
+
"needsHumanCause",
|
|
1963
|
+
"needsHumanDetail",
|
|
1964
|
+
"lifecycle"
|
|
1965
|
+
];
|
|
1966
|
+
const itemId = z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items");
|
|
1967
|
+
const outcome = (item) => toolResult(pickFields([item], ITEM_OUTCOME_FIELDS)[0]);
|
|
1968
|
+
/**
|
|
1969
|
+
* Each tool is registered under the capability its route requires, so a
|
|
1970
|
+
* key sees exactly what it may call: reads on backlog:read, the create and
|
|
1971
|
+
* the board move on backlog:write, queueing on both backlog:write and
|
|
1972
|
+
* products:write (the state route checks both for a drop into queue), the
|
|
1973
|
+
* resume on products:write alone, as its route is (the item ID it takes
|
|
1974
|
+
* also comes back from a create or a queue), and the plan verdicts on
|
|
1975
|
+
* products:write together with backlog:read, because the plan ID they take
|
|
1976
|
+
* comes only from the plan read: a key that cannot read plans has nothing
|
|
1977
|
+
* to feed them.
|
|
1978
|
+
*/
|
|
1845
1979
|
function registerBacklogTools(server, client) {
|
|
1846
|
-
|
|
1980
|
+
const may = (capability) => client.hasCapability(capability);
|
|
1981
|
+
if (may("backlog:read")) registerBacklogReads(server, client);
|
|
1982
|
+
if (may("backlog:write")) registerBacklogWrites(server, client);
|
|
1983
|
+
if (may("backlog:write") && may("products:write")) registerQueue(server, client);
|
|
1984
|
+
if (may("products:write")) registerResume(server, client);
|
|
1985
|
+
if (may("backlog:read") && may("products:write")) registerPlanVerdicts(server, client);
|
|
1986
|
+
}
|
|
1987
|
+
function registerBacklogReads(server, client) {
|
|
1847
1988
|
server.registerTool("taiga_list_backlog_items", {
|
|
1848
1989
|
title: "List backlog items",
|
|
1849
1990
|
description: "List a product's backlog items (initiatives): titles, category, status and where each one is on the line (backlog, todo, queue, plan, build, done). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
|
|
@@ -1883,6 +2024,95 @@ function registerBacklogTools(server, client) {
|
|
|
1883
2024
|
}, (params) => runTool(async () => {
|
|
1884
2025
|
return toolResult((await client.getBacklogItem(params.itemId)).data);
|
|
1885
2026
|
}));
|
|
2027
|
+
server.registerTool("taiga_get_plan", {
|
|
2028
|
+
title: "Get the active plan",
|
|
2029
|
+
description: "Get an initiative's active plan with its steps: what each step changes, the files it writes and reads, its layer and dependencies. Read this before taiga_approve_plan or taiga_reject_plan, and to see what a run will do. NOT_FOUND when the initiative has no active plan yet (planningStatus says whether one is being generated).",
|
|
2030
|
+
inputSchema: { itemId },
|
|
2031
|
+
annotations: READ_ONLY
|
|
2032
|
+
}, (params) => runTool(async () => {
|
|
2033
|
+
return toolResult((await client.getActivePlan(params.itemId)).data);
|
|
2034
|
+
}));
|
|
2035
|
+
}
|
|
2036
|
+
function registerBacklogWrites(server, client) {
|
|
2037
|
+
server.registerTool("taiga_create_backlog_item", {
|
|
2038
|
+
title: "Create backlog item",
|
|
2039
|
+
description: "Create an initiative in the Backlog column of a product: a title and a brief in Markdown (the problem, what it looks like, what it covers and leaves alone), its category, priority and estimate. It starts nothing: the planner reads it only once it is queued (taiga_queue_backlog_item) or moved to Plan (taiga_move_backlog_item). Returns the new item with its ID.",
|
|
2040
|
+
inputSchema: {
|
|
2041
|
+
productId,
|
|
2042
|
+
title: z.string().min(1).max(500).describe("The initiative title, as it appears on the board"),
|
|
2043
|
+
description: z.string().min(1).max(2e4).describe("The brief in Markdown: the problem, what it looks like, what it covers and leaves alone"),
|
|
2044
|
+
category: z.enum([
|
|
2045
|
+
"feature",
|
|
2046
|
+
"bug",
|
|
2047
|
+
"tech_debt",
|
|
2048
|
+
"improvement"
|
|
2049
|
+
]).optional().describe("What kind of work (default feature)"),
|
|
2050
|
+
priority: z.number().int().min(1).optional().describe("1 is the most urgent (default 3)"),
|
|
2051
|
+
estimate: z.number().int().min(0).optional().describe("Story points, 0 for unestimated (default 0)")
|
|
2052
|
+
},
|
|
2053
|
+
annotations: WRITE
|
|
2054
|
+
}, (params) => runTool(async () => {
|
|
2055
|
+
const { productId: id, ...body } = params;
|
|
2056
|
+
return outcome((await client.createBacklogItem(id, body)).data);
|
|
2057
|
+
}));
|
|
2058
|
+
server.registerTool("taiga_move_backlog_item", {
|
|
2059
|
+
title: "Move backlog item on the board",
|
|
2060
|
+
description: `Move an initiative to a column, or order it within one: backlog and todo are the idle columns; queue puts it in the line (taiga_queue_backlog_item says more); plan starts planning now; done and canceled take it off the line. A move to ${PR_CLOSING_STATES.join(", ")} CLOSES a pull request the initiative is still waiting on (a move to queue keeps it, and done leaves it to be merged). The initiative state machine refuses a move that is not allowed (CONFLICT: build cannot be dropped into, an initiative being planned cannot leave plan). A move into queue or plan starts paid work and needs products:write on the key; a key without it is refused here. Ask the person before a move that starts work (queue, plan) or closes a pull request (${PR_CLOSING_STATES.join(", ")} from build).`,
|
|
2061
|
+
inputSchema: {
|
|
2062
|
+
itemId,
|
|
2063
|
+
state: z.enum(INITIATIVE_TRANSITION_STATES).describe("The column to move into"),
|
|
2064
|
+
afterId: z.string().uuid().nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order")
|
|
2065
|
+
},
|
|
2066
|
+
annotations: OPERATE
|
|
2067
|
+
}, (params) => runTool(async () => {
|
|
2068
|
+
const { itemId: id, ...body } = params;
|
|
2069
|
+
if (LINE_STARTING_STATES.has(body.state) && !client.hasCapability("products:write")) return toolError("FORBIDDEN", `A move into ${body.state} starts agent work and needs products:write, which this key lacks.`);
|
|
2070
|
+
return outcome((await client.transitionInitiative(id, body)).data);
|
|
2071
|
+
}));
|
|
2072
|
+
}
|
|
2073
|
+
/** The columns a drop into starts paid work, which the state route gates on products:write. */
|
|
2074
|
+
const LINE_STARTING_STATES = /* @__PURE__ */ new Set(["queue", "plan"]);
|
|
2075
|
+
function registerQueue(server, client) {
|
|
2076
|
+
server.registerTool("taiga_queue_backlog_item", {
|
|
2077
|
+
title: "Queue backlog item",
|
|
2078
|
+
description: "Add an initiative to the product queue, the line the coding agent works: queued initiatives are planned and built one at a time, each advancing when the previous pull request merges. This starts paid work: a planner run, then (when the plan is approved, or at once if auto-build is on) a coding run that pushes commits and opens a pull request in the product repository. Ask the person before calling this. An initiative already in the queue stays where it is unless afterId says where to go; one being planned or built cannot move (CONFLICT). Returns the initiative with its new state.",
|
|
2079
|
+
inputSchema: {
|
|
2080
|
+
itemId,
|
|
2081
|
+
afterId: z.string().uuid().nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is")
|
|
2082
|
+
},
|
|
2083
|
+
annotations: OPERATE
|
|
2084
|
+
}, (params) => runTool(async () => {
|
|
2085
|
+
const { itemId: id, ...placement } = params;
|
|
2086
|
+
return outcome((await client.transitionInitiative(id, {
|
|
2087
|
+
state: "queue",
|
|
2088
|
+
...placement
|
|
2089
|
+
})).data);
|
|
2090
|
+
}));
|
|
2091
|
+
}
|
|
2092
|
+
function registerResume(server, client) {
|
|
2093
|
+
server.registerTool("taiga_resume_backlog_item", {
|
|
2094
|
+
title: "Resume backlog item",
|
|
2095
|
+
description: "Resume an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first incomplete step, a refused build start is cleared and retried. Does whatever the named cause needs, so prefer it to taiga_resume_build when the initiative is flagged. Refused (CONFLICT) when nothing is flagged or the cause has no resume. Starts paid work; ask the person first.",
|
|
2096
|
+
inputSchema: { itemId },
|
|
2097
|
+
annotations: OPERATE
|
|
2098
|
+
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
|
|
2099
|
+
}
|
|
2100
|
+
function registerPlanVerdicts(server, client) {
|
|
2101
|
+
server.registerTool("taiga_approve_plan", {
|
|
2102
|
+
title: "Approve a plan",
|
|
2103
|
+
description: "Approve an initiative's plan: its build starts now, as the approver, the same start the Start run button makes. Read the plan first with taiga_get_plan and name its ID; an approval of a plan that was replaced since is refused (CONFLICT), as is one while a new plan is being generated or a build is already running. Starts a coding run that pushes commits and opens a pull request; ask the person first.",
|
|
2104
|
+
inputSchema: { planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan") },
|
|
2105
|
+
annotations: OPERATE
|
|
2106
|
+
}, (params) => runTool(async () => outcome((await client.approvePlan(params.planId)).data)));
|
|
2107
|
+
server.registerTool("taiga_reject_plan", {
|
|
2108
|
+
title: "Reject a plan",
|
|
2109
|
+
description: "Send an initiative's plan back with direction: a new plan is generated with the comment as reviewer feedback and the rejected plan as its starting point, which supersedes it when the new one lands. Read the plan first with taiga_get_plan and name its ID. Starts a planner run; ask the person first.",
|
|
2110
|
+
inputSchema: {
|
|
2111
|
+
planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan"),
|
|
2112
|
+
comment: z.string().trim().min(1).max(PLAN_REJECTION_COMMENT_MAX_CHARS).describe("What the next plan should do differently: a paragraph of direction")
|
|
2113
|
+
},
|
|
2114
|
+
annotations: OPERATE
|
|
2115
|
+
}, (params) => runTool(async () => outcome((await client.rejectPlan(params.planId, { comment: params.comment })).data)));
|
|
1886
2116
|
}
|
|
1887
2117
|
//#endregion
|
|
1888
2118
|
//#region src/tools/builds.ts
|
|
@@ -1890,6 +2120,7 @@ function registerBacklogTools(server, client) {
|
|
|
1890
2120
|
const BUILD_SUMMARY_FIELDS = [
|
|
1891
2121
|
"id",
|
|
1892
2122
|
"runNumber",
|
|
2123
|
+
"productId",
|
|
1893
2124
|
"backlogItemId",
|
|
1894
2125
|
"targetTitle",
|
|
1895
2126
|
"planId",
|
|
@@ -1906,29 +2137,79 @@ const BUILD_SUMMARY_FIELDS = [
|
|
|
1906
2137
|
"prState",
|
|
1907
2138
|
"prDraft",
|
|
1908
2139
|
"prMerged",
|
|
2140
|
+
"actions",
|
|
1909
2141
|
"createdAt",
|
|
1910
2142
|
"startedAt",
|
|
1911
2143
|
"completedAt"
|
|
1912
2144
|
];
|
|
2145
|
+
/** What a run says about itself after a stop or a resume: its state and what can be done next. */
|
|
2146
|
+
const RUN_OUTCOME_FIELDS = [
|
|
2147
|
+
"id",
|
|
2148
|
+
"runNumber",
|
|
2149
|
+
"backlogItemId",
|
|
2150
|
+
"status",
|
|
2151
|
+
"failedReason",
|
|
2152
|
+
"completedSteps",
|
|
2153
|
+
"totalSteps",
|
|
2154
|
+
"prUrl",
|
|
2155
|
+
"actions"
|
|
2156
|
+
];
|
|
2157
|
+
/** How many products the org-wide list names; past this a row keeps its productId only. */
|
|
2158
|
+
const NAMED_PRODUCTS = 100;
|
|
2159
|
+
const buildId = z.string().uuid().describe("Build run ID -- get from taiga_list_builds");
|
|
2160
|
+
/**
|
|
2161
|
+
* The runs of every product the key can see, each row naming its product.
|
|
2162
|
+
* The route carries product ids only; one products read names them, and a
|
|
2163
|
+
* failure of that read leaves the rows unnamed rather than failing the list.
|
|
2164
|
+
*/
|
|
2165
|
+
async function activityRows(client, opts) {
|
|
2166
|
+
const [runs, products] = await Promise.all([client.listBuildRunActivity(opts), client.listProducts({ limit: NAMED_PRODUCTS }).catch((error) => {
|
|
2167
|
+
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
2168
|
+
throw error;
|
|
2169
|
+
})]);
|
|
2170
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
2171
|
+
for (const product of products?.data ?? []) nameById.set(String(product.id), {
|
|
2172
|
+
name: product.name,
|
|
2173
|
+
slug: product.slug
|
|
2174
|
+
});
|
|
2175
|
+
const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map((row) => {
|
|
2176
|
+
const product = nameById.get(String(row.productId));
|
|
2177
|
+
return product ? {
|
|
2178
|
+
...row,
|
|
2179
|
+
product
|
|
2180
|
+
} : row;
|
|
2181
|
+
});
|
|
2182
|
+
const note = !rows.some((row) => !("product" in row)) ? null : !products ? "The products could not be read, so rows carry productId only; taiga_get_product names one." : products.nextCursor !== null ? `Only the first ${NAMED_PRODUCTS} products could be named; a row without product carries productId only.` : "A row without product belongs to a product the key cannot list (deleted since, or not visible to it); it carries productId only.";
|
|
2183
|
+
return {
|
|
2184
|
+
...page(rows, runs.nextCursor),
|
|
2185
|
+
...note ? { note } : {}
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
1913
2188
|
function registerBuildTools(server, client) {
|
|
1914
2189
|
if (!client.hasCapability("products:read")) return;
|
|
1915
2190
|
server.registerTool("taiga_list_builds", {
|
|
1916
2191
|
title: "List build runs",
|
|
1917
|
-
description: "List the coding agent's runs
|
|
2192
|
+
description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each (stop is taiga_stop_build; resume and retry_from_failed_step are both taiga_resume_build, the same route; start_over is only in the web UI for now). With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
|
|
1918
2193
|
inputSchema: {
|
|
1919
|
-
productId,
|
|
1920
|
-
backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items"),
|
|
2194
|
+
productId: productId.optional(),
|
|
2195
|
+
backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
|
|
2196
|
+
active: z.boolean().optional().describe("Only runs the agent is executing now (cloning through reviewing; a queued pending run is not yet executing and is left out); for the org-wide list"),
|
|
1921
2197
|
...pagination
|
|
1922
2198
|
},
|
|
1923
2199
|
annotations: READ_ONLY
|
|
1924
2200
|
}, (params) => runTool(async () => {
|
|
1925
|
-
|
|
1926
|
-
|
|
2201
|
+
if (!params.productId && params.backlogItemId) return toolError("VALIDATION_FAILED", "backlogItemId narrows the runs of one product: pass its productId as well (the item carries it).");
|
|
2202
|
+
if (params.productId && params.active) return toolError("VALIDATION_FAILED", "active narrows the org-wide list only: omit productId, or read the product's runs and judge each by its status.");
|
|
2203
|
+
if (params.productId) {
|
|
2204
|
+
const result = await client.listBuildRuns(params.productId, params);
|
|
2205
|
+
return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
|
|
2206
|
+
}
|
|
2207
|
+
return toolResult(await activityRows(client, params));
|
|
1927
2208
|
}));
|
|
1928
2209
|
server.registerTool("taiga_get_build", {
|
|
1929
2210
|
title: "Get build run",
|
|
1930
|
-
description: "Get one run in full with its steps: what each step was meant to change, what it did change (commit, files), its status and any error. Use this to pick up where the coding agent stopped, or to understand the pull request it opened.",
|
|
1931
|
-
inputSchema: { buildId
|
|
2211
|
+
description: "Get one run in full with its steps: what each step was meant to change, what it did change (commit, files), its status and any error. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions list says what it accepts: stop (taiga_stop_build), resume or retry_from_failed_step (both taiga_resume_build), start_over (web UI only for now).",
|
|
2212
|
+
inputSchema: { buildId },
|
|
1932
2213
|
annotations: READ_ONLY
|
|
1933
2214
|
}, (params) => runTool(async () => {
|
|
1934
2215
|
const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
|
|
@@ -1937,6 +2218,23 @@ function registerBuildTools(server, client) {
|
|
|
1937
2218
|
steps: steps.data
|
|
1938
2219
|
});
|
|
1939
2220
|
}));
|
|
2221
|
+
if (!client.hasCapability("products:write")) return;
|
|
2222
|
+
server.registerTool("taiga_stop_build", {
|
|
2223
|
+
title: "Stop a build run",
|
|
2224
|
+
description: "Stop a run the coding agent is executing: the agent halts, its branch and any draft pull request stay, and the run can be resumed later with taiga_resume_build. Only a run whose actions include stop (taiga_list_builds shows them); the API refuses the rest with CONFLICT. Ask the person before calling this: it interrupts work in progress.",
|
|
2225
|
+
inputSchema: { buildId },
|
|
2226
|
+
annotations: OPERATE
|
|
2227
|
+
}, (params) => runTool(async () => {
|
|
2228
|
+
return toolResult(pickFields([(await client.stopBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
|
|
2229
|
+
}));
|
|
2230
|
+
server.registerTool("taiga_resume_build", {
|
|
2231
|
+
title: "Resume a build run",
|
|
2232
|
+
description: "Resume a stopped or failed run from its first incomplete step: the agent starts again on the same branch, which spends tokens and may push commits and open a pull request. This is the route behind both the resume and the retry_from_failed_step actions: the API accepts a run whose actions include one of them and refuses the rest with CONFLICT. start_over (a new run) is web-only for now. For an initiative the line stopped on, taiga_resume_backlog_item does whatever its cause needs instead. Ask the person before calling this.",
|
|
2233
|
+
inputSchema: { buildId },
|
|
2234
|
+
annotations: OPERATE
|
|
2235
|
+
}, (params) => runTool(async () => {
|
|
2236
|
+
return toolResult(pickFields([(await client.resumeBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
|
|
2237
|
+
}));
|
|
1940
2238
|
}
|
|
1941
2239
|
//#endregion
|
|
1942
2240
|
//#region src/tools/context.ts
|
|
@@ -2300,16 +2598,16 @@ function registerPrompts(server, client) {
|
|
|
2300
2598
|
title: "Start a session on a product",
|
|
2301
2599
|
description: "Open a coding session with everything Taiga knows about the product: its factory instructions, specification and architecture, the policies that apply, the open backlog and the latest runs. Name the product by the git remote URL of the checkout, its slug or its ID.",
|
|
2302
2600
|
argsSchema: {
|
|
2303
|
-
remoteUrl: remoteUrl.optional(),
|
|
2304
2601
|
slug: slug.optional(),
|
|
2305
|
-
productId: productId.optional()
|
|
2602
|
+
productId: productId.optional(),
|
|
2603
|
+
remoteUrl: remoteUrl.optional()
|
|
2306
2604
|
}
|
|
2307
2605
|
}, async (args) => {
|
|
2308
2606
|
const resolved = await resolveProductRef(client, args, [
|
|
2309
|
-
"remoteUrl",
|
|
2310
2607
|
"slug",
|
|
2311
|
-
"productId"
|
|
2312
|
-
|
|
2608
|
+
"productId",
|
|
2609
|
+
"remoteUrl"
|
|
2610
|
+
], "Pass the slug as the first argument (/taiga_start_session <slug>, the slug from the Taiga URL); taiga_list_products lists the slugs.");
|
|
2313
2611
|
if ("refusal" in resolved) refuse(resolved.refusal);
|
|
2314
2612
|
return message([
|
|
2315
2613
|
"You are working on a product managed in Taiga. The fenced JSON below is its context: the product and",
|
|
@@ -2567,7 +2865,16 @@ const SERVER_INSTRUCTIONS = [
|
|
|
2567
2865
|
"",
|
|
2568
2866
|
"**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
|
|
2569
2867
|
"The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
|
|
2570
|
-
"for them: runs, steps, commits and pull requests.",
|
|
2868
|
+
"for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
|
|
2869
|
+
"every product the key can see, and active: true the ones executing now.",
|
|
2870
|
+
"",
|
|
2871
|
+
"**Operating the line** (taiga_create_backlog_item / taiga_queue_backlog_item / taiga_move_backlog_item /",
|
|
2872
|
+
"taiga_resume_backlog_item / taiga_get_plan / taiga_approve_plan / taiga_reject_plan / taiga_stop_build /",
|
|
2873
|
+
"taiga_resume_build): create an initiative, put it in the queue the coding agent works, move it on the",
|
|
2874
|
+
"board, read and approve or reject its plan, stop or resume a run. Queueing, approving and resuming start",
|
|
2875
|
+
"paid agent work that pushes commits and opens pull requests; stopping interrupts it. Tell the person what",
|
|
2876
|
+
"you are about to start or stop and wait for their yes before calling these; a client that prompts on",
|
|
2877
|
+
"destructive tools does that for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
|
|
2571
2878
|
"",
|
|
2572
2879
|
"**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
|
|
2573
2880
|
"provenance fields you can read past: `source` says where the text originally came from (template:",
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -295,6 +295,44 @@ export class TaigaClient {
|
|
|
295
295
|
return this.request<{ data: unknown }>('GET', `/backlog/${seg(itemId)}`);
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
async createBacklogItem(
|
|
299
|
+
productId: string,
|
|
300
|
+
body: {
|
|
301
|
+
title: string;
|
|
302
|
+
description: string;
|
|
303
|
+
category?: string | undefined;
|
|
304
|
+
priority?: number | undefined;
|
|
305
|
+
estimate?: number | undefined;
|
|
306
|
+
}
|
|
307
|
+
) {
|
|
308
|
+
// The route's body schema names the product too, as the web's
|
|
309
|
+
// CreateBacklogItemInput does (the live run found it; the handler then
|
|
310
|
+
// takes the one from the path). Dropping it is an API contract change.
|
|
311
|
+
return this.request<{ data: unknown }>('POST', `/products/${seg(productId)}/backlog`, { ...body, productId });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A person's move on the line board: the column dropped into, and what to follow there. */
|
|
315
|
+
async transitionInitiative(itemId: string, body: { state: string; afterId?: string | null | undefined }) {
|
|
316
|
+
return this.request<{ data: unknown }>('PUT', `/backlog/${seg(itemId)}/state`, body);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Answer the needs-human flag: plan again, resume the run, or clear a refused start. */
|
|
320
|
+
async resumeInitiative(itemId: string) {
|
|
321
|
+
return this.request<{ data: unknown }>('POST', `/backlog/${seg(itemId)}/resume`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async getActivePlan(itemId: string) {
|
|
325
|
+
return this.request<{ data: unknown }>('GET', `/backlog/${seg(itemId)}/plans/active`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async approvePlan(planId: string) {
|
|
329
|
+
return this.request<{ data: unknown }>('POST', `/plans/${seg(planId)}/approve`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async rejectPlan(planId: string, body: { comment: string }) {
|
|
333
|
+
return this.request<{ data: unknown }>('POST', `/plans/${seg(planId)}/reject`, body);
|
|
334
|
+
}
|
|
335
|
+
|
|
298
336
|
// ---------------------------------------------------------------------------
|
|
299
337
|
// Build runs
|
|
300
338
|
// ---------------------------------------------------------------------------
|
|
@@ -314,10 +352,33 @@ export class TaigaClient {
|
|
|
314
352
|
);
|
|
315
353
|
}
|
|
316
354
|
|
|
355
|
+
/** The runs of every product the key can see, newest first; `active` narrows to the ones executing now. */
|
|
356
|
+
async listBuildRunActivity(opts?: {
|
|
357
|
+
active?: boolean | undefined;
|
|
358
|
+
limit?: number | undefined;
|
|
359
|
+
cursor?: string | undefined;
|
|
360
|
+
}) {
|
|
361
|
+
const params = new URLSearchParams();
|
|
362
|
+
// The route reads the literal strings 'true' and 'false'.
|
|
363
|
+
if (opts?.active) params.set('active', 'true');
|
|
364
|
+
if (opts?.limit) params.set('limit', String(opts.limit));
|
|
365
|
+
if (opts?.cursor) params.set('cursor', opts.cursor);
|
|
366
|
+
const qs = params.toString();
|
|
367
|
+
return this.request<{ data: unknown[]; nextCursor: string | null }>('GET', `/builds${qs ? `?${qs}` : ''}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
317
370
|
async getBuildRun(id: string) {
|
|
318
371
|
return this.request<{ data: unknown }>('GET', `/builds/${seg(id)}`);
|
|
319
372
|
}
|
|
320
373
|
|
|
374
|
+
async stopBuildRun(id: string) {
|
|
375
|
+
return this.request<{ data: unknown }>('POST', `/builds/${seg(id)}/stop`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async resumeBuildRun(id: string) {
|
|
379
|
+
return this.request<{ data: unknown }>('POST', `/builds/${seg(id)}/resume`);
|
|
380
|
+
}
|
|
381
|
+
|
|
321
382
|
async listBuildRunSteps(id: string) {
|
|
322
383
|
return this.request<{ data: unknown[] }>('GET', `/builds/${seg(id)}/steps`);
|
|
323
384
|
}
|