@taiga-ai/mcp-server 0.16.0-dev.437 → 0.17.0-dev.438
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 +451 -146
- package/package.json +1 -1
- 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 +32 -12
- 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.438.tgz +0 -0
- package/taiga-ai-mcp-server-0.16.0-dev.437.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)"),
|
|
@@ -2111,14 +2120,91 @@ function emptyOriginNote(origin, product) {
|
|
|
2111
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.";
|
|
2112
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.";
|
|
2113
2122
|
switch (importStatusOf(product)) {
|
|
2114
|
-
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.";
|
|
2115
2124
|
case "analyzing":
|
|
2116
2125
|
case "generating_docs": return "No reality documents yet: the import is running and will derive them from the code.";
|
|
2117
|
-
case "cancelled": return "No reality documents: the import was cancelled before it derived any.
|
|
2118
|
-
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.";
|
|
2119
2128
|
default: return "No reality documents: the import derives them from the code, and none have been published.";
|
|
2120
2129
|
}
|
|
2121
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
|
+
}
|
|
2122
2208
|
//#endregion
|
|
2123
2209
|
//#region src/tools/errors.ts
|
|
2124
2210
|
/** A successful tool result: one JSON text block the model reads. */
|
|
@@ -2162,21 +2248,19 @@ function networkFailureDetail(error) {
|
|
|
2162
2248
|
const why = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : null;
|
|
2163
2249
|
return why ? `${error.message} (${why})` : error.message;
|
|
2164
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
|
+
*/
|
|
2165
2256
|
const CODE_BY_STATUS = {
|
|
2166
|
-
400: "
|
|
2257
|
+
400: "VALIDATION_ERROR",
|
|
2167
2258
|
401: "UNAUTHENTICATED",
|
|
2168
2259
|
403: "FORBIDDEN",
|
|
2169
2260
|
404: "NOT_FOUND",
|
|
2170
2261
|
409: "CONFLICT",
|
|
2171
2262
|
429: "RATE_LIMITED"
|
|
2172
2263
|
};
|
|
2173
|
-
/**
|
|
2174
|
-
* Run a tool handler and turn a failure into a structured error. An API
|
|
2175
|
-
* refusal keeps the API's own code when it sent one and its message whole:
|
|
2176
|
-
* on a refused publish that names the section holding template residue,
|
|
2177
|
-
* which is what the model needs to fix the draft. A failed connection is
|
|
2178
|
-
* structured too, so every failure the model sees has the same shape.
|
|
2179
|
-
*/
|
|
2180
2264
|
/** What the API said, in one phrase: the status, the code it sent (or the one its status implies), its message. */
|
|
2181
2265
|
function apiFailureText(error) {
|
|
2182
2266
|
return `${error.status} ${apiFailureCode(error)}${error.apiMessage ? `: ${error.apiMessage}` : ""}`;
|
|
@@ -2189,11 +2273,22 @@ function apiFailureCode(error) {
|
|
|
2189
2273
|
function isMissOrRefusal(error) {
|
|
2190
2274
|
return error instanceof TaigaApiError && (error.status === 404 || error.status === 403);
|
|
2191
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
|
+
*/
|
|
2192
2283
|
async function runTool(handler) {
|
|
2193
2284
|
try {
|
|
2194
2285
|
return await handler();
|
|
2195
2286
|
} catch (error) {
|
|
2196
|
-
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
|
+
}
|
|
2197
2292
|
if (isNetworkFailure(error)) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${networkFailureDetail(error)}. Check TAIGA_API_URL.`);
|
|
2198
2293
|
throw error;
|
|
2199
2294
|
}
|
|
@@ -2374,6 +2469,24 @@ function budgetSections(doc, budget) {
|
|
|
2374
2469
|
}
|
|
2375
2470
|
return cutTo(doc, sections, Object.fromEntries(kept));
|
|
2376
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
|
+
}
|
|
2377
2490
|
//#endregion
|
|
2378
2491
|
//#region src/tools/summarize.ts
|
|
2379
2492
|
/**
|
|
@@ -2419,6 +2532,44 @@ function itemRows(items, fields) {
|
|
|
2419
2532
|
criteria: criteriaCounts(items[index])
|
|
2420
2533
|
} : row);
|
|
2421
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.";
|
|
2422
2573
|
//#endregion
|
|
2423
2574
|
//#region src/tools/products.ts
|
|
2424
2575
|
/** What a product row needs to say for the model to pick one and read it in full. */
|
|
@@ -2428,9 +2579,10 @@ const PRODUCT_SUMMARY_FIELDS = [
|
|
|
2428
2579
|
"slug",
|
|
2429
2580
|
"status",
|
|
2430
2581
|
"factoryId",
|
|
2431
|
-
"kind",
|
|
2432
2582
|
"discoveryStep"
|
|
2433
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`);
|
|
2434
2586
|
/**
|
|
2435
2587
|
* What a document row needs to say for the model to pick one and read it in
|
|
2436
2588
|
* full: which document, on which timeline, how current. Not the derived-from
|
|
@@ -2577,7 +2729,7 @@ async function resolveProductRef(client, ref, accepted, usage) {
|
|
|
2577
2729
|
].filter((v) => v !== void 0).length;
|
|
2578
2730
|
if (given !== 1) {
|
|
2579
2731
|
const how = given === 0 ? usage ?? "taiga_list_products shows what exists." : "Two names could mean two products.";
|
|
2580
|
-
return refusal("
|
|
2732
|
+
return refusal("VALIDATION_ERROR", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
|
|
2581
2733
|
}
|
|
2582
2734
|
if (ref.productId) try {
|
|
2583
2735
|
return { product: (await client.getProduct(ref.productId)).data };
|
|
@@ -2606,30 +2758,59 @@ async function resolveProductRef(client, ref, accepted, usage) {
|
|
|
2606
2758
|
function registerProductTools(server, client) {
|
|
2607
2759
|
if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
|
|
2608
2760
|
title: "List factories",
|
|
2609
|
-
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.",
|
|
2610
|
-
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
|
+
}),
|
|
2611
2766
|
annotations: READ_ONLY
|
|
2612
2767
|
}, (params) => runTool(async () => {
|
|
2613
|
-
|
|
2614
|
-
|
|
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
|
+
});
|
|
2615
2781
|
}));
|
|
2616
2782
|
if (client.hasCapability("products:read")) {
|
|
2617
2783
|
server.registerTool("taiga_list_products", {
|
|
2618
2784
|
title: "List products",
|
|
2619
|
-
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}`,
|
|
2620
2786
|
inputSchema: z.object({
|
|
2621
|
-
|
|
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"),
|
|
2622
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."),
|
|
2623
2790
|
...pagination
|
|
2624
2791
|
}),
|
|
2625
2792
|
annotations: READ_ONLY
|
|
2626
2793
|
}, (params) => runTool(async () => {
|
|
2627
|
-
const
|
|
2628
|
-
|
|
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
|
+
});
|
|
2629
2810
|
}));
|
|
2630
2811
|
server.registerTool("taiga_get_product", {
|
|
2631
2812
|
title: "Get product",
|
|
2632
|
-
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.",
|
|
2633
2814
|
inputSchema: z.object({
|
|
2634
2815
|
productId: productId.optional(),
|
|
2635
2816
|
slug: slug.optional()
|
|
@@ -2637,7 +2818,12 @@ function registerProductTools(server, client) {
|
|
|
2637
2818
|
annotations: READ_ONLY
|
|
2638
2819
|
}, (params) => runTool(async () => {
|
|
2639
2820
|
const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
|
|
2640
|
-
|
|
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);
|
|
2641
2827
|
}));
|
|
2642
2828
|
server.registerTool("taiga_resolve_product", {
|
|
2643
2829
|
title: "Resolve product from git remote",
|
|
@@ -2667,7 +2853,7 @@ function registerProductTools(server, client) {
|
|
|
2667
2853
|
}));
|
|
2668
2854
|
server.registerTool("taiga_get_product_document", {
|
|
2669
2855
|
title: "Get product document",
|
|
2670
|
-
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.",
|
|
2671
2857
|
inputSchema: z.object({
|
|
2672
2858
|
productId,
|
|
2673
2859
|
type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type -- taiga_list_product_documents shows which ones the product has"),
|
|
@@ -2682,15 +2868,7 @@ function registerProductTools(server, client) {
|
|
|
2682
2868
|
try {
|
|
2683
2869
|
const doc = stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data);
|
|
2684
2870
|
if (!params.sections) return toolResult(doc);
|
|
2685
|
-
|
|
2686
|
-
if (!cut) return toolError("VALIDATION_FAILED", `The ${origin} ${params.type} is not made of sections; read it without sections.`);
|
|
2687
|
-
const all = Object.keys(doc.data.sections);
|
|
2688
|
-
const unknown = params.sections.filter((id) => !all.includes(id));
|
|
2689
|
-
if (unknown.length > 0) return toolError("NOT_FOUND", `The ${origin} ${params.type} has no section ${unknown.join(", ")}; its sections are ${all.join(", ")}.`);
|
|
2690
|
-
return toolResult({
|
|
2691
|
-
...cut.doc,
|
|
2692
|
-
omittedSections: cut.omittedSections
|
|
2693
|
-
});
|
|
2871
|
+
return sectionReadResult(doc, params.sections, `${origin} ${params.type}`);
|
|
2694
2872
|
} catch (error) {
|
|
2695
2873
|
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
2696
2874
|
product ??= (await client.getProduct(params.productId)).data;
|
|
@@ -2812,12 +2990,16 @@ function registerDocumentTools(server, client) {
|
|
|
2812
2990
|
}));
|
|
2813
2991
|
server.registerTool("taiga_get_document", {
|
|
2814
2992
|
title: "Get company document",
|
|
2815
|
-
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.",
|
|
2816
|
-
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
|
+
}),
|
|
2817
2998
|
annotations: READ_ONLY
|
|
2818
2999
|
}, (params) => runTool(async () => {
|
|
2819
3000
|
try {
|
|
2820
|
-
|
|
3001
|
+
const doc = stripProvenance((await client.getTenantDocument(params.type)).data);
|
|
3002
|
+
return params.sections ? sectionReadResult(doc, params.sections, params.type) : toolResult(doc);
|
|
2821
3003
|
} catch (error) {
|
|
2822
3004
|
if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
|
|
2823
3005
|
const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(params.type), client.listTenantDocuments({ limit: 100 })]);
|
|
@@ -2861,7 +3043,7 @@ function registerDocumentTools(server, client) {
|
|
|
2861
3043
|
title: "List factory documents",
|
|
2862
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.`,
|
|
2863
3045
|
inputSchema: z.object({
|
|
2864
|
-
factoryId:
|
|
3046
|
+
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
|
|
2865
3047
|
...pagination
|
|
2866
3048
|
}),
|
|
2867
3049
|
annotations: READ_ONLY
|
|
@@ -2873,7 +3055,7 @@ function registerDocumentTools(server, client) {
|
|
|
2873
3055
|
title: "Get factory instructions",
|
|
2874
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.",
|
|
2875
3057
|
inputSchema: z.object({
|
|
2876
|
-
factoryId:
|
|
3058
|
+
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
|
|
2877
3059
|
type: z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type")
|
|
2878
3060
|
}),
|
|
2879
3061
|
annotations: READ_ONLY
|
|
@@ -2898,7 +3080,7 @@ function registerDocumentTools(server, client) {
|
|
|
2898
3080
|
saveDraft: (data) => client.saveFactoryDocumentDraft(factoryId, type, { data }),
|
|
2899
3081
|
publish: () => client.publishFactoryDocument(factoryId, type)
|
|
2900
3082
|
});
|
|
2901
|
-
const factoryId =
|
|
3083
|
+
const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
|
|
2902
3084
|
const factoryDocumentType = z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type");
|
|
2903
3085
|
server.registerTool("taiga_update_factory_document", {
|
|
2904
3086
|
title: "Update a section of factory instructions",
|
|
@@ -2993,7 +3175,7 @@ function registerJobTools(server, client) {
|
|
|
2993
3175
|
server.registerTool("taiga_get_job", {
|
|
2994
3176
|
title: "Get an agent job",
|
|
2995
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.",
|
|
2996
|
-
inputSchema: z.object({ jobId:
|
|
3178
|
+
inputSchema: z.object({ jobId: idInput("job", "the tool that started the job returned it") }),
|
|
2997
3179
|
annotations: READ_ONLY
|
|
2998
3180
|
}, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
|
|
2999
3181
|
}
|
|
@@ -3214,7 +3396,8 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
3214
3396
|
"archivedAt",
|
|
3215
3397
|
"lifecycle"
|
|
3216
3398
|
];
|
|
3217
|
-
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");
|
|
3218
3401
|
/** The plan without its steps' prose: enough to approve it or say what a run will do. */
|
|
3219
3402
|
const PLAN_SUMMARY_FIELDS = [
|
|
3220
3403
|
"id",
|
|
@@ -3274,9 +3457,10 @@ function registerBacklogTools(server, client) {
|
|
|
3274
3457
|
function registerBacklogReads(server, client) {
|
|
3275
3458
|
server.registerTool("taiga_list_backlog_items", {
|
|
3276
3459
|
title: "List backlog items",
|
|
3277
|
-
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.",
|
|
3278
3461
|
inputSchema: z.object({
|
|
3279
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"),
|
|
3280
3464
|
status: z.enum([
|
|
3281
3465
|
"backlog",
|
|
3282
3466
|
"in_progress",
|
|
@@ -3300,13 +3484,26 @@ function registerBacklogReads(server, client) {
|
|
|
3300
3484
|
}),
|
|
3301
3485
|
annotations: READ_ONLY
|
|
3302
3486
|
}, (params) => runTool(async () => {
|
|
3303
|
-
const
|
|
3304
|
-
|
|
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
|
+
});
|
|
3305
3502
|
}));
|
|
3306
3503
|
server.registerTool("taiga_get_backlog_item", {
|
|
3307
3504
|
title: "Get backlog item",
|
|
3308
|
-
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.",
|
|
3309
|
-
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 }),
|
|
3310
3507
|
annotations: READ_ONLY
|
|
3311
3508
|
}, (params) => runTool(async () => {
|
|
3312
3509
|
return toolResult((await client.getBacklogItem(params.itemId)).data);
|
|
@@ -3357,7 +3554,7 @@ function registerMove(server, client) {
|
|
|
3357
3554
|
inputSchema: z.object({
|
|
3358
3555
|
itemId,
|
|
3359
3556
|
state: z.enum(USER_DROP_STATES).describe("The column to move into"),
|
|
3360
|
-
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"),
|
|
3361
3558
|
archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
|
|
3362
3559
|
}),
|
|
3363
3560
|
annotations: OPERATE
|
|
@@ -3434,7 +3631,7 @@ function registerQueue(server, client) {
|
|
|
3434
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.",
|
|
3435
3632
|
inputSchema: z.object({
|
|
3436
3633
|
itemId,
|
|
3437
|
-
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"),
|
|
3438
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")
|
|
3439
3636
|
}),
|
|
3440
3637
|
annotations: OPERATE
|
|
@@ -3476,7 +3673,7 @@ function registerReviewGate(server, client) {
|
|
|
3476
3673
|
function registerResume(server, client) {
|
|
3477
3674
|
server.registerTool("taiga_resume_backlog_item", {
|
|
3478
3675
|
title: "Resume backlog item",
|
|
3479
|
-
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.",
|
|
3480
3677
|
inputSchema: z.object({ itemId }),
|
|
3481
3678
|
annotations: OPERATE
|
|
3482
3679
|
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
|
|
@@ -3485,14 +3682,14 @@ function registerPlanVerdicts(server, client) {
|
|
|
3485
3682
|
server.registerTool("taiga_approve_plan", {
|
|
3486
3683
|
title: "Approve a plan",
|
|
3487
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.",
|
|
3488
|
-
inputSchema: z.object({ planId
|
|
3685
|
+
inputSchema: z.object({ planId }),
|
|
3489
3686
|
annotations: OPERATE
|
|
3490
3687
|
}, (params) => runTool(async () => outcome((await client.approvePlan(params.planId)).data)));
|
|
3491
3688
|
server.registerTool("taiga_reject_plan", {
|
|
3492
3689
|
title: "Reject a plan",
|
|
3493
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.",
|
|
3494
3691
|
inputSchema: z.object({
|
|
3495
|
-
planId
|
|
3692
|
+
planId,
|
|
3496
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")
|
|
3497
3694
|
}),
|
|
3498
3695
|
annotations: OPERATE
|
|
@@ -3538,9 +3735,24 @@ const RUN_OUTCOME_FIELDS = [
|
|
|
3538
3735
|
"prUrl",
|
|
3539
3736
|
"actions"
|
|
3540
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
|
+
}
|
|
3541
3753
|
/** How many products the org-wide list names; past this a row keeps its productId only. */
|
|
3542
3754
|
const NAMED_PRODUCTS = 100;
|
|
3543
|
-
const buildId =
|
|
3755
|
+
const buildId = idInput("build run", "taiga_list_builds gives it");
|
|
3544
3756
|
/** A step without its instructions: what it is, how it stands and what it changed. */
|
|
3545
3757
|
const STEP_SUMMARY_FIELDS = [
|
|
3546
3758
|
"stepOrder",
|
|
@@ -3601,14 +3813,14 @@ function registerBuildTools(server, client) {
|
|
|
3601
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.",
|
|
3602
3814
|
inputSchema: z.object({
|
|
3603
3815
|
productId: productId.optional(),
|
|
3604
|
-
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"),
|
|
3605
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"),
|
|
3606
3818
|
...pagination
|
|
3607
3819
|
}),
|
|
3608
3820
|
annotations: READ_ONLY
|
|
3609
3821
|
}, (params) => runTool(async () => {
|
|
3610
|
-
if (!params.productId && params.backlogItemId) return toolError("
|
|
3611
|
-
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.");
|
|
3612
3824
|
if (params.productId) {
|
|
3613
3825
|
const result = await client.listBuildRuns(params.productId, params);
|
|
3614
3826
|
return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
|
|
@@ -3617,7 +3829,7 @@ function registerBuildTools(server, client) {
|
|
|
3617
3829
|
}));
|
|
3618
3830
|
server.registerTool("taiga_get_build", {
|
|
3619
3831
|
title: "Get build run",
|
|
3620
|
-
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.",
|
|
3621
3833
|
inputSchema: z.object({
|
|
3622
3834
|
buildId,
|
|
3623
3835
|
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
@@ -3625,13 +3837,18 @@ function registerBuildTools(server, client) {
|
|
|
3625
3837
|
annotations: READ_ONLY
|
|
3626
3838
|
}, (params) => runTool(async () => {
|
|
3627
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 } : {};
|
|
3628
3844
|
if (params.full) return toolResult({
|
|
3629
|
-
...
|
|
3845
|
+
...runRow,
|
|
3846
|
+
...stop,
|
|
3630
3847
|
steps: steps.data
|
|
3631
3848
|
});
|
|
3632
|
-
const rows = steps.data;
|
|
3633
3849
|
return toolResult({
|
|
3634
|
-
...
|
|
3850
|
+
...runRow,
|
|
3851
|
+
...stop,
|
|
3635
3852
|
steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
|
|
3636
3853
|
...step,
|
|
3637
3854
|
headline: summaryHeadline(rows[index].summary)
|
|
@@ -3922,10 +4139,12 @@ async function productContext(client, product, options) {
|
|
|
3922
4139
|
};
|
|
3923
4140
|
const parts = Object.keys(fetchers).filter(granted);
|
|
3924
4141
|
const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
|
|
4142
|
+
const nextStep = setupNextStep(product);
|
|
3925
4143
|
const bundle = { product: {
|
|
3926
4144
|
...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
|
|
3927
4145
|
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
3928
|
-
defaultOrigin: defaultOriginFor(product)
|
|
4146
|
+
defaultOrigin: defaultOriginFor(product),
|
|
4147
|
+
...nextStep ? { nextStep } : {}
|
|
3929
4148
|
} };
|
|
3930
4149
|
const unavailable = {};
|
|
3931
4150
|
parts.forEach((part, index) => {
|
|
@@ -3938,7 +4157,7 @@ async function productContext(client, product, options) {
|
|
|
3938
4157
|
...bundle,
|
|
3939
4158
|
notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
|
|
3940
4159
|
unavailable,
|
|
3941
|
-
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."
|
|
3942
4161
|
};
|
|
3943
4162
|
let characters = JSON.stringify(result, null, 2).length;
|
|
3944
4163
|
for (let measured = -1; measured !== characters;) {
|
|
@@ -3989,7 +4208,7 @@ function registerAutonomyTools(server, client) {
|
|
|
3989
4208
|
}),
|
|
3990
4209
|
annotations: OPERATE_SETTING
|
|
3991
4210
|
}, (params) => runTool(async () => {
|
|
3992
|
-
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.");
|
|
3993
4212
|
const autoMerge = params.autoMerge === true ? null : params.autoMerge;
|
|
3994
4213
|
const product = (await client.updateProduct(params.productId, {
|
|
3995
4214
|
...params.autoBuild !== void 0 ? { autoBuild: params.autoBuild } : {},
|
|
@@ -4007,7 +4226,7 @@ function registerAutonomyTools(server, client) {
|
|
|
4007
4226
|
title: "Set whether an initiative's pull request merges on its own",
|
|
4008
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.",
|
|
4009
4228
|
inputSchema: z.object({
|
|
4010
|
-
itemId:
|
|
4229
|
+
itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
|
|
4011
4230
|
autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
|
|
4012
4231
|
}),
|
|
4013
4232
|
annotations: OPERATE_SETTING
|
|
@@ -4027,6 +4246,7 @@ function registerAutonomyTools(server, client) {
|
|
|
4027
4246
|
const ITEM_FIELDS = [
|
|
4028
4247
|
"id",
|
|
4029
4248
|
"productId",
|
|
4249
|
+
"productName",
|
|
4030
4250
|
"category",
|
|
4031
4251
|
"kind",
|
|
4032
4252
|
"subjectType",
|
|
@@ -4043,6 +4263,7 @@ const ITEM_FIELDS = [
|
|
|
4043
4263
|
const SETUP_TASK_FIELDS = [
|
|
4044
4264
|
"id",
|
|
4045
4265
|
"productId",
|
|
4266
|
+
"productName",
|
|
4046
4267
|
"category",
|
|
4047
4268
|
"phase",
|
|
4048
4269
|
"target",
|
|
@@ -4059,7 +4280,7 @@ function registerInboxTools(server, client) {
|
|
|
4059
4280
|
if (!client.hasCapability("products:read")) return;
|
|
4060
4281
|
server.registerTool("taiga_list_action_required", {
|
|
4061
4282
|
title: "List what needs a person",
|
|
4062
|
-
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.",
|
|
4063
4284
|
inputSchema: z.object({
|
|
4064
4285
|
productId: productId.optional(),
|
|
4065
4286
|
category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
|
|
@@ -4084,14 +4305,19 @@ function registerInboxTools(server, client) {
|
|
|
4084
4305
|
}));
|
|
4085
4306
|
server.registerTool("taiga_list_setup_tasks", {
|
|
4086
4307
|
title: "List setup tasks",
|
|
4087
|
-
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.",
|
|
4088
4309
|
inputSchema: z.object({
|
|
4089
4310
|
productId: productId.optional(),
|
|
4311
|
+
includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
|
|
4090
4312
|
...pagination
|
|
4091
4313
|
}),
|
|
4092
4314
|
annotations: READ_ONLY
|
|
4093
4315
|
}, (params) => runTool(async () => {
|
|
4094
|
-
const
|
|
4316
|
+
const { includeDone, ...rest } = params;
|
|
4317
|
+
const result = await client.listSetupTasks({
|
|
4318
|
+
...rest,
|
|
4319
|
+
...includeDone ? {} : { openOnly: true }
|
|
4320
|
+
});
|
|
4095
4321
|
return toolResult(page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor));
|
|
4096
4322
|
}));
|
|
4097
4323
|
if (!client.hasCapability("products:write")) return;
|
|
@@ -4099,7 +4325,7 @@ function registerInboxTools(server, client) {
|
|
|
4099
4325
|
title: "Resolve or snooze an Action Required item",
|
|
4100
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.",
|
|
4101
4327
|
inputSchema: z.object({
|
|
4102
|
-
itemId:
|
|
4328
|
+
itemId: idInput("Action Required item", "taiga_list_action_required gives it"),
|
|
4103
4329
|
status: z.enum([
|
|
4104
4330
|
"in_progress",
|
|
4105
4331
|
"done",
|
|
@@ -4109,7 +4335,7 @@ function registerInboxTools(server, client) {
|
|
|
4109
4335
|
}),
|
|
4110
4336
|
annotations: OPERATE
|
|
4111
4337
|
}, (params) => runTool(async () => {
|
|
4112
|
-
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.");
|
|
4113
4339
|
return toolResult(pickFields([(await client.updateInboxItem(params.itemId, {
|
|
4114
4340
|
...params.status !== void 0 ? { status: params.status } : {},
|
|
4115
4341
|
...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
|
|
@@ -4133,10 +4359,10 @@ const KNOWLEDGE_FIELDS = [
|
|
|
4133
4359
|
];
|
|
4134
4360
|
/** Past this many characters a text file's content is cut when read; the download URL has it all. */
|
|
4135
4361
|
const TEXT_LIMIT = 2e4;
|
|
4136
|
-
const knowledgeId =
|
|
4362
|
+
const knowledgeId = idInput("knowledge file", "taiga_list_knowledge gives it");
|
|
4137
4363
|
const scopeInputs = {
|
|
4138
|
-
factoryId:
|
|
4139
|
-
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")
|
|
4140
4366
|
};
|
|
4141
4367
|
/** Bytes of a text file the read takes at most; the download URL has the rest. */
|
|
4142
4368
|
const READ_BYTES = 262144;
|
|
@@ -4200,7 +4426,7 @@ function registerKnowledgeTools(server, client) {
|
|
|
4200
4426
|
annotations: READ_ONLY
|
|
4201
4427
|
}, (params) => runTool(async () => {
|
|
4202
4428
|
const twoLevels = oneLevel(params);
|
|
4203
|
-
if (twoLevels) return toolError("
|
|
4429
|
+
if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
|
|
4204
4430
|
const scope = params.productId ? { productId: params.productId } : params.factoryId ? {
|
|
4205
4431
|
factoryId: params.factoryId,
|
|
4206
4432
|
productId: null
|
|
@@ -4257,7 +4483,7 @@ function registerKnowledgeTools(server, client) {
|
|
|
4257
4483
|
annotations: OPERATE
|
|
4258
4484
|
}, (params) => runTool(async () => {
|
|
4259
4485
|
const twoLevels = oneLevel(params);
|
|
4260
|
-
if (twoLevels) return toolError("
|
|
4486
|
+
if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
|
|
4261
4487
|
const mime = params.fileName.toLowerCase().endsWith(".md") ? "text/markdown" : "text/plain";
|
|
4262
4488
|
const result = await client.addKnowledge({
|
|
4263
4489
|
fileName: params.fileName,
|
|
@@ -4343,7 +4569,7 @@ const VIOLATION_FIELDS = [
|
|
|
4343
4569
|
"verifyRequestedAt",
|
|
4344
4570
|
"assessedAt"
|
|
4345
4571
|
];
|
|
4346
|
-
const findingId =
|
|
4572
|
+
const findingId = idInput("finding", "taiga_list_findings gives it");
|
|
4347
4573
|
function registerMaintenanceTools(server, client) {
|
|
4348
4574
|
if (!client.hasCapability("products:read")) return;
|
|
4349
4575
|
server.registerTool("taiga_get_maintenance", {
|
|
@@ -4388,7 +4614,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4388
4614
|
}, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
|
|
4389
4615
|
server.registerTool("taiga_get_policy_standing", {
|
|
4390
4616
|
title: "Get a product's policy standing",
|
|
4391
|
-
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.",
|
|
4392
4618
|
inputSchema: z.object({
|
|
4393
4619
|
productId,
|
|
4394
4620
|
full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
|
|
@@ -4396,16 +4622,32 @@ function registerMaintenanceTools(server, client) {
|
|
|
4396
4622
|
annotations: READ_ONLY
|
|
4397
4623
|
}, (params) => runTool(async () => {
|
|
4398
4624
|
const standing = (await client.getPolicyStanding(params.productId)).data;
|
|
4399
|
-
if (params.full) return toolResult(standing);
|
|
4400
4625
|
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
4401
4626
|
const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
|
|
4402
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
|
+
});
|
|
4403
4644
|
return toolResult({
|
|
4645
|
+
summary,
|
|
4404
4646
|
productId: standing.productId,
|
|
4405
4647
|
hasPolicies: standing.hasPolicies,
|
|
4406
4648
|
assessment: standing.assessment,
|
|
4407
4649
|
documents: standing.documents,
|
|
4408
|
-
controls: pickFields(
|
|
4650
|
+
controls: pickFields(listed, [
|
|
4409
4651
|
"controlId",
|
|
4410
4652
|
"policyType",
|
|
4411
4653
|
"text",
|
|
@@ -4414,8 +4656,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4414
4656
|
"findingCount",
|
|
4415
4657
|
"plannedCount"
|
|
4416
4658
|
]),
|
|
4417
|
-
violations: pickFields(violations, VIOLATION_FIELDS)
|
|
4418
|
-
resolvedViolations: resolved
|
|
4659
|
+
violations: pickFields(violations, VIOLATION_FIELDS)
|
|
4419
4660
|
});
|
|
4420
4661
|
}));
|
|
4421
4662
|
if (!client.hasCapability("products:write")) return;
|
|
@@ -4431,7 +4672,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
4431
4672
|
}),
|
|
4432
4673
|
annotations: OPERATE
|
|
4433
4674
|
}, (params) => runTool(async () => {
|
|
4434
|
-
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).");
|
|
4435
4676
|
const body = params.ignored ? {
|
|
4436
4677
|
status: "ignored",
|
|
4437
4678
|
ignoreReason: params.reason,
|
|
@@ -4473,11 +4714,11 @@ function registerMaintenanceTools(server, client) {
|
|
|
4473
4714
|
inputSchema: z.object({
|
|
4474
4715
|
productId,
|
|
4475
4716
|
controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
|
|
4476
|
-
violationId:
|
|
4717
|
+
violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
|
|
4477
4718
|
}),
|
|
4478
4719
|
annotations: OPERATE
|
|
4479
4720
|
}, (params) => runTool(async () => {
|
|
4480
|
-
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.");
|
|
4481
4722
|
return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
|
|
4482
4723
|
}));
|
|
4483
4724
|
}
|
|
@@ -4511,7 +4752,7 @@ const ENVIRONMENT_FIELDS = [
|
|
|
4511
4752
|
"isProtected",
|
|
4512
4753
|
"url"
|
|
4513
4754
|
];
|
|
4514
|
-
const environmentId =
|
|
4755
|
+
const environmentId = idInput("environment", "taiga_get_deployments lists them; a product ID is not one");
|
|
4515
4756
|
const windowDays = (choices) => z.number().int().optional().describe(`Days to look back: ${choices} (default 30)`);
|
|
4516
4757
|
/** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
|
|
4517
4758
|
const isApiFailure = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
|
|
@@ -4570,6 +4811,7 @@ function registerObserveTools(server, client) {
|
|
|
4570
4811
|
part(() => client.getDeploymentSyncStatus(params.productId))
|
|
4571
4812
|
]);
|
|
4572
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;
|
|
4573
4815
|
return toolResult({
|
|
4574
4816
|
environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
|
|
4575
4817
|
...environments.more ? { moreEnvironments: true } : {},
|
|
@@ -4579,7 +4821,8 @@ function registerObserveTools(server, client) {
|
|
|
4579
4821
|
..."value" in sync ? { syncStatus: sync.value.data } : {
|
|
4580
4822
|
syncStatus: null,
|
|
4581
4823
|
syncUnavailable: sync.unavailable
|
|
4582
|
-
}
|
|
4824
|
+
},
|
|
4825
|
+
...note ? { note } : {}
|
|
4583
4826
|
});
|
|
4584
4827
|
}));
|
|
4585
4828
|
server.registerTool("taiga_get_delivery_metrics", {
|
|
@@ -4597,7 +4840,7 @@ function registerObserveTools(server, client) {
|
|
|
4597
4840
|
30,
|
|
4598
4841
|
60,
|
|
4599
4842
|
90
|
|
4600
|
-
].includes(days)) return toolError("
|
|
4843
|
+
].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90.");
|
|
4601
4844
|
return toolResult((await client.getDeliveryMetrics(params.productId, params.environmentId, days)).data);
|
|
4602
4845
|
}));
|
|
4603
4846
|
server.registerTool("taiga_get_monitoring", {
|
|
@@ -4611,22 +4854,32 @@ function registerObserveTools(server, client) {
|
|
|
4611
4854
|
annotations: READ_ONLY
|
|
4612
4855
|
}, (params) => runTool(async () => {
|
|
4613
4856
|
const health = await client.getMonitoringHealth(params.productId);
|
|
4614
|
-
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
|
+
});
|
|
4615
4861
|
const days = params.windowDays ?? 30;
|
|
4616
4862
|
if (![
|
|
4617
4863
|
30,
|
|
4618
4864
|
60,
|
|
4619
4865
|
90
|
|
4620
|
-
].includes(days)) return toolError("
|
|
4866
|
+
].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90, the windows the page offers.");
|
|
4621
4867
|
const env = params.environmentId;
|
|
4622
4868
|
const [uptime, experience, clientHealth] = await Promise.all([
|
|
4623
4869
|
part(() => client.getMonitoringSignal(params.productId, "uptime", env, days)),
|
|
4624
4870
|
part(() => client.getMonitoringSignal(params.productId, "rum", env, days)),
|
|
4625
4871
|
part(() => client.getMonitoringSignal(params.productId, "client-health", env, days))
|
|
4626
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"));
|
|
4627
4878
|
const value = (p) => "value" in p ? p.value.data : { unavailable: p.unavailable };
|
|
4879
|
+
const envHealth = health.data.filter((row) => row.environmentId === env);
|
|
4628
4880
|
return toolResult({
|
|
4629
|
-
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." } : {},
|
|
4630
4883
|
windowDays: days,
|
|
4631
4884
|
uptime: value(uptime),
|
|
4632
4885
|
realUserExperience: value(experience),
|
|
@@ -4662,7 +4915,7 @@ function registerObserveTools(server, client) {
|
|
|
4662
4915
|
annotations: OPERATE_SETTING
|
|
4663
4916
|
}, (params) => runTool(async () => {
|
|
4664
4917
|
const { environmentId: id, ...changes } = params;
|
|
4665
|
-
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.");
|
|
4666
4919
|
const updated = (await client.updateEnvironment(id, changes)).data;
|
|
4667
4920
|
return toolResult(pickFields([updated], ENVIRONMENT_FIELDS)[0]);
|
|
4668
4921
|
}));
|
|
@@ -19386,7 +19639,7 @@ function registerPrompts(server, client) {
|
|
|
19386
19639
|
if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_backlog_item", {
|
|
19387
19640
|
title: "Implement a backlog item",
|
|
19388
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.",
|
|
19389
|
-
argsSchema: z.object({ itemId:
|
|
19642
|
+
argsSchema: z.object({ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it") })
|
|
19390
19643
|
}, async (args) => {
|
|
19391
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;
|
|
19392
19645
|
const productId = String(item.productId);
|
|
@@ -19481,7 +19734,7 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
19481
19734
|
"description",
|
|
19482
19735
|
"language"
|
|
19483
19736
|
];
|
|
19484
|
-
const repositoryId =
|
|
19737
|
+
const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
|
|
19485
19738
|
function registerRepositoryTools(server, client) {
|
|
19486
19739
|
if (client.hasCapability("products:read")) {
|
|
19487
19740
|
server.registerTool("taiga_list_product_repositories", {
|
|
@@ -19513,7 +19766,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19513
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.",
|
|
19514
19767
|
inputSchema: z.object({
|
|
19515
19768
|
query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
|
|
19516
|
-
installationId:
|
|
19769
|
+
installationId: idInput("GitHub account", "this tool lists them").optional(),
|
|
19517
19770
|
...pagination
|
|
19518
19771
|
}),
|
|
19519
19772
|
annotations: READ_ONLY
|
|
@@ -19523,8 +19776,8 @@ function registerRepositoryTools(server, client) {
|
|
|
19523
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.");
|
|
19524
19777
|
const chosen = params.installationId ? installations.filter((i) => i.id === params.installationId) : installations;
|
|
19525
19778
|
if (chosen.length === 0) return toolError("NOT_FOUND", "No such GitHub connection; these are the connected accounts.", { accounts });
|
|
19526
|
-
if (params.cursor !== void 0 && params.query !== void 0) return toolError("
|
|
19527
|
-
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 });
|
|
19528
19781
|
const max = params.limit ?? 25;
|
|
19529
19782
|
const rows = (data) => pickFields(data, GITHUB_REPOSITORY_FIELDS);
|
|
19530
19783
|
if (chosen.length === 1) {
|
|
@@ -19558,7 +19811,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19558
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.",
|
|
19559
19812
|
inputSchema: z.object({
|
|
19560
19813
|
productId,
|
|
19561
|
-
githubRepositoryId:
|
|
19814
|
+
githubRepositoryId: idInput("GitHub repository", "taiga_list_linkable_repositories gives it"),
|
|
19562
19815
|
replacesRepositoryId: repositoryId.optional().describe("The link this one replaces")
|
|
19563
19816
|
}),
|
|
19564
19817
|
annotations: OPERATE
|
|
@@ -19579,7 +19832,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19579
19832
|
}),
|
|
19580
19833
|
annotations: OPERATE_SETTING
|
|
19581
19834
|
}, (params) => runTool(async () => {
|
|
19582
|
-
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.");
|
|
19583
19836
|
const updated = (await client.updateRepository(params.productId, params.repositoryId, {
|
|
19584
19837
|
...params.baseBranch !== void 0 ? { baseBranch: params.baseBranch } : {},
|
|
19585
19838
|
...params.label !== void 0 ? { label: params.label } : {}
|
|
@@ -19746,26 +19999,56 @@ const FACTORY_FIELDS = [
|
|
|
19746
19999
|
"slug",
|
|
19747
20000
|
"isDefault"
|
|
19748
20001
|
];
|
|
19749
|
-
const factoryId =
|
|
20002
|
+
const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
|
|
19750
20003
|
const buildScope = {
|
|
19751
20004
|
ownsInfrastructure: z.boolean().nullable().optional().describe("Whether Taiga writes the infrastructure code; null follows the organization"),
|
|
19752
20005
|
ownsDeployment: z.boolean().nullable().optional().describe("Whether Taiga writes the CI/CD pipelines; null follows the organization")
|
|
19753
20006
|
};
|
|
19754
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.";
|
|
19755
20010
|
/** A product row the model can act on, saying how it started as the other product tools say it. */
|
|
19756
|
-
function productRow(product) {
|
|
20011
|
+
async function productRow(client, product) {
|
|
19757
20012
|
const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
|
|
20013
|
+
const nextStep = setupNextStep(await withDiscoveryStep(client, product));
|
|
19758
20014
|
return {
|
|
19759
20015
|
...rest,
|
|
19760
|
-
projectType: isImportedProduct(product) ? "brownfield" : "greenfield"
|
|
20016
|
+
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
20017
|
+
...nextStep ? { nextStep } : {}
|
|
19761
20018
|
};
|
|
19762
20019
|
}
|
|
19763
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
|
+
/**
|
|
19764
20047
|
* The sentence is for a half Taiga does not build; the web asks for it only
|
|
19765
20048
|
* then. When both halves are decided on here, a sentence has nothing to say.
|
|
19766
20049
|
*/
|
|
19767
20050
|
function notesWithNothingToDescribe(params) {
|
|
19768
|
-
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;
|
|
19769
20052
|
}
|
|
19770
20053
|
function registerSetupTools(server, client) {
|
|
19771
20054
|
if (client.hasCapability("factories:write")) {
|
|
@@ -19808,7 +20091,7 @@ function registerSetupTools(server, client) {
|
|
|
19808
20091
|
...params.stopLine ? { stopLine: true } : {}
|
|
19809
20092
|
});
|
|
19810
20093
|
return toolResult({
|
|
19811
|
-
product: productRow(moved.data),
|
|
20094
|
+
product: await productRow(client, moved.data),
|
|
19812
20095
|
changed: moved.changed,
|
|
19813
20096
|
previousFactoryId: moved.previousFactoryId,
|
|
19814
20097
|
lineStopped: moved.lineStopped,
|
|
@@ -19839,7 +20122,7 @@ function registerSetupTools(server, client) {
|
|
|
19839
20122
|
...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
|
|
19840
20123
|
...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {}
|
|
19841
20124
|
})).data;
|
|
19842
|
-
return toolResult(productRow(product));
|
|
20125
|
+
return toolResult(await productRow(client, product));
|
|
19843
20126
|
}));
|
|
19844
20127
|
server.registerTool("taiga_update_product", {
|
|
19845
20128
|
title: "Change a product's settings",
|
|
@@ -19854,10 +20137,15 @@ function registerSetupTools(server, client) {
|
|
|
19854
20137
|
annotations: OPERATE_SETTING
|
|
19855
20138
|
}, (params) => runTool(async () => {
|
|
19856
20139
|
const { productId: id, ...changes } = params;
|
|
19857
|
-
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.");
|
|
19858
20141
|
const contradiction = notesWithNothingToDescribe(params);
|
|
19859
20142
|
if (contradiction) return contradiction;
|
|
19860
|
-
|
|
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
|
+
});
|
|
19861
20149
|
}));
|
|
19862
20150
|
server.registerTool("taiga_refresh_reality_documents", {
|
|
19863
20151
|
title: "Refresh a product's documents from its code",
|
|
@@ -19870,16 +20158,23 @@ function registerSetupTools(server, client) {
|
|
|
19870
20158
|
}, (params) => runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data)));
|
|
19871
20159
|
server.registerTool("taiga_finish_discovery", {
|
|
19872
20160
|
title: "Finish Discovery",
|
|
19873
|
-
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.",
|
|
19874
20162
|
inputSchema: z.object({ productId }),
|
|
19875
20163
|
annotations: OPERATE
|
|
19876
|
-
}, (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
|
+
}));
|
|
19877
20172
|
server.registerTool("taiga_reopen_discovery", {
|
|
19878
20173
|
title: "Reopen Discovery",
|
|
19879
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.",
|
|
19880
20175
|
inputSchema: z.object({ productId }),
|
|
19881
20176
|
annotations: OPERATE
|
|
19882
|
-
}, (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))));
|
|
19883
20178
|
if (!client.hasCapability("products:read")) return;
|
|
19884
20179
|
server.registerTool("taiga_start_import", {
|
|
19885
20180
|
title: "Import a codebase into a product",
|
|
@@ -20159,7 +20454,7 @@ function registerWaitTools(server, client, options) {
|
|
|
20159
20454
|
title: "Wait for an initiative",
|
|
20160
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.",
|
|
20161
20456
|
inputSchema: z.object({
|
|
20162
|
-
itemId:
|
|
20457
|
+
itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
|
|
20163
20458
|
until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
|
|
20164
20459
|
timeoutSeconds
|
|
20165
20460
|
}),
|
|
@@ -20192,7 +20487,7 @@ function registerWaitTools(server, client, options) {
|
|
|
20192
20487
|
title: "Wait for a run",
|
|
20193
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.",
|
|
20194
20489
|
inputSchema: z.object({
|
|
20195
|
-
buildId:
|
|
20490
|
+
buildId: idInput("build run", "taiga_list_builds gives it"),
|
|
20196
20491
|
until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
|
|
20197
20492
|
timeoutSeconds
|
|
20198
20493
|
}),
|
|
@@ -20213,7 +20508,7 @@ function registerWaitTools(server, client, options) {
|
|
|
20213
20508
|
title: "Wait for an agent job",
|
|
20214
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.",
|
|
20215
20510
|
inputSchema: z.object({
|
|
20216
|
-
jobId:
|
|
20511
|
+
jobId: idInput("job", "the tool that started the job returned it"),
|
|
20217
20512
|
timeoutSeconds
|
|
20218
20513
|
}),
|
|
20219
20514
|
annotations: READ_ONLY
|
|
@@ -20232,7 +20527,8 @@ function registerWaitTools(server, client, options) {
|
|
|
20232
20527
|
//#endregion
|
|
20233
20528
|
//#region src/tools/index.ts
|
|
20234
20529
|
/**
|
|
20235
|
-
* 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
|
|
20236
20532
|
* strips one silently: a review note passed to a tool with no place for it
|
|
20237
20533
|
* vanished and the call still succeeded, and a misspelt option quietly
|
|
20238
20534
|
* became its default. Strict, the call fails with "Unrecognized key" and the
|
|
@@ -20244,10 +20540,12 @@ function withStrictInputs(server) {
|
|
|
20244
20540
|
server.registerTool = ((name, config, callback) => {
|
|
20245
20541
|
const schema = config.inputSchema;
|
|
20246
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));
|
|
20247
20545
|
return register(name, schema ? {
|
|
20248
20546
|
...config,
|
|
20249
20547
|
inputSchema: schema.strict()
|
|
20250
|
-
} : config,
|
|
20548
|
+
} : config, withHint);
|
|
20251
20549
|
});
|
|
20252
20550
|
return server;
|
|
20253
20551
|
}
|
|
@@ -20277,17 +20575,30 @@ function registerAllTools(server, client, options = {}) {
|
|
|
20277
20575
|
registerResources(server, client);
|
|
20278
20576
|
registerPrompts(server, client);
|
|
20279
20577
|
}
|
|
20280
|
-
//#endregion
|
|
20281
|
-
//#region src/tools/instructions.ts
|
|
20282
|
-
/**
|
|
20283
|
-
* What the server tells a client at connect time: the knowledge base, the
|
|
20284
|
-
* order to read it in, the two document timelines, what section metadata
|
|
20285
|
-
* means, and when to write. Kept as data so a test can hold it to the
|
|
20286
|
-
* catalogue the same way it holds the tool descriptions.
|
|
20287
|
-
*/
|
|
20288
20578
|
const SERVER_INSTRUCTIONS = [
|
|
20289
|
-
|
|
20290
|
-
|
|
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"),
|
|
20291
20602
|
"",
|
|
20292
20603
|
"**Company documents** (taiga_list_documents / taiga_get_document):",
|
|
20293
20604
|
`The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
|
|
@@ -20381,15 +20692,9 @@ const SERVER_INSTRUCTIONS = [
|
|
|
20381
20692
|
"`priority: core` marks the sections every organization keeps. Neither changes what the section",
|
|
20382
20693
|
"requires of you.",
|
|
20383
20694
|
"",
|
|
20384
|
-
"
|
|
20385
|
-
"
|
|
20386
|
-
"
|
|
20387
|
-
"taiga_get_product_document to read by section), every policy as its section ids, the open backlog",
|
|
20388
|
-
"items, the repositories and the latest runs in one call, and names any part it could not read. Then read",
|
|
20389
|
-
"what you need in full: a policy with taiga_get_document, a backlog item with taiga_get_backlog_item.",
|
|
20390
|
-
"Without it, use taiga_list_factories and taiga_list_products, then read the factory instructions,",
|
|
20391
|
-
"the product's specification and architecture, and the backlog item you are working on. A document",
|
|
20392
|
-
"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.",
|
|
20393
20698
|
"",
|
|
20394
20699
|
"The same documents are resources (taiga://documents/{type}, taiga://products/{productId}/documents/{type},",
|
|
20395
20700
|
"taiga://factories/{factoryId}/instructions, taiga://backlog/{itemId}) for clients that attach them, and three",
|