@taiga-ai/mcp-server 0.11.0 → 0.12.0-rc.419

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
@@ -114,7 +114,7 @@ var TaigaClient = class {
114
114
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
115
115
  this.apiKey = opts.apiKey;
116
116
  }
117
- async request(method, path, body, keep) {
117
+ async request(method, path, body, options = {}) {
118
118
  const url = `${this.baseUrl}/v1${path}`;
119
119
  const headers = {
120
120
  Authorization: `Bearer ${this.apiKey}`,
@@ -124,17 +124,18 @@ var TaigaClient = class {
124
124
  const response = await fetch(url, {
125
125
  method,
126
126
  headers,
127
- body: body === void 0 ? null : JSON.stringify(body)
127
+ body: body === void 0 ? null : JSON.stringify(body),
128
+ ...options.signal ? { signal: options.signal } : {}
128
129
  });
129
130
  if (!response.ok) {
130
131
  const text = await response.text().catch(() => "");
131
132
  throw new TaigaApiError(method, path, response.status, text);
132
133
  }
133
134
  const parsed = await response.json();
134
- return keep ? parsed : withoutTenant(parsed);
135
+ return options.keepTenant ? parsed : withoutTenant(parsed);
135
136
  }
136
137
  async introspect() {
137
- const result = await this.request("GET", "/auth/introspect", void 0, { tenantId: true });
138
+ const result = await this.request("GET", "/auth/introspect", void 0, { keepTenant: true });
138
139
  this.introspection = result.data;
139
140
  return result.data;
140
141
  }
@@ -157,8 +158,8 @@ var TaigaClient = class {
157
158
  const qs = params.toString();
158
159
  return this.request("GET", `/products${qs ? `?${qs}` : ""}`);
159
160
  }
160
- async getProduct(id) {
161
- return this.request("GET", `/products/${seg(id)}`);
161
+ async getProduct(id, signal) {
162
+ return this.request("GET", `/products/${seg(id)}`, void 0, { signal });
162
163
  }
163
164
  async listProductRepositories(productId) {
164
165
  return this.request("GET", `/products/${seg(productId)}/repositories`);
@@ -225,8 +226,8 @@ var TaigaClient = class {
225
226
  const qs = params.toString();
226
227
  return this.request("GET", `/products/${seg(productId)}/backlog${qs ? `?${qs}` : ""}`);
227
228
  }
228
- async getBacklogItem(itemId) {
229
- return this.request("GET", `/backlog/${seg(itemId)}`);
229
+ async getBacklogItem(itemId, signal) {
230
+ return this.request("GET", `/backlog/${seg(itemId)}`, void 0, { signal });
230
231
  }
231
232
  async createBacklogItem(productId, body) {
232
233
  return this.request("POST", `/products/${seg(productId)}/backlog`, {
@@ -242,6 +243,14 @@ var TaigaClient = class {
242
243
  async resumeInitiative(itemId) {
243
244
  return this.request("POST", `/backlog/${seg(itemId)}/resume`);
244
245
  }
246
+ /** The initiative's own review gate: true builds a landed plan at once, false stops for a person, null follows the product. */
247
+ async setAutoBuild(itemId, autoBuild) {
248
+ return this.request("PUT", `/backlog/${seg(itemId)}/auto-build`, { autoBuild });
249
+ }
250
+ /** The product's live queue, in order. */
251
+ async listQueue(productId) {
252
+ return this.request("GET", `/products/${seg(productId)}/queue`);
253
+ }
245
254
  async getActivePlan(itemId) {
246
255
  return this.request("GET", `/backlog/${seg(itemId)}/plans/active`);
247
256
  }
@@ -268,8 +277,8 @@ var TaigaClient = class {
268
277
  const qs = params.toString();
269
278
  return this.request("GET", `/builds${qs ? `?${qs}` : ""}`);
270
279
  }
271
- async getBuildRun(id) {
272
- return this.request("GET", `/builds/${seg(id)}`);
280
+ async getBuildRun(id, signal) {
281
+ return this.request("GET", `/builds/${seg(id)}`, void 0, { signal });
273
282
  }
274
283
  async stopBuildRun(id) {
275
284
  return this.request("POST", `/builds/${seg(id)}/stop`);
@@ -662,7 +671,14 @@ const QUEUE_GLOBAL_BLOCKING_STATES = ["active"];
662
671
  */
663
672
  const QUEUE_POSITIONAL_BLOCKING_STATES = ["halted"];
664
673
  [...QUEUE_GLOBAL_BLOCKING_STATES, ...QUEUE_POSITIONAL_BLOCKING_STATES];
665
- Object.keys({
674
+ /**
675
+ * Why the queue stopped, as data.
676
+ *
677
+ * The `message` values for causes that already existed are VERBATIM the strings
678
+ * the reconciler used to write, so this change is invisible to anyone reading
679
+ * their queue.
680
+ */
681
+ const HALT_CAUSES = {
666
682
  /**
667
683
  * A PERSON stopped the plan (PRO-1144). Distinct from `planning_failed`
668
684
  * because the two leave the initiative in the same state and mean opposite
@@ -842,7 +858,52 @@ Object.keys({
842
858
  clearedWhen: "never on its own; there is no principal left to run as",
843
859
  exits: ["remove"]
844
860
  }
845
- }).filter((c) => c !== "awaiting_plan_review" && c !== "armed_start_missing");
861
+ };
862
+ Object.keys(HALT_CAUSES).filter((c) => c !== "awaiting_plan_review" && c !== "armed_start_missing");
863
+ function haltClearsItself(cause) {
864
+ return haltHasExit(cause, "auto");
865
+ }
866
+ /**
867
+ * May a person lift this halt by hand?
868
+ *
869
+ * The complement of `haltClearsItself`, and NOT its negation: a cause can be
870
+ * neither (queued_by_erased, whose only exit is removal, because there is no
871
+ * principal left to run as no matter who presses what). Reads the registry so
872
+ * the API's guard and the button's visibility come from one place instead of
873
+ * two lists that drift.
874
+ */
875
+ function canResumeHalt(cause) {
876
+ return haltHasExit(cause, "resume");
877
+ }
878
+ /**
879
+ * The one place that indexes the registry by a cause, so the one place that has
880
+ * to survive a cause it does not know.
881
+ *
882
+ * The type says that cannot happen, and server-side it cannot: the column has a
883
+ * CHECK generated from this same list. But the BROWSER holds a bundle that can
884
+ * be older than the API it is talking to, and `HALT_CAUSES[unknown].exits`
885
+ * throws, which takes down the whole list rather than one badge. Fail closed:
886
+ * an unknown cause offers no exits, so it neither auto-clears nor shows a
887
+ * Resume button, and the row falls back to the prose the server sent.
888
+ */
889
+ function haltHasExit(cause, exit) {
890
+ const spec = HALT_CAUSES[cause];
891
+ return spec ? spec.exits.includes(exit) : false;
892
+ }
893
+ /**
894
+ * The sentence for a halt, rendered from its cause and params.
895
+ *
896
+ * ONE renderer, used both by the reconciler that stores the prose today and by
897
+ * the read path that will compute it instead (PRO-1039 PR 3). A slot with no
898
+ * matching param renders as the empty string rather than leaking `{name}` into
899
+ * a user's queue.
900
+ */
901
+ function renderHalt(h) {
902
+ const spec = HALT_CAUSES[h.cause];
903
+ if (!spec) return "";
904
+ const params = h.params;
905
+ return spec.message.replace(/\{(\w+)\}/g, (_m, key) => params[key] ?? "");
906
+ }
846
907
  //#endregion
847
908
  //#region ../../packages/types/dist/build.js
848
909
  const BUILD_RUN_STATUSES = [
@@ -892,7 +953,17 @@ function runDisposition(status) {
892
953
  * = in_flight minus 'pending' (queued is not yet executing).
893
954
  */
894
955
  const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) === "in_flight" && s !== "pending");
895
- BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
956
+ /**
957
+ * Terminal-or-handoff statuses: the run will not move again WITHOUT a human
958
+ * (dispatcher failure path preserves them; the FE realtime hook refreshes list
959
+ * views when a phase_change carries one). 'needs_review' (the gate) is included
960
+ * because the agent is done and the FE treats it like a terminal for
961
+ * list-display purposes; 'failed'/'stopped' are re-armable but settled.
962
+ * = everything that is not in_flight.
963
+ * NB: buildRunStateMachine.terminalStatuses ({completed, closed}) is a
964
+ * DIFFERENT, narrower concept: transitions a user command may never leave.
965
+ */
966
+ const BUILD_RUN_TERMINAL_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
896
967
  [...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
897
968
  [...TURN_EVENT_TRIGGERS];
898
969
  //#endregion
@@ -1073,6 +1144,29 @@ const ARTIFACT_SCALE_FRAME_NAMES = [
1073
1144
  ].map((token) => token.frame);
1074
1145
  [...ARTIFACT_CSS_VARIABLES, ...ARTIFACT_SCALE_FRAME_NAMES];
1075
1146
  new Set(ARTIFACT_CSS_VARIABLES.filter((name) => name !== "--radius"));
1147
+ //#endregion
1148
+ //#region ../../packages/types/dist/auto-build.js
1149
+ /**
1150
+ * Auto-build: the review-plans gate (PRO-1305).
1151
+ *
1152
+ * "Build as soon as the plan is ready?" is asked at two scopes -- the project's
1153
+ * default (`projects.auto_build`) and the initiative's own standing answer
1154
+ * (`backlog_items.auto_build`) -- in the same polarity as auto-merge, and this
1155
+ * is the one place they are combined, so the line, the queue's halt
1156
+ * derivation and the settings surfaces cannot drift apart on what "gated"
1157
+ * means.
1158
+ */
1159
+ /**
1160
+ * Does this plan build without stopping for a person to read it?
1161
+ *
1162
+ * The initiative's own answer wins; the project's default answers when it has
1163
+ * none. Unlike auto-merge there is no ceiling above the project: reviewing a
1164
+ * plan is a working preference, not a permission, and PRO-1305 gives the
1165
+ * initiative the override in both directions for exactly that reason.
1166
+ */
1167
+ function resolveAutoBuild(scopes) {
1168
+ return scopes.initiative ?? scopes.product;
1169
+ }
1076
1170
  Object.freeze({
1077
1171
  productKinds: true,
1078
1172
  applicationOwnership: true
@@ -1481,6 +1575,56 @@ function instructionsText(sections) {
1481
1575
  }
1482
1576
  return parts.join("\n\n").trim() || null;
1483
1577
  }
1578
+ function cutTo(doc, sections, kept) {
1579
+ const omittedSections = Object.keys(sections).filter((id) => !Object.hasOwn(kept, id)).map((id) => ({
1580
+ id,
1581
+ characters: sectionContent(sections, id)?.length ?? 0
1582
+ }));
1583
+ return {
1584
+ doc: {
1585
+ ...doc,
1586
+ data: {
1587
+ ...doc.data,
1588
+ sections: kept
1589
+ }
1590
+ },
1591
+ omittedSections
1592
+ };
1593
+ }
1594
+ /**
1595
+ * The named sections of a section-shaped document, in the order the API
1596
+ * returns them (a jsonb column: shorter ids first, then by bytes, not the
1597
+ * order a person wrote them);
1598
+ * every other section is listed with its size so the reader knows what else
1599
+ * there is. Null when the document is not made of sections.
1600
+ */
1601
+ function pickSections(doc, ids) {
1602
+ const sections = isRecord(doc.data) ? sectionsOf(doc.data) : null;
1603
+ if (!sections) return null;
1604
+ return cutTo(doc, sections, Object.fromEntries(Object.entries(sections).filter(([id]) => ids.has(id))));
1605
+ }
1606
+ /**
1607
+ * As many whole sections as fit `budget` characters of text, the rest listed
1608
+ * with their sizes so the reader can ask for them by id. Whole, never cut: a
1609
+ * section's text is one argument, and half of it reads as all of it. Not "the
1610
+ * opening": the document's own order is not in the data (a jsonb column
1611
+ * hands back shorter ids first), so a section that does not fit is skipped
1612
+ * and a later, smaller one may still be carried. Null when the document is
1613
+ * not made of sections.
1614
+ */
1615
+ function budgetSections(doc, budget) {
1616
+ const sections = isRecord(doc.data) ? sectionsOf(doc.data) : null;
1617
+ if (!sections) return null;
1618
+ const kept = [];
1619
+ let used = 0;
1620
+ for (const id of Object.keys(sections)) {
1621
+ const length = sectionContent(sections, id)?.length ?? 0;
1622
+ if (used + length > budget) continue;
1623
+ kept.push([id, sections[id]]);
1624
+ used += length;
1625
+ }
1626
+ return cutTo(doc, sections, Object.fromEntries(kept));
1627
+ }
1484
1628
  //#endregion
1485
1629
  //#region src/tools/summarize.ts
1486
1630
  /**
@@ -1502,6 +1646,30 @@ function page(data, nextCursor) {
1502
1646
  nextCursor: nextCursor ?? null
1503
1647
  };
1504
1648
  }
1649
+ /**
1650
+ * How many of an initiative's acceptance criteria are met: one pair a row can
1651
+ * carry, so "done with two unmet" shows in a list without reading the item.
1652
+ * Null while it has none (an unplanned initiative).
1653
+ */
1654
+ function criteriaCounts(item) {
1655
+ const criteria = item.acceptanceCriteria;
1656
+ if (!Array.isArray(criteria) || criteria.length === 0) return null;
1657
+ return {
1658
+ met: criteria.filter((c) => typeof c === "object" && c !== null && c.met === true).length,
1659
+ total: criteria.length
1660
+ };
1661
+ }
1662
+ /**
1663
+ * Initiative rows cut to `fields`, each with its criteria counts. A row that
1664
+ * does not carry `acceptanceCriteria` at all gets no count rather than a
1665
+ * null that would read as "has none".
1666
+ */
1667
+ function itemRows(items, fields) {
1668
+ return pickFields(items, fields).map((row, index) => "acceptanceCriteria" in items[index] ? {
1669
+ ...row,
1670
+ criteria: criteriaCounts(items[index])
1671
+ } : row);
1672
+ }
1505
1673
  //#endregion
1506
1674
  //#region src/tools/products.ts
1507
1675
  /** What a product row needs to say for the model to pick one and read it in full. */
@@ -1750,11 +1918,12 @@ function registerProductTools(server, client) {
1750
1918
  }));
1751
1919
  server.registerTool("taiga_get_product_document", {
1752
1920
  title: "Get product document",
1753
- description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A read that finds nothing says which documents and timelines the product does have.",
1921
+ description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A large document runs to tens of thousands of characters: name the sections you need (the ids taiga_get_product_context lists) and the read carries only those, listing the rest with their sizes. A read that finds nothing says which documents and timelines the product does have.",
1754
1922
  inputSchema: {
1755
1923
  productId,
1756
1924
  type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type -- taiga_list_product_documents shows which ones the product has"),
1757
- origin: originInput
1925
+ origin: originInput,
1926
+ sections: z.array(z.string().min(1).max(200)).min(1).max(50).optional().describe("Only these sections, by id; omit for the whole document")
1758
1927
  },
1759
1928
  annotations: READ_ONLY
1760
1929
  }, (params) => runTool(async () => {
@@ -1762,7 +1931,17 @@ function registerProductTools(server, client) {
1762
1931
  if (!params.origin) product = (await client.getProduct(params.productId)).data;
1763
1932
  const origin = params.origin ?? defaultOriginFor(product, params.type);
1764
1933
  try {
1765
- return toolResult(stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data));
1934
+ const doc = stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data);
1935
+ if (!params.sections) return toolResult(doc);
1936
+ const cut = pickSections(doc, new Set(params.sections));
1937
+ if (!cut) return toolError("VALIDATION_FAILED", `The ${origin} ${params.type} is not made of sections; read it without sections.`);
1938
+ const all = Object.keys(doc.data.sections);
1939
+ const unknown = params.sections.filter((id) => !all.includes(id));
1940
+ if (unknown.length > 0) return toolError("NOT_FOUND", `The ${origin} ${params.type} has no section ${unknown.join(", ")}; its sections are ${all.join(", ")}.`);
1941
+ return toolResult({
1942
+ ...cut.doc,
1943
+ omittedSections: cut.omittedSections
1944
+ });
1766
1945
  } catch (error) {
1767
1946
  if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
1768
1947
  product ??= (await client.getProduct(params.productId)).data;
@@ -1983,6 +2162,130 @@ function registerDocumentTools(server, client) {
1983
2162
  }
1984
2163
  }
1985
2164
  //#endregion
2165
+ //#region src/tools/queue.ts
2166
+ /** How many entries the read names; past this an entry keeps its backlogItemId only. */
2167
+ const NAMED_ENTRIES = 25;
2168
+ const MOVE_ASIDE = "or move it down (taiga_queue_backlog_item with afterId) or off the line (taiga_move_backlog_item) to let the rest pass.";
2169
+ /**
2170
+ * What lifts a halt, in the tools that do it. Read from the halt registry in
2171
+ * @taiga/types (how each cause clears, and whether a person may lift it), so
2172
+ * the advice cannot drift from what the API accepts; plan review is the one
2173
+ * cause a tool answers directly.
2174
+ */
2175
+ function haltNextStep(cause) {
2176
+ 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).";
2177
+ const spec = cause ? HALT_CAUSES[cause] : void 0;
2178
+ if (!cause || !spec) return `Read the initiative with taiga_get_backlog_item to see why, ${MOVE_ASIDE}`;
2179
+ const known = cause;
2180
+ if (canResumeHalt(known)) return `taiga_resume_backlog_item lifts it, ${MOVE_ASIDE}`;
2181
+ if (haltClearsItself(known)) return `It clears when ${spec.clearedWhen}; ${MOVE_ASIDE}`;
2182
+ return `Only taking it off the line clears it (taiga_move_backlog_item): ${spec.clearedWhen}.`;
2183
+ }
2184
+ /**
2185
+ * The entry the line waits on, by the queue's two kinds of blocker
2186
+ * (@taiga/types): an active entry holds every other one wherever it sits, so
2187
+ * it answers first; without one, the first halted entry holds the entries
2188
+ * behind it. -1 when neither is there.
2189
+ */
2190
+ function blockingIndex(entries, stateOf) {
2191
+ const active = entries.findIndex((entry) => stateOf(entry) === "active");
2192
+ return active >= 0 ? active : entries.findIndex((entry) => stateOf(entry) === "halted");
2193
+ }
2194
+ /** The one sentence a person needs: what the line is doing, or what it is waiting on. */
2195
+ function waitingOn(rows) {
2196
+ const at = blockingIndex(rows, (row) => row.entryState);
2197
+ if (at < 0) return rows.length > 0 ? "Nothing in the queue is running; the first queued initiative starts when the line is free." : null;
2198
+ const head = rows[at];
2199
+ const name = typeof head.title === "string" ? `"${head.title}"` : `Initiative ${String(head.backlogItemId)}`;
2200
+ if (head.entryState === "halted") {
2201
+ const halt = head.halt;
2202
+ const ahead = at > 0 ? " Entries ahead of it are not held by it." : "";
2203
+ return `${name} halted the queue, holding the entries behind it: ${halt.reason} ${halt.nextStep}${ahead}`;
2204
+ }
2205
+ const lifecycle = head.lifecycle;
2206
+ const held = "every other queued initiative waits until its pull request merges.";
2207
+ if (!lifecycle) return `${name} is the active entry; ${held}`;
2208
+ const pr = typeof head.prUrl === "string" ? ` (${head.prUrl})` : "";
2209
+ if (lifecycle.cta === "start_build") return `${name} has its plan and waits for the Build slot; ${held}`;
2210
+ if (lifecycle.turn === "you") return `${name} is waiting on a person (${String(lifecycle.cta ?? lifecycle.stage)})${pr}; ${held}`;
2211
+ return `${name} is ${String(lifecycle.stage)}${pr}; ${held}`;
2212
+ }
2213
+ /**
2214
+ * The product's queue as a person reads it: each live entry in order with its
2215
+ * initiative's title and where that initiative is, a halted entry with why and
2216
+ * what lifts it, and one sentence saying what the line is waiting on. The
2217
+ * route carries item ids only; each is named by one item read (when the key
2218
+ * may read the backlog), and a read the API fails leaves that entry unnamed
2219
+ * rather than failing the queue.
2220
+ */
2221
+ async function queueView(client, product, options = { name: "all" }) {
2222
+ const entries = (await client.listQueue(product)).data.filter((entry) => entry.state !== "settled");
2223
+ const canName = client.hasCapability("backlog:read");
2224
+ const blocking = blockingIndex(entries, (entry) => entry.state);
2225
+ const toName = !canName ? [] : options.name === "all" ? entries.slice(0, NAMED_ENTRIES).map((_, index) => index) : blocking >= 0 ? [blocking] : [];
2226
+ const settled = await Promise.allSettled(toName.map((index) => client.getBacklogItem(String(entries[index].backlogItemId))));
2227
+ const reads = new Map(toName.map((index, at) => [index, settled[at]]));
2228
+ let unnamed = options.name === "all" ? entries.length - toName.length : 0;
2229
+ const rows = entries.map((entry, index) => {
2230
+ const read = reads.get(index);
2231
+ if (read?.status === "rejected") {
2232
+ if (!(read.reason instanceof TaigaApiError || isNetworkFailure(read.reason))) throw read.reason;
2233
+ unnamed += 1;
2234
+ }
2235
+ const item = read?.status === "fulfilled" ? read.value.data : null;
2236
+ const lifecycle = item?.lifecycle;
2237
+ const latestBuild = lifecycle?.latestBuild;
2238
+ const row = {
2239
+ position: index + 1,
2240
+ backlogItemId: entry.backlogItemId,
2241
+ entryState: entry.state
2242
+ };
2243
+ if (item) Object.assign(row, {
2244
+ title: item.title,
2245
+ state: item.state,
2246
+ planningStatus: item.planningStatus,
2247
+ needsHumanCause: item.needsHumanCause,
2248
+ criteria: criteriaCounts(item),
2249
+ lifecycle: lifecycle ? {
2250
+ turn: lifecycle.turn,
2251
+ stage: lifecycle.stage,
2252
+ cta: lifecycle.cta
2253
+ } : null,
2254
+ ...latestBuild ? {
2255
+ buildId: latestBuild.id,
2256
+ prUrl: latestBuild.prUrl ?? null
2257
+ } : {}
2258
+ });
2259
+ if (entry.state === "halted") {
2260
+ const cause = typeof entry.haltCause === "string" ? entry.haltCause : null;
2261
+ row.halt = {
2262
+ cause,
2263
+ reason: (cause ? renderHalt({
2264
+ cause,
2265
+ params: entry.haltParams ?? {}
2266
+ }) : "") || (typeof entry.haltReason === "string" ? entry.haltReason : "The queue stopped here."),
2267
+ nextStep: haltNextStep(cause)
2268
+ };
2269
+ }
2270
+ return row;
2271
+ });
2272
+ const note = unnamed === 0 ? null : !canName ? "Entries carry backlogItemId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry backlogItemId only.` : "An entry whose initiative could not be read carries backlogItemId only; taiga_get_backlog_item reads it.";
2273
+ return {
2274
+ entries: rows,
2275
+ waitingOn: waitingOn(rows),
2276
+ ...note ? { note } : {}
2277
+ };
2278
+ }
2279
+ function registerQueueTools(server, client) {
2280
+ if (!client.hasCapability("products:read")) return;
2281
+ server.registerTool("taiga_get_queue", {
2282
+ title: "Get the product queue",
2283
+ 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, 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.",
2284
+ inputSchema: { productId },
2285
+ annotations: READ_ONLY
2286
+ }, (params) => runTool(async () => toolResult(await queueView(client, params.productId))));
2287
+ }
2288
+ //#endregion
1986
2289
  //#region src/tools/backlogs.ts
1987
2290
  /** What a list row needs to say for the model to pick an item to read in full. */
1988
2291
  const BACKLOG_SUMMARY_FIELDS = [
@@ -2014,6 +2317,7 @@ const ITEM_OUTCOME_FIELDS = [
2014
2317
  "planningStatus",
2015
2318
  "needsHumanCause",
2016
2319
  "needsHumanDetail",
2320
+ "autoBuild",
2017
2321
  "archivedAt",
2018
2322
  "lifecycle"
2019
2323
  ];
@@ -2036,14 +2340,22 @@ const PLAN_STEP_SUMMARY_FIELDS = [
2036
2340
  "reads",
2037
2341
  "createsResources"
2038
2342
  ];
2039
- const outcome = (item) => toolResult(pickFields([item], ITEM_OUTCOME_FIELDS)[0]);
2343
+ /** The outcome fields of an initiative, with how many of its criteria are met. */
2344
+ const outcomeOf = (item) => {
2345
+ const row = item;
2346
+ return {
2347
+ ...pickFields([row], ITEM_OUTCOME_FIELDS)[0],
2348
+ criteria: criteriaCounts(row)
2349
+ };
2350
+ };
2351
+ const outcome = (item) => toolResult(outcomeOf(item));
2040
2352
  /**
2041
2353
  * Each tool is registered under the capability its route requires, so a
2042
2354
  * key sees exactly what it may call: reads on backlog:read, the create and
2043
2355
  * the board move on backlog:write, queueing on both backlog:write and
2044
2356
  * products:write (the state route checks both for a drop into queue), the
2045
- * resume on products:write alone, as its route is (the item ID it takes
2046
- * also comes back from a create or a queue), and the plan verdicts on
2357
+ * resume and the review gate on products:write alone, as their routes are
2358
+ * (the item ID they take also comes back from a create or a queue), and the plan verdicts on
2047
2359
  * products:write together with backlog:read, because the plan ID they take
2048
2360
  * comes only from the plan read: a key that cannot read plans has nothing
2049
2361
  * to feed them.
@@ -2054,12 +2366,13 @@ function registerBacklogTools(server, client) {
2054
2366
  if (may("backlog:write")) registerBacklogWrites(server, client);
2055
2367
  if (may("backlog:write") && may("products:write")) registerQueue(server, client);
2056
2368
  if (may("products:write")) registerResume(server, client);
2369
+ if (may("products:write")) registerReviewGate(server, client);
2057
2370
  if (may("backlog:read") && may("products:write")) registerPlanVerdicts(server, client);
2058
2371
  }
2059
2372
  function registerBacklogReads(server, client) {
2060
2373
  server.registerTool("taiga_list_backlog_items", {
2061
2374
  title: "List backlog items",
2062
- description: "List a product's backlog items (initiatives): titles, category, status and where each one is on the line (backlog, todo, queue, plan, build, done). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
2375
+ description: "List a product's backlog items (initiatives): titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done) and criteria, how many of its acceptance criteria are met (null before it is planned). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
2063
2376
  inputSchema: {
2064
2377
  productId,
2065
2378
  status: z.enum([
@@ -2086,7 +2399,7 @@ function registerBacklogReads(server, client) {
2086
2399
  annotations: READ_ONLY
2087
2400
  }, (params) => runTool(async () => {
2088
2401
  const result = await client.listBacklogItems(params.productId, params);
2089
- return toolResult(page(pickFields(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
2402
+ return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
2090
2403
  }));
2091
2404
  server.registerTool("taiga_get_backlog_item", {
2092
2405
  title: "Get backlog item",
@@ -2154,21 +2467,89 @@ function registerBacklogWrites(server, client) {
2154
2467
  }
2155
2468
  /** The columns a drop into starts paid work, which the state route gates on products:write. */
2156
2469
  const LINE_STARTING_STATES = /* @__PURE__ */ new Set(["queue", "plan"]);
2470
+ /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
2471
+ const isApiFailure$1 = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
2472
+ /**
2473
+ * Whether this initiative's plan builds the moment it lands, and which scope
2474
+ * decided it: its own setting, or the product's default when it has none.
2475
+ * Null when the product cannot be read; the outcome then says so instead.
2476
+ */
2477
+ async function reviewGate(client, item) {
2478
+ const own = typeof item.autoBuild === "boolean" ? item.autoBuild : null;
2479
+ try {
2480
+ const product = own === null ? (await client.getProduct(String(item.productId))).data : {};
2481
+ const builds = resolveAutoBuild({
2482
+ initiative: own,
2483
+ product: typeof product.autoBuild === "boolean" ? product.autoBuild : true
2484
+ });
2485
+ return {
2486
+ buildsWithoutReview: builds,
2487
+ decidedBy: own === null ? "product" : "initiative",
2488
+ meaning: builds ? "The plan is built as soon as it lands; nobody reads it first." : "The line stops at the plan for a person to approve or reject it (taiga_get_plan, taiga_approve_plan)."
2489
+ };
2490
+ } catch (error) {
2491
+ if (!isApiFailure$1(error)) throw error;
2492
+ return null;
2493
+ }
2494
+ }
2495
+ /** Where a queued initiative stands in its product's line; null, and the outcome says so, when the queue cannot be read. */
2496
+ async function placeInQueue(client, item) {
2497
+ try {
2498
+ const view = await queueView(client, String(item.productId), { name: "blocking" });
2499
+ return {
2500
+ position: view.entries.find((row) => row.backlogItemId === item.id)?.position ?? null,
2501
+ of: view.entries.length,
2502
+ waitingOn: view.waitingOn
2503
+ };
2504
+ } catch (error) {
2505
+ if (!isApiFailure$1(error)) throw error;
2506
+ return null;
2507
+ }
2508
+ }
2509
+ const autoBuildInput = z.boolean().nullable().describe("false stops the line at this initiative's plan for a person to read it; true builds it as soon as it lands; null follows the product's default");
2157
2510
  function registerQueue(server, client) {
2158
2511
  server.registerTool("taiga_queue_backlog_item", {
2159
2512
  title: "Queue backlog item",
2160
- description: "Add an initiative to the product queue, the line the coding agent works: queued initiatives are planned and built one at a time, each advancing when the previous pull request merges. This starts paid work: a planner run, then (when the plan is approved, or at once if auto-build is on) a coding run that pushes commits and opens a pull request in the product repository. Ask the person before calling this. An initiative already in the queue stays where it is unless afterId says where to go; one being planned or built cannot move (CONFLICT). Returns the initiative with its new state.",
2513
+ description: "Add an initiative to the product queue, the line the coding agent works: queued initiatives are planned and built one at a time, each advancing when the previous pull request merges. This starts paid work: a planner run, then a coding run that pushes commits and opens a pull request in the product repository. Whether the plan is built at once or waits for a person is the initiative's review gate: pass autoBuild: false to read the plan before anything is built (the product's default applies otherwise, and the outcome says which it is); the gate is set first and stays set if the move is then refused. Ask the person before calling this. An initiative already in the queue stays where it is unless afterId says where to go; one being planned or built cannot move (CONFLICT). Returns the initiative with its new state, its review gate and its place in the queue.",
2161
2514
  inputSchema: {
2162
2515
  itemId,
2163
- afterId: z.string().uuid().nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is")
2516
+ afterId: z.string().uuid().nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is"),
2517
+ autoBuild: autoBuildInput.optional().describe("Set the initiative's review gate before queueing: false to read the plan first, true to build it at once, null to follow the product; omit to leave the gate as it is")
2164
2518
  },
2165
2519
  annotations: OPERATE
2166
2520
  }, (params) => runTool(async () => {
2167
- const { itemId: id, ...placement } = params;
2168
- return outcome((await client.transitionInitiative(id, {
2521
+ const { itemId: id, autoBuild, ...placement } = params;
2522
+ if (autoBuild !== void 0) await client.setAutoBuild(id, autoBuild);
2523
+ const item = (await client.transitionInitiative(id, {
2169
2524
  state: "queue",
2170
2525
  ...placement
2171
- })).data);
2526
+ })).data;
2527
+ const [gate, queue] = await Promise.all([reviewGate(client, item), placeInQueue(client, item)]);
2528
+ return toolResult({
2529
+ ...outcomeOf(item),
2530
+ reviewGate: gate,
2531
+ queue,
2532
+ ...gate && queue ? {} : { note: "Part of the outcome could not be read; taiga_get_queue and taiga_get_product say the rest." }
2533
+ });
2534
+ }));
2535
+ }
2536
+ function registerReviewGate(server, client) {
2537
+ server.registerTool("taiga_set_auto_build", {
2538
+ title: "Set an initiative's review gate",
2539
+ description: "Set whether an initiative's plan is built as soon as it lands (true) or waits for a person to read it and approve or reject it (false); null follows the product's default. Standing: it applies to every plan the initiative gets, including one being written now. Opening the gate on a plan that is already waiting starts its build, so ask the person first. Returns the initiative with the gate that now applies.",
2540
+ inputSchema: {
2541
+ itemId,
2542
+ autoBuild: autoBuildInput
2543
+ },
2544
+ annotations: OPERATE
2545
+ }, (params) => runTool(async () => {
2546
+ const item = (await client.setAutoBuild(params.itemId, params.autoBuild)).data;
2547
+ const gate = await reviewGate(client, item);
2548
+ return toolResult({
2549
+ ...outcomeOf(item),
2550
+ reviewGate: gate,
2551
+ ...gate ? {} : { note: "The product's default could not be read; taiga_get_product says it." }
2552
+ });
2172
2553
  }));
2173
2554
  }
2174
2555
  function registerResume(server, client) {
@@ -2239,6 +2620,31 @@ const RUN_OUTCOME_FIELDS = [
2239
2620
  /** How many products the org-wide list names; past this a row keeps its productId only. */
2240
2621
  const NAMED_PRODUCTS = 100;
2241
2622
  const buildId = z.string().uuid().describe("Build run ID -- get from taiga_list_builds");
2623
+ /** A step without its instructions: what it is, how it stands and what it changed. */
2624
+ const STEP_SUMMARY_FIELDS = [
2625
+ "stepOrder",
2626
+ "title",
2627
+ "status",
2628
+ "currentPhase",
2629
+ "commitSha",
2630
+ "filesChanged",
2631
+ "verification",
2632
+ "error",
2633
+ "errorClassification",
2634
+ "startedAt",
2635
+ "completedAt"
2636
+ ];
2637
+ /** Past this many characters a step's headline is cut; the full read has the rest. */
2638
+ const HEADLINE_LIMIT = 600;
2639
+ /**
2640
+ * The first paragraph of a step's summary: the agent writes its result
2641
+ * there (what changed, which checks passed), and the detail after it.
2642
+ */
2643
+ function summaryHeadline(summary) {
2644
+ if (typeof summary !== "string" || !summary.trim()) return null;
2645
+ const first = summary.trim().split(/\n\s*\n/, 1)[0].trim();
2646
+ return first.length > HEADLINE_LIMIT ? `${first.slice(0, HEADLINE_LIMIT).trimEnd()}...` : first;
2647
+ }
2242
2648
  /**
2243
2649
  * The runs of every product the key can see, each row naming its product.
2244
2650
  * The route carries product ids only; one products read names them, and a
@@ -2290,15 +2696,26 @@ function registerBuildTools(server, client) {
2290
2696
  }));
2291
2697
  server.registerTool("taiga_get_build", {
2292
2698
  title: "Get build run",
2293
- description: "Get one run in full with its steps: what each step was meant to change, what it did change (commit, files), its status and any error. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions list says what it accepts: stop (taiga_stop_build), resume or retry_from_failed_step (both taiga_resume_build), start_over (web UI only for now).",
2294
- inputSchema: { buildId },
2699
+ description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions list says what it accepts: stop (taiga_stop_build), resume or retry_from_failed_step (both taiga_resume_build), start_over (web UI only for now).",
2700
+ inputSchema: {
2701
+ buildId,
2702
+ full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
2703
+ },
2295
2704
  annotations: READ_ONLY
2296
2705
  }, (params) => runTool(async () => {
2297
2706
  const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
2298
- return toolResult({
2707
+ if (params.full) return toolResult({
2299
2708
  ...run.data,
2300
2709
  steps: steps.data
2301
2710
  });
2711
+ const rows = steps.data;
2712
+ return toolResult({
2713
+ ...run.data,
2714
+ steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
2715
+ ...step,
2716
+ headline: summaryHeadline(rows[index].summary)
2717
+ }))
2718
+ });
2302
2719
  }));
2303
2720
  if (!client.hasCapability("products:write")) return;
2304
2721
  server.registerTool("taiga_stop_build", {
@@ -2341,8 +2758,66 @@ const REPOSITORY_SUMMARY_FIELDS = [
2341
2758
  ];
2342
2759
  /** How many of the latest runs to carry; taiga_list_builds pages the rest. */
2343
2760
  const BUILDS_IN_CONTEXT = 10;
2761
+ /** A run as the bundle carries it: enough to say what was built and where; taiga_get_build has the rest. */
2762
+ const CONTEXT_BUILD_FIELDS = [
2763
+ "id",
2764
+ "runNumber",
2765
+ "backlogItemId",
2766
+ "targetTitle",
2767
+ "status",
2768
+ "failedReason",
2769
+ "completedSteps",
2770
+ "totalSteps",
2771
+ "prUrl",
2772
+ "prState",
2773
+ "prMerged",
2774
+ "createdAt"
2775
+ ];
2776
+ /**
2777
+ * Characters of section text each of the two documents may bring by default.
2778
+ * Two of them plus the rest of the bundle stay well inside the 25k-token
2779
+ * tool result Claude Code accepts (a 67k-character bundle was refused); a
2780
+ * larger product's documents come cut, with the sections left out named.
2781
+ */
2782
+ const DOCUMENT_BUDGET = 12e3;
2783
+ /**
2784
+ * The document without each section's citations: on a reality document they
2785
+ * are the code evidence behind every claim and as long as the text itself,
2786
+ * which a session's opening read does not need. The full read keeps them.
2787
+ * Says whether any were dropped, so the bundle can tell the reader.
2788
+ */
2789
+ function withoutCitations(doc) {
2790
+ const sections = isRecord(doc.data) ? sectionsOf(doc.data) : null;
2791
+ if (!sections) return {
2792
+ doc,
2793
+ dropped: false
2794
+ };
2795
+ let dropped = false;
2796
+ const kept = Object.fromEntries(Object.entries(sections).map(([id, section]) => {
2797
+ if (!isRecord(section) || !("citations" in section)) return [id, section];
2798
+ dropped = true;
2799
+ const { citations: _citations, ...rest } = section;
2800
+ return [id, rest];
2801
+ }));
2802
+ return {
2803
+ doc: {
2804
+ ...doc,
2805
+ data: {
2806
+ ...doc.data,
2807
+ sections: kept
2808
+ }
2809
+ },
2810
+ dropped
2811
+ };
2812
+ }
2344
2813
  /** Pages of 100 the backlog part follows before it stops and says so. */
2345
2814
  const BACKLOG_PAGE_LIMIT = 10;
2815
+ /**
2816
+ * Open items the bundle carries, the newest of those read; past this it says truncated and
2817
+ * taiga_list_backlog_items pages the rest. Keeps a product with hundreds of
2818
+ * open initiatives inside the tool result the document budget is sized for.
2819
+ */
2820
+ const BACKLOG_IN_CONTEXT = 50;
2346
2821
  const PARTS = {
2347
2822
  factory: "factories:read",
2348
2823
  documents: "products:read",
@@ -2396,11 +2871,11 @@ async function factoryPart(client, factoryId) {
2396
2871
  }
2397
2872
  }
2398
2873
  /**
2399
- * The specification and architecture from the product's own timeline: in full
2400
- * by default, or as section ids when the caller wants a small bundle. Null,
2401
- * with a note, where unpublished.
2874
+ * The specification and architecture from the product's own timeline: their
2875
+ * whole sections up to a budget by default, in full or as section ids when
2876
+ * asked. Null, with a note, where unpublished.
2402
2877
  */
2403
- async function documentsPart(client, product, full) {
2878
+ async function documentsPart(client, product, depth) {
2404
2879
  const productId = String(product.id);
2405
2880
  const origin = defaultOriginFor(product);
2406
2881
  const reads = await Promise.allSettled(CONTEXT_DOCUMENT_TYPES.map((type) => client.getProductDocument(productId, type, origin)));
@@ -2410,8 +2885,26 @@ async function documentsPart(client, product, full) {
2410
2885
  const read = reads[index];
2411
2886
  if (read.status === "fulfilled") {
2412
2887
  const doc = stripProvenance(read.value.data);
2413
- if (full) out[type] = doc;
2414
- else {
2888
+ if (depth === "full") out[type] = doc;
2889
+ else if (depth === "budget") {
2890
+ const lean = withoutCitations(doc);
2891
+ if (lean.dropped) notes.push(`The ${origin} ${type} leaves out each section's citations (its code evidence); taiga_get_product_document carries them.`);
2892
+ const cut = budgetSections(lean.doc, DOCUMENT_BUDGET);
2893
+ if (!cut) {
2894
+ const small = JSON.stringify(lean.doc).length <= DOCUMENT_BUDGET;
2895
+ out[type] = small ? lean.doc : {
2896
+ version: doc.version,
2897
+ sections: null
2898
+ };
2899
+ if (!small) notes.push(`The ${origin} ${type} is not made of sections; read it with taiga_get_product_document.`);
2900
+ } else if (cut.omittedSections.length > 0) {
2901
+ out[type] = {
2902
+ ...cut.doc,
2903
+ omittedSections: cut.omittedSections
2904
+ };
2905
+ notes.push(`The ${origin} ${type} carries the whole sections that fit; read the ones under omittedSections with taiga_get_product_document, naming them in sections.`);
2906
+ } else out[type] = cut.doc;
2907
+ } else {
2415
2908
  const sections = sectionsOf(doc.data ?? {});
2416
2909
  if (sections) out[type] = {
2417
2910
  version: doc.version,
@@ -2457,11 +2950,13 @@ const OPEN_BACKLOG_STATUSES = ["backlog", "in_progress"];
2457
2950
  async function backlogPart(client, productId) {
2458
2951
  const open = /* @__PURE__ */ new Map();
2459
2952
  let truncated = false;
2953
+ let pageCapped = false;
2460
2954
  for (const status of OPEN_BACKLOG_STATUSES) {
2461
2955
  let cursor;
2462
2956
  for (let pageIndex = 0;; pageIndex += 1) {
2463
2957
  if (pageIndex === BACKLOG_PAGE_LIMIT) {
2464
2958
  truncated = true;
2959
+ pageCapped = true;
2465
2960
  break;
2466
2961
  }
2467
2962
  const result = await client.listBacklogItems(productId, {
@@ -2477,9 +2972,12 @@ async function backlogPart(client, productId) {
2477
2972
  cursor = result.nextCursor;
2478
2973
  }
2479
2974
  }
2975
+ const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
2976
+ if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
2480
2977
  return {
2481
- items: pickFields([...open.values()], BACKLOG_SUMMARY_FIELDS),
2482
- truncated
2978
+ items: itemRows(rows.slice(-50), BACKLOG_SUMMARY_FIELDS),
2979
+ truncated,
2980
+ ...pageCapped ? { note: "More than 1000 open items in one status: only the oldest of them were read, so the newest may be missing here; taiga_list_backlog_items pages the rest." } : {}
2483
2981
  };
2484
2982
  }
2485
2983
  /**
@@ -2495,7 +2993,7 @@ async function repositoriesPart(client, productId) {
2495
2993
  }
2496
2994
  async function buildsPart(client, productId) {
2497
2995
  const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
2498
- return pickFields(rows, BUILD_SUMMARY_FIELDS);
2996
+ return pickFields(rows, CONTEXT_BUILD_FIELDS);
2499
2997
  }
2500
2998
  async function productContext(client, product, options) {
2501
2999
  const productId = String(product.id);
@@ -2503,7 +3001,7 @@ async function productContext(client, product, options) {
2503
3001
  const notGranted = Object.keys(PARTS).filter((part) => !granted(part));
2504
3002
  const fetchers = {
2505
3003
  factory: () => factoryPart(client, String(product.factoryId)),
2506
- documents: () => documentsPart(client, product, options.fullDocuments),
3004
+ documents: () => documentsPart(client, product, options.documents),
2507
3005
  policies: () => policiesPart(client),
2508
3006
  backlog: () => backlogPart(client, productId),
2509
3007
  repositories: () => repositoriesPart(client, productId),
@@ -2523,23 +3021,35 @@ async function productContext(client, product, options) {
2523
3021
  else if (isApiFailure(result.reason)) unavailable[part] = unavailableReason(result.reason);
2524
3022
  else throw result.reason;
2525
3023
  });
2526
- return toolResult({
3024
+ const result = {
2527
3025
  ...bundle,
2528
3026
  notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
2529
3027
  unavailable,
2530
- readNext: "Read a policy in full with taiga_get_document, another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item and a run with taiga_get_build."
3028
+ readNext: "Read a policy in full with taiga_get_document, another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item (taiga_list_backlog_items pages a truncated backlog) and a run with taiga_get_build."
3029
+ };
3030
+ let characters = JSON.stringify(result, null, 2).length;
3031
+ for (let measured = -1; measured !== characters;) {
3032
+ measured = characters;
3033
+ characters = JSON.stringify({
3034
+ ...result,
3035
+ characters: measured
3036
+ }, null, 2).length;
3037
+ }
3038
+ return toolResult({
3039
+ ...result,
3040
+ characters
2531
3041
  });
2532
3042
  }
2533
3043
  function registerContextTools(server, client) {
2534
3044
  if (!client.hasCapability("products:read")) return;
2535
3045
  server.registerTool("taiga_get_product_context", {
2536
3046
  title: "Get product context",
2537
- description: "Everything a coding session starts from, in one call: the product and which document timeline is its own, its factory with the instructions in full, the specification and architecture from that timeline, every published policy as its section ids, the open backlog items, the linked repositories and the latest runs. Name the product by ID, slug or the git remote URL of the checkout. A part the API refused or failed is named under unavailable with what it said; a part the key cannot read is named under notGranted; neither is shown as empty. The two documents come in full, which on a large product is several thousand tokens; pass fullDocuments: false for their section ids only. Call this first, then read what you need in full.",
3047
+ description: `Everything a coding session starts from, in one call: the product and which document timeline is its own, its factory with the instructions in full, the specification and architecture from that timeline, every published policy as its section ids, the open backlog items, the linked repositories and the latest runs. Name the product by ID, slug or the git remote URL of the checkout. A part the API refused or failed is named under unavailable with what it said; a part the key cannot read is named under notGranted; neither is shown as empty. The two documents bring as many whole sections as fit, about ${DOCUMENT_BUDGET / 1e3}k characters each, and name the sections left out (omittedSections) for taiga_get_product_document to read; fullDocuments: true carries them whole, tens of thousands of tokens on a large product and more than some clients accept, and false carries their section ids only. characters says how large the result is. Call this first, then read what you need.`,
2538
3048
  inputSchema: {
2539
3049
  productId: productId.optional(),
2540
3050
  slug: slug.optional(),
2541
3051
  remoteUrl: remoteUrl.optional(),
2542
- fullDocuments: z.boolean().default(true).describe("Carry the specification and architecture in full (default) or as section ids only")
3052
+ fullDocuments: z.boolean().optional().describe("true carries the specification and architecture whole, false as section ids only; omit for the whole sections that fit a budget")
2543
3053
  },
2544
3054
  annotations: READ_ONLY
2545
3055
  }, (params) => runTool(async () => {
@@ -2549,7 +3059,8 @@ function registerContextTools(server, client) {
2549
3059
  "remoteUrl"
2550
3060
  ]);
2551
3061
  if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
2552
- return productContext(client, resolved.product, { fullDocuments: params.fullDocuments });
3062
+ const documents = params.fullDocuments === void 0 ? "budget" : params.fullDocuments ? "full" : "sectionIds";
3063
+ return productContext(client, resolved.product, { documents });
2553
3064
  }));
2554
3065
  }
2555
3066
  //#endregion
@@ -17187,7 +17698,7 @@ function registerPrompts(server, client) {
17187
17698
  "item you are implementing with taiga_get_backlog_item. Parts listed under notGranted or unavailable",
17188
17699
  "could not be read; say so rather than assuming they are empty.",
17189
17700
  "",
17190
- fence("json", (await productContext(client, resolved.product, { fullDocuments: true })).content[0]?.text ?? "{}")
17701
+ fence("json", (await productContext(client, resolved.product, { documents: "full" })).content[0]?.text ?? "{}")
17191
17702
  ].join("\n"));
17192
17703
  });
17193
17704
  if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_backlog_item", {
@@ -17379,17 +17890,357 @@ function registerResources(server, client) {
17379
17890
  });
17380
17891
  }
17381
17892
  //#endregion
17893
+ //#region src/tools/wait.ts
17894
+ const DEFAULT_WAIT_OPTIONS = { pollIntervalMs: 5e3 };
17895
+ /**
17896
+ * Under the 60 seconds many clients allow a call by default (the MCP SDK's
17897
+ * own request timeout), with room for the last read.
17898
+ */
17899
+ const DEFAULT_TIMEOUT_SECONDS = 50;
17900
+ const timeoutSeconds = z.number().int().min(1).max(600).optional().describe(`How long to wait at most (default ${DEFAULT_TIMEOUT_SECONDS}). Keep it under your client's tool-call limit; many stop a call at 60 seconds.`);
17901
+ function pause(ms, signal) {
17902
+ return new Promise((resolve) => {
17903
+ if (signal.aborted) return resolve();
17904
+ const timer = setTimeout(done, ms);
17905
+ function done() {
17906
+ clearTimeout(timer);
17907
+ signal.removeEventListener("abort", done);
17908
+ resolve();
17909
+ }
17910
+ signal.addEventListener("abort", done, { once: true });
17911
+ });
17912
+ }
17913
+ /** Failed reads in a row a wait rides out before it gives up and says why. */
17914
+ const TRANSIENT_FAILURES_TOLERATED = 3;
17915
+ /** A failure the next read may not hit: rate limited, the API erroring or unreachable. Not a refusal or a miss. */
17916
+ function isTransient(error) {
17917
+ if (isNetworkFailure(error)) return true;
17918
+ return error instanceof TaigaApiError && (error.status === 429 || error.status >= 500);
17919
+ }
17920
+ /**
17921
+ * Read until `judge` gives a verdict or the time is up. Every read is bound
17922
+ * by what is left of the timeout and by the call's cancellation, so a slow
17923
+ * API cannot hold the wait past either: a read cut short ends the wait with
17924
+ * the last state seen (none, when not even the first read came back). A
17925
+ * read that fails transiently is ridden out, a few in a row and the first
17926
+ * read included, so one 502 does not throw away minutes of waiting; a
17927
+ * refusal fails as it comes. Each
17928
+ * read that leaves the wait open reports progress when the client asked for
17929
+ * it, which also keeps a client that resets its timeout on progress from
17930
+ * cutting the wait short.
17931
+ */
17932
+ async function waitFor(read, judge, timeoutMs, options, progress) {
17933
+ const started = Date.now();
17934
+ const deadline = started + timeoutMs;
17935
+ let first = null;
17936
+ let now = null;
17937
+ let verdict = null;
17938
+ let failures = 0;
17939
+ for (let attempt = 0; !progress.signal.aborted; attempt += 1) {
17940
+ if (attempt > 0) {
17941
+ const left = deadline - Date.now();
17942
+ if (left <= 0) break;
17943
+ await pause(Math.min(options.pollIntervalMs, left), progress.signal);
17944
+ }
17945
+ const remaining = deadline - Date.now();
17946
+ if (progress.signal.aborted || remaining <= 0) break;
17947
+ const bound = AbortSignal.timeout(remaining);
17948
+ try {
17949
+ now = await read(AbortSignal.any([progress.signal, bound]));
17950
+ first ??= now;
17951
+ failures = 0;
17952
+ } catch (error) {
17953
+ if (bound.aborted || progress.signal.aborted) break;
17954
+ failures += 1;
17955
+ if (!isTransient(error) || failures >= TRANSIENT_FAILURES_TOLERATED) throw error;
17956
+ continue;
17957
+ }
17958
+ verdict = judge(now, first);
17959
+ if (verdict) break;
17960
+ if (attempt > 0 && progress.progressToken !== void 0) await progress.sendNotification({
17961
+ method: "notifications/progress",
17962
+ params: {
17963
+ progressToken: progress.progressToken,
17964
+ progress: Date.now() - started,
17965
+ total: timeoutMs,
17966
+ message: "Still waiting"
17967
+ }
17968
+ }).catch(() => void 0);
17969
+ }
17970
+ const waitedSeconds = Math.round((Date.now() - started) / 1e3);
17971
+ if (verdict) return {
17972
+ ...verdict,
17973
+ waitedSeconds,
17974
+ now
17975
+ };
17976
+ return {
17977
+ reached: false,
17978
+ stoppedBecause: progress.signal.aborted ? "cancelled" : "timeout",
17979
+ waitedSeconds,
17980
+ now
17981
+ };
17982
+ }
17983
+ const SETTLED_RUN = new Set(BUILD_RUN_TERMINAL_STATUSES);
17984
+ /**
17985
+ * A pull request that is open now. A run keeps its prUrl after the pull
17986
+ * request merges or closes, so the URL alone says one existed, not that it
17987
+ * is open; the mirrored state and the merged latch say which. An unknown
17988
+ * state (null, just opened) counts as open.
17989
+ */
17990
+ function prIsOpen(run) {
17991
+ return typeof run.prUrl === "string" && run.prUrl !== "" && run.prMerged !== true && run.prState !== "closed";
17992
+ }
17993
+ const ITEM_CONDITIONS = [
17994
+ "changed",
17995
+ "planned",
17996
+ "your_turn",
17997
+ "pr_open",
17998
+ "run_settled",
17999
+ "done"
18000
+ ];
18001
+ const latestBuildOf = (item) => item.lifecycle?.latestBuild ?? null;
18002
+ const lifecycleOf = (item) => item.lifecycle ?? {};
18003
+ /** What "changed" compares: where the item is, whose turn it is and how its latest run stands. */
18004
+ function itemSignature(item) {
18005
+ const lifecycle = lifecycleOf(item);
18006
+ const run = latestBuildOf(item);
18007
+ return JSON.stringify([
18008
+ item.state,
18009
+ item.planningStatus,
18010
+ item.needsHumanCause,
18011
+ lifecycle.stage,
18012
+ lifecycle.turn,
18013
+ lifecycle.cta,
18014
+ run?.id,
18015
+ run?.status,
18016
+ run?.completedSteps,
18017
+ run?.prUrl,
18018
+ run?.prState,
18019
+ run?.prMerged
18020
+ ]);
18021
+ }
18022
+ /**
18023
+ * Whether the initiative waits on a person now: yes, no, or unknown. The
18024
+ * lifecycle says `turn: 'you'` for every landed plan (`start_build`), but a
18025
+ * plan whose review gate is open is waiting for the Build slot, not for
18026
+ * anyone: the queue keeps such an entry active and starts it on its own. When
18027
+ * the gate cannot be resolved (no gate of its own, and the product's default
18028
+ * unreadable) the answer is unknown, and the wait says so rather than guess.
18029
+ */
18030
+ function personTurn(item, gate) {
18031
+ const lifecycle = lifecycleOf(item);
18032
+ if (lifecycle.turn !== "you") return "no";
18033
+ if (lifecycle.cta !== "start_build") return "yes";
18034
+ const own = typeof item.autoBuild === "boolean" ? item.autoBuild : null;
18035
+ const product = typeof gate.product === "boolean" ? gate.product : null;
18036
+ if (own === null && product === null) return "unknown";
18037
+ return resolveAutoBuild({
18038
+ initiative: own,
18039
+ product: product ?? true
18040
+ }) ? "no" : "yes";
18041
+ }
18042
+ function itemHolds(until, item, first, gate) {
18043
+ const lifecycle = lifecycleOf(item);
18044
+ const run = latestBuildOf(item);
18045
+ switch (until) {
18046
+ case "changed": return itemSignature(item) !== itemSignature(first);
18047
+ case "planned": return item.planningStatus === "planned";
18048
+ case "your_turn": return personTurn(item, gate) === "yes";
18049
+ case "pr_open": return run !== null && prIsOpen(run);
18050
+ case "run_settled": return run !== null && SETTLED_RUN.has(String(run.status));
18051
+ case "done": return lifecycle.stage === "done";
18052
+ }
18053
+ }
18054
+ function judgeItem(until, gate) {
18055
+ return (item, first) => {
18056
+ if (itemHolds(until, item, first, gate)) return { reached: true };
18057
+ if (until === "changed") return null;
18058
+ if (typeof item.needsHumanCause === "string") return {
18059
+ reached: false,
18060
+ stoppedBecause: `needs a person: ${item.needsHumanCause}`
18061
+ };
18062
+ const lifecycle = lifecycleOf(item);
18063
+ const turn = personTurn(item, gate);
18064
+ if (until !== "your_turn" && turn === "yes") return {
18065
+ reached: false,
18066
+ stoppedBecause: `waits on a person: ${String(lifecycle.cta ?? lifecycle.stage)}`
18067
+ };
18068
+ if (turn === "unknown") return {
18069
+ reached: false,
18070
+ stoppedBecause: "the plan has landed, and whether it waits for a review is unknown: the product's default could not be read (it needs products:read)"
18071
+ };
18072
+ if (until === "planned" && item.planningStatus === "failed") return {
18073
+ reached: false,
18074
+ stoppedBecause: "planning failed"
18075
+ };
18076
+ const stage = lifecycleOf(item).stage;
18077
+ if (stage === "canceled" || until !== "done" && stage === "done") return {
18078
+ reached: false,
18079
+ stoppedBecause: `the initiative is ${String(stage)}`
18080
+ };
18081
+ return null;
18082
+ };
18083
+ }
18084
+ /** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
18085
+ function itemReport(item) {
18086
+ const run = latestBuildOf(item);
18087
+ return {
18088
+ ...pickFields([item], [
18089
+ "id",
18090
+ "title",
18091
+ "state",
18092
+ "planningStatus",
18093
+ "needsHumanCause",
18094
+ "needsHumanDetail",
18095
+ "autoBuild"
18096
+ ])[0],
18097
+ lifecycle: pickFields([lifecycleOf(item)], [
18098
+ "turn",
18099
+ "stage",
18100
+ "cta",
18101
+ "planFailureCause"
18102
+ ])[0],
18103
+ latestBuild: run ? pickFields([run], [
18104
+ "id",
18105
+ "status",
18106
+ "completedSteps",
18107
+ "totalSteps",
18108
+ "prUrl",
18109
+ "prState",
18110
+ "prMerged",
18111
+ "actions"
18112
+ ])[0] : null,
18113
+ criteria: criteriaCounts(item)
18114
+ };
18115
+ }
18116
+ const BUILD_CONDITIONS = [
18117
+ "changed",
18118
+ "step_done",
18119
+ "pr_open",
18120
+ "settled"
18121
+ ];
18122
+ const RUN_REPORT_FIELDS = [
18123
+ "id",
18124
+ "backlogItemId",
18125
+ "targetTitle",
18126
+ "status",
18127
+ "failedReason",
18128
+ "completedSteps",
18129
+ "totalSteps",
18130
+ "prUrl",
18131
+ "prState",
18132
+ "prDraft",
18133
+ "prMerged",
18134
+ "actions"
18135
+ ];
18136
+ const runSignature = (run) => JSON.stringify([
18137
+ run.status,
18138
+ run.completedSteps,
18139
+ run.prUrl,
18140
+ run.prState,
18141
+ run.prMerged
18142
+ ]);
18143
+ function judgeBuild(until) {
18144
+ return (run, first) => {
18145
+ if (until === "changed" ? runSignature(run) !== runSignature(first) : until === "step_done" ? Number(run.completedSteps ?? 0) > Number(first.completedSteps ?? 0) : until === "pr_open" ? prIsOpen(run) : SETTLED_RUN.has(String(run.status))) return { reached: true };
18146
+ if (until !== "changed" && SETTLED_RUN.has(String(run.status))) return {
18147
+ reached: false,
18148
+ stoppedBecause: `the run is ${String(run.status)}`
18149
+ };
18150
+ return null;
18151
+ };
18152
+ }
18153
+ function registerWaitTools(server, client, options) {
18154
+ if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_backlog_item", {
18155
+ title: "Wait for an initiative",
18156
+ 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.",
18157
+ inputSchema: {
18158
+ itemId: z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items"),
18159
+ until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
18160
+ timeoutSeconds
18161
+ },
18162
+ annotations: READ_ONLY
18163
+ }, (params, extra) => runTool(async () => {
18164
+ const gate = { product: void 0 };
18165
+ const read = async (signal) => {
18166
+ const item = (await client.getBacklogItem(params.itemId, signal)).data;
18167
+ if (gate.product === void 0 && typeof item.autoBuild !== "boolean") gate.product = client.hasCapability("products:read") ? await client.getProduct(String(item.productId), signal).then((p) => {
18168
+ const value = p.data.autoBuild;
18169
+ return typeof value === "boolean" ? value : true;
18170
+ }).catch((error) => {
18171
+ if (error instanceof TaigaApiError && (error.status === 403 || error.status === 404)) return null;
18172
+ throw error;
18173
+ }) : null;
18174
+ return item;
18175
+ };
18176
+ const { now, ...outcome } = await waitFor(read, judgeItem(params.until, gate), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
18177
+ progressToken: extra._meta?.progressToken,
18178
+ sendNotification: extra.sendNotification,
18179
+ signal: extra.signal
18180
+ });
18181
+ return toolResult({
18182
+ until: params.until,
18183
+ ...outcome,
18184
+ item: now ? itemReport(now) : null
18185
+ });
18186
+ }));
18187
+ if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_build", {
18188
+ title: "Wait for a run",
18189
+ 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.",
18190
+ inputSchema: {
18191
+ buildId: z.string().uuid().describe("Build run ID -- get from taiga_list_builds"),
18192
+ until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
18193
+ timeoutSeconds
18194
+ },
18195
+ annotations: READ_ONLY
18196
+ }, (params, extra) => runTool(async () => {
18197
+ const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.buildId, signal)).data, judgeBuild(params.until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
18198
+ progressToken: extra._meta?.progressToken,
18199
+ sendNotification: extra.sendNotification,
18200
+ signal: extra.signal
18201
+ });
18202
+ return toolResult({
18203
+ until: params.until,
18204
+ ...outcome,
18205
+ run: now ? pickFields([now], RUN_REPORT_FIELDS)[0] : null
18206
+ });
18207
+ }));
18208
+ }
18209
+ //#endregion
17382
18210
  //#region src/tools/index.ts
17383
18211
  /**
18212
+ * Every tool refuses an argument it does not take. A plain object schema
18213
+ * strips one silently: a review note passed to a tool with no place for it
18214
+ * vanished and the call still succeeded, and a misspelt option quietly
18215
+ * became its default. Strict, the call fails with "Unrecognized key" and the
18216
+ * model learns the tool's real shape. Applied here, once, so no tool can be
18217
+ * registered without it.
18218
+ */
18219
+ function withStrictInputs(server) {
18220
+ const register = server.registerTool.bind(server);
18221
+ server.registerTool = ((name, config, callback) => {
18222
+ const shape = config.inputSchema;
18223
+ const isRawShape = typeof shape === "object" && shape !== null && !("_zod" in shape);
18224
+ return register(name, isRawShape ? {
18225
+ ...config,
18226
+ inputSchema: z.strictObject(shape)
18227
+ } : config, callback);
18228
+ });
18229
+ return server;
18230
+ }
18231
+ /**
17384
18232
  * Register all MCP tools, resources and prompts based on the API key's effective capabilities.
17385
18233
  * Only tools matching the key's scopes are registered -- the AI assistant
17386
18234
  * never sees tools it can't use.
17387
18235
  */
17388
- function registerAllTools(server, client) {
18236
+ function registerAllTools(server, client, options = {}) {
18237
+ withStrictInputs(server);
17389
18238
  registerProductTools(server, client);
17390
18239
  registerDocumentTools(server, client);
17391
18240
  registerBacklogTools(server, client);
17392
18241
  registerBuildTools(server, client);
18242
+ registerQueueTools(server, client);
18243
+ registerWaitTools(server, client, options.wait ?? DEFAULT_WAIT_OPTIONS);
17393
18244
  registerContextTools(server, client);
17394
18245
  registerResources(server, client);
17395
18246
  registerPrompts(server, client);
@@ -17435,12 +18286,19 @@ const SERVER_INSTRUCTIONS = [
17435
18286
  "every product the key can see, and active: true the ones executing now.",
17436
18287
  "",
17437
18288
  "**Operating the line** (taiga_create_backlog_item / taiga_queue_backlog_item / taiga_move_backlog_item /",
17438
- "taiga_resume_backlog_item / taiga_get_plan / taiga_approve_plan / taiga_reject_plan / taiga_stop_build /",
17439
- "taiga_resume_build): create an initiative, put it in the queue the coding agent works, move it on the",
17440
- "board, read and approve or reject its plan, stop or resume a run. Queueing, approving and resuming start",
17441
- "paid agent work that pushes commits and opens pull requests; stopping interrupts it. Tell the person what",
17442
- "you are about to start or stop and wait for their yes before calling these; a client that prompts on",
17443
- "destructive tools does that for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
18289
+ "taiga_resume_backlog_item / taiga_set_auto_build / taiga_get_plan / taiga_approve_plan / taiga_reject_plan /",
18290
+ "taiga_stop_build / taiga_resume_build): create an initiative, put it in the queue the coding agent works,",
18291
+ "move it on the board, decide whether its plan waits for a review (autoBuild: false) or is built at once, read",
18292
+ "and approve or reject its plan, stop or resume a run. Queueing, approving and resuming start paid agent work",
18293
+ "that pushes commits and opens pull requests; stopping interrupts it. Tell the person what you are about to",
18294
+ "start or stop and wait for their yes before calling these; a client that prompts on destructive tools does",
18295
+ "that for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
18296
+ "",
18297
+ "**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build): the queue",
18298
+ "read says what the line is waiting on (a plan to review, a pull request to merge, a halt and what lifts",
18299
+ "it). Planning takes minutes and a build up to an hour: wait with the wait tools rather than reading the",
18300
+ "same item again and again; each returns when the point is reached, when only a person can move it on,",
18301
+ "or at its timeout (call again to keep waiting).",
17444
18302
  "",
17445
18303
  "**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
17446
18304
  "provenance fields you can read past: `source` says where the text originally came from (template:",
@@ -17452,7 +18310,8 @@ const SERVER_INSTRUCTIONS = [
17452
18310
  "",
17453
18311
  "Workflow: start with taiga_get_product_context, naming the product by its git remote URL inside a checkout",
17454
18312
  "(git remote get-url origin), else by slug or ID from taiga_list_products. It returns the product, the",
17455
- "factory instructions, the specification and architecture, every policy as its section ids, the open backlog",
18313
+ "factory instructions, the specification and architecture as the whole sections that fit (the rest named for",
18314
+ "taiga_get_product_document to read by section), every policy as its section ids, the open backlog",
17456
18315
  "items, the repositories and the latest runs in one call, and names any part it could not read. Then read",
17457
18316
  "what you need in full: a policy with taiga_get_document, a backlog item with taiga_get_backlog_item.",
17458
18317
  "Without it, use taiga_list_factories and taiga_list_products, then read the factory instructions,",