@taiga-ai/mcp-server 0.26.0-rc.480 → 0.26.0-rc.494
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 +438 -205
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -124,6 +124,14 @@ function parseErrorBody(body) {
|
|
|
124
124
|
*/
|
|
125
125
|
var TaigaClient = class {
|
|
126
126
|
baseUrl;
|
|
127
|
+
/** The API this client talks to, as its host: the environment a reader is in (PRO-2823). */
|
|
128
|
+
get apiHost() {
|
|
129
|
+
try {
|
|
130
|
+
return new URL(this.baseUrl).host;
|
|
131
|
+
} catch {
|
|
132
|
+
return this.baseUrl;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
127
135
|
apiKey;
|
|
128
136
|
introspection = null;
|
|
129
137
|
constructor(opts) {
|
|
@@ -269,6 +277,9 @@ var TaigaClient = class {
|
|
|
269
277
|
if (opts?.status) params.set("status", opts.status);
|
|
270
278
|
if (opts?.planningStatus) params.set("planningStatus", opts.planningStatus);
|
|
271
279
|
if (opts?.category) params.set("category", opts.category);
|
|
280
|
+
if (opts?.state) params.set("state", opts.state);
|
|
281
|
+
if (opts?.open) params.set("open", "true");
|
|
282
|
+
if (opts?.order) params.set("order", opts.order);
|
|
272
283
|
if (opts?.includeArchived) params.set("includeArchived", "true");
|
|
273
284
|
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
274
285
|
if (opts?.cursor) params.set("cursor", opts.cursor);
|
|
@@ -870,9 +881,6 @@ const PRODUCT_DOCUMENT_DISPLAY_NAMES = {
|
|
|
870
881
|
const PRODUCT_DOCUMENT_ORIGINS = ["plan", "reality"];
|
|
871
882
|
//#endregion
|
|
872
883
|
//#region ../../packages/types/dist/backlog.js
|
|
873
|
-
/** Dispositions that end an item's pipeline life: settled work neither blocks
|
|
874
|
-
* dependants nor counts toward completion denominators. */
|
|
875
|
-
const SETTLED_BACKLOG_STATUSES = ["done", "canceled"];
|
|
876
884
|
/**
|
|
877
885
|
* The initiative's STORED state on the line board (PRO-1305, phase 1).
|
|
878
886
|
*
|
|
@@ -913,19 +921,15 @@ const INITIATIVE_STATES = [
|
|
|
913
921
|
* for every reader that means "on the line": the flag the API clears when an
|
|
914
922
|
* initiative leaves it, the remediation settle on the same departure, and the
|
|
915
923
|
* MCP's account of where an initiative stands (PRO-2840). Everything else
|
|
916
|
-
* (backlog, todo, done, canceled) is off it. The lifecycle derivation
|
|
917
|
-
*
|
|
924
|
+
* (backlog, todo, done, canceled) is off it. The lifecycle derivation draws a
|
|
925
|
+
* narrower line (PRO-2828): a run that ended is the initiative's state only at
|
|
926
|
+
* a station, Plan or Build; in Queue it is history, as it is off the line.
|
|
918
927
|
*/
|
|
919
928
|
const LINE_STATES = [
|
|
920
929
|
"queue",
|
|
921
930
|
"plan",
|
|
922
931
|
"build"
|
|
923
932
|
];
|
|
924
|
-
/** Finished work: settled by a person or a merge, or archived. The one
|
|
925
|
-
* predicate every "can this still move / plan / build" guard reads. */
|
|
926
|
-
function isSettledOrArchived(item) {
|
|
927
|
-
return item.archivedAt != null || SETTLED_BACKLOG_STATUSES.includes(item.status);
|
|
928
|
-
}
|
|
929
933
|
/**
|
|
930
934
|
* The columns a person can drop an initiative into by hand.
|
|
931
935
|
*
|
|
@@ -1348,7 +1352,8 @@ const READY_BLOCKERS = [
|
|
|
1348
1352
|
* - `ci_fix_stalled`: a fix round on the failing head pushed nothing;
|
|
1349
1353
|
* - `conflict_cap`: the branch still conflicts with its base at the cap;
|
|
1350
1354
|
* - `ready_blocked`: CI is green, but the description says why it is a draft;
|
|
1351
|
-
* - `check_weakened`: CI went green only after a fix
|
|
1355
|
+
* - `check_weakened`: CI went green only after a fix changed a CI workflow or
|
|
1356
|
+
* deleted a test, which may have weakened a check;
|
|
1352
1357
|
* - `review_bot_cap`: a review BOT (Copilot, CodeRabbit) kept reviewing after
|
|
1353
1358
|
* the automatic rounds ran out (PRO-2480). A bot's review never resets the
|
|
1354
1359
|
* streak a person's does, so the agent and the bot cannot loop without end.
|
|
@@ -1384,6 +1389,49 @@ z.object({
|
|
|
1384
1389
|
...e.failingChecks === void 0 ? {} : { failingChecks: e.failingChecks },
|
|
1385
1390
|
...e.blockers === void 0 ? {} : { blockers: e.blockers }
|
|
1386
1391
|
}));
|
|
1392
|
+
/**
|
|
1393
|
+
* What a run's hardening came to, coverage included (PRO-2829). `passed`
|
|
1394
|
+
* counted only blocking findings, so a run whose scanners never ran said
|
|
1395
|
+
* "passed" all the same.
|
|
1396
|
+
*
|
|
1397
|
+
* - `failed`: blocking findings remain (the gaps are still listed).
|
|
1398
|
+
* - `not_scanned`: no scanner that applies to the repository ran to the end.
|
|
1399
|
+
* - `passed_with_gaps`: nothing blocks, but some scanner did not run.
|
|
1400
|
+
* - `passed`: nothing blocks and every scanner that applies ran.
|
|
1401
|
+
*/
|
|
1402
|
+
const HARDENING_VERDICTS = [
|
|
1403
|
+
"passed",
|
|
1404
|
+
"passed_with_gaps",
|
|
1405
|
+
"not_scanned",
|
|
1406
|
+
"failed"
|
|
1407
|
+
];
|
|
1408
|
+
/**
|
|
1409
|
+
* The verdict and the gaps of a hardening result (PRO-2829), for every reader
|
|
1410
|
+
* (the run read, the web's run page, the MCP), from what the agent reported.
|
|
1411
|
+
*
|
|
1412
|
+
* A gap is a scanner that applies and did not run: `not_run` (the scan never
|
|
1413
|
+
* reached it), `skipped` (its tool was missing), or `failed` with a reason,
|
|
1414
|
+
* which is a scanner that could not finish. A `failed` scanner without one
|
|
1415
|
+
* found something (tflint fails on a MAJOR that blocks nothing, and rows from
|
|
1416
|
+
* before `scannerReasons` used `failed` for "had findings"), so it ran and is
|
|
1417
|
+
* no gap. A scanner listed in `notApplicable` is no gap; on a result from
|
|
1418
|
+
* before that list, a skipped tflint is read the same way, as the only reason
|
|
1419
|
+
* the agent ever skipped it was a repository with no Terraform.
|
|
1420
|
+
*/
|
|
1421
|
+
function hardeningCoverage(result) {
|
|
1422
|
+
const notApplicable = new Set(result.notApplicable ?? []);
|
|
1423
|
+
const applies = Object.entries(result.scanners ?? {}).filter(([scanner, status]) => !notApplicable.has(scanner) && !(result.notApplicable === void 0 && scanner === "tflint" && status === "skipped"));
|
|
1424
|
+
const reasons = result.scannerReasons ?? {};
|
|
1425
|
+
const gaps = applies.filter(([scanner, status]) => status === "not_run" || status === "skipped" || status === "failed" && scanner in reasons).map(([scanner, status]) => ({
|
|
1426
|
+
scanner,
|
|
1427
|
+
status,
|
|
1428
|
+
reason: reasons[scanner] ?? null
|
|
1429
|
+
}));
|
|
1430
|
+
return {
|
|
1431
|
+
verdict: !result.passed ? "failed" : gaps.length === applies.length ? "not_scanned" : gaps.length > 0 ? "passed_with_gaps" : "passed",
|
|
1432
|
+
gaps
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1387
1435
|
[...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
|
|
1388
1436
|
//#endregion
|
|
1389
1437
|
//#region ../../packages/types/dist/agents.js
|
|
@@ -1763,6 +1811,92 @@ const MAINTENANCE_IGNORE_REASONS = [
|
|
|
1763
1811
|
"accepted_risk",
|
|
1764
1812
|
"wont_fix"
|
|
1765
1813
|
];
|
|
1814
|
+
/**
|
|
1815
|
+
* What one lockfile's group list says, read against what its osv-scanner
|
|
1816
|
+
* (osv-scalibr) extractor actually writes. Checked per extractor, because the
|
|
1817
|
+
* same list means different things in different files:
|
|
1818
|
+
*
|
|
1819
|
+
* - package-lock.json / npm-shrinkwrap.json: every package entry carries npm's
|
|
1820
|
+
* `dev` / `optional` / `devOptional` flags, transitive ones included, so an
|
|
1821
|
+
* EMPTY list is a runtime dependency, `dev` alone is dev-only, `optional`
|
|
1822
|
+
* alone is shipped, and `dev` + `optional` (devOptional) is left undecided.
|
|
1823
|
+
* - pnpm-lock.yaml: `dev` comes from a per-package `dev: true` that lockfile
|
|
1824
|
+
* v9 no longer writes, so an empty list says nothing; `dev` still means dev.
|
|
1825
|
+
* For a package a workspace declares under `dependencies` or
|
|
1826
|
+
* `optionalDependencies`, the sweep adds Taiga's own `prod`, so that copy
|
|
1827
|
+
* reads runtime. A `devDependencies` declaration is not turned into `dev`:
|
|
1828
|
+
* a runtime dependency's snapshot may pull the same copy in, which only the
|
|
1829
|
+
* `snapshots` graph could rule out, so it stays unknown.
|
|
1830
|
+
* - poetry.lock: group names with `main` dropped; a non-main, non-optional
|
|
1831
|
+
* group is not in the runtime install. Poetry 1.x lockfiles carry no groups
|
|
1832
|
+
* at all, so an empty list says nothing.
|
|
1833
|
+
* - uv.lock: only direct members of a group are tagged, so an empty list says
|
|
1834
|
+
* nothing; `dev` means dev.
|
|
1835
|
+
* - yarn.lock, go.mod/go.sum, Cargo.lock, Gemfile.lock and the rest carry no
|
|
1836
|
+
* group information: unknown whatever the list says.
|
|
1837
|
+
*
|
|
1838
|
+
* Anything this cannot place is `unknown`, never `runtime`: a wrong "runtime"
|
|
1839
|
+
* on a build tool is the claim this exists to stop, and a wrong "dev only" on a
|
|
1840
|
+
* shipped package is worse.
|
|
1841
|
+
*/
|
|
1842
|
+
function lockfileScope(path, groups) {
|
|
1843
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
1844
|
+
const set = new Set(groups.map((g) => g.trim().toLowerCase()).filter((g) => g !== ""));
|
|
1845
|
+
switch (name) {
|
|
1846
|
+
case "package-lock.json":
|
|
1847
|
+
case "npm-shrinkwrap.json": {
|
|
1848
|
+
if (set.size === 0) return "runtime";
|
|
1849
|
+
const dev = set.has("dev");
|
|
1850
|
+
const optional = set.has("optional");
|
|
1851
|
+
if (dev && optional) return "unknown";
|
|
1852
|
+
if (dev) return "dev";
|
|
1853
|
+
if (optional && set.size === 1) return "runtime";
|
|
1854
|
+
return "unknown";
|
|
1855
|
+
}
|
|
1856
|
+
case "pnpm-lock.yaml":
|
|
1857
|
+
if (set.has("prod")) return "runtime";
|
|
1858
|
+
return set.size === 1 && set.has("dev") ? "dev" : "unknown";
|
|
1859
|
+
case "uv.lock": return set.size === 1 && set.has("dev") ? "dev" : "unknown";
|
|
1860
|
+
case "poetry.lock": return set.size > 0 && !set.has("optional") && !set.has("main") ? "dev" : "unknown";
|
|
1861
|
+
default: return "unknown";
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Whether a dependency finding sits only in development dependencies, in at
|
|
1866
|
+
* least one runtime dependency set, or in lockfiles that do not say. Decided
|
|
1867
|
+
* per lockfile (`lockfileScope`) and combined pessimistically: one runtime
|
|
1868
|
+
* lockfile makes the finding runtime, one lockfile that cannot say makes it
|
|
1869
|
+
* unknown, and only all-dev reads as dev. Groups the sweep did not record
|
|
1870
|
+
* (older images, code kinds) are unknown. Presentation only: the tier and the
|
|
1871
|
+
* auto-remediation gate do not read this, and a malicious package runs its
|
|
1872
|
+
* install script in any group.
|
|
1873
|
+
*/
|
|
1874
|
+
function dependencyScope(finding) {
|
|
1875
|
+
if (finding.kind !== "vulnerable_dependency" && finding.kind !== "outdated_dependency") return "unknown";
|
|
1876
|
+
const entries = Object.entries(finding.dependencyGroups ?? {});
|
|
1877
|
+
if (entries.length === 0) return "unknown";
|
|
1878
|
+
return combineScopes(entries.map(([path, groups]) => lockfileScope(path, Array.isArray(groups) ? groups : [])));
|
|
1879
|
+
}
|
|
1880
|
+
function combineScopes(scopes) {
|
|
1881
|
+
if (scopes.includes("runtime")) return "runtime";
|
|
1882
|
+
if (scopes.includes("unknown") || scopes.length === 0) return "unknown";
|
|
1883
|
+
return "dev";
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Whether the repository declares the package itself. Per lockfile the sweep
|
|
1887
|
+
* says direct or transitive; across lockfiles one direct declaration makes the
|
|
1888
|
+
* finding direct (that copy is the repository's to bump), transitive only when
|
|
1889
|
+
* every readable lockfile says so, unknown when the sweep could read none of
|
|
1890
|
+
* them or did not record it (older images, code kinds). Presentation only; the
|
|
1891
|
+
* remediation step already finds every resolved copy and its parent itself.
|
|
1892
|
+
*/
|
|
1893
|
+
function dependencyRelation(finding) {
|
|
1894
|
+
if (finding.kind !== "vulnerable_dependency" && finding.kind !== "outdated_dependency") return "unknown";
|
|
1895
|
+
const values = Object.values(finding.dependencyRelation ?? {});
|
|
1896
|
+
if (values.includes("direct")) return "direct";
|
|
1897
|
+
if (values.length > 0 && values.every((v) => v === "transitive")) return "transitive";
|
|
1898
|
+
return "unknown";
|
|
1899
|
+
}
|
|
1766
1900
|
new Set(CODE_REMEDIATION_KINDS);
|
|
1767
1901
|
/** A non-empty string, or null. */
|
|
1768
1902
|
const locationText = z.string().min(1).nullable().catch(null);
|
|
@@ -2115,7 +2249,7 @@ const DOCUMENT_INPUTS = Object.freeze({
|
|
|
2115
2249
|
* reads as deleted) and fields they explain would simply be missing. So it
|
|
2116
2250
|
* refuses to start, and says what to do.
|
|
2117
2251
|
*/
|
|
2118
|
-
function apiLevelProblem(introspection, apiUrl, required =
|
|
2252
|
+
function apiLevelProblem(introspection, apiUrl, required = 2, version = SERVER_VERSION) {
|
|
2119
2253
|
const offered = introspection.mcpApiLevel ?? 0;
|
|
2120
2254
|
if (offered >= required) return null;
|
|
2121
2255
|
return `The Taiga API at ${apiUrl} is older than @taiga-ai/mcp-server ${version} needs (API level ${offered}, needs ${required}). Run the version published for it, or wait for that API to be updated; an earlier release of this package works against it.`;
|
|
@@ -2372,7 +2506,7 @@ const BY_ARGUMENT = {
|
|
|
2372
2506
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2373
2507
|
const BY_TOOL = {
|
|
2374
2508
|
taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
|
|
2375
|
-
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative says whether one is being
|
|
2509
|
+
taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_initiative's situation says whether one is being planned or failed, and taiga_plan_again asks for a new one." : ITEM_ID_HINT
|
|
2376
2510
|
};
|
|
2377
2511
|
/**
|
|
2378
2512
|
* What the API calls the thing each ID names, as its 404 says it ("Environment
|
|
@@ -2510,11 +2644,27 @@ function inProductWords(value) {
|
|
|
2510
2644
|
function inProductWording(said) {
|
|
2511
2645
|
return said.replace(/^Backlog item not found/i, "Initiative not found").replace(/^Build run step not found/i, "Run step not found").replace(/^Build run not found/i, "Run not found").replace(/^Custom document not found/i, "Knowledge file not found").replace(/^(Branch "[^"]*" does not exist in \S+?)\.?$/i, "$1. taiga_list_repository_branches lists the branches it has.").replace(/^This product already has a repository\. Change it instead of linking another\./i, "This product already has a repository. Pass replacesRepositoryId to swap it in one step, or taiga_unlink_repository first (taiga_list_product_repositories gives its ID).");
|
|
2512
2646
|
}
|
|
2647
|
+
/**
|
|
2648
|
+
* A float without its binary noise: six significant digits. Delivery metrics
|
|
2649
|
+
* came as `deploysPerWeek: 6.766666666666667`, sixteen digits nobody reads
|
|
2650
|
+
* (PRO-2847). Significant digits rather than fixed decimals: an EPSS score of
|
|
2651
|
+
* 0.00043, a 0.4% failure rate or 99.995% uptime keep what they say, where
|
|
2652
|
+
* two decimals read them as 0, 0 and 100.
|
|
2653
|
+
*/
|
|
2654
|
+
const roundFloats = (_key, value) => typeof value === "number" && Number.isFinite(value) && !Number.isInteger(value) ? Number(value.toPrecision(6)) : value;
|
|
2655
|
+
/**
|
|
2656
|
+
* A result as the model reads it: compact JSON, in the product's words, its
|
|
2657
|
+
* floats rounded. Indentation was a fifth of a list's characters (PRO-2847),
|
|
2658
|
+
* paid on every call; the model reads JSON without it.
|
|
2659
|
+
*/
|
|
2660
|
+
function serializeResult(value) {
|
|
2661
|
+
return JSON.stringify(inProductWords(value), roundFloats);
|
|
2662
|
+
}
|
|
2513
2663
|
/** A successful tool result: one JSON text block the model reads. */
|
|
2514
2664
|
function toolResult(value) {
|
|
2515
2665
|
return { content: [{
|
|
2516
2666
|
type: "text",
|
|
2517
|
-
text:
|
|
2667
|
+
text: serializeResult(value)
|
|
2518
2668
|
}] };
|
|
2519
2669
|
}
|
|
2520
2670
|
/**
|
|
@@ -3785,14 +3935,14 @@ function handbackSaid(escalation) {
|
|
|
3785
3935
|
case "review_bot_cap": return `review bots still have open comments${rounds}`;
|
|
3786
3936
|
case "ci_never_settled": return "CI did not finish in time";
|
|
3787
3937
|
case "ready_blocked": return `CI is green, but Taiga kept the pull request a draft${blockers.length ? `: ${blockers.join("; ")}` : ""}`;
|
|
3788
|
-
case "check_weakened": return `CI went green only after a
|
|
3938
|
+
case "check_weakened": return `CI went green only after a fix changed ${typeof e.file === "string" ? e.file : "a CI workflow or a test"}`;
|
|
3789
3939
|
default: return typeof e.reason === "string" ? e.reason : null;
|
|
3790
3940
|
}
|
|
3791
3941
|
}
|
|
3792
3942
|
/** What a person does about a handback, by its reason, as the web's handback detail says it. */
|
|
3793
3943
|
function handbackNextStep(escalation) {
|
|
3794
3944
|
const reason = escalation && typeof escalation === "object" ? escalation.reason : null;
|
|
3795
|
-
if (reason === "check_weakened") return "review
|
|
3945
|
+
if (reason === "check_weakened") return "review that change on GitHub before merging";
|
|
3796
3946
|
if (reason === "ci_never_settled") return "look at what is still running or queued in CI on GitHub, then review the pull request";
|
|
3797
3947
|
return "fix it by hand, or review the pull request on GitHub, which hands it back to the agent";
|
|
3798
3948
|
}
|
|
@@ -3855,6 +4005,34 @@ function hardeningSaid(blocking, named) {
|
|
|
3855
4005
|
const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
|
|
3856
4006
|
return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
|
|
3857
4007
|
}
|
|
4008
|
+
/** How each hardening verdict reads (PRO-2829): "passed" only when every scanner that applies ran. */
|
|
4009
|
+
const VERDICT_SAID = {
|
|
4010
|
+
passed: "Hardening passed: every scanner that applies ran and nothing blocks",
|
|
4011
|
+
passed_with_gaps: "Hardening passed, but not every scanner ran, so its checks are missing from the result",
|
|
4012
|
+
not_scanned: "No security scanner ran in hardening: \"passed\" only means no blocking finding was recorded",
|
|
4013
|
+
failed: "Hardening failed: blocking findings remain"
|
|
4014
|
+
};
|
|
4015
|
+
/**
|
|
4016
|
+
* What a run's hardening came to, coverage included (PRO-2829), from the
|
|
4017
|
+
* verdict the API reads off it, or read here from an API from before that.
|
|
4018
|
+
* Null without a hardening result.
|
|
4019
|
+
*/
|
|
4020
|
+
function scanSaid(hardening) {
|
|
4021
|
+
if (!hardening || typeof hardening !== "object") return null;
|
|
4022
|
+
const h = hardening;
|
|
4023
|
+
const coverage = typeof h.verdict === "string" && HARDENING_VERDICTS.includes(h.verdict) && Array.isArray(h.gaps) ? {
|
|
4024
|
+
verdict: h.verdict,
|
|
4025
|
+
gaps: h.gaps
|
|
4026
|
+
} : typeof h.passed === "boolean" && h.scanners ? hardeningCoverage(h) : null;
|
|
4027
|
+
if (!coverage) return null;
|
|
4028
|
+
const gaps = coverage.gaps.map((gap) => `${gap.scanner}${gap.status === "failed" ? " did not finish" : ""}${gap.reason ? ` (${gap.reason})` : ""}`);
|
|
4029
|
+
const missing = gaps.length > 0 ? `; did not run: ${gaps.join(", ")}` : "";
|
|
4030
|
+
return {
|
|
4031
|
+
verdict: coverage.verdict,
|
|
4032
|
+
gaps: gaps.length,
|
|
4033
|
+
said: `${VERDICT_SAID[coverage.verdict]}${missing}.`
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
3858
4036
|
/**
|
|
3859
4037
|
* What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
|
|
3860
4038
|
* bare enums. As @taiga/types documents them.
|
|
@@ -3875,20 +4053,23 @@ const FAILED_REASON_MEANS = {
|
|
|
3875
4053
|
* text out: its first sentence is in the outcome, and taiga_get_run has it all.
|
|
3876
4054
|
*/
|
|
3877
4055
|
function readableRun(run, opts = {}) {
|
|
3878
|
-
const { prDraft, error, reviewEscalation, runtime: _runtime, ...rest } = run;
|
|
4056
|
+
const { prDraft, error, reviewEscalation, runtime: _runtime, origin, ...rest } = run;
|
|
3879
4057
|
const said = runOutcome(run);
|
|
3880
4058
|
const means = FAILED_REASON_MEANS[String(run["failedReason"])];
|
|
3881
4059
|
const hardening = run["hardeningResult"];
|
|
3882
4060
|
const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
|
|
3883
4061
|
const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
|
|
4062
|
+
const scan = scanSaid(hardening);
|
|
3884
4063
|
return {
|
|
3885
4064
|
...rest,
|
|
4065
|
+
...origin === void 0 ? {} : { origin: origin === "backlog" ? "initiative" : origin },
|
|
3886
4066
|
...means ? { failedReasonMeans: means } : {},
|
|
4067
|
+
...scan && scan.gaps + (scan.verdict === "not_scanned" ? 1 : 0) > 0 ? { scanCoverage: scan.said } : {},
|
|
3887
4068
|
...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
|
|
3888
4069
|
...opts.row || error === void 0 ? {} : { error },
|
|
3889
4070
|
...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
|
|
3890
4071
|
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
3891
|
-
...said
|
|
4072
|
+
...said ? { outcome: said.outcome } : {}
|
|
3892
4073
|
};
|
|
3893
4074
|
}
|
|
3894
4075
|
/** Distinct initiatives a run list reads to say which moved on; past this a waiting row says it was not checked. */
|
|
@@ -3940,7 +4121,8 @@ const INITIATIVE_ENDED = {
|
|
|
3940
4121
|
/** A readable run row that also says when its initiative ended while its pull request waited. */
|
|
3941
4122
|
function withInitiativeState(row, states) {
|
|
3942
4123
|
const state = states.get(String(row["backlogItemId"]));
|
|
3943
|
-
|
|
4124
|
+
const status = String(row["status"]);
|
|
4125
|
+
if (!(BUILD_RUN_STATUSES.includes(status) && ["gated", "unmerged"].includes(landedDisposition(status, row["prMerged"] === true)))) return row;
|
|
3944
4126
|
if (state === "unchecked") return {
|
|
3945
4127
|
...row,
|
|
3946
4128
|
outcome: `${String(row["outcome"])} Whether its initiative still wants it was not checked.`
|
|
@@ -3951,7 +4133,6 @@ function withInitiativeState(row, states) {
|
|
|
3951
4133
|
const open = row["prState"] === "open" ? ` ${pr} is still open on the forge.` : "";
|
|
3952
4134
|
return {
|
|
3953
4135
|
...row,
|
|
3954
|
-
disposition: "abandoned",
|
|
3955
4136
|
actions: [],
|
|
3956
4137
|
outcome: `Its initiative ${ended}, so nobody is waiting on this run.${open}`
|
|
3957
4138
|
};
|
|
@@ -4060,19 +4241,20 @@ function registerBuildTools(server, client) {
|
|
|
4060
4241
|
if (!client.hasCapability("products:read")) return;
|
|
4061
4242
|
server.registerTool("taiga_list_runs", {
|
|
4062
4243
|
title: "List runs",
|
|
4063
|
-
description: "List coding agent runs, newest first: branch, pull request, step progress, outcome
|
|
4244
|
+
description: "List coding agent runs, newest first: status, branch, pull request, step progress, actions and outcome, one sentence saying how it stands: running, waiting for a person to review and merge, merged, failed, stopped, its pull request closed unmerged, or nobody waiting on it because its initiative ended. Without productId, every product's runs; active: true keeps running ones. taiga_get_run reads one; carry work on from the initiative (taiga_resume_initiative).",
|
|
4064
4245
|
inputSchema: z.object({
|
|
4065
4246
|
productId: productId.optional(),
|
|
4066
|
-
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
|
|
4247
|
+
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"),
|
|
4067
4248
|
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"),
|
|
4068
4249
|
...pagination
|
|
4069
4250
|
}),
|
|
4070
4251
|
annotations: READ_ONLY
|
|
4071
4252
|
}, (params) => runTool(async () => {
|
|
4072
|
-
if (
|
|
4073
|
-
if (params.productId && params.
|
|
4074
|
-
|
|
4075
|
-
|
|
4253
|
+
if (params.active && (params.productId || params.initiativeId)) return toolError("VALIDATION_ERROR", "active narrows the org-wide list only: omit productId and initiativeId, or read the product's runs and judge each by its status.");
|
|
4254
|
+
if (!params.productId && params.initiativeId && !client.hasCapability("backlog:read")) return toolError("VALIDATION_ERROR", "This key cannot read initiatives, so it cannot find the initiative's product: pass productId as well.");
|
|
4255
|
+
const productIdOf = params.productId ?? (params.initiativeId ? (await client.getBacklogItem(params.initiativeId)).data.productId : void 0);
|
|
4256
|
+
if (productIdOf) {
|
|
4257
|
+
const result = await client.listBuildRuns(productIdOf, {
|
|
4076
4258
|
backlogItemId: params.initiativeId,
|
|
4077
4259
|
limit: params.limit,
|
|
4078
4260
|
cursor: params.cursor
|
|
@@ -4085,7 +4267,7 @@ function registerBuildTools(server, client) {
|
|
|
4085
4267
|
}));
|
|
4086
4268
|
server.registerTool("taiga_get_run", {
|
|
4087
4269
|
title: "Get run",
|
|
4088
|
-
description: "Read one run with its steps:
|
|
4270
|
+
description: "Read one run with its steps: status, outcome, pull request, hardening (scanCoverage names the security scanners that did not run, and why; hardeningResult.verdict is passed only when every one ran), and per step its title, status, commit, files changed, verification and headline (first paragraph of the agent's report). verification is Taiga's own check after the step (tests, lint, build), so a docs-only step can fail on the repository's failing tests (verificationNote says when one failed throughout). A failed or stopped run has stoppedIn, lastProgressAt and failedReasonMeans (what its failedReason means). full: true adds complete instructions and reports. actions: resume and retry_from_failed_step are taiga_resume_initiative; webOnlyActions (start_over) are in the web; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
|
|
4089
4271
|
inputSchema: z.object({
|
|
4090
4272
|
runId,
|
|
4091
4273
|
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
@@ -4212,9 +4394,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4212
4394
|
if (item) {
|
|
4213
4395
|
Object.assign(row, {
|
|
4214
4396
|
title: item.title,
|
|
4215
|
-
|
|
4216
|
-
planningStatus: item.planningStatus,
|
|
4217
|
-
needsHumanCause: item.needsHumanCause,
|
|
4397
|
+
...standing(item),
|
|
4218
4398
|
criteria: criteriaCounts(item),
|
|
4219
4399
|
lifecycle: lifecycle ? {
|
|
4220
4400
|
turn: lifecycle.turn,
|
|
@@ -4246,7 +4426,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4246
4426
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
4247
4427
|
return {
|
|
4248
4428
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
4249
|
-
entries: rows,
|
|
4429
|
+
entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
|
|
4250
4430
|
waitingOn: line ?? waitingOn(rows),
|
|
4251
4431
|
...note ? { note } : {}
|
|
4252
4432
|
};
|
|
@@ -4292,18 +4472,21 @@ function registerQueueTools(server, client) {
|
|
|
4292
4472
|
}
|
|
4293
4473
|
//#endregion
|
|
4294
4474
|
//#region src/tools/backlogs.ts
|
|
4295
|
-
/**
|
|
4475
|
+
/**
|
|
4476
|
+
* What a list row needs to say for the model to pick an item to read in full.
|
|
4477
|
+
* One vocabulary for where an initiative stands (PRO-2828): its board column,
|
|
4478
|
+
* whose turn it is and the situation sentence. The API's own fields for it
|
|
4479
|
+
* (status, planningStatus, needsHumanCause, lifecycle) said the same thing
|
|
4480
|
+
* four more ways, and a reviewing agent had to reconcile them; they are in
|
|
4481
|
+
* taiga_get_initiative with full: true.
|
|
4482
|
+
*/
|
|
4296
4483
|
const BACKLOG_SUMMARY_FIELDS = [
|
|
4297
4484
|
"id",
|
|
4298
4485
|
"title",
|
|
4299
4486
|
"category",
|
|
4300
|
-
"status",
|
|
4301
|
-
"state",
|
|
4302
|
-
"planningStatus",
|
|
4303
4487
|
"priority",
|
|
4304
4488
|
"estimate",
|
|
4305
4489
|
"origin",
|
|
4306
|
-
"needsHumanCause",
|
|
4307
4490
|
"archivedAt",
|
|
4308
4491
|
"createdAt",
|
|
4309
4492
|
"updatedAt"
|
|
@@ -4317,21 +4500,44 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
4317
4500
|
"id",
|
|
4318
4501
|
"productId",
|
|
4319
4502
|
"title",
|
|
4320
|
-
"status",
|
|
4321
|
-
"state",
|
|
4322
|
-
"planningStatus",
|
|
4323
|
-
"needsHumanCause",
|
|
4324
|
-
"needsHumanDetail",
|
|
4325
4503
|
"autoBuild",
|
|
4326
|
-
"archivedAt"
|
|
4327
|
-
"lifecycle"
|
|
4504
|
+
"archivedAt"
|
|
4328
4505
|
];
|
|
4506
|
+
/** The fields of an initiative's summary read: its row, plus what it is about. */
|
|
4507
|
+
const ITEM_SUMMARY_FIELDS = [
|
|
4508
|
+
...BACKLOG_SUMMARY_FIELDS,
|
|
4509
|
+
"productId",
|
|
4510
|
+
"description",
|
|
4511
|
+
"autoBuild",
|
|
4512
|
+
"dependencies"
|
|
4513
|
+
];
|
|
4514
|
+
/** How much of a description a summary read carries; full: true has the rest. */
|
|
4515
|
+
const DESCRIPTION_IN_SUMMARY = 600;
|
|
4516
|
+
/**
|
|
4517
|
+
* Where an initiative stands, in the one vocabulary every read uses
|
|
4518
|
+
* (PRO-2828): its board column, whose turn it is, the situation sentence, and
|
|
4519
|
+
* its latest run to open with taiga_get_run.
|
|
4520
|
+
*/
|
|
4521
|
+
function standing(item) {
|
|
4522
|
+
const life = item["lifecycle"];
|
|
4523
|
+
const run = life?.latestBuild;
|
|
4524
|
+
const situation = situationOf(item);
|
|
4525
|
+
return {
|
|
4526
|
+
column: typeof item["state"] === "string" ? item["state"] : null,
|
|
4527
|
+
turn: typeof life?.turn === "string" ? life.turn : null,
|
|
4528
|
+
...situation ? { situation } : {},
|
|
4529
|
+
latestRun: run && typeof run.id === "string" ? {
|
|
4530
|
+
id: run.id,
|
|
4531
|
+
...typeof run.prNumber === "number" ? { prNumber: run.prNumber } : {}
|
|
4532
|
+
} : null
|
|
4533
|
+
};
|
|
4534
|
+
}
|
|
4329
4535
|
/**
|
|
4330
4536
|
* What the lifecycle on an initiative says, and the call each next step is.
|
|
4331
4537
|
* needsHumanCause is only the line's flag; a plan that failed outside the line
|
|
4332
4538
|
* leaves it null, and the lifecycle is what says why and what moves it on.
|
|
4333
4539
|
*/
|
|
4334
|
-
const LIFECYCLE_MEANING = "situation says in a sentence where it stands and
|
|
4540
|
+
const LIFECYCLE_MEANING = "situation says in a sentence where it stands and names the next call; turn says whose move it is (you, agent, or idle: nobody owes one).";
|
|
4335
4541
|
/** The move each call to action is, in the tools' words. */
|
|
4336
4542
|
const CTA_MOVE = {
|
|
4337
4543
|
start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
|
|
@@ -4340,12 +4546,12 @@ const CTA_MOVE = {
|
|
|
4340
4546
|
resume_build: "taiga_resume_initiative carries the run on (paid work: ask first)",
|
|
4341
4547
|
retry_build: "a new run starts over from the web; taiga_move_initiative takes it off the line",
|
|
4342
4548
|
review_pr: "a person reviews and merges the pull request",
|
|
4343
|
-
view_pr: "the
|
|
4549
|
+
view_pr: "the agent waits for its checks and fixes what fails; it merges on its own only with auto-merge on"
|
|
4344
4550
|
};
|
|
4345
4551
|
/** The move for an initiative off the line whose last run did not land: there is no line to take it off (PRO-2840). */
|
|
4346
4552
|
const OFF_THE_LINE_MOVE = "taiga_queue_initiative puts it back on the line (paid work: ask first)";
|
|
4347
|
-
/** What happens to a queued initiative
|
|
4348
|
-
const REQUEUED_MOVE = "the line
|
|
4553
|
+
/** What happens to a queued initiative: the line takes it up when its turn comes, replanning a run that did not land (PRO-2840). */
|
|
4554
|
+
const REQUEUED_MOVE = "the line takes it up when its turn comes";
|
|
4349
4555
|
/** How a run stands, as the end of "its latest run ...", by disposition. */
|
|
4350
4556
|
const RUN_STANDS = {
|
|
4351
4557
|
in_flight: "is running",
|
|
@@ -4382,7 +4588,10 @@ function situationOf(item) {
|
|
|
4382
4588
|
const column = typeof item["state"] === "string" ? item["state"] : null;
|
|
4383
4589
|
const offTheLine = column !== null && !LINE_STATES.includes(column);
|
|
4384
4590
|
const stalled = life.stage === "building" && life.turn !== "agent";
|
|
4385
|
-
const requeued =
|
|
4591
|
+
const requeued = column === "queue" && (stalled || life.turn === "idle" && life.cta == null);
|
|
4592
|
+
const offTheLineStart = offTheLine && (life.cta === "start_build" || life.cta === "start_plan");
|
|
4593
|
+
if (requeued && !stalled) stage = life.stage === "backlog" ? "Queued" : `Queued, ${stage.charAt(0).toLowerCase()}${stage.slice(1)}`;
|
|
4594
|
+
else if (offTheLineStart && life.stage === "planned") stage = "Planned, off the line";
|
|
4386
4595
|
if (stalled) stage = requeued ? "Queued" : offTheLine ? "Off the line" : column === null ? "Its build stopped" : "On the line, but its build stopped";
|
|
4387
4596
|
const build = life.latestBuild;
|
|
4388
4597
|
if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
|
|
@@ -4403,7 +4612,7 @@ function situationOf(item) {
|
|
|
4403
4612
|
}
|
|
4404
4613
|
const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
|
|
4405
4614
|
const handedBack = handbackSaid(build?.reviewEscalation);
|
|
4406
|
-
const move = life.cta === "review_pr" && handedBack ? `a person should ${handbackNextStep(build.reviewEscalation)}` : stalled && offTheLine ? OFF_THE_LINE_MOVE : typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
|
|
4615
|
+
const move = life.cta === "review_pr" && handedBack ? `a person should ${handbackNextStep(build.reviewEscalation)}` : stalled && offTheLine || offTheLineStart && life.stage !== "backlog" ? OFF_THE_LINE_MOVE : typeof life.cta === "string" ? CTA_MOVE[life.cta] : void 0;
|
|
4407
4616
|
if (requeued) parts.push(`Nothing to do: ${REQUEUED_MOVE}.`);
|
|
4408
4617
|
else if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
|
|
4409
4618
|
else if (turn && move) parts.push(`${turn}: ${move}.`);
|
|
@@ -4431,15 +4640,14 @@ function endingSaid(item) {
|
|
|
4431
4640
|
if (next && typeof next.id === "string") said.push(`; superseded by "${String(next.title)}" (${next.id})`);
|
|
4432
4641
|
return said.join("");
|
|
4433
4642
|
}
|
|
4434
|
-
/** A row or item with
|
|
4643
|
+
/** A row or item with where it stands added (`standing`). */
|
|
4435
4644
|
function withSituation(item, row = item) {
|
|
4436
|
-
|
|
4437
|
-
return situation ? {
|
|
4645
|
+
return {
|
|
4438
4646
|
...row,
|
|
4439
|
-
|
|
4440
|
-
}
|
|
4647
|
+
...standing(item)
|
|
4648
|
+
};
|
|
4441
4649
|
}
|
|
4442
|
-
/** List rows, each with
|
|
4650
|
+
/** List rows, each with where it stands. */
|
|
4443
4651
|
function situatedRows(items) {
|
|
4444
4652
|
return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index], row));
|
|
4445
4653
|
}
|
|
@@ -4516,40 +4724,6 @@ const outcomeOf = (item) => withSituation(item, {
|
|
|
4516
4724
|
criteria: criteriaCounts(item)
|
|
4517
4725
|
});
|
|
4518
4726
|
const outcome = (item) => toolResult(outcomeOf(item));
|
|
4519
|
-
/** The statuses an initiative in each board column can have: the API keeps in_progress after a first run. */
|
|
4520
|
-
const COLUMN_STATUSES = {
|
|
4521
|
-
backlog: [
|
|
4522
|
-
"all",
|
|
4523
|
-
"open",
|
|
4524
|
-
"backlog",
|
|
4525
|
-
"in_progress"
|
|
4526
|
-
],
|
|
4527
|
-
todo: [
|
|
4528
|
-
"all",
|
|
4529
|
-
"open",
|
|
4530
|
-
"backlog",
|
|
4531
|
-
"in_progress"
|
|
4532
|
-
],
|
|
4533
|
-
queue: [
|
|
4534
|
-
"all",
|
|
4535
|
-
"open",
|
|
4536
|
-
"backlog",
|
|
4537
|
-
"in_progress"
|
|
4538
|
-
],
|
|
4539
|
-
plan: [
|
|
4540
|
-
"all",
|
|
4541
|
-
"open",
|
|
4542
|
-
"backlog",
|
|
4543
|
-
"in_progress"
|
|
4544
|
-
],
|
|
4545
|
-
build: [
|
|
4546
|
-
"all",
|
|
4547
|
-
"open",
|
|
4548
|
-
"in_progress"
|
|
4549
|
-
],
|
|
4550
|
-
done: ["all", "done"],
|
|
4551
|
-
canceled: ["all", "canceled"]
|
|
4552
|
-
};
|
|
4553
4727
|
/**
|
|
4554
4728
|
* Each tool is registered under the capability its route requires, so a
|
|
4555
4729
|
* key sees exactly what it may call: reads on backlog:read; the board move on
|
|
@@ -4582,25 +4756,15 @@ function registerBacklogTools(server, client) {
|
|
|
4582
4756
|
function registerBacklogReads(server, client) {
|
|
4583
4757
|
server.registerTool("taiga_list_initiatives", {
|
|
4584
4758
|
title: "List initiatives",
|
|
4585
|
-
description: "List a product's open
|
|
4759
|
+
description: "List a product's initiatives, newest first: open ones (not done or canceled) unless column says otherwise. Each row has its title, category, column (backlog, todo, queue, plan, build, done, canceled), turn (you, agent or idle), situation (where it stands and what moves it on, in a sentence), latestRun and criteria (acceptance criteria met; null before planning). query finds one by title; taiga_get_initiative reads one. Archived ones are left out unless asked for.",
|
|
4586
4760
|
inputSchema: z.object({
|
|
4587
4761
|
productId,
|
|
4588
4762
|
query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
|
|
4589
|
-
|
|
4763
|
+
column: z.enum([
|
|
4590
4764
|
"open",
|
|
4591
4765
|
"all",
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
"done",
|
|
4595
|
-
"canceled"
|
|
4596
|
-
]).optional().describe("open (default without state): not done or canceled; all: every one; in_progress covers everything between backlog and done"),
|
|
4597
|
-
planningStatus: z.enum([
|
|
4598
|
-
"unplanned",
|
|
4599
|
-
"planning",
|
|
4600
|
-
"planned",
|
|
4601
|
-
"failed"
|
|
4602
|
-
]).optional().describe("Only items whose plan is in this state"),
|
|
4603
|
-
state: z.enum(INITIATIVE_STATES).optional().describe("Only items in this column of the board: backlog, todo, queue, plan, build, done or canceled (status groups them coarser)"),
|
|
4766
|
+
...INITIATIVE_STATES
|
|
4767
|
+
]).optional().describe("open (default): not done or canceled; all: every one; or one column of the board"),
|
|
4604
4768
|
category: z.enum([
|
|
4605
4769
|
"feature",
|
|
4606
4770
|
"bug",
|
|
@@ -4612,56 +4776,66 @@ function registerBacklogReads(server, client) {
|
|
|
4612
4776
|
}),
|
|
4613
4777
|
annotations: READ_ONLY
|
|
4614
4778
|
}, (params) => runTool(async () => {
|
|
4615
|
-
const { query,
|
|
4616
|
-
if (state !== void 0 && !COLUMN_STATUSES[state].includes(status)) return toolError("VALIDATION_ERROR", `No initiative is in the ${state} column with status ${status}: pass state alone, or a status it can have (${COLUMN_STATUSES[state].join(", ")}).`);
|
|
4779
|
+
const { query, column = "open", productId: id, ...filters } = params;
|
|
4617
4780
|
const apiFilters = {
|
|
4618
4781
|
...filters,
|
|
4619
|
-
|
|
4782
|
+
order: "newest",
|
|
4783
|
+
...column === "open" ? { open: true } : column === "all" ? {} : { state: column }
|
|
4620
4784
|
};
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
const result = await client.listBacklogItems(params.productId, apiFilters);
|
|
4785
|
+
if (query === void 0) {
|
|
4786
|
+
const result = await client.listBacklogItems(id, apiFilters);
|
|
4624
4787
|
return toolResult(page(situatedRows(result.data), result.nextCursor));
|
|
4625
4788
|
}
|
|
4626
|
-
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "
|
|
4789
|
+
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "A title search answers one page; narrow the title, or leave query out to page through a column.");
|
|
4627
4790
|
let closedSeen = 0;
|
|
4628
|
-
const needle =
|
|
4629
|
-
const found = await searchPages((cursor) => client.listBacklogItems(
|
|
4791
|
+
const needle = query.toLowerCase();
|
|
4792
|
+
const found = await searchPages((cursor) => client.listBacklogItems(id, {
|
|
4630
4793
|
...apiFilters,
|
|
4794
|
+
...column === "open" ? { open: void 0 } : {},
|
|
4631
4795
|
limit: 100,
|
|
4632
4796
|
cursor
|
|
4633
|
-
}), query
|
|
4634
|
-
if (
|
|
4635
|
-
if (
|
|
4636
|
-
if (query !== void 0 && String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
|
|
4797
|
+
}), query, (row) => [row.title], params.limit ?? 25, (row) => {
|
|
4798
|
+
if (column !== "open" || !["done", "canceled"].includes(String(row.state))) return true;
|
|
4799
|
+
if (String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
|
|
4637
4800
|
return false;
|
|
4638
4801
|
});
|
|
4639
|
-
const hidden =
|
|
4802
|
+
const hidden = column === "open" && !found.more ? closedSeen : 0;
|
|
4640
4803
|
return toolResult({
|
|
4641
4804
|
...page(situatedRows(found.rows), null),
|
|
4642
|
-
...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"};
|
|
4643
|
-
...found.more ? { more: "There may be more than the limit carries: narrow
|
|
4805
|
+
...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"}; column: 'all' lists them.` } : {},
|
|
4806
|
+
...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {}
|
|
4644
4807
|
});
|
|
4645
4808
|
}));
|
|
4646
4809
|
server.registerTool("taiga_get_initiative", {
|
|
4647
4810
|
title: "Get initiative",
|
|
4648
|
-
description: `Read one initiative
|
|
4649
|
-
inputSchema: z.object({
|
|
4811
|
+
description: `Read one initiative: where it stands (column, turn, situation, latestRun), its acceptance criteria count and the start of its description. full: true reads it whole before implementing: the whole description, each acceptance criterion and whether it is met, technical context, source documents, and the API's own state fields. A failed plan carries planFailure (cause, meaning, when, by whom). ${LIFECYCLE_MEANING}`,
|
|
4812
|
+
inputSchema: z.object({
|
|
4813
|
+
initiativeId,
|
|
4814
|
+
full: z.boolean().optional().describe("Read it whole: every criterion, technical context and the raw state fields (default false)")
|
|
4815
|
+
}),
|
|
4650
4816
|
annotations: READ_ONLY
|
|
4651
4817
|
}, (params) => runTool(async () => {
|
|
4652
4818
|
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
4653
4819
|
const row = item;
|
|
4654
|
-
|
|
4655
|
-
const
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
...
|
|
4659
|
-
|
|
4660
|
-
|
|
4820
|
+
const planFailure = item.planningStatus === "failed" ? await planFailureOf(client, params.initiativeId, item.lifecycle?.planFailureCause) : null;
|
|
4821
|
+
const failure = planFailure ? { planFailure } : {};
|
|
4822
|
+
if (params.full) return toolResult({
|
|
4823
|
+
...withSituation(row),
|
|
4824
|
+
...failure
|
|
4825
|
+
});
|
|
4826
|
+
const summary = itemRows([item], ITEM_SUMMARY_FIELDS)[0];
|
|
4827
|
+
const description = String(summary["description"] ?? "");
|
|
4828
|
+
return toolResult({
|
|
4829
|
+
...withSituation(row, {
|
|
4830
|
+
...summary,
|
|
4831
|
+
description: description.length > DESCRIPTION_IN_SUMMARY ? `${description.slice(0, DESCRIPTION_IN_SUMMARY).trimEnd()}... (full: true has the rest)` : description
|
|
4832
|
+
}),
|
|
4833
|
+
...failure
|
|
4834
|
+
});
|
|
4661
4835
|
}));
|
|
4662
4836
|
server.registerTool("taiga_get_plan", {
|
|
4663
4837
|
title: "Get the active plan",
|
|
4664
|
-
description: "Read an initiative's active plan: its steps in order, each with title, layer, dependencies (the orderIndex of the steps it builds on), files written and read, and resources created. Enough to approve or reject it (taiga_approve_plan, taiga_reject_plan); full: true adds each step's complete instructions (thousands of tokens). NOT_FOUND when there is no active plan yet;
|
|
4838
|
+
description: "Read an initiative's active plan: its steps in order, each with title, layer, dependencies (the orderIndex of the steps it builds on), files written and read, and resources created. Enough to approve or reject it (taiga_approve_plan, taiga_reject_plan); full: true adds each step's complete instructions (thousands of tokens). NOT_FOUND when there is no active plan yet; taiga_get_initiative's situation says whether one is being generated.",
|
|
4665
4839
|
inputSchema: z.object({
|
|
4666
4840
|
initiativeId,
|
|
4667
4841
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
|
|
@@ -4824,7 +4998,7 @@ function registerReviewGate(server, client) {
|
|
|
4824
4998
|
function registerResume(server, client) {
|
|
4825
4999
|
server.registerTool("taiga_resume_initiative", {
|
|
4826
5000
|
title: "Resume initiative",
|
|
4827
|
-
description: "Carry on an initiative the line stopped on (
|
|
5001
|
+
description: "Carry on an initiative the line stopped on (its situation says the line stopped on it): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first unfinished step (committed steps are kept), a refused run start is cleared and retried. Refused (CONFLICT) when nothing is flagged or the cause has no resume; a plan that failed off the line (its situation names taiga_plan_again, and planFailure says why) needs taiga_plan_again.",
|
|
4828
5002
|
inputSchema: z.object({ initiativeId }),
|
|
4829
5003
|
annotations: OPERATE
|
|
4830
5004
|
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
|
|
@@ -4892,6 +5066,40 @@ function withFixability(row) {
|
|
|
4892
5066
|
};
|
|
4893
5067
|
return row;
|
|
4894
5068
|
}
|
|
5069
|
+
/**
|
|
5070
|
+
* Dev-only, runtime or unknown, said on a dependency row (PRO-2861) in place of the raw per-lockfile groups
|
|
5071
|
+
* the API carries: the groups need the lockfile name to read, which `dependencyScope` does once here.
|
|
5072
|
+
*/
|
|
5073
|
+
function withDependencyScope(row, { keepGroups = false } = {}) {
|
|
5074
|
+
const { dependencyGroups: _groups, dependencyRelation: _relation, ...lean } = row;
|
|
5075
|
+
const rest = keepGroups ? {
|
|
5076
|
+
...row,
|
|
5077
|
+
dependencyRelation: void 0
|
|
5078
|
+
} : lean;
|
|
5079
|
+
if (!(row["kind"] === "vulnerable_dependency" || row["kind"] === "outdated_dependency")) return rest;
|
|
5080
|
+
const kind = row["kind"];
|
|
5081
|
+
return {
|
|
5082
|
+
...rest,
|
|
5083
|
+
dependencyScope: dependencyScope({
|
|
5084
|
+
kind,
|
|
5085
|
+
dependencyGroups: row["dependencyGroups"]
|
|
5086
|
+
}),
|
|
5087
|
+
dependencyRelation: dependencyRelation({
|
|
5088
|
+
kind,
|
|
5089
|
+
dependencyRelation: row["dependencyRelation"]
|
|
5090
|
+
}),
|
|
5091
|
+
...keepGroups ? { dependencyRelationByLockfile: row["dependencyRelation"] ?? null } : {}
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
5094
|
+
/** A finding as a tool answers it: the row fields, the scope, and whether Fix would take it. */
|
|
5095
|
+
function findingRows(findings, extra = []) {
|
|
5096
|
+
return pickFields(findings, [
|
|
5097
|
+
...FINDING_ROW_FIELDS,
|
|
5098
|
+
"dependencyGroups",
|
|
5099
|
+
"dependencyRelation",
|
|
5100
|
+
...extra
|
|
5101
|
+
]).map((row) => withDependencyScope(row)).map(withFixability);
|
|
5102
|
+
}
|
|
4895
5103
|
/** The parts of an advisory record an agent reads; the rest (every affected range, every reference) is full: true. */
|
|
4896
5104
|
const ADVISORY_KEPT = [
|
|
4897
5105
|
"id",
|
|
@@ -4970,10 +5178,13 @@ function standingSummary(standing) {
|
|
|
4970
5178
|
const severity = String(violation["severity"] ?? "unknown");
|
|
4971
5179
|
bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
|
|
4972
5180
|
}
|
|
5181
|
+
const stated = (state) => controls.filter((c) => c["state"] === state).length;
|
|
4973
5182
|
return {
|
|
4974
5183
|
controls: listed.length,
|
|
4975
5184
|
controlsInPolicies: controls.length,
|
|
4976
|
-
|
|
5185
|
+
controlsMet: stated("met"),
|
|
5186
|
+
controlsNotApplicable: stated("not_applicable"),
|
|
5187
|
+
controlsNotAssessed: stated("no_evidence"),
|
|
4977
5188
|
violations: violations.length,
|
|
4978
5189
|
bySeverity,
|
|
4979
5190
|
resolvedViolations: resolved
|
|
@@ -5015,7 +5226,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5015
5226
|
sweep: null,
|
|
5016
5227
|
sweepNote: "The sweep overview could not be read: an empty list here is not an all-clear."
|
|
5017
5228
|
} : sweep ? { sweep } : {},
|
|
5018
|
-
...page(
|
|
5229
|
+
...page(findingRows(result.data), result.nextCursor),
|
|
5019
5230
|
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {},
|
|
5020
5231
|
...params.status === void 0 && params.severity === void 0 && params.kind === void 0 && typeof result.totalCount === "number" && sweep && sweep !== "unread" && typeof sweep["openFindings"] === "number" && sweep["openFindings"] !== result.totalCount ? { totalNote: "totalCount includes findings with a fix under way (remediating) and ignored findings whose ignore has expired; sweep.openFindings counts open ones only." } : {}
|
|
5021
5232
|
});
|
|
@@ -5030,10 +5241,10 @@ function registerMaintenanceTools(server, client) {
|
|
|
5030
5241
|
annotations: READ_ONLY
|
|
5031
5242
|
}, (params) => runTool(async () => {
|
|
5032
5243
|
const { runId, ...raw } = (await client.getFinding(params.findingId)).data;
|
|
5033
|
-
const finding = withFixability(params.full ? raw : {
|
|
5244
|
+
const finding = withFixability(params.full ? withDependencyScope(raw, { keepGroups: true }) : withDependencyScope({
|
|
5034
5245
|
...raw,
|
|
5035
5246
|
data: leanAdvisory(raw["data"])
|
|
5036
|
-
});
|
|
5247
|
+
}));
|
|
5037
5248
|
return toolResult(runId === void 0 ? finding : {
|
|
5038
5249
|
sweepId: runId,
|
|
5039
5250
|
...finding
|
|
@@ -5041,11 +5252,12 @@ function registerMaintenanceTools(server, client) {
|
|
|
5041
5252
|
}));
|
|
5042
5253
|
server.registerTool("taiga_get_policy_standing", {
|
|
5043
5254
|
title: "Get a product's policy standing",
|
|
5044
|
-
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep).
|
|
5255
|
+
description: "How a product stands against the organization's published policies: summary counts, when the documents were assessed, and each violated control with its violations (critical and high only, citing the policy sentence and the contradicting passage) and the work on each. state: breached (open scanner findings map to it, by findingsByTier; routine-only is not urgent) or diverged (the documents contradict it). live: false means the violation's initiative ended without fixing it: it needs a person or a new fix. raisedFor: false means the link is the backlog planner's: it said an initiative a person asked it for would close the violation, which may be a dedicated fix or broader work; true means a fix was raised for it (taiga_fix_policy_violation or the sweep). summary counts the other controls: met or not applicable as the documents show it, or not assessed (no evidence either way, which is no pass). controlId reads one control: its state, the passage behind a verdict, its document violations and how many scanner findings breach it. full: true adds evidence and resolved ones. Fix with taiga_fix_policy_violation.",
|
|
5045
5256
|
inputSchema: z.object({
|
|
5046
5257
|
productId,
|
|
5047
5258
|
full: z.boolean().optional().describe("Carry each violation's evidence and policy excerpt, cut to about 1,500 characters each, and up to 20 resolved violations (default false)"),
|
|
5048
|
-
violationId: idInput("violation", "this tool lists them").optional().describe("Only this violation, in full and uncut: its evidence, policy excerpt and work")
|
|
5259
|
+
violationId: idInput("violation", "this tool lists them").optional().describe("Only this violation, in full and uncut: its evidence, policy excerpt and work"),
|
|
5260
|
+
controlId: z.string().trim().min(1).max(40).optional().describe("Only this control, such as SEC-5.1: whether it is met, not applicable, violated or not assessed, with the passage")
|
|
5049
5261
|
}),
|
|
5050
5262
|
annotations: READ_ONLY
|
|
5051
5263
|
}, (params) => runTool(async () => {
|
|
@@ -5057,6 +5269,20 @@ function registerMaintenanceTools(server, client) {
|
|
|
5057
5269
|
const note = standing.assessment?.state === "never" ? "Never assessed: the documents have not been checked against the policies yet, so nothing here is an all-clear. An assessment runs when the reality documents are derived or refreshed (taiga_refresh_reality_documents, paid work: ask first)." : null;
|
|
5058
5270
|
const resolvedAny = Array.isArray(standing.resolvedDivergences) && standing.resolvedDivergences.length > 0;
|
|
5059
5271
|
const wording = violations.length > 0 || resolvedAny ? { wordingNote: "A violation's evidence is from its assessedAt; its title and summary may be from when it was first recorded. Where they disagree, the evidence is current." } : {};
|
|
5272
|
+
if (params.controlId && params.violationId) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both.");
|
|
5273
|
+
if (params.controlId) {
|
|
5274
|
+
const wanted = params.controlId.toUpperCase();
|
|
5275
|
+
const control = controls.find((c) => String(c.controlId).toUpperCase() === wanted);
|
|
5276
|
+
if (!control) return toolError("NOT_FOUND", "No control with that id in this product's policies; taiga_get_document (type, sections: []) lists a policy's controls.");
|
|
5277
|
+
const own = violations.filter((violation) => String(violation["controlId"]).toUpperCase() === wanted);
|
|
5278
|
+
return toolResult({
|
|
5279
|
+
...control,
|
|
5280
|
+
...control.state === "no_evidence" ? { meaning: "Not assessed: no finding or violation is recorded against it, and the documents were not found to show it met. That is no evidence either way, not a pass." } : {},
|
|
5281
|
+
violations: pickFields(own, VIOLATION_FIELDS),
|
|
5282
|
+
...Number(control.findingCount ?? 0) > 0 ? { findingsNote: `${String(control.findingCount)} open scanner finding${control.findingCount === 1 ? "" : "s"} breach it; taiga_list_findings (productId) lists them with the controls each breaches.` } : {},
|
|
5283
|
+
...own.length > 0 ? wording : {}
|
|
5284
|
+
});
|
|
5285
|
+
}
|
|
5060
5286
|
if (params.violationId) {
|
|
5061
5287
|
const one = [...violations, ...Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : []].find((violation) => violation["id"] === params.violationId);
|
|
5062
5288
|
if (!one) return toolError("NOT_FOUND", "No violation with that ID on this product; this tool without violationId lists them.");
|
|
@@ -5120,7 +5346,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5120
5346
|
...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
|
|
5121
5347
|
} : { status: "open" };
|
|
5122
5348
|
const finding = (await client.updateFinding(params.findingId, body)).data;
|
|
5123
|
-
return toolResult(
|
|
5349
|
+
return toolResult(findingRows([finding], ["ignoreNote"])[0]);
|
|
5124
5350
|
}));
|
|
5125
5351
|
server.registerTool("taiga_fix_finding", {
|
|
5126
5352
|
title: "Fix a finding",
|
|
@@ -5136,7 +5362,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5136
5362
|
const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
|
|
5137
5363
|
return toolResult({
|
|
5138
5364
|
backlogItemId: fixed.backlogItemId,
|
|
5139
|
-
finding: fixed.finding ?
|
|
5365
|
+
finding: fixed.finding ? findingRows([fixed.finding])[0] : null
|
|
5140
5366
|
});
|
|
5141
5367
|
}
|
|
5142
5368
|
const finding = (await client.getFinding(params.findingId)).data;
|
|
@@ -5215,6 +5441,28 @@ function readWith(row, backlogRead) {
|
|
|
5215
5441
|
default: return;
|
|
5216
5442
|
}
|
|
5217
5443
|
}
|
|
5444
|
+
/**
|
|
5445
|
+
* The glossary's words for what the API names after its tables (PRO-2823): a reviewer met kind review_backlog,
|
|
5446
|
+
* subjectType backlog_item and inbox_task beside a glossary that says initiative and run, and had to infer they
|
|
5447
|
+
* were the same things.
|
|
5448
|
+
*/
|
|
5449
|
+
const KIND_SAID = { review_backlog: "review_initiatives" };
|
|
5450
|
+
const SUBJECT_SAID = {
|
|
5451
|
+
backlog_item: "initiative",
|
|
5452
|
+
build_run: "run",
|
|
5453
|
+
inbox_task: "setup_task"
|
|
5454
|
+
};
|
|
5455
|
+
const kindSaid = (kind) => KIND_SAID[String(kind)] ?? kind;
|
|
5456
|
+
/** A setup task row in the glossary's words: the Action Required item it is, by the name taiga_resolve_action takes. */
|
|
5457
|
+
function setupTaskRow(row) {
|
|
5458
|
+
const { inboxItemId, inboxItemStatus, alsoFiledAs, ...rest } = row;
|
|
5459
|
+
return {
|
|
5460
|
+
...rest,
|
|
5461
|
+
...inboxItemId !== void 0 ? { actionId: inboxItemId } : {},
|
|
5462
|
+
...inboxItemStatus !== void 0 ? { actionStatus: inboxItemStatus } : {},
|
|
5463
|
+
...Array.isArray(alsoFiledAs) ? { alsoFiledAs: alsoFiledAs.map(setupTaskRow) } : {}
|
|
5464
|
+
};
|
|
5465
|
+
}
|
|
5218
5466
|
/** A summary past its first sentence: review_doc rows quoted two documents, 800 to 1,200 characters each. */
|
|
5219
5467
|
const SUMMARY_LIMIT = 200;
|
|
5220
5468
|
function firstSentence(text) {
|
|
@@ -5232,9 +5480,12 @@ function itemRow(row, now, full, backlogRead) {
|
|
|
5232
5480
|
const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
|
|
5233
5481
|
const { summary, status, blocking, ...lean } = rest;
|
|
5234
5482
|
const read = readWith(row, backlogRead);
|
|
5483
|
+
const saysNothing = row["kind"] === "setup_manual" && String(summary).trim().toLowerCase() === "manual config";
|
|
5235
5484
|
return {
|
|
5236
5485
|
...lean,
|
|
5237
|
-
|
|
5486
|
+
kind: kindSaid(lean["kind"]),
|
|
5487
|
+
...typeof lean["subjectType"] === "string" ? { subjectType: SUBJECT_SAID[lean["subjectType"]] ?? lean["subjectType"] } : {},
|
|
5488
|
+
...saysNothing ? {} : full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
|
|
5238
5489
|
...full || status !== "open" ? { status } : {},
|
|
5239
5490
|
...full || blocking === true ? { blocking } : {},
|
|
5240
5491
|
...read ? { readWith: read } : {},
|
|
@@ -5266,7 +5517,8 @@ function summaryOf(rows, stale) {
|
|
|
5266
5517
|
if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
|
|
5267
5518
|
else {
|
|
5268
5519
|
entry.open += 1;
|
|
5269
|
-
|
|
5520
|
+
const kind = String(kindSaid(row["kind"]));
|
|
5521
|
+
entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
|
|
5270
5522
|
}
|
|
5271
5523
|
byProduct.set(key, entry);
|
|
5272
5524
|
}
|
|
@@ -5322,15 +5574,10 @@ async function staleBacklogReviews(client, rows, out) {
|
|
|
5322
5574
|
const empty = /* @__PURE__ */ new Set();
|
|
5323
5575
|
await Promise.all(checked.map(async (productId) => {
|
|
5324
5576
|
try {
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
limit:
|
|
5328
|
-
})));
|
|
5329
|
-
if (pages.some((page) => page.data.some((item) => item.state === "backlog" || item.state === "todo"))) return;
|
|
5330
|
-
if (pages.some((page) => page.nextCursor)) {
|
|
5331
|
-
unchecked += asks.filter((row) => row["productId"] === productId).length;
|
|
5332
|
-
return;
|
|
5333
|
-
}
|
|
5577
|
+
if ((await Promise.all(["backlog", "todo"].map((state) => client.listBacklogItems(productId, {
|
|
5578
|
+
state,
|
|
5579
|
+
limit: 1
|
|
5580
|
+
})))).some((page) => page.data.length > 0)) return;
|
|
5334
5581
|
empty.add(productId);
|
|
5335
5582
|
} catch (error) {
|
|
5336
5583
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
@@ -5413,6 +5660,7 @@ function sameKeyOnce(rows) {
|
|
|
5413
5660
|
id: from["id"],
|
|
5414
5661
|
category: from["category"],
|
|
5415
5662
|
title: from["title"],
|
|
5663
|
+
inboxItemId: from["inboxItemId"],
|
|
5416
5664
|
inboxItemStatus: from["inboxItemStatus"]
|
|
5417
5665
|
});
|
|
5418
5666
|
if (!isOpen(kept) && isOpen(row)) {
|
|
@@ -5512,7 +5760,7 @@ function registerInboxTools(server, client) {
|
|
|
5512
5760
|
...params.cursor === void 0 ? { withTotal: true } : {}
|
|
5513
5761
|
});
|
|
5514
5762
|
return toolResult({
|
|
5515
|
-
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor, result.data.length),
|
|
5763
|
+
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)).map(setupTaskRow), result.nextCursor, result.data.length),
|
|
5516
5764
|
...totalOf(result)
|
|
5517
5765
|
});
|
|
5518
5766
|
}));
|
|
@@ -5765,42 +6013,31 @@ async function policiesPart(client) {
|
|
|
5765
6013
|
sections: Object.keys(sectionsOf(row["data"] ?? {}) ?? {}).length
|
|
5766
6014
|
}));
|
|
5767
6015
|
}
|
|
5768
|
-
/** The statuses an item is open in; the API's filter takes one at a time. */
|
|
5769
|
-
const OPEN_BACKLOG_STATUSES = ["backlog", "in_progress"];
|
|
5770
6016
|
/**
|
|
5771
|
-
* The items still open: one
|
|
5772
|
-
*
|
|
5773
|
-
* reports. The list is oldest first and the open items are the newest, so a
|
|
5774
|
-
* single page would miss exactly the ones that matter. Rows carrying a
|
|
5775
|
-
* retired status (readable for a rolling deploy) match neither filter;
|
|
5776
|
-
* the settled check below keeps the shape honest for whatever comes back.
|
|
6017
|
+
* The items still open, newest first (PRO-2843): one read of the open ones,
|
|
6018
|
+
* paged up to a cap the result reports. The API leaves archived items out.
|
|
5777
6019
|
*/
|
|
5778
6020
|
async function backlogPart(client, productId) {
|
|
5779
|
-
const
|
|
6021
|
+
const rows = [];
|
|
5780
6022
|
let truncated = false;
|
|
5781
6023
|
let pageCapped = false;
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
break;
|
|
5789
|
-
}
|
|
5790
|
-
const result = await client.listBacklogItems(productId, {
|
|
5791
|
-
status,
|
|
5792
|
-
limit: 100,
|
|
5793
|
-
cursor
|
|
5794
|
-
});
|
|
5795
|
-
for (const row of result.data) if (!isSettledOrArchived({
|
|
5796
|
-
status: String(row.status),
|
|
5797
|
-
archivedAt: row.archivedAt ?? null
|
|
5798
|
-
})) open.set(String(row.id), row);
|
|
5799
|
-
if (!result.nextCursor) break;
|
|
5800
|
-
cursor = result.nextCursor;
|
|
6024
|
+
let cursor;
|
|
6025
|
+
for (let pageIndex = 0;; pageIndex += 1) {
|
|
6026
|
+
if (pageIndex === BACKLOG_PAGE_LIMIT) {
|
|
6027
|
+
truncated = true;
|
|
6028
|
+
pageCapped = true;
|
|
6029
|
+
break;
|
|
5801
6030
|
}
|
|
6031
|
+
const result = await client.listBacklogItems(productId, {
|
|
6032
|
+
open: true,
|
|
6033
|
+
order: "newest",
|
|
6034
|
+
limit: 100,
|
|
6035
|
+
cursor
|
|
6036
|
+
});
|
|
6037
|
+
rows.push(...result.data);
|
|
6038
|
+
if (!result.nextCursor) break;
|
|
6039
|
+
cursor = result.nextCursor;
|
|
5802
6040
|
}
|
|
5803
|
-
const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
|
|
5804
6041
|
if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
|
|
5805
6042
|
const byColumn = {
|
|
5806
6043
|
backlog: 0,
|
|
@@ -5812,9 +6049,9 @@ async function backlogPart(client, productId) {
|
|
|
5812
6049
|
for (const row of rows) if (row.state) byColumn[row.state] = (byColumn[row.state] ?? 0) + 1;
|
|
5813
6050
|
return {
|
|
5814
6051
|
byColumn,
|
|
5815
|
-
items: situatedRows(rows.slice(
|
|
6052
|
+
items: situatedRows(rows.slice(0, BACKLOG_IN_CONTEXT)),
|
|
5816
6053
|
truncated,
|
|
5817
|
-
...pageCapped ? { note: "More than 1000 open items
|
|
6054
|
+
...pageCapped ? { note: "More than 1000 open items: byColumn counts the newest 1000; taiga_list_initiatives pages the rest." } : {}
|
|
5818
6055
|
};
|
|
5819
6056
|
}
|
|
5820
6057
|
/**
|
|
@@ -5839,7 +6076,7 @@ const CONTEXT_RUN_SAID = [
|
|
|
5839
6076
|
"runNumber",
|
|
5840
6077
|
"backlogItemId",
|
|
5841
6078
|
"targetTitle",
|
|
5842
|
-
"
|
|
6079
|
+
"status",
|
|
5843
6080
|
"outcome",
|
|
5844
6081
|
"failedReasonMeans",
|
|
5845
6082
|
"hardeningNote",
|
|
@@ -5925,24 +6162,24 @@ async function productContext(client, product, options) {
|
|
|
5925
6162
|
unavailable,
|
|
5926
6163
|
readNext: "Read a policy, which is the organization's and takes no productId, with taiga_get_document (type, then sections: [] for its outline or a control id such as SEC-5.1); the product's own specification, architecture and other documents with taiga_get_product_document (productId); an initiative with taiga_get_initiative (taiga_list_initiatives pages a truncated backlog) and a run with taiga_get_run."
|
|
5927
6164
|
};
|
|
5928
|
-
let characters =
|
|
6165
|
+
let characters = serializeResult(result).length;
|
|
5929
6166
|
for (let measured = -1; measured !== characters;) {
|
|
5930
6167
|
measured = characters;
|
|
5931
|
-
characters =
|
|
6168
|
+
characters = serializeResult({
|
|
5932
6169
|
...result,
|
|
5933
6170
|
characters: measured
|
|
5934
|
-
})
|
|
6171
|
+
}).length;
|
|
5935
6172
|
}
|
|
5936
6173
|
return toolResult({
|
|
5937
|
-
|
|
5938
|
-
|
|
6174
|
+
characters,
|
|
6175
|
+
...result
|
|
5939
6176
|
});
|
|
5940
6177
|
}
|
|
5941
6178
|
function registerContextTools(server, client) {
|
|
5942
6179
|
if (!client.hasCapability("products:read")) return;
|
|
5943
6180
|
server.registerTool("taiga_get_product_context", {
|
|
5944
6181
|
title: "Get product context",
|
|
5945
|
-
description: `Read where a product stands in one call; the first read for any product question. Name it by ID, slug or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, specification and architecture outlines (section ids, sizes), policies with section counts, open initiatives with a count per board column (byColumn; taiga_list_initiatives
|
|
6182
|
+
description: `Read where a product stands in one call; the first read for any product question. Name it by ID, slug or git remote URL. Returns product (in setup, readiness and next move), factory with its instructions, specification and architecture outlines (section ids, sizes), policies with section counts, open initiatives with a count per board column (byColumn; taiga_list_initiatives column: 'all' has the done and canceled), repositories, the latest five runs (taiga_list_runs has more), and counts for Action Required, open findings and policy violations. A part that failed is under unavailable, one the key cannot read under notGranted; neither shows as empty. documents: budget returns whole sections up to about ${DOCUMENT_BUDGET / 1e3}k characters each, full returns all.`,
|
|
5946
6183
|
inputSchema: z.object({
|
|
5947
6184
|
productId: productId.optional(),
|
|
5948
6185
|
slug: slug.optional(),
|
|
@@ -6313,12 +6550,13 @@ function registerObserveTools(server, client) {
|
|
|
6313
6550
|
]);
|
|
6314
6551
|
const now = current.data;
|
|
6315
6552
|
const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_create_environment, and its deployments are mirrored from the provider once it is." : history.data.length === 0 && !params.cursor ? "No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last synced), or nothing has been deployed." : null;
|
|
6553
|
+
const live = new Set([...now.current ?? [], ...now.deploying ?? []].map((row) => row.id));
|
|
6316
6554
|
return toolResult({
|
|
6317
6555
|
environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
|
|
6318
6556
|
...environments.more ? { moreEnvironments: true } : {},
|
|
6319
6557
|
running: deploymentLines(now.current ?? []),
|
|
6320
6558
|
deploying: deploymentLines(now.deploying ?? []),
|
|
6321
|
-
history: page(deploymentLines(history.data), history.nextCursor),
|
|
6559
|
+
history: page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor),
|
|
6322
6560
|
..."value" in sync ? { syncStatus: sync.value.data } : {
|
|
6323
6561
|
syncStatus: null,
|
|
6324
6562
|
syncUnavailable: sync.unavailable
|
|
@@ -22019,29 +22257,19 @@ function judgeItem(until, gate) {
|
|
|
22019
22257
|
/** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
|
|
22020
22258
|
function itemReport(item) {
|
|
22021
22259
|
const run = latestBuildOf(item);
|
|
22022
|
-
const situation = situationOf(item);
|
|
22023
22260
|
return {
|
|
22024
|
-
...situation ? { situation } : {},
|
|
22025
22261
|
...pickFields([item], [
|
|
22026
22262
|
"id",
|
|
22027
22263
|
"title",
|
|
22028
|
-
"state",
|
|
22029
|
-
"planningStatus",
|
|
22030
|
-
"needsHumanCause",
|
|
22031
|
-
"needsHumanDetail",
|
|
22032
22264
|
"autoBuild"
|
|
22033
22265
|
])[0],
|
|
22034
|
-
|
|
22035
|
-
|
|
22036
|
-
"stage",
|
|
22037
|
-
"cta",
|
|
22038
|
-
"planFailureCause"
|
|
22039
|
-
])[0],
|
|
22040
|
-
latestBuild: run ? pickFields([run], [
|
|
22266
|
+
...standing(item),
|
|
22267
|
+
latestRun: run ? pickFields([run], [
|
|
22041
22268
|
"id",
|
|
22042
22269
|
"status",
|
|
22043
22270
|
"completedSteps",
|
|
22044
22271
|
"totalSteps",
|
|
22272
|
+
"prNumber",
|
|
22045
22273
|
"prUrl",
|
|
22046
22274
|
"prState",
|
|
22047
22275
|
"prMerged",
|
|
@@ -22225,6 +22453,10 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
|
|
|
22225
22453
|
message: `${withStop(issue.message)} ${pointers.join(" ")}`
|
|
22226
22454
|
};
|
|
22227
22455
|
}
|
|
22456
|
+
if (issue.code === "invalid_value") return {
|
|
22457
|
+
...issue,
|
|
22458
|
+
message: `${withStop(issue.message)} Pass one of those values exactly.`
|
|
22459
|
+
};
|
|
22228
22460
|
const argument = issue.path?.length === 1 ? String(issue.path[0]) : void 0;
|
|
22229
22461
|
const source = argument ? idSource(argument) : void 0;
|
|
22230
22462
|
if (source && /received undefined/.test(issue.message)) return {
|
|
@@ -22373,7 +22605,7 @@ function registerAllTools(server, client, options = {}) {
|
|
|
22373
22605
|
function registerWhoami(server, client, registered) {
|
|
22374
22606
|
server.registerTool("taiga_whoami", {
|
|
22375
22607
|
title: "Who am I",
|
|
22376
|
-
description: "Read who this API key acts as: the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and its tools split into reads, writes, and ask-first writes (paid agent work, changes others see). A tool the key cannot use is not offered at all.",
|
|
22608
|
+
description: "Read who this API key acts as: the API it talks to (api, which says the environment), the person's email and organization role, the key's scopes (null means every capability the person has), its capabilities, and its tools split into reads, writes, and ask-first writes (paid agent work, changes others see). A tool the key cannot use is not offered at all.",
|
|
22377
22609
|
inputSchema: z.object({}),
|
|
22378
22610
|
annotations: READ_ONLY
|
|
22379
22611
|
}, () => runTool(async () => {
|
|
@@ -22381,6 +22613,7 @@ function registerWhoami(server, client, registered) {
|
|
|
22381
22613
|
const tools = [...registered.entries()].filter(([name]) => name !== "taiga_whoami");
|
|
22382
22614
|
const named = (pick) => tools.filter(([, notes]) => pick(notes)).map(([name]) => name);
|
|
22383
22615
|
return toolResult({
|
|
22616
|
+
api: client.apiHost,
|
|
22384
22617
|
email: me?.email ?? null,
|
|
22385
22618
|
role: me?.role ?? null,
|
|
22386
22619
|
scopes: me?.scopes ?? null,
|
|
@@ -22423,8 +22656,8 @@ const SERVER_INSTRUCTIONS = [
|
|
|
22423
22656
|
"conversation that specifies a new product. A factory owns products.",
|
|
22424
22657
|
"",
|
|
22425
22658
|
"**IDs** are UUIDs; the product, factory and initiative lists take query to find one by name. A NOT_FOUND names",
|
|
22426
|
-
"the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones
|
|
22427
|
-
"
|
|
22659
|
+
"the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones, newest",
|
|
22660
|
+
"first. outcome, situation and nextStep are one-sentence summaries; setup writes return readiness.",
|
|
22428
22661
|
"Product documents have two timelines: plan (intent) and reality (from the code).",
|
|
22429
22662
|
"",
|
|
22430
22663
|
"**Writing.** A tool that writes opens with \"Writes.\", \"Ask first (paid agent work).\" or \"Ask first (others",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taiga-ai/mcp-server",
|
|
3
|
-
"version": "0.26.0-rc.
|
|
3
|
+
"version": "0.26.0-rc.494",
|
|
4
4
|
"description": "Taiga for AI coding assistants: read products, policies, initiatives and runs, and operate the line, over MCP",
|
|
5
5
|
"mcpName": "ga.tai/mcp-server",
|
|
6
6
|
"type": "module",
|