@taiga-ai/mcp-server 0.25.0-dev.468 → 0.25.0-dev.470
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -3741,6 +3741,22 @@ function runOutcome(run) {
|
|
|
3741
3741
|
};
|
|
3742
3742
|
}
|
|
3743
3743
|
/**
|
|
3744
|
+
* What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
|
|
3745
|
+
* it recorded them, how many.
|
|
3746
|
+
*/
|
|
3747
|
+
function hardeningSaid(blocking, named) {
|
|
3748
|
+
const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? "" : "s"} it could not fix`;
|
|
3749
|
+
if (named.length === 0) return `${count}; the run records how many, not which.`;
|
|
3750
|
+
const shown = named.slice(0, 3).map((finding) => {
|
|
3751
|
+
const message = String(finding["message"]);
|
|
3752
|
+
const where = finding["line"] ? `${String(finding["path"])}:${String(finding["line"])}` : String(finding["path"]);
|
|
3753
|
+
return `${message} (${String(finding["tool"])}, ${where})`;
|
|
3754
|
+
});
|
|
3755
|
+
const rest = blocking - shown.length;
|
|
3756
|
+
const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
|
|
3757
|
+
return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
|
|
3758
|
+
}
|
|
3759
|
+
/**
|
|
3744
3760
|
* What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
|
|
3745
3761
|
* bare enums. As @taiga/types documents them.
|
|
3746
3762
|
*/
|
|
@@ -3765,10 +3781,11 @@ function readableRun(run, opts = {}) {
|
|
|
3765
3781
|
const means = FAILED_REASON_MEANS[String(run["failedReason"])];
|
|
3766
3782
|
const hardening = run["hardeningResult"];
|
|
3767
3783
|
const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
|
|
3784
|
+
const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
|
|
3768
3785
|
return {
|
|
3769
3786
|
...rest,
|
|
3770
3787
|
...means ? { failedReasonMeans: means } : {},
|
|
3771
|
-
...blocking > 0 ? { hardeningNote:
|
|
3788
|
+
...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
|
|
3772
3789
|
...opts.row || error === void 0 ? {} : { error },
|
|
3773
3790
|
...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
|
|
3774
3791
|
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
@@ -5227,7 +5244,8 @@ const SETUP_TASK_FIELDS = [
|
|
|
5227
5244
|
"providerActionUrl",
|
|
5228
5245
|
"inboxItemId",
|
|
5229
5246
|
"inboxItemStatus",
|
|
5230
|
-
"createdAt"
|
|
5247
|
+
"createdAt",
|
|
5248
|
+
"stale"
|
|
5231
5249
|
];
|
|
5232
5250
|
/** The API's count across every page, on the first page; nothing from an API that does not send one. */
|
|
5233
5251
|
function totalOf(result) {
|
|
@@ -5267,7 +5285,10 @@ function sameKeyOnce(rows) {
|
|
|
5267
5285
|
const { alsoFiledAs: _also, ...was } = kept;
|
|
5268
5286
|
for (const field of Object.keys(kept)) delete kept[field];
|
|
5269
5287
|
Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
|
|
5270
|
-
} else
|
|
5288
|
+
} else {
|
|
5289
|
+
kept["alsoFiledAs"] = [...also, record(row)];
|
|
5290
|
+
if (isOpen(row) && row["stale"] === void 0) delete kept["stale"];
|
|
5291
|
+
}
|
|
5271
5292
|
}
|
|
5272
5293
|
return out;
|
|
5273
5294
|
}
|
|
@@ -5342,7 +5363,7 @@ function registerInboxTools(server, client) {
|
|
|
5342
5363
|
}));
|
|
5343
5364
|
server.registerTool("taiga_list_setup_tasks", {
|
|
5344
5365
|
title: "List setup tasks",
|
|
5345
|
-
description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
|
|
5366
|
+
description: "List what a person must set up for a product (secrets, variables, accounts), with exactly what and where: key (the name to create; Taiga never stores values), the provider page, and phase: prereq (the deploy fails without it), wiring (generated values back to providers) or followup (can wait until after launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a page that folded some says recordsOnPage. stale on an open task says no document derived from the code since it was raised mentions its key: it may not apply any more, so check before asking anyone to set it. Unlike taiga_list_action_required, which lists the same work as items to resolve.",
|
|
5346
5367
|
inputSchema: z.object({
|
|
5347
5368
|
productId: productId.optional(),
|
|
5348
5369
|
includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
|
package/package.json
CHANGED
|
@@ -6856,6 +6856,52 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
|
|
|
6856
6856
|
expect(sentUrl(fetchSpy, '/v1/inbox').searchParams.has('withTotal')).toBe(false);
|
|
6857
6857
|
});
|
|
6858
6858
|
|
|
6859
|
+
it("passes on a setup task's stale note (PRO-2819)", async () => {
|
|
6860
|
+
const stale = "None of the current code's documents, derived since this task was raised, mention JWT_SECRET.";
|
|
6861
|
+
const { client } = await bootMcpPair({
|
|
6862
|
+
capabilities: ALL_CAPABILITIES,
|
|
6863
|
+
routes: {
|
|
6864
|
+
'GET /v1/setup-tasks': {
|
|
6865
|
+
data: [
|
|
6866
|
+
{
|
|
6867
|
+
id: ENV_ID,
|
|
6868
|
+
productId: ENV_ID,
|
|
6869
|
+
key: 'JWT_SECRET',
|
|
6870
|
+
inboxItemStatus: 'open',
|
|
6871
|
+
title: 'Set JWT_SECRET',
|
|
6872
|
+
stale,
|
|
6873
|
+
},
|
|
6874
|
+
],
|
|
6875
|
+
nextCursor: null,
|
|
6876
|
+
},
|
|
6877
|
+
},
|
|
6878
|
+
});
|
|
6879
|
+
|
|
6880
|
+
const tasks = parseToolResult(await client.callTool({ name: 'taiga_list_setup_tasks', arguments: {} }));
|
|
6881
|
+
expect(tasks.data[0].stale).toBe(stale);
|
|
6882
|
+
});
|
|
6883
|
+
|
|
6884
|
+
it('drops the stale note from a folded variable when one open record of it is live (PRO-2819)', async () => {
|
|
6885
|
+
const stale = "None of the current code's documents, derived since this task was raised, mention JWT_SECRET.";
|
|
6886
|
+
const record = { productId: ENV_ID, key: 'JWT_SECRET', inboxItemStatus: 'open' };
|
|
6887
|
+
const { client } = await bootMcpPair({
|
|
6888
|
+
capabilities: ALL_CAPABILITIES,
|
|
6889
|
+
routes: {
|
|
6890
|
+
'GET /v1/setup-tasks': {
|
|
6891
|
+
data: [
|
|
6892
|
+
{ ...record, id: 'old', category: 'secret', title: 'Set JWT_SECRET', stale },
|
|
6893
|
+
{ ...record, id: 'new', category: 'env_var', title: 'Set JWT_SECRET' },
|
|
6894
|
+
],
|
|
6895
|
+
nextCursor: null,
|
|
6896
|
+
},
|
|
6897
|
+
},
|
|
6898
|
+
});
|
|
6899
|
+
|
|
6900
|
+
const tasks = parseToolResult(await client.callTool({ name: 'taiga_list_setup_tasks', arguments: {} }));
|
|
6901
|
+
expect(tasks.data).toHaveLength(1);
|
|
6902
|
+
expect(tasks.data[0].stale).toBeUndefined();
|
|
6903
|
+
});
|
|
6904
|
+
|
|
6859
6905
|
it('carries who cancelled a failed plan and when, and leaves the item as it was when the history is refused', async () => {
|
|
6860
6906
|
const failed = { id: ITEM_ID, title: 'Login', planningStatus: 'failed', planFailureCause: 'cancelled' };
|
|
6861
6907
|
const history = {
|
|
@@ -8904,6 +8950,78 @@ describe('what the seventh agent review found (PRO-2800)', () => {
|
|
|
8904
8950
|
expect(bundle.runs[0]).not.toHaveProperty('hardeningResult');
|
|
8905
8951
|
});
|
|
8906
8952
|
|
|
8953
|
+
it('names the blocking hardening findings a run recorded', async () => {
|
|
8954
|
+
const { client } = await bootMcpPair({
|
|
8955
|
+
capabilities: ['products:read'],
|
|
8956
|
+
routes: {
|
|
8957
|
+
[`/v1/builds/${BUILD_ID}/steps`]: { data: [] },
|
|
8958
|
+
[`/v1/builds/${BUILD_ID}`]: {
|
|
8959
|
+
data: {
|
|
8960
|
+
id: BUILD_ID,
|
|
8961
|
+
productId: PRODUCT_ID,
|
|
8962
|
+
status: 'needs_review',
|
|
8963
|
+
prNumber: 4,
|
|
8964
|
+
hardeningResult: {
|
|
8965
|
+
passed: false,
|
|
8966
|
+
scanners: {},
|
|
8967
|
+
blockingFindings: 1,
|
|
8968
|
+
blocking: [
|
|
8969
|
+
{
|
|
8970
|
+
tool: 'betterleaks',
|
|
8971
|
+
severity: 'critical',
|
|
8972
|
+
path: 'src/config.rs',
|
|
8973
|
+
line: 12,
|
|
8974
|
+
message: 'generic-api-key: Hardcoded API key',
|
|
8975
|
+
},
|
|
8976
|
+
],
|
|
8977
|
+
},
|
|
8978
|
+
},
|
|
8979
|
+
},
|
|
8980
|
+
},
|
|
8981
|
+
});
|
|
8982
|
+
|
|
8983
|
+
const run = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
|
|
8984
|
+
|
|
8985
|
+
expect(run.hardeningNote).toBe(
|
|
8986
|
+
'Hardening ended with 1 blocking finding it could not fix: generic-api-key: Hardcoded API key (betterleaks, src/config.rs:12).'
|
|
8987
|
+
);
|
|
8988
|
+
});
|
|
8989
|
+
|
|
8990
|
+
it('always says where a blocking finding is, even when its title names the path', async () => {
|
|
8991
|
+
const { client } = await bootMcpPair({
|
|
8992
|
+
capabilities: ['products:read'],
|
|
8993
|
+
routes: {
|
|
8994
|
+
[`/v1/builds/${BUILD_ID}/steps`]: { data: [] },
|
|
8995
|
+
[`/v1/builds/${BUILD_ID}`]: {
|
|
8996
|
+
data: {
|
|
8997
|
+
id: BUILD_ID,
|
|
8998
|
+
productId: PRODUCT_ID,
|
|
8999
|
+
status: 'needs_review',
|
|
9000
|
+
hardeningResult: {
|
|
9001
|
+
passed: false,
|
|
9002
|
+
blockingFindings: 1,
|
|
9003
|
+
blocking: [
|
|
9004
|
+
{
|
|
9005
|
+
tool: 'betterleaks',
|
|
9006
|
+
severity: 'critical',
|
|
9007
|
+
path: 'src/config.rs',
|
|
9008
|
+
line: 12,
|
|
9009
|
+
message: 'aws-key: AWS key in src/config.rs',
|
|
9010
|
+
},
|
|
9011
|
+
],
|
|
9012
|
+
},
|
|
9013
|
+
},
|
|
9014
|
+
},
|
|
9015
|
+
},
|
|
9016
|
+
});
|
|
9017
|
+
|
|
9018
|
+
const run = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
|
|
9019
|
+
|
|
9020
|
+
expect(run.hardeningNote).toBe(
|
|
9021
|
+
'Hardening ended with 1 blocking finding it could not fix: aws-key: AWS key in src/config.rs (betterleaks, src/config.rs:12).'
|
|
9022
|
+
);
|
|
9023
|
+
});
|
|
9024
|
+
|
|
8907
9025
|
it('says what the API says when it names the product holding a repository', async () => {
|
|
8908
9026
|
const { client, fetchSpy } = await bootMcpPair({
|
|
8909
9027
|
capabilities: ALL_CAPABILITIES,
|
package/src/tools/builds.ts
CHANGED
|
@@ -222,6 +222,24 @@ export function runOutcome(run: Record<string, unknown>): { disposition: string;
|
|
|
222
222
|
return { disposition, outcome: sentence[disposition] ?? status };
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* What hardening left blocking, in a sentence: the findings the run names (PRO-2817), or, for a run reported before
|
|
227
|
+
* it recorded them, how many.
|
|
228
|
+
*/
|
|
229
|
+
function hardeningSaid(blocking: number, named: Record<string, unknown>[]): string {
|
|
230
|
+
const count = `Hardening ended with ${blocking} blocking finding${blocking === 1 ? '' : 's'} it could not fix`;
|
|
231
|
+
if (named.length === 0) return `${count}; the run records how many, not which.`;
|
|
232
|
+
const shown = named.slice(0, 3).map((finding) => {
|
|
233
|
+
const message = String(finding['message']);
|
|
234
|
+
const where = finding['line'] ? `${String(finding['path'])}:${String(finding['line'])}` : String(finding['path']);
|
|
235
|
+
// Always where it is, though a scanner's title may repeat the path: a substring test would drop a real one.
|
|
236
|
+
return `${message} (${String(finding['tool'])}, ${where})`;
|
|
237
|
+
});
|
|
238
|
+
const rest = blocking - shown.length;
|
|
239
|
+
const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : '';
|
|
240
|
+
return `${count}: ${shown.join('; ')}${rest > 0 ? `; and ${rest} more${recorded}` : ''}.`;
|
|
241
|
+
}
|
|
242
|
+
|
|
225
243
|
/**
|
|
226
244
|
* What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
|
|
227
245
|
* bare enums. As @taiga/types documents them.
|
|
@@ -247,14 +265,15 @@ export function readableRun(run: Record<string, unknown>, opts: { row?: boolean
|
|
|
247
265
|
const { prDraft, error, reviewEscalation, runtime: _runtime, ...rest } = run;
|
|
248
266
|
const said = runOutcome(run);
|
|
249
267
|
const means = FAILED_REASON_MEANS[String(run['failedReason'])];
|
|
250
|
-
const hardening = run['hardeningResult'] as { blockingFindings?: unknown } | null | undefined;
|
|
268
|
+
const hardening = run['hardeningResult'] as { blockingFindings?: unknown; blocking?: unknown } | null | undefined;
|
|
251
269
|
const blocking = typeof hardening?.blockingFindings === 'number' ? hardening.blockingFindings : 0;
|
|
270
|
+
const named = Array.isArray(hardening?.blocking) ? (hardening.blocking as Record<string, unknown>[]) : [];
|
|
252
271
|
return {
|
|
253
272
|
...rest,
|
|
254
273
|
...(means ? { failedReasonMeans: means } : {}),
|
|
255
274
|
...(blocking > 0
|
|
256
275
|
? {
|
|
257
|
-
hardeningNote:
|
|
276
|
+
hardeningNote: hardeningSaid(blocking, named),
|
|
258
277
|
}
|
|
259
278
|
: {}),
|
|
260
279
|
// A row's outcome says the error and the handback; the full read keeps them whole.
|
package/src/tools/inbox.ts
CHANGED
|
@@ -306,6 +306,7 @@ const SETUP_TASK_FIELDS = [
|
|
|
306
306
|
'inboxItemId',
|
|
307
307
|
'inboxItemStatus',
|
|
308
308
|
'createdAt',
|
|
309
|
+
'stale',
|
|
309
310
|
] as const;
|
|
310
311
|
|
|
311
312
|
/** The API's count across every page, on the first page; nothing from an API that does not send one. */
|
|
@@ -350,6 +351,9 @@ export function sameKeyOnce(rows: Row[]): Row[] {
|
|
|
350
351
|
Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
|
|
351
352
|
} else {
|
|
352
353
|
kept['alsoFiledAs'] = [...also, record(row)];
|
|
354
|
+
// Stale only while every open record is (PRO-2819): one raised after the code's documents were derived
|
|
355
|
+
// is a live ask, whichever record leads.
|
|
356
|
+
if (isOpen(row) && row['stale'] === undefined) delete kept['stale'];
|
|
353
357
|
}
|
|
354
358
|
}
|
|
355
359
|
return out;
|
|
@@ -471,7 +475,9 @@ export function registerInboxTools(server: McpServer, client: TaigaClient) {
|
|
|
471
475
|
'deploy fails without it), wiring (generated values back to providers) or followup (can wait until after ' +
|
|
472
476
|
'launch). Runs do not wait for them. One product or all, oldest first; includeDone: true adds done and ' +
|
|
473
477
|
'dismissed. A variable filed twice lists the others under alsoFiledAs; totalCount counts records, and a ' +
|
|
474
|
-
'page that folded some says recordsOnPage.
|
|
478
|
+
'page that folded some says recordsOnPage. stale on an open task says no document derived from the code ' +
|
|
479
|
+
'since it was raised mentions its key: it may not apply any more, so check before asking anyone to set ' +
|
|
480
|
+
'it. Unlike ' +
|
|
475
481
|
'taiga_list_action_required, which lists the same work as items to resolve.',
|
|
476
482
|
inputSchema: z.object({
|
|
477
483
|
productId: productId.optional(),
|
|
Binary file
|
|
Binary file
|