@taiga-ai/mcp-server 0.18.0-rc.445 → 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,14 +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
  */
2210
+ const ITEM_ID_HINT = "taiga_list_initiatives lists a product's initiatives with their IDs; a run or plan ID is not one.";
2172
2211
  const BY_ARGUMENT = {
2173
- itemId: "taiga_list_backlog_items lists a product's initiatives with their IDs; a run or plan ID is not one.",
2174
- backlogItemId: "taiga_list_backlog_items lists a product's initiatives with their IDs.",
2175
- 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.",
2176
2214
  planId: "taiga_get_plan gives the initiative's active plan and its ID.",
2177
2215
  repositoryId: "taiga_list_product_repositories lists the product's repository links; a GitHub repository id is not a link ID.",
2178
2216
  replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
@@ -2189,17 +2227,41 @@ const BY_ARGUMENT = {
2189
2227
  /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2190
2228
  const BY_TOOL = {
2191
2229
  taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2192
- taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_backlog_item says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one." : BY_ARGUMENT.itemId
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
2193
2253
  };
2194
- /** 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. */
2195
2255
  function notFoundHint(tool, args) {
2196
2256
  if (BY_TOOL[tool]) return BY_TOOL[tool];
2197
2257
  if (typeof args !== "object" || args === null) return void 0;
2198
2258
  const given = Object.keys(args).filter((key) => args[key] != null && BY_ARGUMENT[key]);
2199
2259
  given.sort((a, b) => Number(a === "productId") - Number(b === "productId"));
2200
2260
  if (given.length === 0) return void 0;
2201
- const hint = given.map((key) => BY_ARGUMENT[key]).join(" ");
2202
- 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
+ };
2203
2265
  }
2204
2266
  /** The hint of the tool call in progress, read by runTool when the API answers 404. */
2205
2267
  const notFoundHints = new AsyncLocalStorage();
@@ -2214,11 +2276,45 @@ function withStop(text) {
2214
2276
  }
2215
2277
  //#endregion
2216
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
+ }
2217
2313
  /** A successful tool result: one JSON text block the model reads. */
2218
2314
  function toolResult(value) {
2219
2315
  return { content: [{
2220
2316
  type: "text",
2221
- text: JSON.stringify(value, null, 2)
2317
+ text: JSON.stringify(inProductWords(value), null, 2)
2222
2318
  }] };
2223
2319
  }
2224
2320
  /**
@@ -2233,7 +2329,7 @@ function toolError(error, message, details) {
2233
2329
  text: JSON.stringify(details ? {
2234
2330
  error,
2235
2331
  message,
2236
- details
2332
+ details: inProductWords(details)
2237
2333
  } : {
2238
2334
  error,
2239
2335
  message
@@ -2366,6 +2462,10 @@ function webOrigin(url) {
2366
2462
  function isRecord(value) {
2367
2463
  return typeof value === "object" && value !== null && !Array.isArray(value);
2368
2464
  }
2465
+ /** A section stored as an object rather than a bare string. */
2466
+ function isSectionObject(value) {
2467
+ return isRecord(value);
2468
+ }
2369
2469
  /** The `sections` map of a document, or null when the document is not section-shaped. */
