@taiga-ai/mcp-server 0.20.0-dev.454 → 0.21.0-dev.455
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 +435 -183
- package/package.json +1 -1
- package/src/server.integration.test.ts +355 -68
- package/src/tools/autonomy.ts +9 -11
- package/src/tools/backlogs.ts +84 -81
- package/src/tools/builds.ts +96 -28
- package/src/tools/catalogue.test.ts +5 -8
- package/src/tools/catalogue.ts +5 -8
- package/src/tools/context.ts +21 -20
- package/src/tools/documents.ts +34 -43
- package/src/tools/errors.ts +2 -1
- package/src/tools/inbox.ts +19 -33
- package/src/tools/index.ts +42 -7
- package/src/tools/inputs.ts +1 -1
- package/src/tools/instructions.ts +17 -19
- package/src/tools/jobs.ts +5 -6
- package/src/tools/knowledge.ts +17 -23
- package/src/tools/maintenance.ts +129 -82
- package/src/tools/observe.ts +30 -39
- package/src/tools/product-documents.ts +20 -26
- package/src/tools/products.ts +23 -73
- package/src/tools/prompts.ts +1 -1
- package/src/tools/queue.ts +10 -12
- package/src/tools/repositories.ts +20 -25
- package/src/tools/sections.ts +41 -6
- package/src/tools/setup.ts +100 -54
- package/src/tools/summarize.ts +4 -1
- package/src/tools/wait.ts +13 -22
- package/taiga-ai-mcp-server-0.21.0-dev.455.tgz +0 -0
- package/taiga-ai-mcp-server-0.20.0-dev.454.tgz +0 -0
package/src/tools/autonomy.ts
CHANGED
|
@@ -22,12 +22,11 @@ export function registerAutonomyTools(server: McpServer, client: TaigaClient) {
|
|
|
22
22
|
{
|
|
23
23
|
title: "Set a product's autonomy defaults",
|
|
24
24
|
description:
|
|
25
|
-
"Set the defaults a product's initiatives follow
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
'initiative can decide otherwise for itself. Changes how work proceeds.',
|
|
25
|
+
"Set the defaults a product's initiatives follow unless they decide for themselves. autoBuild: false " +
|
|
26
|
+
'makes every plan wait for a person to approve or reject it; true builds it as soon as the product has ' +
|
|
27
|
+
'room. autoMerge: false makes pull requests wait for a person to merge; null follows the organization ' +
|
|
28
|
+
'and factory (autonomous merge needs the organization switch on, a factory can turn it off, and neither ' +
|
|
29
|
+
'is changed here).',
|
|
31
30
|
inputSchema: z.object({
|
|
32
31
|
productId,
|
|
33
32
|
autoBuild: z.boolean().optional().describe('Build landed plans without a review by default'),
|
|
@@ -64,11 +63,10 @@ export function registerAutonomyTools(server: McpServer, client: TaigaClient) {
|
|
|
64
63
|
{
|
|
65
64
|
title: "Set whether an initiative's pull request merges on its own",
|
|
66
65
|
description:
|
|
67
|
-
"Set whether one initiative's pull request merges
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'Taiga merges through your rules, never around them.',
|
|
66
|
+
"Set whether one initiative's pull request merges itself once the repository's rules pass (required " +
|
|
67
|
+
'checks, reviews, a code reviewer Taiga waits for): true, false (a person merges), or null (product ' +
|
|
68
|
+
'default). Where the organization or factory has turned autonomous merge off, true changes nothing. ' +
|
|
69
|
+
"Taiga merges through the repository's rules, never around them.",
|
|
72
70
|
inputSchema: z.object({
|
|
73
71
|
initiativeId: idInput('initiative', 'taiga_list_initiatives (query finds one by title) gives it'),
|
|
74
72
|
autoMerge: z
|
package/src/tools/backlogs.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type ActivityActor,
|
|
6
6
|
type BacklogItem,
|
|
7
7
|
BUILD_RUN_STATUSES,
|
|
8
|
+
INITIATIVE_STATES,
|
|
8
9
|
PLAN_REJECTION_COMMENT_MAX_CHARS,
|
|
9
10
|
PR_CLOSING_STATES,
|
|
10
11
|
USER_DROP_STATES,
|
|
@@ -74,10 +75,9 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
74
75
|
* leaves it null, and the lifecycle is what says why and what moves it on.
|
|
75
76
|
*/
|
|
76
77
|
const LIFECYCLE_MEANING =
|
|
77
|
-
'situation says in
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'resume_build taiga_resume_initiative, retry_build a fresh run (start over, in the web), review_pr and ' +
|
|
78
|
+
'situation says in a sentence where it stands and what moves it on. lifecycle.cta names the next call: ' +
|
|
79
|
+
'start_plan taiga_queue_initiative, retry_plan taiga_plan_again (planFailureCause says why), start_build ' +
|
|
80
|
+
'taiga_approve_plan, resume_build taiga_resume_initiative, retry_build a fresh run (web only), review_pr and ' +
|
|
81
81
|
'view_pr the pull request. needsHumanCause is set only when the line stopped on it.';
|
|
82
82
|
|
|
83
83
|
/** The move each call to action is, in the tools' words. */
|
|
@@ -304,13 +304,11 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
304
304
|
{
|
|
305
305
|
title: 'List initiatives',
|
|
306
306
|
description:
|
|
307
|
-
"List a product's initiatives
|
|
308
|
-
'
|
|
309
|
-
'
|
|
310
|
-
'
|
|
311
|
-
'
|
|
312
|
-
'finds one by title), then read its acceptance criteria and technical context with taiga_get_initiative. ' +
|
|
313
|
-
'Archived items are left out unless asked for.',
|
|
307
|
+
"List a product's initiatives: title, category, status, place on the line (backlog, todo, queue, plan, " +
|
|
308
|
+
'build, done), situation (where it stands and what moves it on) and criteria (acceptance criteria met, ' +
|
|
309
|
+
'confirmed with evidence or checked off by a person; null before planning, so a done initiative can show ' +
|
|
310
|
+
'fewer met than it has). query finds one by title; taiga_get_initiative reads it in full. Archived ones ' +
|
|
311
|
+
'are left out unless asked for.',
|
|
314
312
|
inputSchema: z.object({
|
|
315
313
|
productId,
|
|
316
314
|
query: z
|
|
@@ -328,6 +326,12 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
328
326
|
.enum(['unplanned', 'planning', 'planned', 'failed'])
|
|
329
327
|
.optional()
|
|
330
328
|
.describe('Only items whose plan is in this state'),
|
|
329
|
+
state: z
|
|
330
|
+
.enum(INITIATIVE_STATES)
|
|
331
|
+
.optional()
|
|
332
|
+
.describe(
|
|
333
|
+
'Only items in this column of the board: backlog, todo, queue, plan, build, done or canceled (status groups them coarser)'
|
|
334
|
+
),
|
|
331
335
|
category: z.enum(['feature', 'bug', 'tech_debt', 'improvement']).optional().describe('Only this category'),
|
|
332
336
|
includeArchived: z.boolean().optional().describe('Include archived items (default false)'),
|
|
333
337
|
...pagination,
|
|
@@ -336,7 +340,29 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
336
340
|
},
|
|
337
341
|
(params) =>
|
|
338
342
|
runTool(async () => {
|
|
339
|
-
const { query, ...filters } = params;
|
|
343
|
+
const { query, state, ...filters } = params;
|
|
344
|
+
// The API filters by status only; a board column is read across pages here (PRO-2800).
|
|
345
|
+
if (state !== undefined) {
|
|
346
|
+
if (params.cursor !== undefined) {
|
|
347
|
+
return toolError(
|
|
348
|
+
'VALIDATION_ERROR',
|
|
349
|
+
'A board-column filter answers one page; narrow it with status or query.'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const found = await searchPages(
|
|
353
|
+
(cursor) => client.listBacklogItems(params.productId, { ...filters, limit: SEARCH_PAGE_SIZE, cursor }),
|
|
354
|
+
query ?? '',
|
|
355
|
+
(row) => [row.title],
|
|
356
|
+
params.limit ?? 25,
|
|
357
|
+
(row) => row.state === state
|
|
358
|
+
);
|
|
359
|
+
return toolResult({
|
|
360
|
+
...page(situatedRows(found.rows), null),
|
|
361
|
+
...(found.more
|
|
362
|
+
? { more: 'More items may be in that column: narrow it with status or query, or raise the limit.' }
|
|
363
|
+
: {}),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
340
366
|
if (query === undefined) {
|
|
341
367
|
const result = await client.listBacklogItems(params.productId, filters);
|
|
342
368
|
return toolResult(page(situatedRows(result.data as Record<string, unknown>[]), result.nextCursor));
|
|
@@ -362,11 +388,9 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
362
388
|
{
|
|
363
389
|
title: 'Get initiative',
|
|
364
390
|
description:
|
|
365
|
-
'
|
|
366
|
-
'
|
|
367
|
-
|
|
368
|
-
'A failed plan carries planFailure: the cause, what it means, when, and by whom. ' +
|
|
369
|
-
LIFECYCLE_MEANING,
|
|
391
|
+
'Read one initiative in full: description, acceptance criteria (which are met), technical context, ' +
|
|
392
|
+
'dependencies and source documents. Read it before implementing. A failed plan carries planFailure ' +
|
|
393
|
+
`(cause, meaning, when, by whom). ${LIFECYCLE_MEANING}`,
|
|
370
394
|
inputSchema: z.object({
|
|
371
395
|
initiativeId,
|
|
372
396
|
}),
|
|
@@ -390,12 +414,11 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
390
414
|
{
|
|
391
415
|
title: 'Get the active plan',
|
|
392
416
|
description:
|
|
393
|
-
"
|
|
394
|
-
'
|
|
395
|
-
|
|
396
|
-
'
|
|
397
|
-
|
|
398
|
-
'when the initiative has no active plan yet (planningStatus says whether one is being generated).',
|
|
417
|
+
"Read an initiative's active plan: its steps in order, each with title, layer, dependencies (the " +
|
|
418
|
+
'orderIndex of the steps it builds on), files written and read, and resources created. Enough to approve ' +
|
|
419
|
+
"or reject it (taiga_approve_plan, taiga_reject_plan); full: true adds each step's complete instructions " +
|
|
420
|
+
'(thousands of tokens). NOT_FOUND when there is no active plan yet; planningStatus says whether one is ' +
|
|
421
|
+
'being generated.',
|
|
399
422
|
inputSchema: z.object({
|
|
400
423
|
initiativeId,
|
|
401
424
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)"),
|
|
@@ -421,18 +444,11 @@ function registerRequest(server: McpServer, client: TaigaClient) {
|
|
|
421
444
|
{
|
|
422
445
|
title: 'Request new initiatives',
|
|
423
446
|
description:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
'
|
|
427
|
-
'
|
|
428
|
-
'
|
|
429
|
-
"the organization's policies does not allow it as asked (named, with the line it runs into and the way to " +
|
|
430
|
-
'decide otherwise). Leave both title and description out and the agent suggests the one next initiative the ' +
|
|
431
|
-
'product most needs. It runs in the background, a few minutes: this returns the job, and taiga_wait_for_job ' +
|
|
432
|
-
'or taiga_get_job (both need products:read) say what came of it. One request per product at a time ' +
|
|
433
|
-
'(CONFLICT while one runs). Inline images in the description are left out; the web is where a request ' +
|
|
434
|
-
'carries images. It starts nothing on the line: new initiatives land in Backlog. Runs an agent; ask the ' +
|
|
435
|
-
'person first.',
|
|
447
|
+
"Ask an agent to turn described work into initiatives in the product's Backlog, and return the job. One " +
|
|
448
|
+
'piece of work becomes one initiative, a program an ordered set with dependencies. It may add none and ' +
|
|
449
|
+
'say why (already covered, not work, outside the product, or a named policy forbids it). Omit title and ' +
|
|
450
|
+
'description for the one next initiative the product most needs. taiga_wait_for_job gives the outcome. ' +
|
|
451
|
+
'One request per product at a time (CONFLICT). Nothing goes on the line.',
|
|
436
452
|
inputSchema: z.object({
|
|
437
453
|
productId,
|
|
438
454
|
title: z.string().max(500).optional().describe('What the work is, in a line'),
|
|
@@ -466,17 +482,13 @@ function registerMove(server: McpServer, client: TaigaClient) {
|
|
|
466
482
|
{
|
|
467
483
|
title: 'Move initiative on the board',
|
|
468
484
|
description:
|
|
469
|
-
'Move an initiative
|
|
470
|
-
'
|
|
471
|
-
'
|
|
472
|
-
|
|
473
|
-
'
|
|
474
|
-
|
|
475
|
-
'
|
|
476
|
-
'state machine refuses a move that is not allowed (CONFLICT). A move into queue starts paid work and ' +
|
|
477
|
-
'needs products:write. Ask the person before a move that starts work, stops work or closes a pull request. ' +
|
|
478
|
-
'archived: true files the initiative where it stands (pass its current column as state; never from the Build station, plan or build) ' +
|
|
479
|
-
'and archived: false brings it back; archived work is left out of lists unless includeArchived asks for it.',
|
|
485
|
+
'Move an initiative between columns, take it off the line, or reorder it. The line never takes from ' +
|
|
486
|
+
'backlog or todo; queue puts it on the line (taiga_queue_initiative), the only way to start work; done ' +
|
|
487
|
+
'and canceled finish it by hand. Nothing moves into Build by hand; moving out of Build stops and ' +
|
|
488
|
+
`discards the plan or run in flight. A move to ${PR_CLOSING_STATES.join(', ')} CLOSES a pull request the ` +
|
|
489
|
+
'initiative waits on (with a comment on it); queue keeps it open, and done leaves it to merge. Refused (CONFLICT) if not allowed; queue needs products:write. Ask first before ' +
|
|
490
|
+
'starting or stopping work or closing a pull request. archived: true archives it in place (pass its ' +
|
|
491
|
+
'current column as state; never from the Build station); false restores it.',
|
|
480
492
|
inputSchema: z.object({
|
|
481
493
|
initiativeId,
|
|
482
494
|
state: z.enum(USER_DROP_STATES).describe('The column to move into'),
|
|
@@ -517,14 +529,11 @@ function registerPlanAgain(server: McpServer, client: TaigaClient) {
|
|
|
517
529
|
{
|
|
518
530
|
title: 'Plan an initiative again',
|
|
519
531
|
description:
|
|
520
|
-
'
|
|
521
|
-
'
|
|
522
|
-
'
|
|
523
|
-
'
|
|
524
|
-
'
|
|
525
|
-
'chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the ' +
|
|
526
|
-
'Build station, put the initiative in the queue instead (taiga_queue_initiative): the queue is the ' +
|
|
527
|
-
'only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run.',
|
|
532
|
+
'Replace the plan of the initiative at the Build station with a new one: for a failed plan, one you do ' +
|
|
533
|
+
'not want built, or a stopped run that went wrong. The old plan is kept; an open pull request stays ' +
|
|
534
|
+
'open. Refused (CONFLICT) while an agent holds the turn, and while a dependency is in flight or a UI ' +
|
|
535
|
+
'direction has no chosen concept unless force is set (the person decides). Off the Build station, use ' +
|
|
536
|
+
'taiga_queue_initiative; to send a plan back with direction, taiga_reject_plan.',
|
|
528
537
|
inputSchema: z.object({
|
|
529
538
|
initiativeId,
|
|
530
539
|
force: z
|
|
@@ -607,15 +616,12 @@ function registerQueue(server: McpServer, client: TaigaClient) {
|
|
|
607
616
|
{
|
|
608
617
|
title: 'Queue initiative',
|
|
609
618
|
description:
|
|
610
|
-
|
|
611
|
-
'
|
|
612
|
-
'
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
'
|
|
616
|
-
'refused. An initiative already ' +
|
|
617
|
-
'in the queue stays where it is unless afterId says where to go; one being planned or built cannot move ' +
|
|
618
|
-
'(CONFLICT). Returns the initiative with its new state, its review gate and its place in the queue.',
|
|
619
|
+
"Put an initiative on the product's line, where initiatives are planned and built one at a time, each " +
|
|
620
|
+
'advancing when the previous pull request merges. Starts a planner run, then a coding run that pushes ' +
|
|
621
|
+
'commits and opens a pull request. autoBuild: false makes the plan wait for a person before it is built ' +
|
|
622
|
+
'(otherwise the product default; the result says which); the gate is set first and stays set if the move ' +
|
|
623
|
+
'is refused. An initiative already queued stays put unless afterId says where; one being planned or ' +
|
|
624
|
+
'built cannot move (CONFLICT). Returns the initiative with its state, review gate and place on the line.',
|
|
619
625
|
inputSchema: z.object({
|
|
620
626
|
initiativeId,
|
|
621
627
|
afterId: idInput('initiative', 'taiga_list_initiatives gives it')
|
|
@@ -661,10 +667,10 @@ function registerReviewGate(server: McpServer, client: TaigaClient) {
|
|
|
661
667
|
{
|
|
662
668
|
title: "Set an initiative's review gate",
|
|
663
669
|
description:
|
|
664
|
-
"Set
|
|
665
|
-
|
|
666
|
-
'
|
|
667
|
-
'
|
|
670
|
+
"Set an initiative's review gate: true builds each plan as soon as it lands, false makes it wait for a " +
|
|
671
|
+
'person to approve or reject it, null follows the product default. Applies to every plan the initiative ' +
|
|
672
|
+
'gets, including one being written now; opening the gate on a plan already waiting starts its run at ' +
|
|
673
|
+
'once. Returns the initiative with the gate now in force.',
|
|
668
674
|
inputSchema: z.object({ initiativeId, autoBuild: autoBuildInput }),
|
|
669
675
|
annotations: OPERATE_SETTING,
|
|
670
676
|
},
|
|
@@ -687,13 +693,11 @@ function registerResume(server: McpServer, client: TaigaClient) {
|
|
|
687
693
|
{
|
|
688
694
|
title: 'Resume initiative',
|
|
689
695
|
description:
|
|
690
|
-
'Carry on an initiative the line stopped on
|
|
691
|
-
'
|
|
692
|
-
'
|
|
693
|
-
'
|
|
694
|
-
'
|
|
695
|
-
'with needsHumanCause null (lifecycle.cta retry_plan, planFailureCause says why) is planned again with ' +
|
|
696
|
-
'taiga_plan_again. Starts paid work.',
|
|
696
|
+
'Carry on an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is ' +
|
|
697
|
+
'planned again, a stopped or failed run resumes from its first unfinished step (committed steps are ' +
|
|
698
|
+
'kept), a refused run start is cleared and retried. Refused (CONFLICT) when nothing is flagged or the ' +
|
|
699
|
+
'cause has no resume; a failed plan with needsHumanCause null (lifecycle.cta retry_plan, ' +
|
|
700
|
+
'planFailureCause says why) needs taiga_plan_again.',
|
|
697
701
|
inputSchema: z.object({ initiativeId }),
|
|
698
702
|
annotations: OPERATE,
|
|
699
703
|
},
|
|
@@ -707,10 +711,9 @@ function registerPlanVerdicts(server: McpServer, client: TaigaClient) {
|
|
|
707
711
|
{
|
|
708
712
|
title: 'Approve a plan',
|
|
709
713
|
description:
|
|
710
|
-
"Approve an initiative's plan
|
|
711
|
-
'
|
|
712
|
-
'
|
|
713
|
-
'already running. Starts a coding run that pushes commits and opens a pull request.',
|
|
714
|
+
"Approve an initiative's plan and start its run, as the Start run button does. Read the plan first with " +
|
|
715
|
+
'taiga_get_plan and pass its ID. Refused (CONFLICT) if the plan was replaced since, while a new plan is ' +
|
|
716
|
+
'being generated, or while a run is already going. The run pushes commits and opens a pull request.',
|
|
714
717
|
inputSchema: z.object({ planId }),
|
|
715
718
|
annotations: OPERATE,
|
|
716
719
|
},
|
|
@@ -722,9 +725,9 @@ function registerPlanVerdicts(server: McpServer, client: TaigaClient) {
|
|
|
722
725
|
{
|
|
723
726
|
title: 'Reject a plan',
|
|
724
727
|
description:
|
|
725
|
-
"Send an initiative's plan back with
|
|
726
|
-
'feedback and the
|
|
727
|
-
'
|
|
728
|
+
"Send an initiative's plan back with a comment: the planner writes a new plan from it, with the comment " +
|
|
729
|
+
'as reviewer feedback, and the new plan replaces this one when it lands. Read the plan first with ' +
|
|
730
|
+
'taiga_get_plan and pass its ID. Starts a planner run.',
|
|
728
731
|
inputSchema: z.object({
|
|
729
732
|
planId,
|
|
730
733
|
comment: z
|
package/src/tools/builds.ts
CHANGED
|
@@ -160,10 +160,10 @@ export function handbackSaid(escalation: unknown): string | null {
|
|
|
160
160
|
/** What a person does about a handback, by its reason, as the web's handback detail says it. */
|
|
161
161
|
export function handbackNextStep(escalation: unknown): string {
|
|
162
162
|
const reason = escalation && typeof escalation === 'object' ? (escalation as { reason?: unknown }).reason : null;
|
|
163
|
-
if (reason === 'check_weakened') return 'review the loosened check before merging
|
|
163
|
+
if (reason === 'check_weakened') return 'review the loosened check on GitHub before merging';
|
|
164
164
|
if (reason === 'ci_never_settled')
|
|
165
|
-
return 'look at what is still running or queued in CI, then review
|
|
166
|
-
return 'fix it by hand or review it back to the agent';
|
|
165
|
+
return 'look at what is still running or queued in CI on GitHub, then review the pull request';
|
|
166
|
+
return 'fix it by hand, or review the pull request on GitHub, which hands it back to the agent';
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/** The web's words for why Taiga keeps a pull request a draft. */
|
|
@@ -243,6 +243,72 @@ export function readableRun(run: Record<string, unknown>, opts: { row?: boolean
|
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/** Distinct initiatives a run list reads to say which moved on; past this a waiting row says it was not checked. */
|
|
247
|
+
const INITIATIVE_CHECKS = 10;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The status of the initiative behind each run still waiting on its pull
|
|
251
|
+
* request (PRO-2800). A reviewing agent read a run of a canceled initiative as
|
|
252
|
+
* "waiting for a person to review and merge" while Action Required called the
|
|
253
|
+
* same review stale. A 404 is 'deleted'. Best effort: an initiative past the
|
|
254
|
+
* cap, one whose read failed, and every one without backlog:read is
|
|
255
|
+
* 'unchecked', which withInitiativeState adds to the row's outcome.
|
|
256
|
+
*/
|
|
257
|
+
export async function initiativeStates(
|
|
258
|
+
client: TaigaClient,
|
|
259
|
+
rows: Record<string, unknown>[]
|
|
260
|
+
): Promise<Map<string, string>> {
|
|
261
|
+
const states = new Map<string, string>();
|
|
262
|
+
// Only the runs that could still be waiting: a settled run's initiative changes nothing about it.
|
|
263
|
+
const waiting = rows.filter(
|
|
264
|
+
(row) => ['needs_review', 'completed'].includes(String(row['status'])) && row['prMerged'] !== true
|
|
265
|
+
);
|
|
266
|
+
const all = [
|
|
267
|
+
...new Set(waiting.map((row) => row['backlogItemId']).filter((id): id is string => typeof id === 'string')),
|
|
268
|
+
];
|
|
269
|
+
// Without backlog:read, or past the cap, a waiting run's initiative is not read: it is marked unchecked so
|
|
270
|
+
// the row does not claim a person is waiting when nobody may be.
|
|
271
|
+
const ids = client.hasCapability('backlog:read') ? all.slice(0, INITIATIVE_CHECKS) : [];
|
|
272
|
+
for (const id of all) if (!ids.includes(id)) states.set(id, 'unchecked');
|
|
273
|
+
await Promise.all(
|
|
274
|
+
ids.map(async (id) => {
|
|
275
|
+
try {
|
|
276
|
+
const item = (await client.getBacklogItem(id)).data as unknown as Record<string, unknown>;
|
|
277
|
+
states.set(id, String(item['status'] ?? item['state']));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error instanceof TaigaApiError && error.status === 404) return void states.set(id, 'deleted');
|
|
280
|
+
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
281
|
+
states.set(id, 'unchecked');
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
return states;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** A readable run row that also says when its initiative was canceled or deleted while its pull request waited. */
|
|
289
|
+
export function withInitiativeState(
|
|
290
|
+
row: Record<string, unknown>,
|
|
291
|
+
states: Map<string, string>
|
|
292
|
+
): Record<string, unknown> {
|
|
293
|
+
const state = states.get(String(row['backlogItemId']));
|
|
294
|
+
// A run still executing is left as it is: its initiative's cancel stops it, and one still going after that is
|
|
295
|
+
// what taiga_stop_run is for.
|
|
296
|
+
if (!['gated', 'unmerged'].includes(String(row['disposition']))) return row;
|
|
297
|
+
if (state === 'unchecked') {
|
|
298
|
+
return { ...row, outcome: `${String(row['outcome'])} Whether its initiative still wants it was not checked.` };
|
|
299
|
+
}
|
|
300
|
+
if (state !== 'canceled' && state !== 'deleted') return row;
|
|
301
|
+
const pr = typeof row['prNumber'] === 'number' ? `PR #${row['prNumber']}` : 'its pull request';
|
|
302
|
+
const open = row['prState'] === 'open' ? ` ${pr} is still open on the forge.` : '';
|
|
303
|
+
return {
|
|
304
|
+
...row,
|
|
305
|
+
disposition: 'abandoned',
|
|
306
|
+
// Retry or start over cannot succeed once the initiative is gone.
|
|
307
|
+
actions: [],
|
|
308
|
+
outcome: `Its initiative was ${state}, so nobody is waiting on this run.${open}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
246
312
|
/** readableRun for a list row. */
|
|
247
313
|
export const readableRow = (run: Record<string, unknown>) => readableRun(run, { row: true });
|
|
248
314
|
|
|
@@ -304,8 +370,10 @@ async function activityRows(
|
|
|
304
370
|
for (const product of products?.data ?? []) {
|
|
305
371
|
nameById.set(product.id, { name: product.name, slug: product.slug });
|
|
306
372
|
}
|
|
373
|
+
const states = await initiativeStates(client, runs.data as Record<string, unknown>[]);
|
|
307
374
|
const rows = pickFields(runs.data as Record<string, unknown>[], BUILD_SUMMARY_FIELDS)
|
|
308
375
|
.map(readableRow)
|
|
376
|
+
.map((row) => withInitiativeState(row, states))
|
|
309
377
|
.map((row) => {
|
|
310
378
|
const product = nameById.get(String(row['productId']));
|
|
311
379
|
return product ? { ...row, product } : row;
|
|
@@ -332,14 +400,11 @@ export function registerBuildTools(server: McpServer, client: TaigaClient) {
|
|
|
332
400
|
{
|
|
333
401
|
title: 'List runs',
|
|
334
402
|
description:
|
|
335
|
-
|
|
336
|
-
'
|
|
337
|
-
'
|
|
338
|
-
|
|
339
|
-
'
|
|
340
|
-
'initiative; without it, the runs of every product the key can see, each row naming its product, and ' +
|
|
341
|
-
'active: true narrows to the runs executing now. taiga_get_run reads one with its steps. Work is carried ' +
|
|
342
|
-
'on from the initiative (taiga_resume_initiative), not the run.',
|
|
403
|
+
'List coding agent runs, newest first: branch, pull request, step progress, outcome (one sentence), ' +
|
|
404
|
+
'actions and disposition: in_flight, gated (a person owes a review and merge), succeeded (merged), ' +
|
|
405
|
+
'failed, stopped, rejected (pull request closed unmerged), unmerged, or abandoned (initiative canceled ' +
|
|
406
|
+
"or deleted). Without productId, every product's runs; active: true keeps running ones. taiga_get_run " +
|
|
407
|
+
'reads one; carry work on from the initiative (taiga_resume_initiative).',
|
|
343
408
|
inputSchema: z.object({
|
|
344
409
|
productId: productId.optional(),
|
|
345
410
|
initiativeId: idInput('initiative', 'taiga_list_initiatives (query finds one by title) gives it')
|
|
@@ -378,9 +443,13 @@ export function registerBuildTools(server: McpServer, client: TaigaClient) {
|
|
|
378
443
|
limit: params.limit,
|
|
379
444
|
cursor: params.cursor,
|
|
380
445
|
});
|
|
446
|
+
const raw = result.data as Record<string, unknown>[];
|
|
447
|
+
const states = await initiativeStates(client, raw);
|
|
381
448
|
return toolResult(
|
|
382
449
|
page(
|
|
383
|
-
pickFields(
|
|
450
|
+
pickFields(raw, BUILD_SUMMARY_FIELDS)
|
|
451
|
+
.map(readableRow)
|
|
452
|
+
.map((row) => withInitiativeState(row, states)),
|
|
384
453
|
result.nextCursor
|
|
385
454
|
)
|
|
386
455
|
);
|
|
@@ -394,15 +463,13 @@ export function registerBuildTools(server: McpServer, client: TaigaClient) {
|
|
|
394
463
|
{
|
|
395
464
|
title: 'Get run',
|
|
396
465
|
description:
|
|
397
|
-
'Read one run with its steps:
|
|
398
|
-
"
|
|
399
|
-
"
|
|
400
|
-
'
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
'
|
|
404
|
-
'retry_from_failed_step are taiga_resume_initiative; start_over is in the web only; to stop it, move the ' +
|
|
405
|
-
'initiative out of Build. A maintenance run has no initiative and is retried from its finding.',
|
|
466
|
+
'Read one run with its steps: outcome, disposition, pull request, hardening, and per step its title, ' +
|
|
467
|
+
"status, commit, files changed, verification and headline (first paragraph of the agent's report). " +
|
|
468
|
+
"verification is Taiga's own check after the step (tests, lint, build), so a docs-only step can fail on " +
|
|
469
|
+
"the repository's failing tests. A failed or stopped run has stoppedIn and lastProgressAt. full: true " +
|
|
470
|
+
'adds complete instructions and reports. actions: resume and retry_from_failed_step are ' +
|
|
471
|
+
'taiga_resume_initiative; start_over is web only; to stop it, move the initiative out of Build. A ' +
|
|
472
|
+
'maintenance run has no initiative and is retried from its finding.',
|
|
406
473
|
inputSchema: z.object({
|
|
407
474
|
runId,
|
|
408
475
|
full: z
|
|
@@ -424,9 +491,11 @@ export function registerBuildTools(server: McpServer, client: TaigaClient) {
|
|
|
424
491
|
const rows = steps.data;
|
|
425
492
|
const where = stoppedIn(runRow, rows);
|
|
426
493
|
const stop = where ? { stoppedIn: where } : {};
|
|
427
|
-
|
|
494
|
+
const states = await initiativeStates(client, [runRow as unknown as Record<string, unknown>]);
|
|
495
|
+
const readable = withInitiativeState(readableRun(runRow), states);
|
|
496
|
+
if (params.full) return toolResult({ ...readable, ...stop, steps: steps.data });
|
|
428
497
|
return toolResult({
|
|
429
|
-
...
|
|
498
|
+
...readable,
|
|
430
499
|
...stop,
|
|
431
500
|
steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
|
|
432
501
|
...step,
|
|
@@ -443,11 +512,10 @@ export function registerBuildTools(server: McpServer, client: TaigaClient) {
|
|
|
443
512
|
{
|
|
444
513
|
title: 'Stop a run',
|
|
445
514
|
description:
|
|
446
|
-
'
|
|
447
|
-
'
|
|
448
|
-
'
|
|
449
|
-
'
|
|
450
|
-
'stay. Only a run whose actions include stop; the API refuses the rest with CONFLICT.',
|
|
515
|
+
'Emergency stop for a run whose initiative cannot be moved out of Build (finished, canceled or gone) but ' +
|
|
516
|
+
'is still going. Normally stop work by moving the initiative out of Build with taiga_move_initiative. ' +
|
|
517
|
+
'The agent halts; its branch and any draft pull request stay. Only a run whose actions include stop; ' +
|
|
518
|
+
'others are refused (CONFLICT).',
|
|
451
519
|
inputSchema: z.object({ runId }),
|
|
452
520
|
annotations: OPERATE,
|
|
453
521
|
},
|
|
@@ -145,13 +145,7 @@ describe('tool descriptions against the catalogue', () => {
|
|
|
145
145
|
// Claude Code loads only tool names and the instructions until the model
|
|
146
146
|
// searches, and cuts the instructions at 2,048 characters (PRO-2800).
|
|
147
147
|
expect(SERVER_INSTRUCTIONS.length).toBeLessThanOrEqual(SERVER_INSTRUCTIONS_LIMIT);
|
|
148
|
-
for (const must of [
|
|
149
|
-
'taiga_get_product_context',
|
|
150
|
-
'Before you write',
|
|
151
|
-
'NOT_FOUND',
|
|
152
|
-
'initiative',
|
|
153
|
-
'Only in the web',
|
|
154
|
-
]) {
|
|
148
|
+
for (const must of ['taiga_get_product_context', 'ask-first', 'NOT_FOUND', 'initiative', 'Only in the web']) {
|
|
155
149
|
expect(SERVER_INSTRUCTIONS).toContain(must);
|
|
156
150
|
}
|
|
157
151
|
});
|
|
@@ -167,6 +161,9 @@ describe('writeNotice', () => {
|
|
|
167
161
|
it('says nothing for a read, and what a write does otherwise', () => {
|
|
168
162
|
expect(writeNotice('r', { readOnlyHint: true })).toBe('');
|
|
169
163
|
expect(writeNotice('w', { readOnlyHint: false })).toBe('Writes. ');
|
|
170
|
-
expect(writeNotice('o', { readOnlyHint: false, destructiveHint: true })).
|
|
164
|
+
expect(writeNotice('o', { readOnlyHint: false, destructiveHint: true })).toBe('Ask first (others see it). ');
|
|
165
|
+
expect(writeNotice('taiga_queue_initiative', { readOnlyHint: false, destructiveHint: true })).toBe(
|
|
166
|
+
'Ask first (paid agent work). '
|
|
167
|
+
);
|
|
171
168
|
});
|
|
172
169
|
});
|
package/src/tools/catalogue.ts
CHANGED
|
@@ -66,13 +66,10 @@ export function named(types: readonly string[]): string {
|
|
|
66
66
|
* tool and the instructions cannot explain them three different ways.
|
|
67
67
|
*/
|
|
68
68
|
export const ORIGIN_MEANING: Record<ProductDocumentOrigin, string> = {
|
|
69
|
-
plan:
|
|
70
|
-
'plan: the discovery documents, authored intent. Every greenfield product has them; ' +
|
|
71
|
-
'they say what is meant to be built.',
|
|
69
|
+
plan: 'plan: the discovery documents, what is meant to be built.',
|
|
72
70
|
reality:
|
|
73
|
-
'reality: derived from the code on the main branch, what is
|
|
74
|
-
'
|
|
75
|
-
'merged pull request marks it done) or the repository is pushed to, and they refresh as code lands.',
|
|
71
|
+
'reality: derived from the code on the main branch, what is built; an imported product has them from ' +
|
|
72
|
+
'its import, any other once its first initiative is done (a merged pull request) or code is pushed.',
|
|
76
73
|
};
|
|
77
74
|
|
|
78
75
|
/**
|
|
@@ -222,5 +219,5 @@ export function setupNextStep(
|
|
|
222
219
|
/** What discoveryStep says, for a description that shows it. */
|
|
223
220
|
export const DISCOVERY_STEP_MEANING =
|
|
224
221
|
'discoveryStep is where a product in setup is: context (not started), conversation or import (the ' +
|
|
225
|
-
'specification
|
|
226
|
-
'
|
|
222
|
+
'specification being written or derived from code), documents (generated and reviewed), checks (stamped in ' +
|
|
223
|
+
'the web); null once it is active.';
|