@taiga-ai/mcp-server 0.18.0-dev.450 → 0.19.0-dev.451

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
@@ -480,6 +480,10 @@ var TaigaClient = class {
480
480
  async refreshRealityDocuments(productId, docTypes) {
481
481
  return this.request("POST", `/products/${seg(productId)}/documents/refresh-reality`, docTypes ? { docTypes } : void 0);
482
482
  }
483
+ /** What still blocks finishing setup: required documents unpublished, required checks unstamped (PRO-2800). */
484
+ async getProductSetupBlockers(productId) {
485
+ return this.request("GET", `/products/${seg(productId)}/setup`);
486
+ }
483
487
  async completeSetup(productId) {
484
488
  return this.request("POST", `/products/${seg(productId)}/setup/complete`);
485
489
  }
@@ -1253,6 +1257,40 @@ function runDisposition(status) {
1253
1257
  return RUN_DISPOSITION[status];
1254
1258
  }
1255
1259
  /**
1260
+ * How the run stands, WITH the fact that decides what 'completed' meant.
1261
+ *
1262
+ * `completed` WAS one word for two outcomes, and the difference is the whole
1263
+ * question for anything that waits on a merge:
1264
+ *
1265
+ * - the pull request merged (`completeMergedRun`, from the merge webhook),
1266
+ * which is the run succeeding; or
1267
+ * - the review loop converged with nothing left to push, which settled the
1268
+ * run on a pull request that was still open, possibly failing, and
1269
+ * definitely not in main.
1270
+ *
1271
+ * The second outcome is no longer produced (PRO-1131): a review never settles a
1272
+ * run terminal now, so `completed` is written only by `completeMergedRun` and
1273
+ * means merged. This function is NOT therefore redundant -- runs settled under
1274
+ * the old rule are still in the table, unmigrated, and keep their status. It
1275
+ * remains the only honest answer to "did this land" for any row, whenever it
1276
+ * was written.
1277
+ *
1278
+ * `runDisposition` cannot tell them apart -- it only has the status -- so it
1279
+ * calls both 'succeeded'. Every caller that actually means "did this land"
1280
+ * must use THIS function instead, which is why it lives next to the map rather
1281
+ * than in the one module that first needed it.
1282
+ *
1283
+ * `prMerged` is a monotonic latch on the run row (a pull request cannot
1284
+ * un-merge), and the merge webhook writes it BEFORE it writes the status, so
1285
+ * 'completed' with `prMerged: false` is never a window -- it is the second
1286
+ * outcome, every time.
1287
+ */
1288
+ function dispositionOf(status, prMerged) {
1289
+ const disposition = runDisposition(status);
1290
+ if (disposition === "succeeded" && !prMerged) return "unmerged";
1291
+ return disposition;
1292
+ }
1293
+ /**
1256
1294
  * Statuses where the coding agent (or review worker) is actively executing.
1257
1295
  * Single source of truth shared by the tenant build feed's `active=true` filter
1258
1296
  * (api) and the dashboard's "running" detection (web), which must agree.
@@ -2074,7 +2112,7 @@ function productDocumentName(type) {
2074
2112
  /** The organization documents that are not policies: goals, standards, voice, instructions. */
2075
2113
  const OTHER_TENANT_DOCUMENT_TYPES = TENANT_DOCUMENT_TYPES.filter((type) => !POLICY_TYPES.includes(type));
2076
2114
  /** `a, b, c` for a description. */
2077
- function named(types) {
2115
+ function named$1(types) {
2078
2116
  return types.join(", ");
2079
2117
  }
2080
2118
  /**
@@ -2165,15 +2203,14 @@ const DISCOVERY_STEP_MEANING = "discoveryStep is where a product in setup is: co
2165
2203
  //#region src/tools/hints.ts
2166
2204
  /**
2167
2205
  * The next step after the API answers 404. The API's own word is bare
2168
- * ("Backlog item not found", "Repository link not found"), and an agent that
2206
+ * ("Initiative not found", "Repository link not found"), and an agent that
2169
2207
  * hears only that guesses another ID, or a name. Each ID a tool takes names
2170
2208
  * the tool that lists the right ones; the tool's arguments say which apply.
2171
2209
  */
2172
- const ITEM_ID_HINT = "taiga_list_backlog_items lists a product's initiatives with their IDs; a run or plan ID is not one.";
2210
+ const ITEM_ID_HINT = "taiga_list_initiatives lists a product's initiatives with their IDs; a run or plan ID is not one.";
2173
2211
  const BY_ARGUMENT = {
2174
- itemId: ITEM_ID_HINT,
2175
- backlogItemId: "taiga_list_backlog_items lists a product's initiatives with their IDs.",
2176
- buildId: "taiga_list_builds lists the runs, of one product or of every product the key can see.",
2212
+ initiativeId: ITEM_ID_HINT,
2213
+ runId: "taiga_list_runs lists the runs, of one product or of every product the key can see.",
2177
2214
  planId: "taiga_get_plan gives the initiative's active plan and its ID.",
2178
2215
  repositoryId: "taiga_list_product_repositories lists the product's repository links; a GitHub repository id is not a link ID.",
2179
2216
  replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
@@ -2190,17 +2227,41 @@ const BY_ARGUMENT = {
2190
2227
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2191
2228
  const BY_TOOL = {
2192
2229
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2193
- 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." : ITEM_ID_HINT
2230
+ taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one." : ITEM_ID_HINT
2231
+ };
2232
+ /**
2233
+ * What the API calls the thing each ID names, as its 404 says it ("Environment
2234
+ * not found"). A tool given a right product and a wrong environment hinted
2235
+ * at both, and the product's hint told the agent its correct product ID was
2236
+ * the mistake (PRO-2800): a 404 that names its thing gets that ID's hint only.
2237
+ */
2238
+ const MISSING_THING = {
2239
+ initiativeId: /backlog item|initiative/i,
2240
+ runId: /\b(build )?run\b/i,
2241
+ planId: /\bplan\b/i,
2242
+ repositoryId: /repository link|\blink\b/i,
2243
+ replacesRepositoryId: /repository link|\blink\b/i,
2244
+ githubRepositoryId: /github repository/i,
2245
+ installationId: /installation|account/i,
2246
+ jobId: /\bjob\b/i,
2247
+ factoryId: /factory|team/i,
2248
+ findingId: /finding/i,
2249
+ violationId: /violation/i,
2250
+ environmentId: /environment/i,
2251
+ knowledgeId: /knowledge|document/i,
2252
+ productId: /\b(product|project) not found/i
2194
2253
  };
2195
- /** The hint for a 404 from one tool call: the tool's own, else one per ID it was given. */
2254
+ /** The hint for a 404 from one tool call: the tool's own, else the one for the ID the API says is missing. */
2196
2255
  function notFoundHint(tool, args) {
2197
2256
  if (BY_TOOL[tool]) return BY_TOOL[tool];
2198
2257
  if (typeof args !== "object" || args === null) return void 0;
2199
2258
  const given = Object.keys(args).filter((key) => args[key] != null && BY_ARGUMENT[key]);
2200
2259
  given.sort((a, b) => Number(a === "productId") - Number(b === "productId"));
2201
2260
  if (given.length === 0) return void 0;
2202
- const hint = given.map((key) => BY_ARGUMENT[key]).join(" ");
2203
- return () => hint;
2261
+ return (said) => {
2262
+ const named = given.filter((key) => MISSING_THING[key]?.test(said));
2263
+ return (named.length > 0 ? named : given).map((key) => BY_ARGUMENT[key]).join(" ");
2264
+ };
2204
2265
  }
2205
2266
  /** The hint of the tool call in progress, read by runTool when the API answers 404. */
2206
2267
  const notFoundHints = new AsyncLocalStorage();
@@ -2215,11 +2276,45 @@ function withStop(text) {
2215
2276
  }
2216
2277
  //#endregion
2217
2278
  //#region src/tools/errors.ts
2279
+ /**
2280
+ * The API's names for the two IDs an agent chains most, in the product's
2281
+ * words the tools take them in (PRO-2800): a row says initiativeId and runId,
2282
+ * as taiga_get_initiative and taiga_get_run ask for them, rather than the
2283
+ * storage names backlogItemId and buildRunId.
2284
+ */
2285
+ const PRODUCT_WORDS = {
2286
+ backlogItemId: "initiativeId",
2287
+ buildRunId: "runId"
2288
+ };
2289
+ /**
2290
+ * Maps whose keys are content, not API fields: a document's sections are keyed
2291
+ * by ids a person or agent chose (a section may be called backlogItemId), and
2292
+ * metadata and settings hold whatever their writer put there. Copied as they
2293
+ * are, keys and all.
2294
+ */
2295
+ const OPAQUE_MAPS = /* @__PURE__ */ new Set([
2296
+ "sections",
2297
+ "metadata",
2298
+ "settings"
2299
+ ]);
2300
+ /** A copy of an API value with its keys in the product's words, values untouched. */
2301
+ function inProductWords(value) {
2302
+ if (Array.isArray(value)) return value.map(inProductWords);
2303
+ if (value === null || typeof value !== "object") return value;
2304
+ const out = {};
2305
+ for (const [key, inner] of Object.entries(value)) Object.defineProperty(out, PRODUCT_WORDS[key] ?? key, {
2306
+ value: OPAQUE_MAPS.has(key) ? inner : inProductWords(inner),
2307
+ enumerable: true,
2308
+ writable: true,
2309
+ configurable: true
2310
+ });
2311
+ return out;
2312
+ }
2218
2313
  /** A successful tool result: one JSON text block the model reads. */
2219
2314
  function toolResult(value) {
2220
2315
  return { content: [{
2221
2316
  type: "text",
2222
- text: JSON.stringify(value, null, 2)
2317
+ text: JSON.stringify(inProductWords(value), null, 2)
2223
2318
  }] };
2224
2319
  }
2225
2320
  /**
@@ -2234,7 +2329,7 @@ function toolError(error, message, details) {
2234
2329
  text: JSON.stringify(details ? {
2235
2330
  error,
2236
2331
  message,
2237
- details
2332
+ details: inProductWords(details)
2238
2333
  } : {
2239
2334
  error,
2240
2335
  message
@@ -2486,7 +2581,7 @@ function budgetSections(doc, budget) {
2486
2581
  * The `sections` input of a document read: some sections by id, none for the
2487
2582
  * outline (every id with its size and no text), or omitted for the whole.
2488
2583
  */
2489
- const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().describe("Only these sections, by id; [] for the outline (every section id and its size, no text); omit for the whole document");
2584
+ const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().describe("Only these sections, by id; [] for the outline (under outline: every section id and its size, no text); omit for the whole document");
2490
2585
  /**
2491
2586
  * The answer to a read that names sections: the ones the document has, the
2492
2587
  * rest listed with their sizes, and the asked-for ids it lacks under
@@ -2500,6 +2595,14 @@ function sectionReadResult(doc, ids, label) {
2500
2595
  if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
2501
2596
  const all = Object.keys(sectionsOf(doc.data) ?? {});
2502
2597
  const unknownSections = ids.filter((id) => !all.includes(id));
2598
+ if (ids.filter((id) => all.includes(id)).length === 0) {
2599
+ const { data: _data, ...meta } = cut.doc;
2600
+ return toolResult({
2601
+ ...meta,
2602
+ outline: cut.omittedSections,
2603
+ ...unknownSections.length > 0 ? { unknownSections } : {}
2604
+ });
2605
+ }
2503
2606
  return toolResult({
2504
2607
  ...cut.doc,
2505
2608
  omittedSections: cut.omittedSections,
@@ -2507,6 +2610,90 @@ function sectionReadResult(doc, ids, label) {
2507
2610
  });
2508
2611
  }
2509
2612
  //#endregion
2613
+ //#region src/tools/readiness.ts
2614
+ const IMPORT_STATE = {
2615
+ pending: "not started",
2616
+ analyzing: "running",
2617
+ generating_docs: "running",
2618
+ complete: "complete",
2619
+ failed: "failed",
2620
+ cancelled: "failed"
2621
+ };
2622
+ /** A failure that is the API's (refused, missing, unreachable), not a bug in this code. */
2623
+ function isApiSide(error) {
2624
+ return error instanceof TaigaApiError || isNetworkFailure(error);
2625
+ }
2626
+ /** The documents by the names the web uses. */
2627
+ function named(types) {
2628
+ return types.map((type) => PRODUCT_DOCUMENT_NAMES[type] ?? type).join(", ");
2629
+ }
2630
+ /** The product's setup readiness, or null once it has finished setup. */
2631
+ async function setupReadiness(client, product) {
2632
+ if (product["status"] !== "draft") return null;
2633
+ const id = String(product["id"]);
2634
+ const imported = isImportedProduct(product);
2635
+ const [repos, blockers, full] = await Promise.all([
2636
+ client.listProductRepositories(id).then((result) => result.data, (error) => {
2637
+ if (isApiSide(error)) return null;
2638
+ throw error;
2639
+ }),
2640
+ client.getProductSetupBlockers(id).then((result) => result.data, (error) => {
2641
+ if (isApiSide(error)) return null;
2642
+ throw error;
2643
+ }),
2644
+ "discoveryStep" in product || imported ? Promise.resolve(product) : client.getProduct(id).then((result) => result.data, (error) => {
2645
+ if (isApiSide(error)) return product;
2646
+ throw error;
2647
+ })
2648
+ ]);
2649
+ const link = repos?.[0];
2650
+ const repository = !repos ? null : link ? {
2651
+ linked: true,
2652
+ name: link["repositoryName"],
2653
+ branch: link["effectiveBranch"]
2654
+ } : { linked: false };
2655
+ const settings = product["settings"] ?? {};
2656
+ const importState = imported ? IMPORT_STATE[String(settings.importStatus)] ?? "not started" : void 0;
2657
+ const missing = blockers ? blockers.missingDocuments : null;
2658
+ const openChecks = blockers ? blockers.openChecks.map((check) => check.criterion) : null;
2659
+ return {
2660
+ ready: blockers ? missing.length === 0 && openChecks.length === 0 : null,
2661
+ repository,
2662
+ ...importState ? { import: importState } : {},
2663
+ ...imported ? {} : { discoveryStep: full["discoveryStep"] ?? null },
2664
+ missingDocuments: missing ? missing.map((type) => named([type])) : null,
2665
+ openChecks,
2666
+ nextStep: nextStepFor({
2667
+ imported,
2668
+ importState,
2669
+ linked: repository?.linked ?? null,
2670
+ missing,
2671
+ openChecks,
2672
+ product: full
2673
+ })
2674
+ };
2675
+ }
2676
+ function nextStepFor(state) {
2677
+ const finish = "taiga_finish_discovery opens the product and locks its documents (ask the person: it is their decision that the documents are right).";
2678
+ if (state.imported) {
2679
+ if (state.linked === false) return "Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.";
2680
+ if (state.linked === null && state.importState === "not started") return setupNextStep(state.product);
2681
+ switch (state.importState) {
2682
+ case "running": return "The import is running and derives the documents from the code; taiga_wait_for_job follows it.";
2683
+ case "failed": return "The last import did not finish: taiga_start_import runs it again.";
2684
+ case "complete":
2685
+ if (state.missing && state.missing.length > 0) return `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? "is" : "are"} not published: taiga_list_product_documents shows them, taiga_publish_product_document publishes a draft.`;
2686
+ return `The import is done: review the reality documents (taiga_list_product_documents), then ${finish}`;
2687
+ default: return "taiga_start_import derives the documents from the code and runs by itself (paid agent work: ask first).";
2688
+ }
2689
+ }
2690
+ if (!state.missing) return setupNextStep(state.product);
2691
+ if (state.missing.includes("specification")) return "The specification comes from the Discovery conversation, held in the web: the person describes the product there. The other documents follow from it." + (state.linked === false ? " Linking a repository (taiga_link_repository) can wait until before the first build." : "");
2692
+ if (state.missing.length > 0) return `Still to publish: ${named(state.missing)}. taiga_generate_product_documents generates the missing ones (paid agent work: ask first); a draft is published with taiga_publish_product_document.`;
2693
+ if (state.openChecks && state.openChecks.length > 0) return `The documents are published; the checks are stamped in the web, then ${finish}`;
2694
+ return `Every required document is published: ${finish}`;
2695
+ }
2696
+ //#endregion
2510
2697
  //#region src/tools/summarize.ts
2511
2698
  /**
2512
2699
  * Keep only the named fields of each item. List tools return summaries (the
@@ -2828,7 +3015,7 @@ function registerProductTools(server, client) {
2828
3015
  }));
2829
3016
  server.registerTool("taiga_get_product", {
2830
3017
  title: "Get product",
2831
- 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.",
3018
+ 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_initiatives for its work and taiga_list_runs for what the coding agent did. A product still in setup carries readiness: whether it can finish setup now, its repository and import, the required documents not yet published, the checks not yet stamped, and nextStep, the one move that gets it closer.",
2832
3019
  inputSchema: z.object({
2833
3020
  productId: productId.optional(),
2834
3021
  slug: slug.optional()
@@ -2837,10 +3024,10 @@ function registerProductTools(server, client) {
2837
3024
  }, (params) => runTool(async () => {
2838
3025
  const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
2839
3026
  if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
2840
- const nextStep = setupNextStep(resolved.product);
2841
- return toolResult(nextStep ? {
3027
+ const readiness = await setupReadiness(client, resolved.product);
3028
+ return toolResult(readiness ? {
2842
3029
  ...resolved.product,
2843
- nextStep
3030
+ readiness
2844
3031
  } : resolved.product);
2845
3032
  }));
2846
3033
  server.registerTool("taiga_resolve_product", {
@@ -2937,7 +3124,7 @@ function sectionIdFrom(readTool) {
2937
3124
  return z.string().min(1).describe(`Section id -- the keys of \`data.sections\` in ${readTool}, e.g. summary, scope, apiDesign. An id that does not exist yet adds a section.`);
2938
3125
  }
2939
3126
  const sectionContentInput = z.string().describe("The full new Markdown text of the section; it replaces the old text");
2940
- const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe(`Document type: a policy (${named(POLICY_TYPES)}) or ${named(OTHER_TENANT_DOCUMENT_TYPES)}`);
3127
+ const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe(`Document type: a policy (${named$1(POLICY_TYPES)}) or ${named$1(OTHER_TENANT_DOCUMENT_TYPES)}`);
2941
3128
  /** Everything the model needs back from a write, without the content it already holds. */
2942
3129
  function documentSummary(doc) {
2943
3130
  const { data, ...rest } = doc;
@@ -2999,7 +3186,7 @@ function registerDocumentTools(server, client) {
2999
3186
  if (client.hasCapability("tenant:documents:read")) {
3000
3187
  server.registerTool("taiga_list_documents", {
3001
3188
  title: "List company documents",
3002
- description: `List the organization's documents as summaries (type, name, description, version, draft state). The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) define the security, data privacy, SDLC, operations, architecture and compliance rules that apply across all factories and products; the other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. Use taiga_get_document with the type to read one in full.`,
3189
+ description: `List the organization's documents as summaries (type, name, description, version, draft state). The ${POLICY_TYPES.length} policies (${named$1(POLICY_TYPES)}) define the security, data privacy, SDLC, operations, architecture and compliance rules that apply across all factories and products; the other documents (${named$1(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. Use taiga_get_document with the type to read one in full.`,
3003
3190
  inputSchema: z.object({ ...pagination }),
3004
3191
  annotations: READ_ONLY
3005
3192
  }, (params) => runTool(async () => {
@@ -3008,7 +3195,7 @@ function registerDocumentTools(server, client) {
3008
3195
  }));
3009
3196
  server.registerTool("taiga_get_document", {
3010
3197
  title: "Get company document",
3011
- 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, or sections: [] for the outline) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds no document says which documents are published and whether a draft exists.",
3198
+ 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, or sections: [] for the outline) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds no document says which documents are published and whether a draft exists. Each section's source says where its text came from (template: Taiga's standard text; overlay-<id>: a jurisdiction or industry overlay; cloud-preset:<providers>: rendered for the organization's clouds) and priority: core marks the sections every organization keeps; neither changes what the section requires.",
3012
3199
  inputSchema: z.object({
3013
3200
  type: tenantDocumentType,
3014
3201
  sections: sectionsInput
@@ -3059,7 +3246,7 @@ function registerDocumentTools(server, client) {
3059
3246
  if (client.hasCapability("factories:read")) {
3060
3247
  server.registerTool("taiga_list_factory_documents", {
3061
3248
  title: "List factory documents",
3062
- 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.`,
3249
+ description: `List a factory's documents, published and draft, as summaries (type, name, version, draft state). A factory has one document type, ${named$1(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.`,
3063
3250
  inputSchema: z.object({
3064
3251
  factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
3065
3252
  ...pagination
@@ -3223,9 +3410,9 @@ function requestPrompt(title, description) {
3223
3410
  }
3224
3411
  //#endregion
3225
3412
  //#region src/tools/queue.ts
3226
- /** How many entries the read names; past this an entry keeps its backlogItemId only. */
3413
+ /** How many entries the read names; past this an entry keeps its initiativeId only. */
3227
3414
  const NAMED_ENTRIES = 25;
3228
- const MOVE_ASIDE = "or move it down (taiga_queue_backlog_item with afterId) or off the line (taiga_move_backlog_item) to let the rest pass.";
3415
+ const MOVE_ASIDE = "or move it down (taiga_queue_initiative with afterId) or off the line (taiga_move_initiative) to let the rest pass.";
3229
3416
  /**
3230
3417
  * What lifts a halt, in the tools that do it. Read from the halt registry in
3231
3418
  * @taiga/types (how each cause clears, and whether a person may lift it), so
@@ -3235,11 +3422,11 @@ const MOVE_ASIDE = "or move it down (taiga_queue_backlog_item with afterId) or o
3235
3422
  function haltNextStep(cause) {
3236
3423
  if (cause === "awaiting_plan_review") return "Read the plan with taiga_get_plan, then approve it (taiga_approve_plan) or send it back (taiga_reject_plan).";
3237
3424
  const spec = cause ? HALT_CAUSES[cause] : void 0;
3238
- if (!cause || !spec) return `Read the initiative with taiga_get_backlog_item to see why, ${MOVE_ASIDE}`;
3425
+ if (!cause || !spec) return `Read the initiative with taiga_get_initiative to see why, ${MOVE_ASIDE}`;
3239
3426
  const known = cause;
3240
- if (canResumeHalt(known)) return `taiga_resume_backlog_item lifts it, ${MOVE_ASIDE}`;
3427
+ if (canResumeHalt(known)) return `taiga_resume_initiative lifts it, ${MOVE_ASIDE}`;
3241
3428
  if (haltClearsItself(known)) return `It clears when ${spec.clearedWhen}; ${MOVE_ASIDE}`;
3242
- return `Only taking it off the line clears it (taiga_move_backlog_item): ${spec.clearedWhen}.`;
3429
+ return `Only taking it off the line clears it (taiga_move_initiative): ${spec.clearedWhen}.`;
3243
3430
  }
3244
3431
  /**
3245
3432
  * The entry the line waits on, by the queue's two kinds of blocker
@@ -3313,7 +3500,7 @@ async function queueView(client, product, options = { name: "all" }) {
3313
3500
  cta: lifecycle.cta
3314
3501
  } : null,
3315
3502
  ...latestBuild ? {
3316
- buildId: latestBuild.id,
3503
+ runId: latestBuild.id,
3317
3504
  prUrl: latestBuild.prUrl ?? null
3318
3505
  } : {}
3319
3506
  });
@@ -3330,7 +3517,7 @@ async function queueView(client, product, options = { name: "all" }) {
3330
3517
  }
3331
3518
  return row;
3332
3519
  });
3333
- const note = unnamed === 0 ? null : !canName ? "Entries carry backlogItemId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry backlogItemId only.` : "An entry whose initiative could not be read carries backlogItemId only; taiga_get_backlog_item reads it.";
3520
+ const note = unnamed === 0 ? null : !canName ? "Entries carry initiativeId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry initiativeId only.` : "An entry whose initiative could not be read carries initiativeId only; taiga_get_initiative reads it.";
3334
3521
  const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
3335
3522
  return {
3336
3523
  linePaused: paused === null ? null : typeof paused === "string",
@@ -3354,7 +3541,7 @@ async function linePausedSince(client, product) {
3354
3541
  }
3355
3542
  function registerQueueTools(server, client) {
3356
3543
  if (!client.hasCapability("products:read")) return;
3357
- server.registerTool("taiga_get_queue", {
3544
+ server.registerTool("taiga_get_line", {
3358
3545
  title: "Get the product queue",
3359
3546
  description: "Read a product's queue, the line the coding agent works one initiative at a time: each entry in order with its title, where it is (planning, building, in review) and whose turn it is, a halted entry with why it stopped and what lifts it, linePaused (null when the product could not be read), and waitingOn, one sentence saying what the line is waiting for. The line advances only when the active entry's pull request merges, so an entry waiting on a review holds everything behind it. Use this when a queued initiative is not moving.",
3360
3547
  inputSchema: z.object({ productId }),
@@ -3363,7 +3550,7 @@ function registerQueueTools(server, client) {
3363
3550
  if (!client.hasCapability("products:write")) return;
3364
3551
  server.registerTool("taiga_pause_line", {
3365
3552
  title: "Pause or resume a product's line",
3366
- description: "Pause the product's whole line, or resume it. Pausing cancels the plan being written and stops the build in flight where it stands (both are flagged), and nothing more is planned or built; initiatives keep their places. Resuming plans the interrupted initiative again from scratch, carries the interrupted build on from its first incomplete step, and the line picks up where it was; a build you had stopped yourself before pausing stays stopped. Idempotent. Pausing interrupts work and resuming starts it: read taiga_get_queue first to see what it would interrupt, and ask the person.",
3553
+ description: "Pause the product's whole line, or resume it. Pausing cancels the plan being written and stops the build in flight where it stands (both are flagged), and nothing more is planned or built; initiatives keep their places. Resuming plans the interrupted initiative again from scratch, carries the interrupted build on from its first incomplete step, and the line picks up where it was; a build you had stopped yourself before pausing stays stopped. Idempotent. Pausing interrupts work and resuming starts it: read taiga_get_line first to see what it would interrupt, and ask the person.",
3367
3554
  inputSchema: z.object({
3368
3555
  productId,
3369
3556
  paused: z.boolean().describe("true pauses the line, false resumes it")
@@ -3379,6 +3566,243 @@ function registerQueueTools(server, client) {
3379
3566
  }));
3380
3567
  }
3381
3568
  //#endregion
3569
+ //#region src/tools/builds.ts
3570
+ /** What a list row needs to say for the model to pick a run to read in full. */
3571
+ const BUILD_SUMMARY_FIELDS = [
3572
+ "id",
3573
+ "runNumber",
3574
+ "productId",
3575
+ "backlogItemId",
3576
+ "targetTitle",
3577
+ "planId",
3578
+ "status",
3579
+ "failedReason",
3580
+ "runtime",
3581
+ "repositoryFullName",
3582
+ "branch",
3583
+ "baseBranch",
3584
+ "completedSteps",
3585
+ "totalSteps",
3586
+ "prNumber",
3587
+ "prUrl",
3588
+ "prState",
3589
+ "prDraft",
3590
+ "prMerged",
3591
+ "actions",
3592
+ "createdAt",
3593
+ "startedAt",
3594
+ "completedAt"
3595
+ ];
3596
+ /** What a run says about itself after a stop or a resume: its state and what can be done next. */
3597
+ const RUN_OUTCOME_FIELDS = [
3598
+ "id",
3599
+ "runNumber",
3600
+ "backlogItemId",
3601
+ "status",
3602
+ "failedReason",
3603
+ "completedSteps",
3604
+ "totalSteps",
3605
+ "prNumber",
3606
+ "prUrl",
3607
+ "prMerged",
3608
+ "actions"
3609
+ ];
3610
+ /**
3611
+ * Where a run that ended without finishing stopped, from its steps and its
3612
+ * hardening result: a failed run with every step completed reads as nothing
3613
+ * failed, and retry_from_failed_step then reads as a mistake, when the agent
3614
+ * stalled in the hardening pass after the steps and the retry continues it.
3615
+ * Null for a run still going or that finished.
3616
+ */
3617
+ function stoppedIn(run, steps) {
3618
+ if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
3619
+ if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
3620
+ const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
3621
+ 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)})`;
3622
+ 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.";
3623
+ return "after the steps and hardening, at the pull request";
3624
+ }
3625
+ /**
3626
+ * The disposition with the merge latch authoritative: a run whose pull
3627
+ * request merged landed, whatever settled status it kept when the completion
3628
+ * write after the merge failed (build.ts allows a merged run to stay failed
3629
+ * or stopped). A run still executing keeps its in-flight disposition.
3630
+ */
3631
+ function landedDisposition(status, prMerged) {
3632
+ const disposition = dispositionOf(status, prMerged);
3633
+ return prMerged && disposition !== "in_flight" ? "succeeded" : disposition;
3634
+ }
3635
+ /** What each executing status is doing, in a phrase. */
3636
+ const RUNNING = {
3637
+ cloning: "setting up the workspace",
3638
+ implementing: "implementing the steps",
3639
+ hardening: "hardening: running the security scanners and fixing what they find",
3640
+ pushing: "pushing the branch",
3641
+ ci_checking: "waiting for the pull request's CI",
3642
+ ci_fixing: "fixing what CI found",
3643
+ reviewing: "answering review comments on the pull request"
3644
+ };
3645
+ /**
3646
+ * How a run stands, in one sentence, with its disposition: the meaning the
3647
+ * status and the pull request facts carry between them
3648
+ * (PRO-2800). An agent read `closed` with failedReason null, every step
3649
+ * completed and hardening passed, and could not tell what had happened; the
3650
+ * status alone cannot say whether a completed run merged (dispositionOf).
3651
+ */
3652
+ function runOutcome(run) {
3653
+ const status = run["status"];
3654
+ if (!BUILD_RUN_STATUSES.includes(status)) return null;
3655
+ const disposition = landedDisposition(status, run["prMerged"] === true);
3656
+ const pr = typeof run["prNumber"] === "number" ? `PR #${run["prNumber"]}` : "its pull request";
3657
+ const progress = typeof run["completedSteps"] === "number" && typeof run["totalSteps"] === "number" ? ` (${run["completedSteps"]} of ${run["totalSteps"]} steps done)` : "";
3658
+ const reason = typeof run["failedReason"] === "string" && run["failedReason"] ? `: ${run["failedReason"]}` : "";
3659
+ return {
3660
+ disposition,
3661
+ outcome: {
3662
+ in_flight: status === "pending" ? "Queued: waiting to start." : `Running: ${RUNNING[status] ?? status}${progress}.`,
3663
+ gated: `Waiting for a person to review and merge ${pr}.`,
3664
+ succeeded: `Merged: ${pr} is in ${typeof run["baseBranch"] === "string" ? run["baseBranch"] : "the base branch"}.`,
3665
+ unmerged: typeof run["prNumber"] === "number" || run["prUrl"] ? `Settled without merging: ${pr} was left unmerged.` : "Settled without a merged pull request.",
3666
+ failed: `Failed${reason}${progress}.`,
3667
+ stopped: `Stopped before it finished${progress}.`,
3668
+ rejected: `${pr} was closed without merging, so the work did not land.`
3669
+ }[disposition] ?? status
3670
+ };
3671
+ }
3672
+ /**
3673
+ * A run row as an agent reads it: its outcome added, and prDraft only where
3674
+ * there is a pull request. The column defaults to true, so a run that never
3675
+ * opened one said it had a draft (PRO-2800).
3676
+ */
3677
+ function readableRun(run) {
3678
+ const { prDraft, ...rest } = run;
3679
+ const said = runOutcome(run);
3680
+ return {
3681
+ ...rest,
3682
+ ...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
3683
+ ...said ?? {}
3684
+ };
3685
+ }
3686
+ /** How many products the org-wide list names; past this a row keeps its productId only. */
3687
+ const NAMED_PRODUCTS = 100;
3688
+ const runId = idInput("run", "taiga_list_runs gives it");
3689
+ /** A step without its instructions: what it is, how it stands and what it changed. */
3690
+ const STEP_SUMMARY_FIELDS = [
3691
+ "stepOrder",
3692
+ "title",
3693
+ "status",
3694
+ "currentPhase",
3695
+ "commitSha",
3696
+ "filesChanged",
3697
+ "verification",
3698
+ "error",
3699
+ "errorClassification",
3700
+ "startedAt",
3701
+ "completedAt"
3702
+ ];
3703
+ /** Past this many characters a step's headline is cut; the full read has the rest. */
3704
+ const HEADLINE_LIMIT = 600;
3705
+ /**
3706
+ * The first paragraph of a step's summary: the agent writes its result
3707
+ * there (what changed, which checks passed), and the detail after it.
3708
+ */
3709
+ function summaryHeadline(summary) {
3710
+ if (typeof summary !== "string" || !summary.trim()) return null;
3711
+ const first = summary.trim().split(/\n\s*\n/, 1)[0].trim();
3712
+ return first.length > HEADLINE_LIMIT ? `${first.slice(0, HEADLINE_LIMIT).trimEnd()}...` : first;
3713
+ }
3714
+ /**
3715
+ * The runs of every product the key can see, each row naming its product.
3716
+ * The route carries product ids only; one products read names them, and a
3717
+ * failure of that read leaves the rows unnamed rather than failing the list.
3718
+ */
3719
+ async function activityRows(client, opts) {
3720
+ const [runs, products] = await Promise.all([client.listBuildRunActivity(opts), client.listProducts({ limit: NAMED_PRODUCTS }).catch((error) => {
3721
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
3722
+ throw error;
3723
+ })]);
3724
+ const nameById = /* @__PURE__ */ new Map();
3725
+ for (const product of products?.data ?? []) nameById.set(product.id, {
3726
+ name: product.name,
3727
+ slug: product.slug
3728
+ });
3729
+ const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map(readableRun).map((row) => {
3730
+ const product = nameById.get(String(row["productId"]));
3731
+ return product ? {
3732
+ ...row,
3733
+ product
3734
+ } : row;
3735
+ });
3736
+ const note = !rows.some((row) => !("product" in row)) ? null : !products ? "The products could not be read, so rows carry productId only; taiga_get_product names one." : products.nextCursor !== null ? `Only the first ${NAMED_PRODUCTS} products could be named; a row without product carries productId only.` : "A row without product belongs to a product the key cannot list (deleted since, or not visible to it); it carries productId only.";
3737
+ return {
3738
+ ...page(rows, runs.nextCursor),
3739
+ ...note ? { note } : {}
3740
+ };
3741
+ }
3742
+ function registerBuildTools(server, client) {
3743
+ if (!client.hasCapability("products:read")) return;
3744
+ server.registerTool("taiga_list_runs", {
3745
+ title: "List runs",
3746
+ description: "List the coding agent's runs, newest first. A run is one attempt at building an initiative: its branch, pull request and step progress. Each row says how it stands in one sentence (outcome) and as a disposition: in_flight (the agent is working), gated (a person owes a review and merge), succeeded (merged), failed, stopped, rejected (the pull request was closed without merging) or unmerged. actions name what a person may do with it. With productId, the runs of that product, optionally of one initiative; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing now. taiga_get_run reads one with its steps. Work is carried on from the initiative (taiga_resume_initiative), not the run.",
3747
+ inputSchema: z.object({
3748
+ productId: productId.optional(),
3749
+ initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional().describe("Only this initiative's runs -- get from taiga_list_initiatives; needs productId"),
3750
+ 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"),
3751
+ ...pagination
3752
+ }),
3753
+ annotations: READ_ONLY
3754
+ }, (params) => runTool(async () => {
3755
+ if (!params.productId && params.initiativeId) return toolError("VALIDATION_ERROR", "initiativeId narrows the runs of one product: pass its productId as well (the initiative carries it).");
3756
+ 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.");
3757
+ if (params.productId) {
3758
+ const result = await client.listBuildRuns(params.productId, {
3759
+ backlogItemId: params.initiativeId,
3760
+ limit: params.limit,
3761
+ cursor: params.cursor
3762
+ });
3763
+ return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS).map(readableRun), result.nextCursor));
3764
+ }
3765
+ return toolResult(await activityRows(client, params));
3766
+ }));
3767
+ server.registerTool("taiga_get_run", {
3768
+ title: "Get run",
3769
+ description: "Get one run with its steps: how it stands (outcome, disposition), its pull request and hardening, and each step's title, status, commit, files changed, verification and headline, the first paragraph of what the agent reported. A step's verification is Taiga's own check after the step (tests, lint, build), independent of what the agent says it ran: a docs-only step can show tests failed because the repository's tests fail. A run that failed or was stopped says where (stoppedIn) and when it last made progress (lastProgressAt). Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Its actions name what the initiative accepts: resume and retry_from_failed_step are taiga_resume_initiative; start_over is in the web only; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
3770
+ inputSchema: z.object({
3771
+ runId,
3772
+ full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
3773
+ }),
3774
+ annotations: READ_ONLY
3775
+ }, (params) => runTool(async () => {
3776
+ const [run, steps] = await Promise.all([client.getBuildRun(params.runId), client.listBuildRunSteps(params.runId)]);
3777
+ const runRow = run.data;
3778
+ const rows = steps.data;
3779
+ const where = stoppedIn(runRow, rows);
3780
+ const stop = where ? { stoppedIn: where } : {};
3781
+ if (params.full) return toolResult({
3782
+ ...readableRun(runRow),
3783
+ ...stop,
3784
+ steps: steps.data
3785
+ });
3786
+ return toolResult({
3787
+ ...readableRun(runRow),
3788
+ ...stop,
3789
+ steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
3790
+ ...step,
3791
+ headline: summaryHeadline(rows[index].summary)
3792
+ }))
3793
+ });
3794
+ }));
3795
+ if (!client.hasCapability("products:write")) return;
3796
+ server.registerTool("taiga_stop_run", {
3797
+ title: "Stop a run",
3798
+ description: "The emergency brake: stop a run the coding agent is executing when its initiative cannot be moved. The way to stop work is on the initiative: move it out of Build (taiga_move_initiative), which stops the plan or run in flight. Use this only for a run whose initiative does not offer that (it is finished, canceled or gone) yet the run is still going. The agent halts, its branch and any draft pull request stay. Only a run whose actions include stop; the API refuses the rest with CONFLICT.",
3799
+ inputSchema: z.object({ runId }),
3800
+ annotations: OPERATE
3801
+ }, (params) => runTool(async () => {
3802
+ return toolResult(readableRun(pickFields([(await client.stopBuildRun(params.runId)).data], RUN_OUTCOME_FIELDS)[0]));
3803
+ }));
3804
+ }
3805
+ //#endregion
3382
3806
  //#region src/tools/backlogs.ts
3383
3807
  /** What a list row needs to say for the model to pick an item to read in full. */
3384
3808
  const BACKLOG_SUMMARY_FIELDS = [
@@ -3414,6 +3838,83 @@ const ITEM_OUTCOME_FIELDS = [
3414
3838
  "archivedAt",
3415
3839
  "lifecycle"
3416
3840
  ];
3841
+ /** The move each call to action is, in the tools' words. */
3842
+ const CTA_MOVE = {
3843
+ start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
3844
+ retry_plan: "taiga_plan_again asks for a new plan (paid work: ask first)",
3845
+ start_build: "read the plan with taiga_get_plan, then taiga_approve_plan builds it or taiga_reject_plan sends it back",
3846
+ resume_build: "taiga_resume_initiative carries the run on (paid work: ask first)",
3847
+ retry_build: "a new run starts over from the web; taiga_move_initiative takes it off the line",
3848
+ review_pr: "a person reviews and merges the pull request",
3849
+ view_pr: "the line merges the pull request once its checks pass"
3850
+ };
3851
+ /** How a run stands, as the end of "its latest run ...", by disposition. */
3852
+ const RUN_STANDS = {
3853
+ in_flight: "is running",
3854
+ gated: "is in review",
3855
+ succeeded: "merged",
3856
+ unmerged: "settled unmerged",
3857
+ failed: "failed",
3858
+ stopped: "was stopped",
3859
+ rejected: "had its pull request closed without merging"
3860
+ };
3861
+ /** Where an initiative is, in a phrase, by its lifecycle stage. */
3862
+ const STAGE = {
3863
+ backlog: "In the backlog, not on the line yet",
3864
+ planning: "Being planned",
3865
+ planned: "Planned",
3866
+ building: "Being built",
3867
+ in_review: "Its pull request is in review",
3868
+ done: "Done",
3869
+ canceled: "Canceled"
3870
+ };
3871
+ /**
3872
+ * Where an initiative stands and what moves it on, in one sentence (PRO-2800).
3873
+ * Five fields carried it (status, state, planningStatus, needsHumanCause,
3874
+ * lifecycle) and a reviewing agent had to read the queue, the initiative and
3875
+ * the run together to say what had happened; the lifecycle the API derives
3876
+ * already decides it. Null for a row without a lifecycle.
3877
+ */
3878
+ function situationOf(item) {
3879
+ const life = item["lifecycle"];
3880
+ if (!life || typeof life.stage !== "string") return null;
3881
+ const parts = [];
3882
+ let stage = STAGE[life.stage] ?? life.stage;
3883
+ if (life.stage === "planning" && life.turn === "you") stage = `Planning stopped${typeof life.planFailureCause === "string" ? ` (${life.planFailureCause})` : ""}`;
3884
+ const build = life.latestBuild;
3885
+ if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
3886
+ const pr = typeof build.prNumber === "number" ? ` (PR #${build.prNumber})` : "";
3887
+ const said = BUILD_RUN_STATUSES.includes(build.status) ? RUN_STANDS[landedDisposition(build.status, build.prMerged === true)] : build.status;
3888
+ stage += `; its latest run ${said}${pr}`;
3889
+ }
3890
+ parts.push(`${stage}.`);
3891
+ if (typeof item["needsHumanCause"] === "string") {
3892
+ const why = [renderHalt({
3893
+ cause: item["needsHumanCause"],
3894
+ params: {}
3895
+ }), typeof item["needsHumanDetail"] === "string" ? item["needsHumanDetail"] : ""].filter(Boolean).map((text) => text.replace(/\.$/, "")).join(". ");
3896
+ if (why) parts.push(`The line stopped on it: ${why}.`);
3897
+ }
3898
+ const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
3899
+ const move = typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
3900
+ if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
3901
+ else if (turn && move) parts.push(`${turn}: ${move}.`);
3902
+ else if (turn) parts.push(`${turn}.`);
3903
+ else if (move) parts.push(`Next: ${move}.`);
3904
+ return parts.join(" ");
3905
+ }
3906
+ /** A row or item with its situation added when it has a lifecycle. */
3907
+ function withSituation(item, row = item) {
3908
+ const situation = situationOf(item);
3909
+ return situation ? {
3910
+ ...row,
3911
+ situation
3912
+ } : row;
3913
+ }
3914
+ /** List rows, each with its situation. */
3915
+ function situatedRows(items) {
3916
+ return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index], row));
3917
+ }
3417
3918
  /** What each plan failure cause means for whoever reads it next (PLAN_FAILURE_CAUSES in @taiga/types). */
3418
3919
  const PLAN_FAILURE_MEANING = {
3419
3920
  agent_error: "The planning agent failed. Planning again is the usual next step.",
@@ -3425,10 +3926,10 @@ const PLAN_FAILURE_MEANING = {
3425
3926
  * (PRO-2780): planFailureCause alone said "cancelled" with nobody behind it.
3426
3927
  * Best effort: a history the API will not give leaves the item as it was.
3427
3928
  */
3428
- async function planFailureOf(client, itemId, itemCause) {
3929
+ async function planFailureOf(client, initiativeId, itemCause) {
3429
3930
  let events;
3430
3931
  try {
3431
- events = (await client.listBacklogItemActivity(itemId)).data;
3932
+ events = (await client.listBacklogItemActivity(initiativeId)).data;
3432
3933
  } catch (error) {
3433
3934
  if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
3434
3935
  throw error;
@@ -3460,7 +3961,7 @@ async function planFailureOf(client, itemId, itemCause) {
3460
3961
  };
3461
3962
  }
3462
3963
  const planId = idInput("plan", "taiga_get_plan gives the initiative's active plan and its ID");
3463
- const itemId = idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it");
3964
+ const initiativeId = idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it");
3464
3965
  /** The plan without its steps' prose: enough to approve it or say what a run will do. */
3465
3966
  const PLAN_SUMMARY_FIELDS = [
3466
3967
  "id",
@@ -3479,8 +3980,8 @@ const PLAN_STEP_SUMMARY_FIELDS = [
3479
3980
  "reads",
3480
3981
  "createsResources"
3481
3982
  ];
3482
- /** The outcome fields of an initiative, with how many of its criteria are met. */
3483
- const outcomeOf = (item) => ({
3983
+ /** The outcome fields of an initiative, with how many of its criteria are met, and where it stands. */
3984
+ const outcomeOf = (item) => withSituation(item, {
3484
3985
  ...pickFields([item], ITEM_OUTCOME_FIELDS)[0],
3485
3986
  criteria: criteriaCounts(item)
3486
3987
  });
@@ -3515,9 +4016,9 @@ function registerBacklogTools(server, client) {
3515
4016
  if (may("backlog:read") && may("backlog:write") && may("products:write")) registerPlanAgain(server, client);
3516
4017
  }
3517
4018
  function registerBacklogReads(server, client) {
3518
- server.registerTool("taiga_list_backlog_items", {
3519
- title: "List backlog items",
3520
- 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.",
4019
+ server.registerTool("taiga_list_initiatives", {
4020
+ title: "List initiatives",
4021
+ description: "List a product's initiatives, its backlog: titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done), situation (where it stands and what moves it on, in a sentence) 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_initiative. Archived items are left out unless asked for.",
3521
4022
  inputSchema: z.object({
3522
4023
  productId,
3523
4024
  query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
@@ -3547,7 +4048,7 @@ function registerBacklogReads(server, client) {
3547
4048
  const { query, ...filters } = params;
3548
4049
  if (query === void 0) {
3549
4050
  const result = await client.listBacklogItems(params.productId, filters);
3550
- return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
4051
+ return toolResult(page(situatedRows(result.data), result.nextCursor));
3551
4052
  }
3552
4053
  if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
3553
4054
  const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
@@ -3556,35 +4057,36 @@ function registerBacklogReads(server, client) {
3556
4057
  cursor
3557
4058
  }), query, (row) => [row.title], params.limit ?? 25);
3558
4059
  return toolResult({
3559
- ...page(itemRows(found.rows, BACKLOG_SUMMARY_FIELDS), null),
4060
+ ...page(situatedRows(found.rows), null),
3560
4061
  ...found.more ? { more: SEARCH_NOTE } : {}
3561
4062
  });
3562
4063
  }));
3563
- server.registerTool("taiga_get_backlog_item", {
3564
- title: "Get backlog item",
3565
- 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. A failed plan carries planFailure: the cause, what it means, when, and by whom. 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.",
3566
- inputSchema: z.object({ itemId }),
4064
+ server.registerTool("taiga_get_initiative", {
4065
+ title: "Get initiative",
4066
+ description: "Get one initiative 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_runs to see what the coding agent already did. A failed plan carries planFailure: the cause, what it means, when, and by whom. situation says in one sentence where it stands, whose turn it is and the call that moves it on. The same as fields: lifecycle says whose turn it is and what moves it on: cta start_plan is taiga_queue_initiative, retry_plan taiga_plan_again (planFailureCause says why the plan stopped), start_build taiga_approve_plan, resume_build taiga_resume_initiative, 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.",
4067
+ inputSchema: z.object({ initiativeId }),
3567
4068
  annotations: READ_ONLY
3568
4069
  }, (params) => runTool(async () => {
3569
- const item = (await client.getBacklogItem(params.itemId)).data;
3570
- if (item.planningStatus !== "failed") return toolResult(item);
4070
+ const item = (await client.getBacklogItem(params.initiativeId)).data;
4071
+ const row = item;
4072
+ if (item.planningStatus !== "failed") return toolResult(withSituation(row));
3571
4073
  const ownCause = item.planFailureCause;
3572
- const planFailure = await planFailureOf(client, params.itemId, ownCause);
3573
- return toolResult(planFailure ? {
4074
+ const planFailure = await planFailureOf(client, params.initiativeId, ownCause);
4075
+ return toolResult(withSituation(row, planFailure ? {
3574
4076
  ...item,
3575
4077
  planFailure
3576
- } : item);
4078
+ } : item));
3577
4079
  }));
3578
4080
  server.registerTool("taiga_get_plan", {
3579
4081
  title: "Get the active plan",
3580
4082
  description: "Get an initiative's active plan with its steps, in order: each step's title, layer, dependencies (the orderIndex of the steps it builds on), the files it writes and reads and the resources it creates. That is enough to approve or reject it (taiga_approve_plan, taiga_reject_plan) and to say what a run will do; pass full: true for each step's complete instructions, several thousand tokens on a large plan. NOT_FOUND when the initiative has no active plan yet (planningStatus says whether one is being generated).",
3581
4083
  inputSchema: z.object({
3582
- itemId,
4084
+ initiativeId,
3583
4085
  full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
3584
4086
  }),
3585
4087
  annotations: READ_ONLY
3586
4088
  }, (params) => runTool(async () => {
3587
- const plan = (await client.getActivePlan(params.itemId)).data;
4089
+ const plan = (await client.getActivePlan(params.initiativeId)).data;
3588
4090
  if (params.full) return toolResult(plan);
3589
4091
  const steps = Array.isArray(plan.steps) ? plan.steps : [];
3590
4092
  return toolResult({
@@ -3615,18 +4117,18 @@ function registerRequest(server, client) {
3615
4117
  }));
3616
4118
  }
3617
4119
  function registerMove(server, client) {
3618
- server.registerTool("taiga_move_backlog_item", {
3619
- title: "Move backlog item on the board",
3620
- description: `Move an initiative on the board, or order it within a column: backlog and todo are yours and the line never moves an initiative out of them; queue puts it in the line (taiga_queue_backlog_item says more); done and canceled finish it by hand. The queue is the only way to start work: there is no "plan this now", and nothing is moved into Build by a person. Moving an initiative out of Build stops the plan being written or the run in flight: that is how work is stopped, and it throws that work away. A move to ${PR_CLOSING_STATES.join(", ")} CLOSES a pull request the initiative is still waiting on (a move to queue keeps it, and done leaves it to be merged); Taiga leaves a comment on the pull request saying why. The state machine refuses a move that is not allowed (CONFLICT). A move into queue starts paid work and needs products:write. Ask the person before a move that starts work, stops work or closes a pull request. archived: true files the initiative where it stands (pass its current column as state; never from the Build station, plan or build) and archived: false brings it back; archived work is left out of lists unless includeArchived asks for it.`,
4120
+ server.registerTool("taiga_move_initiative", {
4121
+ title: "Move initiative on the board",
4122
+ description: `Move an initiative on the board, or order it within a column: backlog and todo are yours and the line never moves an initiative out of them; queue puts it in the line (taiga_queue_initiative says more); done and canceled finish it by hand. The queue is the only way to start work: there is no "plan this now", and nothing is moved into Build by a person. Moving an initiative out of Build stops the plan being written or the run in flight: that is how work is stopped, and it throws that work away. A move to ${PR_CLOSING_STATES.join(", ")} CLOSES a pull request the initiative is still waiting on (a move to queue keeps it, and done leaves it to be merged); Taiga leaves a comment on the pull request saying why. The state machine refuses a move that is not allowed (CONFLICT). A move into queue starts paid work and needs products:write. Ask the person before a move that starts work, stops work or closes a pull request. archived: true files the initiative where it stands (pass its current column as state; never from the Build station, plan or build) and archived: false brings it back; archived work is left out of lists unless includeArchived asks for it.`,
3621
4123
  inputSchema: z.object({
3622
- itemId,
4124
+ initiativeId,
3623
4125
  state: z.enum(USER_DROP_STATES).describe("The column to move into"),
3624
- 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"),
4126
+ afterId: idInput("initiative", "taiga_list_initiatives gives it").nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
3625
4127
  archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
3626
4128
  }),
3627
4129
  annotations: OPERATE
3628
4130
  }, (params) => runTool(async () => {
3629
- const { itemId: id, ...body } = params;
4131
+ const { initiativeId: id, ...body } = params;
3630
4132
  if (body.state === "queue" && !client.hasCapability("products:write")) return toolError("FORBIDDEN", "A move into queue starts agent work and needs products:write, which this key lacks.");
3631
4133
  return outcome((await client.transitionInitiative(id, body)).data);
3632
4134
  }));
@@ -3636,20 +4138,20 @@ const BUILD_STATION = /* @__PURE__ */ new Set(["plan", "build"]);
3636
4138
  function registerPlanAgain(server, client) {
3637
4139
  server.registerTool("taiga_plan_again", {
3638
4140
  title: "Plan an initiative again",
3639
- description: "Supersede the plan of the initiative at the Build station and ask the planner for a new one: for a plan that failed, a plan you do not want built, or a run whose direction was wrong once it has stopped. The earlier plan is kept, and every run records which plan version it built; a pull request still open stays open. Offered only when the turn is yours: refused (CONFLICT) while an agent holds it (planning, building, reviewing). Also refused while a dependency is still in flight or a UI direction has no chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the Build station, put the initiative in the queue instead (taiga_queue_backlog_item): the queue is the only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run.",
4141
+ description: "Supersede the plan of the initiative at the Build station and ask the planner for a new one: for a plan that failed, a plan you do not want built, or a run whose direction was wrong once it has stopped. The earlier plan is kept, and every run records which plan version it built; a pull request still open stays open. Offered only when the turn is yours: refused (CONFLICT) while an agent holds it (planning, building, reviewing). Also refused while a dependency is still in flight or a UI direction has no chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the Build station, put the initiative in the queue instead (taiga_queue_initiative): the queue is the only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run.",
3640
4142
  inputSchema: z.object({
3641
- itemId,
4143
+ initiativeId,
3642
4144
  force: z.boolean().optional().describe("Plan anyway past an unsettled dependency or an unchosen UI direction; only when the person says so")
3643
4145
  }),
3644
4146
  annotations: OPERATE
3645
4147
  }, (params) => runTool(async () => {
3646
- const item = (await client.getBacklogItem(params.itemId)).data;
3647
- if (!BUILD_STATION.has(String(item.state))) return toolError("CONFLICT", `This initiative is in ${String(item.state)}, not at the Build station. The queue is the only way to start work: put it in the queue with taiga_queue_backlog_item and it is planned when its turn comes.`);
4148
+ const item = (await client.getBacklogItem(params.initiativeId)).data;
4149
+ if (!BUILD_STATION.has(String(item.state))) return toolError("CONFLICT", `This initiative is in ${String(item.state)}, not at the Build station. The queue is the only way to start work: put it in the queue with taiga_queue_initiative and it is planned when its turn comes.`);
3648
4150
  const body = {
3649
4151
  state: "plan",
3650
4152
  ...params.force ? { force: true } : {}
3651
4153
  };
3652
- return outcome((await client.transitionInitiative(params.itemId, body)).data);
4154
+ return outcome((await client.transitionInitiative(params.initiativeId, body)).data);
3653
4155
  }));
3654
4156
  }
3655
4157
  /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
@@ -3693,17 +4195,17 @@ async function placeInQueue(client, item) {
3693
4195
  }
3694
4196
  const autoBuildInput = z.boolean().nullable().describe("false stops the line at this initiative's plan for a person to read it; true builds it as soon as it lands; null follows the product's default");
3695
4197
  function registerQueue(server, client) {
3696
- server.registerTool("taiga_queue_backlog_item", {
3697
- title: "Queue backlog item",
3698
- 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.",
4198
+ server.registerTool("taiga_queue_initiative", {
4199
+ title: "Queue initiative",
4200
+ 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. 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.",
3699
4201
  inputSchema: z.object({
3700
- itemId,
3701
- 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"),
4202
+ initiativeId,
4203
+ afterId: idInput("initiative", "taiga_list_initiatives 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"),
3702
4204
  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")
3703
4205
  }),
3704
4206
  annotations: OPERATE
3705
4207
  }, (params) => runTool(async () => {
3706
- const { itemId: id, autoBuild, ...placement } = params;
4208
+ const { initiativeId: id, autoBuild, ...placement } = params;
3707
4209
  if (autoBuild !== void 0) await client.setAutoBuild(id, autoBuild);
3708
4210
  const item = (await client.transitionInitiative(id, {
3709
4211
  state: "queue",
@@ -3714,7 +4216,7 @@ function registerQueue(server, client) {
3714
4216
  ...outcomeOf(item),
3715
4217
  reviewGate: gate,
3716
4218
  queue,
3717
- ...gate && queue ? {} : { note: "Part of the outcome could not be read; taiga_get_queue and taiga_get_product say the rest." }
4219
+ ...gate && queue ? {} : { note: "Part of the outcome could not be read; taiga_get_line and taiga_get_product say the rest." }
3718
4220
  });
3719
4221
  }));
3720
4222
  }
@@ -3723,12 +4225,12 @@ function registerReviewGate(server, client) {
3723
4225
  title: "Set an initiative's review gate",
3724
4226
  description: "Set whether an initiative's plan is built as soon as it lands (true) or waits for a person to read it and approve or reject it (false); null follows the product's default. Standing: it applies to every plan the initiative gets, including one being written now. Opening the gate on a plan that is already waiting starts its build at once. Returns the initiative with the gate that now applies.",
3725
4227
  inputSchema: z.object({
3726
- itemId,
4228
+ initiativeId,
3727
4229
  autoBuild: autoBuildInput
3728
4230
  }),
3729
4231
  annotations: OPERATE_SETTING
3730
4232
  }, (params) => runTool(async () => {
3731
- const item = (await client.setAutoBuild(params.itemId, params.autoBuild)).data;
4233
+ const item = (await client.setAutoBuild(params.initiativeId, params.autoBuild)).data;
3732
4234
  const gate = await reviewGate(client, item);
3733
4235
  return toolResult({
3734
4236
  ...outcomeOf(item),
@@ -3738,12 +4240,12 @@ function registerReviewGate(server, client) {
3738
4240
  }));
3739
4241
  }
3740
4242
  function registerResume(server, client) {
3741
- server.registerTool("taiga_resume_backlog_item", {
3742
- title: "Resume backlog item",
4243
+ server.registerTool("taiga_resume_initiative", {
4244
+ title: "Resume initiative",
3743
4245
  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.",
3744
- inputSchema: z.object({ itemId }),
4246
+ inputSchema: z.object({ initiativeId }),
3745
4247
  annotations: OPERATE
3746
- }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
4248
+ }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
3747
4249
  }
3748
4250
  function registerPlanVerdicts(server, client) {
3749
4251
  server.registerTool("taiga_approve_plan", {
@@ -3763,173 +4265,234 @@ function registerPlanVerdicts(server, client) {
3763
4265
  }, (params) => runTool(async () => outcome((await client.rejectPlan(params.planId, { comment: params.comment })).data)));
3764
4266
  }
3765
4267
  //#endregion
3766
- //#region src/tools/builds.ts
3767
- /** What a list row needs to say for the model to pick a run to read in full. */
3768
- const BUILD_SUMMARY_FIELDS = [
4268
+ //#region src/tools/maintenance.ts
4269
+ /**
4270
+ * Maintaining (docs.tai.ga, Operate): what the sweeps found in a product's
4271
+ * repository and where it stands against the organization's own policies,
4272
+ * and the two things a person does with a finding, Ignore and Fix. Sweeps run
4273
+ * themselves and nothing is marked fixed by hand: a finding resolves when the
4274
+ * next sweep no longer detects it, so there is no tool for either.
4275
+ */
4276
+ /** A finding as a list row: enough to decide whether to open, fix or ignore it. */
4277
+ const FINDING_ROW_FIELDS = [
3769
4278
  "id",
3770
- "runNumber",
3771
- "productId",
4279
+ "kind",
4280
+ "severity",
4281
+ "tier",
4282
+ "status",
4283
+ "title",
4284
+ "ecosystem",
4285
+ "packageName",
4286
+ "currentVersion",
4287
+ "recommendedVersion",
4288
+ "advisoryId",
4289
+ "location",
4290
+ "sourcePaths",
4291
+ "productRepositoryId",
4292
+ "ownership",
4293
+ "remediableBy",
4294
+ "controls",
3772
4295
  "backlogItemId",
3773
- "targetTitle",
3774
- "planId",
4296
+ "detectedAt",
4297
+ "ignoreReason",
4298
+ "ignoreExpiresAt"
4299
+ ];
4300
+ /** A sweep as the record of when the product was last checked, and whether that check held. */
4301
+ const SWEEP_FIELDS = [
4302
+ "id",
4303
+ "trigger",
3775
4304
  "status",
3776
- "failedReason",
3777
- "runtime",
3778
- "repositoryFullName",
3779
- "branch",
3780
- "baseBranch",
3781
- "completedSteps",
3782
- "totalSteps",
3783
- "prNumber",
3784
- "prUrl",
3785
- "prState",
3786
- "prDraft",
3787
- "prMerged",
3788
- "actions",
4305
+ "scanners",
4306
+ "findingsCount",
4307
+ "newFindingsCount",
4308
+ "error",
3789
4309
  "createdAt",
3790
- "startedAt",
3791
4310
  "completedAt"
3792
4311
  ];
3793
- /** What a run says about itself after a stop or a resume: its state and what can be done next. */
3794
- const RUN_OUTCOME_FIELDS = [
4312
+ /** A policy violation in a list: what it is, how bad, and what is being done about it. */
4313
+ const VIOLATION_FIELDS = [
3795
4314
  "id",
3796
- "runNumber",
3797
- "backlogItemId",
4315
+ "controlId",
4316
+ "policyType",
4317
+ "severity",
3798
4318
  "status",
3799
- "failedReason",
3800
- "completedSteps",
3801
- "totalSteps",
3802
- "prUrl",
3803
- "actions"
3804
- ];
3805
- /**
3806
- * Where a run that ended without finishing stopped, from its steps and its
3807
- * hardening result: a failed run with every step completed reads as nothing
3808
- * failed, and retry_from_failed_step then reads as a mistake, when the agent
3809
- * stalled in the hardening pass after the steps and the retry continues it.
3810
- * Null for a run still going or that finished.
3811
- */
3812
- function stoppedIn(run, steps) {
3813
- if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
3814
- if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
3815
- const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
3816
- 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)})`;
3817
- 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.";
3818
- return "after the steps and hardening, at the pull request";
3819
- }
3820
- /** How many products the org-wide list names; past this a row keeps its productId only. */
3821
- const NAMED_PRODUCTS = 100;
3822
- const buildId = idInput("build run", "taiga_list_builds gives it");
3823
- /** A step without its instructions: what it is, how it stands and what it changed. */
3824
- const STEP_SUMMARY_FIELDS = [
3825
- "stepOrder",
3826
4319
  "title",
3827
- "status",
3828
- "currentPhase",
3829
- "commitSha",
3830
- "filesChanged",
3831
- "verification",
3832
- "error",
3833
- "errorClassification",
3834
- "startedAt",
3835
- "completedAt"
4320
+ "summary",
4321
+ "work",
4322
+ "unconfirmedSince",
4323
+ "verifyRequestedAt",
4324
+ "assessedAt"
3836
4325
  ];
3837
- /** Past this many characters a step's headline is cut; the full read has the rest. */
3838
- const HEADLINE_LIMIT = 600;
3839
- /**
3840
- * The first paragraph of a step's summary: the agent writes its result
3841
- * there (what changed, which checks passed), and the detail after it.
3842
- */
3843
- function summaryHeadline(summary) {
3844
- if (typeof summary !== "string" || !summary.trim()) return null;
3845
- const first = summary.trim().split(/\n\s*\n/, 1)[0].trim();
3846
- return first.length > HEADLINE_LIMIT ? `${first.slice(0, HEADLINE_LIMIT).trimEnd()}...` : first;
3847
- }
3848
4326
  /**
3849
- * The runs of every product the key can see, each row naming its product.
3850
- * The route carries product ids only; one products read names them, and a
3851
- * failure of that read leaves the rows unnamed rather than failing the list.
3852
- */
3853
- async function activityRows(client, opts) {
3854
- const [runs, products] = await Promise.all([client.listBuildRunActivity(opts), client.listProducts({ limit: NAMED_PRODUCTS }).catch((error) => {
3855
- if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
3856
- throw error;
3857
- })]);
3858
- const nameById = /* @__PURE__ */ new Map();
3859
- for (const product of products?.data ?? []) nameById.set(product.id, {
3860
- name: product.name,
3861
- slug: product.slug
3862
- });
3863
- const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map((row) => {
3864
- const product = nameById.get(String(row.productId));
3865
- return product ? {
3866
- ...row,
3867
- product
3868
- } : row;
3869
- });
3870
- const note = !rows.some((row) => !("product" in row)) ? null : !products ? "The products could not be read, so rows carry productId only; taiga_get_product names one." : products.nextCursor !== null ? `Only the first ${NAMED_PRODUCTS} products could be named; a row without product carries productId only.` : "A row without product belongs to a product the key cannot list (deleted since, or not visible to it); it carries productId only.";
4327
+ * How bad a product's policy standing is, as counts: the controls something is
4328
+ * recorded against, the open violations by severity, the resolved ones. The
4329
+ * API's standing carries the lists only; this tool and the product context
4330
+ * both count them here.
4331
+ */
4332
+ function standingSummary(standing) {
4333
+ const controls = Array.isArray(standing.controls) ? standing.controls : [];
4334
+ const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
4335
+ const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
4336
+ const listed = controls.filter((c) => Number(c["divergenceCount"] ?? 0) > 0 || Number(c["findingCount"] ?? 0) > 0);
4337
+ const bySeverity = {};
4338
+ for (const violation of violations) {
4339
+ const severity = String(violation["severity"] ?? "unknown");
4340
+ bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
4341
+ }
3871
4342
  return {
3872
- ...page(rows, runs.nextCursor),
3873
- ...note ? { note } : {}
4343
+ controls: listed.length,
4344
+ violations: violations.length,
4345
+ bySeverity,
4346
+ resolvedViolations: resolved
3874
4347
  };
3875
4348
  }
3876
- function registerBuildTools(server, client) {
4349
+ const findingId = idInput("finding", "taiga_list_findings gives it");
4350
+ function registerMaintenanceTools(server, client) {
3877
4351
  if (!client.hasCapability("products:read")) return;
3878
- server.registerTool("taiga_list_builds", {
3879
- title: "List build runs",
3880
- 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.",
4352
+ server.registerTool("taiga_get_maintenance", {
4353
+ title: "Get a product's maintenance standing",
4354
+ description: "Where a product's repository stands: open findings by severity and kind, the most pressing ones, and the latest sweeps, so you can tell when the product was last checked and whether that check succeeded. A quiet list from a sweep that last ran long ago, or that failed, is not an all-clear; a product never swept says so. Sweeps run on their own after a push and periodically; nothing here starts one. This is the overview of the scanners alone; taiga_list_findings lists each finding to act on, and taiga_get_policy_standing judges the product against the policies (scanner findings and the documents).",
4355
+ inputSchema: z.object({ productId }),
4356
+ annotations: READ_ONLY
4357
+ }, (params) => runTool(async () => {
4358
+ const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
4359
+ const latest = pickFields(sweeps.data, SWEEP_FIELDS);
4360
+ const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
4361
+ return toolResult({
4362
+ ...standing,
4363
+ latestSweeps: latest,
4364
+ ...latest.length === 0 ? { note: "This product has never been swept: no result is not an all-clear." } : {}
4365
+ });
4366
+ }));
4367
+ server.registerTool("taiga_list_findings", {
4368
+ title: "List maintenance findings",
4369
+ description: "List what the sweeps found in a product's repository: vulnerable or outdated dependencies, committed secrets, insecure code (sast) and infrastructure misconfigurations (iac_misconfig). Each row carries its severity and its priority tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), where it is, who can fix it (remediableBy: coding_agent, or upstream_only for a submodule or a dependency with no fixed version), the policy controls it breaches, and the initiative already fixing it (initiativeId). Omit status for the findings that need action; work from the top tier down. Read one in full with taiga_get_finding. taiga_get_maintenance has the counts and when the last sweep ran, which says whether an empty list can be trusted.",
3881
4370
  inputSchema: z.object({
3882
- productId: productId.optional(),
3883
- 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"),
3884
- 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"),
4371
+ productId,
4372
+ status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
4373
+ severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
4374
+ kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
3885
4375
  ...pagination
3886
4376
  }),
3887
4377
  annotations: READ_ONLY
3888
4378
  }, (params) => runTool(async () => {
3889
- 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).");
3890
- 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.");
3891
- if (params.productId) {
3892
- const result = await client.listBuildRuns(params.productId, params);
3893
- return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
3894
- }
3895
- return toolResult(await activityRows(client, params));
4379
+ const { productId: id, ...filters } = params;
4380
+ const result = await client.listFindings(id, filters);
4381
+ return toolResult({
4382
+ ...page(pickFields(result.data, FINDING_ROW_FIELDS), result.nextCursor),
4383
+ ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
4384
+ });
4385
+ }));
4386
+ server.registerTool("taiga_get_finding", {
4387
+ title: "Get a maintenance finding",
4388
+ description: "Read one finding in full: what it is, how likely it is to be exploited (EPSS, the known-exploited list), the advisory and the fixed version, the place in the code, the policy controls it breaches, why it is ranked where it is, its history (reintroduced, previously resolved) and the remediation under way. A secret's value is never stored; the finding names the rule and the location only.",
4389
+ inputSchema: z.object({ findingId }),
4390
+ annotations: READ_ONLY
4391
+ }, (params) => runTool(async () => {
4392
+ const { runId, ...finding } = (await client.getFinding(params.findingId)).data;
4393
+ return toolResult(runId === void 0 ? finding : {
4394
+ sweepId: runId,
4395
+ ...finding
4396
+ });
3896
4397
  }));
3897
- server.registerTool("taiga_get_build", {
3898
- title: "Get build run",
3899
- 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.",
4398
+ server.registerTool("taiga_get_policy_standing", {
4399
+ title: "Get a product's policy standing",
4400
+ 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 is breached when open scanner findings map to it, diverged when the assessment of the product documents found them contradicting it (a control can be both; state names the stronger, breached). A violation's work is the initiative raised for it; live: false means that initiative is done or canceled without the violation going away, so it needs a person or a new fix. A control breached by scanner findings counts them by tier (findingsByTier): a breach of routine findings only, such as secrets in test fixtures, is not an urgent one. 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.",
3900
4401
  inputSchema: z.object({
3901
- buildId,
3902
- full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
4402
+ productId,
4403
+ full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
3903
4404
  }),
3904
4405
  annotations: READ_ONLY
3905
4406
  }, (params) => runTool(async () => {
3906
- const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
3907
- const runRow = run.data;
3908
- const rows = steps.data;
3909
- const where = stoppedIn(runRow, rows);
3910
- const stop = where ? { stoppedIn: where } : {};
4407
+ const standing = (await client.getPolicyStanding(params.productId)).data;
4408
+ const controls = Array.isArray(standing.controls) ? standing.controls : [];
4409
+ const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
4410
+ const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
4411
+ const summary = standingSummary(standing);
3911
4412
  if (params.full) return toolResult({
3912
- ...runRow,
3913
- ...stop,
3914
- steps: steps.data
4413
+ summary,
4414
+ ...standing
3915
4415
  });
3916
4416
  return toolResult({
3917
- ...runRow,
3918
- ...stop,
3919
- steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
3920
- ...step,
3921
- headline: summaryHeadline(rows[index].summary)
3922
- }))
4417
+ summary,
4418
+ productId: standing.productId,
4419
+ hasPolicies: standing.hasPolicies,
4420
+ assessment: standing.assessment,
4421
+ documents: standing.documents,
4422
+ controls: pickFields(listed, [
4423
+ "controlId",
4424
+ "policyType",
4425
+ "text",
4426
+ "state",
4427
+ "divergenceCount",
4428
+ "findingCount",
4429
+ "findingsByTier",
4430
+ "plannedCount"
4431
+ ]),
4432
+ violations: pickFields(violations, VIOLATION_FIELDS)
3923
4433
  });
3924
4434
  }));
3925
4435
  if (!client.hasCapability("products:write")) return;
3926
- server.registerTool("taiga_stop_build", {
3927
- title: "Stop a build run",
3928
- description: "The emergency brake: stop a run the coding agent is executing when its initiative cannot be moved. The way to stop work is on the initiative: move it out of Build (taiga_move_backlog_item), which stops the plan or run in flight. Use this only for a run whose initiative does not offer that (it is finished, canceled or gone) yet the run is still going. The agent halts, its branch and any draft pull request stay. Only a run whose actions include stop; the API refuses the rest with CONFLICT. Ask the person before calling this: it interrupts work in progress.",
3929
- inputSchema: z.object({ buildId }),
4436
+ server.registerTool("taiga_ignore_finding", {
4437
+ title: "Ignore or reopen a finding",
4438
+ description: "Ignore a finding that does not apply or whose risk is accepted, with the reason (false_positive, not_reachable, accepted_risk, wont_fix), an optional note and an optional expiry after which it resurfaces if it still exists; or reopen an ignored one (ignored: false). Ignored findings are kept and reviewable. There is no \"mark fixed\": a finding resolves when the next sweep no longer detects it. For a secret that is only in history, rotate the credential outside Taiga, then ignore it with a reason. Ignoring hides a shared signal and can record the team's acceptance of a risk, which is the person's decision: ask the person first.",
4439
+ inputSchema: z.object({
4440
+ findingId,
4441
+ ignored: z.boolean().describe("true ignores the finding, false reopens it"),
4442
+ reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe("Why it is ignored; required when ignoring"),
4443
+ note: z.string().trim().max(280).optional().describe("A short justification, kept for audit"),
4444
+ expiresAt: z.string().datetime({ offset: true }).optional().describe("When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)")
4445
+ }),
4446
+ annotations: OPERATE
4447
+ }, (params) => runTool(async () => {
4448
+ if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
4449
+ const body = params.ignored ? {
4450
+ status: "ignored",
4451
+ ignoreReason: params.reason,
4452
+ ...params.note !== void 0 ? { ignoreNote: params.note } : {},
4453
+ ...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
4454
+ } : { status: "open" };
4455
+ const finding = (await client.updateFinding(params.findingId, body)).data;
4456
+ return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, "ignoreNote"])[0]);
4457
+ }));
4458
+ server.registerTool("taiga_fix_finding", {
4459
+ title: "Fix a finding",
4460
+ description: "Ask for a finding's fix: Taiga raises an initiative and adds it to the end of the product's queue, where the line plans it in turn; whether the build then waits for a person is the initiative's review gate, as for any initiative. The fix is a normal run that ends in a pull request you review and merge. With wholeGroup: true it fixes the finding's whole group in one initiative, as the page does: for a dependency, every open advisory on that package at that installed version in one version bump; for code or infrastructure, every location the same rule flagged. Absent for a secret only in history, anything in a submodule, and a dependency with no fixed version (remediableBy says upstream_only); the API refuses those. Starts paid work.",
4461
+ inputSchema: z.object({
4462
+ productId,
4463
+ findingId,
4464
+ wholeGroup: z.boolean().optional().describe("Fix every finding in this finding's group (its package and version, or its rule) together")
4465
+ }),
4466
+ annotations: OPERATE
4467
+ }, (params) => runTool(async () => {
4468
+ if (!params.wholeGroup) {
4469
+ const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
4470
+ return toolResult({
4471
+ backlogItemId: fixed.backlogItemId,
4472
+ finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null
4473
+ });
4474
+ }
4475
+ const finding = (await client.getFinding(params.findingId)).data;
4476
+ if (finding.productId !== params.productId) return toolError("NOT_FOUND", "No such finding in this product.");
4477
+ return toolResult((await client.remediatePackage(params.productId, {
4478
+ ecosystem: String(finding.ecosystem),
4479
+ packageName: String(finding.packageName),
4480
+ currentVersion: typeof finding.currentVersion === "string" ? finding.currentVersion : null,
4481
+ productRepositoryId: typeof finding.productRepositoryId === "string" ? finding.productRepositoryId : null
4482
+ })).data);
4483
+ }));
4484
+ server.registerTool("taiga_fix_policy_violation", {
4485
+ title: "Fix policy violations",
4486
+ description: "Ask for the fix of a product's policy violations: name a control to cover every violation recorded against it that has no initiative yet, or a single violation (violationId) when the control has more than one. Taiga raises one initiative and adds it to the end of the product's queue, exactly as a finding fix does. The violation stays listed while the work is under way: only the next assessment can record that the documents no longer show it. Starts paid work.",
4487
+ inputSchema: z.object({
4488
+ productId,
4489
+ controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
4490
+ violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
4491
+ }),
3930
4492
  annotations: OPERATE
3931
4493
  }, (params) => runTool(async () => {
3932
- return toolResult(pickFields([(await client.stopBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
4494
+ 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.");
4495
+ return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
3933
4496
  }));
3934
4497
  }
3935
4498
  //#endregion
@@ -3953,9 +4516,9 @@ const REPOSITORY_SUMMARY_FIELDS = [
3953
4516
  "effectiveBranch",
3954
4517
  "isManaged"
3955
4518
  ];
3956
- /** How many of the latest runs to carry; taiga_list_builds pages the rest. */
4519
+ /** How many of the latest runs to carry; taiga_list_runs pages the rest. */
3957
4520
  const BUILDS_IN_CONTEXT = 10;
3958
- /** A run as the bundle carries it: enough to say what was built and where; taiga_get_build has the rest. */
4521
+ /** A run as the bundle carries it: enough to say what was built and where; taiga_get_run has the rest. */
3959
4522
  const CONTEXT_BUILD_FIELDS = [
3960
4523
  "id",
3961
4524
  "runNumber",
@@ -3965,6 +4528,8 @@ const CONTEXT_BUILD_FIELDS = [
3965
4528
  "failedReason",
3966
4529
  "completedSteps",
3967
4530
  "totalSteps",
4531
+ "baseBranch",
4532
+ "prNumber",
3968
4533
  "prUrl",
3969
4534
  "prState",
3970
4535
  "prMerged",
@@ -4022,7 +4587,7 @@ const DOCUMENT_DEPTHS = [
4022
4587
  const BACKLOG_PAGE_LIMIT = 10;
4023
4588
  /**
4024
4589
  * Open items the bundle carries, the newest of those read; past this it says truncated and
4025
- * taiga_list_backlog_items pages the rest. Keeps a product with hundreds of
4590
+ * taiga_list_initiatives pages the rest. Keeps a product with hundreds of
4026
4591
  * open initiatives inside the tool result the document budget is sized for.
4027
4592
  */
4028
4593
  const BACKLOG_IN_CONTEXT = 50;
@@ -4032,7 +4597,10 @@ const PARTS = {
4032
4597
  policies: "tenant:documents:read",
4033
4598
  backlog: "backlog:read",
4034
4599
  repositories: "products:read",
4035
- builds: "products:read"
4600
+ builds: "products:read",
4601
+ attention: "products:read",
4602
+ maintenance: "products:read",
4603
+ standing: "products:read"
4036
4604
  };
4037
4605
  /** What the API said when a part failed, in the words the bundle reports. */
4038
4606
  function unavailableReason(reason) {
@@ -4188,9 +4756,9 @@ async function backlogPart(client, productId) {
4188
4756
  const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
4189
4757
  if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
4190
4758
  return {
4191
- items: itemRows(rows.slice(-50), BACKLOG_SUMMARY_FIELDS),
4759
+ items: situatedRows(rows.slice(-50)),
4192
4760
  truncated,
4193
- ...pageCapped ? { note: "More than 1000 open items in one status: only the oldest of them were read, so the newest may be missing here; taiga_list_backlog_items pages the rest." } : {}
4761
+ ...pageCapped ? { note: "More than 1000 open items in one status: only the oldest of them were read, so the newest may be missing here; taiga_list_initiatives pages the rest." } : {}
4194
4762
  };
4195
4763
  }
4196
4764
  /**
@@ -4206,7 +4774,53 @@ async function repositoriesPart(client, productId) {
4206
4774
  }
4207
4775
  async function buildsPart(client, productId) {
4208
4776
  const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
4209
- return pickFields(rows, CONTEXT_BUILD_FIELDS);
4777
+ return pickFields(rows, CONTEXT_BUILD_FIELDS).map(readableRun);
4778
+ }
4779
+ /**
4780
+ * What waits on a person for this product, counted by kind (PRO-2800): the
4781
+ * reviewing agent called four tools to answer "how does it stand"; the
4782
+ * counts say whether taiga_list_action_required is worth a call.
4783
+ */
4784
+ async function attentionPart(client, productId) {
4785
+ const result = await client.listInbox({
4786
+ productId,
4787
+ limit: 100,
4788
+ withTotal: true
4789
+ });
4790
+ const now = Date.now();
4791
+ const rows = result.data;
4792
+ const byKind = {};
4793
+ for (const row of rows) byKind[String(row["kind"])] = (byKind[String(row["kind"])] ?? 0) + 1;
4794
+ const snoozed = rows.filter((row) => row["status"] === "snoozed" && typeof row["snoozedUntil"] === "string" && Date.parse(row["snoozedUntil"]) > now).length;
4795
+ return {
4796
+ open: typeof result.totalCount === "number" ? result.totalCount : rows.length,
4797
+ byKind,
4798
+ ...snoozed > 0 ? { snoozed } : {},
4799
+ ...result.nextCursor ? { note: "byKind and snoozed count the first 100; taiga_list_action_required pages the rest." } : {}
4800
+ };
4801
+ }
4802
+ /** The latest sweep and what it left open, as counts; taiga_list_findings lists them. */
4803
+ async function maintenancePart(client, productId) {
4804
+ const summary = (await client.getMaintenanceSummary(productId)).data;
4805
+ const last = summary["lastRun"];
4806
+ return {
4807
+ openFindings: summary["openFindings"],
4808
+ bySeverity: summary["severityCounts"],
4809
+ lastSweep: last ? {
4810
+ status: last["status"],
4811
+ completedAt: last["completedAt"]
4812
+ } : null,
4813
+ ...last ? {} : { note: "Never swept: no findings is not an all-clear." }
4814
+ };
4815
+ }
4816
+ /** Where the product stands against the policies, as counts; taiga_get_policy_standing has each violation. */
4817
+ async function standingPart(client, productId) {
4818
+ const summary = standingSummary((await client.getPolicyStanding(productId)).data);
4819
+ return {
4820
+ openViolations: summary.violations,
4821
+ bySeverity: summary.bySeverity,
4822
+ controlsViolated: summary.controls
4823
+ };
4210
4824
  }
4211
4825
  async function productContext(client, product, options) {
4212
4826
  const productId = product.id;
@@ -4218,16 +4832,18 @@ async function productContext(client, product, options) {
4218
4832
  policies: () => policiesPart(client),
4219
4833
  backlog: () => backlogPart(client, productId),
4220
4834
  repositories: () => repositoriesPart(client, productId),
4221
- builds: () => buildsPart(client, productId)
4835
+ builds: () => buildsPart(client, productId),
4836
+ attention: () => attentionPart(client, productId),
4837
+ maintenance: () => maintenancePart(client, productId),
4838
+ standing: () => standingPart(client, productId)
4222
4839
  };
4223
4840
  const parts = Object.keys(fetchers).filter(granted);
4224
- const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
4225
- const nextStep = setupNextStep(product);
4841
+ const [settled, readiness] = await Promise.all([Promise.allSettled(parts.map((part) => fetchers[part]())), setupReadiness(client, product)]);
4226
4842
  const bundle = { product: {
4227
4843
  ...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
4228
4844
  projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
4229
4845
  defaultOrigin: defaultOriginFor(product),
4230
- ...nextStep ? { nextStep } : {}
4846
+ ...readiness ? { readiness } : {}
4231
4847
  } };
4232
4848
  const unavailable = {};
4233
4849
  parts.forEach((part, index) => {
@@ -4240,15 +4856,15 @@ async function productContext(client, product, options) {
4240
4856
  ...bundle,
4241
4857
  notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
4242
4858
  unavailable,
4243
- readNext: "Read a policy with taiga_get_document (sections: the ids listed here, for only those), the specification's or architecture's sections or 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."
4859
+ readNext: "Read a policy with taiga_get_document (sections: the ids listed here, for only those), the specification's or architecture's sections or another product document with taiga_get_product_document, an initiative with taiga_get_initiative (taiga_list_initiatives pages a truncated backlog) and a run with taiga_get_run."
4244
4860
  };
4245
- let characters = JSON.stringify(result, null, 2).length;
4861
+ let characters = JSON.stringify(inProductWords(result), null, 2).length;
4246
4862
  for (let measured = -1; measured !== characters;) {
4247
4863
  measured = characters;
4248
- characters = JSON.stringify({
4864
+ characters = JSON.stringify(inProductWords({
4249
4865
  ...result,
4250
4866
  characters: measured
4251
- }, null, 2).length;
4867
+ }), null, 2).length;
4252
4868
  }
4253
4869
  return toolResult({
4254
4870
  ...result,
@@ -4259,7 +4875,7 @@ function registerContextTools(server, client) {
4259
4875
  if (!client.hasCapability("products:read")) return;
4260
4876
  server.registerTool("taiga_get_product_context", {
4261
4877
  title: "Get product context",
4262
- description: `Everything a coding session starts from, in one call: the product and which document timeline is its own, its factory with the instructions in full, the specification and architecture from that timeline, every published policy as its section ids, the open backlog items, the linked repositories and the latest runs. Name the product by ID, slug or the git remote URL of the checkout. A part the API refused or failed is named under unavailable with what it said; a part the key cannot read is named under notGranted; neither is shown as empty. The two documents come as their outline by default: each section id with its size, to read with taiga_get_product_document naming the sections. Before writing code, documents: budget brings as many whole sections as fit, about ${DOCUMENT_BUDGET / 1e3}k characters each, naming the ones left out; full brings them whole, tens of thousands of tokens on a large product and more than some clients accept. characters says how large the result is. Call this first, then read what you need.`,
4878
+ description: `Where a product stands and everything a coding session starts from, in one call. Name the product by ID, slug or the git remote URL of a checkout. It carries: the product (and, while in setup, readiness: what it still needs and the next move); its factory with the instructions in full; the specification and architecture as outlines (section ids and sizes); every published policy as its section ids; the open initiatives (not the done or canceled ones: taiga_list_initiatives has those); the linked repositories; the latest runs, each with its outcome in a sentence; attention, what waits on a person for it, counted by kind; maintenance, open findings by severity and the last sweep; standing, open policy violations by severity. A part the API refused or failed is named under unavailable; a part the key cannot read under notGranted; neither is shown as empty. Before writing code, documents: budget brings the whole sections that fit, about ${DOCUMENT_BUDGET / 1e3}k characters each, and full brings them whole (tens of thousands of tokens on a large product). Unlike taiga_get_product, which is the product record and its settings alone. Call this first, then read what you need.`,
4263
4879
  inputSchema: z.object({
4264
4880
  productId: productId.optional(),
4265
4881
  slug: slug.optional(),
@@ -4316,12 +4932,12 @@ function registerAutonomyTools(server, client) {
4316
4932
  title: "Set whether an initiative's pull request merges on its own",
4317
4933
  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.",
4318
4934
  inputSchema: z.object({
4319
- itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
4935
+ initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
4320
4936
  autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
4321
4937
  }),
4322
4938
  annotations: OPERATE_SETTING
4323
4939
  }, (params) => runTool(async () => {
4324
- const item = (await client.updateBacklogItem(params.itemId, { autoMerge: params.autoMerge })).data;
4940
+ const item = (await client.updateBacklogItem(params.initiativeId, { autoMerge: params.autoMerge })).data;
4325
4941
  return toolResult(pickFields([item], [
4326
4942
  "id",
4327
4943
  "title",
@@ -4334,11 +4950,9 @@ function registerAutonomyTools(server, client) {
4334
4950
  //#endregion
4335
4951
  //#region src/tools/inbox.ts
4336
4952
  /**
4337
- * Action Required (docs.tai.ga, Deliver): where work comes back to a person.
4338
- * Things to review (a plan, a pull request, findings, policy violations) and
4339
- * things to set up that only a person can (a secret, an account, a cloud
4340
- * permission). The inbox is the team's: resolving an item clears it for
4341
- * everybody, so these tools say what they clear.
4953
+ * An item as a row: what it is, whose product, how long it has waited. Not
4954
+ * its metadata by default: a document decision carried its whole text there,
4955
+ * and fifty rows came to 57 KB (PRO-2800); full: true brings it.
4342
4956
  */
4343
4957
  const ITEM_FIELDS = [
4344
4958
  "id",
@@ -4353,10 +4967,48 @@ const ITEM_FIELDS = [
4353
4967
  "status",
4354
4968
  "blocking",
4355
4969
  "snoozedUntil",
4356
- "metadata",
4357
4970
  "createdAt",
4358
4971
  "resolvedAt"
4359
4972
  ];
4973
+ const DAY_MS = 864e5;
4974
+ /** When an item stopped waiting: its resolution, or now while it is open. */
4975
+ function waitedUntil(resolvedAt, now) {
4976
+ const at = typeof resolvedAt === "string" ? Date.parse(resolvedAt) : NaN;
4977
+ return Number.isNaN(at) ? now : at;
4978
+ }
4979
+ /** A row as an agent reads it: the pull request to open when there is one, and how many days it has waited. */
4980
+ function itemRow(row, now, full) {
4981
+ const { snoozedUntil, resolvedAt, ...rest } = pickFields([row], ITEM_FIELDS)[0];
4982
+ const metadata = row["metadata"] ?? {};
4983
+ const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
4984
+ return {
4985
+ ...rest,
4986
+ ...snoozedUntil ? { snoozedUntil } : {},
4987
+ ...resolvedAt ? { resolvedAt } : {},
4988
+ ...typeof metadata["prUrl"] === "string" ? { prUrl: metadata["prUrl"] } : {},
4989
+ ...Number.isNaN(created) ? {} : { waitingDays: Math.floor((waitedUntil(resolvedAt, now) - created) / DAY_MS) },
4990
+ ...full ? { metadata: row["metadata"] } : {}
4991
+ };
4992
+ }
4993
+ /** What is open, counted by product and kind: the answer to "what needs attention" without reading every row. */
4994
+ function summaryOf(rows) {
4995
+ const byProduct = /* @__PURE__ */ new Map();
4996
+ for (const row of rows) {
4997
+ const key = String(row["productId"]);
4998
+ const entry = byProduct.get(key) ?? {
4999
+ productId: key,
5000
+ product: row["productName"] ?? key,
5001
+ open: 0,
5002
+ byKind: {}
5003
+ };
5004
+ entry.open += 1;
5005
+ entry.byKind[String(row["kind"])] = (entry.byKind[String(row["kind"])] ?? 0) + 1;
5006
+ byProduct.set(key, entry);
5007
+ }
5008
+ return [...byProduct.values()].sort((a, b) => b.open - a.open);
5009
+ }
5010
+ /** Pages read for the summary; past this it says it counted only those. */
5011
+ const SUMMARY_PAGE = 100;
4360
5012
  const SETUP_TASK_FIELDS = [
4361
5013
  "id",
4362
5014
  "productId",
@@ -4377,38 +5029,94 @@ const SETUP_TASK_FIELDS = [
4377
5029
  function totalOf(result) {
4378
5030
  return typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {};
4379
5031
  }
5032
+ /**
5033
+ * One row per variable: tasks filed before the API merged a key's categories
5034
+ * (PRO-2782) still list JWT_SECRET once as a secret and once as an env_var.
5035
+ * The oldest row stays, and names the others under alsoFiledAs, so the
5036
+ * duplicates read as one thing to set with several records, not several things.
5037
+ */
5038
+ function sameKeyOnce(rows) {
5039
+ const first = /* @__PURE__ */ new Map();
5040
+ const out = [];
5041
+ const isOpen = (row) => row["inboxItemStatus"] !== "done" && row["inboxItemStatus"] !== "dismissed";
5042
+ for (const row of rows) {
5043
+ if (typeof row["key"] !== "string" || !row["key"]) {
5044
+ out.push(row);
5045
+ continue;
5046
+ }
5047
+ const key = `${String(row["productId"])}:${row["key"]}`;
5048
+ const kept = first.get(key);
5049
+ if (!kept) {
5050
+ const copy = { ...row };
5051
+ first.set(key, copy);
5052
+ out.push(copy);
5053
+ continue;
5054
+ }
5055
+ const also = kept["alsoFiledAs"] ?? [];
5056
+ const record = (from) => ({
5057
+ id: from["id"],
5058
+ category: from["category"],
5059
+ title: from["title"],
5060
+ inboxItemStatus: from["inboxItemStatus"]
5061
+ });
5062
+ if (!isOpen(kept) && isOpen(row)) {
5063
+ const { alsoFiledAs: _also, ...was } = kept;
5064
+ for (const field of Object.keys(kept)) delete kept[field];
5065
+ Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
5066
+ } else kept["alsoFiledAs"] = [...also, record(row)];
5067
+ }
5068
+ return out;
5069
+ }
4380
5070
  function registerInboxTools(server, client) {
4381
5071
  if (!client.hasCapability("products:read")) return;
4382
5072
  server.registerTool("taiga_list_action_required", {
4383
5073
  title: "List what needs a person",
4384
- 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). The first page says how many there are on every page (totalCount, snoozed ones included). taiga_list_setup_tasks has the setup tasks with what exactly to set.",
5074
+ description: "Read Action Required, where work comes back to a person, shared by the team: things to review (a plan, a set of initiatives, a document decision, a pull request, maintenance findings, new policy violations) 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. The first page opens with summary: what is open, counted by product and kind, so \"what needs attention\" is one call, and totalCount. Each row says how many days it has waited (waitingDays) and, for a pull request review, the prUrl; an old item is still open, not stale. category gate is the reviews, setup the setup items; resolved: true reads the archive. An item snoozed until later is left out until then (snoozedHidden counts them). full: true adds each item's metadata. Unlike taiga_list_setup_tasks, which lists the setup items with exactly what to set and where.",
4385
5075
  inputSchema: z.object({
4386
5076
  productId: productId.optional(),
4387
5077
  category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
4388
5078
  resolved: z.boolean().optional().describe("Read the resolved archive (done and dismissed) instead"),
5079
+ full: z.boolean().optional().describe("Carry each item's metadata, such as a document decision's whole text (default false)"),
4389
5080
  ...pagination
4390
5081
  }),
4391
5082
  annotations: READ_ONLY
4392
5083
  }, (params) => runTool(async () => {
4393
- const { resolved, ...rest } = params;
4394
- const result = await client.listInbox({
5084
+ const { resolved, full, ...rest } = params;
5085
+ const query = {
4395
5086
  ...rest,
4396
- ...resolved ? { resolved: true } : {},
4397
- ...params.cursor === void 0 ? { withTotal: true } : {}
5087
+ ...resolved ? { resolved: true } : {}
5088
+ };
5089
+ const first = params.cursor === void 0;
5090
+ const result = await client.listInbox({
5091
+ ...query,
5092
+ ...first ? { withTotal: true } : {}
5093
+ });
5094
+ const overview = !first || resolved ? null : !result.nextCursor ? result : await client.listInbox({
5095
+ ...query,
5096
+ limit: SUMMARY_PAGE,
5097
+ cursor: void 0
5098
+ }).catch((error) => {
5099
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
5100
+ throw error;
4398
5101
  });
4399
5102
  const now = Date.now();
5103
+ const isDue = (row) => !(row["status"] === "snoozed" && typeof row["snoozedUntil"] === "string" && Date.parse(row["snoozedUntil"]) > now);
4400
5104
  const rows = result.data;
4401
- const due = rows.filter((row) => !(row.status === "snoozed" && typeof row.snoozedUntil === "string" && Date.parse(row.snoozedUntil) > now));
5105
+ const due = rows.filter(isDue);
4402
5106
  const hidden = rows.length - due.length;
4403
5107
  return toolResult({
4404
- ...page(pickFields(due, ITEM_FIELDS), result.nextCursor),
5108
+ ...overview ? {
5109
+ summary: summaryOf(overview.data.filter(isDue)),
5110
+ ...overview.nextCursor ? { summaryNote: `The summary counts the first ${SUMMARY_PAGE} items.` } : {}
5111
+ } : {},
5112
+ ...page(due.map((row) => itemRow(row, now, full === true)), result.nextCursor),
4405
5113
  ...totalOf(result),
4406
5114
  ...hidden > 0 ? { snoozedHidden: hidden } : {}
4407
5115
  });
4408
5116
  }));
4409
5117
  server.registerTool("taiga_list_setup_tasks", {
4410
5118
  title: "List setup tasks",
4411
- 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. The first page says how many there are on every page (totalCount).",
5119
+ 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. The first page says how many there are on every page (totalCount). One row per variable: a variable filed twice (as a secret and as an environment variable) lists the other records under alsoFiledAs, an open one first; within a page, and totalCount counts every record. Unlike taiga_list_action_required, whose setup items are the same work as rows to resolve, this says exactly what to set and where.",
4412
5120
  inputSchema: z.object({
4413
5121
  productId: productId.optional(),
4414
5122
  includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
@@ -4423,7 +5131,7 @@ function registerInboxTools(server, client) {
4423
5131
  ...params.cursor === void 0 ? { withTotal: true } : {}
4424
5132
  });
4425
5133
  return toolResult({
4426
- ...page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor),
5134
+ ...page(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor),
4427
5135
  ...totalOf(result)
4428
5136
  });
4429
5137
  }));
@@ -4432,7 +5140,7 @@ function registerInboxTools(server, client) {
4432
5140
  title: "Resolve or snooze an Action Required item",
4433
5141
  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.",
4434
5142
  inputSchema: z.object({
4435
- itemId: idInput("Action Required item", "taiga_list_action_required gives it"),
5143
+ actionId: idInput("Action Required item", "taiga_list_action_required gives it"),
4436
5144
  status: z.enum([
4437
5145
  "in_progress",
4438
5146
  "done",
@@ -4443,7 +5151,7 @@ function registerInboxTools(server, client) {
4443
5151
  annotations: OPERATE
4444
5152
  }, (params) => runTool(async () => {
4445
5153
  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.");
4446
- return toolResult(pickFields([(await client.updateInboxItem(params.itemId, {
5154
+ return toolResult(pickFields([(await client.updateInboxItem(params.actionId, {
4447
5155
  ...params.status !== void 0 ? { status: params.status } : {},
4448
5156
  ...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
4449
5157
  })).data], ITEM_FIELDS)[0]);
@@ -4633,219 +5341,6 @@ function registerKnowledgeTools(server, client) {
4633
5341
  }));
4634
5342
  }
4635
5343
  //#endregion
4636
- //#region src/tools/maintenance.ts
4637
- /**
4638
- * Maintaining (docs.tai.ga, Operate): what the sweeps found in a product's
4639
- * repository and where it stands against the organization's own policies,
4640
- * and the two things a person does with a finding, Ignore and Fix. Sweeps run
4641
- * themselves and nothing is marked fixed by hand: a finding resolves when the
4642
- * next sweep no longer detects it, so there is no tool for either.
4643
- */
4644
- /** A finding as a list row: enough to decide whether to open, fix or ignore it. */
4645
- const FINDING_ROW_FIELDS = [
4646
- "id",
4647
- "kind",
4648
- "severity",
4649
- "tier",
4650
- "status",
4651
- "title",
4652
- "ecosystem",
4653
- "packageName",
4654
- "currentVersion",
4655
- "recommendedVersion",
4656
- "advisoryId",
4657
- "location",
4658
- "sourcePaths",
4659
- "productRepositoryId",
4660
- "ownership",
4661
- "remediableBy",
4662
- "controls",
4663
- "backlogItemId",
4664
- "detectedAt",
4665
- "ignoreReason",
4666
- "ignoreExpiresAt"
4667
- ];
4668
- /** A sweep as the record of when the product was last checked, and whether that check held. */
4669
- const SWEEP_FIELDS = [
4670
- "id",
4671
- "trigger",
4672
- "status",
4673
- "scanners",
4674
- "findingsCount",
4675
- "newFindingsCount",
4676
- "error",
4677
- "createdAt",
4678
- "completedAt"
4679
- ];
4680
- /** A policy violation in a list: what it is, how bad, and what is being done about it. */
4681
- const VIOLATION_FIELDS = [
4682
- "id",
4683
- "controlId",
4684
- "policyType",
4685
- "severity",
4686
- "status",
4687
- "title",
4688
- "summary",
4689
- "work",
4690
- "unconfirmedSince",
4691
- "verifyRequestedAt",
4692
- "assessedAt"
4693
- ];
4694
- const findingId = idInput("finding", "taiga_list_findings gives it");
4695
- function registerMaintenanceTools(server, client) {
4696
- if (!client.hasCapability("products:read")) return;
4697
- server.registerTool("taiga_get_maintenance", {
4698
- title: "Get a product's maintenance standing",
4699
- description: "Where a product's repository stands: open findings by severity and kind, the most pressing ones, and the latest sweeps, so you can tell when the product was last checked and whether that check succeeded. A quiet list from a sweep that last ran long ago, or that failed, is not an all-clear; a product never swept says so. Sweeps run on their own after a push and periodically; nothing here starts one. Read the findings with taiga_list_findings and the policy standing with taiga_get_policy_standing.",
4700
- inputSchema: z.object({ productId }),
4701
- annotations: READ_ONLY
4702
- }, (params) => runTool(async () => {
4703
- const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
4704
- const latest = pickFields(sweeps.data, SWEEP_FIELDS);
4705
- const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
4706
- return toolResult({
4707
- ...standing,
4708
- latestSweeps: latest,
4709
- ...latest.length === 0 ? { note: "This product has never been swept: no result is not an all-clear." } : {}
4710
- });
4711
- }));
4712
- server.registerTool("taiga_list_findings", {
4713
- title: "List maintenance findings",
4714
- description: "List what the sweeps found in a product's repository: vulnerable or outdated dependencies, committed secrets, insecure code (sast) and infrastructure misconfigurations (iac_misconfig). Each row carries its severity and its priority tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), where it is, who can fix it (remediableBy: coding_agent, or upstream_only for a submodule or a dependency with no fixed version), the policy controls it breaches, and the initiative already fixing it (backlogItemId). Omit status for the findings that need action; work from the top tier down. Read one in full with taiga_get_finding.",
4715
- inputSchema: z.object({
4716
- productId,
4717
- status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
4718
- severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
4719
- kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
4720
- ...pagination
4721
- }),
4722
- annotations: READ_ONLY
4723
- }, (params) => runTool(async () => {
4724
- const { productId: id, ...filters } = params;
4725
- const result = await client.listFindings(id, filters);
4726
- return toolResult({
4727
- ...page(pickFields(result.data, FINDING_ROW_FIELDS), result.nextCursor),
4728
- ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
4729
- });
4730
- }));
4731
- server.registerTool("taiga_get_finding", {
4732
- title: "Get a maintenance finding",
4733
- description: "Read one finding in full: what it is, how likely it is to be exploited (EPSS, the known-exploited list), the advisory and the fixed version, the place in the code, the policy controls it breaches, why it is ranked where it is, its history (reintroduced, previously resolved) and the remediation under way. A secret's value is never stored; the finding names the rule and the location only.",
4734
- inputSchema: z.object({ findingId }),
4735
- annotations: READ_ONLY
4736
- }, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
4737
- server.registerTool("taiga_get_policy_standing", {
4738
- title: "Get a product's policy standing",
4739
- 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 breached by scanner findings counts them by tier (findingsByTier): a breach of routine findings only, such as secrets in test fixtures, is not an urgent one. 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.",
4740
- inputSchema: z.object({
4741
- productId,
4742
- full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
4743
- }),
4744
- annotations: READ_ONLY
4745
- }, (params) => runTool(async () => {
4746
- const standing = (await client.getPolicyStanding(params.productId)).data;
4747
- const controls = Array.isArray(standing.controls) ? standing.controls : [];
4748
- const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
4749
- const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
4750
- const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
4751
- const bySeverity = {};
4752
- for (const violation of violations) {
4753
- const severity = String(violation.severity ?? "unknown");
4754
- bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
4755
- }
4756
- const summary = {
4757
- controls: listed.length,
4758
- violations: violations.length,
4759
- bySeverity,
4760
- resolvedViolations: resolved
4761
- };
4762
- if (params.full) return toolResult({
4763
- summary,
4764
- ...standing
4765
- });
4766
- return toolResult({
4767
- summary,
4768
- productId: standing.productId,
4769
- hasPolicies: standing.hasPolicies,
4770
- assessment: standing.assessment,
4771
- documents: standing.documents,
4772
- controls: pickFields(listed, [
4773
- "controlId",
4774
- "policyType",
4775
- "text",
4776
- "state",
4777
- "divergenceCount",
4778
- "findingCount",
4779
- "findingsByTier",
4780
- "plannedCount"
4781
- ]),
4782
- violations: pickFields(violations, VIOLATION_FIELDS)
4783
- });
4784
- }));
4785
- if (!client.hasCapability("products:write")) return;
4786
- server.registerTool("taiga_ignore_finding", {
4787
- title: "Ignore or reopen a finding",
4788
- description: "Ignore a finding that does not apply or whose risk is accepted, with the reason (false_positive, not_reachable, accepted_risk, wont_fix), an optional note and an optional expiry after which it resurfaces if it still exists; or reopen an ignored one (ignored: false). Ignored findings are kept and reviewable. There is no \"mark fixed\": a finding resolves when the next sweep no longer detects it. For a secret that is only in history, rotate the credential outside Taiga, then ignore it with a reason. Ignoring hides a shared signal and can record the team's acceptance of a risk, which is the person's decision: ask the person first.",
4789
- inputSchema: z.object({
4790
- findingId,
4791
- ignored: z.boolean().describe("true ignores the finding, false reopens it"),
4792
- reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe("Why it is ignored; required when ignoring"),
4793
- note: z.string().trim().max(280).optional().describe("A short justification, kept for audit"),
4794
- expiresAt: z.string().datetime({ offset: true }).optional().describe("When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)")
4795
- }),
4796
- annotations: OPERATE
4797
- }, (params) => runTool(async () => {
4798
- if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
4799
- const body = params.ignored ? {
4800
- status: "ignored",
4801
- ignoreReason: params.reason,
4802
- ...params.note !== void 0 ? { ignoreNote: params.note } : {},
4803
- ...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
4804
- } : { status: "open" };
4805
- const finding = (await client.updateFinding(params.findingId, body)).data;
4806
- return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, "ignoreNote"])[0]);
4807
- }));
4808
- server.registerTool("taiga_fix_finding", {
4809
- title: "Fix a finding",
4810
- description: "Ask for a finding's fix: Taiga raises an initiative and adds it to the end of the product's queue, where the line plans it in turn; whether the build then waits for a person is the initiative's review gate, as for any initiative. The fix is a normal run that ends in a pull request you review and merge. With wholeGroup: true it fixes the finding's whole group in one initiative, as the page does: for a dependency, every open advisory on that package at that installed version in one version bump; for code or infrastructure, every location the same rule flagged. Absent for a secret only in history, anything in a submodule, and a dependency with no fixed version (remediableBy says upstream_only); the API refuses those. Starts paid work.",
4811
- inputSchema: z.object({
4812
- productId,
4813
- findingId,
4814
- wholeGroup: z.boolean().optional().describe("Fix every finding in this finding's group (its package and version, or its rule) together")
4815
- }),
4816
- annotations: OPERATE
4817
- }, (params) => runTool(async () => {
4818
- if (!params.wholeGroup) {
4819
- const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
4820
- return toolResult({
4821
- backlogItemId: fixed.backlogItemId,
4822
- finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null
4823
- });
4824
- }
4825
- const finding = (await client.getFinding(params.findingId)).data;
4826
- if (finding.productId !== params.productId) return toolError("NOT_FOUND", "No such finding in this product.");
4827
- return toolResult((await client.remediatePackage(params.productId, {
4828
- ecosystem: String(finding.ecosystem),
4829
- packageName: String(finding.packageName),
4830
- currentVersion: typeof finding.currentVersion === "string" ? finding.currentVersion : null,
4831
- productRepositoryId: typeof finding.productRepositoryId === "string" ? finding.productRepositoryId : null
4832
- })).data);
4833
- }));
4834
- server.registerTool("taiga_fix_policy_violation", {
4835
- title: "Fix policy violations",
4836
- description: "Ask for the fix of a product's policy violations: name a control to cover every violation recorded against it that has no initiative yet, or a single violation (violationId) when the control has more than one. Taiga raises one initiative and adds it to the end of the product's queue, exactly as a finding fix does. The violation stays listed while the work is under way: only the next assessment can record that the documents no longer show it. Starts paid work.",
4837
- inputSchema: z.object({
4838
- productId,
4839
- controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
4840
- violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
4841
- }),
4842
- annotations: OPERATE
4843
- }, (params) => runTool(async () => {
4844
- 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.");
4845
- return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
4846
- }));
4847
- }
4848
- //#endregion
4849
5344
  //#region src/tools/observe.ts
4850
5345
  /**
4851
5346
  * What runs where and how it is doing (docs.tai.ga, Deliver: Deployments;
@@ -5102,7 +5597,7 @@ function registerProductDocumentWrites(server, client) {
5102
5597
  }, (params) => runTool(() => updateSection(productScope(params.productId, params.type), params.type, params.sectionId, params.content)));
5103
5598
  server.registerTool("taiga_publish_product_document", {
5104
5599
  title: "Publish a product document draft",
5105
- description: "Publish a product document's draft as its new version, the one agents work from. Documents generated from it are not rewritten: each is marked outdated, and is regenerated when someone chooses to (taiga_regenerate_product_document, or taiga_generate_product_documents for every outdated and missing one). Ask the person before publishing.",
5600
+ description: "Publish a product document's draft as its new version, the one agents work from. Documents generated from it are not rewritten: each is marked outdated, and is regenerated when someone chooses to (taiga_regenerate_product_document, or taiga_generate_product_documents for every outdated and missing one).",
5106
5601
  inputSchema: z.object({
5107
5602
  productId,
5108
5603
  type: documentType
@@ -19782,18 +20277,18 @@ function registerPrompts(server, client) {
19782
20277
  "",
19783
20278
  "Before writing code: follow the factory instructions; read the policy sections that touch what you are",
19784
20279
  "about to change with taiga_get_document (security, data, SDLC, architecture at least); read the backlog",
19785
- "item you are implementing with taiga_get_backlog_item. Parts listed under notGranted or unavailable",
20280
+ "item you are implementing with taiga_get_initiative. Parts listed under notGranted or unavailable",
19786
20281
  "could not be read; say so rather than assuming they are empty.",
19787
20282
  "",
19788
20283
  fence("json", (await productContext(client, resolved.product, { documents: "full" })).content[0]?.text ?? "{}")
19789
20284
  ].join("\n"));
19790
20285
  });
19791
- if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_backlog_item", {
19792
- title: "Implement a backlog item",
19793
- 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.",
19794
- argsSchema: z.object({ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it") })
20286
+ if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_initiative", {
20287
+ title: "Implement an initiative",
20288
+ 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_initiatives gives the item ID.",
20289
+ argsSchema: z.object({ initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it") })
19795
20290
  }, async (args) => {
19796
- 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;
20291
+ const item = (await served$1(client.getBacklogItem(args.initiativeId), "No initiative with that ID the key can see; taiga_list_initiatives shows what exists")).data;
19797
20292
  const productId = String(item.productId);
19798
20293
  const product = (await served$1(client.getProduct(productId), "The product of that item is not visible to the key")).data;
19799
20294
  const origin = defaultOriginFor(product, "architecture");
@@ -19892,7 +20387,8 @@ const GITHUB_REPOSITORY_FIELDS = [
19892
20387
  "archived",
19893
20388
  "defaultBranch",
19894
20389
  "description",
19895
- "language"
20390
+ "language",
20391
+ "linked"
19896
20392
  ];
19897
20393
  const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
19898
20394
  /** Round-robin over lists: the first of each, then the second of each, and so on. */
@@ -19934,7 +20430,7 @@ function registerRepositoryTools(server, client) {
19934
20430
  }
19935
20431
  if (client.hasCapability("vcs:read")) server.registerTool("taiga_list_linkable_repositories", {
19936
20432
  title: "Find a repository to link",
19937
- 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. Name the GitHub account (account: its login, the owner in owner/repo, or installationId) to read only its repositories; without one it reads every connected account and takes rows from each in turn, so one account cannot fill the page. No connection means GitHub is not connected yet: that is done once, in the web, by someone who can install the app.",
20433
+ 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. Name the GitHub account (account: its login, the owner in owner/repo, or installationId) to read only its repositories; without one it reads every connected account and takes rows from each in turn, so one account cannot fill the page. linked: true means a product already links it (taiga_resolve_product says which). No connection means GitHub is not connected yet: that is done once, in the web, by someone who can install the app.",
19938
20434
  inputSchema: z.object({
19939
20435
  query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
19940
20436
  account: z.string().trim().min(1).max(140).optional().describe("The GitHub account login (the owner in owner/repo), case-insensitive"),
@@ -20138,13 +20634,13 @@ function registerResources(server, client) {
20138
20634
  return read(uri, productId, type, defaultOriginFor(product, type));
20139
20635
  });
20140
20636
  }
20141
- if (client.hasCapability("backlog:read")) server.registerResource("backlog-item", new ResourceTemplate("taiga://backlog/{itemId}", { list: void 0 }), {
20142
- title: "Backlog item",
20143
- description: "An initiative in full: description, acceptance criteria, technical context. taiga_list_backlog_items gives the item ID.",
20637
+ if (client.hasCapability("backlog:read")) server.registerResource("backlog-item", new ResourceTemplate("taiga://initiatives/{initiativeId}", { list: void 0 }), {
20638
+ title: "Initiative",
20639
+ description: "An initiative in full: description, acceptance criteria, technical context. taiga_list_initiatives gives the item ID.",
20144
20640
  mimeType: "application/json"
20145
20641
  }, async (uri, variables) => {
20146
- const itemId = requireId("Backlog item ID", String(variables["itemId"]));
20147
- const item = (await served(client.getBacklogItem(itemId), "No backlog item with that ID the key can see; taiga_list_backlog_items shows what exists")).data;
20642
+ const initiativeId = requireId("Initiative ID", String(variables["initiativeId"]));
20643
+ const item = (await served(client.getBacklogItem(initiativeId), "No initiative with that ID the key can see; taiga_list_initiatives shows what exists")).data;
20148
20644
  return { contents: [{
20149
20645
  uri: uri.href,
20150
20646
  mimeType: "application/json",
@@ -20188,33 +20684,17 @@ const buildScope = {
20188
20684
  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");
20189
20685
  /** How a missing required document comes to exist, said with a documents_incomplete refusal. */
20190
20686
  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.";
20191
- /** A product row the model can act on, saying how it started as the other product tools say it. */
20687
+ /** A product row the model can act on: how it started, and while in setup what it still needs. */
20192
20688
  async function productRow(client, product) {
20193
20689
  const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
20194
- const nextStep = setupNextStep(await withDiscoveryStep(client, product));
20690
+ const readiness = await setupReadiness(client, product);
20195
20691
  return {
20196
20692
  ...rest,
20197
20693
  projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
20198
- ...nextStep ? { nextStep } : {}
20694
+ ...readiness ? { readiness } : {}
20199
20695
  };
20200
20696
  }
20201
20697
  /**
20202
- * The product with its discoveryStep. The API derives it on reads only, not
20203
- * on the answer to a write (products.ts), and a draft started from scratch
20204
- * needs it to say what comes next; an imported one reads its import status
20205
- * instead. Best effort: a failed read leaves the product without a nextStep
20206
- * rather than failing the write that already happened.
20207
- */
20208
- async function withDiscoveryStep(client, product) {
20209
- if (product.status !== "draft" || "discoveryStep" in product || isImportedProduct(product)) return product;
20210
- try {
20211
- return (await client.getProduct(String(product.id))).data;
20212
- } catch (error) {
20213
- if (error instanceof TaigaApiError || isNetworkFailure(error)) return product;
20214
- throw error;
20215
- }
20216
- }
20217
- /**
20218
20698
  * A half turned off with nothing said about how it is done today. Allowed (the
20219
20699
  * web asks for the sentence but does not insist), so a warning rather than a
20220
20700
  * refusal: plans then design against a gap.
@@ -20259,7 +20739,7 @@ function registerSetupTools(server, client) {
20259
20739
  }, (params) => runTool(async () => toolResult((await client.previewProductMove(params.productId, params.factoryId)).data)));
20260
20740
  server.registerTool("taiga_move_product", {
20261
20741
  title: "Move a product to another factory",
20262
- description: "Move a product to another factory. Access follows the factory, so this changes who can reach the product (taiga_preview_product_move says who); the product's own knowledge files are re-filed onto the new factory, and factory knowledge and instructions stay with the factory that owns them. Refused (CONFLICT) while a plan or build is in flight: stopLine: true stops the line first and then moves, which interrupts that work.",
20742
+ description: "Move a product to another factory. Access follows the factory, so this changes who can reach the product (taiga_preview_product_move says who); the product's own knowledge files are re-filed onto the new factory, and factory knowledge and instructions stay with the factory that owns them. Refused (WORK_IN_FLIGHT) while a plan or build is in flight. Then ask the person: stopLine: true stops the line first and then moves, which interrupts that work.",
20263
20743
  inputSchema: z.object({
20264
20744
  productId,
20265
20745
  factoryId: factoryId.describe("The factory to move it to"),
@@ -20267,10 +20747,16 @@ function registerSetupTools(server, client) {
20267
20747
  }),
20268
20748
  annotations: OPERATE
20269
20749
  }, (params) => runTool(async () => {
20270
- const moved = await client.moveProduct(params.productId, {
20271
- factoryId: params.factoryId,
20272
- ...params.stopLine ? { stopLine: true } : {}
20273
- });
20750
+ let moved;
20751
+ try {
20752
+ moved = await client.moveProduct(params.productId, {
20753
+ factoryId: params.factoryId,
20754
+ ...params.stopLine ? { stopLine: true } : {}
20755
+ });
20756
+ } catch (error) {
20757
+ if (error instanceof TaigaApiError && error.code === "WORK_IN_FLIGHT") return toolError("WORK_IN_FLIGHT", `${withStop((error.apiMessage ?? "Work is in flight on this product").split(/(?<=[.!?])\s/)[0])} Moving it now means stopping the line, which interrupts that work: ask the person, and pass stopLine: true only if they say so.`, error.details);
20758
+ throw error;
20759
+ }
20274
20760
  return toolResult({
20275
20761
  product: await productRow(client, moved.data),
20276
20762
  changed: moved.changed,
@@ -20564,7 +21050,9 @@ function judgeItem(until, gate) {
20564
21050
  /** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
20565
21051
  function itemReport(item) {
20566
21052
  const run = latestBuildOf(item);
21053
+ const situation = situationOf(item);
20567
21054
  return {
21055
+ ...situation ? { situation } : {},
20568
21056
  ...pickFields([item], [
20569
21057
  "id",
20570
21058
  "title",
@@ -20611,6 +21099,8 @@ const RUN_REPORT_FIELDS = [
20611
21099
  "prState",
20612
21100
  "prDraft",
20613
21101
  "prMerged",
21102
+ "prNumber",
21103
+ "baseBranch",
20614
21104
  "actions"
20615
21105
  ];
20616
21106
  const runSignature = (run) => JSON.stringify([
@@ -20631,11 +21121,11 @@ function judgeBuild(until) {
20631
21121
  };
20632
21122
  }
20633
21123
  function registerWaitTools(server, client, options) {
20634
- if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_backlog_item", {
21124
+ if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_initiative", {
20635
21125
  title: "Wait for an initiative",
20636
21126
  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.",
20637
21127
  inputSchema: z.object({
20638
- itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
21128
+ initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
20639
21129
  until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
20640
21130
  timeoutSeconds
20641
21131
  }),
@@ -20643,7 +21133,7 @@ function registerWaitTools(server, client, options) {
20643
21133
  }, (params, ctx) => runTool(async () => {
20644
21134
  const gate = { product: void 0 };
20645
21135
  const read = async (signal) => {
20646
- const item = (await client.getBacklogItem(params.itemId, signal)).data;
21136
+ const item = (await client.getBacklogItem(params.initiativeId, signal)).data;
20647
21137
  if (gate.product === void 0 && typeof item.autoBuild !== "boolean") gate.product = client.hasCapability("products:read") ? await client.getProduct(String(item.productId), signal).then((p) => {
20648
21138
  const value = p.data.autoBuild;
20649
21139
  return typeof value === "boolean" ? value : true;
@@ -20664,17 +21154,17 @@ function registerWaitTools(server, client, options) {
20664
21154
  item: now ? itemReport(now) : null
20665
21155
  });
20666
21156
  }));
20667
- if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_build", {
21157
+ if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_run", {
20668
21158
  title: "Wait for a run",
20669
21159
  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.",
20670
21160
  inputSchema: z.object({
20671
- buildId: idInput("build run", "taiga_list_builds gives it"),
21161
+ runId: idInput("run", "taiga_list_runs gives it"),
20672
21162
  until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
20673
21163
  timeoutSeconds
20674
21164
  }),
20675
21165
  annotations: READ_ONLY
20676
21166
  }, (params, ctx) => runTool(async () => {
20677
- const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.buildId, signal)).data, judgeBuild(params.until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
21167
+ const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.runId, signal)).data, judgeBuild(params.until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
20678
21168
  progressToken: ctx.mcpReq._meta?.progressToken,
20679
21169
  notify: ctx.mcpReq.notify,
20680
21170
  signal: ctx.mcpReq.signal
@@ -20682,7 +21172,7 @@ function registerWaitTools(server, client, options) {
20682
21172
  return toolResult({
20683
21173
  until: params.until,
20684
21174
  ...outcome,
20685
- run: now ? pickFields([now], RUN_REPORT_FIELDS)[0] : null
21175
+ run: now ? readableRun(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
20686
21176
  });
20687
21177
  }));
20688
21178
  if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
@@ -20725,7 +21215,8 @@ function withStrictInputs(server) {
20725
21215
  const withHint = (args, ctx) => notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
20726
21216
  const described = {
20727
21217
  ...config,
20728
- description: `${writeNotice(name, config.annotations)}${config.description ?? ""}`
21218
+ description: `${writeNotice(name, config.annotations)}${config.description ?? ""}`,
21219
+ ...ALWAYS_LOAD.has(name) ? { _meta: { "anthropic/alwaysLoad": true } } : {}
20729
21220
  };
20730
21221
  return register(name, schema ? {
20731
21222
  ...described,
@@ -20735,6 +21226,19 @@ function withStrictInputs(server) {
20735
21226
  return server;
20736
21227
  }
20737
21228
  /**
21229
+ * The entry points, loaded with their descriptions at session start
21230
+ * (PRO-2800). Claude Code defers every other MCP tool behind tool search and
21231
+ * shows the model only its name until it searches, so an agent meeting Taiga
21232
+ * sees these three in full: where a product stands, which products there
21233
+ * are, and what waits on a person. Three, because each one loaded upfront
21234
+ * costs every session its description.
21235
+ */
21236
+ const ALWAYS_LOAD = /* @__PURE__ */ new Set([
21237
+ "taiga_get_product_context",
21238
+ "taiga_list_products",
21239
+ "taiga_list_action_required"
21240
+ ]);
21241
+ /**
20738
21242
  * What a tool's annotations say, in its description's first words (PRO-2778).
20739
21243
  * Annotations are for the client: Claude Code does not show them to the
20740
21244
  * model, so an agent read 79 tools without knowing which ones write. A tool
@@ -20771,136 +21275,40 @@ function registerAllTools(server, client, options = {}) {
20771
21275
  registerResources(server, client);
20772
21276
  registerPrompts(server, client);
20773
21277
  }
21278
+ //#endregion
21279
+ //#region src/tools/instructions.ts
21280
+ /**
21281
+ * What the server tells a client at connect time. Claude Code loads only the
21282
+ * tool NAMES and these instructions until the model searches for a tool, and
21283
+ * cuts each server's instructions at 2,048 characters (code.claude.com, MCP
21284
+ * tool search), so this is the whole introduction an agent gets: what Taiga
21285
+ * holds, when to search these tools, where to start, the words, when to ask,
21286
+ * and what only the web does. Everything else lives in the tool descriptions,
21287
+ * which load with the tool. The earlier instructions ran to about 8,000
21288
+ * characters, and everything past the first 2,000 never reached the model
21289
+ * (PRO-2800).
21290
+ */
20774
21291
  const SERVER_INSTRUCTIONS = [
20775
- [
20776
- "You have access to Taiga, a policy-aware software development platform: the organization's",
20777
- "engineering knowledge base, its products, their backlog and what the coding agent has built.",
20778
- "",
20779
- "**Start here.** Call taiga_get_product_context first, naming the product by its git remote URL inside a",
20780
- "checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with",
20781
- "query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory",
20782
- "instructions, the outline of its specification and architecture, every policy as its section ids, the",
20783
- "open initiatives, the repositories and the latest runs. Then read only what you need: sections by id with",
20784
- "taiga_get_document or taiga_get_product_document (sections: [] is any document's outline), an",
20785
- "initiative with taiga_get_backlog_item. Check Action Required (taiga_list_action_required) for what",
20786
- "waits on a person.",
20787
- "",
20788
- "**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that",
20789
- "starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it",
20790
- "(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). Such a",
20791
- "tool says so first in its description; a client that prompts on destructive tools does part of it.",
20792
- "",
20793
- "**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the",
20794
- "right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.",
20795
- "",
20796
- "**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action",
20797
- "Required is the inbox; the line is the queue; a factory is the group that owns a set of products."
20798
- ].join("\n"),
20799
- "",
20800
- "**Company documents** (taiga_list_documents / taiga_get_document):",
20801
- `The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
20802
- `architecture rules that apply to all code. The other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)})`,
20803
- "carry the business goals, design and brand standards, tone of voice and standing instructions.",
20804
- "Read the relevant policy BEFORE writing code that touches security, data handling or infrastructure.",
20805
- "",
20806
- "**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
20807
- "A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
20808
- "tech preferences (approved stack, patterns) and conventions. Read these to understand HOW this factory",
20809
- "builds software. A factory that has not written any yet has none; that is normal.",
20810
- "",
20811
- "**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
20812
- `${Object.entries(PRODUCT_DOCUMENT_NAMES).map(([type, name]) => `${name} (${type})`).join(", ")}.`,
20813
- "A product has two timelines of these, and the list shows both:",
20814
- `- ${ORIGIN_MEANING.plan}`,
20815
- `- ${ORIGIN_MEANING.reality}`,
20816
- "An imported (brownfield) product has no generated discovery chain: its derived documents are on the",
20817
- "reality side, while the documents a person writes (Look & Feel, Product Instructions) are on the plan",
20818
- "side of every product. Omit origin on a read and the server picks the right side (the list reports the",
20819
- "product's own as defaultOrigin). To understand",
20820
- "what to build, read the plan where there is one; to understand the code you are changing, read reality",
20821
- "when it exists, the ground truth for architecture, data flow and threats as built.",
20822
- "",
20823
- "**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
20824
- "The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
20825
- "for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
20826
- "every product the key can see, and active: true the ones executing now.",
20827
- "",
20828
- "**Operating the line** works the way Taiga is meant to be used. New work is described, not written:",
20829
- "taiga_request_initiatives asks an agent, which reads the product and its repository, expands the request",
20830
- "into initiatives in the Backlog (or says why it added none: already covered, not work, out of scope, or a",
20831
- "policy that does not allow it) -- follow it with taiga_wait_for_job. The queue is the only way to start",
20832
- "work (taiga_queue_backlog_item): Taiga plans and builds one initiative at a time, in queue order, and starts",
20833
- "the next when the pull request merges. Whether a landed plan waits for a review is the review gate",
20834
- "(taiga_set_auto_build, or autoBuild on the queue); read it with taiga_get_plan and answer with",
20835
- "taiga_approve_plan or taiga_reject_plan. Decisions about the work are made on the initiative: stop work by",
20836
- "moving it out of Build (taiga_move_backlog_item), carry it on with taiga_resume_backlog_item, ask for a new",
20837
- "plan with taiga_plan_again; a run is the record of an attempt. taiga_pause_line pauses the whole line.",
20838
- "taiga_stop_build is the emergency brake for a run whose initiative cannot be moved. Queueing, approving,",
20839
- "resuming and requesting start paid agent work; stopping and pausing interrupt it. Tell the person what you",
20840
- "are about to start or stop and wait for their yes; a client that prompts on destructive tools does that",
20841
- "for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
20842
- "",
20843
- "**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build /",
20844
- "taiga_get_job / taiga_wait_for_job): the queue read says what the line is waiting on (a paused line, a plan",
20845
- "to review, a pull request to merge, a halt and what lifts it). Planning takes minutes and a build up to an",
20846
- "hour: wait with the wait tools rather than reading the same item again and again; each returns when the",
20847
- "point is reached, when only a person can move it on, or at its timeout (call again to keep waiting).",
20848
- "",
20849
- "**Action Required** (taiga_list_action_required / taiga_list_setup_tasks / taiga_resolve_action): where work",
20850
- "comes back to a person, shared by the team. Check it first. Resolving an item clears it for everybody; a",
20851
- "setup task is marked done only by the person who did the setup.",
20852
- "",
20853
- "**Maintaining** (taiga_get_maintenance / taiga_list_findings / taiga_get_finding / taiga_ignore_finding /",
20854
- "taiga_fix_finding / taiga_get_policy_standing / taiga_fix_policy_violation): what the sweeps found and where",
20855
- "the product stands against its own policies. Sweeps run on their own; check when the last one ran before",
20856
- "trusting a quiet list. A finding is ignored with a reason or fixed; a fix raises an initiative at the end of",
20857
- "the queue. Nothing is marked fixed by hand: the next sweep or assessment decides.",
20858
- "",
20859
- "**Deployments and monitoring** (taiga_get_deployments / taiga_get_delivery_metrics / taiga_get_monitoring /",
20860
- "taiga_define_environment / taiga_update_environment): what runs where, how delivery is going, and how each",
20861
- "environment is doing. Taiga observes deployments and never performs them. An environment needs only its App",
20862
- "URL for monitoring to start.",
20863
- "",
20864
- "**Knowledge** (taiga_list_knowledge / taiga_get_knowledge / taiga_add_knowledge / taiga_retry_knowledge /",
20865
- "taiga_delete_knowledge): reference material agents search when work calls for it, at the organization, a",
20866
- "factory or a product. A rule that applies every time belongs in instructions instead.",
20867
- "",
20868
- "**Setting a product up** (taiga_create_factory / taiga_create_product / taiga_update_product /",
20869
- "taiga_list_linkable_repositories / taiga_link_repository / taiga_start_import / taiga_finish_discovery /",
20870
- "taiga_reopen_discovery / taiga_move_product): a product starts from scratch (Discovery is a conversation,",
20871
- "held in the web) or by importing a repository, chosen when it is created. An import links the repository,",
20872
- "starts the import and runs by itself; finishing Discovery locks the documents and opens the rest of the",
20873
- "product. Linking a repository and creating or moving factories are for owners and admins. GitHub is",
20874
- "connected once, in the web; members and deleting stay there too.",
20875
- "",
20876
- "**Product documents and autonomy** (taiga_update_product_document / taiga_publish_product_document /",
20877
- "taiga_regenerate_product_document / taiga_generate_product_documents / taiga_stop_generating_documents /",
20878
- "taiga_mark_product_document_reviewed /",
20879
- "taiga_set_product_autonomy / taiga_set_auto_merge): edit a product document in its draft and publish it;",
20880
- "documents built from it are marked outdated and regenerated, not rewritten. Set whether plans wait for a",
20881
- "review and pull requests for a person to merge. Organization-wide settings, members, roles and connections",
20882
- "are administrators' work in the web.",
21292
+ "Taiga is a policy-aware software delivery platform: an organization's policies and knowledge, its factories",
21293
+ "and products, each product's initiatives (its backlog), the line that plans and builds them one at a time, the",
21294
+ "coding agent's runs and pull requests, maintenance findings, and Action Required, where work waits on a person.",
21295
+ "Search these tools whenever the person asks about a Taiga product, an initiative, a run or pull request, policy",
21296
+ "or compliance, findings, deployments or monitoring, or setting a product up.",
20883
21297
  "",
20884
- "**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
20885
- "provenance fields you can read past: `source` says where the text originally came from (template:",
20886
- "Taiga's standard text; overlay-<id>: a jurisdiction or industry overlay; cloud-preset:<providers>:",
20887
- "rendered for the organization's cloud providers; any other tag names the derivation that wrote the",
20888
- "text; a hand edit keeps the source it had) and",
20889
- "`priority: core` marks the sections every organization keeps. Neither changes what the section",
20890
- "requires of you.",
21298
+ "**Start here.** taiga_get_product_context (the product by git remote URL, slug or ID) gives its state in one",
21299
+ "call: what it still needs while in setup, its documents as outlines, policies as section ids, open initiatives,",
21300
+ "recent runs with their outcome, and what waits on a person. Then read only what you need. Across products,",
21301
+ "taiga_list_products and taiga_list_action_required.",
20891
21302
  "",
20892
- "Without taiga_get_product_context, use taiga_list_factories and taiga_list_products, then read the",
20893
- "factory instructions, the product's specification and architecture, and the initiative you are working",
20894
- "on. A document read that finds nothing tells you what does exist and which tool reads or writes it.",
21303
+ "**Words.** An initiative is a unit of work in the backlog; a run is one attempt by the coding agent to build it;",
21304
+ "the line is the product's queue; a factory owns a set of products. IDs are UUIDs, never names; a NOT_FOUND names",
21305
+ "the tool that lists the right ones. A refusal carries the reason: say it as it is.",
20895
21306
  "",
20896
- "The same documents are resources (taiga://documents/{type}, taiga://products/{productId}/documents/{type},",
20897
- "taiga://factories/{factoryId}/instructions, taiga://backlog/{itemId}) for clients that attach them, and three",
20898
- "prompts open a session: taiga_start_session, taiga_implement_backlog_item, taiga_review_against_policies.",
21307
+ "**Before you write.** A tool that writes says so first: \"Writes.\" or \"Acts on paid or shared work: ask the",
21308
+ "person first.\" Ask before the second kind: it starts paid agent work, interrupts it, or changes what others see.",
20899
21309
  "",
20900
- "When you notice a policy or factory instructions are outdated, fix the section with",
20901
- "taiga_update_document or taiga_update_factory_document. That saves a draft nobody follows yet;",
20902
- "publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
20903
- "This keeps the knowledge base accurate for everyone."
21310
+ "**Only in the web:** connecting GitHub, members and roles, the Discovery conversation of a product started from",
21311
+ "scratch, stamping checks, starting a run over, and deleting anything."
20904
21312
  ].join("\n");
20905
21313
  //#endregion
20906
21314
  //#region src/server.ts