2370
2470
  function sectionsOf(data) {
2371
2471
  return isRecord(data.sections) ? data.sections : null;
@@ -2375,7 +2475,7 @@ function sectionContent(sections, sectionId) {
2375
2475
  const section = sections[sectionId];
2376
2476
  if (section === void 0 || section === null) return null;
2377
2477
  if (typeof section === "string") return section;
2378
- return isRecord(section) && typeof section.content === "string" ? section.content : "";
2478
+ return isSectionObject(section) && typeof section.content === "string" ? section.content : "";
2379
2479
  }
2380
2480
  /** The map with one section's text replaced (or the section added); every other section untouched. */
2381
2481
  function applySectionEdit(sections, sectionId, content) {
@@ -2397,8 +2497,9 @@ function applySectionEdit(sections, sectionId, content) {
2397
2497
  */
2398
2498
  function stripProvenance(doc) {
2399
2499
  const { derivedFrom: _derivedFrom, outdated: _outdated, ...rest } = doc;
2400
- if (!isRecord(rest.data)) return rest;
2401
- const sections = sectionsOf(rest.data);
2500
+ const { data } = doc;
2501
+ if (!isRecord(data)) return rest;
2502
+ const sections = sectionsOf(data);
2402
2503
  if (!sections) return rest;
2403
2504
  const stripped = Object.fromEntries(Object.entries(sections).map(([id, section]) => {
2404
2505
  if (!isRecord(section)) return [id, section];
@@ -2408,7 +2509,7 @@ function stripProvenance(doc) {
2408
2509
  return {
2409
2510
  ...rest,
2410
2511
  data: {
2411
- ...rest.data,
2512
+ ...data,
2412
2513
  sections: stripped
2413
2514
  }
2414
2515
  };
@@ -2421,7 +2522,7 @@ function stripProvenance(doc) {
2421
2522
  function instructionsText(sections) {
2422
2523
  const parts = [];
2423
2524
  for (const section of Object.values(sections)) {
2424
- const content = isRecord(section) ? section.content : void 0;
2525
+ const content = isSectionObject(section) ? section.content : void 0;
2425
2526
  if (typeof content === "string" && content.trim()) parts.push(content.trim());
2426
2527
  }
2427
2528
  return parts.join("\n\n").trim() || null;
@@ -2480,7 +2581,7 @@ function budgetSections(doc, budget) {
2480
2581
  * The `sections` input of a document read: some sections by id, none for the
2481
2582
  * outline (every id with its size and no text), or omitted for the whole.
2482
2583
  */
2483
- 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");
2484
2585
  /**
2485
2586
  * The answer to a read that names sections: the ones the document has, the
2486
2587
  * rest listed with their sizes, and the asked-for ids it lacks under
@@ -2494,6 +2595,14 @@ function sectionReadResult(doc, ids, label) {
2494
2595
  if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
2495
2596
  const all = Object.keys(sectionsOf(doc.data) ?? {});
2496
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
+ }
2497
2606
  return toolResult({
2498
2607
  ...cut.doc,
2499
2608
  omittedSections: cut.omittedSections,
@@ -2501,6 +2610,90 @@ function sectionReadResult(doc, ids, label) {
2501
2610
  });
2502
2611
  }
2503
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
2504
2697
  //#region src/tools/summarize.ts
2505
2698
  /**
2506
2699
  * Keep only the named fields of each item. List tools return summaries (the
@@ -2650,9 +2843,8 @@ async function linkedProducts(client, repository, repositoryHost) {
2650
2843
  }
2651
2844
  /** A document list row as the model sees it: the summary fields plus a display name and the staleness fact. */
2652
2845
  function documentSummaries(rows) {
2653
- const typed = rows;
2654
- return pickFields(typed, DOCUMENT_SUMMARY_FIELDS$1).map((picked, index) => {
2655
- const outdated = typed[index].outdated;
2846
+ return pickFields(rows, DOCUMENT_SUMMARY_FIELDS$1).map((picked, index) => {
2847
+ const outdated = rows[index].outdated;
2656
2848
  const fact = picked.isDraft !== false ? void 0 : outdated === null ? false : Array.isArray(outdated?.sources) ? outdated.sources.length > 0 : void 0;
2657
2849
  return {
2658
2850
  type: picked.type,
@@ -2823,7 +3015,7 @@ function registerProductTools(server, client) {
2823
3015
  }));
2824
3016
  server.registerTool("taiga_get_product", {
2825
3017
  title: "Get product",
2826
- 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.",
2827
3019
  inputSchema: z.object({
2828
3020
  productId: productId.optional(),
2829
3021
  slug: slug.optional()
@@ -2832,10 +3024,10 @@ function registerProductTools(server, client) {
2832
3024
  }, (params) => runTool(async () => {
2833
3025
  const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
2834
3026
  if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
2835
- const nextStep = setupNextStep(resolved.product);
2836
- return toolResult(nextStep ? {
3027
+ const readiness = await setupReadiness(client, resolved.product);
3028
+ return toolResult(readiness ? {
2837
3029
  ...resolved.product,
2838
- nextStep
3030
+ readiness
2839
3031
  } : resolved.product);
2840
3032
  }));
2841
3033
  server.registerTool("taiga_resolve_product", {
@@ -2932,7 +3124,7 @@ function sectionIdFrom(readTool) {
2932
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.`);
2933
3125
  }
2934
3126
  const sectionContentInput = z.string().describe("The full new Markdown text of the section; it replaces the old text");
2935
- 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)}`);
2936
3128
  /** Everything the model needs back from a write, without the content it already holds. */
2937
3129
  function documentSummary(doc) {
2938
3130
  const { data, ...rest } = doc;
@@ -2994,7 +3186,7 @@ function registerDocumentTools(server, client) {
2994
3186
  if (client.hasCapability("tenant:documents:read")) {
2995
3187
  server.registerTool("taiga_list_documents", {
2996
3188
  title: "List company documents",
2997
- 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.`,
2998
3190
  inputSchema: z.object({ ...pagination }),
2999
3191
  annotations: READ_ONLY
3000
3192
  }, (params) => runTool(async () => {
@@ -3003,7 +3195,7 @@ function registerDocumentTools(server, client) {
3003
3195
  }));
3004
3196
  server.registerTool("taiga_get_document", {
3005
3197
  title: "Get company document",
3006
- 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.",
3007
3199
  inputSchema: z.object({
3008
3200
  type: tenantDocumentType,
3009
3201
  sections: sectionsInput
@@ -3054,7 +3246,7 @@ function registerDocumentTools(server, client) {
3054
3246
  if (client.hasCapability("factories:read")) {
3055
3247
  server.registerTool("taiga_list_factory_documents", {
3056
3248
  title: "List factory documents",
3057
- 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.`,
3058
3250
  inputSchema: z.object({
3059
3251
  factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
3060
3252
  ...pagination
@@ -3218,9 +3410,9 @@ function requestPrompt(title, description) {
3218
3410
  }
3219
3411
  //#endregion
3220
3412
  //#region src/tools/queue.ts
3221
- /** 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. */
3222
3414
  const NAMED_ENTRIES = 25;
3223
- 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.";
3224
3416
  /**
3225
3417
  * What lifts a halt, in the tools that do it. Read from the halt registry in
3226
3418
  * @taiga/types (how each cause clears, and whether a person may lift it), so
@@ -3230,11 +3422,11 @@ const MOVE_ASIDE = "or move it down (taiga_queue_backlog_item with afterId) or o
3230
3422
  function haltNextStep(cause) {
3231
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).";
3232
3424
  const spec = cause ? HALT_CAUSES[cause] : void 0;
3233
- 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}`;
3234
3426
  const known = cause;
3235
- if (canResumeHalt(known)) return `taiga_resume_backlog_item lifts it, ${MOVE_ASIDE}`;
3427
+ if (canResumeHalt(known)) return `taiga_resume_initiative lifts it, ${MOVE_ASIDE}`;
3236
3428
  if (haltClearsItself(known)) return `It clears when ${spec.clearedWhen}; ${MOVE_ASIDE}`;
3237
- 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}.`;
3238
3430
  }
3239
3431
  /**
3240
3432
  * The entry the line waits on, by the queue's two kinds of blocker
@@ -3279,7 +3471,7 @@ async function queueView(client, product, options = { name: "all" }) {
3279
3471
  const canName = client.hasCapability("backlog:read");
3280
3472
  const blocking = blockingIndex(entries, (entry) => entry.state);
3281
3473
  const toName = !canName ? [] : options.name === "all" ? entries.slice(0, NAMED_ENTRIES).map((_, index) => index) : blocking >= 0 ? [blocking] : [];
3282
- const settled = await Promise.allSettled(toName.map((index) => client.getBacklogItem(String(entries[index].backlogItemId))));
3474
+ const settled = await Promise.allSettled(toName.map((index) => client.getBacklogItem(entries[index].backlogItemId)));
3283
3475
  const reads = new Map(toName.map((index, at) => [index, settled[at]]));
3284
3476
  let unnamed = options.name === "all" ? entries.length - toName.length : 0;
3285
3477
  const rows = entries.map((entry, index) => {
@@ -3308,7 +3500,7 @@ async function queueView(client, product, options = { name: "all" }) {
3308
3500
  cta: lifecycle.cta
3309
3501
  } : null,
3310
3502
  ...latestBuild ? {
3311
- buildId: latestBuild.id,
3503
+ runId: latestBuild.id,
3312
3504
  prUrl: latestBuild.prUrl ?? null
3313
3505
  } : {}
3314
3506
  });
@@ -3325,7 +3517,7 @@ async function queueView(client, product, options = { name: "all" }) {
3325
3517
  }
3326
3518
  return row;
3327
3519
  });
3328
- 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.";
3329
3521
  const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
3330
3522
  return {
3331
3523
  linePaused: paused === null ? null : typeof paused === "string",
@@ -3349,7 +3541,7 @@ async function linePausedSince(client, product) {
3349
3541
  }
3350
3542
  function registerQueueTools(server, client) {
3351
3543
  if (!client.hasCapability("products:read")) return;
3352
- server.registerTool("taiga_get_queue", {
3544
+ server.registerTool("taiga_get_line", {
3353
3545
  title: "Get the product queue",
3354
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.",
3355
3547
  inputSchema: z.object({ productId }),
@@ -3358,7 +3550,7 @@ function registerQueueTools(server, client) {
3358
3550
  if (!client.hasCapability("products:write")) return;
3359
3551
  server.registerTool("taiga_pause_line", {
3360
3552
  title: "Pause or resume a product's line",
3361
- 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.",
3362
3554
  inputSchema: z.object({
3363
3555
  productId,
3364
3556
  paused: z.boolean().describe("true pauses the line, false resumes it")
@@ -3374,6 +3566,243 @@ function registerQueueTools(server, client) {
3374
3566
  }));
3375
3567
  }
3376
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
3377
3806
  //#region src/tools/backlogs.ts
3378
3807
  /** What a list row needs to say for the model to pick an item to read in full. */
3379
3808
  const BACKLOG_SUMMARY_FIELDS = [
@@ -3409,6 +3838,83 @@ const ITEM_OUTCOME_FIELDS = [
3409
3838
  "archivedAt",
3410
3839
  "lifecycle"
3411
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
+ }
3412
3918
  /** What each plan failure cause means for whoever reads it next (PLAN_FAILURE_CAUSES in @taiga/types). */
3413
3919
  const PLAN_FAILURE_MEANING = {
3414
3920
  agent_error: "The planning agent failed. Planning again is the usual next step.",
@@ -3420,10 +3926,10 @@ const PLAN_FAILURE_MEANING = {
3420
3926
  * (PRO-2780): planFailureCause alone said "cancelled" with nobody behind it.
3421
3927
  * Best effort: a history the API will not give leaves the item as it was.
3422
3928
  */
3423
- async function planFailureOf(client, itemId, itemCause) {
3929
+ async function planFailureOf(client, initiativeId, itemCause) {
3424
3930
  let events;
3425
3931
  try {
3426
- events = (await client.listBacklogItemActivity(itemId)).data;
3932
+ events = (await client.listBacklogItemActivity(initiativeId)).data;
3427
3933
  } catch (error) {
3428
3934
  if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
3429
3935
  throw error;
@@ -3455,7 +3961,7 @@ async function planFailureOf(client, itemId, itemCause) {
3455
3961
  };
3456
3962
  }
3457
3963
  const planId = idInput("plan", "taiga_get_plan gives the initiative's active plan and its ID");
3458
- 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");
3459
3965
  /** The plan without its steps' prose: enough to approve it or say what a run will do. */
3460
3966
  const PLAN_SUMMARY_FIELDS = [
3461
3967
  "id",
@@ -3474,14 +3980,11 @@ const PLAN_STEP_SUMMARY_FIELDS = [
3474
3980
  "reads",
3475
3981
  "createsResources"
3476
3982
  ];
3477
- /** The outcome fields of an initiative, with how many of its criteria are met. */
3478
- const outcomeOf = (item) => {
3479
- const row = item;
3480
- return {
3481
- ...pickFields([row], ITEM_OUTCOME_FIELDS)[0],
3482
- criteria: criteriaCounts(row)
3483
- };
3484
- };
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, {
3985
+ ...pickFields([item], ITEM_OUTCOME_FIELDS)[0],
3986
+ criteria: criteriaCounts(item)
3987
+ });
3485
3988
  const outcome = (item) => toolResult(outcomeOf(item));
3486
3989
  /**
3487
3990
  * Each tool is registered under the capability its route requires, so a
@@ -3513,9 +4016,9 @@ function registerBacklogTools(server, client) {
3513
4016
  if (may("backlog:read") && may("backlog:write") && may("products:write")) registerPlanAgain(server, client);
3514
4017
  }
3515
4018
  function registerBacklogReads(server, client) {
3516
- server.registerTool("taiga_list_backlog_items", {
3517
- title: "List backlog items",
3518
- 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.",
3519
4022
  inputSchema: z.object({
3520
4023
  productId,
3521
4024
  query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
@@ -3545,7 +4048,7 @@ function registerBacklogReads(server, client) {
3545
4048
  const { query, ...filters } = params;
3546
4049
  if (query === void 0) {
3547
4050
  const result = await client.listBacklogItems(params.productId, filters);
3548
- return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
4051
+ return toolResult(page(situatedRows(result.data), result.nextCursor));
3549
4052
  }
3550
4053
  if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
3551
4054
  const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
@@ -3554,34 +4057,36 @@ function registerBacklogReads(server, client) {
3554
4057
  cursor
3555
4058
  }), query, (row) => [row.title], params.limit ?? 25);
3556
4059
  return toolResult({
3557
- ...page(itemRows(found.rows, BACKLOG_SUMMARY_FIELDS), null),
4060
+ ...page(situatedRows(found.rows), null),
3558
4061
  ...found.more ? { more: SEARCH_NOTE } : {}
3559
4062
  });
3560
4063
  }));
3561
- server.registerTool("taiga_get_backlog_item", {
3562
- title: "Get backlog item",
3563
- 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.",
3564
- 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 }),
3565
4068
  annotations: READ_ONLY
3566
4069
  }, (params) => runTool(async () => {
3567
- const item = (await client.getBacklogItem(params.itemId)).data;
3568
- if (item.planningStatus !== "failed") return toolResult(item);
3569
- const planFailure = await planFailureOf(client, params.itemId, item.planFailureCause);
3570
- return toolResult(planFailure ? {
4070
+ const item = (await client.getBacklogItem(params.initiativeId)).data;
4071
+ const row = item;
4072
+ if (item.planningStatus !== "failed") return toolResult(withSituation(row));
4073
+ const ownCause = item.planFailureCause;
4074
+ const planFailure = await planFailureOf(client, params.initiativeId, ownCause);
4075
+ return toolResult(withSituation(row, planFailure ? {
3571
4076
  ...item,
3572
4077
  planFailure
3573
- } : item);
4078
+ } : item));
3574
4079
  }));
3575
4080
  server.registerTool("taiga_get_plan", {
3576
4081
  title: "Get the active plan",
3577
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).",
3578
4083
  inputSchema: z.object({
3579
- itemId,
4084
+ initiativeId,
3580
4085
  full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
3581
4086
  }),
3582
4087
  annotations: READ_ONLY
3583
4088
  }, (params) => runTool(async () => {
3584
- const plan = (await client.getActivePlan(params.itemId)).data;
4089
+ const plan = (await client.getActivePlan(params.initiativeId)).data;
3585
4090
  if (params.full) return toolResult(plan);
3586
4091
  const steps = Array.isArray(plan.steps) ? plan.steps : [];
3587
4092
  return toolResult({
@@ -3612,18 +4117,18 @@ function registerRequest(server, client) {
3612
4117
  }));
3613
4118
  }
3614
4119
  function registerMove(server, client) {
3615
- server.registerTool("taiga_move_backlog_item", {
3616
- title: "Move backlog item on the board",
3617
- 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.`,
3618
4123
  inputSchema: z.object({
3619
- itemId,
4124
+ initiativeId,
3620
4125
  state: z.enum(USER_DROP_STATES).describe("The column to move into"),
3621
- 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"),
3622
4127
  archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
3623
4128
  }),
3624
4129
  annotations: OPERATE
3625
4130
  }, (params) => runTool(async () => {
3626
- const { itemId: id, ...body } = params;
4131
+ const { initiativeId: id, ...body } = params;
3627
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.");
3628
4133
  return outcome((await client.transitionInitiative(id, body)).data);
3629
4134
  }));
@@ -3633,20 +4138,20 @@ const BUILD_STATION = /* @__PURE__ */ new Set(["plan", "build"]);
3633
4138
  function registerPlanAgain(server, client) {
3634
4139
  server.registerTool("taiga_plan_again", {
3635
4140
  title: "Plan an initiative again",
3636
- 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.",
3637
4142
  inputSchema: z.object({
3638
- itemId,
4143
+ initiativeId,
3639
4144
  force: z.boolean().optional().describe("Plan anyway past an unsettled dependency or an unchosen UI direction; only when the person says so")
3640
4145
  }),
3641
4146
  annotations: OPERATE
3642
4147
  }, (params) => runTool(async () => {
3643
- const item = (await client.getBacklogItem(params.itemId)).data;
3644
- 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.`);
3645
4150
  const body = {
3646
4151
  state: "plan",
3647
4152
  ...params.force ? { force: true } : {}
3648
4153
  };
3649
- return outcome((await client.transitionInitiative(params.itemId, body)).data);
4154
+ return outcome((await client.transitionInitiative(params.initiativeId, body)).data);
3650
4155
  }));
3651
4156
  }
3652
4157
  /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
@@ -3659,7 +4164,7 @@ const isApiFailure$2 = (error) => error instanceof TaigaApiError || isNetworkFai
3659
4164
  async function reviewGate(client, item) {
3660
4165
  const own = typeof item.autoBuild === "boolean" ? item.autoBuild : null;
3661
4166
  try {
3662
- const product = own === null ? (await client.getProduct(String(item.productId))).data : {};
4167
+ const product = own === null ? (await client.getProduct(item.productId)).data : {};
3663
4168
  const builds = resolveAutoBuild({
3664
4169
  initiative: own,
3665
4170
  product: typeof product.autoBuild === "boolean" ? product.autoBuild : true
@@ -3677,7 +4182,7 @@ async function reviewGate(client, item) {
3677
4182
  /** Where a queued initiative stands in its product's line; null, and the outcome says so, when the queue cannot be read. */
3678
4183
  async function placeInQueue(client, item) {
3679
4184
  try {
3680
- const view = await queueView(client, String(item.productId), { name: "blocking" });
4185
+ const view = await queueView(client, item.productId, { name: "blocking" });
3681
4186
  return {
3682
4187
  position: view.entries.find((row) => row.backlogItemId === item.id)?.position ?? null,
3683
4188
  of: view.entries.length,
@@ -3690,17 +4195,17 @@ async function placeInQueue(client, item) {
3690
4195
  }
3691
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");
3692
4197
  function registerQueue(server, client) {
3693
- server.registerTool("taiga_queue_backlog_item", {
3694
- title: "Queue backlog item",
3695
- 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.",
3696
4201
  inputSchema: z.object({
3697
- itemId,
3698
- 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"),
3699
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")
3700
4205
  }),
3701
4206
  annotations: OPERATE
3702
4207
  }, (params) => runTool(async () => {
3703
- const { itemId: id, autoBuild, ...placement } = params;
4208
+ const { initiativeId: id, autoBuild, ...placement } = params;
3704
4209
  if (autoBuild !== void 0) await client.setAutoBuild(id, autoBuild);
3705
4210
  const item = (await client.transitionInitiative(id, {
3706
4211
  state: "queue",
@@ -3711,7 +4216,7 @@ function registerQueue(server, client) {
3711
4216
  ...outcomeOf(item),
3712
4217
  reviewGate: gate,
3713
4218
  queue,
3714
- ...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." }
3715
4220
  });
3716
4221
  }));
3717
4222
  }
@@ -3720,12 +4225,12 @@ function registerReviewGate(server, client) {
3720
4225
  title: "Set an initiative's review gate",
3721
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.",
3722
4227
  inputSchema: z.object({
3723
- itemId,
4228
+ initiativeId,
3724
4229
  autoBuild: autoBuildInput
3725
4230
  }),
3726
4231
  annotations: OPERATE_SETTING
3727
4232
  }, (params) => runTool(async () => {
3728
- const item = (await client.setAutoBuild(params.itemId, params.autoBuild)).data;
4233
+ const item = (await client.setAutoBuild(params.initiativeId, params.autoBuild)).data;
3729
4234
  const gate = await reviewGate(client, item);
3730
4235
  return toolResult({
3731
4236
  ...outcomeOf(item),
@@ -3735,12 +4240,12 @@ function registerReviewGate(server, client) {
3735
4240
  }));
3736
4241
  }
3737
4242
  function registerResume(server, client) {
3738
- server.registerTool("taiga_resume_backlog_item", {
3739
- title: "Resume backlog item",
4243
+ server.registerTool("taiga_resume_initiative", {
4244
+ title: "Resume initiative",
3740
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.",
3741
- inputSchema: z.object({ itemId }),
4246
+ inputSchema: z.object({ initiativeId }),
3742
4247
  annotations: OPERATE
3743
- }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
4248
+ }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
3744
4249
  }
3745
4250
  function registerPlanVerdicts(server, client) {
3746
4251
  server.registerTool("taiga_approve_plan", {
@@ -3760,173 +4265,234 @@ function registerPlanVerdicts(server, client) {
3760
4265
  }, (params) => runTool(async () => outcome((await client.rejectPlan(params.planId, { comment: params.comment })).data)));
3761
4266
  }
3762
4267
  //#endregion
3763
- //#region src/tools/builds.ts
3764
- /** What a list row needs to say for the model to pick a run to read in full. */
3765
- 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 = [
3766
4278
  "id",
3767
- "runNumber",
3768
- "productId",
3769
- "backlogItemId",
3770
- "targetTitle",
3771
- "planId",
4279
+ "kind",
4280
+ "severity",
4281
+ "tier",
3772
4282
  "status",
3773
- "failedReason",
3774
- "runtime",
3775
- "repositoryFullName",
3776
- "branch",
3777
- "baseBranch",
3778
- "completedSteps",
3779
- "totalSteps",
3780
- "prNumber",
3781
- "prUrl",
3782
- "prState",
3783
- "prDraft",
3784
- "prMerged",
3785
- "actions",
3786
- "createdAt",
3787
- "startedAt",
3788
- "completedAt"
3789
- ];
3790
- /** What a run says about itself after a stop or a resume: its state and what can be done next. */
3791
- const RUN_OUTCOME_FIELDS = [
3792
- "id",
3793
- "runNumber",
4283
+ "title",
4284
+ "ecosystem",
4285
+ "packageName",
4286
+ "currentVersion",
4287
+ "recommendedVersion",
4288
+ "advisoryId",
4289
+ "location",
4290
+ "sourcePaths",
4291
+ "productRepositoryId",
4292
+ "ownership",
4293
+ "remediableBy",
4294
+ "controls",
3794
4295
  "backlogItemId",
3795
- "status",
3796
- "failedReason",
3797
- "completedSteps",
3798
- "totalSteps",
3799
- "prUrl",
3800
- "actions"
4296
+ "detectedAt",
4297
+ "ignoreReason",
4298
+ "ignoreExpiresAt"
3801
4299
  ];
3802
- /**
3803
- * Where a run that ended without finishing stopped, from its steps and its
3804
- * hardening result: a failed run with every step completed reads as nothing
3805
- * failed, and retry_from_failed_step then reads as a mistake, when the agent
3806
- * stalled in the hardening pass after the steps and the retry continues it.
3807
- * Null for a run still going or that finished.
3808
- */
3809
- function stoppedIn(run, steps) {
3810
- if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
3811
- if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
3812
- const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
3813
- 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)})`;
3814
- 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.";
3815
- return "after the steps and hardening, at the pull request";
3816
- }
3817
- /** How many products the org-wide list names; past this a row keeps its productId only. */
3818
- const NAMED_PRODUCTS = 100;
3819
- const buildId = idInput("build run", "taiga_list_builds gives it");
3820
- /** A step without its instructions: what it is, how it stands and what it changed. */
3821
- const STEP_SUMMARY_FIELDS = [
3822
- "stepOrder",
3823
- "title",
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",
3824
4304
  "status",
3825
- "currentPhase",
3826
- "commitSha",
3827
- "filesChanged",
3828
- "verification",
4305
+ "scanners",
4306
+ "findingsCount",
4307
+ "newFindingsCount",
3829
4308
  "error",
3830
- "errorClassification",
3831
- "startedAt",
4309
+ "createdAt",
3832
4310
  "completedAt"
3833
4311
  ];
3834
- /** Past this many characters a step's headline is cut; the full read has the rest. */
3835
- const HEADLINE_LIMIT = 600;
3836
- /**
3837
- * The first paragraph of a step's summary: the agent writes its result
3838
- * there (what changed, which checks passed), and the detail after it.
3839
- */
3840
- function summaryHeadline(summary) {
3841
- if (typeof summary !== "string" || !summary.trim()) return null;
3842
- const first = summary.trim().split(/\n\s*\n/, 1)[0].trim();
3843
- return first.length > HEADLINE_LIMIT ? `${first.slice(0, HEADLINE_LIMIT).trimEnd()}...` : first;
3844
- }
4312
+ /** A policy violation in a list: what it is, how bad, and what is being done about it. */
4313
+ const VIOLATION_FIELDS = [
4314
+ "id",
4315
+ "controlId",
4316
+ "policyType",
4317
+ "severity",
4318
+ "status",
4319
+ "title",
4320
+ "summary",
4321
+ "work",
4322
+ "unconfirmedSince",
4323
+ "verifyRequestedAt",
4324
+ "assessedAt"
4325
+ ];
3845
4326
  /**
3846
- * The runs of every product the key can see, each row naming its product.
3847
- * The route carries product ids only; one products read names them, and a
3848
- * failure of that read leaves the rows unnamed rather than failing the list.
3849
- */
3850
- async function activityRows(client, opts) {
3851
- const [runs, products] = await Promise.all([client.listBuildRunActivity(opts), client.listProducts({ limit: NAMED_PRODUCTS }).catch((error) => {
3852
- if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
3853
- throw error;
3854
- })]);
3855
- const nameById = /* @__PURE__ */ new Map();
3856
- for (const product of products?.data ?? []) nameById.set(String(product.id), {
3857
- name: product.name,
3858
- slug: product.slug
3859
- });
3860
- const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map((row) => {
3861
- const product = nameById.get(String(row.productId));
3862
- return product ? {
3863
- ...row,
3864
- product
3865
- } : row;
3866
- });
3867
- 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
+ }
3868
4342
  return {
3869
- ...page(rows, runs.nextCursor),
3870
- ...note ? { note } : {}
4343
+ controls: listed.length,
4344
+ violations: violations.length,
4345
+ bySeverity,
4346
+ resolvedViolations: resolved
3871
4347
  };
3872
4348
  }
3873
- function registerBuildTools(server, client) {
4349
+ const findingId = idInput("finding", "taiga_list_findings gives it");
4350
+ function registerMaintenanceTools(server, client) {
3874
4351
  if (!client.hasCapability("products:read")) return;
3875
- server.registerTool("taiga_list_builds", {
3876
- title: "List build runs",
3877
- 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.",
3878
4370
  inputSchema: z.object({
3879
- productId: productId.optional(),
3880
- 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"),
3881
- 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"),
3882
4375
  ...pagination
3883
4376
  }),
3884
4377
  annotations: READ_ONLY
3885
4378
  }, (params) => runTool(async () => {
3886
- 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).");
3887
- 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.");
3888
- if (params.productId) {
3889
- const result = await client.listBuildRuns(params.productId, params);
3890
- return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
3891
- }
3892
- 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
+ });
3893
4385
  }));
3894
- server.registerTool("taiga_get_build", {
3895
- title: "Get build run",
3896
- 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.",
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
+ });
4397
+ }));
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.",
3897
4401
  inputSchema: z.object({
3898
- buildId,
3899
- 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)")
3900
4404
  }),
3901
4405
  annotations: READ_ONLY
3902
4406
  }, (params) => runTool(async () => {
3903
- const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
3904
- const runRow = run.data;
3905
- const rows = steps.data;
3906
- const where = stoppedIn(runRow, rows);
3907
- 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);
3908
4412
  if (params.full) return toolResult({
3909
- ...runRow,
3910
- ...stop,
3911
- steps: steps.data
4413
+ summary,
4414
+ ...standing
3912
4415
  });
3913
4416
  return toolResult({
3914
- ...runRow,
3915
- ...stop,
3916
- steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
3917
- ...step,
3918
- headline: summaryHeadline(rows[index].summary)
3919
- }))
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)
3920
4433
  });
3921
4434
  }));
3922
4435
  if (!client.hasCapability("products:write")) return;
3923
- server.registerTool("taiga_stop_build", {
3924
- title: "Stop a build run",
3925
- 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.",
3926
- 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
+ }),
3927
4466
  annotations: OPERATE
3928
4467
  }, (params) => runTool(async () => {
3929
- return toolResult(pickFields([(await client.stopBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
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
+ }),
4492
+ annotations: OPERATE
4493
+ }, (params) => runTool(async () => {
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);
3930
4496
  }));
3931
4497
  }
3932
4498
  //#endregion
@@ -3950,9 +4516,9 @@ const REPOSITORY_SUMMARY_FIELDS = [
3950
4516
  "effectiveBranch",
3951
4517
  "isManaged"
3952
4518
  ];
3953
- /** 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. */
3954
4520
  const BUILDS_IN_CONTEXT = 10;
3955
- /** 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. */
3956
4522
  const CONTEXT_BUILD_FIELDS = [
3957
4523
  "id",
3958
4524
  "runNumber",
@@ -3962,6 +4528,8 @@ const CONTEXT_BUILD_FIELDS = [
3962
4528
  "failedReason",
3963
4529
  "completedSteps",
3964
4530
  "totalSteps",
4531
+ "baseBranch",
4532
+ "prNumber",
3965
4533
  "prUrl",
3966
4534
  "prState",
3967
4535
  "prMerged",
@@ -4019,7 +4587,7 @@ const DOCUMENT_DEPTHS = [
4019
4587
  const BACKLOG_PAGE_LIMIT = 10;
4020
4588
  /**
4021
4589
  * Open items the bundle carries, the newest of those read; past this it says truncated and
4022
- * 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
4023
4591
  * open initiatives inside the tool result the document budget is sized for.
4024
4592
  */
4025
4593
  const BACKLOG_IN_CONTEXT = 50;
@@ -4029,7 +4597,10 @@ const PARTS = {
4029
4597
  policies: "tenant:documents:read",
4030
4598
  backlog: "backlog:read",
4031
4599
  repositories: "products:read",
4032
- builds: "products:read"
4600
+ builds: "products:read",
4601
+ attention: "products:read",
4602
+ maintenance: "products:read",
4603
+ standing: "products:read"
4033
4604
  };
4034
4605
  /** What the API said when a part failed, in the words the bundle reports. */
4035
4606
  function unavailableReason(reason) {
@@ -4081,7 +4652,7 @@ async function factoryPart(client, factoryId) {
4081
4652
  * asked. Null, with a note, where unpublished.
4082
4653
  */
4083
4654
  async function documentsPart(client, product, depth) {
4084
- const productId = String(product.id);
4655
+ const productId = product.id;
4085
4656
  const origin = defaultOriginFor(product);
4086
4657
  const reads = await Promise.allSettled(CONTEXT_DOCUMENT_TYPES.map((type) => client.getProductDocument(productId, type, origin)));
4087
4658
  const out = { origin };
@@ -4185,9 +4756,9 @@ async function backlogPart(client, productId) {
4185
4756
  const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
4186
4757
  if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
4187
4758
  return {
4188
- items: itemRows(rows.slice(-50), BACKLOG_SUMMARY_FIELDS),
4759
+ items: situatedRows(rows.slice(-50)),
4189
4760
  truncated,
4190
- ...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." } : {}
4191
4762
  };
4192
4763
  }
4193
4764
  /**
@@ -4203,10 +4774,56 @@ async function repositoriesPart(client, productId) {
4203
4774
  }
4204
4775
  async function buildsPart(client, productId) {
4205
4776
  const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
4206
- 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
+ };
4207
4824
  }
4208
4825
  async function productContext(client, product, options) {
4209
- const productId = String(product.id);
4826
+ const productId = product.id;
4210
4827
  const granted = (part) => client.hasCapability(PARTS[part]);
4211
4828
  const notGranted = Object.keys(PARTS).filter((part) => !granted(part));
4212
4829
  const fetchers = {
@@ -4215,16 +4832,18 @@ async function productContext(client, product, options) {
4215
4832
  policies: () => policiesPart(client),
4216
4833
  backlog: () => backlogPart(client, productId),
4217
4834
  repositories: () => repositoriesPart(client, productId),
4218
- builds: () => buildsPart(client, productId)
4835
+ builds: () => buildsPart(client, productId),
4836
+ attention: () => attentionPart(client, productId),
4837
+ maintenance: () => maintenancePart(client, productId),
4838
+ standing: () => standingPart(client, productId)
4219
4839
  };
4220
4840
  const parts = Object.keys(fetchers).filter(granted);
4221
- const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
4222
- const nextStep = setupNextStep(product);
4841
+ const [settled, readiness] = await Promise.all([Promise.allSettled(parts.map((part) => fetchers[part]())), setupReadiness(client, product)]);
4223
4842
  const bundle = { product: {
4224
4843
  ...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
4225
4844
  projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
4226
4845
  defaultOrigin: defaultOriginFor(product),
4227
- ...nextStep ? { nextStep } : {}
4846
+ ...readiness ? { readiness } : {}
4228
4847
  } };
4229
4848
  const unavailable = {};
4230
4849
  parts.forEach((part, index) => {
@@ -4237,15 +4856,15 @@ async function productContext(client, product, options) {
4237
4856
  ...bundle,
4238
4857
  notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
4239
4858
  unavailable,
4240
- 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."
4241
4860
  };
4242
- let characters = JSON.stringify(result, null, 2).length;
4861
+ let characters = JSON.stringify(inProductWords(result), null, 2).length;
4243
4862
  for (let measured = -1; measured !== characters;) {
4244
4863
  measured = characters;
4245
- characters = JSON.stringify({
4864
+ characters = JSON.stringify(inProductWords({
4246
4865
  ...result,
4247
4866
  characters: measured
4248
- }, null, 2).length;
4867
+ }), null, 2).length;
4249
4868
  }
4250
4869
  return toolResult({
4251
4870
  ...result,
@@ -4256,7 +4875,7 @@ function registerContextTools(server, client) {
4256
4875
  if (!client.hasCapability("products:read")) return;
4257
4876
  server.registerTool("taiga_get_product_context", {
4258
4877
  title: "Get product context",
4259
- 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.`,
4260
4879
  inputSchema: z.object({
4261
4880
  productId: productId.optional(),
4262
4881
  slug: slug.optional(),
@@ -4276,6 +4895,14 @@ function registerContextTools(server, client) {
4276
4895
  }
4277
4896
  //#endregion
4278
4897
  //#region src/tools/autonomy.ts
4898
+ /**
4899
+ * Autonomy (docs.tai.ga, Deliver: Initiatives, Auto-merge): whether a landed
4900
+ * plan is built without a review, and whether a pull request merges on its
4901
+ * own. The organization and its factories are ceilings for merging, set by
4902
+ * an administrator in the web; a product sets defaults; an initiative's own
4903
+ * switch is the final word within the ceilings. The initiative's build gate
4904
+ * is taiga_set_auto_build.
4905
+ */
4279
4906
  function registerAutonomyTools(server, client) {
4280
4907
  if (client.hasCapability("products:write")) server.registerTool("taiga_set_product_autonomy", {
4281
4908
  title: "Set a product's autonomy defaults",
@@ -4305,12 +4932,12 @@ function registerAutonomyTools(server, client) {
4305
4932
  title: "Set whether an initiative's pull request merges on its own",
4306
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.",
4307
4934
  inputSchema: z.object({
4308
- 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"),
4309
4936
  autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
4310
4937
  }),
4311
4938
  annotations: OPERATE_SETTING
4312
4939
  }, (params) => runTool(async () => {
4313
- const item = (await client.updateBacklogItem(params.itemId, { autoMerge: params.autoMerge })).data;
4940
+ const item = (await client.updateBacklogItem(params.initiativeId, { autoMerge: params.autoMerge })).data;
4314
4941
  return toolResult(pickFields([item], [
4315
4942
  "id",
4316
4943
  "title",
@@ -4322,6 +4949,11 @@ function registerAutonomyTools(server, client) {
4322
4949
  }
4323
4950
  //#endregion
4324
4951
  //#region src/tools/inbox.ts
4952
+ /**
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.
4956
+ */
4325
4957
  const ITEM_FIELDS = [
4326
4958
  "id",
4327
4959
  "productId",
@@ -4335,10 +4967,48 @@ const ITEM_FIELDS = [
4335
4967
  "status",
4336
4968
  "blocking",
4337
4969
  "snoozedUntil",
4338
- "metadata",
4339
4970
  "createdAt",
4340
4971
  "resolvedAt"
4341
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;
4342
5012
  const SETUP_TASK_FIELDS = [
4343
5013
  "id",
4344
5014
  "productId",
@@ -4359,38 +5029,94 @@ const SETUP_TASK_FIELDS = [
4359
5029
  function totalOf(result) {
4360
5030
  return typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {};
4361
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
+ }
4362
5070
  function registerInboxTools(server, client) {
4363
5071
  if (!client.hasCapability("products:read")) return;
4364
5072
  server.registerTool("taiga_list_action_required", {
4365
5073
  title: "List what needs a person",
4366
- 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.",
4367
5075
  inputSchema: z.object({
4368
5076
  productId: productId.optional(),
4369
5077
  category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
4370
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)"),
4371
5080
  ...pagination
4372
5081
  }),
4373
5082
  annotations: READ_ONLY
4374
5083
  }, (params) => runTool(async () => {
4375
- const { resolved, ...rest } = params;
4376
- const result = await client.listInbox({
5084
+ const { resolved, full, ...rest } = params;
5085
+ const query = {
4377
5086
  ...rest,
4378
- ...resolved ? { resolved: true } : {},
4379
- ...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;
4380
5101
  });
4381
5102
  const now = Date.now();
5103
+ const isDue = (row) => !(row["status"] === "snoozed" && typeof row["snoozedUntil"] === "string" && Date.parse(row["snoozedUntil"]) > now);
4382
5104
  const rows = result.data;
4383
- const due = rows.filter((row) => !(row.status === "snoozed" && typeof row.snoozedUntil === "string" && Date.parse(row.snoozedUntil) > now));
5105
+ const due = rows.filter(isDue);
4384
5106
  const hidden = rows.length - due.length;
4385
5107
  return toolResult({
4386
- ...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),
4387
5113
  ...totalOf(result),
4388
5114
  ...hidden > 0 ? { snoozedHidden: hidden } : {}
4389
5115
  });
4390
5116
  }));
4391
5117
  server.registerTool("taiga_list_setup_tasks", {
4392
5118
  title: "List setup tasks",
4393
- 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.",
4394
5120
  inputSchema: z.object({
4395
5121
  productId: productId.optional(),
4396
5122
  includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
@@ -4405,7 +5131,7 @@ function registerInboxTools(server, client) {
4405
5131
  ...params.cursor === void 0 ? { withTotal: true } : {}
4406
5132
  });
4407
5133
  return toolResult({
4408
- ...page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor),
5134
+ ...page(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor),
4409
5135
  ...totalOf(result)
4410
5136
  });
4411
5137
  }));
@@ -4414,7 +5140,7 @@ function registerInboxTools(server, client) {
4414
5140
  title: "Resolve or snooze an Action Required item",
4415
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.",
4416
5142
  inputSchema: z.object({
4417
- itemId: idInput("Action Required item", "taiga_list_action_required gives it"),
5143
+ actionId: idInput("Action Required item", "taiga_list_action_required gives it"),
4418
5144
  status: z.enum([
4419
5145
  "in_progress",
4420
5146
  "done",
@@ -4425,7 +5151,7 @@ function registerInboxTools(server, client) {
4425
5151
  annotations: OPERATE
4426
5152
  }, (params) => runTool(async () => {
4427
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.");
4428
- return toolResult(pickFields([(await client.updateInboxItem(params.itemId, {
5154
+ return toolResult(pickFields([(await client.updateInboxItem(params.actionId, {
4429
5155
  ...params.status !== void 0 ? { status: params.status } : {},
4430
5156
  ...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
4431
5157
  })).data], ITEM_FIELDS)[0]);
@@ -4433,6 +5159,14 @@ function registerInboxTools(server, client) {
4433
5159
  }
4434
5160
  //#endregion
4435
5161
  //#region src/tools/knowledge.ts
5162
+ /**
5163
+ * Knowledge (docs.tai.ga, Context): reference material agents search when
5164
+ * the work calls for it, at the organization, a factory or a product, and
5165
+ * narrowing as it comes down. Not instructions: a rule belongs in
5166
+ * instructions, a forty-page integration guide here. Members manage it on
5167
+ * their factories and products; organization knowledge takes an owner or
5168
+ * admin, which the API enforces.
5169
+ */
4436
5170
  const KNOWLEDGE_FIELDS = [
4437
5171
  "id",
4438
5172
  "factoryId",
@@ -4607,213 +5341,14 @@ function registerKnowledgeTools(server, client) {
4607
5341
  }));
4608
5342
  }
4609
5343
  //#endregion
4610
- //#region src/tools/maintenance.ts
4611
- /** A finding as a list row: enough to decide whether to open, fix or ignore it. */
4612
- const FINDING_ROW_FIELDS = [
4613
- "id",
4614
- "kind",
4615
- "severity",
4616
- "tier",
4617
- "status",
4618
- "title",
4619
- "ecosystem",
4620
- "packageName",
4621
- "currentVersion",
4622
- "recommendedVersion",
4623
- "advisoryId",
4624
- "location",
4625
- "sourcePaths",
4626
- "productRepositoryId",
4627
- "ownership",
4628
- "remediableBy",
4629
- "controls",
4630
- "backlogItemId",
4631
- "detectedAt",
4632
- "ignoreReason",
4633
- "ignoreExpiresAt"
4634
- ];
4635
- /** A sweep as the record of when the product was last checked, and whether that check held. */
4636
- const SWEEP_FIELDS = [
4637
- "id",
4638
- "trigger",
4639
- "status",
4640
- "scanners",
4641
- "findingsCount",
4642
- "newFindingsCount",
4643
- "error",
4644
- "createdAt",
4645
- "completedAt"
4646
- ];
4647
- /** A policy violation in a list: what it is, how bad, and what is being done about it. */
4648
- const VIOLATION_FIELDS = [
4649
- "id",
4650
- "controlId",
4651
- "policyType",
4652
- "severity",
4653
- "status",
4654
- "title",
4655
- "summary",
4656
- "work",
4657
- "unconfirmedSince",
4658
- "verifyRequestedAt",
4659
- "assessedAt"
4660
- ];
4661
- const findingId = idInput("finding", "taiga_list_findings gives it");
4662
- function registerMaintenanceTools(server, client) {
4663
- if (!client.hasCapability("products:read")) return;
4664
- server.registerTool("taiga_get_maintenance", {
4665
- title: "Get a product's maintenance standing",
4666
- 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.",
4667
- inputSchema: z.object({ productId }),
4668
- annotations: READ_ONLY
4669
- }, (params) => runTool(async () => {
4670
- const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
4671
- const latest = pickFields(sweeps.data, SWEEP_FIELDS);
4672
- const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
4673
- return toolResult({
4674
- ...standing,
4675
- latestSweeps: latest,
4676
- ...latest.length === 0 ? { note: "This product has never been swept: no result is not an all-clear." } : {}
4677
- });
4678
- }));
4679
- server.registerTool("taiga_list_findings", {
4680
- title: "List maintenance findings",
4681
- 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.",
4682
- inputSchema: z.object({
4683
- productId,
4684
- status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
4685
- severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
4686
- kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
4687
- ...pagination
4688
- }),
4689
- annotations: READ_ONLY
4690
- }, (params) => runTool(async () => {
4691
- const { productId: id, ...filters } = params;
4692
- const result = await client.listFindings(id, filters);
4693
- return toolResult({
4694
- ...page(pickFields(result.data, FINDING_ROW_FIELDS), result.nextCursor),
4695
- ...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
4696
- });
4697
- }));
4698
- server.registerTool("taiga_get_finding", {
4699
- title: "Get a maintenance finding",
4700
- 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.",
4701
- inputSchema: z.object({ findingId }),
4702
- annotations: READ_ONLY
4703
- }, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
4704
- server.registerTool("taiga_get_policy_standing", {
4705
- title: "Get a product's policy standing",
4706
- 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.",
4707
- inputSchema: z.object({
4708
- productId,
4709
- full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
4710
- }),
4711
- annotations: READ_ONLY
4712
- }, (params) => runTool(async () => {
4713
- const standing = (await client.getPolicyStanding(params.productId)).data;
4714
- const controls = Array.isArray(standing.controls) ? standing.controls : [];
4715
- const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
4716
- const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
4717
- const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
4718
- const bySeverity = {};
4719
- for (const violation of violations) {
4720
- const severity = String(violation.severity ?? "unknown");
4721
- bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
4722
- }
4723
- const summary = {
4724
- controls: listed.length,
4725
- violations: violations.length,
4726
- bySeverity,
4727
- resolvedViolations: resolved
4728
- };
4729
- if (params.full) return toolResult({
4730
- summary,
4731
- ...standing
4732
- });
4733
- return toolResult({
4734
- summary,
4735
- productId: standing.productId,
4736
- hasPolicies: standing.hasPolicies,
4737
- assessment: standing.assessment,
4738
- documents: standing.documents,
4739
- controls: pickFields(listed, [
4740
- "controlId",
4741
- "policyType",
4742
- "text",
4743
- "state",
4744
- "divergenceCount",
4745
- "findingCount",
4746
- "findingsByTier",
4747
- "plannedCount"
4748
- ]),
4749
- violations: pickFields(violations, VIOLATION_FIELDS)
4750
- });
4751
- }));
4752
- if (!client.hasCapability("products:write")) return;
4753
- server.registerTool("taiga_ignore_finding", {
4754
- title: "Ignore or reopen a finding",
4755
- 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.",
4756
- inputSchema: z.object({
4757
- findingId,
4758
- ignored: z.boolean().describe("true ignores the finding, false reopens it"),
4759
- reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe("Why it is ignored; required when ignoring"),
4760
- note: z.string().trim().max(280).optional().describe("A short justification, kept for audit"),
4761
- 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)")
4762
- }),
4763
- annotations: OPERATE
4764
- }, (params) => runTool(async () => {
4765
- if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
4766
- const body = params.ignored ? {
4767
- status: "ignored",
4768
- ignoreReason: params.reason,
4769
- ...params.note !== void 0 ? { ignoreNote: params.note } : {},
4770
- ...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
4771
- } : { status: "open" };
4772
- const finding = (await client.updateFinding(params.findingId, body)).data;
4773
- return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, "ignoreNote"])[0]);
4774
- }));
4775
- server.registerTool("taiga_fix_finding", {
4776
- title: "Fix a finding",
4777
- 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.",
4778
- inputSchema: z.object({
4779
- productId,
4780
- findingId,
4781
- wholeGroup: z.boolean().optional().describe("Fix every finding in this finding's group (its package and version, or its rule) together")
4782
- }),
4783
- annotations: OPERATE
4784
- }, (params) => runTool(async () => {
4785
- if (!params.wholeGroup) {
4786
- const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
4787
- return toolResult({
4788
- backlogItemId: fixed.backlogItemId,
4789
- finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null
4790
- });
4791
- }
4792
- const finding = (await client.getFinding(params.findingId)).data;
4793
- if (finding.productId !== params.productId) return toolError("NOT_FOUND", "No such finding in this product.");
4794
- return toolResult((await client.remediatePackage(params.productId, {
4795
- ecosystem: String(finding.ecosystem),
4796
- packageName: String(finding.packageName),
4797
- currentVersion: typeof finding.currentVersion === "string" ? finding.currentVersion : null,
4798
- productRepositoryId: typeof finding.productRepositoryId === "string" ? finding.productRepositoryId : null
4799
- })).data);
4800
- }));
4801
- server.registerTool("taiga_fix_policy_violation", {
4802
- title: "Fix policy violations",
4803
- 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.",
4804
- inputSchema: z.object({
4805
- productId,
4806
- controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
4807
- violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
4808
- }),
4809
- annotations: OPERATE
4810
- }, (params) => runTool(async () => {
4811
- 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.");
4812
- return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
4813
- }));
4814
- }
4815
- //#endregion
4816
5344
  //#region src/tools/observe.ts
5345
+ /**
5346
+ * What runs where and how it is doing (docs.tai.ga, Deliver: Deployments;
5347
+ * Operate: Monitoring). Taiga observes deployments, it does not perform them:
5348
+ * the history is mirrored from the provider, so these tools read and never
5349
+ * deploy, approve or cancel. Importing history reaches into the connected
5350
+ * provider and is an administrator's action in the web.
5351
+ */
4817
5352
  /** A deployment as a line of history: what shipped where, how it ended, and who shipped it. */
4818
5353
  const DEPLOYMENT_FIELDS = [
4819
5354
  "id",
@@ -4842,6 +5377,20 @@ const ENVIRONMENT_FIELDS = [
4842
5377
  "isProtected",
4843
5378
  "url"
4844
5379
  ];
5380
+ /**
5381
+ * Deployments as lines, each naming the repository that deployed it: every
5382
+ * repository runs its own deployment in an environment, so two running in
5383
+ * production are two repositories, not a contradiction (PRO-2528).
5384
+ */
5385
+ function deploymentLines(rows) {
5386
+ return rows.map((row) => {
5387
+ const repository = row.metadata?.["repositoryFullName"];
5388
+ return {
5389
+ ...pickFields([row], DEPLOYMENT_FIELDS)[0],
5390
+ repository: typeof repository === "string" ? repository : null
5391
+ };
5392
+ });
5393
+ }
4845
5394
  const environmentId = idInput("environment", "taiga_list_environments lists them; a product ID is not one");
4846
5395
  /** The windows the pages offer, as the schema's own values rather than a number checked afterwards. */
4847
5396
  const windowDays = z.literal([
@@ -4910,9 +5459,9 @@ function registerObserveTools(server, client) {
4910
5459
  return toolResult({
4911
5460
  environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
4912
5461
  ...environments.more ? { moreEnvironments: true } : {},
4913
- running: pickFields(now.current ?? [], DEPLOYMENT_FIELDS),
4914
- deploying: pickFields(now.deploying ?? [], DEPLOYMENT_FIELDS),
4915
- history: page(pickFields(history.data, DEPLOYMENT_FIELDS), history.nextCursor),
5462
+ running: deploymentLines(now.current ?? []),
5463
+ deploying: deploymentLines(now.deploying ?? []),
5464
+ history: page(deploymentLines(history.data), history.nextCursor),
4916
5465
  ..."value" in sync ? { syncStatus: sync.value.data } : {
4917
5466
  syncStatus: null,
4918
5467
  syncUnavailable: sync.unavailable
@@ -5031,7 +5580,7 @@ function registerProductDocumentWrites(server, client) {
5031
5580
  const canRead = client.hasCapability("products:read");
5032
5581
  const productScope = (product, type) => ({
5033
5582
  getDraft: () => client.getProductDocumentDraft(product, type),
5034
- getPublished: async () => await client.getProductDocument(product, type, "plan"),
5583
+ getPublished: () => client.getProductDocument(product, type, "plan"),
5035
5584
  saveDraft: (data) => client.saveProductDocumentDraft(product, type, { data }),
5036
5585
  publish: () => client.publishProductDocument(product, type)
5037
5586
  });
@@ -5048,7 +5597,7 @@ function registerProductDocumentWrites(server, client) {
5048
5597
  }, (params) => runTool(() => updateSection(productScope(params.productId, params.type), params.type, params.sectionId, params.content)));
5049
5598
  server.registerTool("taiga_publish_product_document", {
5050
5599
  title: "Publish a product document draft",
5051
- 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).",
5052
5601
  inputSchema: z.object({
5053
5602
  productId,
5054
5603
  type: documentType
@@ -19540,7 +20089,7 @@ function renderDocument(doc) {
19540
20089
  ].filter((key) => doc[key] !== void 0 && doc[key] !== null).map((key) => `${key}: ${String(doc[key])}`);
19541
20090
  const core = Object.keys(sections).filter((id) => {
19542
20091
  const section = sections[id];
19543
- return isRecord(section) && section.priority === "core";
20092
+ return isSectionObject(section) && section.priority === "core";
19544
20093
  });
19545
20094
  if (core.length > 0) facts.push(`core sections: ${core.join(", ")}`);
19546
20095
  if (facts.length > 0) lines.push("", facts.join(" | "));
@@ -19636,7 +20185,7 @@ function atxOf(depth, children) {
19636
20185
  }
19637
20186
  /** The section's title when it carries one, else its id. */
19638
20187
  function sectionTitle(section, id) {
19639
- return isRecord(section) && typeof section.title === "string" && section.title.trim() ? section.title.trim() : id;
20188
+ return isSectionObject(section) && typeof section.title === "string" && section.title.trim() ? section.title.trim() : id;
19640
20189
  }
19641
20190
  /** The mime type a read will answer with, for a list entry that must not disagree with it. */
19642
20191
  function mimeTypeOf(doc) {
@@ -19728,18 +20277,18 @@ function registerPrompts(server, client) {
19728
20277
  "",
19729
20278
  "Before writing code: follow the factory instructions; read the policy sections that touch what you are",
19730
20279
  "about to change with taiga_get_document (security, data, SDLC, architecture at least); read the backlog",
19731
- "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",
19732
20281
  "could not be read; say so rather than assuming they are empty.",
19733
20282
  "",
19734
20283
  fence("json", (await productContext(client, resolved.product, { documents: "full" })).content[0]?.text ?? "{}")
19735
20284
  ].join("\n"));
19736
20285
  });
19737
- if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_backlog_item", {
19738
- title: "Implement a backlog item",
19739
- 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.",
19740
- 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") })
19741
20290
  }, async (args) => {
19742
- 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;
19743
20292
  const productId = String(item.productId);
19744
20293
  const product = (await served$1(client.getProduct(productId), "The product of that item is not visible to the key")).data;
19745
20294
  const origin = defaultOriginFor(product, "architecture");
@@ -19803,6 +20352,14 @@ var NotGranted = class extends Error {
19803
20352
  };
19804
20353
  //#endregion
19805
20354
  //#region src/tools/repositories.ts
20355
+ /**
20356
+ * A product's repository (docs.tai.ga, Administration: Integrations and
20357
+ * environments, Source control). A linked repository is the prerequisite for
20358
+ * most of what Taiga does: planning reads the code, runs work on a branch of
20359
+ * it, sweeps scan it and the as-built documents are derived from it. GitHub is
20360
+ * connected once for the organization, in the web; linking one of the
20361
+ * repositories it can reach to a product is a separate act, per product.
20362
+ */
19806
20363
  const LINK_FIELDS = [
19807
20364
  "id",
19808
20365
  "provider",
@@ -19830,7 +20387,8 @@ const GITHUB_REPOSITORY_FIELDS = [
19830
20387
  "archived",
19831
20388
  "defaultBranch",
19832
20389
  "description",
19833
- "language"
20390
+ "language",
20391
+ "linked"
19834
20392
  ];
19835
20393
  const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
19836
20394
  /** Round-robin over lists: the first of each, then the second of each, and so on. */
@@ -19872,7 +20430,7 @@ function registerRepositoryTools(server, client) {
19872
20430
  }
19873
20431
  if (client.hasCapability("vcs:read")) server.registerTool("taiga_list_linkable_repositories", {
19874
20432
  title: "Find a repository to link",
19875
- 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.",
19876
20434
  inputSchema: z.object({
19877
20435
  query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
19878
20436
  account: z.string().trim().min(1).max(140).optional().describe("The GitHub account login (the owner in owner/repo), case-insensitive"),
@@ -20024,7 +20582,7 @@ function registerResources(server, client) {
20024
20582
  title: "Organization document",
20025
20583
  description: "A published organization document: a policy, the business goals, the design or brand standards, the tone of voice or the standing instructions. Sections rendered as headings."
20026
20584
  }, async (uri, variables) => {
20027
- const type = requireOneOf("Document type", String(variables.type), TENANT_DOCUMENT_TYPES);
20585
+ const type = requireOneOf("Document type", String(variables["type"]), TENANT_DOCUMENT_TYPES);
20028
20586
  try {
20029
20587
  return contents(uri.href, (await client.getTenantDocument(type)).data);
20030
20588
  } catch (error) {
@@ -20041,7 +20599,7 @@ function registerResources(server, client) {
20041
20599
  title: "Factory instructions",
20042
20600
  description: "A factory's published standing guidance; taiga_list_factories gives the factory ID. Markdown for a section map, JSON otherwise (the read says which)."
20043
20601
  }, async (uri, variables) => {
20044
- const factoryId = requireId("Factory ID", String(variables.factoryId));
20602
+ const factoryId = requireId("Factory ID", String(variables["factoryId"]));
20045
20603
  const doc = await served(client.getFactoryDocument(factoryId, FACTORY_DOCUMENT_TYPES[0]), "This factory has not published instructions, or the key cannot see the factory");
20046
20604
  return contents(uri.href, doc.data);
20047
20605
  });
@@ -20061,28 +20619,28 @@ function registerResources(server, client) {
20061
20619
  title: "Product document on a timeline",
20062
20620
  description: "A product document from the named timeline: origin=plan (discovery) or origin=reality (derived from the code). Markdown for a section map, JSON otherwise (the read says which)."
20063
20621
  }, async (uri, variables) => {
20064
- const productId = requireId("Product ID", String(variables.productId));
20065
- const type = documentType(String(variables.type));
20066
- const origin = requireOneOf("Origin", String(variables.origin), PRODUCT_DOCUMENT_ORIGINS);
20622
+ const productId = requireId("Product ID", String(variables["productId"]));
20623
+ const type = documentType(String(variables["type"]));
20624
+ const origin = requireOneOf("Origin", String(variables["origin"]), PRODUCT_DOCUMENT_ORIGINS);
20067
20625
  return read(uri, productId, type, origin);
20068
20626
  });
20069
20627
  server.registerResource("product-document", new ResourceTemplate("taiga://products/{productId}/documents/{type}", { list: void 0 }), {
20070
20628
  title: "Product document",
20071
20629
  description: "A product document from the product's own timeline (reality for an imported product, plan otherwise; the documents a person writes are always on the plan side). taiga_list_product_documents shows what exists; add ?origin=plan or ?origin=reality to read the other side. Markdown for a section map, JSON otherwise (the read says which)."
20072
20630
  }, async (uri, variables) => {
20073
- const productId = requireId("Product ID", String(variables.productId));
20074
- const type = documentType(String(variables.type));
20631
+ const productId = requireId("Product ID", String(variables["productId"]));
20632
+ const type = documentType(String(variables["type"]));
20075
20633
  const product = (await served(client.getProduct(productId), "No product with that ID the key can see; taiga_list_products shows them")).data;
20076
20634
  return read(uri, productId, type, defaultOriginFor(product, type));
20077
20635
  });
20078
20636
  }
20079
- if (client.hasCapability("backlog:read")) server.registerResource("backlog-item", new ResourceTemplate("taiga://backlog/{itemId}", { list: void 0 }), {
20080
- title: "Backlog item",
20081
- 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.",
20082
20640
  mimeType: "application/json"
20083
20641
  }, async (uri, variables) => {
20084
- const itemId = requireId("Backlog item ID", String(variables.itemId));
20085
- 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;
20086
20644
  return { contents: [{
20087
20645
  uri: uri.href,
20088
20646
  mimeType: "application/json",
@@ -20092,6 +20650,14 @@ function registerResources(server, client) {
20092
20650
  }
20093
20651
  //#endregion
20094
20652
  //#region src/tools/setup.ts
20653
+ /**
20654
+ * Setting a product up (docs.tai.ga, Start: Your first product, Learn: Import
20655
+ * an existing codebase, Organizations, factories and products): factories to
20656
+ * put products in, a product started from scratch or from an imported
20657
+ * codebase, what Taiga builds for it, the import, and finishing or reopening
20658
+ * Discovery. Who may reach a factory (its members) and deleting a factory or
20659
+ * a product stay in the web.
20660
+ */
20095
20661
  const PRODUCT_FIELDS = [
20096
20662
  "id",
20097
20663
  "name",
@@ -20118,33 +20684,17 @@ const buildScope = {
20118
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");
20119
20685
  /** How a missing required document comes to exist, said with a documents_incomplete refusal. */
20120
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.";
20121
- /** 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. */
20122
20688
  async function productRow(client, product) {
20123
20689
  const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
20124
- const nextStep = setupNextStep(await withDiscoveryStep(client, product));
20690
+ const readiness = await setupReadiness(client, product);
20125
20691
  return {
20126
20692
  ...rest,
20127
20693
  projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
20128
- ...nextStep ? { nextStep } : {}
20694
+ ...readiness ? { readiness } : {}
20129
20695
  };
20130
20696
  }
20131
20697
  /**
20132
- * The product with its discoveryStep. The API derives it on reads only, not
20133
- * on the answer to a write (products.ts), and a draft started from scratch
20134
- * needs it to say what comes next; an imported one reads its import status
20135
- * instead. Best effort: a failed read leaves the product without a nextStep
20136
- * rather than failing the write that already happened.
20137
- */
20138
- async function withDiscoveryStep(client, product) {
20139
- if (product.status !== "draft" || "discoveryStep" in product || isImportedProduct(product)) return product;
20140
- try {
20141
- return (await client.getProduct(String(product.id))).data;
20142
- } catch (error) {
20143
- if (error instanceof TaigaApiError || isNetworkFailure(error)) return product;
20144
- throw error;
20145
- }
20146
- }
20147
- /**
20148
20698
  * A half turned off with nothing said about how it is done today. Allowed (the
20149
20699
  * web asks for the sentence but does not insist), so a warning rather than a
20150
20700
  * refusal: plans then design against a gap.
@@ -20189,7 +20739,7 @@ function registerSetupTools(server, client) {
20189
20739
  }, (params) => runTool(async () => toolResult((await client.previewProductMove(params.productId, params.factoryId)).data)));
20190
20740
  server.registerTool("taiga_move_product", {
20191
20741
  title: "Move a product to another factory",
20192
- 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.",
20193
20743
  inputSchema: z.object({
20194
20744
  productId,
20195
20745
  factoryId: factoryId.describe("The factory to move it to"),
@@ -20197,10 +20747,16 @@ function registerSetupTools(server, client) {
20197
20747
  }),
20198
20748
  annotations: OPERATE
20199
20749
  }, (params) => runTool(async () => {
20200
- const moved = await client.moveProduct(params.productId, {
20201
- factoryId: params.factoryId,
20202
- ...params.stopLine ? { stopLine: true } : {}
20203
- });
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
+ }
20204
20760
  return toolResult({
20205
20761
  product: await productRow(client, moved.data),
20206
20762
  changed: moved.changed,
@@ -20276,7 +20832,7 @@ function registerSetupTools(server, client) {
20276
20832
  try {
20277
20833
  return toolResult(await productRow(client, (await client.completeSetup(params.productId)).data));
20278
20834
  } catch (error) {
20279
- if (!(error instanceof TaigaApiError) || error.details?.reason !== "documents_incomplete") throw error;
20835
+ if (!(error instanceof TaigaApiError) || error.details?.["reason"] !== "documents_incomplete") throw error;
20280
20836
  return toolError(error.code ?? "CONFLICT", `${withStop(error.apiMessage ?? "Required documents are not published yet")} ${MISSING_DOCUMENTS_HOW}`, error.details);
20281
20837
  }
20282
20838
  }));
@@ -20494,7 +21050,9 @@ function judgeItem(until, gate) {
20494
21050
  /** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
20495
21051
  function itemReport(item) {
20496
21052
  const run = latestBuildOf(item);
21053
+ const situation = situationOf(item);
20497
21054
  return {
21055
+ ...situation ? { situation } : {},
20498
21056
  ...pickFields([item], [
20499
21057
  "id",
20500
21058
  "title",
@@ -20541,6 +21099,8 @@ const RUN_REPORT_FIELDS = [
20541
21099
  "prState",
20542
21100
  "prDraft",
20543
21101
  "prMerged",
21102
+ "prNumber",
21103
+ "baseBranch",
20544
21104
  "actions"
20545
21105
  ];
20546
21106
  const runSignature = (run) => JSON.stringify([
@@ -20561,11 +21121,11 @@ function judgeBuild(until) {
20561
21121
  };
20562
21122
  }
20563
21123
  function registerWaitTools(server, client, options) {
20564
- if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_backlog_item", {
21124
+ if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_initiative", {
20565
21125
  title: "Wait for an initiative",
20566
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.",
20567
21127
  inputSchema: z.object({
20568
- 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"),
20569
21129
  until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
20570
21130
  timeoutSeconds
20571
21131
  }),
@@ -20573,7 +21133,7 @@ function registerWaitTools(server, client, options) {
20573
21133
  }, (params, ctx) => runTool(async () => {
20574
21134
  const gate = { product: void 0 };
20575
21135
  const read = async (signal) => {
20576
- const item = (await client.getBacklogItem(params.itemId, signal)).data;
21136
+ const item = (await client.getBacklogItem(params.initiativeId, signal)).data;
20577
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) => {
20578
21138
  const value = p.data.autoBuild;
20579
21139
  return typeof value === "boolean" ? value : true;
@@ -20594,17 +21154,17 @@ function registerWaitTools(server, client, options) {
20594
21154
  item: now ? itemReport(now) : null
20595
21155
  });
20596
21156
  }));
20597
- if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_build", {
21157
+ if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_run", {
20598
21158
  title: "Wait for a run",
20599
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.",
20600
21160
  inputSchema: z.object({
20601
- buildId: idInput("build run", "taiga_list_builds gives it"),
21161
+ runId: idInput("run", "taiga_list_runs gives it"),
20602
21162
  until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
20603
21163
  timeoutSeconds
20604
21164
  }),
20605
21165
  annotations: READ_ONLY
20606
21166
  }, (params, ctx) => runTool(async () => {
20607
- 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, {
20608
21168
  progressToken: ctx.mcpReq._meta?.progressToken,
20609
21169
  notify: ctx.mcpReq.notify,
20610
21170
  signal: ctx.mcpReq.signal
@@ -20612,7 +21172,7 @@ function registerWaitTools(server, client, options) {
20612
21172
  return toolResult({
20613
21173
  until: params.until,
20614
21174
  ...outcome,
20615
- run: now ? pickFields([now], RUN_REPORT_FIELDS)[0] : null
21175
+ run: now ? readableRun(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
20616
21176
  });
20617
21177
  }));
20618
21178
  if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
@@ -20655,7 +21215,8 @@ function withStrictInputs(server) {
20655
21215
  const withHint = (args, ctx) => notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
20656
21216
  const described = {
20657
21217
  ...config,
20658
- description: `${writeNotice(name, config.annotations)}${config.description ?? ""}`
21218
+ description: `${writeNotice(name, config.annotations)}${config.description ?? ""}`,
21219
+ ...ALWAYS_LOAD.has(name) ? { _meta: { "anthropic/alwaysLoad": true } } : {}
20659
21220
  };
20660
21221
  return register(name, schema ? {
20661
21222
  ...described,
@@ -20665,6 +21226,19 @@ function withStrictInputs(server) {
20665
21226
  return server;
20666
21227
  }
20667
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
+ /**
20668
21242
  * What a tool's annotations say, in its description's first words (PRO-2778).
20669
21243
  * Annotations are for the client: Claude Code does not show them to the
20670
21244
  * model, so an agent read 79 tools without knowing which ones write. A tool
@@ -20701,136 +21275,40 @@ function registerAllTools(server, client, options = {}) {
20701
21275
  registerResources(server, client);
20702
21276
  registerPrompts(server, client);
20703
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
+ */
20704
21291
  const SERVER_INSTRUCTIONS = [
20705
- [
20706
- "You have access to Taiga, a policy-aware software development platform: the organization's",
20707
- "engineering knowledge base, its products, their backlog and what the coding agent has built.",
20708
- "",
20709
- "**Start here.** Call taiga_get_product_context first, naming the product by its git remote URL inside a",
20710
- "checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with",
20711
- "query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory",
20712
- "instructions, the outline of its specification and architecture, every policy as its section ids, the",
20713
- "open initiatives, the repositories and the latest runs. Then read only what you need: sections by id with",
20714
- "taiga_get_document or taiga_get_product_document (sections: [] is any document's outline), an",
20715
- "initiative with taiga_get_backlog_item. Check Action Required (taiga_list_action_required) for what",
20716
- "waits on a person.",
20717
- "",
20718
- "**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that",
20719
- "starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it",
20720
- "(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). Such a",
20721
- "tool says so first in its description; a client that prompts on destructive tools does part of it.",
20722
- "",
20723
- "**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the",
20724
- "right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.",
20725
- "",
20726
- "**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action",
20727
- "Required is the inbox; the line is the queue; a factory is the group that owns a set of products."
20728
- ].join("\n"),
20729
- "",
20730
- "**Company documents** (taiga_list_documents / taiga_get_document):",
20731
- `The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
20732
- `architecture rules that apply to all code. The other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)})`,
20733
- "carry the business goals, design and brand standards, tone of voice and standing instructions.",
20734
- "Read the relevant policy BEFORE writing code that touches security, data handling or infrastructure.",
20735
- "",
20736
- "**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
20737
- "A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
20738
- "tech preferences (approved stack, patterns) and conventions. Read these to understand HOW this factory",
20739
- "builds software. A factory that has not written any yet has none; that is normal.",
20740
- "",
20741
- "**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
20742
- `${Object.entries(PRODUCT_DOCUMENT_NAMES).map(([type, name]) => `${name} (${type})`).join(", ")}.`,
20743
- "A product has two timelines of these, and the list shows both:",
20744
- `- ${ORIGIN_MEANING.plan}`,
20745
- `- ${ORIGIN_MEANING.reality}`,
20746
- "An imported (brownfield) product has no generated discovery chain: its derived documents are on the",
20747
- "reality side, while the documents a person writes (Look & Feel, Product Instructions) are on the plan",
20748
- "side of every product. Omit origin on a read and the server picks the right side (the list reports the",
20749
- "product's own as defaultOrigin). To understand",
20750
- "what to build, read the plan where there is one; to understand the code you are changing, read reality",
20751
- "when it exists, the ground truth for architecture, data flow and threats as built.",
20752
- "",
20753
- "**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
20754
- "The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
20755
- "for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
20756
- "every product the key can see, and active: true the ones executing now.",
20757
- "",
20758
- "**Operating the line** works the way Taiga is meant to be used. New work is described, not written:",
20759
- "taiga_request_initiatives asks an agent, which reads the product and its repository, expands the request",
20760
- "into initiatives in the Backlog (or says why it added none: already covered, not work, out of scope, or a",
20761
- "policy that does not allow it) -- follow it with taiga_wait_for_job. The queue is the only way to start",
20762
- "work (taiga_queue_backlog_item): Taiga plans and builds one initiative at a time, in queue order, and starts",
20763
- "the next when the pull request merges. Whether a landed plan waits for a review is the review gate",
20764
- "(taiga_set_auto_build, or autoBuild on the queue); read it with taiga_get_plan and answer with",
20765
- "taiga_approve_plan or taiga_reject_plan. Decisions about the work are made on the initiative: stop work by",
20766
- "moving it out of Build (taiga_move_backlog_item), carry it on with taiga_resume_backlog_item, ask for a new",
20767
- "plan with taiga_plan_again; a run is the record of an attempt. taiga_pause_line pauses the whole line.",
20768
- "taiga_stop_build is the emergency brake for a run whose initiative cannot be moved. Queueing, approving,",
20769
- "resuming and requesting start paid agent work; stopping and pausing interrupt it. Tell the person what you",
20770
- "are about to start or stop and wait for their yes; a client that prompts on destructive tools does that",
20771
- "for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
20772
- "",
20773
- "**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build /",
20774
- "taiga_get_job / taiga_wait_for_job): the queue read says what the line is waiting on (a paused line, a plan",
20775
- "to review, a pull request to merge, a halt and what lifts it). Planning takes minutes and a build up to an",
20776
- "hour: wait with the wait tools rather than reading the same item again and again; each returns when the",
20777
- "point is reached, when only a person can move it on, or at its timeout (call again to keep waiting).",
20778
- "",
20779
- "**Action Required** (taiga_list_action_required / taiga_list_setup_tasks / taiga_resolve_action): where work",
20780
- "comes back to a person, shared by the team. Check it first. Resolving an item clears it for everybody; a",
20781
- "setup task is marked done only by the person who did the setup.",
20782
- "",
20783
- "**Maintaining** (taiga_get_maintenance / taiga_list_findings / taiga_get_finding / taiga_ignore_finding /",
20784
- "taiga_fix_finding / taiga_get_policy_standing / taiga_fix_policy_violation): what the sweeps found and where",
20785
- "the product stands against its own policies. Sweeps run on their own; check when the last one ran before",
20786
- "trusting a quiet list. A finding is ignored with a reason or fixed; a fix raises an initiative at the end of",
20787
- "the queue. Nothing is marked fixed by hand: the next sweep or assessment decides.",
20788
- "",
20789
- "**Deployments and monitoring** (taiga_get_deployments / taiga_get_delivery_metrics / taiga_get_monitoring /",
20790
- "taiga_define_environment / taiga_update_environment): what runs where, how delivery is going, and how each",
20791
- "environment is doing. Taiga observes deployments and never performs them. An environment needs only its App",
20792
- "URL for monitoring to start.",
20793
- "",
20794
- "**Knowledge** (taiga_list_knowledge / taiga_get_knowledge / taiga_add_knowledge / taiga_retry_knowledge /",
20795
- "taiga_delete_knowledge): reference material agents search when work calls for it, at the organization, a",
20796
- "factory or a product. A rule that applies every time belongs in instructions instead.",
20797
- "",
20798
- "**Setting a product up** (taiga_create_factory / taiga_create_product / taiga_update_product /",
20799
- "taiga_list_linkable_repositories / taiga_link_repository / taiga_start_import / taiga_finish_discovery /",
20800
- "taiga_reopen_discovery / taiga_move_product): a product starts from scratch (Discovery is a conversation,",
20801
- "held in the web) or by importing a repository, chosen when it is created. An import links the repository,",
20802
- "starts the import and runs by itself; finishing Discovery locks the documents and opens the rest of the",
20803
- "product. Linking a repository and creating or moving factories are for owners and admins. GitHub is",
20804
- "connected once, in the web; members and deleting stay there too.",
20805
- "",
20806
- "**Product documents and autonomy** (taiga_update_product_document / taiga_publish_product_document /",
20807
- "taiga_regenerate_product_document / taiga_generate_product_documents / taiga_stop_generating_documents /",
20808
- "taiga_mark_product_document_reviewed /",
20809
- "taiga_set_product_autonomy / taiga_set_auto_merge): edit a product document in its draft and publish it;",
20810
- "documents built from it are marked outdated and regenerated, not rewritten. Set whether plans wait for a",
20811
- "review and pull requests for a person to merge. Organization-wide settings, members, roles and connections",
20812
- "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.",
20813
21297
  "",
20814
- "**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
20815
- "provenance fields you can read past: `source` says where the text originally came from (template:",
20816
- "Taiga's standard text; overlay-<id>: a jurisdiction or industry overlay; cloud-preset:<providers>:",
20817
- "rendered for the organization's cloud providers; any other tag names the derivation that wrote the",
20818
- "text; a hand edit keeps the source it had) and",
20819
- "`priority: core` marks the sections every organization keeps. Neither changes what the section",
20820
- "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.",
20821
21302
  "",
20822
- "Without taiga_get_product_context, use taiga_list_factories and taiga_list_products, then read the",
20823
- "factory instructions, the product's specification and architecture, and the initiative you are working",
20824
- "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.",
20825
21306
  "",
20826
- "The same documents are resources (taiga://documents/{type}, taiga://products/{productId}/documents/{type},",
20827
- "taiga://factories/{factoryId}/instructions, taiga://backlog/{itemId}) for clients that attach them, and three",
20828
- "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.",
20829
21309
  "",
20830
- "When you notice a policy or factory instructions are outdated, fix the section with",
20831
- "taiga_update_document or taiga_update_factory_document. That saves a draft nobody follows yet;",
20832
- "publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
20833
- "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."
20834
21312
  ].join("\n");
20835
21313
  //#endregion
20836
21314
  //#region src/server.ts