@taiga-ai/mcp-server 0.26.0-rc.480 → 0.26.0-rc.494

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 +438 -205
  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
  *
@@ -913,19 +921,15 @@ const INITIATIVE_STATES = [
913
921
  * for every reader that means "on the line": the flag the API clears when an
914
922
  * initiative leaves it, the remediation settle on the same departure, and the
915
923
  * MCP's account of where an initiative stands (PRO-2840). Everything else
916
- * (backlog, todo, done, canceled) is off it. The lifecycle derivation does not
917
- * read it: it derives the stage from the run alone (PRO-2828).
924
+ * (backlog, todo, done, canceled) is off it. The lifecycle derivation draws a
925
+ * narrower line (PRO-2828): a run that ended is the initiative's state only at
926
+ * a station, Plan or Build; in Queue it is history, as it is off the line.
918
927
  */
919
928
  const LINE_STATES = [
920
929
  "queue",
921
930
  "plan",
922
931
  "build"
923
932
  ];
924
- /** Finished work: settled by a person or a merge, or archived. The one
925
- * predicate every "can this still move / plan / build" guard reads. */
926
- function isSettledOrArchived(item) {
927
- return item.archivedAt != null || SETTLED_BACKLOG_STATUSES.includes(item.status);
928
- }
929
933
  /**
930
934
  * The columns a person can drop an initiative into by hand.
931
935
  *
@@ -1348,7 +1352,8 @@ const READY_BLOCKERS = [
1348
1352
  * - `ci_fix_stalled`: a fix round on the failing head pushed nothing;
1349
1353
  * - `conflict_cap`: the branch still conflicts with its base at the cap;
1350
1354
  * - `ready_blocked`: CI is green, but the description says why it is a draft;
1351
- * - `check_weakened`: CI went green only after a fix loosened a check;
1355
+ * - `check_weakened`: CI went green only after a fix changed a CI workflow or
1356
+ * deleted a test, which may have weakened a check;
1352
1357
  * - `review_bot_cap`: a review BOT (Copilot, CodeRabbit) kept reviewing after
1353
1358
  * the automatic rounds ran out (PRO-2480). A bot's review never resets the
1354
1359
  * streak a person's does, so the agent and the bot cannot loop without end.
@@ -1384,6 +1389,49 @@ z.object({
1384
1389
  ...e.failingChecks === void 0 ? {} : { failingChecks: e.failingChecks },
1385
1390
  ...e.blockers === void 0 ? {} : { blockers: e.blockers }
1386
1391
  }));
1392
+ /**
1393
+ * What a run's hardening came to, coverage included (PRO-2829). `passed`
1394
+ * counted only blocking findings, so a run whose scanners never ran said
1395
+ * "passed" all the same.
1396
+ *
1397
+ * - `failed`: blocking findings remain (the gaps are still listed).
1398
+ * - `not_scanned`: no scanner that applies to the repository ran to the end.
1399
+ * - `passed_with_gaps`: nothing blocks, but some scanner did not run.
1400
+ * - `passed`: nothing blocks and every scanner that applies ran.
1401
+ */
1402
+ const HARDENING_VERDICTS = [
1403
+ "passed",
1404
+ "passed_with_gaps",
1405
+ "not_scanned",
1406
+ "failed"
1407
+ ];
1408
+ /**
1409
+ * The verdict and the gaps of a hardening result (PRO-2829), for every reader
1410
+ * (the run read, the web's run page, the MCP), from what the agent reported.
1411
+ *
1412
+ * A gap is a scanner that applies and did not run: `not_run` (the scan never
1413
+ * reached it), `skipped` (its tool was missing), or `failed` with a reason,
1414
+ * which is a scanner that could not finish. A `failed` scanner without one
1415
+ * found something (tflint fails on a MAJOR that blocks nothing, and rows from
1416
+ * before `scannerReasons` used `failed` for "had findings"), so it ran and is
1417
+ * no gap. A scanner listed in `notApplicable` is no gap; on a result from
1418
+ * before that list, a skipped tflint is read the same way, as the only reason
1419
+ * the agent ever skipped it was a repository with no Terraform.
1420
+ */
1421
+ function hardeningCoverage(result) {
1422
+ const notApplicable = new Set(result.notApplicable ?? []);
1423
+ const applies = Object.entries(result.scanners ?? {}).filter(([scanner, status]) => !notApplicable.has(scanner) && !(result.notApplicable === void 0 && scanner === "tflint" && status === "skipped"));
1424
+ const reasons = result.scannerReasons ?? {};
1425
+ const gaps = applies.filter(([scanner, status]) => status === "not_run" || status === "skipped" || status === "failed" && scanner in reasons).map(([scanner, status]) => ({
1426
+ scanner,
1427
+ status,
1428
+ reason: reasons[scanner] ?? null
1429
+ }));
1430
+ return {
1431
+ verdict: !result.passed ? "failed" : gaps.length === applies.length ? "not_scanned" : gaps.length > 0 ? "passed_with_gaps" : "passed",
1432
+ gaps
1433
+ };
1434
+ }
1387
1435
  [...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
1388
1436
  //#endregion
1389
1437
  //#region ../../packages/types/dist/agents.js
@@ -1763,6 +1811,92 @@ const MAINTENANCE_IGNORE_REASONS = [
1763
1811
  "accepted_risk",
1764
1812
  "wont_fix"
1765
1813
  ];
1814
+ /**
1815
+ * What one lockfile's group list says, read against what its osv-scanner
1816
+ * (osv-scalibr) extractor actually writes. Checked per extractor, because the
1817
+ * same list means different things in different files:
1818
+ *
1819
+ * - package-lock.json / npm-shrinkwrap.json: every package entry carries npm's
1820
+ * `dev` / `optional` / `devOptional` flags, transitive ones included, so an
1821
+ * EMPTY list is a runtime dependency, `dev` alone is dev-only, `optional`
1822
+ * alone is shipped, and `dev` + `optional` (devOptional) is left undecided.
1823
+ * - pnpm-lock.yaml: `dev` comes from a per-package `dev: true` that lockfile
1824
+ * v9 no longer writes, so an empty list says nothing; `dev` still means dev.
1825
+ * For a package a workspace declares under `dependencies` or
1826
+ * `optionalDependencies`, the sweep adds Taiga's own `prod`, so that copy
1827
+ * reads runtime. A `devDependencies` declaration is not turned into `dev`:
1828
+ * a runtime dependency's snapshot may pull the same copy in, which only the
1829
+ * `snapshots` graph could rule out, so it stays unknown.
1830
+ * - poetry.lock: group names with `main` dropped; a non-main, non-optional
1831
+ * group is not in the runtime install. Poetry 1.x lockfiles carry no groups
1832
+ * at all, so an empty list says nothing.
1833
+ * - uv.lock: only direct members of a group are tagged, so an empty list says
1834
+ * nothing; `dev` means dev.
1835
+ * - yarn.lock, go.mod/go.sum, Cargo.lock, Gemfile.lock and the rest carry no
1836
+ * group information: unknown whatever the list says.
1837
+ *
1838
+ * Anything this cannot place is `unknown`, never `runtime`: a wrong "runtime"
1839
+ * on a build tool is the claim this exists to stop, and a wrong "dev only" on a
1840
+ * shipped package is worse.
1841
+ */
1842
+ function lockfileScope(path, groups) {
1843
+ const name = path.slice(path.lastIndexOf("/") + 1);
1844
+ const set = new Set(groups.map((g) => g.trim().toLowerCase()).filter((g) => g !== ""));
1845
+ switch (name) {
1846
+ case "package-lock.json":
1847
+ case "npm-shrinkwrap.json": {
1848
+ if (set.size === 0) return "runtime";
1849
+ const dev = set.has("dev");
1850
+ const optional = set.has("optional");
1851
+ if (dev && optional) return "unknown";
1852
+ if (dev) return "dev";
1853
+ if (optional && set.size === 1) return "runtime";
1854
+ return "unknown";
1855
+ }
1856
+ case "pnpm-lock.yaml":
1857
+ if (set.has("prod")) return "runtime";
1858
+ return set.size === 1 && set.has("dev") ? "dev" : "unknown";
1859
+ case "uv.lock": return set.size === 1 && set.has("dev") ? "dev" : "unknown";
1860
+ case "poetry.lock": return set.size > 0 && !set.has("optional") && !set.has("main") ? "dev" : "unknown";
1861
+ default: return "unknown";
1862
+ }
1863
+ }
1864
+ /**
1865
+ * Whether a dependency finding sits only in development dependencies, in at
1866
+ * least one runtime dependency set, or in lockfiles that do not say. Decided
1867
+ * per lockfile (`lockfileScope`) and combined pessimistically: one runtime
1868
+ * lockfile makes the finding runtime, one lockfile that cannot say makes it
1869
+ * unknown, and only all-dev reads as dev. Groups the sweep did not record
1870
+ * (older images, code kinds) are unknown. Presentation only: the tier and the
1871
+ * auto-remediation gate do not read this, and a malicious package runs its
1872
+ * install script in any group.
1873
+ */
1874
+ function dependencyScope(finding) {
1875
+ if (finding.kind !== "vulnerable_dependency" && finding.kind !== "outdated_dependency") return "unknown";
1876
+ const entries = Object.entries(finding.dependencyGroups ?? {});
1877
+ if (entries.length === 0) return "unknown";
1878
+ return combineScopes(entries.map(([path, groups]) => lockfileScope(path, Array.isArray(groups) ? groups : [])));
1879
+ }
1880
+ function combineScopes(scopes) {
1881
+ if (scopes.includes("runtime")) return "runtime";
1882
+ if (scopes.includes("unknown") || scopes.length === 0) return "unknown";
1883
+ return "dev";
1884
+ }
1885
+ /**
1886
+ * Whether the repository declares the package itself. Per lockfile the sweep
1887
+ * says direct or transitive; across lockfiles one direct declaration makes the
1888
+ * finding direct (that copy is the repository's to bump), transitive only when
1889
+ * every readable lockfile says so, unknown when the sweep could read none of
1890
+ * them or did not record it (older images, code kinds). Presentation only; the
1891
+ * remediation step already finds every resolved copy and its parent itself.
1892
+ */
1893
+ function dependencyRelation(finding) {
1894
+ if (finding.kind !== "vulnerable_dependency" && finding.kind !== "outdated_dependency") return "unknown";
1895
+ const values = Object.values(finding.dependencyRelation ?? {});
1896
+ if (values.includes("direct")) return "direct";
1897
+ if (values.length > 0 && values.every((v) => v === "transitive")) return "transitive";
1898
+ return "unknown";
1899
+ }
1766
1900
  new Set(CODE_REMEDIATION_KINDS);
1767
1901
  /** A non-empty string, or null. */
1768
1902
  const locationText = z.string().min(1).nullable().catch(null);
@@ -2115,7 +2249,7 @@ const DOCUMENT_INPUTS = Object.freeze({
2115
2249
  * reads as deleted) and fields they explain would simply be missing. So it
2116
2250
  * refuses to start, and says what to do.
2117
2251
  */
2118
- function apiLevelProblem(introspection, apiUrl, required = 1, version = SERVER_VERSION) {
2252
+ function apiLevelProblem(introspection, apiUrl, required = 2, version = SERVER_VERSION) {
2119
2253
  const offered = introspection.mcpApiLevel ?? 0;
2120
2254
  if (offered >= required) return null;
2121
2255
  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.`;
@@ -2372,7 +2506,7 @@ const BY_ARGUMENT = {
2372
2506
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2373
2507
  const BY_TOOL = {
2374
2508
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2375
- 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
2509
+ 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
2376
2510
  };
2377
2511
  /**
2378
2512
  * What the API calls the thing each ID names, as its 404 says it ("Environment
@@ -2510,11 +2644,27 @@ function inProductWords(value) {
2510
2644
  function inProductWording(said) {
2511
2645
  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).");
2512
2646
  }
2647
+ /**
2648
+ * A float without its binary noise: six significant digits. Delivery metrics
2649
+ * came as `deploysPerWeek: 6.766666666666667`, sixteen digits nobody reads
2650
+ * (PRO-2847). Significant digits rather than fixed decimals: an EPSS score of
2651
+ * 0.00043, a 0.4% failure rate or 99.995% uptime keep what they say, where
2652
+ * two decimals read them as 0, 0 and 100.
2653
+ */
2654
+ const roundFloats = (_key, value) => typeof value === "number" && Number.isFinite(value) && !Number.isInteger(value) ? Number(value.toPrecision(6)) : value;
2655
+ /**
2656
+ * A result as the model reads it: compact JSON, in the product's words, its
2657
+ * floats rounded. Indentation was a fifth of a list's characters (PRO-2847),
2658
+ * paid on every call; the model reads JSON without it.
2659
+ */
2660
+ function serializeResult(value) {
2661
+ return JSON.stringify(inProductWords(value), roundFloats);
2662
+ }
2513
2663
  /** A successful tool result: one JSON text block the model reads. */
2514
2664
  function toolResult(value) {
2515
2665
  return { content: [{
2516
2666
  type: "text",
2517
- text: JSON.stringify(inProductWords(value), null, 2)
2667
+ text: serializeResult(value)
2518
2668
  }] };
2519
2669
  }
2520
2670
  /**
@@ -3785,14 +3935,14 @@ function handbackSaid(escalation) {
3785
3935
  case "review_bot_cap": return `review bots still have open comments${rounds}`;
3786
3936
  case "ci_never_settled": return "CI did not finish in time";
3787
3937
  case "ready_blocked": return `CI is green, but Taiga kept the pull request a draft${blockers.length ? `: ${blockers.join("; ")}` : ""}`;
3788
- case "check_weakened": return `CI went green only after a check was loosened${typeof e.file === "string" ? ` in ${e.file}` : ""}`;
3938
+ case "check_weakened": return `CI went green only after a fix changed ${typeof e.file === "string" ? e.file : "a CI workflow or a test"}`;
3789
3939
  default: return typeof e.reason === "string" ? e.reason : null;
3790
3940
  }
3791
3941
  }
3792
3942
  /** What a person does about a handback, by its reason, as the web's handback detail says it. */
3793
3943
  function handbackNextStep(escalation) {
3794
3944
  const reason = escalation && typeof escalation === "object" ? escalation.reason : null;
3795
- if (reason === "check_weakened") return "review the loosened check on GitHub before merging";
3945
+ if (reason === "check_weakened") return "review that change on GitHub before merging";
3796
3946
  if (reason === "ci_never_settled") return "look at what is still running or queued in CI on GitHub, then review the pull request";
3797
3947
  return "fix it by hand, or review the pull request on GitHub, which hands it back to the agent";
3798
3948
  }
@@ -3855,6 +4005,34 @@ function hardeningSaid(blocking, named) {
3855
4005
  const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
3856
4006
  return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
3857
4007
  }
4008
+ /** How each hardening verdict reads (PRO-2829): "passed" only when every scanner that applies ran. */
4009
+ const VERDICT_SAID = {
4010
+ passed: "Hardening passed: every scanner that applies ran and nothing blocks",
4011
+ passed_with_gaps: "Hardening passed, but not every scanner ran, so its checks are missing from the result",
4012
+ not_scanned: "No security scanner ran in hardening: \"passed\" only means no blocking finding was recorded",
4013
+ failed: "Hardening failed: blocking findings remain"
4014
+ };
4015
+ /**
4016
+ * What a run's hardening came to, coverage included (PRO-2829), from the
4017
+ * verdict the API reads off it, or read here from an API from before that.
4018
+ * Null without a hardening result.
4019
+ */
4020
+ function scanSaid(hardening) {
4021
+ if (!hardening || typeof hardening !== "object") return null;
4022
+ const h = hardening;
4023
+ const coverage = typeof h.verdict === "string" && HARDENING_VERDICTS.includes(h.verdict) && Array.isArray(h.gaps) ? {
4024
+ verdict: h.verdict,
4025
+ gaps: h.gaps
4026
+ } : typeof h.passed === "boolean" && h.scanners ? hardeningCoverage(h) : null;
4027
+ if (!coverage) return null;
4028
+ const gaps = coverage.gaps.map((gap) => `${gap.scanner}${gap.status === "failed" ? " did not finish" : ""}${gap.reason ? ` (${gap.reason})` : ""}`);
4029
+ const missing = gaps.length > 0 ? `; did not run: ${gaps.join(", ")}` : "";
4030
+ return {
4031
+ verdict: coverage.verdict,
4032
+ gaps: gaps.length,
4033
+ said: `${VERDICT_SAID[coverage.verdict]}${missing}.`
4034
+ };
4035
+ }
3858
4036
  /**
3859
4037
  * What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
3860
4038
  * bare enums. As @taiga/types documents them.
@@ -3875,20 +4053,23 @@ const FAILED_REASON_MEANS = {
3875
4053
  * text out: its first sentence is in the outcome, and taiga_get_run has it all.
3876
4054
  */
3877
4055
  function readableRun(run, opts = {}) {
3878
- const { prDraft, error, reviewEscalation, runtime: _runtime, ...rest } = run;
4056
+ const { prDraft, error, reviewEscalation, runtime: _runtime, origin, ...rest } = run;
3879
4057
  const said = runOutcome(run);
3880
4058
  const means = FAILED_REASON_MEANS[String(run["failedReason"])];
3881
4059
  const hardening = run["hardeningResult"];
3882
4060
  const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
3883
4061
  const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
4062
+ const scan = scanSaid(hardening);
3884
4063
  return {
3885
4064
  ...rest,
4065
+ ...origin === void 0 ? {} : { origin: origin === "backlog" ? "initiative" : origin },
3886
4066
  ...means ? { failedReasonMeans: means } : {},
4067
+ ...scan && scan.gaps + (scan.verdict === "not_scanned" ? 1 : 0) > 0 ? { scanCoverage: scan.said } : {},
3887
4068
  ...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
3888
4069
  ...opts.row || error === void 0 ? {} : { error },
3889
4070
  ...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
3890
4071
  ...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
3891
- ...said ?? {}
4072
+ ...said ? { outcome: said.outcome } : {}
3892
4073
  };
3893
4074
  }
3894
4075
  /** Distinct initiatives a run list reads to say which moved on; past this a waiting row says it was not checked. */
@@ -3940,7 +4121,8 @@ const INITIATIVE_ENDED = {
3940
4121
  /** A readable run row that also says when its initiative ended while its pull request waited. */
3941
4122
  function withInitiativeState(row, states) {
3942
4123
  const state = states.get(String(row["backlogItemId"]));
3943
- if (!["gated", "unmerged"].includes(String(row["disposition"]))) return row;
4124
+ const status = String(row["status"]);
4125
+ if (!(BUILD_RUN_STATUSES.includes(status) && ["gated", "unmerged"].includes(landedDisposition(status, row["prMerged"] === true)))) return row;
3944
4126
  if (state === "unchecked") return {
3945
4127
  ...row,
3946
4128
  outcome: `${String(row["outcome"])} Whether its initiative still wants it was not checked.`
@@ -3951,7 +4133,6 @@ function withInitiativeState(row, states) {
3951
4133
  const open = row["prState"] === "open" ? ` ${pr} is still open on the forge.` : "";
3952
4134
  return {
3953
4135
  ...row,
3954
- disposition: "abandoned",
3955
4136
  actions: [],
3956
4137
  outcome: `Its initiative ${ended}, so nobody is waiting on this run.${open}`
3957
4138
  };
@@ -4060,19 +4241,20 @@ function registerBuildTools(server, client) {
4060
4241
  if (!client.hasCapability("products:read")) return;
4061
4242
  server.registerTool("taiga_list_runs", {
4062
4243
  title: "List runs",
4063
- 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).",
4244
+ 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).",
4064
4245
  inputSchema: z.object({
4065
4246
  productId: productId.optional(),
4066
- 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"),
4247
+ 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"),
4067
4248
  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"),
4068
4249
  ...pagination
4069
4250
  }),
4070
4251
  annotations: READ_ONLY
4071
4252
  }, (params) => runTool(async () => {
4072
- 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).");
4073
- 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.");
4074
- if (params.productId) {
4075
- const result = await client.listBuildRuns(params.productId, {
4253
+ 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.");
4254
+ 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.");
4255
+ const productIdOf = params.productId ?? (params.initiativeId ? (await client.getBacklogItem(params.initiativeId)).data.productId : void 0);
4256
+ if (productIdOf) {
4257
+ const result = await client.listBuildRuns(productIdOf, {
4076
4258
  backlogItemId: params.initiativeId,
4077
4259
  limit: params.limit,
4078
4260
  cursor: params.cursor
@@ -4085,7 +4267,7 @@ function registerBuildTools(server, client) {
4085
4267
  }));
4086
4268
  server.registerTool("taiga_get_run", {
4087
4269
  title: "Get run",
4088
- description: "Read one run with its steps: outcome, disposition, pull request, hardening, 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.",
4270
+ 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.",
4089
4271
  inputSchema: z.object({
4090
4272
  runId,
4091
4273
  full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
@@ -4212,9 +4394,7 @@ async function queueView(client, product, options = { name: "all" }) {
4212
4394
  if (item) {
4213
4395
  Object.assign(row, {
4214
4396
  title: item.title,
4215
- state: item.state,
4216
- planningStatus: item.planningStatus,
4217
- needsHumanCause: item.needsHumanCause,
4397
+ ...standing(item),
4218
4398
  criteria: criteriaCounts(item),
4219
4399
  lifecycle: lifecycle ? {
4220
4400
  turn: lifecycle.turn,
@@ -4246,7 +4426,7 @@ async function queueView(client, product, options = { name: "all" }) {
4246
4426
  const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
4247
4427
  return {
4248
4428
  linePaused: paused === null ? null : typeof paused === "string",
4249
- entries: rows,
4429
+ entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
4250
4430
  waitingOn: line ?? waitingOn(rows),
4251
4431
  ...note ? { note } : {}
4252
4432
  };
@@ -4292,18 +4472,21 @@ function registerQueueTools(server, client) {
4292
4472
  }
4293
4473
  //#endregion
4294
4474
  //#region src/tools/backlogs.ts
4295
- /** What a list row needs to say for the model to pick an item to read in full. */
4475
+ /**
4476
+ * What a list row needs to say for the model to pick an item to read in full.
4477
+ * One vocabulary for where an initiative stands (PRO-2828): its board column,
4478
+ * whose turn it is and the situation sentence. The API's own fields for it
4479
+ * (status, planningStatus, needsHumanCause, lifecycle) said the same thing
4480
+ * four more ways, and a reviewing agent had to reconcile them; they are in
4481
+ * taiga_get_initiative with full: true.
4482
+ */
4296
4483
  const BACKLOG_SUMMARY_FIELDS = [
4297
4484
  "id",
4298
4485
  "title",
4299
4486
  "category",
4300
- "status",
4301
- "state",
4302
- "planningStatus",
4303
4487
  "priority",
4304
4488
  "estimate",
4305
4489
  "origin",
4306
- "needsHumanCause",
4307
4490
  "archivedAt",
4308
4491
  "createdAt",
4309
4492
  "updatedAt"
@@ -4317,21 +4500,44 @@ const ITEM_OUTCOME_FIELDS = [
4317
4500
  "id",
4318
4501
  "productId",
4319
4502
  "title",
4320
- "status",
4321
- "state",
4322
- "planningStatus",
4323
- "needsHumanCause",
4324
- "needsHumanDetail",
4325
4503
  "autoBuild",
4326
- "archivedAt",
4327
- "lifecycle"
4504
+ "archivedAt"
4328
4505
  ];
4506
+ /** The fields of an initiative's summary read: its row, plus what it is about. */
4507
+ const ITEM_SUMMARY_FIELDS = [
4508
+ ...BACKLOG_SUMMARY_FIELDS,
4509
+ "productId",
4510
+ "description",
4511
+ "autoBuild",
4512
+ "dependencies"
4513
+ ];
4514
+ /** How much of a description a summary read carries; full: true has the rest. */
4515
+ const DESCRIPTION_IN_SUMMARY = 600;
4516
+ /**
4517
+ * Where an initiative stands, in the one vocabulary every read uses
4518
+ * (PRO-2828): its board column, whose turn it is, the situation sentence, and
4519
+ * its latest run to open with taiga_get_run.
4520
+ */
4521
+ function standing(item) {
4522
+ const life = item["lifecycle"];
4523
+ const run = life?.latestBuild;
4524
+ const situation = situationOf(item);
4525
+ return {
4526
+ column: typeof item["state"] === "string" ? item["state"] : null,
4527
+ turn: typeof life?.turn === "string" ? life.turn : null,
4528
+ ...situation ? { situation } : {},
4529
+ latestRun: run && typeof run.id === "string" ? {
4530
+ id: run.id,
4531
+ ...typeof run.prNumber === "number" ? { prNumber: run.prNumber } : {}
4532
+ } : null
4533
+ };
4534
+ }
4329
4535
  /**
4330
4536
  * What the lifecycle on an initiative says, and the call each next step is.
4331
4537
  * needsHumanCause is only the line's flag; a plan that failed outside the line
4332
4538
  * leaves it null, and the lifecycle is what says why and what moves it on.
4333
4539
  */
4334
- 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.";
4540
+ 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).";
4335
4541
  /** The move each call to action is, in the tools' words. */
4336
4542
  const CTA_MOVE = {
4337
4543
  start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
@@ -4340,12 +4546,12 @@ const CTA_MOVE = {
4340
4546
  resume_build: "taiga_resume_initiative carries the run on (paid work: ask first)",
4341
4547
  retry_build: "a new run starts over from the web; taiga_move_initiative takes it off the line",
4342
4548
  review_pr: "a person reviews and merges the pull request",
4343
- view_pr: "the line merges the pull request once its checks pass"
4549
+ view_pr: "the agent waits for its checks and fixes what fails; it merges on its own only with auto-merge on"
4344
4550
  };
4345
4551
  /** The move for an initiative off the line whose last run did not land: there is no line to take it off (PRO-2840). */
4346
4552
  const OFF_THE_LINE_MOVE = "taiga_queue_initiative puts it back on the line (paid work: ask first)";
4347
- /** What happens to a queued initiative whose last run did not land: the line plans it again (PRO-2840). */
4348
- const REQUEUED_MOVE = "the line plans it again when its turn comes";
4553
+ /** What happens to a queued initiative: the line takes it up when its turn comes, replanning a run that did not land (PRO-2840). */
4554
+ const REQUEUED_MOVE = "the line takes it up when its turn comes";
4349
4555
  /** How a run stands, as the end of "its latest run ...", by disposition. */
4350
4556
  const RUN_STANDS = {
4351
4557
  in_flight: "is running",
@@ -4382,7 +4588,10 @@ function situationOf(item) {
4382
4588
  const column = typeof item["state"] === "string" ? item["state"] : null;
4383
4589
  const offTheLine = column !== null && !LINE_STATES.includes(column);
4384
4590
  const stalled = life.stage === "building" && life.turn !== "agent";
4385
- const requeued = stalled && column === "queue";
4591
+ const requeued = column === "queue" && (stalled || life.turn === "idle" && life.cta == null);
4592
+ const offTheLineStart = offTheLine && (life.cta === "start_build" || life.cta === "start_plan");
4593
+ if (requeued && !stalled) stage = life.stage === "backlog" ? "Queued" : `Queued, ${stage.charAt(0).toLowerCase()}${stage.slice(1)}`;
4594
+ else if (offTheLineStart && life.stage === "planned") stage = "Planned, off the line";
4386
4595
  if (stalled) stage = requeued ? "Queued" : offTheLine ? "Off the line" : column === null ? "Its build stopped" : "On the line, but its build stopped";
4387
4596
  const build = life.latestBuild;
4388
4597
  if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
@@ -4403,7 +4612,7 @@ function situationOf(item) {
4403
4612
  }
4404
4613
  const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
4405
4614
  const handedBack = handbackSaid(build?.reviewEscalation);
4406
- 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;
4615
+ 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;
4407
4616
  if (requeued) parts.push(`Nothing to do: ${REQUEUED_MOVE}.`);
4408
4617
  else if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
4409
4618
  else if (turn && move) parts.push(`${turn}: ${move}.`);
@@ -4431,15 +4640,14 @@ function endingSaid(item) {
4431
4640
  if (next && typeof next.id === "string") said.push(`; superseded by "${String(next.title)}" (${next.id})`);
4432
4641
  return said.join("");
4433
4642
  }
4434
- /** A row or item with its situation added when it has a lifecycle. */
4643
+ /** A row or item with where it stands added (`standing`). */
4435
4644
  function withSituation(item, row = item) {
4436
- const situation = situationOf(item);
4437
- return situation ? {
4645
+ return {
4438
4646
  ...row,
4439
- situation
4440
- } : row;
4647
+ ...standing(item)
4648
+ };
4441
4649
  }
4442
- /** List rows, each with its situation. */
4650
+ /** List rows, each with where it stands. */
4443
4651
  function situatedRows(items) {
4444
4652
  return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index], row));
4445
4653
  }
@@ -4516,40 +4724,6 @@ const outcomeOf = (item) => withSituation(item, {
4516
4724
  criteria: criteriaCounts(item)
4517
4725
  });
4518
4726
  const outcome = (item) => toolResult(outcomeOf(item));
4519
- /** The statuses an initiative in each board column can have: the API keeps in_progress after a first run. */
4520
- const COLUMN_STATUSES = {
4521
- backlog: [
4522
- "all",
4523
- "open",
4524
- "backlog",
4525
- "in_progress"
4526
- ],
4527
- todo: [
4528
- "all",
4529
- "open",
4530
- "backlog",
4531
- "in_progress"
4532
- ],
4533
- queue: [
4534
- "all",
4535
- "open",
4536
- "backlog",
4537
- "in_progress"
4538
- ],
4539
- plan: [
4540
- "all",
4541
- "open",
4542
- "backlog",
4543
- "in_progress"
4544
- ],
4545
- build: [
4546
- "all",
4547
- "open",
4548
- "in_progress"
4549
- ],
4550
- done: ["all", "done"],
4551
- canceled: ["all", "canceled"]
4552
- };
4553
4727
  /**
4554
4728
  * Each tool is registered under the capability its route requires, so a
4555
4729
  * key sees exactly what it may call: reads on backlog:read; the board move on
@@ -4582,25 +4756,15 @@ function registerBacklogTools(server, client) {
4582
4756
  function registerBacklogReads(server, client) {
4583
4757
  server.registerTool("taiga_list_initiatives", {
4584
4758
  title: "List initiatives",
4585
- 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.",
4759
+ 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.",
4586
4760
  inputSchema: z.object({
4587
4761
  productId,
4588
4762
  query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
4589
- status: z.enum([
4763
+ column: z.enum([
4590
4764
  "open",
4591
4765
  "all",
4592
- "backlog",
4593
- "in_progress",
4594
- "done",
4595
- "canceled"
4596
- ]).optional().describe("open (default without state): not done or canceled; all: every one; in_progress covers everything between backlog and done"),
4597
- planningStatus: z.enum([
4598
- "unplanned",
4599
- "planning",
4600
- "planned",
4601
- "failed"
4602
- ]).optional().describe("Only items whose plan is in this state"),
4603
- 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)"),
4766
+ ...INITIATIVE_STATES
4767
+ ]).optional().describe("open (default): not done or canceled; all: every one; or one column of the board"),
4604
4768
  category: z.enum([
4605
4769
  "feature",
4606
4770
  "bug",
@@ -4612,56 +4776,66 @@ function registerBacklogReads(server, client) {
4612
4776
  }),
4613
4777
  annotations: READ_ONLY
4614
4778
  }, (params) => runTool(async () => {
4615
- const { query, state, status = state === void 0 ? "open" : "all", ...filters } = params;
4616
- 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(", ")}).`);
4779
+ const { query, column = "open", productId: id, ...filters } = params;
4617
4780
  const apiFilters = {
4618
4781
  ...filters,
4619
- status: status === "open" || status === "all" ? void 0 : status
4782
+ order: "newest",
4783
+ ...column === "open" ? { open: true } : column === "all" ? {} : { state: column }
4620
4784
  };
4621
- const open = status === "open";
4622
- if (query === void 0 && state === void 0 && !open) {
4623
- const result = await client.listBacklogItems(params.productId, apiFilters);
4785
+ if (query === void 0) {
4786
+ const result = await client.listBacklogItems(id, apiFilters);
4624
4787
  return toolResult(page(situatedRows(result.data), result.nextCursor));
4625
4788
  }
4626
- 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.");
4789
+ 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.");
4627
4790
  let closedSeen = 0;
4628
- const needle = (query ?? "").toLowerCase();
4629
- const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
4791
+ const needle = query.toLowerCase();
4792
+ const found = await searchPages((cursor) => client.listBacklogItems(id, {
4630
4793
  ...apiFilters,
4794
+ ...column === "open" ? { open: void 0 } : {},
4631
4795
  limit: 100,
4632
4796
  cursor
4633
- }), query ?? "", (row) => [row.title], params.limit ?? 25, (row) => {
4634
- if (state !== void 0 && row.state !== state) return false;
4635
- if (!open || !["done", "canceled"].includes(row.status)) return true;
4636
- if (query !== void 0 && String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
4797
+ }), query, (row) => [row.title], params.limit ?? 25, (row) => {
4798
+ if (column !== "open" || !["done", "canceled"].includes(String(row.state))) return true;
4799
+ if (String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
4637
4800
  return false;
4638
4801
  });
4639
- const hidden = query !== void 0 && state === void 0 && !found.more ? closedSeen : 0;
4802
+ const hidden = column === "open" && !found.more ? closedSeen : 0;
4640
4803
  return toolResult({
4641
4804
  ...page(situatedRows(found.rows), null),
4642
- ...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"}; status: 'all' lists them.` } : {},
4643
- ...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." } : {}
4805
+ ...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"}; column: 'all' lists them.` } : {},
4806
+ ...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {}
4644
4807
  });
4645
4808
  }));
4646
4809
  server.registerTool("taiga_get_initiative", {
4647
4810
  title: "Get initiative",
4648
- 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}`,
4649
- inputSchema: z.object({ initiativeId }),
4811
+ 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}`,
4812
+ inputSchema: z.object({
4813
+ initiativeId,
4814
+ full: z.boolean().optional().describe("Read it whole: every criterion, technical context and the raw state fields (default false)")
4815
+ }),
4650
4816
  annotations: READ_ONLY
4651
4817
  }, (params) => runTool(async () => {
4652
4818
  const item = (await client.getBacklogItem(params.initiativeId)).data;
4653
4819
  const row = item;
4654
- if (item.planningStatus !== "failed") return toolResult(withSituation(row));
4655
- const ownCause = item.lifecycle?.planFailureCause;
4656
- const planFailure = await planFailureOf(client, params.initiativeId, ownCause);
4657
- return toolResult(withSituation(row, planFailure ? {
4658
- ...item,
4659
- planFailure
4660
- } : item));
4820
+ const planFailure = item.planningStatus === "failed" ? await planFailureOf(client, params.initiativeId, item.lifecycle?.planFailureCause) : null;
4821
+ const failure = planFailure ? { planFailure } : {};
4822
+ if (params.full) return toolResult({
4823
+ ...withSituation(row),
4824
+ ...failure
4825
+ });
4826
+ const summary = itemRows([item], ITEM_SUMMARY_FIELDS)[0];
4827
+ const description = String(summary["description"] ?? "");
4828
+ return toolResult({
4829
+ ...withSituation(row, {
4830
+ ...summary,
4831
+ description: description.length > DESCRIPTION_IN_SUMMARY ? `${description.slice(0, DESCRIPTION_IN_SUMMARY).trimEnd()}... (full: true has the rest)` : description
4832
+ }),
4833
+ ...failure
4834
+ });
4661
4835
  }));
4662
4836
  server.registerTool("taiga_get_plan", {
4663
4837
  title: "Get the active plan",
4664
- 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.",
4838
+ 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.",
4665
4839
  inputSchema: z.object({
4666
4840
  initiativeId,
4667
4841
  full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
@@ -4824,7 +4998,7 @@ function registerReviewGate(server, client) {
4824
4998
  function registerResume(server, client) {
4825
4999
  server.registerTool("taiga_resume_initiative", {
4826
5000
  title: "Resume initiative",
4827
- 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.",
5001
+ 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.",
4828
5002
  inputSchema: z.object({ initiativeId }),
4829
5003
  annotations: OPERATE
4830
5004
  }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
@@ -4892,6 +5066,40 @@ function withFixability(row) {
4892
5066
  };
4893
5067
  return row;
4894
5068
  }
5069
+ /**
5070
+ * Dev-only, runtime or unknown, said on a dependency row (PRO-2861) in place of the raw per-lockfile groups
5071
+ * the API carries: the groups need the lockfile name to read, which `dependencyScope` does once here.
5072
+ */
5073
+ function withDependencyScope(row, { keepGroups = false } = {}) {
5074
+ const { dependencyGroups: _groups, dependencyRelation: _relation, ...lean } = row;
5075
+ const rest = keepGroups ? {
5076
+ ...row,
5077
+ dependencyRelation: void 0
5078
+ } : lean;
5079
+ if (!(row["kind"] === "vulnerable_dependency" || row["kind"] === "outdated_dependency")) return rest;
5080
+ const kind = row["kind"];
5081
+ return {
5082
+ ...rest,
5083
+ dependencyScope: dependencyScope({
5084
+ kind,
5085
+ dependencyGroups: row["dependencyGroups"]
5086
+ }),
5087
+ dependencyRelation: dependencyRelation({
5088
+ kind,
5089
+ dependencyRelation: row["dependencyRelation"]
5090
+ }),
5091
+ ...keepGroups ? { dependencyRelationByLockfile: row["dependencyRelation"] ?? null } : {}
5092
+ };
5093
+ }
5094
+ /** A finding as a tool answers it: the row fields, the scope, and whether Fix would take it. */
5095
+ function findingRows(findings, extra = []) {
5096
+ return pickFields(findings, [
5097
+ ...FINDING_ROW_FIELDS,
5098
+ "dependencyGroups",
5099
+ "dependencyRelation",
5100
+ ...extra
5101
+ ]).map((row) => withDependencyScope(row)).map(withFixability);
5102
+ }
4895
5103
  /** The parts of an advisory record an agent reads; the rest (every affected range, every reference) is full: true. */
4896
5104
  const ADVISORY_KEPT = [
4897
5105
  "id",
@@ -4970,10 +5178,13 @@ function standingSummary(standing) {
4970
5178
  const severity = String(violation["severity"] ?? "unknown");
4971
5179
  bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
4972
5180
  }
5181
+ const stated = (state) => controls.filter((c) => c["state"] === state).length;
4973
5182
  return {
4974
5183
  controls: listed.length,
4975
5184
  controlsInPolicies: controls.length,
4976
- controlsWithNothingRecorded: controls.length - listed.length,
5185
+ controlsMet: stated("met"),
5186
+ controlsNotApplicable: stated("not_applicable"),
5187
+ controlsNotAssessed: stated("no_evidence"),
4977
5188
  violations: violations.length,
4978
5189
  bySeverity,
4979
5190
  resolvedViolations: resolved
@@ -5015,7 +5226,7 @@ function registerMaintenanceTools(server, client) {
5015
5226
  sweep: null,
5016
5227
  sweepNote: "The sweep overview could not be read: an empty list here is not an all-clear."
5017
5228
  } : sweep ? { sweep } : {},
5018
- ...page(pickFields(result.data, FINDING_ROW_FIELDS).map(withFixability), result.nextCursor),
5229
+ ...page(findingRows(result.data), result.nextCursor),
5019
5230
  ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {},
5020
5231
  ...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." } : {}
5021
5232
  });
@@ -5030,10 +5241,10 @@ function registerMaintenanceTools(server, client) {
5030
5241
  annotations: READ_ONLY
5031
5242
  }, (params) => runTool(async () => {
5032
5243
  const { runId, ...raw } = (await client.getFinding(params.findingId)).data;
5033
- const finding = withFixability(params.full ? raw : {
5244
+ const finding = withFixability(params.full ? withDependencyScope(raw, { keepGroups: true }) : withDependencyScope({
5034
5245
  ...raw,
5035
5246
  data: leanAdvisory(raw["data"])
5036
- });
5247
+ }));
5037
5248
  return toolResult(runId === void 0 ? finding : {
5038
5249
  sweepId: runId,
5039
5250
  ...finding
@@ -5041,11 +5252,12 @@ function registerMaintenanceTools(server, client) {
5041
5252
  }));
5042
5253
  server.registerTool("taiga_get_policy_standing", {
5043
5254
  title: "Get a product's policy standing",
5044
- description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep). An unlisted control is not \"passing\" (summary.controlsWithNothingRecorded counts them). full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
5255
+ description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep). summary counts the other controls: met or not applicable as the documents show it, or not assessed (no evidence either way, which is no pass). controlId reads one control: its state, the passage behind a verdict, its document violations and how many scanner findings breach it. full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
5045
5256
  inputSchema: z.object({
5046
5257
  productId,
5047
5258
  full: z.boolean().optional().describe("Carry each violation's evidence and policy excerpt, cut to about 1,500 characters each, and up to 20 resolved violations (default false)"),
5048
- violationId: idInput("violation", "this tool lists them").optional().describe("Only this violation, in full and uncut: its evidence, policy excerpt and work")
5259
+ violationId: idInput("violation", "this tool lists them").optional().describe("Only this violation, in full and uncut: its evidence, policy excerpt and work"),
5260
+ controlId: z.string().trim().min(1).max(40).optional().describe("Only this control, such as SEC-5.1: whether it is met, not applicable, violated or not assessed, with the passage")
5049
5261
  }),
5050
5262
  annotations: READ_ONLY
5051
5263
  }, (params) => runTool(async () => {
@@ -5057,6 +5269,20 @@ function registerMaintenanceTools(server, client) {
5057
5269
  const note = standing.assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first)." : null;
5058
5270
  const resolvedAny = Array.isArray(standing.resolvedDivergences) && standing.resolvedDivergences.length > 0;
5059
5271
  const wording = violations.length > 0 || resolvedAny ? { wordingNote: "A violation's evidence is from its assessedAt; its title and summary may be from when it was first recorded. Where they disagree, the evidence is current." } : {};
5272
+ if (params.controlId && params.violationId) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both.");
5273
+ if (params.controlId) {
5274
+ const wanted = params.controlId.toUpperCase();
5275
+ const control = controls.find((c) => String(c.controlId).toUpperCase() === wanted);
5276
+ if (!control) return toolError("NOT_FOUND", "No control with that id in this product's policies; taiga_get_document (type, sections: []) lists a policy's controls.");
5277
+ const own = violations.filter((violation) => String(violation["controlId"]).toUpperCase() === wanted);
5278
+ return toolResult({
5279
+ ...control,
5280
+ ...control.state === "no_evidence" ? { meaning: "Not assessed: no finding or violation is recorded against it, and the documents were not found to show it met. That is no evidence either way, not a pass." } : {},
5281
+ violations: pickFields(own, VIOLATION_FIELDS),
5282
+ ...Number(control.findingCount ?? 0) > 0 ? { findingsNote: `${String(control.findingCount)} open scanner finding${control.findingCount === 1 ? "" : "s"} breach it; taiga_list_findings (productId) lists them with the controls each breaches.` } : {},
5283
+ ...own.length > 0 ? wording : {}
5284
+ });
5285
+ }
5060
5286
  if (params.violationId) {
5061
5287
  const one = [...violations, ...Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : []].find((violation) => violation["id"] === params.violationId);
5062
5288
  if (!one) return toolError("NOT_FOUND", "No violation with that ID on this product; this tool without violationId lists them.");
@@ -5120,7 +5346,7 @@ function registerMaintenanceTools(server, client) {
5120
5346
  ...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
5121
5347
  } : { status: "open" };
5122
5348
  const finding = (await client.updateFinding(params.findingId, body)).data;
5123
- return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, "ignoreNote"])[0]);
5349
+ return toolResult(findingRows([finding], ["ignoreNote"])[0]);
5124
5350
  }));
5125
5351
  server.registerTool("taiga_fix_finding", {
5126
5352
  title: "Fix a finding",
@@ -5136,7 +5362,7 @@ function registerMaintenanceTools(server, client) {
5136
5362
  const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
5137
5363
  return toolResult({
5138
5364
  backlogItemId: fixed.backlogItemId,
5139
- finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null
5365
+ finding: fixed.finding ? findingRows([fixed.finding])[0] : null
5140
5366
  });
5141
5367
  }
5142
5368
  const finding = (await client.getFinding(params.findingId)).data;
@@ -5215,6 +5441,28 @@ function readWith(row, backlogRead) {
5215
5441
  default: return;
5216
5442
  }
5217
5443
  }
5444
+ /**
5445
+ * The glossary's words for what the API names after its tables (PRO-2823): a reviewer met kind review_backlog,
5446
+ * subjectType backlog_item and inbox_task beside a glossary that says initiative and run, and had to infer they
5447
+ * were the same things.
5448
+ */
5449
+ const KIND_SAID = { review_backlog: "review_initiatives" };
5450
+ const SUBJECT_SAID = {
5451
+ backlog_item: "initiative",
5452
+ build_run: "run",
5453
+ inbox_task: "setup_task"
5454
+ };
5455
+ const kindSaid = (kind) => KIND_SAID[String(kind)] ?? kind;
5456
+ /** A setup task row in the glossary's words: the Action Required item it is, by the name taiga_resolve_action takes. */
5457
+ function setupTaskRow(row) {
5458
+ const { inboxItemId, inboxItemStatus, alsoFiledAs, ...rest } = row;
5459
+ return {
5460
+ ...rest,
5461
+ ...inboxItemId !== void 0 ? { actionId: inboxItemId } : {},
5462
+ ...inboxItemStatus !== void 0 ? { actionStatus: inboxItemStatus } : {},
5463
+ ...Array.isArray(alsoFiledAs) ? { alsoFiledAs: alsoFiledAs.map(setupTaskRow) } : {}
5464
+ };
5465
+ }
5218
5466
  /** A summary past its first sentence: review_doc rows quoted two documents, 800 to 1,200 characters each. */
5219
5467
  const SUMMARY_LIMIT = 200;
5220
5468
  function firstSentence(text) {
@@ -5232,9 +5480,12 @@ function itemRow(row, now, full, backlogRead) {
5232
5480
  const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
5233
5481
  const { summary, status, blocking, ...lean } = rest;
5234
5482
  const read = readWith(row, backlogRead);
5483
+ const saysNothing = row["kind"] === "setup_manual" && String(summary).trim().toLowerCase() === "manual config";
5235
5484
  return {
5236
5485
  ...lean,
5237
- ...full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
5486
+ kind: kindSaid(lean["kind"]),
5487
+ ...typeof lean["subjectType"] === "string" ? { subjectType: SUBJECT_SAID[lean["subjectType"]] ?? lean["subjectType"] } : {},
5488
+ ...saysNothing ? {} : full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
5238
5489
  ...full || status !== "open" ? { status } : {},
5239
5490
  ...full || blocking === true ? { blocking } : {},
5240
5491
  ...read ? { readWith: read } : {},
@@ -5266,7 +5517,8 @@ function summaryOf(rows, stale) {
5266
5517
  if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
5267
5518
  else {
5268
5519
  entry.open += 1;
5269
- entry.byKind[String(row["kind"])] = (entry.byKind[String(row["kind"])] ?? 0) + 1;
5520
+ const kind = String(kindSaid(row["kind"]));
5521
+ entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
5270
5522
  }
5271
5523
  byProduct.set(key, entry);
5272
5524
  }
@@ -5322,15 +5574,10 @@ async function staleBacklogReviews(client, rows, out) {
5322
5574
  const empty = /* @__PURE__ */ new Set();
5323
5575
  await Promise.all(checked.map(async (productId) => {
5324
5576
  try {
5325
- const pages = await Promise.all(["backlog", "in_progress"].map((status) => client.listBacklogItems(productId, {
5326
- status,
5327
- limit: 100
5328
- })));
5329
- if (pages.some((page) => page.data.some((item) => item.state === "backlog" || item.state === "todo"))) return;
5330
- if (pages.some((page) => page.nextCursor)) {
5331
- unchecked += asks.filter((row) => row["productId"] === productId).length;
5332
- return;
5333
- }
5577
+ if ((await Promise.all(["backlog", "todo"].map((state) => client.listBacklogItems(productId, {
5578
+ state,
5579
+ limit: 1
5580
+ })))).some((page) => page.data.length > 0)) return;
5334
5581
  empty.add(productId);
5335
5582
  } catch (error) {
5336
5583
  if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
@@ -5413,6 +5660,7 @@ function sameKeyOnce(rows) {
5413
5660
  id: from["id"],
5414
5661
  category: from["category"],
5415
5662
  title: from["title"],
5663
+ inboxItemId: from["inboxItemId"],
5416
5664
  inboxItemStatus: from["inboxItemStatus"]
5417
5665
  });
5418
5666
  if (!isOpen(kept) && isOpen(row)) {
@@ -5512,7 +5760,7 @@ function registerInboxTools(server, client) {
5512
5760
  ...params.cursor === void 0 ? { withTotal: true } : {}
5513
5761
  });
5514
5762
  return toolResult({
5515
- ...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor, result.data.length),
5763
+ ...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)).map(setupTaskRow), result.nextCursor, result.data.length),
5516
5764
  ...totalOf(result)
5517
5765
  });
5518
5766
  }));
@@ -5765,42 +6013,31 @@ async function policiesPart(client) {
5765
6013
  sections: Object.keys(sectionsOf(row["data"] ?? {}) ?? {}).length
5766
6014
  }));
5767
6015
  }
5768
- /** The statuses an item is open in; the API's filter takes one at a time. */
5769
- const OPEN_BACKLOG_STATUSES = ["backlog", "in_progress"];
5770
6016
  /**
5771
- * The items still open: one status-filtered read per open status (the API
5772
- * leaves archived items out), each paged in full up to a cap the result
5773
- * reports. The list is oldest first and the open items are the newest, so a
5774
- * single page would miss exactly the ones that matter. Rows carrying a
5775
- * retired status (readable for a rolling deploy) match neither filter;
5776
- * the settled check below keeps the shape honest for whatever comes back.
6017
+ * The items still open, newest first (PRO-2843): one read of the open ones,
6018
+ * paged up to a cap the result reports. The API leaves archived items out.
5777
6019
  */
5778
6020
  async function backlogPart(client, productId) {
5779
- const open = /* @__PURE__ */ new Map();
6021
+ const rows = [];
5780
6022
  let truncated = false;
5781
6023
  let pageCapped = false;
5782
- for (const status of OPEN_BACKLOG_STATUSES) {
5783
- let cursor;
5784
- for (let pageIndex = 0;; pageIndex += 1) {
5785
- if (pageIndex === BACKLOG_PAGE_LIMIT) {
5786
- truncated = true;
5787
- pageCapped = true;
5788
- break;
5789
- }
5790
- const result = await client.listBacklogItems(productId, {
5791
- status,
5792
- limit: 100,
5793
- cursor
5794
- });
5795
- for (const row of result.data) if (!isSettledOrArchived({
5796
- status: String(row.status),
5797
- archivedAt: row.archivedAt ?? null
5798
- })) open.set(String(row.id), row);
5799
- if (!result.nextCursor) break;
5800
- cursor = result.nextCursor;
6024
+ let cursor;
6025
+ for (let pageIndex = 0;; pageIndex += 1) {
6026
+ if (pageIndex === BACKLOG_PAGE_LIMIT) {
6027
+ truncated = true;
6028
+ pageCapped = true;
6029
+ break;
5801
6030
  }
6031
+ const result = await client.listBacklogItems(productId, {
6032
+ open: true,
6033
+ order: "newest",
6034
+ limit: 100,
6035
+ cursor
6036
+ });
6037
+ rows.push(...result.data);
6038
+ if (!result.nextCursor) break;
6039
+ cursor = result.nextCursor;
5802
6040
  }
5803
- const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
5804
6041
  if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
5805
6042
  const byColumn = {
5806
6043
  backlog: 0,
@@ -5812,9 +6049,9 @@ async function backlogPart(client, productId) {
5812
6049
  for (const row of rows) if (row.state) byColumn[row.state] = (byColumn[row.state] ?? 0) + 1;
5813
6050
  return {
5814
6051
  byColumn,
5815
- items: situatedRows(rows.slice(-50)),
6052
+ items: situatedRows(rows.slice(0, BACKLOG_IN_CONTEXT)),
5816
6053
  truncated,
5817
- ...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." } : {}
6054
+ ...pageCapped ? { note: "More than 1000 open items: byColumn counts the newest 1000; taiga_list_initiatives pages the rest." } : {}
5818
6055
  };
5819
6056
  }
5820
6057
  /**
@@ -5839,7 +6076,7 @@ const CONTEXT_RUN_SAID = [
5839
6076
  "runNumber",
5840
6077
  "backlogItemId",
5841
6078
  "targetTitle",
5842
- "disposition",
6079
+ "status",
5843
6080
  "outcome",
5844
6081
  "failedReasonMeans",
5845
6082
  "hardeningNote",
@@ -5925,24 +6162,24 @@ async function productContext(client, product, options) {
5925
6162
  unavailable,
5926
6163
  readNext: "Read a policy, which is the organization's and takes no productId, with taiga_get_document (type, then sections: [] for its outline or a control id such as SEC-5.1); the product's own specification, architecture and other documents with taiga_get_product_document (productId); an initiative with taiga_get_initiative (taiga_list_initiatives pages a truncated backlog) and a run with taiga_get_run."
5927
6164
  };
5928
- let characters = JSON.stringify(inProductWords(result), null, 2).length;
6165
+ let characters = serializeResult(result).length;
5929
6166
  for (let measured = -1; measured !== characters;) {
5930
6167
  measured = characters;
5931
- characters = JSON.stringify(inProductWords({
6168
+ characters = serializeResult({
5932
6169
  ...result,
5933
6170
  characters: measured
5934
- }), null, 2).length;
6171
+ }).length;
5935
6172
  }
5936
6173
  return toolResult({
5937
- ...result,
5938
- characters
6174
+ characters,
6175
+ ...result
5939
6176
  });
5940
6177
  }
5941
6178
  function registerContextTools(server, client) {
5942
6179
  if (!client.hasCapability("products:read")) return;
5943
6180
  server.registerTool("taiga_get_product_context", {
5944
6181
  title: "Get product context",
5945
- 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.`,
6182
+ 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.`,
5946
6183
  inputSchema: z.object({
5947
6184
  productId: productId.optional(),
5948
6185
  slug: slug.optional(),
@@ -6313,12 +6550,13 @@ function registerObserveTools(server, client) {
6313
6550
  ]);
6314
6551
  const now = current.data;
6315
6552
  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;
6553
+ const live = new Set([...now.current ?? [], ...now.deploying ?? []].map((row) => row.id));
6316
6554
  return toolResult({
6317
6555
  environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
6318
6556
  ...environments.more ? { moreEnvironments: true } : {},
6319
6557
  running: deploymentLines(now.current ?? []),
6320
6558
  deploying: deploymentLines(now.deploying ?? []),
6321
- history: page(deploymentLines(history.data), history.nextCursor),
6559
+ history: page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor),
6322
6560
  ..."value" in sync ? { syncStatus: sync.value.data } : {
6323
6561
  syncStatus: null,
6324
6562
  syncUnavailable: sync.unavailable
@@ -22019,29 +22257,19 @@ function judgeItem(until, gate) {
22019
22257
  /** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
22020
22258
  function itemReport(item) {
22021
22259
  const run = latestBuildOf(item);
22022
- const situation = situationOf(item);
22023
22260
  return {
22024
- ...situation ? { situation } : {},
22025
22261
  ...pickFields([item], [
22026
22262
  "id",
22027
22263
  "title",
22028
- "state",
22029
- "planningStatus",
22030
- "needsHumanCause",
22031
- "needsHumanDetail",
22032
22264
  "autoBuild"
22033
22265
  ])[0],
22034
- lifecycle: pickFields([lifecycleOf(item)], [
22035
- "turn",
22036
- "stage",
22037
- "cta",
22038
- "planFailureCause"
22039
- ])[0],
22040
- latestBuild: run ? pickFields([run], [
22266
+ ...standing(item),
22267
+ latestRun: run ? pickFields([run], [
22041
22268
  "id",
22042
22269
  "status",
22043
22270
  "completedSteps",
22044
22271
  "totalSteps",
22272
+ "prNumber",
22045
22273
  "prUrl",
22046
22274
  "prState",
22047
22275
  "prMerged",
@@ -22225,6 +22453,10 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
22225
22453
  message: `${withStop(issue.message)} ${pointers.join(" ")}`
22226
22454
  };
22227
22455
  }
22456
+ if (issue.code === "invalid_value") return {
22457
+ ...issue,
22458
+ message: `${withStop(issue.message)} Pass one of those values exactly.`
22459
+ };
22228
22460
  const argument = issue.path?.length === 1 ? String(issue.path[0]) : void 0;
22229
22461
  const source = argument ? idSource(argument) : void 0;
22230
22462
  if (source && /received undefined/.test(issue.message)) return {
@@ -22373,7 +22605,7 @@ function registerAllTools(server, client, options = {}) {
22373
22605
  function registerWhoami(server, client, registered) {
22374
22606
  server.registerTool("taiga_whoami", {
22375
22607
  title: "Who am I",
22376
- 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.",
22608
+ 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.",
22377
22609
  inputSchema: z.object({}),
22378
22610
  annotations: READ_ONLY
22379
22611
  }, () => runTool(async () => {
@@ -22381,6 +22613,7 @@ function registerWhoami(server, client, registered) {
22381
22613
  const tools = [...registered.entries()].filter(([name]) => name !== "taiga_whoami");
22382
22614
  const named = (pick) => tools.filter(([, notes]) => pick(notes)).map(([name]) => name);
22383
22615
  return toolResult({
22616
+ api: client.apiHost,
22384
22617
  email: me?.email ?? null,
22385
22618
  role: me?.role ?? null,
22386
22619
  scopes: me?.scopes ?? null,
@@ -22423,8 +22656,8 @@ const SERVER_INSTRUCTIONS = [
22423
22656
  "conversation that specifies a new product. A factory owns products.",
22424
22657
  "",
22425
22658
  "**IDs** are UUIDs; the product, factory and initiative lists take query to find one by name. A NOT_FOUND names",
22426
- "the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones unless",
22427
- "status: 'all'. outcome, situation and nextStep are one-sentence summaries; setup writes return readiness.",
22659
+ "the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones, newest",
22660
+ "first. outcome, situation and nextStep are one-sentence summaries; setup writes return readiness.",
22428
22661
  "Product documents have two timelines: plan (intent) and reality (from the code).",
22429
22662
  "",
22430
22663
  "**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-rc.480",
3
+ "version": "0.26.0-rc.494",
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",