@taiga-ai/mcp-server 0.12.0-dev.421 → 0.13.0-dev.422

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
@@ -211,6 +211,21 @@ var TaigaClient = class {
211
211
  const qs = params.toString();
212
212
  return this.request("GET", `/products/${seg(productId)}/documents${qs ? `?${qs}` : ""}`);
213
213
  }
214
+ /**
215
+ * Stop or start the product's line. Stopping halts pulls and builds and
216
+ * stops the plan and build in flight; starting picks the interrupted work
217
+ * back up.
218
+ */
219
+ async setLineStopped(productId, stopped) {
220
+ return this.request("PUT", `/products/${seg(productId)}/line`, { stopped });
221
+ }
222
+ /** Ask an agent for work: the job is queued and runs in the background. */
223
+ async createAgentJob(body) {
224
+ return this.request("POST", "/agent-jobs", body);
225
+ }
226
+ async getAgentJob(id, signal) {
227
+ return this.request("GET", `/agent-jobs/${seg(id)}`, void 0, { signal });
228
+ }
214
229
  async getProductDocument(productId, type, origin) {
215
230
  const qs = origin ? `?${new URLSearchParams({ origin })}` : "";
216
231
  return this.request("GET", `/products/${seg(productId)}/documents/${seg(type)}${qs}`);
@@ -229,12 +244,6 @@ var TaigaClient = class {
229
244
  async getBacklogItem(itemId, signal) {
230
245
  return this.request("GET", `/backlog/${seg(itemId)}`, void 0, { signal });
231
246
  }
232
- async createBacklogItem(productId, body) {
233
- return this.request("POST", `/products/${seg(productId)}/backlog`, {
234
- ...body,
235
- productId
236
- });
237
- }
238
247
  /** A person's move on the line board: the column dropped into, and what to follow there. */
239
248
  async transitionInitiative(itemId, body) {
240
249
  return this.request("PUT", `/backlog/${seg(itemId)}/state`, body);
@@ -283,9 +292,6 @@ var TaigaClient = class {
283
292
  async stopBuildRun(id) {
284
293
  return this.request("POST", `/builds/${seg(id)}/stop`);
285
294
  }
286
- async resumeBuildRun(id) {
287
- return this.request("POST", `/builds/${seg(id)}/resume`);
288
- }
289
295
  async listBuildRunSteps(id) {
290
296
  return this.request("GET", `/builds/${seg(id)}/steps`);
291
297
  }
@@ -329,13 +335,6 @@ const READ_ONLY = {
329
335
  idempotentHint: true,
330
336
  openWorldHint: false
331
337
  };
332
- /** A write that adds or edits a row and starts nothing: a created initiative, a moved one. */
333
- const WRITE = {
334
- readOnlyHint: false,
335
- destructiveHint: false,
336
- idempotentHint: false,
337
- openWorldHint: false
338
- };
339
338
  /**
340
339
  * A write that starts or stops paid work, or opens the door to it: queueing
341
340
  * an initiative, approving a plan, resuming or stopping a run. Each starts
@@ -347,6 +346,14 @@ const OPERATE = {
347
346
  idempotentHint: false,
348
347
  openWorldHint: false
349
348
  };
349
+ /**
350
+ * An OPERATE write that sets a value rather than adding one: the same call
351
+ * twice leaves things as one call does (a review gate set, a line paused).
352
+ */
353
+ const OPERATE_SETTING = {
354
+ ...OPERATE,
355
+ idempotentHint: true
356
+ };
350
357
  //#endregion
351
358
  //#region src/tools/inputs.ts
352
359
  /** The inputs several tools share, so a product or a page is named the same way everywhere. */
@@ -617,15 +624,7 @@ const PR_CLOSING_STATES = [
617
624
  "todo",
618
625
  "canceled"
619
626
  ];
620
- /**
621
- * What `PUT /backlog/:id/state` accepts: the drops, plus `plan`.
622
- *
623
- * `plan` is not a column a person drops into any more, but it is still a state
624
- * the endpoint takes -- it is how the Build station is told to start over
625
- * ("Plan again"). One endpoint for both because they are the same write: the
626
- * initiative's state, decided by a person.
627
- */
628
- const INITIATIVE_TRANSITION_STATES = [...USER_DROP_STATES, "plan"];
627
+ [...USER_DROP_STATES];
629
628
  /** Longest rejection comment on a plan (PRO-1324): a paragraph of direction,
630
629
  * small enough that the prompt section it becomes stays a section. */
631
630
  const PLAN_REJECTION_COMMENT_MAX_CHARS = 2e3;
@@ -965,6 +964,91 @@ const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDispositio
965
964
  */
966
965
  const BUILD_RUN_TERMINAL_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
967
966
  [...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
967
+ //#endregion
968
+ //#region ../../packages/types/dist/agents.js
969
+ /**
970
+ * Why a PROMPTED `generate_backlog` job planned nothing.
971
+ *
972
+ * A prompted add asks the planner a question, and "no new initiative" is one of
973
+ * its honest answers -- the backlog already covers the request, the request is
974
+ * not a piece of work, or it is outside what this project builds. None of those
975
+ * is a failed job, so each arrives as a completed one carrying the reason.
976
+ *
977
+ * The INITIAL bulk generate has no prompt and no such answer: an empty backlog
978
+ * there is a broken run and still fails.
979
+ */
980
+ const BACKLOG_GENERATION_EMPTY_OUTCOMES = [
981
+ "already_covered",
982
+ "invalid_request",
983
+ "out_of_scope",
984
+ "needs_decision"
985
+ ];
986
+ function isRecord$1(value) {
987
+ return typeof value === "object" && value !== null;
988
+ }
989
+ function isStringOrNull(value) {
990
+ return value === null || typeof value === "string";
991
+ }
992
+ function isDecisionReason(value) {
993
+ if (!isRecord$1(value)) return false;
994
+ const { policyCode, quotedSpan, conflict, alternative, overridePath } = value;
995
+ return [
996
+ policyCode,
997
+ quotedSpan,
998
+ conflict,
999
+ alternative,
1000
+ overridePath
1001
+ ].every((f) => typeof f === "string");
1002
+ }
1003
+ function isCoveringInitiative(value) {
1004
+ return isRecord$1(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.title === "string" && typeof value.priority === "number";
1005
+ }
1006
+ /**
1007
+ * Narrow an `agent_jobs.result` blob to {@link BacklogGenerationEmptyResult}.
1008
+ *
1009
+ * The blob is whatever the worker stored, and the reader navigates on
1010
+ * `coveredInitiative.id` and renders `message`, so EVERY field is checked: a
1011
+ * blob that only has the right `outcome` is not this shape, and narrowing it
1012
+ * would hand the page an undefined id to navigate to.
1013
+ */
1014
+ function isBacklogGenerationEmptyResult(value) {
1015
+ if (!isRecord$1(value)) return false;
1016
+ const { outcome, message, coveredByPriority, coveredById, coveredInitiative, reason } = value;
1017
+ return typeof outcome === "string" && BACKLOG_GENERATION_EMPTY_OUTCOMES.includes(outcome) && isStringOrNull(message) && (coveredByPriority === null || typeof coveredByPriority === "number" && Number.isInteger(coveredByPriority)) && isStringOrNull(coveredById) && (coveredInitiative === null || isCoveringInitiative(coveredInitiative)) && (reason === null || reason === void 0 || isDecisionReason(reason));
1018
+ }
1019
+ /**
1020
+ * The initiatives a `generate_backlog` job created, from its stored result
1021
+ * (PRO-2483), or null when the result does not say: a job finished before the
1022
+ * worker recorded them, a verdict that created nothing, or a blob that is not
1023
+ * this shape. Every entry must be a non-empty string, or none is trusted --
1024
+ * the reader navigates on them.
1025
+ */
1026
+ function createdInitiativeIdsOf(result) {
1027
+ if (!isRecord$1(result)) return null;
1028
+ const ids = result.createdInitiativeIds;
1029
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) return null;
1030
+ return ids;
1031
+ }
1032
+ /**
1033
+ * The initiatives a `generate_backlog` run added, each with the title it was
1034
+ * created with, as the worker writes them on `agent_jobs.result.createdInitiatives`
1035
+ * (PRO-2496), in the order they were created: what a reader needs to NAME
1036
+ * them, where {@link createdInitiativeIdsOf} (PRO-2483) only links them.
1037
+ *
1038
+ * Null when the result carries no such list: a run from before PRO-2496, which
1039
+ * may still carry the ids alone, a run that added nothing (see
1040
+ * {@link isBacklogGenerationEmptyResult}), or a run still going. A list with a malformed entry is null too rather than partly
1041
+ * read, so a reader never links an undefined id.
1042
+ */
1043
+ function createdInitiativesOf(result) {
1044
+ if (!isRecord$1(result)) return null;
1045
+ const list = result.createdInitiatives;
1046
+ if (!Array.isArray(list) || list.length === 0) return null;
1047
+ return list.every((entry) => isRecord$1(entry) && typeof entry.id === "string" && entry.id.length > 0 && typeof entry.title === "string") ? list.map((entry) => ({
1048
+ id: entry.id,
1049
+ title: entry.title
1050
+ })) : null;
1051
+ }
968
1052
  [...TURN_EVENT_TRIGGERS];
969
1053
  //#endregion
970
1054
  //#region ../../packages/types/dist/maintenance.js
@@ -2163,6 +2247,105 @@ function registerDocumentTools(server, client) {
2163
2247
  }
2164
2248
  }
2165
2249
  //#endregion
2250
+ //#region src/tools/jobs.ts
2251
+ /** What a job says about itself: its kind, how far it got, and why it failed. */
2252
+ const JOB_FIELDS = [
2253
+ "id",
2254
+ "type",
2255
+ "documentType",
2256
+ "targetId",
2257
+ "status",
2258
+ "progress",
2259
+ "progressMessage",
2260
+ "error",
2261
+ "queuePosition",
2262
+ "createdAt",
2263
+ "startedAt",
2264
+ "completedAt",
2265
+ "cancelledAt"
2266
+ ];
2267
+ /** A job that will not change again: it finished, failed, or was cancelled. */
2268
+ const SETTLED_JOB_STATUSES = /* @__PURE__ */ new Set([
2269
+ "completed",
2270
+ "failed",
2271
+ "cancelled"
2272
+ ]);
2273
+ /**
2274
+ * What an initiative request came to, read off its finished job: the
2275
+ * initiatives it added, or the answer that added none (the backlog already
2276
+ * covers it, it is not work to build, it is outside the product, or one of
2277
+ * the organization's policies does not allow it as asked, with the policy's
2278
+ * own words and the way to decide otherwise). Null while the job runs, and
2279
+ * for a job that is not an initiative request.
2280
+ */
2281
+ function requestOutcome(job) {
2282
+ if (job.type !== "generate_backlog" || job.status !== "completed") return null;
2283
+ const created = createdInitiativesOf(job.result);
2284
+ if (created) return {
2285
+ outcome: "created",
2286
+ initiatives: created
2287
+ };
2288
+ const ids = createdInitiativeIdsOf(job.result);
2289
+ if (ids && ids.length > 0) return {
2290
+ outcome: "created",
2291
+ initiatives: ids.map((id) => ({ id }))
2292
+ };
2293
+ if (isBacklogGenerationEmptyResult(job.result)) {
2294
+ const result = job.result;
2295
+ return {
2296
+ outcome: result.outcome,
2297
+ message: result.message,
2298
+ coveredBy: result.coveredInitiative,
2299
+ policy: result.reason ?? null
2300
+ };
2301
+ }
2302
+ return {
2303
+ outcome: "unknown",
2304
+ note: "The job finished without a result this server can read."
2305
+ };
2306
+ }
2307
+ /** A job as the tools hand it over: its own fields, and for an initiative request, what came of it. */
2308
+ function jobReport(job) {
2309
+ const outcome = requestOutcome(job);
2310
+ return {
2311
+ ...pickFields([job], JOB_FIELDS)[0],
2312
+ ...outcome ? { outcome } : {}
2313
+ };
2314
+ }
2315
+ function registerJobTools(server, client) {
2316
+ if (!client.hasCapability("products:read")) return;
2317
+ server.registerTool("taiga_get_job", {
2318
+ title: "Get an agent job",
2319
+ description: "Read one agent job: background work a person asked an agent for, such as an initiative request (taiga_request_initiatives). It says where the job is (pending, processing, completed, failed, cancelled), its progress and any error. A finished initiative request also says what came of it: the initiatives it added, or why it added none (already covered by an existing initiative, not readable as work, outside the product, or not allowed by a policy, with the policy, the line it runs into and the way to decide otherwise). taiga_wait_for_job waits for a job to finish.",
2320
+ inputSchema: { jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it") },
2321
+ annotations: READ_ONLY
2322
+ }, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
2323
+ }
2324
+ //#endregion
2325
+ //#region src/tools/request-prompt.ts
2326
+ /**
2327
+ * The planner's prompt for an initiative request, built as the web builds it
2328
+ * (apps/web/src/features/loops/delivery/components/planning/generateBacklogJob.ts):
2329
+ * title and description trimmed, the description's inline-image markdown
2330
+ * removed and the blank lines it leaves collapsed, joined by a blank line.
2331
+ * The planner reads `userPrompt` and nothing else of the request, so the same
2332
+ * words must make the same prompt from either surface.
2333
+ *
2334
+ * The image pattern is the web's (inline-image-doc-ids.ts), which keeps it in
2335
+ * lockstep with the API's `extractInlineImageDocIds`: the `![...](...)` form,
2336
+ * an exact UUID, and a terminator after `/image`.
2337
+ */
2338
+ const INLINE_IMAGE_RE = /!\[(?:[^\]\\]|\\.)*\]\([^)]*?\/custom-documents\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\/image(?=[)?#\s])[^)]*\)/g;
2339
+ /** The description without its inline images, as the web strips it. */
2340
+ function stripInlineImages(markdown) {
2341
+ return markdown.replace(INLINE_IMAGE_RE, "").replace(/\n{3,}/g, "\n\n").trim();
2342
+ }
2343
+ /** The prompt for a title and a description; undefined when both are empty, which asks for a suggestion. */
2344
+ function requestPrompt(title, description) {
2345
+ const body = stripInlineImages((description ?? "").trim());
2346
+ return [(title ?? "").trim(), body].filter(Boolean).join("\n\n") || void 0;
2347
+ }
2348
+ //#endregion
2166
2349
  //#region src/tools/queue.ts
2167
2350
  /** How many entries the read names; past this an entry keeps its backlogItemId only. */
2168
2351
  const NAMED_ENTRIES = 25;
@@ -2220,7 +2403,8 @@ function waitingOn(rows) {
2220
2403
  * rather than failing the queue.
2221
2404
  */
2222
2405
  async function queueView(client, product, options = { name: "all" }) {
2223
- const entries = (await client.listQueue(product)).data.filter((entry) => entry.state !== "settled");
2406
+ const [queue, paused] = await Promise.all([client.listQueue(product), linePausedSince(client, product)]);
2407
+ const entries = queue.data.filter((entry) => entry.state !== "settled");
2224
2408
  const canName = client.hasCapability("backlog:read");
2225
2409
  const blocking = blockingIndex(entries, (entry) => entry.state);
2226
2410
  const toName = !canName ? [] : options.name === "all" ? entries.slice(0, NAMED_ENTRIES).map((_, index) => index) : blocking >= 0 ? [blocking] : [];
@@ -2271,20 +2455,52 @@ async function queueView(client, product, options = { name: "all" }) {
2271
2455
  return row;
2272
2456
  });
2273
2457
  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.";
2458
+ const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
2274
2459
  return {
2460
+ linePaused: paused === null ? null : typeof paused === "string",
2275
2461
  entries: rows,
2276
- waitingOn: waitingOn(rows),
2462
+ waitingOn: line ?? waitingOn(rows),
2277
2463
  ...note ? { note } : {}
2278
2464
  };
2279
2465
  }
2466
+ /**
2467
+ * When the product's line was paused, false when it runs, null when the
2468
+ * product cannot be read (the queue is still worth reading without it).
2469
+ */
2470
+ async function linePausedSince(client, product) {
2471
+ try {
2472
+ const row = (await client.getProduct(product)).data;
2473
+ return typeof row.lineStoppedAt === "string" ? row.lineStoppedAt : false;
2474
+ } catch (error) {
2475
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
2476
+ throw error;
2477
+ }
2478
+ }
2280
2479
  function registerQueueTools(server, client) {
2281
2480
  if (!client.hasCapability("products:read")) return;
2282
2481
  server.registerTool("taiga_get_queue", {
2283
2482
  title: "Get the product queue",
2284
- 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.",
2483
+ 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.",
2285
2484
  inputSchema: { productId },
2286
2485
  annotations: READ_ONLY
2287
2486
  }, (params) => runTool(async () => toolResult(await queueView(client, params.productId))));
2487
+ if (!client.hasCapability("products:write")) return;
2488
+ server.registerTool("taiga_pause_line", {
2489
+ title: "Pause or resume a product's line",
2490
+ description: "Pause the product's whole line, or resume it. Pausing cancels the plan being written and stops the build in flight where it stands (both are flagged), and nothing more is planned or built; initiatives keep their places. Resuming plans the interrupted initiative again from scratch, carries the interrupted build on from its first incomplete step, and the line picks up where it was; a build you had stopped yourself before pausing stays stopped. Idempotent. Pausing interrupts work and resuming starts it: read taiga_get_queue first to see what it would interrupt, and ask the person.",
2491
+ inputSchema: {
2492
+ productId,
2493
+ paused: z.boolean().describe("true pauses the line, false resumes it")
2494
+ },
2495
+ annotations: OPERATE_SETTING
2496
+ }, (params) => runTool(async () => {
2497
+ const row = (await client.setLineStopped(params.productId, params.paused)).data;
2498
+ return toolResult({
2499
+ productId: row.id,
2500
+ linePaused: typeof row.lineStoppedAt === "string",
2501
+ lineStoppedAt: row.lineStoppedAt ?? null
2502
+ });
2503
+ }));
2288
2504
  }
2289
2505
  //#endregion
2290
2506
  //#region src/tools/backlogs.ts
@@ -2352,23 +2568,32 @@ const outcomeOf = (item) => {
2352
2568
  const outcome = (item) => toolResult(outcomeOf(item));
2353
2569
  /**
2354
2570
  * Each tool is registered under the capability its route requires, so a
2355
- * key sees exactly what it may call: reads on backlog:read, the create and
2356
- * the board move on backlog:write, queueing on both backlog:write and
2357
- * products:write (the state route checks both for a drop into queue), the
2358
- * resume and the review gate on products:write alone, as their routes are
2359
- * (the item ID they take also comes back from a create or a queue), and the plan verdicts on
2360
- * products:write together with backlog:read, because the plan ID they take
2361
- * comes only from the plan read: a key that cannot read plans has nothing
2362
- * to feed them.
2571
+ * key sees exactly what it may call: reads on backlog:read; the board move on
2572
+ * backlog:write; queueing on both backlog:write and products:write (the
2573
+ * state route checks both for a drop into queue); an initiative request
2574
+ * (an agent job), the resume and the review gate on products:write alone, as
2575
+ * their routes are (the item ID they take also comes back from a request or a
2576
+ * queue); the plan verdicts on products:write together with backlog:read,
2577
+ * because the plan ID they take comes only from the plan read; and Plan again
2578
+ * on the state route's two (backlog:write, then products:write for a move
2579
+ * into plan) together with backlog:read, because it reads the initiative's
2580
+ * place at the Build station first.
2581
+ *
2582
+ * The tools follow how Taiga is meant to be used (docs.tai.ga, Deliver):
2583
+ * new work is described and an agent writes the initiative; the queue is
2584
+ * the only way to start work; a run is stopped by moving its initiative out
2585
+ * of Build and carried on from the initiative.
2363
2586
  */
2364
2587
  function registerBacklogTools(server, client) {
2365
2588
  const may = (capability) => client.hasCapability(capability);
2366
2589
  if (may("backlog:read")) registerBacklogReads(server, client);
2367
- if (may("backlog:write")) registerBacklogWrites(server, client);
2590
+ if (may("backlog:write")) registerMove(server, client);
2591
+ if (may("products:write")) registerRequest(server, client);
2368
2592
  if (may("backlog:write") && may("products:write")) registerQueue(server, client);
2369
2593
  if (may("products:write")) registerResume(server, client);
2370
2594
  if (may("products:write")) registerReviewGate(server, client);
2371
2595
  if (may("backlog:read") && may("products:write")) registerPlanVerdicts(server, client);
2596
+ if (may("backlog:read") && may("backlog:write") && may("products:write")) registerPlanAgain(server, client);
2372
2597
  }
2373
2598
  function registerBacklogReads(server, client) {
2374
2599
  server.registerTool("taiga_list_backlog_items", {
@@ -2428,46 +2653,65 @@ function registerBacklogReads(server, client) {
2428
2653
  });
2429
2654
  }));
2430
2655
  }
2431
- function registerBacklogWrites(server, client) {
2432
- server.registerTool("taiga_create_backlog_item", {
2433
- title: "Create backlog item",
2434
- description: "Create an initiative in the Backlog column of a product: a title and a brief in Markdown (the problem, what it looks like, what it covers and leaves alone), its category, priority and estimate. It starts nothing: the planner reads it only once it is queued (taiga_queue_backlog_item) or moved to Plan (taiga_move_backlog_item). Returns the new item with its ID.",
2656
+ function registerRequest(server, client) {
2657
+ server.registerTool("taiga_request_initiatives", {
2658
+ title: "Request new initiatives",
2659
+ description: "Add work the way Taiga adds it: describe it, and an agent turns the request into initiatives in the product's Backlog. The agent reads the product documents and its repository, expands the request into a full brief, and sizes it: one piece of work stays one initiative, a program of work comes back as an ordered set with its dependencies linked. It can also add nothing, and says why: an existing initiative already covers the request, the request is not readable as work, it is outside what the product builds, or one of the organization's policies does not allow it as asked (named, with the line it runs into and the way to decide otherwise). Leave both title and description out and the agent suggests the one next initiative the product most needs. It runs in the background, a few minutes: this returns the job, and taiga_wait_for_job or taiga_get_job (both need products:read) say what came of it. One request per product at a time (CONFLICT while one runs). Inline images in the description are left out; the web is where a request carries images. It starts nothing on the line: new initiatives land in Backlog. Runs an agent; ask the person first.",
2435
2660
  inputSchema: {
2436
2661
  productId,
2437
- title: z.string().min(1).max(500).describe("The initiative title, as it appears on the board"),
2438
- description: z.string().min(1).max(2e4).describe("The brief in Markdown: the problem, what it looks like, what it covers and leaves alone"),
2439
- category: z.enum([
2440
- "feature",
2441
- "bug",
2442
- "tech_debt",
2443
- "improvement"
2444
- ]).optional().describe("What kind of work (default feature)"),
2445
- priority: z.number().int().min(1).optional().describe("1 is the most urgent (default 3)"),
2446
- estimate: z.number().int().min(0).optional().describe("Story points, 0 for unestimated (default 0)")
2662
+ title: z.string().max(500).optional().describe("What the work is, in a line"),
2663
+ description: z.string().max(2e4).optional().describe("The request in Markdown: the problem, what it should look like, what it covers and leaves alone")
2447
2664
  },
2448
- annotations: WRITE
2665
+ annotations: OPERATE
2449
2666
  }, (params) => runTool(async () => {
2450
- const { productId: id, ...body } = params;
2451
- return outcome((await client.createBacklogItem(id, body)).data);
2667
+ const userPrompt = requestPrompt(params.title, params.description);
2668
+ const job = (await client.createAgentJob({
2669
+ scope: "product",
2670
+ type: "generate_backlog",
2671
+ targetId: params.productId,
2672
+ ...userPrompt ? { metadata: { userPrompt } } : {}
2673
+ })).data;
2674
+ return toolResult(jobReport(job));
2452
2675
  }));
2676
+ }
2677
+ function registerMove(server, client) {
2453
2678
  server.registerTool("taiga_move_backlog_item", {
2454
2679
  title: "Move backlog item on the board",
2455
- description: `Move an initiative to a column, or order it within one: backlog and todo are the idle columns; queue puts it in the line (taiga_queue_backlog_item says more); plan starts planning now; done and canceled take it off the line. 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). The initiative state machine refuses a move that is not allowed (CONFLICT: build cannot be dropped into, an initiative being planned cannot leave plan). A move into queue or plan starts paid work and needs products:write on the key; a key without it is refused here. Ask the person before a move that starts work (queue, plan) or closes a pull request (${PR_CLOSING_STATES.join(", ")} from build). archived: true files the initiative where it stands (pass its current column as state; from any column off the line, never from plan or build) and archived: false brings it back; an archived initiative is left out of lists unless includeArchived asks for it.`,
2680
+ description: `Move an initiative on the board, or order it within a column: backlog and todo are yours and the line never moves an initiative out of them; queue puts it in the line (taiga_queue_backlog_item says more); done and canceled finish it by hand. The queue is the only way to start work: there is no "plan this now", and nothing is moved into Build by a person. Moving an initiative out of Build stops the plan being written or the run in flight: that is how work is stopped, and it throws that work away. A move to ${PR_CLOSING_STATES.join(", ")} CLOSES a pull request the initiative is still waiting on (a move to queue keeps it, and done leaves it to be merged); Taiga leaves a comment on the pull request saying why. The state machine refuses a move that is not allowed (CONFLICT). A move into queue starts paid work and needs products:write. Ask the person before a move that starts work, stops work or closes a pull request. archived: true files the initiative where it stands (pass its current column as state; never from the Build station, plan or build) and archived: false brings it back; archived work is left out of lists unless includeArchived asks for it.`,
2456
2681
  inputSchema: {
2457
2682
  itemId,
2458
- state: z.enum(INITIATIVE_TRANSITION_STATES).describe("The column to move into"),
2683
+ state: z.enum(USER_DROP_STATES).describe("The column to move into"),
2459
2684
  afterId: z.string().uuid().nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
2460
2685
  archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
2461
2686
  },
2462
2687
  annotations: OPERATE
2463
2688
  }, (params) => runTool(async () => {
2464
2689
  const { itemId: id, ...body } = params;
2465
- if (LINE_STARTING_STATES.has(body.state) && !client.hasCapability("products:write")) return toolError("FORBIDDEN", `A move into ${body.state} starts agent work and needs products:write, which this key lacks.`);
2690
+ if (body.state === "queue" && !client.hasCapability("products:write")) return toolError("FORBIDDEN", "A move into queue starts agent work and needs products:write, which this key lacks.");
2466
2691
  return outcome((await client.transitionInitiative(id, body)).data);
2467
2692
  }));
2468
2693
  }
2469
- /** The columns a drop into starts paid work, which the state route gates on products:write. */
2470
- const LINE_STARTING_STATES = /* @__PURE__ */ new Set(["queue", "plan"]);
2694
+ /** The two halves of the Build station, where Plan again is offered. */
2695
+ const BUILD_STATION = /* @__PURE__ */ new Set(["plan", "build"]);
2696
+ function registerPlanAgain(server, client) {
2697
+ server.registerTool("taiga_plan_again", {
2698
+ title: "Plan an initiative again",
2699
+ description: "Supersede the plan of the initiative at the Build station and ask the planner for a new one: for a plan that failed, a plan you do not want built, or a run whose direction was wrong once it has stopped. The earlier plan is kept, and every run records which plan version it built; a pull request still open stays open. Offered only when the turn is yours: refused (CONFLICT) while an agent holds it (planning, building, reviewing). Also refused while a dependency is still in flight or a UI direction has no chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the Build station, put the initiative in the queue instead (taiga_queue_backlog_item): the queue is the only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run; ask the person first.",
2700
+ inputSchema: {
2701
+ itemId,
2702
+ force: z.boolean().optional().describe("Plan anyway past an unsettled dependency or an unchosen UI direction; only when the person says so")
2703
+ },
2704
+ annotations: OPERATE
2705
+ }, (params) => runTool(async () => {
2706
+ const item = (await client.getBacklogItem(params.itemId)).data;
2707
+ if (!BUILD_STATION.has(String(item.state))) return toolError("CONFLICT", `This initiative is in ${String(item.state)}, not at the Build station. The queue is the only way to start work: put it in the queue with taiga_queue_backlog_item and it is planned when its turn comes.`);
2708
+ const body = {
2709
+ state: "plan",
2710
+ ...params.force ? { force: true } : {}
2711
+ };
2712
+ return outcome((await client.transitionInitiative(params.itemId, body)).data);
2713
+ }));
2714
+ }
2471
2715
  /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
2472
2716
  const isApiFailure$1 = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
2473
2717
  /**
@@ -2542,7 +2786,7 @@ function registerReviewGate(server, client) {
2542
2786
  itemId,
2543
2787
  autoBuild: autoBuildInput
2544
2788
  },
2545
- annotations: OPERATE
2789
+ annotations: OPERATE_SETTING
2546
2790
  }, (params) => runTool(async () => {
2547
2791
  const item = (await client.setAutoBuild(params.itemId, params.autoBuild)).data;
2548
2792
  const gate = await reviewGate(client, item);
@@ -2556,7 +2800,7 @@ function registerReviewGate(server, client) {
2556
2800
  function registerResume(server, client) {
2557
2801
  server.registerTool("taiga_resume_backlog_item", {
2558
2802
  title: "Resume backlog item",
2559
- description: "Resume an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first incomplete step, a refused build start is cleared and retried. Does whatever the named cause needs, so prefer it to taiga_resume_build when the initiative is flagged. Refused (CONFLICT) when nothing is flagged or the cause has no resume. Starts paid work; ask the person first.",
2803
+ description: "Resume an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first incomplete step (the steps already committed are kept), a refused build start is cleared and retried. This is the way to carry work on: a run is the record of an attempt, and decisions about the work are made on the initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume. Starts paid work; ask the person first.",
2560
2804
  inputSchema: { itemId },
2561
2805
  annotations: OPERATE
2562
2806
  }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
@@ -2678,7 +2922,7 @@ function registerBuildTools(server, client) {
2678
2922
  if (!client.hasCapability("products:read")) return;
2679
2923
  server.registerTool("taiga_list_builds", {
2680
2924
  title: "List build runs",
2681
- description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each (stop is taiga_stop_build; resume and retry_from_failed_step are both taiga_resume_build, the same route; start_over is only in the web UI for now). With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
2925
+ description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each. A run is the record of an attempt: work is carried on from its initiative (resume and retry_from_failed_step are taiga_resume_backlog_item; start_over is in the web only for now) and stopped by moving the initiative out of Build; a maintenance run, which has no initiative, is retried from its finding. With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
2682
2926
  inputSchema: {
2683
2927
  productId: productId.optional(),
2684
2928
  backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
@@ -2697,7 +2941,7 @@ function registerBuildTools(server, client) {
2697
2941
  }));
2698
2942
  server.registerTool("taiga_get_build", {
2699
2943
  title: "Get build run",
2700
- 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).",
2944
+ description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions name what the initiative accepts for it: resume and retry_from_failed_step are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
2701
2945
  inputSchema: {
2702
2946
  buildId,
2703
2947
  full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
@@ -2721,20 +2965,12 @@ function registerBuildTools(server, client) {
2721
2965
  if (!client.hasCapability("products:write")) return;
2722
2966
  server.registerTool("taiga_stop_build", {
2723
2967
  title: "Stop a build run",
2724
- description: "Stop a run the coding agent is executing: the agent halts, its branch and any draft pull request stay, and the run can be resumed later with taiga_resume_build. Only a run whose actions include stop (taiga_list_builds shows them); the API refuses the rest with CONFLICT. Ask the person before calling this: it interrupts work in progress.",
2968
+ description: "The emergency brake: stop a run the coding agent is executing when its initiative cannot be moved. The way to stop work is on the initiative: move it out of Build (taiga_move_backlog_item), which stops the plan or run in flight. Use this only for a run whose initiative does not offer that (it is finished, canceled or gone) yet the run is still going. The agent halts, its branch and any draft pull request stay. Only a run whose actions include stop; the API refuses the rest with CONFLICT. Ask the person before calling this: it interrupts work in progress.",
2725
2969
  inputSchema: { buildId },
2726
2970
  annotations: OPERATE
2727
2971
  }, (params) => runTool(async () => {
2728
2972
  return toolResult(pickFields([(await client.stopBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
2729
2973
  }));
2730
- server.registerTool("taiga_resume_build", {
2731
- title: "Resume a build run",
2732
- description: "Resume a stopped or failed run from its first incomplete step: the agent starts again on the same branch, which spends tokens and may push commits and open a pull request. This is the route behind both the resume and the retry_from_failed_step actions: the API accepts a run whose actions include one of them and refuses the rest with CONFLICT. start_over (a new run) is web-only for now. For an initiative the line stopped on, taiga_resume_backlog_item does whatever its cause needs instead. Ask the person before calling this.",
2733
- inputSchema: { buildId },
2734
- annotations: OPERATE
2735
- }, (params) => runTool(async () => {
2736
- return toolResult(pickFields([(await client.resumeBuildRun(params.buildId)).data], RUN_OUTCOME_FIELDS)[0]);
2737
- }));
2738
2974
  }
2739
2975
  //#endregion
2740
2976
  //#region src/tools/context.ts
@@ -18009,6 +18245,7 @@ function itemSignature(item) {
18009
18245
  item.state,
18010
18246
  item.planningStatus,
18011
18247
  item.needsHumanCause,
18248
+ item.autoBuild,
18012
18249
  lifecycle.stage,
18013
18250
  lifecycle.turn,
18014
18251
  lifecycle.cta,
@@ -18206,6 +18443,25 @@ function registerWaitTools(server, client, options) {
18206
18443
  run: now ? pickFields([now], RUN_REPORT_FIELDS)[0] : null
18207
18444
  });
18208
18445
  }));
18446
+ if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
18447
+ title: "Wait for an agent job",
18448
+ description: "Wait until an agent job finishes (completed, failed or cancelled): an initiative request, a document, a refresh. Returns the job as taiga_get_job reads it, with what came of an initiative request, as soon as it settles (reached: true), or at the timeout with reached: false: call again to keep waiting. Most jobs take a few minutes.",
18449
+ inputSchema: {
18450
+ jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it"),
18451
+ timeoutSeconds
18452
+ },
18453
+ annotations: READ_ONLY
18454
+ }, (params, extra) => runTool(async () => {
18455
+ const { now, ...outcome } = await waitFor(async (signal) => (await client.getAgentJob(params.jobId, signal)).data, (job) => SETTLED_JOB_STATUSES.has(String(job.status)) ? { reached: true } : null, (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
18456
+ progressToken: extra._meta?.progressToken,
18457
+ sendNotification: extra.sendNotification,
18458
+ signal: extra.signal
18459
+ });
18460
+ return toolResult({
18461
+ ...outcome,
18462
+ job: now ? jobReport(now) : null
18463
+ });
18464
+ }));
18209
18465
  }
18210
18466
  //#endregion
18211
18467
  //#region src/tools/index.ts
@@ -18241,6 +18497,7 @@ function registerAllTools(server, client, options = {}) {
18241
18497
  registerBacklogTools(server, client);
18242
18498
  registerBuildTools(server, client);
18243
18499
  registerQueueTools(server, client);
18500
+ registerJobTools(server, client);
18244
18501
  registerWaitTools(server, client, options.wait ?? DEFAULT_WAIT_OPTIONS);
18245
18502
  registerContextTools(server, client);
18246
18503
  registerResources(server, client);
@@ -18286,20 +18543,26 @@ const SERVER_INSTRUCTIONS = [
18286
18543
  "for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
18287
18544
  "every product the key can see, and active: true the ones executing now.",
18288
18545
  "",
18289
- "**Operating the line** (taiga_create_backlog_item / taiga_queue_backlog_item / taiga_move_backlog_item /",
18290
- "taiga_resume_backlog_item / taiga_set_auto_build / taiga_get_plan / taiga_approve_plan / taiga_reject_plan /",
18291
- "taiga_stop_build / taiga_resume_build): create an initiative, put it in the queue the coding agent works,",
18292
- "move it on the board, decide whether its plan waits for a review (autoBuild: false) or is built at once, read",
18293
- "and approve or reject its plan, stop or resume a run. Queueing, approving and resuming start paid agent work",
18294
- "that pushes commits and opens pull requests; stopping interrupts it. Tell the person what you are about to",
18295
- "start or stop and wait for their yes before calling these; a client that prompts on destructive tools does",
18296
- "that for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
18546
+ "**Operating the line** works the way Taiga is meant to be used. New work is described, not written:",
18547
+ "taiga_request_initiatives asks an agent, which reads the product and its repository, expands the request",
18548
+ "into initiatives in the Backlog (or says why it added none: already covered, not work, out of scope, or a",
18549
+ "policy that does not allow it) -- follow it with taiga_wait_for_job. The queue is the only way to start",
18550
+ "work (taiga_queue_backlog_item): Taiga plans and builds one initiative at a time, in queue order, and starts",
18551
+ "the next when the pull request merges. Whether a landed plan waits for a review is the review gate",
18552
+ "(taiga_set_auto_build, or autoBuild on the queue); read it with taiga_get_plan and answer with",
18553
+ "taiga_approve_plan or taiga_reject_plan. Decisions about the work are made on the initiative: stop work by",
18554
+ "moving it out of Build (taiga_move_backlog_item), carry it on with taiga_resume_backlog_item, ask for a new",
18555
+ "plan with taiga_plan_again; a run is the record of an attempt. taiga_pause_line pauses the whole line.",
18556
+ "taiga_stop_build is the emergency brake for a run whose initiative cannot be moved. Queueing, approving,",
18557
+ "resuming and requesting start paid agent work; stopping and pausing interrupt it. Tell the person what you",
18558
+ "are about to start or stop and wait for their yes; a client that prompts on destructive tools does that",
18559
+ "for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
18297
18560
  "",
18298
- "**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build): the queue",
18299
- "read says what the line is waiting on (a plan to review, a pull request to merge, a halt and what lifts",
18300
- "it). Planning takes minutes and a build up to an hour: wait with the wait tools rather than reading the",
18301
- "same item again and again; each returns when the point is reached, when only a person can move it on,",
18302
- "or at its timeout (call again to keep waiting).",
18561
+ "**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build /",
18562
+ "taiga_get_job / taiga_wait_for_job): the queue read says what the line is waiting on (a paused line, a plan",
18563
+ "to review, a pull request to merge, a halt and what lifts it). Planning takes minutes and a build up to an",
18564
+ "hour: wait with the wait tools rather than reading the same item again and again; each returns when the",
18565
+ "point is reached, when only a person can move it on, or at its timeout (call again to keep waiting).",
18303
18566
  "",
18304
18567
  "**Sections.** Policies and instructions are maps of sections. Each section carries its text and two",
18305
18568
  "provenance fields you can read past: `source` says where the text originally came from (template:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ai/mcp-server",
3
- "version": "0.12.0-dev.421",
3
+ "version": "0.13.0-dev.422",
4
4
  "description": "MCP server for Taiga -- gives AI assistants access to documents and products",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",