@taiga-ai/mcp-server 0.16.0-dev.437 → 0.17.0-dev.438

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
4
4
  import { z } from "zod";
5
5
  import { McpServer, ProtocolError, ProtocolErrorCode, ResourceTemplate } from "@modelcontextprotocol/server";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
6
7
  //#region \0rolldown/runtime.js
7
8
  var __defProp = Object.defineProperty;
8
9
  var __exportAll = (all, no_symbols) => {
@@ -96,7 +97,7 @@ function seg(value) {
96
97
  * is one. Every value goes through URLSearchParams, so nothing a tool passes
97
98
  * can add a parameter of its own.
98
99
  */
99
- function query(params) {
100
+ function query$1(params) {
100
101
  const search = new URLSearchParams();
101
102
  for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== null && value !== "") search.set(key, String(value));
102
103
  const text = search.toString();
@@ -338,10 +339,10 @@ var TaigaClient = class {
338
339
  return this.request("GET", `/products/${seg(productId)}/maintenance/summary`);
339
340
  }
340
341
  async listMaintenanceSweeps(productId, opts) {
341
- return this.request("GET", `/products/${seg(productId)}/maintenance/runs${query({ limit: opts?.limit })}`);
342
+ return this.request("GET", `/products/${seg(productId)}/maintenance/runs${query$1({ limit: opts?.limit })}`);
342
343
  }
343
344
  async listFindings(productId, opts) {
344
- return this.request("GET", `/products/${seg(productId)}/maintenance/findings${query({ ...opts })}`);
345
+ return this.request("GET", `/products/${seg(productId)}/maintenance/findings${query$1({ ...opts })}`);
345
346
  }
346
347
  async getFinding(id) {
347
348
  return this.request("GET", `/maintenance/findings/${seg(id)}`);
@@ -367,7 +368,7 @@ var TaigaClient = class {
367
368
  async listInbox(opts) {
368
369
  const { productId, ...rest } = opts;
369
370
  const path = productId ? `/products/${seg(productId)}/inbox` : "/inbox";
370
- return this.request("GET", `${path}${query({ ...rest })}`);
371
+ return this.request("GET", `${path}${query$1({ ...rest })}`);
371
372
  }
372
373
  async updateInboxItem(id, body) {
373
374
  return this.request("PATCH", `/inbox/${seg(id)}`, body);
@@ -375,10 +376,10 @@ var TaigaClient = class {
375
376
  async listSetupTasks(opts) {
376
377
  const { productId, ...rest } = opts;
377
378
  const path = productId ? `/products/${seg(productId)}/setup-tasks` : "/setup-tasks";
378
- return this.request("GET", `${path}${query({ ...rest })}`);
379
+ return this.request("GET", `${path}${query$1({ ...rest })}`);
379
380
  }
380
381
  async listEnvironments(productId, opts) {
381
- return this.request("GET", `/products/${seg(productId)}/environments${query({ ...opts })}`);
382
+ return this.request("GET", `/products/${seg(productId)}/environments${query$1({ ...opts })}`);
382
383
  }
383
384
  async createEnvironment(productId, body) {
384
385
  return this.request("POST", `/products/${seg(productId)}/environments`, body);
@@ -390,13 +391,13 @@ var TaigaClient = class {
390
391
  return this.request("GET", `/products/${seg(productId)}/deployments/current`);
391
392
  }
392
393
  async listDeployments(productId, opts) {
393
- return this.request("GET", `/products/${seg(productId)}/deployments${query({ ...opts })}`);
394
+ return this.request("GET", `/products/${seg(productId)}/deployments${query$1({ ...opts })}`);
394
395
  }
395
396
  async getDeploymentSyncStatus(productId) {
396
397
  return this.request("GET", `/products/${seg(productId)}/deployments/sync-status`);
397
398
  }
398
399
  async getDeliveryMetrics(productId, environmentId, windowDays) {
399
- return this.request("GET", `/products/${seg(productId)}/deployments/insights${query({
400
+ return this.request("GET", `/products/${seg(productId)}/deployments/insights${query$1({
400
401
  environmentId,
401
402
  windowDays
402
403
  })}`);
@@ -405,14 +406,14 @@ var TaigaClient = class {
405
406
  return this.request("GET", `/products/${seg(productId)}/monitoring/health`);
406
407
  }
407
408
  async getMonitoringSignal(productId, signal, environmentId, windowDays) {
408
- return this.request("GET", `/products/${seg(productId)}/monitoring/${signal}${query({
409
+ return this.request("GET", `/products/${seg(productId)}/monitoring/${signal}${query$1({
409
410
  environmentId,
410
411
  windowDays
411
412
  })}`);
412
413
  }
413
414
  async listKnowledge(opts) {
414
415
  const scope = (value) => value === null ? "null" : value;
415
- return this.request("GET", `/custom-documents${query({
416
+ return this.request("GET", `/custom-documents${query$1({
416
417
  ...opts,
417
418
  factoryId: scope(opts.factoryId),
418
419
  productId: scope(opts.productId)
@@ -464,7 +465,7 @@ var TaigaClient = class {
464
465
  return this.request("POST", "/products", body);
465
466
  }
466
467
  async previewProductMove(productId, factoryId) {
467
- return this.request("GET", `/products/${seg(productId)}/factory/preview${query({ factoryId })}`);
468
+ return this.request("GET", `/products/${seg(productId)}/factory/preview${query$1({ factoryId })}`);
468
469
  }
469
470
  async moveProduct(productId, body) {
470
471
  return this.request("PUT", `/products/${seg(productId)}/factory`, body);
@@ -486,7 +487,7 @@ var TaigaClient = class {
486
487
  const data = [];
487
488
  let cursor;
488
489
  do {
489
- const result = await this.request("GET", `/github/installations${query({
490
+ const result = await this.request("GET", `/github/installations${query$1({
490
491
  limit: 100,
491
492
  cursor
492
493
  })}`);
@@ -496,7 +497,7 @@ var TaigaClient = class {
496
497
  return { data };
497
498
  }
498
499
  async listInstallationRepositories(installationId, opts) {
499
- return this.request("GET", `/github/installations/${seg(installationId)}/repositories${query({ ...opts })}`);
500
+ return this.request("GET", `/github/installations/${seg(installationId)}/repositories${query$1({ ...opts })}`);
500
501
  }
501
502
  async linkRepository(productId, body) {
502
503
  return this.request("POST", `/products/${seg(productId)}/repositories`, body);
@@ -569,7 +570,15 @@ const OPERATE_SETTING = {
569
570
  //#endregion
570
571
  //#region src/tools/inputs.ts
571
572
  /** The inputs several tools share, so a product or a page is named the same way everywhere. */
572
- const productId = z.string().uuid().describe("Product ID -- taiga_get_product_context, taiga_resolve_product or taiga_list_products give it");
573
+ /**
574
+ * An ID input whose refusal says where the ID comes from. The client shows the
575
+ * schema's message, not the description, when a call fails validation, and
576
+ * "Invalid UUID" alone sends a model that passed a name to guess again.
577
+ */
578
+ function idInput(what, from) {
579
+ return z.string().uuid({ message: `A ${what} ID is a UUID, not a name: ${from}` }).describe(`${what[0].toUpperCase()}${what.slice(1)} ID -- ${from}`);
580
+ }
581
+ const productId = idInput("product", "taiga_list_products (query finds one by name), taiga_resolve_product or taiga_get_product_context give it");
573
582
  /** The page inputs every list tool takes; `nextCursor` of one page is the `cursor` of the next. */
574
583
  const pagination = {
575
584
  limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)"),
@@ -2111,14 +2120,91 @@ function emptyOriginNote(origin, product) {
2111
2120
  if (origin === "plan") return imported ? `This product was imported, so no discovery chain was generated for it: its derived documents are on the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(", ")}) would appear here.` : "No plan documents are published yet: they are written during discovery in Taiga.";
2112
2121
  if (!imported) return "No reality documents yet: they are derived from the code once the first initiative is done (a merged pull request marks it done) or the repository is pushed to. Until then read the plan side.";
2113
2122
  switch (importStatusOf(product)) {
2114
- case "pending": return "No reality documents yet: the import has not started. Link the repository and run the import in Taiga.";
2123
+ case "pending": return "No reality documents yet: the import has not started. Link the repository, then taiga_start_import.";
2115
2124
  case "analyzing":
2116
2125
  case "generating_docs": return "No reality documents yet: the import is running and will derive them from the code.";
2117
- case "cancelled": return "No reality documents: the import was cancelled before it derived any. Run it again in Taiga.";
2118
- case "failed": return "No reality documents: the import failed before it derived any. Run it again in Taiga.";
2126
+ case "cancelled": return "No reality documents: the import was cancelled before it derived any. taiga_start_import runs it again.";
2127
+ case "failed": return "No reality documents: the import failed before it derived any. taiga_start_import runs it again.";
2119
2128
  default: return "No reality documents: the import derives them from the code, and none have been published.";
2120
2129
  }
2121
2130
  }
2131
+ /**
2132
+ * What a product in setup waits for, as the next call to make. Nothing on a
2133
+ * draft product says it otherwise: discoveryStep names the step and
2134
+ * importStatus the import, and the way on from each lives in tool
2135
+ * descriptions an agent reads only when it already knows which. Null once the
2136
+ * product is active.
2137
+ */
2138
+ function setupNextStep(product) {
2139
+ if (product?.status !== "draft") return null;
2140
+ if (isImportedProduct(product)) switch (importStatusOf(product)) {
2141
+ case "analyzing":
2142
+ case "generating_docs": return "The import is running and derives the documents from the code (taiga_wait_for_job follows its job). Then review them, and taiga_finish_discovery opens the product.";
2143
+ case "complete": return "The import is done: review the reality documents (taiga_list_product_documents), then taiga_finish_discovery opens the product.";
2144
+ case "failed":
2145
+ case "cancelled": return "The last import did not finish: taiga_start_import runs it again.";
2146
+ default: return "Link its repository if it has none (taiga_list_linkable_repositories, taiga_link_repository), then taiga_start_import: the import derives the documents from the code and runs by itself.";
2147
+ }
2148
+ if (product.discoveryStep === void 0) return null;
2149
+ switch (product.discoveryStep) {
2150
+ case "documents": return "The specification is done; the other documents are generated from it (taiga_generate_product_documents starts the ones still missing). Once every required one is published, taiga_finish_discovery opens the product.";
2151
+ case "checks": return "The documents are published; the checks are stamped in the web, then taiga_finish_discovery opens the product.";
2152
+ default: return "Discovery is a conversation held in the web: the person describes the product there and the specification comes out of it. The other documents follow, then taiga_finish_discovery opens the product.";
2153
+ }
2154
+ }
2155
+ /** What discoveryStep says, for a description that shows it. */
2156
+ const DISCOVERY_STEP_MEANING = "discoveryStep is where a product in setup is: context (not started), conversation or import (the specification is being talked out, or derived from the code), documents (the rest being generated and reviewed), checks (stamped in the web); null once it is active.";
2157
+ //#endregion
2158
+ //#region src/tools/hints.ts
2159
+ /**
2160
+ * The next step after the API answers 404. The API's own word is bare
2161
+ * ("Backlog item not found", "Repository link not found"), and an agent that
2162
+ * hears only that guesses another ID, or a name. Each ID a tool takes names
2163
+ * the tool that lists the right ones; the tool's arguments say which apply.
2164
+ */
2165
+ const BY_ARGUMENT = {
2166
+ itemId: "taiga_list_backlog_items lists a product's initiatives with their IDs; a run or plan ID is not one.",
2167
+ backlogItemId: "taiga_list_backlog_items lists a product's initiatives with their IDs.",
2168
+ buildId: "taiga_list_builds lists the runs, of one product or of every product the key can see.",
2169
+ planId: "taiga_get_plan gives the initiative's active plan and its ID.",
2170
+ repositoryId: "taiga_list_product_repositories lists the product's repository links; a GitHub repository id is not a link ID.",
2171
+ replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
2172
+ githubRepositoryId: "taiga_list_linkable_repositories lists the repositories the GitHub connection reaches, with their ids.",
2173
+ installationId: "taiga_list_linkable_repositories lists the connected GitHub accounts.",
2174
+ jobId: "A job ID is what the tool that started the job returned (taiga_request_initiatives, taiga_start_import).",
2175
+ factoryId: "taiga_list_factories lists the factories the key can see.",
2176
+ findingId: "taiga_list_findings lists a product's findings.",
2177
+ violationId: "taiga_get_policy_standing lists a product's policy violations.",
2178
+ environmentId: "taiga_get_deployments lists the product's environments with their IDs; a product ID is not one.",
2179
+ knowledgeId: "taiga_list_knowledge lists the knowledge files.",
2180
+ productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
2181
+ };
2182
+ /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2183
+ const BY_TOOL = {
2184
+ taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2185
+ taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_backlog_item says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one." : BY_ARGUMENT.itemId
2186
+ };
2187
+ /** The hint for a 404 from one tool call: the tool's own, else one per ID it was given. */
2188
+ function notFoundHint(tool, args) {
2189
+ if (BY_TOOL[tool]) return BY_TOOL[tool];
2190
+ if (typeof args !== "object" || args === null) return void 0;
2191
+ const given = Object.keys(args).filter((key) => args[key] != null && BY_ARGUMENT[key]);
2192
+ given.sort((a, b) => Number(a === "productId") - Number(b === "productId"));
2193
+ if (given.length === 0) return void 0;
2194
+ const hint = given.map((key) => BY_ARGUMENT[key]).join(" ");
2195
+ return () => hint;
2196
+ }
2197
+ /** The hint of the tool call in progress, read by runTool when the API answers 404. */
2198
+ const notFoundHints = new AsyncLocalStorage();
2199
+ /** The API's 404 message with the hint of the call in progress, if it has one. */
2200
+ function withNotFoundHint(said) {
2201
+ const hint = notFoundHints.getStore()?.(said);
2202
+ return hint ? `${withStop(said)} ${hint}` : said;
2203
+ }
2204
+ /** A sentence ends in a stop before the next one is added to it. */
2205
+ function withStop(text) {
2206
+ return /[.!?]$/.test(text.trimEnd()) ? text.trimEnd() : `${text.trimEnd()}.`;
2207
+ }
2122
2208
  //#endregion
2123
2209
  //#region src/tools/errors.ts
2124
2210
  /** A successful tool result: one JSON text block the model reads. */
@@ -2162,21 +2248,19 @@ function networkFailureDetail(error) {
2162
2248
  const why = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : null;
2163
2249
  return why ? `${error.message} (${why})` : error.message;
2164
2250
  }
2251
+ /**
2252
+ * The code a status implies when the API sent none. 400 is VALIDATION_ERROR,
2253
+ * the code the API itself sends, so a refusal by this server and one by the
2254
+ * API read the same.
2255
+ */
2165
2256
  const CODE_BY_STATUS = {
2166
- 400: "VALIDATION_FAILED",
2257
+ 400: "VALIDATION_ERROR",
2167
2258
  401: "UNAUTHENTICATED",
2168
2259
  403: "FORBIDDEN",
2169
2260
  404: "NOT_FOUND",
2170
2261
  409: "CONFLICT",
2171
2262
  429: "RATE_LIMITED"
2172
2263
  };
2173
- /**
2174
- * Run a tool handler and turn a failure into a structured error. An API
2175
- * refusal keeps the API's own code when it sent one and its message whole:
2176
- * on a refused publish that names the section holding template residue,
2177
- * which is what the model needs to fix the draft. A failed connection is
2178
- * structured too, so every failure the model sees has the same shape.
2179
- */
2180
2264
  /** What the API said, in one phrase: the status, the code it sent (or the one its status implies), its message. */
2181
2265
  function apiFailureText(error) {
2182
2266
  return `${error.status} ${apiFailureCode(error)}${error.apiMessage ? `: ${error.apiMessage}` : ""}`;
@@ -2189,11 +2273,22 @@ function apiFailureCode(error) {
2189
2273
  function isMissOrRefusal(error) {
2190
2274
  return error instanceof TaigaApiError && (error.status === 404 || error.status === 403);
2191
2275
  }
2276
+ /**
2277
+ * Run a tool handler and turn a failure into a structured error. An API
2278
+ * refusal keeps the API's own code when it sent one and its message whole:
2279
+ * on a refused publish that names the section holding template residue,
2280
+ * which is what the model needs to fix the draft. A failed connection is
2281
+ * structured too, so every failure the model sees has the same shape.
2282
+ */
2192
2283
  async function runTool(handler) {
2193
2284
  try {
2194
2285
  return await handler();
2195
2286
  } catch (error) {
2196
- if (error instanceof TaigaApiError) return toolError(error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR", error.apiMessage ?? `Taiga API answered ${error.status}.`, error.details);
2287
+ if (error instanceof TaigaApiError) {
2288
+ const code = error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR";
2289
+ const said = error.apiMessage ?? `Taiga API answered ${error.status}.`;
2290
+ return toolError(code, error.status === 404 ? withNotFoundHint(said) : said, error.details);
2291
+ }
2197
2292
  if (isNetworkFailure(error)) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${networkFailureDetail(error)}. Check TAIGA_API_URL.`);
2198
2293
  throw error;
2199
2294
  }
@@ -2374,6 +2469,24 @@ function budgetSections(doc, budget) {
2374
2469
  }
2375
2470
  return cutTo(doc, sections, Object.fromEntries(kept));
2376
2471
  }
2472
+ /**
2473
+ * The answer to a read that names sections: the ones the document has, the
2474
+ * rest listed with their sizes, and the asked-for ids it lacks under
2475
+ * unknownSections. One wrong id does not throw away the right ones (the
2476
+ * model retries only the miss); none right is a miss that lists the real ids.
2477
+ */
2478
+ function sectionReadResult(doc, ids, label) {
2479
+ const cut = pickSections(doc, new Set(ids));
2480
+ if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
2481
+ const all = Object.keys(sectionsOf(doc.data) ?? {});
2482
+ const unknownSections = ids.filter((id) => !all.includes(id));
2483
+ if (unknownSections.length === ids.length) return toolError("NOT_FOUND", `The ${label} has no section ${ids.join(", ")}; its sections are ${all.join(", ")}.`);
2484
+ return toolResult({
2485
+ ...cut.doc,
2486
+ omittedSections: cut.omittedSections,
2487
+ ...unknownSections.length > 0 ? { unknownSections } : {}
2488
+ });
2489
+ }
2377
2490
  //#endregion
2378
2491
  //#region src/tools/summarize.ts
2379
2492
  /**
@@ -2419,6 +2532,44 @@ function itemRows(items, fields) {
2419
2532
  criteria: criteriaCounts(items[index])
2420
2533
  } : row);
2421
2534
  }
2535
+ /** Pages a name search reads before it stops and says so: 1,000 rows at the API's page ceiling. */
2536
+ const SEARCH_PAGE_LIMIT = 10;
2537
+ /**
2538
+ * Rows whose text contains `query`, ignoring case, read page by page until
2539
+ * `limit` match or the rows run out. The API has no name search, and a model
2540
+ * told the person's word for a thing ("the Sandbox factory") otherwise pages
2541
+ * the whole list to find its ID. `more` is true when it stopped before the end.
2542
+ */
2543
+ async function searchPages(fetchPage, query, text, limit) {
2544
+ const needle = query.toLowerCase();
2545
+ const rows = [];
2546
+ let cursor;
2547
+ for (let index = 0; index < SEARCH_PAGE_LIMIT; index += 1) {
2548
+ const result = await fetchPage(cursor);
2549
+ const page = result.data;
2550
+ for (const [at, row] of page.entries()) {
2551
+ if (!text(row).some((value) => typeof value === "string" && value.toLowerCase().includes(needle))) continue;
2552
+ rows.push(row);
2553
+ if (rows.length === limit) return {
2554
+ rows,
2555
+ more: at < page.length - 1 || result.nextCursor !== null
2556
+ };
2557
+ }
2558
+ if (!result.nextCursor) return {
2559
+ rows,
2560
+ more: false
2561
+ };
2562
+ cursor = result.nextCursor;
2563
+ }
2564
+ return {
2565
+ rows,
2566
+ more: true
2567
+ };
2568
+ }
2569
+ /** A search reads its own pages, so a cursor from a plain page has nothing to continue. */
2570
+ const SEARCH_WITH_CURSOR = "A search answers one page; narrow the query instead of paging it.";
2571
+ /** What a search says when it stopped before reading every row. */
2572
+ const SEARCH_NOTE = "There may be more matches than the limit carries; narrow the query or raise the limit.";
2422
2573
  //#endregion
2423
2574
  //#region src/tools/products.ts
2424
2575
  /** What a product row needs to say for the model to pick one and read it in full. */
@@ -2428,9 +2579,10 @@ const PRODUCT_SUMMARY_FIELDS = [
2428
2579
  "slug",
2429
2580
  "status",
2430
2581
  "factoryId",
2431
- "kind",
2432
2582
  "discoveryStep"
2433
2583
  ];
2584
+ /** A list's name search: part of a name, any case. */
2585
+ const query = (what) => z.string().trim().min(1).max(200).optional().describe(`Part of the ${what}, any case: only the rows that contain it`);
2434
2586
  /**
2435
2587
  * What a document row needs to say for the model to pick one and read it in
2436
2588
  * full: which document, on which timeline, how current. Not the derived-from
@@ -2577,7 +2729,7 @@ async function resolveProductRef(client, ref, accepted, usage) {
2577
2729
  ].filter((v) => v !== void 0).length;
2578
2730
  if (given !== 1) {
2579
2731
  const how = given === 0 ? usage ?? "taiga_list_products shows what exists." : "Two names could mean two products.";
2580
- return refusal("VALIDATION_FAILED", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
2732
+ return refusal("VALIDATION_ERROR", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
2581
2733
  }
2582
2734
  if (ref.productId) try {
2583
2735
  return { product: (await client.getProduct(ref.productId)).data };
@@ -2606,30 +2758,59 @@ async function resolveProductRef(client, ref, accepted, usage) {
2606
2758
  function registerProductTools(server, client) {
2607
2759
  if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
2608
2760
  title: "List factories",
2609
- description: "List all factories in the organization. A factory is the group that owns a set of products. Use this to find factory IDs for reading factory instructions. Factory instructions carry the factory's standing guidance: domain context, tech preferences, and the conventions its products follow.",
2610
- inputSchema: z.object({ ...pagination }),
2761
+ description: "List all factories in the organization. A factory is the group that owns a set of products. Use this to find factory IDs for reading factory instructions; query finds one by name. Factory instructions carry the factory's standing guidance: domain context, tech preferences, and the conventions its products follow.",
2762
+ inputSchema: z.object({
2763
+ query: query("factory name"),
2764
+ ...pagination
2765
+ }),
2611
2766
  annotations: READ_ONLY
2612
2767
  }, (params) => runTool(async () => {
2613
- const result = await client.listFactories(params);
2614
- return toolResult(page(result.data, result.nextCursor));
2768
+ if (params.query === void 0) {
2769
+ const result = await client.listFactories(params);
2770
+ return toolResult(page(result.data, result.nextCursor));
2771
+ }
2772
+ if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
2773
+ const found = await searchPages((cursor) => client.listFactories({
2774
+ limit: 100,
2775
+ cursor
2776
+ }), params.query, (row) => [row.name, row.slug], params.limit ?? 25);
2777
+ return toolResult({
2778
+ ...page(found.rows, null),
2779
+ ...found.more ? { more: SEARCH_NOTE } : {}
2780
+ });
2615
2781
  }));
2616
2782
  if (client.hasCapability("products:read")) {
2617
2783
  server.registerTool("taiga_list_products", {
2618
2784
  title: "List products",
2619
- description: "List products in the organization, as summaries (id, name, slug, status, factory, kind). Use this to find a product by name; inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full.",
2785
+ description: `List products in the organization, as summaries (id, name, slug, status, factory, discoveryStep). Use this to find a product by name (query); inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full. ${DISCOVERY_STEP_MEANING}`,
2620
2786
  inputSchema: z.object({
2621
- factoryId: z.string().uuid().optional().describe("Filter by factory ID -- get factory IDs from taiga_list_factories"),
2787
+ query: query("product name or slug"),
2788
+ factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it").optional().describe("Only the products of this factory"),
2622
2789
  status: z.enum(PRODUCT_STATUSES).optional().describe("Filter by status: draft until the product finishes setup in Taiga, active after that, archived when retired."),
2623
2790
  ...pagination
2624
2791
  }),
2625
2792
  annotations: READ_ONLY
2626
2793
  }, (params) => runTool(async () => {
2627
- const result = await client.listProducts(params);
2628
- return toolResult(page(pickFields(result.data, PRODUCT_SUMMARY_FIELDS), result.nextCursor));
2794
+ const { query: text, ...filters } = params;
2795
+ if (text === void 0) {
2796
+ const result = await client.listProducts(filters);
2797
+ return toolResult(page(pickFields(result.data, PRODUCT_SUMMARY_FIELDS), result.nextCursor));
2798
+ }
2799
+ if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
2800
+ const found = await searchPages((cursor) => client.listProducts({
2801
+ factoryId: filters.factoryId,
2802
+ status: filters.status,
2803
+ limit: 100,
2804
+ cursor
2805
+ }), text, (row) => [row.name, row.slug], params.limit ?? 25);
2806
+ return toolResult({
2807
+ ...page(pickFields(found.rows, PRODUCT_SUMMARY_FIELDS), null),
2808
+ ...found.more ? { more: SEARCH_NOTE } : {}
2809
+ });
2629
2810
  }));
2630
2811
  server.registerTool("taiga_get_product", {
2631
2812
  title: "Get product",
2632
- description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did.",
2813
+ description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries; taiga_get_product_context carries this summary with the rest a session starts from. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did. A product still in setup carries nextStep: what it waits for and the call that moves it on.",
2633
2814
  inputSchema: z.object({
2634
2815
  productId: productId.optional(),
2635
2816
  slug: slug.optional()
@@ -2637,7 +2818,12 @@ function registerProductTools(server, client) {
2637
2818
  annotations: READ_ONLY
2638
2819
  }, (params) => runTool(async () => {
2639
2820
  const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
2640
- return "refusal" in resolved ? toolError(resolved.refusal.code, resolved.refusal.message) : toolResult(resolved.product);
2821
+ if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
2822
+ const nextStep = setupNextStep(resolved.product);
2823
+ return toolResult(nextStep ? {
2824
+ ...resolved.product,
2825
+ nextStep
2826
+ } : resolved.product);
2641
2827
  }));
2642
2828
  server.registerTool("taiga_resolve_product", {
2643
2829
  title: "Resolve product from git remote",
@@ -2667,7 +2853,7 @@ function registerProductTools(server, client) {
2667
2853
  }));
2668
2854
  server.registerTool("taiga_get_product_document", {
2669
2855
  title: "Get product document",
2670
- description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A large document runs to tens of thousands of characters: name the sections you need (the ids taiga_get_product_context lists) and the read carries only those, listing the rest with their sizes. A read that finds nothing says which documents and timelines the product does have.",
2856
+ description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A large document runs to tens of thousands of characters: name the sections you need (the ids taiga_get_product_context lists) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds nothing says which documents and timelines the product does have.",
2671
2857
  inputSchema: z.object({
2672
2858
  productId,
2673
2859
  type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type -- taiga_list_product_documents shows which ones the product has"),
@@ -2682,15 +2868,7 @@ function registerProductTools(server, client) {
2682
2868
  try {
2683
2869
  const doc = stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data);
2684
2870
  if (!params.sections) return toolResult(doc);
2685
- const cut = pickSections(doc, new Set(params.sections));
2686
- if (!cut) return toolError("VALIDATION_FAILED", `The ${origin} ${params.type} is not made of sections; read it without sections.`);
2687
- const all = Object.keys(doc.data.sections);
2688
- const unknown = params.sections.filter((id) => !all.includes(id));
2689
- if (unknown.length > 0) return toolError("NOT_FOUND", `The ${origin} ${params.type} has no section ${unknown.join(", ")}; its sections are ${all.join(", ")}.`);
2690
- return toolResult({
2691
- ...cut.doc,
2692
- omittedSections: cut.omittedSections
2693
- });
2871
+ return sectionReadResult(doc, params.sections, `${origin} ${params.type}`);
2694
2872
  } catch (error) {
2695
2873
  if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
2696
2874
  product ??= (await client.getProduct(params.productId)).data;
@@ -2812,12 +2990,16 @@ function registerDocumentTools(server, client) {
2812
2990
  }));
2813
2991
  server.registerTool("taiga_get_document", {
2814
2992
  title: "Get company document",
2815
- description: "Get the full content of one of the organization's documents, sections included. Use this when you need the security requirements, data handling rules, architecture standards or compliance controls that apply to your code, or the goals, standards and voice the organization has set down. A read that finds nothing says which documents are published and whether a draft exists.",
2816
- inputSchema: z.object({ type: tenantDocumentType }),
2993
+ description: "Get the full content of one of the organization's documents, sections included. Use this when you need the security requirements, data handling rules, architecture standards or compliance controls that apply to your code, or the goals, standards and voice the organization has set down. A policy runs to thousands of tokens: name the sections you need (the ids taiga_get_product_context lists for every policy) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds nothing says which documents are published and whether a draft exists.",
2994
+ inputSchema: z.object({
2995
+ type: tenantDocumentType,
2996
+ sections: z.array(z.string().min(1).max(200)).min(1).max(50).optional().describe("Only these sections, by id; omit for the whole document")
2997
+ }),
2817
2998
  annotations: READ_ONLY
2818
2999
  }, (params) => runTool(async () => {
2819
3000
  try {
2820
- return toolResult(stripProvenance((await client.getTenantDocument(params.type)).data));
3001
+ const doc = stripProvenance((await client.getTenantDocument(params.type)).data);
3002
+ return params.sections ? sectionReadResult(doc, params.sections, params.type) : toolResult(doc);
2821
3003
  } catch (error) {
2822
3004
  if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
2823
3005
  const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(params.type), client.listTenantDocuments({ limit: 100 })]);
@@ -2861,7 +3043,7 @@ function registerDocumentTools(server, client) {
2861
3043
  title: "List factory documents",
2862
3044
  description: `List a factory's documents, published and draft, as summaries (type, name, version, draft state). A factory has one document type, ${named(FACTORY_DOCUMENT_TYPES)}: its free-form standing guidance (domain context, tech preferences, conventions) that applies to every product it owns. Drafts are listed too (isDraft true); an empty list means neither a publication nor a draft exists. Use taiga_get_factory_document to read the full content.`,
2863
3045
  inputSchema: z.object({
2864
- factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
3046
+ factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
2865
3047
  ...pagination
2866
3048
  }),
2867
3049
  annotations: READ_ONLY
@@ -2873,7 +3055,7 @@ function registerDocumentTools(server, client) {
2873
3055
  title: "Get factory instructions",
2874
3056
  description: "Get the full content of a factory's instructions. Use this when you need the domain context, tech preferences, or conventions the factory expects every product it owns to follow. A read that finds nothing says whether a draft exists and how to write one.",
2875
3057
  inputSchema: z.object({
2876
- factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
3058
+ factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
2877
3059
  type: z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type")
2878
3060
  }),
2879
3061
  annotations: READ_ONLY
@@ -2898,7 +3080,7 @@ function registerDocumentTools(server, client) {
2898
3080
  saveDraft: (data) => client.saveFactoryDocumentDraft(factoryId, type, { data }),
2899
3081
  publish: () => client.publishFactoryDocument(factoryId, type)
2900
3082
  });
2901
- const factoryId = z.string().uuid().describe("Factory ID -- get from taiga_list_factories");
3083
+ const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
2902
3084
  const factoryDocumentType = z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type");
2903
3085
  server.registerTool("taiga_update_factory_document", {
2904
3086
  title: "Update a section of factory instructions",
@@ -2993,7 +3175,7 @@ function registerJobTools(server, client) {
2993
3175
  server.registerTool("taiga_get_job", {
2994
3176
  title: "Get an agent job",
2995
3177
  description: "Read one agent job: background work a person asked an agent for, such as an initiative request (taiga_request_initiatives). It says where the job is (pending, processing, completed, failed, cancelled), its progress and any error. A finished initiative request also says what came of it: the initiatives it added, or why it added none (already covered by an existing initiative, not readable as work, outside the product, or not allowed by a policy, with the policy, the line it runs into and the way to decide otherwise). taiga_wait_for_job waits for a job to finish.",
2996
- inputSchema: z.object({ jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it") }),
3178
+ inputSchema: z.object({ jobId: idInput("job", "the tool that started the job returned it") }),
2997
3179
  annotations: READ_ONLY
2998
3180
  }, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
2999
3181
  }
@@ -3214,7 +3396,8 @@ const ITEM_OUTCOME_FIELDS = [
3214
3396
  "archivedAt",
3215
3397
  "lifecycle"
3216
3398
  ];
3217
- const itemId = z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items");
3399
+ const planId = idInput("plan", "taiga_get_plan gives the initiative's active plan and its ID");
3400
+ const itemId = idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it");
3218
3401
  /** The plan without its steps' prose: enough to approve it or say what a run will do. */
3219
3402
  const PLAN_SUMMARY_FIELDS = [
3220
3403
  "id",
@@ -3274,9 +3457,10 @@ function registerBacklogTools(server, client) {
3274
3457
  function registerBacklogReads(server, client) {
3275
3458
  server.registerTool("taiga_list_backlog_items", {
3276
3459
  title: "List backlog items",
3277
- description: "List a product's backlog items (initiatives): titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done) and criteria, how many of its acceptance criteria are met (null before it is planned). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
3460
+ description: "List a product's backlog items (initiatives): titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done) and criteria, how many of its acceptance criteria are met (null before it is planned; met counts the criteria the agent confirmed with evidence or a person checked off, so a done item can show fewer met than it has). Use this to find the item you are working on (query finds one by title), then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
3278
3461
  inputSchema: z.object({
3279
3462
  productId,
3463
+ query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
3280
3464
  status: z.enum([
3281
3465
  "backlog",
3282
3466
  "in_progress",
@@ -3300,13 +3484,26 @@ function registerBacklogReads(server, client) {
3300
3484
  }),
3301
3485
  annotations: READ_ONLY
3302
3486
  }, (params) => runTool(async () => {
3303
- const result = await client.listBacklogItems(params.productId, params);
3304
- return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
3487
+ const { query, ...filters } = params;
3488
+ if (query === void 0) {
3489
+ const result = await client.listBacklogItems(params.productId, filters);
3490
+ return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
3491
+ }
3492
+ if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
3493
+ const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
3494
+ ...filters,
3495
+ limit: 100,
3496
+ cursor
3497
+ }), query, (row) => [row.title], params.limit ?? 25);
3498
+ return toolResult({
3499
+ ...page(itemRows(found.rows, BACKLOG_SUMMARY_FIELDS), null),
3500
+ ...found.more ? { more: SEARCH_NOTE } : {}
3501
+ });
3305
3502
  }));
3306
3503
  server.registerTool("taiga_get_backlog_item", {
3307
3504
  title: "Get backlog item",
3308
- description: "Get one backlog item in full: description, acceptance criteria (and which are met), technical context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before implementing an item, and read its runs with taiga_list_builds to see what the coding agent already did.",
3309
- inputSchema: z.object({ itemId: z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items") }),
3505
+ description: "Get one backlog item in full: description, acceptance criteria (and which are met), technical context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before implementing an item, and read its runs with taiga_list_builds to see what the coding agent already did. lifecycle says whose turn it is and what moves it on: cta start_plan is taiga_queue_backlog_item, retry_plan taiga_plan_again (planFailureCause says why the plan stopped), start_build taiga_approve_plan, resume_build taiga_resume_backlog_item, retry_build a fresh run (start over, in the web), review_pr and view_pr the pull request. needsHumanCause is set only when the line stopped on it.",
3506
+ inputSchema: z.object({ itemId }),
3310
3507
  annotations: READ_ONLY
3311
3508
  }, (params) => runTool(async () => {
3312
3509
  return toolResult((await client.getBacklogItem(params.itemId)).data);
@@ -3357,7 +3554,7 @@ function registerMove(server, client) {
3357
3554
  inputSchema: z.object({
3358
3555
  itemId,
3359
3556
  state: z.enum(USER_DROP_STATES).describe("The column to move into"),
3360
- afterId: z.string().uuid().nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
3557
+ afterId: idInput("backlog item", "taiga_list_backlog_items gives it").nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
3361
3558
  archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
3362
3559
  }),
3363
3560
  annotations: OPERATE
@@ -3434,7 +3631,7 @@ function registerQueue(server, client) {
3434
3631
  description: "Add an initiative to the product queue, the line the coding agent works: queued initiatives are planned and built one at a time, each advancing when the previous pull request merges. This starts paid work: a planner run, then a coding run that pushes commits and opens a pull request in the product repository. Whether the plan is built at once or waits for a person is the initiative's review gate: pass autoBuild: false to read the plan before anything is built (the product's default applies otherwise, and the outcome says which it is); the gate is set first and stays set if the move is then refused. Ask the person before calling this. An initiative already in the queue stays where it is unless afterId says where to go; one being planned or built cannot move (CONFLICT). Returns the initiative with its new state, its review gate and its place in the queue.",
3435
3632
  inputSchema: z.object({
3436
3633
  itemId,
3437
- afterId: z.string().uuid().nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is"),
3634
+ afterId: idInput("backlog item", "taiga_list_backlog_items gives it").nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is"),
3438
3635
  autoBuild: autoBuildInput.optional().describe("Set the initiative's review gate before queueing: false to read the plan first, true to build it at once, null to follow the product; omit to leave the gate as it is")
3439
3636
  }),
3440
3637
  annotations: OPERATE
@@ -3476,7 +3673,7 @@ function registerReviewGate(server, client) {
3476
3673
  function registerResume(server, client) {
3477
3674
  server.registerTool("taiga_resume_backlog_item", {
3478
3675
  title: "Resume backlog item",
3479
- description: "Resume 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 incomplete step (the steps already committed are kept), a refused build start is cleared and retried. This is the way to carry work on: a run is the record of an attempt, and decisions about the work are made on the initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume. Starts paid work; ask the person first.",
3676
+ description: "Resume an initiative the line stopped on, which is when needsHumanCause is set (the line sets it): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first unfinished part (the steps already committed are kept), a refused build start is cleared and retried. This is the way to carry work on: a run is the record of an attempt, and decisions about the work are made on the initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume: a plan that failed with needsHumanCause null (lifecycle.cta retry_plan, planFailureCause says why) is planned again with taiga_plan_again. Starts paid work; ask the person first.",
3480
3677
  inputSchema: z.object({ itemId }),
3481
3678
  annotations: OPERATE
3482
3679
  }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
@@ -3485,14 +3682,14 @@ function registerPlanVerdicts(server, client) {
3485
3682
  server.registerTool("taiga_approve_plan", {
3486
3683
  title: "Approve a plan",
3487
3684
  description: "Approve an initiative's plan: its build starts now, as the approver, the same start the Start run button makes. Read the plan first with taiga_get_plan and name its ID; an approval of a plan that was replaced since is refused (CONFLICT), as is one while a new plan is being generated or a build is already running. Starts a coding run that pushes commits and opens a pull request; ask the person first.",
3488
- inputSchema: z.object({ planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan") }),
3685
+ inputSchema: z.object({ planId }),
3489
3686
  annotations: OPERATE
3490
3687
  }, (params) => runTool(async () => outcome((await client.approvePlan(params.planId)).data)));
3491
3688
  server.registerTool("taiga_reject_plan", {
3492
3689
  title: "Reject a plan",
3493
3690
  description: "Send an initiative's plan back with direction: a new plan is generated with the comment as reviewer feedback and the rejected plan as its starting point, which supersedes it when the new one lands. Read the plan first with taiga_get_plan and name its ID. Starts a planner run; ask the person first.",
3494
3691
  inputSchema: z.object({
3495
- planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan"),
3692
+ planId,
3496
3693
  comment: z.string().trim().min(1).max(PLAN_REJECTION_COMMENT_MAX_CHARS).describe("What the next plan should do differently: a paragraph of direction")
3497
3694
  }),
3498
3695
  annotations: OPERATE
@@ -3538,9 +3735,24 @@ const RUN_OUTCOME_FIELDS = [
3538
3735
  "prUrl",
3539
3736
  "actions"
3540
3737
  ];
3738
+ /**
3739
+ * Where a run that ended without finishing stopped, from its steps and its
3740
+ * hardening result: a failed run with every step completed reads as nothing
3741
+ * failed, and retry_from_failed_step then reads as a mistake, when the agent
3742
+ * stalled in the hardening pass after the steps and the retry continues it.
3743
+ * Null for a run still going or that finished.
3744
+ */
3745
+ function stoppedIn(run, steps) {
3746
+ if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
3747
+ if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
3748
+ const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
3749
+ if (open) return open.status === "planned" ? `before step ${String(open.stepOrder)}, ${String(open.title)}, started (the ones before it completed)` : `step ${String(open.stepOrder)}, ${String(open.title)} (${String(open.status)})`;
3750
+ if (run.hardeningResult == null) return "after the last step, in hardening: the security scanners and their fixes the agent runs before the pull request is ready. A retry continues from there; the steps stay.";
3751
+ return "after the steps and hardening, at the pull request";
3752
+ }
3541
3753
  /** How many products the org-wide list names; past this a row keeps its productId only. */
3542
3754
  const NAMED_PRODUCTS = 100;
3543
- const buildId = z.string().uuid().describe("Build run ID -- get from taiga_list_builds");
3755
+ const buildId = idInput("build run", "taiga_list_builds gives it");
3544
3756
  /** A step without its instructions: what it is, how it stands and what it changed. */
3545
3757
  const STEP_SUMMARY_FIELDS = [
3546
3758
  "stepOrder",
@@ -3601,14 +3813,14 @@ function registerBuildTools(server, client) {
3601
3813
  description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each. A run is the record of an attempt: work is carried on from its initiative (resume and retry_from_failed_step are taiga_resume_backlog_item; start_over is in the web only for now) and stopped by moving the initiative out of Build; a maintenance run, which has no initiative, is retried from its finding. With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
3602
3814
  inputSchema: z.object({
3603
3815
  productId: productId.optional(),
3604
- backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
3816
+ backlogItemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it").optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
3605
3817
  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"),
3606
3818
  ...pagination
3607
3819
  }),
3608
3820
  annotations: READ_ONLY
3609
3821
  }, (params) => runTool(async () => {
3610
- if (!params.productId && params.backlogItemId) return toolError("VALIDATION_FAILED", "backlogItemId narrows the runs of one product: pass its productId as well (the item carries it).");
3611
- if (params.productId && params.active) return toolError("VALIDATION_FAILED", "active narrows the org-wide list only: omit productId, or read the product's runs and judge each by its status.");
3822
+ if (!params.productId && params.backlogItemId) return toolError("VALIDATION_ERROR", "backlogItemId narrows the runs of one product: pass its productId as well (the item carries it).");
3823
+ 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.");
3612
3824
  if (params.productId) {
3613
3825
  const result = await client.listBuildRuns(params.productId, params);
3614
3826
  return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
@@ -3617,7 +3829,7 @@ function registerBuildTools(server, client) {
3617
3829
  }));
3618
3830
  server.registerTool("taiga_get_build", {
3619
3831
  title: "Get build run",
3620
- description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions name what the initiative accepts for it: resume and retry_from_failed_step are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
3832
+ description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. A run that failed or was stopped says where (stoppedIn: a step, or the hardening pass after them) and when it last made progress (lastProgressAt). Its actions name what the initiative accepts for it: resume and retry_from_failed_step (which continues from the first unfinished part) are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
3621
3833
  inputSchema: z.object({
3622
3834
  buildId,
3623
3835
  full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
@@ -3625,13 +3837,18 @@ function registerBuildTools(server, client) {
3625
3837
  annotations: READ_ONLY
3626
3838
  }, (params) => runTool(async () => {
3627
3839
  const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
3840
+ const runRow = run.data;
3841
+ const rows = steps.data;
3842
+ const where = stoppedIn(runRow, rows);
3843
+ const stop = where ? { stoppedIn: where } : {};
3628
3844
  if (params.full) return toolResult({
3629
- ...run.data,
3845
+ ...runRow,
3846
+ ...stop,
3630
3847
  steps: steps.data
3631
3848
  });
3632
- const rows = steps.data;
3633
3849
  return toolResult({
3634
- ...run.data,
3850
+ ...runRow,
3851
+ ...stop,
3635
3852
  steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
3636
3853
  ...step,
3637
3854
  headline: summaryHeadline(rows[index].summary)
@@ -3922,10 +4139,12 @@ async function productContext(client, product, options) {
3922
4139
  };
3923
4140
  const parts = Object.keys(fetchers).filter(granted);
3924
4141
  const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
4142
+ const nextStep = setupNextStep(product);
3925
4143
  const bundle = { product: {
3926
4144
  ...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
3927
4145
  projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
3928
- defaultOrigin: defaultOriginFor(product)
4146
+ defaultOrigin: defaultOriginFor(product),
4147
+ ...nextStep ? { nextStep } : {}
3929
4148
  } };
3930
4149
  const unavailable = {};
3931
4150
  parts.forEach((part, index) => {
@@ -3938,7 +4157,7 @@ async function productContext(client, product, options) {
3938
4157
  ...bundle,
3939
4158
  notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
3940
4159
  unavailable,
3941
- readNext: "Read a policy in full with taiga_get_document, another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item (taiga_list_backlog_items pages a truncated backlog) and a run with taiga_get_build."
4160
+ readNext: "Read a policy with taiga_get_document (sections: the ids listed here, for only those), another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item (taiga_list_backlog_items pages a truncated backlog) and a run with taiga_get_build."
3942
4161
  };
3943
4162
  let characters = JSON.stringify(result, null, 2).length;
3944
4163
  for (let measured = -1; measured !== characters;) {
@@ -3989,7 +4208,7 @@ function registerAutonomyTools(server, client) {
3989
4208
  }),
3990
4209
  annotations: OPERATE_SETTING
3991
4210
  }, (params) => runTool(async () => {
3992
- if (params.autoBuild === void 0 && params.autoMerge === void 0) return toolError("VALIDATION_FAILED", "Set autoBuild, autoMerge or both.");
4211
+ if (params.autoBuild === void 0 && params.autoMerge === void 0) return toolError("VALIDATION_ERROR", "Set autoBuild, autoMerge or both.");
3993
4212
  const autoMerge = params.autoMerge === true ? null : params.autoMerge;
3994
4213
  const product = (await client.updateProduct(params.productId, {
3995
4214
  ...params.autoBuild !== void 0 ? { autoBuild: params.autoBuild } : {},
@@ -4007,7 +4226,7 @@ function registerAutonomyTools(server, client) {
4007
4226
  title: "Set whether an initiative's pull request merges on its own",
4008
4227
  description: "An initiative's own switch for merging on its own: true lets its pull request merge itself once the repository's own rules pass (required checks, reviews, a code reviewer you made Taiga wait for); false makes it wait for a person; null follows the product's default. The final word within what the organization and factories allow: where either has turned autonomous merge off, true changes nothing. Taiga merges through your rules, never around them. Ask the person first.",
4009
4228
  inputSchema: z.object({
4010
- itemId: z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items"),
4229
+ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
4011
4230
  autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
4012
4231
  }),
4013
4232
  annotations: OPERATE_SETTING
@@ -4027,6 +4246,7 @@ function registerAutonomyTools(server, client) {
4027
4246
  const ITEM_FIELDS = [
4028
4247
  "id",
4029
4248
  "productId",
4249
+ "productName",
4030
4250
  "category",
4031
4251
  "kind",
4032
4252
  "subjectType",
@@ -4043,6 +4263,7 @@ const ITEM_FIELDS = [
4043
4263
  const SETUP_TASK_FIELDS = [
4044
4264
  "id",
4045
4265
  "productId",
4266
+ "productName",
4046
4267
  "category",
4047
4268
  "phase",
4048
4269
  "target",
@@ -4059,7 +4280,7 @@ function registerInboxTools(server, client) {
4059
4280
  if (!client.hasCapability("products:read")) return;
4060
4281
  server.registerTool("taiga_list_action_required", {
4061
4282
  title: "List what needs a person",
4062
- description: "Read Action Required, where work comes back to a person: things to review (a plan, a set of initiatives, a document, a pull request, maintenance findings, new policy violations), each a decision an agent handed back rather than made, and things to set up that only a person can (a secret, an environment variable, an account, a package registry, a cloud permission). Across every product the key can open, or one product. category gate is the reviews, setup the setup items; resolved: true reads the archive of done and dismissed ones. The inbox is shared: it is the team's, not one person's. An item snoozed until later is left out until then, as the page leaves it out (snoozedHidden counts them). taiga_list_setup_tasks has the setup tasks with what exactly to set.",
4283
+ description: "Read Action Required, where work comes back to a person: things to review (a plan, a set of initiatives, a document, a pull request, maintenance findings, new policy violations), each a decision an agent handed back rather than made, and things to set up that only a person can (a secret, an environment variable, an account, a package registry, a cloud permission). Across every product the key can open (each row names its product), or one product. category gate is the reviews, setup the setup items; resolved: true reads the archive of done and dismissed ones. The inbox is shared: it is the team's, not one person's. An item snoozed until later is left out until then, as the page leaves it out (snoozedHidden counts them). taiga_list_setup_tasks has the setup tasks with what exactly to set.",
4063
4284
  inputSchema: z.object({
4064
4285
  productId: productId.optional(),
4065
4286
  category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
@@ -4084,14 +4305,19 @@ function registerInboxTools(server, client) {
4084
4305
  }));
4085
4306
  server.registerTool("taiga_list_setup_tasks", {
4086
4307
  title: "List setup tasks",
4087
- description: "The setup tasks agents raised for work only a person can do, with what exactly to set and where: the name of an environment variable, secret or API key to create (key; never its value, which Taiga does not store or display), the provider page to do it on, and the phase that says when it blocks you (prereq: before the deploy, which fails without it; wiring: connecting generated values back to providers; followup: billing approvals, extra permissions and the like that can wait until after launch). A build does not wait for any of them. Across every product the key can open, or one.",
4308
+ description: "The setup tasks agents raised for work only a person can do, with what exactly to set and where: the name of an environment variable, secret or API key to create (key; never its value, which Taiga does not store or display), the provider page to do it on, and the phase that says when it blocks you (prereq: before the deploy, which fails without it; wiring: connecting generated values back to providers; followup: billing approvals, extra permissions and the like that can wait until after launch). A build does not wait for any of them. Across every product the key can open (each row names its product), or one. The tasks still to do, oldest first, the order they depend on each other; includeDone: true adds the done and dismissed ones.",
4088
4309
  inputSchema: z.object({
4089
4310
  productId: productId.optional(),
4311
+ includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
4090
4312
  ...pagination
4091
4313
  }),
4092
4314
  annotations: READ_ONLY
4093
4315
  }, (params) => runTool(async () => {
4094
- const result = await client.listSetupTasks(params);
4316
+ const { includeDone, ...rest } = params;
4317
+ const result = await client.listSetupTasks({
4318
+ ...rest,
4319
+ ...includeDone ? {} : { openOnly: true }
4320
+ });
4095
4321
  return toolResult(page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor));
4096
4322
  }));
4097
4323
  if (!client.hasCapability("products:write")) return;
@@ -4099,7 +4325,7 @@ function registerInboxTools(server, client) {
4099
4325
  title: "Resolve or snooze an Action Required item",
4100
4326
  description: "Clear an item from Action Required, for everybody: done means you did it, dismissed means it does not apply and is not going to, in_progress means someone has picked it up. Or snooze it until a time (an hour, tomorrow, a week; null wakes it). Nothing marks a setup task done for you: setting the variable or connecting the registry is the work, and marking it done is still a person's. Resolving a review item does not approve or merge anything; the plan or pull request is answered where it lives. One exception: marking the product documents review item done records, in your name, that every published document was reviewed, and setting it back to in_progress withdraws that; do it only when the person says they read them. Ask the person before clearing what they have not dealt with.",
4101
4327
  inputSchema: z.object({
4102
- itemId: z.string().uuid().describe("Action Required item ID -- taiga_list_action_required gives it"),
4328
+ itemId: idInput("Action Required item", "taiga_list_action_required gives it"),
4103
4329
  status: z.enum([
4104
4330
  "in_progress",
4105
4331
  "done",
@@ -4109,7 +4335,7 @@ function registerInboxTools(server, client) {
4109
4335
  }),
4110
4336
  annotations: OPERATE
4111
4337
  }, (params) => runTool(async () => {
4112
- if (params.status === void 0 && params.snoozedUntil === void 0) return toolError("VALIDATION_FAILED", "Say what became of the item (status) or until when to snooze it.");
4338
+ if (params.status === void 0 && params.snoozedUntil === void 0) return toolError("VALIDATION_ERROR", "Say what became of the item (status) or until when to snooze it.");
4113
4339
  return toolResult(pickFields([(await client.updateInboxItem(params.itemId, {
4114
4340
  ...params.status !== void 0 ? { status: params.status } : {},
4115
4341
  ...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
@@ -4133,10 +4359,10 @@ const KNOWLEDGE_FIELDS = [
4133
4359
  ];
4134
4360
  /** Past this many characters a text file's content is cut when read; the download URL has it all. */
4135
4361
  const TEXT_LIMIT = 2e4;
4136
- const knowledgeId = z.string().uuid().describe("Knowledge file ID -- taiga_list_knowledge gives it");
4362
+ const knowledgeId = idInput("knowledge file", "taiga_list_knowledge gives it");
4137
4363
  const scopeInputs = {
4138
- factoryId: z.string().uuid().optional().describe("A factory's knowledge"),
4139
- productId: z.string().uuid().optional().describe("A product's knowledge")
4364
+ factoryId: idInput("factory", "taiga_list_factories gives it").optional().describe("A factory's knowledge"),
4365
+ productId: productId.optional().describe("A product's knowledge")
4140
4366
  };
4141
4367
  /** Bytes of a text file the read takes at most; the download URL has the rest. */
4142
4368
  const READ_BYTES = 262144;
@@ -4200,7 +4426,7 @@ function registerKnowledgeTools(server, client) {
4200
4426
  annotations: READ_ONLY
4201
4427
  }, (params) => runTool(async () => {
4202
4428
  const twoLevels = oneLevel(params);
4203
- if (twoLevels) return toolError("VALIDATION_FAILED", twoLevels);
4429
+ if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
4204
4430
  const scope = params.productId ? { productId: params.productId } : params.factoryId ? {
4205
4431
  factoryId: params.factoryId,
4206
4432
  productId: null
@@ -4257,7 +4483,7 @@ function registerKnowledgeTools(server, client) {
4257
4483
  annotations: OPERATE
4258
4484
  }, (params) => runTool(async () => {
4259
4485
  const twoLevels = oneLevel(params);
4260
- if (twoLevels) return toolError("VALIDATION_FAILED", twoLevels);
4486
+ if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
4261
4487
  const mime = params.fileName.toLowerCase().endsWith(".md") ? "text/markdown" : "text/plain";
4262
4488
  const result = await client.addKnowledge({
4263
4489
  fileName: params.fileName,
@@ -4343,7 +4569,7 @@ const VIOLATION_FIELDS = [
4343
4569
  "verifyRequestedAt",
4344
4570
  "assessedAt"
4345
4571
  ];
4346
- const findingId = z.string().uuid().describe("Finding ID -- taiga_list_findings gives it");
4572
+ const findingId = idInput("finding", "taiga_list_findings gives it");
4347
4573
  function registerMaintenanceTools(server, client) {
4348
4574
  if (!client.hasCapability("products:read")) return;
4349
4575
  server.registerTool("taiga_get_maintenance", {
@@ -4388,7 +4614,7 @@ function registerMaintenanceTools(server, client) {
4388
4614
  }, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
4389
4615
  server.registerTool("taiga_get_policy_standing", {
4390
4616
  title: "Get a product's policy standing",
4391
- description: "Where a product stands against the organization's own published policies: the controls it violates, each with its violations (critical and high only, each citing the policy's sentence and the document passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, awaiting verification), and when the documents were derived and assessed. A control with no violation is not listed, and it is not \"passing\" either: there is no such state. Pass full: true for every violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
4617
+ description: "Where a product stands against the organization's own published policies: the controls it violates, each with its violations (critical and high only, each citing the policy's sentence and the document passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, awaiting verification), and when the documents were derived and assessed, led by a summary of the counts. A control with no violation is not listed, and it is not \"passing\" either: there is no such state. Pass full: true for every violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
4392
4618
  inputSchema: z.object({
4393
4619
  productId,
4394
4620
  full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
@@ -4396,16 +4622,32 @@ function registerMaintenanceTools(server, client) {
4396
4622
  annotations: READ_ONLY
4397
4623
  }, (params) => runTool(async () => {
4398
4624
  const standing = (await client.getPolicyStanding(params.productId)).data;
4399
- if (params.full) return toolResult(standing);
4400
4625
  const controls = Array.isArray(standing.controls) ? standing.controls : [];
4401
4626
  const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
4402
4627
  const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
4628
+ const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
4629
+ const bySeverity = {};
4630
+ for (const violation of violations) {
4631
+ const severity = String(violation.severity ?? "unknown");
4632
+ bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
4633
+ }
4634
+ const summary = {
4635
+ controls: listed.length,
4636
+ violations: violations.length,
4637
+ bySeverity,
4638
+ resolvedViolations: resolved
4639
+ };
4640
+ if (params.full) return toolResult({
4641
+ summary,
4642
+ ...standing
4643
+ });
4403
4644
  return toolResult({
4645
+ summary,
4404
4646
  productId: standing.productId,
4405
4647
  hasPolicies: standing.hasPolicies,
4406
4648
  assessment: standing.assessment,
4407
4649
  documents: standing.documents,
4408
- controls: pickFields(controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0), [
4650
+ controls: pickFields(listed, [
4409
4651
  "controlId",
4410
4652
  "policyType",
4411
4653
  "text",
@@ -4414,8 +4656,7 @@ function registerMaintenanceTools(server, client) {
4414
4656
  "findingCount",
4415
4657
  "plannedCount"
4416
4658
  ]),
4417
- violations: pickFields(violations, VIOLATION_FIELDS),
4418
- resolvedViolations: resolved
4659
+ violations: pickFields(violations, VIOLATION_FIELDS)
4419
4660
  });
4420
4661
  }));
4421
4662
  if (!client.hasCapability("products:write")) return;
@@ -4431,7 +4672,7 @@ function registerMaintenanceTools(server, client) {
4431
4672
  }),
4432
4673
  annotations: OPERATE
4433
4674
  }, (params) => runTool(async () => {
4434
- if (params.ignored && params.reason === void 0) return toolError("VALIDATION_FAILED", "Say why the finding is ignored (reason).");
4675
+ if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
4435
4676
  const body = params.ignored ? {
4436
4677
  status: "ignored",
4437
4678
  ignoreReason: params.reason,
@@ -4473,11 +4714,11 @@ function registerMaintenanceTools(server, client) {
4473
4714
  inputSchema: z.object({
4474
4715
  productId,
4475
4716
  controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
4476
- violationId: z.string().uuid().optional().describe("One violation -- taiga_get_policy_standing gives it")
4717
+ violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
4477
4718
  }),
4478
4719
  annotations: OPERATE
4479
4720
  }, (params) => runTool(async () => {
4480
- if (params.controlId === void 0 === (params.violationId === void 0)) return toolError("VALIDATION_FAILED", "Name either a control (controlId) or one violation (violationId), not both or neither.");
4721
+ if (params.controlId === void 0 === (params.violationId === void 0)) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both or neither.");
4481
4722
  return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
4482
4723
  }));
4483
4724
  }
@@ -4511,7 +4752,7 @@ const ENVIRONMENT_FIELDS = [
4511
4752
  "isProtected",
4512
4753
  "url"
4513
4754
  ];
4514
- const environmentId = z.string().uuid().describe("Environment ID -- taiga_get_deployments lists them");
4755
+ const environmentId = idInput("environment", "taiga_get_deployments lists them; a product ID is not one");
4515
4756
  const windowDays = (choices) => z.number().int().optional().describe(`Days to look back: ${choices} (default 30)`);
4516
4757
  /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
4517
4758
  const isApiFailure = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
@@ -4570,6 +4811,7 @@ function registerObserveTools(server, client) {
4570
4811
  part(() => client.getDeploymentSyncStatus(params.productId))
4571
4812
  ]);
4572
4813
  const now = current.data;
4814
+ const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_define_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;
4573
4815
  return toolResult({
4574
4816
  environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
4575
4817
  ...environments.more ? { moreEnvironments: true } : {},
@@ -4579,7 +4821,8 @@ function registerObserveTools(server, client) {
4579
4821
  ..."value" in sync ? { syncStatus: sync.value.data } : {
4580
4822
  syncStatus: null,
4581
4823
  syncUnavailable: sync.unavailable
4582
- }
4824
+ },
4825
+ ...note ? { note } : {}
4583
4826
  });
4584
4827
  }));
4585
4828
  server.registerTool("taiga_get_delivery_metrics", {
@@ -4597,7 +4840,7 @@ function registerObserveTools(server, client) {
4597
4840
  30,
4598
4841
  60,
4599
4842
  90
4600
- ].includes(days)) return toolError("VALIDATION_FAILED", "windowDays is 30, 60 or 90.");
4843
+ ].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90.");
4601
4844
  return toolResult((await client.getDeliveryMetrics(params.productId, params.environmentId, days)).data);
4602
4845
  }));
4603
4846
  server.registerTool("taiga_get_monitoring", {
@@ -4611,22 +4854,32 @@ function registerObserveTools(server, client) {
4611
4854
  annotations: READ_ONLY
4612
4855
  }, (params) => runTool(async () => {
4613
4856
  const health = await client.getMonitoringHealth(params.productId);
4614
- if (!params.environmentId) return toolResult({ health: health.data });
4857
+ if (!params.environmentId) return toolResult({
4858
+ health: health.data,
4859
+ ...health.data.length === 0 ? { note: "No environment is monitored: monitoring starts when an environment has its App URL (taiga_define_environment, taiga_update_environment). No health is not good health." } : {}
4860
+ });
4615
4861
  const days = params.windowDays ?? 30;
4616
4862
  if (![
4617
4863
  30,
4618
4864
  60,
4619
4865
  90
4620
- ].includes(days)) return toolError("VALIDATION_FAILED", "windowDays is 30, 60 or 90, the windows the page offers.");
4866
+ ].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90, the windows the page offers.");
4621
4867
  const env = params.environmentId;
4622
4868
  const [uptime, experience, clientHealth] = await Promise.all([
4623
4869
  part(() => client.getMonitoringSignal(params.productId, "uptime", env, days)),
4624
4870
  part(() => client.getMonitoringSignal(params.productId, "rum", env, days)),
4625
4871
  part(() => client.getMonitoringSignal(params.productId, "client-health", env, days))
4626
4872
  ]);
4873
+ if ("unavailable" in uptime && [
4874
+ uptime,
4875
+ experience,
4876
+ clientHealth
4877
+ ].every((p) => "unavailable" in p && p.unavailable.startsWith("404"))) return toolError("NOT_FOUND", withNotFoundHint(uptime.unavailable.replace(/^404:\s*/, "") || "Environment not found"));
4627
4878
  const value = (p) => "value" in p ? p.value.data : { unavailable: p.unavailable };
4879
+ const envHealth = health.data.filter((row) => row.environmentId === env);
4628
4880
  return toolResult({
4629
- health: health.data.filter((row) => row.environmentId === env),
4881
+ health: envHealth,
4882
+ ...envHealth.length === 0 ? { note: "No health for this environment: it has no App URL to check yet (taiga_update_environment sets one). No health is not good health." } : {},
4630
4883
  windowDays: days,
4631
4884
  uptime: value(uptime),
4632
4885
  realUserExperience: value(experience),
@@ -4662,7 +4915,7 @@ function registerObserveTools(server, client) {
4662
4915
  annotations: OPERATE_SETTING
4663
4916
  }, (params) => runTool(async () => {
4664
4917
  const { environmentId: id, ...changes } = params;
4665
- if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_FAILED", "Change at least one of name, type or url.");
4918
+ if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_ERROR", "Change at least one of name, type or url.");
4666
4919
  const updated = (await client.updateEnvironment(id, changes)).data;
4667
4920
  return toolResult(pickFields([updated], ENVIRONMENT_FIELDS)[0]);
4668
4921
  }));
@@ -19386,7 +19639,7 @@ function registerPrompts(server, client) {
19386
19639
  if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_backlog_item", {
19387
19640
  title: "Implement a backlog item",
19388
19641
  description: "Implement one initiative: its description and acceptance criteria, the architecture of the product from its own timeline, the factory's instructions and the policy sections to check against. taiga_list_backlog_items gives the item ID.",
19389
- argsSchema: z.object({ itemId: productId.describe("The backlog item ID") })
19642
+ argsSchema: z.object({ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it") })
19390
19643
  }, async (args) => {
19391
19644
  const item = (await served$1(client.getBacklogItem(args.itemId), "No backlog item with that ID the key can see; taiga_list_backlog_items shows what exists")).data;
19392
19645
  const productId = String(item.productId);
@@ -19481,7 +19734,7 @@ const GITHUB_REPOSITORY_FIELDS = [
19481
19734
  "description",
19482
19735
  "language"
19483
19736
  ];
19484
- const repositoryId = z.string().uuid().describe("The product's repository link ID -- taiga_list_product_repositories gives it");
19737
+ const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
19485
19738
  function registerRepositoryTools(server, client) {
19486
19739
  if (client.hasCapability("products:read")) {
19487
19740
  server.registerTool("taiga_list_product_repositories", {
@@ -19513,7 +19766,7 @@ function registerRepositoryTools(server, client) {
19513
19766
  description: "The repositories the organization's GitHub connection can reach, to link one to a product (taiga_link_repository takes its id). Searches by name with query; without installationId it reads every connected GitHub account. No connection means GitHub is not connected yet: that is done once, in the web, by someone who can install the app.",
19514
19767
  inputSchema: z.object({
19515
19768
  query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
19516
- installationId: z.string().uuid().optional().describe("One GitHub account -- this tool lists them"),
19769
+ installationId: idInput("GitHub account", "this tool lists them").optional(),
19517
19770
  ...pagination
19518
19771
  }),
19519
19772
  annotations: READ_ONLY
@@ -19523,8 +19776,8 @@ function registerRepositoryTools(server, client) {
19523
19776
  if (installations.length === 0) return toolError("NOT_CONNECTED", "GitHub is not connected for this organization. It is connected once, in the web; then link.");
19524
19777
  const chosen = params.installationId ? installations.filter((i) => i.id === params.installationId) : installations;
19525
19778
  if (chosen.length === 0) return toolError("NOT_FOUND", "No such GitHub connection; these are the connected accounts.", { accounts });
19526
- if (params.cursor !== void 0 && params.query !== void 0) return toolError("VALIDATION_FAILED", "A search answers one page; narrow the query instead of paging it.");
19527
- if (chosen.length > 1 && params.cursor !== void 0) return toolError("VALIDATION_FAILED", "A cursor pages one account: name its installationId.", { accounts });
19779
+ if (params.cursor !== void 0 && params.query !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
19780
+ if (chosen.length > 1 && params.cursor !== void 0) return toolError("VALIDATION_ERROR", "A cursor pages one account: name its installationId.", { accounts });
19528
19781
  const max = params.limit ?? 25;
19529
19782
  const rows = (data) => pickFields(data, GITHUB_REPOSITORY_FIELDS);
19530
19783
  if (chosen.length === 1) {
@@ -19558,7 +19811,7 @@ function registerRepositoryTools(server, client) {
19558
19811
  description: "Link a repository the GitHub connection can reach (taiga_list_linkable_repositories gives its id) to a product, or with replacesRepositoryId swap the one linked now for it in one step (naming the same repository there changes nothing; linking one already linked without it is refused). A link Taiga manages (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported in the background. Ask the person first.",
19559
19812
  inputSchema: z.object({
19560
19813
  productId,
19561
- githubRepositoryId: z.string().uuid().describe("The repository id -- taiga_list_linkable_repositories gives it"),
19814
+ githubRepositoryId: idInput("GitHub repository", "taiga_list_linkable_repositories gives it"),
19562
19815
  replacesRepositoryId: repositoryId.optional().describe("The link this one replaces")
19563
19816
  }),
19564
19817
  annotations: OPERATE
@@ -19579,7 +19832,7 @@ function registerRepositoryTools(server, client) {
19579
19832
  }),
19580
19833
  annotations: OPERATE_SETTING
19581
19834
  }, (params) => runTool(async () => {
19582
- if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_FAILED", "Set baseBranch, label or both.");
19835
+ if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_ERROR", "Set baseBranch, label or both.");
19583
19836
  const updated = (await client.updateRepository(params.productId, params.repositoryId, {
19584
19837
  ...params.baseBranch !== void 0 ? { baseBranch: params.baseBranch } : {},
19585
19838
  ...params.label !== void 0 ? { label: params.label } : {}
@@ -19746,26 +19999,56 @@ const FACTORY_FIELDS = [
19746
19999
  "slug",
19747
20000
  "isDefault"
19748
20001
  ];
19749
- const factoryId = z.string().uuid().describe("Factory ID -- taiga_list_factories gives it");
20002
+ const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
19750
20003
  const buildScope = {
19751
20004
  ownsInfrastructure: z.boolean().nullable().optional().describe("Whether Taiga writes the infrastructure code; null follows the organization"),
19752
20005
  ownsDeployment: z.boolean().nullable().optional().describe("Whether Taiga writes the CI/CD pipelines; null follows the organization")
19753
20006
  };
19754
20007
  const deploymentNotes = z.string().trim().min(1).max(DEPLOYMENT_NOTES_MAX_LENGTH).describe("When a half is off: a sentence on how it is done today, which Taiga designs and plans against");
20008
+ /** How a missing required document comes to exist, said with a documents_incomplete refusal. */
20009
+ const MISSING_DOCUMENTS_HOW = "An imported product gets them from its import (taiga_start_import); a product started from scratch gets the specification from the Discovery conversation in the web and the rest from taiga_generate_product_documents. A draft is published with taiga_publish_product_document.";
19755
20010
  /** A product row the model can act on, saying how it started as the other product tools say it. */
19756
- function productRow(product) {
20011
+ async function productRow(client, product) {
19757
20012
  const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
20013
+ const nextStep = setupNextStep(await withDiscoveryStep(client, product));
19758
20014
  return {
19759
20015
  ...rest,
19760
- projectType: isImportedProduct(product) ? "brownfield" : "greenfield"
20016
+ projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
20017
+ ...nextStep ? { nextStep } : {}
19761
20018
  };
19762
20019
  }
19763
20020
  /**
20021
+ * The product with its discoveryStep. The API derives it on reads only, not
20022
+ * on the answer to a write (products.ts), and a draft started from scratch
20023
+ * needs it to say what comes next; an imported one reads its import status
20024
+ * instead. Best effort: a failed read leaves the product without a nextStep
20025
+ * rather than failing the write that already happened.
20026
+ */
20027
+ async function withDiscoveryStep(client, product) {
20028
+ if (product.status !== "draft" || "discoveryStep" in product || isImportedProduct(product)) return product;
20029
+ try {
20030
+ return (await client.getProduct(String(product.id))).data;
20031
+ } catch (error) {
20032
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return product;
20033
+ throw error;
20034
+ }
20035
+ }
20036
+ /**
20037
+ * A half turned off with nothing said about how it is done today. Allowed (the
20038
+ * web asks for the sentence but does not insist), so a warning rather than a
20039
+ * refusal: plans then design against a gap.
20040
+ */
20041
+ function missingNotesWarning(product) {
20042
+ const off = product.ownsInfrastructure === false || product.ownsDeployment === false;
20043
+ const notes = typeof product.deploymentNotes === "string" ? product.deploymentNotes.trim() : "";
20044
+ return off && !notes ? ["A half Taiga does not build has no deploymentNotes: plans cannot design against how it is done today. Ask the person for a sentence and set it."] : [];
20045
+ }
20046
+ /**
19764
20047
  * The sentence is for a half Taiga does not build; the web asks for it only
19765
20048
  * then. When both halves are decided on here, a sentence has nothing to say.
19766
20049
  */
19767
20050
  function notesWithNothingToDescribe(params) {
19768
- return typeof params.deploymentNotes === "string" && params.ownsInfrastructure === true && params.ownsDeployment === true ? toolError("VALIDATION_FAILED", "deploymentNotes describes a half Taiga does not build; both are on, so leave it out.") : null;
20051
+ return typeof params.deploymentNotes === "string" && params.ownsInfrastructure === true && params.ownsDeployment === true ? toolError("VALIDATION_ERROR", "deploymentNotes describes a half Taiga does not build; both are on, so leave it out.") : null;
19769
20052
  }
19770
20053
  function registerSetupTools(server, client) {
19771
20054
  if (client.hasCapability("factories:write")) {
@@ -19808,7 +20091,7 @@ function registerSetupTools(server, client) {
19808
20091
  ...params.stopLine ? { stopLine: true } : {}
19809
20092
  });
19810
20093
  return toolResult({
19811
- product: productRow(moved.data),
20094
+ product: await productRow(client, moved.data),
19812
20095
  changed: moved.changed,
19813
20096
  previousFactoryId: moved.previousFactoryId,
19814
20097
  lineStopped: moved.lineStopped,
@@ -19839,7 +20122,7 @@ function registerSetupTools(server, client) {
19839
20122
  ...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
19840
20123
  ...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {}
19841
20124
  })).data;
19842
- return toolResult(productRow(product));
20125
+ return toolResult(await productRow(client, product));
19843
20126
  }));
19844
20127
  server.registerTool("taiga_update_product", {
19845
20128
  title: "Change a product's settings",
@@ -19854,10 +20137,15 @@ function registerSetupTools(server, client) {
19854
20137
  annotations: OPERATE_SETTING
19855
20138
  }, (params) => runTool(async () => {
19856
20139
  const { productId: id, ...changes } = params;
19857
- if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_FAILED", "Change at least one setting.");
20140
+ if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_ERROR", "Change at least one setting.");
19858
20141
  const contradiction = notesWithNothingToDescribe(params);
19859
20142
  if (contradiction) return contradiction;
19860
- return toolResult(productRow((await client.updateProduct(id, changes)).data));
20143
+ const updated = (await client.updateProduct(id, changes)).data;
20144
+ const warnings = missingNotesWarning(updated);
20145
+ return toolResult({
20146
+ ...await productRow(client, updated),
20147
+ ...warnings.length > 0 ? { warnings } : {}
20148
+ });
19861
20149
  }));
19862
20150
  server.registerTool("taiga_refresh_reality_documents", {
19863
20151
  title: "Refresh a product's documents from its code",
@@ -19870,16 +20158,23 @@ function registerSetupTools(server, client) {
19870
20158
  }, (params) => runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data)));
19871
20159
  server.registerTool("taiga_finish_discovery", {
19872
20160
  title: "Finish Discovery",
19873
- description: "Finish a product's Discovery. Refused (CONFLICT) with details.reason: documents_incomplete names the required documents still unpublished (details.missingDocuments); not_in_setup means it is already finished; a required check left unstamped (stamped in the web) is refused with its details too. It locks the document set and opens the rest of the product; for an imported product it is also when the first initiatives are planned. A locked document changes only after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.",
20161
+ description: "Finish a product's Discovery. Refused (CONFLICT) with details.reason: documents_incomplete names the required documents still unpublished (details.missingDocuments) and how they are made; not_in_setup means it is already finished; a required check left unstamped (stamped in the web) is refused with its details too. It locks the document set and opens the rest of the product; for an imported product it is also when the first initiatives are planned. A locked document changes only after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.",
19874
20162
  inputSchema: z.object({ productId }),
19875
20163
  annotations: OPERATE
19876
- }, (params) => runTool(async () => toolResult(productRow((await client.completeSetup(params.productId)).data))));
20164
+ }, (params) => runTool(async () => {
20165
+ try {
20166
+ return toolResult(await productRow(client, (await client.completeSetup(params.productId)).data));
20167
+ } catch (error) {
20168
+ if (!(error instanceof TaigaApiError) || error.details?.reason !== "documents_incomplete") throw error;
20169
+ return toolError(error.code ?? "CONFLICT", `${withStop(error.apiMessage ?? "Required documents are not published yet")} ${MISSING_DOCUMENTS_HOW}`, error.details);
20170
+ }
20171
+ }));
19877
20172
  server.registerTool("taiga_reopen_discovery", {
19878
20173
  title: "Reopen Discovery",
19879
20174
  description: "Reopen a finished product's Discovery so its documents can be amended; finish it again afterwards. Nothing built from them is undone. Ask the person first.",
19880
20175
  inputSchema: z.object({ productId }),
19881
20176
  annotations: OPERATE
19882
- }, (params) => runTool(async () => toolResult(productRow((await client.reopenSetup(params.productId)).data))));
20177
+ }, (params) => runTool(async () => toolResult(await productRow(client, (await client.reopenSetup(params.productId)).data))));
19883
20178
  if (!client.hasCapability("products:read")) return;
19884
20179
  server.registerTool("taiga_start_import", {
19885
20180
  title: "Import a codebase into a product",
@@ -20159,7 +20454,7 @@ function registerWaitTools(server, client, options) {
20159
20454
  title: "Wait for an initiative",
20160
20455
  description: "Wait until an initiative reaches a point, instead of reading it again and again: changed (anything about where it is or its latest run), planned (its plan landed), your_turn (it waits on a person: a plan to review, a pull request, a flag), pr_open (a pull request open now; a merged or closed one does not count), run_settled (its latest run stopped moving) or done (finished; a canceled one stops the wait). Returns as soon as that holds (reached: true), early when it cannot hold without a person (a flag, a failed plan, a plan waiting for its verdict, a pull request waiting for a review, a cancelled initiative, or a landed plan whose review gate cannot be read; stoppedBecause says which), or at the timeout with reached: false: call again to keep waiting. Conditions are states, not events: an initiative whose latest run is already settled answers run_settled at once; item is null when not even the first read answered in time. Planning takes minutes and a build up to an hour.",
20161
20456
  inputSchema: z.object({
20162
- itemId: z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items"),
20457
+ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
20163
20458
  until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
20164
20459
  timeoutSeconds
20165
20460
  }),
@@ -20192,7 +20487,7 @@ function registerWaitTools(server, client, options) {
20192
20487
  title: "Wait for a run",
20193
20488
  description: "Wait until a coding agent's run reaches a point: changed (status, steps or pull request), step_done (one more step finished), pr_open (open now, not merged or closed) or settled (it stopped moving: in review, completed, failed, stopped). Returns as soon as that holds (reached: true), early when the run settled without it (stoppedBecause; changed waits on, since a person resuming the run is a change), or at the timeout with reached: false: call again to keep waiting. Conditions are states: a run already settled answers settled at once; run is null when not even the first read answered in time.",
20194
20489
  inputSchema: z.object({
20195
- buildId: z.string().uuid().describe("Build run ID -- get from taiga_list_builds"),
20490
+ buildId: idInput("build run", "taiga_list_builds gives it"),
20196
20491
  until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
20197
20492
  timeoutSeconds
20198
20493
  }),
@@ -20213,7 +20508,7 @@ function registerWaitTools(server, client, options) {
20213
20508
  title: "Wait for an agent job",
20214
20509
  description: "Wait until an agent job finishes (completed, failed or cancelled): an initiative request, a document, a refresh. Returns the job as taiga_get_job reads it, with what came of an initiative request, as soon as it settles (reached: true), or at the timeout with reached: false: call again to keep waiting. Most jobs take a few minutes.",
20215
20510
  inputSchema: z.object({
20216
- jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it"),
20511
+ jobId: idInput("job", "the tool that started the job returned it"),
20217
20512
  timeoutSeconds
20218
20513
  }),
20219
20514
  annotations: READ_ONLY
@@ -20232,7 +20527,8 @@ function registerWaitTools(server, client, options) {
20232
20527
  //#endregion
20233
20528
  //#region src/tools/index.ts
20234
20529
  /**
20235
- * Every tool refuses an argument it does not take. A plain object schema
20530
+ * Every tool refuses an argument it does not take, and every tool's 404 says
20531
+ * where the right IDs are (hints.ts). A plain object schema
20236
20532
  * strips one silently: a review note passed to a tool with no place for it
20237
20533
  * vanished and the call still succeeded, and a misspelt option quietly
20238
20534
  * became its default. Strict, the call fails with "Unrecognized key" and the
@@ -20244,10 +20540,12 @@ function withStrictInputs(server) {
20244
20540
  server.registerTool = ((name, config, callback) => {
20245
20541
  const schema = config.inputSchema;
20246
20542
  if (schema !== void 0 && !(schema instanceof z.ZodObject)) throw new Error(`Tool ${name}: inputSchema must be a z.object() so unknown arguments can be refused`);
20543
+ const handler = callback;
20544
+ const withHint = (args, ctx) => notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
20247
20545
  return register(name, schema ? {
20248
20546
  ...config,
20249
20547
  inputSchema: schema.strict()
20250
- } : config, callback);
20548
+ } : config, withHint);
20251
20549
  });
20252
20550
  return server;
20253
20551
  }
@@ -20277,17 +20575,30 @@ function registerAllTools(server, client, options = {}) {
20277
20575
  registerResources(server, client);
20278
20576
  registerPrompts(server, client);
20279
20577
  }
20280
- //#endregion
20281
- //#region src/tools/instructions.ts
20282
- /**
20283
- * What the server tells a client at connect time: the knowledge base, the
20284
- * order to read it in, the two document timelines, what section metadata
20285
- * means, and when to write. Kept as data so a test can hold it to the
20286
- * catalogue the same way it holds the tool descriptions.
20287
- */
20288
20578
  const SERVER_INSTRUCTIONS = [
20289
- "You have access to Taiga, a policy-aware software development platform: the organization's",
20290
- "engineering knowledge base, its products, their backlog and what the coding agent has built.",
20579
+ [
20580
+ "You have access to Taiga, a policy-aware software development platform: the organization's",
20581
+ "engineering knowledge base, its products, their backlog and what the coding agent has built.",
20582
+ "",
20583
+ "**Start here.** Call taiga_get_product_context first, naming the product by its git remote URL inside a",
20584
+ "checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with",
20585
+ "query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory",
20586
+ "instructions, its specification and architecture, every policy as its section ids, the open initiatives,",
20587
+ "the repositories and the latest runs. Then read only what you need: the policy sections with",
20588
+ "taiga_get_document, an initiative with taiga_get_backlog_item. Check Action Required",
20589
+ "(taiga_list_action_required) for what waits on a person.",
20590
+ "",
20591
+ "**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that",
20592
+ "starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it",
20593
+ "(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). A client",
20594
+ "that prompts on destructive tools does part of that for you.",
20595
+ "",
20596
+ "**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the",
20597
+ "right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.",
20598
+ "",
20599
+ "**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action",
20600
+ "Required is the inbox; the line is the queue; a factory is the group that owns a set of products."
20601
+ ].join("\n"),
20291
20602
  "",
20292
20603
  "**Company documents** (taiga_list_documents / taiga_get_document):",
20293
20604
  `The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
@@ -20381,15 +20692,9 @@ const SERVER_INSTRUCTIONS = [
20381
20692
  "`priority: core` marks the sections every organization keeps. Neither changes what the section",
20382
20693
  "requires of you.",
20383
20694
  "",
20384
- "Workflow: start with taiga_get_product_context, naming the product by its git remote URL inside a checkout",
20385
- "(git remote get-url origin), else by slug or ID from taiga_list_products. It returns the product, the",
20386
- "factory instructions, the specification and architecture as the whole sections that fit (the rest named for",
20387
- "taiga_get_product_document to read by section), every policy as its section ids, the open backlog",
20388
- "items, the repositories and the latest runs in one call, and names any part it could not read. Then read",
20389
- "what you need in full: a policy with taiga_get_document, a backlog item with taiga_get_backlog_item.",
20390
- "Without it, use taiga_list_factories and taiga_list_products, then read the factory instructions,",
20391
- "the product's specification and architecture, and the backlog item you are working on. A document",
20392
- "read that finds nothing tells you what does exist and which tool reads or writes it.",
20695
+ "Without taiga_get_product_context, use taiga_list_factories and taiga_list_products, then read the",
20696
+ "factory instructions, the product's specification and architecture, and the initiative you are working",
20697
+ "on. A document read that finds nothing tells you what does exist and which tool reads or writes it.",
20393
20698
  "",
20394
20699
  "The same documents are resources (taiga://documents/{type}, taiga://products/{productId}/documents/{type},",
20395
20700
  "taiga://factories/{factoryId}/instructions, taiga://backlog/{itemId}) for clients that attach them, and three",