@taiga-ai/mcp-server 0.18.0-rc.445 → 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 +1128 -650
- package/package.json +1 -1
- package/src/client.test.ts +2 -2
- package/src/client.ts +201 -145
- package/src/modern.integration.test.ts +1 -1
- package/src/server.integration.test.ts +782 -243
- package/src/tools/autonomy.ts +3 -5
- package/src/tools/backlogs.ts +182 -72
- package/src/tools/builds.ts +142 -53
- package/src/tools/catalogue.test.ts +28 -22
- package/src/tools/context.ts +126 -56
- package/src/tools/documents.ts +12 -5
- package/src/tools/errors.ts +40 -2
- package/src/tools/hints.ts +37 -9
- package/src/tools/inbox.ts +149 -28
- package/src/tools/index.ts +15 -1
- package/src/tools/instructions.ts +32 -154
- package/src/tools/jobs.ts +6 -5
- package/src/tools/knowledge.ts +4 -6
- package/src/tools/maintenance.ts +53 -32
- package/src/tools/markdown.ts +13 -5
- package/src/tools/observe.ts +32 -14
- package/src/tools/product-documents.ts +4 -6
- package/src/tools/products.ts +32 -27
- package/src/tools/prompts.ts +14 -20
- package/src/tools/queue.ts +47 -26
- package/src/tools/readiness.ts +172 -0
- package/src/tools/repositories.ts +11 -9
- package/src/tools/resources.ts +16 -19
- package/src/tools/sections.test.ts +3 -3
- package/src/tools/sections.ts +57 -16
- package/src/tools/setup.ts +47 -51
- package/src/tools/summarize.test.ts +20 -0
- package/src/tools/summarize.ts +17 -13
- package/src/tools/wait.ts +39 -26
- package/taiga-ai-mcp-server-0.19.0-dev.451.tgz +0 -0
- package/tsconfig.json +1 -5
- package/taiga-ai-mcp-server-0.18.0-rc.445.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,14 +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
|
*/
|
|
2210
|
+
const ITEM_ID_HINT = "taiga_list_initiatives lists a product's initiatives with their IDs; a run or plan ID is not one.";
|
|
2172
2211
|
const BY_ARGUMENT = {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
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.",
|
|
2176
2214
|
planId: "taiga_get_plan gives the initiative's active plan and its ID.",
|
|
2177
2215
|
repositoryId: "taiga_list_product_repositories lists the product's repository links; a GitHub repository id is not a link ID.",
|
|
2178
2216
|
replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
|
|
@@ -2189,17 +2227,41 @@ const BY_ARGUMENT = {
|
|
|
2189
2227
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2190
2228
|
const BY_TOOL = {
|
|
2191
2229
|
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2192
|
-
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
|
|
2193
2253
|
};
|
|
2194
|
-
/** 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. */
|
|
2195
2255
|
function notFoundHint(tool, args) {
|
|
2196
2256
|
if (BY_TOOL[tool]) return BY_TOOL[tool];
|
|
2197
2257
|
if (typeof args !== "object" || args === null) return void 0;
|
|
2198
2258
|
const given = Object.keys(args).filter((key) => args[key] != null && BY_ARGUMENT[key]);
|
|
2199
2259
|
given.sort((a, b) => Number(a === "productId") - Number(b === "productId"));
|
|
2200
2260
|
if (given.length === 0) return void 0;
|
|
2201
|
-
|
|
2202
|
-
|
|
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
|
+
};
|
|
2203
2265
|
}
|
|
2204
2266
|
/** The hint of the tool call in progress, read by runTool when the API answers 404. */
|
|
2205
2267
|
const notFoundHints = new AsyncLocalStorage();
|
|
@@ -2214,11 +2276,45 @@ function withStop(text) {
|
|
|
2214
2276
|
}
|
|
2215
2277
|
//#endregion
|
|
2216
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
|
+
}
|
|
2217
2313
|
/** A successful tool result: one JSON text block the model reads. */
|
|
2218
2314
|
function toolResult(value) {
|
|
2219
2315
|
return { content: [{
|
|
2220
2316
|
type: "text",
|
|
2221
|
-
text: JSON.stringify(value, null, 2)
|
|
2317
|
+
text: JSON.stringify(inProductWords(value), null, 2)
|
|
2222
2318
|
}] };
|
|
2223
2319
|
}
|
|
2224
2320
|
/**
|
|
@@ -2233,7 +2329,7 @@ function toolError(error, message, details) {
|
|
|
2233
2329
|
text: JSON.stringify(details ? {
|
|
2234
2330
|
error,
|
|
2235
2331
|
message,
|
|
2236
|
-
details
|
|
2332
|
+
details: inProductWords(details)
|
|
2237
2333
|
} : {
|
|
2238
2334
|
error,
|
|
2239
2335
|
message
|
|
@@ -2366,6 +2462,10 @@ function webOrigin(url) {
|
|
|
2366
2462
|
function isRecord(value) {
|
|
2367
2463
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2368
2464
|
}
|
|
2465
|
+
/** A section stored as an object rather than a bare string. */
|
|
2466
|
+
function isSectionObject(value) {
|
|
2467
|
+
return isRecord(value);
|
|
2468
|
+
}
|
|
2369
2469
|
/** The `sections` map of a document, or null when the document is not section-shaped. */
|
|
2370
2470
|
function sectionsOf(data) {
|
|
2371
2471
|
return isRecord(data.sections) ? data.sections : null;
|
|
@@ -2375,7 +2475,7 @@ function sectionContent(sections, sectionId) {
|
|
|
2375
2475
|
const section = sections[sectionId];
|
|
2376
2476
|
if (section === void 0 || section === null) return null;
|
|
2377
2477
|
if (typeof section === "string") return section;
|
|
2378
|
-
return
|
|
2478
|
+
return isSectionObject(section) && typeof section.content === "string" ? section.content : "";
|
|
2379
2479
|
}
|
|
2380
2480
|
/** The map with one section's text replaced (or the section added); every other section untouched. */
|
|
2381
2481
|
function applySectionEdit(sections, sectionId, content) {
|
|
@@ -2397,8 +2497,9 @@ function applySectionEdit(sections, sectionId, content) {
|
|
|
2397
2497
|
*/
|
|
2398
2498
|
function stripProvenance(doc) {
|
|
2399
2499
|
const { derivedFrom: _derivedFrom, outdated: _outdated, ...rest } = doc;
|
|
2400
|
-
|
|
2401
|
-
|
|
2500
|
+
const { data } = doc;
|
|
2501
|
+
if (!isRecord(data)) return rest;
|
|
2502
|
+
const sections = sectionsOf(data);
|
|
2402
2503
|
if (!sections) return rest;
|
|
2403
2504
|
const stripped = Object.fromEntries(Object.entries(sections).map(([id, section]) => {
|
|
2404
2505
|
if (!isRecord(section)) return [id, section];
|
|
@@ -2408,7 +2509,7 @@ function stripProvenance(doc) {
|
|
|
2408
2509
|
return {
|
|
2409
2510
|
...rest,
|
|
2410
2511
|
data: {
|
|
2411
|
-
...
|
|
2512
|
+
...data,
|
|
2412
2513
|
sections: stripped
|
|
2413
2514
|
}
|
|
2414
2515
|
};
|
|
@@ -2421,7 +2522,7 @@ function stripProvenance(doc) {
|
|
|
2421
2522
|
function instructionsText(sections) {
|
|
2422
2523
|
const parts = [];
|
|
2423
2524
|
for (const section of Object.values(sections)) {
|
|
2424
|
-
const content =
|
|
2525
|
+
const content = isSectionObject(section) ? section.content : void 0;
|
|
2425
2526
|
if (typeof content === "string" && content.trim()) parts.push(content.trim());
|
|
2426
2527
|
}
|
|
2427
2528
|
return parts.join("\n\n").trim() || null;
|
|
@@ -2480,7 +2581,7 @@ function budgetSections(doc, budget) {
|
|
|
2480
2581
|
* The `sections` input of a document read: some sections by id, none for the
|
|
2481
2582
|
* outline (every id with its size and no text), or omitted for the whole.
|
|
2482
2583
|
*/
|
|
2483
|
-
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");
|
|
2484
2585
|
/**
|
|
2485
2586
|
* The answer to a read that names sections: the ones the document has, the
|
|
2486
2587
|
* rest listed with their sizes, and the asked-for ids it lacks under
|
|
@@ -2494,6 +2595,14 @@ function sectionReadResult(doc, ids, label) {
|
|
|
2494
2595
|
if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
|
|
2495
2596
|
const all = Object.keys(sectionsOf(doc.data) ?? {});
|
|
2496
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
|
+
}
|
|
2497
2606
|
return toolResult({
|
|
2498
2607
|
...cut.doc,
|
|
2499
2608
|
omittedSections: cut.omittedSections,
|
|
@@ -2501,6 +2610,90 @@ function sectionReadResult(doc, ids, label) {
|
|
|
2501
2610
|
});
|
|
2502
2611
|
}
|
|
2503
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
|
|
2504
2697
|
//#region src/tools/summarize.ts
|
|
2505
2698
|
/**
|
|
2506
2699
|
* Keep only the named fields of each item. List tools return summaries (the
|
|
@@ -2650,9 +2843,8 @@ async function linkedProducts(client, repository, repositoryHost) {
|
|
|
2650
2843
|
}
|
|
2651
2844
|
/** A document list row as the model sees it: the summary fields plus a display name and the staleness fact. */
|
|
2652
2845
|
function documentSummaries(rows) {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
const outdated = typed[index].outdated;
|
|
2846
|
+
return pickFields(rows, DOCUMENT_SUMMARY_FIELDS$1).map((picked, index) => {
|
|
2847
|
+
const outdated = rows[index].outdated;
|
|
2656
2848
|
const fact = picked.isDraft !== false ? void 0 : outdated === null ? false : Array.isArray(outdated?.sources) ? outdated.sources.length > 0 : void 0;
|
|
2657
2849
|
return {
|
|
2658
2850
|
type: picked.type,
|
|
@@ -2823,7 +3015,7 @@ function registerProductTools(server, client) {
|
|
|
2823
3015
|
}));
|
|
2824
3016
|
server.registerTool("taiga_get_product", {
|
|
2825
3017
|
title: "Get product",
|
|
2826
|
-
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.",
|
|
2827
3019
|
inputSchema: z.object({
|
|
2828
3020
|
productId: productId.optional(),
|
|
2829
3021
|
slug: slug.optional()
|
|
@@ -2832,10 +3024,10 @@ function registerProductTools(server, client) {
|
|
|
2832
3024
|
}, (params) => runTool(async () => {
|
|
2833
3025
|
const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
|
|
2834
3026
|
if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
|
|
2835
|
-
const
|
|
2836
|
-
return toolResult(
|
|
3027
|
+
const readiness = await setupReadiness(client, resolved.product);
|
|
3028
|
+
return toolResult(readiness ? {
|
|
2837
3029
|
...resolved.product,
|
|
2838
|
-
|
|
3030
|
+
readiness
|
|
2839
3031
|
} : resolved.product);
|
|
2840
3032
|
}));
|
|
2841
3033
|
server.registerTool("taiga_resolve_product", {
|
|
@@ -2932,7 +3124,7 @@ function sectionIdFrom(readTool) {
|
|
|
2932
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.`);
|
|
2933
3125
|
}
|
|
2934
3126
|
const sectionContentInput = z.string().describe("The full new Markdown text of the section; it replaces the old text");
|
|
2935
|
-
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)}`);
|
|
2936
3128
|
/** Everything the model needs back from a write, without the content it already holds. */
|
|
2937
3129
|
function documentSummary(doc) {
|
|
2938
3130
|
const { data, ...rest } = doc;
|
|
@@ -2994,7 +3186,7 @@ function registerDocumentTools(server, client) {
|
|
|
2994
3186
|
if (client.hasCapability("tenant:documents:read")) {
|
|
2995
3187
|
server.registerTool("taiga_list_documents", {
|
|
2996
3188
|
title: "List company documents",
|
|
2997
|
-
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.`,
|
|
2998
3190
|
inputSchema: z.object({ ...pagination }),
|
|
2999
3191
|
annotations: READ_ONLY
|
|
3000
3192
|
}, (params) => runTool(async () => {
|
|
@@ -3003,7 +3195,7 @@ function registerDocumentTools(server, client) {
|
|
|
3003
3195
|
}));
|
|
3004
3196
|
server.registerTool("taiga_get_document", {
|
|
3005
3197
|
title: "Get company document",
|
|
3006
|
-
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.",
|
|
3007
3199
|
inputSchema: z.object({
|
|
3008
3200
|
type: tenantDocumentType,
|
|
3009
3201
|
sections: sectionsInput
|
|
@@ -3054,7 +3246,7 @@ function registerDocumentTools(server, client) {
|
|
|
3054
3246
|
if (client.hasCapability("factories:read")) {
|
|
3055
3247
|
server.registerTool("taiga_list_factory_documents", {
|
|
3056
3248
|
title: "List factory documents",
|
|
3057
|
-
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.`,
|
|
3058
3250
|
inputSchema: z.object({
|
|
3059
3251
|
factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
|
|
3060
3252
|
...pagination
|
|
@@ -3218,9 +3410,9 @@ function requestPrompt(title, description) {
|
|
|
3218
3410
|
}
|
|
3219
3411
|
//#endregion
|
|
3220
3412
|
//#region src/tools/queue.ts
|
|
3221
|
-
/** 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. */
|
|
3222
3414
|
const NAMED_ENTRIES = 25;
|
|
3223
|
-
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.";
|
|
3224
3416
|
/**
|
|
3225
3417
|
* What lifts a halt, in the tools that do it. Read from the halt registry in
|
|
3226
3418
|
* @taiga/types (how each cause clears, and whether a person may lift it), so
|
|
@@ -3230,11 +3422,11 @@ const MOVE_ASIDE = "or move it down (taiga_queue_backlog_item with afterId) or o
|
|
|
3230
3422
|
function haltNextStep(cause) {
|
|
3231
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).";
|
|
3232
3424
|
const spec = cause ? HALT_CAUSES[cause] : void 0;
|
|
3233
|
-
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}`;
|
|
3234
3426
|
const known = cause;
|
|
3235
|
-
if (canResumeHalt(known)) return `
|
|
3427
|
+
if (canResumeHalt(known)) return `taiga_resume_initiative lifts it, ${MOVE_ASIDE}`;
|
|
3236
3428
|
if (haltClearsItself(known)) return `It clears when ${spec.clearedWhen}; ${MOVE_ASIDE}`;
|
|
3237
|
-
return `Only taking it off the line clears it (
|
|
3429
|
+
return `Only taking it off the line clears it (taiga_move_initiative): ${spec.clearedWhen}.`;
|
|
3238
3430
|
}
|
|
3239
3431
|
/**
|
|
3240
3432
|
* The entry the line waits on, by the queue's two kinds of blocker
|
|
@@ -3279,7 +3471,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
3279
3471
|
const canName = client.hasCapability("backlog:read");
|
|
3280
3472
|
const blocking = blockingIndex(entries, (entry) => entry.state);
|
|
3281
3473
|
const toName = !canName ? [] : options.name === "all" ? entries.slice(0, NAMED_ENTRIES).map((_, index) => index) : blocking >= 0 ? [blocking] : [];
|
|
3282
|
-
const settled = await Promise.allSettled(toName.map((index) => client.getBacklogItem(
|
|
3474
|
+
const settled = await Promise.allSettled(toName.map((index) => client.getBacklogItem(entries[index].backlogItemId)));
|
|
3283
3475
|
const reads = new Map(toName.map((index, at) => [index, settled[at]]));
|
|
3284
3476
|
let unnamed = options.name === "all" ? entries.length - toName.length : 0;
|
|
3285
3477
|
const rows = entries.map((entry, index) => {
|
|
@@ -3308,7 +3500,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
3308
3500
|
cta: lifecycle.cta
|
|
3309
3501
|
} : null,
|
|
3310
3502
|
...latestBuild ? {
|
|
3311
|
-
|
|
3503
|
+
runId: latestBuild.id,
|
|
3312
3504
|
prUrl: latestBuild.prUrl ?? null
|
|
3313
3505
|
} : {}
|
|
3314
3506
|
});
|
|
@@ -3325,7 +3517,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
3325
3517
|
}
|
|
3326
3518
|
return row;
|
|
3327
3519
|
});
|
|
3328
|
-
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.";
|
|
3329
3521
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
3330
3522
|
return {
|
|
3331
3523
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
@@ -3349,7 +3541,7 @@ async function linePausedSince(client, product) {
|
|
|
3349
3541
|
}
|
|
3350
3542
|
function registerQueueTools(server, client) {
|
|
3351
3543
|
if (!client.hasCapability("products:read")) return;
|
|
3352
|
-
server.registerTool("
|
|
3544
|
+
server.registerTool("taiga_get_line", {
|
|
3353
3545
|
title: "Get the product queue",
|
|
3354
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.",
|
|
3355
3547
|
inputSchema: z.object({ productId }),
|
|
@@ -3358,7 +3550,7 @@ function registerQueueTools(server, client) {
|
|
|
3358
3550
|
if (!client.hasCapability("products:write")) return;
|
|
3359
3551
|
server.registerTool("taiga_pause_line", {
|
|
3360
3552
|
title: "Pause or resume a product's line",
|
|
3361
|
-
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.",
|
|
3362
3554
|
inputSchema: z.object({
|
|
3363
3555
|
productId,
|
|
3364
3556
|
paused: z.boolean().describe("true pauses the line, false resumes it")
|
|
@@ -3374,6 +3566,243 @@ function registerQueueTools(server, client) {
|
|
|
3374
3566
|
}));
|
|
3375
3567
|
}
|
|
3376
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
|
|
3377
3806
|
//#region src/tools/backlogs.ts
|
|
3378
3807
|
/** What a list row needs to say for the model to pick an item to read in full. */
|
|
3379
3808
|
const BACKLOG_SUMMARY_FIELDS = [
|
|
@@ -3409,6 +3838,83 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
3409
3838
|
"archivedAt",
|
|
3410
3839
|
"lifecycle"
|
|
3411
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
|
+
}
|
|
3412
3918
|
/** What each plan failure cause means for whoever reads it next (PLAN_FAILURE_CAUSES in @taiga/types). */
|
|
3413
3919
|
const PLAN_FAILURE_MEANING = {
|
|
3414
3920
|
agent_error: "The planning agent failed. Planning again is the usual next step.",
|
|
@@ -3420,10 +3926,10 @@ const PLAN_FAILURE_MEANING = {
|
|
|
3420
3926
|
* (PRO-2780): planFailureCause alone said "cancelled" with nobody behind it.
|
|
3421
3927
|
* Best effort: a history the API will not give leaves the item as it was.
|
|
3422
3928
|
*/
|
|
3423
|
-
async function planFailureOf(client,
|
|
3929
|
+
async function planFailureOf(client, initiativeId, itemCause) {
|
|
3424
3930
|
let events;
|
|
3425
3931
|
try {
|
|
3426
|
-
events = (await client.listBacklogItemActivity(
|
|
3932
|
+
events = (await client.listBacklogItemActivity(initiativeId)).data;
|
|
3427
3933
|
} catch (error) {
|
|
3428
3934
|
if (error instanceof TaigaApiError || isNetworkFailure(error)) return null;
|
|
3429
3935
|
throw error;
|
|
@@ -3455,7 +3961,7 @@ async function planFailureOf(client, itemId, itemCause) {
|
|
|
3455
3961
|
};
|
|
3456
3962
|
}
|
|
3457
3963
|
const planId = idInput("plan", "taiga_get_plan gives the initiative's active plan and its ID");
|
|
3458
|
-
const
|
|
3964
|
+
const initiativeId = idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it");
|
|
3459
3965
|
/** The plan without its steps' prose: enough to approve it or say what a run will do. */
|
|
3460
3966
|
const PLAN_SUMMARY_FIELDS = [
|
|
3461
3967
|
"id",
|
|
@@ -3474,14 +3980,11 @@ const PLAN_STEP_SUMMARY_FIELDS = [
|
|
|
3474
3980
|
"reads",
|
|
3475
3981
|
"createsResources"
|
|
3476
3982
|
];
|
|
3477
|
-
/** The outcome fields of an initiative, with how many of its criteria are met. */
|
|
3478
|
-
const outcomeOf = (item) => {
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
criteria: criteriaCounts(row)
|
|
3483
|
-
};
|
|
3484
|
-
};
|
|
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, {
|
|
3985
|
+
...pickFields([item], ITEM_OUTCOME_FIELDS)[0],
|
|
3986
|
+
criteria: criteriaCounts(item)
|
|
3987
|
+
});
|
|
3485
3988
|
const outcome = (item) => toolResult(outcomeOf(item));
|
|
3486
3989
|
/**
|
|
3487
3990
|
* Each tool is registered under the capability its route requires, so a
|
|
@@ -3513,9 +4016,9 @@ function registerBacklogTools(server, client) {
|
|
|
3513
4016
|
if (may("backlog:read") && may("backlog:write") && may("products:write")) registerPlanAgain(server, client);
|
|
3514
4017
|
}
|
|
3515
4018
|
function registerBacklogReads(server, client) {
|
|
3516
|
-
server.registerTool("
|
|
3517
|
-
title: "List
|
|
3518
|
-
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.",
|
|
3519
4022
|
inputSchema: z.object({
|
|
3520
4023
|
productId,
|
|
3521
4024
|
query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
|
|
@@ -3545,7 +4048,7 @@ function registerBacklogReads(server, client) {
|
|
|
3545
4048
|
const { query, ...filters } = params;
|
|
3546
4049
|
if (query === void 0) {
|
|
3547
4050
|
const result = await client.listBacklogItems(params.productId, filters);
|
|
3548
|
-
return toolResult(page(
|
|
4051
|
+
return toolResult(page(situatedRows(result.data), result.nextCursor));
|
|
3549
4052
|
}
|
|
3550
4053
|
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
|
|
3551
4054
|
const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
|
|
@@ -3554,34 +4057,36 @@ function registerBacklogReads(server, client) {
|
|
|
3554
4057
|
cursor
|
|
3555
4058
|
}), query, (row) => [row.title], params.limit ?? 25);
|
|
3556
4059
|
return toolResult({
|
|
3557
|
-
...page(
|
|
4060
|
+
...page(situatedRows(found.rows), null),
|
|
3558
4061
|
...found.more ? { more: SEARCH_NOTE } : {}
|
|
3559
4062
|
});
|
|
3560
4063
|
}));
|
|
3561
|
-
server.registerTool("
|
|
3562
|
-
title: "Get
|
|
3563
|
-
description: "Get one
|
|
3564
|
-
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 }),
|
|
3565
4068
|
annotations: READ_ONLY
|
|
3566
4069
|
}, (params) => runTool(async () => {
|
|
3567
|
-
const item = (await client.getBacklogItem(params.
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
4070
|
+
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
4071
|
+
const row = item;
|
|
4072
|
+
if (item.planningStatus !== "failed") return toolResult(withSituation(row));
|
|
4073
|
+
const ownCause = item.planFailureCause;
|
|
4074
|
+
const planFailure = await planFailureOf(client, params.initiativeId, ownCause);
|
|
4075
|
+
return toolResult(withSituation(row, planFailure ? {
|
|
3571
4076
|
...item,
|
|
3572
4077
|
planFailure
|
|
3573
|
-
} : item);
|
|
4078
|
+
} : item));
|
|
3574
4079
|
}));
|
|
3575
4080
|
server.registerTool("taiga_get_plan", {
|
|
3576
4081
|
title: "Get the active plan",
|
|
3577
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).",
|
|
3578
4083
|
inputSchema: z.object({
|
|
3579
|
-
|
|
4084
|
+
initiativeId,
|
|
3580
4085
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
|
|
3581
4086
|
}),
|
|
3582
4087
|
annotations: READ_ONLY
|
|
3583
4088
|
}, (params) => runTool(async () => {
|
|
3584
|
-
const plan = (await client.getActivePlan(params.
|
|
4089
|
+
const plan = (await client.getActivePlan(params.initiativeId)).data;
|
|
3585
4090
|
if (params.full) return toolResult(plan);
|
|
3586
4091
|
const steps = Array.isArray(plan.steps) ? plan.steps : [];
|
|
3587
4092
|
return toolResult({
|
|
@@ -3612,18 +4117,18 @@ function registerRequest(server, client) {
|
|
|
3612
4117
|
}));
|
|
3613
4118
|
}
|
|
3614
4119
|
function registerMove(server, client) {
|
|
3615
|
-
server.registerTool("
|
|
3616
|
-
title: "Move
|
|
3617
|
-
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.`,
|
|
3618
4123
|
inputSchema: z.object({
|
|
3619
|
-
|
|
4124
|
+
initiativeId,
|
|
3620
4125
|
state: z.enum(USER_DROP_STATES).describe("The column to move into"),
|
|
3621
|
-
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"),
|
|
3622
4127
|
archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
|
|
3623
4128
|
}),
|
|
3624
4129
|
annotations: OPERATE
|
|
3625
4130
|
}, (params) => runTool(async () => {
|
|
3626
|
-
const {
|
|
4131
|
+
const { initiativeId: id, ...body } = params;
|
|
3627
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.");
|
|
3628
4133
|
return outcome((await client.transitionInitiative(id, body)).data);
|
|
3629
4134
|
}));
|
|
@@ -3633,20 +4138,20 @@ const BUILD_STATION = /* @__PURE__ */ new Set(["plan", "build"]);
|
|
|
3633
4138
|
function registerPlanAgain(server, client) {
|
|
3634
4139
|
server.registerTool("taiga_plan_again", {
|
|
3635
4140
|
title: "Plan an initiative again",
|
|
3636
|
-
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.",
|
|
3637
4142
|
inputSchema: z.object({
|
|
3638
|
-
|
|
4143
|
+
initiativeId,
|
|
3639
4144
|
force: z.boolean().optional().describe("Plan anyway past an unsettled dependency or an unchosen UI direction; only when the person says so")
|
|
3640
4145
|
}),
|
|
3641
4146
|
annotations: OPERATE
|
|
3642
4147
|
}, (params) => runTool(async () => {
|
|
3643
|
-
const item = (await client.getBacklogItem(params.
|
|
3644
|
-
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.`);
|
|
3645
4150
|
const body = {
|
|
3646
4151
|
state: "plan",
|
|
3647
4152
|
...params.force ? { force: true } : {}
|
|
3648
4153
|
};
|
|
3649
|
-
return outcome((await client.transitionInitiative(params.
|
|
4154
|
+
return outcome((await client.transitionInitiative(params.initiativeId, body)).data);
|
|
3650
4155
|
}));
|
|
3651
4156
|
}
|
|
3652
4157
|
/** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
|
|
@@ -3659,7 +4164,7 @@ const isApiFailure$2 = (error) => error instanceof TaigaApiError || isNetworkFai
|
|
|
3659
4164
|
async function reviewGate(client, item) {
|
|
3660
4165
|
const own = typeof item.autoBuild === "boolean" ? item.autoBuild : null;
|
|
3661
4166
|
try {
|
|
3662
|
-
const product = own === null ? (await client.getProduct(
|
|
4167
|
+
const product = own === null ? (await client.getProduct(item.productId)).data : {};
|
|
3663
4168
|
const builds = resolveAutoBuild({
|
|
3664
4169
|
initiative: own,
|
|
3665
4170
|
product: typeof product.autoBuild === "boolean" ? product.autoBuild : true
|
|
@@ -3677,7 +4182,7 @@ async function reviewGate(client, item) {
|
|
|
3677
4182
|
/** Where a queued initiative stands in its product's line; null, and the outcome says so, when the queue cannot be read. */
|
|
3678
4183
|
async function placeInQueue(client, item) {
|
|
3679
4184
|
try {
|
|
3680
|
-
const view = await queueView(client,
|
|
4185
|
+
const view = await queueView(client, item.productId, { name: "blocking" });
|
|
3681
4186
|
return {
|
|
3682
4187
|
position: view.entries.find((row) => row.backlogItemId === item.id)?.position ?? null,
|
|
3683
4188
|
of: view.entries.length,
|
|
@@ -3690,17 +4195,17 @@ async function placeInQueue(client, item) {
|
|
|
3690
4195
|
}
|
|
3691
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");
|
|
3692
4197
|
function registerQueue(server, client) {
|
|
3693
|
-
server.registerTool("
|
|
3694
|
-
title: "Queue
|
|
3695
|
-
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.",
|
|
3696
4201
|
inputSchema: z.object({
|
|
3697
|
-
|
|
3698
|
-
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"),
|
|
3699
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")
|
|
3700
4205
|
}),
|
|
3701
4206
|
annotations: OPERATE
|
|
3702
4207
|
}, (params) => runTool(async () => {
|
|
3703
|
-
const {
|
|
4208
|
+
const { initiativeId: id, autoBuild, ...placement } = params;
|
|
3704
4209
|
if (autoBuild !== void 0) await client.setAutoBuild(id, autoBuild);
|
|
3705
4210
|
const item = (await client.transitionInitiative(id, {
|
|
3706
4211
|
state: "queue",
|
|
@@ -3711,7 +4216,7 @@ function registerQueue(server, client) {
|
|
|
3711
4216
|
...outcomeOf(item),
|
|
3712
4217
|
reviewGate: gate,
|
|
3713
4218
|
queue,
|
|
3714
|
-
...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." }
|
|
3715
4220
|
});
|
|
3716
4221
|
}));
|
|
3717
4222
|
}
|
|
@@ -3720,12 +4225,12 @@ function registerReviewGate(server, client) {
|
|
|
3720
4225
|
title: "Set an initiative's review gate",
|
|
3721
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.",
|
|
3722
4227
|
inputSchema: z.object({
|
|
3723
|
-
|
|
4228
|
+
initiativeId,
|
|
3724
4229
|
autoBuild: autoBuildInput
|
|
3725
4230
|
}),
|
|
3726
4231
|
annotations: OPERATE_SETTING
|
|
3727
4232
|
}, (params) => runTool(async () => {
|
|
3728
|
-
const item = (await client.setAutoBuild(params.
|
|
4233
|
+
const item = (await client.setAutoBuild(params.initiativeId, params.autoBuild)).data;
|
|
3729
4234
|
const gate = await reviewGate(client, item);
|
|
3730
4235
|
return toolResult({
|
|
3731
4236
|
...outcomeOf(item),
|
|
@@ -3735,12 +4240,12 @@ function registerReviewGate(server, client) {
|
|
|
3735
4240
|
}));
|
|
3736
4241
|
}
|
|
3737
4242
|
function registerResume(server, client) {
|
|
3738
|
-
server.registerTool("
|
|
3739
|
-
title: "Resume
|
|
4243
|
+
server.registerTool("taiga_resume_initiative", {
|
|
4244
|
+
title: "Resume initiative",
|
|
3740
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.",
|
|
3741
|
-
inputSchema: z.object({
|
|
4246
|
+
inputSchema: z.object({ initiativeId }),
|
|
3742
4247
|
annotations: OPERATE
|
|
3743
|
-
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.
|
|
4248
|
+
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
|
|
3744
4249
|
}
|
|
3745
4250
|
function registerPlanVerdicts(server, client) {
|
|
3746
4251
|
server.registerTool("taiga_approve_plan", {
|
|
@@ -3760,173 +4265,234 @@ function registerPlanVerdicts(server, client) {
|
|
|
3760
4265
|
}, (params) => runTool(async () => outcome((await client.rejectPlan(params.planId, { comment: params.comment })).data)));
|
|
3761
4266
|
}
|
|
3762
4267
|
//#endregion
|
|
3763
|
-
//#region src/tools/
|
|
3764
|
-
/**
|
|
3765
|
-
|
|
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 = [
|
|
3766
4278
|
"id",
|
|
3767
|
-
"
|
|
3768
|
-
"
|
|
3769
|
-
"
|
|
3770
|
-
"targetTitle",
|
|
3771
|
-
"planId",
|
|
4279
|
+
"kind",
|
|
4280
|
+
"severity",
|
|
4281
|
+
"tier",
|
|
3772
4282
|
"status",
|
|
3773
|
-
"
|
|
3774
|
-
"
|
|
3775
|
-
"
|
|
3776
|
-
"
|
|
3777
|
-
"
|
|
3778
|
-
"
|
|
3779
|
-
"
|
|
3780
|
-
"
|
|
3781
|
-
"
|
|
3782
|
-
"
|
|
3783
|
-
"
|
|
3784
|
-
"
|
|
3785
|
-
"actions",
|
|
3786
|
-
"createdAt",
|
|
3787
|
-
"startedAt",
|
|
3788
|
-
"completedAt"
|
|
3789
|
-
];
|
|
3790
|
-
/** What a run says about itself after a stop or a resume: its state and what can be done next. */
|
|
3791
|
-
const RUN_OUTCOME_FIELDS = [
|
|
3792
|
-
"id",
|
|
3793
|
-
"runNumber",
|
|
4283
|
+
"title",
|
|
4284
|
+
"ecosystem",
|
|
4285
|
+
"packageName",
|
|
4286
|
+
"currentVersion",
|
|
4287
|
+
"recommendedVersion",
|
|
4288
|
+
"advisoryId",
|
|
4289
|
+
"location",
|
|
4290
|
+
"sourcePaths",
|
|
4291
|
+
"productRepositoryId",
|
|
4292
|
+
"ownership",
|
|
4293
|
+
"remediableBy",
|
|
4294
|
+
"controls",
|
|
3794
4295
|
"backlogItemId",
|
|
3795
|
-
"
|
|
3796
|
-
"
|
|
3797
|
-
"
|
|
3798
|
-
"totalSteps",
|
|
3799
|
-
"prUrl",
|
|
3800
|
-
"actions"
|
|
4296
|
+
"detectedAt",
|
|
4297
|
+
"ignoreReason",
|
|
4298
|
+
"ignoreExpiresAt"
|
|
3801
4299
|
];
|
|
3802
|
-
/**
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
* stalled in the hardening pass after the steps and the retry continues it.
|
|
3807
|
-
* Null for a run still going or that finished.
|
|
3808
|
-
*/
|
|
3809
|
-
function stoppedIn(run, steps) {
|
|
3810
|
-
if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
|
|
3811
|
-
if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
|
|
3812
|
-
const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
|
|
3813
|
-
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)})`;
|
|
3814
|
-
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.";
|
|
3815
|
-
return "after the steps and hardening, at the pull request";
|
|
3816
|
-
}
|
|
3817
|
-
/** How many products the org-wide list names; past this a row keeps its productId only. */
|
|
3818
|
-
const NAMED_PRODUCTS = 100;
|
|
3819
|
-
const buildId = idInput("build run", "taiga_list_builds gives it");
|
|
3820
|
-
/** A step without its instructions: what it is, how it stands and what it changed. */
|
|
3821
|
-
const STEP_SUMMARY_FIELDS = [
|
|
3822
|
-
"stepOrder",
|
|
3823
|
-
"title",
|
|
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",
|
|
3824
4304
|
"status",
|
|
3825
|
-
"
|
|
3826
|
-
"
|
|
3827
|
-
"
|
|
3828
|
-
"verification",
|
|
4305
|
+
"scanners",
|
|
4306
|
+
"findingsCount",
|
|
4307
|
+
"newFindingsCount",
|
|
3829
4308
|
"error",
|
|
3830
|
-
"
|
|
3831
|
-
"startedAt",
|
|
4309
|
+
"createdAt",
|
|
3832
4310
|
"completedAt"
|
|
3833
4311
|
];
|
|
3834
|
-
/**
|
|
3835
|
-
const
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
4312
|
+
/** A policy violation in a list: what it is, how bad, and what is being done about it. */
|
|
4313
|
+
const VIOLATION_FIELDS = [
|
|
4314
|
+
"id",
|
|
4315
|
+
"controlId",
|
|
4316
|
+
"policyType",
|
|
4317
|
+
"severity",
|
|
4318
|
+
"status",
|
|
4319
|
+
"title",
|
|
4320
|
+
"summary",
|
|
4321
|
+
"work",
|
|
4322
|
+
"unconfirmedSince",
|
|
4323
|
+
"verifyRequestedAt",
|
|
4324
|
+
"assessedAt"
|
|
4325
|
+
];
|
|
3845
4326
|
/**
|
|
3846
|
-
*
|
|
3847
|
-
*
|
|
3848
|
-
*
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
const
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
const product = nameById.get(String(row.productId));
|
|
3862
|
-
return product ? {
|
|
3863
|
-
...row,
|
|
3864
|
-
product
|
|
3865
|
-
} : row;
|
|
3866
|
-
});
|
|
3867
|
-
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
|
+
}
|
|
3868
4342
|
return {
|
|
3869
|
-
|
|
3870
|
-
|
|
4343
|
+
controls: listed.length,
|
|
4344
|
+
violations: violations.length,
|
|
4345
|
+
bySeverity,
|
|
4346
|
+
resolvedViolations: resolved
|
|
3871
4347
|
};
|
|
3872
4348
|
}
|
|
3873
|
-
|
|
4349
|
+
const findingId = idInput("finding", "taiga_list_findings gives it");
|
|
4350
|
+
function registerMaintenanceTools(server, client) {
|
|
3874
4351
|
if (!client.hasCapability("products:read")) return;
|
|
3875
|
-
server.registerTool("
|
|
3876
|
-
title: "
|
|
3877
|
-
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.",
|
|
3878
4370
|
inputSchema: z.object({
|
|
3879
|
-
productId
|
|
3880
|
-
|
|
3881
|
-
|
|
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"),
|
|
3882
4375
|
...pagination
|
|
3883
4376
|
}),
|
|
3884
4377
|
annotations: READ_ONLY
|
|
3885
4378
|
}, (params) => runTool(async () => {
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
}
|
|
3892
|
-
return toolResult(await activityRows(client, params));
|
|
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
|
+
});
|
|
3893
4385
|
}));
|
|
3894
|
-
server.registerTool("
|
|
3895
|
-
title: "Get
|
|
3896
|
-
description: "
|
|
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
|
+
});
|
|
4397
|
+
}));
|
|
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.",
|
|
3897
4401
|
inputSchema: z.object({
|
|
3898
|
-
|
|
3899
|
-
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)")
|
|
3900
4404
|
}),
|
|
3901
4405
|
annotations: READ_ONLY
|
|
3902
4406
|
}, (params) => runTool(async () => {
|
|
3903
|
-
const
|
|
3904
|
-
const
|
|
3905
|
-
const
|
|
3906
|
-
const
|
|
3907
|
-
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);
|
|
3908
4412
|
if (params.full) return toolResult({
|
|
3909
|
-
|
|
3910
|
-
...
|
|
3911
|
-
steps: steps.data
|
|
4413
|
+
summary,
|
|
4414
|
+
...standing
|
|
3912
4415
|
});
|
|
3913
4416
|
return toolResult({
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
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)
|
|
3920
4433
|
});
|
|
3921
4434
|
}));
|
|
3922
4435
|
if (!client.hasCapability("products:write")) return;
|
|
3923
|
-
server.registerTool("
|
|
3924
|
-
title: "
|
|
3925
|
-
description: "
|
|
3926
|
-
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
|
+
}),
|
|
3927
4466
|
annotations: OPERATE
|
|
3928
4467
|
}, (params) => runTool(async () => {
|
|
3929
|
-
|
|
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
|
+
}),
|
|
4492
|
+
annotations: OPERATE
|
|
4493
|
+
}, (params) => runTool(async () => {
|
|
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);
|
|
3930
4496
|
}));
|
|
3931
4497
|
}
|
|
3932
4498
|
//#endregion
|
|
@@ -3950,9 +4516,9 @@ const REPOSITORY_SUMMARY_FIELDS = [
|
|
|
3950
4516
|
"effectiveBranch",
|
|
3951
4517
|
"isManaged"
|
|
3952
4518
|
];
|
|
3953
|
-
/** How many of the latest runs to carry;
|
|
4519
|
+
/** How many of the latest runs to carry; taiga_list_runs pages the rest. */
|
|
3954
4520
|
const BUILDS_IN_CONTEXT = 10;
|
|
3955
|
-
/** 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. */
|
|
3956
4522
|
const CONTEXT_BUILD_FIELDS = [
|
|
3957
4523
|
"id",
|
|
3958
4524
|
"runNumber",
|
|
@@ -3962,6 +4528,8 @@ const CONTEXT_BUILD_FIELDS = [
|
|
|
3962
4528
|
"failedReason",
|
|
3963
4529
|
"completedSteps",
|
|
3964
4530
|
"totalSteps",
|
|
4531
|
+
"baseBranch",
|
|
4532
|
+
"prNumber",
|
|
3965
4533
|
"prUrl",
|
|
3966
4534
|
"prState",
|
|
3967
4535
|
"prMerged",
|
|
@@ -4019,7 +4587,7 @@ const DOCUMENT_DEPTHS = [
|
|
|
4019
4587
|
const BACKLOG_PAGE_LIMIT = 10;
|
|
4020
4588
|
/**
|
|
4021
4589
|
* Open items the bundle carries, the newest of those read; past this it says truncated and
|
|
4022
|
-
*
|
|
4590
|
+
* taiga_list_initiatives pages the rest. Keeps a product with hundreds of
|
|
4023
4591
|
* open initiatives inside the tool result the document budget is sized for.
|
|
4024
4592
|
*/
|
|
4025
4593
|
const BACKLOG_IN_CONTEXT = 50;
|
|
@@ -4029,7 +4597,10 @@ const PARTS = {
|
|
|
4029
4597
|
policies: "tenant:documents:read",
|
|
4030
4598
|
backlog: "backlog:read",
|
|
4031
4599
|
repositories: "products:read",
|
|
4032
|
-
builds: "products:read"
|
|
4600
|
+
builds: "products:read",
|
|
4601
|
+
attention: "products:read",
|
|
4602
|
+
maintenance: "products:read",
|
|
4603
|
+
standing: "products:read"
|
|
4033
4604
|
};
|
|
4034
4605
|
/** What the API said when a part failed, in the words the bundle reports. */
|
|
4035
4606
|
function unavailableReason(reason) {
|
|
@@ -4081,7 +4652,7 @@ async function factoryPart(client, factoryId) {
|
|
|
4081
4652
|
* asked. Null, with a note, where unpublished.
|
|
4082
4653
|
*/
|
|
4083
4654
|
async function documentsPart(client, product, depth) {
|
|
4084
|
-
const productId =
|
|
4655
|
+
const productId = product.id;
|
|
4085
4656
|
const origin = defaultOriginFor(product);
|
|
4086
4657
|
const reads = await Promise.allSettled(CONTEXT_DOCUMENT_TYPES.map((type) => client.getProductDocument(productId, type, origin)));
|
|
4087
4658
|
const out = { origin };
|
|
@@ -4185,9 +4756,9 @@ async function backlogPart(client, productId) {
|
|
|
4185
4756
|
const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
|
|
4186
4757
|
if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
|
|
4187
4758
|
return {
|
|
4188
|
-
items:
|
|
4759
|
+
items: situatedRows(rows.slice(-50)),
|
|
4189
4760
|
truncated,
|
|
4190
|
-
...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." } : {}
|
|
4191
4762
|
};
|
|
4192
4763
|
}
|
|
4193
4764
|
/**
|
|
@@ -4203,10 +4774,56 @@ async function repositoriesPart(client, productId) {
|
|
|
4203
4774
|
}
|
|
4204
4775
|
async function buildsPart(client, productId) {
|
|
4205
4776
|
const rows = (await client.listBuildRuns(productId, { limit: BUILDS_IN_CONTEXT })).data;
|
|
4206
|
-
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
|
+
};
|
|
4207
4824
|
}
|
|
4208
4825
|
async function productContext(client, product, options) {
|
|
4209
|
-
const productId =
|
|
4826
|
+
const productId = product.id;
|
|
4210
4827
|
const granted = (part) => client.hasCapability(PARTS[part]);
|
|
4211
4828
|
const notGranted = Object.keys(PARTS).filter((part) => !granted(part));
|
|
4212
4829
|
const fetchers = {
|
|
@@ -4215,16 +4832,18 @@ async function productContext(client, product, options) {
|
|
|
4215
4832
|
policies: () => policiesPart(client),
|
|
4216
4833
|
backlog: () => backlogPart(client, productId),
|
|
4217
4834
|
repositories: () => repositoriesPart(client, productId),
|
|
4218
|
-
builds: () => buildsPart(client, productId)
|
|
4835
|
+
builds: () => buildsPart(client, productId),
|
|
4836
|
+
attention: () => attentionPart(client, productId),
|
|
4837
|
+
maintenance: () => maintenancePart(client, productId),
|
|
4838
|
+
standing: () => standingPart(client, productId)
|
|
4219
4839
|
};
|
|
4220
4840
|
const parts = Object.keys(fetchers).filter(granted);
|
|
4221
|
-
const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
|
|
4222
|
-
const nextStep = setupNextStep(product);
|
|
4841
|
+
const [settled, readiness] = await Promise.all([Promise.allSettled(parts.map((part) => fetchers[part]())), setupReadiness(client, product)]);
|
|
4223
4842
|
const bundle = { product: {
|
|
4224
4843
|
...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
|
|
4225
4844
|
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
4226
4845
|
defaultOrigin: defaultOriginFor(product),
|
|
4227
|
-
...
|
|
4846
|
+
...readiness ? { readiness } : {}
|
|
4228
4847
|
} };
|
|
4229
4848
|
const unavailable = {};
|
|
4230
4849
|
parts.forEach((part, index) => {
|
|
@@ -4237,15 +4856,15 @@ async function productContext(client, product, options) {
|
|
|
4237
4856
|
...bundle,
|
|
4238
4857
|
notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
|
|
4239
4858
|
unavailable,
|
|
4240
|
-
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."
|
|
4241
4860
|
};
|
|
4242
|
-
let characters = JSON.stringify(result, null, 2).length;
|
|
4861
|
+
let characters = JSON.stringify(inProductWords(result), null, 2).length;
|
|
4243
4862
|
for (let measured = -1; measured !== characters;) {
|
|
4244
4863
|
measured = characters;
|
|
4245
|
-
characters = JSON.stringify({
|
|
4864
|
+
characters = JSON.stringify(inProductWords({
|
|
4246
4865
|
...result,
|
|
4247
4866
|
characters: measured
|
|
4248
|
-
}, null, 2).length;
|
|
4867
|
+
}), null, 2).length;
|
|
4249
4868
|
}
|
|
4250
4869
|
return toolResult({
|
|
4251
4870
|
...result,
|
|
@@ -4256,7 +4875,7 @@ function registerContextTools(server, client) {
|
|
|
4256
4875
|
if (!client.hasCapability("products:read")) return;
|
|
4257
4876
|
server.registerTool("taiga_get_product_context", {
|
|
4258
4877
|
title: "Get product context",
|
|
4259
|
-
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.`,
|
|
4260
4879
|
inputSchema: z.object({
|
|
4261
4880
|
productId: productId.optional(),
|
|
4262
4881
|
slug: slug.optional(),
|
|
@@ -4276,6 +4895,14 @@ function registerContextTools(server, client) {
|
|
|
4276
4895
|
}
|
|
4277
4896
|
//#endregion
|
|
4278
4897
|
//#region src/tools/autonomy.ts
|
|
4898
|
+
/**
|
|
4899
|
+
* Autonomy (docs.tai.ga, Deliver: Initiatives, Auto-merge): whether a landed
|
|
4900
|
+
* plan is built without a review, and whether a pull request merges on its
|
|
4901
|
+
* own. The organization and its factories are ceilings for merging, set by
|
|
4902
|
+
* an administrator in the web; a product sets defaults; an initiative's own
|
|
4903
|
+
* switch is the final word within the ceilings. The initiative's build gate
|
|
4904
|
+
* is taiga_set_auto_build.
|
|
4905
|
+
*/
|
|
4279
4906
|
function registerAutonomyTools(server, client) {
|
|
4280
4907
|
if (client.hasCapability("products:write")) server.registerTool("taiga_set_product_autonomy", {
|
|
4281
4908
|
title: "Set a product's autonomy defaults",
|
|
@@ -4305,12 +4932,12 @@ function registerAutonomyTools(server, client) {
|
|
|
4305
4932
|
title: "Set whether an initiative's pull request merges on its own",
|
|
4306
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.",
|
|
4307
4934
|
inputSchema: z.object({
|
|
4308
|
-
|
|
4935
|
+
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
|
|
4309
4936
|
autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
|
|
4310
4937
|
}),
|
|
4311
4938
|
annotations: OPERATE_SETTING
|
|
4312
4939
|
}, (params) => runTool(async () => {
|
|
4313
|
-
const item = (await client.updateBacklogItem(params.
|
|
4940
|
+
const item = (await client.updateBacklogItem(params.initiativeId, { autoMerge: params.autoMerge })).data;
|
|
4314
4941
|
return toolResult(pickFields([item], [
|
|
4315
4942
|
"id",
|
|
4316
4943
|
"title",
|
|
@@ -4322,6 +4949,11 @@ function registerAutonomyTools(server, client) {
|
|
|
4322
4949
|
}
|
|
4323
4950
|
//#endregion
|
|
4324
4951
|
//#region src/tools/inbox.ts
|
|
4952
|
+
/**
|
|
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.
|
|
4956
|
+
*/
|
|
4325
4957
|
const ITEM_FIELDS = [
|
|
4326
4958
|
"id",
|
|
4327
4959
|
"productId",
|
|
@@ -4335,10 +4967,48 @@ const ITEM_FIELDS = [
|
|
|
4335
4967
|
"status",
|
|
4336
4968
|
"blocking",
|
|
4337
4969
|
"snoozedUntil",
|
|
4338
|
-
"metadata",
|
|
4339
4970
|
"createdAt",
|
|
4340
4971
|
"resolvedAt"
|
|
4341
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;
|
|
4342
5012
|
const SETUP_TASK_FIELDS = [
|
|
4343
5013
|
"id",
|
|
4344
5014
|
"productId",
|
|
@@ -4359,38 +5029,94 @@ const SETUP_TASK_FIELDS = [
|
|
|
4359
5029
|
function totalOf(result) {
|
|
4360
5030
|
return typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {};
|
|
4361
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
|
+
}
|
|
4362
5070
|
function registerInboxTools(server, client) {
|
|
4363
5071
|
if (!client.hasCapability("products:read")) return;
|
|
4364
5072
|
server.registerTool("taiga_list_action_required", {
|
|
4365
5073
|
title: "List what needs a person",
|
|
4366
|
-
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.",
|
|
4367
5075
|
inputSchema: z.object({
|
|
4368
5076
|
productId: productId.optional(),
|
|
4369
5077
|
category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
|
|
4370
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)"),
|
|
4371
5080
|
...pagination
|
|
4372
5081
|
}),
|
|
4373
5082
|
annotations: READ_ONLY
|
|
4374
5083
|
}, (params) => runTool(async () => {
|
|
4375
|
-
const { resolved, ...rest } = params;
|
|
4376
|
-
const
|
|
5084
|
+
const { resolved, full, ...rest } = params;
|
|
5085
|
+
const query = {
|
|
4377
5086
|
...rest,
|
|
4378
|
-
...resolved ? { resolved: true } : {}
|
|
4379
|
-
|
|
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;
|
|
4380
5101
|
});
|
|
4381
5102
|
const now = Date.now();
|
|
5103
|
+
const isDue = (row) => !(row["status"] === "snoozed" && typeof row["snoozedUntil"] === "string" && Date.parse(row["snoozedUntil"]) > now);
|
|
4382
5104
|
const rows = result.data;
|
|
4383
|
-
const due = rows.filter(
|
|
5105
|
+
const due = rows.filter(isDue);
|
|
4384
5106
|
const hidden = rows.length - due.length;
|
|
4385
5107
|
return toolResult({
|
|
4386
|
-
...
|
|
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),
|
|
4387
5113
|
...totalOf(result),
|
|
4388
5114
|
...hidden > 0 ? { snoozedHidden: hidden } : {}
|
|
4389
5115
|
});
|
|
4390
5116
|
}));
|
|
4391
5117
|
server.registerTool("taiga_list_setup_tasks", {
|
|
4392
5118
|
title: "List setup tasks",
|
|
4393
|
-
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.",
|
|
4394
5120
|
inputSchema: z.object({
|
|
4395
5121
|
productId: productId.optional(),
|
|
4396
5122
|
includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
|
|
@@ -4405,7 +5131,7 @@ function registerInboxTools(server, client) {
|
|
|
4405
5131
|
...params.cursor === void 0 ? { withTotal: true } : {}
|
|
4406
5132
|
});
|
|
4407
5133
|
return toolResult({
|
|
4408
|
-
...page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor),
|
|
5134
|
+
...page(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor),
|
|
4409
5135
|
...totalOf(result)
|
|
4410
5136
|
});
|
|
4411
5137
|
}));
|
|
@@ -4414,7 +5140,7 @@ function registerInboxTools(server, client) {
|
|
|
4414
5140
|
title: "Resolve or snooze an Action Required item",
|
|
4415
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.",
|
|
4416
5142
|
inputSchema: z.object({
|
|
4417
|
-
|
|
5143
|
+
actionId: idInput("Action Required item", "taiga_list_action_required gives it"),
|
|
4418
5144
|
status: z.enum([
|
|
4419
5145
|
"in_progress",
|
|
4420
5146
|
"done",
|
|
@@ -4425,7 +5151,7 @@ function registerInboxTools(server, client) {
|
|
|
4425
5151
|
annotations: OPERATE
|
|
4426
5152
|
}, (params) => runTool(async () => {
|
|
4427
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.");
|
|
4428
|
-
return toolResult(pickFields([(await client.updateInboxItem(params.
|
|
5154
|
+
return toolResult(pickFields([(await client.updateInboxItem(params.actionId, {
|
|
4429
5155
|
...params.status !== void 0 ? { status: params.status } : {},
|
|
4430
5156
|
...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
|
|
4431
5157
|
})).data], ITEM_FIELDS)[0]);
|
|
@@ -4433,6 +5159,14 @@ function registerInboxTools(server, client) {
|
|
|
4433
5159
|
}
|
|
4434
5160
|
//#endregion
|
|
4435
5161
|
//#region src/tools/knowledge.ts
|
|
5162
|
+
/**
|
|
5163
|
+
* Knowledge (docs.tai.ga, Context): reference material agents search when
|
|
5164
|
+
* the work calls for it, at the organization, a factory or a product, and
|
|
5165
|
+
* narrowing as it comes down. Not instructions: a rule belongs in
|
|
5166
|
+
* instructions, a forty-page integration guide here. Members manage it on
|
|
5167
|
+
* their factories and products; organization knowledge takes an owner or
|
|
5168
|
+
* admin, which the API enforces.
|
|
5169
|
+
*/
|
|
4436
5170
|
const KNOWLEDGE_FIELDS = [
|
|
4437
5171
|
"id",
|
|
4438
5172
|
"factoryId",
|
|
@@ -4607,213 +5341,14 @@ function registerKnowledgeTools(server, client) {
|
|
|
4607
5341
|
}));
|
|
4608
5342
|
}
|
|
4609
5343
|
//#endregion
|
|
4610
|
-
//#region src/tools/maintenance.ts
|
|
4611
|
-
/** A finding as a list row: enough to decide whether to open, fix or ignore it. */
|
|
4612
|
-
const FINDING_ROW_FIELDS = [
|
|
4613
|
-
"id",
|
|
4614
|
-
"kind",
|
|
4615
|
-
"severity",
|
|
4616
|
-
"tier",
|
|
4617
|
-
"status",
|
|
4618
|
-
"title",
|
|
4619
|
-
"ecosystem",
|
|
4620
|
-
"packageName",
|
|
4621
|
-
"currentVersion",
|
|
4622
|
-
"recommendedVersion",
|
|
4623
|
-
"advisoryId",
|
|
4624
|
-
"location",
|
|
4625
|
-
"sourcePaths",
|
|
4626
|
-
"productRepositoryId",
|
|
4627
|
-
"ownership",
|
|
4628
|
-
"remediableBy",
|
|
4629
|
-
"controls",
|
|
4630
|
-
"backlogItemId",
|
|
4631
|
-
"detectedAt",
|
|
4632
|
-
"ignoreReason",
|
|
4633
|
-
"ignoreExpiresAt"
|
|
4634
|
-
];
|
|
4635
|
-
/** A sweep as the record of when the product was last checked, and whether that check held. */
|
|
4636
|
-
const SWEEP_FIELDS = [
|
|
4637
|
-
"id",
|
|
4638
|
-
"trigger",
|
|
4639
|
-
"status",
|
|
4640
|
-
"scanners",
|
|
4641
|
-
"findingsCount",
|
|
4642
|
-
"newFindingsCount",
|
|
4643
|
-
"error",
|
|
4644
|
-
"createdAt",
|
|
4645
|
-
"completedAt"
|
|
4646
|
-
];
|
|
4647
|
-
/** A policy violation in a list: what it is, how bad, and what is being done about it. */
|
|
4648
|
-
const VIOLATION_FIELDS = [
|
|
4649
|
-
"id",
|
|
4650
|
-
"controlId",
|
|
4651
|
-
"policyType",
|
|
4652
|
-
"severity",
|
|
4653
|
-
"status",
|
|
4654
|
-
"title",
|
|
4655
|
-
"summary",
|
|
4656
|
-
"work",
|
|
4657
|
-
"unconfirmedSince",
|
|
4658
|
-
"verifyRequestedAt",
|
|
4659
|
-
"assessedAt"
|
|
4660
|
-
];
|
|
4661
|
-
const findingId = idInput("finding", "taiga_list_findings gives it");
|
|
4662
|
-
function registerMaintenanceTools(server, client) {
|
|
4663
|
-
if (!client.hasCapability("products:read")) return;
|
|
4664
|
-
server.registerTool("taiga_get_maintenance", {
|
|
4665
|
-
title: "Get a product's maintenance standing",
|
|
4666
|
-
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.",
|
|
4667
|
-
inputSchema: z.object({ productId }),
|
|
4668
|
-
annotations: READ_ONLY
|
|
4669
|
-
}, (params) => runTool(async () => {
|
|
4670
|
-
const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
|
|
4671
|
-
const latest = pickFields(sweeps.data, SWEEP_FIELDS);
|
|
4672
|
-
const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
|
|
4673
|
-
return toolResult({
|
|
4674
|
-
...standing,
|
|
4675
|
-
latestSweeps: latest,
|
|
4676
|
-
...latest.length === 0 ? { note: "This product has never been swept: no result is not an all-clear." } : {}
|
|
4677
|
-
});
|
|
4678
|
-
}));
|
|
4679
|
-
server.registerTool("taiga_list_findings", {
|
|
4680
|
-
title: "List maintenance findings",
|
|
4681
|
-
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.",
|
|
4682
|
-
inputSchema: z.object({
|
|
4683
|
-
productId,
|
|
4684
|
-
status: z.enum(MAINTENANCE_FINDING_STATUSES).optional().describe("Only findings in this status; omit for the ones needing action"),
|
|
4685
|
-
severity: z.enum(MAINTENANCE_FINDING_SEVERITIES).optional().describe("Only this severity"),
|
|
4686
|
-
kind: z.enum(MAINTENANCE_FINDING_KINDS).optional().describe("Only this kind of finding"),
|
|
4687
|
-
...pagination
|
|
4688
|
-
}),
|
|
4689
|
-
annotations: READ_ONLY
|
|
4690
|
-
}, (params) => runTool(async () => {
|
|
4691
|
-
const { productId: id, ...filters } = params;
|
|
4692
|
-
const result = await client.listFindings(id, filters);
|
|
4693
|
-
return toolResult({
|
|
4694
|
-
...page(pickFields(result.data, FINDING_ROW_FIELDS), result.nextCursor),
|
|
4695
|
-
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {}
|
|
4696
|
-
});
|
|
4697
|
-
}));
|
|
4698
|
-
server.registerTool("taiga_get_finding", {
|
|
4699
|
-
title: "Get a maintenance finding",
|
|
4700
|
-
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.",
|
|
4701
|
-
inputSchema: z.object({ findingId }),
|
|
4702
|
-
annotations: READ_ONLY
|
|
4703
|
-
}, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
|
|
4704
|
-
server.registerTool("taiga_get_policy_standing", {
|
|
4705
|
-
title: "Get a product's policy standing",
|
|
4706
|
-
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.",
|
|
4707
|
-
inputSchema: z.object({
|
|
4708
|
-
productId,
|
|
4709
|
-
full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
|
|
4710
|
-
}),
|
|
4711
|
-
annotations: READ_ONLY
|
|
4712
|
-
}, (params) => runTool(async () => {
|
|
4713
|
-
const standing = (await client.getPolicyStanding(params.productId)).data;
|
|
4714
|
-
const controls = Array.isArray(standing.controls) ? standing.controls : [];
|
|
4715
|
-
const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
|
|
4716
|
-
const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
|
|
4717
|
-
const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
|
|
4718
|
-
const bySeverity = {};
|
|
4719
|
-
for (const violation of violations) {
|
|
4720
|
-
const severity = String(violation.severity ?? "unknown");
|
|
4721
|
-
bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
|
|
4722
|
-
}
|
|
4723
|
-
const summary = {
|
|
4724
|
-
controls: listed.length,
|
|
4725
|
-
violations: violations.length,
|
|
4726
|
-
bySeverity,
|
|
4727
|
-
resolvedViolations: resolved
|
|
4728
|
-
};
|
|
4729
|
-
if (params.full) return toolResult({
|
|
4730
|
-
summary,
|
|
4731
|
-
...standing
|
|
4732
|
-
});
|
|
4733
|
-
return toolResult({
|
|
4734
|
-
summary,
|
|
4735
|
-
productId: standing.productId,
|
|
4736
|
-
hasPolicies: standing.hasPolicies,
|
|
4737
|
-
assessment: standing.assessment,
|
|
4738
|
-
documents: standing.documents,
|
|
4739
|
-
controls: pickFields(listed, [
|
|
4740
|
-
"controlId",
|
|
4741
|
-
"policyType",
|
|
4742
|
-
"text",
|
|
4743
|
-
"state",
|
|
4744
|
-
"divergenceCount",
|
|
4745
|
-
"findingCount",
|
|
4746
|
-
"findingsByTier",
|
|
4747
|
-
"plannedCount"
|
|
4748
|
-
]),
|
|
4749
|
-
violations: pickFields(violations, VIOLATION_FIELDS)
|
|
4750
|
-
});
|
|
4751
|
-
}));
|
|
4752
|
-
if (!client.hasCapability("products:write")) return;
|
|
4753
|
-
server.registerTool("taiga_ignore_finding", {
|
|
4754
|
-
title: "Ignore or reopen a finding",
|
|
4755
|
-
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.",
|
|
4756
|
-
inputSchema: z.object({
|
|
4757
|
-
findingId,
|
|
4758
|
-
ignored: z.boolean().describe("true ignores the finding, false reopens it"),
|
|
4759
|
-
reason: z.enum(MAINTENANCE_IGNORE_REASONS).optional().describe("Why it is ignored; required when ignoring"),
|
|
4760
|
-
note: z.string().trim().max(280).optional().describe("A short justification, kept for audit"),
|
|
4761
|
-
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)")
|
|
4762
|
-
}),
|
|
4763
|
-
annotations: OPERATE
|
|
4764
|
-
}, (params) => runTool(async () => {
|
|
4765
|
-
if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
|
|
4766
|
-
const body = params.ignored ? {
|
|
4767
|
-
status: "ignored",
|
|
4768
|
-
ignoreReason: params.reason,
|
|
4769
|
-
...params.note !== void 0 ? { ignoreNote: params.note } : {},
|
|
4770
|
-
...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
|
|
4771
|
-
} : { status: "open" };
|
|
4772
|
-
const finding = (await client.updateFinding(params.findingId, body)).data;
|
|
4773
|
-
return toolResult(pickFields([finding], [...FINDING_ROW_FIELDS, "ignoreNote"])[0]);
|
|
4774
|
-
}));
|
|
4775
|
-
server.registerTool("taiga_fix_finding", {
|
|
4776
|
-
title: "Fix a finding",
|
|
4777
|
-
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.",
|
|
4778
|
-
inputSchema: z.object({
|
|
4779
|
-
productId,
|
|
4780
|
-
findingId,
|
|
4781
|
-
wholeGroup: z.boolean().optional().describe("Fix every finding in this finding's group (its package and version, or its rule) together")
|
|
4782
|
-
}),
|
|
4783
|
-
annotations: OPERATE
|
|
4784
|
-
}, (params) => runTool(async () => {
|
|
4785
|
-
if (!params.wholeGroup) {
|
|
4786
|
-
const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
|
|
4787
|
-
return toolResult({
|
|
4788
|
-
backlogItemId: fixed.backlogItemId,
|
|
4789
|
-
finding: fixed.finding ? pickFields([fixed.finding], FINDING_ROW_FIELDS)[0] : null
|
|
4790
|
-
});
|
|
4791
|
-
}
|
|
4792
|
-
const finding = (await client.getFinding(params.findingId)).data;
|
|
4793
|
-
if (finding.productId !== params.productId) return toolError("NOT_FOUND", "No such finding in this product.");
|
|
4794
|
-
return toolResult((await client.remediatePackage(params.productId, {
|
|
4795
|
-
ecosystem: String(finding.ecosystem),
|
|
4796
|
-
packageName: String(finding.packageName),
|
|
4797
|
-
currentVersion: typeof finding.currentVersion === "string" ? finding.currentVersion : null,
|
|
4798
|
-
productRepositoryId: typeof finding.productRepositoryId === "string" ? finding.productRepositoryId : null
|
|
4799
|
-
})).data);
|
|
4800
|
-
}));
|
|
4801
|
-
server.registerTool("taiga_fix_policy_violation", {
|
|
4802
|
-
title: "Fix policy violations",
|
|
4803
|
-
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.",
|
|
4804
|
-
inputSchema: z.object({
|
|
4805
|
-
productId,
|
|
4806
|
-
controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
|
|
4807
|
-
violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
|
|
4808
|
-
}),
|
|
4809
|
-
annotations: OPERATE
|
|
4810
|
-
}, (params) => runTool(async () => {
|
|
4811
|
-
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.");
|
|
4812
|
-
return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
|
|
4813
|
-
}));
|
|
4814
|
-
}
|
|
4815
|
-
//#endregion
|
|
4816
5344
|
//#region src/tools/observe.ts
|
|
5345
|
+
/**
|
|
5346
|
+
* What runs where and how it is doing (docs.tai.ga, Deliver: Deployments;
|
|
5347
|
+
* Operate: Monitoring). Taiga observes deployments, it does not perform them:
|
|
5348
|
+
* the history is mirrored from the provider, so these tools read and never
|
|
5349
|
+
* deploy, approve or cancel. Importing history reaches into the connected
|
|
5350
|
+
* provider and is an administrator's action in the web.
|
|
5351
|
+
*/
|
|
4817
5352
|
/** A deployment as a line of history: what shipped where, how it ended, and who shipped it. */
|
|
4818
5353
|
const DEPLOYMENT_FIELDS = [
|
|
4819
5354
|
"id",
|
|
@@ -4842,6 +5377,20 @@ const ENVIRONMENT_FIELDS = [
|
|
|
4842
5377
|
"isProtected",
|
|
4843
5378
|
"url"
|
|
4844
5379
|
];
|
|
5380
|
+
/**
|
|
5381
|
+
* Deployments as lines, each naming the repository that deployed it: every
|
|
5382
|
+
* repository runs its own deployment in an environment, so two running in
|
|
5383
|
+
* production are two repositories, not a contradiction (PRO-2528).
|
|
5384
|
+
*/
|
|
5385
|
+
function deploymentLines(rows) {
|
|
5386
|
+
return rows.map((row) => {
|
|
5387
|
+
const repository = row.metadata?.["repositoryFullName"];
|
|
5388
|
+
return {
|
|
5389
|
+
...pickFields([row], DEPLOYMENT_FIELDS)[0],
|
|
5390
|
+
repository: typeof repository === "string" ? repository : null
|
|
5391
|
+
};
|
|
5392
|
+
});
|
|
5393
|
+
}
|
|
4845
5394
|
const environmentId = idInput("environment", "taiga_list_environments lists them; a product ID is not one");
|
|
4846
5395
|
/** The windows the pages offer, as the schema's own values rather than a number checked afterwards. */
|
|
4847
5396
|
const windowDays = z.literal([
|
|
@@ -4910,9 +5459,9 @@ function registerObserveTools(server, client) {
|
|
|
4910
5459
|
return toolResult({
|
|
4911
5460
|
environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
|
|
4912
5461
|
...environments.more ? { moreEnvironments: true } : {},
|
|
4913
|
-
running:
|
|
4914
|
-
deploying:
|
|
4915
|
-
history: page(
|
|
5462
|
+
running: deploymentLines(now.current ?? []),
|
|
5463
|
+
deploying: deploymentLines(now.deploying ?? []),
|
|
5464
|
+
history: page(deploymentLines(history.data), history.nextCursor),
|
|
4916
5465
|
..."value" in sync ? { syncStatus: sync.value.data } : {
|
|
4917
5466
|
syncStatus: null,
|
|
4918
5467
|
syncUnavailable: sync.unavailable
|
|
@@ -5031,7 +5580,7 @@ function registerProductDocumentWrites(server, client) {
|
|
|
5031
5580
|
const canRead = client.hasCapability("products:read");
|
|
5032
5581
|
const productScope = (product, type) => ({
|
|
5033
5582
|
getDraft: () => client.getProductDocumentDraft(product, type),
|
|
5034
|
-
getPublished:
|
|
5583
|
+
getPublished: () => client.getProductDocument(product, type, "plan"),
|
|
5035
5584
|
saveDraft: (data) => client.saveProductDocumentDraft(product, type, { data }),
|
|
5036
5585
|
publish: () => client.publishProductDocument(product, type)
|
|
5037
5586
|
});
|
|
@@ -5048,7 +5597,7 @@ function registerProductDocumentWrites(server, client) {
|
|
|
5048
5597
|
}, (params) => runTool(() => updateSection(productScope(params.productId, params.type), params.type, params.sectionId, params.content)));
|
|
5049
5598
|
server.registerTool("taiga_publish_product_document", {
|
|
5050
5599
|
title: "Publish a product document draft",
|
|
5051
|
-
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).",
|
|
5052
5601
|
inputSchema: z.object({
|
|
5053
5602
|
productId,
|
|
5054
5603
|
type: documentType
|
|
@@ -19540,7 +20089,7 @@ function renderDocument(doc) {
|
|
|
19540
20089
|
].filter((key) => doc[key] !== void 0 && doc[key] !== null).map((key) => `${key}: ${String(doc[key])}`);
|
|
19541
20090
|
const core = Object.keys(sections).filter((id) => {
|
|
19542
20091
|
const section = sections[id];
|
|
19543
|
-
return
|
|
20092
|
+
return isSectionObject(section) && section.priority === "core";
|
|
19544
20093
|
});
|
|
19545
20094
|
if (core.length > 0) facts.push(`core sections: ${core.join(", ")}`);
|
|
19546
20095
|
if (facts.length > 0) lines.push("", facts.join(" | "));
|
|
@@ -19636,7 +20185,7 @@ function atxOf(depth, children) {
|
|
|
19636
20185
|
}
|
|
19637
20186
|
/** The section's title when it carries one, else its id. */
|
|
19638
20187
|
function sectionTitle(section, id) {
|
|
19639
|
-
return
|
|
20188
|
+
return isSectionObject(section) && typeof section.title === "string" && section.title.trim() ? section.title.trim() : id;
|
|
19640
20189
|
}
|
|
19641
20190
|
/** The mime type a read will answer with, for a list entry that must not disagree with it. */
|
|
19642
20191
|
function mimeTypeOf(doc) {
|
|
@@ -19728,18 +20277,18 @@ function registerPrompts(server, client) {
|
|
|
19728
20277
|
"",
|
|
19729
20278
|
"Before writing code: follow the factory instructions; read the policy sections that touch what you are",
|
|
19730
20279
|
"about to change with taiga_get_document (security, data, SDLC, architecture at least); read the backlog",
|
|
19731
|
-
"item you are implementing with
|
|
20280
|
+
"item you are implementing with taiga_get_initiative. Parts listed under notGranted or unavailable",
|
|
19732
20281
|
"could not be read; say so rather than assuming they are empty.",
|
|
19733
20282
|
"",
|
|
19734
20283
|
fence("json", (await productContext(client, resolved.product, { documents: "full" })).content[0]?.text ?? "{}")
|
|
19735
20284
|
].join("\n"));
|
|
19736
20285
|
});
|
|
19737
|
-
if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("
|
|
19738
|
-
title: "Implement
|
|
19739
|
-
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.
|
|
19740
|
-
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") })
|
|
19741
20290
|
}, async (args) => {
|
|
19742
|
-
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;
|
|
19743
20292
|
const productId = String(item.productId);
|
|
19744
20293
|
const product = (await served$1(client.getProduct(productId), "The product of that item is not visible to the key")).data;
|
|
19745
20294
|
const origin = defaultOriginFor(product, "architecture");
|
|
@@ -19803,6 +20352,14 @@ var NotGranted = class extends Error {
|
|
|
19803
20352
|
};
|
|
19804
20353
|
//#endregion
|
|
19805
20354
|
//#region src/tools/repositories.ts
|
|
20355
|
+
/**
|
|
20356
|
+
* A product's repository (docs.tai.ga, Administration: Integrations and
|
|
20357
|
+
* environments, Source control). A linked repository is the prerequisite for
|
|
20358
|
+
* most of what Taiga does: planning reads the code, runs work on a branch of
|
|
20359
|
+
* it, sweeps scan it and the as-built documents are derived from it. GitHub is
|
|
20360
|
+
* connected once for the organization, in the web; linking one of the
|
|
20361
|
+
* repositories it can reach to a product is a separate act, per product.
|
|
20362
|
+
*/
|
|
19806
20363
|
const LINK_FIELDS = [
|
|
19807
20364
|
"id",
|
|
19808
20365
|
"provider",
|
|
@@ -19830,7 +20387,8 @@ const GITHUB_REPOSITORY_FIELDS = [
|
|
|
19830
20387
|
"archived",
|
|
19831
20388
|
"defaultBranch",
|
|
19832
20389
|
"description",
|
|
19833
|
-
"language"
|
|
20390
|
+
"language",
|
|
20391
|
+
"linked"
|
|
19834
20392
|
];
|
|
19835
20393
|
const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
|
|
19836
20394
|
/** Round-robin over lists: the first of each, then the second of each, and so on. */
|
|
@@ -19872,7 +20430,7 @@ function registerRepositoryTools(server, client) {
|
|
|
19872
20430
|
}
|
|
19873
20431
|
if (client.hasCapability("vcs:read")) server.registerTool("taiga_list_linkable_repositories", {
|
|
19874
20432
|
title: "Find a repository to link",
|
|
19875
|
-
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.",
|
|
19876
20434
|
inputSchema: z.object({
|
|
19877
20435
|
query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
|
|
19878
20436
|
account: z.string().trim().min(1).max(140).optional().describe("The GitHub account login (the owner in owner/repo), case-insensitive"),
|
|
@@ -20024,7 +20582,7 @@ function registerResources(server, client) {
|
|
|
20024
20582
|
title: "Organization document",
|
|
20025
20583
|
description: "A published organization document: a policy, the business goals, the design or brand standards, the tone of voice or the standing instructions. Sections rendered as headings."
|
|
20026
20584
|
}, async (uri, variables) => {
|
|
20027
|
-
const type = requireOneOf("Document type", String(variables
|
|
20585
|
+
const type = requireOneOf("Document type", String(variables["type"]), TENANT_DOCUMENT_TYPES);
|
|
20028
20586
|
try {
|
|
20029
20587
|
return contents(uri.href, (await client.getTenantDocument(type)).data);
|
|
20030
20588
|
} catch (error) {
|
|
@@ -20041,7 +20599,7 @@ function registerResources(server, client) {
|
|
|
20041
20599
|
title: "Factory instructions",
|
|
20042
20600
|
description: "A factory's published standing guidance; taiga_list_factories gives the factory ID. Markdown for a section map, JSON otherwise (the read says which)."
|
|
20043
20601
|
}, async (uri, variables) => {
|
|
20044
|
-
const factoryId = requireId("Factory ID", String(variables
|
|
20602
|
+
const factoryId = requireId("Factory ID", String(variables["factoryId"]));
|
|
20045
20603
|
const doc = await served(client.getFactoryDocument(factoryId, FACTORY_DOCUMENT_TYPES[0]), "This factory has not published instructions, or the key cannot see the factory");
|
|
20046
20604
|
return contents(uri.href, doc.data);
|
|
20047
20605
|
});
|
|
@@ -20061,28 +20619,28 @@ function registerResources(server, client) {
|
|
|
20061
20619
|
title: "Product document on a timeline",
|
|
20062
20620
|
description: "A product document from the named timeline: origin=plan (discovery) or origin=reality (derived from the code). Markdown for a section map, JSON otherwise (the read says which)."
|
|
20063
20621
|
}, async (uri, variables) => {
|
|
20064
|
-
const productId = requireId("Product ID", String(variables
|
|
20065
|
-
const type = documentType(String(variables
|
|
20066
|
-
const origin = requireOneOf("Origin", String(variables
|
|
20622
|
+
const productId = requireId("Product ID", String(variables["productId"]));
|
|
20623
|
+
const type = documentType(String(variables["type"]));
|
|
20624
|
+
const origin = requireOneOf("Origin", String(variables["origin"]), PRODUCT_DOCUMENT_ORIGINS);
|
|
20067
20625
|
return read(uri, productId, type, origin);
|
|
20068
20626
|
});
|
|
20069
20627
|
server.registerResource("product-document", new ResourceTemplate("taiga://products/{productId}/documents/{type}", { list: void 0 }), {
|
|
20070
20628
|
title: "Product document",
|
|
20071
20629
|
description: "A product document from the product's own timeline (reality for an imported product, plan otherwise; the documents a person writes are always on the plan side). taiga_list_product_documents shows what exists; add ?origin=plan or ?origin=reality to read the other side. Markdown for a section map, JSON otherwise (the read says which)."
|
|
20072
20630
|
}, async (uri, variables) => {
|
|
20073
|
-
const productId = requireId("Product ID", String(variables
|
|
20074
|
-
const type = documentType(String(variables
|
|
20631
|
+
const productId = requireId("Product ID", String(variables["productId"]));
|
|
20632
|
+
const type = documentType(String(variables["type"]));
|
|
20075
20633
|
const product = (await served(client.getProduct(productId), "No product with that ID the key can see; taiga_list_products shows them")).data;
|
|
20076
20634
|
return read(uri, productId, type, defaultOriginFor(product, type));
|
|
20077
20635
|
});
|
|
20078
20636
|
}
|
|
20079
|
-
if (client.hasCapability("backlog:read")) server.registerResource("backlog-item", new ResourceTemplate("taiga://
|
|
20080
|
-
title: "
|
|
20081
|
-
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.",
|
|
20082
20640
|
mimeType: "application/json"
|
|
20083
20641
|
}, async (uri, variables) => {
|
|
20084
|
-
const
|
|
20085
|
-
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;
|
|
20086
20644
|
return { contents: [{
|
|
20087
20645
|
uri: uri.href,
|
|
20088
20646
|
mimeType: "application/json",
|
|
@@ -20092,6 +20650,14 @@ function registerResources(server, client) {
|
|
|
20092
20650
|
}
|
|
20093
20651
|
//#endregion
|
|
20094
20652
|
//#region src/tools/setup.ts
|
|
20653
|
+
/**
|
|
20654
|
+
* Setting a product up (docs.tai.ga, Start: Your first product, Learn: Import
|
|
20655
|
+
* an existing codebase, Organizations, factories and products): factories to
|
|
20656
|
+
* put products in, a product started from scratch or from an imported
|
|
20657
|
+
* codebase, what Taiga builds for it, the import, and finishing or reopening
|
|
20658
|
+
* Discovery. Who may reach a factory (its members) and deleting a factory or
|
|
20659
|
+
* a product stay in the web.
|
|
20660
|
+
*/
|
|
20095
20661
|
const PRODUCT_FIELDS = [
|
|
20096
20662
|
"id",
|
|
20097
20663
|
"name",
|
|
@@ -20118,33 +20684,17 @@ const buildScope = {
|
|
|
20118
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");
|
|
20119
20685
|
/** How a missing required document comes to exist, said with a documents_incomplete refusal. */
|
|
20120
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.";
|
|
20121
|
-
/** 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. */
|
|
20122
20688
|
async function productRow(client, product) {
|
|
20123
20689
|
const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
|
|
20124
|
-
const
|
|
20690
|
+
const readiness = await setupReadiness(client, product);
|
|
20125
20691
|
return {
|
|
20126
20692
|
...rest,
|
|
20127
20693
|
projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
|
|
20128
|
-
...
|
|
20694
|
+
...readiness ? { readiness } : {}
|
|
20129
20695
|
};
|
|
20130
20696
|
}
|
|
20131
20697
|
/**
|
|
20132
|
-
* The product with its discoveryStep. The API derives it on reads only, not
|
|
20133
|
-
* on the answer to a write (products.ts), and a draft started from scratch
|
|
20134
|
-
* needs it to say what comes next; an imported one reads its import status
|
|
20135
|
-
* instead. Best effort: a failed read leaves the product without a nextStep
|
|
20136
|
-
* rather than failing the write that already happened.
|
|
20137
|
-
*/
|
|
20138
|
-
async function withDiscoveryStep(client, product) {
|
|
20139
|
-
if (product.status !== "draft" || "discoveryStep" in product || isImportedProduct(product)) return product;
|
|
20140
|
-
try {
|
|
20141
|
-
return (await client.getProduct(String(product.id))).data;
|
|
20142
|
-
} catch (error) {
|
|
20143
|
-
if (error instanceof TaigaApiError || isNetworkFailure(error)) return product;
|
|
20144
|
-
throw error;
|
|
20145
|
-
}
|
|
20146
|
-
}
|
|
20147
|
-
/**
|
|
20148
20698
|
* A half turned off with nothing said about how it is done today. Allowed (the
|
|
20149
20699
|
* web asks for the sentence but does not insist), so a warning rather than a
|
|
20150
20700
|
* refusal: plans then design against a gap.
|
|
@@ -20189,7 +20739,7 @@ function registerSetupTools(server, client) {
|
|
|
20189
20739
|
}, (params) => runTool(async () => toolResult((await client.previewProductMove(params.productId, params.factoryId)).data)));
|
|
20190
20740
|
server.registerTool("taiga_move_product", {
|
|
20191
20741
|
title: "Move a product to another factory",
|
|
20192
|
-
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.",
|
|
20193
20743
|
inputSchema: z.object({
|
|
20194
20744
|
productId,
|
|
20195
20745
|
factoryId: factoryId.describe("The factory to move it to"),
|
|
@@ -20197,10 +20747,16 @@ function registerSetupTools(server, client) {
|
|
|
20197
20747
|
}),
|
|
20198
20748
|
annotations: OPERATE
|
|
20199
20749
|
}, (params) => runTool(async () => {
|
|
20200
|
-
|
|
20201
|
-
|
|
20202
|
-
|
|
20203
|
-
|
|
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
|
+
}
|
|
20204
20760
|
return toolResult({
|
|
20205
20761
|
product: await productRow(client, moved.data),
|
|
20206
20762
|
changed: moved.changed,
|
|
@@ -20276,7 +20832,7 @@ function registerSetupTools(server, client) {
|
|
|
20276
20832
|
try {
|
|
20277
20833
|
return toolResult(await productRow(client, (await client.completeSetup(params.productId)).data));
|
|
20278
20834
|
} catch (error) {
|
|
20279
|
-
if (!(error instanceof TaigaApiError) || error.details?.reason !== "documents_incomplete") throw error;
|
|
20835
|
+
if (!(error instanceof TaigaApiError) || error.details?.["reason"] !== "documents_incomplete") throw error;
|
|
20280
20836
|
return toolError(error.code ?? "CONFLICT", `${withStop(error.apiMessage ?? "Required documents are not published yet")} ${MISSING_DOCUMENTS_HOW}`, error.details);
|
|
20281
20837
|
}
|
|
20282
20838
|
}));
|
|
@@ -20494,7 +21050,9 @@ function judgeItem(until, gate) {
|
|
|
20494
21050
|
/** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
|
|
20495
21051
|
function itemReport(item) {
|
|
20496
21052
|
const run = latestBuildOf(item);
|
|
21053
|
+
const situation = situationOf(item);
|
|
20497
21054
|
return {
|
|
21055
|
+
...situation ? { situation } : {},
|
|
20498
21056
|
...pickFields([item], [
|
|
20499
21057
|
"id",
|
|
20500
21058
|
"title",
|
|
@@ -20541,6 +21099,8 @@ const RUN_REPORT_FIELDS = [
|
|
|
20541
21099
|
"prState",
|
|
20542
21100
|
"prDraft",
|
|
20543
21101
|
"prMerged",
|
|
21102
|
+
"prNumber",
|
|
21103
|
+
"baseBranch",
|
|
20544
21104
|
"actions"
|
|
20545
21105
|
];
|
|
20546
21106
|
const runSignature = (run) => JSON.stringify([
|
|
@@ -20561,11 +21121,11 @@ function judgeBuild(until) {
|
|
|
20561
21121
|
};
|
|
20562
21122
|
}
|
|
20563
21123
|
function registerWaitTools(server, client, options) {
|
|
20564
|
-
if (client.hasCapability("backlog:read")) server.registerTool("
|
|
21124
|
+
if (client.hasCapability("backlog:read")) server.registerTool("taiga_wait_for_initiative", {
|
|
20565
21125
|
title: "Wait for an initiative",
|
|
20566
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.",
|
|
20567
21127
|
inputSchema: z.object({
|
|
20568
|
-
|
|
21128
|
+
initiativeId: idInput("initiative", "taiga_list_initiatives (query finds one by title) gives it"),
|
|
20569
21129
|
until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
|
|
20570
21130
|
timeoutSeconds
|
|
20571
21131
|
}),
|
|
@@ -20573,7 +21133,7 @@ function registerWaitTools(server, client, options) {
|
|
|
20573
21133
|
}, (params, ctx) => runTool(async () => {
|
|
20574
21134
|
const gate = { product: void 0 };
|
|
20575
21135
|
const read = async (signal) => {
|
|
20576
|
-
const item = (await client.getBacklogItem(params.
|
|
21136
|
+
const item = (await client.getBacklogItem(params.initiativeId, signal)).data;
|
|
20577
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) => {
|
|
20578
21138
|
const value = p.data.autoBuild;
|
|
20579
21139
|
return typeof value === "boolean" ? value : true;
|
|
@@ -20594,17 +21154,17 @@ function registerWaitTools(server, client, options) {
|
|
|
20594
21154
|
item: now ? itemReport(now) : null
|
|
20595
21155
|
});
|
|
20596
21156
|
}));
|
|
20597
|
-
if (client.hasCapability("products:read")) server.registerTool("
|
|
21157
|
+
if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_run", {
|
|
20598
21158
|
title: "Wait for a run",
|
|
20599
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.",
|
|
20600
21160
|
inputSchema: z.object({
|
|
20601
|
-
|
|
21161
|
+
runId: idInput("run", "taiga_list_runs gives it"),
|
|
20602
21162
|
until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
|
|
20603
21163
|
timeoutSeconds
|
|
20604
21164
|
}),
|
|
20605
21165
|
annotations: READ_ONLY
|
|
20606
21166
|
}, (params, ctx) => runTool(async () => {
|
|
20607
|
-
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, {
|
|
20608
21168
|
progressToken: ctx.mcpReq._meta?.progressToken,
|
|
20609
21169
|
notify: ctx.mcpReq.notify,
|
|
20610
21170
|
signal: ctx.mcpReq.signal
|
|
@@ -20612,7 +21172,7 @@ function registerWaitTools(server, client, options) {
|
|
|
20612
21172
|
return toolResult({
|
|
20613
21173
|
until: params.until,
|
|
20614
21174
|
...outcome,
|
|
20615
|
-
run: now ? pickFields([now], RUN_REPORT_FIELDS)[0] : null
|
|
21175
|
+
run: now ? readableRun(pickFields([now], RUN_REPORT_FIELDS)[0]) : null
|
|
20616
21176
|
});
|
|
20617
21177
|
}));
|
|
20618
21178
|
if (client.hasCapability("products:read")) server.registerTool("taiga_wait_for_job", {
|
|
@@ -20655,7 +21215,8 @@ function withStrictInputs(server) {
|
|
|
20655
21215
|
const withHint = (args, ctx) => notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
|
|
20656
21216
|
const described = {
|
|
20657
21217
|
...config,
|
|
20658
|
-
description: `${writeNotice(name, config.annotations)}${config.description ?? ""}
|
|
21218
|
+
description: `${writeNotice(name, config.annotations)}${config.description ?? ""}`,
|
|
21219
|
+
...ALWAYS_LOAD.has(name) ? { _meta: { "anthropic/alwaysLoad": true } } : {}
|
|
20659
21220
|
};
|
|
20660
21221
|
return register(name, schema ? {
|
|
20661
21222
|
...described,
|
|
@@ -20665,6 +21226,19 @@ function withStrictInputs(server) {
|
|
|
20665
21226
|
return server;
|
|
20666
21227
|
}
|
|
20667
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
|
+
/**
|
|
20668
21242
|
* What a tool's annotations say, in its description's first words (PRO-2778).
|
|
20669
21243
|
* Annotations are for the client: Claude Code does not show them to the
|
|
20670
21244
|
* model, so an agent read 79 tools without knowing which ones write. A tool
|
|
@@ -20701,136 +21275,40 @@ function registerAllTools(server, client, options = {}) {
|
|
|
20701
21275
|
registerResources(server, client);
|
|
20702
21276
|
registerPrompts(server, client);
|
|
20703
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
|
+
*/
|
|
20704
21291
|
const SERVER_INSTRUCTIONS = [
|
|
20705
|
-
|
|
20706
|
-
|
|
20707
|
-
|
|
20708
|
-
|
|
20709
|
-
|
|
20710
|
-
"checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with",
|
|
20711
|
-
"query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory",
|
|
20712
|
-
"instructions, the outline of its specification and architecture, every policy as its section ids, the",
|
|
20713
|
-
"open initiatives, the repositories and the latest runs. Then read only what you need: sections by id with",
|
|
20714
|
-
"taiga_get_document or taiga_get_product_document (sections: [] is any document's outline), an",
|
|
20715
|
-
"initiative with taiga_get_backlog_item. Check Action Required (taiga_list_action_required) for what",
|
|
20716
|
-
"waits on a person.",
|
|
20717
|
-
"",
|
|
20718
|
-
"**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that",
|
|
20719
|
-
"starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it",
|
|
20720
|
-
"(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). Such a",
|
|
20721
|
-
"tool says so first in its description; a client that prompts on destructive tools does part of it.",
|
|
20722
|
-
"",
|
|
20723
|
-
"**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the",
|
|
20724
|
-
"right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.",
|
|
20725
|
-
"",
|
|
20726
|
-
"**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action",
|
|
20727
|
-
"Required is the inbox; the line is the queue; a factory is the group that owns a set of products."
|
|
20728
|
-
].join("\n"),
|
|
20729
|
-
"",
|
|
20730
|
-
"**Company documents** (taiga_list_documents / taiga_get_document):",
|
|
20731
|
-
`The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
|
|
20732
|
-
`architecture rules that apply to all code. The other documents (${named(OTHER_TENANT_DOCUMENT_TYPES)})`,
|
|
20733
|
-
"carry the business goals, design and brand standards, tone of voice and standing instructions.",
|
|
20734
|
-
"Read the relevant policy BEFORE writing code that touches security, data handling or infrastructure.",
|
|
20735
|
-
"",
|
|
20736
|
-
"**Factory instructions** (taiga_list_factory_documents / taiga_get_factory_document):",
|
|
20737
|
-
"A factory's standing guidance: domain context (ubiquitous language, entities, business rules),",
|
|
20738
|
-
"tech preferences (approved stack, patterns) and conventions. Read these to understand HOW this factory",
|
|
20739
|
-
"builds software. A factory that has not written any yet has none; that is normal.",
|
|
20740
|
-
"",
|
|
20741
|
-
"**Product documents** (taiga_list_product_documents / taiga_get_product_document):",
|
|
20742
|
-
`${Object.entries(PRODUCT_DOCUMENT_NAMES).map(([type, name]) => `${name} (${type})`).join(", ")}.`,
|
|
20743
|
-
"A product has two timelines of these, and the list shows both:",
|
|
20744
|
-
`- ${ORIGIN_MEANING.plan}`,
|
|
20745
|
-
`- ${ORIGIN_MEANING.reality}`,
|
|
20746
|
-
"An imported (brownfield) product has no generated discovery chain: its derived documents are on the",
|
|
20747
|
-
"reality side, while the documents a person writes (Look & Feel, Product Instructions) are on the plan",
|
|
20748
|
-
"side of every product. Omit origin on a read and the server picks the right side (the list reports the",
|
|
20749
|
-
"product's own as defaultOrigin). To understand",
|
|
20750
|
-
"what to build, read the plan where there is one; to understand the code you are changing, read reality",
|
|
20751
|
-
"when it exists, the ground truth for architecture, data flow and threats as built.",
|
|
20752
|
-
"",
|
|
20753
|
-
"**Backlog and runs** (taiga_list_backlog_items / taiga_get_backlog_item / taiga_list_builds / taiga_get_build):",
|
|
20754
|
-
"The product's initiatives with their acceptance criteria, and what Taiga's coding agent already built",
|
|
20755
|
-
"for them: runs, steps, commits and pull requests. taiga_list_builds without a product lists the runs of",
|
|
20756
|
-
"every product the key can see, and active: true the ones executing now.",
|
|
20757
|
-
"",
|
|
20758
|
-
"**Operating the line** works the way Taiga is meant to be used. New work is described, not written:",
|
|
20759
|
-
"taiga_request_initiatives asks an agent, which reads the product and its repository, expands the request",
|
|
20760
|
-
"into initiatives in the Backlog (or says why it added none: already covered, not work, out of scope, or a",
|
|
20761
|
-
"policy that does not allow it) -- follow it with taiga_wait_for_job. The queue is the only way to start",
|
|
20762
|
-
"work (taiga_queue_backlog_item): Taiga plans and builds one initiative at a time, in queue order, and starts",
|
|
20763
|
-
"the next when the pull request merges. Whether a landed plan waits for a review is the review gate",
|
|
20764
|
-
"(taiga_set_auto_build, or autoBuild on the queue); read it with taiga_get_plan and answer with",
|
|
20765
|
-
"taiga_approve_plan or taiga_reject_plan. Decisions about the work are made on the initiative: stop work by",
|
|
20766
|
-
"moving it out of Build (taiga_move_backlog_item), carry it on with taiga_resume_backlog_item, ask for a new",
|
|
20767
|
-
"plan with taiga_plan_again; a run is the record of an attempt. taiga_pause_line pauses the whole line.",
|
|
20768
|
-
"taiga_stop_build is the emergency brake for a run whose initiative cannot be moved. Queueing, approving,",
|
|
20769
|
-
"resuming and requesting start paid agent work; stopping and pausing interrupt it. Tell the person what you",
|
|
20770
|
-
"are about to start or stop and wait for their yes; a client that prompts on destructive tools does that",
|
|
20771
|
-
"for you. A refusal (CONFLICT) carries the API reason: say it as it is.",
|
|
20772
|
-
"",
|
|
20773
|
-
"**Watching the line** (taiga_get_queue / taiga_wait_for_backlog_item / taiga_wait_for_build /",
|
|
20774
|
-
"taiga_get_job / taiga_wait_for_job): the queue read says what the line is waiting on (a paused line, a plan",
|
|
20775
|
-
"to review, a pull request to merge, a halt and what lifts it). Planning takes minutes and a build up to an",
|
|
20776
|
-
"hour: wait with the wait tools rather than reading the same item again and again; each returns when the",
|
|
20777
|
-
"point is reached, when only a person can move it on, or at its timeout (call again to keep waiting).",
|
|
20778
|
-
"",
|
|
20779
|
-
"**Action Required** (taiga_list_action_required / taiga_list_setup_tasks / taiga_resolve_action): where work",
|
|
20780
|
-
"comes back to a person, shared by the team. Check it first. Resolving an item clears it for everybody; a",
|
|
20781
|
-
"setup task is marked done only by the person who did the setup.",
|
|
20782
|
-
"",
|
|
20783
|
-
"**Maintaining** (taiga_get_maintenance / taiga_list_findings / taiga_get_finding / taiga_ignore_finding /",
|
|
20784
|
-
"taiga_fix_finding / taiga_get_policy_standing / taiga_fix_policy_violation): what the sweeps found and where",
|
|
20785
|
-
"the product stands against its own policies. Sweeps run on their own; check when the last one ran before",
|
|
20786
|
-
"trusting a quiet list. A finding is ignored with a reason or fixed; a fix raises an initiative at the end of",
|
|
20787
|
-
"the queue. Nothing is marked fixed by hand: the next sweep or assessment decides.",
|
|
20788
|
-
"",
|
|
20789
|
-
"**Deployments and monitoring** (taiga_get_deployments / taiga_get_delivery_metrics / taiga_get_monitoring /",
|
|
20790
|
-
"taiga_define_environment / taiga_update_environment): what runs where, how delivery is going, and how each",
|
|
20791
|
-
"environment is doing. Taiga observes deployments and never performs them. An environment needs only its App",
|
|
20792
|
-
"URL for monitoring to start.",
|
|
20793
|
-
"",
|
|
20794
|
-
"**Knowledge** (taiga_list_knowledge / taiga_get_knowledge / taiga_add_knowledge / taiga_retry_knowledge /",
|
|
20795
|
-
"taiga_delete_knowledge): reference material agents search when work calls for it, at the organization, a",
|
|
20796
|
-
"factory or a product. A rule that applies every time belongs in instructions instead.",
|
|
20797
|
-
"",
|
|
20798
|
-
"**Setting a product up** (taiga_create_factory / taiga_create_product / taiga_update_product /",
|
|
20799
|
-
"taiga_list_linkable_repositories / taiga_link_repository / taiga_start_import / taiga_finish_discovery /",
|
|
20800
|
-
"taiga_reopen_discovery / taiga_move_product): a product starts from scratch (Discovery is a conversation,",
|
|
20801
|
-
"held in the web) or by importing a repository, chosen when it is created. An import links the repository,",
|
|
20802
|
-
"starts the import and runs by itself; finishing Discovery locks the documents and opens the rest of the",
|
|
20803
|
-
"product. Linking a repository and creating or moving factories are for owners and admins. GitHub is",
|
|
20804
|
-
"connected once, in the web; members and deleting stay there too.",
|
|
20805
|
-
"",
|
|
20806
|
-
"**Product documents and autonomy** (taiga_update_product_document / taiga_publish_product_document /",
|
|
20807
|
-
"taiga_regenerate_product_document / taiga_generate_product_documents / taiga_stop_generating_documents /",
|
|
20808
|
-
"taiga_mark_product_document_reviewed /",
|
|
20809
|
-
"taiga_set_product_autonomy / taiga_set_auto_merge): edit a product document in its draft and publish it;",
|
|
20810
|
-
"documents built from it are marked outdated and regenerated, not rewritten. Set whether plans wait for a",
|
|
20811
|
-
"review and pull requests for a person to merge. Organization-wide settings, members, roles and connections",
|
|
20812
|
-
"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.",
|
|
20813
21297
|
"",
|
|
20814
|
-
"**
|
|
20815
|
-
"
|
|
20816
|
-
"
|
|
20817
|
-
"
|
|
20818
|
-
"text; a hand edit keeps the source it had) and",
|
|
20819
|
-
"`priority: core` marks the sections every organization keeps. Neither changes what the section",
|
|
20820
|
-
"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.",
|
|
20821
21302
|
"",
|
|
20822
|
-
"
|
|
20823
|
-
"
|
|
20824
|
-
"
|
|
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.",
|
|
20825
21306
|
"",
|
|
20826
|
-
"
|
|
20827
|
-
"
|
|
20828
|
-
"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.",
|
|
20829
21309
|
"",
|
|
20830
|
-
"
|
|
20831
|
-
"
|
|
20832
|
-
"publish it with taiga_publish_document or taiga_publish_factory_document only when the person asks.",
|
|
20833
|
-
"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."
|
|
20834
21312
|
].join("\n");
|
|
20835
21313
|
//#endregion
|
|
20836
21314
|
//#region src/server.ts
|