@taiga-ai/mcp-server 0.26.0-rc.514 → 0.26.0-rc.519

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 +261 -110
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -321,6 +321,7 @@ var TaigaClient = class {
321
321
  async listBuildRuns(productId, opts) {
322
322
  const params = new URLSearchParams();
323
323
  if (opts?.backlogItemId) params.set("backlogItemId", opts.backlogItemId);
324
+ if (opts?.status?.length) params.set("status", opts.status.join(","));
324
325
  if (opts?.limit) params.set("limit", String(opts.limit));
325
326
  if (opts?.cursor) params.set("cursor", opts.cursor);
326
327
  const qs = params.toString();
@@ -330,6 +331,7 @@ var TaigaClient = class {
330
331
  async listBuildRunActivity(opts) {
331
332
  const params = new URLSearchParams();
332
333
  if (opts?.active) params.set("active", "true");
334
+ if (opts?.status?.length) params.set("status", opts.status.join(","));
333
335
  if (opts?.limit) params.set("limit", String(opts.limit));
334
336
  if (opts?.cursor) params.set("cursor", opts.cursor);
335
337
  const qs = params.toString();
@@ -2472,7 +2474,7 @@ const DOCUMENT_INPUTS = Object.freeze({
2472
2474
  * reads as deleted) and fields they explain would simply be missing. So it
2473
2475
  * refuses to start, and says what to do.
2474
2476
  */
2475
- function apiLevelProblem(introspection, apiUrl, required = 3, version = SERVER_VERSION) {
2477
+ function apiLevelProblem(introspection, apiUrl, required = 4, version = SERVER_VERSION) {
2476
2478
  const offered = introspection.mcpApiLevel ?? 0;
2477
2479
  if (offered >= required) return null;
2478
2480
  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.`;
@@ -2686,7 +2688,7 @@ function setupNextStep(product) {
2686
2688
  if (product?.status !== "draft") return null;
2687
2689
  if (isImportedProduct(product)) switch (importStatusOf(product)) {
2688
2690
  case "analyzing":
2689
- case "generating_docs": return `The import is running and derives the documents from the code (taiga_wait_for_job follows its job). Then review them, and ${FINISH_DISCOVERY}`;
2691
+ case "generating_docs": return `The import is running and derives the documents from the code (taiga_wait (jobId) follows its job). Then review them, and ${FINISH_DISCOVERY}`;
2690
2692
  case "complete": return "The import is done: review the reality documents (taiga_get_product_document without type), then taiga_finish_discovery opens the product and starts planning its first initiatives (paid agent work: ask the person first).";
2691
2693
  case "failed":
2692
2694
  case "cancelled": return "The last import did not finish: taiga_start_import runs it again.";
@@ -2714,21 +2716,22 @@ const BY_ARGUMENT = {
2714
2716
  initiativeId: ITEM_ID_HINT,
2715
2717
  runId: "taiga_list_runs lists the runs, of one product or of every product the key can see.",
2716
2718
  planId: "taiga_get_plan gives the initiative's active plan and its ID.",
2717
- repositoryId: "taiga_list_product_repositories lists the product's repository links; a linkable repository id is not a link ID.",
2718
- replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
2719
+ linkId: "taiga_list_product_repositories lists the product's repository links; a linkable repository id is not a link ID.",
2720
+ replacesLinkId: "taiga_list_product_repositories lists the product's repository links.",
2719
2721
  linkableRepositoryId: "taiga_list_linkable_repositories lists the repositories the GitHub connection reaches, with their ids.",
2720
2722
  installationId: "taiga_list_linkable_repositories lists the connected GitHub accounts.",
2721
2723
  jobId: "A job ID is what the tool that started the job returned (taiga_request_initiatives, taiga_start_import).",
2722
2724
  factoryId: "taiga_list_factories lists the factories the key can see.",
2723
2725
  findingId: "taiga_list_findings lists a product's findings.",
2724
2726
  violationId: "taiga_get_policy_standing lists a product's policy violations.",
2725
- environmentId: "taiga_list_environments lists the product's environments with their IDs; a product ID is not one.",
2727
+ environmentId: "taiga_get_deployments lists the product's environments with their IDs; a product ID is not one.",
2726
2728
  knowledgeId: "taiga_list_knowledge lists the knowledge files.",
2727
2729
  productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
2728
2730
  };
2729
2731
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2730
2732
  const BY_TOOL = {
2731
2733
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2734
+ taiga_get_sbom: () => "An SBOM exists once a sweep has inventoried that repository, where the licenses feature is enabled; taiga_list_packages (linkId) says whether it has packages, and taiga_list_product_repositories lists the links.",
2732
2735
  taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and names the call that asks for one (taiga_queue_initiative off the line, taiga_plan_again for a failed plan)." : ITEM_ID_HINT
2733
2736
  };
2734
2737
  /**
@@ -2741,8 +2744,8 @@ const MISSING_THING = {
2741
2744
  initiativeId: /backlog item|initiative/i,
2742
2745
  runId: /\b(build )?run\b/i,
2743
2746
  planId: /\bplan\b/i,
2744
- repositoryId: /repository link|\blink\b/i,
2745
- replacesRepositoryId: /repository link|\blink\b/i,
2747
+ linkId: /repository link|\blink\b|repository/i,
2748
+ replacesLinkId: /repository link|\blink\b/i,
2746
2749
  linkableRepositoryId: /github repository/i,
2747
2750
  installationId: /installation|account/i,
2748
2751
  jobId: /\bjob\b/i,
@@ -2823,6 +2826,7 @@ function ownArgumentLike(key, own) {
2823
2826
  const PRODUCT_WORDS = {
2824
2827
  backlogItemId: "initiativeId",
2825
2828
  buildRunId: "runId",
2829
+ productRepositoryId: "linkId",
2826
2830
  latestBuild: "latestRun"
2827
2831
  };
2828
2832
  /**
@@ -2865,7 +2869,7 @@ function inProductWords(value) {
2865
2869
  }
2866
2870
  /** The API's 404 sentence in the product's words: "Backlog item not found" is an initiative not found. */
2867
2871
  function inProductWording(said) {
2868
- return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass replacesRepositoryId to swap it in one step, or taiga_unlink_repository first (taiga_list_product_repositories gives its ID).");
2872
+ return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass replacesLinkId to swap it in one step, or taiga_unlink_repository first (taiga_list_product_repositories gives its ID).");
2869
2873
  }
2870
2874
  /**
2871
2875
  * A float without its binary noise: six significant digits. Delivery metrics
@@ -3304,7 +3308,7 @@ function nextStepFor(state) {
3304
3308
  if (state.linked === false) return "Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.";
3305
3309
  if (state.linked === null && state.importState === "not started") return setupNextStep(state.product);
3306
3310
  switch (state.importState) {
3307
- case "running": return "The import is running and derives the documents from the code; taiga_wait_for_job follows it.";
3311
+ case "running": return "The import is running and derives the documents from the code; taiga_wait (jobId) follows it.";
3308
3312
  case "failed": return "The last import did not finish: taiga_start_import runs it again.";
3309
3313
  case "complete":
3310
3314
  if (state.missing && state.missing.length > 0) return `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? "is" : "are"} not published: taiga_get_product_document without type shows them, taiga_publish_product_document publishes a draft.`;
@@ -3783,7 +3787,7 @@ function registerProductTools(server, client) {
3783
3787
  }));
3784
3788
  server.registerTool("taiga_get_product", {
3785
3789
  title: "Get product",
3786
- description: "Read one product and its settings by ID, slug or git remote URL (git remote get-url origin; a URL linked to more than one product is refused with their IDs): name, factory, kind, autonomy, what Taiga builds (application, infrastructure, deployment) and network access, unsetMeans saying what a null setting follows. In setup, readiness says whether it can finish now and what is missing (repository, import, unpublished documents, unstamped checks), plus nextStep. For initiatives, runs and findings, use taiga_get_product_context.",
3790
+ description: "Read one product's settings only (for where it stands, use taiga_get_product_context), by ID, slug or git remote URL (git remote get-url origin; a URL linked to more than one product is refused with their IDs): name, factory, kind, autonomy, what Taiga builds (application, infrastructure, deployment) and network access, unsetMeans saying what a null setting follows. In setup, readiness says whether it can finish now and what is missing (repository, import, unpublished documents, unstamped checks), plus nextStep. For initiatives, runs and findings, use taiga_get_product_context.",
3787
3791
  inputSchema: z.object({
3788
3792
  productId: productId.optional(),
3789
3793
  slug: slug.optional(),
@@ -4135,7 +4139,7 @@ function registerJobTools(server, client) {
4135
4139
  if (!client.hasCapability("products:read")) return;
4136
4140
  server.registerTool("taiga_get_job", {
4137
4141
  title: "Get an agent job",
4138
- description: "Read one agent job, such as a taiga_request_initiatives request: status (pending, processing, completed, failed, cancelled), progress and any error. A finished initiative request lists the initiatives it added, or why none (already covered, not readable as work, outside the product, or not allowed by a policy, with the rule it conflicts with and how to decide otherwise). taiga_wait_for_job waits for it.",
4142
+ description: "Read one agent job, such as a taiga_request_initiatives request: status (pending, processing, completed, failed, cancelled), progress and any error. A finished initiative request lists the initiatives it added, or why none (already covered, not readable as work, outside the product, or not allowed by a policy, with the rule it conflicts with and how to decide otherwise). taiga_wait (jobId) waits for it.",
4139
4143
  inputSchema: z.object({ jobId: idInput("job", "the tool that started the job returned it") }),
4140
4144
  annotations: READ_ONLY
4141
4145
  }, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
@@ -4217,6 +4221,20 @@ const RUN_OUTCOME_FIELDS = [
4217
4221
  * stalled in the hardening pass after the steps and the retry continues it.
4218
4222
  * Null for a run still going or that finished.
4219
4223
  */
4224
+ /**
4225
+ * The line of a failed step's error that says why (PRO-2941): a review found the reason buried in a
4226
+ * 2,000-character dump of git and lint output, behind runtime warnings. The first line that reads as an error,
4227
+ * else the first line; capped. Null when no step failed with an error.
4228
+ */
4229
+ function failureCause(steps) {
4230
+ const failed = steps.find((step) => step["status"] === "failed" && typeof step["error"] === "string");
4231
+ if (!failed) return null;
4232
+ const lines = String(failed["error"]).split("\n").map((line) => line.trim()).filter((line) => line && !/^\(node:\d+\)|^\(Use `node|^[⋯✔↓]/u.test(line));
4233
+ const telling = lines.find((line) => /\berror\b|cannot|not found|denied|refused|no such/iu.test(line)) ?? lines.find((line) => /fail|✖/iu.test(line)) ?? lines[0];
4234
+ if (!telling) return null;
4235
+ const said = telling.length > 200 ? `${telling.slice(0, 197)}...` : telling;
4236
+ return `step ${String(failed["stepOrder"])}: ${said}`;
4237
+ }
4220
4238
  function stoppedIn(run, steps) {
4221
4239
  if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
4222
4240
  if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
@@ -4593,11 +4611,12 @@ function registerBuildTools(server, client) {
4593
4611
  if (!client.hasCapability("products:read")) return;
4594
4612
  server.registerTool("taiga_list_runs", {
4595
4613
  title: "List runs",
4596
- 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).",
4614
+ 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, and status narrows to some statuses (status: [\"failed\", \"stopped\"] for what failed). taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
4597
4615
  inputSchema: z.object({
4598
4616
  productId: productId.optional(),
4599
4617
  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"),
4600
4618
  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"),
4619
+ status: z.array(z.enum(BUILD_RUN_STATUSES)).min(1).optional().describe("Only runs in these statuses, such as [\"failed\", \"stopped\"]"),
4601
4620
  ...pagination
4602
4621
  }),
4603
4622
  annotations: READ_ONLY
@@ -4608,6 +4627,7 @@ function registerBuildTools(server, client) {
4608
4627
  if (productIdOf) {
4609
4628
  const result = await client.listBuildRuns(productIdOf, {
4610
4629
  backlogItemId: params.initiativeId,
4630
+ status: params.status,
4611
4631
  limit: params.limit,
4612
4632
  cursor: params.cursor
4613
4633
  });
@@ -4619,7 +4639,7 @@ function registerBuildTools(server, client) {
4619
4639
  }));
4620
4640
  server.registerTool("taiga_get_run", {
4621
4641
  title: "Get run",
4622
- description: "Read one coding-agent run: status and outcome (one sentence), pull request, hardening, and each step with its status, commit, files, verification and headline. scanCoverage names security scanners that did not run, and why. verification is Taiga's check after a step (tests, lint, build); verificationNote says when one failed throughout. A failed or stopped run has stoppedIn and failedReasonMeans. full: true adds the complete instructions and reports. To resume: taiga_resume_initiative; to stop: move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
4642
+ description: "Read one coding-agent run: status and outcome (one sentence), pull request, hardening, and each step with its status, commit, files, verification and headline. scanCoverage names security scanners that did not run, and why. verification is Taiga's check after a step (tests, lint, build); verificationNote says when one failed throughout. A failed or stopped run has stoppedIn, failedReasonMeans and cause (the line of the failed step's error that says why). full: true adds the complete instructions and reports. To resume: taiga_resume_initiative; to stop: move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
4623
4643
  inputSchema: z.object({
4624
4644
  runId,
4625
4645
  full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
@@ -4630,7 +4650,11 @@ function registerBuildTools(server, client) {
4630
4650
  const runRow = run.data;
4631
4651
  const rows = steps.data.map((step) => neverStarted(step));
4632
4652
  const where = stoppedIn(runRow, rows);
4633
- const stop = where ? { stoppedIn: where } : {};
4653
+ const cause = failureCause(rows);
4654
+ const stop = {
4655
+ ...where ? { stoppedIn: where } : {},
4656
+ ...cause ? { cause } : {}
4657
+ };
4634
4658
  const states = await initiativeStates(client, [runRow]);
4635
4659
  const readable = withInitiativeState(readableRun(runRow), states);
4636
4660
  const note = verificationNote(rows);
@@ -4812,7 +4836,7 @@ async function queueView(client, product, options = { name: "all" }) {
4812
4836
  return {
4813
4837
  linePaused: paused === null ? null : typeof paused === "string",
4814
4838
  entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
4815
- waitingOn: line ?? waitingOn(rows, idle ? station : null),
4839
+ waitingOn: line ?? waitingOn(rows, idle ? station : null) ?? (rows.length === 0 && "offTheLine" in offLine ? `Nothing is queued. ${offLine.offTheLine}` : null),
4816
4840
  ...offLine,
4817
4841
  ...note ? { note } : {}
4818
4842
  };
@@ -5241,7 +5265,7 @@ function registerBacklogReads(server, client) {
5241
5265
  function registerRequest(server, client) {
5242
5266
  server.registerTool("taiga_request_initiatives", {
5243
5267
  title: "Request new initiatives",
5244
- description: "Ask an agent to turn described work into initiatives in the product's Backlog, and return the job. One piece of work becomes one initiative, a program an ordered set with dependencies. It may add none and say why (already covered, not work, outside the product, or a named policy forbids it). Omit title and description for the one next initiative the product most needs. taiga_wait_for_job gives the outcome. One request per product at a time (CONFLICT). Nothing goes on the line.",
5268
+ description: "Ask an agent to turn described work into initiatives in the product's Backlog, and return the job. One piece of work becomes one initiative, a program an ordered set with dependencies. It may add none and say why (already covered, not work, outside the product, or a named policy forbids it). Omit title and description for the one next initiative the product most needs. taiga_wait (jobId) gives the outcome. One request per product at a time (CONFLICT). Nothing goes on the line.",
5245
5269
  inputSchema: z.object({
5246
5270
  productId,
5247
5271
  title: z.string().max(500).optional().describe("What the work is, in a line"),
@@ -5453,6 +5477,98 @@ const FINDING_ROW_FIELDS = [
5453
5477
  "ignoreReason",
5454
5478
  "ignoreExpiresAt"
5455
5479
  ];
5480
+ /** Locations a folded row names before it says how many more there are. */
5481
+ const GROUP_WHERE = 5;
5482
+ /** What makes two findings one rule's: its rule id for code, infrastructure and secrets, its license verdict for a license. */
5483
+ /** The scanner's title cap (deps.py _COLUMN_CAP): a title cut there ends in "...". */
5484
+ const TITLE_CAP = 300;
5485
+ /**
5486
+ * The license verdict of a license finding's title: what follows "<package> <version>: ". A title the scanner cut
5487
+ * at its cap has no whole verdict, so it folds with nothing: two cut titles could share a prefix and differ.
5488
+ */
5489
+ function licenseVerdict(row) {
5490
+ const title = String(row["title"] ?? "");
5491
+ if (title.length >= TITLE_CAP && title.endsWith("...")) return "";
5492
+ return title.split(": ").slice(1).join(": ");
5493
+ }
5494
+ function groupKey(row) {
5495
+ const status = String(row["status"] ?? "");
5496
+ const link = String(row["productRepositoryId"] ?? "");
5497
+ if (row["kind"] === "license_violation") {
5498
+ const verdict = licenseVerdict(row);
5499
+ return verdict ? `license|${status}|${link}|${verdict}` : null;
5500
+ }
5501
+ const location = row["location"];
5502
+ const rule = typeof location?.ruleId === "string" ? location.ruleId : null;
5503
+ return rule ? `${String(row["kind"])}|${String(row["severity"])}|${status}|${link}|${rule}` : null;
5504
+ }
5505
+ /**
5506
+ * Where one finding is, in a few characters: its file and line (and the resource a scanner names), or its package.
5507
+ * A review read seven "CKV_GHA_7" where the location keeps no path: the file is in sourcePaths.
5508
+ */
5509
+ function whereOf(row) {
5510
+ const location = row["location"];
5511
+ const paths = Array.isArray(row["sourcePaths"]) ? row["sourcePaths"] : [];
5512
+ const file = typeof location?.path === "string" ? location.path : typeof paths[0] === "string" ? paths[0] : null;
5513
+ if (file) {
5514
+ const at = location?.line ? `${file}:${String(location.line)}` : file;
5515
+ return typeof location?.resource === "string" ? `${at} ${location.resource}` : at;
5516
+ }
5517
+ return [row["packageName"], row["currentVersion"]].filter(Boolean).join(" ") || String(row["id"]);
5518
+ }
5519
+ /** The words every member's title opens with, at a word boundary; empty when they share fewer than 20 characters. */
5520
+ function sharedTitle(titles) {
5521
+ let prefix = titles[0] ?? "";
5522
+ for (const title of titles.slice(1)) {
5523
+ let at = 0;
5524
+ while (at < prefix.length && prefix[at] === title[at]) at += 1;
5525
+ prefix = prefix.slice(0, at);
5526
+ }
5527
+ const cut = (prefix.length === (titles[0] ?? "").length ? prefix : prefix.slice(0, Math.max(0, prefix.lastIndexOf(" ")))).trim().replace(/[\s,(:;-]+$/, "");
5528
+ return cut.length >= 20 ? cut : "";
5529
+ }
5530
+ /**
5531
+ * Findings of one rule or license verdict on a page, folded into one row (PRO-2941): a review read seven
5532
+ * CKV_GHA_7 rows and fifteen sharp license rows, identical but for where. The first stands for them, with
5533
+ * count, every findingId (what fix and ignore take) and the first few locations.
5534
+ */
5535
+ function grouped(rows, fold) {
5536
+ if (!fold) return rows;
5537
+ const groups = /* @__PURE__ */ new Map();
5538
+ const order = [];
5539
+ for (const row of rows) {
5540
+ const key = groupKey(row);
5541
+ if (!key) {
5542
+ order.push(row);
5543
+ continue;
5544
+ }
5545
+ if (!groups.has(key)) {
5546
+ groups.set(key, []);
5547
+ order.push(key);
5548
+ }
5549
+ groups.get(key).push(row);
5550
+ }
5551
+ return order.map((entry) => {
5552
+ if (typeof entry !== "string") return entry;
5553
+ const members = groups.get(entry);
5554
+ if (members.length === 1) return members[0];
5555
+ const where = members.map(whereOf);
5556
+ const first = members[0];
5557
+ const shared = Object.fromEntries(Object.entries(first).filter(([key, value]) => key !== "id" && key !== "title" && members.every((member) => JSON.stringify(member[key]) === JSON.stringify(value))));
5558
+ const location = first["location"];
5559
+ const words = sharedTitle(members.map((member) => String(member["title"] ?? "")));
5560
+ const rule = typeof location?.ruleId === "string" ? location.ruleId : null;
5561
+ const said = first["kind"] === "license_violation" ? licenseVerdict(first) : words ? rule && !words.includes(rule) ? `${rule}: ${words}` : words : rule ?? String(first["title"]);
5562
+ return {
5563
+ ...shared,
5564
+ title: said,
5565
+ count: members.length,
5566
+ findingIds: members.map((member) => member["id"]),
5567
+ where: where.slice(0, GROUP_WHERE),
5568
+ ...where.length > GROUP_WHERE ? { moreWhere: where.length - GROUP_WHERE } : {}
5569
+ };
5570
+ });
5571
+ }
5456
5572
  /**
5457
5573
  * Whether taiga_fix_finding would take a finding, said on the row (PRO-2800): three reviews found a dependency
5458
5574
  * marked remediableBy coding_agent with no fixed version, which the API refuses to fix. The API's own refusals,
@@ -5584,8 +5700,11 @@ function standingSummary(standing) {
5584
5700
  bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
5585
5701
  }
5586
5702
  const stated = (state) => controls.filter((c) => c["state"] === state).length;
5703
+ const withAny = (key) => controls.filter((control) => Number(control[key] ?? 0) > 0).length;
5587
5704
  return {
5588
- controls: listed.length,
5705
+ controlsWithViolations: listed.length,
5706
+ controlsBreached: withAny("findingCount"),
5707
+ controlsDiverged: withAny("divergenceCount"),
5589
5708
  controlsInPolicies: controls.length,
5590
5709
  controlsMet: stated("met"),
5591
5710
  controlsNotApplicable: stated("not_applicable"),
@@ -5611,13 +5730,14 @@ function registerMaintenanceTools(server, client) {
5611
5730
  if (!client.hasCapability("products:read")) return;
5612
5731
  server.registerTool("taiga_list_findings", {
5613
5732
  title: "List maintenance findings",
5614
- description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status for open findings. controlId lists the findings that breach one control. taiga_get_finding reads one.",
5733
+ description: "List what maintenance sweeps found in a product's repository: dependencies, secrets, insecure code (sast) and infrastructure. The first page opens with sweep (open counts, latest sweeps), so an old or failed sweep is not read as an all-clear. Rows: severity, tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), location, remediableBy (coding_agent: a run here can fix it; upstream_only: the code is in a submodule, so the fix belongs in its own repository), fixable: false when no fixed version is known yet, breached controls and initiativeId of a fix under way. Omit status for open findings. controlId lists the findings that breach one control. Findings of one rule or license verdict in one repository fold into one row per page, with count, findingIds (what fix and ignore take), where, and only the fields they share; group: false lists each. taiga_get_finding reads one.",
5615
5734
  inputSchema: z.object({
5616
5735
  productId,
5617
5736
  controlId: z.string().trim().min(1).max(40).optional().describe("Only the findings that breach this control, such as SEC-5.1 (one page, searched across all)"),
5618
5737
  status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
5619
5738
  severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
5620
5739
  kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
5740
+ group: z.boolean().optional().describe("Fold findings of one rule or license on this page into one row (default true); false lists each"),
5621
5741
  ...pagination
5622
5742
  }),
5623
5743
  annotations: READ_ONLY
@@ -5632,7 +5752,7 @@ function registerMaintenanceTools(server, client) {
5632
5752
  cursor
5633
5753
  }), "", () => [""], params.limit ?? 25, (row) => Array.isArray(row["controls"]) && row["controls"].some((c) => String(c).toUpperCase() === wanted));
5634
5754
  return toolResult({
5635
- ...page(findingRows(found.rows), null),
5755
+ ...page(grouped(findingRows(found.rows), params.group !== false), null),
5636
5756
  ...found.more ? { more: found.rows.length < (params.limit ?? 25) ? `Only the first 1000 findings were searched; narrow by status, severity or kind to reach the rest.` : "There may be more than the limit carries: raise the limit, or narrow by status, severity or kind." } : {},
5637
5757
  ...found.rows.length === 0 && !found.more ? { note: `No ${params.status ? `${params.status} ` : "open "}finding breaches ${wanted}. That is no all-clear on its own: taiga_list_findings without controlId says when the product was last swept.` } : {}
5638
5758
  });
@@ -5646,7 +5766,7 @@ function registerMaintenanceTools(server, client) {
5646
5766
  sweep: null,
5647
5767
  sweepNote: "The sweep overview could not be read: an empty list here is not an all-clear."
5648
5768
  } : sweep ? { sweep } : {},
5649
- ...page(findingRows(result.data), result.nextCursor),
5769
+ ...page(grouped(findingRows(result.data), params.group !== false), result.nextCursor),
5650
5770
  ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {},
5651
5771
  ...params.status === void 0 && params.severity === void 0 && params.kind === void 0 && typeof result.totalCount === "number" && sweep && sweep !== "unread" && typeof sweep["openFindings"] === "number" && sweep["openFindings"] !== result.totalCount ? { totalNote: "totalCount includes findings with a fix under way (remediating) and ignored findings whose ignore has expired; sweep.openFindings counts open ones only." } : {}
5652
5772
  });
@@ -5658,13 +5778,16 @@ function registerMaintenanceTools(server, client) {
5658
5778
  productId,
5659
5779
  license: z.string().min(1).max(4096).optional().describe("Only packages carrying exactly this SPDX string"),
5660
5780
  search: z.string().min(1).max(255).optional().describe("Case-insensitive substring of the package name"),
5661
- productRepositoryId: idInput("repository", "taiga_list_product_repositories gives it", "only this repository").optional(),
5781
+ linkId: idInput("repository link", "taiga_list_product_repositories gives it", "only this repository").optional(),
5662
5782
  ...pagination
5663
5783
  }),
5664
5784
  annotations: READ_ONLY
5665
5785
  }, (params) => runTool(async () => {
5666
- const { productId: id, ...filters } = params;
5667
- const [result, summary] = await Promise.all([client.listPackages(id, filters), params.cursor === void 0 ? client.getLicenseSummary(id) : Promise.resolve(null)]);
5786
+ const { productId: id, linkId: link, ...filters } = params;
5787
+ const [result, summary] = await Promise.all([client.listPackages(id, {
5788
+ ...filters,
5789
+ ...link ? { productRepositoryId: link } : {}
5790
+ }), params.cursor === void 0 ? client.getLicenseSummary(id) : Promise.resolve(null)]);
5668
5791
  return toolResult({
5669
5792
  ...summary ? { licenses: summary.data } : {},
5670
5793
  ...page(pickFields(result.data, PACKAGE_ROW_FIELDS), result.nextCursor),
@@ -5676,10 +5799,10 @@ function registerMaintenanceTools(server, client) {
5676
5799
  description: "One repository's software bill of materials as a CycloneDX 1.5 JSON document, rendered from the stored package inventory: metadata (timestamp, tools, the repository), every package with its purl, license and scope, and the packages the repository declares directly. The whole document is returned, so for a large repository the answer is large; read taiga_list_packages when a summary is enough. NOT_FOUND where the licenses feature is not enabled, and for a repository no sweep has inventoried yet.",
5677
5800
  inputSchema: z.object({
5678
5801
  productId,
5679
- repositoryId: idInput("repository", "taiga_list_product_repositories gives it")
5802
+ linkId: idInput("repository link", "taiga_list_product_repositories gives it")
5680
5803
  }),
5681
5804
  annotations: READ_ONLY
5682
- }, (params) => runTool(async () => toolResult(await client.getSbom(params.productId, params.repositoryId))));
5805
+ }, (params) => runTool(async () => toolResult(await client.getSbom(params.productId, params.linkId))));
5683
5806
  server.registerTool("taiga_get_finding", {
5684
5807
  title: "Get a maintenance finding",
5685
5808
  description: "Read one maintenance finding: exploit likelihood (EPSS, known-exploited list), the advisory summarized (full: true for its whole record: every affected range and reference) and fixed version, code location, breached policy controls, why it ranks where it does, history (reintroduced, previously resolved) and the remediation under way. fixable: false with fixNote says taiga_fix_finding would refuse it. A secret's value is never stored, only the rule and location.",
@@ -6067,6 +6190,8 @@ function isDue(row, now) {
6067
6190
  }
6068
6191
  /** Pages read for the summary; past this it says it counted only those. */
6069
6192
  const SUMMARY_PAGE = 100;
6193
+ /** Rows an org-wide first page carries when the caller names no limit: the summary answers the rest. */
6194
+ const ORG_WIDE_ROWS = 10;
6070
6195
  /** Pages the summary reads before it says it stopped: 1,000 items. */
6071
6196
  const SUMMARY_PAGES = 10;
6072
6197
  /** Every item the summary counts, up to SUMMARY_PAGES pages; nextCursor set when it stopped short. Null on failure. */
@@ -6188,8 +6313,10 @@ function registerInboxTools(server, client) {
6188
6313
  ...resolved ? { resolved: true } : {}
6189
6314
  };
6190
6315
  const first = params.cursor === void 0;
6316
+ const limit = params.limit ?? (params.productId ? void 0 : ORG_WIDE_ROWS);
6191
6317
  const result = await client.listInbox({
6192
6318
  ...query,
6319
+ ...limit !== void 0 ? { limit } : {},
6193
6320
  ...first ? { withTotal: true } : {}
6194
6321
  });
6195
6322
  const overview = !first || resolved ? null : !result.nextCursor ? result : await everyItem(client, query);
@@ -6228,7 +6355,7 @@ function registerInboxTools(server, client) {
6228
6355
  ...checked.unchecked > 0 ? (() => {
6229
6356
  const onPage = due.filter((row) => checked.uncheckedIds.has(String(row["id"]))).length;
6230
6357
  const beyond = checked.unchecked - onPage;
6231
- return { staleNote: `${[...onPage > 0 ? [`${onPage} review${onPage === 1 ? "" : "s"} on this page could not be checked for whether the subject moved on: those rows say staleChecked: false, and their readWith read tells`] : [], ...beyond > 0 ? [`${beyond} review${beyond === 1 ? "" : "s"} counted in the summary but not on this page could not be checked either, so the summary counts them open`] : []].join("; ")}.` };
6358
+ return { staleNote: `${[...onPage > 0 ? [`${onPage} review${onPage === 1 ? "" : "s"} on this page could not be checked for whether the subject moved on: those rows say staleChecked: false, and their readWith read tells`] : [], ...beyond > 0 ? [`${beyond} review${beyond === 1 ? "" : "s"} past this page could not be checked, so the summary counts them as open`] : []].join("; ")}.` };
6232
6359
  })() : {}
6233
6360
  });
6234
6361
  }));
@@ -6619,13 +6746,12 @@ async function standingPart(client, productId) {
6619
6746
  const standing = (await client.getPolicyStanding(productId)).data;
6620
6747
  const summary = standingSummary(standing);
6621
6748
  const state = standing.assessment?.state;
6622
- const controls = Array.isArray(standing.controls) ? standing.controls : [];
6623
- const withAny = (key) => controls.filter((control) => Number(control[key] ?? 0) > 0).length;
6624
6749
  return {
6625
6750
  openViolations: summary.violations,
6626
6751
  bySeverity: summary.bySeverity,
6627
- controlsBreached: withAny("findingCount"),
6628
- controlsDiverged: withAny("divergenceCount"),
6752
+ controlsWithViolations: summary.controlsWithViolations,
6753
+ controlsBreached: summary.controlsBreached,
6754
+ controlsDiverged: summary.controlsDiverged,
6629
6755
  controlsMet: summary.controlsMet,
6630
6756
  controlsNotApplicable: summary.controlsNotApplicable,
6631
6757
  ...state === "never" || state === "failed" ? { note: `The assessment ${state === "never" ? "has never run" : "failed"}: no violations here is not an all-clear.` } : {}
@@ -6992,7 +7118,7 @@ function deploymentLines(rows) {
6992
7118
  };
6993
7119
  });
6994
7120
  }
6995
- const environmentId = idInput("environment", "taiga_list_environments lists them; a product ID is not one");
7121
+ const environmentId = idInput("environment", "taiga_get_deployments lists them; a product ID is not one");
6996
7122
  /** The windows the pages offer, as the schema's own values rather than a number checked afterwards. */
6997
7123
  const windowDays = z.literal([
6998
7124
  30,
@@ -7002,11 +7128,11 @@ const windowDays = z.literal([
7002
7128
  /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
7003
7129
  const isApiFailure = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
7004
7130
  /** A part that may fail on its own without failing the read: null and the reason, or the value. */
7005
- async function part(read) {
7131
+ async function part(read, rethrow = () => false) {
7006
7132
  try {
7007
7133
  return { value: await read() };
7008
7134
  } catch (error) {
7009
- if (!isApiFailure(error)) throw error;
7135
+ if (!isApiFailure(error) || rethrow(error)) throw error;
7010
7136
  return { unavailable: error instanceof TaigaApiError ? `${error.status}: ${error.apiMessage ?? ""}`.trim() : "unreachable" };
7011
7137
  }
7012
7138
  }
@@ -7037,7 +7163,7 @@ function registerObserveTools(server, client) {
7037
7163
  if (!client.hasCapability("products:read")) return;
7038
7164
  server.registerTool("taiga_get_deployments", {
7039
7165
  title: "Get what runs where",
7040
- description: "Read what runs in each of a product's environments and its recent deployments, newest first: commit, pull request, status, deployer and, on failure, the provider's error. What runs is the latest successful deployment, so a newer failure shows beside it. History is mirrored from the provider, including deployments made outside Taiga; syncStatus says when it last synced. environmentId narrows to one environment. Taiga never deploys.",
7166
+ description: "Read a product's environments with their IDs, what runs in each, and its recent deployments, newest first: commit, pull request, status, deployer and, on failure, the provider's error. What runs is the latest successful deployment, so a newer failure shows beside it. History is mirrored from the provider, including deployments made outside Taiga; syncStatus says when it last synced. environmentId narrows to one environment. Taiga never deploys.",
7041
7167
  inputSchema: z.object({
7042
7168
  productId,
7043
7169
  environmentId: environmentId.optional(),
@@ -7045,25 +7171,31 @@ function registerObserveTools(server, client) {
7045
7171
  }),
7046
7172
  annotations: READ_ONLY
7047
7173
  }, (params) => runTool(async () => {
7048
- const [environments, current, history, sync] = await Promise.all([
7174
+ const [environments, currentRead, historyRead, sync] = await Promise.all([
7049
7175
  allEnvironments(client, params.productId),
7050
- client.getCurrentDeployments(params.productId),
7051
- client.listDeployments(params.productId, {
7176
+ part(() => client.getCurrentDeployments(params.productId)),
7177
+ part(() => client.listDeployments(params.productId, {
7052
7178
  environmentId: params.environmentId,
7053
7179
  limit: params.limit ?? 10,
7054
7180
  cursor: params.cursor
7055
- }),
7181
+ }), (error) => Boolean(params.environmentId) && error instanceof TaigaApiError && error.status === 404),
7056
7182
  part(() => client.getDeploymentSyncStatus(params.productId))
7057
7183
  ]);
7058
- const now = current.data;
7059
- const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_create_environment, and its deployments are mirrored from the provider once it is." : history.data.length === 0 && !params.cursor ? "No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last synced), or nothing has been deployed." : null;
7060
- const live = new Set([...now.current ?? [], ...now.deploying ?? []].map((row) => row.id));
7184
+ const now = "value" in currentRead ? currentRead.value.data : null;
7185
+ const history = "value" in historyRead ? historyRead.value : null;
7186
+ const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_create_environment, and its deployments are mirrored from the provider once it is." : history && history.data.length === 0 && !params.cursor ? "No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last synced), or nothing has been deployed." : null;
7187
+ const live = new Set([...now?.current ?? [], ...now?.deploying ?? []].map((row) => row.id));
7188
+ const unavailable = {
7189
+ ..."unavailable" in currentRead ? { running: currentRead.unavailable } : {},
7190
+ ..."unavailable" in historyRead ? { history: historyRead.unavailable } : {}
7191
+ };
7061
7192
  return toolResult({
7062
7193
  environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
7063
7194
  ...environments.more ? { moreEnvironments: true } : {},
7064
- running: deploymentLines(now.current ?? []),
7065
- deploying: deploymentLines(now.deploying ?? []),
7066
- history: page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor),
7195
+ running: now ? deploymentLines(now.current ?? []) : null,
7196
+ deploying: now ? deploymentLines(now.deploying ?? []) : null,
7197
+ history: history ? page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor) : null,
7198
+ ...Object.keys(unavailable).length > 0 ? { unavailable } : {},
7067
7199
  ..."value" in sync ? { syncStatus: sync.value.data } : {
7068
7200
  syncStatus: null,
7069
7201
  syncUnavailable: sync.unavailable
@@ -7071,19 +7203,6 @@ function registerObserveTools(server, client) {
7071
7203
  ...note ? { note } : {}
7072
7204
  });
7073
7205
  }));
7074
- server.registerTool("taiga_list_environments", {
7075
- title: "List a product's environments",
7076
- description: "List a product's environments with ID, type, order and App URL. These are the environment IDs taiga_get_delivery_metrics, taiga_get_monitoring and taiga_update_environment take. taiga_get_deployments lists them too, with what runs where.",
7077
- inputSchema: z.object({ productId }),
7078
- annotations: READ_ONLY
7079
- }, (params) => runTool(async () => {
7080
- const { rows, more } = await allEnvironments(client, params.productId);
7081
- return toolResult({
7082
- data: pickFields(rows, ENVIRONMENT_FIELDS),
7083
- ...more ? { more: "More environments than this answer reads; the rest were left out." } : {},
7084
- ...rows.length === 0 ? { note: "No environment is defined yet; taiga_create_environment adds one." } : {}
7085
- });
7086
- }));
7087
7206
  server.registerTool("taiga_get_delivery_metrics", {
7088
7207
  title: "Get delivery metrics",
7089
7208
  description: "Read DORA's five delivery metrics for one environment (the page shows production) over 30, 60 or 90 days: deployment frequency, lead time, failed deployment recovery time, change-failure rate and rework rate, each with its trend against the window before and a performance band (none for rework rate). Computed from deployments, not incidents; previews and checks do not count. Under ten deployments a trend is approximate; a metric with no data says so instead of a number. Without environmentId, the product's production environment when it has exactly one.",
@@ -7103,7 +7222,7 @@ function registerObserveTools(server, client) {
7103
7222
  note: "This product has no environments, so there is nothing to measure: an environment is defined with taiga_create_environment, and its deployments are mirrored from the provider once it is."
7104
7223
  });
7105
7224
  const production = all.filter((env) => env.type === "production");
7106
- if (production.length !== 1 || more) return toolError("VALIDATION_ERROR", `Pass environmentId: the product has ${more ? "more environments than one read holds, and " : ""}${production.length === 0 ? "no" : production.length} production environment${production.length === 1 ? "" : "s"}${more ? " among the first" : ""}. taiga_list_environments lists them with their IDs.`, { environments: all.map((env) => ({
7225
+ if (production.length !== 1 || more) return toolError("VALIDATION_ERROR", `Pass environmentId: the product has ${more ? "more environments than one read holds, and " : ""}${production.length === 0 ? "no" : production.length} production environment${production.length === 1 ? "" : "s"}${more ? " among the first" : ""}. taiga_get_deployments lists them with their IDs.`, { environments: all.map((env) => ({
7107
7226
  id: env.id,
7108
7227
  name: env.name,
7109
7228
  type: env.type
@@ -7232,7 +7351,7 @@ function registerProductDocumentWrites(server, client) {
7232
7351
  }, (params) => runTool(async () => toolResult((await client.markProductDocumentReviewed(params.productId, params.type)).data)));
7233
7352
  if (canRead) server.registerTool("taiga_regenerate_product_document", {
7234
7353
  title: "Regenerate a product document",
7235
- description: "Regenerate one product document from the documents it derives from, when it is outdated or needs another pass, and return the job. The result is published and documents derived from it are marked outdated. The Specification (from the Discovery conversation) and the Look & Feel (from its concepts) are made in the web. Documents Discovery requires regenerate only while Discovery is open; the optional Service Blueprint any time. taiga_wait_for_job says when it is done.",
7354
+ description: "Regenerate one product document from the documents it derives from, when it is outdated or needs another pass, and return the job. The result is published and documents derived from it are marked outdated. The Specification (from the Discovery conversation) and the Look & Feel (from its concepts) are made in the web. Documents Discovery requires regenerate only while Discovery is open; the optional Service Blueprint any time. taiga_wait (jobId) says when it is done.",
7236
7355
  inputSchema: z.object({
7237
7356
  productId,
7238
7357
  type: generatedType
@@ -22008,7 +22127,11 @@ const GITHUB_REPOSITORY_FIELDS = [
22008
22127
  "linked",
22009
22128
  "linkedProduct"
22010
22129
  ];
22011
- const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a linkable repository id is not one");
22130
+ /**
22131
+ * A product's link to a repository: one name for it in every tool (PRO-2941). Reviews met it as repositoryId
22132
+ * here, productRepositoryId on findings and packages, and linkableRepositoryId (a different thing) beside both.
22133
+ */
22134
+ const linkId = idInput("repository link", "taiga_list_product_repositories gives it (its id); a linkable repository id is not one");
22012
22135
  /** Round-robin over lists: the first of each, then the second of each, and so on. */
22013
22136
  function interleave(lists) {
22014
22137
  const out = [];
@@ -22072,11 +22195,11 @@ function registerRepositoryTools(server, client) {
22072
22195
  description: "List the branches of a product's linked repository, each with whether it is protected, to choose the one Taiga works on (taiga_set_repository_branch). truncated: true means there are more than listed.",
22073
22196
  inputSchema: z.object({
22074
22197
  productId,
22075
- repositoryId
22198
+ linkId
22076
22199
  }),
22077
22200
  annotations: READ_ONLY
22078
22201
  }, (params) => runTool(async () => {
22079
- const result = await client.listRepositoryBranches(params.productId, params.repositoryId);
22202
+ const result = await client.listRepositoryBranches(params.productId, params.linkId);
22080
22203
  return toolResult({
22081
22204
  data: result.data,
22082
22205
  truncated: result.truncated
@@ -22132,11 +22255,11 @@ function registerRepositoryTools(server, client) {
22132
22255
  if (!client.hasCapability("vcs:manage")) return;
22133
22256
  server.registerTool("taiga_link_repository", {
22134
22257
  title: "Link a repository to a product",
22135
- description: "Link a GitHub repository to a product (taiga_list_linkable_repositories gives its id). replacesRepositoryId swaps out the one linked now in one step (the same one changes nothing); linking while one is linked without it is refused. A managed link (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported.",
22258
+ description: "Link a GitHub repository to a product (taiga_list_linkable_repositories gives its id). replacesLinkId swaps out that link in one step (the same one changes nothing). Whether a product may link more than one repository depends on the organization; when it may not, a second link is refused and the refusal says to pass replacesLinkId. A managed link (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported.",
22136
22259
  inputSchema: z.object({
22137
22260
  productId,
22138
22261
  linkableRepositoryId: idInput("linkable repository", "taiga_list_linkable_repositories gives it (a row's id; not GitHub's numeric id, not a link ID)"),
22139
- replacesRepositoryId: repositoryId.optional().describe("The link this one replaces")
22262
+ replacesLinkId: linkId.optional().describe("The link this one replaces")
22140
22263
  }),
22141
22264
  annotations: OPERATE
22142
22265
  }, (params) => runTool(async () => {
@@ -22144,7 +22267,7 @@ function registerRepositoryTools(server, client) {
22144
22267
  try {
22145
22268
  linked = await client.linkRepository(params.productId, {
22146
22269
  githubRepositoryId: params.linkableRepositoryId,
22147
- ...params.replacesRepositoryId ? { replacesRepositoryId: params.replacesRepositoryId } : {}
22270
+ ...params.replacesLinkId ? { replacesRepositoryId: params.replacesLinkId } : {}
22148
22271
  });
22149
22272
  } catch (error) {
22150
22273
  const holder = await holderOf(client, error, params.productId);
@@ -22161,14 +22284,14 @@ function registerRepositoryTools(server, client) {
22161
22284
  description: "Choose the branch Taiga branches from and opens pull requests against (taiga_list_repository_branches lists them), or null for the repository's default. A protected branch is accepted, but Taiga may not be able to merge into it; the result says when it is protected. Optionally sets a label for the link.",
22162
22285
  inputSchema: z.object({
22163
22286
  productId,
22164
- repositoryId,
22287
+ linkId,
22165
22288
  baseBranch: z.string().trim().min(1).max(255).nullable().optional().describe("The branch; null for the default"),
22166
22289
  label: z.string().trim().min(1).max(100).nullable().optional().describe("A label for the link; null clears it")
22167
22290
  }),
22168
22291
  annotations: OPERATE_SETTING
22169
22292
  }, (params) => runTool(async () => {
22170
22293
  if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_ERROR", "Set baseBranch, label or both.");
22171
- const updated = (await client.updateRepository(params.productId, params.repositoryId, {
22294
+ const updated = (await client.updateRepository(params.productId, params.linkId, {
22172
22295
  ...params.baseBranch !== void 0 ? { baseBranch: params.baseBranch } : {},
22173
22296
  ...params.label !== void 0 ? { label: params.label } : {}
22174
22297
  })).data;
@@ -22180,16 +22303,16 @@ function registerRepositoryTools(server, client) {
22180
22303
  }));
22181
22304
  server.registerTool("taiga_unlink_repository", {
22182
22305
  title: "Unlink a repository from a product",
22183
- description: "Unlink a repository from a product. The product keeps its documents and initiatives, but nothing is planned against code, built or swept. The repository itself is untouched; one Taiga created for the product cannot be unlinked. To swap it, use taiga_link_repository with replacesRepositoryId.",
22306
+ description: "Unlink a repository from a product. The product keeps its documents and initiatives, but nothing is planned against code, built or swept. The repository itself is untouched; one Taiga created for the product cannot be unlinked. To swap it, use taiga_link_repository with replacesLinkId.",
22184
22307
  inputSchema: z.object({
22185
22308
  productId,
22186
- repositoryId
22309
+ linkId
22187
22310
  }),
22188
22311
  annotations: OPERATE
22189
22312
  }, (params) => runTool(async () => {
22190
- await client.unlinkRepository(params.productId, params.repositoryId);
22313
+ await client.unlinkRepository(params.productId, params.linkId);
22191
22314
  return toolResult({
22192
- repositoryId: params.repositoryId,
22315
+ linkId: params.linkId,
22193
22316
  unlinked: true
22194
22317
  });
22195
22318
  }));
@@ -22573,7 +22696,7 @@ function registerSetupTools(server, client) {
22573
22696
  }));
22574
22697
  server.registerTool("taiga_refresh_reality_documents", {
22575
22698
  title: "Refresh a product's documents from its code",
22576
- description: "Derive the reality documents again from the code on the branch Taiga works on (effectiveBranch): all, or the ones named. For a document the import misread, refresh it instead of importing again; unchanged sections are not republished. Returns the job; taiga_wait_for_job says when it is done.",
22699
+ description: "Derive the reality documents again from the code on the branch Taiga works on (effectiveBranch): all, or the ones named. For a document the import misread, refresh it instead of importing again; unchanged sections are not republished. Returns the job; taiga_wait (jobId) says when it is done.",
22577
22700
  inputSchema: z.object({
22578
22701
  productId,
22579
22702
  docTypes: z.array(z.enum(REALITY_DERIVED_DOCUMENT_TYPES)).min(1).max(REALITY_DERIVED_DOCUMENT_TYPES.length).optional().describe("The documents to refresh; omit for all")
@@ -22602,7 +22725,7 @@ function registerSetupTools(server, client) {
22602
22725
  if (!client.hasCapability("products:read")) return;
22603
22726
  server.registerTool("taiga_start_import", {
22604
22727
  title: "Import a codebase into a product",
22605
- description: "Start importing a product created for import, and return the job. Taiga reads the linked repository and derives the full document set from the code, citing the files. Then the scanners sweep it, the documents are assessed against policies, and the first initiatives are planned when Discovery finishes. Refused for a scratch product, without a linked repository, or while an import runs. taiga_wait_for_job says when it is done (it takes a while).",
22728
+ description: "Start importing a product created for import, and return the job. Taiga reads the linked repository and derives the full document set from the code, citing the files. Then the scanners sweep it, the documents are assessed against policies, and the first initiatives are planned when Discovery finishes. Refused for a scratch product, without a linked repository, or while an import runs. taiga_wait (jobId) says when it is done (it takes a while).",
22606
22729
  inputSchema: z.object({ productId }),
22607
22730
  annotations: OPERATE
22608
22731
  }, (params) => runTool(async () => {
@@ -22869,20 +22992,25 @@ function judgeBuild(until) {
22869
22992
  return null;
22870
22993
  };
22871
22994
  }
22995
+ /** What each target may be waited for. A job has one condition, finished, so it takes none. */
22996
+ const UNTIL_FOR = {
22997
+ initiative: ITEM_CONDITIONS,
22998
+ run: BUILD_CONDITIONS
22999
+ };
23000
+ const ALL_CONDITIONS = [.../* @__PURE__ */ new Set([...ITEM_CONDITIONS, ...BUILD_CONDITIONS])];
23001
+ /**
23002
+ * One tool for waiting (PRO-2941): three wait tools, one per thing waited on, were two more names in a catalogue
23003
+ * every review ranked too large. The ID says what is waited on; until says for what, refused when it does not
23004
+ * apply to that thing.
23005
+ */
22872
23006
  function registerWaitTools(server, client, options) {
22873
- if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_initiative", {
22874
- title: "Wait for an initiative",
22875
- description: "Wait until an initiative reaches a condition instead of polling: changed, planned, your_turn (waits on a person), pr_open (open now; merged or closed does not count), run_settled (latest run stopped moving) or done. Returns reached: true when it holds; early with stoppedBecause when only a person can move it (a flag, failed plan, plan or pull request awaiting review, a cancel); or reached: false at the timeout: call again. A condition already true answers at once. Planning takes minutes, a run up to an hour.",
22876
- inputSchema: z.object({
22877
- initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
22878
- until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
22879
- timeoutSeconds
22880
- }),
22881
- annotations: READ_ONLY
22882
- }, (params, ctx) => runTool(async () => {
23007
+ const items = client.hasCapability("backlog:read");
23008
+ const runs = client.hasCapability("products:read");
23009
+ if (!items && !runs) return;
23010
+ const waitInitiative = async (target, until, params, ctx) => {
22883
23011
  const gate = { product: void 0 };
22884
23012
  const read = async (signal) => {
22885
- const item = (await client.getBacklogItem(params.initiativeId, signal)).data;
23013
+ const item = (await client.getBacklogItem(target, signal)).data;
22886
23014
  if (gate.product === void 0 && typeof item.autoBuild !== "boolean") gate.product = client.hasCapability("products:read") ? await client.getProduct(String(item.productId), signal).then((p) => {
22887
23015
  const value = p.data.autoBuild;
22888
23016
  return typeof value === "boolean" ? value : true;
@@ -22892,48 +23020,31 @@ function registerWaitTools(server, client, options) {
22892
23020
  }) : null;
22893
23021
  return item;
22894
23022
  };
22895
- const { now, ...outcome } = await waitFor(read, judgeItem(params.until, gate), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23023
+ const { now, ...outcome } = await waitFor(read, judgeItem(until, gate), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
22896
23024
  progressToken: ctx.mcpReq._meta?.progressToken,
22897
23025
  notify: ctx.mcpReq.notify,
22898
23026
  signal: ctx.mcpReq.signal
22899
23027
  });
22900
23028
  return toolResult({
22901
- until: params.until,
23029
+ until,
22902
23030
  ...outcome,
22903
23031
  item: now ? itemReport(now) : null
22904
23032
  });
22905
- }));
22906
- if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_run", {
22907
- title: "Wait for a run",
22908
- description: "Wait until a run reaches a condition: changed, step_done (one more step finished), pr_open (open now) or settled (in review, completed, failed, stopped). Returns reached: true when it holds; early with stoppedBecause if the run settled without it; or reached: false at the timeout: call again. A settled run answers settled at once; run is null if the first read timed out.",
22909
- inputSchema: z.object({
22910
- runId: idInput("run", "taiga_list_runs gives it"),
22911
- until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
22912
- timeoutSeconds
22913
- }),
22914
- annotations: READ_ONLY
22915
- }, (params, ctx) => runTool(async () => {
22916
- const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.runId, signal)).data, judgeBuild(params.until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23033
+ };
23034
+ const waitRun = async (target, until, params, ctx) => {
23035
+ const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(target, signal)).data, judgeBuild(until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
22917
23036
  progressToken: ctx.mcpReq._meta?.progressToken,
22918
23037
  notify: ctx.mcpReq.notify,
22919
23038
  signal: ctx.mcpReq.signal
22920
23039
  });
22921
23040
  return toolResult({
22922
- until: params.until,
23041
+ until,
22923
23042
  ...outcome,
22924
23043
  run: now ? readableRow(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
22925
23044
  });
22926
- }));
22927
- if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
22928
- title: "Wait for an agent job",
22929
- description: "Wait until an agent job finishes (completed, failed or cancelled). Returns the job as taiga_get_job reads it, with an initiative request's outcome, and reached: true; or reached: false at the timeout: call again to keep waiting. Most jobs take a few minutes.",
22930
- inputSchema: z.object({
22931
- jobId: idInput("job", "the tool that started the job returned it"),
22932
- timeoutSeconds
22933
- }),
22934
- annotations: READ_ONLY
22935
- }, (params, ctx) => runTool(async () => {
22936
- const { now, ...outcome } = await waitFor(async (signal) => (await client.getAgentJob(params.jobId, signal)).data, (job) => SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null, (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
23045
+ };
23046
+ const waitJob = async (target, params, ctx) => {
23047
+ const { now, ...outcome } = await waitFor(async (signal) => (await client.getAgentJob(target, signal)).data, (job) => SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null, (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
22937
23048
  progressToken: ctx.mcpReq._meta?.progressToken,
22938
23049
  notify: ctx.mcpReq.notify,
22939
23050
  signal: ctx.mcpReq.signal
@@ -22942,6 +23053,38 @@ function registerWaitTools(server, client, options) {
22942
23053
  ...outcome,
22943
23054
  job: now ? jobReport(now) : null
22944
23055
  });
23056
+ };
23057
+ server.registerTool("taiga_wait", {
23058
+ title: "Wait for an initiative, run or job",
23059
+ description: "Wait instead of polling, for one of: an initiative (initiativeId) to reach until: changed, planned, your_turn (waits on a person), pr_open (open now; merged or closed does not count), run_settled (latest run stopped moving) or done; a run (runId) to reach until: changed, step_done, pr_open or settled (in review, completed, failed, stopped); or an agent job (jobId) to finish, which takes no until. Returns reached: true when it holds; early with stoppedBecause when it cannot happen without a person; or reached: false at the timeout: call again. A condition already true answers at once. Planning takes minutes, a run up to an hour, most jobs a few minutes.",
23060
+ inputSchema: z.object({
23061
+ initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional(),
23062
+ runId: idInput("run", "taiga_list_runs gives it").optional(),
23063
+ jobId: idInput("job", "the tool that started the job returned it").optional(),
23064
+ until: z.enum(ALL_CONDITIONS).optional().describe("What to wait for: an initiative and a run need one"),
23065
+ timeoutSeconds
23066
+ }),
23067
+ annotations: READ_ONLY
23068
+ }, (params, ctx) => runTool(async () => {
23069
+ if ([
23070
+ "initiativeId",
23071
+ "runId",
23072
+ "jobId"
23073
+ ].filter((key) => params[key] !== void 0).length !== 1) return toolError("VALIDATION_ERROR", "Name exactly one of initiativeId, runId or jobId: the thing to wait on.");
23074
+ if (params.jobId !== void 0) {
23075
+ if (params.until !== void 0) return toolError("VALIDATION_ERROR", "A job is waited on until it finishes: leave until out.");
23076
+ if (!runs) return toolError("FORBIDDEN", "Waiting on a job needs products:read, which this key lacks.");
23077
+ return waitJob(params.jobId, params, ctx);
23078
+ }
23079
+ const kind = params.initiativeId !== void 0 ? "initiative" : "run";
23080
+ const allowed = UNTIL_FOR[kind];
23081
+ if (params.until === void 0 || !allowed.includes(params.until)) return toolError("VALIDATION_ERROR", `Waiting on ${kind === "initiative" ? "an initiative" : "a run"} takes until: ${allowed.join(", ")}.`);
23082
+ if (kind === "initiative") {
23083
+ if (!items) return toolError("FORBIDDEN", "Waiting on an initiative needs backlog:read, which this key lacks.");
23084
+ return waitInitiative(params.initiativeId, params.until, params, ctx);
23085
+ }
23086
+ if (!runs) return toolError("FORBIDDEN", "Waiting on a run needs products:read, which this key lacks.");
23087
+ return waitRun(params.runId, params.until, params, ctx);
22945
23088
  }));
22946
23089
  }
22947
23090
  //#endregion
@@ -22999,6 +23142,12 @@ function refusedArguments(args) {
22999
23142
  }));
23000
23143
  return toolError("VALIDATION_ERROR", `Invalid arguments for ${tool}. ${lines.map((line) => line.argument ? `${line.argument}: ${line.message}` : line.message).join(" ")}`, { arguments: lines });
23001
23144
  }
23145
+ /** Argument names this release replaced, and what replaced them. */
23146
+ const RENAMED_ARGUMENTS = {
23147
+ repositoryId: "linkId",
23148
+ replacesRepositoryId: "replacesLinkId",
23149
+ productRepositoryId: "linkId"
23150
+ };
23002
23151
  /**
23003
23152
  * A refused call says what to do next (PRO-2841), as a 404 already does. The
23004
23153
  * SDK validates the arguments before the handler runs, where the 404 hints
@@ -23013,6 +23162,8 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
23013
23162
  const hinted = (issue) => {
23014
23163
  if (issue.code === "unrecognized_keys" && issue.keys?.length) {
23015
23164
  const pointers = issue.keys.map((key) => {
23165
+ const renamed = RENAMED_ARGUMENTS[key];
23166
+ if (renamed && (inputs.get(tool) ?? []).includes(renamed)) return `${tool} takes ${renamed}, which replaced ${key}.`;
23016
23167
  const own = ownArgumentLike(key, inputs.get(tool) ?? []);
23017
23168
  if (own) return `${tool} takes ${own}, spelled that way.`;
23018
23169
  const other = closestToolTaking(key, tool, inputs, isRead);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.26.0-rc.514",
3
+ "version": "0.26.0-rc.519",
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",