@taiga-ai/mcp-server 0.26.0-dev.487 → 0.26.0-dev.489
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 +210 -185
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -124,6 +124,14 @@ function parseErrorBody(body) {
|
|
|
124
124
|
*/
|
|
125
125
|
var TaigaClient = class {
|
|
126
126
|
baseUrl;
|
|
127
|
+
/** The API this client talks to, as its host: the environment a reader is in (PRO-2823). */
|
|
128
|
+
get apiHost() {
|
|
129
|
+
try {
|
|
130
|
+
return new URL(this.baseUrl).host;
|
|
131
|
+
} catch {
|
|
132
|
+
return this.baseUrl;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
127
135
|
apiKey;
|
|
128
136
|
introspection = null;
|
|
129
137
|
constructor(opts) {
|
|
@@ -269,6 +277,9 @@ var TaigaClient = class {
|
|
|
269
277
|
if (opts?.status) params.set("status", opts.status);
|
|
270
278
|
if (opts?.planningStatus) params.set("planningStatus", opts.planningStatus);
|
|
271
279
|
if (opts?.category) params.set("category", opts.category);
|
|
280
|
+
if (opts?.state) params.set("state", opts.state);
|
|
281
|
+
if (opts?.open) params.set("open", "true");
|
|
282
|
+
if (opts?.order) params.set("order", opts.order);
|
|
272
283
|
if (opts?.includeArchived) params.set("includeArchived", "true");
|
|
273
284
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
274
285
|
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
@@ -870,9 +881,6 @@ const PRODUCT_DOCUMENT_DISPLAY_NAMES = {
|
|
|
870
881
|
const PRODUCT_DOCUMENT_ORIGINS = ["plan", "reality"];
|
|
871
882
|
//#endregion
|
|
872
883
|
//#region ../../packages/types/dist/backlog.js
|
|
873
|
-
/** Dispositions that end an item's pipeline life: settled work neither blocks
|
|
874
|
-
* dependants nor counts toward completion denominators. */
|
|
875
|
-
const SETTLED_BACKLOG_STATUSES = ["done", "canceled"];
|
|
876
884
|
/**
|
|
877
885
|
* The initiative's STORED state on the line board (PRO-1305, phase 1).
|
|
878
886
|
*
|
|
@@ -922,11 +930,6 @@ const LINE_STATES = [
|
|
|
922
930
|
"plan",
|
|
923
931
|
"build"
|
|
924
932
|
];
|
|
925
|
-
/** Finished work: settled by a person or a merge, or archived. The one
|
|
926
|
-
* predicate every "can this still move / plan / build" guard reads. */
|
|
927
|
-
function isSettledOrArchived(item) {
|
|
928
|
-
return item.archivedAt != null || SETTLED_BACKLOG_STATUSES.includes(item.status);
|
|
929
|
-
}
|
|
930
933
|
/**
|
|
931
934
|
* The columns a person can drop an initiative into by hand.
|
|
932
935
|
*
|
|
@@ -2245,7 +2248,7 @@ const DOCUMENT_INPUTS = Object.freeze({
|
|
|
2245
2248
|
* reads as deleted) and fields they explain would simply be missing. So it
|
|
2246
2249
|
* refuses to start, and says what to do.
|
|
2247
2250
|
*/
|
|
2248
|
-
function apiLevelProblem(introspection, apiUrl, required =
|
|
2251
|
+
function apiLevelProblem(introspection, apiUrl, required = 2, version = SERVER_VERSION) {
|
|
2249
2252
|
const offered = introspection.mcpApiLevel ?? 0;
|
|
2250
2253
|
if (offered >= required) return null;
|
|
2251
2254
|
return `The Taiga API at ${apiUrl} is older than @taiga-ai/mcp-server ${version} needs (API level ${offered}, needs ${required}). Run the version published for it, or wait for that API to be updated; an earlier release of this package works against it.`;
|
|
@@ -2502,7 +2505,7 @@ const BY_ARGUMENT = {
|
|
|
2502
2505
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2503
2506
|
const BY_TOOL = {
|
|
2504
2507
|
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2505
|
-
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative says whether one is being
|
|
2508
|
+
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and taiga_plan_again asks for a new one." : ITEM_ID_HINT
|
|
2506
2509
|
};
|
|
2507
2510
|
/**
|
|
2508
2511
|
* What the API calls the thing each ID names, as its 404 says it ("Environment
|
|
@@ -4033,7 +4036,7 @@ const FAILED_REASON_MEANS = {
|
|
|
4033
4036
|
* text out: its first sentence is in the outcome, and taiga_get_run has it all.
|
|
4034
4037
|
*/
|
|
4035
4038
|
function readableRun(run, opts = {}) {
|
|
4036
|
-
const { prDraft, error, reviewEscalation, runtime: _runtime, ...rest } = run;
|
|
4039
|
+
const { prDraft, error, reviewEscalation, runtime: _runtime, origin, ...rest } = run;
|
|
4037
4040
|
const said = runOutcome(run);
|
|
4038
4041
|
const means = FAILED_REASON_MEANS[String(run["failedReason"])];
|
|
4039
4042
|
const hardening = run["hardeningResult"];
|
|
@@ -4042,13 +4045,14 @@ function readableRun(run, opts = {}) {
|
|
|
4042
4045
|
const scan = scanSaid(hardening);
|
|
4043
4046
|
return {
|
|
4044
4047
|
...rest,
|
|
4048
|
+
...origin === void 0 ? {} : { origin: origin === "backlog" ? "initiative" : origin },
|
|
4045
4049
|
...means ? { failedReasonMeans: means } : {},
|
|
4046
4050
|
...scan && scan.gaps + (scan.verdict === "not_scanned" ? 1 : 0) > 0 ? { scanCoverage: scan.said } : {},
|
|
4047
4051
|
...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
|
|
4048
4052
|
...opts.row || error === void 0 ? {} : { error },
|
|
4049
4053
|
...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
|
|
4050
4054
|
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
4051
|
-
...said
|
|
4055
|
+
...said ? { outcome: said.outcome } : {}
|
|
4052
4056
|
};
|
|
4053
4057
|
}
|
|
4054
4058
|
/** Distinct initiatives a run list reads to say which moved on; past this a waiting row says it was not checked. */
|
|
@@ -4100,7 +4104,8 @@ const INITIATIVE_ENDED = {
|
|
|
4100
4104
|
/** A readable run row that also says when its initiative ended while its pull request waited. */
|
|
4101
4105
|
function withInitiativeState(row, states) {
|
|
4102
4106
|
const state = states.get(String(row["backlogItemId"]));
|
|
4103
|
-
|
|
4107
|
+
const status = String(row["status"]);
|
|
4108
|
+
if (!(BUILD_RUN_STATUSES.includes(status) && ["gated", "unmerged"].includes(landedDisposition(status, row["prMerged"] === true)))) return row;
|
|
4104
4109
|
if (state === "unchecked") return {
|
|
4105
4110
|
...row,
|
|
4106
4111
|
outcome: `${String(row["outcome"])} Whether its initiative still wants it was not checked.`
|
|
@@ -4111,7 +4116,6 @@ function withInitiativeState(row, states) {
|
|
|
4111
4116
|
const open = row["prState"] === "open" ? ` ${pr} is still open on the forge.` : "";
|
|
4112
4117
|
return {
|
|
4113
4118
|
...row,
|
|
4114
|
-
disposition: "abandoned",
|
|
4115
4119
|
actions: [],
|
|
4116
4120
|
outcome: `Its initiative ${ended}, so nobody is waiting on this run.${open}`
|
|
4117
4121
|
};
|
|
@@ -4220,19 +4224,20 @@ function registerBuildTools(server, client) {
|
|
|
4220
4224
|
if (!client.hasCapability("products:read")) return;
|
|
4221
4225
|
server.registerTool("taiga_list_runs", {
|
|
4222
4226
|
title: "List runs",
|
|
4223
|
-
description: "List coding agent runs, newest first: branch, pull request, step progress, outcome
|
|
4227
|
+
description: "List coding agent runs, newest first: status, branch, pull request, step progress, actions and outcome, one sentence saying how it stands: running, waiting for a person to review and merge, merged, failed, stopped, its pull request closed unmerged, or nobody waiting on it because its initiative ended. Without productId, every product's runs; active: true keeps running ones. taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
|
|
4224
4228
|
inputSchema: z.object({
|
|
4225
4229
|
productId: productId.optional(),
|
|
4226
|
-
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional().describe("Only this initiative's runs -- get from taiga_list_initiatives
|
|
4230
|
+
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional().describe("Only this initiative's runs -- get from taiga_list_initiatives"),
|
|
4227
4231
|
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"),
|
|
4228
4232
|
...pagination
|
|
4229
4233
|
}),
|
|
4230
4234
|
annotations: READ_ONLY
|
|
4231
4235
|
}, (params) => runTool(async () => {
|
|
4232
|
-
if (
|
|
4233
|
-
if (params.productId && params.
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
+
if (params.active && (params.productId || params.initiativeId)) return toolError("VALIDATION_ERROR", "active narrows the org-wide list only: omit productId and initiativeId, or read the product's runs and judge each by its status.");
|
|
4237
|
+
if (!params.productId && params.initiativeId && !client.hasCapability("backlog:read")) return toolError("VALIDATION_ERROR", "This key cannot read initiatives, so it cannot find the initiative's product: pass productId as well.");
|
|
4238
|
+
const productIdOf = params.productId ?? (params.initiativeId ? (await client.getBacklogItem(params.initiativeId)).data.productId : void 0);
|
|
4239
|
+
if (productIdOf) {
|
|
4240
|
+
const result = await client.listBuildRuns(productIdOf, {
|
|
4236
4241
|
backlogItemId: params.initiativeId,
|
|
4237
4242
|
limit: params.limit,
|
|
4238
4243
|
cursor: params.cursor
|
|
@@ -4245,7 +4250,7 @@ function registerBuildTools(server, client) {
|
|
|
4245
4250
|
}));
|
|
4246
4251
|
server.registerTool("taiga_get_run", {
|
|
4247
4252
|
title: "Get run",
|
|
4248
|
-
description: "Read one run with its steps:
|
|
4253
|
+
description: "Read one run with its steps: status, outcome, pull request, hardening (scanCoverage names the security scanners that did not run, and why; hardeningResult.verdict is passed only when every one ran), and per step its title, status, commit, files changed, verification and headline (first paragraph of the agent's report). verification is Taiga's own check after the step (tests, lint, build), so a docs-only step can fail on the repository's failing tests (verificationNote says when one failed throughout). A failed or stopped run has stoppedIn, lastProgressAt and failedReasonMeans (what its failedReason means). full: true adds complete instructions and reports. actions: resume and retry_from_failed_step are taiga_resume_initiative; webOnlyActions (start_over) are in the web; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
|
|
4249
4254
|
inputSchema: z.object({
|
|
4250
4255
|
runId,
|
|
4251
4256
|
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
@@ -4372,9 +4377,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4372
4377
|
if (item) {
|
|
4373
4378
|
Object.assign(row, {
|
|
4374
4379
|
title: item.title,
|
|
4375
|
-
|
|
4376
|
-
planningStatus: item.planningStatus,
|
|
4377
|
-
needsHumanCause: item.needsHumanCause,
|
|
4380
|
+
...standing(item),
|
|
4378
4381
|
criteria: criteriaCounts(item),
|
|
4379
4382
|
lifecycle: lifecycle ? {
|
|
4380
4383
|
turn: lifecycle.turn,
|
|
@@ -4406,7 +4409,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4406
4409
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
4407
4410
|
return {
|
|
4408
4411
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
4409
|
-
entries: rows,
|
|
4412
|
+
entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
|
|
4410
4413
|
waitingOn: line ?? waitingOn(rows),
|
|
4411
4414
|
...note ? { note } : {}
|
|
4412
4415
|
};
|
|
@@ -4452,18 +4455,21 @@ function registerQueueTools(server, client) {
|
|
|
4452
4455
|
}
|
|
4453
4456
|
//#endregion
|
|
4454
4457
|
//#region src/tools/backlogs.ts
|
|
4455
|
-
/**
|
|
4458
|
+
/**
|
|
4459
|
+
* What a list row needs to say for the model to pick an item to read in full.
|
|
4460
|
+
* One vocabulary for where an initiative stands (PRO-2828): its board column,
|
|
4461
|
+
* whose turn it is and the situation sentence. The API's own fields for it
|
|
4462
|
+
* (status, planningStatus, needsHumanCause, lifecycle) said the same thing
|
|
4463
|
+
* four more ways, and a reviewing agent had to reconcile them; they are in
|
|
4464
|
+
* taiga_get_initiative with full: true.
|
|
4465
|
+
*/
|
|
4456
4466
|
const BACKLOG_SUMMARY_FIELDS = [
|
|
4457
4467
|
"id",
|
|
4458
4468
|
"title",
|
|
4459
4469
|
"category",
|
|
4460
|
-
"status",
|
|
4461
|
-
"state",
|
|
4462
|
-
"planningStatus",
|
|
4463
4470
|
"priority",
|
|
4464
4471
|
"estimate",
|
|
4465
4472
|
"origin",
|
|
4466
|
-
"needsHumanCause",
|
|
4467
4473
|
"archivedAt",
|
|
4468
4474
|
"createdAt",
|
|
4469
4475
|
"updatedAt"
|
|
@@ -4477,21 +4483,44 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
4477
4483
|
"id",
|
|
4478
4484
|
"productId",
|
|
4479
4485
|
"title",
|
|
4480
|
-
"status",
|
|
4481
|
-
"state",
|
|
4482
|
-
"planningStatus",
|
|
4483
|
-
"needsHumanCause",
|
|
4484
|
-
"needsHumanDetail",
|
|
4485
4486
|
"autoBuild",
|
|
4486
|
-
"archivedAt"
|
|
4487
|
-
|
|
4487
|
+
"archivedAt"
|
|
4488
|
+
];
|
|
4489
|
+
/** The fields of an initiative's summary read: its row, plus what it is about. */
|
|
4490
|
+
const ITEM_SUMMARY_FIELDS = [
|
|
4491
|
+
...BACKLOG_SUMMARY_FIELDS,
|
|
4492
|
+
"productId",
|
|
4493
|
+
"description",
|
|
4494
|
+
"autoBuild",
|
|
4495
|
+
"dependencies"
|
|
4488
4496
|
];
|
|
4497
|
+
/** How much of a description a summary read carries; full: true has the rest. */
|
|
4498
|
+
const DESCRIPTION_IN_SUMMARY = 600;
|
|
4499
|
+
/**
|
|
4500
|
+
* Where an initiative stands, in the one vocabulary every read uses
|
|
4501
|
+
* (PRO-2828): its board column, whose turn it is, the situation sentence, and
|
|
4502
|
+
* its latest run to open with taiga_get_run.
|
|
4503
|
+
*/
|
|
4504
|
+
function standing(item) {
|
|
4505
|
+
const life = item["lifecycle"];
|
|
4506
|
+
const run = life?.latestBuild;
|
|
4507
|
+
const situation = situationOf(item);
|
|
4508
|
+
return {
|
|
4509
|
+
column: typeof item["state"] === "string" ? item["state"] : null,
|
|
4510
|
+
turn: typeof life?.turn === "string" ? life.turn : null,
|
|
4511
|
+
...situation ? { situation } : {},
|
|
4512
|
+
latestRun: run && typeof run.id === "string" ? {
|
|
4513
|
+
id: run.id,
|
|
4514
|
+
...typeof run.prNumber === "number" ? { prNumber: run.prNumber } : {}
|
|
4515
|
+
} : null
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4489
4518
|
/**
|
|
4490
4519
|
* What the lifecycle on an initiative says, and the call each next step is.
|
|
4491
4520
|
* needsHumanCause is only the line's flag; a plan that failed outside the line
|
|
4492
4521
|
* leaves it null, and the lifecycle is what says why and what moves it on.
|
|
4493
4522
|
*/
|
|
4494
|
-
const LIFECYCLE_MEANING = "situation says in a sentence where it stands and
|
|
4523
|
+
const LIFECYCLE_MEANING = "situation says in a sentence where it stands and names the next call; turn says whose move it is (you, agent, or idle: nobody owes one).";
|
|
4495
4524
|
/** The move each call to action is, in the tools' words. */
|
|
4496
4525
|
const CTA_MOVE = {
|
|
4497
4526
|
start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
|
|
@@ -4504,8 +4533,8 @@ const CTA_MOVE = {
|
|
|
4504
4533
|
};
|
|
4505
4534
|
/** The move for an initiative off the line whose last run did not land: there is no line to take it off (PRO-2840). */
|
|
4506
4535
|
const OFF_THE_LINE_MOVE = "taiga_queue_initiative puts it back on the line (paid work: ask first)";
|
|
4507
|
-
/** What happens to a queued initiative
|
|
4508
|
-
const REQUEUED_MOVE = "the line
|
|
4536
|
+
/** What happens to a queued initiative: the line takes it up when its turn comes, replanning a run that did not land (PRO-2840). */
|
|
4537
|
+
const REQUEUED_MOVE = "the line takes it up when its turn comes";
|
|
4509
4538
|
/** How a run stands, as the end of "its latest run ...", by disposition. */
|
|
4510
4539
|
const RUN_STANDS = {
|
|
4511
4540
|
in_flight: "is running",
|
|
@@ -4542,7 +4571,10 @@ function situationOf(item) {
|
|
|
4542
4571
|
const column = typeof item["state"] === "string" ? item["state"] : null;
|
|
4543
4572
|
const offTheLine = column !== null && !LINE_STATES.includes(column);
|
|
4544
4573
|
const stalled = life.stage === "building" && life.turn !== "agent";
|
|
4545
|
-
const requeued =
|
|
4574
|
+
const requeued = column === "queue" && (stalled || life.turn === "idle" && life.cta == null);
|
|
4575
|
+
const offTheLineStart = offTheLine && (life.cta === "start_build" || life.cta === "start_plan");
|
|
4576
|
+
if (requeued && !stalled) stage = life.stage === "backlog" ? "Queued" : `Queued, ${stage.charAt(0).toLowerCase()}${stage.slice(1)}`;
|
|
4577
|
+
else if (offTheLineStart && life.stage === "planned") stage = "Planned, off the line";
|
|
4546
4578
|
if (stalled) stage = requeued ? "Queued" : offTheLine ? "Off the line" : column === null ? "Its build stopped" : "On the line, but its build stopped";
|
|
4547
4579
|
const build = life.latestBuild;
|
|
4548
4580
|
if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
|
|
@@ -4563,7 +4595,7 @@ function situationOf(item) {
|
|
|
4563
4595
|
}
|
|
4564
4596
|
const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
|
|
4565
4597
|
const handedBack = handbackSaid(build?.reviewEscalation);
|
|
4566
|
-
const move = life.cta === "review_pr" && handedBack ? `a person should ${handbackNextStep(build.reviewEscalation)}` : stalled && offTheLine ? OFF_THE_LINE_MOVE : typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
|
|
4598
|
+
const move = life.cta === "review_pr" && handedBack ? `a person should ${handbackNextStep(build.reviewEscalation)}` : stalled && offTheLine || offTheLineStart && life.stage !== "backlog" ? OFF_THE_LINE_MOVE : typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
|
|
4567
4599
|
if (requeued) parts.push(`Nothing to do: ${REQUEUED_MOVE}.`);
|
|
4568
4600
|
else if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
|
|
4569
4601
|
else if (turn && move) parts.push(`${turn}: ${move}.`);
|
|
@@ -4591,15 +4623,14 @@ function endingSaid(item) {
|
|
|
4591
4623
|
if (next && typeof next.id === "string") said.push(`; superseded by "${String(next.title)}" (${next.id})`);
|
|
4592
4624
|
return said.join("");
|
|
4593
4625
|
}
|
|
4594
|
-
/** A row or item with
|
|
4626
|
+
/** A row or item with where it stands added (`standing`). */
|
|
4595
4627
|
function withSituation(item, row = item) {
|
|
4596
|
-
|
|
4597
|
-
return situation ? {
|
|
4628
|
+
return {
|
|
4598
4629
|
...row,
|
|
4599
|
-
|
|
4600
|
-
}
|
|
4630
|
+
...standing(item)
|
|
4631
|
+
};
|
|
4601
4632
|
}
|
|
4602
|
-
/** List rows, each with
|
|
4633
|
+
/** List rows, each with where it stands. */
|
|
4603
4634
|
function situatedRows(items) {
|
|
4604
4635
|
return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index], row));
|
|
4605
4636
|
}
|
|
@@ -4676,36 +4707,6 @@ const outcomeOf = (item) => withSituation(item, {
|
|
|
4676
4707
|
criteria: criteriaCounts(item)
|
|
4677
4708
|
});
|
|
4678
4709
|
const outcome = (item) => toolResult(outcomeOf(item));
|
|
4679
|
-
/** The statuses an initiative in each board column can have: the column decides it (statusForState, PRO-2828). */
|
|
4680
|
-
const COLUMN_STATUSES = {
|
|
4681
|
-
backlog: [
|
|
4682
|
-
"all",
|
|
4683
|
-
"open",
|
|
4684
|
-
"backlog"
|
|
4685
|
-
],
|
|
4686
|
-
todo: [
|
|
4687
|
-
"all",
|
|
4688
|
-
"open",
|
|
4689
|
-
"backlog"
|
|
4690
|
-
],
|
|
4691
|
-
queue: [
|
|
4692
|
-
"all",
|
|
4693
|
-
"open",
|
|
4694
|
-
"backlog"
|
|
4695
|
-
],
|
|
4696
|
-
plan: [
|
|
4697
|
-
"all",
|
|
4698
|
-
"open",
|
|
4699
|
-
"backlog"
|
|
4700
|
-
],
|
|
4701
|
-
build: [
|
|
4702
|
-
"all",
|
|
4703
|
-
"open",
|
|
4704
|
-
"in_progress"
|
|
4705
|
-
],
|
|
4706
|
-
done: ["all", "done"],
|
|
4707
|
-
canceled: ["all", "canceled"]
|
|
4708
|
-
};
|
|
4709
4710
|
/**
|
|
4710
4711
|
* Each tool is registered under the capability its route requires, so a
|
|
4711
4712
|
* key sees exactly what it may call: reads on backlog:read; the board move on
|
|
@@ -4738,25 +4739,15 @@ function registerBacklogTools(server, client) {
|
|
|
4738
4739
|
function registerBacklogReads(server, client) {
|
|
4739
4740
|
server.registerTool("taiga_list_initiatives", {
|
|
4740
4741
|
title: "List initiatives",
|
|
4741
|
-
description: "List a product's open
|
|
4742
|
+
description: "List a product's initiatives, newest first: open ones (not done or canceled) unless column says otherwise. Each row has its title, category, column (backlog, todo, queue, plan, build, done, canceled), turn (you, agent or idle), situation (where it stands and what moves it on, in a sentence), latestRun and criteria (acceptance criteria met; null before planning). query finds one by title; taiga_get_initiative reads one. Archived ones are left out unless asked for.",
|
|
4742
4743
|
inputSchema: z.object({
|
|
4743
4744
|
productId,
|
|
4744
4745
|
query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
|
|
4745
|
-
|
|
4746
|
+
column: z.enum([
|
|
4746
4747
|
"open",
|
|
4747
4748
|
"all",
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
"done",
|
|
4751
|
-
"canceled"
|
|
4752
|
-
]).optional().describe("open (default without state): not done or canceled; all: every one; in_progress is the Build column, backlog every other open one"),
|
|
4753
|
-
planningStatus: z.enum([
|
|
4754
|
-
"unplanned",
|
|
4755
|
-
"planning",
|
|
4756
|
-
"planned",
|
|
4757
|
-
"failed"
|
|
4758
|
-
]).optional().describe("Only items whose plan is in this state"),
|
|
4759
|
-
state: z.enum(INITIATIVE_STATES).optional().describe("Only items in this column of the board: backlog, todo, queue, plan, build, done or canceled (status groups them coarser)"),
|
|
4749
|
+
...INITIATIVE_STATES
|
|
4750
|
+
]).optional().describe("open (default): not done or canceled; all: every one; or one column of the board"),
|
|
4760
4751
|
category: z.enum([
|
|
4761
4752
|
"feature",
|
|
4762
4753
|
"bug",
|
|
@@ -4768,56 +4759,66 @@ function registerBacklogReads(server, client) {
|
|
|
4768
4759
|
}),
|
|
4769
4760
|
annotations: READ_ONLY
|
|
4770
4761
|
}, (params) => runTool(async () => {
|
|
4771
|
-
const { query,
|
|
4772
|
-
if (state !== void 0 && !COLUMN_STATUSES[state].includes(status)) return toolError("VALIDATION_ERROR", `No initiative is in the ${state} column with status ${status}: pass state alone, or a status it can have (${COLUMN_STATUSES[state].join(", ")}).`);
|
|
4762
|
+
const { query, column = "open", productId: id, ...filters } = params;
|
|
4773
4763
|
const apiFilters = {
|
|
4774
4764
|
...filters,
|
|
4775
|
-
|
|
4765
|
+
order: "newest",
|
|
4766
|
+
...column === "open" ? { open: true } : column === "all" ? {} : { state: column }
|
|
4776
4767
|
};
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
const result = await client.listBacklogItems(params.productId, apiFilters);
|
|
4768
|
+
if (query === void 0) {
|
|
4769
|
+
const result = await client.listBacklogItems(id, apiFilters);
|
|
4780
4770
|
return toolResult(page(situatedRows(result.data), result.nextCursor));
|
|
4781
4771
|
}
|
|
4782
|
-
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "
|
|
4772
|
+
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "A title search answers one page; narrow the title, or leave query out to page through a column.");
|
|
4783
4773
|
let closedSeen = 0;
|
|
4784
|
-
const needle =
|
|
4785
|
-
const found = await searchPages((cursor) => client.listBacklogItems(
|
|
4774
|
+
const needle = query.toLowerCase();
|
|
4775
|
+
const found = await searchPages((cursor) => client.listBacklogItems(id, {
|
|
4786
4776
|
...apiFilters,
|
|
4777
|
+
...column === "open" ? { open: void 0 } : {},
|
|
4787
4778
|
limit: 100,
|
|
4788
4779
|
cursor
|
|
4789
|
-
}), query
|
|
4790
|
-
if (
|
|
4791
|
-
if (
|
|
4792
|
-
if (query !== void 0 && String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
|
|
4780
|
+
}), query, (row) => [row.title], params.limit ?? 25, (row) => {
|
|
4781
|
+
if (column !== "open" || !["done", "canceled"].includes(String(row.state))) return true;
|
|
4782
|
+
if (String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
|
|
4793
4783
|
return false;
|
|
4794
4784
|
});
|
|
4795
|
-
const hidden =
|
|
4785
|
+
const hidden = column === "open" && !found.more ? closedSeen : 0;
|
|
4796
4786
|
return toolResult({
|
|
4797
4787
|
...page(situatedRows(found.rows), null),
|
|
4798
|
-
...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"};
|
|
4799
|
-
...found.more ? { more: "There may be more than the limit carries: narrow
|
|
4788
|
+
...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"}; column: 'all' lists them.` } : {},
|
|
4789
|
+
...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {}
|
|
4800
4790
|
});
|
|
4801
4791
|
}));
|
|
4802
4792
|
server.registerTool("taiga_get_initiative", {
|
|
4803
4793
|
title: "Get initiative",
|
|
4804
|
-
description: `Read one initiative
|
|
4805
|
-
inputSchema: z.object({
|
|
4794
|
+
description: `Read one initiative: where it stands (column, turn, situation, latestRun), its acceptance criteria count and the start of its description. full: true reads it whole before implementing: the whole description, each acceptance criterion and whether it is met, technical context, source documents, and the API's own state fields. A failed plan carries planFailure (cause, meaning, when, by whom). ${LIFECYCLE_MEANING}`,
|
|
4795
|
+
inputSchema: z.object({
|
|
4796
|
+
initiativeId,
|
|
4797
|
+
full: z.boolean().optional().describe("Read it whole: every criterion, technical context and the raw state fields (default false)")
|
|
4798
|
+
}),
|
|
4806
4799
|
annotations: READ_ONLY
|
|
4807
4800
|
}, (params) => runTool(async () => {
|
|
4808
4801
|
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
4809
4802
|
const row = item;
|
|
4810
|
-
|
|
4811
|
-
const
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
...
|
|
4815
|
-
|
|
4816
|
-
|
|
4803
|
+
const planFailure = item.planningStatus === "failed" ? await planFailureOf(client, params.initiativeId, item.lifecycle?.planFailureCause) : null;
|
|
4804
|
+
const failure = planFailure ? { planFailure } : {};
|
|
4805
|
+
if (params.full) return toolResult({
|
|
4806
|
+
...withSituation(row),
|
|
4807
|
+
...failure
|
|
4808
|
+
});
|
|
4809
|
+
const summary = itemRows([item], ITEM_SUMMARY_FIELDS)[0];
|
|
4810
|
+
const description = String(summary["description"] ?? "");
|
|
4811
|
+
return toolResult({
|
|
4812
|
+
...withSituation(row, {
|
|
4813
|
+
...summary,
|
|
4814
|
+
description: description.length > DESCRIPTION_IN_SUMMARY ? `${description.slice(0, DESCRIPTION_IN_SUMMARY).trimEnd()}... (full: true has the rest)` : description
|
|
4815
|
+
}),
|
|
4816
|
+
...failure
|
|
4817
|
+
});
|
|
4817
4818
|
}));
|
|
4818
4819
|
server.registerTool("taiga_get_plan", {
|
|
4819
4820
|
title: "Get the active plan",
|
|
4820
|
-
description: "Read an initiative's active plan: its steps in order, each with title, layer, dependencies (the orderIndex of the steps it builds on), files written and read, and resources created. Enough to approve or reject it (taiga_approve_plan, taiga_reject_plan); full: true adds each step's complete instructions (thousands of tokens). NOT_FOUND when there is no active plan yet;
|
|
4821
|
+
description: "Read an initiative's active plan: its steps in order, each with title, layer, dependencies (the orderIndex of the steps it builds on), files written and read, and resources created. Enough to approve or reject it (taiga_approve_plan, taiga_reject_plan); full: true adds each step's complete instructions (thousands of tokens). NOT_FOUND when there is no active plan yet; taiga_get_initiative's situation says whether one is being generated.",
|
|
4821
4822
|
inputSchema: z.object({
|
|
4822
4823
|
initiativeId,
|
|
4823
4824
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
|
|
@@ -4980,7 +4981,7 @@ function registerReviewGate(server, client) {
|
|
|
4980
4981
|
function registerResume(server, client) {
|
|
4981
4982
|
server.registerTool("taiga_resume_initiative", {
|
|
4982
4983
|
title: "Resume initiative",
|
|
4983
|
-
description: "Carry on an initiative the line stopped on (
|
|
4984
|
+
description: "Carry on an initiative the line stopped on (its situation says the line stopped on it): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first unfinished step (committed steps are kept), a refused run start is cleared and retried. Refused (CONFLICT) when nothing is flagged or the cause has no resume; a plan that failed off the line (its situation names taiga_plan_again, and planFailure says why) needs taiga_plan_again.",
|
|
4984
4985
|
inputSchema: z.object({ initiativeId }),
|
|
4985
4986
|
annotations: OPERATE
|
|
4986
4987
|
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
|
|
@@ -5160,10 +5161,13 @@ function standingSummary(standing) {
|
|
|
5160
5161
|
const severity = String(violation["severity"] ?? "unknown");
|
|
5161
5162
|
bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
|
|
5162
5163
|
}
|
|
5164
|
+
const stated = (state) => controls.filter((c) => c["state"] === state).length;
|
|
5163
5165
|
return {
|
|
5164
5166
|
controls: listed.length,
|
|
5165
5167
|
controlsInPolicies: controls.length,
|
|
5166
|
-
|
|
5168
|
+
controlsMet: stated("met"),
|
|
5169
|
+
controlsNotApplicable: stated("not_applicable"),
|
|
5170
|
+
controlsNotAssessed: stated("no_evidence"),
|
|
5167
5171
|
violations: violations.length,
|
|
5168
5172
|
bySeverity,
|
|
5169
5173
|
resolvedViolations: resolved
|
|
@@ -5231,11 +5235,12 @@ function registerMaintenanceTools(server, client) {
|
|
|
5231
5235
|
}));
|
|
5232
5236
|
server.registerTool("taiga_get_policy_standing", {
|
|
5233
5237
|
title: "Get a product's policy standing",
|
|
5234
|
-
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep).
|
|
5238
|
+
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep). summary counts the other controls: met or not applicable as the documents show it, or not assessed (no evidence either way, which is no pass). controlId reads one control: its state, the passage behind a verdict, its document violations and how many scanner findings breach it. full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
|
|
5235
5239
|
inputSchema: z.object({
|
|
5236
5240
|
productId,
|
|
5237
5241
|
full: z.boolean().optional().describe("Carry each violation's evidence and policy excerpt, cut to about 1,500 characters each, and up to 20 resolved violations (default false)"),
|
|
5238
|
-
violationId: idInput("violation", "this tool lists them").optional().describe("Only this violation, in full and uncut: its evidence, policy excerpt and work")
|
|
5242
|
+
violationId: idInput("violation", "this tool lists them").optional().describe("Only this violation, in full and uncut: its evidence, policy excerpt and work"),
|
|
5243
|
+
controlId: z.string().trim().min(1).max(40).optional().describe("Only this control, such as SEC-5.1: whether it is met, not applicable, violated or not assessed, with the passage")
|
|
5239
5244
|
}),
|
|
5240
5245
|
annotations: READ_ONLY
|
|
5241
5246
|
}, (params) => runTool(async () => {
|
|
@@ -5247,6 +5252,20 @@ function registerMaintenanceTools(server, client) {
|
|
|
5247
5252
|
const note = standing.assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first)." : null;
|
|
5248
5253
|
const resolvedAny = Array.isArray(standing.resolvedDivergences) && standing.resolvedDivergences.length > 0;
|
|
5249
5254
|
const wording = violations.length > 0 || resolvedAny ? { wordingNote: "A violation's evidence is from its assessedAt; its title and summary may be from when it was first recorded. Where they disagree, the evidence is current." } : {};
|
|
5255
|
+
if (params.controlId && params.violationId) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both.");
|
|
5256
|
+
if (params.controlId) {
|
|
5257
|
+
const wanted = params.controlId.toUpperCase();
|
|
5258
|
+
const control = controls.find((c) => String(c.controlId).toUpperCase() === wanted);
|
|
5259
|
+
if (!control) return toolError("NOT_FOUND", "No control with that id in this product's policies; taiga_get_document (type, sections: []) lists a policy's controls.");
|
|
5260
|
+
const own = violations.filter((violation) => String(violation["controlId"]).toUpperCase() === wanted);
|
|
5261
|
+
return toolResult({
|
|
5262
|
+
...control,
|
|
5263
|
+
...control.state === "no_evidence" ? { meaning: "Not assessed: no finding or violation is recorded against it, and the documents were not found to show it met. That is no evidence either way, not a pass." } : {},
|
|
5264
|
+
violations: pickFields(own, VIOLATION_FIELDS),
|
|
5265
|
+
...Number(control.findingCount ?? 0) > 0 ? { findingsNote: `${String(control.findingCount)} open scanner finding${control.findingCount === 1 ? "" : "s"} breach it; taiga_list_findings (productId) lists them with the controls each breaches.` } : {},
|
|
5266
|
+
...own.length > 0 ? wording : {}
|
|
5267
|
+
});
|
|
5268
|
+
}
|
|
5250
5269
|
if (params.violationId) {
|
|
5251
5270
|
const one = [...violations, ...Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : []].find((violation) => violation["id"] === params.violationId);
|
|
5252
5271
|
if (!one) return toolError("NOT_FOUND", "No violation with that ID on this product; this tool without violationId lists them.");
|
|
@@ -5405,6 +5424,28 @@ function readWith(row, backlogRead) {
|
|
|
5405
5424
|
default: return;
|
|
5406
5425
|
}
|
|
5407
5426
|
}
|
|
5427
|
+
/**
|
|
5428
|
+
* The glossary's words for what the API names after its tables (PRO-2823): a reviewer met kind review_backlog,
|
|
5429
|
+
* subjectType backlog_item and inbox_task beside a glossary that says initiative and run, and had to infer they
|
|
5430
|
+
* were the same things.
|
|
5431
|
+
*/
|
|
5432
|
+
const KIND_SAID = { review_backlog: "review_initiatives" };
|
|
5433
|
+
const SUBJECT_SAID = {
|
|
5434
|
+
backlog_item: "initiative",
|
|
5435
|
+
build_run: "run",
|
|
5436
|
+
inbox_task: "setup_task"
|
|
5437
|
+
};
|
|
5438
|
+
const kindSaid = (kind) => KIND_SAID[String(kind)] ?? kind;
|
|
5439
|
+
/** A setup task row in the glossary's words: the Action Required item it is, by the name taiga_resolve_action takes. */
|
|
5440
|
+
function setupTaskRow(row) {
|
|
5441
|
+
const { inboxItemId, inboxItemStatus, alsoFiledAs, ...rest } = row;
|
|
5442
|
+
return {
|
|
5443
|
+
...rest,
|
|
5444
|
+
...inboxItemId !== void 0 ? { actionId: inboxItemId } : {},
|
|
5445
|
+
...inboxItemStatus !== void 0 ? { actionStatus: inboxItemStatus } : {},
|
|
5446
|
+
...Array.isArray(alsoFiledAs) ? { alsoFiledAs: alsoFiledAs.map(setupTaskRow) } : {}
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5408
5449
|
/** A summary past its first sentence: review_doc rows quoted two documents, 800 to 1,200 characters each. */
|
|
5409
5450
|
const SUMMARY_LIMIT = 200;
|
|
5410
5451
|
function firstSentence(text) {
|
|
@@ -5422,9 +5463,12 @@ function itemRow(row, now, full, backlogRead) {
|
|
|
5422
5463
|
const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
|
|
5423
5464
|
const { summary, status, blocking, ...lean } = rest;
|
|
5424
5465
|
const read = readWith(row, backlogRead);
|
|
5466
|
+
const saysNothing = row["kind"] === "setup_manual" && String(summary).trim().toLowerCase() === "manual config";
|
|
5425
5467
|
return {
|
|
5426
5468
|
...lean,
|
|
5427
|
-
|
|
5469
|
+
kind: kindSaid(lean["kind"]),
|
|
5470
|
+
...typeof lean["subjectType"] === "string" ? { subjectType: SUBJECT_SAID[lean["subjectType"]] ?? lean["subjectType"] } : {},
|
|
5471
|
+
...saysNothing ? {} : full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
|
|
5428
5472
|
...full || status !== "open" ? { status } : {},
|
|
5429
5473
|
...full || blocking === true ? { blocking } : {},
|
|
5430
5474
|
...read ? { readWith: read } : {},
|
|
@@ -5456,7 +5500,8 @@ function summaryOf(rows, stale) {
|
|
|
5456
5500
|
if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
|
|
5457
5501
|
else {
|
|
5458
5502
|
entry.open += 1;
|
|
5459
|
-
|
|
5503
|
+
const kind = String(kindSaid(row["kind"]));
|
|
5504
|
+
entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
|
|
5460
5505
|
}
|
|
5461
5506
|
byProduct.set(key, entry);
|
|
5462
5507
|
}
|
|
@@ -5512,15 +5557,10 @@ async function staleBacklogReviews(client, rows, out) {
|
|
|
5512
5557
|
const empty = /* @__PURE__ */ new Set();
|
|
5513
5558
|
await Promise.all(checked.map(async (productId) => {
|
|
5514
5559
|
try {
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
limit:
|
|
5518
|
-
})));
|
|
5519
|
-
if (pages.some((page) => page.data.some((item) => item.state === "backlog" || item.state === "todo"))) return;
|
|
5520
|
-
if (pages.some((page) => page.nextCursor)) {
|
|
5521
|
-
unchecked += asks.filter((row) => row["productId"] === productId).length;
|
|
5522
|
-
return;
|
|
5523
|
-
}
|
|
5560
|
+
if ((await Promise.all(["backlog", "todo"].map((state) => client.listBacklogItems(productId, {
|
|
5561
|
+
state,
|
|
5562
|
+
limit: 1
|
|
5563
|
+
})))).some((page) => page.data.length > 0)) return;
|
|
5524
5564
|
empty.add(productId);
|
|
5525
5565
|
} catch (error) {
|
|
5526
5566
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
@@ -5603,6 +5643,7 @@ function sameKeyOnce(rows) {
|
|
|
5603
5643
|
id: from["id"],
|
|
5604
5644
|
category: from["category"],
|
|
5605
5645
|
title: from["title"],
|
|
5646
|
+
inboxItemId: from["inboxItemId"],
|
|
5606
5647
|
inboxItemStatus: from["inboxItemStatus"]
|
|
5607
5648
|
});
|
|
5608
5649
|
if (!isOpen(kept) && isOpen(row)) {
|
|
@@ -5702,7 +5743,7 @@ function registerInboxTools(server, client) {
|
|
|
5702
5743
|
...params.cursor === void 0 ? { withTotal: true } : {}
|
|
5703
5744
|
});
|
|
5704
5745
|
return toolResult({
|
|
5705
|
-
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor, result.data.length),
|
|
5746
|
+
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)).map(setupTaskRow), result.nextCursor, result.data.length),
|
|
5706
5747
|
...totalOf(result)
|
|
5707
5748
|
});
|
|
5708
5749
|
}));
|
|
@@ -5955,42 +5996,31 @@ async function policiesPart(client) {
|
|
|
5955
5996
|
sections: Object.keys(sectionsOf(row["data"] ?? {}) ?? {}).length
|
|
5956
5997
|
}));
|
|
5957
5998
|
}
|
|
5958
|
-
/** The statuses an item is open in; the API's filter takes one at a time. */
|
|
5959
|
-
const OPEN_BACKLOG_STATUSES = ["backlog", "in_progress"];
|
|
5960
5999
|
/**
|
|
5961
|
-
* The items still open: one
|
|
5962
|
-
*
|
|
5963
|
-
* reports. The list is oldest first and the open items are the newest, so a
|
|
5964
|
-
* single page would miss exactly the ones that matter. Rows carrying a
|
|
5965
|
-
* retired status (readable for a rolling deploy) match neither filter;
|
|
5966
|
-
* the settled check below keeps the shape honest for whatever comes back.
|
|
6000
|
+
* The items still open, newest first (PRO-2843): one read of the open ones,
|
|
6001
|
+
* paged up to a cap the result reports. The API leaves archived items out.
|
|
5967
6002
|
*/
|
|
5968
6003
|
async function backlogPart(client, productId) {
|
|
5969
|
-
const
|
|
6004
|
+
const rows = [];
|
|
5970
6005
|
let truncated = false;
|
|
5971
6006
|
let pageCapped = false;
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
break;
|
|
5979
|
-
}
|
|
5980
|
-
const result = await client.listBacklogItems(productId, {
|
|
5981
|
-
status,
|
|
5982
|
-
limit: 100,
|
|
5983
|
-
cursor
|
|
5984
|
-
});
|
|
5985
|
-
for (const row of result.data) if (!isSettledOrArchived({
|
|
5986
|
-
status: String(row.status),
|
|
5987
|
-
archivedAt: row.archivedAt ?? null
|
|
5988
|
-
})) open.set(String(row.id), row);
|
|
5989
|
-
if (!result.nextCursor) break;
|
|
5990
|
-
cursor = result.nextCursor;
|
|
6007
|
+
let cursor;
|
|
6008
|
+
for (let pageIndex = 0;; pageIndex += 1) {
|
|
6009
|
+
if (pageIndex === BACKLOG_PAGE_LIMIT) {
|
|
6010
|
+
truncated = true;
|
|
6011
|
+
pageCapped = true;
|
|
6012
|
+
break;
|
|
5991
6013
|
}
|
|
6014
|
+
const result = await client.listBacklogItems(productId, {
|
|
6015
|
+
open: true,
|
|
6016
|
+
order: "newest",
|
|
6017
|
+
limit: 100,
|
|
6018
|
+
cursor
|
|
6019
|
+
});
|
|
6020
|
+
rows.push(...result.data);
|
|
6021
|
+
if (!result.nextCursor) break;
|
|
6022
|
+
cursor = result.nextCursor;
|
|
5992
6023
|
}
|
|
5993
|
-
const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
|
|
5994
6024
|
if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
|
|
5995
6025
|
const byColumn = {
|
|
5996
6026
|
backlog: 0,
|
|
@@ -6002,9 +6032,9 @@ async function backlogPart(client, productId) {
|
|
|
6002
6032
|
for (const row of rows) if (row.state) byColumn[row.state] = (byColumn[row.state] ?? 0) + 1;
|
|
6003
6033
|
return {
|
|
6004
6034
|
byColumn,
|
|
6005
|
-
items: situatedRows(rows.slice(
|
|
6035
|
+
items: situatedRows(rows.slice(0, BACKLOG_IN_CONTEXT)),
|
|
6006
6036
|
truncated,
|
|
6007
|
-
...pageCapped ? { note: "More than 1000 open items
|
|
6037
|
+
...pageCapped ? { note: "More than 1000 open items: byColumn counts the newest 1000; taiga_list_initiatives pages the rest." } : {}
|
|
6008
6038
|
};
|
|
6009
6039
|
}
|
|
6010
6040
|
/**
|
|
@@ -6029,7 +6059,7 @@ const CONTEXT_RUN_SAID = [
|
|
|
6029
6059
|
"runNumber",
|
|
6030
6060
|
"backlogItemId",
|
|
6031
6061
|
"targetTitle",
|
|
6032
|
-
"
|
|
6062
|
+
"status",
|
|
6033
6063
|
"outcome",
|
|
6034
6064
|
"failedReasonMeans",
|
|
6035
6065
|
"hardeningNote",
|
|
@@ -6124,15 +6154,15 @@ async function productContext(client, product, options) {
|
|
|
6124
6154
|
}), null, 2).length;
|
|
6125
6155
|
}
|
|
6126
6156
|
return toolResult({
|
|
6127
|
-
|
|
6128
|
-
|
|
6157
|
+
characters,
|
|
6158
|
+
...result
|
|
6129
6159
|
});
|
|
6130
6160
|
}
|
|
6131
6161
|
function registerContextTools(server, client) {
|
|
6132
6162
|
if (!client.hasCapability("products:read")) return;
|
|
6133
6163
|
server.registerTool("taiga_get_product_context", {
|
|
6134
6164
|
title: "Get product context",
|
|
6135
|
-
description: `Read where a product stands in one call; the first read for any product question. Name it by ID, slug or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, specification and architecture outlines (section ids, sizes), policies with section counts, open initiatives with a count per board column (byColumn; taiga_list_initiatives
|
|
6165
|
+
description: `Read where a product stands in one call; the first read for any product question. Name it by ID, slug or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, specification and architecture outlines (section ids, sizes), policies with section counts, open initiatives with a count per board column (byColumn; taiga_list_initiatives column: 'all' has the done and canceled), repositories, the latest five runs (taiga_list_runs has more), and counts for Action Required, open findings and policy violations. A part that failed is under unavailable, one the key cannot read under notGranted; neither shows as empty. documents: budget returns whole sections up to about ${DOCUMENT_BUDGET / 1e3}k characters each, full returns all.`,
|
|
6136
6166
|
inputSchema: z.object({
|
|
6137
6167
|
productId: productId.optional(),
|
|
6138
6168
|
slug: slug.optional(),
|
|
@@ -22209,29 +22239,19 @@ function judgeItem(until, gate) {
|
|
|
22209
22239
|
/** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
|
|
22210
22240
|
function itemReport(item) {
|
|
22211
22241
|
const run = latestBuildOf(item);
|
|
22212
|
-
const situation = situationOf(item);
|
|
22213
22242
|
return {
|
|
22214
|
-
...situation ? { situation } : {},
|
|
22215
22243
|
...pickFields([item], [
|
|
22216
22244
|
"id",
|
|
22217
22245
|
"title",
|
|
22218
|
-
"state",
|
|
22219
|
-
"planningStatus",
|
|
22220
|
-
"needsHumanCause",
|
|
22221
|
-
"needsHumanDetail",
|
|
22222
22246
|
"autoBuild"
|
|
22223
22247
|
])[0],
|
|
22224
|
-
|
|
22225
|
-
|
|
22226
|
-
"stage",
|
|
22227
|
-
"cta",
|
|
22228
|
-
"planFailureCause"
|
|
22229
|
-
])[0],
|
|
22230
|
-
latestBuild: run ? pickFields([run], [
|
|
22248
|
+
...standing(item),
|
|
22249
|
+
latestRun: run ? pickFields([run], [
|
|
22231
22250
|
"id",
|
|
22232
22251
|
"status",
|
|
22233
22252
|
"completedSteps",
|
|
22234
22253
|
"totalSteps",
|
|
22254
|
+
"prNumber",
|
|
22235
22255
|
"prUrl",
|
|
22236
22256
|
"prState",
|
|
22237
22257
|
"prMerged",
|
|
@@ -22415,6 +22435,10 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
|
|
|
22415
22435
|
message: `${withStop(issue.message)} ${pointers.join(" ")}`
|
|
22416
22436
|
};
|
|
22417
22437
|
}
|
|
22438
|
+
if (issue.code === "invalid_value") return {
|
|
22439
|
+
...issue,
|
|
22440
|
+
message: `${withStop(issue.message)} Pass one of those values exactly.`
|
|
22441
|
+
};
|
|
22418
22442
|
const argument = issue.path?.length === 1 ? String(issue.path[0]) : void 0;
|
|
22419
22443
|
const source = argument ? idSource(argument) : void 0;
|
|
22420
22444
|
if (source && /received undefined/.test(issue.message)) return {
|
|
@@ -22563,7 +22587,7 @@ function registerAllTools(server, client, options = {}) {
|
|
|
22563
22587
|
function registerWhoami(server, client, registered) {
|
|
22564
22588
|
server.registerTool("taiga_whoami", {
|
|
22565
22589
|
title: "Who am I",
|
|
22566
|
-
description: "Read who this API key acts as: the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and its tools split into reads, writes, and ask-first writes (paid agent work, changes others see). A tool the key cannot use is not offered at all.",
|
|
22590
|
+
description: "Read who this API key acts as: the API it talks to (api, which says the environment), the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and its tools split into reads, writes, and ask-first writes (paid agent work, changes others see). A tool the key cannot use is not offered at all.",
|
|
22567
22591
|
inputSchema: z.object({}),
|
|
22568
22592
|
annotations: READ_ONLY
|
|
22569
22593
|
}, () => runTool(async () => {
|
|
@@ -22571,6 +22595,7 @@ function registerWhoami(server, client, registered) {
|
|
|
22571
22595
|
const tools = [...registered.entries()].filter(([name]) => name !== "taiga_whoami");
|
|
22572
22596
|
const named = (pick) => tools.filter(([, notes]) => pick(notes)).map(([name]) => name);
|
|
22573
22597
|
return toolResult({
|
|
22598
|
+
api: client.apiHost,
|
|
22574
22599
|
email: me?.email ?? null,
|
|
22575
22600
|
role: me?.role ?? null,
|
|
22576
22601
|
scopes: me?.scopes ?? null,
|
|
@@ -22613,8 +22638,8 @@ const SERVER_INSTRUCTIONS = [
|
|
|
22613
22638
|
"conversation that specifies a new product. A factory owns products.",
|
|
22614
22639
|
"",
|
|
22615
22640
|
"**IDs** are UUIDs; the product, factory and initiative lists take query to find one by name. A NOT_FOUND names",
|
|
22616
|
-
"the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones
|
|
22617
|
-
"
|
|
22641
|
+
"the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones, newest",
|
|
22642
|
+
"first. outcome, situation and nextStep are one-sentence summaries; setup writes return readiness.",
|
|
22618
22643
|
"Product documents have two timelines: plan (intent) and reality (from the code).",
|
|
22619
22644
|
"",
|
|
22620
22645
|
"**Writing.** A tool that writes opens with \"Writes.\", \"Ask first (paid agent work).\" or \"Ask first (others",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.26.0-dev.
|
|
3
|
+
"version": "0.26.0-dev.489",
|
|
4
4
|
"description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
|
|
5
5
|
"mcpName": "ga.tai/mcp-server",
|
|
6
6
|
"type": "module",
|