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

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