@taiga-ai/mcp-server 0.26.0-dev.487 → 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.
Files changed (2) hide show
  1. package/dist/index.js +189 -182
  2. 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 = 1, version = SERVER_VERSION) {
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 generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one." : ITEM_ID_HINT
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
- if (!["gated", "unmerged"].includes(String(row["disposition"]))) return row;
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 (one sentence), actions and disposition: in_flight, gated (a person owes a review and merge), succeeded (merged), failed, stopped, rejected (pull request closed unmerged), unmerged, or abandoned (its initiative ended: done, canceled, archived or deleted). 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).",
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; needs productId"),
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 (!params.productId && params.initiativeId) return toolError("VALIDATION_ERROR", "initiativeId narrows the runs of one product: pass its productId as well (the initiative carries it).");
4233
- if (params.productId && params.active) return toolError("VALIDATION_ERROR", "active narrows the org-wide list only: omit productId, or read the product's runs and judge each by its status.");
4234
- if (params.productId) {
4235
- const result = await client.listBuildRuns(params.productId, {
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: outcome, disposition, 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.",
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
- state: item.state,
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
- /** What a list row needs to say for the model to pick an item to read in full. */
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
- "lifecycle"
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 what moves it on. lifecycle.cta names the next call: start_plan taiga_queue_initiative, retry_plan taiga_plan_again (planFailureCause says why), start_build taiga_approve_plan, resume_build taiga_resume_initiative, retry_build a fresh run (web only; off the line, taiga_queue_initiative instead, as situation says), review_pr and view_pr the pull request. needsHumanCause is set only when the line stopped on it.";
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 whose last run did not land: the line plans it again (PRO-2840). */
4508
- const REQUEUED_MOVE = "the line plans it again when its turn comes";
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 = stalled && column === "queue";
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 its situation added when it has a lifecycle. */
4626
+ /** A row or item with where it stands added (`standing`). */
4595
4627
  function withSituation(item, row = item) {
4596
- const situation = situationOf(item);
4597
- return situation ? {
4628
+ return {
4598
4629
  ...row,
4599
- situation
4600
- } : row;
4630
+ ...standing(item)
4631
+ };
4601
4632
  }
4602
- /** List rows, each with its situation. */
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 initiatives (not done or canceled; status: 'all' for every one): title, category, status, place on the line (backlog, todo, queue, plan, build, done), situation (where it stands and what moves it on) and criteria (acceptance criteria met, confirmed with evidence or checked off by a person; null before planning, so a done initiative can show fewer met than it has). query finds one by title, state one board column; taiga_get_initiative reads it in full. Archived ones are left out unless asked for.",
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
- status: z.enum([
4746
+ column: z.enum([
4746
4747
  "open",
4747
4748
  "all",
4748
- "backlog",
4749
- "in_progress",
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, state, status = state === void 0 ? "open" : "all", ...filters } = params;
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
- status: status === "open" || status === "all" ? void 0 : status
4765
+ order: "newest",
4766
+ ...column === "open" ? { open: true } : column === "all" ? {} : { state: column }
4776
4767
  };
4777
- const open = status === "open";
4778
- if (query === void 0 && state === void 0 && !open) {
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", "Open items, a board column or a title search answer one page; narrow them, or pass status: 'all' (or one status) to page through every item.");
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 = (query ?? "").toLowerCase();
4785
- const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
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 ?? "", (row) => [row.title], params.limit ?? 25, (row) => {
4790
- if (state !== void 0 && row.state !== state) return false;
4791
- if (!open || !["done", "canceled"].includes(row.status)) return true;
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 = query !== void 0 && state === void 0 && !found.more ? closedSeen : 0;
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"}; status: 'all' lists them.` } : {},
4799
- ...found.more ? { more: "There may be more than the limit carries: narrow it with state or query, or list one status (backlog, in_progress, done or canceled) without them, which pages." } : {}
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 in full: description, acceptance criteria (which are met), technical context, dependencies and source documents. Read it before implementing. A failed plan carries planFailure (cause, meaning, when, by whom). ${LIFECYCLE_MEANING}`,
4805
- inputSchema: z.object({ initiativeId }),
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
- if (item.planningStatus !== "failed") return toolResult(withSituation(row));
4811
- const ownCause = item.lifecycle?.planFailureCause;
4812
- const planFailure = await planFailureOf(client, params.initiativeId, ownCause);
4813
- return toolResult(withSituation(row, planFailure ? {
4814
- ...item,
4815
- planFailure
4816
- } : item));
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; planningStatus says whether one is being generated.",
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 (needsHumanCause is set): 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 failed plan with needsHumanCause null (lifecycle.cta retry_plan, planFailureCause says why) needs taiga_plan_again.",
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)));
@@ -5405,6 +5406,28 @@ function readWith(row, backlogRead) {
5405
5406
  default: return;
5406
5407
  }
5407
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
+ }
5408
5431
  /** A summary past its first sentence: review_doc rows quoted two documents, 800 to 1,200 characters each. */
5409
5432
  const SUMMARY_LIMIT = 200;
5410
5433
  function firstSentence(text) {
@@ -5422,9 +5445,12 @@ function itemRow(row, now, full, backlogRead) {
5422
5445
  const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
5423
5446
  const { summary, status, blocking, ...lean } = rest;
5424
5447
  const read = readWith(row, backlogRead);
5448
+ const saysNothing = row["kind"] === "setup_manual" && String(summary).trim().toLowerCase() === "manual config";
5425
5449
  return {
5426
5450
  ...lean,
5427
- ...full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
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) },
5428
5454
  ...full || status !== "open" ? { status } : {},
5429
5455
  ...full || blocking === true ? { blocking } : {},
5430
5456
  ...read ? { readWith: read } : {},
@@ -5456,7 +5482,8 @@ function summaryOf(rows, stale) {
5456
5482
  if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
5457
5483
  else {
5458
5484
  entry.open += 1;
5459
- entry.byKind[String(row["kind"])] = (entry.byKind[String(row["kind"])] ?? 0) + 1;
5485
+ const kind = String(kindSaid(row["kind"]));
5486
+ entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
5460
5487
  }
5461
5488
  byProduct.set(key, entry);
5462
5489
  }
@@ -5512,15 +5539,10 @@ async function staleBacklogReviews(client, rows, out) {
5512
5539
  const empty = /* @__PURE__ */ new Set();
5513
5540
  await Promise.all(checked.map(async (productId) => {
5514
5541
  try {
5515
- const pages = await Promise.all(["backlog", "in_progress"].map((status) => client.listBacklogItems(productId, {
5516
- status,
5517
- limit: 100
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
- }
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;
5524
5546
  empty.add(productId);
5525
5547
  } catch (error) {
5526
5548
  if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
@@ -5603,6 +5625,7 @@ function sameKeyOnce(rows) {
5603
5625
  id: from["id"],
5604
5626
  category: from["category"],
5605
5627
  title: from["title"],
5628
+ inboxItemId: from["inboxItemId"],
5606
5629
  inboxItemStatus: from["inboxItemStatus"]
5607
5630
  });
5608
5631
  if (!isOpen(kept) && isOpen(row)) {
@@ -5702,7 +5725,7 @@ function registerInboxTools(server, client) {
5702
5725
  ...params.cursor === void 0 ? { withTotal: true } : {}
5703
5726
  });
5704
5727
  return toolResult({
5705
- ...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),
5706
5729
  ...totalOf(result)
5707
5730
  });
5708
5731
  }));
@@ -5955,42 +5978,31 @@ async function policiesPart(client) {
5955
5978
  sections: Object.keys(sectionsOf(row["data"] ?? {}) ?? {}).length
5956
5979
  }));
5957
5980
  }
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
5981
  /**
5961
- * The items still open: one status-filtered read per open status (the API
5962
- * leaves archived items out), each paged in full up to a cap the result
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.
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.
5967
5984
  */
5968
5985
  async function backlogPart(client, productId) {
5969
- const open = /* @__PURE__ */ new Map();
5986
+ const rows = [];
5970
5987
  let truncated = false;
5971
5988
  let pageCapped = false;
5972
- for (const status of OPEN_BACKLOG_STATUSES) {
5973
- let cursor;
5974
- for (let pageIndex = 0;; pageIndex += 1) {
5975
- if (pageIndex === BACKLOG_PAGE_LIMIT) {
5976
- truncated = true;
5977
- pageCapped = true;
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;
5989
+ let cursor;
5990
+ for (let pageIndex = 0;; pageIndex += 1) {
5991
+ if (pageIndex === BACKLOG_PAGE_LIMIT) {
5992
+ truncated = true;
5993
+ pageCapped = true;
5994
+ break;
5991
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;
5992
6005
  }
5993
- const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
5994
6006
  if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
5995
6007
  const byColumn = {
5996
6008
  backlog: 0,
@@ -6002,9 +6014,9 @@ async function backlogPart(client, productId) {
6002
6014
  for (const row of rows) if (row.state) byColumn[row.state] = (byColumn[row.state] ?? 0) + 1;
6003
6015
  return {
6004
6016
  byColumn,
6005
- items: situatedRows(rows.slice(-50)),
6017
+ items: situatedRows(rows.slice(0, BACKLOG_IN_CONTEXT)),
6006
6018
  truncated,
6007
- ...pageCapped ? { note: "More than 1000 open items in one status: only the oldest of them were read, so the newest may be missing here and byColumn counts only those read; taiga_list_initiatives pages the rest." } : {}
6019
+ ...pageCapped ? { note: "More than 1000 open items: byColumn counts the newest 1000; taiga_list_initiatives pages the rest." } : {}
6008
6020
  };
6009
6021
  }
6010
6022
  /**
@@ -6029,7 +6041,7 @@ const CONTEXT_RUN_SAID = [
6029
6041
  "runNumber",
6030
6042
  "backlogItemId",
6031
6043
  "targetTitle",
6032
- "disposition",
6044
+ "status",
6033
6045
  "outcome",
6034
6046
  "failedReasonMeans",
6035
6047
  "hardeningNote",
@@ -6124,15 +6136,15 @@ async function productContext(client, product, options) {
6124
6136
  }), null, 2).length;
6125
6137
  }
6126
6138
  return toolResult({
6127
- ...result,
6128
- characters
6139
+ characters,
6140
+ ...result
6129
6141
  });
6130
6142
  }
6131
6143
  function registerContextTools(server, client) {
6132
6144
  if (!client.hasCapability("products:read")) return;
6133
6145
  server.registerTool("taiga_get_product_context", {
6134
6146
  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 status: '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.`,
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.`,
6136
6148
  inputSchema: z.object({
6137
6149
  productId: productId.optional(),
6138
6150
  slug: slug.optional(),
@@ -22209,29 +22221,19 @@ function judgeItem(until, gate) {
22209
22221
  /** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
22210
22222
  function itemReport(item) {
22211
22223
  const run = latestBuildOf(item);
22212
- const situation = situationOf(item);
22213
22224
  return {
22214
- ...situation ? { situation } : {},
22215
22225
  ...pickFields([item], [
22216
22226
  "id",
22217
22227
  "title",
22218
- "state",
22219
- "planningStatus",
22220
- "needsHumanCause",
22221
- "needsHumanDetail",
22222
22228
  "autoBuild"
22223
22229
  ])[0],
22224
- lifecycle: pickFields([lifecycleOf(item)], [
22225
- "turn",
22226
- "stage",
22227
- "cta",
22228
- "planFailureCause"
22229
- ])[0],
22230
- latestBuild: run ? pickFields([run], [
22230
+ ...standing(item),
22231
+ latestRun: run ? pickFields([run], [
22231
22232
  "id",
22232
22233
  "status",
22233
22234
  "completedSteps",
22234
22235
  "totalSteps",
22236
+ "prNumber",
22235
22237
  "prUrl",
22236
22238
  "prState",
22237
22239
  "prMerged",
@@ -22415,6 +22417,10 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
22415
22417
  message: `${withStop(issue.message)} ${pointers.join(" ")}`
22416
22418
  };
22417
22419
  }
22420
+ if (issue.code === "invalid_value") return {
22421
+ ...issue,
22422
+ message: `${withStop(issue.message)} Pass one of those values exactly.`
22423
+ };
22418
22424
  const argument = issue.path?.length === 1 ? String(issue.path[0]) : void 0;
22419
22425
  const source = argument ? idSource(argument) : void 0;
22420
22426
  if (source && /received undefined/.test(issue.message)) return {
@@ -22563,7 +22569,7 @@ function registerAllTools(server, client, options = {}) {
22563
22569
  function registerWhoami(server, client, registered) {
22564
22570
  server.registerTool("taiga_whoami", {
22565
22571
  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.",
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.",
22567
22573
  inputSchema: z.object({}),
22568
22574
  annotations: READ_ONLY
22569
22575
  }, () => runTool(async () => {
@@ -22571,6 +22577,7 @@ function registerWhoami(server, client, registered) {
22571
22577
  const tools = [...registered.entries()].filter(([name]) => name !== "taiga_whoami");
22572
22578
  const named = (pick) => tools.filter(([, notes]) => pick(notes)).map(([name]) => name);
22573
22579
  return toolResult({
22580
+ api: client.apiHost,
22574
22581
  email: me?.email ?? null,
22575
22582
  role: me?.role ?? null,
22576
22583
  scopes: me?.scopes ?? null,
@@ -22613,8 +22620,8 @@ const SERVER_INSTRUCTIONS = [
22613
22620
  "conversation that specifies a new product. A factory owns products.",
22614
22621
  "",
22615
22622
  "**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 unless",
22617
- "status: 'all'. outcome, situation and nextStep are one-sentence summaries; setup writes return readiness.",
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.",
22618
22625
  "Product documents have two timelines: plan (intent) and reality (from the code).",
22619
22626
  "",
22620
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.487",
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",