@taiga-ai/mcp-server 0.26.0-dev.486 → 0.26.0-dev.488
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 +262 -182
- 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
|
*
|
|
@@ -1385,6 +1388,49 @@ z.object({
|
|
|
1385
1388
|
...e.failingChecks === void 0 ? {} : { failingChecks: e.failingChecks },
|
|
1386
1389
|
...e.blockers === void 0 ? {} : { blockers: e.blockers }
|
|
1387
1390
|
}));
|
|
1391
|
+
/**
|
|
1392
|
+
* What a run's hardening came to, coverage included (PRO-2829). `passed`
|
|
1393
|
+
* counted only blocking findings, so a run whose scanners never ran said
|
|
1394
|
+
* "passed" all the same.
|
|
1395
|
+
*
|
|
1396
|
+
* - `failed`: blocking findings remain (the gaps are still listed).
|
|
1397
|
+
* - `not_scanned`: no scanner that applies to the repository ran to the end.
|
|
1398
|
+
* - `passed_with_gaps`: nothing blocks, but some scanner did not run.
|
|
1399
|
+
* - `passed`: nothing blocks and every scanner that applies ran.
|
|
1400
|
+
*/
|
|
1401
|
+
const HARDENING_VERDICTS = [
|
|
1402
|
+
"passed",
|
|
1403
|
+
"passed_with_gaps",
|
|
1404
|
+
"not_scanned",
|
|
1405
|
+
"failed"
|
|
1406
|
+
];
|
|
1407
|
+
/**
|
|
1408
|
+
* The verdict and the gaps of a hardening result (PRO-2829), for every reader
|
|
1409
|
+
* (the run read, the web's run page, the MCP), from what the agent reported.
|
|
1410
|
+
*
|
|
1411
|
+
* A gap is a scanner that applies and did not run: `not_run` (the scan never
|
|
1412
|
+
* reached it), `skipped` (its tool was missing), or `failed` with a reason,
|
|
1413
|
+
* which is a scanner that could not finish. A `failed` scanner without one
|
|
1414
|
+
* found something (tflint fails on a MAJOR that blocks nothing, and rows from
|
|
1415
|
+
* before `scannerReasons` used `failed` for "had findings"), so it ran and is
|
|
1416
|
+
* no gap. A scanner listed in `notApplicable` is no gap; on a result from
|
|
1417
|
+
* before that list, a skipped tflint is read the same way, as the only reason
|
|
1418
|
+
* the agent ever skipped it was a repository with no Terraform.
|
|
1419
|
+
*/
|
|
1420
|
+
function hardeningCoverage(result) {
|
|
1421
|
+
const notApplicable = new Set(result.notApplicable ?? []);
|
|
1422
|
+
const applies = Object.entries(result.scanners ?? {}).filter(([scanner, status]) => !notApplicable.has(scanner) && !(result.notApplicable === void 0 && scanner === "tflint" && status === "skipped"));
|
|
1423
|
+
const reasons = result.scannerReasons ?? {};
|
|
1424
|
+
const gaps = applies.filter(([scanner, status]) => status === "not_run" || status === "skipped" || status === "failed" && scanner in reasons).map(([scanner, status]) => ({
|
|
1425
|
+
scanner,
|
|
1426
|
+
status,
|
|
1427
|
+
reason: reasons[scanner] ?? null
|
|
1428
|
+
}));
|
|
1429
|
+
return {
|
|
1430
|
+
verdict: !result.passed ? "failed" : gaps.length === applies.length ? "not_scanned" : gaps.length > 0 ? "passed_with_gaps" : "passed",
|
|
1431
|
+
gaps
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1388
1434
|
[...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
|
|
1389
1435
|
//#endregion
|
|
1390
1436
|
//#region ../../packages/types/dist/agents.js
|
|
@@ -2202,7 +2248,7 @@ const DOCUMENT_INPUTS = Object.freeze({
|
|
|
2202
2248
|
* reads as deleted) and fields they explain would simply be missing. So it
|
|
2203
2249
|
* refuses to start, and says what to do.
|
|
2204
2250
|
*/
|
|
2205
|
-
function apiLevelProblem(introspection, apiUrl, required =
|
|
2251
|
+
function apiLevelProblem(introspection, apiUrl, required = 2, version = SERVER_VERSION) {
|
|
2206
2252
|
const offered = introspection.mcpApiLevel ?? 0;
|
|
2207
2253
|
if (offered >= required) return null;
|
|
2208
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.`;
|
|
@@ -2459,7 +2505,7 @@ const BY_ARGUMENT = {
|
|
|
2459
2505
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2460
2506
|
const BY_TOOL = {
|
|
2461
2507
|
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2462
|
-
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
|
|
2463
2509
|
};
|
|
2464
2510
|
/**
|
|
2465
2511
|
* What the API calls the thing each ID names, as its 404 says it ("Environment
|
|
@@ -3942,6 +3988,34 @@ function hardeningSaid(blocking, named) {
|
|
|
3942
3988
|
const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
|
|
3943
3989
|
return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
|
|
3944
3990
|
}
|
|
3991
|
+
/** How each hardening verdict reads (PRO-2829): "passed" only when every scanner that applies ran. */
|
|
3992
|
+
const VERDICT_SAID = {
|
|
3993
|
+
passed: "Hardening passed: every scanner that applies ran and nothing blocks",
|
|
3994
|
+
passed_with_gaps: "Hardening passed, but not every scanner ran, so its checks are missing from the result",
|
|
3995
|
+
not_scanned: "No security scanner ran in hardening: \"passed\" only means no blocking finding was recorded",
|
|
3996
|
+
failed: "Hardening failed: blocking findings remain"
|
|
3997
|
+
};
|
|
3998
|
+
/**
|
|
3999
|
+
* What a run's hardening came to, coverage included (PRO-2829), from the
|
|
4000
|
+
* verdict the API reads off it, or read here from an API from before that.
|
|
4001
|
+
* Null without a hardening result.
|
|
4002
|
+
*/
|
|
4003
|
+
function scanSaid(hardening) {
|
|
4004
|
+
if (!hardening || typeof hardening !== "object") return null;
|
|
4005
|
+
const h = hardening;
|
|
4006
|
+
const coverage = typeof h.verdict === "string" && HARDENING_VERDICTS.includes(h.verdict) && Array.isArray(h.gaps) ? {
|
|
4007
|
+
verdict: h.verdict,
|
|
4008
|
+
gaps: h.gaps
|
|
4009
|
+
} : typeof h.passed === "boolean" && h.scanners ? hardeningCoverage(h) : null;
|
|
4010
|
+
if (!coverage) return null;
|
|
4011
|
+
const gaps = coverage.gaps.map((gap) => `${gap.scanner}${gap.status === "failed" ? " did not finish" : ""}${gap.reason ? ` (${gap.reason})` : ""}`);
|
|
4012
|
+
const missing = gaps.length > 0 ? `; did not run: ${gaps.join(", ")}` : "";
|
|
4013
|
+
return {
|
|
4014
|
+
verdict: coverage.verdict,
|
|
4015
|
+
gaps: gaps.length,
|
|
4016
|
+
said: `${VERDICT_SAID[coverage.verdict]}${missing}.`
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
3945
4019
|
/**
|
|
3946
4020
|
* What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
|
|
3947
4021
|
* bare enums. As @taiga/types documents them.
|
|
@@ -3962,20 +4036,23 @@ const FAILED_REASON_MEANS = {
|
|
|
3962
4036
|
* text out: its first sentence is in the outcome, and taiga_get_run has it all.
|
|
3963
4037
|
*/
|
|
3964
4038
|
function readableRun(run, opts = {}) {
|
|
3965
|
-
const { prDraft, error, reviewEscalation, runtime: _runtime, ...rest } = run;
|
|
4039
|
+
const { prDraft, error, reviewEscalation, runtime: _runtime, origin, ...rest } = run;
|
|
3966
4040
|
const said = runOutcome(run);
|
|
3967
4041
|
const means = FAILED_REASON_MEANS[String(run["failedReason"])];
|
|
3968
4042
|
const hardening = run["hardeningResult"];
|
|
3969
4043
|
const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
|
|
3970
4044
|
const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
|
|
4045
|
+
const scan = scanSaid(hardening);
|
|
3971
4046
|
return {
|
|
3972
4047
|
...rest,
|
|
4048
|
+
...origin === void 0 ? {} : { origin: origin === "backlog" ? "initiative" : origin },
|
|
3973
4049
|
...means ? { failedReasonMeans: means } : {},
|
|
4050
|
+
...scan && scan.gaps + (scan.verdict === "not_scanned" ? 1 : 0) > 0 ? { scanCoverage: scan.said } : {},
|
|
3974
4051
|
...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
|
|
3975
4052
|
...opts.row || error === void 0 ? {} : { error },
|
|
3976
4053
|
...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
|
|
3977
4054
|
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
3978
|
-
...said
|
|
4055
|
+
...said ? { outcome: said.outcome } : {}
|
|
3979
4056
|
};
|
|
3980
4057
|
}
|
|
3981
4058
|
/** Distinct initiatives a run list reads to say which moved on; past this a waiting row says it was not checked. */
|
|
@@ -4027,7 +4104,8 @@ const INITIATIVE_ENDED = {
|
|
|
4027
4104
|
/** A readable run row that also says when its initiative ended while its pull request waited. */
|
|
4028
4105
|
function withInitiativeState(row, states) {
|
|
4029
4106
|
const state = states.get(String(row["backlogItemId"]));
|
|
4030
|
-
|
|
4107
|
+
const status = String(row["status"]);
|
|
4108
|
+
if (!(BUILD_RUN_STATUSES.includes(status) && ["gated", "unmerged"].includes(landedDisposition(status, row["prMerged"] === true)))) return row;
|
|
4031
4109
|
if (state === "unchecked") return {
|
|
4032
4110
|
...row,
|
|
4033
4111
|
outcome: `${String(row["outcome"])} Whether its initiative still wants it was not checked.`
|
|
@@ -4038,7 +4116,6 @@ function withInitiativeState(row, states) {
|
|
|
4038
4116
|
const open = row["prState"] === "open" ? ` ${pr} is still open on the forge.` : "";
|
|
4039
4117
|
return {
|
|
4040
4118
|
...row,
|
|
4041
|
-
disposition: "abandoned",
|
|
4042
4119
|
actions: [],
|
|
4043
4120
|
outcome: `Its initiative ${ended}, so nobody is waiting on this run.${open}`
|
|
4044
4121
|
};
|
|
@@ -4147,19 +4224,20 @@ function registerBuildTools(server, client) {
|
|
|
4147
4224
|
if (!client.hasCapability("products:read")) return;
|
|
4148
4225
|
server.registerTool("taiga_list_runs", {
|
|
4149
4226
|
title: "List runs",
|
|
4150
|
-
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).",
|
|
4151
4228
|
inputSchema: z.object({
|
|
4152
4229
|
productId: productId.optional(),
|
|
4153
|
-
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"),
|
|
4154
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"),
|
|
4155
4232
|
...pagination
|
|
4156
4233
|
}),
|
|
4157
4234
|
annotations: READ_ONLY
|
|
4158
4235
|
}, (params) => runTool(async () => {
|
|
4159
|
-
if (
|
|
4160
|
-
if (params.productId && params.
|
|
4161
|
-
|
|
4162
|
-
|
|
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, {
|
|
4163
4241
|
backlogItemId: params.initiativeId,
|
|
4164
4242
|
limit: params.limit,
|
|
4165
4243
|
cursor: params.cursor
|
|
@@ -4172,7 +4250,7 @@ function registerBuildTools(server, client) {
|
|
|
4172
4250
|
}));
|
|
4173
4251
|
server.registerTool("taiga_get_run", {
|
|
4174
4252
|
title: "Get run",
|
|
4175
|
-
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.",
|
|
4176
4254
|
inputSchema: z.object({
|
|
4177
4255
|
runId,
|
|
4178
4256
|
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
@@ -4299,9 +4377,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4299
4377
|
if (item) {
|
|
4300
4378
|
Object.assign(row, {
|
|
4301
4379
|
title: item.title,
|
|
4302
|
-
|
|
4303
|
-
planningStatus: item.planningStatus,
|
|
4304
|
-
needsHumanCause: item.needsHumanCause,
|
|
4380
|
+
...standing(item),
|
|
4305
4381
|
criteria: criteriaCounts(item),
|
|
4306
4382
|
lifecycle: lifecycle ? {
|
|
4307
4383
|
turn: lifecycle.turn,
|
|
@@ -4333,7 +4409,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4333
4409
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
4334
4410
|
return {
|
|
4335
4411
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
4336
|
-
entries: rows,
|
|
4412
|
+
entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
|
|
4337
4413
|
waitingOn: line ?? waitingOn(rows),
|
|
4338
4414
|
...note ? { note } : {}
|
|
4339
4415
|
};
|
|
@@ -4379,18 +4455,21 @@ function registerQueueTools(server, client) {
|
|
|
4379
4455
|
}
|
|
4380
4456
|
//#endregion
|
|
4381
4457
|
//#region src/tools/backlogs.ts
|
|
4382
|
-
/**
|
|
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
|
+
*/
|
|
4383
4466
|
const BACKLOG_SUMMARY_FIELDS = [
|
|
4384
4467
|
"id",
|
|
4385
4468
|
"title",
|
|
4386
4469
|
"category",
|
|
4387
|
-
"status",
|
|
4388
|
-
"state",
|
|
4389
|
-
"planningStatus",
|
|
4390
4470
|
"priority",
|
|
4391
4471
|
"estimate",
|
|
4392
4472
|
"origin",
|
|
4393
|
-
"needsHumanCause",
|
|
4394
4473
|
"archivedAt",
|
|
4395
4474
|
"createdAt",
|
|
4396
4475
|
"updatedAt"
|
|
@@ -4404,21 +4483,44 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
4404
4483
|
"id",
|
|
4405
4484
|
"productId",
|
|
4406
4485
|
"title",
|
|
4407
|
-
"status",
|
|
4408
|
-
"state",
|
|
4409
|
-
"planningStatus",
|
|
4410
|
-
"needsHumanCause",
|
|
4411
|
-
"needsHumanDetail",
|
|
4412
4486
|
"autoBuild",
|
|
4413
|
-
"archivedAt"
|
|
4414
|
-
"lifecycle"
|
|
4487
|
+
"archivedAt"
|
|
4415
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"
|
|
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
|
+
}
|
|
4416
4518
|
/**
|
|
4417
4519
|
* What the lifecycle on an initiative says, and the call each next step is.
|
|
4418
4520
|
* needsHumanCause is only the line's flag; a plan that failed outside the line
|
|
4419
4521
|
* leaves it null, and the lifecycle is what says why and what moves it on.
|
|
4420
4522
|
*/
|
|
4421
|
-
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).";
|
|
4422
4524
|
/** The move each call to action is, in the tools' words. */
|
|
4423
4525
|
const CTA_MOVE = {
|
|
4424
4526
|
start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
|
|
@@ -4431,8 +4533,8 @@ const CTA_MOVE = {
|
|
|
4431
4533
|
};
|
|
4432
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). */
|
|
4433
4535
|
const OFF_THE_LINE_MOVE = "taiga_queue_initiative puts it back on the line (paid work: ask first)";
|
|
4434
|
-
/** What happens to a queued initiative
|
|
4435
|
-
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";
|
|
4436
4538
|
/** How a run stands, as the end of "its latest run ...", by disposition. */
|
|
4437
4539
|
const RUN_STANDS = {
|
|
4438
4540
|
in_flight: "is running",
|
|
@@ -4469,7 +4571,10 @@ function situationOf(item) {
|
|
|
4469
4571
|
const column = typeof item["state"] === "string" ? item["state"] : null;
|
|
4470
4572
|
const offTheLine = column !== null && !LINE_STATES.includes(column);
|
|
4471
4573
|
const stalled = life.stage === "building" && life.turn !== "agent";
|
|
4472
|
-
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";
|
|
4473
4578
|
if (stalled) stage = requeued ? "Queued" : offTheLine ? "Off the line" : column === null ? "Its build stopped" : "On the line, but its build stopped";
|
|
4474
4579
|
const build = life.latestBuild;
|
|
4475
4580
|
if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
|
|
@@ -4490,7 +4595,7 @@ function situationOf(item) {
|
|
|
4490
4595
|
}
|
|
4491
4596
|
const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
|
|
4492
4597
|
const handedBack = handbackSaid(build?.reviewEscalation);
|
|
4493
|
-
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;
|
|
4494
4599
|
if (requeued) parts.push(`Nothing to do: ${REQUEUED_MOVE}.`);
|
|
4495
4600
|
else if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
|
|
4496
4601
|
else if (turn && move) parts.push(`${turn}: ${move}.`);
|
|
@@ -4518,15 +4623,14 @@ function endingSaid(item) {
|
|
|
4518
4623
|
if (next && typeof next.id === "string") said.push(`; superseded by "${String(next.title)}" (${next.id})`);
|
|
4519
4624
|
return said.join("");
|
|
4520
4625
|
}
|
|
4521
|
-
/** A row or item with
|
|
4626
|
+
/** A row or item with where it stands added (`standing`). */
|
|
4522
4627
|
function withSituation(item, row = item) {
|
|
4523
|
-
|
|
4524
|
-
return situation ? {
|
|
4628
|
+
return {
|
|
4525
4629
|
...row,
|
|
4526
|
-
|
|
4527
|
-
}
|
|
4630
|
+
...standing(item)
|
|
4631
|
+
};
|
|
4528
4632
|
}
|
|
4529
|
-
/** List rows, each with
|
|
4633
|
+
/** List rows, each with where it stands. */
|
|
4530
4634
|
function situatedRows(items) {
|
|
4531
4635
|
return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index], row));
|
|
4532
4636
|
}
|
|
@@ -4603,36 +4707,6 @@ const outcomeOf = (item) => withSituation(item, {
|
|
|
4603
4707
|
criteria: criteriaCounts(item)
|
|
4604
4708
|
});
|
|
4605
4709
|
const outcome = (item) => toolResult(outcomeOf(item));
|
|
4606
|
-
/** The statuses an initiative in each board column can have: the column decides it (statusForState, PRO-2828). */
|
|
4607
|
-
const COLUMN_STATUSES = {
|
|
4608
|
-
backlog: [
|
|
4609
|
-
"all",
|
|
4610
|
-
"open",
|
|
4611
|
-
"backlog"
|
|
4612
|
-
],
|
|
4613
|
-
todo: [
|
|
4614
|
-
"all",
|
|
4615
|
-
"open",
|
|
4616
|
-
"backlog"
|
|
4617
|
-
],
|
|
4618
|
-
queue: [
|
|
4619
|
-
"all",
|
|
4620
|
-
"open",
|
|
4621
|
-
"backlog"
|
|
4622
|
-
],
|
|
4623
|
-
plan: [
|
|
4624
|
-
"all",
|
|
4625
|
-
"open",
|
|
4626
|
-
"backlog"
|
|
4627
|
-
],
|
|
4628
|
-
build: [
|
|
4629
|
-
"all",
|
|
4630
|
-
"open",
|
|
4631
|
-
"in_progress"
|
|
4632
|
-
],
|
|
4633
|
-
done: ["all", "done"],
|
|
4634
|
-
canceled: ["all", "canceled"]
|
|
4635
|
-
};
|
|
4636
4710
|
/**
|
|
4637
4711
|
* Each tool is registered under the capability its route requires, so a
|
|
4638
4712
|
* key sees exactly what it may call: reads on backlog:read; the board move on
|
|
@@ -4665,25 +4739,15 @@ function registerBacklogTools(server, client) {
|
|
|
4665
4739
|
function registerBacklogReads(server, client) {
|
|
4666
4740
|
server.registerTool("taiga_list_initiatives", {
|
|
4667
4741
|
title: "List initiatives",
|
|
4668
|
-
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.",
|
|
4669
4743
|
inputSchema: z.object({
|
|
4670
4744
|
productId,
|
|
4671
4745
|
query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
|
|
4672
|
-
|
|
4746
|
+
column: z.enum([
|
|
4673
4747
|
"open",
|
|
4674
4748
|
"all",
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
"done",
|
|
4678
|
-
"canceled"
|
|
4679
|
-
]).optional().describe("open (default without state): not done or canceled; all: every one; in_progress is the Build column, backlog every other open one"),
|
|
4680
|
-
planningStatus: z.enum([
|
|
4681
|
-
"unplanned",
|
|
4682
|
-
"planning",
|
|
4683
|
-
"planned",
|
|
4684
|
-
"failed"
|
|
4685
|
-
]).optional().describe("Only items whose plan is in this state"),
|
|
4686
|
-
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"),
|
|
4687
4751
|
category: z.enum([
|
|
4688
4752
|
"feature",
|
|
4689
4753
|
"bug",
|
|
@@ -4695,56 +4759,66 @@ function registerBacklogReads(server, client) {
|
|
|
4695
4759
|
}),
|
|
4696
4760
|
annotations: READ_ONLY
|
|
4697
4761
|
}, (params) => runTool(async () => {
|
|
4698
|
-
const { query,
|
|
4699
|
-
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;
|
|
4700
4763
|
const apiFilters = {
|
|
4701
4764
|
...filters,
|
|
4702
|
-
|
|
4765
|
+
order: "newest",
|
|
4766
|
+
...column === "open" ? { open: true } : column === "all" ? {} : { state: column }
|
|
4703
4767
|
};
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
const result = await client.listBacklogItems(params.productId, apiFilters);
|
|
4768
|
+
if (query === void 0) {
|
|
4769
|
+
const result = await client.listBacklogItems(id, apiFilters);
|
|
4707
4770
|
return toolResult(page(situatedRows(result.data), result.nextCursor));
|
|
4708
4771
|
}
|
|
4709
|
-
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.");
|
|
4710
4773
|
let closedSeen = 0;
|
|
4711
|
-
const needle =
|
|
4712
|
-
const found = await searchPages((cursor) => client.listBacklogItems(
|
|
4774
|
+
const needle = query.toLowerCase();
|
|
4775
|
+
const found = await searchPages((cursor) => client.listBacklogItems(id, {
|
|
4713
4776
|
...apiFilters,
|
|
4777
|
+
...column === "open" ? { open: void 0 } : {},
|
|
4714
4778
|
limit: 100,
|
|
4715
4779
|
cursor
|
|
4716
|
-
}), query
|
|
4717
|
-
if (
|
|
4718
|
-
if (
|
|
4719
|
-
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;
|
|
4720
4783
|
return false;
|
|
4721
4784
|
});
|
|
4722
|
-
const hidden =
|
|
4785
|
+
const hidden = column === "open" && !found.more ? closedSeen : 0;
|
|
4723
4786
|
return toolResult({
|
|
4724
4787
|
...page(situatedRows(found.rows), null),
|
|
4725
|
-
...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"};
|
|
4726
|
-
...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." } : {}
|
|
4727
4790
|
});
|
|
4728
4791
|
}));
|
|
4729
4792
|
server.registerTool("taiga_get_initiative", {
|
|
4730
4793
|
title: "Get initiative",
|
|
4731
|
-
description: `Read one initiative
|
|
4732
|
-
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
|
+
}),
|
|
4733
4799
|
annotations: READ_ONLY
|
|
4734
4800
|
}, (params) => runTool(async () => {
|
|
4735
4801
|
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
4736
4802
|
const row = item;
|
|
4737
|
-
|
|
4738
|
-
const
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
...
|
|
4742
|
-
|
|
4743
|
-
|
|
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
|
+
});
|
|
4744
4818
|
}));
|
|
4745
4819
|
server.registerTool("taiga_get_plan", {
|
|
4746
4820
|
title: "Get the active plan",
|
|
4747
|
-
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.",
|
|
4748
4822
|
inputSchema: z.object({
|
|
4749
4823
|
initiativeId,
|
|
4750
4824
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
|
|
@@ -4907,7 +4981,7 @@ function registerReviewGate(server, client) {
|
|
|
4907
4981
|
function registerResume(server, client) {
|
|
4908
4982
|
server.registerTool("taiga_resume_initiative", {
|
|
4909
4983
|
title: "Resume initiative",
|
|
4910
|
-
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.",
|
|
4911
4985
|
inputSchema: z.object({ initiativeId }),
|
|
4912
4986
|
annotations: OPERATE
|
|
4913
4987
|
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
|
|
@@ -5332,6 +5406,28 @@ function readWith(row, backlogRead) {
|
|
|
5332
5406
|
default: return;
|
|
5333
5407
|
}
|
|
5334
5408
|
}
|
|
5409
|
+
/**
|
|
5410
|
+
* The glossary's words for what the API names after its tables (PRO-2823): a reviewer met kind review_backlog,
|
|
5411
|
+
* subjectType backlog_item and inbox_task beside a glossary that says initiative and run, and had to infer they
|
|
5412
|
+
* were the same things.
|
|
5413
|
+
*/
|
|
5414
|
+
const KIND_SAID = { review_backlog: "review_initiatives" };
|
|
5415
|
+
const SUBJECT_SAID = {
|
|
5416
|
+
backlog_item: "initiative",
|
|
5417
|
+
build_run: "run",
|
|
5418
|
+
inbox_task: "setup_task"
|
|
5419
|
+
};
|
|
5420
|
+
const kindSaid = (kind) => KIND_SAID[String(kind)] ?? kind;
|
|
5421
|
+
/** A setup task row in the glossary's words: the Action Required item it is, by the name taiga_resolve_action takes. */
|
|
5422
|
+
function setupTaskRow(row) {
|
|
5423
|
+
const { inboxItemId, inboxItemStatus, alsoFiledAs, ...rest } = row;
|
|
5424
|
+
return {
|
|
5425
|
+
...rest,
|
|
5426
|
+
...inboxItemId !== void 0 ? { actionId: inboxItemId } : {},
|
|
5427
|
+
...inboxItemStatus !== void 0 ? { actionStatus: inboxItemStatus } : {},
|
|
5428
|
+
...Array.isArray(alsoFiledAs) ? { alsoFiledAs: alsoFiledAs.map(setupTaskRow) } : {}
|
|
5429
|
+
};
|
|
5430
|
+
}
|
|
5335
5431
|
/** A summary past its first sentence: review_doc rows quoted two documents, 800 to 1,200 characters each. */
|
|
5336
5432
|
const SUMMARY_LIMIT = 200;
|
|
5337
5433
|
function firstSentence(text) {
|
|
@@ -5349,9 +5445,12 @@ function itemRow(row, now, full, backlogRead) {
|
|
|
5349
5445
|
const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
|
|
5350
5446
|
const { summary, status, blocking, ...lean } = rest;
|
|
5351
5447
|
const read = readWith(row, backlogRead);
|
|
5448
|
+
const saysNothing = row["kind"] === "setup_manual" && String(summary).trim().toLowerCase() === "manual config";
|
|
5352
5449
|
return {
|
|
5353
5450
|
...lean,
|
|
5354
|
-
|
|
5451
|
+
kind: kindSaid(lean["kind"]),
|
|
5452
|
+
...typeof lean["subjectType"] === "string" ? { subjectType: SUBJECT_SAID[lean["subjectType"]] ?? lean["subjectType"] } : {},
|
|
5453
|
+
...saysNothing ? {} : full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
|
|
5355
5454
|
...full || status !== "open" ? { status } : {},
|
|
5356
5455
|
...full || blocking === true ? { blocking } : {},
|
|
5357
5456
|
...read ? { readWith: read } : {},
|
|
@@ -5383,7 +5482,8 @@ function summaryOf(rows, stale) {
|
|
|
5383
5482
|
if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
|
|
5384
5483
|
else {
|
|
5385
5484
|
entry.open += 1;
|
|
5386
|
-
|
|
5485
|
+
const kind = String(kindSaid(row["kind"]));
|
|
5486
|
+
entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
|
|
5387
5487
|
}
|
|
5388
5488
|
byProduct.set(key, entry);
|
|
5389
5489
|
}
|
|
@@ -5439,15 +5539,10 @@ async function staleBacklogReviews(client, rows, out) {
|
|
|
5439
5539
|
const empty = /* @__PURE__ */ new Set();
|
|
5440
5540
|
await Promise.all(checked.map(async (productId) => {
|
|
5441
5541
|
try {
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
limit:
|
|
5445
|
-
})));
|
|
5446
|
-
if (pages.some((page) => page.data.some((item) => item.state === "backlog" || item.state === "todo"))) return;
|
|
5447
|
-
if (pages.some((page) => page.nextCursor)) {
|
|
5448
|
-
unchecked += asks.filter((row) => row["productId"] === productId).length;
|
|
5449
|
-
return;
|
|
5450
|
-
}
|
|
5542
|
+
if ((await Promise.all(["backlog", "todo"].map((state) => client.listBacklogItems(productId, {
|
|
5543
|
+
state,
|
|
5544
|
+
limit: 1
|
|
5545
|
+
})))).some((page) => page.data.length > 0)) return;
|
|
5451
5546
|
empty.add(productId);
|
|
5452
5547
|
} catch (error) {
|
|
5453
5548
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
@@ -5530,6 +5625,7 @@ function sameKeyOnce(rows) {
|
|
|
5530
5625
|
id: from["id"],
|
|
5531
5626
|
category: from["category"],
|
|
5532
5627
|
title: from["title"],
|
|
5628
|
+
inboxItemId: from["inboxItemId"],
|
|
5533
5629
|
inboxItemStatus: from["inboxItemStatus"]
|
|
5534
5630
|
});
|
|
5535
5631
|
if (!isOpen(kept) && isOpen(row)) {
|
|
@@ -5629,7 +5725,7 @@ function registerInboxTools(server, client) {
|
|
|
5629
5725
|
...params.cursor === void 0 ? { withTotal: true } : {}
|
|
5630
5726
|
});
|
|
5631
5727
|
return toolResult({
|
|
5632
|
-
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor, result.data.length),
|
|
5728
|
+
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)).map(setupTaskRow), result.nextCursor, result.data.length),
|
|
5633
5729
|
...totalOf(result)
|
|
5634
5730
|
});
|
|
5635
5731
|
}));
|
|
@@ -5882,42 +5978,31 @@ async function policiesPart(client) {
|
|
|
5882
5978
|
sections: Object.keys(sectionsOf(row["data"] ?? {}) ?? {}).length
|
|
5883
5979
|
}));
|
|
5884
5980
|
}
|
|
5885
|
-
/** The statuses an item is open in; the API's filter takes one at a time. */
|
|
5886
|
-
const OPEN_BACKLOG_STATUSES = ["backlog", "in_progress"];
|
|
5887
5981
|
/**
|
|
5888
|
-
* The items still open: one
|
|
5889
|
-
*
|
|
5890
|
-
* reports. The list is oldest first and the open items are the newest, so a
|
|
5891
|
-
* single page would miss exactly the ones that matter. Rows carrying a
|
|
5892
|
-
* retired status (readable for a rolling deploy) match neither filter;
|
|
5893
|
-
* the settled check below keeps the shape honest for whatever comes back.
|
|
5982
|
+
* The items still open, newest first (PRO-2843): one read of the open ones,
|
|
5983
|
+
* paged up to a cap the result reports. The API leaves archived items out.
|
|
5894
5984
|
*/
|
|
5895
5985
|
async function backlogPart(client, productId) {
|
|
5896
|
-
const
|
|
5986
|
+
const rows = [];
|
|
5897
5987
|
let truncated = false;
|
|
5898
5988
|
let pageCapped = false;
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
break;
|
|
5906
|
-
}
|
|
5907
|
-
const result = await client.listBacklogItems(productId, {
|
|
5908
|
-
status,
|
|
5909
|
-
limit: 100,
|
|
5910
|
-
cursor
|
|
5911
|
-
});
|
|
5912
|
-
for (const row of result.data) if (!isSettledOrArchived({
|
|
5913
|
-
status: String(row.status),
|
|
5914
|
-
archivedAt: row.archivedAt ?? null
|
|
5915
|
-
})) open.set(String(row.id), row);
|
|
5916
|
-
if (!result.nextCursor) break;
|
|
5917
|
-
cursor = result.nextCursor;
|
|
5989
|
+
let cursor;
|
|
5990
|
+
for (let pageIndex = 0;; pageIndex += 1) {
|
|
5991
|
+
if (pageIndex === BACKLOG_PAGE_LIMIT) {
|
|
5992
|
+
truncated = true;
|
|
5993
|
+
pageCapped = true;
|
|
5994
|
+
break;
|
|
5918
5995
|
}
|
|
5996
|
+
const result = await client.listBacklogItems(productId, {
|
|
5997
|
+
open: true,
|
|
5998
|
+
order: "newest",
|
|
5999
|
+
limit: 100,
|
|
6000
|
+
cursor
|
|
6001
|
+
});
|
|
6002
|
+
rows.push(...result.data);
|
|
6003
|
+
if (!result.nextCursor) break;
|
|
6004
|
+
cursor = result.nextCursor;
|
|
5919
6005
|
}
|
|
5920
|
-
const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
|
|
5921
6006
|
if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
|
|
5922
6007
|
const byColumn = {
|
|
5923
6008
|
backlog: 0,
|
|
@@ -5929,9 +6014,9 @@ async function backlogPart(client, productId) {
|
|
|
5929
6014
|
for (const row of rows) if (row.state) byColumn[row.state] = (byColumn[row.state] ?? 0) + 1;
|
|
5930
6015
|
return {
|
|
5931
6016
|
byColumn,
|
|
5932
|
-
items: situatedRows(rows.slice(
|
|
6017
|
+
items: situatedRows(rows.slice(0, BACKLOG_IN_CONTEXT)),
|
|
5933
6018
|
truncated,
|
|
5934
|
-
...pageCapped ? { note: "More than 1000 open items
|
|
6019
|
+
...pageCapped ? { note: "More than 1000 open items: byColumn counts the newest 1000; taiga_list_initiatives pages the rest." } : {}
|
|
5935
6020
|
};
|
|
5936
6021
|
}
|
|
5937
6022
|
/**
|
|
@@ -5956,7 +6041,7 @@ const CONTEXT_RUN_SAID = [
|
|
|
5956
6041
|
"runNumber",
|
|
5957
6042
|
"backlogItemId",
|
|
5958
6043
|
"targetTitle",
|
|
5959
|
-
"
|
|
6044
|
+
"status",
|
|
5960
6045
|
"outcome",
|
|
5961
6046
|
"failedReasonMeans",
|
|
5962
6047
|
"hardeningNote",
|
|
@@ -6051,15 +6136,15 @@ async function productContext(client, product, options) {
|
|
|
6051
6136
|
}), null, 2).length;
|
|
6052
6137
|
}
|
|
6053
6138
|
return toolResult({
|
|
6054
|
-
|
|
6055
|
-
|
|
6139
|
+
characters,
|
|
6140
|
+
...result
|
|
6056
6141
|
});
|
|
6057
6142
|
}
|
|
6058
6143
|
function registerContextTools(server, client) {
|
|
6059
6144
|
if (!client.hasCapability("products:read")) return;
|
|
6060
6145
|
server.registerTool("taiga_get_product_context", {
|
|
6061
6146
|
title: "Get product context",
|
|
6062
|
-
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
|
|
6147
|
+
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.`,
|
|
6063
6148
|
inputSchema: z.object({
|
|
6064
6149
|
productId: productId.optional(),
|
|
6065
6150
|
slug: slug.optional(),
|
|
@@ -22136,29 +22221,19 @@ function judgeItem(until, gate) {
|
|
|
22136
22221
|
/** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
|
|
22137
22222
|
function itemReport(item) {
|
|
22138
22223
|
const run = latestBuildOf(item);
|
|
22139
|
-
const situation = situationOf(item);
|
|
22140
22224
|
return {
|
|
22141
|
-
...situation ? { situation } : {},
|
|
22142
22225
|
...pickFields([item], [
|
|
22143
22226
|
"id",
|
|
22144
22227
|
"title",
|
|
22145
|
-
"state",
|
|
22146
|
-
"planningStatus",
|
|
22147
|
-
"needsHumanCause",
|
|
22148
|
-
"needsHumanDetail",
|
|
22149
22228
|
"autoBuild"
|
|
22150
22229
|
])[0],
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
"stage",
|
|
22154
|
-
"cta",
|
|
22155
|
-
"planFailureCause"
|
|
22156
|
-
])[0],
|
|
22157
|
-
latestBuild: run ? pickFields([run], [
|
|
22230
|
+
...standing(item),
|
|
22231
|
+
latestRun: run ? pickFields([run], [
|
|
22158
22232
|
"id",
|
|
22159
22233
|
"status",
|
|
22160
22234
|
"completedSteps",
|
|
22161
22235
|
"totalSteps",
|
|
22236
|
+
"prNumber",
|
|
22162
22237
|
"prUrl",
|
|
22163
22238
|
"prState",
|
|
22164
22239
|
"prMerged",
|
|
@@ -22342,6 +22417,10 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
|
|
|
22342
22417
|
message: `${withStop(issue.message)} ${pointers.join(" ")}`
|
|
22343
22418
|
};
|
|
22344
22419
|
}
|
|
22420
|
+
if (issue.code === "invalid_value") return {
|
|
22421
|
+
...issue,
|
|
22422
|
+
message: `${withStop(issue.message)} Pass one of those values exactly.`
|
|
22423
|
+
};
|
|
22345
22424
|
const argument = issue.path?.length === 1 ? String(issue.path[0]) : void 0;
|
|
22346
22425
|
const source = argument ? idSource(argument) : void 0;
|
|
22347
22426
|
if (source && /received undefined/.test(issue.message)) return {
|
|
@@ -22490,7 +22569,7 @@ function registerAllTools(server, client, options = {}) {
|
|
|
22490
22569
|
function registerWhoami(server, client, registered) {
|
|
22491
22570
|
server.registerTool("taiga_whoami", {
|
|
22492
22571
|
title: "Who am I",
|
|
22493
|
-
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.",
|
|
22572
|
+
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.",
|
|
22494
22573
|
inputSchema: z.object({}),
|
|
22495
22574
|
annotations: READ_ONLY
|
|
22496
22575
|
}, () => runTool(async () => {
|
|
@@ -22498,6 +22577,7 @@ function registerWhoami(server, client, registered) {
|
|
|
22498
22577
|
const tools = [...registered.entries()].filter(([name]) => name !== "taiga_whoami");
|
|
22499
22578
|
const named = (pick) => tools.filter(([, notes]) => pick(notes)).map(([name]) => name);
|
|
22500
22579
|
return toolResult({
|
|
22580
|
+
api: client.apiHost,
|
|
22501
22581
|
email: me?.email ?? null,
|
|
22502
22582
|
role: me?.role ?? null,
|
|
22503
22583
|
scopes: me?.scopes ?? null,
|
|
@@ -22540,8 +22620,8 @@ const SERVER_INSTRUCTIONS = [
|
|
|
22540
22620
|
"conversation that specifies a new product. A factory owns products.",
|
|
22541
22621
|
"",
|
|
22542
22622
|
"**IDs** are UUIDs; the product, factory and initiative lists take query to find one by name. A NOT_FOUND names",
|
|
22543
|
-
"the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones
|
|
22544
|
-
"
|
|
22623
|
+
"the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones, newest",
|
|
22624
|
+
"first. outcome, situation and nextStep are one-sentence summaries; setup writes return readiness.",
|
|
22545
22625
|
"Product documents have two timelines: plan (intent) and reality (from the code).",
|
|
22546
22626
|
"",
|
|
22547
22627
|
"**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.488",
|
|
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",
|