@taiga-ai/mcp-server 0.18.0-dev.450 → 0.19.0-dev.451
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1036 -628
- package/package.json +1 -1
- package/src/client.ts +11 -0
- package/src/modern.integration.test.ts +1 -1
- package/src/server.integration.test.ts +767 -232
- package/src/tools/autonomy.ts +2 -2
- package/src/tools/backlogs.ts +164 -51
- package/src/tools/builds.ts +134 -47
- package/src/tools/catalogue.test.ts +23 -17
- package/src/tools/context.ts +98 -30
- package/src/tools/documents.ts +4 -1
- package/src/tools/errors.ts +40 -2
- package/src/tools/hints.ts +34 -9
- package/src/tools/inbox.ts +151 -28
- package/src/tools/index.ts +15 -1
- package/src/tools/instructions.ts +32 -154
- package/src/tools/maintenance.ts +44 -18
- package/src/tools/product-documents.ts +1 -1
- package/src/tools/products.ts +7 -5
- package/src/tools/prompts.ts +7 -7
- package/src/tools/queue.ts +12 -12
- package/src/tools/readiness.ts +172 -0
- package/src/tools/repositories.ts +4 -1
- package/src/tools/resources.ts +6 -6
- package/src/tools/sections.ts +13 -1
- package/src/tools/setup.ts +30 -30
- package/src/tools/wait.ts +13 -7
- package/taiga-ai-mcp-server-0.19.0-dev.451.tgz +0 -0
- package/taiga-ai-mcp-server-0.18.0-dev.450.tgz +0 -0
package/dist/index.js
CHANGED
|
@@ -480,6 +480,10 @@ var TaigaClient = class {
|
|
|
480
480
|
async refreshRealityDocuments(productId, docTypes) {
|
|
481
481
|
return this.request("POST", `/products/${seg(productId)}/documents/refresh-reality`, docTypes ? { docTypes } : void 0);
|
|
482
482
|
}
|
|
483
|
+
/** What still blocks finishing setup: required documents unpublished, required checks unstamped (PRO-2800). */
|
|
484
|
+
async getProductSetupBlockers(productId) {
|
|
485
|
+
return this.request("GET", `/products/${seg(productId)}/setup`);
|
|
486
|
+
}
|
|
483
487
|
async completeSetup(productId) {
|
|
484
488
|
return this.request("POST", `/products/${seg(productId)}/setup/complete`);
|
|
485
489
|
}
|
|
@@ -1253,6 +1257,40 @@ function runDisposition(status) {
|
|
|
1253
1257
|
return RUN_DISPOSITION[status];
|
|
1254
1258
|
}
|
|
1255
1259
|
/**
|
|
1260
|
+
* How the run stands, WITH the fact that decides what 'completed' meant.
|
|
1261
|
+
*
|
|
1262
|
+
* `completed` WAS one word for two outcomes, and the difference is the whole
|
|
1263
|
+
* question for anything that waits on a merge:
|
|
1264
|
+
*
|
|
1265
|
+
* - the pull request merged (`completeMergedRun`, from the merge webhook),
|
|
1266
|
+
* which is the run succeeding; or
|
|
1267
|
+
* - the review loop converged with nothing left to push, which settled the
|
|
1268
|
+
* run on a pull request that was still open, possibly failing, and
|
|
1269
|
+
* definitely not in main.
|
|
1270
|
+
*
|
|
1271
|
+
* The second outcome is no longer produced (PRO-1131): a review never settles a
|
|
1272
|
+
* run terminal now, so `completed` is written only by `completeMergedRun` and
|
|
1273
|
+
* means merged. This function is NOT therefore redundant -- runs settled under
|
|
1274
|
+
* the old rule are still in the table, unmigrated, and keep their status. It
|
|
1275
|
+
* remains the only honest answer to "did this land" for any row, whenever it
|
|
1276
|
+
* was written.
|
|
1277
|
+
*
|
|
1278
|
+
* `runDisposition` cannot tell them apart -- it only has the status -- so it
|
|
1279
|
+
* calls both 'succeeded'. Every caller that actually means "did this land"
|
|
1280
|
+
* must use THIS function instead, which is why it lives next to the map rather
|
|
1281
|
+
* than in the one module that first needed it.
|
|
1282
|
+
*
|
|
1283
|
+
* `prMerged` is a monotonic latch on the run row (a pull request cannot
|
|
1284
|
+
* un-merge), and the merge webhook writes it BEFORE it writes the status, so
|
|
1285
|
+
* 'completed' with `prMerged: false` is never a window -- it is the second
|
|
1286
|
+
* outcome, every time.
|
|
1287
|
+
*/
|
|
1288
|
+
function dispositionOf(status, prMerged) {
|
|
1289
|
+
const disposition = runDisposition(status);
|
|
1290
|
+
if (disposition === "succeeded" && !prMerged) return "unmerged";
|
|
1291
|
+
return disposition;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1256
1294
|
* Statuses where the coding agent (or review worker) is actively executing.
|
|
1257
1295
|
* Single source of truth shared by the tenant build feed's `active=true` filter
|
|
1258
1296
|
* (api) and the dashboard's "running" detection (web), which must agree.
|
|
@@ -2074,7 +2112,7 @@ function productDocumentName(type) {
|
|
|
2074
2112
|
/** The organization documents that are not policies: goals, standards, voice, instructions. */
|
|
2075
2113
|
const OTHER_TENANT_DOCUMENT_TYPES = TENANT_DOCUMENT_TYPES.filter((type) => !POLICY_TYPES.includes(type));
|
|
2076
2114
|
/** `a, b, c` for a description. */
|
|
2077
|
-
function named(types) {
|
|
2115
|
+
function named$1(types) {
|
|
2078
2116
|
return types.join(", ");
|
|
2079
2117
|
}
|
|
2080
2118
|
/**
|
|
@@ -2165,15 +2203,14 @@ const DISCOVERY_STEP_MEANING = "discoveryStep is where a product in setup is: co
|
|
|
2165
2203
|
//#region src/tools/hints.ts
|
|
2166
2204
|
/**
|
|
2167
2205
|
* The next step after the API answers 404. The API's own word is bare
|
|
2168
|
-
* ("
|
|
2206
|
+
* ("Initiative not found", "Repository link not found"), and an agent that
|
|
2169
2207
|
* hears only that guesses another ID, or a name. Each ID a tool takes names
|
|
2170
2208
|
* the tool that lists the right ones; the tool's arguments say which apply.
|
|
2171
2209
|
*/
|
|
2172
|
-
const ITEM_ID_HINT = "
|
|
2210
|
+
const ITEM_ID_HINT = "taiga_list_initiatives lists a product's initiatives with their IDs; a run or plan ID is not one.";
|
|
2173
2211
|
const BY_ARGUMENT = {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
buildId: "taiga_list_builds lists the runs, of one product or of every product the key can see.",
|
|
2212
|
+
initiativeId: ITEM_ID_HINT,
|
|
2213
|
+
runId: "taiga_list_runs lists the runs, of one product or of every product the key can see.",
|
|
2177
2214
|
planId: "taiga_get_plan gives the initiative's active plan and its ID.",
|
|
2178
2215
|
repositoryId: "taiga_list_product_repositories lists the product's repository links; a GitHub repository id is not a link ID.",
|
|
2179
2216
|
replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
|
|
@@ -2190,17 +2227,41 @@ const BY_ARGUMENT = {
|
|
|
2190
2227
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2191
2228
|
const BY_TOOL = {
|
|
2192
2229
|
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2193
|
-
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "
|
|
2230
|
+
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one." : ITEM_ID_HINT
|
|
2231
|
+
};
|
|
2232
|
+
/**
|
|
2233
|
+
* What the API calls the thing each ID names, as its 404 says it ("Environment
|
|
2234
|
+
* not found"). A tool given a right product and a wrong environment hinted
|
|
2235
|
+
* at both, and the product's hint told the agent its correct product ID was
|
|
2236
|
+
* the mistake (PRO-2800): a 404 that names its thing gets that ID's hint only.
|
|
2237
|
+
*/
|
|
2238
|
+
const MISSING_THING = {
|
|
2239
|
+
initiativeId: /backlog item|initiative/i,
|
|
2240
|
+
runId: /\b(build )?run\b/i,
|
|
2241
|
+
planId: /\bplan\b/i,
|
|
2242
|
+
repositoryId: /repository link|\blink\b/i,
|
|
2243
|
+
replacesRepositoryId: /repository link|\blink\b/i,
|
|
2244
|
+
githubRepositoryId: /github repository/i,
|
|
2245
|
+
installationId: /installation|account/i,
|
|
2246
|
+
jobId: /\bjob\b/i,
|
|
2247
|
+
factoryId: /factory|team/i,
|
|
2248
|
+
findingId: /finding/i,
|
|
2249
|
+
violationId: /violation/i,
|
|
2250
|
+
environmentId: /environment/i,
|
|
2251
|
+
knowledgeId: /knowledge|document/i,
|
|
2252
|
+
productId: /\b(product|project) not found/i
|
|
2194
2253
|
};
|
|
2195
|
-
/** The hint for a 404 from one tool call: the tool's own, else one
|
|
2254
|
+
/** The hint for a 404 from one tool call: the tool's own, else the one for the ID the API says is missing. */
|
|
2196
2255
|
function notFoundHint(tool, args) {
|
|
2197
2256
|
if (BY_TOOL[tool]) return BY_TOOL[tool];
|
|
2198
2257
|
if (typeof args !== "object" || args === null) return void 0;
|
|
2199
2258
|
const given = Object.keys(args).filter((key) => args[key] != null && BY_ARGUMENT[key]);
|
|
2200
2259
|
given.sort((a, b) => Number(a === "productId") - Number(b === "productId"));
|
|
2201
2260
|
if (given.length === 0) return void 0;
|
|
2202
|
-
|
|
2203
|
-
|
|
2261
|
+
return (said) => {
|
|
2262
|
+
const named = given.filter((key) => MISSING_THING[key]?.test(said));
|
|
2263
|
+
return (named.length > 0 ? named : given).map((key) => BY_ARGUMENT[key]).join(" ");
|
|
2264
|
+
};
|
|
2204
2265
|
}
|
|
2205
2266
|
/** The hint of the tool call in progress, read by runTool when the API answers 404. */
|
|
2206
2267
|
const notFoundHints = new AsyncLocalStorage();
|
|
@@ -2215,11 +2276,45 @@ function withStop(text) {
|
|
|
2215
2276
|
}
|
|
2216
2277
|
//#endregion
|
|
2217
2278
|
//#region src/tools/errors.ts
|
|
2279
|
+
/**
|
|
2280
|
+
* The API's names for the two IDs an agent chains most, in the product's
|
|
2281
|
+
* words the tools take them in (PRO-2800): a row says initiativeId and runId,
|
|
2282
|
+
* as taiga_get_initiative and taiga_get_run ask for them, rather than the
|
|
2283
|
+
* storage names backlogItemId and buildRunId.
|
|
2284
|
+
*/
|
|
2285
|
+
const PRODUCT_WORDS = {
|
|
2286
|
+
backlogItemId: "initiativeId",
|
|
2287
|
+
buildRunId: "runId"
|
|
2288
|
+
};
|
|
2289
|
+
/**
|
|
2290
|
+
* Maps whose keys are content, not API fields: a document's sections are keyed
|
|
2291
|
+
* by ids a person or agent chose (a section may be called backlogItemId), and
|
|
2292
|
+
* metadata and settings hold whatever their writer put there. Copied as they
|
|
2293
|
+
* are, keys and all.
|
|
2294
|
+
*/
|
|
2295
|
+
const OPAQUE_MAPS = /* @__PURE__ */ new Set([
|
|
2296
|
+
"sections",
|
|
2297
|
+
"metadata",
|
|
2298
|
+
"settings"
|
|
2299
|
+
]);
|
|
2300
|
+
/** A copy of an API value with its keys in the product's words, values untouched. */
|
|
2301
|
+
function inProductWords(value) {
|
|
2302
|
+
if (Array.isArray(value)) return value.map(inProductWords);
|
|
2303
|
+
if (value === null || typeof value !== "object") return value;
|
|
2304
|
+
const out = {};
|
|
2305
|
+
for (const [key, inner] of Object.entries(value)) Object.defineProperty(out, PRODUCT_WORDS[key] ?? key, {
|
|
2306
|
+
value: OPAQUE_MAPS.has(key) ? inner : inProductWords(inner),
|
|
2307
|
+
enumerable: true,
|
|
2308
|
+
writable: true,
|
|
2309
|
+
configurable: true
|
|
2310
|
+
});
|
|
2311
|
+
return out;
|
|
2312
|
+
}
|
|
2218
2313
|
/** A successful tool result: one JSON text block the model reads. */
|
|
2219
2314
|
function toolResult(value) {
|
|
2220
2315
|
return { content: [{
|
|
2221
2316
|
type: "text",
|
|
2222
|
-
text: JSON.stringify(value, null, 2)
|
|
2317
|
+
text: JSON.stringify(inProductWords(value), null, 2)
|
|
2223
2318
|
}] };
|
|
2224
2319
|
}
|
|
2225
2320
|
/**
|
|
@@ -2234,7 +2329,7 @@ function toolError(error, message, details) {
|
|
|
2234
2329
|
text: JSON.stringify(details ? {
|
|
2235
2330
|
error,
|
|
2236
2331
|
message,
|
|
2237
|
-
details
|
|
2332
|
+
details: inProductWords(details)
|
|
2238
2333
|
} : {
|
|
2239
2334
|
error,
|
|
2240
2335
|
message
|
|
@@ -2486,7 +2581,7 @@ function budgetSections(doc, budget) {
|
|
|
2486
2581
|
* The `sections` input of a document read: some sections by id, none for the
|
|
2487
2582
|
* outline (every id with its size and no text), or omitted for the whole.
|
|
2488
2583
|
*/
|
|
2489
|
-
const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().describe("Only these sections, by id; [] for the outline (every section id and its size, no text); omit for the whole document");
|
|
2584
|
+
const sectionsInput = z.array(z.string().min(1).max(200)).max(50).optional().describe("Only these sections, by id; [] for the outline (under outline: every section id and its size, no text); omit for the whole document");
|
|
2490
2585
|
/**
|
|
2491
2586
|
* The answer to a read that names sections: the ones the document has, the
|
|
2492
2587
|
* rest listed with their sizes, and the asked-for ids it lacks under
|
|
@@ -2500,6 +2595,14 @@ function sectionReadResult(doc, ids, label) {
|
|
|
2500
2595
|
if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
|
|
2501
2596
|
const all = Object.keys(sectionsOf(doc.data) ?? {});
|
|
2502
2597
|
const unknownSections = ids.filter((id) => !all.includes(id));
|
|
2598
|
+
if (ids.filter((id) => all.includes(id)).length === 0) {
|
|
2599
|
+
const { data: _data, ...meta } = cut.doc;
|
|
2600
|
+
return toolResult({
|
|
2601
|
+
...meta,
|
|
2602
|
+
outline: cut.omittedSections,
|
|
2603
|
+
...unknownSections.length > 0 ? { unknownSections } : {}
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2503
2606
|
return toolResult({
|
|
2504
2607
|
...cut.doc,
|
|
2505
2608
|
omittedSections: cut.omittedSections,
|
|
@@ -2507,6 +2610,90 @@ function sectionReadResult(doc, ids, label) {
|
|
|
2507
2610
|
});
|
|
2508
2611
|
}
|
|
2509
2612
|
//#endregion
|
|
2613
|
+
//#region src/tools/readiness.ts
|
|
2614
|
+
const IMPORT_STATE = {
|
|
2615
|
+
pending: "not started",
|
|
2616
|
+
analyzing: "running",
|
|
2617
|
+
generating_docs: "running",
|
|
2618
|
+
complete: "complete",
|
|
2619
|
+
failed: "failed",
|
|
2620
|
+
cancelled: "failed"
|
|
2621
|
+
};
|
|
2622
|
+
/** A failure that is the API's (refused, missing, unreachable), not a bug in this code. */
|
|
2623
|
+
function isApiSide(error) {
|
|
2624
|
+
return error instanceof TaigaApiError || isNetworkFailure(error);
|
|
2625
|
+
}
|
|
2626
|
+
/** The documents by the names the web uses. */
|
|
2627
|
+
function named(types) {
|
|
2628
|
+
return types.map((type) => PRODUCT_DOCUMENT_NAMES[type] ?? type).join(", ");
|
|
2629
|
+
}
|
|
2630
|
+
/** The product's setup readiness, or null once it has finished setup. */
|
|
2631
|
+
async function setupReadiness(client, product) {
|
|
2632
|
+
if (product["status"] !== "draft") return null;
|
|
2633
|
+
const id = String(product["id"]);
|
|
2634
|
+
const imported = isImportedProduct(product);
|
|
2635
|
+
const [repos, blockers, full] = await Promise.all([
|
|
2636
|
+
client.listProductRepositories(id).then((result) => result.data, (error) => {
|
|
2637
|
+
if (isApiSide(error)) return null;
|
|
2638
|
+
throw error;
|
|
2639
|
+
}),
|
|
2640
|
+
client.getProductSetupBlockers(id).then((result) => result.data, (error) => {
|
|
2641
|
+
if (isApiSide(error)) return null;
|
|
2642
|
+
throw error;
|
|
2643
|
+
}),
|
|
2644
|
+
"discoveryStep" in product || imported ? Promise.resolve(product) : client.getProduct(id).then((result) => result.data, (error) => {
|
|
2645
|
+
if (isApiSide(error)) return product;
|
|
2646
|
+
throw error;
|
|
2647
|
+
})
|
|
2648
|
+
]);
|
|
2649
|
+
const link = repos?.[0];
|
|
2650
|
+
const repository = !repos ? null : link ? {
|
|
2651
|
+
linked: true,
|
|
2652
|
+
name: link["repositoryName"],
|
|
2653
|
+
branch: link["effectiveBranch"]
|
|
2654
|
+
} : { linked: false };
|
|
2655
|
+
const settings = product["settings"] ?? {};
|
|
2656
|
+
const importState = imported ? IMPORT_STATE[String(settings.importStatus)] ?? "not started" : void 0;
|
|
2657
|
+
const missing = blockers ? blockers.missingDocuments : null;
|
|
2658
|
+
const openChecks = blockers ? blockers.openChecks.map((check) => check.criterion) : null;
|
|
2659
|
+
return {
|
|
2660
|
+
ready: blockers ? missing.length === 0 && openChecks.length === 0 : null,
|
|
2661
|
+
repository,
|
|
2662
|
+
...importState ? { import: importState } : {},
|
|
2663
|
+
...imported ? {} : { discoveryStep: full["discoveryStep"] ?? null },
|
|
2664
|
+
missingDocuments: missing ? missing.map((type) => named([type])) : null,
|
|
2665
|
+
openChecks,
|
|
2666
|
+
nextStep: nextStepFor({
|
|
2667
|
+
imported,
|
|
2668
|
+
importState,
|
|
2669
|
+
linked: repository?.linked ?? null,
|
|
2670
|
+
missing,
|
|
2671
|
+
openChecks,
|
|
2672
|
+
product: full
|
|
2673
|
+
})
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
function nextStepFor(state) {
|
|
2677
|
+
const finish = "taiga_finish_discovery opens the product and locks its documents (ask the person: it is their decision that the documents are right).";
|
|
2678
|
+
if (state.imported) {
|
|
2679
|
+
if (state.linked === false) return "Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.";
|
|
2680
|
+
if (state.linked === null && state.importState === "not started") return setupNextStep(state.product);
|
|
2681
|
+
switch (state.importState) {
|
|
2682
|
+
case "running": return "The import is running and derives the documents from the code; taiga_wait_for_job follows it.";
|
|
2683
|
+
case "failed": return "The last import did not finish: taiga_start_import runs it again.";
|
|
2684
|
+
case "complete":
|
|
2685
|
+
if (state.missing && state.missing.length > 0) return `The import finished but ${named(state.missing)} ${state.missing.length === 1 ? "is" : "are"} not published: taiga_list_product_documents shows them, taiga_publish_product_document publishes a draft.`;
|
|
2686
|
+
return `The import is done: review the reality documents (taiga_list_product_documents), then ${finish}`;
|
|
2687
|
+
default: return "taiga_start_import derives the documents from the code and runs by itself (paid agent work: ask first).";
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
if (!state.missing) return setupNextStep(state.product);
|
|
2691
|
+
if (state.missing.includes("specification")) return "The specification comes from the Discovery conversation, held in the web: the person describes the product there. The other documents follow from it." + (state.linked === false ? " Linking a repository (taiga_link_repository) can wait until before the first build." : "");
|
|
2692
|
+
if (state.missing.length > 0) return `Still to publish: ${named(state.missing)}. taiga_generate_product_documents generates the missing ones (paid agent work: ask first); a draft is published with taiga_publish_product_document.`;
|
|
2693
|
+
if (state.openChecks && state.openChecks.length > 0) return `The documents are published; the checks are stamped in the web, then ${finish}`;
|
|
2694
|
+
return `Every required document is published: ${finish}`;
|
|
2695
|
+
}
|
|
2696
|
+
//#endregion
|
|
2510
2697
|
//#region src/tools/summarize.ts
|
|
2511
2698
|
/**
|
|
2512
2699
|
* Keep only the named fields of each item. List tools return summaries (the
|
|
@@ -2828,7 +3015,7 @@ function registerProductTools(server, client) {
|
|
|
2828
3015
|
}));
|
|
2829
3016
|
server.registerTool("taiga_get_product", {
|
|
2830
3017
|
title: "Get product",
|
|
2831
|
-
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,
|
|
3018
|
+
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_initiatives for its work and taiga_list_runs for what the coding agent did. A product still in setup carries readiness: whether it can finish setup now, its repository and import, the required documents not yet published, the checks not yet stamped, and nextStep, the one move that gets it closer.",
|
|
2832
3019
|
inputSchema: z.object({
|
|
2833
3020
|
productId: productId.optional(),
|
|
2834
3021
|
slug: slug.optional()
|
|
@@ -2837,10 +3024,10 @@ function registerProductTools(server, client) {
|
|
|
2837
3024
|
}, (params) => runTool(async () => {
|
|
2838
3025
|
const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
|
|
2839
3026
|
if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
|
|
2840
|
-
const
|
|
2841
|
-
return toolResult(
|
|
3027
|
+
const readiness = await setupReadiness(client, resolved.product);
|
|
3028
|
+
return toolResult(readiness ? {
|
|
2842
3029
|
...resolved.product,
|
|
2843
|
-
|
|
3030
|
+
readiness
|
|
2844
3031
|
} : resolved.product);
|
|
2845
3032
|
}));
|
|
2846
3033
|
server.registerTool("taiga_resolve_product", {
|
|
@@ -2937,7 +3124,7 @@ function sectionIdFrom(readTool) {
|
|
|
2937
3124
|
return z.string().min(1).describe(`Section id -- the keys of \`data.sections\` in ${readTool}, e.g. summary, scope, apiDesign. An id that does not exist yet adds a section.`);
|
|
2938
3125
|
}
|
|
2939
3126
|
const sectionContentInput = z.string().describe("The full new Markdown text of the section; it replaces the old text");
|
|
2940
|
-
const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe(`Document type: a policy (${named(POLICY_TYPES)}) or ${named(OTHER_TENANT_DOCUMENT_TYPES)}`);
|
|
3127
|
+
const tenantDocumentType = z.enum(TENANT_DOCUMENT_TYPES).describe(`Document type: a policy (${named$1(POLICY_TYPES)}) or ${named$1(OTHER_TENANT_DOCUMENT_TYPES)}`);
|
|
2941
3128
|
/** Everything the model needs back from a write, without the content it already holds. */
|
|
2942
3129
|
function documentSummary(doc) {
|
|
2943
3130
|
const { data, ...rest } = doc;
|
|
@@ -2999,7 +3186,7 @@ function registerDocumentTools(server, client) {
|
|
|
2999
3186
|
if (client.hasCapability("tenant:documents:read")) {
|
|
3000
3187
|
server.registerTool("taiga_list_documents", {
|
|
3001
3188
|
title: "List company documents",
|
|
3002
|
-
description: `List the organization's documents as summaries (type, name, description, version, draft state). The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) define the security, data privacy, SDLC, operations, architecture and compliance rules that apply across all factories and products; the other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. Use taiga_get_document with the type to read one in full.`,
|
|
3189
|
+
description: `List the organization's documents as summaries (type, name, description, version, draft state). The ${POLICY_TYPES.length} policies (${named$1(POLICY_TYPES)}) define the security, data privacy, SDLC, operations, architecture and compliance rules that apply across all factories and products; the other documents (${named$1(OTHER_TENANT_DOCUMENT_TYPES)}) carry goals, standards, voice and standing instructions. Use taiga_get_document with the type to read one in full.`,
|
|
3003
3190
|
inputSchema: z.object({ ...pagination }),
|
|
3004
3191
|
annotations: READ_ONLY
|
|
3005
3192
|
}, (params) => runTool(async () => {
|
|
@@ -3008,7 +3195,7 @@ function registerDocumentTools(server, client) {
|
|
|
3008
3195
|
}));
|
|
3009
3196
|
server.registerTool("taiga_get_document", {
|
|
3010
3197
|
title: "Get company document",
|
|
3011
|
-
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, or sections: [] for the outline) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds no document says which documents are published and whether a draft exists.",
|
|
3198
|
+
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, or sections: [] for the outline) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds no document says which documents are published and whether a draft exists. Each section's source says where its text came from (template: Taiga's standard text; overlay-<id>: a jurisdiction or industry overlay; cloud-preset:<providers>: rendered for the organization's clouds) and priority: core marks the sections every organization keeps; neither changes what the section requires.",
|
|
3012
3199
|
inputSchema: z.object({
|
|
3013
3200
|
type: tenantDocumentType,
|
|
3014
3201
|
sections: sectionsInput
|
|
@@ -3059,7 +3246,7 @@ function registerDocumentTools(server, client) {
|
|
|
3059
3246
|
if (client.hasCapability("factories:read")) {
|
|
3060
3247
|
server.registerTool("taiga_list_factory_documents", {
|
|
3061
3248
|
title: "List factory documents",
|
|
3062
|
-
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.`,
|
|
3249
|
+
description: `List a factory's documents, published and draft, as summaries (type, name, version, draft state). A factory has one document type, ${named$1(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.`,
|
|
3063
3250
|
inputSchema: z.object({
|
|
3064
3251
|
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
|
|
3065
3252
|
...pagination
|
|
@@ -3223,9 +3410,9 @@ function requestPrompt(title, description) {
|
|
|
3223
3410
|
}
|
|
3224
3411
|
//#endregion
|
|
3225
3412
|
//#region src/tools/queue.ts
|
|
3226
|
-
/** How many entries the read names; past this an entry keeps its
|
|
3413
|
+
/** How many entries the read names; past this an entry keeps its initiativeId only. */
|
|
3227
3414
|
const NAMED_ENTRIES = 25;
|
|
3228
|
-
const MOVE_ASIDE = "or move it down (
|
|
3415
|
+
const MOVE_ASIDE = "or move it down (taiga_queue_initiative with afterId) or off the line (taiga_move_initiative) to let the rest pass.";
|
|
3229
3416
|
/**
|
|
3230
3417
|
* What lifts a halt, in the tools that do it. Read from the halt registry in
|
|
3231
3418
|
* @taiga/types (how each cause clears, and whether a person may lift it), so
|
|
@@ -3235,11 +3422,11 @@ const MOVE_ASIDE = "or move it down (taiga_queue_backlog_item with afterId) or o
|
|
|
3235
3422
|
function haltNextStep(cause) {
|
|
3236
3423
|
if (cause === "awaiting_plan_review") return "Read the plan with taiga_get_plan, then approve it (taiga_approve_plan) or send it back (taiga_reject_plan).";
|
|
3237
3424
|
const spec = cause ? HALT_CAUSES[cause] : void 0;
|
|
3238
|
-
if (!cause || !spec) return `Read the initiative with
|
|
3425
|
+
if (!cause || !spec) return `Read the initiative with taiga_get_initiative to see why, ${MOVE_ASIDE}`;
|
|
3239
3426
|
const known = cause;
|
|
3240
|
-
if (canResumeHalt(known)) return `
|
|
3427
|
+
if (canResumeHalt(known)) return `taiga_resume_initiative lifts it, ${MOVE_ASIDE}`;
|
|
3241
3428
|
if (haltClearsItself(known)) return `It clears when ${spec.clearedWhen}; ${MOVE_ASIDE}`;
|
|
3242
|
-
return `Only taking it off the line clears it (
|
|
3429
|
+
return `Only taking it off the line clears it (taiga_move_initiative): ${spec.clearedWhen}.`;
|
|
3243
3430
|
}
|
|
3244
3431
|
/**
|
|
3245
3432
|
* The entry the line waits on, by the queue's two kinds of blocker
|
|
@@ -3313,7 +3500,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
3313
3500
|
cta: lifecycle.cta
|
|
3314
3501
|
} : null,
|
|
3315
3502
|
...latestBuild ? {
|
|
3316
|
-
|
|
3503
|
+
runId: latestBuild.id,
|
|
3317
3504
|
prUrl: latestBuild.prUrl ?? null
|
|
3318
3505
|
} : {}
|
|
3319
3506
|
});
|
|
@@ -3330,7 +3517,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
3330
3517
|
}
|
|
3331
3518
|
return row;
|
|
3332
3519
|
});
|
|
3333
|
-
const note = unnamed === 0 ? null : !canName ? "Entries carry
|
|
3520
|
+
const note = unnamed === 0 ? null : !canName ? "Entries carry initiativeId only: naming them needs backlog:read, which this key lacks." : entries.length > NAMED_ENTRIES ? `Only the first ${NAMED_ENTRIES} entries are named; the rest carry initiativeId only.` : "An entry whose initiative could not be read carries initiativeId only; taiga_get_initiative reads it.";
|
|
3334
3521
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
3335
3522
|
return {
|
|
3336
3523
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
@@ -3354,7 +3541,7 @@ async function linePausedSince(client, product) {
|
|
|
3354
3541
|
}
|
|
3355
3542
|
function registerQueueTools(server, client) {
|
|
3356
3543
|
if (!client.hasCapability("products:read")) return;
|
|
3357
|
-
server.registerTool("
|
|
3544
|
+
server.registerTool("taiga_get_line", {
|
|
3358
3545
|
title: "Get the product queue",
|
|
3359
3546
|
description: "Read a product's queue, the line the coding agent works one initiative at a time: each entry in order with its title, where it is (planning, building, in review) and whose turn it is, a halted entry with why it stopped and what lifts it, linePaused (null when the product could not be read), and waitingOn, one sentence saying what the line is waiting for. The line advances only when the active entry's pull request merges, so an entry waiting on a review holds everything behind it. Use this when a queued initiative is not moving.",
|
|
3360
3547
|
inputSchema: z.object({ productId }),
|
|
@@ -3363,7 +3550,7 @@ function registerQueueTools(server, client) {
|
|
|
3363
3550
|
if (!client.hasCapability("products:write")) return;
|
|
3364
3551
|
server.registerTool("taiga_pause_line", {
|
|
3365
3552
|
title: "Pause or resume a product's line",
|
|
3366
|
-
description: "Pause the product's whole line, or resume it. Pausing cancels the plan being written and stops the build in flight where it stands (both are flagged), and nothing more is planned or built; initiatives keep their places. Resuming plans the interrupted initiative again from scratch, carries the interrupted build on from its first incomplete step, and the line picks up where it was; a build you had stopped yourself before pausing stays stopped. Idempotent. Pausing interrupts work and resuming starts it: read
|
|
3553
|
+
description: "Pause the product's whole line, or resume it. Pausing cancels the plan being written and stops the build in flight where it stands (both are flagged), and nothing more is planned or built; initiatives keep their places. Resuming plans the interrupted initiative again from scratch, carries the interrupted build on from its first incomplete step, and the line picks up where it was; a build you had stopped yourself before pausing stays stopped. Idempotent. Pausing interrupts work and resuming starts it: read taiga_get_line first to see what it would interrupt, and ask the person.",
|
|
3367
3554
|
inputSchema: z.object({
|
|
3368
3555
|
productId,
|
|
3369
3556
|
paused: z.boolean().describe("true pauses the line, false resumes it")
|
|
@@ -3379,6 +3566,243 @@ function registerQueueTools(server, client) {
|
|
|
3379
3566
|
}));
|
|
3380
3567
|
}
|
|
3381
3568
|
//#endregion
|
|
3569
|
+
//#region src/tools/builds.ts
|
|
3570
|
+
/** What a list row needs to say for the model to pick a run to read in full. */
|
|
3571
|
+
const BUILD_SUMMARY_FIELDS = [
|
|
3572
|
+
"id",
|
|
3573
|
+
"runNumber",
|
|
3574
|
+
"productId",
|
|
3575
|
+
"backlogItemId",
|
|
3576
|
+
"targetTitle",
|
|
3577
|
+
"planId",
|
|
3578
|
+
"status",
|
|
3579
|
+
"failedReason",
|
|
3580
|
+
"runtime",
|
|
3581
|
+
"repositoryFullName",
|
|
3582
|
+
"branch",
|
|
3583
|
+
"baseBranch",
|
|
3584
|
+
"completedSteps",
|
|
3585
|
+
"totalSteps",
|
|
3586
|
+
"prNumber",
|
|
3587
|
+
"prUrl",
|
|
3588
|
+
"prState",
|
|
3589
|
+
"prDraft",
|
|
3590
|
+
"prMerged",
|
|
3591
|
+
"actions",
|
|
3592
|
+
"createdAt",
|
|
3593
|
+
"startedAt",
|
|
3594
|
+
"completedAt"
|
|
3595
|
+
];
|
|
3596
|
+
/** What a run says about itself after a stop or a resume: its state and what can be done next. */
|
|
3597
|
+
const RUN_OUTCOME_FIELDS = [
|
|
3598
|
+
"id",
|
|
3599
|
+
"runNumber",
|
|
3600
|
+
"backlogItemId",
|
|
3601
|
+
"status",
|
|
3602
|
+
"failedReason",
|
|
3603
|
+
"completedSteps",
|
|
3604
|
+
"totalSteps",
|
|
3605
|
+
"prNumber",
|
|
3606
|
+
"prUrl",
|
|
3607
|
+
"prMerged",
|
|
3608
|
+
"actions"
|
|
3609
|
+
];
|
|
3610
|
+
/**
|
|
3611
|
+
* Where a run that ended without finishing stopped, from its steps and its
|
|
3612
|
+
* hardening result: a failed run with every step completed reads as nothing
|
|
3613
|
+
* failed, and retry_from_failed_step then reads as a mistake, when the agent
|
|
3614
|
+
* stalled in the hardening pass after the steps and the retry continues it.
|
|
3615
|
+
* Null for a run still going or that finished.
|
|
3616
|
+
*/
|
|
3617
|
+
function stoppedIn(run, steps) {
|
|
3618
|
+
if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
|
|
3619
|
+
if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
|
|
3620
|
+
const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
|
|
3621
|
+
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)})`;
|
|
3622
|
+
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.";
|
|
3623
|
+
return "after the steps and hardening, at the pull request";
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* The disposition with the merge latch authoritative: a run whose pull
|
|
3627
|
+
* request merged landed, whatever settled status it kept when the completion
|
|
3628
|
+
* write after the merge failed (build.ts allows a merged run to stay failed
|
|
3629
|
+
* or stopped). A run still executing keeps its in-flight disposition.
|
|
3630
|
+
*/
|
|
3631
|
+
function landedDisposition(status, prMerged) {
|
|
3632
|
+
const disposition = dispositionOf(status, prMerged);
|
|
3633
|
+
return prMerged && disposition !== "in_flight" ? "succeeded" : disposition;
|
|
3634
|
+
}
|
|
3635
|
+
/** What each executing status is doing, in a phrase. */
|
|
3636
|
+
const RUNNING = {
|
|
3637
|
+
cloning: "setting up the workspace",
|
|
3638
|
+
implementing: "implementing the steps",
|
|
3639
|
+
hardening: "hardening: running the security scanners and fixing what they find",
|
|
3640
|
+
pushing: "pushing the branch",
|
|
3641
|
+
ci_checking: "waiting for the pull request's CI",
|
|
3642
|
+
ci_fixing: "fixing what CI found",
|
|
3643
|
+
reviewing: "answering review comments on the pull request"
|
|
3644
|
+
};
|
|
3645
|
+
/**
|
|
3646
|
+
* How a run stands, in one sentence, with its disposition: the meaning the
|
|
3647
|
+
* status and the pull request facts carry between them
|
|
3648
|
+
* (PRO-2800). An agent read `closed` with failedReason null, every step
|
|
3649
|
+
* completed and hardening passed, and could not tell what had happened; the
|
|
3650
|
+
* status alone cannot say whether a completed run merged (dispositionOf).
|
|
3651
|
+
*/
|
|
3652
|
+
function runOutcome(run) {
|
|
3653
|
+
const status = run["status"];
|
|
3654
|
+
if (!BUILD_RUN_STATUSES.includes(status)) return null;
|
|
3655
|
+
const disposition = landedDisposition(status, run["prMerged"] === true);
|
|
3656
|
+
const pr = typeof run["prNumber"] === "number" ? `PR #${run["prNumber"]}` : "its pull request";
|
|
3657
|
+
const progress = typeof run["completedSteps"] === "number" && typeof run["totalSteps"] === "number" ? ` (${run["completedSteps"]} of ${run["totalSteps"]} steps done)` : "";
|
|
3658
|
+
const reason = typeof run["failedReason"] === "string" && run["failedReason"] ? `: ${run["failedReason"]}` : "";
|
|
3659
|
+
return {
|
|
3660
|
+
disposition,
|
|
3661
|
+
outcome: {
|
|
3662
|
+
in_flight: status === "pending" ? "Queued: waiting to start." : `Running: ${RUNNING[status] ?? status}${progress}.`,
|
|
3663
|
+
gated: `Waiting for a person to review and merge ${pr}.`,
|
|
3664
|
+
succeeded: `Merged: ${pr} is in ${typeof run["baseBranch"] === "string" ? run["baseBranch"] : "the base branch"}.`,
|
|
3665
|
+
unmerged: typeof run["prNumber"] === "number" || run["prUrl"] ? `Settled without merging: ${pr} was left unmerged.` : "Settled without a merged pull request.",
|
|
3666
|
+
failed: `Failed${reason}${progress}.`,
|
|
3667
|
+
stopped: `Stopped before it finished${progress}.`,
|
|
3668
|
+
rejected: `${pr} was closed without merging, so the work did not land.`
|
|
3669
|
+
}[disposition] ?? status
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
/**
|
|
3673
|
+
* A run row as an agent reads it: its outcome added, and prDraft only where
|
|
3674
|
+
* there is a pull request. The column defaults to true, so a run that never
|
|
3675
|
+
* opened one said it had a draft (PRO-2800).
|
|
3676
|
+
*/
|
|
3677
|
+
function readableRun(run) {
|
|
3678
|
+
const { prDraft, ...rest } = run;
|
|
3679
|
+
const said = runOutcome(run);
|
|
3680
|
+
return {
|
|
3681
|
+
...rest,
|
|
3682
|
+
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
3683
|
+
...said ?? {}
|
|
3684
|
+
};
|
|
3685
|
+
}
|
|
3686
|
+
/** How many products the org-wide list names; past this a row keeps its productId only. */
|
|
3687
|
+
const NAMED_PRODUCTS = 100;
|
|
3688
|
+
const runId = idInput("run", "taiga_list_runs gives it");
|
|
3689
|
+
/** A step without its instructions: what it is, how it stands and what it changed. */
|
|
3690
|
+
const STEP_SUMMARY_FIELDS = [
|
|
3691
|
+
"stepOrder",
|
|
3692
|
+
"title",
|
|
3693
|
+
"status",
|
|
3694
|
+
"currentPhase",
|
|
3695
|
+
"commitSha",
|
|
3696
|
+
"filesChanged",
|
|
3697
|
+
"verification",
|
|
3698
|
+
"error",
|
|
3699
|
+
"errorClassification",
|
|
3700
|
+
"startedAt",
|
|
3701
|
+
"completedAt"
|
|
3702
|
+
];
|
|
3703
|
+
/** Past this many characters a step's headline is cut; the full read has the rest. */
|
|
3704
|
+
const HEADLINE_LIMIT = 600;
|
|
3705
|
+
/**
|
|
3706
|
+
* The first paragraph of a step's summary: the agent writes its result
|
|
3707
|
+
* there (what changed, which checks passed), and the detail after it.
|
|
3708
|
+
*/
|
|
3709
|
+
function summaryHeadline(summary) {
|
|
3710
|
+
if (typeof summary !== "string" || !summary.trim()) return null;
|
|
3711
|
+
const first = summary.trim().split(/\n\s*\n/, 1)[0].trim();
|
|
3712
|
+
return first.length > HEADLINE_LIMIT ? `${first.slice(0, HEADLINE_LIMIT).trimEnd()}...` : first;
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* The runs of every product the key can see, each row naming its product.
|
|
3716
|
+
* The route carries product ids only; one products read names them, and a
|
|
3717
|
+
* failure of that read leaves the rows unnamed rather than failing the list.
|
|
3718
|
+
*/
|
|
3719
|
+
async function activityRows(client, opts) {
|
|
3720
|
+
const [runs, products] = await Promise.all([client.listBuildRunActivity(opts), client.listProducts({ limit: NAMED_PRODUCTS }).catch((error) => {
|
|
3721
|
+
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
3722
|
+
throw error;
|
|
3723
|
+
})]);
|
|
3724
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
3725
|
+
for (const product of products?.data ?? []) nameById.set(product.id, {
|
|
3726
|
+
name: product.name,
|
|
3727
|
+
slug: product.slug
|
|
3728
|
+
});
|
|
3729
|
+
const rows = pickFields(runs.data, BUILD_SUMMARY_FIELDS).map(readableRun).map((row) => {
|
|
3730
|
+
const product = nameById.get(String(row["productId"]));
|
|
3731
|
+
return product ? {
|
|
3732
|
+
...row,
|
|
3733
|
+
product
|
|
3734
|
+
} : row;
|
|
3735
|
+
});
|
|
3736
|
+
const note = !rows.some((row) => !("product" in row)) ? null : !products ? "The products could not be read, so rows carry productId only; taiga_get_product names one." : products.nextCursor !== null ? `Only the first ${NAMED_PRODUCTS} products could be named; a row without product carries productId only.` : "A row without product belongs to a product the key cannot list (deleted since, or not visible to it); it carries productId only.";
|
|
3737
|
+
return {
|
|
3738
|
+
...page(rows, runs.nextCursor),
|
|
3739
|
+
...note ? { note } : {}
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
function registerBuildTools(server, client) {
|
|
3743
|
+
if (!client.hasCapability("products:read")) return;
|
|
3744
|
+
server.registerTool("taiga_list_runs", {
|
|
3745
|
+
title: "List runs",
|
|
3746
|
+
description: "List the coding agent's runs, newest first. A run is one attempt at building an initiative: its branch, pull request and step progress. Each row says how it stands in one sentence (outcome) and as a disposition: in_flight (the agent is working), gated (a person owes a review and merge), succeeded (merged), failed, stopped, rejected (the pull request was closed without merging) or unmerged. actions name what a person may do with it. With productId, the runs of that product, optionally of one initiative; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing now. taiga_get_run reads one with its steps. Work is carried on from the initiative (taiga_resume_initiative), not the run.",
|
|
3747
|
+
inputSchema: z.object({
|
|
3748
|
+
productId: productId.optional(),
|
|
3749
|
+
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it").optional().describe("Only this initiative's runs -- get from taiga_list_initiatives; needs productId"),
|
|
3750
|
+
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"),
|
|
3751
|
+
...pagination
|
|
3752
|
+
}),
|
|
3753
|
+
annotations: READ_ONLY
|
|
3754
|
+
}, (params) => runTool(async () => {
|
|
3755
|
+
if (!params.productId && params.initiativeId) return toolError("VALIDATION_ERROR", "initiativeId narrows the runs of one product: pass its productId as well (the initiative carries it).");
|
|
3756
|
+
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.");
|
|
3757
|
+
if (params.productId) {
|
|
3758
|
+
const result = await client.listBuildRuns(params.productId, {
|
|
3759
|
+
backlogItemId: params.initiativeId,
|
|
3760
|
+
limit: params.limit,
|
|
3761
|
+
cursor: params.cursor
|
|
3762
|
+
});
|
|
3763
|
+
return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS).map(readableRun), result.nextCursor));
|
|
3764
|
+
}
|
|
3765
|
+
return toolResult(await activityRows(client, params));
|
|
3766
|
+
}));
|
|
3767
|
+
server.registerTool("taiga_get_run", {
|
|
3768
|
+
title: "Get run",
|
|
3769
|
+
description: "Get one run with its steps: how it stands (outcome, disposition), its pull request and hardening, and each step's title, status, commit, files changed, verification and headline, the first paragraph of what the agent reported. A step's verification is Taiga's own check after the step (tests, lint, build), independent of what the agent says it ran: a docs-only step can show tests failed because the repository's tests fail. A run that failed or was stopped says where (stoppedIn) and when it last made progress (lastProgressAt). Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Its actions name what the initiative accepts: resume and retry_from_failed_step are taiga_resume_initiative; start_over is in the web only; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
|
|
3770
|
+
inputSchema: z.object({
|
|
3771
|
+
runId,
|
|
3772
|
+
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
3773
|
+
}),
|
|
3774
|
+
annotations: READ_ONLY
|
|
3775
|
+
}, (params) => runTool(async () => {
|
|
3776
|
+
const [run, steps] = await Promise.all([client.getBuildRun(params.runId), client.listBuildRunSteps(params.runId)]);
|
|
3777
|
+
const runRow = run.data;
|
|
3778
|
+
const rows = steps.data;
|
|
3779
|
+
const where = stoppedIn(runRow, rows);
|
|
3780
|
+
const stop = where ? { stoppedIn: where } : {};
|
|
3781
|
+
if (params.full) return toolResult({
|
|
3782
|
+
...readableRun(runRow),
|
|
3783
|
+
...stop,
|
|
3784
|
+
steps: steps.data
|
|
3785
|
+
});
|
|
3786
|
+
return toolResult({
|
|
3787
|
+
...readableRun(runRow),
|
|
3788
|
+
...stop,
|
|
3789
|
+
steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
|
|
3790
|
+
...step,
|
|
3791
|
+
headline: summaryHeadline(rows[index].summary)
|
|
3792
|
+
}))
|
|
3793
|
+
});
|
|
3794
|
+
}));
|
|
3795
|
+
if (!client.hasCapability("products:write")) return;
|
|
3796
|
+
server.registerTool("taiga_stop_run", {
|
|
3797
|
+
title: "Stop a run",
|
|
3798
|
+
description: "The emergency brake: stop a run the coding agent is executing when its initiative cannot be moved. The way to stop work is on the initiative: move it out of Build (taiga_move_initiative), which stops the plan or run in flight. Use this only for a run whose initiative does not offer that (it is finished, canceled or gone) yet the run is still going. The agent halts, its branch and any draft pull request stay. Only a run whose actions include stop; the API refuses the rest with CONFLICT.",
|
|
3799
|
+
inputSchema: z.object({ runId }),
|
|
3800
|
+
annotations: OPERATE
|
|
3801
|
+
}, (params) => runTool(async () => {
|
|
3802
|
+
return toolResult(readableRun(pickFields([(await client.stopBuildRun(params.runId)).data], RUN_OUTCOME_FIELDS)[0]));
|
|
3803
|
+
}));
|
|
3804
|
+
}
|
|
3805
|
+
//#endregion
|
|
3382
3806
|
//#region src/tools/backlogs.ts
|
|
3383
3807
|
/** What a list row needs to say for the model to pick an item to read in full. */
|
|
3384
3808
|
const BACKLOG_SUMMARY_FIELDS = [
|
|
@@ -3414,6 +3838,83 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
3414
3838
|
"archivedAt",
|
|
3415
3839
|
"lifecycle"
|
|
3416
3840
|
];
|
|
3841
|
+
/** The move each call to action is, in the tools' words. */
|
|
3842
|
+
const CTA_MOVE = {
|
|
3843
|
+
start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
|
|
3844
|
+
retry_plan: "taiga_plan_again asks for a new plan (paid work: ask first)",
|
|
3845
|
+
start_build: "read the plan with taiga_get_plan, then taiga_approve_plan builds it or taiga_reject_plan sends it back",
|
|
3846
|
+
resume_build: "taiga_resume_initiative carries the run on (paid work: ask first)",
|
|
3847
|
+
retry_build: "a new run starts over from the web; taiga_move_initiative takes it off the line",
|
|
3848
|
+
review_pr: "a person reviews and merges the pull request",
|
|
3849
|
+
view_pr: "the line merges the pull request once its checks pass"
|
|
3850
|
+
};
|
|
3851
|
+
/** How a run stands, as the end of "its latest run ...", by disposition. */
|
|
3852
|
+
const RUN_STANDS = {
|
|
3853
|
+
in_flight: "is running",
|
|
3854
|
+
gated: "is in review",
|
|
3855
|
+
succeeded: "merged",
|
|
3856
|
+
unmerged: "settled unmerged",
|
|
3857
|
+
failed: "failed",
|
|
3858
|
+
stopped: "was stopped",
|
|
3859
|
+
rejected: "had its pull request closed without merging"
|
|
3860
|
+
};
|
|
3861
|
+
/** Where an initiative is, in a phrase, by its lifecycle stage. */
|
|
3862
|
+
const STAGE = {
|
|
3863
|
+
backlog: "In the backlog, not on the line yet",
|
|
3864
|
+
planning: "Being planned",
|
|
3865
|
+
planned: "Planned",
|
|
3866
|
+
building: "Being built",
|
|
3867
|
+
in_review: "Its pull request is in review",
|
|
3868
|
+
done: "Done",
|
|
3869
|
+
canceled: "Canceled"
|
|
3870
|
+
};
|
|
3871
|
+
/**
|
|
3872
|
+
* Where an initiative stands and what moves it on, in one sentence (PRO-2800).
|
|
3873
|
+
* Five fields carried it (status, state, planningStatus, needsHumanCause,
|
|
3874
|
+
* lifecycle) and a reviewing agent had to read the queue, the initiative and
|
|
3875
|
+
* the run together to say what had happened; the lifecycle the API derives
|
|
3876
|
+
* already decides it. Null for a row without a lifecycle.
|
|
3877
|
+
*/
|
|
3878
|
+
function situationOf(item) {
|
|
3879
|
+
const life = item["lifecycle"];
|
|
3880
|
+
if (!life || typeof life.stage !== "string") return null;
|
|
3881
|
+
const parts = [];
|
|
3882
|
+
let stage = STAGE[life.stage] ?? life.stage;
|
|
3883
|
+
if (life.stage === "planning" && life.turn === "you") stage = `Planning stopped${typeof life.planFailureCause === "string" ? ` (${life.planFailureCause})` : ""}`;
|
|
3884
|
+
const build = life.latestBuild;
|
|
3885
|
+
if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
|
|
3886
|
+
const pr = typeof build.prNumber === "number" ? ` (PR #${build.prNumber})` : "";
|
|
3887
|
+
const said = BUILD_RUN_STATUSES.includes(build.status) ? RUN_STANDS[landedDisposition(build.status, build.prMerged === true)] : build.status;
|
|
3888
|
+
stage += `; its latest run ${said}${pr}`;
|
|
3889
|
+
}
|
|
3890
|
+
parts.push(`${stage}.`);
|
|
3891
|
+
if (typeof item["needsHumanCause"] === "string") {
|
|
3892
|
+
const why = [renderHalt({
|
|
3893
|
+
cause: item["needsHumanCause"],
|
|
3894
|
+
params: {}
|
|
3895
|
+
}), typeof item["needsHumanDetail"] === "string" ? item["needsHumanDetail"] : ""].filter(Boolean).map((text) => text.replace(/\.$/, "")).join(". ");
|
|
3896
|
+
if (why) parts.push(`The line stopped on it: ${why}.`);
|
|
3897
|
+
}
|
|
3898
|
+
const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
|
|
3899
|
+
const move = typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
|
|
3900
|
+
if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
|
|
3901
|
+
else if (turn && move) parts.push(`${turn}: ${move}.`);
|
|
3902
|
+
else if (turn) parts.push(`${turn}.`);
|
|
3903
|
+
else if (move) parts.push(`Next: ${move}.`);
|
|
3904
|
+
return parts.join(" ");
|
|
3905
|
+
}
|
|
3906
|
+
/** A row or item with its situation added when it has a lifecycle. */
|
|
3907
|
+
function withSituation(item, row = item) {
|
|
3908
|
+
const situation = situationOf(item);
|
|
3909
|
+
return situation ? {
|
|
3910
|
+
...row,
|
|
3911
|
+
situation
|
|
3912
|
+
} : row;
|
|
3913
|
+
}
|
|
3914
|
+
/** List rows, each with its situation. */
|
|
3915
|
+
function situatedRows(items) {
|
|
3916
|
+
return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index], row));
|
|
3917
|
+
}
|
|
3417
3918
|
/** What each plan failure cause means for whoever reads it next (PLAN_FAILURE_CAUSES in @taiga/types). */
|
|
3418
3919
|
const PLAN_FAILURE_MEANING = {
|
|
3419
3920
|
agent_error: "The planning agent failed. Planning again is the usual next step.",
|
|
@@ -3425,10 +3926,10 @@ const PLAN_FAILURE_MEANING = {
|
|
|
3425
3926
|
* (PRO-2780): planFailureCause alone said "cancelled" with nobody behind it.
|
|
3426
3927
|
* Best effort: a history the API will not give leaves the item as it was.
|
|
3427
3928
|
*/
|
|
3428
|
-
async function planFailureOf(client,
|
|
3929
|
+
async function planFailureOf(client, initiativeId, itemCause) {
|
|
3429
3930
|
let events;
|
|
3430
3931
|
try {
|
|
3431
|
-
events = (await client.listBacklogItemActivity(
|
|
3932
|
+
events = (await client.listBacklogItemActivity(initiativeId)).data;
|
|
3432
3933
|
} catch (error) {
|
|
3433
3934
|
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
3434
3935
|
throw error;
|
|
@@ -3460,7 +3961,7 @@ async function planFailureOf(client, itemId, itemCause) {
|
|
|
3460
3961
|
};
|
|
3461
3962
|
}
|
|
3462
3963
|
const planId = idInput("plan", "taiga_get_plan gives the initiative's active plan and its ID");
|
|
3463
|
-
const
|
|
3964
|
+
const initiativeId = idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it");
|
|
3464
3965
|
/** The plan without its steps' prose: enough to approve it or say what a run will do. */
|
|
3465
3966
|
const PLAN_SUMMARY_FIELDS = [
|
|
3466
3967
|
"id",
|
|
@@ -3479,8 +3980,8 @@ const PLAN_STEP_SUMMARY_FIELDS = [
|
|
|
3479
3980
|
"reads",
|
|
3480
3981
|
"createsResources"
|
|
3481
3982
|
];
|
|
3482
|
-
/** The outcome fields of an initiative, with how many of its criteria are met. */
|
|
3483
|
-
const outcomeOf = (item) => ({
|
|
3983
|
+
/** The outcome fields of an initiative, with how many of its criteria are met, and where it stands. */
|
|
3984
|
+
const outcomeOf = (item) => withSituation(item, {
|
|
3484
3985
|
...pickFields([item], ITEM_OUTCOME_FIELDS)[0],
|
|
3485
3986
|
criteria: criteriaCounts(item)
|
|
3486
3987
|
});
|
|
@@ -3515,9 +4016,9 @@ function registerBacklogTools(server, client) {
|
|
|
3515
4016
|
if (may("backlog:read") && may("backlog:write") && may("products:write")) registerPlanAgain(server, client);
|
|
3516
4017
|
}
|
|
3517
4018
|
function registerBacklogReads(server, client) {
|
|
3518
|
-
server.registerTool("
|
|
3519
|
-
title: "List
|
|
3520
|
-
description: "List a product's
|
|
4019
|
+
server.registerTool("taiga_list_initiatives", {
|
|
4020
|
+
title: "List initiatives",
|
|
4021
|
+
description: "List a product's initiatives, its backlog: titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done), situation (where it stands and what moves it on, in a sentence) 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_initiative. Archived items are left out unless asked for.",
|
|
3521
4022
|
inputSchema: z.object({
|
|
3522
4023
|
productId,
|
|
3523
4024
|
query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
|
|
@@ -3547,7 +4048,7 @@ function registerBacklogReads(server, client) {
|
|
|
3547
4048
|
const { query, ...filters } = params;
|
|
3548
4049
|
if (query === void 0) {
|
|
3549
4050
|
const result = await client.listBacklogItems(params.productId, filters);
|
|
3550
|
-
return toolResult(page(
|
|
4051
|
+
return toolResult(page(situatedRows(result.data), result.nextCursor));
|
|
3551
4052
|
}
|
|
3552
4053
|
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
|
|
3553
4054
|
const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
|
|
@@ -3556,35 +4057,36 @@ function registerBacklogReads(server, client) {
|
|
|
3556
4057
|
cursor
|
|
3557
4058
|
}), query, (row) => [row.title], params.limit ?? 25);
|
|
3558
4059
|
return toolResult({
|
|
3559
|
-
...page(
|
|
4060
|
+
...page(situatedRows(found.rows), null),
|
|
3560
4061
|
...found.more ? { more: SEARCH_NOTE } : {}
|
|
3561
4062
|
});
|
|
3562
4063
|
}));
|
|
3563
|
-
server.registerTool("
|
|
3564
|
-
title: "Get
|
|
3565
|
-
description: "Get one
|
|
3566
|
-
inputSchema: z.object({
|
|
4064
|
+
server.registerTool("taiga_get_initiative", {
|
|
4065
|
+
title: "Get initiative",
|
|
4066
|
+
description: "Get one initiative 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_runs to see what the coding agent already did. A failed plan carries planFailure: the cause, what it means, when, and by whom. situation says in one sentence where it stands, whose turn it is and the call that moves it on. The same as fields: lifecycle says whose turn it is and what moves it on: cta start_plan is taiga_queue_initiative, retry_plan taiga_plan_again (planFailureCause says why the plan stopped), start_build taiga_approve_plan, resume_build taiga_resume_initiative, 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.",
|
|
4067
|
+
inputSchema: z.object({ initiativeId }),
|
|
3567
4068
|
annotations: READ_ONLY
|
|
3568
4069
|
}, (params) => runTool(async () => {
|
|
3569
|
-
const item = (await client.getBacklogItem(params.
|
|
3570
|
-
|
|
4070
|
+
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
4071
|
+
const row = item;
|
|
4072
|
+
if (item.planningStatus !== "failed") return toolResult(withSituation(row));
|
|
3571
4073
|
const ownCause = item.planFailureCause;
|
|
3572
|
-
const planFailure = await planFailureOf(client, params.
|
|
3573
|
-
return toolResult(planFailure ? {
|
|
4074
|
+
const planFailure = await planFailureOf(client, params.initiativeId, ownCause);
|
|
4075
|
+
return toolResult(withSituation(row, planFailure ? {
|
|
3574
4076
|
...item,
|
|
3575
4077
|
planFailure
|
|
3576
|
-
} : item);
|
|
4078
|
+
} : item));
|
|
3577
4079
|
}));
|
|
3578
4080
|
server.registerTool("taiga_get_plan", {
|
|
3579
4081
|
title: "Get the active plan",
|
|
3580
4082
|
description: "Get an initiative's active plan with its steps, in order: each step's title, layer, dependencies (the orderIndex of the steps it builds on), the files it writes and reads and the resources it creates. That is enough to approve or reject it (taiga_approve_plan, taiga_reject_plan) and to say what a run will do; pass full: true for each step's complete instructions, several thousand tokens on a large plan. NOT_FOUND when the initiative has no active plan yet (planningStatus says whether one is being generated).",
|
|
3581
4083
|
inputSchema: z.object({
|
|
3582
|
-
|
|
4084
|
+
initiativeId,
|
|
3583
4085
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
|
|
3584
4086
|
}),
|
|
3585
4087
|
annotations: READ_ONLY
|
|
3586
4088
|
}, (params) => runTool(async () => {
|
|
3587
|
-
const plan = (await client.getActivePlan(params.
|
|
4089
|
+
const plan = (await client.getActivePlan(params.initiativeId)).data;
|
|
3588
4090
|
if (params.full) return toolResult(plan);
|
|
3589
4091
|
const steps = Array.isArray(plan.steps) ? plan.steps : [];
|
|
3590
4092
|
return toolResult({
|
|
@@ -3615,18 +4117,18 @@ function registerRequest(server, client) {
|
|
|
3615
4117
|
}));
|
|
3616
4118
|
}
|
|
3617
4119
|
function registerMove(server, client) {
|
|
3618
|
-
server.registerTool("
|
|
3619
|
-
title: "Move
|
|
3620
|
-
description: `Move an initiative on the board, or order it within a column: backlog and todo are yours and the line never moves an initiative out of them; queue puts it in the line (
|
|
4120
|
+
server.registerTool("taiga_move_initiative", {
|
|
4121
|
+
title: "Move initiative on the board",
|
|
4122
|
+
description: `Move an initiative on the board, or order it within a column: backlog and todo are yours and the line never moves an initiative out of them; queue puts it in the line (taiga_queue_initiative says more); done and canceled finish it by hand. The queue is the only way to start work: there is no "plan this now", and nothing is moved into Build by a person. Moving an initiative out of Build stops the plan being written or the run in flight: that is how work is stopped, and it throws that work away. A move to ${PR_CLOSING_STATES.join(", ")} CLOSES a pull request the initiative is still waiting on (a move to queue keeps it, and done leaves it to be merged); Taiga leaves a comment on the pull request saying why. The state machine refuses a move that is not allowed (CONFLICT). A move into queue starts paid work and needs products:write. Ask the person before a move that starts work, stops work or closes a pull request. archived: true files the initiative where it stands (pass its current column as state; never from the Build station, plan or build) and archived: false brings it back; archived work is left out of lists unless includeArchived asks for it.`,
|
|
3621
4123
|
inputSchema: z.object({
|
|
3622
|
-
|
|
4124
|
+
initiativeId,
|
|
3623
4125
|
state: z.enum(USER_DROP_STATES).describe("The column to move into"),
|
|
3624
|
-
afterId: idInput("
|
|
4126
|
+
afterId: idInput("initiative", "taiga_list_initiatives gives it").nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
|
|
3625
4127
|
archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
|
|
3626
4128
|
}),
|
|
3627
4129
|
annotations: OPERATE
|
|
3628
4130
|
}, (params) => runTool(async () => {
|
|
3629
|
-
const {
|
|
4131
|
+
const { initiativeId: id, ...body } = params;
|
|
3630
4132
|
if (body.state === "queue" && !client.hasCapability("products:write")) return toolError("FORBIDDEN", "A move into queue starts agent work and needs products:write, which this key lacks.");
|
|
3631
4133
|
return outcome((await client.transitionInitiative(id, body)).data);
|
|
3632
4134
|
}));
|
|
@@ -3636,20 +4138,20 @@ const BUILD_STATION = /* @__PURE__ */ new Set(["plan", "build"]);
|
|
|
3636
4138
|
function registerPlanAgain(server, client) {
|
|
3637
4139
|
server.registerTool("taiga_plan_again", {
|
|
3638
4140
|
title: "Plan an initiative again",
|
|
3639
|
-
description: "Supersede the plan of the initiative at the Build station and ask the planner for a new one: for a plan that failed, a plan you do not want built, or a run whose direction was wrong once it has stopped. The earlier plan is kept, and every run records which plan version it built; a pull request still open stays open. Offered only when the turn is yours: refused (CONFLICT) while an agent holds it (planning, building, reviewing). Also refused while a dependency is still in flight or a UI direction has no chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the Build station, put the initiative in the queue instead (
|
|
4141
|
+
description: "Supersede the plan of the initiative at the Build station and ask the planner for a new one: for a plan that failed, a plan you do not want built, or a run whose direction was wrong once it has stopped. The earlier plan is kept, and every run records which plan version it built; a pull request still open stays open. Offered only when the turn is yours: refused (CONFLICT) while an agent holds it (planning, building, reviewing). Also refused while a dependency is still in flight or a UI direction has no chosen concept, unless force says to plan anyway, which only the person decides. Anywhere off the Build station, put the initiative in the queue instead (taiga_queue_initiative): the queue is the only way to start work. To send a plan back with direction, taiga_reject_plan. Starts a planner run.",
|
|
3640
4142
|
inputSchema: z.object({
|
|
3641
|
-
|
|
4143
|
+
initiativeId,
|
|
3642
4144
|
force: z.boolean().optional().describe("Plan anyway past an unsettled dependency or an unchosen UI direction; only when the person says so")
|
|
3643
4145
|
}),
|
|
3644
4146
|
annotations: OPERATE
|
|
3645
4147
|
}, (params) => runTool(async () => {
|
|
3646
|
-
const item = (await client.getBacklogItem(params.
|
|
3647
|
-
if (!BUILD_STATION.has(String(item.state))) return toolError("CONFLICT", `This initiative is in ${String(item.state)}, not at the Build station. The queue is the only way to start work: put it in the queue with
|
|
4148
|
+
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
4149
|
+
if (!BUILD_STATION.has(String(item.state))) return toolError("CONFLICT", `This initiative is in ${String(item.state)}, not at the Build station. The queue is the only way to start work: put it in the queue with taiga_queue_initiative and it is planned when its turn comes.`);
|
|
3648
4150
|
const body = {
|
|
3649
4151
|
state: "plan",
|
|
3650
4152
|
...params.force ? { force: true } : {}
|
|
3651
4153
|
};
|
|
3652
|
-
return outcome((await client.transitionInitiative(params.
|
|
4154
|
+
return outcome((await client.transitionInitiative(params.initiativeId, body)).data);
|
|
3653
4155
|
}));
|
|
3654
4156
|
}
|
|
3655
4157
|
/** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
|
|
@@ -3693,17 +4195,17 @@ async function placeInQueue(client, item) {
|
|
|
3693
4195
|
}
|
|
3694
4196
|
const autoBuildInput = z.boolean().nullable().describe("false stops the line at this initiative's plan for a person to read it; true builds it as soon as it lands; null follows the product's default");
|
|
3695
4197
|
function registerQueue(server, client) {
|
|
3696
|
-
server.registerTool("
|
|
3697
|
-
title: "Queue
|
|
3698
|
-
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.
|
|
4198
|
+
server.registerTool("taiga_queue_initiative", {
|
|
4199
|
+
title: "Queue initiative",
|
|
4200
|
+
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. 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.",
|
|
3699
4201
|
inputSchema: z.object({
|
|
3700
|
-
|
|
3701
|
-
afterId: idInput("
|
|
4202
|
+
initiativeId,
|
|
4203
|
+
afterId: idInput("initiative", "taiga_list_initiatives 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"),
|
|
3702
4204
|
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")
|
|
3703
4205
|
}),
|
|
3704
4206
|
annotations: OPERATE
|
|
3705
4207
|
}, (params) => runTool(async () => {
|
|
3706
|
-
const {
|
|
4208
|
+
const { initiativeId: id, autoBuild, ...placement } = params;
|
|
3707
4209
|
if (autoBuild !== void 0) await client.setAutoBuild(id, autoBuild);
|
|
3708
4210
|
const item = (await client.transitionInitiative(id, {
|
|
3709
4211
|
state: "queue",
|
|
@@ -3714,7 +4216,7 @@ function registerQueue(server, client) {
|
|
|
3714
4216
|
...outcomeOf(item),
|
|
3715
4217
|
reviewGate: gate,
|
|
3716
4218
|
queue,
|
|
3717
|
-
...gate && queue ? {} : { note: "Part of the outcome could not be read;
|
|
4219
|
+
...gate && queue ? {} : { note: "Part of the outcome could not be read; taiga_get_line and taiga_get_product say the rest." }
|
|
3718
4220
|
});
|
|
3719
4221
|
}));
|
|
3720
4222
|
}
|
|
@@ -3723,12 +4225,12 @@ function registerReviewGate(server, client) {
|
|
|
3723
4225
|
title: "Set an initiative's review gate",
|
|
3724
4226
|
description: "Set whether an initiative's plan is built as soon as it lands (true) or waits for a person to read it and approve or reject it (false); null follows the product's default. Standing: it applies to every plan the initiative gets, including one being written now. Opening the gate on a plan that is already waiting starts its build at once. Returns the initiative with the gate that now applies.",
|
|
3725
4227
|
inputSchema: z.object({
|
|
3726
|
-
|
|
4228
|
+
initiativeId,
|
|
3727
4229
|
autoBuild: autoBuildInput
|
|
3728
4230
|
}),
|
|
3729
4231
|
annotations: OPERATE_SETTING
|
|
3730
4232
|
}, (params) => runTool(async () => {
|
|
3731
|
-
const item = (await client.setAutoBuild(params.
|
|
4233
|
+
const item = (await client.setAutoBuild(params.initiativeId, params.autoBuild)).data;
|
|
3732
4234
|
const gate = await reviewGate(client, item);
|
|
3733
4235
|
return toolResult({
|
|
3734
4236
|
...outcomeOf(item),
|
|
@@ -3738,12 +4240,12 @@ function registerReviewGate(server, client) {
|
|
|
3738
4240
|
}));
|
|
3739
4241
|
}
|
|
3740
4242
|
function registerResume(server, client) {
|
|
3741
|
-
server.registerTool("
|
|
3742
|
-
title: "Resume
|
|
4243
|
+
server.registerTool("taiga_resume_initiative", {
|
|
4244
|
+
title: "Resume initiative",
|
|
3743
4245
|
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.",
|
|
3744
|
-
inputSchema: z.object({
|
|
4246
|
+
inputSchema: z.object({ initiativeId }),
|
|
3745
4247
|
annotations: OPERATE
|
|
3746
|
-
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.
|
|
4248
|
+
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
|
|
3747
4249
|
}
|
|
3748
4250
|
function registerPlanVerdicts(server, client) {
|
|
3749
4251
|
server.registerTool("taiga_approve_plan", {
|
|
@@ -3763,173 +4265,234 @@ function registerPlanVerdicts(server, client) {
|
|
|
3763
4265
|
}, (params) => runTool(async () => outcome((await client.rejectPlan(params.planId, { comment: params.comment })).data)));
|
|
3764
4266
|
}
|
|
3765
4267
|
//#endregion
|
|
3766
|
-
//#region src/tools/
|
|
3767
|
-
/**
|
|
3768
|
-
|
|
4268
|
+
//#region src/tools/maintenance.ts
|
|
4269
|
+
/**
|
|
4270
|
+
* Maintaining (docs.tai.ga, Operate): what the sweeps found in a product's
|
|
4271
|
+
* repository and where it stands against the organization's own policies,
|
|
4272
|
+
* and the two things a person does with a finding, Ignore and Fix. Sweeps run
|
|
4273
|
+
* themselves and nothing is marked fixed by hand: a finding resolves when the
|
|
4274
|
+
* next sweep no longer detects it, so there is no tool for either.
|
|
4275
|
+
*/
|
|
4276
|
+
/** A finding as a list row: enough to decide whether to open, fix or ignore it. */
|
|
4277
|
+
const FINDING_ROW_FIELDS = [
|
|
3769
4278
|
"id",
|
|
3770
|
-
"
|
|
3771
|
-
"
|
|
4279
|
+
"kind",
|
|
4280
|
+
"severity",
|
|
4281
|
+
"tier",
|
|
4282
|
+
"status",
|
|
4283
|
+
"title",
|
|
4284
|
+
"ecosystem",
|
|
4285
|
+
"packageName",
|
|
4286
|
+
"currentVersion",
|
|
4287
|
+
"recommendedVersion",
|
|
4288
|
+
"advisoryId",
|
|
4289
|
+
"location",
|
|
4290
|
+
"sourcePaths",
|
|
4291
|
+
"productRepositoryId",
|
|
4292
|
+
"ownership",
|
|
4293
|
+
"remediableBy",
|
|
4294
|
+
"controls",
|
|
3772
4295
|
"backlogItemId",
|
|
3773
|
-
"
|
|
3774
|
-
"
|
|
4296
|
+
"detectedAt",
|
|
4297
|
+
"ignoreReason",
|
|
4298
|
+
"ignoreExpiresAt"
|
|
4299
|
+
];
|
|
4300
|
+
/** A sweep as the record of when the product was last checked, and whether that check held. */
|
|
4301
|
+
const SWEEP_FIELDS = [
|
|
4302
|
+
"id",
|
|
4303
|
+
"trigger",
|
|
3775
4304
|
"status",
|
|
3776
|
-
"
|
|
3777
|
-
"
|
|
3778
|
-
"
|
|
3779
|
-
"
|
|
3780
|
-
"baseBranch",
|
|
3781
|
-
"completedSteps",
|
|
3782
|
-
"totalSteps",
|
|
3783
|
-
"prNumber",
|
|
3784
|
-
"prUrl",
|
|
3785
|
-
"prState",
|
|
3786
|
-
"prDraft",
|
|
3787
|
-
"prMerged",
|
|
3788
|
-
"actions",
|
|
4305
|
+
"scanners",
|
|
4306
|
+
"findingsCount",
|
|
4307
|
+
"newFindingsCount",
|
|
4308
|
+
"error",
|
|
3789
4309
|
"createdAt",
|
|
3790
|
-
"startedAt",
|
|
3791
4310
|
"completedAt"
|
|
3792
4311
|
];
|
|
3793
|
-
/**
|
|
3794
|
-
const
|
|
4312
|
+
/** A policy violation in a list: what it is, how bad, and what is being done about it. */
|
|
4313
|
+
const VIOLATION_FIELDS = [
|
|
3795
4314
|
"id",
|
|
3796
|
-
"
|
|
3797
|
-
"
|
|
4315
|
+
"controlId",
|
|
4316
|
+
"policyType",
|
|
4317
|
+
"severity",
|
|
3798
4318
|
"status",
|
|
3799
|
-
"failedReason",
|
|
3800
|
-
"completedSteps",
|
|
3801
|
-
"totalSteps",
|
|
3802
|
-
"prUrl",
|
|
3803
|
-
"actions"
|
|
3804
|
-
];
|
|
3805
|
-
/**
|
|
3806
|
-
* Where a run that ended without finishing stopped, from its steps and its
|
|
3807
|
-
* hardening result: a failed run with every step completed reads as nothing
|
|
3808
|
-
* failed, and retry_from_failed_step then reads as a mistake, when the agent
|
|
3809
|
-
* stalled in the hardening pass after the steps and the retry continues it.
|
|
3810
|
-
* Null for a run still going or that finished.
|
|
3811
|
-
*/
|
|
3812
|
-
function stoppedIn(run, steps) {
|
|
3813
|
-
if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
|
|
3814
|
-
if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
|
|
3815
|
-
const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
|
|
3816
|
-
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)})`;
|
|
3817
|
-
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.";
|
|
3818
|
-
return "after the steps and hardening, at the pull request";
|
|
3819
|
-
}
|
|
3820
|
-
/** How many products the org-wide list names; past this a row keeps its productId only. */
|
|
3821
|
-
const NAMED_PRODUCTS = 100;
|
|
3822
|
-
const buildId = idInput("build run", "taiga_list_builds gives it");
|
|
3823
|
-
/** A step without its instructions: what it is, how it stands and what it changed. */
|
|
3824
|
-
const STEP_SUMMARY_FIELDS = [
|
|
3825
|
-
"stepOrder",
|
|
3826
4319
|
"title",
|
|
3827
|
-
"
|
|
3828
|
-
"
|
|
3829
|
-
"
|
|
3830
|
-
"
|
|
3831
|
-
"
|
|
3832
|
-
"error",
|
|
3833
|
-
"errorClassification",
|
|
3834
|
-
"startedAt",
|
|
3835
|
-
"completedAt"
|
|
4320
|
+
"summary",
|
|
4321
|
+
"work",
|
|
4322
|
+
"unconfirmedSince",
|
|
4323
|
+
"verifyRequestedAt",
|
|
4324
|
+
"assessedAt"
|
|
3836
4325
|
];
|
|
3837
|
-
/** Past this many characters a step's headline is cut; the full read has the rest. */
|
|
3838
|
-
const HEADLINE_LIMIT = 600;
|
|
3839
|
-
/**
|
|
3840
|
-
* The first paragraph of a step's summary: the agent writes its result
|
|
3841
|
-
* there (what changed, which checks passed), and the detail after it.
|
|
3842
|
-
*/
|
|
3843
|
-
function summaryHeadline(summary) {
|
|
3844
|
-
if (typeof summary !== "string" || !summary.trim()) return null;
|
|
3845
|
-
const first = summary.trim().split(/\n\s*\n/, 1)[0].trim();
|
|
3846
|
-
return first.length > HEADLINE_LIMIT ? `${first.slice(0, HEADLINE_LIMIT).trimEnd()}...` : first;
|
|
3847
|
-
}
|
|
3848
4326
|
/**
|
|
3849
|
-
*
|
|
3850
|
-
*
|
|
3851
|
-
*
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
const
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
const product = nameById.get(String(row.productId));
|
|
3865
|
-
return product ? {
|
|
3866
|
-
...row,
|
|
3867
|
-
product
|
|
3868
|
-
} : row;
|
|
3869
|
-
});
|
|
3870
|
-
const note = !rows.some((row) => !("product" in row)) ? null : !products ? "The products could not be read, so rows carry productId only; taiga_get_product names one." : products.nextCursor !== null ? `Only the first ${NAMED_PRODUCTS} products could be named; a row without product carries productId only.` : "A row without product belongs to a product the key cannot list (deleted since, or not visible to it); it carries productId only.";
|
|
4327
|
+
* How bad a product's policy standing is, as counts: the controls something is
|
|
4328
|
+
* recorded against, the open violations by severity, the resolved ones. The
|
|
4329
|
+
* API's standing carries the lists only; this tool and the product context
|
|
4330
|
+
* both count them here.
|
|
4331
|
+
*/
|
|
4332
|
+
function standingSummary(standing) {
|
|
4333
|
+
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
4334
|
+
const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
|
|
4335
|
+
const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
|
|
4336
|
+
const listed = controls.filter((c) => Number(c["divergenceCount"] ?? 0) > 0 || Number(c["findingCount"] ?? 0) > 0);
|
|
4337
|
+
const bySeverity = {};
|
|
4338
|
+
for (const violation of violations) {
|
|
4339
|
+
const severity = String(violation["severity"] ?? "unknown");
|
|
4340
|
+
bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
|
|
4341
|
+
}
|
|
3871
4342
|
return {
|
|
3872
|
-
|
|
3873
|
-
|
|
4343
|
+
controls: listed.length,
|
|
4344
|
+
violations: violations.length,
|
|
4345
|
+
bySeverity,
|
|
4346
|
+
resolvedViolations: resolved
|
|
3874
4347
|
};
|
|
3875
4348
|
}
|
|
3876
|
-
|
|
4349
|
+
const findingId = idInput("finding", "taiga_list_findings gives it");
|
|
4350
|
+
function registerMaintenanceTools(server, client) {
|
|
3877
4351
|
if (!client.hasCapability("products:read")) return;
|
|
3878
|
-
server.registerTool("
|
|
3879
|
-
title: "
|
|
3880
|
-
description: "
|
|
4352
|
+
server.registerTool("taiga_get_maintenance", {
|
|
4353
|
+
title: "Get a product's maintenance standing",
|
|
4354
|
+
description: "Where a product's repository stands: open findings by severity and kind, the most pressing ones, and the latest sweeps, so you can tell when the product was last checked and whether that check succeeded. A quiet list from a sweep that last ran long ago, or that failed, is not an all-clear; a product never swept says so. Sweeps run on their own after a push and periodically; nothing here starts one. This is the overview of the scanners alone; taiga_list_findings lists each finding to act on, and taiga_get_policy_standing judges the product against the policies (scanner findings and the documents).",
|
|
4355
|
+
inputSchema: z.object({ productId }),
|
|
4356
|
+
annotations: READ_ONLY
|
|
4357
|
+
}, (params) => runTool(async () => {
|
|
4358
|
+
const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
|
|
4359
|
+
const latest = pickFields(sweeps.data, SWEEP_FIELDS);
|
|
4360
|
+
const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
|
|
4361
|
+
return toolResult({
|
|
4362
|
+
...standing,
|
|
4363
|
+
latestSweeps: latest,
|
|
4364
|
+
...latest.length === 0 ? { note: "This product has never been swept: no result is not an all-clear." } : {}
|
|
4365
|
+
});
|
|
4366
|
+
}));
|
|
4367
|
+
server.registerTool("taiga_list_findings", {
|
|
4368
|
+
title: "List maintenance findings",
|
|
4369
|
+
description: "List what the sweeps found in a product's repository: vulnerable or outdated dependencies, committed secrets, insecure code (sast) and infrastructure misconfigurations (iac_misconfig). Each row carries its severity and its priority tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), where it is, who can fix it (remediableBy: coding_agent, or upstream_only for a submodule or a dependency with no fixed version), the policy controls it breaches, and the initiative already fixing it (initiativeId). Omit status for the findings that need action; work from the top tier down. Read one in full with taiga_get_finding. taiga_get_maintenance has the counts and when the last sweep ran, which says whether an empty list can be trusted.",
|
|
3881
4370
|
inputSchema: z.object({
|
|
3882
|
-
productId
|
|
3883
|
-
|
|
3884
|
-
|
|
4371
|
+
productId,
|
|
4372
|
+
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
4373
|
+
severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
|
|
4374
|
+
kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
|
|
3885
4375
|
...pagination
|
|
3886
4376
|
}),
|
|
3887
4377
|
annotations: READ_ONLY
|
|
3888
4378
|
}, (params) => runTool(async () => {
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
}
|
|
3895
|
-
|
|
4379
|
+
const { productId: id, ...filters } = params;
|
|
4380
|
+
const result = await client.listFindings(id, filters);
|
|
4381
|
+
return toolResult({
|
|
4382
|
+
...page(pickFields(result.data, FINDING_ROW_FIELDS), result.nextCursor),
|
|
4383
|
+
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
|
|
4384
|
+
});
|
|
4385
|
+
}));
|
|
4386
|
+
server.registerTool("taiga_get_finding", {
|
|
4387
|
+
title: "Get a maintenance finding",
|
|
4388
|
+
description: "Read one finding in full: what it is, how likely it is to be exploited (EPSS, the known-exploited list), the advisory and the fixed version, the place in the code, the policy controls it breaches, why it is ranked where it is, its history (reintroduced, previously resolved) and the remediation under way. A secret's value is never stored; the finding names the rule and the location only.",
|
|
4389
|
+
inputSchema: z.object({ findingId }),
|
|
4390
|
+
annotations: READ_ONLY
|
|
4391
|
+
}, (params) => runTool(async () => {
|
|
4392
|
+
const { runId, ...finding } = (await client.getFinding(params.findingId)).data;
|
|
4393
|
+
return toolResult(runId === void 0 ? finding : {
|
|
4394
|
+
sweepId: runId,
|
|
4395
|
+
...finding
|
|
4396
|
+
});
|
|
3896
4397
|
}));
|
|
3897
|
-
server.registerTool("
|
|
3898
|
-
title: "Get
|
|
3899
|
-
description: "
|
|
4398
|
+
server.registerTool("taiga_get_policy_standing", {
|
|
4399
|
+
title: "Get a product's policy standing",
|
|
4400
|
+
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 is breached when open scanner findings map to it, diverged when the assessment of the product documents found them contradicting it (a control can be both; state names the stronger, breached). A violation's work is the initiative raised for it; live: false means that initiative is done or canceled without the violation going away, so it needs a person or a new fix. A control breached by scanner findings counts them by tier (findingsByTier): a breach of routine findings only, such as secrets in test fixtures, is not an urgent one. 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.",
|
|
3900
4401
|
inputSchema: z.object({
|
|
3901
|
-
|
|
3902
|
-
full: z.boolean().optional().describe("Carry each
|
|
4402
|
+
productId,
|
|
4403
|
+
full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
|
|
3903
4404
|
}),
|
|
3904
4405
|
annotations: READ_ONLY
|
|
3905
4406
|
}, (params) => runTool(async () => {
|
|
3906
|
-
const
|
|
3907
|
-
const
|
|
3908
|
-
const
|
|
3909
|
-
const
|
|
3910
|
-
const
|
|
4407
|
+
const standing = (await client.getPolicyStanding(params.productId)).data;
|
|
4408
|
+
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
4409
|
+
const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
|
|
4410
|
+
const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
|
|
4411
|
+
const summary = standingSummary(standing);
|
|
3911
4412
|
if (params.full) return toolResult({
|
|
3912
|
-
|
|
3913
|
-
...
|
|
3914
|
-
steps: steps.data
|
|
4413
|
+
summary,
|
|
4414
|
+
...standing
|
|
3915
4415
|
});
|
|
3916
4416
|
return toolResult({
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
4417
|
+
summary,
|
|
4418
|
+
productId: standing.productId,
|
|
4419
|
+
hasPolicies: standing.hasPolicies,
|
|
4420
|
+
assessment: standing.assessment,
|
|
4421
|
+
documents: standing.documents,
|
|
4422
|
+
controls: pickFields(listed, [
|
|
4423
|
+
"controlId",
|
|
4424
|
+
"policyType",
|
|
4425
|
+
"text",
|
|
4426
|
+
"state",
|
|
4427
|
+
"divergenceCount",
|
|
4428
|
+
"findingCount",
|
|
4429
|
+
"findingsByTier",
|
|
4430
|
+
"plannedCount"
|
|
4431
|
+
]),
|
|
4432
|
+
violations: pickFields(violations, VIOLATION_FIELDS)
|
|
3923
4433
|
});
|
|
3924
4434
|
}));
|
|
3925
4435
|
if (!client.hasCapability("products:write")) return;
|
|
3926
|
-
server.registerTool("
|
|
3927
|
-
title: "
|
|
3928
|
-
description: "
|
|
3929
|
-
inputSchema: z.object({
|
|
4436
|
+
server.registerTool("taiga_ignore_finding", {
|
|
4437
|
+
title: "Ignore or reopen a finding",
|
|
4438
|
+
description: "Ignore a finding that does not apply or whose risk is accepted, with the reason (false_positive, not_reachable, accepted_risk, wont_fix), an optional note and an optional expiry after which it resurfaces if it still exists; or reopen an ignored one (ignored: false). Ignored findings are kept and reviewable. There is no \"mark fixed\": a finding resolves when the next sweep no longer detects it. For a secret that is only in history, rotate the credential outside Taiga, then ignore it with a reason. Ignoring hides a shared signal and can record the team's acceptance of a risk, which is the person's decision: ask the person first.",
|
|
4439
|
+
inputSchema: z.object({
|
|
4440
|
+
findingId,
|
|
4441
|
+
ignored: z.boolean().describe("true ignores the finding, false reopens it"),
|
|
4442
|
+
reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe("Why it is ignored; required when ignoring"),
|
|
4443
|
+
note: z.string().trim().max(280).optional().describe("A short justification, kept for audit"),
|
|
4444
|
+
expiresAt: z.string().datetime({ offset: true }).optional().describe("When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)")
|
|
4445
|
+
}),
|
|
4446
|
+
annotations: OPERATE
|
|
4447
|
+
}, (params) => runTool(async () => {
|
|
4448
|
+
if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
|
|
4449
|
+
const body = params.ignored ? {
|
|
4450
|
+
status: "ignored",
|
|
4451
|
+
ignoreReason: params.reason,
|
|
4452
|
+
...params.note !== void 0 ? { ignoreNote: params.note } : {},
|
|
4453
|
+
...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
|
|
4454
|
+
} : { status: "open" };
|
|
4455
|
+
const finding = (await client.updateFinding(params.findingId, body)).data;
|
|
4456
|
+
return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, "ignoreNote"])[0]);
|
|
4457
|
+
}));
|
|
4458
|
+
server.registerTool("taiga_fix_finding", {
|
|
4459
|
+
title: "Fix a finding",
|
|
4460
|
+
description: "Ask for a finding's fix: Taiga raises an initiative and adds it to the end of the product's queue, where the line plans it in turn; whether the build then waits for a person is the initiative's review gate, as for any initiative. The fix is a normal run that ends in a pull request you review and merge. With wholeGroup: true it fixes the finding's whole group in one initiative, as the page does: for a dependency, every open advisory on that package at that installed version in one version bump; for code or infrastructure, every location the same rule flagged. Absent for a secret only in history, anything in a submodule, and a dependency with no fixed version (remediableBy says upstream_only); the API refuses those. Starts paid work.",
|
|
4461
|
+
inputSchema: z.object({
|
|
4462
|
+
productId,
|
|
4463
|
+
findingId,
|
|
4464
|
+
wholeGroup: z.boolean().optional().describe("Fix every finding in this finding's group (its package and version, or its rule) together")
|
|
4465
|
+
}),
|
|
4466
|
+
annotations: OPERATE
|
|
4467
|
+
}, (params) => runTool(async () => {
|
|
4468
|
+
if (!params.wholeGroup) {
|
|
4469
|
+
const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
|
|
4470
|
+
return toolResult({
|
|
4471
|
+
backlogItemId: fixed.backlogItemId,
|
|
4472
|
+
finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null
|
|
4473
|
+
});
|
|
4474
|
+
}
|
|
4475
|
+
const finding = (await client.getFinding(params.findingId)).data;
|
|
4476
|
+
if (finding.productId !== params.productId) return toolError("NOT_FOUND", "No such finding in this product.");
|
|
4477
|
+
return toolResult((await client.remediatePackage(params.productId, {
|
|
4478
|
+
ecosystem: String(finding.ecosystem),
|
|
4479
|
+
packageName: String(finding.packageName),
|
|
4480
|
+
currentVersion: typeof finding.currentVersion === "string" ? finding.currentVersion : null,
|
|
4481
|
+
productRepositoryId: typeof finding.productRepositoryId === "string" ? finding.productRepositoryId : null
|
|
4482
|
+
})).data);
|
|
4483
|
+
}));
|
|
4484
|
+
server.registerTool("taiga_fix_policy_violation", {
|
|
4485
|
+
title: "Fix policy violations",
|
|
4486
|
+
description: "Ask for the fix of a product's policy violations: name a control to cover every violation recorded against it that has no initiative yet, or a single violation (violationId) when the control has more than one. Taiga raises one initiative and adds it to the end of the product's queue, exactly as a finding fix does. The violation stays listed while the work is under way: only the next assessment can record that the documents no longer show it. Starts paid work.",
|
|
4487
|
+
inputSchema: z.object({
|
|
4488
|
+
productId,
|
|
4489
|
+
controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
|
|
4490
|
+
violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
|
|
4491
|
+
}),
|
|
3930
4492
|
annotations: OPERATE
|
|
3931
4493
|
}, (params) => runTool(async () => {
|
|
3932
|
-
|
|
4494
|
+
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.");
|
|
4495
|
+
return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
|
|
3933
4496
|
}));
|
|
3934
4497
|
}
|
|
3935
4498
|
//#endregion
|
|
@@ -3953,9 +4516,9 @@ const REPOSITORY_SUMMARY_FIELDS = [
|
|
|
3953
4516
|
"effectiveBranch",
|
|
3954
4517
|
"isManaged"
|
|
3955
4518
|
];
|
|
3956
|
-
/** How many of the latest runs to carry;
|
|
4519
|
+
/** How many of the latest runs to carry; taiga_list_runs pages the rest. */
|
|
3957
4520
|
const BUILDS_IN_CONTEXT = 10;
|
|
3958
|
-
/** A run as the bundle carries it: enough to say what was built and where;
|
|
4521
|
+
/** A run as the bundle carries it: enough to say what was built and where; taiga_get_run has the rest. */
|
|
3959
4522
|
const CONTEXT_BUILD_FIELDS = [
|
|
3960
4523
|
"id",
|
|
3961
4524
|
"runNumber",
|
|
@@ -3965,6 +4528,8 @@ const CONTEXT_BUILD_FIELDS = [
|
|
|
3965
4528
|
"failedReason",
|
|
3966
4529
|
"completedSteps",
|
|
3967
4530
|
"totalSteps",
|
|
4531
|
+
"baseBranch",
|
|
4532
|
+
"prNumber",
|
|
3968
4533
|
"prUrl",
|
|
3969
4534
|
"prState",
|
|
3970
4535
|
"prMerged",
|
|
@@ -4022,7 +4587,7 @@ const DOCUMENT_DEPTHS = [
|
|
|
4022
4587
|
const BACKLOG_PAGE_LIMIT = 10;
|
|
4023
4588
|
/**
|
|
4024
4589
|
* Open items the bundle carries, the newest of those read; past this it says truncated and
|
|
4025
|
-
*
|
|
4590
|
+
* taiga_list_initiatives pages the rest. Keeps a product with hundreds of
|
|
4026
4591
|
* open initiatives inside the tool result the document budget is sized for.
|
|
4027
4592
|
*/
|
|
4028
4593
|
const BACKLOG_IN_CONTEXT = 50;
|
|
@@ -4032,7 +4597,10 @@ const PARTS = {
|
|
|
4032
4597
|
policies: "tenant:documents:read",
|
|
4033
4598
|
backlog: "backlog:read",
|
|
4034
4599
|
repositories: "products:read",
|
|
4035
|
-
builds: "products:read"
|
|
4600
|
+
builds: "products:read",
|
|
4601
|
+
attention: "products:read",
|
|
4602
|
+
maintenance: "products:read",
|
|
4603
|
+
standing: "products:read"
|
|
4036
4604
|
};
|
|
4037
4605
|
/** What the API said when a part failed, in the words the bundle reports. */
|
|
4038
4606
|
function unavailableReason(reason) {
|
|
@@ -4188,9 +4756,9 @@ async function backlogPart(client, productId) {
|
|
|
4188
4756
|
const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
|
|
4189
4757
|
if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
|
|
4190
4758
|
return {
|
|
4191
|
-
items:
|
|
4759
|
+
items: situatedRows(rows.slice(-50)),
|
|
4192
4760
|
truncated,
|
|
4193
|
-
...pageCapped ? { note: "More than 1000 open items in one status: only the oldest of them were read, so the newest may be missing here;
|
|
4761
|
+
...pageCapped ? { note: "More than 1000 open items in one status: only the oldest of them were read, so the newest may be missing here; taiga_list_initiatives pages the rest." } : {}
|
|
4194
4762
|
};
|
|
4195
4763
|
}
|
|
4196
4764
|
/**
|
|
@@ -4206,7 +4774,53 @@ async function repositoriesPart(client, productId) {
|
|
|
4206
4774
|
}
|
|
4207
4775
|
async function buildsPart(client, productId) {
|
|
4208
4776
|
const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
|
|
4209
|
-
return pickFields(rows, CONTEXT_BUILD_FIELDS);
|
|
4777
|
+
return pickFields(rows, CONTEXT_BUILD_FIELDS).map(readableRun);
|
|
4778
|
+
}
|
|
4779
|
+
/**
|
|
4780
|
+
* What waits on a person for this product, counted by kind (PRO-2800): the
|
|
4781
|
+
* reviewing agent called four tools to answer "how does it stand"; the
|
|
4782
|
+
* counts say whether taiga_list_action_required is worth a call.
|
|
4783
|
+
*/
|
|
4784
|
+
async function attentionPart(client, productId) {
|
|
4785
|
+
const result = await client.listInbox({
|
|
4786
|
+
productId,
|
|
4787
|
+
limit: 100,
|
|
4788
|
+
withTotal: true
|
|
4789
|
+
});
|
|
4790
|
+
const now = Date.now();
|
|
4791
|
+
const rows = result.data;
|
|
4792
|
+
const byKind = {};
|
|
4793
|
+
for (const row of rows) byKind[String(row["kind"])] = (byKind[String(row["kind"])] ?? 0) + 1;
|
|
4794
|
+
const snoozed = rows.filter((row) => row["status"] === "snoozed" && typeof row["snoozedUntil"] === "string" && Date.parse(row["snoozedUntil"]) > now).length;
|
|
4795
|
+
return {
|
|
4796
|
+
open: typeof result.totalCount === "number" ? result.totalCount : rows.length,
|
|
4797
|
+
byKind,
|
|
4798
|
+
...snoozed > 0 ? { snoozed } : {},
|
|
4799
|
+
...result.nextCursor ? { note: "byKind and snoozed count the first 100; taiga_list_action_required pages the rest." } : {}
|
|
4800
|
+
};
|
|
4801
|
+
}
|
|
4802
|
+
/** The latest sweep and what it left open, as counts; taiga_list_findings lists them. */
|
|
4803
|
+
async function maintenancePart(client, productId) {
|
|
4804
|
+
const summary = (await client.getMaintenanceSummary(productId)).data;
|
|
4805
|
+
const last = summary["lastRun"];
|
|
4806
|
+
return {
|
|
4807
|
+
openFindings: summary["openFindings"],
|
|
4808
|
+
bySeverity: summary["severityCounts"],
|
|
4809
|
+
lastSweep: last ? {
|
|
4810
|
+
status: last["status"],
|
|
4811
|
+
completedAt: last["completedAt"]
|
|
4812
|
+
} : null,
|
|
4813
|
+
...last ? {} : { note: "Never swept: no findings is not an all-clear." }
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4816
|
+
/** Where the product stands against the policies, as counts; taiga_get_policy_standing has each violation. */
|
|
4817
|
+
async function standingPart(client, productId) {
|
|
4818
|
+
const summary = standingSummary((await client.getPolicyStanding(productId)).data);
|
|
4819
|
+
return {
|
|
4820
|
+
openViolations: summary.violations,
|
|
4821
|
+
bySeverity: summary.bySeverity,
|
|
4822
|
+
controlsViolated: summary.controls
|
|
4823
|
+
};
|
|
4210
4824
|
}
|
|
4211
4825
|
async function productContext(client, product, options) {
|
|
4212
4826
|
const productId = product.id;
|
|
@@ -4218,16 +4832,18 @@ async function productContext(client, product, options) {
|
|
|
4218
4832
|
policies: () => policiesPart(client),
|
|
4219
4833
|
backlog: () => backlogPart(client, productId),
|
|
4220
4834
|
repositories: () => repositoriesPart(client, productId),
|
|
4221
|
-
builds: () => buildsPart(client, productId)
|
|
4835
|
+
builds: () => buildsPart(client, productId),
|
|
4836
|
+
attention: () => attentionPart(client, productId),
|
|
4837
|
+
maintenance: () => maintenancePart(client, productId),
|
|
4838
|
+
standing: () => standingPart(client, productId)
|
|
4222
4839
|
};
|
|
4223
4840
|
const parts = Object.keys(fetchers).filter(granted);
|
|
4224
|
-
const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
|
|
4225
|
-
const nextStep = setupNextStep(product);
|
|
4841
|
+
const [settled, readiness] = await Promise.all([Promise.allSettled(parts.map((part) => fetchers[part]())), setupReadiness(client, product)]);
|
|
4226
4842
|
const bundle = { product: {
|
|
4227
4843
|
...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
|
|
4228
4844
|
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
4229
4845
|
defaultOrigin: defaultOriginFor(product),
|
|
4230
|
-
...
|
|
4846
|
+
...readiness ? { readiness } : {}
|
|
4231
4847
|
} };
|
|
4232
4848
|
const unavailable = {};
|
|
4233
4849
|
parts.forEach((part, index) => {
|
|
@@ -4240,15 +4856,15 @@ async function productContext(client, product, options) {
|
|
|
4240
4856
|
...bundle,
|
|
4241
4857
|
notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
|
|
4242
4858
|
unavailable,
|
|
4243
|
-
readNext: "Read a policy with taiga_get_document (sections: the ids listed here, for only those), the specification's or architecture's sections or another product document with taiga_get_product_document,
|
|
4859
|
+
readNext: "Read a policy with taiga_get_document (sections: the ids listed here, for only those), the specification's or architecture's sections or another product document with taiga_get_product_document, an initiative with taiga_get_initiative (taiga_list_initiatives pages a truncated backlog) and a run with taiga_get_run."
|
|
4244
4860
|
};
|
|
4245
|
-
let characters = JSON.stringify(result, null, 2).length;
|
|
4861
|
+
let characters = JSON.stringify(inProductWords(result), null, 2).length;
|
|
4246
4862
|
for (let measured = -1; measured !== characters;) {
|
|
4247
4863
|
measured = characters;
|
|
4248
|
-
characters = JSON.stringify({
|
|
4864
|
+
characters = JSON.stringify(inProductWords({
|
|
4249
4865
|
...result,
|
|
4250
4866
|
characters: measured
|
|
4251
|
-
}, null, 2).length;
|
|
4867
|
+
}), null, 2).length;
|
|
4252
4868
|
}
|
|
4253
4869
|
return toolResult({
|
|
4254
4870
|
...result,
|
|
@@ -4259,7 +4875,7 @@ function registerContextTools(server, client) {
|
|
|
4259
4875
|
if (!client.hasCapability("products:read")) return;
|
|
4260
4876
|
server.registerTool("taiga_get_product_context", {
|
|
4261
4877
|
title: "Get product context",
|
|
4262
|
-
description: `
|
|
4878
|
+
description: `Where a product stands and everything a coding session starts from, in one call. Name the product by ID, slug or the git remote URL of a checkout. It carries: the product (and, while in setup, readiness: what it still needs and the next move); its factory with the instructions in full; the specification and architecture as outlines (section ids and sizes); every published policy as its section ids; the open initiatives (not the done or canceled ones: taiga_list_initiatives has those); the linked repositories; the latest runs, each with its outcome in a sentence; attention, what waits on a person for it, counted by kind; maintenance, open findings by severity and the last sweep; standing, open policy violations by severity. A part the API refused or failed is named under unavailable; a part the key cannot read under notGranted; neither is shown as empty. Before writing code, documents: budget brings the whole sections that fit, about ${DOCUMENT_BUDGET / 1e3}k characters each, and full brings them whole (tens of thousands of tokens on a large product). Unlike taiga_get_product, which is the product record and its settings alone. Call this first, then read what you need.`,
|
|
4263
4879
|
inputSchema: z.object({
|
|
4264
4880
|
productId: productId.optional(),
|
|
4265
4881
|
slug: slug.optional(),
|
|
@@ -4316,12 +4932,12 @@ function registerAutonomyTools(server, client) {
|
|
|
4316
4932
|
title: "Set whether an initiative's pull request merges on its own",
|
|
4317
4933
|
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.",
|
|
4318
4934
|
inputSchema: z.object({
|
|
4319
|
-
|
|
4935
|
+
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
|
|
4320
4936
|
autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
|
|
4321
4937
|
}),
|
|
4322
4938
|
annotations: OPERATE_SETTING
|
|
4323
4939
|
}, (params) => runTool(async () => {
|
|
4324
|
-
const item = (await client.updateBacklogItem(params.
|
|
4940
|
+
const item = (await client.updateBacklogItem(params.initiativeId, { autoMerge: params.autoMerge })).data;
|
|
4325
4941
|
return toolResult(pickFields([item], [
|
|
4326
4942
|
"id",
|
|
4327
4943
|
"title",
|
|
@@ -4334,11 +4950,9 @@ function registerAutonomyTools(server, client) {
|
|
|
4334
4950
|
//#endregion
|
|
4335
4951
|
//#region src/tools/inbox.ts
|
|
4336
4952
|
/**
|
|
4337
|
-
*
|
|
4338
|
-
*
|
|
4339
|
-
*
|
|
4340
|
-
* permission). The inbox is the team's: resolving an item clears it for
|
|
4341
|
-
* everybody, so these tools say what they clear.
|
|
4953
|
+
* An item as a row: what it is, whose product, how long it has waited. Not
|
|
4954
|
+
* its metadata by default: a document decision carried its whole text there,
|
|
4955
|
+
* and fifty rows came to 57 KB (PRO-2800); full: true brings it.
|
|
4342
4956
|
*/
|
|
4343
4957
|
const ITEM_FIELDS = [
|
|
4344
4958
|
"id",
|
|
@@ -4353,10 +4967,48 @@ const ITEM_FIELDS = [
|
|
|
4353
4967
|
"status",
|
|
4354
4968
|
"blocking",
|
|
4355
4969
|
"snoozedUntil",
|
|
4356
|
-
"metadata",
|
|
4357
4970
|
"createdAt",
|
|
4358
4971
|
"resolvedAt"
|
|
4359
4972
|
];
|
|
4973
|
+
const DAY_MS = 864e5;
|
|
4974
|
+
/** When an item stopped waiting: its resolution, or now while it is open. */
|
|
4975
|
+
function waitedUntil(resolvedAt, now) {
|
|
4976
|
+
const at = typeof resolvedAt === "string" ? Date.parse(resolvedAt) : NaN;
|
|
4977
|
+
return Number.isNaN(at) ? now : at;
|
|
4978
|
+
}
|
|
4979
|
+
/** A row as an agent reads it: the pull request to open when there is one, and how many days it has waited. */
|
|
4980
|
+
function itemRow(row, now, full) {
|
|
4981
|
+
const { snoozedUntil, resolvedAt, ...rest } = pickFields([row], ITEM_FIELDS)[0];
|
|
4982
|
+
const metadata = row["metadata"] ?? {};
|
|
4983
|
+
const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
|
|
4984
|
+
return {
|
|
4985
|
+
...rest,
|
|
4986
|
+
...snoozedUntil ? { snoozedUntil } : {},
|
|
4987
|
+
...resolvedAt ? { resolvedAt } : {},
|
|
4988
|
+
...typeof metadata["prUrl"] === "string" ? { prUrl: metadata["prUrl"] } : {},
|
|
4989
|
+
...Number.isNaN(created) ? {} : { waitingDays: Math.floor((waitedUntil(resolvedAt, now) - created) / DAY_MS) },
|
|
4990
|
+
...full ? { metadata: row["metadata"] } : {}
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
/** What is open, counted by product and kind: the answer to "what needs attention" without reading every row. */
|
|
4994
|
+
function summaryOf(rows) {
|
|
4995
|
+
const byProduct = /* @__PURE__ */ new Map();
|
|
4996
|
+
for (const row of rows) {
|
|
4997
|
+
const key = String(row["productId"]);
|
|
4998
|
+
const entry = byProduct.get(key) ?? {
|
|
4999
|
+
productId: key,
|
|
5000
|
+
product: row["productName"] ?? key,
|
|
5001
|
+
open: 0,
|
|
5002
|
+
byKind: {}
|
|
5003
|
+
};
|
|
5004
|
+
entry.open += 1;
|
|
5005
|
+
entry.byKind[String(row["kind"])] = (entry.byKind[String(row["kind"])] ?? 0) + 1;
|
|
5006
|
+
byProduct.set(key, entry);
|
|
5007
|
+
}
|
|
5008
|
+
return [...byProduct.values()].sort((a, b) => b.open - a.open);
|
|
5009
|
+
}
|
|
5010
|
+
/** Pages read for the summary; past this it says it counted only those. */
|
|
5011
|
+
const SUMMARY_PAGE = 100;
|
|
4360
5012
|
const SETUP_TASK_FIELDS = [
|
|
4361
5013
|
"id",
|
|
4362
5014
|
"productId",
|
|
@@ -4377,38 +5029,94 @@ const SETUP_TASK_FIELDS = [
|
|
|
4377
5029
|
function totalOf(result) {
|
|
4378
5030
|
return typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {};
|
|
4379
5031
|
}
|
|
5032
|
+
/**
|
|
5033
|
+
* One row per variable: tasks filed before the API merged a key's categories
|
|
5034
|
+
* (PRO-2782) still list JWT_SECRET once as a secret and once as an env_var.
|
|
5035
|
+
* The oldest row stays, and names the others under alsoFiledAs, so the
|
|
5036
|
+
* duplicates read as one thing to set with several records, not several things.
|
|
5037
|
+
*/
|
|
5038
|
+
function sameKeyOnce(rows) {
|
|
5039
|
+
const first = /* @__PURE__ */ new Map();
|
|
5040
|
+
const out = [];
|
|
5041
|
+
const isOpen = (row) => row["inboxItemStatus"] !== "done" && row["inboxItemStatus"] !== "dismissed";
|
|
5042
|
+
for (const row of rows) {
|
|
5043
|
+
if (typeof row["key"] !== "string" || !row["key"]) {
|
|
5044
|
+
out.push(row);
|
|
5045
|
+
continue;
|
|
5046
|
+
}
|
|
5047
|
+
const key = `${String(row["productId"])}:${row["key"]}`;
|
|
5048
|
+
const kept = first.get(key);
|
|
5049
|
+
if (!kept) {
|
|
5050
|
+
const copy = { ...row };
|
|
5051
|
+
first.set(key, copy);
|
|
5052
|
+
out.push(copy);
|
|
5053
|
+
continue;
|
|
5054
|
+
}
|
|
5055
|
+
const also = kept["alsoFiledAs"] ?? [];
|
|
5056
|
+
const record = (from) => ({
|
|
5057
|
+
id: from["id"],
|
|
5058
|
+
category: from["category"],
|
|
5059
|
+
title: from["title"],
|
|
5060
|
+
inboxItemStatus: from["inboxItemStatus"]
|
|
5061
|
+
});
|
|
5062
|
+
if (!isOpen(kept) && isOpen(row)) {
|
|
5063
|
+
const { alsoFiledAs: _also, ...was } = kept;
|
|
5064
|
+
for (const field of Object.keys(kept)) delete kept[field];
|
|
5065
|
+
Object.assign(kept, row, { alsoFiledAs: [...also, record(was)] });
|
|
5066
|
+
} else kept["alsoFiledAs"] = [...also, record(row)];
|
|
5067
|
+
}
|
|
5068
|
+
return out;
|
|
5069
|
+
}
|
|
4380
5070
|
function registerInboxTools(server, client) {
|
|
4381
5071
|
if (!client.hasCapability("products:read")) return;
|
|
4382
5072
|
server.registerTool("taiga_list_action_required", {
|
|
4383
5073
|
title: "List what needs a person",
|
|
4384
|
-
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)
|
|
5074
|
+
description: "Read Action Required, where work comes back to a person, shared by the team: things to review (a plan, a set of initiatives, a document decision, a pull request, maintenance findings, new policy violations) 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. The first page opens with summary: what is open, counted by product and kind, so \"what needs attention\" is one call, and totalCount. Each row says how many days it has waited (waitingDays) and, for a pull request review, the prUrl; an old item is still open, not stale. category gate is the reviews, setup the setup items; resolved: true reads the archive. An item snoozed until later is left out until then (snoozedHidden counts them). full: true adds each item's metadata. Unlike taiga_list_setup_tasks, which lists the setup items with exactly what to set and where.",
|
|
4385
5075
|
inputSchema: z.object({
|
|
4386
5076
|
productId: productId.optional(),
|
|
4387
5077
|
category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
|
|
4388
5078
|
resolved: z.boolean().optional().describe("Read the resolved archive (done and dismissed) instead"),
|
|
5079
|
+
full: z.boolean().optional().describe("Carry each item's metadata, such as a document decision's whole text (default false)"),
|
|
4389
5080
|
...pagination
|
|
4390
5081
|
}),
|
|
4391
5082
|
annotations: READ_ONLY
|
|
4392
5083
|
}, (params) => runTool(async () => {
|
|
4393
|
-
const { resolved, ...rest } = params;
|
|
4394
|
-
const
|
|
5084
|
+
const { resolved, full, ...rest } = params;
|
|
5085
|
+
const query = {
|
|
4395
5086
|
...rest,
|
|
4396
|
-
...resolved ? { resolved: true } : {}
|
|
4397
|
-
|
|
5087
|
+
...resolved ? { resolved: true } : {}
|
|
5088
|
+
};
|
|
5089
|
+
const first = params.cursor === void 0;
|
|
5090
|
+
const result = await client.listInbox({
|
|
5091
|
+
...query,
|
|
5092
|
+
...first ? { withTotal: true } : {}
|
|
5093
|
+
});
|
|
5094
|
+
const overview = !first || resolved ? null : !result.nextCursor ? result : await client.listInbox({
|
|
5095
|
+
...query,
|
|
5096
|
+
limit: SUMMARY_PAGE,
|
|
5097
|
+
cursor: void 0
|
|
5098
|
+
}).catch((error) => {
|
|
5099
|
+
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
5100
|
+
throw error;
|
|
4398
5101
|
});
|
|
4399
5102
|
const now = Date.now();
|
|
5103
|
+
const isDue = (row) => !(row["status"] === "snoozed" && typeof row["snoozedUntil"] === "string" && Date.parse(row["snoozedUntil"]) > now);
|
|
4400
5104
|
const rows = result.data;
|
|
4401
|
-
const due = rows.filter(
|
|
5105
|
+
const due = rows.filter(isDue);
|
|
4402
5106
|
const hidden = rows.length - due.length;
|
|
4403
5107
|
return toolResult({
|
|
4404
|
-
...
|
|
5108
|
+
...overview ? {
|
|
5109
|
+
summary: summaryOf(overview.data.filter(isDue)),
|
|
5110
|
+
...overview.nextCursor ? { summaryNote: `The summary counts the first ${SUMMARY_PAGE} items.` } : {}
|
|
5111
|
+
} : {},
|
|
5112
|
+
...page(due.map((row) => itemRow(row, now, full === true)), result.nextCursor),
|
|
4405
5113
|
...totalOf(result),
|
|
4406
5114
|
...hidden > 0 ? { snoozedHidden: hidden } : {}
|
|
4407
5115
|
});
|
|
4408
5116
|
}));
|
|
4409
5117
|
server.registerTool("taiga_list_setup_tasks", {
|
|
4410
5118
|
title: "List setup tasks",
|
|
4411
|
-
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. The first page says how many there are on every page (totalCount).",
|
|
5119
|
+
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. The first page says how many there are on every page (totalCount). One row per variable: a variable filed twice (as a secret and as an environment variable) lists the other records under alsoFiledAs, an open one first; within a page, and totalCount counts every record. Unlike taiga_list_action_required, whose setup items are the same work as rows to resolve, this says exactly what to set and where.",
|
|
4412
5120
|
inputSchema: z.object({
|
|
4413
5121
|
productId: productId.optional(),
|
|
4414
5122
|
includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
|
|
@@ -4423,7 +5131,7 @@ function registerInboxTools(server, client) {
|
|
|
4423
5131
|
...params.cursor === void 0 ? { withTotal: true } : {}
|
|
4424
5132
|
});
|
|
4425
5133
|
return toolResult({
|
|
4426
|
-
...page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor),
|
|
5134
|
+
...page(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor),
|
|
4427
5135
|
...totalOf(result)
|
|
4428
5136
|
});
|
|
4429
5137
|
}));
|
|
@@ -4432,7 +5140,7 @@ function registerInboxTools(server, client) {
|
|
|
4432
5140
|
title: "Resolve or snooze an Action Required item",
|
|
4433
5141
|
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.",
|
|
4434
5142
|
inputSchema: z.object({
|
|
4435
|
-
|
|
5143
|
+
actionId: idInput("Action Required item", "taiga_list_action_required gives it"),
|
|
4436
5144
|
status: z.enum([
|
|
4437
5145
|
"in_progress",
|
|
4438
5146
|
"done",
|
|
@@ -4443,7 +5151,7 @@ function registerInboxTools(server, client) {
|
|
|
4443
5151
|
annotations: OPERATE
|
|
4444
5152
|
}, (params) => runTool(async () => {
|
|
4445
5153
|
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.");
|
|
4446
|
-
return toolResult(pickFields([(await client.updateInboxItem(params.
|
|
5154
|
+
return toolResult(pickFields([(await client.updateInboxItem(params.actionId, {
|
|
4447
5155
|
...params.status !== void 0 ? { status: params.status } : {},
|
|
4448
5156
|
...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
|
|
4449
5157
|
})).data], ITEM_FIELDS)[0]);
|
|
@@ -4633,219 +5341,6 @@ function registerKnowledgeTools(server, client) {
|
|
|
4633
5341
|
}));
|
|
4634
5342
|
}
|
|
4635
5343
|
//#endregion
|
|
4636
|
-
//#region src/tools/maintenance.ts
|
|
4637
|
-
/**
|
|
4638
|
-
* Maintaining (docs.tai.ga, Operate): what the sweeps found in a product's
|
|
4639
|
-
* repository and where it stands against the organization's own policies,
|
|
4640
|
-
* and the two things a person does with a finding, Ignore and Fix. Sweeps run
|
|
4641
|
-
* themselves and nothing is marked fixed by hand: a finding resolves when the
|
|
4642
|
-
* next sweep no longer detects it, so there is no tool for either.
|
|
4643
|
-
*/
|
|
4644
|
-
/** A finding as a list row: enough to decide whether to open, fix or ignore it. */
|
|
4645
|
-
const FINDING_ROW_FIELDS = [
|
|
4646
|
-
"id",
|
|
4647
|
-
"kind",
|
|
4648
|
-
"severity",
|
|
4649
|
-
"tier",
|
|
4650
|
-
"status",
|
|
4651
|
-
"title",
|
|
4652
|
-
"ecosystem",
|
|
4653
|
-
"packageName",
|
|
4654
|
-
"currentVersion",
|
|
4655
|
-
"recommendedVersion",
|
|
4656
|
-
"advisoryId",
|
|
4657
|
-
"location",
|
|
4658
|
-
"sourcePaths",
|
|
4659
|
-
"productRepositoryId",
|
|
4660
|
-
"ownership",
|
|
4661
|
-
"remediableBy",
|
|
4662
|
-
"controls",
|
|
4663
|
-
"backlogItemId",
|
|
4664
|
-
"detectedAt",
|
|
4665
|
-
"ignoreReason",
|
|
4666
|
-
"ignoreExpiresAt"
|
|
4667
|
-
];
|
|
4668
|
-
/** A sweep as the record of when the product was last checked, and whether that check held. */
|
|
4669
|
-
const SWEEP_FIELDS = [
|
|
4670
|
-
"id",
|
|
4671
|
-
"trigger",
|
|
4672
|
-
"status",
|
|
4673
|
-
"scanners",
|
|
4674
|
-
"findingsCount",
|
|
4675
|
-
"newFindingsCount",
|
|
4676
|
-
"error",
|
|
4677
|
-
"createdAt",
|
|
4678
|
-
"completedAt"
|
|
4679
|
-
];
|
|
4680
|
-
/** A policy violation in a list: what it is, how bad, and what is being done about it. */
|
|
4681
|
-
const VIOLATION_FIELDS = [
|
|
4682
|
-
"id",
|
|
4683
|
-
"controlId",
|
|
4684
|
-
"policyType",
|
|
4685
|
-
"severity",
|
|
4686
|
-
"status",
|
|
4687
|
-
"title",
|
|
4688
|
-
"summary",
|
|
4689
|
-
"work",
|
|
4690
|
-
"unconfirmedSince",
|
|
4691
|
-
"verifyRequestedAt",
|
|
4692
|
-
"assessedAt"
|
|
4693
|
-
];
|
|
4694
|
-
const findingId = idInput("finding", "taiga_list_findings gives it");
|
|
4695
|
-
function registerMaintenanceTools(server, client) {
|
|
4696
|
-
if (!client.hasCapability("products:read")) return;
|
|
4697
|
-
server.registerTool("taiga_get_maintenance", {
|
|
4698
|
-
title: "Get a product's maintenance standing",
|
|
4699
|
-
description: "Where a product's repository stands: open findings by severity and kind, the most pressing ones, and the latest sweeps, so you can tell when the product was last checked and whether that check succeeded. A quiet list from a sweep that last ran long ago, or that failed, is not an all-clear; a product never swept says so. Sweeps run on their own after a push and periodically; nothing here starts one. Read the findings with taiga_list_findings and the policy standing with taiga_get_policy_standing.",
|
|
4700
|
-
inputSchema: z.object({ productId }),
|
|
4701
|
-
annotations: READ_ONLY
|
|
4702
|
-
}, (params) => runTool(async () => {
|
|
4703
|
-
const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
|
|
4704
|
-
const latest = pickFields(sweeps.data, SWEEP_FIELDS);
|
|
4705
|
-
const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
|
|
4706
|
-
return toolResult({
|
|
4707
|
-
...standing,
|
|
4708
|
-
latestSweeps: latest,
|
|
4709
|
-
...latest.length === 0 ? { note: "This product has never been swept: no result is not an all-clear." } : {}
|
|
4710
|
-
});
|
|
4711
|
-
}));
|
|
4712
|
-
server.registerTool("taiga_list_findings", {
|
|
4713
|
-
title: "List maintenance findings",
|
|
4714
|
-
description: "List what the sweeps found in a product's repository: vulnerable or outdated dependencies, committed secrets, insecure code (sast) and infrastructure misconfigurations (iac_misconfig). Each row carries its severity and its priority tier (act_now: known exploited; urgent: likely and serious; elevated: one of the two; routine), where it is, who can fix it (remediableBy: coding_agent, or upstream_only for a submodule or a dependency with no fixed version), the policy controls it breaches, and the initiative already fixing it (backlogItemId). Omit status for the findings that need action; work from the top tier down. Read one in full with taiga_get_finding.",
|
|
4715
|
-
inputSchema: z.object({
|
|
4716
|
-
productId,
|
|
4717
|
-
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
4718
|
-
severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
|
|
4719
|
-
kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
|
|
4720
|
-
...pagination
|
|
4721
|
-
}),
|
|
4722
|
-
annotations: READ_ONLY
|
|
4723
|
-
}, (params) => runTool(async () => {
|
|
4724
|
-
const { productId: id, ...filters } = params;
|
|
4725
|
-
const result = await client.listFindings(id, filters);
|
|
4726
|
-
return toolResult({
|
|
4727
|
-
...page(pickFields(result.data, FINDING_ROW_FIELDS), result.nextCursor),
|
|
4728
|
-
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
|
|
4729
|
-
});
|
|
4730
|
-
}));
|
|
4731
|
-
server.registerTool("taiga_get_finding", {
|
|
4732
|
-
title: "Get a maintenance finding",
|
|
4733
|
-
description: "Read one finding in full: what it is, how likely it is to be exploited (EPSS, the known-exploited list), the advisory and the fixed version, the place in the code, the policy controls it breaches, why it is ranked where it is, its history (reintroduced, previously resolved) and the remediation under way. A secret's value is never stored; the finding names the rule and the location only.",
|
|
4734
|
-
inputSchema: z.object({ findingId }),
|
|
4735
|
-
annotations: READ_ONLY
|
|
4736
|
-
}, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
|
|
4737
|
-
server.registerTool("taiga_get_policy_standing", {
|
|
4738
|
-
title: "Get a product's policy standing",
|
|
4739
|
-
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 breached by scanner findings counts them by tier (findingsByTier): a breach of routine findings only, such as secrets in test fixtures, is not an urgent one. 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.",
|
|
4740
|
-
inputSchema: z.object({
|
|
4741
|
-
productId,
|
|
4742
|
-
full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
|
|
4743
|
-
}),
|
|
4744
|
-
annotations: READ_ONLY
|
|
4745
|
-
}, (params) => runTool(async () => {
|
|
4746
|
-
const standing = (await client.getPolicyStanding(params.productId)).data;
|
|
4747
|
-
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
4748
|
-
const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
|
|
4749
|
-
const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
|
|
4750
|
-
const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
|
|
4751
|
-
const bySeverity = {};
|
|
4752
|
-
for (const violation of violations) {
|
|
4753
|
-
const severity = String(violation.severity ?? "unknown");
|
|
4754
|
-
bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
|
|
4755
|
-
}
|
|
4756
|
-
const summary = {
|
|
4757
|
-
controls: listed.length,
|
|
4758
|
-
violations: violations.length,
|
|
4759
|
-
bySeverity,
|
|
4760
|
-
resolvedViolations: resolved
|
|
4761
|
-
};
|
|
4762
|
-
if (params.full) return toolResult({
|
|
4763
|
-
summary,
|
|
4764
|
-
...standing
|
|
4765
|
-
});
|
|
4766
|
-
return toolResult({
|
|
4767
|
-
summary,
|
|
4768
|
-
productId: standing.productId,
|
|
4769
|
-
hasPolicies: standing.hasPolicies,
|
|
4770
|
-
assessment: standing.assessment,
|
|
4771
|
-
documents: standing.documents,
|
|
4772
|
-
controls: pickFields(listed, [
|
|
4773
|
-
"controlId",
|
|
4774
|
-
"policyType",
|
|
4775
|
-
"text",
|
|
4776
|
-
"state",
|
|
4777
|
-
"divergenceCount",
|
|
4778
|
-
"findingCount",
|
|
4779
|
-
"findingsByTier",
|
|
4780
|
-
"plannedCount"
|
|
4781
|
-
]),
|
|
4782
|
-
violations: pickFields(violations, VIOLATION_FIELDS)
|
|
4783
|
-
});
|
|
4784
|
-
}));
|
|
4785
|
-
if (!client.hasCapability("products:write")) return;
|
|
4786
|
-
server.registerTool("taiga_ignore_finding", {
|
|
4787
|
-
title: "Ignore or reopen a finding",
|
|
4788
|
-
description: "Ignore a finding that does not apply or whose risk is accepted, with the reason (false_positive, not_reachable, accepted_risk, wont_fix), an optional note and an optional expiry after which it resurfaces if it still exists; or reopen an ignored one (ignored: false). Ignored findings are kept and reviewable. There is no \"mark fixed\": a finding resolves when the next sweep no longer detects it. For a secret that is only in history, rotate the credential outside Taiga, then ignore it with a reason. Ignoring hides a shared signal and can record the team's acceptance of a risk, which is the person's decision: ask the person first.",
|
|
4789
|
-
inputSchema: z.object({
|
|
4790
|
-
findingId,
|
|
4791
|
-
ignored: z.boolean().describe("true ignores the finding, false reopens it"),
|
|
4792
|
-
reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe("Why it is ignored; required when ignoring"),
|
|
4793
|
-
note: z.string().trim().max(280).optional().describe("A short justification, kept for audit"),
|
|
4794
|
-
expiresAt: z.string().datetime({ offset: true }).optional().describe("When the ignore lapses and the finding resurfaces if it still exists (ISO 8601, in the future)")
|
|
4795
|
-
}),
|
|
4796
|
-
annotations: OPERATE
|
|
4797
|
-
}, (params) => runTool(async () => {
|
|
4798
|
-
if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
|
|
4799
|
-
const body = params.ignored ? {
|
|
4800
|
-
status: "ignored",
|
|
4801
|
-
ignoreReason: params.reason,
|
|
4802
|
-
...params.note !== void 0 ? { ignoreNote: params.note } : {},
|
|
4803
|
-
...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
|
|
4804
|
-
} : { status: "open" };
|
|
4805
|
-
const finding = (await client.updateFinding(params.findingId, body)).data;
|
|
4806
|
-
return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, "ignoreNote"])[0]);
|
|
4807
|
-
}));
|
|
4808
|
-
server.registerTool("taiga_fix_finding", {
|
|
4809
|
-
title: "Fix a finding",
|
|
4810
|
-
description: "Ask for a finding's fix: Taiga raises an initiative and adds it to the end of the product's queue, where the line plans it in turn; whether the build then waits for a person is the initiative's review gate, as for any initiative. The fix is a normal run that ends in a pull request you review and merge. With wholeGroup: true it fixes the finding's whole group in one initiative, as the page does: for a dependency, every open advisory on that package at that installed version in one version bump; for code or infrastructure, every location the same rule flagged. Absent for a secret only in history, anything in a submodule, and a dependency with no fixed version (remediableBy says upstream_only); the API refuses those. Starts paid work.",
|
|
4811
|
-
inputSchema: z.object({
|
|
4812
|
-
productId,
|
|
4813
|
-
findingId,
|
|
4814
|
-
wholeGroup: z.boolean().optional().describe("Fix every finding in this finding's group (its package and version, or its rule) together")
|
|
4815
|
-
}),
|
|
4816
|
-
annotations: OPERATE
|
|
4817
|
-
}, (params) => runTool(async () => {
|
|
4818
|
-
if (!params.wholeGroup) {
|
|
4819
|
-
const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
|
|
4820
|
-
return toolResult({
|
|
4821
|
-
backlogItemId: fixed.backlogItemId,
|
|
4822
|
-
finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null
|
|
4823
|
-
});
|
|
4824
|
-
}
|
|
4825
|
-
const finding = (await client.getFinding(params.findingId)).data;
|
|
4826
|
-
if (finding.productId !== params.productId) return toolError("NOT_FOUND", "No such finding in this product.");
|
|
4827
|
-
return toolResult((await client.remediatePackage(params.productId, {
|
|
4828
|
-
ecosystem: String(finding.ecosystem),
|
|
4829
|
-
packageName: String(finding.packageName),
|
|
4830
|
-
currentVersion: typeof finding.currentVersion === "string" ? finding.currentVersion : null,
|
|
4831
|
-
productRepositoryId: typeof finding.productRepositoryId === "string" ? finding.productRepositoryId : null
|
|
4832
|
-
})).data);
|
|
4833
|
-
}));
|
|
4834
|
-
server.registerTool("taiga_fix_policy_violation", {
|
|
4835
|
-
title: "Fix policy violations",
|
|
4836
|
-
description: "Ask for the fix of a product's policy violations: name a control to cover every violation recorded against it that has no initiative yet, or a single violation (violationId) when the control has more than one. Taiga raises one initiative and adds it to the end of the product's queue, exactly as a finding fix does. The violation stays listed while the work is under way: only the next assessment can record that the documents no longer show it. Starts paid work.",
|
|
4837
|
-
inputSchema: z.object({
|
|
4838
|
-
productId,
|
|
4839
|
-
controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
|
|
4840
|
-
violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
|
|
4841
|
-
}),
|
|
4842
|
-
annotations: OPERATE
|
|
4843
|
-
}, (params) => runTool(async () => {
|
|
4844
|
-
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.");
|
|
4845
|
-
return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
|
|
4846
|
-
}));
|
|
4847
|
-
}
|
|
4848
|
-
//#endregion
|
|
4849
5344
|
//#region src/tools/observe.ts
|
|
4850
5345
|
/**
|
|
4851
5346
|
* What runs where and how it is doing (docs.tai.ga, Deliver: Deployments;
|
|
@@ -5102,7 +5597,7 @@ function registerProductDocumentWrites(server, client) {
|
|
|
5102
5597
|
}, (params) => runTool(() => updateSection(productScope(params.productId, params.type), params.type, params.sectionId, params.content)));
|
|
5103
5598
|
server.registerTool("taiga_publish_product_document", {
|
|
5104
5599
|
title: "Publish a product document draft",
|
|
5105
|
-
description: "Publish a product document's draft as its new version, the one agents work from. Documents generated from it are not rewritten: each is marked outdated, and is regenerated when someone chooses to (taiga_regenerate_product_document, or taiga_generate_product_documents for every outdated and missing one).
|
|
5600
|
+
description: "Publish a product document's draft as its new version, the one agents work from. Documents generated from it are not rewritten: each is marked outdated, and is regenerated when someone chooses to (taiga_regenerate_product_document, or taiga_generate_product_documents for every outdated and missing one).",
|
|
5106
5601
|
inputSchema: z.object({
|
|
5107
5602
|
productId,
|
|
5108
5603
|
type: documentType
|
|
@@ -19782,18 +20277,18 @@ function registerPrompts(server, client) {
|
|
|
19782
20277
|
"",
|
|
19783
20278
|
"Before writing code: follow the factory instructions; read the policy sections that touch what you are",
|
|
19784
20279
|
"about to change with taiga_get_document (security, data, SDLC, architecture at least); read the backlog",
|
|
19785
|
-
"item you are implementing with
|
|
20280
|
+
"item you are implementing with taiga_get_initiative. Parts listed under notGranted or unavailable",
|
|
19786
20281
|
"could not be read; say so rather than assuming they are empty.",
|
|
19787
20282
|
"",
|
|
19788
20283
|
fence("json", (await productContext(client, resolved.product, { documents: "full" })).content[0]?.text ?? "{}")
|
|
19789
20284
|
].join("\n"));
|
|
19790
20285
|
});
|
|
19791
|
-
if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("
|
|
19792
|
-
title: "Implement
|
|
19793
|
-
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.
|
|
19794
|
-
argsSchema: z.object({
|
|
20286
|
+
if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_initiative", {
|
|
20287
|
+
title: "Implement an initiative",
|
|
20288
|
+
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_initiatives gives the item ID.",
|
|
20289
|
+
argsSchema: z.object({ initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it") })
|
|
19795
20290
|
}, async (args) => {
|
|
19796
|
-
const item = (await served$1(client.getBacklogItem(args.
|
|
20291
|
+
const item = (await served$1(client.getBacklogItem(args.initiativeId), "No initiative with that ID the key can see; taiga_list_initiatives shows what exists")).data;
|
|
19797
20292
|
const productId = String(item.productId);
|
|
19798
20293
|
const product = (await served$1(client.getProduct(productId), "The product of that item is not visible to the key")).data;
|
|
19799
20294
|
const origin = defaultOriginFor(product, "architecture");
|
|
@@ -19892,7 +20387,8 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
19892
20387
|
"archived",
|
|
19893
20388
|
"defaultBranch",
|
|
19894
20389
|
"description",
|
|
19895
|
-
"language"
|
|
20390
|
+
"language",
|
|
20391
|
+
"linked"
|
|
19896
20392
|
];
|
|
19897
20393
|
const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
|
|
19898
20394
|
/** Round-robin over lists: the first of each, then the second of each, and so on. */
|
|
@@ -19934,7 +20430,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19934
20430
|
}
|
|
19935
20431
|
if (client.hasCapability("vcs:read")) server.registerTool("taiga_list_linkable_repositories", {
|
|
19936
20432
|
title: "Find a repository to link",
|
|
19937
|
-
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. Name the GitHub account (account: its login, the owner in owner/repo, or installationId) to read only its repositories; without one it reads every connected account and takes rows from each in turn, so one account cannot fill the page. No connection means GitHub is not connected yet: that is done once, in the web, by someone who can install the app.",
|
|
20433
|
+
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. Name the GitHub account (account: its login, the owner in owner/repo, or installationId) to read only its repositories; without one it reads every connected account and takes rows from each in turn, so one account cannot fill the page. linked: true means a product already links it (taiga_resolve_product says which). No connection means GitHub is not connected yet: that is done once, in the web, by someone who can install the app.",
|
|
19938
20434
|
inputSchema: z.object({
|
|
19939
20435
|
query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
|
|
19940
20436
|
account: z.string().trim().min(1).max(140).optional().describe("The GitHub account login (the owner in owner/repo), case-insensitive"),
|
|
@@ -20138,13 +20634,13 @@ function registerResources(server, client) {
|
|
|
20138
20634
|
return read(uri, productId, type, defaultOriginFor(product, type));
|
|
20139
20635
|
});
|
|
20140
20636
|
}
|
|
20141
|
-
if (client.hasCapability("backlog:read")) server.registerResource("backlog-item", new ResourceTemplate("taiga://
|
|
20142
|
-
title: "
|
|
20143
|
-
description: "An initiative in full: description, acceptance criteria, technical context.
|
|
20637
|
+
if (client.hasCapability("backlog:read")) server.registerResource("backlog-item", new ResourceTemplate("taiga://initiatives/{initiativeId}", { list: void 0 }), {
|
|
20638
|
+
title: "Initiative",
|
|
20639
|
+
description: "An initiative in full: description, acceptance criteria, technical context. taiga_list_initiatives gives the item ID.",
|
|
20144
20640
|
mimeType: "application/json"
|
|
20145
20641
|
}, async (uri, variables) => {
|
|
20146
|
-
const
|
|
20147
|
-
const item = (await served(client.getBacklogItem(
|
|
20642
|
+
const initiativeId = requireId("Initiative ID", String(variables["initiativeId"]));
|
|
20643
|
+
const item = (await served(client.getBacklogItem(initiativeId), "No initiative with that ID the key can see; taiga_list_initiatives shows what exists")).data;
|
|
20148
20644
|
return { contents: [{
|
|
20149
20645
|
uri: uri.href,
|
|
20150
20646
|
mimeType: "application/json",
|
|
@@ -20188,33 +20684,17 @@ const buildScope = {
|
|
|
20188
20684
|
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");
|
|
20189
20685
|
/** How a missing required document comes to exist, said with a documents_incomplete refusal. */
|
|
20190
20686
|
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.";
|
|
20191
|
-
/** A product row the model can act on
|
|
20687
|
+
/** A product row the model can act on: how it started, and while in setup what it still needs. */
|
|
20192
20688
|
async function productRow(client, product) {
|
|
20193
20689
|
const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
|
|
20194
|
-
const
|
|
20690
|
+
const readiness = await setupReadiness(client, product);
|
|
20195
20691
|
return {
|
|
20196
20692
|
...rest,
|
|
20197
20693
|
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
20198
|
-
...
|
|
20694
|
+
...readiness ? { readiness } : {}
|
|
20199
20695
|
};
|
|
20200
20696
|
}
|
|
20201
20697
|
/**
|
|
20202
|
-
* The product with its discoveryStep. The API derives it on reads only, not
|
|
20203
|
-
* on the answer to a write (products.ts), and a draft started from scratch
|
|
20204
|
-
* needs it to say what comes next; an imported one reads its import status
|
|
20205
|
-
* instead. Best effort: a failed read leaves the product without a nextStep
|
|
20206
|
-
* rather than failing the write that already happened.
|
|
20207
|
-
*/
|
|
20208
|
-
async function withDiscoveryStep(client, product) {
|
|
20209
|
-
if (product.status !== "draft" || "discoveryStep" in product || isImportedProduct(product)) return product;
|
|
20210
|
-
try {
|
|
20211
|
-
return (await client.getProduct(String(product.id))).data;
|
|
20212
|
-
} catch (error) {
|
|
20213
|
-
if (error instanceof TaigaApiError || isNetworkFailure(error)) return product;
|
|
20214
|
-
throw error;
|
|
20215
|
-
}
|
|
20216
|
-
}
|
|
20217
|
-
/**
|
|
20218
20698
|
* A half turned off with nothing said about how it is done today. Allowed (the
|
|
20219
20699
|
* web asks for the sentence but does not insist), so a warning rather than a
|
|
20220
20700
|
* refusal: plans then design against a gap.
|
|
@@ -20259,7 +20739,7 @@ function registerSetupTools(server, client) {
|
|
|
20259
20739
|
}, (params) => runTool(async () => toolResult((await client.previewProductMove(params.productId, params.factoryId)).data)));
|
|
20260
20740
|
server.registerTool("taiga_move_product", {
|
|
20261
20741
|
title: "Move a product to another factory",
|
|
20262
|
-
description: "Move a product to another factory. Access follows the factory, so this changes who can reach the product (taiga_preview_product_move says who); the product's own knowledge files are re-filed onto the new factory, and factory knowledge and instructions stay with the factory that owns them. Refused (
|
|
20742
|
+
description: "Move a product to another factory. Access follows the factory, so this changes who can reach the product (taiga_preview_product_move says who); the product's own knowledge files are re-filed onto the new factory, and factory knowledge and instructions stay with the factory that owns them. Refused (WORK_IN_FLIGHT) while a plan or build is in flight. Then ask the person: stopLine: true stops the line first and then moves, which interrupts that work.",
|
|
20263
20743
|
inputSchema: z.object({
|
|
20264
20744
|
productId,
|
|
20265
20745
|
factoryId: factoryId.describe("The factory to move it to"),
|
|
@@ -20267,10 +20747,16 @@ function registerSetupTools(server, client) {
|
|
|
20267
20747
|
}),
|
|
20268
20748
|
annotations: OPERATE
|
|
20269
20749
|
}, (params) => runTool(async () => {
|
|
20270
|
-
|
|
20271
|
-
|
|
20272
|
-
|
|
20273
|
-
|
|
20750
|
+
let moved;
|
|
20751
|
+
try {
|
|
20752
|
+
moved = await client.moveProduct(params.productId, {
|
|
20753
|
+
factoryId: params.factoryId,
|
|
20754
|
+
...params.stopLine ? { stopLine: true } : {}
|
|
20755
|
+
});
|
|
20756
|
+
} catch (error) {
|
|
20757
|
+
if (error instanceof TaigaApiError && error.code === "WORK_IN_FLIGHT") return toolError("WORK_IN_FLIGHT", `${withStop((error.apiMessage ?? "Work is in flight on this product").split(/(?<=[.!?])\s/)[0])} Moving it now means stopping the line, which interrupts that work: ask the person, and pass stopLine: true only if they say so.`, error.details);
|
|
20758
|
+
throw error;
|
|
20759
|
+
}
|
|
20274
20760
|
return toolResult({
|
|
20275
20761
|
product: await productRow(client, moved.data),
|
|
20276
20762
|
changed: moved.changed,
|
|
@@ -20564,7 +21050,9 @@ function judgeItem(until, gate) {
|
|
|
20564
21050
|
/** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
|
|
20565
21051
|
function itemReport(item) {
|
|
20566
21052
|
const run = latestBuildOf(item);
|
|
21053
|
+
const situation = situationOf(item);
|
|
20567
21054
|
return {
|
|
21055
|
+
...situation ? { situation } : {},
|
|
20568
21056
|
...pickFields([item], [
|
|
20569
21057
|
"id",
|
|
20570
21058
|
"title",
|
|
@@ -20611,6 +21099,8 @@ const RUN_REPORT_FIELDS = [
|
|
|
20611
21099
|
"prState",
|
|
20612
21100
|
"prDraft",
|
|
20613
21101
|
"prMerged",
|
|
21102
|
+
"prNumber",
|
|
21103
|
+
"baseBranch",
|
|
20614
21104
|
"actions"
|
|
20615
21105
|
];
|
|
20616
21106
|
const runSignature = (run) => JSON.stringify([
|
|
@@ -20631,11 +21121,11 @@ function judgeBuild(until) {
|
|
|
20631
21121
|
};
|
|
20632
21122
|
}
|
|
20633
21123
|
function registerWaitTools(server, client, options) {
|
|
20634
|
-
if (client.hasCapability("backlog:read")) server.registerTool("
|
|
21124
|
+
if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_initiative", {
|
|
20635
21125
|
title: "Wait for an initiative",
|
|
20636
21126
|
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.",
|
|
20637
21127
|
inputSchema: z.object({
|
|
20638
|
-
|
|
21128
|
+
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
|
|
20639
21129
|
until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
|
|
20640
21130
|
timeoutSeconds
|
|
20641
21131
|
}),
|
|
@@ -20643,7 +21133,7 @@ function registerWaitTools(server, client, options) {
|
|
|
20643
21133
|
}, (params, ctx) => runTool(async () => {
|
|
20644
21134
|
const gate = { product: void 0 };
|
|
20645
21135
|
const read = async (signal) => {
|
|
20646
|
-
const item = (await client.getBacklogItem(params.
|
|
21136
|
+
const item = (await client.getBacklogItem(params.initiativeId, signal)).data;
|
|
20647
21137
|
if (gate.product === void 0 && typeof item.autoBuild !== "boolean") gate.product = client.hasCapability("products:read") ? await client.getProduct(String(item.productId), signal).then((p) => {
|
|
20648
21138
|
const value = p.data.autoBuild;
|
|
20649
21139
|
return typeof value === "boolean" ? value : true;
|
|
@@ -20664,17 +21154,17 @@ function registerWaitTools(server, client, options) {
|
|
|
20664
21154
|
item: now ? itemReport(now) : null
|
|
20665
21155
|
});
|
|
20666
21156
|
}));
|
|
20667
|
-
if (client.hasCapability("products:read")) server.registerTool("
|
|
21157
|
+
if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_run", {
|
|
20668
21158
|
title: "Wait for a run",
|
|
20669
21159
|
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.",
|
|
20670
21160
|
inputSchema: z.object({
|
|
20671
|
-
|
|
21161
|
+
runId: idInput("run", "taiga_list_runs gives it"),
|
|
20672
21162
|
until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
|
|
20673
21163
|
timeoutSeconds
|
|
20674
21164
|
}),
|
|
20675
21165
|
annotations: READ_ONLY
|
|
20676
21166
|
}, (params, ctx) => runTool(async () => {
|
|
20677
|
-
const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.
|
|
21167
|
+
const { now, ...outcome } = await waitFor(async (signal) => (await client.getBuildRun(params.runId, signal)).data, judgeBuild(params.until), (params.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3, options, {
|
|
20678
21168
|
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
20679
21169
|
notify: ctx.mcpReq.notify,
|
|
20680
21170
|
signal: ctx.mcpReq.signal
|
|
@@ -20682,7 +21172,7 @@ function registerWaitTools(server, client, options) {
|
|
|
20682
21172
|
return toolResult({
|
|
20683
21173
|
until: params.until,
|
|
20684
21174
|
...outcome,
|
|
20685
|
-
run: now ? pickFields([now], RUN_REPORT_FIELDS)[0] : null
|
|
21175
|
+
run: now ? readableRun(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
|
|
20686
21176
|
});
|
|
20687
21177
|
}));
|
|
20688
21178
|
if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
|
|
@@ -20725,7 +21215,8 @@ function withStrictInputs(server) {
|
|
|
20725
21215
|
const withHint = (args, ctx) => notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
|
|
20726
21216
|
const described = {
|
|
20727
21217
|
...config,
|
|
20728
|
-
description: `${writeNotice(name, config.annotations)}${config.description ?? ""}
|
|
21218
|
+
description: `${writeNotice(name, config.annotations)}${config.description ?? ""}`,
|
|
21219
|
+
...ALWAYS_LOAD.has(name) ? { _meta: { "anthropic/alwaysLoad": true } } : {}
|
|
20729
21220
|
};
|
|
20730
21221
|
return register(name, schema ? {
|
|
20731
21222
|
...described,
|
|
@@ -20735,6 +21226,19 @@ function withStrictInputs(server) {
|
|
|
20735
21226
|
return server;
|
|
20736
21227
|
}
|
|
20737
21228
|
/**
|
|
21229
|
+
* The entry points, loaded with their descriptions at session start
|
|
21230
|
+
* (PRO-2800). Claude Code defers every other MCP tool behind tool search and
|
|
21231
|
+
* shows the model only its name until it searches, so an agent meeting Taiga
|
|
21232
|
+
* sees these three in full: where a product stands, which products there
|
|
21233
|
+
* are, and what waits on a person. Three, because each one loaded upfront
|
|
21234
|
+
* costs every session its description.
|
|
21235
|
+
*/
|
|
21236
|
+
const ALWAYS_LOAD = /* @__PURE__ */ new Set([
|
|
21237
|
+
"taiga_get_product_context",
|
|
21238
|
+
"taiga_list_products",
|
|
21239
|
+
"taiga_list_action_required"
|
|
21240
|
+
]);
|
|
21241
|
+
/**
|
|
20738
21242
|
* What a tool's annotations say, in its description's first words (PRO-2778).
|
|
20739
21243
|
* Annotations are for the client: Claude Code does not show them to the
|
|
20740
21244
|
* model, so an agent read 79 tools without knowing which ones write. A tool
|
|
@@ -20771,136 +21275,40 @@ function registerAllTools(server, client, options = {}) {
|
|
|
20771
21275
|
registerResources(server, client);
|
|
20772
21276
|
registerPrompts(server, client);
|
|
20773
21277
|
}
|
|
21278
|
+
//#endregion
|
|
21279
|
+
//#region src/tools/instructions.ts
|
|
21280
|
+
/**
|
|
21281
|
+
* What the server tells a client at connect time. Claude Code loads only the
|
|
21282
|
+
* tool NAMES and these instructions until the model searches for a tool, and
|
|
21283
|
+
* cuts each server's instructions at 2,048 characters (code.claude.com, MCP
|
|
21284
|
+
* tool search), so this is the whole introduction an agent gets: what Taiga
|
|
21285
|
+
* holds, when to search these tools, where to start, the words, when to ask,
|
|
21286
|
+
* and what only the web does. Everything else lives in the tool descriptions,
|
|
21287
|
+
* which load with the tool. The earlier instructions ran to about 8,000
|
|
21288
|
+
* characters, and everything past the first 2,000 never reached the model
|
|
21289
|
+
* (PRO-2800).
|
|
21290
|
+
*/
|
|
20774
21291
|
const SERVER_INSTRUCTIONS = [
|
|
20775
|
-
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
20780
|
-
"checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with",
|
|
20781
|
-
"query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory",
|
|
20782
|
-
"instructions, the outline of its specification and architecture, every policy as its section ids, the",
|
|
20783
|
-
"open initiatives, the repositories and the latest runs. Then read only what you need: sections by id with",
|
|
20784
|
-
"taiga_get_document or taiga_get_product_document (sections: [] is any document's outline), an",
|
|
20785
|
-
"initiative with taiga_get_backlog_item. Check Action Required (taiga_list_action_required) for what",
|
|
20786
|
-
"waits on a person.",
|
|
20787
|
-
"",
|
|
20788
|
-
"**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that",
|
|
20789
|
-
"starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it",
|
|
20790
|
-
"(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). Such a",
|
|
20791
|
-
"tool says so first in its description; a client that prompts on destructive tools does part of it.",
|
|
20792
|
-
"",
|
|
20793
|
-
"**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the",
|
|
20794
|
-
"right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.",
|
|
20795
|
-
"",
|
|
20796
|
-
"**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action",
|
|
20797
|
-
"Required is the inbox; the line is the queue; a factory is the group that owns a set of products."
|
|
20798
|
-
].join("\n"),
|
|
20799
|
-
"",
|
|
20800
|
-
"**Company documents** (taiga_list_documents / taiga_get_document):",
|
|
20801
|
-
`The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
|
|
20802
|
-
`architecture rules that apply to all code. The other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)})`,
|
|
20803
|
-
"carry the business goals, design and brand standards, tone of voice and standing instructions.",
|
|
20804
|
-
"Read the relevant policy BEFORE writing code that touches security, data handling or infrastructure.",
|
|
20805
|
-
"",
|
|
20806
|
-
"**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
|
|
20807
|
-
"A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
|
|
20808
|
-
"tech preferences (approved stack, patterns) and conventions. Read these to understand HOW this factory",
|
|
20809
|
-
"builds software. A factory that has not written any yet has none; that is normal.",
|
|
20810
|
-
"",
|
|
20811
|
-
"**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
|
|
20812
|
-
`${Object.entries(PRODUCT_DOCUMENT_NAMES).map(([type, name]) => `${name} (${type})`).join(", ")}.`,
|
|
20813
|
-
"A product has two timelines of these, and the list shows both:",
|
|
20814
|
-
`- ${ORIGIN_MEANING.plan}`,
|
|
20815
|
-
`- ${ORIGIN_MEANING.reality}`,
|
|
20816
|
-
"An imported (brownfield) product has no generated discovery chain: its derived documents are on the",
|
|
20817
|
-
"reality side, while the documents a person writes (Look & Feel, Product Instructions) are on the plan",
|
|
20818
|
-
"side of every product. Omit origin on a read and the server picks the right side (the list reports the",
|
|
20819
|
-
"product's own as defaultOrigin). To understand",
|
|
20820
|
-
"what to build, read the plan where there is one; to understand the code you are changing, read reality",
|
|
20821
|
-
"when it exists, the ground truth for architecture, data flow and threats as built.",
|
|
20822
|
-
"",
|
|
20823
|
-
"**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
|
|
20824
|
-
"The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
|
|
20825
|
-
"for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
|
|
20826
|
-
"every product the key can see, and active: true the ones executing now.",
|
|
20827
|
-
"",
|
|
20828
|
-
"**Operating the line** works the way Taiga is meant to be used. New work is described, not written:",
|
|
20829
|
-
"taiga_request_initiatives asks an agent, which reads the product and its repository, expands the request",
|
|
20830
|
-
"into initiatives in the Backlog (or says why it added none: already covered, not work, out of scope, or a",
|
|
20831
|
-
"policy that does not allow it) -- follow it with taiga_wait_for_job. The queue is the only way to start",
|
|
20832
|
-
"work (taiga_queue_backlog_item): Taiga plans and builds one initiative at a time, in queue order, and starts",
|
|
20833
|
-
"the next when the pull request merges. Whether a landed plan waits for a review is the review gate",
|
|
20834
|
-
"(taiga_set_auto_build, or autoBuild on the queue); read it with taiga_get_plan and answer with",
|
|
20835
|
-
"taiga_approve_plan or taiga_reject_plan. Decisions about the work are made on the initiative: stop work by",
|
|
20836
|
-
"moving it out of Build (taiga_move_backlog_item), carry it on with taiga_resume_backlog_item, ask for a new",
|
|
20837
|
-
"plan with taiga_plan_again; a run is the record of an attempt. taiga_pause_line pauses the whole line.",
|
|
20838
|
-
"taiga_stop_build is the emergency brake for a run whose initiative cannot be moved. Queueing, approving,",
|
|
20839
|
-
"resuming and requesting start paid agent work; stopping and pausing interrupt it. Tell the person what you",
|
|
20840
|
-
"are about to start or stop and wait for their yes; a client that prompts on destructive tools does that",
|
|
20841
|
-
"for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
|
|
20842
|
-
"",
|
|
20843
|
-
"**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build /",
|
|
20844
|
-
"taiga_get_job / taiga_wait_for_job): the queue read says what the line is waiting on (a paused line, a plan",
|
|
20845
|
-
"to review, a pull request to merge, a halt and what lifts it). Planning takes minutes and a build up to an",
|
|
20846
|
-
"hour: wait with the wait tools rather than reading the same item again and again; each returns when the",
|
|
20847
|
-
"point is reached, when only a person can move it on, or at its timeout (call again to keep waiting).",
|
|
20848
|
-
"",
|
|
20849
|
-
"**Action Required** (taiga_list_action_required / taiga_list_setup_tasks / taiga_resolve_action): where work",
|
|
20850
|
-
"comes back to a person, shared by the team. Check it first. Resolving an item clears it for everybody; a",
|
|
20851
|
-
"setup task is marked done only by the person who did the setup.",
|
|
20852
|
-
"",
|
|
20853
|
-
"**Maintaining** (taiga_get_maintenance / taiga_list_findings / taiga_get_finding / taiga_ignore_finding /",
|
|
20854
|
-
"taiga_fix_finding / taiga_get_policy_standing / taiga_fix_policy_violation): what the sweeps found and where",
|
|
20855
|
-
"the product stands against its own policies. Sweeps run on their own; check when the last one ran before",
|
|
20856
|
-
"trusting a quiet list. A finding is ignored with a reason or fixed; a fix raises an initiative at the end of",
|
|
20857
|
-
"the queue. Nothing is marked fixed by hand: the next sweep or assessment decides.",
|
|
20858
|
-
"",
|
|
20859
|
-
"**Deployments and monitoring** (taiga_get_deployments / taiga_get_delivery_metrics / taiga_get_monitoring /",
|
|
20860
|
-
"taiga_define_environment / taiga_update_environment): what runs where, how delivery is going, and how each",
|
|
20861
|
-
"environment is doing. Taiga observes deployments and never performs them. An environment needs only its App",
|
|
20862
|
-
"URL for monitoring to start.",
|
|
20863
|
-
"",
|
|
20864
|
-
"**Knowledge** (taiga_list_knowledge / taiga_get_knowledge / taiga_add_knowledge / taiga_retry_knowledge /",
|
|
20865
|
-
"taiga_delete_knowledge): reference material agents search when work calls for it, at the organization, a",
|
|
20866
|
-
"factory or a product. A rule that applies every time belongs in instructions instead.",
|
|
20867
|
-
"",
|
|
20868
|
-
"**Setting a product up** (taiga_create_factory / taiga_create_product / taiga_update_product /",
|
|
20869
|
-
"taiga_list_linkable_repositories / taiga_link_repository / taiga_start_import / taiga_finish_discovery /",
|
|
20870
|
-
"taiga_reopen_discovery / taiga_move_product): a product starts from scratch (Discovery is a conversation,",
|
|
20871
|
-
"held in the web) or by importing a repository, chosen when it is created. An import links the repository,",
|
|
20872
|
-
"starts the import and runs by itself; finishing Discovery locks the documents and opens the rest of the",
|
|
20873
|
-
"product. Linking a repository and creating or moving factories are for owners and admins. GitHub is",
|
|
20874
|
-
"connected once, in the web; members and deleting stay there too.",
|
|
20875
|
-
"",
|
|
20876
|
-
"**Product documents and autonomy** (taiga_update_product_document / taiga_publish_product_document /",
|
|
20877
|
-
"taiga_regenerate_product_document / taiga_generate_product_documents / taiga_stop_generating_documents /",
|
|
20878
|
-
"taiga_mark_product_document_reviewed /",
|
|
20879
|
-
"taiga_set_product_autonomy / taiga_set_auto_merge): edit a product document in its draft and publish it;",
|
|
20880
|
-
"documents built from it are marked outdated and regenerated, not rewritten. Set whether plans wait for a",
|
|
20881
|
-
"review and pull requests for a person to merge. Organization-wide settings, members, roles and connections",
|
|
20882
|
-
"are administrators' work in the web.",
|
|
21292
|
+
"Taiga is a policy-aware software delivery platform: an organization's policies and knowledge, its factories",
|
|
21293
|
+
"and products, each product's initiatives (its backlog), the line that plans and builds them one at a time, the",
|
|
21294
|
+
"coding agent's runs and pull requests, maintenance findings, and Action Required, where work waits on a person.",
|
|
21295
|
+
"Search these tools whenever the person asks about a Taiga product, an initiative, a run or pull request, policy",
|
|
21296
|
+
"or compliance, findings, deployments or monitoring, or setting a product up.",
|
|
20883
21297
|
"",
|
|
20884
|
-
"**
|
|
20885
|
-
"
|
|
20886
|
-
"
|
|
20887
|
-
"
|
|
20888
|
-
"text; a hand edit keeps the source it had) and",
|
|
20889
|
-
"`priority: core` marks the sections every organization keeps. Neither changes what the section",
|
|
20890
|
-
"requires of you.",
|
|
21298
|
+
"**Start here.** taiga_get_product_context (the product by git remote URL, slug or ID) gives its state in one",
|
|
21299
|
+
"call: what it still needs while in setup, its documents as outlines, policies as section ids, open initiatives,",
|
|
21300
|
+
"recent runs with their outcome, and what waits on a person. Then read only what you need. Across products,",
|
|
21301
|
+
"taiga_list_products and taiga_list_action_required.",
|
|
20891
21302
|
"",
|
|
20892
|
-
"
|
|
20893
|
-
"
|
|
20894
|
-
"
|
|
21303
|
+
"**Words.** An initiative is a unit of work in the backlog; a run is one attempt by the coding agent to build it;",
|
|
21304
|
+
"the line is the product's queue; a factory owns a set of products. IDs are UUIDs, never names; a NOT_FOUND names",
|
|
21305
|
+
"the tool that lists the right ones. A refusal carries the reason: say it as it is.",
|
|
20895
21306
|
"",
|
|
20896
|
-
"
|
|
20897
|
-
"
|
|
20898
|
-
"prompts open a session: taiga_start_session, taiga_implement_backlog_item, taiga_review_against_policies.",
|
|
21307
|
+
"**Before you write.** A tool that writes says so first: \"Writes.\" or \"Acts on paid or shared work: ask the",
|
|
21308
|
+
"person first.\" Ask before the second kind: it starts paid agent work, interrupts it, or changes what others see.",
|
|
20899
21309
|
"",
|
|
20900
|
-
"
|
|
20901
|
-
"
|
|
20902
|
-
"publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
|
|
20903
|
-
"This keeps the knowledge base accurate for everyone."
|
|
21310
|
+
"**Only in the web:** connecting GitHub, members and roles, the Discovery conversation of a product started from",
|
|
21311
|
+
"scratch, stamping checks, starting a run over, and deleting anything."
|
|
20904
21312
|
].join("\n");
|
|
20905
21313
|
//#endregion
|
|
20906
21314
|
//#region src/server.ts
|