@taiga-ai/mcp-server 0.19.0-rc.452 → 0.20.0-dev.454

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
@@ -2284,7 +2284,8 @@ function withStop(text) {
2284
2284
  */
2285
2285
  const PRODUCT_WORDS = {
2286
2286
  backlogItemId: "initiativeId",
2287
- buildRunId: "runId"
2287
+ buildRunId: "runId",
2288
+ latestBuild: "latestRun"
2288
2289
  };
2289
2290
  /**
2290
2291
  * Maps whose keys are content, not API fields: a document's sections are keyed
@@ -2310,6 +2311,10 @@ function inProductWords(value) {
2310
2311
  });
2311
2312
  return out;
2312
2313
  }
2314
+ /** The API's 404 sentence in the product's words: "Backlog item not found" is an initiative not found. */
2315
+ function inProductWording(said) {
2316
+ return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found");
2317
+ }
2313
2318
  /** A successful tool result: one JSON text block the model reads. */
2314
2319
  function toolResult(value) {
2315
2320
  return { content: [{
@@ -2390,7 +2395,7 @@ async function runTool(handler) {
2390
2395
  if (error instanceof TaigaApiError) {
2391
2396
  const code = error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR";
2392
2397
  const said = error.apiMessage ?? `Taiga API answered ${error.status}.`;
2393
- return toolError(code, error.status === 404 ? withNotFoundHint(said) : said, error.details);
2398
+ return toolError(code, error.status === 404 ? withNotFoundHint(inProductWording(said)) : said, error.details);
2394
2399
  }
2395
2400
  if (isNetworkFailure(error)) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${networkFailureDetail(error)}. Check TAIGA_API_URL.`);
2396
2401
  throw error;
@@ -2581,7 +2586,26 @@ function budgetSections(doc, budget) {
2581
2586
  * The `sections` input of a document read: some sections by id, none for the
2582
2587
  * outline (every id with its size and no text), or omitted for the whole.
2583
2588
  */
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");
2589
+ const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().describe("Only these sections, by id, or a policy control id (SEC-5.1) for the sections that state it; [] for the outline (under outline: every section id and its size, no text); omit for the whole document");
2590
+ /** A policy control id as the standing cites it: SEC-5.1, SDLC-3.1, GDPR-2.3. */
2591
+ const CONTROL_ID = /^[A-Z][A-Z0-9]*-\d+(\.\d+)*$/;
2592
+ /**
2593
+ * The sections that state each control id asked for (PRO-2800): policy
2594
+ * standing cites SEC-5.1, and the section holding it has an id of its own
2595
+ * (secretsManagement) an agent could only guess. A control id is found in
2596
+ * the sections' text, matched as a whole id so SEC-5.1 is not SEC-5.10.
2597
+ */
2598
+ function controlSections(sections, ids) {
2599
+ const found = {};
2600
+ for (const id of ids) {
2601
+ if (!CONTROL_ID.test(id) || Object.hasOwn(sections, id)) continue;
2602
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2603
+ const pattern = new RegExp(`(^|[^A-Za-z0-9.-])${escaped}(?![A-Za-z0-9-]|\\.[A-Za-z0-9])`);
2604
+ const holders = Object.keys(sections).filter((sectionId) => pattern.test(sectionContent(sections, sectionId) ?? ""));
2605
+ if (holders.length > 0) found[id] = holders;
2606
+ }
2607
+ return found;
2608
+ }
2585
2609
  /**
2586
2610
  * The answer to a read that names sections: the ones the document has, the
2587
2611
  * rest listed with their sizes, and the asked-for ids it lacks under
@@ -2590,12 +2614,17 @@ const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().des
2590
2614
  * real id with its size, so the retry needs nothing else. No ids is the
2591
2615
  * outline.
2592
2616
  */
2593
- function sectionReadResult(doc, ids, label) {
2617
+ function sectionReadResult(doc, asked, label) {
2618
+ const before = sectionsOf(doc.data);
2619
+ const controls = before ? controlSections(before, asked) : {};
2620
+ const ids = [.../* @__PURE__ */ new Set([...asked.filter((id) => !Object.hasOwn(controls, id)), ...Object.values(controls).flat()])];
2594
2621
  const cut = pickSections(doc, new Set(ids));
2595
2622
  if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
2596
2623
  const all = Object.keys(sectionsOf(doc.data) ?? {});
2597
2624
  const unknownSections = ids.filter((id) => !all.includes(id));
2598
- if (ids.filter((id) => all.includes(id)).length === 0) {
2625
+ const known = ids.filter((id) => all.includes(id));
2626
+ const found = Object.keys(controls).length > 0 ? { controls } : {};
2627
+ if (known.length === 0) {
2599
2628
  const { data: _data, ...meta } = cut.doc;
2600
2629
  return toolResult({
2601
2630
  ...meta,
@@ -2605,6 +2634,7 @@ function sectionReadResult(doc, ids, label) {
2605
2634
  }
2606
2635
  return toolResult({
2607
2636
  ...cut.doc,
2637
+ ...found,
2608
2638
  omittedSections: cut.omittedSections,
2609
2639
  ...unknownSections.length > 0 ? { unknownSections } : {}
2610
2640
  });
@@ -2628,12 +2658,12 @@ function named(types) {
2628
2658
  return types.map((type) => PRODUCT_DOCUMENT_NAMES[type] ?? type).join(", ");
2629
2659
  }
2630
2660
  /** The product's setup readiness, or null once it has finished setup. */
2631
- async function setupReadiness(client, product) {
2661
+ async function setupReadiness(client, product, links) {
2632
2662
  if (product["status"] !== "draft") return null;
2633
2663
  const id = String(product["id"]);
2634
2664
  const imported = isImportedProduct(product);
2635
2665
  const [repos, blockers, full] = await Promise.all([
2636
- client.listProductRepositories(id).then((result) => result.data, (error) => {
2666
+ (links ? links() : client.listProductRepositories(id)).then((result) => result.data, (error) => {
2637
2667
  if (isApiSide(error)) return null;
2638
2668
  throw error;
2639
2669
  }),
@@ -2659,9 +2689,9 @@ async function setupReadiness(client, product) {
2659
2689
  return {
2660
2690
  ready: blockers ? missing.length === 0 && openChecks.length === 0 : null,
2661
2691
  repository,
2662
- ...importState ? { import: importState } : {},
2692
+ ...imported ? { import: typeof settings.importStatus === "string" ? settings.importStatus : "pending" } : {},
2663
2693
  ...imported ? {} : { discoveryStep: full["discoveryStep"] ?? null },
2664
- missingDocuments: missing ? missing.map((type) => named([type])) : null,
2694
+ missingDocuments: missing,
2665
2695
  openChecks,
2666
2696
  nextStep: nextStepFor({
2667
2697
  imported,
@@ -2787,6 +2817,41 @@ const PRODUCT_SUMMARY_FIELDS = [
2787
2817
  "factoryId",
2788
2818
  "discoveryStep"
2789
2819
  ];
2820
+ /**
2821
+ * Product rows with their factory's name beside its id (PRO-2800): "what
2822
+ * products does the organization have" needed a second list to name the
2823
+ * factories. One factories read; without factories:read, or when it fails,
2824
+ * the rows keep the id alone.
2825
+ */
2826
+ async function withFactoryNames(client, rows) {
2827
+ if (!client.hasCapability("factories:read") || rows.length === 0) return rows;
2828
+ try {
2829
+ const wanted = new Set(rows.map((row) => String(row.factoryId)));
2830
+ const names = /* @__PURE__ */ new Map();
2831
+ let cursor;
2832
+ for (let pageIndex = 0; pageIndex < FACTORY_PAGES; pageIndex += 1) {
2833
+ const result = await client.listFactories({
2834
+ limit: 100,
2835
+ cursor
2836
+ });
2837
+ for (const factory of result.data) names.set(factory.id, factory.name);
2838
+ if (!result.nextCursor || [...wanted].every((id) => names.has(id))) break;
2839
+ cursor = result.nextCursor;
2840
+ }
2841
+ return rows.map((row) => {
2842
+ const name = names.get(String(row.factoryId));
2843
+ return name === void 0 ? row : {
2844
+ ...row,
2845
+ factoryName: name
2846
+ };
2847
+ });
2848
+ } catch (error) {
2849
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return rows;
2850
+ throw error;
2851
+ }
2852
+ }
2853
+ /** Factory pages read to name products' factories. */
2854
+ const FACTORY_PAGES = 10;
2790
2855
  /** A list's name search: part of a name, any case. */
2791
2856
  const query = (what) => z.string().trim().min(1).max(200).optional().describe(`Part of the ${what}, any case: only the rows that contain it`);
2792
2857
  /**
@@ -2987,7 +3052,7 @@ function registerProductTools(server, client) {
2987
3052
  if (client.hasCapability("products:read")) {
2988
3053
  server.registerTool("taiga_list_products", {
2989
3054
  title: "List products",
2990
- description: `List products in the organization, as summaries (id, name, slug, status, factory, discoveryStep). Use this to find a product by name (query); inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full. ${DISCOVERY_STEP_MEANING}`,
3055
+ description: `List products in the organization, as summaries (id, name, slug, status, factoryId and factoryName, discoveryStep). Use this to find a product by name (query); inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full. ${DISCOVERY_STEP_MEANING}`,
2991
3056
  inputSchema: z.object({
2992
3057
  query: query("product name or slug"),
2993
3058
  factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it").optional().describe("Only the products of this factory"),
@@ -2999,7 +3064,7 @@ function registerProductTools(server, client) {
2999
3064
  const { query: text, ...filters } = params;
3000
3065
  if (text === void 0) {
3001
3066
  const result = await client.listProducts(filters);
3002
- return toolResult(page(pickFields(result.data, PRODUCT_SUMMARY_FIELDS), result.nextCursor));
3067
+ return toolResult(page(await withFactoryNames(client, pickFields(result.data, PRODUCT_SUMMARY_FIELDS)), result.nextCursor));
3003
3068
  }
3004
3069
  if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
3005
3070
  const found = await searchPages((cursor) => client.listProducts({
@@ -3009,13 +3074,13 @@ function registerProductTools(server, client) {
3009
3074
  cursor
3010
3075
  }), text, (row) => [row.name, row.slug], params.limit ?? 25);
3011
3076
  return toolResult({
3012
- ...page(pickFields(found.rows, PRODUCT_SUMMARY_FIELDS), null),
3077
+ ...page(await withFactoryNames(client, pickFields(found.rows, PRODUCT_SUMMARY_FIELDS)), null),
3013
3078
  ...found.more ? { more: SEARCH_NOTE } : {}
3014
3079
  });
3015
3080
  }));
3016
3081
  server.registerTool("taiga_get_product", {
3017
3082
  title: "Get product",
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.",
3083
+ description: "Read one product record and its settings, 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.",
3019
3084
  inputSchema: z.object({
3020
3085
  productId: productId.optional(),
3021
3086
  slug: slug.optional()
@@ -3409,163 +3474,6 @@ function requestPrompt(title, description) {
3409
3474
  return [(title ?? "").trim(), body].filter(Boolean).join("\n\n") || void 0;
3410
3475
  }
3411
3476
  //#endregion
3412
- //#region src/tools/queue.ts
3413
- /** How many entries the read names; past this an entry keeps its initiativeId only. */
3414
- const NAMED_ENTRIES = 25;
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.";
3416
- /**
3417
- * What lifts a halt, in the tools that do it. Read from the halt registry in
3418
- * @taiga/types (how each cause clears, and whether a person may lift it), so
3419
- * the advice cannot drift from what the API accepts; plan review is the one
3420
- * cause a tool answers directly.
3421
- */
3422
- function haltNextStep(cause) {
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).";
3424
- const spec = cause ? HALT_CAUSES[cause] : void 0;
3425
- if (!cause || !spec) return `Read the initiative with taiga_get_initiative to see why, ${MOVE_ASIDE}`;
3426
- const known = cause;
3427
- if (canResumeHalt(known)) return `taiga_resume_initiative lifts it, ${MOVE_ASIDE}`;
3428
- if (haltClearsItself(known)) return `It clears when ${spec.clearedWhen}; ${MOVE_ASIDE}`;
3429
- return `Only taking it off the line clears it (taiga_move_initiative): ${spec.clearedWhen}.`;
3430
- }
3431
- /**
3432
- * The entry the line waits on, by the queue's two kinds of blocker
3433
- * (@taiga/types): an active entry holds every other one wherever it sits, so
3434
- * it answers first; without one, the first halted entry holds the entries
3435
- * behind it. -1 when neither is there.
3436
- */
3437
- function blockingIndex(entries, stateOf) {
3438
- const active = entries.findIndex((entry) => stateOf(entry) === "active");
3439
- return active >= 0 ? active : entries.findIndex((entry) => stateOf(entry) === "halted");
3440
- }
3441
- /** The one sentence a person needs: what the line is doing, or what it is waiting on. */
3442
- function waitingOn(rows) {
3443
- const at = blockingIndex(rows, (row) => row.entryState);
3444
- if (at < 0) return rows.length > 0 ? "Nothing in the queue is running; the first queued initiative starts when the line is free." : null;
3445
- const head = rows[at];
3446
- const name = typeof head.title === "string" ? `"${head.title}"` : `Initiative ${String(head.backlogItemId)}`;
3447
- if (head.entryState === "halted") {
3448
- const halt = head.halt;
3449
- const ahead = at > 0 ? " Entries ahead of it are not held by it." : "";
3450
- return `${name} halted the queue, holding the entries behind it: ${halt.reason} ${halt.nextStep}${ahead}`;
3451
- }
3452
- const lifecycle = head.lifecycle;
3453
- const held = "every other queued initiative waits until its pull request merges.";
3454
- if (!lifecycle) return `${name} is the active entry; ${held}`;
3455
- const pr = typeof head.prUrl === "string" ? ` (${head.prUrl})` : "";
3456
- if (lifecycle.cta === "start_build") return `${name} has its plan and waits for the Build slot; ${held}`;
3457
- if (lifecycle.turn === "you") return `${name} is waiting on a person (${String(lifecycle.cta ?? lifecycle.stage)})${pr}; ${held}`;
3458
- return `${name} is ${String(lifecycle.stage)}${pr}; ${held}`;
3459
- }
3460
- /**
3461
- * The product's queue as a person reads it: each live entry in order with its
3462
- * initiative's title and where that initiative is, a halted entry with why and
3463
- * what lifts it, and one sentence saying what the line is waiting on. The
3464
- * route carries item ids only; each is named by one item read (when the key
3465
- * may read the backlog), and a read the API fails leaves that entry unnamed
3466
- * rather than failing the queue.
3467
- */
3468
- async function queueView(client, product, options = { name: "all" }) {
3469
- const [queue, paused] = await Promise.all([client.listQueue(product), linePausedSince(client, product)]);
3470
- const entries = queue.data.filter((entry) => entry.state !== "settled");
3471
- const canName = client.hasCapability("backlog:read");
3472
- const blocking = blockingIndex(entries, (entry) => entry.state);
3473
- const toName = !canName ? [] : options.name === "all" ? entries.slice(0, NAMED_ENTRIES).map((_, index) => index) : blocking >= 0 ? [blocking] : [];
3474
- const settled = await Promise.allSettled(toName.map((index) => client.getBacklogItem(entries[index].backlogItemId)));
3475
- const reads = new Map(toName.map((index, at) => [index, settled[at]]));
3476
- let unnamed = options.name === "all" ? entries.length - toName.length : 0;
3477
- const rows = entries.map((entry, index) => {
3478
- const read = reads.get(index);
3479
- if (read?.status === "rejected") {
3480
- if (!(read.reason instanceof TaigaApiError || isNetworkFailure(read.reason))) throw read.reason;
3481
- unnamed += 1;
3482
- }
3483
- const item = read?.status === "fulfilled" ? read.value.data : null;
3484
- const lifecycle = item?.lifecycle;
3485
- const latestBuild = lifecycle?.latestBuild;
3486
- const row = {
3487
- position: index + 1,
3488
- backlogItemId: entry.backlogItemId,
3489
- entryState: entry.state
3490
- };
3491
- if (item) Object.assign(row, {
3492
- title: item.title,
3493
- state: item.state,
3494
- planningStatus: item.planningStatus,
3495
- needsHumanCause: item.needsHumanCause,
3496
- criteria: criteriaCounts(item),
3497
- lifecycle: lifecycle ? {
3498
- turn: lifecycle.turn,
3499
- stage: lifecycle.stage,
3500
- cta: lifecycle.cta
3501
- } : null,
3502
- ...latestBuild ? {
3503
- runId: latestBuild.id,
3504
- prUrl: latestBuild.prUrl ?? null
3505
- } : {}
3506
- });
3507
- if (entry.state === "halted") {
3508
- const cause = typeof entry.haltCause === "string" ? entry.haltCause : null;
3509
- row.halt = {
3510
- cause,
3511
- reason: (cause ? renderHalt({
3512
- cause,
3513
- params: entry.haltParams ?? {}
3514
- }) : "") || (typeof entry.haltReason === "string" ? entry.haltReason : "The queue stopped here."),
3515
- nextStep: haltNextStep(cause)
3516
- };
3517
- }
3518
- return row;
3519
- });
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.";
3521
- const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
3522
- return {
3523
- linePaused: paused === null ? null : typeof paused === "string",
3524
- entries: rows,
3525
- waitingOn: line ?? waitingOn(rows),
3526
- ...note ? { note } : {}
3527
- };
3528
- }
3529
- /**
3530
- * When the product's line was paused, false when it runs, null when the
3531
- * product cannot be read (the queue is still worth reading without it).
3532
- */
3533
- async function linePausedSince(client, product) {
3534
- try {
3535
- const row = (await client.getProduct(product)).data;
3536
- return typeof row.lineStoppedAt === "string" ? row.lineStoppedAt : false;
3537
- } catch (error) {
3538
- if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
3539
- throw error;
3540
- }
3541
- }
3542
- function registerQueueTools(server, client) {
3543
- if (!client.hasCapability("products:read")) return;
3544
- server.registerTool("taiga_get_line", {
3545
- title: "Get the product queue",
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.",
3547
- inputSchema: z.object({ productId }),
3548
- annotations: READ_ONLY
3549
- }, (params) => runTool(async () => toolResult(await queueView(client, params.productId))));
3550
- if (!client.hasCapability("products:write")) return;
3551
- server.registerTool("taiga_pause_line", {
3552
- title: "Pause or resume a product's line",
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.",
3554
- inputSchema: z.object({
3555
- productId,
3556
- paused: z.boolean().describe("true pauses the line, false resumes it")
3557
- }),
3558
- annotations: OPERATE_SETTING
3559
- }, (params) => runTool(async () => {
3560
- const row = (await client.setLineStopped(params.productId, params.paused)).data;
3561
- return toolResult({
3562
- productId: row.id,
3563
- linePaused: typeof row.lineStoppedAt === "string",
3564
- lineStoppedAt: row.lineStoppedAt ?? null
3565
- });
3566
- }));
3567
- }
3568
- //#endregion
3569
3477
  //#region src/tools/builds.ts
3570
3478
  /** What a list row needs to say for the model to pick a run to read in full. */
3571
3479
  const BUILD_SUMMARY_FIELDS = [
@@ -3588,6 +3496,8 @@ const BUILD_SUMMARY_FIELDS = [
3588
3496
  "prState",
3589
3497
  "prDraft",
3590
3498
  "prMerged",
3499
+ "reviewEscalation",
3500
+ "error",
3591
3501
  "actions",
3592
3502
  "createdAt",
3593
3503
  "startedAt",
@@ -3604,7 +3514,10 @@ const RUN_OUTCOME_FIELDS = [
3604
3514
  "totalSteps",
3605
3515
  "prNumber",
3606
3516
  "prUrl",
3517
+ "prDraft",
3607
3518
  "prMerged",
3519
+ "reviewEscalation",
3520
+ "error",
3608
3521
  "actions"
3609
3522
  ];
3610
3523
  /**
@@ -3642,6 +3555,46 @@ const RUNNING = {
3642
3555
  ci_fixing: "fixing what CI found",
3643
3556
  reviewing: "answering review comments on the pull request"
3644
3557
  };
3558
+ /** Why the agent handed a run back, in the web's words (apps/web handback), with the checks it names. */
3559
+ function handbackSaid(escalation) {
3560
+ if (!escalation || typeof escalation !== "object") return null;
3561
+ const e = escalation;
3562
+ const checks = Array.isArray(e.failingChecks) ? e.failingChecks.map((check) => String(check.name)).join(", ") : "";
3563
+ const on = checks ? ` on ${checks}` : "";
3564
+ const rounds = typeof e.rounds === "number" && e.rounds > 0 ? ` after ${e.rounds} automatic round${e.rounds === 1 ? "" : "s"}` : "";
3565
+ const blockers = Array.isArray(e.blockers) ? e.blockers.map((b) => READY_BLOCKER_SAID[String(b)] ?? String(b)) : [];
3566
+ switch (e.reason) {
3567
+ case "ci_fix_cap": return `CI is still failing${on}${rounds}`;
3568
+ case "ci_fix_stalled": return `CI is still failing${on}, and the last fix round changed nothing`;
3569
+ case "conflict_cap": return `the branch still conflicts with its base${rounds}`;
3570
+ case "review_bot_cap": return `review bots still have open comments${rounds}`;
3571
+ case "ci_never_settled": return "CI did not finish in time";
3572
+ case "ready_blocked": return `CI is green, but Taiga kept the pull request a draft${blockers.length ? `: ${blockers.join("; ")}` : ""}`;
3573
+ case "check_weakened": return `CI went green only after a check was loosened${typeof e.file === "string" ? ` in ${e.file}` : ""}`;
3574
+ default: return typeof e.reason === "string" ? e.reason : null;
3575
+ }
3576
+ }
3577
+ /** What a person does about a handback, by its reason, as the web's handback detail says it. */
3578
+ function handbackNextStep(escalation) {
3579
+ const reason = escalation && typeof escalation === "object" ? escalation.reason : null;
3580
+ if (reason === "check_weakened") return "review the loosened check before merging, or review it back to the agent";
3581
+ if (reason === "ci_never_settled") return "look at what is still running or queued in CI, then review it back to the agent";
3582
+ return "fix it by hand or review it back to the agent";
3583
+ }
3584
+ /** The web's words for why Taiga keeps a pull request a draft. */
3585
+ const READY_BLOCKER_SAID = {
3586
+ plan_incomplete: "not every planned step shipped",
3587
+ tests_failed: "the tests Taiga ran after the build failed",
3588
+ bar_lowering: "a fix lowered a check instead of fixing the code",
3589
+ bar_lowering_unchecked: "Taiga could not check whether a fix lowered a check",
3590
+ criticals_unresolved: "Taiga's own review left a critical issue unresolved"
3591
+ };
3592
+ /** The first sentence of a run's error, short enough for a row. */
3593
+ function errorSaid(error) {
3594
+ if (typeof error !== "string" || !error.trim()) return null;
3595
+ const first = error.trim().split(/\n|(?<=[.!?])\s/)[0].replace(/[.!?]$/, "");
3596
+ return first.length > 240 ? `${first.slice(0, 240).trimEnd()}...` : first;
3597
+ }
3645
3598
  /**
3646
3599
  * How a run stands, in one sentence, with its disposition: the meaning the
3647
3600
  * status and the pull request facts carry between them
@@ -3655,34 +3608,41 @@ function runOutcome(run) {
3655
3608
  const disposition = landedDisposition(status, run["prMerged"] === true);
3656
3609
  const pr = typeof run["prNumber"] === "number" ? `PR #${run["prNumber"]}` : "its pull request";
3657
3610
  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"]}` : "";
3611
+ const why = errorSaid(run["error"]) ?? (typeof run["failedReason"] === "string" ? run["failedReason"] : null);
3612
+ const handback = handbackSaid(run["reviewEscalation"]);
3613
+ const draft = run["prDraft"] === true ? `, still a draft` : "";
3659
3614
  return {
3660
3615
  disposition,
3661
3616
  outcome: {
3662
3617
  in_flight: status === "pending" ? "Queued: waiting to start." : `Running: ${RUNNING[status] ?? status}${progress}.`,
3663
- gated: `Waiting for a person to review and merge ${pr}.`,
3618
+ gated: handback ? `Handed back to a person: ${handback}. ${pr}${draft ? `${draft},` : ""} waits for them to ${handbackNextStep(run["reviewEscalation"])}.` : `Waiting for a person to review and merge ${pr}${draft}.`,
3664
3619
  succeeded: `Merged: ${pr} is in ${typeof run["baseBranch"] === "string" ? run["baseBranch"] : "the base branch"}.`,
3665
3620
  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}.`,
3621
+ failed: `Failed${why ? `: ${why}` : ""}${progress}.`,
3667
3622
  stopped: `Stopped before it finished${progress}.`,
3668
- rejected: `${pr} was closed without merging, so the work did not land.`
3623
+ rejected: `${pr} was closed without merging, so the work did not land.${handback ? ` It had been handed back: ${handback}.` : ""}`
3669
3624
  }[disposition] ?? status
3670
3625
  };
3671
3626
  }
3672
3627
  /**
3673
3628
  * A run row as an agent reads it: its outcome added, and prDraft only where
3674
3629
  * 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).
3630
+ * opened one said it had a draft (PRO-2800). A list row leaves the full error
3631
+ * text out: its first sentence is in the outcome, and taiga_get_run has it all.
3676
3632
  */
3677
- function readableRun(run) {
3678
- const { prDraft, ...rest } = run;
3633
+ function readableRun(run, opts = {}) {
3634
+ const { prDraft, error, reviewEscalation, ...rest } = run;
3679
3635
  const said = runOutcome(run);
3680
3636
  return {
3681
3637
  ...rest,
3638
+ ...opts.row || error === void 0 ? {} : { error },
3639
+ ...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
3682
3640
  ...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
3683
3641
  ...said ?? {}
3684
3642
  };
3685
3643
  }
3644
+ /** readableRun for a list row. */
3645
+ const readableRow = (run) => readableRun(run, { row: true });
3686
3646
  /** How many products the org-wide list names; past this a row keeps its productId only. */
3687
3647
  const NAMED_PRODUCTS = 100;
3688
3648
  const runId = idInput("run", "taiga_list_runs gives it");
@@ -3726,7 +3686,7 @@ async function activityRows(client, opts) {
3726
3686
  name: product.name,
3727
3687
  slug: product.slug
3728
3688
  });
3729
- const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map(readableRun).map((row) => {
3689
+ const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map(readableRow).map((row) => {
3730
3690
  const product = nameById.get(String(row["productId"]));
3731
3691
  return product ? {
3732
3692
  ...row,
@@ -3760,13 +3720,13 @@ function registerBuildTools(server, client) {
3760
3720
  limit: params.limit,
3761
3721
  cursor: params.cursor
3762
3722
  });
3763
- return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS).map(readableRun), result.nextCursor));
3723
+ return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS).map(readableRow), result.nextCursor));
3764
3724
  }
3765
3725
  return toolResult(await activityRows(client, params));
3766
3726
  }));
3767
3727
  server.registerTool("taiga_get_run", {
3768
3728
  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.",
3729
+ description: "Read 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
3730
  inputSchema: z.object({
3771
3731
  runId,
3772
3732
  full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
@@ -3799,7 +3759,170 @@ function registerBuildTools(server, client) {
3799
3759
  inputSchema: z.object({ runId }),
3800
3760
  annotations: OPERATE
3801
3761
  }, (params) => runTool(async () => {
3802
- return toolResult(readableRun(pickFields([(await client.stopBuildRun(params.runId)).data], RUN_OUTCOME_FIELDS)[0]));
3762
+ const result = await client.stopBuildRun(params.runId);
3763
+ return toolResult(readableRow(pickFields([result.data], RUN_OUTCOME_FIELDS)[0]));
3764
+ }));
3765
+ }
3766
+ //#endregion
3767
+ //#region src/tools/queue.ts
3768
+ /** How many entries the read names; past this an entry keeps its initiativeId only. */
3769
+ const NAMED_ENTRIES = 25;
3770
+ const MOVE_ASIDE = "or move it down (taiga_queue_initiative with afterId) or off the line (taiga_move_initiative) to let the rest pass.";
3771
+ /**
3772
+ * What lifts a halt, in the tools that do it. Read from the halt registry in
3773
+ * @taiga/types (how each cause clears, and whether a person may lift it), so
3774
+ * the advice cannot drift from what the API accepts; plan review is the one
3775
+ * cause a tool answers directly.
3776
+ */
3777
+ function haltNextStep(cause) {
3778
+ 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).";
3779
+ const spec = cause ? HALT_CAUSES[cause] : void 0;
3780
+ if (!cause || !spec) return `Read the initiative with taiga_get_initiative to see why, ${MOVE_ASIDE}`;
3781
+ const known = cause;
3782
+ if (canResumeHalt(known)) return `taiga_resume_initiative lifts it, ${MOVE_ASIDE}`;
3783
+ if (haltClearsItself(known)) return `It clears when ${spec.clearedWhen}; ${MOVE_ASIDE}`;
3784
+ return `Only taking it off the line clears it (taiga_move_initiative): ${spec.clearedWhen}.`;
3785
+ }
3786
+ /**
3787
+ * The entry the line waits on, by the queue's two kinds of blocker
3788
+ * (@taiga/types): an active entry holds every other one wherever it sits, so
3789
+ * it answers first; without one, the first halted entry holds the entries
3790
+ * behind it. -1 when neither is there.
3791
+ */
3792
+ function blockingIndex(entries, stateOf) {
3793
+ const active = entries.findIndex((entry) => stateOf(entry) === "active");
3794
+ return active >= 0 ? active : entries.findIndex((entry) => stateOf(entry) === "halted");
3795
+ }
3796
+ /** The one sentence a person needs: what the line is doing, or what it is waiting on. */
3797
+ function waitingOn(rows) {
3798
+ const at = blockingIndex(rows, (row) => row.entryState);
3799
+ if (at < 0) return rows.length > 0 ? "Nothing in the queue is running; the first queued initiative starts when the line is free." : null;
3800
+ const head = rows[at];
3801
+ const name = typeof head.title === "string" ? `"${head.title}"` : `Initiative ${String(head.backlogItemId)}`;
3802
+ if (head.entryState === "halted") {
3803
+ const halt = head.halt;
3804
+ const ahead = at > 0 ? " Entries ahead of it are not held by it." : "";
3805
+ return `${name} halted the queue, holding the entries behind it: ${halt.reason} ${halt.nextStep}${ahead}`;
3806
+ }
3807
+ const lifecycle = head.lifecycle;
3808
+ const held = "every other queued initiative waits until its pull request merges.";
3809
+ if (!lifecycle) return `${name} is the active entry; ${held}`;
3810
+ const pr = typeof head.prUrl === "string" ? ` (${head.prUrl})` : "";
3811
+ if (lifecycle.cta === "start_build") return `${name} has its plan and waits for the Build slot; ${held}`;
3812
+ if (head.handedBack) return `${name} was handed back to a person: ${head.handedBack}${pr}; ${held}`;
3813
+ if (lifecycle.turn === "you") return `${name} is waiting on a person (${String(lifecycle.cta ?? lifecycle.stage)})${pr}; ${held}`;
3814
+ return `${name} is ${String(lifecycle.stage)}${pr}; ${held}`;
3815
+ }
3816
+ /**
3817
+ * The product's queue as a person reads it: each live entry in order with its
3818
+ * initiative's title and where that initiative is, a halted entry with why and
3819
+ * what lifts it, and one sentence saying what the line is waiting on. The
3820
+ * route carries item ids only; each is named by one item read (when the key
3821
+ * may read the backlog), and a read the API fails leaves that entry unnamed
3822
+ * rather than failing the queue.
3823
+ */
3824
+ async function queueView(client, product, options = { name: "all" }) {
3825
+ const [queue, paused] = await Promise.all([client.listQueue(product), linePausedSince(client, product)]);
3826
+ const entries = queue.data.filter((entry) => entry.state !== "settled");
3827
+ const canName = client.hasCapability("backlog:read");
3828
+ const blocking = blockingIndex(entries, (entry) => entry.state);
3829
+ const toName = !canName ? [] : options.name === "all" ? entries.slice(0, NAMED_ENTRIES).map((_, index) => index) : blocking >= 0 ? [blocking] : [];
3830
+ const settled = await Promise.allSettled(toName.map((index) => client.getBacklogItem(entries[index].backlogItemId)));
3831
+ const reads = new Map(toName.map((index, at) => [index, settled[at]]));
3832
+ let unnamed = options.name === "all" ? entries.length - toName.length : 0;
3833
+ const rows = entries.map((entry, index) => {
3834
+ const read = reads.get(index);
3835
+ if (read?.status === "rejected") {
3836
+ if (!(read.reason instanceof TaigaApiError || isNetworkFailure(read.reason))) throw read.reason;
3837
+ unnamed += 1;
3838
+ }
3839
+ const item = read?.status === "fulfilled" ? read.value.data : null;
3840
+ const lifecycle = item?.lifecycle;
3841
+ const latestBuild = lifecycle?.latestBuild;
3842
+ const row = {
3843
+ position: index + 1,
3844
+ backlogItemId: entry.backlogItemId,
3845
+ entryState: entry.state
3846
+ };
3847
+ if (item) {
3848
+ Object.assign(row, {
3849
+ title: item.title,
3850
+ state: item.state,
3851
+ planningStatus: item.planningStatus,
3852
+ needsHumanCause: item.needsHumanCause,
3853
+ criteria: criteriaCounts(item),
3854
+ lifecycle: lifecycle ? {
3855
+ turn: lifecycle.turn,
3856
+ stage: lifecycle.stage,
3857
+ cta: lifecycle.cta
3858
+ } : null,
3859
+ ...latestBuild ? {
3860
+ runId: latestBuild.id,
3861
+ prUrl: latestBuild.prUrl ?? null
3862
+ } : {}
3863
+ });
3864
+ const handback = handbackSaid(latestBuild?.reviewEscalation);
3865
+ if (handback) row.handedBack = handback;
3866
+ }
3867
+ if (entry.state === "halted") {
3868
+ const cause = typeof entry.haltCause === "string" ? entry.haltCause : null;
3869
+ row.halt = {
3870
+ cause,
3871
+ reason: (cause ? renderHalt({
3872
+ cause,
3873
+ params: entry.haltParams ?? {}
3874
+ }) : "") || (typeof entry.haltReason === "string" ? entry.haltReason : "The queue stopped here."),
3875
+ nextStep: haltNextStep(cause)
3876
+ };
3877
+ }
3878
+ return row;
3879
+ });
3880
+ 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.";
3881
+ const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
3882
+ return {
3883
+ linePaused: paused === null ? null : typeof paused === "string",
3884
+ entries: rows,
3885
+ waitingOn: line ?? waitingOn(rows),
3886
+ ...note ? { note } : {}
3887
+ };
3888
+ }
3889
+ /**
3890
+ * When the product's line was paused, false when it runs, null when the
3891
+ * product cannot be read (the queue is still worth reading without it).
3892
+ */
3893
+ async function linePausedSince(client, product) {
3894
+ try {
3895
+ const row = (await client.getProduct(product)).data;
3896
+ return typeof row.lineStoppedAt === "string" ? row.lineStoppedAt : false;
3897
+ } catch (error) {
3898
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
3899
+ throw error;
3900
+ }
3901
+ }
3902
+ function registerQueueTools(server, client) {
3903
+ if (!client.hasCapability("products:read")) return;
3904
+ server.registerTool("taiga_get_line", {
3905
+ title: "Get the product queue",
3906
+ description: "Read a product's line, the queue the coding agent works one initiative at a time, and what it waits on: 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.",
3907
+ inputSchema: z.object({ productId }),
3908
+ annotations: READ_ONLY
3909
+ }, (params) => runTool(async () => toolResult(await queueView(client, params.productId))));
3910
+ if (!client.hasCapability("products:write")) return;
3911
+ server.registerTool("taiga_pause_line", {
3912
+ title: "Pause or resume a product's line",
3913
+ 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.",
3914
+ inputSchema: z.object({
3915
+ productId,
3916
+ paused: z.boolean().describe("true pauses the line, false resumes it")
3917
+ }),
3918
+ annotations: OPERATE_SETTING
3919
+ }, (params) => runTool(async () => {
3920
+ const row = (await client.setLineStopped(params.productId, params.paused)).data;
3921
+ return toolResult({
3922
+ productId: row.id,
3923
+ linePaused: typeof row.lineStoppedAt === "string",
3924
+ lineStoppedAt: row.lineStoppedAt ?? null
3925
+ });
3803
3926
  }));
3804
3927
  }
3805
3928
  //#endregion
@@ -3886,6 +4009,8 @@ function situationOf(item) {
3886
4009
  const pr = typeof build.prNumber === "number" ? ` (PR #${build.prNumber})` : "";
3887
4010
  const said = BUILD_RUN_STATUSES.includes(build.status) ? RUN_STANDS[landedDisposition(build.status, build.prMerged === true)] : build.status;
3888
4011
  stage += `; its latest run ${said}${pr}`;
4012
+ const handback = handbackSaid(build.reviewEscalation);
4013
+ if (handback) stage += `, handed back because ${handback}`;
3889
4014
  }
3890
4015
  parts.push(`${stage}.`);
3891
4016
  if (typeof item["needsHumanCause"] === "string") {
@@ -3896,7 +4021,8 @@ function situationOf(item) {
3896
4021
  if (why) parts.push(`The line stopped on it: ${why}.`);
3897
4022
  }
3898
4023
  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;
4024
+ const handedBack = handbackSaid(build?.reviewEscalation);
4025
+ const move = life.cta === "review_pr" && handedBack ? `a person should ${handbackNextStep(build.reviewEscalation)}` : typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
3900
4026
  if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
3901
4027
  else if (turn && move) parts.push(`${turn}: ${move}.`);
3902
4028
  else if (turn) parts.push(`${turn}.`);
@@ -4018,7 +4144,7 @@ function registerBacklogTools(server, client) {
4018
4144
  function registerBacklogReads(server, client) {
4019
4145
  server.registerTool("taiga_list_initiatives", {
4020
4146
  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.",
4147
+ 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.",
4022
4148
  inputSchema: z.object({
4023
4149
  productId,
4024
4150
  query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
@@ -4119,7 +4245,7 @@ function registerRequest(server, client) {
4119
4245
  function registerMove(server, client) {
4120
4246
  server.registerTool("taiga_move_initiative", {
4121
4247
  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.`,
4248
+ description: `Move an initiative on the board, take it off the line, 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.`,
4123
4249
  inputSchema: z.object({
4124
4250
  initiativeId,
4125
4251
  state: z.enum(USER_DROP_STATES).describe("The column to move into"),
@@ -4242,7 +4368,7 @@ function registerReviewGate(server, client) {
4242
4368
  function registerResume(server, client) {
4243
4369
  server.registerTool("taiga_resume_initiative", {
4244
4370
  title: "Resume initiative",
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.",
4371
+ description: "Carry on 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.",
4246
4372
  inputSchema: z.object({ initiativeId }),
4247
4373
  annotations: OPERATE
4248
4374
  }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
@@ -4341,6 +4467,8 @@ function standingSummary(standing) {
4341
4467
  }
4342
4468
  return {
4343
4469
  controls: listed.length,
4470
+ controlsInPolicies: controls.length,
4471
+ controlsWithNothingRecorded: controls.length - listed.length,
4344
4472
  violations: violations.length,
4345
4473
  bySeverity,
4346
4474
  resolvedViolations: resolved
@@ -4385,7 +4513,7 @@ function registerMaintenanceTools(server, client) {
4385
4513
  }));
4386
4514
  server.registerTool("taiga_get_finding", {
4387
4515
  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.",
4516
+ description: "Read one maintenance 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
4517
  inputSchema: z.object({ findingId }),
4390
4518
  annotations: READ_ONLY
4391
4519
  }, (params) => runTool(async () => {
@@ -4397,7 +4525,7 @@ function registerMaintenanceTools(server, client) {
4397
4525
  }));
4398
4526
  server.registerTool("taiga_get_policy_standing", {
4399
4527
  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.",
4528
+ description: "How 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; summary.controlsWithNothingRecorded counts them. Pass full: true for every violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
4401
4529
  inputSchema: z.object({
4402
4530
  productId,
4403
4531
  full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
@@ -4409,12 +4537,15 @@ function registerMaintenanceTools(server, client) {
4409
4537
  const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
4410
4538
  const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
4411
4539
  const summary = standingSummary(standing);
4540
+ const note = standing.assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first)." : null;
4412
4541
  if (params.full) return toolResult({
4413
4542
  summary,
4543
+ ...note ? { note } : {},
4414
4544
  ...standing
4415
4545
  });
4416
4546
  return toolResult({
4417
4547
  summary,
4548
+ ...note ? { note } : {},
4418
4549
  productId: standing.productId,
4419
4550
  hasPolicies: standing.hasPolicies,
4420
4551
  assessment: standing.assessment,
@@ -4435,7 +4566,7 @@ function registerMaintenanceTools(server, client) {
4435
4566
  if (!client.hasCapability("products:write")) return;
4436
4567
  server.registerTool("taiga_ignore_finding", {
4437
4568
  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.",
4569
+ description: "Ignore a maintenance finding, with a reason, or reopen it. Ignore one 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
4570
  inputSchema: z.object({
4440
4571
  findingId,
4441
4572
  ignored: z.boolean().describe("true ignores the finding, false reopens it"),
@@ -4457,7 +4588,7 @@ function registerMaintenanceTools(server, client) {
4457
4588
  }));
4458
4589
  server.registerTool("taiga_fix_finding", {
4459
4590
  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.",
4591
+ description: "Ask Taiga to fix a maintenance finding: it 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
4592
  inputSchema: z.object({
4462
4593
  productId,
4463
4594
  findingId,
@@ -4483,7 +4614,7 @@ function registerMaintenanceTools(server, client) {
4483
4614
  }));
4484
4615
  server.registerTool("taiga_fix_policy_violation", {
4485
4616
  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.",
4617
+ description: "Ask Taiga to fix a product's policy violations (it raises initiatives): 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
4618
  inputSchema: z.object({
4488
4619
  productId,
4489
4620
  controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
@@ -4533,6 +4664,9 @@ const CONTEXT_BUILD_FIELDS = [
4533
4664
  "prUrl",
4534
4665
  "prState",
4535
4666
  "prMerged",
4667
+ "prDraft",
4668
+ "reviewEscalation",
4669
+ "error",
4536
4670
  "createdAt"
4537
4671
  ];
4538
4672
  /**
@@ -4595,12 +4729,12 @@ const PARTS = {
4595
4729
  factory: "factories:read",
4596
4730
  documents: "products:read",
4597
4731
  policies: "tenant:documents:read",
4598
- backlog: "backlog:read",
4732
+ initiatives: "backlog:read",
4599
4733
  repositories: "products:read",
4600
- builds: "products:read",
4601
- attention: "products:read",
4734
+ runs: "products:read",
4735
+ actionRequired: "products:read",
4602
4736
  maintenance: "products:read",
4603
- standing: "products:read"
4737
+ policyStanding: "products:read"
4604
4738
  };
4605
4739
  /** What the API said when a part failed, in the words the bundle reports. */
4606
4740
  function unavailableReason(reason) {
@@ -4765,8 +4899,8 @@ async function backlogPart(client, productId) {
4765
4899
  * The linked repositories. The route serves one page of 100 and takes no
4766
4900
  * cursor, so past that the bundle says the list is cut rather than complete.
4767
4901
  */
4768
- async function repositoriesPart(client, productId) {
4769
- const result = await client.listProductRepositories(productId);
4902
+ async function repositoriesPart(links) {
4903
+ const result = await links();
4770
4904
  return {
4771
4905
  items: pickFields(result.data, REPOSITORY_SUMMARY_FIELDS),
4772
4906
  truncated: result.nextCursor !== null
@@ -4774,7 +4908,7 @@ async function repositoriesPart(client, productId) {
4774
4908
  }
4775
4909
  async function buildsPart(client, productId) {
4776
4910
  const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
4777
- return pickFields(rows, CONTEXT_BUILD_FIELDS).map(readableRun);
4911
+ return pickFields(rows, CONTEXT_BUILD_FIELDS).map(readableRow);
4778
4912
  }
4779
4913
  /**
4780
4914
  * What waits on a person for this product, counted by kind (PRO-2800): the
@@ -4815,30 +4949,35 @@ async function maintenancePart(client, productId) {
4815
4949
  }
4816
4950
  /** Where the product stands against the policies, as counts; taiga_get_policy_standing has each violation. */
4817
4951
  async function standingPart(client, productId) {
4818
- const summary = standingSummary((await client.getPolicyStanding(productId)).data);
4952
+ const standing = (await client.getPolicyStanding(productId)).data;
4953
+ const summary = standingSummary(standing);
4954
+ const state = standing.assessment?.state;
4819
4955
  return {
4820
4956
  openViolations: summary.violations,
4821
4957
  bySeverity: summary.bySeverity,
4822
- controlsViolated: summary.controls
4958
+ controlsViolated: summary.controls,
4959
+ ...state === "never" || state === "failed" ? { note: `The assessment ${state === "never" ? "has never run" : "failed"}: no violations here is not an all-clear.` } : {}
4823
4960
  };
4824
4961
  }
4825
4962
  async function productContext(client, product, options) {
4826
4963
  const productId = product.id;
4827
4964
  const granted = (part) => client.hasCapability(PARTS[part]);
4828
4965
  const notGranted = Object.keys(PARTS).filter((part) => !granted(part));
4966
+ let linksRead;
4967
+ const links = () => linksRead ??= client.listProductRepositories(productId);
4829
4968
  const fetchers = {
4830
4969
  factory: () => factoryPart(client, String(product.factoryId)),
4831
4970
  documents: () => documentsPart(client, product, options.documents),
4832
4971
  policies: () => policiesPart(client),
4833
- backlog: () => backlogPart(client, productId),
4834
- repositories: () => repositoriesPart(client, productId),
4835
- builds: () => buildsPart(client, productId),
4836
- attention: () => attentionPart(client, productId),
4972
+ initiatives: () => backlogPart(client, productId),
4973
+ repositories: () => repositoriesPart(links),
4974
+ runs: () => buildsPart(client, productId),
4975
+ actionRequired: () => attentionPart(client, productId),
4837
4976
  maintenance: () => maintenancePart(client, productId),
4838
- standing: () => standingPart(client, productId)
4977
+ policyStanding: () => standingPart(client, productId)
4839
4978
  };
4840
4979
  const parts = Object.keys(fetchers).filter(granted);
4841
- const [settled, readiness] = await Promise.all([Promise.allSettled(parts.map((part) => fetchers[part]())), setupReadiness(client, product)]);
4980
+ const [settled, readiness] = await Promise.all([Promise.allSettled(parts.map((part) => fetchers[part]())), setupReadiness(client, product, links)]);
4842
4981
  const bundle = { product: {
4843
4982
  ...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
4844
4983
  projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
@@ -4875,7 +5014,7 @@ function registerContextTools(server, client) {
4875
5014
  if (!client.hasCapability("products:read")) return;
4876
5015
  server.registerTool("taiga_get_product_context", {
4877
5016
  title: "Get product context",
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.`,
5017
+ description: `Where a product stands, in one call: the first read for any question about a product. Name it by ID, slug or the git remote URL of a checkout. The parts: product (and, while in setup, readiness: what it still needs and the next move); factory, with its instructions in full; documents, the specification and architecture as outlines (section ids and sizes); policies, each published policy with its section ids; initiatives, the open ones (taiga_list_initiatives has the done and canceled); repositories; runs, the latest ten, each with its outcome in a sentence; actionRequired, what waits on a person, counted by kind; maintenance, open findings by severity and the last sweep; policyStanding, 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. taiga_get_product is the product record and its settings alone.`,
4879
5018
  inputSchema: z.object({
4880
5019
  productId: productId.optional(),
4881
5020
  slug: slug.optional(),
@@ -4930,7 +5069,7 @@ function registerAutonomyTools(server, client) {
4930
5069
  }));
4931
5070
  if (client.hasCapability("backlog:write")) server.registerTool("taiga_set_auto_merge", {
4932
5071
  title: "Set whether an initiative's pull request merges on its own",
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.",
5072
+ description: "Set whether one initiative's pull request merges 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.",
4934
5073
  inputSchema: z.object({
4935
5074
  initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
4936
5075
  autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
@@ -4991,22 +5130,62 @@ function itemRow(row, now, full) {
4991
5130
  };
4992
5131
  }
4993
5132
  /** What is open, counted by product and kind: the answer to "what needs attention" without reading every row. */
4994
- function summaryOf(rows) {
5133
+ function summaryOf(rows, stale) {
4995
5134
  const byProduct = /* @__PURE__ */ new Map();
4996
5135
  for (const row of rows) {
4997
5136
  const key = String(row["productId"]);
4998
- const entry = byProduct.get(key) ?? {
5137
+ const entry = byProduct.get(key) ?? (row["productName"] ? {
4999
5138
  productId: key,
5000
- product: row["productName"] ?? key,
5139
+ product: row["productName"],
5001
5140
  open: 0,
5002
5141
  byKind: {}
5003
- };
5142
+ } : {
5143
+ productId: key,
5144
+ open: 0,
5145
+ byKind: {}
5146
+ });
5004
5147
  entry.open += 1;
5005
5148
  entry.byKind[String(row["kind"])] = (entry.byKind[String(row["kind"])] ?? 0) + 1;
5149
+ if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
5006
5150
  byProduct.set(key, entry);
5007
5151
  }
5008
5152
  return [...byProduct.values()].sort((a, b) => b.open - a.open);
5009
5153
  }
5154
+ /** Pull request reviews checked against their run and initiative; past this a row is left as it is. */
5155
+ const STALE_CHECKS = 10;
5156
+ /**
5157
+ * Which "Review & merge" items no longer stand, and why (PRO-2800): the run's
5158
+ * pull request closed or merged, or its initiative was canceled or finished
5159
+ * without it. Two reviewing agents counted such leftovers as live gates; the
5160
+ * row still is open in Action Required, so this says what it is now rather
5161
+ * than hiding it. Best effort, a handful of runs: a failed read leaves the
5162
+ * row as it was.
5163
+ */
5164
+ async function staleReviews(client, rows) {
5165
+ const byId = new Map(rows.filter((row) => row["kind"] === "review_pr" && row["subjectType"] === "build_run").map((row) => [String(row["id"]), row]));
5166
+ const candidates = [...byId.values()].slice(0, STALE_CHECKS);
5167
+ let unchecked = byId.size - candidates.length;
5168
+ const out = /* @__PURE__ */ new Map();
5169
+ await Promise.all(candidates.map(async (row) => {
5170
+ try {
5171
+ const run = (await client.getBuildRun(String(row["subjectId"]))).data;
5172
+ if (run["prMerged"] === true) return void out.set(String(row["id"]), "its pull request has merged");
5173
+ if (run["prState"] === "closed") return void out.set(String(row["id"]), "its pull request was closed");
5174
+ if (typeof run["backlogItemId"] !== "string") return;
5175
+ if (!client.hasCapability("backlog:read")) return void (unchecked += 1);
5176
+ const item = (await client.getBacklogItem(run["backlogItemId"])).data;
5177
+ if (item["state"] === "canceled" || item["status"] === "canceled") out.set(String(row["id"]), "its initiative was canceled; the pull request is still open on the forge");
5178
+ else if (item["state"] === "done" || item["status"] === "done") out.set(String(row["id"]), "its initiative is done; this run is not the one that landed it");
5179
+ } catch (error) {
5180
+ if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
5181
+ unchecked += 1;
5182
+ }
5183
+ }));
5184
+ return {
5185
+ stale: out,
5186
+ unchecked
5187
+ };
5188
+ }
5010
5189
  /** Pages read for the summary; past this it says it counted only those. */
5011
5190
  const SUMMARY_PAGE = 100;
5012
5191
  const SETUP_TASK_FIELDS = [
@@ -5067,11 +5246,18 @@ function sameKeyOnce(rows) {
5067
5246
  }
5068
5247
  return out;
5069
5248
  }
5249
+ /** A page of folded rows, saying how many records it folded so totalCount (records) and the rows reconcile. */
5250
+ function folded(rows, nextCursor, records) {
5251
+ return {
5252
+ ...page(rows, nextCursor),
5253
+ ...rows.length < records ? { recordsOnPage: records } : {}
5254
+ };
5255
+ }
5070
5256
  function registerInboxTools(server, client) {
5071
5257
  if (!client.hasCapability("products:read")) return;
5072
5258
  server.registerTool("taiga_list_action_required", {
5073
5259
  title: "List what needs a person",
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.",
5260
+ description: "What waits on a person: reviews and setup items, with a summary by product. The inbox is the team's, not one person's. It holds 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. A pull request review that no longer stands (its pull request merged or closed, or its initiative canceled or done) says so under stale, and the summary counts those per product. 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.",
5075
5261
  inputSchema: z.object({
5076
5262
  productId: productId.optional(),
5077
5263
  category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
@@ -5104,19 +5290,33 @@ function registerInboxTools(server, client) {
5104
5290
  const rows = result.data;
5105
5291
  const due = rows.filter(isDue);
5106
5292
  const hidden = rows.length - due.length;
5293
+ const overviewDue = overview ? overview.data.filter(isDue) : [];
5294
+ const checked = resolved ? {
5295
+ stale: /* @__PURE__ */ new Map(),
5296
+ unchecked: 0
5297
+ } : await staleReviews(client, [...due, ...overviewDue]);
5298
+ const stale = checked.stale;
5107
5299
  return toolResult({
5108
5300
  ...overview ? {
5109
- summary: summaryOf(overview.data.filter(isDue)),
5301
+ summary: summaryOf(overviewDue, stale),
5110
5302
  ...overview.nextCursor ? { summaryNote: `The summary counts the first ${SUMMARY_PAGE} items.` } : {}
5111
5303
  } : {},
5112
- ...page(due.map((row) => itemRow(row, now, full === true)), result.nextCursor),
5304
+ ...page(due.map((row) => {
5305
+ const item = itemRow(row, now, full === true);
5306
+ const why = stale.get(String(row["id"]));
5307
+ return why ? {
5308
+ ...item,
5309
+ stale: why
5310
+ } : item;
5311
+ }), result.nextCursor),
5113
5312
  ...totalOf(result),
5114
- ...hidden > 0 ? { snoozedHidden: hidden } : {}
5313
+ ...hidden > 0 ? { snoozedHidden: hidden } : {},
5314
+ ...checked.unchecked > 0 ? { staleNote: `${checked.unchecked} pull request review${checked.unchecked === 1 ? " was" : "s were"} not checked for staleness (past the cap, or the read was refused).` } : {}
5115
5315
  });
5116
5316
  }));
5117
5317
  server.registerTool("taiga_list_setup_tasks", {
5118
5318
  title: "List setup tasks",
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.",
5319
+ description: "What a person must set up for a product (secrets, variables, accounts), with exactly what and where. These are the tasks agents raised for work only a person can do: 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 (folded within a page). totalCount counts records, and a page that folded some says recordsOnPage. Unlike taiga_list_action_required, whose setup items are the same work as rows to resolve, this says exactly what to set and where.",
5120
5320
  inputSchema: z.object({
5121
5321
  productId: productId.optional(),
5122
5322
  includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
@@ -5131,7 +5331,7 @@ function registerInboxTools(server, client) {
5131
5331
  ...params.cursor === void 0 ? { withTotal: true } : {}
5132
5332
  });
5133
5333
  return toolResult({
5134
- ...page(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor),
5334
+ ...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor, result.data.length),
5135
5335
  ...totalOf(result)
5136
5336
  });
5137
5337
  }));
@@ -5297,7 +5497,7 @@ function registerKnowledgeTools(server, client) {
5297
5497
  if (!client.hasCapability("customDocuments:write")) return;
5298
5498
  server.registerTool("taiga_add_knowledge", {
5299
5499
  title: "Add knowledge",
5300
- description: "File a Markdown or plain-text reference document as knowledge, at the level it is relevant: a product (productId), a factory (factoryId), or the organization (neither; that takes an owner or admin, because every factory and product inherits it). Good candidates are what a new engineer would be handed to read: specifications and contracts to conform to, glossaries and code lists, third-party documentation, internal write-ups. A rule that applies every time belongs in instructions instead. The file is processed before agents can read it; taiga_list_knowledge says when it is ready. PDFs and spreadsheets are added in the web. The same content already filed at that level comes back as that file, with deduplicated: true: it was not created here, so do not delete it as if it were. Every agent at that level reads it from then on, and processing it is paid work.",
5500
+ description: "File a reference document as knowledge for agents to search: Markdown or plain text, at the level it is relevant: a product (productId), a factory (factoryId), or the organization (neither; that takes an owner or admin, because every factory and product inherits it). Good candidates are what a new engineer would be handed to read: specifications and contracts to conform to, glossaries and code lists, third-party documentation, internal write-ups. A rule that applies every time belongs in instructions instead. The file is processed before agents can read it; taiga_list_knowledge says when it is ready. PDFs and spreadsheets are added in the web. The same content already filed at that level comes back as that file, with deduplicated: true: it was not created here, so do not delete it as if it were. Every agent at that level reads it from then on, and processing it is paid work.",
5301
5501
  inputSchema: z.object({
5302
5502
  ...scopeInputs,
5303
5503
  fileName: z.string().trim().min(1).max(200).regex(/\.(md|txt)$/i, "A Markdown (.md) or plain-text (.txt) file name").describe("The file name, ending in .md or .txt"),
@@ -5436,7 +5636,7 @@ function registerObserveTools(server, client) {
5436
5636
  if (!client.hasCapability("products:read")) return;
5437
5637
  server.registerTool("taiga_get_deployments", {
5438
5638
  title: "Get what runs where",
5439
- description: "What is running in each of a product's environments now, anything deploying to them, and the recent history, newest first: each deployment with its commit, pull request, status, who deployed it and, for a failure, the error the provider reported. What runs is the most recent deployment that actually succeeded, so a newer failed attempt shows beside the version still running, not in its place. The history is mirrored from the provider, including deployments made outside Taiga; syncStatus says when it last synced. Pass environmentId for one environment's history. Taiga observes deployments and never performs them.",
5639
+ description: "What runs in each of a product's environments, and recent deployments: what is running now, anything deploying, and the recent history, newest first: each deployment with its commit, pull request, status, who deployed it and, for a failure, the error the provider reported. What runs is the most recent deployment that actually succeeded, so a newer failed attempt shows beside the version still running, not in its place. The history is mirrored from the provider, including deployments made outside Taiga; syncStatus says when it last synced. Pass environmentId for one environment's history. Taiga observes deployments and never performs them.",
5440
5640
  inputSchema: z.object({
5441
5641
  productId,
5442
5642
  environmentId: environmentId.optional(),
@@ -5484,7 +5684,7 @@ function registerObserveTools(server, client) {
5484
5684
  }));
5485
5685
  server.registerTool("taiga_get_delivery_metrics", {
5486
5686
  title: "Get delivery metrics",
5487
- description: "DORA's five software delivery metrics for one environment (the one marked production, as the page reports them), computed from its deployment history over the last 30, 60 or 90 days: deployment frequency, lead time and failed deployment recovery time (throughput), change-failure rate and rework rate (instability), each with its trend against the window before, and a performance band for all but rework rate, which DORA has published none for. Failures and recoveries are read from deployments, not incidents. A trend built on fewer than ten deployments is approximate; a metric no deployment fed says so instead of showing a number. Previews and checks do not count.",
5687
+ description: "DORA's five delivery metrics for one environment (the one marked production, as the page reports them), computed from its deployment history over the last 30, 60 or 90 days: deployment frequency, lead time and failed deployment recovery time (throughput), change-failure rate and rework rate (instability), each with its trend against the window before, and a performance band for all but rework rate, which DORA has published none for. Failures and recoveries are read from deployments, not incidents. A trend built on fewer than ten deployments is approximate; a metric no deployment fed says so instead of showing a number. Previews and checks do not count.",
5488
5688
  inputSchema: z.object({
5489
5689
  productId,
5490
5690
  environmentId,
@@ -5497,7 +5697,7 @@ function registerObserveTools(server, client) {
5497
5697
  }));
5498
5698
  server.registerTool("taiga_get_monitoring", {
5499
5699
  title: "Get a product's monitoring",
5500
- description: "How a product's environments are doing: each environment's current health from Taiga's availability checks of its App URL, with its uptime and when it was last checked; a status Taiga cannot verify now reads as not checked rather than the last thing it knew, and a new environment as baseline learning. With environmentId, that environment's uptime over a 30, 60 or 90-day window, and the experience signals the beacon reports (Core Web Vitals, JavaScript errors, client-observed API health) where it is installed; until then they read as not set up, which means no data, not good data. Setting up the beacon raises an initiative from the environment's settings in the web.",
5700
+ description: "How a product's environments are doing (health, uptime): each environment's current health from Taiga's availability checks of its App URL, with its uptime and when it was last checked; a status Taiga cannot verify now reads as not checked rather than the last thing it knew, and a new environment as baseline learning. With environmentId, that environment's uptime over a 30, 60 or 90-day window, and the experience signals the beacon reports (Core Web Vitals, JavaScript errors, client-observed API health) where it is installed; until then they read as not set up, which means no data, not good data. Setting up the beacon raises an initiative from the environment's settings in the web.",
5501
5701
  inputSchema: z.object({
5502
5702
  productId,
5503
5703
  environmentId: environmentId.optional(),
@@ -5606,7 +5806,7 @@ function registerProductDocumentWrites(server, client) {
5606
5806
  }, (params) => runTool(() => publishDraft(productScope(params.productId, params.type))));
5607
5807
  server.registerTool("taiga_mark_product_document_reviewed", {
5608
5808
  title: "Mark a product document reviewed",
5609
- description: "Record that a person reviewed a product document's latest published version, as the Documents page shows it beside each version; once every published document is reviewed, the product's review item in Action Required resolves. The mark says a human read it, so make it only when the person tells you they have: reading it yourself is not a review.",
5809
+ description: "Record that a person reviewed a product document: its latest published version, as the Documents page shows it beside each version; once every published document is reviewed, the product's review item in Action Required resolves. The mark says a human read it, so make it only when the person tells you they have: reading it yourself is not a review.",
5610
5810
  inputSchema: z.object({
5611
5811
  productId,
5612
5812
  type: reviewableType
@@ -20477,7 +20677,7 @@ function registerRepositoryTools(server, client) {
20477
20677
  if (!client.hasCapability("vcs:manage")) return;
20478
20678
  server.registerTool("taiga_link_repository", {
20479
20679
  title: "Link a repository to a product",
20480
- description: "Link a repository the GitHub connection can reach (taiga_list_linkable_repositories gives its id) to a product, or with replacesRepositoryId swap the one linked now for it in one step (naming the same repository there changes nothing; linking one already linked without it is refused). A link Taiga manages (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported in the background.",
20680
+ description: "Link a GitHub repository to a product: one the GitHub connection can reach (taiga_list_linkable_repositories gives its id), or with replacesRepositoryId swap the one linked now for it in one step (naming the same repository there changes nothing; linking one already linked without it is refused). A link Taiga manages (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported in the background.",
20481
20681
  inputSchema: z.object({
20482
20682
  productId,
20483
20683
  githubRepositoryId: idInput("GitHub repository", "taiga_list_linkable_repositories gives it"),
@@ -20793,7 +20993,7 @@ function registerSetupTools(server, client) {
20793
20993
  }));
20794
20994
  server.registerTool("taiga_update_product", {
20795
20995
  title: "Change a product's settings",
20796
- description: "Change a product's name (its slug follows the name, so links to the product change) or what Taiga builds for it: its infrastructure code and CI/CD pipelines (null returns one to the organization default), the sentence on how a half Taiga does not build is done today, and whether it inherits the organization's design standards (null follows the default for how the product started). Later plans read the new answer; work already planned is not redone. Autonomy is taiga_set_product_autonomy.",
20996
+ description: "Change a product's name or what Taiga builds for it. A new name changes the slug, so links to the product change. What Taiga builds for it: its infrastructure code and CI/CD pipelines (null returns one to the organization default), the sentence on how a half Taiga does not build is done today, and whether it inherits the organization's design standards (null follows the default for how the product started). Later plans read the new answer; work already planned is not redone. Autonomy is taiga_set_product_autonomy.",
20797
20997
  inputSchema: z.object({
20798
20998
  productId,
20799
20999
  name: z.string().trim().min(1).max(255).optional().describe("A new name"),
@@ -20816,7 +21016,7 @@ function registerSetupTools(server, client) {
20816
21016
  }));
20817
21017
  server.registerTool("taiga_refresh_reality_documents", {
20818
21018
  title: "Refresh a product's documents from its code",
20819
- description: "Derive the reality documents again from the code on the branch Taiga works on (the link's effectiveBranch: the chosen branch, or the repository's default): all of them, or the ones named (with what they are derived from). For a document the import misread, refresh it rather than importing again; unchanged sections are not republished. Runs an agent in the background and returns the job; taiga_wait_for_job says when it is done.",
21019
+ description: "Derive the reality documents again from the code, on the branch Taiga works on (the link's effectiveBranch: the chosen branch, or the repository's default): all of them, or the ones named (with what they are derived from). For a document the import misread, refresh it rather than importing again; unchanged sections are not republished. Runs an agent in the background and returns the job; taiga_wait_for_job says when it is done.",
20820
21020
  inputSchema: z.object({
20821
21021
  productId,
20822
21022
  docTypes: z.array(z.enum(REALITY_DERIVED_DOCUMENT_TYPES)).min(1).max(REALITY_DERIVED_DOCUMENT_TYPES.length).optional().describe("The documents to refresh; omit for all")
@@ -21099,6 +21299,8 @@ const RUN_REPORT_FIELDS = [
21099
21299
  "prState",
21100
21300
  "prDraft",
21101
21301
  "prMerged",
21302
+ "reviewEscalation",
21303
+ "error",
21102
21304
  "prNumber",
21103
21305
  "baseBranch",
21104
21306
  "actions"
@@ -21123,7 +21325,7 @@ function judgeBuild(until) {
21123
21325
  function registerWaitTools(server, client, options) {
21124
21326
  if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_initiative", {
21125
21327
  title: "Wait for an initiative",
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.",
21328
+ description: "Wait for an initiative to reach a point (planned, building, a pull request, done), 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.",
21127
21329
  inputSchema: z.object({
21128
21330
  initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
21129
21331
  until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
@@ -21156,7 +21358,7 @@ function registerWaitTools(server, client, options) {
21156
21358
  }));
21157
21359
  if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_run", {
21158
21360
  title: "Wait for a run",
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.",
21361
+ description: "Wait for a run to reach a point (a step, a pull request, settled): 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.",
21160
21362
  inputSchema: z.object({
21161
21363
  runId: idInput("run", "taiga_list_runs gives it"),
21162
21364
  until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
@@ -21172,7 +21374,7 @@ function registerWaitTools(server, client, options) {
21172
21374
  return toolResult({
21173
21375
  until: params.until,
21174
21376
  ...outcome,
21175
- run: now ? readableRun(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
21377
+ run: now ? readableRow(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
21176
21378
  });
21177
21379
  }));
21178
21380
  if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
@@ -21206,9 +21408,10 @@ function registerWaitTools(server, client, options) {
21206
21408
  * model learns the tool's real shape. Applied here, once, so no tool can be
21207
21409
  * registered without it.
21208
21410
  */
21209
- function withStrictInputs(server) {
21411
+ function withStrictInputs(server, registered) {
21210
21412
  const register = server.registerTool.bind(server);
21211
21413
  server.registerTool = ((name, config, callback) => {
21414
+ registered.set(name, config.annotations);
21212
21415
  const schema = config.inputSchema;
21213
21416
  if (schema !== void 0 && !(schema instanceof z.ZodObject)) throw new Error(`Tool ${name}: inputSchema must be a z.object() so unknown arguments can be refused`);
21214
21417
  const handler = callback;
@@ -21247,7 +21450,7 @@ const ALWAYS_LOAD = /* @__PURE__ */ new Set([
21247
21450
  function writeNotice(name, annotations) {
21248
21451
  if (!annotations || annotations.readOnlyHint === void 0) throw new Error(`Tool ${name}: annotations must say whether it is read-only`);
21249
21452
  if (annotations.readOnlyHint) return "";
21250
- return annotations.destructiveHint ? "Acts on paid or shared work: ask the person first. " : "Writes. ";
21453
+ return annotations.destructiveHint ? "Ask first: paid or shared work. " : "Writes. ";
21251
21454
  }
21252
21455
  /**
21253
21456
  * Register all MCP tools, resources and prompts based on the API key's effective capabilities.
@@ -21255,7 +21458,8 @@ function writeNotice(name, annotations) {
21255
21458
  * never sees tools it can't use.
21256
21459
  */
21257
21460
  function registerAllTools(server, client, options = {}) {
21258
- withStrictInputs(server);
21461
+ const registered = /* @__PURE__ */ new Map();
21462
+ withStrictInputs(server, registered);
21259
21463
  registerProductTools(server, client);
21260
21464
  registerDocumentTools(server, client);
21261
21465
  registerBacklogTools(server, client);
@@ -21272,9 +21476,39 @@ function registerAllTools(server, client, options = {}) {
21272
21476
  registerRepositoryTools(server, client);
21273
21477
  registerWaitTools(server, client, options.wait ?? DEFAULT_WAIT_OPTIONS);
21274
21478
  registerContextTools(server, client);
21479
+ if (registered.size > 0) registerWhoami(server, client, registered);
21275
21480
  registerResources(server, client);
21276
21481
  registerPrompts(server, client);
21277
21482
  }
21483
+ /**
21484
+ * Who this key is and what it may do (PRO-2800): reviewing agents read tool
21485
+ * descriptions saying "owners and admins link repositories" with no way to
21486
+ * learn their own role until a write was refused. Registered last, so it can
21487
+ * name every tool the key was given.
21488
+ */
21489
+ function registerWhoami(server, client, registered) {
21490
+ server.registerTool("taiga_whoami", {
21491
+ title: "Who am I",
21492
+ description: "Who this API key acts as and what it may do: the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and the tools it was given, split into reads, writes, and the ask-first writes (paid agent work or changes others see). A tool the key's capabilities do not cover is not offered at all, rather than refused when called.",
21493
+ inputSchema: z.object({}),
21494
+ annotations: READ_ONLY
21495
+ }, () => runTool(async () => {
21496
+ const me = client.getIntrospection();
21497
+ const tools = [...registered.entries()].filter(([name]) => name !== "taiga_whoami");
21498
+ const named = (pick) => tools.filter(([, notes]) => pick(notes)).map(([name]) => name);
21499
+ return toolResult({
21500
+ email: me?.email ?? null,
21501
+ role: me?.role ?? null,
21502
+ scopes: me?.scopes ?? null,
21503
+ capabilities: me?.capabilities ?? [],
21504
+ tools: {
21505
+ reads: named((notes) => notes?.readOnlyHint === true),
21506
+ writes: named((notes) => notes?.readOnlyHint === false && notes.destructiveHint !== true),
21507
+ askFirst: named((notes) => notes?.readOnlyHint === false && notes.destructiveHint === true)
21508
+ }
21509
+ });
21510
+ }));
21511
+ }
21278
21512
  //#endregion
21279
21513
  //#region src/tools/instructions.ts
21280
21514
  /**
@@ -21292,20 +21526,25 @@ const SERVER_INSTRUCTIONS = [
21292
21526
  "Taiga is a policy-aware software delivery platform: an organization's policies and knowledge, its factories",
21293
21527
  "and products, each product's initiatives (its backlog), the line that plans and builds them one at a time, the",
21294
21528
  "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.",
21529
+ "Use these tools (names start with taiga_) whenever the person asks about a Taiga product, an initiative, a run",
21530
+ "or pull request, policy or compliance, findings, deployments, or setting a product up.",
21297
21531
  "",
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.",
21532
+ "**Start here.** taiga_get_product_context (a product by git remote URL, slug or ID) says where it stands in one",
21533
+ "call: what it still needs while in setup, documents as outlines, policies as section ids, open initiatives,",
21534
+ "recent runs with their outcome, what waits on a person, findings and policy violations. Then read only what you",
21535
+ "need. Across products: taiga_list_products, taiga_list_action_required. taiga_whoami says what this key may do.",
21302
21536
  "",
21303
21537
  "**Words.** An initiative is a unit of work in the backlog; a run is one attempt by the coding agent to build it;",
21304
21538
  "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.",
21539
+ "the tool that lists the right ones. A GitHub repository id is not a repository link id. Say a refusal as it is.",
21540
+ "",
21541
+ "**Reading.** Lists page with nextCursor (Action Required, setup tasks, findings: totalCount). A document has two",
21542
+ "timelines: plan (what was meant) and reality (derived from the code). outcome, situation and nextStep are",
21543
+ "one-sentence summaries of the record they sit on; read the record for the detail.",
21306
21544
  "",
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.",
21545
+ "**Before you write.** A tool that writes opens with \"Writes.\" or \"Ask first:\". Before an \"Ask first\" tool, tell",
21546
+ "the person what you are about to do and wait for their yes: it starts paid agent work, interrupts it, or changes",
21547
+ "what others see.",
21309
21548
  "",
21310
21549
  "**Only in the web:** connecting GitHub, members and roles, the Discovery conversation of a product started from",
21311
21550
  "scratch, stamping checks, starting a run over, and deleting anything."