@taiga-ai/mcp-server 0.18.0-rc.445 → 0.19.0-dev.451
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1128 -650
- package/package.json +1 -1
- package/src/client.test.ts +2 -2
- package/src/client.ts +201 -145
- package/src/modern.integration.test.ts +1 -1
- package/src/server.integration.test.ts +782 -243
- package/src/tools/autonomy.ts +3 -5
- package/src/tools/backlogs.ts +182 -72
- package/src/tools/builds.ts +142 -53
- package/src/tools/catalogue.test.ts +28 -22
- package/src/tools/context.ts +126 -56
- package/src/tools/documents.ts +12 -5
- package/src/tools/errors.ts +40 -2
- package/src/tools/hints.ts +37 -9
- package/src/tools/inbox.ts +149 -28
- package/src/tools/index.ts +15 -1
- package/src/tools/instructions.ts +32 -154
- package/src/tools/jobs.ts +6 -5
- package/src/tools/knowledge.ts +4 -6
- package/src/tools/maintenance.ts +53 -32
- package/src/tools/markdown.ts +13 -5
- package/src/tools/observe.ts +32 -14
- package/src/tools/product-documents.ts +4 -6
- package/src/tools/products.ts +32 -27
- package/src/tools/prompts.ts +14 -20
- package/src/tools/queue.ts +47 -26
- package/src/tools/readiness.ts +172 -0
- package/src/tools/repositories.ts +11 -9
- package/src/tools/resources.ts +16 -19
- package/src/tools/sections.test.ts +3 -3
- package/src/tools/sections.ts +57 -16
- package/src/tools/setup.ts +47 -51
- package/src/tools/summarize.test.ts +20 -0
- package/src/tools/summarize.ts +17 -13
- package/src/tools/wait.ts +39 -26
- package/taiga-ai-mcp-server-0.19.0-dev.451.tgz +0 -0
- package/tsconfig.json +1 -5
- package/taiga-ai-mcp-server-0.18.0-rc.445.tgz +0 -0
package/src/tools/autonomy.ts
CHANGED
|
@@ -15,8 +15,6 @@ import { pickFields } from './summarize.js';
|
|
|
15
15
|
* is taiga_set_auto_build.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
type Row = Record<string, unknown>;
|
|
19
|
-
|
|
20
18
|
export function registerAutonomyTools(server: McpServer, client: TaigaClient) {
|
|
21
19
|
if (client.hasCapability('products:write')) {
|
|
22
20
|
server.registerTool(
|
|
@@ -54,7 +52,7 @@ export function registerAutonomyTools(server: McpServer, client: TaigaClient) {
|
|
|
54
52
|
...(params.autoBuild !== undefined ? { autoBuild: params.autoBuild } : {}),
|
|
55
53
|
...(autoMerge !== undefined ? { autoMerge } : {}),
|
|
56
54
|
})
|
|
57
|
-
).data
|
|
55
|
+
).data;
|
|
58
56
|
return toolResult(pickFields([product], ['id', 'name', 'slug', 'autoBuild', 'autoMerge'])[0]);
|
|
59
57
|
})
|
|
60
58
|
);
|
|
@@ -72,7 +70,7 @@ export function registerAutonomyTools(server: McpServer, client: TaigaClient) {
|
|
|
72
70
|
'organization and factories allow: where either has turned autonomous merge off, true changes nothing. ' +
|
|
73
71
|
'Taiga merges through your rules, never around them.',
|
|
74
72
|
inputSchema: z.object({
|
|
75
|
-
|
|
73
|
+
initiativeId: idInput('initiative', 'taiga_list_initiatives (query finds one by title) gives it'),
|
|
76
74
|
autoMerge: z
|
|
77
75
|
.boolean()
|
|
78
76
|
.nullable()
|
|
@@ -82,7 +80,7 @@ export function registerAutonomyTools(server: McpServer, client: TaigaClient) {
|
|
|
82
80
|
},
|
|
83
81
|
(params) =>
|
|
84
82
|
runTool(async () => {
|
|
85
|
-
const item = (await client.updateBacklogItem(params.
|
|
83
|
+
const item = (await client.updateBacklogItem(params.initiativeId, { autoMerge: params.autoMerge })).data;
|
|
86
84
|
return toolResult(pickFields([item], ['id', 'title', 'state', 'autoMerge', 'autoBuild'])[0]);
|
|
87
85
|
})
|
|
88
86
|
);
|
package/src/tools/backlogs.ts
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
type ActivityEvent,
|
|
5
|
+
type ActivityActor,
|
|
6
|
+
type BacklogItem,
|
|
7
|
+
BUILD_RUN_STATUSES,
|
|
8
|
+
PLAN_REJECTION_COMMENT_MAX_CHARS,
|
|
9
|
+
PR_CLOSING_STATES,
|
|
10
|
+
USER_DROP_STATES,
|
|
11
|
+
renderHalt,
|
|
12
|
+
resolveAutoBuild,
|
|
13
|
+
type BuildRunStatus,
|
|
14
|
+
type Halt,
|
|
15
|
+
} from '@taiga/types';
|
|
16
|
+
import { TaigaApiError, type TaigaClient, type WithoutTenant } from '../client.js';
|
|
5
17
|
import { OPERATE, OPERATE_SETTING, READ_ONLY } from './annotations.js';
|
|
6
18
|
import { idInput, pagination, productId } from './inputs.js';
|
|
7
19
|
import { isNetworkFailure, runTool, toolError, toolResult } from './errors.js';
|
|
8
20
|
import { jobReport } from './jobs.js';
|
|
9
21
|
import { requestPrompt } from './request-prompt.js';
|
|
10
22
|
import { queueView } from './queue.js';
|
|
23
|
+
import { landedDisposition } from './builds.js';
|
|
11
24
|
import {
|
|
12
25
|
SEARCH_NOTE,
|
|
13
26
|
SEARCH_PAGE_SIZE,
|
|
@@ -61,11 +74,106 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
61
74
|
* leaves it null, and the lifecycle is what says why and what moves it on.
|
|
62
75
|
*/
|
|
63
76
|
const LIFECYCLE_MEANING =
|
|
64
|
-
'
|
|
77
|
+
'situation says in one sentence where it stands, whose turn it is and the call that moves it on. The same ' +
|
|
78
|
+
'as fields: lifecycle says whose turn it is and what moves it on: cta start_plan is taiga_queue_initiative, ' +
|
|
65
79
|
'retry_plan taiga_plan_again (planFailureCause says why the plan stopped), start_build taiga_approve_plan, ' +
|
|
66
|
-
'resume_build
|
|
80
|
+
'resume_build taiga_resume_initiative, retry_build a fresh run (start over, in the web), review_pr and ' +
|
|
67
81
|
'view_pr the pull request. needsHumanCause is set only when the line stopped on it.';
|
|
68
82
|
|
|
83
|
+
/** The move each call to action is, in the tools' words. */
|
|
84
|
+
const CTA_MOVE: Record<string, string> = {
|
|
85
|
+
start_plan: 'taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)',
|
|
86
|
+
retry_plan: 'taiga_plan_again asks for a new plan (paid work: ask first)',
|
|
87
|
+
start_build:
|
|
88
|
+
'read the plan with taiga_get_plan, then taiga_approve_plan builds it or taiga_reject_plan sends it back',
|
|
89
|
+
resume_build: 'taiga_resume_initiative carries the run on (paid work: ask first)',
|
|
90
|
+
retry_build: 'a new run starts over from the web; taiga_move_initiative takes it off the line',
|
|
91
|
+
review_pr: 'a person reviews and merges the pull request',
|
|
92
|
+
view_pr: 'the line merges the pull request once its checks pass',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** How a run stands, as the end of "its latest run ...", by disposition. */
|
|
96
|
+
const RUN_STANDS: Record<string, string> = {
|
|
97
|
+
in_flight: 'is running',
|
|
98
|
+
gated: 'is in review',
|
|
99
|
+
succeeded: 'merged',
|
|
100
|
+
unmerged: 'settled unmerged',
|
|
101
|
+
failed: 'failed',
|
|
102
|
+
stopped: 'was stopped',
|
|
103
|
+
rejected: 'had its pull request closed without merging',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Where an initiative is, in a phrase, by its lifecycle stage. */
|
|
107
|
+
const STAGE: Record<string, string> = {
|
|
108
|
+
backlog: 'In the backlog, not on the line yet',
|
|
109
|
+
planning: 'Being planned',
|
|
110
|
+
planned: 'Planned',
|
|
111
|
+
building: 'Being built',
|
|
112
|
+
in_review: 'Its pull request is in review',
|
|
113
|
+
done: 'Done',
|
|
114
|
+
canceled: 'Canceled',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Where an initiative stands and what moves it on, in one sentence (PRO-2800).
|
|
119
|
+
* Five fields carried it (status, state, planningStatus, needsHumanCause,
|
|
120
|
+
* lifecycle) and a reviewing agent had to read the queue, the initiative and
|
|
121
|
+
* the run together to say what had happened; the lifecycle the API derives
|
|
122
|
+
* already decides it. Null for a row without a lifecycle.
|
|
123
|
+
*/
|
|
124
|
+
export function situationOf(item: Record<string, unknown>): string | null {
|
|
125
|
+
const life = item['lifecycle'] as
|
|
126
|
+
| { turn?: unknown; stage?: unknown; cta?: unknown; planFailureCause?: unknown; latestBuild?: unknown }
|
|
127
|
+
| null
|
|
128
|
+
| undefined;
|
|
129
|
+
if (!life || typeof life.stage !== 'string') return null;
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
let stage = STAGE[life.stage] ?? life.stage;
|
|
132
|
+
if (life.stage === 'planning' && life.turn === 'you') {
|
|
133
|
+
stage = `Planning stopped${typeof life.planFailureCause === 'string' ? ` (${life.planFailureCause})` : ''}`;
|
|
134
|
+
}
|
|
135
|
+
const build = life.latestBuild as { status?: unknown; prNumber?: unknown; prMerged?: unknown } | null | undefined;
|
|
136
|
+
if (build && typeof build.status === 'string' && life.stage !== 'done' && life.stage !== 'canceled') {
|
|
137
|
+
const pr = typeof build.prNumber === 'number' ? ` (PR #${build.prNumber})` : '';
|
|
138
|
+
const said = (BUILD_RUN_STATUSES as readonly string[]).includes(build.status)
|
|
139
|
+
? RUN_STANDS[landedDisposition(build.status as BuildRunStatus, build.prMerged === true)]
|
|
140
|
+
: build.status;
|
|
141
|
+
stage += `; its latest run ${said}${pr}`;
|
|
142
|
+
}
|
|
143
|
+
parts.push(`${stage}.`);
|
|
144
|
+
if (typeof item['needsHumanCause'] === 'string') {
|
|
145
|
+
const halt = renderHalt({ cause: item['needsHumanCause'], params: {} } as unknown as Halt);
|
|
146
|
+
const detail = typeof item['needsHumanDetail'] === 'string' ? item['needsHumanDetail'] : '';
|
|
147
|
+
// The halt says what stopped the line; the detail is the run's own reason, kept beside it.
|
|
148
|
+
const why = [halt, detail]
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.map((text) => text.replace(/\.$/, ''))
|
|
151
|
+
.join('. ');
|
|
152
|
+
if (why) parts.push(`The line stopped on it: ${why}.`);
|
|
153
|
+
}
|
|
154
|
+
const turn = life.turn === 'you' ? "A person's turn" : life.turn === 'agent' ? "The agent's turn" : null;
|
|
155
|
+
const move = typeof life.cta === 'string' ? CTA_MOVE[life.cta] : undefined;
|
|
156
|
+
if (life.cta === 'view_pr') parts.push(`Nothing to do: ${move}.`);
|
|
157
|
+
else if (turn && move) parts.push(`${turn}: ${move}.`);
|
|
158
|
+
else if (turn) parts.push(`${turn}.`);
|
|
159
|
+
else if (move) parts.push(`Next: ${move}.`);
|
|
160
|
+
return parts.join(' ');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A row or item with its situation added when it has a lifecycle. */
|
|
164
|
+
export function withSituation(
|
|
165
|
+
item: Record<string, unknown>,
|
|
166
|
+
row: Record<string, unknown> = item
|
|
167
|
+
): Record<string, unknown> {
|
|
168
|
+
const situation = situationOf(item);
|
|
169
|
+
return situation ? { ...row, situation } : row;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** List rows, each with its situation. */
|
|
173
|
+
export function situatedRows(items: Record<string, unknown>[]): Record<string, unknown>[] {
|
|
174
|
+
return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index]!, row));
|
|
175
|
+
}
|
|
176
|
+
|
|
69
177
|
/** What each plan failure cause means for whoever reads it next (PLAN_FAILURE_CAUSES in @taiga/types). */
|
|
70
178
|
const PLAN_FAILURE_MEANING: Record<string, string> = {
|
|
71
179
|
agent_error: 'The planning agent failed. Planning again is the usual next step.',
|
|
@@ -80,19 +188,19 @@ const PLAN_FAILURE_MEANING: Record<string, string> = {
|
|
|
80
188
|
*/
|
|
81
189
|
async function planFailureOf(
|
|
82
190
|
client: TaigaClient,
|
|
83
|
-
|
|
191
|
+
initiativeId: string,
|
|
84
192
|
itemCause: unknown
|
|
85
193
|
): Promise<Record<string, unknown> | null> {
|
|
86
|
-
let events:
|
|
194
|
+
let events: WithoutTenant<ActivityEvent>[];
|
|
87
195
|
try {
|
|
88
|
-
events = (await client.listBacklogItemActivity(
|
|
196
|
+
events = (await client.listBacklogItemActivity(initiativeId)).data;
|
|
89
197
|
} catch (error) {
|
|
90
198
|
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
91
199
|
throw error;
|
|
92
200
|
}
|
|
93
201
|
// Only a failure of the latest attempt: one before the last plan_started
|
|
94
202
|
// belongs to an earlier attempt, even when it failed for the same reason.
|
|
95
|
-
let failed:
|
|
203
|
+
let failed: WithoutTenant<ActivityEvent> | undefined;
|
|
96
204
|
for (const event of [...events].reverse()) {
|
|
97
205
|
if (event.kind !== 'turn') continue;
|
|
98
206
|
if (event.trigger === 'plan_started') break;
|
|
@@ -102,8 +210,8 @@ async function planFailureOf(
|
|
|
102
210
|
}
|
|
103
211
|
}
|
|
104
212
|
if (!failed) return null;
|
|
105
|
-
const actor =
|
|
106
|
-
const data =
|
|
213
|
+
const actor: Partial<ActivityActor> = failed.actor ?? {};
|
|
214
|
+
const data: { cause?: unknown } = failed.data ?? {};
|
|
107
215
|
const cause = typeof data.cause === 'string' ? data.cause : null;
|
|
108
216
|
// The event is recorded best effort: one that disagrees with the item's own
|
|
109
217
|
// cause belongs to an earlier failure, and its who and when are not this one's.
|
|
@@ -125,7 +233,7 @@ async function planFailureOf(
|
|
|
125
233
|
|
|
126
234
|
const planId = idInput('plan', "taiga_get_plan gives the initiative's active plan and its ID");
|
|
127
235
|
|
|
128
|
-
const
|
|
236
|
+
const initiativeId = idInput('initiative', 'taiga_list_initiatives (query finds one by title) gives it');
|
|
129
237
|
|
|
130
238
|
/** The plan without its steps' prose: enough to approve it or say what a run will do. */
|
|
131
239
|
const PLAN_SUMMARY_FIELDS = ['id', 'backlogItemId', 'version', 'status', 'createdAt'] as const;
|
|
@@ -140,13 +248,16 @@ const PLAN_STEP_SUMMARY_FIELDS = [
|
|
|
140
248
|
'createsResources',
|
|
141
249
|
] as const;
|
|
142
250
|
|
|
143
|
-
|
|
144
|
-
const outcomeOf = (item: unknown) => {
|
|
145
|
-
const row = item as Record<string, unknown>;
|
|
146
|
-
return { ...pickFields([row], ITEM_OUTCOME_FIELDS)[0], criteria: criteriaCounts(row) };
|
|
147
|
-
};
|
|
251
|
+
type Item = WithoutTenant<BacklogItem>;
|
|
148
252
|
|
|
149
|
-
|
|
253
|
+
/** The outcome fields of an initiative, with how many of its criteria are met, and where it stands. */
|
|
254
|
+
const outcomeOf = (item: Item) =>
|
|
255
|
+
withSituation(item as unknown as Record<string, unknown>, {
|
|
256
|
+
...pickFields([item], ITEM_OUTCOME_FIELDS)[0],
|
|
257
|
+
criteria: criteriaCounts(item),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const outcome = (item: Item) => toolResult(outcomeOf(item));
|
|
150
261
|
|
|
151
262
|
/**
|
|
152
263
|
* Each tool is registered under the capability its route requires, so a
|
|
@@ -180,15 +291,16 @@ export function registerBacklogTools(server: McpServer, client: TaigaClient) {
|
|
|
180
291
|
|
|
181
292
|
function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
182
293
|
server.registerTool(
|
|
183
|
-
'
|
|
294
|
+
'taiga_list_initiatives',
|
|
184
295
|
{
|
|
185
|
-
title: 'List
|
|
296
|
+
title: 'List initiatives',
|
|
186
297
|
description:
|
|
187
|
-
"List a product's
|
|
188
|
-
'line (backlog, todo, queue, plan, build, done)
|
|
298
|
+
"List a product's initiatives, its backlog: titles, category, status, where each one is on the " +
|
|
299
|
+
'line (backlog, todo, queue, plan, build, done), situation (where it stands and what moves it on, in a ' +
|
|
300
|
+
'sentence) and criteria, how many of its acceptance criteria are met ' +
|
|
189
301
|
'(null before it is planned; met counts the criteria the agent confirmed with evidence or a person checked ' +
|
|
190
302
|
'off, so a done item can show fewer met than it has). Use this to find the item you are working on (query ' +
|
|
191
|
-
'finds one by title), then read its acceptance criteria and technical context with
|
|
303
|
+
'finds one by title), then read its acceptance criteria and technical context with taiga_get_initiative. ' +
|
|
192
304
|
'Archived items are left out unless asked for.',
|
|
193
305
|
inputSchema: z.object({
|
|
194
306
|
productId,
|
|
@@ -218,9 +330,7 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
218
330
|
const { query, ...filters } = params;
|
|
219
331
|
if (query === undefined) {
|
|
220
332
|
const result = await client.listBacklogItems(params.productId, filters);
|
|
221
|
-
return toolResult(
|
|
222
|
-
page(itemRows(result.data as Record<string, unknown>[], BACKLOG_SUMMARY_FIELDS), result.nextCursor)
|
|
223
|
-
);
|
|
333
|
+
return toolResult(page(situatedRows(result.data as Record<string, unknown>[]), result.nextCursor));
|
|
224
334
|
}
|
|
225
335
|
if (params.cursor !== undefined) {
|
|
226
336
|
return toolError('VALIDATION_ERROR', SEARCH_WITH_CURSOR);
|
|
@@ -232,33 +342,37 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
232
342
|
params.limit ?? 25
|
|
233
343
|
);
|
|
234
344
|
return toolResult({
|
|
235
|
-
...page(
|
|
345
|
+
...page(situatedRows(found.rows), null),
|
|
236
346
|
...(found.more ? { more: SEARCH_NOTE } : {}),
|
|
237
347
|
});
|
|
238
348
|
})
|
|
239
349
|
);
|
|
240
350
|
|
|
241
351
|
server.registerTool(
|
|
242
|
-
'
|
|
352
|
+
'taiga_get_initiative',
|
|
243
353
|
{
|
|
244
|
-
title: 'Get
|
|
354
|
+
title: 'Get initiative',
|
|
245
355
|
description:
|
|
246
|
-
'Get one
|
|
356
|
+
'Get one initiative in full: description, acceptance criteria (and which are met), technical ' +
|
|
247
357
|
'context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before ' +
|
|
248
|
-
'implementing an item, and read its runs with
|
|
358
|
+
'implementing an item, and read its runs with taiga_list_runs to see what the coding agent already did. ' +
|
|
249
359
|
'A failed plan carries planFailure: the cause, what it means, when, and by whom. ' +
|
|
250
360
|
LIFECYCLE_MEANING,
|
|
251
361
|
inputSchema: z.object({
|
|
252
|
-
|
|
362
|
+
initiativeId,
|
|
253
363
|
}),
|
|
254
364
|
annotations: READ_ONLY,
|
|
255
365
|
},
|
|
256
366
|
(params) =>
|
|
257
367
|
runTool(async () => {
|
|
258
|
-
const item = (await client.getBacklogItem(params.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
368
|
+
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
369
|
+
const row = item as unknown as Record<string, unknown>;
|
|
370
|
+
if (item.planningStatus !== 'failed') return toolResult(withSituation(row));
|
|
371
|
+
// Always undefined: the API sends the cause only under `lifecycle`, so
|
|
372
|
+
// planFailureOf's cross-check never fires (PRO-2799). Kept as it was.
|
|
373
|
+
const ownCause = (item as { planFailureCause?: unknown }).planFailureCause;
|
|
374
|
+
const planFailure = await planFailureOf(client, params.initiativeId, ownCause);
|
|
375
|
+
return toolResult(withSituation(row, planFailure ? { ...item, planFailure } : item));
|
|
262
376
|
})
|
|
263
377
|
);
|
|
264
378
|
|
|
@@ -274,16 +388,16 @@ function registerBacklogReads(server: McpServer, client: TaigaClient) {
|
|
|
274
388
|
"full: true for each step's complete instructions, several thousand tokens on a large plan. NOT_FOUND " +
|
|
275
389
|
'when the initiative has no active plan yet (planningStatus says whether one is being generated).',
|
|
276
390
|
inputSchema: z.object({
|
|
277
|
-
|
|
391
|
+
initiativeId,
|
|
278
392
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)"),
|
|
279
393
|
}),
|
|
280
394
|
annotations: READ_ONLY,
|
|
281
395
|
},
|
|
282
396
|
(params) =>
|
|
283
397
|
runTool(async () => {
|
|
284
|
-
const plan = (await client.getActivePlan(params.
|
|
398
|
+
const plan = (await client.getActivePlan(params.initiativeId)).data;
|
|
285
399
|
if (params.full) return toolResult(plan);
|
|
286
|
-
const steps = Array.isArray(plan.steps) ?
|
|
400
|
+
const steps = Array.isArray(plan.steps) ? plan.steps : [];
|
|
287
401
|
return toolResult({
|
|
288
402
|
...pickFields([plan], PLAN_SUMMARY_FIELDS)[0],
|
|
289
403
|
steps: pickFields(steps, PLAN_STEP_SUMMARY_FIELDS),
|
|
@@ -331,7 +445,7 @@ function registerRequest(server: McpServer, client: TaigaClient) {
|
|
|
331
445
|
targetId: params.productId,
|
|
332
446
|
...(userPrompt ? { metadata: { userPrompt } } : {}),
|
|
333
447
|
})
|
|
334
|
-
).data
|
|
448
|
+
).data;
|
|
335
449
|
return toolResult(jobReport(job));
|
|
336
450
|
})
|
|
337
451
|
);
|
|
@@ -339,12 +453,12 @@ function registerRequest(server: McpServer, client: TaigaClient) {
|
|
|
339
453
|
|
|
340
454
|
function registerMove(server: McpServer, client: TaigaClient) {
|
|
341
455
|
server.registerTool(
|
|
342
|
-
'
|
|
456
|
+
'taiga_move_initiative',
|
|
343
457
|
{
|
|
344
|
-
title: 'Move
|
|
458
|
+
title: 'Move initiative on the board',
|
|
345
459
|
description:
|
|
346
460
|
'Move an initiative on the board, or order it within a column: backlog and todo are yours and the line ' +
|
|
347
|
-
'never moves an initiative out of them; queue puts it in the line (
|
|
461
|
+
'never moves an initiative out of them; queue puts it in the line (taiga_queue_initiative says more); ' +
|
|
348
462
|
'done and canceled finish it by hand. The queue is the only way to start work: there is no "plan this ' +
|
|
349
463
|
'now", and nothing is moved into Build by a person. Moving an initiative out of Build stops the plan ' +
|
|
350
464
|
'being written or the run in flight: that is how work is stopped, and it throws that work away. A move to ' +
|
|
@@ -355,9 +469,9 @@ function registerMove(server: McpServer, client: TaigaClient) {
|
|
|
355
469
|
'archived: true files the initiative where it stands (pass its current column as state; never from the Build station, plan or build) ' +
|
|
356
470
|
'and archived: false brings it back; archived work is left out of lists unless includeArchived asks for it.',
|
|
357
471
|
inputSchema: z.object({
|
|
358
|
-
|
|
472
|
+
initiativeId,
|
|
359
473
|
state: z.enum(USER_DROP_STATES).describe('The column to move into'),
|
|
360
|
-
afterId: idInput('
|
|
474
|
+
afterId: idInput('initiative', 'taiga_list_initiatives gives it')
|
|
361
475
|
.nullable()
|
|
362
476
|
.optional()
|
|
363
477
|
.describe('The initiative to follow in that column; null for the head; omit to keep rank order'),
|
|
@@ -370,7 +484,7 @@ function registerMove(server: McpServer, client: TaigaClient) {
|
|
|
370
484
|
},
|
|
371
485
|
(params) =>
|
|
372
486
|
runTool(async () => {
|
|
373
|
-
const {
|
|
487
|
+
const { initiativeId: id, ...body } = params;
|
|
374
488
|
// What the route checks on top of backlog:write for a drop into the
|
|
375
489
|
// queue; refused here so the model learns which capability, not just
|
|
376
490
|
// that it was forbidden.
|
|
@@ -400,10 +514,10 @@ function registerPlanAgain(server: McpServer, client: TaigaClient) {
|
|
|
400
514
|
'stays open. Offered only when the turn is yours: refused (CONFLICT) while an agent holds it (planning, ' +
|
|
401
515
|
'building, reviewing). Also refused while a dependency is still in flight or a UI direction has no ' +
|
|
402
516
|
'chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the ' +
|
|
403
|
-
'Build station, put the initiative in the queue instead (
|
|
517
|
+
'Build station, put the initiative in the queue instead (taiga_queue_initiative): the queue is the ' +
|
|
404
518
|
'only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run.',
|
|
405
519
|
inputSchema: z.object({
|
|
406
|
-
|
|
520
|
+
initiativeId,
|
|
407
521
|
force: z
|
|
408
522
|
.boolean()
|
|
409
523
|
.optional()
|
|
@@ -415,16 +529,16 @@ function registerPlanAgain(server: McpServer, client: TaigaClient) {
|
|
|
415
529
|
},
|
|
416
530
|
(params) =>
|
|
417
531
|
runTool(async () => {
|
|
418
|
-
const item = (await client.getBacklogItem(params.
|
|
532
|
+
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
419
533
|
if (!BUILD_STATION.has(String(item.state))) {
|
|
420
534
|
return toolError(
|
|
421
535
|
'CONFLICT',
|
|
422
536
|
`This initiative is in ${String(item.state)}, not at the Build station. The queue is the only way to ` +
|
|
423
|
-
'start work: put it in the queue with
|
|
537
|
+
'start work: put it in the queue with taiga_queue_initiative and it is planned when its turn comes.'
|
|
424
538
|
);
|
|
425
539
|
}
|
|
426
540
|
const body = { state: 'plan', ...(params.force ? { force: true } : {}) };
|
|
427
|
-
return outcome((await client.transitionInitiative(params.
|
|
541
|
+
return outcome((await client.transitionInitiative(params.initiativeId, body)).data);
|
|
428
542
|
})
|
|
429
543
|
);
|
|
430
544
|
}
|
|
@@ -437,12 +551,11 @@ const isApiFailure = (error: unknown) => error instanceof TaigaApiError || isNet
|
|
|
437
551
|
* decided it: its own setting, or the product's default when it has none.
|
|
438
552
|
* Null when the product cannot be read; the outcome then says so instead.
|
|
439
553
|
*/
|
|
440
|
-
async function reviewGate(client: TaigaClient, item:
|
|
554
|
+
async function reviewGate(client: TaigaClient, item: Item) {
|
|
441
555
|
const own = typeof item.autoBuild === 'boolean' ? item.autoBuild : null;
|
|
442
556
|
try {
|
|
443
557
|
// The product's default answers only when the initiative has no answer of its own.
|
|
444
|
-
const product =
|
|
445
|
-
own === null ? ((await client.getProduct(String(item.productId))).data as { autoBuild?: unknown }) : {};
|
|
558
|
+
const product: { autoBuild?: boolean | null } = own === null ? (await client.getProduct(item.productId)).data : {};
|
|
446
559
|
// Absent on the product reads as its true default (@taiga/types).
|
|
447
560
|
const productDefault = typeof product.autoBuild === 'boolean' ? product.autoBuild : true;
|
|
448
561
|
const builds = resolveAutoBuild({ initiative: own, product: productDefault });
|
|
@@ -460,9 +573,9 @@ async function reviewGate(client: TaigaClient, item: Record<string, unknown>) {
|
|
|
460
573
|
}
|
|
461
574
|
|
|
462
575
|
/** Where a queued initiative stands in its product's line; null, and the outcome says so, when the queue cannot be read. */
|
|
463
|
-
async function placeInQueue(client: TaigaClient, item:
|
|
576
|
+
async function placeInQueue(client: TaigaClient, item: Item) {
|
|
464
577
|
try {
|
|
465
|
-
const view = await queueView(client,
|
|
578
|
+
const view = await queueView(client, item.productId, { name: 'blocking' });
|
|
466
579
|
const entry = view.entries.find((row) => row.backlogItemId === item.id);
|
|
467
580
|
return { position: entry?.position ?? null, of: view.entries.length, waitingOn: view.waitingOn };
|
|
468
581
|
} catch (error) {
|
|
@@ -481,9 +594,9 @@ const autoBuildInput = z
|
|
|
481
594
|
|
|
482
595
|
function registerQueue(server: McpServer, client: TaigaClient) {
|
|
483
596
|
server.registerTool(
|
|
484
|
-
'
|
|
597
|
+
'taiga_queue_initiative',
|
|
485
598
|
{
|
|
486
|
-
title: 'Queue
|
|
599
|
+
title: 'Queue initiative',
|
|
487
600
|
description:
|
|
488
601
|
'Add an initiative to the product queue, the line the coding agent works: queued initiatives are ' +
|
|
489
602
|
'planned and built one at a time, each advancing when the previous pull request merges. This starts ' +
|
|
@@ -491,12 +604,12 @@ function registerQueue(server: McpServer, client: TaigaClient) {
|
|
|
491
604
|
"repository. Whether the plan is built at once or waits for a person is the initiative's review gate: " +
|
|
492
605
|
"pass autoBuild: false to read the plan before anything is built (the product's default applies " +
|
|
493
606
|
'otherwise, and the outcome says which it is); the gate is set first and stays set if the move is then ' +
|
|
494
|
-
'refused.
|
|
607
|
+
'refused. An initiative already ' +
|
|
495
608
|
'in the queue stays where it is unless afterId says where to go; one being planned or built cannot move ' +
|
|
496
609
|
'(CONFLICT). Returns the initiative with its new state, its review gate and its place in the queue.',
|
|
497
610
|
inputSchema: z.object({
|
|
498
|
-
|
|
499
|
-
afterId: idInput('
|
|
611
|
+
initiativeId,
|
|
612
|
+
afterId: idInput('initiative', 'taiga_list_initiatives gives it')
|
|
500
613
|
.nullable()
|
|
501
614
|
.optional()
|
|
502
615
|
.describe(
|
|
@@ -513,16 +626,13 @@ function registerQueue(server: McpServer, client: TaigaClient) {
|
|
|
513
626
|
},
|
|
514
627
|
(params) =>
|
|
515
628
|
runTool(async () => {
|
|
516
|
-
const {
|
|
629
|
+
const { initiativeId: id, autoBuild, ...placement } = params;
|
|
517
630
|
// The gate first: set after the move, a plan landing in between
|
|
518
631
|
// would be built under the old answer.
|
|
519
632
|
if (autoBuild !== undefined) await client.setAutoBuild(id, autoBuild);
|
|
520
633
|
// The move the web makes: one write that answers with the refreshed
|
|
521
634
|
// item, rather than the queue route plus two reads around it.
|
|
522
|
-
const item = (await client.transitionInitiative(id, { state: 'queue', ...placement })).data
|
|
523
|
-
string,
|
|
524
|
-
unknown
|
|
525
|
-
>;
|
|
635
|
+
const item = (await client.transitionInitiative(id, { state: 'queue', ...placement })).data;
|
|
526
636
|
const [gate, queue] = await Promise.all([reviewGate(client, item), placeInQueue(client, item)]);
|
|
527
637
|
return toolResult({
|
|
528
638
|
...outcomeOf(item),
|
|
@@ -530,7 +640,7 @@ function registerQueue(server: McpServer, client: TaigaClient) {
|
|
|
530
640
|
queue,
|
|
531
641
|
...(gate && queue
|
|
532
642
|
? {}
|
|
533
|
-
: { note: 'Part of the outcome could not be read;
|
|
643
|
+
: { note: 'Part of the outcome could not be read; taiga_get_line and taiga_get_product say the rest.' }),
|
|
534
644
|
});
|
|
535
645
|
})
|
|
536
646
|
);
|
|
@@ -546,12 +656,12 @@ function registerReviewGate(server: McpServer, client: TaigaClient) {
|
|
|
546
656
|
"approve or reject it (false); null follows the product's default. Standing: it applies to every plan the " +
|
|
547
657
|
'initiative gets, including one being written now. Opening the gate on a plan that is already waiting ' +
|
|
548
658
|
'starts its build at once. Returns the initiative with the gate that now applies.',
|
|
549
|
-
inputSchema: z.object({
|
|
659
|
+
inputSchema: z.object({ initiativeId, autoBuild: autoBuildInput }),
|
|
550
660
|
annotations: OPERATE_SETTING,
|
|
551
661
|
},
|
|
552
662
|
(params) =>
|
|
553
663
|
runTool(async () => {
|
|
554
|
-
const item = (await client.setAutoBuild(params.
|
|
664
|
+
const item = (await client.setAutoBuild(params.initiativeId, params.autoBuild)).data;
|
|
555
665
|
const gate = await reviewGate(client, item);
|
|
556
666
|
return toolResult({
|
|
557
667
|
...outcomeOf(item),
|
|
@@ -564,9 +674,9 @@ function registerReviewGate(server: McpServer, client: TaigaClient) {
|
|
|
564
674
|
|
|
565
675
|
function registerResume(server: McpServer, client: TaigaClient) {
|
|
566
676
|
server.registerTool(
|
|
567
|
-
'
|
|
677
|
+
'taiga_resume_initiative',
|
|
568
678
|
{
|
|
569
|
-
title: 'Resume
|
|
679
|
+
title: 'Resume initiative',
|
|
570
680
|
description:
|
|
571
681
|
'Resume an initiative the line stopped on, which is when needsHumanCause is set (the line sets it): a ' +
|
|
572
682
|
'failed or cancelled plan is planned again, a stopped or failed run resumes from its first unfinished ' +
|
|
@@ -575,10 +685,10 @@ function registerResume(server: McpServer, client: TaigaClient) {
|
|
|
575
685
|
'initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume: a plan that failed ' +
|
|
576
686
|
'with needsHumanCause null (lifecycle.cta retry_plan, planFailureCause says why) is planned again with ' +
|
|
577
687
|
'taiga_plan_again. Starts paid work.',
|
|
578
|
-
inputSchema: z.object({
|
|
688
|
+
inputSchema: z.object({ initiativeId }),
|
|
579
689
|
annotations: OPERATE,
|
|
580
690
|
},
|
|
581
|
-
(params) => runTool(async () => outcome((await client.resumeInitiative(params.
|
|
691
|
+
(params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data))
|
|
582
692
|
);
|
|
583
693
|
}
|
|
584
694
|
|