@taiga-ai/mcp-server 0.26.0-rc.480 → 0.26.0-rc.491
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 +433 -201
- 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
|
*
|
|
@@ -1384,6 +1388,49 @@ z.object({
|
|
|
1384
1388
|
...e.failingChecks === void 0 ? {} : { failingChecks: e.failingChecks },
|
|
1385
1389
|
...e.blockers === void 0 ? {} : { blockers: e.blockers }
|
|
1386
1390
|
}));
|
|
1391
|
+
/**
|
|
1392
|
+
* What a run's hardening came to, coverage included (PRO-2829). `passed`
|
|
1393
|
+
* counted only blocking findings, so a run whose scanners never ran said
|
|
1394
|
+
* "passed" all the same.
|
|
1395
|
+
*
|
|
1396
|
+
* - `failed`: blocking findings remain (the gaps are still listed).
|
|
1397
|
+
* - `not_scanned`: no scanner that applies to the repository ran to the end.
|
|
1398
|
+
* - `passed_with_gaps`: nothing blocks, but some scanner did not run.
|
|
1399
|
+
* - `passed`: nothing blocks and every scanner that applies ran.
|
|
1400
|
+
*/
|
|
1401
|
+
const HARDENING_VERDICTS = [
|
|
1402
|
+
"passed",
|
|
1403
|
+
"passed_with_gaps",
|
|
1404
|
+
"not_scanned",
|
|
1405
|
+
"failed"
|
|
1406
|
+
];
|
|
1407
|
+
/**
|
|
1408
|
+
* The verdict and the gaps of a hardening result (PRO-2829), for every reader
|
|
1409
|
+
* (the run read, the web's run page, the MCP), from what the agent reported.
|
|
1410
|
+
*
|
|
1411
|
+
* A gap is a scanner that applies and did not run: `not_run` (the scan never
|
|
1412
|
+
* reached it), `skipped` (its tool was missing), or `failed` with a reason,
|
|
1413
|
+
* which is a scanner that could not finish. A `failed` scanner without one
|
|
1414
|
+
* found something (tflint fails on a MAJOR that blocks nothing, and rows from
|
|
1415
|
+
* before `scannerReasons` used `failed` for "had findings"), so it ran and is
|
|
1416
|
+
* no gap. A scanner listed in `notApplicable` is no gap; on a result from
|
|
1417
|
+
* before that list, a skipped tflint is read the same way, as the only reason
|
|
1418
|
+
* the agent ever skipped it was a repository with no Terraform.
|
|
1419
|
+
*/
|
|
1420
|
+
function hardeningCoverage(result) {
|
|
1421
|
+
const notApplicable = new Set(result.notApplicable ?? []);
|
|
1422
|
+
const applies = Object.entries(result.scanners ?? {}).filter(([scanner, status]) => !notApplicable.has(scanner) && !(result.notApplicable === void 0 && scanner === "tflint" && status === "skipped"));
|
|
1423
|
+
const reasons = result.scannerReasons ?? {};
|
|
1424
|
+
const gaps = applies.filter(([scanner, status]) => status === "not_run" || status === "skipped" || status === "failed" && scanner in reasons).map(([scanner, status]) => ({
|
|
1425
|
+
scanner,
|
|
1426
|
+
status,
|
|
1427
|
+
reason: reasons[scanner] ?? null
|
|
1428
|
+
}));
|
|
1429
|
+
return {
|
|
1430
|
+
verdict: !result.passed ? "failed" : gaps.length === applies.length ? "not_scanned" : gaps.length > 0 ? "passed_with_gaps" : "passed",
|
|
1431
|
+
gaps
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1387
1434
|
[...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
|
|
1388
1435
|
//#endregion
|
|
1389
1436
|
//#region ../../packages/types/dist/agents.js
|
|
@@ -1763,6 +1810,92 @@ const MAINTENANCE_IGNORE_REASONS = [
|
|
|
1763
1810
|
"accepted_risk",
|
|
1764
1811
|
"wont_fix"
|
|
1765
1812
|
];
|
|
1813
|
+
/**
|
|
1814
|
+
* What one lockfile's group list says, read against what its osv-scanner
|
|
1815
|
+
* (osv-scalibr) extractor actually writes. Checked per extractor, because the
|
|
1816
|
+
* same list means different things in different files:
|
|
1817
|
+
*
|
|
1818
|
+
* - package-lock.json / npm-shrinkwrap.json: every package entry carries npm's
|
|
1819
|
+
* `dev` / `optional` / `devOptional` flags, transitive ones included, so an
|
|
1820
|
+
* EMPTY list is a runtime dependency, `dev` alone is dev-only, `optional`
|
|
1821
|
+
* alone is shipped, and `dev` + `optional` (devOptional) is left undecided.
|
|
1822
|
+
* - pnpm-lock.yaml: `dev` comes from a per-package `dev: true` that lockfile
|
|
1823
|
+
* v9 no longer writes, so an empty list says nothing; `dev` still means dev.
|
|
1824
|
+
* For a package a workspace declares under `dependencies` or
|
|
1825
|
+
* `optionalDependencies`, the sweep adds Taiga's own `prod`, so that copy
|
|
1826
|
+
* reads runtime. A `devDependencies` declaration is not turned into `dev`:
|
|
1827
|
+
* a runtime dependency's snapshot may pull the same copy in, which only the
|
|
1828
|
+
* `snapshots` graph could rule out, so it stays unknown.
|
|
1829
|
+
* - poetry.lock: group names with `main` dropped; a non-main, non-optional
|
|
1830
|
+
* group is not in the runtime install. Poetry 1.x lockfiles carry no groups
|
|
1831
|
+
* at all, so an empty list says nothing.
|
|
1832
|
+
* - uv.lock: only direct members of a group are tagged, so an empty list says
|
|
1833
|
+
* nothing; `dev` means dev.
|
|
1834
|
+
* - yarn.lock, go.mod/go.sum, Cargo.lock, Gemfile.lock and the rest carry no
|
|
1835
|
+
* group information: unknown whatever the list says.
|
|
1836
|
+
*
|
|
1837
|
+
* Anything this cannot place is `unknown`, never `runtime`: a wrong "runtime"
|
|
1838
|
+
* on a build tool is the claim this exists to stop, and a wrong "dev only" on a
|
|
1839
|
+
* shipped package is worse.
|
|
1840
|
+
*/
|
|
1841
|
+
function lockfileScope(path, groups) {
|
|
1842
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
1843
|
+
const set = new Set(groups.map((g) => g.trim().toLowerCase()).filter((g) => g !== ""));
|
|
1844
|
+
switch (name) {
|
|
1845
|
+
case "package-lock.json":
|
|
1846
|
+
case "npm-shrinkwrap.json": {
|
|
1847
|
+
if (set.size === 0) return "runtime";
|
|
1848
|
+
const dev = set.has("dev");
|
|
1849
|
+
const optional = set.has("optional");
|
|
1850
|
+
if (dev && optional) return "unknown";
|
|
1851
|
+
if (dev) return "dev";
|
|
1852
|
+
if (optional && set.size === 1) return "runtime";
|
|
1853
|
+
return "unknown";
|
|
1854
|
+
}
|
|
1855
|
+
case "pnpm-lock.yaml":
|
|
1856
|
+
if (set.has("prod")) return "runtime";
|
|
1857
|
+
return set.size === 1 && set.has("dev") ? "dev" : "unknown";
|
|
1858
|
+
case "uv.lock": return set.size === 1 && set.has("dev") ? "dev" : "unknown";
|
|
1859
|
+
case "poetry.lock": return set.size > 0 && !set.has("optional") && !set.has("main") ? "dev" : "unknown";
|
|
1860
|
+
default: return "unknown";
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Whether a dependency finding sits only in development dependencies, in at
|
|
1865
|
+
* least one runtime dependency set, or in lockfiles that do not say. Decided
|
|
1866
|
+
* per lockfile (`lockfileScope`) and combined pessimistically: one runtime
|
|
1867
|
+
* lockfile makes the finding runtime, one lockfile that cannot say makes it
|
|
1868
|
+
* unknown, and only all-dev reads as dev. Groups the sweep did not record
|
|
1869
|
+
* (older images, code kinds) are unknown. Presentation only: the tier and the
|
|
1870
|
+
* auto-remediation gate do not read this, and a malicious package runs its
|
|
1871
|
+
* install script in any group.
|
|
1872
|
+
*/
|
|
1873
|
+
function dependencyScope(finding) {
|
|
1874
|
+
if (finding.kind !== "vulnerable_dependency" && finding.kind !== "outdated_dependency") return "unknown";
|
|
1875
|
+
const entries = Object.entries(finding.dependencyGroups ?? {});
|
|
1876
|
+
if (entries.length === 0) return "unknown";
|
|
1877
|
+
return combineScopes(entries.map(([path, groups]) => lockfileScope(path, Array.isArray(groups) ? groups : [])));
|
|
1878
|
+
}
|
|
1879
|
+
function combineScopes(scopes) {
|
|
1880
|
+
if (scopes.includes("runtime")) return "runtime";
|
|
1881
|
+
if (scopes.includes("unknown") || scopes.length === 0) return "unknown";
|
|
1882
|
+
return "dev";
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Whether the repository declares the package itself. Per lockfile the sweep
|
|
1886
|
+
* says direct or transitive; across lockfiles one direct declaration makes the
|
|
1887
|
+
* finding direct (that copy is the repository's to bump), transitive only when
|
|
1888
|
+
* every readable lockfile says so, unknown when the sweep could read none of
|
|
1889
|
+
* them or did not record it (older images, code kinds). Presentation only; the
|
|
1890
|
+
* remediation step already finds every resolved copy and its parent itself.
|
|
1891
|
+
*/
|
|
1892
|
+
function dependencyRelation(finding) {
|
|
1893
|
+
if (finding.kind !== "vulnerable_dependency" && finding.kind !== "outdated_dependency") return "unknown";
|
|
1894
|
+
const values = Object.values(finding.dependencyRelation ?? {});
|
|
1895
|
+
if (values.includes("direct")) return "direct";
|
|
1896
|
+
if (values.length > 0 && values.every((v) => v === "transitive")) return "transitive";
|
|
1897
|
+
return "unknown";
|
|
1898
|
+
}
|
|
1766
1899
|
new Set(CODE_REMEDIATION_KINDS);
|
|
1767
1900
|
/** A non-empty string, or null. */
|
|
1768
1901
|
const locationText = z.string().min(1).nullable().catch(null);
|
|
@@ -2115,7 +2248,7 @@ const DOCUMENT_INPUTS = Object.freeze({
|
|
|
2115
2248
|
* reads as deleted) and fields they explain would simply be missing. So it
|
|
2116
2249
|
* refuses to start, and says what to do.
|
|
2117
2250
|
*/
|
|
2118
|
-
function apiLevelProblem(introspection, apiUrl, required =
|
|
2251
|
+
function apiLevelProblem(introspection, apiUrl, required = 2, version = SERVER_VERSION) {
|
|
2119
2252
|
const offered = introspection.mcpApiLevel ?? 0;
|
|
2120
2253
|
if (offered >= required) return null;
|
|
2121
2254
|
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 +2505,7 @@ const BY_ARGUMENT = {
|
|
|
2372
2505
|
/** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
|
|
2373
2506
|
const BY_TOOL = {
|
|
2374
2507
|
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
|
|
2508
|
+
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
2509
|
};
|
|
2377
2510
|
/**
|
|
2378
2511
|
* What the API calls the thing each ID names, as its 404 says it ("Environment
|
|
@@ -2510,11 +2643,27 @@ function inProductWords(value) {
|
|
|
2510
2643
|
function inProductWording(said) {
|
|
2511
2644
|
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
2645
|
}
|
|
2646
|
+
/**
|
|
2647
|
+
* A float without its binary noise: six significant digits. Delivery metrics
|
|
2648
|
+
* came as `deploysPerWeek: 6.766666666666667`, sixteen digits nobody reads
|
|
2649
|
+
* (PRO-2847). Significant digits rather than fixed decimals: an EPSS score of
|
|
2650
|
+
* 0.00043, a 0.4% failure rate or 99.995% uptime keep what they say, where
|
|
2651
|
+
* two decimals read them as 0, 0 and 100.
|
|
2652
|
+
*/
|
|
2653
|
+
const roundFloats = (_key, value) => typeof value === "number" && Number.isFinite(value) && !Number.isInteger(value) ? Number(value.toPrecision(6)) : value;
|
|
2654
|
+
/**
|
|
2655
|
+
* A result as the model reads it: compact JSON, in the product's words, its
|
|
2656
|
+
* floats rounded. Indentation was a fifth of a list's characters (PRO-2847),
|
|
2657
|
+
* paid on every call; the model reads JSON without it.
|
|
2658
|
+
*/
|
|
2659
|
+
function serializeResult(value) {
|
|
2660
|
+
return JSON.stringify(inProductWords(value), roundFloats);
|
|
2661
|
+
}
|
|
2513
2662
|
/** A successful tool result: one JSON text block the model reads. */
|
|
2514
2663
|
function toolResult(value) {
|
|
2515
2664
|
return { content: [{
|
|
2516
2665
|
type: "text",
|
|
2517
|
-
text:
|
|
2666
|
+
text: serializeResult(value)
|
|
2518
2667
|
}] };
|
|
2519
2668
|
}
|
|
2520
2669
|
/**
|
|
@@ -3855,6 +4004,34 @@ function hardeningSaid(blocking, named) {
|
|
|
3855
4004
|
const recorded = named.length > shown.length ? `, ${named.length} recorded in taiga_get_run` : "";
|
|
3856
4005
|
return `${count}: ${shown.join("; ")}${rest > 0 ? `; and ${rest} more${recorded}` : ""}.`;
|
|
3857
4006
|
}
|
|
4007
|
+
/** How each hardening verdict reads (PRO-2829): "passed" only when every scanner that applies ran. */
|
|
4008
|
+
const VERDICT_SAID = {
|
|
4009
|
+
passed: "Hardening passed: every scanner that applies ran and nothing blocks",
|
|
4010
|
+
passed_with_gaps: "Hardening passed, but not every scanner ran, so its checks are missing from the result",
|
|
4011
|
+
not_scanned: "No security scanner ran in hardening: \"passed\" only means no blocking finding was recorded",
|
|
4012
|
+
failed: "Hardening failed: blocking findings remain"
|
|
4013
|
+
};
|
|
4014
|
+
/**
|
|
4015
|
+
* What a run's hardening came to, coverage included (PRO-2829), from the
|
|
4016
|
+
* verdict the API reads off it, or read here from an API from before that.
|
|
4017
|
+
* Null without a hardening result.
|
|
4018
|
+
*/
|
|
4019
|
+
function scanSaid(hardening) {
|
|
4020
|
+
if (!hardening || typeof hardening !== "object") return null;
|
|
4021
|
+
const h = hardening;
|
|
4022
|
+
const coverage = typeof h.verdict === "string" && HARDENING_VERDICTS.includes(h.verdict) && Array.isArray(h.gaps) ? {
|
|
4023
|
+
verdict: h.verdict,
|
|
4024
|
+
gaps: h.gaps
|
|
4025
|
+
} : typeof h.passed === "boolean" && h.scanners ? hardeningCoverage(h) : null;
|
|
4026
|
+
if (!coverage) return null;
|
|
4027
|
+
const gaps = coverage.gaps.map((gap) => `${gap.scanner}${gap.status === "failed" ? " did not finish" : ""}${gap.reason ? ` (${gap.reason})` : ""}`);
|
|
4028
|
+
const missing = gaps.length > 0 ? `; did not run: ${gaps.join(", ")}` : "";
|
|
4029
|
+
return {
|
|
4030
|
+
verdict: coverage.verdict,
|
|
4031
|
+
gaps: gaps.length,
|
|
4032
|
+
said: `${VERDICT_SAID[coverage.verdict]}${missing}.`
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
3858
4035
|
/**
|
|
3859
4036
|
* What each failedReason means for the next move (PRO-2800): reviewers met dispatch_rejected and agent_reported as
|
|
3860
4037
|
* bare enums. As @taiga/types documents them.
|
|
@@ -3875,20 +4052,23 @@ const FAILED_REASON_MEANS = {
|
|
|
3875
4052
|
* text out: its first sentence is in the outcome, and taiga_get_run has it all.
|
|
3876
4053
|
*/
|
|
3877
4054
|
function readableRun(run, opts = {}) {
|
|
3878
|
-
const { prDraft, error, reviewEscalation, runtime: _runtime, ...rest } = run;
|
|
4055
|
+
const { prDraft, error, reviewEscalation, runtime: _runtime, origin, ...rest } = run;
|
|
3879
4056
|
const said = runOutcome(run);
|
|
3880
4057
|
const means = FAILED_REASON_MEANS[String(run["failedReason"])];
|
|
3881
4058
|
const hardening = run["hardeningResult"];
|
|
3882
4059
|
const blocking = typeof hardening?.blockingFindings === "number" ? hardening.blockingFindings : 0;
|
|
3883
4060
|
const named = Array.isArray(hardening?.blocking) ? hardening.blocking : [];
|
|
4061
|
+
const scan = scanSaid(hardening);
|
|
3884
4062
|
return {
|
|
3885
4063
|
...rest,
|
|
4064
|
+
...origin === void 0 ? {} : { origin: origin === "backlog" ? "initiative" : origin },
|
|
3886
4065
|
...means ? { failedReasonMeans: means } : {},
|
|
4066
|
+
...scan && scan.gaps + (scan.verdict === "not_scanned" ? 1 : 0) > 0 ? { scanCoverage: scan.said } : {},
|
|
3887
4067
|
...blocking > 0 ? { hardeningNote: hardeningSaid(blocking, named) } : {},
|
|
3888
4068
|
...opts.row || error === void 0 ? {} : { error },
|
|
3889
4069
|
...opts.row || reviewEscalation === void 0 ? {} : { reviewEscalation },
|
|
3890
4070
|
...typeof rest["prNumber"] === "number" || rest["prUrl"] ? { prDraft } : {},
|
|
3891
|
-
...said
|
|
4071
|
+
...said ? { outcome: said.outcome } : {}
|
|
3892
4072
|
};
|
|
3893
4073
|
}
|
|
3894
4074
|
/** Distinct initiatives a run list reads to say which moved on; past this a waiting row says it was not checked. */
|
|
@@ -3940,7 +4120,8 @@ const INITIATIVE_ENDED = {
|
|
|
3940
4120
|
/** A readable run row that also says when its initiative ended while its pull request waited. */
|
|
3941
4121
|
function withInitiativeState(row, states) {
|
|
3942
4122
|
const state = states.get(String(row["backlogItemId"]));
|
|
3943
|
-
|
|
4123
|
+
const status = String(row["status"]);
|
|
4124
|
+
if (!(BUILD_RUN_STATUSES.includes(status) && ["gated", "unmerged"].includes(landedDisposition(status, row["prMerged"] === true)))) return row;
|
|
3944
4125
|
if (state === "unchecked") return {
|
|
3945
4126
|
...row,
|
|
3946
4127
|
outcome: `${String(row["outcome"])} Whether its initiative still wants it was not checked.`
|
|
@@ -3951,7 +4132,6 @@ function withInitiativeState(row, states) {
|
|
|
3951
4132
|
const open = row["prState"] === "open" ? ` ${pr} is still open on the forge.` : "";
|
|
3952
4133
|
return {
|
|
3953
4134
|
...row,
|
|
3954
|
-
disposition: "abandoned",
|
|
3955
4135
|
actions: [],
|
|
3956
4136
|
outcome: `Its initiative ${ended}, so nobody is waiting on this run.${open}`
|
|
3957
4137
|
};
|
|
@@ -4060,19 +4240,20 @@ function registerBuildTools(server, client) {
|
|
|
4060
4240
|
if (!client.hasCapability("products:read")) return;
|
|
4061
4241
|
server.registerTool("taiga_list_runs", {
|
|
4062
4242
|
title: "List runs",
|
|
4063
|
-
description: "List coding agent runs, newest first: branch, pull request, step progress, outcome
|
|
4243
|
+
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
4244
|
inputSchema: z.object({
|
|
4065
4245
|
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
|
|
4246
|
+
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
4247
|
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
4248
|
...pagination
|
|
4069
4249
|
}),
|
|
4070
4250
|
annotations: READ_ONLY
|
|
4071
4251
|
}, (params) => runTool(async () => {
|
|
4072
|
-
if (
|
|
4073
|
-
if (params.productId && params.
|
|
4074
|
-
|
|
4075
|
-
|
|
4252
|
+
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.");
|
|
4253
|
+
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.");
|
|
4254
|
+
const productIdOf = params.productId ?? (params.initiativeId ? (await client.getBacklogItem(params.initiativeId)).data.productId : void 0);
|
|
4255
|
+
if (productIdOf) {
|
|
4256
|
+
const result = await client.listBuildRuns(productIdOf, {
|
|
4076
4257
|
backlogItemId: params.initiativeId,
|
|
4077
4258
|
limit: params.limit,
|
|
4078
4259
|
cursor: params.cursor
|
|
@@ -4085,7 +4266,7 @@ function registerBuildTools(server, client) {
|
|
|
4085
4266
|
}));
|
|
4086
4267
|
server.registerTool("taiga_get_run", {
|
|
4087
4268
|
title: "Get run",
|
|
4088
|
-
description: "Read one run with its steps:
|
|
4269
|
+
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
4270
|
inputSchema: z.object({
|
|
4090
4271
|
runId,
|
|
4091
4272
|
full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
|
|
@@ -4212,9 +4393,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4212
4393
|
if (item) {
|
|
4213
4394
|
Object.assign(row, {
|
|
4214
4395
|
title: item.title,
|
|
4215
|
-
|
|
4216
|
-
planningStatus: item.planningStatus,
|
|
4217
|
-
needsHumanCause: item.needsHumanCause,
|
|
4396
|
+
...standing(item),
|
|
4218
4397
|
criteria: criteriaCounts(item),
|
|
4219
4398
|
lifecycle: lifecycle ? {
|
|
4220
4399
|
turn: lifecycle.turn,
|
|
@@ -4246,7 +4425,7 @@ async function queueView(client, product, options = { name: "all" }) {
|
|
|
4246
4425
|
const line = typeof paused === "string" ? `The line is paused (since ${paused}): nothing is planned or built until a person resumes it.` : null;
|
|
4247
4426
|
return {
|
|
4248
4427
|
linePaused: paused === null ? null : typeof paused === "string",
|
|
4249
|
-
entries: rows,
|
|
4428
|
+
entries: rows.map(({ lifecycle: _lifecycle, runId: _runId, ...row }) => row),
|
|
4250
4429
|
waitingOn: line ?? waitingOn(rows),
|
|
4251
4430
|
...note ? { note } : {}
|
|
4252
4431
|
};
|
|
@@ -4292,18 +4471,21 @@ function registerQueueTools(server, client) {
|
|
|
4292
4471
|
}
|
|
4293
4472
|
//#endregion
|
|
4294
4473
|
//#region src/tools/backlogs.ts
|
|
4295
|
-
/**
|
|
4474
|
+
/**
|
|
4475
|
+
* What a list row needs to say for the model to pick an item to read in full.
|
|
4476
|
+
* One vocabulary for where an initiative stands (PRO-2828): its board column,
|
|
4477
|
+
* whose turn it is and the situation sentence. The API's own fields for it
|
|
4478
|
+
* (status, planningStatus, needsHumanCause, lifecycle) said the same thing
|
|
4479
|
+
* four more ways, and a reviewing agent had to reconcile them; they are in
|
|
4480
|
+
* taiga_get_initiative with full: true.
|
|
4481
|
+
*/
|
|
4296
4482
|
const BACKLOG_SUMMARY_FIELDS = [
|
|
4297
4483
|
"id",
|
|
4298
4484
|
"title",
|
|
4299
4485
|
"category",
|
|
4300
|
-
"status",
|
|
4301
|
-
"state",
|
|
4302
|
-
"planningStatus",
|
|
4303
4486
|
"priority",
|
|
4304
4487
|
"estimate",
|
|
4305
4488
|
"origin",
|
|
4306
|
-
"needsHumanCause",
|
|
4307
4489
|
"archivedAt",
|
|
4308
4490
|
"createdAt",
|
|
4309
4491
|
"updatedAt"
|
|
@@ -4317,21 +4499,44 @@ const ITEM_OUTCOME_FIELDS = [
|
|
|
4317
4499
|
"id",
|
|
4318
4500
|
"productId",
|
|
4319
4501
|
"title",
|
|
4320
|
-
"status",
|
|
4321
|
-
"state",
|
|
4322
|
-
"planningStatus",
|
|
4323
|
-
"needsHumanCause",
|
|
4324
|
-
"needsHumanDetail",
|
|
4325
4502
|
"autoBuild",
|
|
4326
|
-
"archivedAt"
|
|
4327
|
-
"lifecycle"
|
|
4503
|
+
"archivedAt"
|
|
4328
4504
|
];
|
|
4505
|
+
/** The fields of an initiative's summary read: its row, plus what it is about. */
|
|
4506
|
+
const ITEM_SUMMARY_FIELDS = [
|
|
4507
|
+
...BACKLOG_SUMMARY_FIELDS,
|
|
4508
|
+
"productId",
|
|
4509
|
+
"description",
|
|
4510
|
+
"autoBuild",
|
|
4511
|
+
"dependencies"
|
|
4512
|
+
];
|
|
4513
|
+
/** How much of a description a summary read carries; full: true has the rest. */
|
|
4514
|
+
const DESCRIPTION_IN_SUMMARY = 600;
|
|
4515
|
+
/**
|
|
4516
|
+
* Where an initiative stands, in the one vocabulary every read uses
|
|
4517
|
+
* (PRO-2828): its board column, whose turn it is, the situation sentence, and
|
|
4518
|
+
* its latest run to open with taiga_get_run.
|
|
4519
|
+
*/
|
|
4520
|
+
function standing(item) {
|
|
4521
|
+
const life = item["lifecycle"];
|
|
4522
|
+
const run = life?.latestBuild;
|
|
4523
|
+
const situation = situationOf(item);
|
|
4524
|
+
return {
|
|
4525
|
+
column: typeof item["state"] === "string" ? item["state"] : null,
|
|
4526
|
+
turn: typeof life?.turn === "string" ? life.turn : null,
|
|
4527
|
+
...situation ? { situation } : {},
|
|
4528
|
+
latestRun: run && typeof run.id === "string" ? {
|
|
4529
|
+
id: run.id,
|
|
4530
|
+
...typeof run.prNumber === "number" ? { prNumber: run.prNumber } : {}
|
|
4531
|
+
} : null
|
|
4532
|
+
};
|
|
4533
|
+
}
|
|
4329
4534
|
/**
|
|
4330
4535
|
* What the lifecycle on an initiative says, and the call each next step is.
|
|
4331
4536
|
* needsHumanCause is only the line's flag; a plan that failed outside the line
|
|
4332
4537
|
* leaves it null, and the lifecycle is what says why and what moves it on.
|
|
4333
4538
|
*/
|
|
4334
|
-
const LIFECYCLE_MEANING = "situation says in a sentence where it stands and
|
|
4539
|
+
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
4540
|
/** The move each call to action is, in the tools' words. */
|
|
4336
4541
|
const CTA_MOVE = {
|
|
4337
4542
|
start_plan: "taiga_queue_initiative puts it on the line to be planned and built (paid work: ask first)",
|
|
@@ -4344,8 +4549,8 @@ const CTA_MOVE = {
|
|
|
4344
4549
|
};
|
|
4345
4550
|
/** 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
4551
|
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
|
|
4552
|
+
/** What happens to a queued initiative: the line takes it up when its turn comes, replanning a run that did not land (PRO-2840). */
|
|
4553
|
+
const REQUEUED_MOVE = "the line takes it up when its turn comes";
|
|
4349
4554
|
/** How a run stands, as the end of "its latest run ...", by disposition. */
|
|
4350
4555
|
const RUN_STANDS = {
|
|
4351
4556
|
in_flight: "is running",
|
|
@@ -4382,7 +4587,10 @@ function situationOf(item) {
|
|
|
4382
4587
|
const column = typeof item["state"] === "string" ? item["state"] : null;
|
|
4383
4588
|
const offTheLine = column !== null && !LINE_STATES.includes(column);
|
|
4384
4589
|
const stalled = life.stage === "building" && life.turn !== "agent";
|
|
4385
|
-
const requeued =
|
|
4590
|
+
const requeued = column === "queue" && (stalled || life.turn === "idle" && life.cta == null);
|
|
4591
|
+
const offTheLineStart = offTheLine && (life.cta === "start_build" || life.cta === "start_plan");
|
|
4592
|
+
if (requeued && !stalled) stage = life.stage === "backlog" ? "Queued" : `Queued, ${stage.charAt(0).toLowerCase()}${stage.slice(1)}`;
|
|
4593
|
+
else if (offTheLineStart && life.stage === "planned") stage = "Planned, off the line";
|
|
4386
4594
|
if (stalled) stage = requeued ? "Queued" : offTheLine ? "Off the line" : column === null ? "Its build stopped" : "On the line, but its build stopped";
|
|
4387
4595
|
const build = life.latestBuild;
|
|
4388
4596
|
if (build && typeof build.status === "string" && life.stage !== "done" && life.stage !== "canceled") {
|
|
@@ -4403,7 +4611,7 @@ function situationOf(item) {
|
|
|
4403
4611
|
}
|
|
4404
4612
|
const turn = life.turn === "you" ? "A person's turn" : life.turn === "agent" ? "The agent's turn" : null;
|
|
4405
4613
|
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;
|
|
4614
|
+
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
4615
|
if (requeued) parts.push(`Nothing to do: ${REQUEUED_MOVE}.`);
|
|
4408
4616
|
else if (life.cta === "view_pr") parts.push(`Nothing to do: ${move}.`);
|
|
4409
4617
|
else if (turn && move) parts.push(`${turn}: ${move}.`);
|
|
@@ -4431,15 +4639,14 @@ function endingSaid(item) {
|
|
|
4431
4639
|
if (next && typeof next.id === "string") said.push(`; superseded by "${String(next.title)}" (${next.id})`);
|
|
4432
4640
|
return said.join("");
|
|
4433
4641
|
}
|
|
4434
|
-
/** A row or item with
|
|
4642
|
+
/** A row or item with where it stands added (`standing`). */
|
|
4435
4643
|
function withSituation(item, row = item) {
|
|
4436
|
-
|
|
4437
|
-
return situation ? {
|
|
4644
|
+
return {
|
|
4438
4645
|
...row,
|
|
4439
|
-
|
|
4440
|
-
}
|
|
4646
|
+
...standing(item)
|
|
4647
|
+
};
|
|
4441
4648
|
}
|
|
4442
|
-
/** List rows, each with
|
|
4649
|
+
/** List rows, each with where it stands. */
|
|
4443
4650
|
function situatedRows(items) {
|
|
4444
4651
|
return itemRows(items, BACKLOG_SUMMARY_FIELDS).map((row, index) => withSituation(items[index], row));
|
|
4445
4652
|
}
|
|
@@ -4516,40 +4723,6 @@ const outcomeOf = (item) => withSituation(item, {
|
|
|
4516
4723
|
criteria: criteriaCounts(item)
|
|
4517
4724
|
});
|
|
4518
4725
|
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
4726
|
/**
|
|
4554
4727
|
* Each tool is registered under the capability its route requires, so a
|
|
4555
4728
|
* key sees exactly what it may call: reads on backlog:read; the board move on
|
|
@@ -4582,25 +4755,15 @@ function registerBacklogTools(server, client) {
|
|
|
4582
4755
|
function registerBacklogReads(server, client) {
|
|
4583
4756
|
server.registerTool("taiga_list_initiatives", {
|
|
4584
4757
|
title: "List initiatives",
|
|
4585
|
-
description: "List a product's open
|
|
4758
|
+
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
4759
|
inputSchema: z.object({
|
|
4587
4760
|
productId,
|
|
4588
4761
|
query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
|
|
4589
|
-
|
|
4762
|
+
column: z.enum([
|
|
4590
4763
|
"open",
|
|
4591
4764
|
"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)"),
|
|
4765
|
+
...INITIATIVE_STATES
|
|
4766
|
+
]).optional().describe("open (default): not done or canceled; all: every one; or one column of the board"),
|
|
4604
4767
|
category: z.enum([
|
|
4605
4768
|
"feature",
|
|
4606
4769
|
"bug",
|
|
@@ -4612,56 +4775,66 @@ function registerBacklogReads(server, client) {
|
|
|
4612
4775
|
}),
|
|
4613
4776
|
annotations: READ_ONLY
|
|
4614
4777
|
}, (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(", ")}).`);
|
|
4778
|
+
const { query, column = "open", productId: id, ...filters } = params;
|
|
4617
4779
|
const apiFilters = {
|
|
4618
4780
|
...filters,
|
|
4619
|
-
|
|
4781
|
+
order: "newest",
|
|
4782
|
+
...column === "open" ? { open: true } : column === "all" ? {} : { state: column }
|
|
4620
4783
|
};
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
const result = await client.listBacklogItems(params.productId, apiFilters);
|
|
4784
|
+
if (query === void 0) {
|
|
4785
|
+
const result = await client.listBacklogItems(id, apiFilters);
|
|
4624
4786
|
return toolResult(page(situatedRows(result.data), result.nextCursor));
|
|
4625
4787
|
}
|
|
4626
|
-
if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", "
|
|
4788
|
+
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
4789
|
let closedSeen = 0;
|
|
4628
|
-
const needle =
|
|
4629
|
-
const found = await searchPages((cursor) => client.listBacklogItems(
|
|
4790
|
+
const needle = query.toLowerCase();
|
|
4791
|
+
const found = await searchPages((cursor) => client.listBacklogItems(id, {
|
|
4630
4792
|
...apiFilters,
|
|
4793
|
+
...column === "open" ? { open: void 0 } : {},
|
|
4631
4794
|
limit: 100,
|
|
4632
4795
|
cursor
|
|
4633
|
-
}), query
|
|
4634
|
-
if (
|
|
4635
|
-
if (
|
|
4636
|
-
if (query !== void 0 && String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
|
|
4796
|
+
}), query, (row) => [row.title], params.limit ?? 25, (row) => {
|
|
4797
|
+
if (column !== "open" || !["done", "canceled"].includes(String(row.state))) return true;
|
|
4798
|
+
if (String(row.title).toLowerCase().includes(needle)) closedSeen += 1;
|
|
4637
4799
|
return false;
|
|
4638
4800
|
});
|
|
4639
|
-
const hidden =
|
|
4801
|
+
const hidden = column === "open" && !found.more ? closedSeen : 0;
|
|
4640
4802
|
return toolResult({
|
|
4641
4803
|
...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
|
|
4804
|
+
...hidden > 0 ? { closedMatches: `${hidden} done or canceled initiative${hidden === 1 ? " also matches" : "s also match"}; column: 'all' lists them.` } : {},
|
|
4805
|
+
...found.more ? { more: "There may be more than the limit carries: narrow the title, or pick one column." } : {}
|
|
4644
4806
|
});
|
|
4645
4807
|
}));
|
|
4646
4808
|
server.registerTool("taiga_get_initiative", {
|
|
4647
4809
|
title: "Get initiative",
|
|
4648
|
-
description: `Read one initiative
|
|
4649
|
-
inputSchema: z.object({
|
|
4810
|
+
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}`,
|
|
4811
|
+
inputSchema: z.object({
|
|
4812
|
+
initiativeId,
|
|
4813
|
+
full: z.boolean().optional().describe("Read it whole: every criterion, technical context and the raw state fields (default false)")
|
|
4814
|
+
}),
|
|
4650
4815
|
annotations: READ_ONLY
|
|
4651
4816
|
}, (params) => runTool(async () => {
|
|
4652
4817
|
const item = (await client.getBacklogItem(params.initiativeId)).data;
|
|
4653
4818
|
const row = item;
|
|
4654
|
-
|
|
4655
|
-
const
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
...
|
|
4659
|
-
|
|
4660
|
-
|
|
4819
|
+
const planFailure = item.planningStatus === "failed" ? await planFailureOf(client, params.initiativeId, item.lifecycle?.planFailureCause) : null;
|
|
4820
|
+
const failure = planFailure ? { planFailure } : {};
|
|
4821
|
+
if (params.full) return toolResult({
|
|
4822
|
+
...withSituation(row),
|
|
4823
|
+
...failure
|
|
4824
|
+
});
|
|
4825
|
+
const summary = itemRows([item], ITEM_SUMMARY_FIELDS)[0];
|
|
4826
|
+
const description = String(summary["description"] ?? "");
|
|
4827
|
+
return toolResult({
|
|
4828
|
+
...withSituation(row, {
|
|
4829
|
+
...summary,
|
|
4830
|
+
description: description.length > DESCRIPTION_IN_SUMMARY ? `${description.slice(0, DESCRIPTION_IN_SUMMARY).trimEnd()}... (full: true has the rest)` : description
|
|
4831
|
+
}),
|
|
4832
|
+
...failure
|
|
4833
|
+
});
|
|
4661
4834
|
}));
|
|
4662
4835
|
server.registerTool("taiga_get_plan", {
|
|
4663
4836
|
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;
|
|
4837
|
+
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
4838
|
inputSchema: z.object({
|
|
4666
4839
|
initiativeId,
|
|
4667
4840
|
full: z.boolean().optional().describe("Carry each step's complete instructions (default false: a summary)")
|
|
@@ -4824,7 +4997,7 @@ function registerReviewGate(server, client) {
|
|
|
4824
4997
|
function registerResume(server, client) {
|
|
4825
4998
|
server.registerTool("taiga_resume_initiative", {
|
|
4826
4999
|
title: "Resume initiative",
|
|
4827
|
-
description: "Carry on an initiative the line stopped on (
|
|
5000
|
+
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
5001
|
inputSchema: z.object({ initiativeId }),
|
|
4829
5002
|
annotations: OPERATE
|
|
4830
5003
|
}, (params) => runTool(async () => outcome((await client.resumeInitiative(params.initiativeId)).data)));
|
|
@@ -4892,6 +5065,40 @@ function withFixability(row) {
|
|
|
4892
5065
|
};
|
|
4893
5066
|
return row;
|
|
4894
5067
|
}
|
|
5068
|
+
/**
|
|
5069
|
+
* Dev-only, runtime or unknown, said on a dependency row (PRO-2861) in place of the raw per-lockfile groups
|
|
5070
|
+
* the API carries: the groups need the lockfile name to read, which `dependencyScope` does once here.
|
|
5071
|
+
*/
|
|
5072
|
+
function withDependencyScope(row, { keepGroups = false } = {}) {
|
|
5073
|
+
const { dependencyGroups: _groups, dependencyRelation: _relation, ...lean } = row;
|
|
5074
|
+
const rest = keepGroups ? {
|
|
5075
|
+
...row,
|
|
5076
|
+
dependencyRelation: void 0
|
|
5077
|
+
} : lean;
|
|
5078
|
+
if (!(row["kind"] === "vulnerable_dependency" || row["kind"] === "outdated_dependency")) return rest;
|
|
5079
|
+
const kind = row["kind"];
|
|
5080
|
+
return {
|
|
5081
|
+
...rest,
|
|
5082
|
+
dependencyScope: dependencyScope({
|
|
5083
|
+
kind,
|
|
5084
|
+
dependencyGroups: row["dependencyGroups"]
|
|
5085
|
+
}),
|
|
5086
|
+
dependencyRelation: dependencyRelation({
|
|
5087
|
+
kind,
|
|
5088
|
+
dependencyRelation: row["dependencyRelation"]
|
|
5089
|
+
}),
|
|
5090
|
+
...keepGroups ? { dependencyRelationByLockfile: row["dependencyRelation"] ?? null } : {}
|
|
5091
|
+
};
|
|
5092
|
+
}
|
|
5093
|
+
/** A finding as a tool answers it: the row fields, the scope, and whether Fix would take it. */
|
|
5094
|
+
function findingRows(findings, extra = []) {
|
|
5095
|
+
return pickFields(findings, [
|
|
5096
|
+
...FINDING_ROW_FIELDS,
|
|
5097
|
+
"dependencyGroups",
|
|
5098
|
+
"dependencyRelation",
|
|
5099
|
+
...extra
|
|
5100
|
+
]).map((row) => withDependencyScope(row)).map(withFixability);
|
|
5101
|
+
}
|
|
4895
5102
|
/** The parts of an advisory record an agent reads; the rest (every affected range, every reference) is full: true. */
|
|
4896
5103
|
const ADVISORY_KEPT = [
|
|
4897
5104
|
"id",
|
|
@@ -4970,10 +5177,13 @@ function standingSummary(standing) {
|
|
|
4970
5177
|
const severity = String(violation["severity"] ?? "unknown");
|
|
4971
5178
|
bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
|
|
4972
5179
|
}
|
|
5180
|
+
const stated = (state) => controls.filter((c) => c["state"] === state).length;
|
|
4973
5181
|
return {
|
|
4974
5182
|
controls: listed.length,
|
|
4975
5183
|
controlsInPolicies: controls.length,
|
|
4976
|
-
|
|
5184
|
+
controlsMet: stated("met"),
|
|
5185
|
+
controlsNotApplicable: stated("not_applicable"),
|
|
5186
|
+
controlsNotAssessed: stated("no_evidence"),
|
|
4977
5187
|
violations: violations.length,
|
|
4978
5188
|
bySeverity,
|
|
4979
5189
|
resolvedViolations: resolved
|
|
@@ -5015,7 +5225,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5015
5225
|
sweep: null,
|
|
5016
5226
|
sweepNote: "The sweep overview could not be read: an empty list here is not an all-clear."
|
|
5017
5227
|
} : sweep ? { sweep } : {},
|
|
5018
|
-
...page(
|
|
5228
|
+
...page(findingRows(result.data), result.nextCursor),
|
|
5019
5229
|
...typeof result.totalCount === "number" ? { totalCount: result.totalCount } : {},
|
|
5020
5230
|
...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
5231
|
});
|
|
@@ -5030,10 +5240,10 @@ function registerMaintenanceTools(server, client) {
|
|
|
5030
5240
|
annotations: READ_ONLY
|
|
5031
5241
|
}, (params) => runTool(async () => {
|
|
5032
5242
|
const { runId, ...raw } = (await client.getFinding(params.findingId)).data;
|
|
5033
|
-
const finding = withFixability(params.full ? raw : {
|
|
5243
|
+
const finding = withFixability(params.full ? withDependencyScope(raw, { keepGroups: true }) : withDependencyScope({
|
|
5034
5244
|
...raw,
|
|
5035
5245
|
data: leanAdvisory(raw["data"])
|
|
5036
|
-
});
|
|
5246
|
+
}));
|
|
5037
5247
|
return toolResult(runId === void 0 ? finding : {
|
|
5038
5248
|
sweepId: runId,
|
|
5039
5249
|
...finding
|
|
@@ -5041,11 +5251,12 @@ function registerMaintenanceTools(server, client) {
|
|
|
5041
5251
|
}));
|
|
5042
5252
|
server.registerTool("taiga_get_policy_standing", {
|
|
5043
5253
|
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).
|
|
5254
|
+
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
5255
|
inputSchema: z.object({
|
|
5046
5256
|
productId,
|
|
5047
5257
|
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")
|
|
5258
|
+
violationId: idInput("violation", "this tool lists them").optional().describe("Only this violation, in full and uncut: its evidence, policy excerpt and work"),
|
|
5259
|
+
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
5260
|
}),
|
|
5050
5261
|
annotations: READ_ONLY
|
|
5051
5262
|
}, (params) => runTool(async () => {
|
|
@@ -5057,6 +5268,20 @@ function registerMaintenanceTools(server, client) {
|
|
|
5057
5268
|
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
5269
|
const resolvedAny = Array.isArray(standing.resolvedDivergences) && standing.resolvedDivergences.length > 0;
|
|
5059
5270
|
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." } : {};
|
|
5271
|
+
if (params.controlId && params.violationId) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both.");
|
|
5272
|
+
if (params.controlId) {
|
|
5273
|
+
const wanted = params.controlId.toUpperCase();
|
|
5274
|
+
const control = controls.find((c) => String(c.controlId).toUpperCase() === wanted);
|
|
5275
|
+
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.");
|
|
5276
|
+
const own = violations.filter((violation) => String(violation["controlId"]).toUpperCase() === wanted);
|
|
5277
|
+
return toolResult({
|
|
5278
|
+
...control,
|
|
5279
|
+
...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." } : {},
|
|
5280
|
+
violations: pickFields(own, VIOLATION_FIELDS),
|
|
5281
|
+
...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.` } : {},
|
|
5282
|
+
...own.length > 0 ? wording : {}
|
|
5283
|
+
});
|
|
5284
|
+
}
|
|
5060
5285
|
if (params.violationId) {
|
|
5061
5286
|
const one = [...violations, ...Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences : []].find((violation) => violation["id"] === params.violationId);
|
|
5062
5287
|
if (!one) return toolError("NOT_FOUND", "No violation with that ID on this product; this tool without violationId lists them.");
|
|
@@ -5120,7 +5345,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5120
5345
|
...params.expiresAt !== void 0 ? { ignoreExpiresAt: params.expiresAt } : {}
|
|
5121
5346
|
} : { status: "open" };
|
|
5122
5347
|
const finding = (await client.updateFinding(params.findingId, body)).data;
|
|
5123
|
-
return toolResult(
|
|
5348
|
+
return toolResult(findingRows([finding], ["ignoreNote"])[0]);
|
|
5124
5349
|
}));
|
|
5125
5350
|
server.registerTool("taiga_fix_finding", {
|
|
5126
5351
|
title: "Fix a finding",
|
|
@@ -5136,7 +5361,7 @@ function registerMaintenanceTools(server, client) {
|
|
|
5136
5361
|
const fixed = (await client.remediateFinding(params.productId, params.findingId)).data;
|
|
5137
5362
|
return toolResult({
|
|
5138
5363
|
backlogItemId: fixed.backlogItemId,
|
|
5139
|
-
finding: fixed.finding ?
|
|
5364
|
+
finding: fixed.finding ? findingRows([fixed.finding])[0] : null
|
|
5140
5365
|
});
|
|
5141
5366
|
}
|
|
5142
5367
|
const finding = (await client.getFinding(params.findingId)).data;
|
|
@@ -5215,6 +5440,28 @@ function readWith(row, backlogRead) {
|
|
|
5215
5440
|
default: return;
|
|
5216
5441
|
}
|
|
5217
5442
|
}
|
|
5443
|
+
/**
|
|
5444
|
+
* The glossary's words for what the API names after its tables (PRO-2823): a reviewer met kind review_backlog,
|
|
5445
|
+
* subjectType backlog_item and inbox_task beside a glossary that says initiative and run, and had to infer they
|
|
5446
|
+
* were the same things.
|
|
5447
|
+
*/
|
|
5448
|
+
const KIND_SAID = { review_backlog: "review_initiatives" };
|
|
5449
|
+
const SUBJECT_SAID = {
|
|
5450
|
+
backlog_item: "initiative",
|
|
5451
|
+
build_run: "run",
|
|
5452
|
+
inbox_task: "setup_task"
|
|
5453
|
+
};
|
|
5454
|
+
const kindSaid = (kind) => KIND_SAID[String(kind)] ?? kind;
|
|
5455
|
+
/** A setup task row in the glossary's words: the Action Required item it is, by the name taiga_resolve_action takes. */
|
|
5456
|
+
function setupTaskRow(row) {
|
|
5457
|
+
const { inboxItemId, inboxItemStatus, alsoFiledAs, ...rest } = row;
|
|
5458
|
+
return {
|
|
5459
|
+
...rest,
|
|
5460
|
+
...inboxItemId !== void 0 ? { actionId: inboxItemId } : {},
|
|
5461
|
+
...inboxItemStatus !== void 0 ? { actionStatus: inboxItemStatus } : {},
|
|
5462
|
+
...Array.isArray(alsoFiledAs) ? { alsoFiledAs: alsoFiledAs.map(setupTaskRow) } : {}
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5218
5465
|
/** A summary past its first sentence: review_doc rows quoted two documents, 800 to 1,200 characters each. */
|
|
5219
5466
|
const SUMMARY_LIMIT = 200;
|
|
5220
5467
|
function firstSentence(text) {
|
|
@@ -5232,9 +5479,12 @@ function itemRow(row, now, full, backlogRead) {
|
|
|
5232
5479
|
const created = typeof row["createdAt"] === "string" ? Date.parse(row["createdAt"]) : NaN;
|
|
5233
5480
|
const { summary, status, blocking, ...lean } = rest;
|
|
5234
5481
|
const read = readWith(row, backlogRead);
|
|
5482
|
+
const saysNothing = row["kind"] === "setup_manual" && String(summary).trim().toLowerCase() === "manual config";
|
|
5235
5483
|
return {
|
|
5236
5484
|
...lean,
|
|
5237
|
-
|
|
5485
|
+
kind: kindSaid(lean["kind"]),
|
|
5486
|
+
...typeof lean["subjectType"] === "string" ? { subjectType: SUBJECT_SAID[lean["subjectType"]] ?? lean["subjectType"] } : {},
|
|
5487
|
+
...saysNothing ? {} : full ? { summary } : row["kind"] === "review_pr" ? {} : { summary: firstSentence(summary) },
|
|
5238
5488
|
...full || status !== "open" ? { status } : {},
|
|
5239
5489
|
...full || blocking === true ? { blocking } : {},
|
|
5240
5490
|
...read ? { readWith: read } : {},
|
|
@@ -5266,7 +5516,8 @@ function summaryOf(rows, stale) {
|
|
|
5266
5516
|
if (stale.has(String(row["id"]))) entry.stale = (entry.stale ?? 0) + 1;
|
|
5267
5517
|
else {
|
|
5268
5518
|
entry.open += 1;
|
|
5269
|
-
|
|
5519
|
+
const kind = String(kindSaid(row["kind"]));
|
|
5520
|
+
entry.byKind[kind] = (entry.byKind[kind] ?? 0) + 1;
|
|
5270
5521
|
}
|
|
5271
5522
|
byProduct.set(key, entry);
|
|
5272
5523
|
}
|
|
@@ -5322,15 +5573,10 @@ async function staleBacklogReviews(client, rows, out) {
|
|
|
5322
5573
|
const empty = /* @__PURE__ */ new Set();
|
|
5323
5574
|
await Promise.all(checked.map(async (productId) => {
|
|
5324
5575
|
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
|
-
}
|
|
5576
|
+
if ((await Promise.all(["backlog", "todo"].map((state) => client.listBacklogItems(productId, {
|
|
5577
|
+
state,
|
|
5578
|
+
limit: 1
|
|
5579
|
+
})))).some((page) => page.data.length > 0)) return;
|
|
5334
5580
|
empty.add(productId);
|
|
5335
5581
|
} catch (error) {
|
|
5336
5582
|
if (!(error instanceof TaigaApiError || isNetworkFailure(error))) throw error;
|
|
@@ -5413,6 +5659,7 @@ function sameKeyOnce(rows) {
|
|
|
5413
5659
|
id: from["id"],
|
|
5414
5660
|
category: from["category"],
|
|
5415
5661
|
title: from["title"],
|
|
5662
|
+
inboxItemId: from["inboxItemId"],
|
|
5416
5663
|
inboxItemStatus: from["inboxItemStatus"]
|
|
5417
5664
|
});
|
|
5418
5665
|
if (!isOpen(kept) && isOpen(row)) {
|
|
@@ -5512,7 +5759,7 @@ function registerInboxTools(server, client) {
|
|
|
5512
5759
|
...params.cursor === void 0 ? { withTotal: true } : {}
|
|
5513
5760
|
});
|
|
5514
5761
|
return toolResult({
|
|
5515
|
-
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)), result.nextCursor, result.data.length),
|
|
5762
|
+
...folded(sameKeyOnce(pickFields(result.data, SETUP_TASK_FIELDS)).map(setupTaskRow), result.nextCursor, result.data.length),
|
|
5516
5763
|
...totalOf(result)
|
|
5517
5764
|
});
|
|
5518
5765
|
}));
|
|
@@ -5765,42 +6012,31 @@ async function policiesPart(client) {
|
|
|
5765
6012
|
sections: Object.keys(sectionsOf(row["data"] ?? {}) ?? {}).length
|
|
5766
6013
|
}));
|
|
5767
6014
|
}
|
|
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
6015
|
/**
|
|
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.
|
|
6016
|
+
* The items still open, newest first (PRO-2843): one read of the open ones,
|
|
6017
|
+
* paged up to a cap the result reports. The API leaves archived items out.
|
|
5777
6018
|
*/
|
|
5778
6019
|
async function backlogPart(client, productId) {
|
|
5779
|
-
const
|
|
6020
|
+
const rows = [];
|
|
5780
6021
|
let truncated = false;
|
|
5781
6022
|
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;
|
|
6023
|
+
let cursor;
|
|
6024
|
+
for (let pageIndex = 0;; pageIndex += 1) {
|
|
6025
|
+
if (pageIndex === BACKLOG_PAGE_LIMIT) {
|
|
6026
|
+
truncated = true;
|
|
6027
|
+
pageCapped = true;
|
|
6028
|
+
break;
|
|
5801
6029
|
}
|
|
6030
|
+
const result = await client.listBacklogItems(productId, {
|
|
6031
|
+
open: true,
|
|
6032
|
+
order: "newest",
|
|
6033
|
+
limit: 100,
|
|
6034
|
+
cursor
|
|
6035
|
+
});
|
|
6036
|
+
rows.push(...result.data);
|
|
6037
|
+
if (!result.nextCursor) break;
|
|
6038
|
+
cursor = result.nextCursor;
|
|
5802
6039
|
}
|
|
5803
|
-
const rows = [...open.values()].sort((a, b) => String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0);
|
|
5804
6040
|
if (rows.length > BACKLOG_IN_CONTEXT) truncated = true;
|
|
5805
6041
|
const byColumn = {
|
|
5806
6042
|
backlog: 0,
|
|
@@ -5812,9 +6048,9 @@ async function backlogPart(client, productId) {
|
|
|
5812
6048
|
for (const row of rows) if (row.state) byColumn[row.state] = (byColumn[row.state] ?? 0) + 1;
|
|
5813
6049
|
return {
|
|
5814
6050
|
byColumn,
|
|
5815
|
-
items: situatedRows(rows.slice(
|
|
6051
|
+
items: situatedRows(rows.slice(0, BACKLOG_IN_CONTEXT)),
|
|
5816
6052
|
truncated,
|
|
5817
|
-
...pageCapped ? { note: "More than 1000 open items
|
|
6053
|
+
...pageCapped ? { note: "More than 1000 open items: byColumn counts the newest 1000; taiga_list_initiatives pages the rest." } : {}
|
|
5818
6054
|
};
|
|
5819
6055
|
}
|
|
5820
6056
|
/**
|
|
@@ -5839,7 +6075,7 @@ const CONTEXT_RUN_SAID = [
|
|
|
5839
6075
|
"runNumber",
|
|
5840
6076
|
"backlogItemId",
|
|
5841
6077
|
"targetTitle",
|
|
5842
|
-
"
|
|
6078
|
+
"status",
|
|
5843
6079
|
"outcome",
|
|
5844
6080
|
"failedReasonMeans",
|
|
5845
6081
|
"hardeningNote",
|
|
@@ -5925,24 +6161,24 @@ async function productContext(client, product, options) {
|
|
|
5925
6161
|
unavailable,
|
|
5926
6162
|
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
6163
|
};
|
|
5928
|
-
let characters =
|
|
6164
|
+
let characters = serializeResult(result).length;
|
|
5929
6165
|
for (let measured = -1; measured !== characters;) {
|
|
5930
6166
|
measured = characters;
|
|
5931
|
-
characters =
|
|
6167
|
+
characters = serializeResult({
|
|
5932
6168
|
...result,
|
|
5933
6169
|
characters: measured
|
|
5934
|
-
})
|
|
6170
|
+
}).length;
|
|
5935
6171
|
}
|
|
5936
6172
|
return toolResult({
|
|
5937
|
-
|
|
5938
|
-
|
|
6173
|
+
characters,
|
|
6174
|
+
...result
|
|
5939
6175
|
});
|
|
5940
6176
|
}
|
|
5941
6177
|
function registerContextTools(server, client) {
|
|
5942
6178
|
if (!client.hasCapability("products:read")) return;
|
|
5943
6179
|
server.registerTool("taiga_get_product_context", {
|
|
5944
6180
|
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
|
|
6181
|
+
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
6182
|
inputSchema: z.object({
|
|
5947
6183
|
productId: productId.optional(),
|
|
5948
6184
|
slug: slug.optional(),
|
|
@@ -6313,12 +6549,13 @@ function registerObserveTools(server, client) {
|
|
|
6313
6549
|
]);
|
|
6314
6550
|
const now = current.data;
|
|
6315
6551
|
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;
|
|
6552
|
+
const live = new Set([...now.current ?? [], ...now.deploying ?? []].map((row) => row.id));
|
|
6316
6553
|
return toolResult({
|
|
6317
6554
|
environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
|
|
6318
6555
|
...environments.more ? { moreEnvironments: true } : {},
|
|
6319
6556
|
running: deploymentLines(now.current ?? []),
|
|
6320
6557
|
deploying: deploymentLines(now.deploying ?? []),
|
|
6321
|
-
history: page(deploymentLines(history.data), history.nextCursor),
|
|
6558
|
+
history: page(deploymentLines(history.data.filter((row) => !live.has(row.id))), history.nextCursor),
|
|
6322
6559
|
..."value" in sync ? { syncStatus: sync.value.data } : {
|
|
6323
6560
|
syncStatus: null,
|
|
6324
6561
|
syncUnavailable: sync.unavailable
|
|
@@ -22019,29 +22256,19 @@ function judgeItem(until, gate) {
|
|
|
22019
22256
|
/** The initiative as a wait reports it: where it is, whose turn, its latest run and its criteria. */
|
|
22020
22257
|
function itemReport(item) {
|
|
22021
22258
|
const run = latestBuildOf(item);
|
|
22022
|
-
const situation = situationOf(item);
|
|
22023
22259
|
return {
|
|
22024
|
-
...situation ? { situation } : {},
|
|
22025
22260
|
...pickFields([item], [
|
|
22026
22261
|
"id",
|
|
22027
22262
|
"title",
|
|
22028
|
-
"state",
|
|
22029
|
-
"planningStatus",
|
|
22030
|
-
"needsHumanCause",
|
|
22031
|
-
"needsHumanDetail",
|
|
22032
22263
|
"autoBuild"
|
|
22033
22264
|
])[0],
|
|
22034
|
-
|
|
22035
|
-
|
|
22036
|
-
"stage",
|
|
22037
|
-
"cta",
|
|
22038
|
-
"planFailureCause"
|
|
22039
|
-
])[0],
|
|
22040
|
-
latestBuild: run ? pickFields([run], [
|
|
22265
|
+
...standing(item),
|
|
22266
|
+
latestRun: run ? pickFields([run], [
|
|
22041
22267
|
"id",
|
|
22042
22268
|
"status",
|
|
22043
22269
|
"completedSteps",
|
|
22044
22270
|
"totalSteps",
|
|
22271
|
+
"prNumber",
|
|
22045
22272
|
"prUrl",
|
|
22046
22273
|
"prState",
|
|
22047
22274
|
"prMerged",
|
|
@@ -22225,6 +22452,10 @@ function withRecoveryHints(tool, schema, inputs, isRead) {
|
|
|
22225
22452
|
message: `${withStop(issue.message)} ${pointers.join(" ")}`
|
|
22226
22453
|
};
|
|
22227
22454
|
}
|
|
22455
|
+
if (issue.code === "invalid_value") return {
|
|
22456
|
+
...issue,
|
|
22457
|
+
message: `${withStop(issue.message)} Pass one of those values exactly.`
|
|
22458
|
+
};
|
|
22228
22459
|
const argument = issue.path?.length === 1 ? String(issue.path[0]) : void 0;
|
|
22229
22460
|
const source = argument ? idSource(argument) : void 0;
|
|
22230
22461
|
if (source && /received undefined/.test(issue.message)) return {
|
|
@@ -22373,7 +22604,7 @@ function registerAllTools(server, client, options = {}) {
|
|
|
22373
22604
|
function registerWhoami(server, client, registered) {
|
|
22374
22605
|
server.registerTool("taiga_whoami", {
|
|
22375
22606
|
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.",
|
|
22607
|
+
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
22608
|
inputSchema: z.object({}),
|
|
22378
22609
|
annotations: READ_ONLY
|
|
22379
22610
|
}, () => runTool(async () => {
|
|
@@ -22381,6 +22612,7 @@ function registerWhoami(server, client, registered) {
|
|
|
22381
22612
|
const tools = [...registered.entries()].filter(([name]) => name !== "taiga_whoami");
|
|
22382
22613
|
const named = (pick) => tools.filter(([, notes]) => pick(notes)).map(([name]) => name);
|
|
22383
22614
|
return toolResult({
|
|
22615
|
+
api: client.apiHost,
|
|
22384
22616
|
email: me?.email ?? null,
|
|
22385
22617
|
role: me?.role ?? null,
|
|
22386
22618
|
scopes: me?.scopes ?? null,
|
|
@@ -22423,8 +22655,8 @@ const SERVER_INSTRUCTIONS = [
|
|
|
22423
22655
|
"conversation that specifies a new product. A factory owns products.",
|
|
22424
22656
|
"",
|
|
22425
22657
|
"**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
|
-
"
|
|
22658
|
+
"the tool that lists the right ones. Lists page with nextCursor; taiga_list_initiatives lists open ones, newest",
|
|
22659
|
+
"first. outcome, situation and nextStep are one-sentence summaries; setup writes return readiness.",
|
|
22428
22660
|
"Product documents have two timelines: plan (intent) and reality (from the code).",
|
|
22429
22661
|
"",
|
|
22430
22662
|
"**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.491",
|
|
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",
|