@taiga-ai/mcp-server 0.16.0-rc.431 → 0.17.0-dev.439
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 +821 -181
- package/package.json +1 -1
- package/scripts/prepare-publish.cjs +4 -3
- package/src/client.ts +1 -0
- package/src/server.integration.test.ts +374 -17
- package/src/tools/autonomy.ts +3 -3
- package/src/tools/backlogs.ts +68 -23
- package/src/tools/builds.ts +44 -10
- package/src/tools/catalogue.test.ts +11 -1
- package/src/tools/catalogue.ts +64 -3
- package/src/tools/context.ts +12 -2
- package/src/tools/documents.ts +17 -8
- package/src/tools/errors.ts +18 -9
- package/src/tools/hints.ts +71 -0
- package/src/tools/inbox.ts +16 -7
- package/src/tools/index.ts +8 -2
- package/src/tools/inputs.ts +16 -4
- package/src/tools/instructions.ts +40 -14
- package/src/tools/jobs.ts +2 -1
- package/src/tools/knowledge.ts +6 -6
- package/src/tools/maintenance.ts +36 -13
- package/src/tools/observe.ts +48 -7
- package/src/tools/products.ts +65 -40
- package/src/tools/prompts.ts +5 -3
- package/src/tools/repositories.ts +11 -14
- package/src/tools/sections.ts +24 -0
- package/src/tools/setup.ts +82 -15
- package/src/tools/summarize.ts +44 -0
- package/src/tools/wait.ts +4 -3
- package/taiga-ai-mcp-server-0.17.0-dev.439.tgz +0 -0
- package/taiga-ai-mcp-server-0.16.0-rc.431.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { McpServer, ProtocolError, ProtocolErrorCode, ResourceTemplate } from "@modelcontextprotocol/server";
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
7
|
//#region \0rolldown/runtime.js
|
|
7
8
|
var __defProp = Object.defineProperty;
|
|
8
9
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -96,7 +97,7 @@ function seg(value) {
|
|
|
96
97
|
* is one. Every value goes through URLSearchParams, so nothing a tool passes
|
|
97
98
|
* can add a parameter of its own.
|
|
98
99
|
*/
|
|
99
|
-
function query(params) {
|
|
100
|
+
function query$1(params) {
|
|
100
101
|
const search = new URLSearchParams();
|
|
101
102
|
for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== null && value !== "") search.set(key, String(value));
|
|
102
103
|
const text = search.toString();
|
|
@@ -338,10 +339,10 @@ var TaigaClient = class {
|
|
|
338
339
|
return this.request("GET", `/products/${seg(productId)}/maintenance/summary`);
|
|
339
340
|
}
|
|
340
341
|
async listMaintenanceSweeps(productId, opts) {
|
|
341
|
-
return this.request("GET", `/products/${seg(productId)}/maintenance/runs${query({ limit: opts?.limit })}`);
|
|
342
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/runs${query$1({ limit: opts?.limit })}`);
|
|
342
343
|
}
|
|
343
344
|
async listFindings(productId, opts) {
|
|
344
|
-
return this.request("GET", `/products/${seg(productId)}/maintenance/findings${query({ ...opts })}`);
|
|
345
|
+
return this.request("GET", `/products/${seg(productId)}/maintenance/findings${query$1({ ...opts })}`);
|
|
345
346
|
}
|
|
346
347
|
async getFinding(id) {
|
|
347
348
|
return this.request("GET", `/maintenance/findings/${seg(id)}`);
|
|
@@ -367,7 +368,7 @@ var TaigaClient = class {
|
|
|
367
368
|
async listInbox(opts) {
|
|
368
369
|
const { productId, ...rest } = opts;
|
|
369
370
|
const path = productId ? `/products/${seg(productId)}/inbox` : "/inbox";
|
|
370
|
-
return this.request("GET", `${path}${query({ ...rest })}`);
|
|
371
|
+
return this.request("GET", `${path}${query$1({ ...rest })}`);
|
|
371
372
|
}
|
|
372
373
|
async updateInboxItem(id, body) {
|
|
373
374
|
return this.request("PATCH", `/inbox/${seg(id)}`, body);
|
|
@@ -375,10 +376,10 @@ var TaigaClient = class {
|
|
|
375
376
|
async listSetupTasks(opts) {
|
|
376
377
|
const { productId, ...rest } = opts;
|
|
377
378
|
const path = productId ? `/products/${seg(productId)}/setup-tasks` : "/setup-tasks";
|
|
378
|
-
return this.request("GET", `${path}${query({ ...rest })}`);
|
|
379
|
+
return this.request("GET", `${path}${query$1({ ...rest })}`);
|
|
379
380
|
}
|
|
380
381
|
async listEnvironments(productId, opts) {
|
|
381
|
-
return this.request("GET", `/products/${seg(productId)}/environments${query({ ...opts })}`);
|
|
382
|
+
return this.request("GET", `/products/${seg(productId)}/environments${query$1({ ...opts })}`);
|
|
382
383
|
}
|
|
383
384
|
async createEnvironment(productId, body) {
|
|
384
385
|
return this.request("POST", `/products/${seg(productId)}/environments`, body);
|
|
@@ -390,13 +391,13 @@ var TaigaClient = class {
|
|
|
390
391
|
return this.request("GET", `/products/${seg(productId)}/deployments/current`);
|
|
391
392
|
}
|
|
392
393
|
async listDeployments(productId, opts) {
|
|
393
|
-
return this.request("GET", `/products/${seg(productId)}/deployments${query({ ...opts })}`);
|
|
394
|
+
return this.request("GET", `/products/${seg(productId)}/deployments${query$1({ ...opts })}`);
|
|
394
395
|
}
|
|
395
396
|
async getDeploymentSyncStatus(productId) {
|
|
396
397
|
return this.request("GET", `/products/${seg(productId)}/deployments/sync-status`);
|
|
397
398
|
}
|
|
398
399
|
async getDeliveryMetrics(productId, environmentId, windowDays) {
|
|
399
|
-
return this.request("GET", `/products/${seg(productId)}/deployments/insights${query({
|
|
400
|
+
return this.request("GET", `/products/${seg(productId)}/deployments/insights${query$1({
|
|
400
401
|
environmentId,
|
|
401
402
|
windowDays
|
|
402
403
|
})}`);
|
|
@@ -405,14 +406,14 @@ var TaigaClient = class {
|
|
|
405
406
|
return this.request("GET", `/products/${seg(productId)}/monitoring/health`);
|
|
406
407
|
}
|
|
407
408
|
async getMonitoringSignal(productId, signal, environmentId, windowDays) {
|
|
408
|
-
return this.request("GET", `/products/${seg(productId)}/monitoring/${signal}${query({
|
|
409
|
+
return this.request("GET", `/products/${seg(productId)}/monitoring/${signal}${query$1({
|
|
409
410
|
environmentId,
|
|
410
411
|
windowDays
|
|
411
412
|
})}`);
|
|
412
413
|
}
|
|
413
414
|
async listKnowledge(opts) {
|
|
414
415
|
const scope = (value) => value === null ? "null" : value;
|
|
415
|
-
return this.request("GET", `/custom-documents${query({
|
|
416
|
+
return this.request("GET", `/custom-documents${query$1({
|
|
416
417
|
...opts,
|
|
417
418
|
factoryId: scope(opts.factoryId),
|
|
418
419
|
productId: scope(opts.productId)
|
|
@@ -464,7 +465,7 @@ var TaigaClient = class {
|
|
|
464
465
|
return this.request("POST", "/products", body);
|
|
465
466
|
}
|
|
466
467
|
async previewProductMove(productId, factoryId) {
|
|
467
|
-
return this.request("GET", `/products/${seg(productId)}/factory/preview${query({ factoryId })}`);
|
|
468
|
+
return this.request("GET", `/products/${seg(productId)}/factory/preview${query$1({ factoryId })}`);
|
|
468
469
|
}
|
|
469
470
|
async moveProduct(productId, body) {
|
|
470
471
|
return this.request("PUT", `/products/${seg(productId)}/factory`, body);
|
|
@@ -486,7 +487,7 @@ var TaigaClient = class {
|
|
|
486
487
|
const data = [];
|
|
487
488
|
let cursor;
|
|
488
489
|
do {
|
|
489
|
-
const result = await this.request("GET", `/github/installations${query({
|
|
490
|
+
const result = await this.request("GET", `/github/installations${query$1({
|
|
490
491
|
limit: 100,
|
|
491
492
|
cursor
|
|
492
493
|
})}`);
|
|
@@ -496,7 +497,7 @@ var TaigaClient = class {
|
|
|
496
497
|
return { data };
|
|
497
498
|
}
|
|
498
499
|
async listInstallationRepositories(installationId, opts) {
|
|
499
|
-
return this.request("GET", `/github/installations/${seg(installationId)}/repositories${query({ ...opts })}`);
|
|
500
|
+
return this.request("GET", `/github/installations/${seg(installationId)}/repositories${query$1({ ...opts })}`);
|
|
500
501
|
}
|
|
501
502
|
async linkRepository(productId, body) {
|
|
502
503
|
return this.request("POST", `/products/${seg(productId)}/repositories`, body);
|
|
@@ -569,7 +570,15 @@ const OPERATE_SETTING = {
|
|
|
569
570
|
//#endregion
|
|
570
571
|
//#region src/tools/inputs.ts
|
|
571
572
|
/** The inputs several tools share, so a product or a page is named the same way everywhere. */
|
|
572
|
-
|
|
573
|
+
/**
|
|
574
|
+
* An ID input whose refusal says where the ID comes from. The client shows the
|
|
575
|
+
* schema's message, not the description, when a call fails validation, and
|
|
576
|
+
* "Invalid UUID" alone sends a model that passed a name to guess again.
|
|
577
|
+
*/
|
|
578
|
+
function idInput(what, from) {
|
|
579
|
+
return z.string().uuid({ message: `A ${what} ID is a UUID, not a name: ${from}` }).describe(`${what[0].toUpperCase()}${what.slice(1)} ID -- ${from}`);
|
|
580
|
+
}
|
|
581
|
+
const productId = idInput("product", "taiga_list_products (query finds one by name), taiga_resolve_product or taiga_get_product_context give it");
|
|
573
582
|
/** The page inputs every list tool takes; `nextCursor` of one page is the `cursor` of the next. */
|
|
574
583
|
const pagination = {
|
|
575
584
|
limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)"),
|
|
@@ -578,6 +587,42 @@ const pagination = {
|
|
|
578
587
|
const slug = z.string().max(63).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "A slug is lowercase words joined by hyphens").describe("Product slug, as it appears in Taiga URLs");
|
|
579
588
|
const remoteUrl = z.string().min(1).max(2048).describe("The git remote URL of a checkout (git remote get-url origin): https://, git@host:owner/repo.git or ssh://");
|
|
580
589
|
//#endregion
|
|
590
|
+
//#region ../../packages/types/dist/lenient-schema.js
|
|
591
|
+
/**
|
|
592
|
+
* Building blocks for reading JSON that arrives untyped -- a stored jsonb
|
|
593
|
+
* column, an agent's answer, a document body -- as a parsed type rather than
|
|
594
|
+
* a `Record<string, unknown>` narrowed field by field (PRO-2514).
|
|
595
|
+
*
|
|
596
|
+
* They encode the two leniencies a hand-written reader already had, so a
|
|
597
|
+
* schema built from them accepts, drops and defaults exactly what the reader
|
|
598
|
+
* it replaces did:
|
|
599
|
+
*
|
|
600
|
+
* - `lenient`: a field read only when it has the expected type, and otherwise
|
|
601
|
+
* treated as absent. Never wrap a schema whose failure must surface: the
|
|
602
|
+
* catch would swallow it and read a malformed value as "absent".
|
|
603
|
+
* - `lenientList`: a list read item by item, where an item that does not parse
|
|
604
|
+
* is skipped and the rest survive.
|
|
605
|
+
*/
|
|
606
|
+
/** A field read only when it has the expected type; anything else reads as absent. */
|
|
607
|
+
function lenient(schema) {
|
|
608
|
+
return schema.optional().catch(void 0);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* The item schema of every `lenientList`, which its transform otherwise hides:
|
|
612
|
+
* read by the agent contract test, which walks each schema's keys to hold the
|
|
613
|
+
* shared fixtures to them.
|
|
614
|
+
*/
|
|
615
|
+
const lenientListItems = /* @__PURE__ */ new WeakMap();
|
|
616
|
+
/** A list read item by item: an item that does not parse is skipped, as the reader's `continue` did. */
|
|
617
|
+
function lenientList(item) {
|
|
618
|
+
const list = z.array(z.unknown()).transform((items) => items.flatMap((entry) => {
|
|
619
|
+
const parsed = item.safeParse(entry);
|
|
620
|
+
return parsed.success ? [parsed.data] : [];
|
|
621
|
+
}));
|
|
622
|
+
lenientListItems.set(list, item);
|
|
623
|
+
return list;
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
581
626
|
//#region ../../packages/types/dist/tenants.js
|
|
582
627
|
const TENANT_DOCUMENT_TYPES = [
|
|
583
628
|
"policy_security",
|
|
@@ -1221,6 +1266,57 @@ const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDispositio
|
|
|
1221
1266
|
* DIFFERENT, narrower concept: transitions a user command may never leave.
|
|
1222
1267
|
*/
|
|
1223
1268
|
const BUILD_RUN_TERMINAL_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
|
|
1269
|
+
/**
|
|
1270
|
+
* Why the agent kept a pull request a draft, as recorded in its description
|
|
1271
|
+
* (PRO-2298). The API reads these markers; the run's escalation carries them.
|
|
1272
|
+
*/
|
|
1273
|
+
const READY_BLOCKERS = [
|
|
1274
|
+
"plan_incomplete",
|
|
1275
|
+
"tests_failed",
|
|
1276
|
+
"bar_lowering",
|
|
1277
|
+
"bar_lowering_unchecked",
|
|
1278
|
+
"criticals_unresolved"
|
|
1279
|
+
];
|
|
1280
|
+
/**
|
|
1281
|
+
* Why the autonomous loop handed a live pull request to a person (PRO-2409).
|
|
1282
|
+
* One per path that sets `reviewEscalated`:
|
|
1283
|
+
* - `ci_fix_cap`: CI kept failing after the automatic fix rounds ran out;
|
|
1284
|
+
* - `ci_fix_stalled`: a fix round on the failing head pushed nothing;
|
|
1285
|
+
* - `conflict_cap`: the branch still conflicts with its base at the cap;
|
|
1286
|
+
* - `ready_blocked`: CI is green, but the description says why it is a draft;
|
|
1287
|
+
* - `check_weakened`: CI went green only after a fix loosened a check;
|
|
1288
|
+
* - `review_bot_cap`: a review BOT (Copilot, CodeRabbit) kept reviewing after
|
|
1289
|
+
* the automatic rounds ran out (PRO-2480). A bot's review never resets the
|
|
1290
|
+
* streak a person's does, so the agent and the bot cannot loop without end.
|
|
1291
|
+
*/
|
|
1292
|
+
const REVIEW_ESCALATION_REASONS = [
|
|
1293
|
+
"ci_fix_cap",
|
|
1294
|
+
"ci_fix_stalled",
|
|
1295
|
+
"conflict_cap",
|
|
1296
|
+
"ready_blocked",
|
|
1297
|
+
"check_weakened",
|
|
1298
|
+
"review_bot_cap"
|
|
1299
|
+
];
|
|
1300
|
+
/** One failing check as stored: the name is required, and the link survives only as http(s). */
|
|
1301
|
+
const storedFailingCheck = z.object({
|
|
1302
|
+
name: z.string(),
|
|
1303
|
+
url: z.string().regex(/^https?:\/\//i).nullable().catch(null)
|
|
1304
|
+
});
|
|
1305
|
+
z.object({
|
|
1306
|
+
reason: z.enum(REVIEW_ESCALATION_REASONS),
|
|
1307
|
+
rounds: lenient(z.number().refine((n) => Number.isInteger(n) && n >= 0)),
|
|
1308
|
+
headSha: lenient(z.string()),
|
|
1309
|
+
file: lenient(z.string()),
|
|
1310
|
+
failingChecks: lenient(lenientList(storedFailingCheck)),
|
|
1311
|
+
blockers: lenient(lenientList(z.enum(READY_BLOCKERS)))
|
|
1312
|
+
}).transform((e) => ({
|
|
1313
|
+
reason: e.reason,
|
|
1314
|
+
...e.rounds === void 0 ? {} : { rounds: e.rounds },
|
|
1315
|
+
...e.headSha === void 0 ? {} : { headSha: e.headSha },
|
|
1316
|
+
...e.file === void 0 ? {} : { file: e.file },
|
|
1317
|
+
...e.failingChecks === void 0 ? {} : { failingChecks: e.failingChecks },
|
|
1318
|
+
...e.blockers === void 0 ? {} : { blockers: e.blockers }
|
|
1319
|
+
}));
|
|
1224
1320
|
[...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
|
|
1225
1321
|
//#endregion
|
|
1226
1322
|
//#region ../../packages/types/dist/agents.js
|
|
@@ -1241,26 +1337,30 @@ const BACKLOG_GENERATION_EMPTY_OUTCOMES = [
|
|
|
1241
1337
|
"out_of_scope",
|
|
1242
1338
|
"needs_decision"
|
|
1243
1339
|
];
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1340
|
+
const decisionReason = z.object({
|
|
1341
|
+
policyCode: z.string(),
|
|
1342
|
+
quotedSpan: z.string(),
|
|
1343
|
+
conflict: z.string(),
|
|
1344
|
+
alternative: z.string(),
|
|
1345
|
+
overridePath: z.string()
|
|
1346
|
+
});
|
|
1347
|
+
const coveringInitiative = z.object({
|
|
1348
|
+
id: z.string().min(1),
|
|
1349
|
+
title: z.string(),
|
|
1350
|
+
priority: z.number()
|
|
1351
|
+
});
|
|
1352
|
+
/**
|
|
1353
|
+
* The empty verdict, every field checked: the reader navigates on
|
|
1354
|
+
* `coveredInitiative.id` and renders `message`.
|
|
1355
|
+
*/
|
|
1356
|
+
const backlogGenerationEmptyResult = z.object({
|
|
1357
|
+
outcome: z.enum(BACKLOG_GENERATION_EMPTY_OUTCOMES),
|
|
1358
|
+
message: z.string().nullable(),
|
|
1359
|
+
coveredByPriority: z.number().refine(Number.isInteger).nullable(),
|
|
1360
|
+
coveredById: z.string().nullable(),
|
|
1361
|
+
coveredInitiative: coveringInitiative.nullable(),
|
|
1362
|
+
reason: decisionReason.nullable().optional()
|
|
1363
|
+
});
|
|
1264
1364
|
/**
|
|
1265
1365
|
* Narrow an `agent_jobs.result` blob to {@link BacklogGenerationEmptyResult}.
|
|
1266
1366
|
*
|
|
@@ -1270,10 +1370,9 @@ function isCoveringInitiative(value) {
|
|
|
1270
1370
|
* would hand the page an undefined id to navigate to.
|
|
1271
1371
|
*/
|
|
1272
1372
|
function isBacklogGenerationEmptyResult(value) {
|
|
1273
|
-
|
|
1274
|
-
const { outcome, message, coveredByPriority, coveredById, coveredInitiative, reason } = value;
|
|
1275
|
-
return typeof outcome === "string" && BACKLOG_GENERATION_EMPTY_OUTCOMES.includes(outcome) && isStringOrNull(message) && (coveredByPriority === null || typeof coveredByPriority === "number" && Number.isInteger(coveredByPriority)) && isStringOrNull(coveredById) && (coveredInitiative === null || isCoveringInitiative(coveredInitiative)) && (reason === null || reason === void 0 || isDecisionReason(reason));
|
|
1373
|
+
return backlogGenerationEmptyResult.safeParse(value).success;
|
|
1276
1374
|
}
|
|
1375
|
+
const withCreatedInitiativeIds = z.object({ createdInitiativeIds: z.array(z.string().min(1)) });
|
|
1277
1376
|
/**
|
|
1278
1377
|
* The initiatives a `generate_backlog` job created, from its stored result
|
|
1279
1378
|
* (PRO-2483), or null when the result does not say: a job finished before the
|
|
@@ -1282,11 +1381,13 @@ function isBacklogGenerationEmptyResult(value) {
|
|
|
1282
1381
|
* the reader navigates on them.
|
|
1283
1382
|
*/
|
|
1284
1383
|
function createdInitiativeIdsOf(result) {
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) return null;
|
|
1288
|
-
return ids;
|
|
1384
|
+
const parsed = withCreatedInitiativeIds.safeParse(result);
|
|
1385
|
+
return parsed.success ? parsed.data.createdInitiativeIds : null;
|
|
1289
1386
|
}
|
|
1387
|
+
const withCreatedInitiatives = z.object({ createdInitiatives: z.array(z.object({
|
|
1388
|
+
id: z.string().min(1),
|
|
1389
|
+
title: z.string()
|
|
1390
|
+
})).min(1) });
|
|
1290
1391
|
/**
|
|
1291
1392
|
* The initiatives a `generate_backlog` run added, each with the title it was
|
|
1292
1393
|
* created with, as the worker writes them on `agent_jobs.result.createdInitiatives`
|
|
@@ -1299,15 +1400,197 @@ function createdInitiativeIdsOf(result) {
|
|
|
1299
1400
|
* read, so a reader never links an undefined id.
|
|
1300
1401
|
*/
|
|
1301
1402
|
function createdInitiativesOf(result) {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
if (!Array.isArray(list) || list.length === 0) return null;
|
|
1305
|
-
return list.every((entry) => isRecord$1(entry) && typeof entry.id === "string" && entry.id.length > 0 && typeof entry.title === "string") ? list.map((entry) => ({
|
|
1306
|
-
id: entry.id,
|
|
1307
|
-
title: entry.title
|
|
1308
|
-
})) : null;
|
|
1403
|
+
const parsed = withCreatedInitiatives.safeParse(result);
|
|
1404
|
+
return parsed.success ? parsed.data.createdInitiatives : null;
|
|
1309
1405
|
}
|
|
1310
1406
|
//#endregion
|
|
1407
|
+
//#region ../../packages/types/dist/chat-stream-events.js
|
|
1408
|
+
/**
|
|
1409
|
+
* The longest generated document, in UTF-16 units: a long brief of a dozen
|
|
1410
|
+
* pages. A document past it is refused by the agent rather than clipped, since
|
|
1411
|
+
* a document with its tail cut off reads as finished.
|
|
1412
|
+
*/
|
|
1413
|
+
const MAX_GENERATED_DOCUMENT_CHARS = 4e4;
|
|
1414
|
+
/** A JSON object whose keys a reader walks itself. */
|
|
1415
|
+
const jsonObject = z.record(z.string(), z.unknown());
|
|
1416
|
+
/** The raw Strands block a tool call starts in. */
|
|
1417
|
+
const contentBlockStart = z.object({ start: lenient(z.object({ toolUse: lenient(jsonObject) })) });
|
|
1418
|
+
/** A raw Strands delta: answer text, or a fragment of a tool's input. */
|
|
1419
|
+
const contentBlockDelta = z.object({ delta: lenient(z.object({
|
|
1420
|
+
text: lenient(z.string()),
|
|
1421
|
+
toolUse: lenient(jsonObject)
|
|
1422
|
+
})) });
|
|
1423
|
+
/**
|
|
1424
|
+
* A frame's `event`, with the keys a reader dispatches on named. The raw
|
|
1425
|
+
* Strands blocks are read here; each synthesized payload is left as it came
|
|
1426
|
+
* and parsed by the reader that acts on it. Every other key passes through.
|
|
1427
|
+
*/
|
|
1428
|
+
const chatStreamFrameEvent = z.looseObject({
|
|
1429
|
+
contentBlockStart: lenient(contentBlockStart),
|
|
1430
|
+
contentBlockDelta: lenient(contentBlockDelta),
|
|
1431
|
+
toolStatus: z.unknown().optional(),
|
|
1432
|
+
retry: z.unknown().optional(),
|
|
1433
|
+
citations: z.unknown().optional(),
|
|
1434
|
+
citationCheck: z.unknown().optional(),
|
|
1435
|
+
usage: z.unknown().optional(),
|
|
1436
|
+
thought: z.unknown().optional(),
|
|
1437
|
+
plan: z.unknown().optional(),
|
|
1438
|
+
readPause: z.unknown().optional(),
|
|
1439
|
+
initiativeProposal: z.unknown().optional(),
|
|
1440
|
+
initiativeEditProposal: z.unknown().optional(),
|
|
1441
|
+
generatedDocument: z.unknown().optional(),
|
|
1442
|
+
documentSections: z.unknown().optional(),
|
|
1443
|
+
specUpdate: z.unknown().optional(),
|
|
1444
|
+
flowSections: z.unknown().optional()
|
|
1445
|
+
});
|
|
1446
|
+
z.object({
|
|
1447
|
+
status: z.unknown().optional(),
|
|
1448
|
+
error: lenient(z.string()),
|
|
1449
|
+
code: lenient(z.string()),
|
|
1450
|
+
message: lenient(z.object({
|
|
1451
|
+
role: z.unknown().optional(),
|
|
1452
|
+
content: lenient(z.array(z.unknown()))
|
|
1453
|
+
})),
|
|
1454
|
+
event: lenient(chatStreamFrameEvent)
|
|
1455
|
+
});
|
|
1456
|
+
//#endregion
|
|
1457
|
+
//#region ../../packages/types/dist/chat-stream-payloads.js
|
|
1458
|
+
/**
|
|
1459
|
+
* The synthesized chat stream payloads that the API persists and the browser
|
|
1460
|
+
* renders, each read by ONE set of rules (PRO-2735).
|
|
1461
|
+
*
|
|
1462
|
+
* The API reads a payload before it saves it with the answer, and the browser
|
|
1463
|
+
* reads it again, live and from the saved message. The two copies of these
|
|
1464
|
+
* readers used to be kept in step by hand (`chat/routes/stream.ts`,
|
|
1465
|
+
* `product-chat/live-turn.ts`); one shared reader means the card a live
|
|
1466
|
+
* stream draws is the card the reloaded transcript draws.
|
|
1467
|
+
*
|
|
1468
|
+
* Each reader takes the payload (the value under its key in `event`, or in the
|
|
1469
|
+
* message metadata) and returns the checked value or null. A payload that
|
|
1470
|
+
* fails is dropped whole, never stored half understood: every one of them is
|
|
1471
|
+
* rendered from the metadata, and the proposals' buttons perform a write.
|
|
1472
|
+
*/
|
|
1473
|
+
/**
|
|
1474
|
+
* The bounds `createBacklogItemBody` enforces on a proposed initiative
|
|
1475
|
+
* (PRO-1879). A proposal outside them would draw a card whose button opens a
|
|
1476
|
+
* dialog the create refuses; the agent applies them too, so a value outside
|
|
1477
|
+
* them is a malformed frame rather than a long brief to trim.
|
|
1478
|
+
*/
|
|
1479
|
+
const MAX_PROPOSAL_TITLE_CHARS = 500;
|
|
1480
|
+
const MAX_PROPOSAL_DESCRIPTION_CHARS = 2e4;
|
|
1481
|
+
/** `updateBacklogItemBody`'s own bound on a description. */
|
|
1482
|
+
const MAX_EDIT_DESCRIPTION_CHARS = 2e4;
|
|
1483
|
+
/** Text that is not empty once trimmed and fits `max` as written. */
|
|
1484
|
+
function contentWithin(max) {
|
|
1485
|
+
return z.string().refine((text) => text.trim().length > 0 && text.length <= max);
|
|
1486
|
+
}
|
|
1487
|
+
/** `revision` counts only as the literal `true`; anything else leaves it off. */
|
|
1488
|
+
function revisionOf(revision) {
|
|
1489
|
+
return revision === true ? { revision: true } : {};
|
|
1490
|
+
}
|
|
1491
|
+
z.object({
|
|
1492
|
+
title: z.string().trim().min(1).max(MAX_PROPOSAL_TITLE_CHARS),
|
|
1493
|
+
description: z.string().trim().min(1).max(MAX_PROPOSAL_DESCRIPTION_CHARS),
|
|
1494
|
+
productId: z.string().trim().min(1),
|
|
1495
|
+
revision: z.unknown().optional()
|
|
1496
|
+
}).transform(({ revision, ...proposal }) => ({
|
|
1497
|
+
...proposal,
|
|
1498
|
+
...revisionOf(revision)
|
|
1499
|
+
}));
|
|
1500
|
+
z.object({
|
|
1501
|
+
initiativeId: z.string().trim().min(1),
|
|
1502
|
+
description: contentWithin(MAX_EDIT_DESCRIPTION_CHARS),
|
|
1503
|
+
basedOn: z.object({
|
|
1504
|
+
updatedAt: z.string().trim().min(1),
|
|
1505
|
+
descriptionHash: z.string().regex(/^[0-9a-f]{64}$/)
|
|
1506
|
+
})
|
|
1507
|
+
});
|
|
1508
|
+
z.object({
|
|
1509
|
+
title: z.string().trim().min(1).max(200),
|
|
1510
|
+
kind: z.string().trim().min(1).max(80),
|
|
1511
|
+
markdown: contentWithin(MAX_GENERATED_DOCUMENT_CHARS),
|
|
1512
|
+
revision: z.unknown().optional()
|
|
1513
|
+
}).transform(({ revision, ...document }) => ({
|
|
1514
|
+
...document,
|
|
1515
|
+
...revisionOf(revision)
|
|
1516
|
+
}));
|
|
1517
|
+
const UNSUPPORTED_REASONS = [
|
|
1518
|
+
"no_quote",
|
|
1519
|
+
"unsupplied_document",
|
|
1520
|
+
"not_in_document"
|
|
1521
|
+
];
|
|
1522
|
+
const OUTCOMES = [
|
|
1523
|
+
"verified",
|
|
1524
|
+
"paraphrase_supported",
|
|
1525
|
+
"repointed",
|
|
1526
|
+
"unsupported"
|
|
1527
|
+
];
|
|
1528
|
+
/**
|
|
1529
|
+
* A reason a newer agent sends, or the `quote_not_found` an agent before
|
|
1530
|
+
* PRO-1752 sent, still names an unsupported statement; either reads as the
|
|
1531
|
+
* least specific reason.
|
|
1532
|
+
*/
|
|
1533
|
+
function unsupportedReason(value) {
|
|
1534
|
+
return UNSUPPORTED_REASONS.find((reason) => reason === value) ?? "not_in_document";
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Which document timeline an entry names (PRO-1731). Absent from an agent
|
|
1538
|
+
* older than PRO-1731, and absent means plan; left off rather than written in,
|
|
1539
|
+
* so "did not say" stays distinguishable from "said plan". A timeline this
|
|
1540
|
+
* contract does not know fails the entry: the origin decides which document a
|
|
1541
|
+
* chip opens, and reading an unknown one as plan links to words that are not
|
|
1542
|
+
* there.
|
|
1543
|
+
*/
|
|
1544
|
+
const origin = z.enum(PRODUCT_DOCUMENT_ORIGINS).optional();
|
|
1545
|
+
/** One statement the check could not support; a missing reason is malformed. */
|
|
1546
|
+
const unverifiedCitation = z.object({
|
|
1547
|
+
documentType: z.string(),
|
|
1548
|
+
version: z.number(),
|
|
1549
|
+
origin,
|
|
1550
|
+
section: lenient(z.string()),
|
|
1551
|
+
reason: z.string()
|
|
1552
|
+
}).transform((u) => ({
|
|
1553
|
+
documentType: u.documentType,
|
|
1554
|
+
version: u.version,
|
|
1555
|
+
...u.origin === void 0 ? {} : { origin: u.origin },
|
|
1556
|
+
...u.section === void 0 ? {} : { section: u.section },
|
|
1557
|
+
reason: unsupportedReason(u.reason)
|
|
1558
|
+
}));
|
|
1559
|
+
/**
|
|
1560
|
+
* One marker's outcome, strictly: an entry with no 1-based marker index, no
|
|
1561
|
+
* source, or an outcome this contract does not name is dropped rather than
|
|
1562
|
+
* guessed at, because the outcome decides whether the claim renders as a
|
|
1563
|
+
* reference at all.
|
|
1564
|
+
*/
|
|
1565
|
+
const citationOutcome = z.object({
|
|
1566
|
+
markerIndex: z.number().refine((n) => Number.isInteger(n) && n >= 1),
|
|
1567
|
+
documentType: z.string(),
|
|
1568
|
+
version: z.number(),
|
|
1569
|
+
origin,
|
|
1570
|
+
section: lenient(z.string()),
|
|
1571
|
+
outcome: z.enum(OUTCOMES),
|
|
1572
|
+
reason: z.unknown().optional(),
|
|
1573
|
+
resolvedSection: lenient(z.string())
|
|
1574
|
+
}).transform((o) => ({
|
|
1575
|
+
markerIndex: o.markerIndex,
|
|
1576
|
+
documentType: o.documentType,
|
|
1577
|
+
version: o.version,
|
|
1578
|
+
...o.origin === void 0 ? {} : { origin: o.origin },
|
|
1579
|
+
...o.section === void 0 ? {} : { section: o.section },
|
|
1580
|
+
outcome: o.outcome,
|
|
1581
|
+
...o.outcome === "unsupported" ? { reason: unsupportedReason(o.reason) } : {},
|
|
1582
|
+
...o.resolvedSection === void 0 ? {} : { resolvedSection: o.resolvedSection }
|
|
1583
|
+
}));
|
|
1584
|
+
z.object({
|
|
1585
|
+
verified: z.number(),
|
|
1586
|
+
unverified: lenientList(unverifiedCitation),
|
|
1587
|
+
outcomes: lenientList(citationOutcome).catch([]).optional()
|
|
1588
|
+
}).transform(({ verified, unverified, outcomes }) => ({
|
|
1589
|
+
verified: Math.max(0, Math.trunc(verified)),
|
|
1590
|
+
unverified,
|
|
1591
|
+
...outcomes === void 0 ? {} : { outcomes }
|
|
1592
|
+
}));
|
|
1593
|
+
//#endregion
|
|
1311
1594
|
//#region ../../packages/types/dist/environments.js
|
|
1312
1595
|
const ENVIRONMENT_TYPES = [
|
|
1313
1596
|
"development",
|
|
@@ -1322,6 +1605,14 @@ const ENVIRONMENT_TYPES = [
|
|
|
1322
1605
|
//#endregion
|
|
1323
1606
|
//#region ../../packages/types/dist/maintenance.js
|
|
1324
1607
|
/**
|
|
1608
|
+
* Maintenance taxonomy + API DTOs.
|
|
1609
|
+
*
|
|
1610
|
+
* Single source of truth shared by the `maintenance_*` DB schema, the
|
|
1611
|
+
* maintenance API, the maintenance batch agent's output contract, and the
|
|
1612
|
+
* Maintaining page. Maintenance is the operational-hygiene system of record,
|
|
1613
|
+
* deliberately SEPARATE from the product backlog.
|
|
1614
|
+
*/
|
|
1615
|
+
/**
|
|
1325
1616
|
* - vulnerable_dependency / outdated_dependency: the dependency scanner
|
|
1326
1617
|
* - secret: the secrets scanner -- a credential committed to the repository
|
|
1327
1618
|
* - sast: the SAST scanner over Taiga-authored rules -- an insecure code pattern
|
|
@@ -1406,6 +1697,34 @@ const MAINTENANCE_IGNORE_REASONS = [
|
|
|
1406
1697
|
"wont_fix"
|
|
1407
1698
|
];
|
|
1408
1699
|
new Set(CODE_REMEDIATION_KINDS);
|
|
1700
|
+
/** A non-empty string, or null. */
|
|
1701
|
+
const locationText = z.string().min(1).nullable().catch(null);
|
|
1702
|
+
/**
|
|
1703
|
+
* A source line is a positive integer or nothing: the response schema declares
|
|
1704
|
+
* `int()`, so a fractional value here would fail serialization for the WHOLE
|
|
1705
|
+
* list rather than blank one row; and 0 is "no position" for scanners that
|
|
1706
|
+
* report [0, 0] for a check with no concrete line.
|
|
1707
|
+
*/
|
|
1708
|
+
const sourceLine = z.number().refine((n) => Number.isInteger(n) && n > 0).nullable().catch(null);
|
|
1709
|
+
z.object({
|
|
1710
|
+
rule_id: locationText,
|
|
1711
|
+
rule_url: locationText,
|
|
1712
|
+
tool: locationText,
|
|
1713
|
+
line: sourceLine,
|
|
1714
|
+
end_line: sourceLine,
|
|
1715
|
+
resource: locationText,
|
|
1716
|
+
in_tree: z.unknown().optional(),
|
|
1717
|
+
commit: z.string().regex(/^[0-9a-f]{7,40}$/).nullable().catch(null)
|
|
1718
|
+
}).transform((d) => ({
|
|
1719
|
+
ruleId: d.rule_id,
|
|
1720
|
+
ruleUrl: d.rule_url,
|
|
1721
|
+
tool: d.tool,
|
|
1722
|
+
line: d.line,
|
|
1723
|
+
endLine: d.end_line,
|
|
1724
|
+
resource: d.resource,
|
|
1725
|
+
historyOnly: d.in_tree === false,
|
|
1726
|
+
commit: d.commit
|
|
1727
|
+
}));
|
|
1409
1728
|
//#endregion
|
|
1410
1729
|
//#region ../../packages/types/dist/design-tokens.js
|
|
1411
1730
|
/**
|
|
@@ -1557,6 +1876,31 @@ const ARTIFACT_SCALE_FRAME_NAMES = [
|
|
|
1557
1876
|
[...ARTIFACT_CSS_VARIABLES, ...ARTIFACT_SCALE_FRAME_NAMES];
|
|
1558
1877
|
new Set(ARTIFACT_CSS_VARIABLES.filter((name) => name !== "--radius"));
|
|
1559
1878
|
//#endregion
|
|
1879
|
+
//#region ../../packages/types/dist/design-emitter.js
|
|
1880
|
+
/**
|
|
1881
|
+
* Design-system emitter (PRO-887): materialise the tenant's INTERNAL Design
|
|
1882
|
+
* Standards document into real files for a generated application.
|
|
1883
|
+
*
|
|
1884
|
+
* Until this existed the internal path reached code only as plan prose: the
|
|
1885
|
+
* planner named tokens, INSPECT flagged literal colours, but nothing wrote a
|
|
1886
|
+
* token file, so the coder hedged every colour as `var(--x, #hex)`. A linked
|
|
1887
|
+
* design system ships its own package and never needs this -- the dispatcher
|
|
1888
|
+
* gates the emitter off whenever one is linked (single source of truth).
|
|
1889
|
+
*
|
|
1890
|
+
* Pure and deterministic: same document, same bytes. No DOM, no React, no
|
|
1891
|
+
* I/O -- the API computes the files, the coding agent writes them.
|
|
1892
|
+
*/
|
|
1893
|
+
const tokenObject = z.record(z.string(), z.unknown());
|
|
1894
|
+
z.object({
|
|
1895
|
+
tokenConvention: lenient(z.string()),
|
|
1896
|
+
tokens: lenient(tokenObject),
|
|
1897
|
+
components: lenient(tokenObject)
|
|
1898
|
+
});
|
|
1899
|
+
z.object({
|
|
1900
|
+
fontFamily: lenient(z.union([z.string(), z.number()])),
|
|
1901
|
+
fontSize: lenient(z.union([z.string(), z.number()]))
|
|
1902
|
+
});
|
|
1903
|
+
//#endregion
|
|
1560
1904
|
//#region ../../packages/types/dist/auto-build.js
|
|
1561
1905
|
/**
|
|
1562
1906
|
* Auto-build: the review-plans gate (PRO-1305).
|
|
@@ -1776,14 +2120,91 @@ function emptyOriginNote(origin, product) {
|
|
|
1776
2120
|
if (origin === "plan") return imported ? `This product was imported, so no discovery chain was generated for it: its derived documents are on the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(", ")}) would appear here.` : "No plan documents are published yet: they are written during discovery in Taiga.";
|
|
1777
2121
|
if (!imported) return "No reality documents yet: they are derived from the code once the first initiative is done (a merged pull request marks it done) or the repository is pushed to. Until then read the plan side.";
|
|
1778
2122
|
switch (importStatusOf(product)) {
|
|
1779
|
-
case "pending": return "No reality documents yet: the import has not started. Link the repository
|
|
2123
|
+
case "pending": return "No reality documents yet: the import has not started. Link the repository, then taiga_start_import.";
|
|
1780
2124
|
case "analyzing":
|
|
1781
2125
|
case "generating_docs": return "No reality documents yet: the import is running and will derive them from the code.";
|
|
1782
|
-
case "cancelled": return "No reality documents: the import was cancelled before it derived any.
|
|
1783
|
-
case "failed": return "No reality documents: the import failed before it derived any.
|
|
2126
|
+
case "cancelled": return "No reality documents: the import was cancelled before it derived any. taiga_start_import runs it again.";
|
|
2127
|
+
case "failed": return "No reality documents: the import failed before it derived any. taiga_start_import runs it again.";
|
|
1784
2128
|
default: return "No reality documents: the import derives them from the code, and none have been published.";
|
|
1785
2129
|
}
|
|
1786
2130
|
}
|
|
2131
|
+
/**
|
|
2132
|
+
* What a product in setup waits for, as the next call to make. Nothing on a
|
|
2133
|
+
* draft product says it otherwise: discoveryStep names the step and
|
|
2134
|
+
* importStatus the import, and the way on from each lives in tool
|
|
2135
|
+
* descriptions an agent reads only when it already knows which. Null once the
|
|
2136
|
+
* product is active.
|
|
2137
|
+
*/
|
|
2138
|
+
function setupNextStep(product) {
|
|
2139
|
+
if (product?.status !== "draft") return null;
|
|
2140
|
+
if (isImportedProduct(product)) switch (importStatusOf(product)) {
|
|
2141
|
+
case "analyzing":
|
|
2142
|
+
case "generating_docs": return "The import is running and derives the documents from the code (taiga_wait_for_job follows its job). Then review them, and taiga_finish_discovery opens the product.";
|
|
2143
|
+
case "complete": return "The import is done: review the reality documents (taiga_list_product_documents), then taiga_finish_discovery opens the product.";
|
|
2144
|
+
case "failed":
|
|
2145
|
+
case "cancelled": return "The last import did not finish: taiga_start_import runs it again.";
|
|
2146
|
+
default: return "Link its repository if it has none (taiga_list_linkable_repositories, taiga_link_repository), then taiga_start_import: the import derives the documents from the code and runs by itself.";
|
|
2147
|
+
}
|
|
2148
|
+
if (product.discoveryStep === void 0) return null;
|
|
2149
|
+
switch (product.discoveryStep) {
|
|
2150
|
+
case "documents": return "The specification is done; the other documents are generated from it (taiga_generate_product_documents starts the ones still missing). Once every required one is published, taiga_finish_discovery opens the product.";
|
|
2151
|
+
case "checks": return "The documents are published; the checks are stamped in the web, then taiga_finish_discovery opens the product.";
|
|
2152
|
+
default: return "Discovery is a conversation held in the web: the person describes the product there and the specification comes out of it. The other documents follow, then taiga_finish_discovery opens the product.";
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
/** What discoveryStep says, for a description that shows it. */
|
|
2156
|
+
const DISCOVERY_STEP_MEANING = "discoveryStep is where a product in setup is: context (not started), conversation or import (the specification is being talked out, or derived from the code), documents (the rest being generated and reviewed), checks (stamped in the web); null once it is active.";
|
|
2157
|
+
//#endregion
|
|
2158
|
+
//#region src/tools/hints.ts
|
|
2159
|
+
/**
|
|
2160
|
+
* The next step after the API answers 404. The API's own word is bare
|
|
2161
|
+
* ("Backlog item not found", "Repository link not found"), and an agent that
|
|
2162
|
+
* hears only that guesses another ID, or a name. Each ID a tool takes names
|
|
2163
|
+
* the tool that lists the right ones; the tool's arguments say which apply.
|
|
2164
|
+
*/
|
|
2165
|
+
const BY_ARGUMENT = {
|
|
2166
|
+
itemId: "taiga_list_backlog_items lists a product's initiatives with their IDs; a run or plan ID is not one.",
|
|
2167
|
+
backlogItemId: "taiga_list_backlog_items lists a product's initiatives with their IDs.",
|
|
2168
|
+
buildId: "taiga_list_builds lists the runs, of one product or of every product the key can see.",
|
|
2169
|
+
planId: "taiga_get_plan gives the initiative's active plan and its ID.",
|
|
2170
|
+
repositoryId: "taiga_list_product_repositories lists the product's repository links; a GitHub repository id is not a link ID.",
|
|
2171
|
+
replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
|
|
2172
|
+
githubRepositoryId: "taiga_list_linkable_repositories lists the repositories the GitHub connection reaches, with their ids.",
|
|
2173
|
+
installationId: "taiga_list_linkable_repositories lists the connected GitHub accounts.",
|
|
2174
|
+
jobId: "A job ID is what the tool that started the job returned (taiga_request_initiatives, taiga_start_import).",
|
|
2175
|
+
factoryId: "taiga_list_factories lists the factories the key can see.",
|
|
2176
|
+
findingId: "taiga_list_findings lists a product's findings.",
|
|
2177
|
+
violationId: "taiga_get_policy_standing lists a product's policy violations.",
|
|
2178
|
+
environmentId: "taiga_get_deployments lists the product's environments with their IDs; a product ID is not one.",
|
|
2179
|
+
knowledgeId: "taiga_list_knowledge lists the knowledge files.",
|
|
2180
|
+
productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
|
|
2181
|
+
};
|
|
2182
|
+
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2183
|
+
const BY_TOOL = {
|
|
2184
|
+
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2185
|
+
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_backlog_item says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one." : BY_ARGUMENT.itemId
|
|
2186
|
+
};
|
|
2187
|
+
/** The hint for a 404 from one tool call: the tool's own, else one per ID it was given. */
|
|
2188
|
+
function notFoundHint(tool, args) {
|
|
2189
|
+
if (BY_TOOL[tool]) return BY_TOOL[tool];
|
|
2190
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
2191
|
+
const given = Object.keys(args).filter((key) => args[key] != null && BY_ARGUMENT[key]);
|
|
2192
|
+
given.sort((a, b) => Number(a === "productId") - Number(b === "productId"));
|
|
2193
|
+
if (given.length === 0) return void 0;
|
|
2194
|
+
const hint = given.map((key) => BY_ARGUMENT[key]).join(" ");
|
|
2195
|
+
return () => hint;
|
|
2196
|
+
}
|
|
2197
|
+
/** The hint of the tool call in progress, read by runTool when the API answers 404. */
|
|
2198
|
+
const notFoundHints = new AsyncLocalStorage();
|
|
2199
|
+
/** The API's 404 message with the hint of the call in progress, if it has one. */
|
|
2200
|
+
function withNotFoundHint(said) {
|
|
2201
|
+
const hint = notFoundHints.getStore()?.(said);
|
|
2202
|
+
return hint ? `${withStop(said)} ${hint}` : said;
|
|
2203
|
+
}
|
|
2204
|
+
/** A sentence ends in a stop before the next one is added to it. */
|
|
2205
|
+
function withStop(text) {
|
|
2206
|
+
return /[.!?]$/.test(text.trimEnd()) ? text.trimEnd() : `${text.trimEnd()}.`;
|
|
2207
|
+
}
|
|
1787
2208
|
//#endregion
|
|
1788
2209
|
//#region src/tools/errors.ts
|
|
1789
2210
|
/** A successful tool result: one JSON text block the model reads. */
|
|
@@ -1827,21 +2248,19 @@ function networkFailureDetail(error) {
|
|
|
1827
2248
|
const why = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : null;
|
|
1828
2249
|
return why ? `${error.message} (${why})` : error.message;
|
|
1829
2250
|
}
|
|
2251
|
+
/**
|
|
2252
|
+
* The code a status implies when the API sent none. 400 is VALIDATION_ERROR,
|
|
2253
|
+
* the code the API itself sends, so a refusal by this server and one by the
|
|
2254
|
+
* API read the same.
|
|
2255
|
+
*/
|
|
1830
2256
|
const CODE_BY_STATUS = {
|
|
1831
|
-
400: "
|
|
2257
|
+
400: "VALIDATION_ERROR",
|
|
1832
2258
|
401: "UNAUTHENTICATED",
|
|
1833
2259
|
403: "FORBIDDEN",
|
|
1834
2260
|
404: "NOT_FOUND",
|
|
1835
2261
|
409: "CONFLICT",
|
|
1836
2262
|
429: "RATE_LIMITED"
|
|
1837
2263
|
};
|
|
1838
|
-
/**
|
|
1839
|
-
* Run a tool handler and turn a failure into a structured error. An API
|
|
1840
|
-
* refusal keeps the API's own code when it sent one and its message whole:
|
|
1841
|
-
* on a refused publish that names the section holding template residue,
|
|
1842
|
-
* which is what the model needs to fix the draft. A failed connection is
|
|
1843
|
-
* structured too, so every failure the model sees has the same shape.
|
|
1844
|
-
*/
|
|
1845
2264
|
/** What the API said, in one phrase: the status, the code it sent (or the one its status implies), its message. */
|
|
1846
2265
|
function apiFailureText(error) {
|
|
1847
2266
|
return `${error.status} ${apiFailureCode(error)}${error.apiMessage ? `: ${error.apiMessage}` : ""}`;
|
|
@@ -1854,11 +2273,22 @@ function apiFailureCode(error) {
|
|
|
1854
2273
|
function isMissOrRefusal(error) {
|
|
1855
2274
|
return error instanceof TaigaApiError && (error.status === 404 || error.status === 403);
|
|
1856
2275
|
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Run a tool handler and turn a failure into a structured error. An API
|
|
2278
|
+
* refusal keeps the API's own code when it sent one and its message whole:
|
|
2279
|
+
* on a refused publish that names the section holding template residue,
|
|
2280
|
+
* which is what the model needs to fix the draft. A failed connection is
|
|
2281
|
+
* structured too, so every failure the model sees has the same shape.
|
|
2282
|
+
*/
|
|
1857
2283
|
async function runTool(handler) {
|
|
1858
2284
|
try {
|
|
1859
2285
|
return await handler();
|
|
1860
2286
|
} catch (error) {
|
|
1861
|
-
if (error instanceof TaigaApiError)
|
|
2287
|
+
if (error instanceof TaigaApiError) {
|
|
2288
|
+
const code = error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR";
|
|
2289
|
+
const said = error.apiMessage ?? `Taiga API answered ${error.status}.`;
|
|
2290
|
+
return toolError(code, error.status === 404 ? withNotFoundHint(said) : said, error.details);
|
|
2291
|
+
}
|
|
1862
2292
|
if (isNetworkFailure(error)) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${networkFailureDetail(error)}. Check TAIGA_API_URL.`);
|
|
1863
2293
|
throw error;
|
|
1864
2294
|
}
|
|
@@ -2039,6 +2469,24 @@ function budgetSections(doc, budget) {
|
|
|
2039
2469
|
}
|
|
2040
2470
|
return cutTo(doc, sections, Object.fromEntries(kept));
|
|
2041
2471
|
}
|
|
2472
|
+
/**
|
|
2473
|
+
* The answer to a read that names sections: the ones the document has, the
|
|
2474
|
+
* rest listed with their sizes, and the asked-for ids it lacks under
|
|
2475
|
+
* unknownSections. One wrong id does not throw away the right ones (the
|
|
2476
|
+
* model retries only the miss); none right is a miss that lists the real ids.
|
|
2477
|
+
*/
|
|
2478
|
+
function sectionReadResult(doc, ids, label) {
|
|
2479
|
+
const cut = pickSections(doc, new Set(ids));
|
|
2480
|
+
if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
|
|
2481
|
+
const all = Object.keys(sectionsOf(doc.data) ?? {});
|
|
2482
|
+
const unknownSections = ids.filter((id) => !all.includes(id));
|
|
2483
|
+
if (unknownSections.length === ids.length) return toolError("NOT_FOUND", `The ${label} has no section ${ids.join(", ")}; its sections are ${all.join(", ")}.`);
|
|
2484
|
+
return toolResult({
|
|
2485
|
+
...cut.doc,
|
|
2486
|
+
omittedSections: cut.omittedSections,
|
|
2487
|
+
...unknownSections.length > 0 ? { unknownSections } : {}
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2042
2490
|
//#endregion
|
|
2043
2491
|
//#region src/tools/summarize.ts
|
|
2044
2492
|
/**
|
|
@@ -2084,6 +2532,44 @@ function itemRows(items, fields) {
|
|
|
2084
2532
|
criteria: criteriaCounts(items[index])
|
|
2085
2533
|
} : row);
|
|
2086
2534
|
}
|
|
2535
|
+
/** Pages a name search reads before it stops and says so: 1,000 rows at the API's page ceiling. */
|
|
2536
|
+
const SEARCH_PAGE_LIMIT = 10;
|
|
2537
|
+
/**
|
|
2538
|
+
* Rows whose text contains `query`, ignoring case, read page by page until
|
|
2539
|
+
* `limit` match or the rows run out. The API has no name search, and a model
|
|
2540
|
+
* told the person's word for a thing ("the Sandbox factory") otherwise pages
|
|
2541
|
+
* the whole list to find its ID. `more` is true when it stopped before the end.
|
|
2542
|
+
*/
|
|
2543
|
+
async function searchPages(fetchPage, query, text, limit) {
|
|
2544
|
+
const needle = query.toLowerCase();
|
|
2545
|
+
const rows = [];
|
|
2546
|
+
let cursor;
|
|
2547
|
+
for (let index = 0; index < SEARCH_PAGE_LIMIT; index += 1) {
|
|
2548
|
+
const result = await fetchPage(cursor);
|
|
2549
|
+
const page = result.data;
|
|
2550
|
+
for (const [at, row] of page.entries()) {
|
|
2551
|
+
if (!text(row).some((value) => typeof value === "string" && value.toLowerCase().includes(needle))) continue;
|
|
2552
|
+
rows.push(row);
|
|
2553
|
+
if (rows.length === limit) return {
|
|
2554
|
+
rows,
|
|
2555
|
+
more: at < page.length - 1 || result.nextCursor !== null
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
if (!result.nextCursor) return {
|
|
2559
|
+
rows,
|
|
2560
|
+
more: false
|
|
2561
|
+
};
|
|
2562
|
+
cursor = result.nextCursor;
|
|
2563
|
+
}
|
|
2564
|
+
return {
|
|
2565
|
+
rows,
|
|
2566
|
+
more: true
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
/** A search reads its own pages, so a cursor from a plain page has nothing to continue. */
|
|
2570
|
+
const SEARCH_WITH_CURSOR = "A search answers one page; narrow the query instead of paging it.";
|
|
2571
|
+
/** What a search says when it stopped before reading every row. */
|
|
2572
|
+
const SEARCH_NOTE = "There may be more matches than the limit carries; narrow the query or raise the limit.";
|
|
2087
2573
|
//#endregion
|
|
2088
2574
|
//#region src/tools/products.ts
|
|
2089
2575
|
/** What a product row needs to say for the model to pick one and read it in full. */
|
|
@@ -2093,9 +2579,10 @@ const PRODUCT_SUMMARY_FIELDS = [
|
|
|
2093
2579
|
"slug",
|
|
2094
2580
|
"status",
|
|
2095
2581
|
"factoryId",
|
|
2096
|
-
"kind",
|
|
2097
2582
|
"discoveryStep"
|
|
2098
2583
|
];
|
|
2584
|
+
/** A list's name search: part of a name, any case. */
|
|
2585
|
+
const query = (what) => z.string().trim().min(1).max(200).optional().describe(`Part of the ${what}, any case: only the rows that contain it`);
|
|
2099
2586
|
/**
|
|
2100
2587
|
* What a document row needs to say for the model to pick one and read it in
|
|
2101
2588
|
* full: which document, on which timeline, how current. Not the derived-from
|
|
@@ -2242,7 +2729,7 @@ async function resolveProductRef(client, ref, accepted, usage) {
|
|
|
2242
2729
|
].filter((v) => v !== void 0).length;
|
|
2243
2730
|
if (given !== 1) {
|
|
2244
2731
|
const how = given === 0 ? usage ?? "taiga_list_products shows what exists." : "Two names could mean two products.";
|
|
2245
|
-
return refusal("
|
|
2732
|
+
return refusal("VALIDATION_ERROR", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
|
|
2246
2733
|
}
|
|
2247
2734
|
if (ref.productId) try {
|
|
2248
2735
|
return { product: (await client.getProduct(ref.productId)).data };
|
|
@@ -2271,30 +2758,59 @@ async function resolveProductRef(client, ref, accepted, usage) {
|
|
|
2271
2758
|
function registerProductTools(server, client) {
|
|
2272
2759
|
if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
|
|
2273
2760
|
title: "List factories",
|
|
2274
|
-
description: "List all factories in the organization. A factory is the group that owns a set of products. Use this to find factory IDs for reading factory instructions. Factory instructions carry the factory's standing guidance: domain context, tech preferences, and the conventions its products follow.",
|
|
2275
|
-
inputSchema: z.object({
|
|
2761
|
+
description: "List all factories in the organization. A factory is the group that owns a set of products. Use this to find factory IDs for reading factory instructions; query finds one by name. Factory instructions carry the factory's standing guidance: domain context, tech preferences, and the conventions its products follow.",
|
|
2762
|
+
inputSchema: z.object({
|
|
2763
|
+
query: query("factory name"),
|
|
2764
|
+
...pagination
|
|
2765
|
+
}),
|
|
2276
2766
|
annotations: READ_ONLY
|
|
2277
2767
|
}, (params) => runTool(async () => {
|
|
2278
|
-
|
|
2279
|
-
|
|
2768
|
+
if (params.query === void 0) {
|
|
2769
|
+
const result = await client.listFactories(params);
|
|
2770
|
+
return toolResult(page(result.data, result.nextCursor));
|
|
2771
|
+
}
|
|
2772
|
+
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
|
|
2773
|
+
const found = await searchPages((cursor) => client.listFactories({
|
|
2774
|
+
limit: 100,
|
|
2775
|
+
cursor
|
|
2776
|
+
}), params.query, (row) => [row.name, row.slug], params.limit ?? 25);
|
|
2777
|
+
return toolResult({
|
|
2778
|
+
...page(found.rows, null),
|
|
2779
|
+
...found.more ? { more: SEARCH_NOTE } : {}
|
|
2780
|
+
});
|
|
2280
2781
|
}));
|
|
2281
2782
|
if (client.hasCapability("products:read")) {
|
|
2282
2783
|
server.registerTool("taiga_list_products", {
|
|
2283
2784
|
title: "List products",
|
|
2284
|
-
description:
|
|
2785
|
+
description: `List products in the organization, as summaries (id, name, slug, status, factory, discoveryStep). Use this to find a product by name (query); inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full. ${DISCOVERY_STEP_MEANING}`,
|
|
2285
2786
|
inputSchema: z.object({
|
|
2286
|
-
|
|
2787
|
+
query: query("product name or slug"),
|
|
2788
|
+
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it").optional().describe("Only the products of this factory"),
|
|
2287
2789
|
status: z.enum(PRODUCT_STATUSES).optional().describe("Filter by status: draft until the product finishes setup in Taiga, active after that, archived when retired."),
|
|
2288
2790
|
...pagination
|
|
2289
2791
|
}),
|
|
2290
2792
|
annotations: READ_ONLY
|
|
2291
2793
|
}, (params) => runTool(async () => {
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2794
|
+
const { query: text, ...filters } = params;
|
|
2795
|
+
if (text === void 0) {
|
|
2796
|
+
const result = await client.listProducts(filters);
|
|
2797
|
+
return toolResult(page(pickFields(result.data, PRODUCT_SUMMARY_FIELDS), result.nextCursor));
|
|
2798
|
+
}
|
|
2799
|
+
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
|
|
2800
|
+
const found = await searchPages((cursor) => client.listProducts({
|
|
2801
|
+
factoryId: filters.factoryId,
|
|
2802
|
+
status: filters.status,
|
|
2803
|
+
limit: 100,
|
|
2804
|
+
cursor
|
|
2805
|
+
}), text, (row) => [row.name, row.slug], params.limit ?? 25);
|
|
2806
|
+
return toolResult({
|
|
2807
|
+
...page(pickFields(found.rows, PRODUCT_SUMMARY_FIELDS), null),
|
|
2808
|
+
...found.more ? { more: SEARCH_NOTE } : {}
|
|
2809
|
+
});
|
|
2294
2810
|
}));
|
|
2295
2811
|
server.registerTool("taiga_get_product", {
|
|
2296
2812
|
title: "Get product",
|
|
2297
|
-
description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did.",
|
|
2813
|
+
description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries; taiga_get_product_context carries this summary with the rest a session starts from. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did. A product still in setup carries nextStep: what it waits for and the call that moves it on.",
|
|
2298
2814
|
inputSchema: z.object({
|
|
2299
2815
|
productId: productId.optional(),
|
|
2300
2816
|
slug: slug.optional()
|
|
@@ -2302,7 +2818,12 @@ function registerProductTools(server, client) {
|
|
|
2302
2818
|
annotations: READ_ONLY
|
|
2303
2819
|
}, (params) => runTool(async () => {
|
|
2304
2820
|
const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
|
|
2305
|
-
|
|
2821
|
+
if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
|
|
2822
|
+
const nextStep = setupNextStep(resolved.product);
|
|
2823
|
+
return toolResult(nextStep ? {
|
|
2824
|
+
...resolved.product,
|
|
2825
|
+
nextStep
|
|
2826
|
+
} : resolved.product);
|
|
2306
2827
|
}));
|
|
2307
2828
|
server.registerTool("taiga_resolve_product", {
|
|
2308
2829
|
title: "Resolve product from git remote",
|
|
@@ -2332,7 +2853,7 @@ function registerProductTools(server, client) {
|
|
|
2332
2853
|
}));
|
|
2333
2854
|
server.registerTool("taiga_get_product_document", {
|
|
2334
2855
|
title: "Get product document",
|
|
2335
|
-
description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A large document runs to tens of thousands of characters: name the sections you need (the ids taiga_get_product_context lists) and the read carries only those, listing the rest with their sizes. A read that finds nothing says which documents and timelines the product does have.",
|
|
2856
|
+
description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A large document runs to tens of thousands of characters: name the sections you need (the ids taiga_get_product_context lists) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds nothing says which documents and timelines the product does have.",
|
|
2336
2857
|
inputSchema: z.object({
|
|
2337
2858
|
productId,
|
|
2338
2859
|
type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type -- taiga_list_product_documents shows which ones the product has"),
|
|
@@ -2347,15 +2868,7 @@ function registerProductTools(server, client) {
|
|
|
2347
2868
|
try {
|
|
2348
2869
|
const doc = stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data);
|
|
2349
2870
|
if (!params.sections) return toolResult(doc);
|
|
2350
|
-
|
|
2351
|
-
if (!cut) return toolError("VALIDATION_FAILED", `The ${origin} ${params.type} is not made of sections; read it without sections.`);
|
|
2352
|
-
const all = Object.keys(doc.data.sections);
|
|
2353
|
-
const unknown = params.sections.filter((id) => !all.includes(id));
|
|
2354
|
-
if (unknown.length > 0) return toolError("NOT_FOUND", `The ${origin} ${params.type} has no section ${unknown.join(", ")}; its sections are ${all.join(", ")}.`);
|
|
2355
|
-
return toolResult({
|
|
2356
|
-
...cut.doc,
|
|
2357
|
-
omittedSections: cut.omittedSections
|
|
2358
|
-
});
|
|
2871
|
+
return sectionReadResult(doc, params.sections, `${origin} ${params.type}`);
|
|
2359
2872
|
} catch (error) {
|
|
2360
2873
|
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
2361
2874
|
product ??= (await client.getProduct(params.productId)).data;
|
|
@@ -2477,12 +2990,16 @@ function registerDocumentTools(server, client) {
|
|
|
2477
2990
|
}));
|
|
2478
2991
|
server.registerTool("taiga_get_document", {
|
|
2479
2992
|
title: "Get company document",
|
|
2480
|
-
description: "Get the full content of one of the organization's documents, sections included. Use this when you need the security requirements, data handling rules, architecture standards or compliance controls that apply to your code, or the goals, standards and voice the organization has set down. A read that finds nothing says which documents are published and whether a draft exists.",
|
|
2481
|
-
inputSchema: z.object({
|
|
2993
|
+
description: "Get the full content of one of the organization's documents, sections included. Use this when you need the security requirements, data handling rules, architecture standards or compliance controls that apply to your code, or the goals, standards and voice the organization has set down. A policy runs to thousands of tokens: name the sections you need (the ids taiga_get_product_context lists for every policy) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds nothing says which documents are published and whether a draft exists.",
|
|
2994
|
+
inputSchema: z.object({
|
|
2995
|
+
type: tenantDocumentType,
|
|
2996
|
+
sections: z.array(z.string().min(1).max(200)).min(1).max(50).optional().describe("Only these sections, by id; omit for the whole document")
|
|
2997
|
+
}),
|
|
2482
2998
|
annotations: READ_ONLY
|
|
2483
2999
|
}, (params) => runTool(async () => {
|
|
2484
3000
|
try {
|
|
2485
|
-
|
|
3001
|
+
const doc = stripProvenance((await client.getTenantDocument(params.type)).data);
|
|
3002
|
+
return params.sections ? sectionReadResult(doc, params.sections, params.type) : toolResult(doc);
|
|
2486
3003
|
} catch (error) {
|
|
2487
3004
|
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
2488
3005
|
const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(params.type), client.listTenantDocuments({ limit: 100 })]);
|
|
@@ -2526,7 +3043,7 @@ function registerDocumentTools(server, client) {
|
|
|
2526
3043
|
title: "List factory documents",
|
|
2527
3044
|
description: `List a factory's documents, published and draft, as summaries (type, name, version, draft state). A factory has one document type, ${named(FACTORY_DOCUMENT_TYPES)}: its free-form standing guidance (domain context, tech preferences, conventions) that applies to every product it owns. Drafts are listed too (isDraft true); an empty list means neither a publication nor a draft exists. Use taiga_get_factory_document to read the full content.`,
|
|
2528
3045
|
inputSchema: z.object({
|
|
2529
|
-
factoryId:
|
|
3046
|
+
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
|
|
2530
3047
|
...pagination
|
|
2531
3048
|
}),
|
|
2532
3049
|
annotations: READ_ONLY
|
|
@@ -2538,7 +3055,7 @@ function registerDocumentTools(server, client) {
|
|
|
2538
3055
|
title: "Get factory instructions",
|
|
2539
3056
|
description: "Get the full content of a factory's instructions. Use this when you need the domain context, tech preferences, or conventions the factory expects every product it owns to follow. A read that finds nothing says whether a draft exists and how to write one.",
|
|
2540
3057
|
inputSchema: z.object({
|
|
2541
|
-
factoryId:
|
|
3058
|
+
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
|
|
2542
3059
|
type: z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type")
|
|
2543
3060
|
}),
|
|
2544
3061
|
annotations: READ_ONLY
|
|
@@ -2563,7 +3080,7 @@ function registerDocumentTools(server, client) {
|
|
|
2563
3080
|
saveDraft: (data) => client.saveFactoryDocumentDraft(factoryId, type, { data }),
|
|
2564
3081
|
publish: () => client.publishFactoryDocument(factoryId, type)
|
|
2565
3082
|
});
|
|
2566
|
-
const factoryId =
|
|
3083
|
+
const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
|
|
2567
3084
|
const factoryDocumentType = z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type");
|
|
2568
3085
|
server.registerTool("taiga_update_factory_document", {
|
|
2569
3086
|
title: "Update a section of factory instructions",
|
|
@@ -2658,7 +3175,7 @@ function registerJobTools(server, client) {
|
|
|
2658
3175
|
server.registerTool("taiga_get_job", {
|
|
2659
3176
|
title: "Get an agent job",
|
|
2660
3177
|
description: "Read one agent job: background work a person asked an agent for, such as an initiative request (taiga_request_initiatives). It says where the job is (pending, processing, completed, failed, cancelled), its progress and any error. A finished initiative request also says what came of it: the initiatives it added, or why it added none (already covered by an existing initiative, not readable as work, outside the product, or not allowed by a policy, with the policy, the line it runs into and the way to decide otherwise). taiga_wait_for_job waits for a job to finish.",
|
|
2661
|
-
inputSchema: z.object({ jobId:
|
|
3178
|
+
inputSchema: z.object({ jobId: idInput("job", "the tool that started the job returned it") }),
|
|
2662
3179
|
annotations: READ_ONLY
|
|
2663
3180
|
}, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
|
|
2664
3181
|
}
|
|
@@ -2879,7 +3396,8 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
2879
3396
|
"archivedAt",
|
|
2880
3397
|
"lifecycle"
|
|
2881
3398
|
];
|
|
2882
|
-
const
|
|
3399
|
+
const planId = idInput("plan", "taiga_get_plan gives the initiative's active plan and its ID");
|
|
3400
|
+
const itemId = idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it");
|
|
2883
3401
|
/** The plan without its steps' prose: enough to approve it or say what a run will do. */
|
|
2884
3402
|
const PLAN_SUMMARY_FIELDS = [
|
|
2885
3403
|
"id",
|
|
@@ -2939,9 +3457,10 @@ function registerBacklogTools(server, client) {
|
|
|
2939
3457
|
function registerBacklogReads(server, client) {
|
|
2940
3458
|
server.registerTool("taiga_list_backlog_items", {
|
|
2941
3459
|
title: "List backlog items",
|
|
2942
|
-
description: "List a product's backlog items (initiatives): titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done) and criteria, how many of its acceptance criteria are met (null before it is planned). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
|
|
3460
|
+
description: "List a product's backlog items (initiatives): titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done) and criteria, how many of its acceptance criteria are met (null before it is planned; met counts the criteria the agent confirmed with evidence or a person checked off, so a done item can show fewer met than it has). Use this to find the item you are working on (query finds one by title), then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
|
|
2943
3461
|
inputSchema: z.object({
|
|
2944
3462
|
productId,
|
|
3463
|
+
query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
|
|
2945
3464
|
status: z.enum([
|
|
2946
3465
|
"backlog",
|
|
2947
3466
|
"in_progress",
|
|
@@ -2965,13 +3484,26 @@ function registerBacklogReads(server, client) {
|
|
|
2965
3484
|
}),
|
|
2966
3485
|
annotations: READ_ONLY
|
|
2967
3486
|
}, (params) => runTool(async () => {
|
|
2968
|
-
const
|
|
2969
|
-
|
|
3487
|
+
const { query, ...filters } = params;
|
|
3488
|
+
if (query === void 0) {
|
|
3489
|
+
const result = await client.listBacklogItems(params.productId, filters);
|
|
3490
|
+
return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
|
|
3491
|
+
}
|
|
3492
|
+
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
|
|
3493
|
+
const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
|
|
3494
|
+
...filters,
|
|
3495
|
+
limit: 100,
|
|
3496
|
+
cursor
|
|
3497
|
+
}), query, (row) => [row.title], params.limit ?? 25);
|
|
3498
|
+
return toolResult({
|
|
3499
|
+
...page(itemRows(found.rows, BACKLOG_SUMMARY_FIELDS), null),
|
|
3500
|
+
...found.more ? { more: SEARCH_NOTE } : {}
|
|
3501
|
+
});
|
|
2970
3502
|
}));
|
|
2971
3503
|
server.registerTool("taiga_get_backlog_item", {
|
|
2972
3504
|
title: "Get backlog item",
|
|
2973
|
-
description: "Get one backlog item in full: description, acceptance criteria (and which are met), technical context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before implementing an item, and read its runs with taiga_list_builds to see what the coding agent already did.",
|
|
2974
|
-
inputSchema: z.object({ itemId
|
|
3505
|
+
description: "Get one backlog item in full: description, acceptance criteria (and which are met), technical context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before implementing an item, and read its runs with taiga_list_builds to see what the coding agent already did. lifecycle says whose turn it is and what moves it on: cta start_plan is taiga_queue_backlog_item, retry_plan taiga_plan_again (planFailureCause says why the plan stopped), start_build taiga_approve_plan, resume_build taiga_resume_backlog_item, retry_build a fresh run (start over, in the web), review_pr and view_pr the pull request. needsHumanCause is set only when the line stopped on it.",
|
|
3506
|
+
inputSchema: z.object({ itemId }),
|
|
2975
3507
|
annotations: READ_ONLY
|
|
2976
3508
|
}, (params) => runTool(async () => {
|
|
2977
3509
|
return toolResult((await client.getBacklogItem(params.itemId)).data);
|
|
@@ -3022,7 +3554,7 @@ function registerMove(server, client) {
|
|
|
3022
3554
|
inputSchema: z.object({
|
|
3023
3555
|
itemId,
|
|
3024
3556
|
state: z.enum(USER_DROP_STATES).describe("The column to move into"),
|
|
3025
|
-
afterId:
|
|
3557
|
+
afterId: idInput("backlog item", "taiga_list_backlog_items gives it").nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
|
|
3026
3558
|
archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
|
|
3027
3559
|
}),
|
|
3028
3560
|
annotations: OPERATE
|
|
@@ -3099,7 +3631,7 @@ function registerQueue(server, client) {
|
|
|
3099
3631
|
description: "Add an initiative to the product queue, the line the coding agent works: queued initiatives are planned and built one at a time, each advancing when the previous pull request merges. This starts paid work: a planner run, then a coding run that pushes commits and opens a pull request in the product repository. Whether the plan is built at once or waits for a person is the initiative's review gate: pass autoBuild: false to read the plan before anything is built (the product's default applies otherwise, and the outcome says which it is); the gate is set first and stays set if the move is then refused. Ask the person before calling this. An initiative already in the queue stays where it is unless afterId says where to go; one being planned or built cannot move (CONFLICT). Returns the initiative with its new state, its review gate and its place in the queue.",
|
|
3100
3632
|
inputSchema: z.object({
|
|
3101
3633
|
itemId,
|
|
3102
|
-
afterId:
|
|
3634
|
+
afterId: idInput("backlog item", "taiga_list_backlog_items gives it").nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is"),
|
|
3103
3635
|
autoBuild: autoBuildInput.optional().describe("Set the initiative's review gate before queueing: false to read the plan first, true to build it at once, null to follow the product; omit to leave the gate as it is")
|
|
3104
3636
|
}),
|
|
3105
3637
|
annotations: OPERATE
|
|
@@ -3141,7 +3673,7 @@ function registerReviewGate(server, client) {
|
|
|
3141
3673
|
function registerResume(server, client) {
|
|
3142
3674
|
server.registerTool("taiga_resume_backlog_item", {
|
|
3143
3675
|
title: "Resume backlog item",
|
|
3144
|
-
description: "Resume an initiative the line stopped on
|
|
3676
|
+
description: "Resume an initiative the line stopped on, which is when needsHumanCause is set (the line sets it): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first unfinished part (the steps already committed are kept), a refused build start is cleared and retried. This is the way to carry work on: a run is the record of an attempt, and decisions about the work are made on the initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume: a plan that failed with needsHumanCause null (lifecycle.cta retry_plan, planFailureCause says why) is planned again with taiga_plan_again. Starts paid work; ask the person first.",
|
|
3145
3677
|
inputSchema: z.object({ itemId }),
|
|
3146
3678
|
annotations: OPERATE
|
|
3147
3679
|
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
|
|
@@ -3150,14 +3682,14 @@ function registerPlanVerdicts(server, client) {
|
|
|
3150
3682
|
server.registerTool("taiga_approve_plan", {
|
|
3151
3683
|
title: "Approve a plan",
|
|
3152
3684
|
description: "Approve an initiative's plan: its build starts now, as the approver, the same start the Start run button makes. Read the plan first with taiga_get_plan and name its ID; an approval of a plan that was replaced since is refused (CONFLICT), as is one while a new plan is being generated or a build is already running. Starts a coding run that pushes commits and opens a pull request; ask the person first.",
|
|
3153
|
-
inputSchema: z.object({ planId
|
|
3685
|
+
inputSchema: z.object({ planId }),
|
|
3154
3686
|
annotations: OPERATE
|
|
3155
3687
|
}, (params) => runTool(async () => outcome((await client.approvePlan(params.planId)).data)));
|
|
3156
3688
|
server.registerTool("taiga_reject_plan", {
|
|
3157
3689
|
title: "Reject a plan",
|
|
3158
3690
|
description: "Send an initiative's plan back with direction: a new plan is generated with the comment as reviewer feedback and the rejected plan as its starting point, which supersedes it when the new one lands. Read the plan first with taiga_get_plan and name its ID. Starts a planner run; ask the person first.",
|
|
3159
3691
|
inputSchema: z.object({
|
|
3160
|
-
planId
|
|
3692
|
+
planId,
|
|
3161
3693
|
comment: z.string().trim().min(1).max(PLAN_REJECTION_COMMENT_MAX_CHARS).describe("What the next plan should do differently: a paragraph of direction")
|
|
3162
3694
|
}),
|
|
3163
3695
|
annotations: OPERATE
|
|
@@ -3203,9 +3735,24 @@ const RUN_OUTCOME_FIELDS = [
|
|
|
3203
3735
|
"prUrl",
|
|
3204
3736
|
"actions"
|
|
3205
3737
|
];
|
|
3738
|
+
/**
|
|
3739
|
+
* Where a run that ended without finishing stopped, from its steps and its
|
|
3740
|
+
* hardening result: a failed run with every step completed reads as nothing
|
|
3741
|
+
* failed, and retry_from_failed_step then reads as a mistake, when the agent
|
|
3742
|
+
* stalled in the hardening pass after the steps and the retry continues it.
|
|
3743
|
+
* Null for a run still going or that finished.
|
|
3744
|
+
*/
|
|
3745
|
+
function stoppedIn(run, steps) {
|
|
3746
|
+
if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
|
|
3747
|
+
if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
|
|
3748
|
+
const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
|
|
3749
|
+
if (open) return open.status === "planned" ? `before step ${String(open.stepOrder)}, ${String(open.title)}, started (the ones before it completed)` : `step ${String(open.stepOrder)}, ${String(open.title)} (${String(open.status)})`;
|
|
3750
|
+
if (run.hardeningResult == null) return "after the last step, in hardening: the security scanners and their fixes the agent runs before the pull request is ready. A retry continues from there; the steps stay.";
|
|
3751
|
+
return "after the steps and hardening, at the pull request";
|
|
3752
|
+
}
|
|
3206
3753
|
/** How many products the org-wide list names; past this a row keeps its productId only. */
|
|
3207
3754
|
const NAMED_PRODUCTS = 100;
|
|
3208
|
-
const buildId =
|
|
3755
|
+
const buildId = idInput("build run", "taiga_list_builds gives it");
|
|
3209
3756
|
/** A step without its instructions: what it is, how it stands and what it changed. */
|
|
3210
3757
|
const STEP_SUMMARY_FIELDS = [
|
|
3211
3758
|
"stepOrder",
|
|
@@ -3266,14 +3813,14 @@ function registerBuildTools(server, client) {
|
|
|
3266
3813
|
description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each. A run is the record of an attempt: work is carried on from its initiative (resume and retry_from_failed_step are taiga_resume_backlog_item; start_over is in the web only for now) and stopped by moving the initiative out of Build; a maintenance run, which has no initiative, is retried from its finding. With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
|
|
3267
3814
|
inputSchema: z.object({
|
|
3268
3815
|
productId: productId.optional(),
|
|
3269
|
-
backlogItemId:
|
|
3816
|
+
backlogItemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it").optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
|
|
3270
3817
|
active: z.boolean().optional().describe("Only runs the agent is executing now (cloning through reviewing; a queued pending run is not yet executing and is left out); for the org-wide list"),
|
|
3271
3818
|
...pagination
|
|
3272
3819
|
}),
|
|
3273
3820
|
annotations: READ_ONLY
|
|
3274
3821
|
}, (params) => runTool(async () => {
|
|
3275
|
-
if (!params.productId && params.backlogItemId) return toolError("
|
|
3276
|
-
if (params.productId && params.active) return toolError("
|
|
3822
|
+
if (!params.productId && params.backlogItemId) return toolError("VALIDATION_ERROR", "backlogItemId narrows the runs of one product: pass its productId as well (the item carries it).");
|
|
3823
|
+
if (params.productId && params.active) return toolError("VALIDATION_ERROR", "active narrows the org-wide list only: omit productId, or read the product's runs and judge each by its status.");
|
|
3277
3824
|
if (params.productId) {
|
|
3278
3825
|
const result = await client.listBuildRuns(params.productId, params);
|
|
3279
3826
|
return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
|
|
@@ -3282,7 +3829,7 @@ function registerBuildTools(server, client) {
|
|
|
3282
3829
|
}));
|
|
3283
3830
|
server.registerTool("taiga_get_build", {
|
|
3284
3831
|
title: "Get build run",
|
|
3285
|
-
description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions name what the initiative accepts for it: resume and retry_from_failed_step are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
|
|
3832
|
+
description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. A run that failed or was stopped says where (stoppedIn: a step, or the hardening pass after them) and when it last made progress (lastProgressAt). Its actions name what the initiative accepts for it: resume and retry_from_failed_step (which continues from the first unfinished part) are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
|
|
3286
3833
|
inputSchema: z.object({
|
|
3287
3834
|
buildId,
|
|
3288
3835
|
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
@@ -3290,13 +3837,18 @@ function registerBuildTools(server, client) {
|
|
|
3290
3837
|
annotations: READ_ONLY
|
|
3291
3838
|
}, (params) => runTool(async () => {
|
|
3292
3839
|
const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
|
|
3840
|
+
const runRow = run.data;
|
|
3841
|
+
const rows = steps.data;
|
|
3842
|
+
const where = stoppedIn(runRow, rows);
|
|
3843
|
+
const stop = where ? { stoppedIn: where } : {};
|
|
3293
3844
|
if (params.full) return toolResult({
|
|
3294
|
-
...
|
|
3845
|
+
...runRow,
|
|
3846
|
+
...stop,
|
|
3295
3847
|
steps: steps.data
|
|
3296
3848
|
});
|
|
3297
|
-
const rows = steps.data;
|
|
3298
3849
|
return toolResult({
|
|
3299
|
-
...
|
|
3850
|
+
...runRow,
|
|
3851
|
+
...stop,
|
|
3300
3852
|
steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
|
|
3301
3853
|
...step,
|
|
3302
3854
|
headline: summaryHeadline(rows[index].summary)
|
|
@@ -3587,10 +4139,12 @@ async function productContext(client, product, options) {
|
|
|
3587
4139
|
};
|
|
3588
4140
|
const parts = Object.keys(fetchers).filter(granted);
|
|
3589
4141
|
const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
|
|
4142
|
+
const nextStep = setupNextStep(product);
|
|
3590
4143
|
const bundle = { product: {
|
|
3591
4144
|
...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
|
|
3592
4145
|
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
3593
|
-
defaultOrigin: defaultOriginFor(product)
|
|
4146
|
+
defaultOrigin: defaultOriginFor(product),
|
|
4147
|
+
...nextStep ? { nextStep } : {}
|
|
3594
4148
|
} };
|
|
3595
4149
|
const unavailable = {};
|
|
3596
4150
|
parts.forEach((part, index) => {
|
|
@@ -3603,7 +4157,7 @@ async function productContext(client, product, options) {
|
|
|
3603
4157
|
...bundle,
|
|
3604
4158
|
notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
|
|
3605
4159
|
unavailable,
|
|
3606
|
-
readNext: "Read a policy
|
|
4160
|
+
readNext: "Read a policy with taiga_get_document (sections: the ids listed here, for only those), another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item (taiga_list_backlog_items pages a truncated backlog) and a run with taiga_get_build."
|
|
3607
4161
|
};
|
|
3608
4162
|
let characters = JSON.stringify(result, null, 2).length;
|
|
3609
4163
|
for (let measured = -1; measured !== characters;) {
|
|
@@ -3654,7 +4208,7 @@ function registerAutonomyTools(server, client) {
|
|
|
3654
4208
|
}),
|
|
3655
4209
|
annotations: OPERATE_SETTING
|
|
3656
4210
|
}, (params) => runTool(async () => {
|
|
3657
|
-
if (params.autoBuild === void 0 && params.autoMerge === void 0) return toolError("
|
|
4211
|
+
if (params.autoBuild === void 0 && params.autoMerge === void 0) return toolError("VALIDATION_ERROR", "Set autoBuild, autoMerge or both.");
|
|
3658
4212
|
const autoMerge = params.autoMerge === true ? null : params.autoMerge;
|
|
3659
4213
|
const product = (await client.updateProduct(params.productId, {
|
|
3660
4214
|
...params.autoBuild !== void 0 ? { autoBuild: params.autoBuild } : {},
|
|
@@ -3672,7 +4226,7 @@ function registerAutonomyTools(server, client) {
|
|
|
3672
4226
|
title: "Set whether an initiative's pull request merges on its own",
|
|
3673
4227
|
description: "An initiative's own switch for merging on its own: true lets its pull request merge itself once the repository's own rules pass (required checks, reviews, a code reviewer you made Taiga wait for); false makes it wait for a person; null follows the product's default. The final word within what the organization and factories allow: where either has turned autonomous merge off, true changes nothing. Taiga merges through your rules, never around them. Ask the person first.",
|
|
3674
4228
|
inputSchema: z.object({
|
|
3675
|
-
itemId:
|
|
4229
|
+
itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
|
|
3676
4230
|
autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
|
|
3677
4231
|
}),
|
|
3678
4232
|
annotations: OPERATE_SETTING
|
|
@@ -3692,6 +4246,7 @@ function registerAutonomyTools(server, client) {
|
|
|
3692
4246
|
const ITEM_FIELDS = [
|
|
3693
4247
|
"id",
|
|
3694
4248
|
"productId",
|
|
4249
|
+
"productName",
|
|
3695
4250
|
"category",
|
|
3696
4251
|
"kind",
|
|
3697
4252
|
"subjectType",
|
|
@@ -3708,6 +4263,7 @@ const ITEM_FIELDS = [
|
|
|
3708
4263
|
const SETUP_TASK_FIELDS = [
|
|
3709
4264
|
"id",
|
|
3710
4265
|
"productId",
|
|
4266
|
+
"productName",
|
|
3711
4267
|
"category",
|
|
3712
4268
|
"phase",
|
|
3713
4269
|
"target",
|
|
@@ -3724,7 +4280,7 @@ function registerInboxTools(server, client) {
|
|
|
3724
4280
|
if (!client.hasCapability("products:read")) return;
|
|
3725
4281
|
server.registerTool("taiga_list_action_required", {
|
|
3726
4282
|
title: "List what needs a person",
|
|
3727
|
-
description: "Read Action Required, where work comes back to a person: things to review (a plan, a set of initiatives, a document, a pull request, maintenance findings, new policy violations), each a decision an agent handed back rather than made, and things to set up that only a person can (a secret, an environment variable, an account, a package registry, a cloud permission). Across every product the key can open, or one product. category gate is the reviews, setup the setup items; resolved: true reads the archive of done and dismissed ones. The inbox is shared: it is the team's, not one person's. An item snoozed until later is left out until then, as the page leaves it out (snoozedHidden counts them). taiga_list_setup_tasks has the setup tasks with what exactly to set.",
|
|
4283
|
+
description: "Read Action Required, where work comes back to a person: things to review (a plan, a set of initiatives, a document, a pull request, maintenance findings, new policy violations), each a decision an agent handed back rather than made, and things to set up that only a person can (a secret, an environment variable, an account, a package registry, a cloud permission). Across every product the key can open (each row names its product), or one product. category gate is the reviews, setup the setup items; resolved: true reads the archive of done and dismissed ones. The inbox is shared: it is the team's, not one person's. An item snoozed until later is left out until then, as the page leaves it out (snoozedHidden counts them). taiga_list_setup_tasks has the setup tasks with what exactly to set.",
|
|
3728
4284
|
inputSchema: z.object({
|
|
3729
4285
|
productId: productId.optional(),
|
|
3730
4286
|
category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
|
|
@@ -3749,14 +4305,19 @@ function registerInboxTools(server, client) {
|
|
|
3749
4305
|
}));
|
|
3750
4306
|
server.registerTool("taiga_list_setup_tasks", {
|
|
3751
4307
|
title: "List setup tasks",
|
|
3752
|
-
description: "The setup tasks agents raised for work only a person can do, with what exactly to set and where: the name of an environment variable, secret or API key to create (key; never its value, which Taiga does not store or display), the provider page to do it on, and the phase that says when it blocks you (prereq: before the deploy, which fails without it; wiring: connecting generated values back to providers; followup: billing approvals, extra permissions and the like that can wait until after launch). A build does not wait for any of them. Across every product the key can open, or one.",
|
|
4308
|
+
description: "The setup tasks agents raised for work only a person can do, with what exactly to set and where: the name of an environment variable, secret or API key to create (key; never its value, which Taiga does not store or display), the provider page to do it on, and the phase that says when it blocks you (prereq: before the deploy, which fails without it; wiring: connecting generated values back to providers; followup: billing approvals, extra permissions and the like that can wait until after launch). A build does not wait for any of them. Across every product the key can open (each row names its product), or one. The tasks still to do, oldest first, the order they depend on each other; includeDone: true adds the done and dismissed ones.",
|
|
3753
4309
|
inputSchema: z.object({
|
|
3754
4310
|
productId: productId.optional(),
|
|
4311
|
+
includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
|
|
3755
4312
|
...pagination
|
|
3756
4313
|
}),
|
|
3757
4314
|
annotations: READ_ONLY
|
|
3758
4315
|
}, (params) => runTool(async () => {
|
|
3759
|
-
const
|
|
4316
|
+
const { includeDone, ...rest } = params;
|
|
4317
|
+
const result = await client.listSetupTasks({
|
|
4318
|
+
...rest,
|
|
4319
|
+
...includeDone ? {} : { openOnly: true }
|
|
4320
|
+
});
|
|
3760
4321
|
return toolResult(page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor));
|
|
3761
4322
|
}));
|
|
3762
4323
|
if (!client.hasCapability("products:write")) return;
|
|
@@ -3764,7 +4325,7 @@ function registerInboxTools(server, client) {
|
|
|
3764
4325
|
title: "Resolve or snooze an Action Required item",
|
|
3765
4326
|
description: "Clear an item from Action Required, for everybody: done means you did it, dismissed means it does not apply and is not going to, in_progress means someone has picked it up. Or snooze it until a time (an hour, tomorrow, a week; null wakes it). Nothing marks a setup task done for you: setting the variable or connecting the registry is the work, and marking it done is still a person's. Resolving a review item does not approve or merge anything; the plan or pull request is answered where it lives. One exception: marking the product documents review item done records, in your name, that every published document was reviewed, and setting it back to in_progress withdraws that; do it only when the person says they read them. Ask the person before clearing what they have not dealt with.",
|
|
3766
4327
|
inputSchema: z.object({
|
|
3767
|
-
itemId:
|
|
4328
|
+
itemId: idInput("Action Required item", "taiga_list_action_required gives it"),
|
|
3768
4329
|
status: z.enum([
|
|
3769
4330
|
"in_progress",
|
|
3770
4331
|
"done",
|
|
@@ -3774,7 +4335,7 @@ function registerInboxTools(server, client) {
|
|
|
3774
4335
|
}),
|
|
3775
4336
|
annotations: OPERATE
|
|
3776
4337
|
}, (params) => runTool(async () => {
|
|
3777
|
-
if (params.status === void 0 && params.snoozedUntil === void 0) return toolError("
|
|
4338
|
+
if (params.status === void 0 && params.snoozedUntil === void 0) return toolError("VALIDATION_ERROR", "Say what became of the item (status) or until when to snooze it.");
|
|
3778
4339
|
return toolResult(pickFields([(await client.updateInboxItem(params.itemId, {
|
|
3779
4340
|
...params.status !== void 0 ? { status: params.status } : {},
|
|
3780
4341
|
...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
|
|
@@ -3798,10 +4359,10 @@ const KNOWLEDGE_FIELDS = [
|
|
|
3798
4359
|
];
|
|
3799
4360
|
/** Past this many characters a text file's content is cut when read; the download URL has it all. */
|
|
3800
4361
|
const TEXT_LIMIT = 2e4;
|
|
3801
|
-
const knowledgeId =
|
|
4362
|
+
const knowledgeId = idInput("knowledge file", "taiga_list_knowledge gives it");
|
|
3802
4363
|
const scopeInputs = {
|
|
3803
|
-
factoryId:
|
|
3804
|
-
productId:
|
|
4364
|
+
factoryId: idInput("factory", "taiga_list_factories gives it").optional().describe("A factory's knowledge"),
|
|
4365
|
+
productId: productId.optional().describe("A product's knowledge")
|
|
3805
4366
|
};
|
|
3806
4367
|
/** Bytes of a text file the read takes at most; the download URL has the rest. */
|
|
3807
4368
|
const READ_BYTES = 262144;
|
|
@@ -3865,7 +4426,7 @@ function registerKnowledgeTools(server, client) {
|
|
|
3865
4426
|
annotations: READ_ONLY
|
|
3866
4427
|
}, (params) => runTool(async () => {
|
|
3867
4428
|
const twoLevels = oneLevel(params);
|
|
3868
|
-
if (twoLevels) return toolError("
|
|
4429
|
+
if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
|
|
3869
4430
|
const scope = params.productId ? { productId: params.productId } : params.factoryId ? {
|
|
3870
4431
|
factoryId: params.factoryId,
|
|
3871
4432
|
productId: null
|
|
@@ -3922,7 +4483,7 @@ function registerKnowledgeTools(server, client) {
|
|
|
3922
4483
|
annotations: OPERATE
|
|
3923
4484
|
}, (params) => runTool(async () => {
|
|
3924
4485
|
const twoLevels = oneLevel(params);
|
|
3925
|
-
if (twoLevels) return toolError("
|
|
4486
|
+
if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
|
|
3926
4487
|
const mime = params.fileName.toLowerCase().endsWith(".md") ? "text/markdown" : "text/plain";
|
|
3927
4488
|
const result = await client.addKnowledge({
|
|
3928
4489
|
fileName: params.fileName,
|
|
@@ -4008,7 +4569,7 @@ const VIOLATION_FIELDS = [
|
|
|
4008
4569
|
"verifyRequestedAt",
|
|
4009
4570
|
"assessedAt"
|
|
4010
4571
|
];
|
|
4011
|
-
const findingId =
|
|
4572
|
+
const findingId = idInput("finding", "taiga_list_findings gives it");
|
|
4012
4573
|
function registerMaintenanceTools(server, client) {
|
|
4013
4574
|
if (!client.hasCapability("products:read")) return;
|
|
4014
4575
|
server.registerTool("taiga_get_maintenance", {
|
|
@@ -4019,7 +4580,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4019
4580
|
}, (params) => runTool(async () => {
|
|
4020
4581
|
const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
|
|
4021
4582
|
const latest = pickFields(sweeps.data, SWEEP_FIELDS);
|
|
4022
|
-
const { lastRun: _lastRun, ...standing } = summary.data;
|
|
4583
|
+
const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
|
|
4023
4584
|
return toolResult({
|
|
4024
4585
|
...standing,
|
|
4025
4586
|
latestSweeps: latest,
|
|
@@ -4053,7 +4614,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4053
4614
|
}, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
|
|
4054
4615
|
server.registerTool("taiga_get_policy_standing", {
|
|
4055
4616
|
title: "Get a product's policy standing",
|
|
4056
|
-
description: "Where a product stands against the organization's own published policies: the controls it violates, each with its violations (critical and high only, each citing the policy's sentence and the document passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, awaiting verification), and when the documents were derived and assessed. A control with no violation is not listed, and it is not \"passing\" either: there is no such state. Pass full: true for every violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
|
|
4617
|
+
description: "Where a product stands against the organization's own published policies: the controls it violates, each with its violations (critical and high only, each citing the policy's sentence and the document passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, awaiting verification), and when the documents were derived and assessed, led by a summary of the counts. A control with no violation is not listed, and it is not \"passing\" either: there is no such state. Pass full: true for every violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
|
|
4057
4618
|
inputSchema: z.object({
|
|
4058
4619
|
productId,
|
|
4059
4620
|
full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
|
|
@@ -4061,16 +4622,32 @@ function registerMaintenanceTools(server, client) {
|
|
|
4061
4622
|
annotations: READ_ONLY
|
|
4062
4623
|
}, (params) => runTool(async () => {
|
|
4063
4624
|
const standing = (await client.getPolicyStanding(params.productId)).data;
|
|
4064
|
-
if (params.full) return toolResult(standing);
|
|
4065
4625
|
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
4066
4626
|
const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
|
|
4067
4627
|
const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
|
|
4628
|
+
const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
|
|
4629
|
+
const bySeverity = {};
|
|
4630
|
+
for (const violation of violations) {
|
|
4631
|
+
const severity = String(violation.severity ?? "unknown");
|
|
4632
|
+
bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
|
|
4633
|
+
}
|
|
4634
|
+
const summary = {
|
|
4635
|
+
controls: listed.length,
|
|
4636
|
+
violations: violations.length,
|
|
4637
|
+
bySeverity,
|
|
4638
|
+
resolvedViolations: resolved
|
|
4639
|
+
};
|
|
4640
|
+
if (params.full) return toolResult({
|
|
4641
|
+
summary,
|
|
4642
|
+
...standing
|
|
4643
|
+
});
|
|
4068
4644
|
return toolResult({
|
|
4645
|
+
summary,
|
|
4069
4646
|
productId: standing.productId,
|
|
4070
4647
|
hasPolicies: standing.hasPolicies,
|
|
4071
4648
|
assessment: standing.assessment,
|
|
4072
4649
|
documents: standing.documents,
|
|
4073
|
-
controls: pickFields(
|
|
4650
|
+
controls: pickFields(listed, [
|
|
4074
4651
|
"controlId",
|
|
4075
4652
|
"policyType",
|
|
4076
4653
|
"text",
|
|
@@ -4079,8 +4656,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4079
4656
|
"findingCount",
|
|
4080
4657
|
"plannedCount"
|
|
4081
4658
|
]),
|
|
4082
|
-
violations: pickFields(violations, VIOLATION_FIELDS)
|
|
4083
|
-
resolvedViolations: resolved
|
|
4659
|
+
violations: pickFields(violations, VIOLATION_FIELDS)
|
|
4084
4660
|
});
|
|
4085
4661
|
}));
|
|
4086
4662
|
if (!client.hasCapability("products:write")) return;
|
|
@@ -4096,7 +4672,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4096
4672
|
}),
|
|
4097
4673
|
annotations: OPERATE
|
|
4098
4674
|
}, (params) => runTool(async () => {
|
|
4099
|
-
if (params.ignored && params.reason === void 0) return toolError("
|
|
4675
|
+
if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
|
|
4100
4676
|
const body = params.ignored ? {
|
|
4101
4677
|
status: "ignored",
|
|
4102
4678
|
ignoreReason: params.reason,
|
|
@@ -4138,11 +4714,11 @@ function registerMaintenanceTools(server, client) {
|
|
|
4138
4714
|
inputSchema: z.object({
|
|
4139
4715
|
productId,
|
|
4140
4716
|
controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
|
|
4141
|
-
violationId:
|
|
4717
|
+
violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
|
|
4142
4718
|
}),
|
|
4143
4719
|
annotations: OPERATE
|
|
4144
4720
|
}, (params) => runTool(async () => {
|
|
4145
|
-
if (params.controlId === void 0 === (params.violationId === void 0)) return toolError("
|
|
4721
|
+
if (params.controlId === void 0 === (params.violationId === void 0)) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both or neither.");
|
|
4146
4722
|
return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
|
|
4147
4723
|
}));
|
|
4148
4724
|
}
|
|
@@ -4176,7 +4752,7 @@ const ENVIRONMENT_FIELDS = [
|
|
|
4176
4752
|
"isProtected",
|
|
4177
4753
|
"url"
|
|
4178
4754
|
];
|
|
4179
|
-
const environmentId =
|
|
4755
|
+
const environmentId = idInput("environment", "taiga_get_deployments lists them; a product ID is not one");
|
|
4180
4756
|
const windowDays = (choices) => z.number().int().optional().describe(`Days to look back: ${choices} (default 30)`);
|
|
4181
4757
|
/** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
|
|
4182
4758
|
const isApiFailure = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
|
|
@@ -4235,6 +4811,7 @@ function registerObserveTools(server, client) {
|
|
|
4235
4811
|
part(() => client.getDeploymentSyncStatus(params.productId))
|
|
4236
4812
|
]);
|
|
4237
4813
|
const now = current.data;
|
|
4814
|
+
const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_define_environment, and its deployments are mirrored from the provider once it is." : history.data.length === 0 && !params.cursor ? "No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last synced), or nothing has been deployed." : null;
|
|
4238
4815
|
return toolResult({
|
|
4239
4816
|
environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
|
|
4240
4817
|
...environments.more ? { moreEnvironments: true } : {},
|
|
@@ -4244,7 +4821,8 @@ function registerObserveTools(server, client) {
|
|
|
4244
4821
|
..."value" in sync ? { syncStatus: sync.value.data } : {
|
|
4245
4822
|
syncStatus: null,
|
|
4246
4823
|
syncUnavailable: sync.unavailable
|
|
4247
|
-
}
|
|
4824
|
+
},
|
|
4825
|
+
...note ? { note } : {}
|
|
4248
4826
|
});
|
|
4249
4827
|
}));
|
|
4250
4828
|
server.registerTool("taiga_get_delivery_metrics", {
|
|
@@ -4262,7 +4840,7 @@ function registerObserveTools(server, client) {
|
|
|
4262
4840
|
30,
|
|
4263
4841
|
60,
|
|
4264
4842
|
90
|
|
4265
|
-
].includes(days)) return toolError("
|
|
4843
|
+
].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90.");
|
|
4266
4844
|
return toolResult((await client.getDeliveryMetrics(params.productId, params.environmentId, days)).data);
|
|
4267
4845
|
}));
|
|
4268
4846
|
server.registerTool("taiga_get_monitoring", {
|
|
@@ -4276,22 +4854,32 @@ function registerObserveTools(server, client) {
|
|
|
4276
4854
|
annotations: READ_ONLY
|
|
4277
4855
|
}, (params) => runTool(async () => {
|
|
4278
4856
|
const health = await client.getMonitoringHealth(params.productId);
|
|
4279
|
-
if (!params.environmentId) return toolResult({
|
|
4857
|
+
if (!params.environmentId) return toolResult({
|
|
4858
|
+
health: health.data,
|
|
4859
|
+
...health.data.length === 0 ? { note: "No environment is monitored: monitoring starts when an environment has its App URL (taiga_define_environment, taiga_update_environment). No health is not good health." } : {}
|
|
4860
|
+
});
|
|
4280
4861
|
const days = params.windowDays ?? 30;
|
|
4281
4862
|
if (![
|
|
4282
4863
|
30,
|
|
4283
4864
|
60,
|
|
4284
4865
|
90
|
|
4285
|
-
].includes(days)) return toolError("
|
|
4866
|
+
].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90, the windows the page offers.");
|
|
4286
4867
|
const env = params.environmentId;
|
|
4287
4868
|
const [uptime, experience, clientHealth] = await Promise.all([
|
|
4288
4869
|
part(() => client.getMonitoringSignal(params.productId, "uptime", env, days)),
|
|
4289
4870
|
part(() => client.getMonitoringSignal(params.productId, "rum", env, days)),
|
|
4290
4871
|
part(() => client.getMonitoringSignal(params.productId, "client-health", env, days))
|
|
4291
4872
|
]);
|
|
4873
|
+
if ("unavailable" in uptime && [
|
|
4874
|
+
uptime,
|
|
4875
|
+
experience,
|
|
4876
|
+
clientHealth
|
|
4877
|
+
].every((p) => "unavailable" in p && p.unavailable.startsWith("404"))) return toolError("NOT_FOUND", withNotFoundHint(uptime.unavailable.replace(/^404:\s*/, "") || "Environment not found"));
|
|
4292
4878
|
const value = (p) => "value" in p ? p.value.data : { unavailable: p.unavailable };
|
|
4879
|
+
const envHealth = health.data.filter((row) => row.environmentId === env);
|
|
4293
4880
|
return toolResult({
|
|
4294
|
-
health:
|
|
4881
|
+
health: envHealth,
|
|
4882
|
+
...envHealth.length === 0 ? { note: "No health for this environment: it has no App URL to check yet (taiga_update_environment sets one). No health is not good health." } : {},
|
|
4295
4883
|
windowDays: days,
|
|
4296
4884
|
uptime: value(uptime),
|
|
4297
4885
|
realUserExperience: value(experience),
|
|
@@ -4327,7 +4915,7 @@ function registerObserveTools(server, client) {
|
|
|
4327
4915
|
annotations: OPERATE_SETTING
|
|
4328
4916
|
}, (params) => runTool(async () => {
|
|
4329
4917
|
const { environmentId: id, ...changes } = params;
|
|
4330
|
-
if (Object.values(changes).every((value) => value === void 0)) return toolError("
|
|
4918
|
+
if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_ERROR", "Change at least one of name, type or url.");
|
|
4331
4919
|
const updated = (await client.updateEnvironment(id, changes)).data;
|
|
4332
4920
|
return toolResult(pickFields([updated], ENVIRONMENT_FIELDS)[0]);
|
|
4333
4921
|
}));
|
|
@@ -19051,7 +19639,7 @@ function registerPrompts(server, client) {
|
|
|
19051
19639
|
if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_backlog_item", {
|
|
19052
19640
|
title: "Implement a backlog item",
|
|
19053
19641
|
description: "Implement one initiative: its description and acceptance criteria, the architecture of the product from its own timeline, the factory's instructions and the policy sections to check against. taiga_list_backlog_items gives the item ID.",
|
|
19054
|
-
argsSchema: z.object({ itemId:
|
|
19642
|
+
argsSchema: z.object({ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it") })
|
|
19055
19643
|
}, async (args) => {
|
|
19056
19644
|
const item = (await served$1(client.getBacklogItem(args.itemId), "No backlog item with that ID the key can see; taiga_list_backlog_items shows what exists")).data;
|
|
19057
19645
|
const productId = String(item.productId);
|
|
@@ -19146,7 +19734,7 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
19146
19734
|
"description",
|
|
19147
19735
|
"language"
|
|
19148
19736
|
];
|
|
19149
|
-
const repositoryId =
|
|
19737
|
+
const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
|
|
19150
19738
|
function registerRepositoryTools(server, client) {
|
|
19151
19739
|
if (client.hasCapability("products:read")) {
|
|
19152
19740
|
server.registerTool("taiga_list_product_repositories", {
|
|
@@ -19178,7 +19766,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19178
19766
|
description: "The repositories the organization's GitHub connection can reach, to link one to a product (taiga_link_repository takes its id). Searches by name with query; without installationId it reads every connected GitHub account. No connection means GitHub is not connected yet: that is done once, in the web, by someone who can install the app.",
|
|
19179
19767
|
inputSchema: z.object({
|
|
19180
19768
|
query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
|
|
19181
|
-
installationId:
|
|
19769
|
+
installationId: idInput("GitHub account", "this tool lists them").optional(),
|
|
19182
19770
|
...pagination
|
|
19183
19771
|
}),
|
|
19184
19772
|
annotations: READ_ONLY
|
|
@@ -19188,8 +19776,8 @@ function registerRepositoryTools(server, client) {
|
|
|
19188
19776
|
if (installations.length === 0) return toolError("NOT_CONNECTED", "GitHub is not connected for this organization. It is connected once, in the web; then link.");
|
|
19189
19777
|
const chosen = params.installationId ? installations.filter((i) => i.id === params.installationId) : installations;
|
|
19190
19778
|
if (chosen.length === 0) return toolError("NOT_FOUND", "No such GitHub connection; these are the connected accounts.", { accounts });
|
|
19191
|
-
if (params.cursor !== void 0 && params.query !== void 0) return toolError("
|
|
19192
|
-
if (chosen.length > 1 && params.cursor !== void 0) return toolError("
|
|
19779
|
+
if (params.cursor !== void 0 && params.query !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
|
|
19780
|
+
if (chosen.length > 1 && params.cursor !== void 0) return toolError("VALIDATION_ERROR", "A cursor pages one account: name its installationId.", { accounts });
|
|
19193
19781
|
const max = params.limit ?? 25;
|
|
19194
19782
|
const rows = (data) => pickFields(data, GITHUB_REPOSITORY_FIELDS);
|
|
19195
19783
|
if (chosen.length === 1) {
|
|
@@ -19223,7 +19811,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19223
19811
|
description: "Link a repository the GitHub connection can reach (taiga_list_linkable_repositories gives its id) to a product, or with replacesRepositoryId swap the one linked now for it in one step (naming the same repository there changes nothing; linking one already linked without it is refused). A link Taiga manages (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported in the background. Ask the person first.",
|
|
19224
19812
|
inputSchema: z.object({
|
|
19225
19813
|
productId,
|
|
19226
|
-
githubRepositoryId:
|
|
19814
|
+
githubRepositoryId: idInput("GitHub repository", "taiga_list_linkable_repositories gives it"),
|
|
19227
19815
|
replacesRepositoryId: repositoryId.optional().describe("The link this one replaces")
|
|
19228
19816
|
}),
|
|
19229
19817
|
annotations: OPERATE
|
|
@@ -19244,7 +19832,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19244
19832
|
}),
|
|
19245
19833
|
annotations: OPERATE_SETTING
|
|
19246
19834
|
}, (params) => runTool(async () => {
|
|
19247
|
-
if (params.baseBranch === void 0 && params.label === void 0) return toolError("
|
|
19835
|
+
if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_ERROR", "Set baseBranch, label or both.");
|
|
19248
19836
|
const updated = (await client.updateRepository(params.productId, params.repositoryId, {
|
|
19249
19837
|
...params.baseBranch !== void 0 ? { baseBranch: params.baseBranch } : {},
|
|
19250
19838
|
...params.label !== void 0 ? { label: params.label } : {}
|
|
@@ -19411,26 +19999,56 @@ const FACTORY_FIELDS = [
|
|
|
19411
19999
|
"slug",
|
|
19412
20000
|
"isDefault"
|
|
19413
20001
|
];
|
|
19414
|
-
const factoryId =
|
|
20002
|
+
const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
|
|
19415
20003
|
const buildScope = {
|
|
19416
20004
|
ownsInfrastructure: z.boolean().nullable().optional().describe("Whether Taiga writes the infrastructure code; null follows the organization"),
|
|
19417
20005
|
ownsDeployment: z.boolean().nullable().optional().describe("Whether Taiga writes the CI/CD pipelines; null follows the organization")
|
|
19418
20006
|
};
|
|
19419
20007
|
const deploymentNotes = z.string().trim().min(1).max(DEPLOYMENT_NOTES_MAX_LENGTH).describe("When a half is off: a sentence on how it is done today, which Taiga designs and plans against");
|
|
20008
|
+
/** How a missing required document comes to exist, said with a documents_incomplete refusal. */
|
|
20009
|
+
const MISSING_DOCUMENTS_HOW = "An imported product gets them from its import (taiga_start_import); a product started from scratch gets the specification from the Discovery conversation in the web and the rest from taiga_generate_product_documents. A draft is published with taiga_publish_product_document.";
|
|
19420
20010
|
/** A product row the model can act on, saying how it started as the other product tools say it. */
|
|
19421
|
-
function productRow(product) {
|
|
20011
|
+
async function productRow(client, product) {
|
|
19422
20012
|
const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
|
|
20013
|
+
const nextStep = setupNextStep(await withDiscoveryStep(client, product));
|
|
19423
20014
|
return {
|
|
19424
20015
|
...rest,
|
|
19425
|
-
projectType: isImportedProduct(product) ? "brownfield" : "greenfield"
|
|
20016
|
+
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
20017
|
+
...nextStep ? { nextStep } : {}
|
|
19426
20018
|
};
|
|
19427
20019
|
}
|
|
19428
20020
|
/**
|
|
20021
|
+
* The product with its discoveryStep. The API derives it on reads only, not
|
|
20022
|
+
* on the answer to a write (products.ts), and a draft started from scratch
|
|
20023
|
+
* needs it to say what comes next; an imported one reads its import status
|
|
20024
|
+
* instead. Best effort: a failed read leaves the product without a nextStep
|
|
20025
|
+
* rather than failing the write that already happened.
|
|
20026
|
+
*/
|
|
20027
|
+
async function withDiscoveryStep(client, product) {
|
|
20028
|
+
if (product.status !== "draft" || "discoveryStep" in product || isImportedProduct(product)) return product;
|
|
20029
|
+
try {
|
|
20030
|
+
return (await client.getProduct(String(product.id))).data;
|
|
20031
|
+
} catch (error) {
|
|
20032
|
+
if (error instanceof TaigaApiError || isNetworkFailure(error)) return product;
|
|
20033
|
+
throw error;
|
|
20034
|
+
}
|
|
20035
|
+
}
|
|
20036
|
+
/**
|
|
20037
|
+
* A half turned off with nothing said about how it is done today. Allowed (the
|
|
20038
|
+
* web asks for the sentence but does not insist), so a warning rather than a
|
|
20039
|
+
* refusal: plans then design against a gap.
|
|
20040
|
+
*/
|
|
20041
|
+
function missingNotesWarning(product) {
|
|
20042
|
+
const off = product.ownsInfrastructure === false || product.ownsDeployment === false;
|
|
20043
|
+
const notes = typeof product.deploymentNotes === "string" ? product.deploymentNotes.trim() : "";
|
|
20044
|
+
return off && !notes ? ["A half Taiga does not build has no deploymentNotes: plans cannot design against how it is done today. Ask the person for a sentence and set it."] : [];
|
|
20045
|
+
}
|
|
20046
|
+
/**
|
|
19429
20047
|
* The sentence is for a half Taiga does not build; the web asks for it only
|
|
19430
20048
|
* then. When both halves are decided on here, a sentence has nothing to say.
|
|
19431
20049
|
*/
|
|
19432
20050
|
function notesWithNothingToDescribe(params) {
|
|
19433
|
-
return typeof params.deploymentNotes === "string" && params.ownsInfrastructure === true && params.ownsDeployment === true ? toolError("
|
|
20051
|
+
return typeof params.deploymentNotes === "string" && params.ownsInfrastructure === true && params.ownsDeployment === true ? toolError("VALIDATION_ERROR", "deploymentNotes describes a half Taiga does not build; both are on, so leave it out.") : null;
|
|
19434
20052
|
}
|
|
19435
20053
|
function registerSetupTools(server, client) {
|
|
19436
20054
|
if (client.hasCapability("factories:write")) {
|
|
@@ -19473,7 +20091,7 @@ function registerSetupTools(server, client) {
|
|
|
19473
20091
|
...params.stopLine ? { stopLine: true } : {}
|
|
19474
20092
|
});
|
|
19475
20093
|
return toolResult({
|
|
19476
|
-
product: productRow(moved.data),
|
|
20094
|
+
product: await productRow(client, moved.data),
|
|
19477
20095
|
changed: moved.changed,
|
|
19478
20096
|
previousFactoryId: moved.previousFactoryId,
|
|
19479
20097
|
lineStopped: moved.lineStopped,
|
|
@@ -19504,7 +20122,7 @@ function registerSetupTools(server, client) {
|
|
|
19504
20122
|
...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
|
|
19505
20123
|
...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {}
|
|
19506
20124
|
})).data;
|
|
19507
|
-
return toolResult(productRow(product));
|
|
20125
|
+
return toolResult(await productRow(client, product));
|
|
19508
20126
|
}));
|
|
19509
20127
|
server.registerTool("taiga_update_product", {
|
|
19510
20128
|
title: "Change a product's settings",
|
|
@@ -19519,10 +20137,15 @@ function registerSetupTools(server, client) {
|
|
|
19519
20137
|
annotations: OPERATE_SETTING
|
|
19520
20138
|
}, (params) => runTool(async () => {
|
|
19521
20139
|
const { productId: id, ...changes } = params;
|
|
19522
|
-
if (Object.values(changes).every((value) => value === void 0)) return toolError("
|
|
20140
|
+
if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_ERROR", "Change at least one setting.");
|
|
19523
20141
|
const contradiction = notesWithNothingToDescribe(params);
|
|
19524
20142
|
if (contradiction) return contradiction;
|
|
19525
|
-
|
|
20143
|
+
const updated = (await client.updateProduct(id, changes)).data;
|
|
20144
|
+
const warnings = missingNotesWarning(updated);
|
|
20145
|
+
return toolResult({
|
|
20146
|
+
...await productRow(client, updated),
|
|
20147
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
20148
|
+
});
|
|
19526
20149
|
}));
|
|
19527
20150
|
server.registerTool("taiga_refresh_reality_documents", {
|
|
19528
20151
|
title: "Refresh a product's documents from its code",
|
|
@@ -19535,16 +20158,23 @@ function registerSetupTools(server, client) {
|
|
|
19535
20158
|
}, (params) => runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data)));
|
|
19536
20159
|
server.registerTool("taiga_finish_discovery", {
|
|
19537
20160
|
title: "Finish Discovery",
|
|
19538
|
-
description: "Finish a product's Discovery. Refused (CONFLICT) with details.reason: documents_incomplete names the required documents still unpublished (details.missingDocuments); not_in_setup means it is already finished; a required check left unstamped (stamped in the web) is refused with its details too. It locks the document set and opens the rest of the product; for an imported product it is also when the first initiatives are planned. A locked document changes only after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.",
|
|
20161
|
+
description: "Finish a product's Discovery. Refused (CONFLICT) with details.reason: documents_incomplete names the required documents still unpublished (details.missingDocuments) and how they are made; not_in_setup means it is already finished; a required check left unstamped (stamped in the web) is refused with its details too. It locks the document set and opens the rest of the product; for an imported product it is also when the first initiatives are planned. A locked document changes only after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.",
|
|
19539
20162
|
inputSchema: z.object({ productId }),
|
|
19540
20163
|
annotations: OPERATE
|
|
19541
|
-
}, (params) => runTool(async () =>
|
|
20164
|
+
}, (params) => runTool(async () => {
|
|
20165
|
+
try {
|
|
20166
|
+
return toolResult(await productRow(client, (await client.completeSetup(params.productId)).data));
|
|
20167
|
+
} catch (error) {
|
|
20168
|
+
if (!(error instanceof TaigaApiError) || error.details?.reason !== "documents_incomplete") throw error;
|
|
20169
|
+
return toolError(error.code ?? "CONFLICT", `${withStop(error.apiMessage ?? "Required documents are not published yet")} ${MISSING_DOCUMENTS_HOW}`, error.details);
|
|
20170
|
+
}
|
|
20171
|
+
}));
|
|
19542
20172
|
server.registerTool("taiga_reopen_discovery", {
|
|
19543
20173
|
title: "Reopen Discovery",
|
|
19544
20174
|
description: "Reopen a finished product's Discovery so its documents can be amended; finish it again afterwards. Nothing built from them is undone. Ask the person first.",
|
|
19545
20175
|
inputSchema: z.object({ productId }),
|
|
19546
20176
|
annotations: OPERATE
|
|
19547
|
-
}, (params) => runTool(async () => toolResult(productRow((await client.reopenSetup(params.productId)).data))));
|
|
20177
|
+
}, (params) => runTool(async () => toolResult(await productRow(client, (await client.reopenSetup(params.productId)).data))));
|
|
19548
20178
|
if (!client.hasCapability("products:read")) return;
|
|
19549
20179
|
server.registerTool("taiga_start_import", {
|
|
19550
20180
|
title: "Import a codebase into a product",
|
|
@@ -19824,7 +20454,7 @@ function registerWaitTools(server, client, options) {
|
|
|
19824
20454
|
title: "Wait for an initiative",
|
|
19825
20455
|
description: "Wait until an initiative reaches a point, instead of reading it again and again: changed (anything about where it is or its latest run), planned (its plan landed), your_turn (it waits on a person: a plan to review, a pull request, a flag), pr_open (a pull request open now; a merged or closed one does not count), run_settled (its latest run stopped moving) or done (finished; a canceled one stops the wait). Returns as soon as that holds (reached: true), early when it cannot hold without a person (a flag, a failed plan, a plan waiting for its verdict, a pull request waiting for a review, a cancelled initiative, or a landed plan whose review gate cannot be read; stoppedBecause says which), or at the timeout with reached: false: call again to keep waiting. Conditions are states, not events: an initiative whose latest run is already settled answers run_settled at once; item is null when not even the first read answered in time. Planning takes minutes and a build up to an hour.",
|
|
19826
20456
|
inputSchema: z.object({
|
|
19827
|
-
itemId:
|
|
20457
|
+
itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
|
|
19828
20458
|
until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
|
|
19829
20459
|
timeoutSeconds
|
|
19830
20460
|
}),
|
|
@@ -19857,7 +20487,7 @@ function registerWaitTools(server, client, options) {
|
|
|
19857
20487
|
title: "Wait for a run",
|
|
19858
20488
|
description: "Wait until a coding agent's run reaches a point: changed (status, steps or pull request), step_done (one more step finished), pr_open (open now, not merged or closed) or settled (it stopped moving: in review, completed, failed, stopped). Returns as soon as that holds (reached: true), early when the run settled without it (stoppedBecause; changed waits on, since a person resuming the run is a change), or at the timeout with reached: false: call again to keep waiting. Conditions are states: a run already settled answers settled at once; run is null when not even the first read answered in time.",
|
|
19859
20489
|
inputSchema: z.object({
|
|
19860
|
-
buildId:
|
|
20490
|
+
buildId: idInput("build run", "taiga_list_builds gives it"),
|
|
19861
20491
|
until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
|
|
19862
20492
|
timeoutSeconds
|
|
19863
20493
|
}),
|
|
@@ -19878,7 +20508,7 @@ function registerWaitTools(server, client, options) {
|
|
|
19878
20508
|
title: "Wait for an agent job",
|
|
19879
20509
|
description: "Wait until an agent job finishes (completed, failed or cancelled): an initiative request, a document, a refresh. Returns the job as taiga_get_job reads it, with what came of an initiative request, as soon as it settles (reached: true), or at the timeout with reached: false: call again to keep waiting. Most jobs take a few minutes.",
|
|
19880
20510
|
inputSchema: z.object({
|
|
19881
|
-
jobId:
|
|
20511
|
+
jobId: idInput("job", "the tool that started the job returned it"),
|
|
19882
20512
|
timeoutSeconds
|
|
19883
20513
|
}),
|
|
19884
20514
|
annotations: READ_ONLY
|
|
@@ -19897,7 +20527,8 @@ function registerWaitTools(server, client, options) {
|
|
|
19897
20527
|
//#endregion
|
|
19898
20528
|
//#region src/tools/index.ts
|
|
19899
20529
|
/**
|
|
19900
|
-
* Every tool refuses an argument it does not take
|
|
20530
|
+
* Every tool refuses an argument it does not take, and every tool's 404 says
|
|
20531
|
+
* where the right IDs are (hints.ts). A plain object schema
|
|
19901
20532
|
* strips one silently: a review note passed to a tool with no place for it
|
|
19902
20533
|
* vanished and the call still succeeded, and a misspelt option quietly
|
|
19903
20534
|
* became its default. Strict, the call fails with "Unrecognized key" and the
|
|
@@ -19909,10 +20540,12 @@ function withStrictInputs(server) {
|
|
|
19909
20540
|
server.registerTool = ((name, config, callback) => {
|
|
19910
20541
|
const schema = config.inputSchema;
|
|
19911
20542
|
if (schema !== void 0 && !(schema instanceof z.ZodObject)) throw new Error(`Tool ${name}: inputSchema must be a z.object() so unknown arguments can be refused`);
|
|
20543
|
+
const handler = callback;
|
|
20544
|
+
const withHint = (args, ctx) => notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
|
|
19912
20545
|
return register(name, schema ? {
|
|
19913
20546
|
...config,
|
|
19914
20547
|
inputSchema: schema.strict()
|
|
19915
|
-
} : config,
|
|
20548
|
+
} : config, withHint);
|
|
19916
20549
|
});
|
|
19917
20550
|
return server;
|
|
19918
20551
|
}
|
|
@@ -19942,17 +20575,30 @@ function registerAllTools(server, client, options = {}) {
|
|
|
19942
20575
|
registerResources(server, client);
|
|
19943
20576
|
registerPrompts(server, client);
|
|
19944
20577
|
}
|
|
19945
|
-
//#endregion
|
|
19946
|
-
//#region src/tools/instructions.ts
|
|
19947
|
-
/**
|
|
19948
|
-
* What the server tells a client at connect time: the knowledge base, the
|
|
19949
|
-
* order to read it in, the two document timelines, what section metadata
|
|
19950
|
-
* means, and when to write. Kept as data so a test can hold it to the
|
|
19951
|
-
* catalogue the same way it holds the tool descriptions.
|
|
19952
|
-
*/
|
|
19953
20578
|
const SERVER_INSTRUCTIONS = [
|
|
19954
|
-
|
|
19955
|
-
|
|
20579
|
+
[
|
|
20580
|
+
"You have access to Taiga, a policy-aware software development platform: the organization's",
|
|
20581
|
+
"engineering knowledge base, its products, their backlog and what the coding agent has built.",
|
|
20582
|
+
"",
|
|
20583
|
+
"**Start here.** Call taiga_get_product_context first, naming the product by its git remote URL inside a",
|
|
20584
|
+
"checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with",
|
|
20585
|
+
"query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory",
|
|
20586
|
+
"instructions, its specification and architecture, every policy as its section ids, the open initiatives,",
|
|
20587
|
+
"the repositories and the latest runs. Then read only what you need: the policy sections with",
|
|
20588
|
+
"taiga_get_document, an initiative with taiga_get_backlog_item. Check Action Required",
|
|
20589
|
+
"(taiga_list_action_required) for what waits on a person.",
|
|
20590
|
+
"",
|
|
20591
|
+
"**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that",
|
|
20592
|
+
"starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it",
|
|
20593
|
+
"(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). A client",
|
|
20594
|
+
"that prompts on destructive tools does part of that for you.",
|
|
20595
|
+
"",
|
|
20596
|
+
"**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the",
|
|
20597
|
+
"right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.",
|
|
20598
|
+
"",
|
|
20599
|
+
"**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action",
|
|
20600
|
+
"Required is the inbox; the line is the queue; a factory is the group that owns a set of products."
|
|
20601
|
+
].join("\n"),
|
|
19956
20602
|
"",
|
|
19957
20603
|
"**Company documents** (taiga_list_documents / taiga_get_document):",
|
|
19958
20604
|
`The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
|
|
@@ -20046,15 +20692,9 @@ const SERVER_INSTRUCTIONS = [
|
|
|
20046
20692
|
"`priority: core` marks the sections every organization keeps. Neither changes what the section",
|
|
20047
20693
|
"requires of you.",
|
|
20048
20694
|
"",
|
|
20049
|
-
"
|
|
20050
|
-
"
|
|
20051
|
-
"
|
|
20052
|
-
"taiga_get_product_document to read by section), every policy as its section ids, the open backlog",
|
|
20053
|
-
"items, the repositories and the latest runs in one call, and names any part it could not read. Then read",
|
|
20054
|
-
"what you need in full: a policy with taiga_get_document, a backlog item with taiga_get_backlog_item.",
|
|
20055
|
-
"Without it, use taiga_list_factories and taiga_list_products, then read the factory instructions,",
|
|
20056
|
-
"the product's specification and architecture, and the backlog item you are working on. A document",
|
|
20057
|
-
"read that finds nothing tells you what does exist and which tool reads or writes it.",
|
|
20695
|
+
"Without taiga_get_product_context, use taiga_list_factories and taiga_list_products, then read the",
|
|
20696
|
+
"factory instructions, the product's specification and architecture, and the initiative you are working",
|
|
20697
|
+
"on. A document read that finds nothing tells you what does exist and which tool reads or writes it.",
|
|
20058
20698
|
"",
|
|
20059
20699
|
"The same documents are resources (taiga://documents/{type}, taiga://products/{productId}/documents/{type},",
|
|
20060
20700
|
"taiga://factories/{factoryId}/instructions, taiga://backlog/{itemId}) for clients that attach them, and three",
|