@taiga-ai/mcp-server 0.25.0-rc.474 → 0.26.0-dev.481
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/README.md +111 -0
- package/dist/index.js +292 -115
- package/package.json +15 -2
- package/scripts/build.mjs +0 -37
- package/scripts/prepare-publish.cjs +0 -49
- package/src/client.test.ts +0 -424
- package/src/client.ts +0 -977
- package/src/config.test.ts +0 -24
- package/src/config.ts +0 -28
- package/src/index.ts +0 -45
- package/src/modern.integration.test.ts +0 -153
- package/src/server.integration.test.ts +0 -9214
- package/src/server.ts +0 -18
- package/src/tools/annotations.ts +0 -38
- package/src/tools/autonomy.ts +0 -86
- package/src/tools/backlogs.ts +0 -802
- package/src/tools/builds.ts +0 -668
- package/src/tools/catalogue.test.ts +0 -170
- package/src/tools/catalogue.ts +0 -223
- package/src/tools/context.ts +0 -513
- package/src/tools/documents.ts +0 -375
- package/src/tools/errors.ts +0 -172
- package/src/tools/hints.ts +0 -99
- package/src/tools/inbox.ts +0 -546
- package/src/tools/index.ts +0 -239
- package/src/tools/inputs.ts +0 -47
- package/src/tools/instructions.ts +0 -41
- package/src/tools/jobs.ts +0 -90
- package/src/tools/knowledge.ts +0 -249
- package/src/tools/maintenance.ts +0 -547
- package/src/tools/markdown.test.ts +0 -249
- package/src/tools/markdown.ts +0 -151
- package/src/tools/observe.ts +0 -311
- package/src/tools/product-documents.ts +0 -184
- package/src/tools/products.ts +0 -529
- package/src/tools/prompts.ts +0 -265
- package/src/tools/queue.ts +0 -250
- package/src/tools/readiness.ts +0 -177
- package/src/tools/remote.test.ts +0 -44
- package/src/tools/remote.ts +0 -107
- package/src/tools/repositories.ts +0 -360
- package/src/tools/request-prompt.test.ts +0 -32
- package/src/tools/request-prompt.ts +0 -28
- package/src/tools/resources.ts +0 -217
- package/src/tools/sections.test.ts +0 -143
- package/src/tools/sections.ts +0 -287
- package/src/tools/setup.ts +0 -455
- package/src/tools/summarize.test.ts +0 -20
- package/src/tools/summarize.ts +0 -96
- package/src/tools/wait.ts +0 -490
- package/src/version.test.ts +0 -10
- package/src/version.ts +0 -14
- package/taiga-ai-mcp-server-0.25.0-rc.474.tgz +0 -0
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
package/src/tools/setup.ts
DELETED
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
-
import { DEPLOYMENT_NOTES_MAX_LENGTH, REALITY_DERIVED_DOCUMENT_TYPES } from '@taiga/types';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import type { Product } from '@taiga/types';
|
|
5
|
-
import { TaigaApiError, type TaigaClient, type WithoutTenant } from '../client.js';
|
|
6
|
-
import { OPERATE, OPERATE_SETTING, READ_ONLY, WRITE } from './annotations.js';
|
|
7
|
-
import { isImportedProduct } from './catalogue.js';
|
|
8
|
-
import { runTool, toolError, toolResult } from './errors.js';
|
|
9
|
-
import { setupReadiness } from './readiness.js';
|
|
10
|
-
import { withStop } from './hints.js';
|
|
11
|
-
import { idInput, productId } from './inputs.js';
|
|
12
|
-
import { pickFields } from './summarize.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Setting a product up (docs.tai.ga, Start: Your first product, Learn: Import
|
|
16
|
-
* an existing codebase, Organizations, factories and products): factories to
|
|
17
|
-
* put products in, a product started from scratch or from an imported
|
|
18
|
-
* codebase, what Taiga builds for it, the import, and finishing or reopening
|
|
19
|
-
* Discovery. Who may reach a factory (its members) and deleting a factory or
|
|
20
|
-
* a product stay in the web.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const PRODUCT_FIELDS = [
|
|
24
|
-
'id',
|
|
25
|
-
'name',
|
|
26
|
-
'slug',
|
|
27
|
-
'status',
|
|
28
|
-
'factoryId',
|
|
29
|
-
'ownsInfrastructure',
|
|
30
|
-
'ownsDeployment',
|
|
31
|
-
'deploymentNotes',
|
|
32
|
-
'emitDesignStandards',
|
|
33
|
-
'settings',
|
|
34
|
-
] as const;
|
|
35
|
-
|
|
36
|
-
const FACTORY_FIELDS = ['id', 'name', 'slug', 'isDefault'] as const;
|
|
37
|
-
|
|
38
|
-
const factoryId = idInput('factory', 'taiga_list_factories (query finds one by name) gives it');
|
|
39
|
-
|
|
40
|
-
const buildScope = {
|
|
41
|
-
ownsInfrastructure: z
|
|
42
|
-
.boolean()
|
|
43
|
-
.nullable()
|
|
44
|
-
.optional()
|
|
45
|
-
.describe('Whether Taiga writes the infrastructure code; null follows the organization'),
|
|
46
|
-
ownsDeployment: z
|
|
47
|
-
.boolean()
|
|
48
|
-
.nullable()
|
|
49
|
-
.optional()
|
|
50
|
-
.describe('Whether Taiga writes the CI/CD pipelines; null follows the organization'),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const deploymentNotes = z
|
|
54
|
-
.string()
|
|
55
|
-
.trim()
|
|
56
|
-
.min(1)
|
|
57
|
-
.max(DEPLOYMENT_NOTES_MAX_LENGTH)
|
|
58
|
-
.describe('When a half is off: a sentence on how it is done today, which Taiga designs and plans against');
|
|
59
|
-
|
|
60
|
-
/** How a missing required document comes to exist, said with a documents_incomplete refusal. */
|
|
61
|
-
const MISSING_DOCUMENTS_HOW =
|
|
62
|
-
'An imported product gets them from its import (taiga_start_import); a product started from scratch gets the ' +
|
|
63
|
-
'specification from the Discovery conversation in the web and the rest from taiga_generate_product_documents. ' +
|
|
64
|
-
'A draft is published with taiga_publish_product_document.';
|
|
65
|
-
|
|
66
|
-
type ProductRow = WithoutTenant<Product>;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Refuse a name already taken (PRO-2800): the API appends -2 to the slug and
|
|
70
|
-
* creates a second one, so a retried call or two agents at once left
|
|
71
|
-
* duplicates nobody asked for. The API checks it in the create's transaction
|
|
72
|
-
* (PRO-2808), so names past any page and two creates at once are caught too.
|
|
73
|
-
* allowDuplicateName says the person means it.
|
|
74
|
-
*/
|
|
75
|
-
const allowDuplicateName = z
|
|
76
|
-
.boolean()
|
|
77
|
-
.optional()
|
|
78
|
-
.describe('Create it even when one with the same name exists (default false: refused, naming the existing one)');
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* The API's refusal of a taken name, as the tool's: it names the one that
|
|
82
|
-
* exists when the caller may read it, and leaves the choice to the person.
|
|
83
|
-
* Another session may have made it, so "use it" would hand the agent someone
|
|
84
|
-
* else's work (PRO-2800, review 6). Anything else is not this refusal.
|
|
85
|
-
*/
|
|
86
|
-
function duplicateName(kind: 'factory' | 'product', error: unknown, where: string) {
|
|
87
|
-
if (!(error instanceof TaigaApiError) || error.status !== 409 || error.details?.['reason'] !== 'duplicate_name') {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
const said = (key: string) => (typeof error.details?.[key] === 'string' ? (error.details[key] as string) : undefined);
|
|
91
|
-
const [existingId, existingSlug, name, createdAt] = [
|
|
92
|
-
said('existingId'),
|
|
93
|
-
said('existingSlug'),
|
|
94
|
-
said('existingName'),
|
|
95
|
-
said('existingCreatedAt'),
|
|
96
|
-
];
|
|
97
|
-
const which = name ? `A ${kind} named "${name}"` : `A ${kind} with this name`;
|
|
98
|
-
const ids = existingId ? ` (${existingId}${createdAt ? `, created ${createdAt}` : ''})` : '';
|
|
99
|
-
// Without the holder's id the caller cannot read it, so "that one" is not a choice they have.
|
|
100
|
-
const choices = existingId
|
|
101
|
-
? "It may not be this person's: ask whether they meant that one, want another name, or want a second"
|
|
102
|
-
: 'The caller cannot read it: ask whether they want another name or a second';
|
|
103
|
-
return toolError('DUPLICATE_NAME', `${which} already exists ${where}${ids}. ${choices} (allowDuplicateName: true).`, {
|
|
104
|
-
...(existingId ? { existingId } : {}),
|
|
105
|
-
...(existingSlug ? { existingSlug } : {}),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** A product row the model can act on: how it started, and while in setup what it still needs. */
|
|
110
|
-
async function productRow(client: TaigaClient, product: ProductRow) {
|
|
111
|
-
const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0]!;
|
|
112
|
-
const readiness = await setupReadiness(client, product);
|
|
113
|
-
return {
|
|
114
|
-
...rest,
|
|
115
|
-
projectType: isImportedProduct(product) ? 'brownfield' : 'greenfield',
|
|
116
|
-
...(readiness ? { readiness } : {}),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* A half turned off with nothing said about how it is done today. Allowed (the
|
|
122
|
-
* web asks for the sentence but does not insist), so a warning rather than a
|
|
123
|
-
* refusal: plans then design against a gap.
|
|
124
|
-
*/
|
|
125
|
-
function missingNotesWarning(product: ProductRow): string[] {
|
|
126
|
-
const off = product.ownsInfrastructure === false || product.ownsDeployment === false;
|
|
127
|
-
const notes = typeof product.deploymentNotes === 'string' ? product.deploymentNotes.trim() : '';
|
|
128
|
-
return off && !notes
|
|
129
|
-
? [
|
|
130
|
-
'A half Taiga does not build has no deploymentNotes: plans cannot design against how it is done today. ' +
|
|
131
|
-
'Ask the person for a sentence and set it.',
|
|
132
|
-
]
|
|
133
|
-
: [];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The sentence is for a half Taiga does not build; the web asks for it only
|
|
138
|
-
* then. When both halves are decided on here, a sentence has nothing to say.
|
|
139
|
-
*/
|
|
140
|
-
function notesWithNothingToDescribe(params: {
|
|
141
|
-
ownsInfrastructure?: boolean | null | undefined;
|
|
142
|
-
ownsDeployment?: boolean | null | undefined;
|
|
143
|
-
deploymentNotes?: string | null | undefined;
|
|
144
|
-
}) {
|
|
145
|
-
return typeof params.deploymentNotes === 'string' &&
|
|
146
|
-
params.ownsInfrastructure === true &&
|
|
147
|
-
params.ownsDeployment === true
|
|
148
|
-
? toolError(
|
|
149
|
-
'VALIDATION_ERROR',
|
|
150
|
-
'deploymentNotes describes a half Taiga does not build; both are on, so leave it out.'
|
|
151
|
-
)
|
|
152
|
-
: null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function registerSetupTools(server: McpServer, client: TaigaClient) {
|
|
156
|
-
if (client.hasCapability('factories:write')) {
|
|
157
|
-
server.registerTool(
|
|
158
|
-
'taiga_create_factory',
|
|
159
|
-
{
|
|
160
|
-
title: 'Create a factory',
|
|
161
|
-
description:
|
|
162
|
-
'Create a factory: a group of products sharing knowledge, one instructions document and members. Owners ' +
|
|
163
|
-
'and admins only; members are added in the web. Then add products with taiga_create_product or ' +
|
|
164
|
-
'taiga_move_product.',
|
|
165
|
-
inputSchema: z.object({
|
|
166
|
-
name: z.string().trim().min(1).max(255).describe('The factory name'),
|
|
167
|
-
allowDuplicateName: allowDuplicateName,
|
|
168
|
-
}),
|
|
169
|
-
annotations: WRITE,
|
|
170
|
-
},
|
|
171
|
-
(params) =>
|
|
172
|
-
runTool(async () => {
|
|
173
|
-
try {
|
|
174
|
-
const made = await client.createFactory({
|
|
175
|
-
name: params.name,
|
|
176
|
-
refuseDuplicateName: !params.allowDuplicateName,
|
|
177
|
-
});
|
|
178
|
-
return toolResult(pickFields([made.data], FACTORY_FIELDS)[0]);
|
|
179
|
-
} catch (error) {
|
|
180
|
-
const refused = duplicateName('factory', error, 'in this organization');
|
|
181
|
-
if (refused) return refused;
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
server.registerTool(
|
|
188
|
-
'taiga_rename_factory',
|
|
189
|
-
{
|
|
190
|
-
title: 'Rename a factory',
|
|
191
|
-
description:
|
|
192
|
-
'Rename a factory. Its slug follows the new name, so links to it change; products, members, knowledge ' +
|
|
193
|
-
'and instructions stay as they are.',
|
|
194
|
-
inputSchema: z.object({ factoryId, name: z.string().trim().min(1).max(255).describe('The new name') }),
|
|
195
|
-
annotations: OPERATE_SETTING,
|
|
196
|
-
},
|
|
197
|
-
(params) =>
|
|
198
|
-
runTool(async () =>
|
|
199
|
-
toolResult(
|
|
200
|
-
pickFields([(await client.updateFactory(params.factoryId, { name: params.name })).data], FACTORY_FIELDS)[0]
|
|
201
|
-
)
|
|
202
|
-
)
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
server.registerTool(
|
|
206
|
-
'taiga_preview_product_move',
|
|
207
|
-
{
|
|
208
|
-
title: 'Preview moving a product to another factory',
|
|
209
|
-
description:
|
|
210
|
-
"Show who gains and who loses access if a product moves to another factory: the source factory's members " +
|
|
211
|
-
"lose it, the target's gain it; people in both, the product's own members, and owners and admins keep " +
|
|
212
|
-
'it, named with why. Show it to the person before taiga_move_product.',
|
|
213
|
-
inputSchema: z.object({ productId, factoryId: factoryId.describe('The factory to move it to') }),
|
|
214
|
-
annotations: READ_ONLY,
|
|
215
|
-
},
|
|
216
|
-
(params) =>
|
|
217
|
-
runTool(async () => toolResult((await client.previewProductMove(params.productId, params.factoryId)).data))
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
server.registerTool(
|
|
221
|
-
'taiga_move_product',
|
|
222
|
-
{
|
|
223
|
-
title: 'Move a product to another factory',
|
|
224
|
-
description:
|
|
225
|
-
'Move a product to another factory. Access follows the factory, so this changes who can reach it ' +
|
|
226
|
-
"(taiga_preview_product_move says who). The product's own knowledge moves with it; factory knowledge and " +
|
|
227
|
-
'instructions stay. Refused (WORK_IN_FLIGHT) while a plan or run is in flight; then ask the person: ' +
|
|
228
|
-
'stopLine: true stops the line first, interrupting that work.',
|
|
229
|
-
inputSchema: z.object({
|
|
230
|
-
productId,
|
|
231
|
-
factoryId: factoryId.describe('The factory to move it to'),
|
|
232
|
-
stopLine: z
|
|
233
|
-
.boolean()
|
|
234
|
-
.optional()
|
|
235
|
-
.describe('Stop the line first, interrupting the plan or build in flight; only when the person says so'),
|
|
236
|
-
}),
|
|
237
|
-
annotations: OPERATE,
|
|
238
|
-
},
|
|
239
|
-
(params) =>
|
|
240
|
-
runTool(async () => {
|
|
241
|
-
let moved;
|
|
242
|
-
try {
|
|
243
|
-
moved = await client.moveProduct(params.productId, {
|
|
244
|
-
factoryId: params.factoryId,
|
|
245
|
-
...(params.stopLine ? { stopLine: true } : {}),
|
|
246
|
-
});
|
|
247
|
-
} catch (error) {
|
|
248
|
-
// The API's own words say "Stop the line, then move the product",
|
|
249
|
-
// which reads as an instruction to the agent; whether to interrupt
|
|
250
|
-
// the work is the person's call (PRO-2800).
|
|
251
|
-
if (error instanceof TaigaApiError && error.code === 'WORK_IN_FLIGHT') {
|
|
252
|
-
return toolError(
|
|
253
|
-
'WORK_IN_FLIGHT',
|
|
254
|
-
// The API's first sentence says what is in flight; its second ("Stop the line, then move the product")
|
|
255
|
-
// is the instruction this replaces.
|
|
256
|
-
`${withStop((error.apiMessage ?? 'Work is in flight on this product').split(/(?<=[.!?])\s/)[0]!)} Moving it now means stopping ` +
|
|
257
|
-
'the line, which interrupts that work: ask the person, and pass stopLine: true only if they say so.',
|
|
258
|
-
error.details
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
return toolResult({
|
|
264
|
-
product: await productRow(client, moved.data),
|
|
265
|
-
changed: moved.changed,
|
|
266
|
-
previousFactoryId: moved.previousFactoryId,
|
|
267
|
-
lineStopped: moved.lineStopped,
|
|
268
|
-
failedDocuments: moved.failedDocuments,
|
|
269
|
-
});
|
|
270
|
-
})
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!client.hasCapability('products:write')) return;
|
|
275
|
-
|
|
276
|
-
server.registerTool(
|
|
277
|
-
'taiga_create_product',
|
|
278
|
-
{
|
|
279
|
-
title: 'Create a product',
|
|
280
|
-
description:
|
|
281
|
-
'Create a product in a factory and return it with readiness (what it needs next). start cannot change ' +
|
|
282
|
-
'later. import (brownfield): Taiga drafts the documents from existing code; link it with ' +
|
|
283
|
-
'taiga_link_repository (owners and admins only), then taiga_start_import. scratch (greenfield): Discovery ' +
|
|
284
|
-
'is a conversation in the web that writes the specification. Infrastructure code and CI/CD ' +
|
|
285
|
-
'pipelines follow the organization unless set here; when one is off, deploymentNotes says how that half ' +
|
|
286
|
-
'is done today. Add source material first with taiga_add_knowledge (productId): Discovery writes from it.',
|
|
287
|
-
inputSchema: z.object({
|
|
288
|
-
factoryId,
|
|
289
|
-
name: z.string().trim().min(1).max(255).describe('The product name'),
|
|
290
|
-
start: z.enum(['scratch', 'import']).describe('scratch: describe it in Discovery; import: from a repository'),
|
|
291
|
-
...buildScope,
|
|
292
|
-
deploymentNotes: deploymentNotes.optional(),
|
|
293
|
-
allowDuplicateName: allowDuplicateName,
|
|
294
|
-
}),
|
|
295
|
-
annotations: WRITE,
|
|
296
|
-
},
|
|
297
|
-
(params) =>
|
|
298
|
-
runTool(async () => {
|
|
299
|
-
const contradiction = notesWithNothingToDescribe(params);
|
|
300
|
-
if (contradiction) return contradiction;
|
|
301
|
-
let product: ProductRow;
|
|
302
|
-
try {
|
|
303
|
-
product = (
|
|
304
|
-
await client.createProduct({
|
|
305
|
-
name: params.name,
|
|
306
|
-
factoryId: params.factoryId,
|
|
307
|
-
...(params.start === 'import' ? { projectType: 'brownfield' as const } : {}),
|
|
308
|
-
...(params.ownsInfrastructure !== undefined ? { ownsInfrastructure: params.ownsInfrastructure } : {}),
|
|
309
|
-
...(params.ownsDeployment !== undefined ? { ownsDeployment: params.ownsDeployment } : {}),
|
|
310
|
-
...(params.deploymentNotes !== undefined ? { deploymentNotes: params.deploymentNotes } : {}),
|
|
311
|
-
// Within the factory it goes into: the same name in another factory is a different product.
|
|
312
|
-
refuseDuplicateName: !params.allowDuplicateName,
|
|
313
|
-
})
|
|
314
|
-
).data;
|
|
315
|
-
} catch (error) {
|
|
316
|
-
const refused = duplicateName('product', error, 'in this factory');
|
|
317
|
-
if (refused) return refused;
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
return toolResult(await productRow(client, product));
|
|
321
|
-
})
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
server.registerTool(
|
|
325
|
-
'taiga_update_product',
|
|
326
|
-
{
|
|
327
|
-
title: "Change a product's settings",
|
|
328
|
-
description:
|
|
329
|
-
"Change a product's name or what Taiga builds for it. A new name changes the slug, so links change. " +
|
|
330
|
-
'Infrastructure code and CI/CD pipelines: null returns one to the organization default; deploymentNotes ' +
|
|
331
|
-
'says how a half Taiga does not build is done; emitDesignStandards: null follows the default for how the ' +
|
|
332
|
-
'product started. Later plans read the change; planned work is not redone. Autonomy is ' +
|
|
333
|
-
'taiga_set_product_autonomy.',
|
|
334
|
-
inputSchema: z.object({
|
|
335
|
-
productId,
|
|
336
|
-
name: z.string().trim().min(1).max(255).optional().describe('A new name'),
|
|
337
|
-
...buildScope,
|
|
338
|
-
deploymentNotes: deploymentNotes.nullable().optional(),
|
|
339
|
-
emitDesignStandards: z
|
|
340
|
-
.boolean()
|
|
341
|
-
.nullable()
|
|
342
|
-
.optional()
|
|
343
|
-
.describe("Inherit the organization's design standards; null follows the default"),
|
|
344
|
-
}),
|
|
345
|
-
annotations: OPERATE_SETTING,
|
|
346
|
-
},
|
|
347
|
-
(params) =>
|
|
348
|
-
runTool(async () => {
|
|
349
|
-
const { productId: id, ...changes } = params;
|
|
350
|
-
if (Object.values(changes).every((value) => value === undefined)) {
|
|
351
|
-
return toolError('VALIDATION_ERROR', 'Change at least one setting.');
|
|
352
|
-
}
|
|
353
|
-
const contradiction = notesWithNothingToDescribe(params);
|
|
354
|
-
if (contradiction) return contradiction;
|
|
355
|
-
const updated = (await client.updateProduct(id, changes)).data;
|
|
356
|
-
const warnings = missingNotesWarning(updated);
|
|
357
|
-
return toolResult({ ...(await productRow(client, updated)), ...(warnings.length > 0 ? { warnings } : {}) });
|
|
358
|
-
})
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
server.registerTool(
|
|
362
|
-
'taiga_refresh_reality_documents',
|
|
363
|
-
{
|
|
364
|
-
title: "Refresh a product's documents from its code",
|
|
365
|
-
description:
|
|
366
|
-
'Derive the reality documents again from the code on the branch Taiga works on (effectiveBranch): all, ' +
|
|
367
|
-
'or the ones named. For a document the import misread, refresh it instead of importing again; unchanged ' +
|
|
368
|
-
'sections are not republished. Returns the job; taiga_wait_for_job says when it is done.',
|
|
369
|
-
inputSchema: z.object({
|
|
370
|
-
productId,
|
|
371
|
-
docTypes: z
|
|
372
|
-
.array(z.enum(REALITY_DERIVED_DOCUMENT_TYPES))
|
|
373
|
-
.min(1)
|
|
374
|
-
.max(REALITY_DERIVED_DOCUMENT_TYPES.length)
|
|
375
|
-
.optional()
|
|
376
|
-
.describe('The documents to refresh; omit for all'),
|
|
377
|
-
}),
|
|
378
|
-
annotations: OPERATE,
|
|
379
|
-
},
|
|
380
|
-
(params) =>
|
|
381
|
-
runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data))
|
|
382
|
-
);
|
|
383
|
-
|
|
384
|
-
server.registerTool(
|
|
385
|
-
'taiga_finish_discovery',
|
|
386
|
-
{
|
|
387
|
-
title: 'Finish Discovery',
|
|
388
|
-
description:
|
|
389
|
-
"Finish a product's Discovery: it locks the documents and opens the rest of the product; for an imported " +
|
|
390
|
-
'product the first initiatives are planned. Refused (CONFLICT) with details.reason: documents_incomplete ' +
|
|
391
|
-
'(details.missingDocuments lists them and how they are made), not_in_setup (already finished), or a ' +
|
|
392
|
-
'required check not yet stamped (stamped in the web). A locked document changes only after ' +
|
|
393
|
-
'taiga_reopen_discovery. The person decides the documents are right: ask first.',
|
|
394
|
-
inputSchema: z.object({ productId }),
|
|
395
|
-
annotations: OPERATE,
|
|
396
|
-
},
|
|
397
|
-
(params) =>
|
|
398
|
-
runTool(async () => {
|
|
399
|
-
try {
|
|
400
|
-
return toolResult(await productRow(client, (await client.completeSetup(params.productId)).data));
|
|
401
|
-
} catch (error) {
|
|
402
|
-
// The refusal names what is missing; it does not say how a missing
|
|
403
|
-
// document comes to exist, which is what the agent asks next.
|
|
404
|
-
if (!(error instanceof TaigaApiError) || error.details?.['reason'] !== 'documents_incomplete') throw error;
|
|
405
|
-
return toolError(
|
|
406
|
-
error.code ?? 'CONFLICT',
|
|
407
|
-
`${withStop(error.apiMessage ?? 'Required documents are not published yet')} ${MISSING_DOCUMENTS_HOW}`,
|
|
408
|
-
error.details
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
})
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
server.registerTool(
|
|
415
|
-
'taiga_reopen_discovery',
|
|
416
|
-
{
|
|
417
|
-
title: 'Reopen Discovery',
|
|
418
|
-
description:
|
|
419
|
-
"Reopen a finished product's Discovery so its documents can be amended; finish it again afterwards. " +
|
|
420
|
-
'Nothing built from them is undone.',
|
|
421
|
-
inputSchema: z.object({ productId }),
|
|
422
|
-
annotations: OPERATE,
|
|
423
|
-
},
|
|
424
|
-
(params) =>
|
|
425
|
-
runTool(async () => toolResult(await productRow(client, (await client.reopenSetup(params.productId)).data)))
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
if (!client.hasCapability('products:read')) return;
|
|
429
|
-
|
|
430
|
-
server.registerTool(
|
|
431
|
-
'taiga_start_import',
|
|
432
|
-
{
|
|
433
|
-
title: 'Import a codebase into a product',
|
|
434
|
-
description:
|
|
435
|
-
'Start importing a product created for import, and return the job. Taiga reads the linked repository and ' +
|
|
436
|
-
'derives the full document set from the code, citing the files. Then the scanners sweep it, the ' +
|
|
437
|
-
'documents are assessed against policies, and the first initiatives are planned when Discovery finishes. ' +
|
|
438
|
-
'Refused for a scratch product, without a linked repository, or while an import runs. taiga_wait_for_job ' +
|
|
439
|
-
'says when it is done (it takes a while).',
|
|
440
|
-
inputSchema: z.object({ productId }),
|
|
441
|
-
annotations: OPERATE,
|
|
442
|
-
},
|
|
443
|
-
(params) =>
|
|
444
|
-
runTool(async () => {
|
|
445
|
-
const product = (await client.getProduct(params.productId)).data;
|
|
446
|
-
if (!isImportedProduct(product)) {
|
|
447
|
-
return toolError(
|
|
448
|
-
'CONFLICT',
|
|
449
|
-
'This product was started from scratch, so it has no import: the choice is made when a product is created.'
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
return toolResult((await client.startImport(params.productId)).data);
|
|
453
|
-
})
|
|
454
|
-
);
|
|
455
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { pickFields } from './summarize.js';
|
|
3
|
-
|
|
4
|
-
describe('pickFields', () => {
|
|
5
|
-
const rows: { id: string; name?: string; content?: string }[] = [
|
|
6
|
-
{ id: 'a', name: 'A', content: 'the prose a list leaves out' },
|
|
7
|
-
{ id: 'b' },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
it('keeps the named fields and leaves out one an item does not carry', () => {
|
|
11
|
-
expect(pickFields(rows, ['id', 'name'])).toEqual([{ id: 'a', name: 'A' }, { id: 'b' }]);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('refuses a field the row type does not have', () => {
|
|
15
|
-
// The *_FIELDS lists are checked against their rows, so a misspelled field
|
|
16
|
-
// fails to compile instead of silently picking nothing (tsc covers tests).
|
|
17
|
-
// @ts-expect-error -- 'nmae' is not a field of the row.
|
|
18
|
-
expect(pickFields([{ id: 'a', name: 'A' }], ['id', 'nmae'])).toEqual([{ id: 'a' }]);
|
|
19
|
-
});
|
|
20
|
-
});
|
package/src/tools/summarize.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keep only the named fields of each item. List tools return summaries (the
|
|
3
|
-
* fields a model needs to pick an item) and get tools return everything;
|
|
4
|
-
* this is how a list drops the prose and the provenance trees.
|
|
5
|
-
*/
|
|
6
|
-
export function pickFields<T extends object, K extends keyof T>(
|
|
7
|
-
items: readonly T[],
|
|
8
|
-
fields: readonly K[]
|
|
9
|
-
): Partial<Pick<T, K>>[] {
|
|
10
|
-
return items.map((item) => {
|
|
11
|
-
// Partial: a field the item does not carry is left out, not set to undefined.
|
|
12
|
-
const picked: Partial<Pick<T, K>> = {};
|
|
13
|
-
for (const field of fields) {
|
|
14
|
-
if (field in item) picked[field] = item[field];
|
|
15
|
-
}
|
|
16
|
-
return picked;
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** The page a list tool returns: the rows the model asked for and where the next page starts. */
|
|
21
|
-
export function page<T>(data: T[], nextCursor: string | null | undefined): { data: T[]; nextCursor: string | null } {
|
|
22
|
-
return { data, nextCursor: nextCursor ?? null };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* How many of an initiative's acceptance criteria are met: one pair a row can
|
|
27
|
-
* carry, so "done with two unmet" shows in a list without reading the item.
|
|
28
|
-
* Null while it has none (an unplanned initiative).
|
|
29
|
-
*/
|
|
30
|
-
export function criteriaCounts(item: { acceptanceCriteria?: unknown }): { met: number; total: number } | null {
|
|
31
|
-
const criteria = item.acceptanceCriteria;
|
|
32
|
-
if (!Array.isArray(criteria) || criteria.length === 0) return null;
|
|
33
|
-
const met = criteria.filter((c) => typeof c === 'object' && c !== null && (c as { met?: unknown }).met === true);
|
|
34
|
-
return { met: met.length, total: criteria.length };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Initiative rows cut to `fields`, each with its criteria counts. A row that
|
|
39
|
-
* does not carry `acceptanceCriteria` at all gets no count rather than a
|
|
40
|
-
* null that would read as "has none".
|
|
41
|
-
*/
|
|
42
|
-
export function itemRows<T extends { acceptanceCriteria?: unknown }, K extends keyof T>(
|
|
43
|
-
items: readonly T[],
|
|
44
|
-
fields: readonly K[]
|
|
45
|
-
): (Partial<Pick<T, K>> & { criteria?: { met: number; total: number } | null })[] {
|
|
46
|
-
return pickFields(items, fields).map((row, index) =>
|
|
47
|
-
'acceptanceCriteria' in items[index]! ? { ...row, criteria: criteriaCounts(items[index]!) } : row
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Pages a name search reads before it stops and says so: 1,000 rows at the API's page ceiling. */
|
|
52
|
-
const SEARCH_PAGE_LIMIT = 10;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Rows whose text contains `query`, ignoring case, read page by page until
|
|
56
|
-
* `limit` match or the rows run out. The API has no name search, and a model
|
|
57
|
-
* told the person's word for a thing ("the Sandbox factory") otherwise pages
|
|
58
|
-
* the whole list to find its ID. `more` is true when it stopped before the end.
|
|
59
|
-
*/
|
|
60
|
-
export async function searchPages<T>(
|
|
61
|
-
fetchPage: (cursor: string | undefined) => Promise<{ data: T[]; nextCursor: string | null }>,
|
|
62
|
-
query: string,
|
|
63
|
-
text: (row: T) => unknown[],
|
|
64
|
-
limit: number,
|
|
65
|
-
/** A further condition a row must meet, besides the query. */
|
|
66
|
-
keep: (row: T) => boolean = () => true
|
|
67
|
-
): Promise<{ rows: T[]; more: boolean }> {
|
|
68
|
-
const needle = query.toLowerCase();
|
|
69
|
-
const rows: T[] = [];
|
|
70
|
-
let cursor: string | undefined;
|
|
71
|
-
for (let index = 0; index < SEARCH_PAGE_LIMIT; index += 1) {
|
|
72
|
-
const result = await fetchPage(cursor);
|
|
73
|
-
const page = result.data;
|
|
74
|
-
for (const [at, row] of page.entries()) {
|
|
75
|
-
if (!keep(row)) continue;
|
|
76
|
-
if (!text(row).some((value) => typeof value === 'string' && value.toLowerCase().includes(needle))) continue;
|
|
77
|
-
rows.push(row);
|
|
78
|
-
// Full: stop reading. \`more\` then means "there may be more", not
|
|
79
|
-
// that a further match was seen; reading on to find out cost up to ten
|
|
80
|
-
// sequential pages for one bit.
|
|
81
|
-
if (rows.length === limit) return { rows, more: at < page.length - 1 || result.nextCursor !== null };
|
|
82
|
-
}
|
|
83
|
-
if (!result.nextCursor) return { rows, more: false };
|
|
84
|
-
cursor = result.nextCursor;
|
|
85
|
-
}
|
|
86
|
-
return { rows, more: true };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The page size a search reads at: the API's ceiling. */
|
|
90
|
-
export const SEARCH_PAGE_SIZE = 100;
|
|
91
|
-
|
|
92
|
-
/** A search reads its own pages, so a cursor from a plain page has nothing to continue. */
|
|
93
|
-
export const SEARCH_WITH_CURSOR = 'A search answers one page; narrow the query instead of paging it.';
|
|
94
|
-
|
|
95
|
-
/** What a search says when it stopped before reading every row. */
|
|
96
|
-
export const SEARCH_NOTE = 'There may be more matches than the limit carries; narrow the query or raise the limit.';
|