@taiga-ai/mcp-server 0.16.0-rc.431 → 0.17.0-dev.439

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 CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
4
4
  import { z } from "zod";
5
5
  import { McpServer, ProtocolError, ProtocolErrorCode, ResourceTemplate } from "@modelcontextprotocol/server";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
6
7
  //#region \0rolldown/runtime.js
7
8
  var __defProp = Object.defineProperty;
8
9
  var __exportAll = (all, no_symbols) => {
@@ -96,7 +97,7 @@ function seg(value) {
96
97
  * is one. Every value goes through URLSearchParams, so nothing a tool passes
97
98
  * can add a parameter of its own.
98
99
  */
99
- function query(params) {
100
+ function query$1(params) {
100
101
  const search = new URLSearchParams();
101
102
  for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== null && value !== "") search.set(key, String(value));
102
103
  const text = search.toString();
@@ -338,10 +339,10 @@ var TaigaClient = class {
338
339
  return this.request("GET", `/products/${seg(productId)}/maintenance/summary`);
339
340
  }
340
341
  async listMaintenanceSweeps(productId, opts) {
341
- return this.request("GET", `/products/${seg(productId)}/maintenance/runs${query({ limit: opts?.limit })}`);
342
+ return this.request("GET", `/products/${seg(productId)}/maintenance/runs${query$1({ limit: opts?.limit })}`);
342
343
  }
343
344
  async listFindings(productId, opts) {
344
- return this.request("GET", `/products/${seg(productId)}/maintenance/findings${query({ ...opts })}`);
345
+ return this.request("GET", `/products/${seg(productId)}/maintenance/findings${query$1({ ...opts })}`);
345
346
  }
346
347
  async getFinding(id) {
347
348
  return this.request("GET", `/maintenance/findings/${seg(id)}`);
@@ -367,7 +368,7 @@ var TaigaClient = class {
367
368
  async listInbox(opts) {
368
369
  const { productId, ...rest } = opts;
369
370
  const path = productId ? `/products/${seg(productId)}/inbox` : "/inbox";
370
- return this.request("GET", `${path}${query({ ...rest })}`);
371
+ return this.request("GET", `${path}${query$1({ ...rest })}`);
371
372
  }
372
373
  async updateInboxItem(id, body) {
373
374
  return this.request("PATCH", `/inbox/${seg(id)}`, body);
@@ -375,10 +376,10 @@ var TaigaClient = class {
375
376
  async listSetupTasks(opts) {
376
377
  const { productId, ...rest } = opts;
377
378
  const path = productId ? `/products/${seg(productId)}/setup-tasks` : "/setup-tasks";
378
- return this.request("GET", `${path}${query({ ...rest })}`);
379
+ return this.request("GET", `${path}${query$1({ ...rest })}`);
379
380
  }
380
381
  async listEnvironments(productId, opts) {
381
- return this.request("GET", `/products/${seg(productId)}/environments${query({ ...opts })}`);
382
+ return this.request("GET", `/products/${seg(productId)}/environments${query$1({ ...opts })}`);
382
383
  }
383
384
  async createEnvironment(productId, body) {
384
385
  return this.request("POST", `/products/${seg(productId)}/environments`, body);
@@ -390,13 +391,13 @@ var TaigaClient = class {
390
391
  return this.request("GET", `/products/${seg(productId)}/deployments/current`);
391
392
  }
392
393
  async listDeployments(productId, opts) {
393
- return this.request("GET", `/products/${seg(productId)}/deployments${query({ ...opts })}`);
394
+ return this.request("GET", `/products/${seg(productId)}/deployments${query$1({ ...opts })}`);
394
395
  }
395
396
  async getDeploymentSyncStatus(productId) {
396
397
  return this.request("GET", `/products/${seg(productId)}/deployments/sync-status`);
397
398
  }
398
399
  async getDeliveryMetrics(productId, environmentId, windowDays) {
399
- return this.request("GET", `/products/${seg(productId)}/deployments/insights${query({
400
+ return this.request("GET", `/products/${seg(productId)}/deployments/insights${query$1({
400
401
  environmentId,
401
402
  windowDays
402
403
  })}`);
@@ -405,14 +406,14 @@ var TaigaClient = class {
405
406
  return this.request("GET", `/products/${seg(productId)}/monitoring/health`);
406
407
  }
407
408
  async getMonitoringSignal(productId, signal, environmentId, windowDays) {
408
- return this.request("GET", `/products/${seg(productId)}/monitoring/${signal}${query({
409
+ return this.request("GET", `/products/${seg(productId)}/monitoring/${signal}${query$1({
409
410
  environmentId,
410
411
  windowDays
411
412
  })}`);
412
413
  }
413
414
  async listKnowledge(opts) {
414
415
  const scope = (value) => value === null ? "null" : value;
415
- return this.request("GET", `/custom-documents${query({
416
+ return this.request("GET", `/custom-documents${query$1({
416
417
  ...opts,
417
418
  factoryId: scope(opts.factoryId),
418
419
  productId: scope(opts.productId)
@@ -464,7 +465,7 @@ var TaigaClient = class {
464
465
  return this.request("POST", "/products", body);
465
466
  }
466
467
  async previewProductMove(productId, factoryId) {
467
- return this.request("GET", `/products/${seg(productId)}/factory/preview${query({ factoryId })}`);
468
+ return this.request("GET", `/products/${seg(productId)}/factory/preview${query$1({ factoryId })}`);
468
469
  }
469
470
  async moveProduct(productId, body) {
470
471
  return this.request("PUT", `/products/${seg(productId)}/factory`, body);
@@ -486,7 +487,7 @@ var TaigaClient = class {
486
487
  const data = [];
487
488
  let cursor;
488
489
  do {
489
- const result = await this.request("GET", `/github/installations${query({
490
+ const result = await this.request("GET", `/github/installations${query$1({
490
491
  limit: 100,
491
492
  cursor
492
493
  })}`);
@@ -496,7 +497,7 @@ var TaigaClient = class {
496
497
  return { data };
497
498
  }
498
499
  async listInstallationRepositories(installationId, opts) {
499
- return this.request("GET", `/github/installations/${seg(installationId)}/repositories${query({ ...opts })}`);
500
+ return this.request("GET", `/github/installations/${seg(installationId)}/repositories${query$1({ ...opts })}`);
500
501
  }
501
502
  async linkRepository(productId, body) {
502
503
  return this.request("POST", `/products/${seg(productId)}/repositories`, body);
@@ -569,7 +570,15 @@ const OPERATE_SETTING = {
569
570
  //#endregion
570
571
  //#region src/tools/inputs.ts
571
572
  /** The inputs several tools share, so a product or a page is named the same way everywhere. */
572
- const productId = z.string().uuid().describe("Product ID -- taiga_get_product_context, taiga_resolve_product or taiga_list_products give it");
573
+ /**
574
+ * An ID input whose refusal says where the ID comes from. The client shows the
575
+ * schema's message, not the description, when a call fails validation, and
576
+ * "Invalid UUID" alone sends a model that passed a name to guess again.
577
+ */
578
+ function idInput(what, from) {
579
+ return z.string().uuid({ message: `A ${what} ID is a UUID, not a name: ${from}` }).describe(`${what[0].toUpperCase()}${what.slice(1)} ID -- ${from}`);
580
+ }
581
+ const productId = idInput("product", "taiga_list_products (query finds one by name), taiga_resolve_product or taiga_get_product_context give it");
573
582
  /** The page inputs every list tool takes; `nextCursor` of one page is the `cursor` of the next. */
574
583
  const pagination = {
575
584
  limit: z.number().int().min(1).max(100).optional().describe("Max results (default 25)"),
@@ -578,6 +587,42 @@ const pagination = {
578
587
  const slug = z.string().max(63).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "A slug is lowercase words joined by hyphens").describe("Product slug, as it appears in Taiga URLs");
579
588
  const remoteUrl = z.string().min(1).max(2048).describe("The git remote URL of a checkout (git remote get-url origin): https://, git@host:owner/repo.git or ssh://");
580
589
  //#endregion
590
+ //#region ../../packages/types/dist/lenient-schema.js
591
+ /**
592
+ * Building blocks for reading JSON that arrives untyped -- a stored jsonb
593
+ * column, an agent's answer, a document body -- as a parsed type rather than
594
+ * a `Record<string, unknown>` narrowed field by field (PRO-2514).
595
+ *
596
+ * They encode the two leniencies a hand-written reader already had, so a
597
+ * schema built from them accepts, drops and defaults exactly what the reader
598
+ * it replaces did:
599
+ *
600
+ * - `lenient`: a field read only when it has the expected type, and otherwise
601
+ * treated as absent. Never wrap a schema whose failure must surface: the
602
+ * catch would swallow it and read a malformed value as "absent".
603
+ * - `lenientList`: a list read item by item, where an item that does not parse
604
+ * is skipped and the rest survive.
605
+ */
606
+ /** A field read only when it has the expected type; anything else reads as absent. */
607
+ function lenient(schema) {
608
+ return schema.optional().catch(void 0);
609
+ }
610
+ /**
611
+ * The item schema of every `lenientList`, which its transform otherwise hides:
612
+ * read by the agent contract test, which walks each schema's keys to hold the
613
+ * shared fixtures to them.
614
+ */
615
+ const lenientListItems = /* @__PURE__ */ new WeakMap();
616
+ /** A list read item by item: an item that does not parse is skipped, as the reader's `continue` did. */
617
+ function lenientList(item) {
618
+ const list = z.array(z.unknown()).transform((items) => items.flatMap((entry) => {
619
+ const parsed = item.safeParse(entry);
620
+ return parsed.success ? [parsed.data] : [];
621
+ }));
622
+ lenientListItems.set(list, item);
623
+ return list;
624
+ }
625
+ //#endregion
581
626
  //#region ../../packages/types/dist/tenants.js
582
627
  const TENANT_DOCUMENT_TYPES = [
583
628
  "policy_security",
@@ -1221,6 +1266,57 @@ const BUILD_RUN_ACTIVE_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDispositio
1221
1266
  * DIFFERENT, narrower concept: transitions a user command may never leave.
1222
1267
  */
1223
1268
  const BUILD_RUN_TERMINAL_STATUSES = BUILD_RUN_STATUSES.filter((s) => runDisposition(s) !== "in_flight");
1269
+ /**
1270
+ * Why the agent kept a pull request a draft, as recorded in its description
1271
+ * (PRO-2298). The API reads these markers; the run's escalation carries them.
1272
+ */
1273
+ const READY_BLOCKERS = [
1274
+ "plan_incomplete",
1275
+ "tests_failed",
1276
+ "bar_lowering",
1277
+ "bar_lowering_unchecked",
1278
+ "criticals_unresolved"
1279
+ ];
1280
+ /**
1281
+ * Why the autonomous loop handed a live pull request to a person (PRO-2409).
1282
+ * One per path that sets `reviewEscalated`:
1283
+ * - `ci_fix_cap`: CI kept failing after the automatic fix rounds ran out;
1284
+ * - `ci_fix_stalled`: a fix round on the failing head pushed nothing;
1285
+ * - `conflict_cap`: the branch still conflicts with its base at the cap;
1286
+ * - `ready_blocked`: CI is green, but the description says why it is a draft;
1287
+ * - `check_weakened`: CI went green only after a fix loosened a check;
1288
+ * - `review_bot_cap`: a review BOT (Copilot, CodeRabbit) kept reviewing after
1289
+ * the automatic rounds ran out (PRO-2480). A bot's review never resets the
1290
+ * streak a person's does, so the agent and the bot cannot loop without end.
1291
+ */
1292
+ const REVIEW_ESCALATION_REASONS = [
1293
+ "ci_fix_cap",
1294
+ "ci_fix_stalled",
1295
+ "conflict_cap",
1296
+ "ready_blocked",
1297
+ "check_weakened",
1298
+ "review_bot_cap"
1299
+ ];
1300
+ /** One failing check as stored: the name is required, and the link survives only as http(s). */
1301
+ const storedFailingCheck = z.object({
1302
+ name: z.string(),
1303
+ url: z.string().regex(/^https?:\/\//i).nullable().catch(null)
1304
+ });
1305
+ z.object({
1306
+ reason: z.enum(REVIEW_ESCALATION_REASONS),
1307
+ rounds: lenient(z.number().refine((n) => Number.isInteger(n) && n >= 0)),
1308
+ headSha: lenient(z.string()),
1309
+ file: lenient(z.string()),
1310
+ failingChecks: lenient(lenientList(storedFailingCheck)),
1311
+ blockers: lenient(lenientList(z.enum(READY_BLOCKERS)))
1312
+ }).transform((e) => ({
1313
+ reason: e.reason,
1314
+ ...e.rounds === void 0 ? {} : { rounds: e.rounds },
1315
+ ...e.headSha === void 0 ? {} : { headSha: e.headSha },
1316
+ ...e.file === void 0 ? {} : { file: e.file },
1317
+ ...e.failingChecks === void 0 ? {} : { failingChecks: e.failingChecks },
1318
+ ...e.blockers === void 0 ? {} : { blockers: e.blockers }
1319
+ }));
1224
1320
  [...BUILD_RUN_ACTIVE_STATUSES.filter((s) => s !== "reviewing")];
1225
1321
  //#endregion
1226
1322
  //#region ../../packages/types/dist/agents.js
@@ -1241,26 +1337,30 @@ const BACKLOG_GENERATION_EMPTY_OUTCOMES = [
1241
1337
  "out_of_scope",
1242
1338
  "needs_decision"
1243
1339
  ];
1244
- function isRecord$1(value) {
1245
- return typeof value === "object" && value !== null;
1246
- }
1247
- function isStringOrNull(value) {
1248
- return value === null || typeof value === "string";
1249
- }
1250
- function isDecisionReason(value) {
1251
- if (!isRecord$1(value)) return false;
1252
- const { policyCode, quotedSpan, conflict, alternative, overridePath } = value;
1253
- return [
1254
- policyCode,
1255
- quotedSpan,
1256
- conflict,
1257
- alternative,
1258
- overridePath
1259
- ].every((f) => typeof f === "string");
1260
- }
1261
- function isCoveringInitiative(value) {
1262
- return isRecord$1(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.title === "string" && typeof value.priority === "number";
1263
- }
1340
+ const decisionReason = z.object({
1341
+ policyCode: z.string(),
1342
+ quotedSpan: z.string(),
1343
+ conflict: z.string(),
1344
+ alternative: z.string(),
1345
+ overridePath: z.string()
1346
+ });
1347
+ const coveringInitiative = z.object({
1348
+ id: z.string().min(1),
1349
+ title: z.string(),
1350
+ priority: z.number()
1351
+ });
1352
+ /**
1353
+ * The empty verdict, every field checked: the reader navigates on
1354
+ * `coveredInitiative.id` and renders `message`.
1355
+ */
1356
+ const backlogGenerationEmptyResult = z.object({
1357
+ outcome: z.enum(BACKLOG_GENERATION_EMPTY_OUTCOMES),
1358
+ message: z.string().nullable(),
1359
+ coveredByPriority: z.number().refine(Number.isInteger).nullable(),
1360
+ coveredById: z.string().nullable(),
1361
+ coveredInitiative: coveringInitiative.nullable(),
1362
+ reason: decisionReason.nullable().optional()
1363
+ });
1264
1364
  /**
1265
1365
  * Narrow an `agent_jobs.result` blob to {@link BacklogGenerationEmptyResult}.
1266
1366
  *
@@ -1270,10 +1370,9 @@ function isCoveringInitiative(value) {
1270
1370
  * would hand the page an undefined id to navigate to.
1271
1371
  */
1272
1372
  function isBacklogGenerationEmptyResult(value) {
1273
- if (!isRecord$1(value)) return false;
1274
- const { outcome, message, coveredByPriority, coveredById, coveredInitiative, reason } = value;
1275
- return typeof outcome === "string" && BACKLOG_GENERATION_EMPTY_OUTCOMES.includes(outcome) && isStringOrNull(message) && (coveredByPriority === null || typeof coveredByPriority === "number" && Number.isInteger(coveredByPriority)) && isStringOrNull(coveredById) && (coveredInitiative === null || isCoveringInitiative(coveredInitiative)) && (reason === null || reason === void 0 || isDecisionReason(reason));
1373
+ return backlogGenerationEmptyResult.safeParse(value).success;
1276
1374
  }
1375
+ const withCreatedInitiativeIds = z.object({ createdInitiativeIds: z.array(z.string().min(1)) });
1277
1376
  /**
1278
1377
  * The initiatives a `generate_backlog` job created, from its stored result
1279
1378
  * (PRO-2483), or null when the result does not say: a job finished before the
@@ -1282,11 +1381,13 @@ function isBacklogGenerationEmptyResult(value) {
1282
1381
  * the reader navigates on them.
1283
1382
  */
1284
1383
  function createdInitiativeIdsOf(result) {
1285
- if (!isRecord$1(result)) return null;
1286
- const ids = result.createdInitiativeIds;
1287
- if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) return null;
1288
- return ids;
1384
+ const parsed = withCreatedInitiativeIds.safeParse(result);
1385
+ return parsed.success ? parsed.data.createdInitiativeIds : null;
1289
1386
  }
1387
+ const withCreatedInitiatives = z.object({ createdInitiatives: z.array(z.object({
1388
+ id: z.string().min(1),
1389
+ title: z.string()
1390
+ })).min(1) });
1290
1391
  /**
1291
1392
  * The initiatives a `generate_backlog` run added, each with the title it was
1292
1393
  * created with, as the worker writes them on `agent_jobs.result.createdInitiatives`
@@ -1299,15 +1400,197 @@ function createdInitiativeIdsOf(result) {
1299
1400
  * read, so a reader never links an undefined id.
1300
1401
  */
1301
1402
  function createdInitiativesOf(result) {
1302
- if (!isRecord$1(result)) return null;
1303
- const list = result.createdInitiatives;
1304
- if (!Array.isArray(list) || list.length === 0) return null;
1305
- return list.every((entry) => isRecord$1(entry) && typeof entry.id === "string" && entry.id.length > 0 && typeof entry.title === "string") ? list.map((entry) => ({
1306
- id: entry.id,
1307
- title: entry.title
1308
- })) : null;
1403
+ const parsed = withCreatedInitiatives.safeParse(result);
1404
+ return parsed.success ? parsed.data.createdInitiatives : null;
1309
1405
  }
1310
1406
  //#endregion
1407
+ //#region ../../packages/types/dist/chat-stream-events.js
1408
+ /**
1409
+ * The longest generated document, in UTF-16 units: a long brief of a dozen
1410
+ * pages. A document past it is refused by the agent rather than clipped, since
1411
+ * a document with its tail cut off reads as finished.
1412
+ */
1413
+ const MAX_GENERATED_DOCUMENT_CHARS = 4e4;
1414
+ /** A JSON object whose keys a reader walks itself. */
1415
+ const jsonObject = z.record(z.string(), z.unknown());
1416
+ /** The raw Strands block a tool call starts in. */
1417
+ const contentBlockStart = z.object({ start: lenient(z.object({ toolUse: lenient(jsonObject) })) });
1418
+ /** A raw Strands delta: answer text, or a fragment of a tool's input. */
1419
+ const contentBlockDelta = z.object({ delta: lenient(z.object({
1420
+ text: lenient(z.string()),
1421
+ toolUse: lenient(jsonObject)
1422
+ })) });
1423
+ /**
1424
+ * A frame's `event`, with the keys a reader dispatches on named. The raw
1425
+ * Strands blocks are read here; each synthesized payload is left as it came
1426
+ * and parsed by the reader that acts on it. Every other key passes through.
1427
+ */
1428
+ const chatStreamFrameEvent = z.looseObject({
1429
+ contentBlockStart: lenient(contentBlockStart),
1430
+ contentBlockDelta: lenient(contentBlockDelta),
1431
+ toolStatus: z.unknown().optional(),
1432
+ retry: z.unknown().optional(),
1433
+ citations: z.unknown().optional(),
1434
+ citationCheck: z.unknown().optional(),
1435
+ usage: z.unknown().optional(),
1436
+ thought: z.unknown().optional(),
1437
+ plan: z.unknown().optional(),
1438
+ readPause: z.unknown().optional(),
1439
+ initiativeProposal: z.unknown().optional(),
1440
+ initiativeEditProposal: z.unknown().optional(),
1441
+ generatedDocument: z.unknown().optional(),
1442
+ documentSections: z.unknown().optional(),
1443
+ specUpdate: z.unknown().optional(),
1444
+ flowSections: z.unknown().optional()
1445
+ });
1446
+ z.object({
1447
+ status: z.unknown().optional(),
1448
+ error: lenient(z.string()),
1449
+ code: lenient(z.string()),
1450
+ message: lenient(z.object({
1451
+ role: z.unknown().optional(),
1452
+ content: lenient(z.array(z.unknown()))
1453
+ })),
1454
+ event: lenient(chatStreamFrameEvent)
1455
+ });
1456
+ //#endregion
1457
+ //#region ../../packages/types/dist/chat-stream-payloads.js
1458
+ /**
1459
+ * The synthesized chat stream payloads that the API persists and the browser
1460
+ * renders, each read by ONE set of rules (PRO-2735).
1461
+ *
1462
+ * The API reads a payload before it saves it with the answer, and the browser
1463
+ * reads it again, live and from the saved message. The two copies of these
1464
+ * readers used to be kept in step by hand (`chat/routes/stream.ts`,
1465
+ * `product-chat/live-turn.ts`); one shared reader means the card a live
1466
+ * stream draws is the card the reloaded transcript draws.
1467
+ *
1468
+ * Each reader takes the payload (the value under its key in `event`, or in the
1469
+ * message metadata) and returns the checked value or null. A payload that
1470
+ * fails is dropped whole, never stored half understood: every one of them is
1471
+ * rendered from the metadata, and the proposals' buttons perform a write.
1472
+ */
1473
+ /**
1474
+ * The bounds `createBacklogItemBody` enforces on a proposed initiative
1475
+ * (PRO-1879). A proposal outside them would draw a card whose button opens a
1476
+ * dialog the create refuses; the agent applies them too, so a value outside
1477
+ * them is a malformed frame rather than a long brief to trim.
1478
+ */
1479
+ const MAX_PROPOSAL_TITLE_CHARS = 500;
1480
+ const MAX_PROPOSAL_DESCRIPTION_CHARS = 2e4;
1481
+ /** `updateBacklogItemBody`'s own bound on a description. */
1482
+ const MAX_EDIT_DESCRIPTION_CHARS = 2e4;
1483
+ /** Text that is not empty once trimmed and fits `max` as written. */
1484
+ function contentWithin(max) {
1485
+ return z.string().refine((text) => text.trim().length > 0 && text.length <= max);
1486
+ }
1487
+ /** `revision` counts only as the literal `true`; anything else leaves it off. */
1488
+ function revisionOf(revision) {
1489
+ return revision === true ? { revision: true } : {};
1490
+ }
1491
+ z.object({
1492
+ title: z.string().trim().min(1).max(MAX_PROPOSAL_TITLE_CHARS),
1493
+ description: z.string().trim().min(1).max(MAX_PROPOSAL_DESCRIPTION_CHARS),
1494
+ productId: z.string().trim().min(1),
1495
+ revision: z.unknown().optional()
1496
+ }).transform(({ revision, ...proposal }) => ({
1497
+ ...proposal,
1498
+ ...revisionOf(revision)
1499
+ }));
1500
+ z.object({
1501
+ initiativeId: z.string().trim().min(1),
1502
+ description: contentWithin(MAX_EDIT_DESCRIPTION_CHARS),
1503
+ basedOn: z.object({
1504
+ updatedAt: z.string().trim().min(1),
1505
+ descriptionHash: z.string().regex(/^[0-9a-f]{64}$/)
1506
+ })
1507
+ });
1508
+ z.object({
1509
+ title: z.string().trim().min(1).max(200),
1510
+ kind: z.string().trim().min(1).max(80),
1511
+ markdown: contentWithin(MAX_GENERATED_DOCUMENT_CHARS),
1512
+ revision: z.unknown().optional()
1513
+ }).transform(({ revision, ...document }) => ({
1514
+ ...document,
1515
+ ...revisionOf(revision)
1516
+ }));
1517
+ const UNSUPPORTED_REASONS = [
1518
+ "no_quote",
1519
+ "unsupplied_document",
1520
+ "not_in_document"
1521
+ ];
1522
+ const OUTCOMES = [
1523
+ "verified",
1524
+ "paraphrase_supported",
1525
+ "repointed",
1526
+ "unsupported"
1527
+ ];
1528
+ /**
1529
+ * A reason a newer agent sends, or the `quote_not_found` an agent before
1530
+ * PRO-1752 sent, still names an unsupported statement; either reads as the
1531
+ * least specific reason.
1532
+ */
1533
+ function unsupportedReason(value) {
1534
+ return UNSUPPORTED_REASONS.find((reason) => reason === value) ?? "not_in_document";
1535
+ }
1536
+ /**
1537
+ * Which document timeline an entry names (PRO-1731). Absent from an agent
1538
+ * older than PRO-1731, and absent means plan; left off rather than written in,
1539
+ * so "did not say" stays distinguishable from "said plan". A timeline this
1540
+ * contract does not know fails the entry: the origin decides which document a
1541
+ * chip opens, and reading an unknown one as plan links to words that are not
1542
+ * there.
1543
+ */
1544
+ const origin = z.enum(PRODUCT_DOCUMENT_ORIGINS).optional();
1545
+ /** One statement the check could not support; a missing reason is malformed. */
1546
+ const unverifiedCitation = z.object({
1547
+ documentType: z.string(),
1548
+ version: z.number(),
1549
+ origin,
1550
+ section: lenient(z.string()),
1551
+ reason: z.string()
1552
+ }).transform((u) => ({
1553
+ documentType: u.documentType,
1554
+ version: u.version,
1555
+ ...u.origin === void 0 ? {} : { origin: u.origin },
1556
+ ...u.section === void 0 ? {} : { section: u.section },
1557
+ reason: unsupportedReason(u.reason)
1558
+ }));
1559
+ /**
1560
+ * One marker's outcome, strictly: an entry with no 1-based marker index, no
1561
+ * source, or an outcome this contract does not name is dropped rather than
1562
+ * guessed at, because the outcome decides whether the claim renders as a
1563
+ * reference at all.
1564
+ */
1565
+ const citationOutcome = z.object({
1566
+ markerIndex: z.number().refine((n) => Number.isInteger(n) && n >= 1),
1567
+ documentType: z.string(),
1568
+ version: z.number(),
1569
+ origin,
1570
+ section: lenient(z.string()),
1571
+ outcome: z.enum(OUTCOMES),
1572
+ reason: z.unknown().optional(),
1573
+ resolvedSection: lenient(z.string())
1574
+ }).transform((o) => ({
1575
+ markerIndex: o.markerIndex,
1576
+ documentType: o.documentType,
1577
+ version: o.version,
1578
+ ...o.origin === void 0 ? {} : { origin: o.origin },
1579
+ ...o.section === void 0 ? {} : { section: o.section },
1580
+ outcome: o.outcome,
1581
+ ...o.outcome === "unsupported" ? { reason: unsupportedReason(o.reason) } : {},
1582
+ ...o.resolvedSection === void 0 ? {} : { resolvedSection: o.resolvedSection }
1583
+ }));
1584
+ z.object({
1585
+ verified: z.number(),
1586
+ unverified: lenientList(unverifiedCitation),
1587
+ outcomes: lenientList(citationOutcome).catch([]).optional()
1588
+ }).transform(({ verified, unverified, outcomes }) => ({
1589
+ verified: Math.max(0, Math.trunc(verified)),
1590
+ unverified,
1591
+ ...outcomes === void 0 ? {} : { outcomes }
1592
+ }));
1593
+ //#endregion
1311
1594
  //#region ../../packages/types/dist/environments.js
1312
1595
  const ENVIRONMENT_TYPES = [
1313
1596
  "development",
@@ -1322,6 +1605,14 @@ const ENVIRONMENT_TYPES = [
1322
1605
  //#endregion
1323
1606
  //#region ../../packages/types/dist/maintenance.js
1324
1607
  /**
1608
+ * Maintenance taxonomy + API DTOs.
1609
+ *
1610
+ * Single source of truth shared by the `maintenance_*` DB schema, the
1611
+ * maintenance API, the maintenance batch agent's output contract, and the
1612
+ * Maintaining page. Maintenance is the operational-hygiene system of record,
1613
+ * deliberately SEPARATE from the product backlog.
1614
+ */
1615
+ /**
1325
1616
  * - vulnerable_dependency / outdated_dependency: the dependency scanner
1326
1617
  * - secret: the secrets scanner -- a credential committed to the repository
1327
1618
  * - sast: the SAST scanner over Taiga-authored rules -- an insecure code pattern
@@ -1406,6 +1697,34 @@ const MAINTENANCE_IGNORE_REASONS = [
1406
1697
  "wont_fix"
1407
1698
  ];
1408
1699
  new Set(CODE_REMEDIATION_KINDS);
1700
+ /** A non-empty string, or null. */
1701
+ const locationText = z.string().min(1).nullable().catch(null);
1702
+ /**
1703
+ * A source line is a positive integer or nothing: the response schema declares
1704
+ * `int()`, so a fractional value here would fail serialization for the WHOLE
1705
+ * list rather than blank one row; and 0 is "no position" for scanners that
1706
+ * report [0, 0] for a check with no concrete line.
1707
+ */
1708
+ const sourceLine = z.number().refine((n) => Number.isInteger(n) && n > 0).nullable().catch(null);
1709
+ z.object({
1710
+ rule_id: locationText,
1711
+ rule_url: locationText,
1712
+ tool: locationText,
1713
+ line: sourceLine,
1714
+ end_line: sourceLine,
1715
+ resource: locationText,
1716
+ in_tree: z.unknown().optional(),
1717
+ commit: z.string().regex(/^[0-9a-f]{7,40}$/).nullable().catch(null)
1718
+ }).transform((d) => ({
1719
+ ruleId: d.rule_id,
1720
+ ruleUrl: d.rule_url,
1721
+ tool: d.tool,
1722
+ line: d.line,
1723
+ endLine: d.end_line,
1724
+ resource: d.resource,
1725
+ historyOnly: d.in_tree === false,
1726
+ commit: d.commit
1727
+ }));
1409
1728
  //#endregion
1410
1729
  //#region ../../packages/types/dist/design-tokens.js
1411
1730
  /**
@@ -1557,6 +1876,31 @@ const ARTIFACT_SCALE_FRAME_NAMES = [
1557
1876
  [...ARTIFACT_CSS_VARIABLES, ...ARTIFACT_SCALE_FRAME_NAMES];
1558
1877
  new Set(ARTIFACT_CSS_VARIABLES.filter((name) => name !== "--radius"));
1559
1878
  //#endregion
1879
+ //#region ../../packages/types/dist/design-emitter.js
1880
+ /**
1881
+ * Design-system emitter (PRO-887): materialise the tenant's INTERNAL Design
1882
+ * Standards document into real files for a generated application.
1883
+ *
1884
+ * Until this existed the internal path reached code only as plan prose: the
1885
+ * planner named tokens, INSPECT flagged literal colours, but nothing wrote a
1886
+ * token file, so the coder hedged every colour as `var(--x, #hex)`. A linked
1887
+ * design system ships its own package and never needs this -- the dispatcher
1888
+ * gates the emitter off whenever one is linked (single source of truth).
1889
+ *
1890
+ * Pure and deterministic: same document, same bytes. No DOM, no React, no
1891
+ * I/O -- the API computes the files, the coding agent writes them.
1892
+ */
1893
+ const tokenObject = z.record(z.string(), z.unknown());
1894
+ z.object({
1895
+ tokenConvention: lenient(z.string()),
1896
+ tokens: lenient(tokenObject),
1897
+ components: lenient(tokenObject)
1898
+ });
1899
+ z.object({
1900
+ fontFamily: lenient(z.union([z.string(), z.number()])),
1901
+ fontSize: lenient(z.union([z.string(), z.number()]))
1902
+ });
1903
+ //#endregion
1560
1904
  //#region ../../packages/types/dist/auto-build.js
1561
1905
  /**
1562
1906
  * Auto-build: the review-plans gate (PRO-1305).
@@ -1776,14 +2120,91 @@ function emptyOriginNote(origin, product) {
1776
2120
  if (origin === "plan") return imported ? `This product was imported, so no discovery chain was generated for it: its derived documents are on the reality side. Only the documents a person writes (${PLAN_ONLY_DOCUMENT_TYPES.map(productDocumentName).join(", ")}) would appear here.` : "No plan documents are published yet: they are written during discovery in Taiga.";
1777
2121
  if (!imported) return "No reality documents yet: they are derived from the code once the first initiative is done (a merged pull request marks it done) or the repository is pushed to. Until then read the plan side.";
1778
2122
  switch (importStatusOf(product)) {
1779
- case "pending": return "No reality documents yet: the import has not started. Link the repository and run the import in Taiga.";
2123
+ case "pending": return "No reality documents yet: the import has not started. Link the repository, then taiga_start_import.";
1780
2124
  case "analyzing":
1781
2125
  case "generating_docs": return "No reality documents yet: the import is running and will derive them from the code.";
1782
- case "cancelled": return "No reality documents: the import was cancelled before it derived any. Run it again in Taiga.";
1783
- case "failed": return "No reality documents: the import failed before it derived any. Run it again in Taiga.";
2126
+ case "cancelled": return "No reality documents: the import was cancelled before it derived any. taiga_start_import runs it again.";
2127
+ case "failed": return "No reality documents: the import failed before it derived any. taiga_start_import runs it again.";
1784
2128
  default: return "No reality documents: the import derives them from the code, and none have been published.";
1785
2129
  }
1786
2130
  }
2131
+ /**
2132
+ * What a product in setup waits for, as the next call to make. Nothing on a
2133
+ * draft product says it otherwise: discoveryStep names the step and
2134
+ * importStatus the import, and the way on from each lives in tool
2135
+ * descriptions an agent reads only when it already knows which. Null once the
2136
+ * product is active.
2137
+ */
2138
+ function setupNextStep(product) {
2139
+ if (product?.status !== "draft") return null;
2140
+ if (isImportedProduct(product)) switch (importStatusOf(product)) {
2141
+ case "analyzing":
2142
+ case "generating_docs": return "The import is running and derives the documents from the code (taiga_wait_for_job follows its job). Then review them, and taiga_finish_discovery opens the product.";
2143
+ case "complete": return "The import is done: review the reality documents (taiga_list_product_documents), then taiga_finish_discovery opens the product.";
2144
+ case "failed":
2145
+ case "cancelled": return "The last import did not finish: taiga_start_import runs it again.";
2146
+ default: return "Link its repository if it has none (taiga_list_linkable_repositories, taiga_link_repository), then taiga_start_import: the import derives the documents from the code and runs by itself.";
2147
+ }
2148
+ if (product.discoveryStep === void 0) return null;
2149
+ switch (product.discoveryStep) {
2150
+ case "documents": return "The specification is done; the other documents are generated from it (taiga_generate_product_documents starts the ones still missing). Once every required one is published, taiga_finish_discovery opens the product.";
2151
+ case "checks": return "The documents are published; the checks are stamped in the web, then taiga_finish_discovery opens the product.";
2152
+ default: return "Discovery is a conversation held in the web: the person describes the product there and the specification comes out of it. The other documents follow, then taiga_finish_discovery opens the product.";
2153
+ }
2154
+ }
2155
+ /** What discoveryStep says, for a description that shows it. */
2156
+ const DISCOVERY_STEP_MEANING = "discoveryStep is where a product in setup is: context (not started), conversation or import (the specification is being talked out, or derived from the code), documents (the rest being generated and reviewed), checks (stamped in the web); null once it is active.";
2157
+ //#endregion
2158
+ //#region src/tools/hints.ts
2159
+ /**
2160
+ * The next step after the API answers 404. The API's own word is bare
2161
+ * ("Backlog item not found", "Repository link not found"), and an agent that
2162
+ * hears only that guesses another ID, or a name. Each ID a tool takes names
2163
+ * the tool that lists the right ones; the tool's arguments say which apply.
2164
+ */
2165
+ const BY_ARGUMENT = {
2166
+ itemId: "taiga_list_backlog_items lists a product's initiatives with their IDs; a run or plan ID is not one.",
2167
+ backlogItemId: "taiga_list_backlog_items lists a product's initiatives with their IDs.",
2168
+ buildId: "taiga_list_builds lists the runs, of one product or of every product the key can see.",
2169
+ planId: "taiga_get_plan gives the initiative's active plan and its ID.",
2170
+ repositoryId: "taiga_list_product_repositories lists the product's repository links; a GitHub repository id is not a link ID.",
2171
+ replacesRepositoryId: "taiga_list_product_repositories lists the product's repository links.",
2172
+ githubRepositoryId: "taiga_list_linkable_repositories lists the repositories the GitHub connection reaches, with their ids.",
2173
+ installationId: "taiga_list_linkable_repositories lists the connected GitHub accounts.",
2174
+ jobId: "A job ID is what the tool that started the job returned (taiga_request_initiatives, taiga_start_import).",
2175
+ factoryId: "taiga_list_factories lists the factories the key can see.",
2176
+ findingId: "taiga_list_findings lists a product's findings.",
2177
+ violationId: "taiga_get_policy_standing lists a product's policy violations.",
2178
+ environmentId: "taiga_get_deployments lists the product's environments with their IDs; a product ID is not one.",
2179
+ knowledgeId: "taiga_list_knowledge lists the knowledge files.",
2180
+ productId: "taiga_list_products lists the products the key can see; a name is not an ID (taiga_get_product takes a slug)."
2181
+ };
2182
+ /** Where an argument means something else on one tool, or the tool's 404 needs its own word. */
2183
+ const BY_TOOL = {
2184
+ taiga_resolve_action: () => "taiga_list_action_required lists the items with their IDs.",
2185
+ taiga_get_plan: (said) => /\bplan\b/i.test(said) ? "taiga_get_backlog_item says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one." : BY_ARGUMENT.itemId
2186
+ };
2187
+ /** The hint for a 404 from one tool call: the tool's own, else one per ID it was given. */
2188
+ function notFoundHint(tool, args) {
2189
+ if (BY_TOOL[tool]) return BY_TOOL[tool];
2190
+ if (typeof args !== "object" || args === null) return void 0;
2191
+ const given = Object.keys(args).filter((key) => args[key] != null && BY_ARGUMENT[key]);
2192
+ given.sort((a, b) => Number(a === "productId") - Number(b === "productId"));
2193
+ if (given.length === 0) return void 0;
2194
+ const hint = given.map((key) => BY_ARGUMENT[key]).join(" ");
2195
+ return () => hint;
2196
+ }
2197
+ /** The hint of the tool call in progress, read by runTool when the API answers 404. */
2198
+ const notFoundHints = new AsyncLocalStorage();
2199
+ /** The API's 404 message with the hint of the call in progress, if it has one. */
2200
+ function withNotFoundHint(said) {
2201
+ const hint = notFoundHints.getStore()?.(said);
2202
+ return hint ? `${withStop(said)} ${hint}` : said;
2203
+ }
2204
+ /** A sentence ends in a stop before the next one is added to it. */
2205
+ function withStop(text) {
2206
+ return /[.!?]$/.test(text.trimEnd()) ? text.trimEnd() : `${text.trimEnd()}.`;
2207
+ }
1787
2208
  //#endregion
1788
2209
  //#region src/tools/errors.ts
1789
2210
  /** A successful tool result: one JSON text block the model reads. */
@@ -1827,21 +2248,19 @@ function networkFailureDetail(error) {
1827
2248
  const why = typeof cause?.code === "string" ? cause.code : typeof cause?.message === "string" ? cause.message : null;
1828
2249
  return why ? `${error.message} (${why})` : error.message;
1829
2250
  }
2251
+ /**
2252
+ * The code a status implies when the API sent none. 400 is VALIDATION_ERROR,
2253
+ * the code the API itself sends, so a refusal by this server and one by the
2254
+ * API read the same.
2255
+ */
1830
2256
  const CODE_BY_STATUS = {
1831
- 400: "VALIDATION_FAILED",
2257
+ 400: "VALIDATION_ERROR",
1832
2258
  401: "UNAUTHENTICATED",
1833
2259
  403: "FORBIDDEN",
1834
2260
  404: "NOT_FOUND",
1835
2261
  409: "CONFLICT",
1836
2262
  429: "RATE_LIMITED"
1837
2263
  };
1838
- /**
1839
- * Run a tool handler and turn a failure into a structured error. An API
1840
- * refusal keeps the API's own code when it sent one and its message whole:
1841
- * on a refused publish that names the section holding template residue,
1842
- * which is what the model needs to fix the draft. A failed connection is
1843
- * structured too, so every failure the model sees has the same shape.
1844
- */
1845
2264
  /** What the API said, in one phrase: the status, the code it sent (or the one its status implies), its message. */
1846
2265
  function apiFailureText(error) {
1847
2266
  return `${error.status} ${apiFailureCode(error)}${error.apiMessage ? `: ${error.apiMessage}` : ""}`;
@@ -1854,11 +2273,22 @@ function apiFailureCode(error) {
1854
2273
  function isMissOrRefusal(error) {
1855
2274
  return error instanceof TaigaApiError && (error.status === 404 || error.status === 403);
1856
2275
  }
2276
+ /**
2277
+ * Run a tool handler and turn a failure into a structured error. An API
2278
+ * refusal keeps the API's own code when it sent one and its message whole:
2279
+ * on a refused publish that names the section holding template residue,
2280
+ * which is what the model needs to fix the draft. A failed connection is
2281
+ * structured too, so every failure the model sees has the same shape.
2282
+ */
1857
2283
  async function runTool(handler) {
1858
2284
  try {
1859
2285
  return await handler();
1860
2286
  } catch (error) {
1861
- if (error instanceof TaigaApiError) return toolError(error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR", error.apiMessage ?? `Taiga API answered ${error.status}.`, error.details);
2287
+ if (error instanceof TaigaApiError) {
2288
+ const code = error.code ?? CODE_BY_STATUS[error.status] ?? "API_ERROR";
2289
+ const said = error.apiMessage ?? `Taiga API answered ${error.status}.`;
2290
+ return toolError(code, error.status === 404 ? withNotFoundHint(said) : said, error.details);
2291
+ }
1862
2292
  if (isNetworkFailure(error)) return toolError("NETWORK_ERROR", `Could not reach the Taiga API: ${networkFailureDetail(error)}. Check TAIGA_API_URL.`);
1863
2293
  throw error;
1864
2294
  }
@@ -2039,6 +2469,24 @@ function budgetSections(doc, budget) {
2039
2469
  }
2040
2470
  return cutTo(doc, sections, Object.fromEntries(kept));
2041
2471
  }
2472
+ /**
2473
+ * The answer to a read that names sections: the ones the document has, the
2474
+ * rest listed with their sizes, and the asked-for ids it lacks under
2475
+ * unknownSections. One wrong id does not throw away the right ones (the
2476
+ * model retries only the miss); none right is a miss that lists the real ids.
2477
+ */
2478
+ function sectionReadResult(doc, ids, label) {
2479
+ const cut = pickSections(doc, new Set(ids));
2480
+ if (!cut) return toolError("VALIDATION_ERROR", `The ${label} is not made of sections; read it without sections.`);
2481
+ const all = Object.keys(sectionsOf(doc.data) ?? {});
2482
+ const unknownSections = ids.filter((id) => !all.includes(id));
2483
+ if (unknownSections.length === ids.length) return toolError("NOT_FOUND", `The ${label} has no section ${ids.join(", ")}; its sections are ${all.join(", ")}.`);
2484
+ return toolResult({
2485
+ ...cut.doc,
2486
+ omittedSections: cut.omittedSections,
2487
+ ...unknownSections.length > 0 ? { unknownSections } : {}
2488
+ });
2489
+ }
2042
2490
  //#endregion
2043
2491
  //#region src/tools/summarize.ts
2044
2492
  /**
@@ -2084,6 +2532,44 @@ function itemRows(items, fields) {
2084
2532
  criteria: criteriaCounts(items[index])
2085
2533
  } : row);
2086
2534
  }
2535
+ /** Pages a name search reads before it stops and says so: 1,000 rows at the API's page ceiling. */
2536
+ const SEARCH_PAGE_LIMIT = 10;
2537
+ /**
2538
+ * Rows whose text contains `query`, ignoring case, read page by page until
2539
+ * `limit` match or the rows run out. The API has no name search, and a model
2540
+ * told the person's word for a thing ("the Sandbox factory") otherwise pages
2541
+ * the whole list to find its ID. `more` is true when it stopped before the end.
2542
+ */
2543
+ async function searchPages(fetchPage, query, text, limit) {
2544
+ const needle = query.toLowerCase();
2545
+ const rows = [];
2546
+ let cursor;
2547
+ for (let index = 0; index < SEARCH_PAGE_LIMIT; index += 1) {
2548
+ const result = await fetchPage(cursor);
2549
+ const page = result.data;
2550
+ for (const [at, row] of page.entries()) {
2551
+ if (!text(row).some((value) => typeof value === "string" && value.toLowerCase().includes(needle))) continue;
2552
+ rows.push(row);
2553
+ if (rows.length === limit) return {
2554
+ rows,
2555
+ more: at < page.length - 1 || result.nextCursor !== null
2556
+ };
2557
+ }
2558
+ if (!result.nextCursor) return {
2559
+ rows,
2560
+ more: false
2561
+ };
2562
+ cursor = result.nextCursor;
2563
+ }
2564
+ return {
2565
+ rows,
2566
+ more: true
2567
+ };
2568
+ }
2569
+ /** A search reads its own pages, so a cursor from a plain page has nothing to continue. */
2570
+ const SEARCH_WITH_CURSOR = "A search answers one page; narrow the query instead of paging it.";
2571
+ /** What a search says when it stopped before reading every row. */
2572
+ const SEARCH_NOTE = "There may be more matches than the limit carries; narrow the query or raise the limit.";
2087
2573
  //#endregion
2088
2574
  //#region src/tools/products.ts
2089
2575
  /** What a product row needs to say for the model to pick one and read it in full. */
@@ -2093,9 +2579,10 @@ const PRODUCT_SUMMARY_FIELDS = [
2093
2579
  "slug",
2094
2580
  "status",
2095
2581
  "factoryId",
2096
- "kind",
2097
2582
  "discoveryStep"
2098
2583
  ];
2584
+ /** A list's name search: part of a name, any case. */
2585
+ const query = (what) => z.string().trim().min(1).max(200).optional().describe(`Part of the ${what}, any case: only the rows that contain it`);
2099
2586
  /**
2100
2587
  * What a document row needs to say for the model to pick one and read it in
2101
2588
  * full: which document, on which timeline, how current. Not the derived-from
@@ -2242,7 +2729,7 @@ async function resolveProductRef(client, ref, accepted, usage) {
2242
2729
  ].filter((v) => v !== void 0).length;
2243
2730
  if (given !== 1) {
2244
2731
  const how = given === 0 ? usage ?? "taiga_list_products shows what exists." : "Two names could mean two products.";
2245
- return refusal("VALIDATION_FAILED", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
2732
+ return refusal("VALIDATION_ERROR", `Name the product exactly once: ${accepted.join(" or ")}. ${how}`);
2246
2733
  }
2247
2734
  if (ref.productId) try {
2248
2735
  return { product: (await client.getProduct(ref.productId)).data };
@@ -2271,30 +2758,59 @@ async function resolveProductRef(client, ref, accepted, usage) {
2271
2758
  function registerProductTools(server, client) {
2272
2759
  if (client.hasCapability("factories:read")) server.registerTool("taiga_list_factories", {
2273
2760
  title: "List factories",
2274
- description: "List all factories in the organization. A factory is the group that owns a set of products. Use this to find factory IDs for reading factory instructions. Factory instructions carry the factory's standing guidance: domain context, tech preferences, and the conventions its products follow.",
2275
- inputSchema: z.object({ ...pagination }),
2761
+ description: "List all factories in the organization. A factory is the group that owns a set of products. Use this to find factory IDs for reading factory instructions; query finds one by name. Factory instructions carry the factory's standing guidance: domain context, tech preferences, and the conventions its products follow.",
2762
+ inputSchema: z.object({
2763
+ query: query("factory name"),
2764
+ ...pagination
2765
+ }),
2276
2766
  annotations: READ_ONLY
2277
2767
  }, (params) => runTool(async () => {
2278
- const result = await client.listFactories(params);
2279
- return toolResult(page(result.data, result.nextCursor));
2768
+ if (params.query === void 0) {
2769
+ const result = await client.listFactories(params);
2770
+ return toolResult(page(result.data, result.nextCursor));
2771
+ }
2772
+ if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
2773
+ const found = await searchPages((cursor) => client.listFactories({
2774
+ limit: 100,
2775
+ cursor
2776
+ }), params.query, (row) => [row.name, row.slug], params.limit ?? 25);
2777
+ return toolResult({
2778
+ ...page(found.rows, null),
2779
+ ...found.more ? { more: SEARCH_NOTE } : {}
2780
+ });
2280
2781
  }));
2281
2782
  if (client.hasCapability("products:read")) {
2282
2783
  server.registerTool("taiga_list_products", {
2283
2784
  title: "List products",
2284
- description: "List products in the organization, as summaries (id, name, slug, status, factory, kind). Use this to find a product by name; inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full.",
2785
+ description: `List products in the organization, as summaries (id, name, slug, status, factory, discoveryStep). Use this to find a product by name (query); inside a git checkout, taiga_resolve_product finds it from the remote URL instead. taiga_get_product reads one in full. ${DISCOVERY_STEP_MEANING}`,
2285
2786
  inputSchema: z.object({
2286
- factoryId: z.string().uuid().optional().describe("Filter by factory ID -- get factory IDs from taiga_list_factories"),
2787
+ query: query("product name or slug"),
2788
+ factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it").optional().describe("Only the products of this factory"),
2287
2789
  status: z.enum(PRODUCT_STATUSES).optional().describe("Filter by status: draft until the product finishes setup in Taiga, active after that, archived when retired."),
2288
2790
  ...pagination
2289
2791
  }),
2290
2792
  annotations: READ_ONLY
2291
2793
  }, (params) => runTool(async () => {
2292
- const result = await client.listProducts(params);
2293
- return toolResult(page(pickFields(result.data, PRODUCT_SUMMARY_FIELDS), result.nextCursor));
2794
+ const { query: text, ...filters } = params;
2795
+ if (text === void 0) {
2796
+ const result = await client.listProducts(filters);
2797
+ return toolResult(page(pickFields(result.data, PRODUCT_SUMMARY_FIELDS), result.nextCursor));
2798
+ }
2799
+ if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
2800
+ const found = await searchPages((cursor) => client.listProducts({
2801
+ factoryId: filters.factoryId,
2802
+ status: filters.status,
2803
+ limit: 100,
2804
+ cursor
2805
+ }), text, (row) => [row.name, row.slug], params.limit ?? 25);
2806
+ return toolResult({
2807
+ ...page(pickFields(found.rows, PRODUCT_SUMMARY_FIELDS), null),
2808
+ ...found.more ? { more: SEARCH_NOTE } : {}
2809
+ });
2294
2810
  }));
2295
2811
  server.registerTool("taiga_get_product", {
2296
2812
  title: "Get product",
2297
- description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did.",
2813
+ description: "Get one product in full, by ID or by slug: name, slug, factory, what Taiga owns for it (application, infrastructure, deployment) and its settings; the list and resolve tools carry summaries; taiga_get_product_context carries this summary with the rest a session starts from. Use taiga_list_product_documents for its specification and design, taiga_list_backlog_items for its work and taiga_list_builds for what the coding agent did. A product still in setup carries nextStep: what it waits for and the call that moves it on.",
2298
2814
  inputSchema: z.object({
2299
2815
  productId: productId.optional(),
2300
2816
  slug: slug.optional()
@@ -2302,7 +2818,12 @@ function registerProductTools(server, client) {
2302
2818
  annotations: READ_ONLY
2303
2819
  }, (params) => runTool(async () => {
2304
2820
  const resolved = await resolveProductRef(client, params, ["productId", "slug"]);
2305
- return "refusal" in resolved ? toolError(resolved.refusal.code, resolved.refusal.message) : toolResult(resolved.product);
2821
+ if ("refusal" in resolved) return toolError(resolved.refusal.code, resolved.refusal.message);
2822
+ const nextStep = setupNextStep(resolved.product);
2823
+ return toolResult(nextStep ? {
2824
+ ...resolved.product,
2825
+ nextStep
2826
+ } : resolved.product);
2306
2827
  }));
2307
2828
  server.registerTool("taiga_resolve_product", {
2308
2829
  title: "Resolve product from git remote",
@@ -2332,7 +2853,7 @@ function registerProductTools(server, client) {
2332
2853
  }));
2333
2854
  server.registerTool("taiga_get_product_document", {
2334
2855
  title: "Get product document",
2335
- description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A large document runs to tens of thousands of characters: name the sections you need (the ids taiga_get_product_context lists) and the read carries only those, listing the rest with their sizes. A read that finds nothing says which documents and timelines the product does have.",
2856
+ description: "Get the full content of one product document from one timeline. Read the plan to learn what to build: specification (requirements), architecture (design), threat_model (security risks), technology_decisions. Read reality, when the product has it, to learn what the code actually does before changing it. Omit origin for the product's own timeline. A large document runs to tens of thousands of characters: name the sections you need (the ids taiga_get_product_context lists) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds nothing says which documents and timelines the product does have.",
2336
2857
  inputSchema: z.object({
2337
2858
  productId,
2338
2859
  type: z.enum(PRODUCT_DOCUMENT_TYPES).describe("Document type -- taiga_list_product_documents shows which ones the product has"),
@@ -2347,15 +2868,7 @@ function registerProductTools(server, client) {
2347
2868
  try {
2348
2869
  const doc = stripProvenance((await client.getProductDocument(params.productId, params.type, origin)).data);
2349
2870
  if (!params.sections) return toolResult(doc);
2350
- const cut = pickSections(doc, new Set(params.sections));
2351
- if (!cut) return toolError("VALIDATION_FAILED", `The ${origin} ${params.type} is not made of sections; read it without sections.`);
2352
- const all = Object.keys(doc.data.sections);
2353
- const unknown = params.sections.filter((id) => !all.includes(id));
2354
- if (unknown.length > 0) return toolError("NOT_FOUND", `The ${origin} ${params.type} has no section ${unknown.join(", ")}; its sections are ${all.join(", ")}.`);
2355
- return toolResult({
2356
- ...cut.doc,
2357
- omittedSections: cut.omittedSections
2358
- });
2871
+ return sectionReadResult(doc, params.sections, `${origin} ${params.type}`);
2359
2872
  } catch (error) {
2360
2873
  if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
2361
2874
  product ??= (await client.getProduct(params.productId)).data;
@@ -2477,12 +2990,16 @@ function registerDocumentTools(server, client) {
2477
2990
  }));
2478
2991
  server.registerTool("taiga_get_document", {
2479
2992
  title: "Get company document",
2480
- description: "Get the full content of one of the organization's documents, sections included. Use this when you need the security requirements, data handling rules, architecture standards or compliance controls that apply to your code, or the goals, standards and voice the organization has set down. A read that finds nothing says which documents are published and whether a draft exists.",
2481
- inputSchema: z.object({ type: tenantDocumentType }),
2993
+ description: "Get the full content of one of the organization's documents, sections included. Use this when you need the security requirements, data handling rules, architecture standards or compliance controls that apply to your code, or the goals, standards and voice the organization has set down. A policy runs to thousands of tokens: name the sections you need (the ids taiga_get_product_context lists for every policy) and the read carries only those, listing the rest with their sizes and any id the document lacks (unknownSections). A read that finds nothing says which documents are published and whether a draft exists.",
2994
+ inputSchema: z.object({
2995
+ type: tenantDocumentType,
2996
+ sections: z.array(z.string().min(1).max(200)).min(1).max(50).optional().describe("Only these sections, by id; omit for the whole document")
2997
+ }),
2482
2998
  annotations: READ_ONLY
2483
2999
  }, (params) => runTool(async () => {
2484
3000
  try {
2485
- return toolResult(stripProvenance((await client.getTenantDocument(params.type)).data));
3001
+ const doc = stripProvenance((await client.getTenantDocument(params.type)).data);
3002
+ return params.sections ? sectionReadResult(doc, params.sections, params.type) : toolResult(doc);
2486
3003
  } catch (error) {
2487
3004
  if (!(error instanceof TaigaApiError) || error.status !== 404) throw error;
2488
3005
  const [draft, all] = await Promise.allSettled([client.getTenantDocumentDraft(params.type), client.listTenantDocuments({ limit: 100 })]);
@@ -2526,7 +3043,7 @@ function registerDocumentTools(server, client) {
2526
3043
  title: "List factory documents",
2527
3044
  description: `List a factory's documents, published and draft, as summaries (type, name, version, draft state). A factory has one document type, ${named(FACTORY_DOCUMENT_TYPES)}: its free-form standing guidance (domain context, tech preferences, conventions) that applies to every product it owns. Drafts are listed too (isDraft true); an empty list means neither a publication nor a draft exists. Use taiga_get_factory_document to read the full content.`,
2528
3045
  inputSchema: z.object({
2529
- factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
3046
+ factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
2530
3047
  ...pagination
2531
3048
  }),
2532
3049
  annotations: READ_ONLY
@@ -2538,7 +3055,7 @@ function registerDocumentTools(server, client) {
2538
3055
  title: "Get factory instructions",
2539
3056
  description: "Get the full content of a factory's instructions. Use this when you need the domain context, tech preferences, or conventions the factory expects every product it owns to follow. A read that finds nothing says whether a draft exists and how to write one.",
2540
3057
  inputSchema: z.object({
2541
- factoryId: z.string().uuid().describe("Factory ID -- get from taiga_list_factories"),
3058
+ factoryId: idInput("factory", "taiga_list_factories (query finds one by name) gives it"),
2542
3059
  type: z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type")
2543
3060
  }),
2544
3061
  annotations: READ_ONLY
@@ -2563,7 +3080,7 @@ function registerDocumentTools(server, client) {
2563
3080
  saveDraft: (data) => client.saveFactoryDocumentDraft(factoryId, type, { data }),
2564
3081
  publish: () => client.publishFactoryDocument(factoryId, type)
2565
3082
  });
2566
- const factoryId = z.string().uuid().describe("Factory ID -- get from taiga_list_factories");
3083
+ const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
2567
3084
  const factoryDocumentType = z.enum(FACTORY_DOCUMENT_TYPES).default("factory_instructions").describe("Document type");
2568
3085
  server.registerTool("taiga_update_factory_document", {
2569
3086
  title: "Update a section of factory instructions",
@@ -2658,7 +3175,7 @@ function registerJobTools(server, client) {
2658
3175
  server.registerTool("taiga_get_job", {
2659
3176
  title: "Get an agent job",
2660
3177
  description: "Read one agent job: background work a person asked an agent for, such as an initiative request (taiga_request_initiatives). It says where the job is (pending, processing, completed, failed, cancelled), its progress and any error. A finished initiative request also says what came of it: the initiatives it added, or why it added none (already covered by an existing initiative, not readable as work, outside the product, or not allowed by a policy, with the policy, the line it runs into and the way to decide otherwise). taiga_wait_for_job waits for a job to finish.",
2661
- inputSchema: z.object({ jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it") }),
3178
+ inputSchema: z.object({ jobId: idInput("job", "the tool that started the job returned it") }),
2662
3179
  annotations: READ_ONLY
2663
3180
  }, (params) => runTool(async () => toolResult(jobReport((await client.getAgentJob(params.jobId)).data))));
2664
3181
  }
@@ -2879,7 +3396,8 @@ const ITEM_OUTCOME_FIELDS = [
2879
3396
  "archivedAt",
2880
3397
  "lifecycle"
2881
3398
  ];
2882
- const itemId = z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items");
3399
+ const planId = idInput("plan", "taiga_get_plan gives the initiative's active plan and its ID");
3400
+ const itemId = idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it");
2883
3401
  /** The plan without its steps' prose: enough to approve it or say what a run will do. */
2884
3402
  const PLAN_SUMMARY_FIELDS = [
2885
3403
  "id",
@@ -2939,9 +3457,10 @@ function registerBacklogTools(server, client) {
2939
3457
  function registerBacklogReads(server, client) {
2940
3458
  server.registerTool("taiga_list_backlog_items", {
2941
3459
  title: "List backlog items",
2942
- description: "List a product's backlog items (initiatives): titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done) and criteria, how many of its acceptance criteria are met (null before it is planned). Use this to find the item you are working on, then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
3460
+ description: "List a product's backlog items (initiatives): titles, category, status, where each one is on the line (backlog, todo, queue, plan, build, done) and criteria, how many of its acceptance criteria are met (null before it is planned; met counts the criteria the agent confirmed with evidence or a person checked off, so a done item can show fewer met than it has). Use this to find the item you are working on (query finds one by title), then read its acceptance criteria and technical context with taiga_get_backlog_item. Archived items are left out unless asked for.",
2943
3461
  inputSchema: z.object({
2944
3462
  productId,
3463
+ query: z.string().trim().min(1).max(200).optional().describe("Part of the title, any case: only the items that contain it"),
2945
3464
  status: z.enum([
2946
3465
  "backlog",
2947
3466
  "in_progress",
@@ -2965,13 +3484,26 @@ function registerBacklogReads(server, client) {
2965
3484
  }),
2966
3485
  annotations: READ_ONLY
2967
3486
  }, (params) => runTool(async () => {
2968
- const result = await client.listBacklogItems(params.productId, params);
2969
- return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
3487
+ const { query, ...filters } = params;
3488
+ if (query === void 0) {
3489
+ const result = await client.listBacklogItems(params.productId, filters);
3490
+ return toolResult(page(itemRows(result.data, BACKLOG_SUMMARY_FIELDS), result.nextCursor));
3491
+ }
3492
+ if (params.cursor !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
3493
+ const found = await searchPages((cursor) => client.listBacklogItems(params.productId, {
3494
+ ...filters,
3495
+ limit: 100,
3496
+ cursor
3497
+ }), query, (row) => [row.title], params.limit ?? 25);
3498
+ return toolResult({
3499
+ ...page(itemRows(found.rows, BACKLOG_SUMMARY_FIELDS), null),
3500
+ ...found.more ? { more: SEARCH_NOTE } : {}
3501
+ });
2970
3502
  }));
2971
3503
  server.registerTool("taiga_get_backlog_item", {
2972
3504
  title: "Get backlog item",
2973
- description: "Get one backlog item in full: description, acceptance criteria (and which are met), technical context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before implementing an item, and read its runs with taiga_list_builds to see what the coding agent already did.",
2974
- inputSchema: z.object({ itemId: z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items") }),
3505
+ description: "Get one backlog item in full: description, acceptance criteria (and which are met), technical context (patterns, constraints, and the assumptions to verify before starting), dependencies and source documents. Read this before implementing an item, and read its runs with taiga_list_builds to see what the coding agent already did. lifecycle says whose turn it is and what moves it on: cta start_plan is taiga_queue_backlog_item, retry_plan taiga_plan_again (planFailureCause says why the plan stopped), start_build taiga_approve_plan, resume_build taiga_resume_backlog_item, retry_build a fresh run (start over, in the web), review_pr and view_pr the pull request. needsHumanCause is set only when the line stopped on it.",
3506
+ inputSchema: z.object({ itemId }),
2975
3507
  annotations: READ_ONLY
2976
3508
  }, (params) => runTool(async () => {
2977
3509
  return toolResult((await client.getBacklogItem(params.itemId)).data);
@@ -3022,7 +3554,7 @@ function registerMove(server, client) {
3022
3554
  inputSchema: z.object({
3023
3555
  itemId,
3024
3556
  state: z.enum(USER_DROP_STATES).describe("The column to move into"),
3025
- afterId: z.string().uuid().nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
3557
+ afterId: idInput("backlog item", "taiga_list_backlog_items gives it").nullable().optional().describe("The initiative to follow in that column; null for the head; omit to keep rank order"),
3026
3558
  archived: z.boolean().optional().describe("true files the initiative where it stands, false brings it back; omit to leave as is")
3027
3559
  }),
3028
3560
  annotations: OPERATE
@@ -3099,7 +3631,7 @@ function registerQueue(server, client) {
3099
3631
  description: "Add an initiative to the product queue, the line the coding agent works: queued initiatives are planned and built one at a time, each advancing when the previous pull request merges. This starts paid work: a planner run, then a coding run that pushes commits and opens a pull request in the product repository. Whether the plan is built at once or waits for a person is the initiative's review gate: pass autoBuild: false to read the plan before anything is built (the product's default applies otherwise, and the outcome says which it is); the gate is set first and stays set if the move is then refused. Ask the person before calling this. An initiative already in the queue stays where it is unless afterId says where to go; one being planned or built cannot move (CONFLICT). Returns the initiative with its new state, its review gate and its place in the queue.",
3100
3632
  inputSchema: z.object({
3101
3633
  itemId,
3102
- afterId: z.string().uuid().nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is"),
3634
+ afterId: idInput("backlog item", "taiga_list_backlog_items gives it").nullable().optional().describe("The queued initiative to follow; null for the head of the queue; omit to append a new entry or leave a queued one where it is"),
3103
3635
  autoBuild: autoBuildInput.optional().describe("Set the initiative's review gate before queueing: false to read the plan first, true to build it at once, null to follow the product; omit to leave the gate as it is")
3104
3636
  }),
3105
3637
  annotations: OPERATE
@@ -3141,7 +3673,7 @@ function registerReviewGate(server, client) {
3141
3673
  function registerResume(server, client) {
3142
3674
  server.registerTool("taiga_resume_backlog_item", {
3143
3675
  title: "Resume backlog item",
3144
- description: "Resume an initiative the line stopped on (needsHumanCause is set): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first incomplete step (the steps already committed are kept), a refused build start is cleared and retried. This is the way to carry work on: a run is the record of an attempt, and decisions about the work are made on the initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume. Starts paid work; ask the person first.",
3676
+ description: "Resume an initiative the line stopped on, which is when needsHumanCause is set (the line sets it): a failed or cancelled plan is planned again, a stopped or failed run resumes from its first unfinished part (the steps already committed are kept), a refused build start is cleared and retried. This is the way to carry work on: a run is the record of an attempt, and decisions about the work are made on the initiative. Refused (CONFLICT) when nothing is flagged or the cause has no resume: a plan that failed with needsHumanCause null (lifecycle.cta retry_plan, planFailureCause says why) is planned again with taiga_plan_again. Starts paid work; ask the person first.",
3145
3677
  inputSchema: z.object({ itemId }),
3146
3678
  annotations: OPERATE
3147
3679
  }, (params) => runTool(async () => outcome((await client.resumeInitiative(params.itemId)).data)));
@@ -3150,14 +3682,14 @@ function registerPlanVerdicts(server, client) {
3150
3682
  server.registerTool("taiga_approve_plan", {
3151
3683
  title: "Approve a plan",
3152
3684
  description: "Approve an initiative's plan: its build starts now, as the approver, the same start the Start run button makes. Read the plan first with taiga_get_plan and name its ID; an approval of a plan that was replaced since is refused (CONFLICT), as is one while a new plan is being generated or a build is already running. Starts a coding run that pushes commits and opens a pull request; ask the person first.",
3153
- inputSchema: z.object({ planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan") }),
3685
+ inputSchema: z.object({ planId }),
3154
3686
  annotations: OPERATE
3155
3687
  }, (params) => runTool(async () => outcome((await client.approvePlan(params.planId)).data)));
3156
3688
  server.registerTool("taiga_reject_plan", {
3157
3689
  title: "Reject a plan",
3158
3690
  description: "Send an initiative's plan back with direction: a new plan is generated with the comment as reviewer feedback and the rejected plan as its starting point, which supersedes it when the new one lands. Read the plan first with taiga_get_plan and name its ID. Starts a planner run; ask the person first.",
3159
3691
  inputSchema: z.object({
3160
- planId: z.string().uuid().describe("Plan ID -- get from taiga_get_plan"),
3692
+ planId,
3161
3693
  comment: z.string().trim().min(1).max(PLAN_REJECTION_COMMENT_MAX_CHARS).describe("What the next plan should do differently: a paragraph of direction")
3162
3694
  }),
3163
3695
  annotations: OPERATE
@@ -3203,9 +3735,24 @@ const RUN_OUTCOME_FIELDS = [
3203
3735
  "prUrl",
3204
3736
  "actions"
3205
3737
  ];
3738
+ /**
3739
+ * Where a run that ended without finishing stopped, from its steps and its
3740
+ * hardening result: a failed run with every step completed reads as nothing
3741
+ * failed, and retry_from_failed_step then reads as a mistake, when the agent
3742
+ * stalled in the hardening pass after the steps and the retry continues it.
3743
+ * Null for a run still going or that finished.
3744
+ */
3745
+ function stoppedIn(run, steps) {
3746
+ if (run.prMerged === true || run.status !== "failed" && run.status !== "stopped") return null;
3747
+ if (!steps.some((step) => step.status !== "planned")) return "before the first step started, while setting up the run (cloning, preparing the workspace)";
3748
+ const open = steps.find((step) => step.status !== "completed" && step.status !== "skipped");
3749
+ if (open) return open.status === "planned" ? `before step ${String(open.stepOrder)}, ${String(open.title)}, started (the ones before it completed)` : `step ${String(open.stepOrder)}, ${String(open.title)} (${String(open.status)})`;
3750
+ if (run.hardeningResult == null) return "after the last step, in hardening: the security scanners and their fixes the agent runs before the pull request is ready. A retry continues from there; the steps stay.";
3751
+ return "after the steps and hardening, at the pull request";
3752
+ }
3206
3753
  /** How many products the org-wide list names; past this a row keeps its productId only. */
3207
3754
  const NAMED_PRODUCTS = 100;
3208
- const buildId = z.string().uuid().describe("Build run ID -- get from taiga_list_builds");
3755
+ const buildId = idInput("build run", "taiga_list_builds gives it");
3209
3756
  /** A step without its instructions: what it is, how it stands and what it changed. */
3210
3757
  const STEP_SUMMARY_FIELDS = [
3211
3758
  "stepOrder",
@@ -3266,14 +3813,14 @@ function registerBuildTools(server, client) {
3266
3813
  description: "List the coding agent's runs, newest first: status, branch, pull request, step progress and the actions a person may take on each. A run is the record of an attempt: work is carried on from its initiative (resume and retry_from_failed_step are taiga_resume_backlog_item; start_over is in the web only for now) and stopped by moving the initiative out of Build; a maintenance run, which has no initiative, is retried from its finding. With productId, the runs of that product, optionally of one backlog item; without it, the runs of every product the key can see, each row naming its product, and active: true narrows to the runs executing right now. Use this to see what Taiga already built or is building, then read one run with taiga_get_build. A run of a deleted initiative keeps its title.",
3267
3814
  inputSchema: z.object({
3268
3815
  productId: productId.optional(),
3269
- backlogItemId: z.string().uuid().optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
3816
+ backlogItemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it").optional().describe("Only this backlog item's runs -- get from taiga_list_backlog_items; needs productId"),
3270
3817
  active: z.boolean().optional().describe("Only runs the agent is executing now (cloning through reviewing; a queued pending run is not yet executing and is left out); for the org-wide list"),
3271
3818
  ...pagination
3272
3819
  }),
3273
3820
  annotations: READ_ONLY
3274
3821
  }, (params) => runTool(async () => {
3275
- if (!params.productId && params.backlogItemId) return toolError("VALIDATION_FAILED", "backlogItemId narrows the runs of one product: pass its productId as well (the item carries it).");
3276
- if (params.productId && params.active) return toolError("VALIDATION_FAILED", "active narrows the org-wide list only: omit productId, or read the product's runs and judge each by its status.");
3822
+ if (!params.productId && params.backlogItemId) return toolError("VALIDATION_ERROR", "backlogItemId narrows the runs of one product: pass its productId as well (the item carries it).");
3823
+ if (params.productId && params.active) return toolError("VALIDATION_ERROR", "active narrows the org-wide list only: omit productId, or read the product's runs and judge each by its status.");
3277
3824
  if (params.productId) {
3278
3825
  const result = await client.listBuildRuns(params.productId, params);
3279
3826
  return toolResult(page(pickFields(result.data, BUILD_SUMMARY_FIELDS), result.nextCursor));
@@ -3282,7 +3829,7 @@ function registerBuildTools(server, client) {
3282
3829
  }));
3283
3830
  server.registerTool("taiga_get_build", {
3284
3831
  title: "Get build run",
3285
- description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. Its actions name what the initiative accepts for it: resume and retry_from_failed_step are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
3832
+ description: "Get one run with its steps: the run (status, pull request, hardening, metrics) and each step's title, status, commit, files changed, checks, any error and headline, the first paragraph of what the agent reported. Pass full: true for each step's complete instructions and report, several thousand tokens on a long run. Use this to pick up where the coding agent stopped, or to understand the pull request it opened. A run that failed or was stopped says where (stoppedIn: a step, or the hardening pass after them) and when it last made progress (lastProgressAt). Its actions name what the initiative accepts for it: resume and retry_from_failed_step (which continues from the first unfinished part) are taiga_resume_backlog_item, start_over is in the web only for now; to stop it, move the initiative out of Build. A maintenance run has no initiative and is retried from its finding.",
3286
3833
  inputSchema: z.object({
3287
3834
  buildId,
3288
3835
  full: z.boolean().optional().describe("Carry each step's complete instructions and report (default false: a summary)")
@@ -3290,13 +3837,18 @@ function registerBuildTools(server, client) {
3290
3837
  annotations: READ_ONLY
3291
3838
  }, (params) => runTool(async () => {
3292
3839
  const [run, steps] = await Promise.all([client.getBuildRun(params.buildId), client.listBuildRunSteps(params.buildId)]);
3840
+ const runRow = run.data;
3841
+ const rows = steps.data;
3842
+ const where = stoppedIn(runRow, rows);
3843
+ const stop = where ? { stoppedIn: where } : {};
3293
3844
  if (params.full) return toolResult({
3294
- ...run.data,
3845
+ ...runRow,
3846
+ ...stop,
3295
3847
  steps: steps.data
3296
3848
  });
3297
- const rows = steps.data;
3298
3849
  return toolResult({
3299
- ...run.data,
3850
+ ...runRow,
3851
+ ...stop,
3300
3852
  steps: pickFields(rows, STEP_SUMMARY_FIELDS).map((step, index) => ({
3301
3853
  ...step,
3302
3854
  headline: summaryHeadline(rows[index].summary)
@@ -3587,10 +4139,12 @@ async function productContext(client, product, options) {
3587
4139
  };
3588
4140
  const parts = Object.keys(fetchers).filter(granted);
3589
4141
  const settled = await Promise.allSettled(parts.map((part) => fetchers[part]()));
4142
+ const nextStep = setupNextStep(product);
3590
4143
  const bundle = { product: {
3591
4144
  ...pickFields([product], PRODUCT_SUMMARY_FIELDS)[0],
3592
4145
  projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
3593
- defaultOrigin: defaultOriginFor(product)
4146
+ defaultOrigin: defaultOriginFor(product),
4147
+ ...nextStep ? { nextStep } : {}
3594
4148
  } };
3595
4149
  const unavailable = {};
3596
4150
  parts.forEach((part, index) => {
@@ -3603,7 +4157,7 @@ async function productContext(client, product, options) {
3603
4157
  ...bundle,
3604
4158
  notGranted: notGranted.map((part) => `${part} (needs ${PARTS[part]})`),
3605
4159
  unavailable,
3606
- readNext: "Read a policy in full with taiga_get_document, another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item (taiga_list_backlog_items pages a truncated backlog) and a run with taiga_get_build."
4160
+ readNext: "Read a policy with taiga_get_document (sections: the ids listed here, for only those), another product document with taiga_get_product_document, a backlog item with taiga_get_backlog_item (taiga_list_backlog_items pages a truncated backlog) and a run with taiga_get_build."
3607
4161
  };
3608
4162
  let characters = JSON.stringify(result, null, 2).length;
3609
4163
  for (let measured = -1; measured !== characters;) {
@@ -3654,7 +4208,7 @@ function registerAutonomyTools(server, client) {
3654
4208
  }),
3655
4209
  annotations: OPERATE_SETTING
3656
4210
  }, (params) => runTool(async () => {
3657
- if (params.autoBuild === void 0 && params.autoMerge === void 0) return toolError("VALIDATION_FAILED", "Set autoBuild, autoMerge or both.");
4211
+ if (params.autoBuild === void 0 && params.autoMerge === void 0) return toolError("VALIDATION_ERROR", "Set autoBuild, autoMerge or both.");
3658
4212
  const autoMerge = params.autoMerge === true ? null : params.autoMerge;
3659
4213
  const product = (await client.updateProduct(params.productId, {
3660
4214
  ...params.autoBuild !== void 0 ? { autoBuild: params.autoBuild } : {},
@@ -3672,7 +4226,7 @@ function registerAutonomyTools(server, client) {
3672
4226
  title: "Set whether an initiative's pull request merges on its own",
3673
4227
  description: "An initiative's own switch for merging on its own: true lets its pull request merge itself once the repository's own rules pass (required checks, reviews, a code reviewer you made Taiga wait for); false makes it wait for a person; null follows the product's default. The final word within what the organization and factories allow: where either has turned autonomous merge off, true changes nothing. Taiga merges through your rules, never around them. Ask the person first.",
3674
4228
  inputSchema: z.object({
3675
- itemId: z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items"),
4229
+ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
3676
4230
  autoMerge: z.boolean().nullable().describe("true merges on its own, false waits for a person, null follows the product")
3677
4231
  }),
3678
4232
  annotations: OPERATE_SETTING
@@ -3692,6 +4246,7 @@ function registerAutonomyTools(server, client) {
3692
4246
  const ITEM_FIELDS = [
3693
4247
  "id",
3694
4248
  "productId",
4249
+ "productName",
3695
4250
  "category",
3696
4251
  "kind",
3697
4252
  "subjectType",
@@ -3708,6 +4263,7 @@ const ITEM_FIELDS = [
3708
4263
  const SETUP_TASK_FIELDS = [
3709
4264
  "id",
3710
4265
  "productId",
4266
+ "productName",
3711
4267
  "category",
3712
4268
  "phase",
3713
4269
  "target",
@@ -3724,7 +4280,7 @@ function registerInboxTools(server, client) {
3724
4280
  if (!client.hasCapability("products:read")) return;
3725
4281
  server.registerTool("taiga_list_action_required", {
3726
4282
  title: "List what needs a person",
3727
- description: "Read Action Required, where work comes back to a person: things to review (a plan, a set of initiatives, a document, a pull request, maintenance findings, new policy violations), each a decision an agent handed back rather than made, and things to set up that only a person can (a secret, an environment variable, an account, a package registry, a cloud permission). Across every product the key can open, or one product. category gate is the reviews, setup the setup items; resolved: true reads the archive of done and dismissed ones. The inbox is shared: it is the team's, not one person's. An item snoozed until later is left out until then, as the page leaves it out (snoozedHidden counts them). taiga_list_setup_tasks has the setup tasks with what exactly to set.",
4283
+ description: "Read Action Required, where work comes back to a person: things to review (a plan, a set of initiatives, a document, a pull request, maintenance findings, new policy violations), each a decision an agent handed back rather than made, and things to set up that only a person can (a secret, an environment variable, an account, a package registry, a cloud permission). Across every product the key can open (each row names its product), or one product. category gate is the reviews, setup the setup items; resolved: true reads the archive of done and dismissed ones. The inbox is shared: it is the team's, not one person's. An item snoozed until later is left out until then, as the page leaves it out (snoozedHidden counts them). taiga_list_setup_tasks has the setup tasks with what exactly to set.",
3728
4284
  inputSchema: z.object({
3729
4285
  productId: productId.optional(),
3730
4286
  category: z.enum(["gate", "setup"]).optional().describe("gate: things to review; setup: things to set up"),
@@ -3749,14 +4305,19 @@ function registerInboxTools(server, client) {
3749
4305
  }));
3750
4306
  server.registerTool("taiga_list_setup_tasks", {
3751
4307
  title: "List setup tasks",
3752
- description: "The setup tasks agents raised for work only a person can do, with what exactly to set and where: the name of an environment variable, secret or API key to create (key; never its value, which Taiga does not store or display), the provider page to do it on, and the phase that says when it blocks you (prereq: before the deploy, which fails without it; wiring: connecting generated values back to providers; followup: billing approvals, extra permissions and the like that can wait until after launch). A build does not wait for any of them. Across every product the key can open, or one.",
4308
+ description: "The setup tasks agents raised for work only a person can do, with what exactly to set and where: the name of an environment variable, secret or API key to create (key; never its value, which Taiga does not store or display), the provider page to do it on, and the phase that says when it blocks you (prereq: before the deploy, which fails without it; wiring: connecting generated values back to providers; followup: billing approvals, extra permissions and the like that can wait until after launch). A build does not wait for any of them. Across every product the key can open (each row names its product), or one. The tasks still to do, oldest first, the order they depend on each other; includeDone: true adds the done and dismissed ones.",
3753
4309
  inputSchema: z.object({
3754
4310
  productId: productId.optional(),
4311
+ includeDone: z.boolean().optional().describe("Also the tasks already done or dismissed (default false)"),
3755
4312
  ...pagination
3756
4313
  }),
3757
4314
  annotations: READ_ONLY
3758
4315
  }, (params) => runTool(async () => {
3759
- const result = await client.listSetupTasks(params);
4316
+ const { includeDone, ...rest } = params;
4317
+ const result = await client.listSetupTasks({
4318
+ ...rest,
4319
+ ...includeDone ? {} : { openOnly: true }
4320
+ });
3760
4321
  return toolResult(page(pickFields(result.data, SETUP_TASK_FIELDS), result.nextCursor));
3761
4322
  }));
3762
4323
  if (!client.hasCapability("products:write")) return;
@@ -3764,7 +4325,7 @@ function registerInboxTools(server, client) {
3764
4325
  title: "Resolve or snooze an Action Required item",
3765
4326
  description: "Clear an item from Action Required, for everybody: done means you did it, dismissed means it does not apply and is not going to, in_progress means someone has picked it up. Or snooze it until a time (an hour, tomorrow, a week; null wakes it). Nothing marks a setup task done for you: setting the variable or connecting the registry is the work, and marking it done is still a person's. Resolving a review item does not approve or merge anything; the plan or pull request is answered where it lives. One exception: marking the product documents review item done records, in your name, that every published document was reviewed, and setting it back to in_progress withdraws that; do it only when the person says they read them. Ask the person before clearing what they have not dealt with.",
3766
4327
  inputSchema: z.object({
3767
- itemId: z.string().uuid().describe("Action Required item ID -- taiga_list_action_required gives it"),
4328
+ itemId: idInput("Action Required item", "taiga_list_action_required gives it"),
3768
4329
  status: z.enum([
3769
4330
  "in_progress",
3770
4331
  "done",
@@ -3774,7 +4335,7 @@ function registerInboxTools(server, client) {
3774
4335
  }),
3775
4336
  annotations: OPERATE
3776
4337
  }, (params) => runTool(async () => {
3777
- if (params.status === void 0 && params.snoozedUntil === void 0) return toolError("VALIDATION_FAILED", "Say what became of the item (status) or until when to snooze it.");
4338
+ if (params.status === void 0 && params.snoozedUntil === void 0) return toolError("VALIDATION_ERROR", "Say what became of the item (status) or until when to snooze it.");
3778
4339
  return toolResult(pickFields([(await client.updateInboxItem(params.itemId, {
3779
4340
  ...params.status !== void 0 ? { status: params.status } : {},
3780
4341
  ...params.snoozedUntil !== void 0 ? { snoozedUntil: params.snoozedUntil } : {}
@@ -3798,10 +4359,10 @@ const KNOWLEDGE_FIELDS = [
3798
4359
  ];
3799
4360
  /** Past this many characters a text file's content is cut when read; the download URL has it all. */
3800
4361
  const TEXT_LIMIT = 2e4;
3801
- const knowledgeId = z.string().uuid().describe("Knowledge file ID -- taiga_list_knowledge gives it");
4362
+ const knowledgeId = idInput("knowledge file", "taiga_list_knowledge gives it");
3802
4363
  const scopeInputs = {
3803
- factoryId: z.string().uuid().optional().describe("A factory's knowledge"),
3804
- productId: z.string().uuid().optional().describe("A product's knowledge")
4364
+ factoryId: idInput("factory", "taiga_list_factories gives it").optional().describe("A factory's knowledge"),
4365
+ productId: productId.optional().describe("A product's knowledge")
3805
4366
  };
3806
4367
  /** Bytes of a text file the read takes at most; the download URL has the rest. */
3807
4368
  const READ_BYTES = 262144;
@@ -3865,7 +4426,7 @@ function registerKnowledgeTools(server, client) {
3865
4426
  annotations: READ_ONLY
3866
4427
  }, (params) => runTool(async () => {
3867
4428
  const twoLevels = oneLevel(params);
3868
- if (twoLevels) return toolError("VALIDATION_FAILED", twoLevels);
4429
+ if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
3869
4430
  const scope = params.productId ? { productId: params.productId } : params.factoryId ? {
3870
4431
  factoryId: params.factoryId,
3871
4432
  productId: null
@@ -3922,7 +4483,7 @@ function registerKnowledgeTools(server, client) {
3922
4483
  annotations: OPERATE
3923
4484
  }, (params) => runTool(async () => {
3924
4485
  const twoLevels = oneLevel(params);
3925
- if (twoLevels) return toolError("VALIDATION_FAILED", twoLevels);
4486
+ if (twoLevels) return toolError("VALIDATION_ERROR", twoLevels);
3926
4487
  const mime = params.fileName.toLowerCase().endsWith(".md") ? "text/markdown" : "text/plain";
3927
4488
  const result = await client.addKnowledge({
3928
4489
  fileName: params.fileName,
@@ -4008,7 +4569,7 @@ const VIOLATION_FIELDS = [
4008
4569
  "verifyRequestedAt",
4009
4570
  "assessedAt"
4010
4571
  ];
4011
- const findingId = z.string().uuid().describe("Finding ID -- taiga_list_findings gives it");
4572
+ const findingId = idInput("finding", "taiga_list_findings gives it");
4012
4573
  function registerMaintenanceTools(server, client) {
4013
4574
  if (!client.hasCapability("products:read")) return;
4014
4575
  server.registerTool("taiga_get_maintenance", {
@@ -4019,7 +4580,7 @@ function registerMaintenanceTools(server, client) {
4019
4580
  }, (params) => runTool(async () => {
4020
4581
  const [summary, sweeps] = await Promise.all([client.getMaintenanceSummary(params.productId), client.listMaintenanceSweeps(params.productId, { limit: 3 })]);
4021
4582
  const latest = pickFields(sweeps.data, SWEEP_FIELDS);
4022
- const { lastRun: _lastRun, ...standing } = summary.data;
4583
+ const { lastRun: _lastRun, lastRuns: _lastRuns, ...standing } = summary.data;
4023
4584
  return toolResult({
4024
4585
  ...standing,
4025
4586
  latestSweeps: latest,
@@ -4053,7 +4614,7 @@ function registerMaintenanceTools(server, client) {
4053
4614
  }, (params) => runTool(async () => toolResult((await client.getFinding(params.findingId)).data)));
4054
4615
  server.registerTool("taiga_get_policy_standing", {
4055
4616
  title: "Get a product's policy standing",
4056
- description: "Where a product stands against the organization's own published policies: the controls it violates, each with its violations (critical and high only, each citing the policy's sentence and the document passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, awaiting verification), and when the documents were derived and assessed. A control with no violation is not listed, and it is not \"passing\" either: there is no such state. Pass full: true for every violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
4617
+ description: "Where a product stands against the organization's own published policies: the controls it violates, each with its violations (critical and high only, each citing the policy's sentence and the document passage that contradicts it), what is being done about each (an initiative raised, a fix in progress, awaiting verification), and when the documents were derived and assessed, led by a summary of the counts. A control with no violation is not listed, and it is not \"passing\" either: there is no such state. Pass full: true for every violation's evidence and the resolved ones. Fix a control with taiga_fix_policy_violation.",
4057
4618
  inputSchema: z.object({
4058
4619
  productId,
4059
4620
  full: z.boolean().optional().describe("Carry each violation's evidence, the policy excerpt and the resolved violations (default false)")
@@ -4061,16 +4622,32 @@ function registerMaintenanceTools(server, client) {
4061
4622
  annotations: READ_ONLY
4062
4623
  }, (params) => runTool(async () => {
4063
4624
  const standing = (await client.getPolicyStanding(params.productId)).data;
4064
- if (params.full) return toolResult(standing);
4065
4625
  const controls = Array.isArray(standing.controls) ? standing.controls : [];
4066
4626
  const violations = Array.isArray(standing.divergences) ? standing.divergences : [];
4067
4627
  const resolved = Array.isArray(standing.resolvedDivergences) ? standing.resolvedDivergences.length : 0;
4628
+ const listed = controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0);
4629
+ const bySeverity = {};
4630
+ for (const violation of violations) {
4631
+ const severity = String(violation.severity ?? "unknown");
4632
+ bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
4633
+ }
4634
+ const summary = {
4635
+ controls: listed.length,
4636
+ violations: violations.length,
4637
+ bySeverity,
4638
+ resolvedViolations: resolved
4639
+ };
4640
+ if (params.full) return toolResult({
4641
+ summary,
4642
+ ...standing
4643
+ });
4068
4644
  return toolResult({
4645
+ summary,
4069
4646
  productId: standing.productId,
4070
4647
  hasPolicies: standing.hasPolicies,
4071
4648
  assessment: standing.assessment,
4072
4649
  documents: standing.documents,
4073
- controls: pickFields(controls.filter((c) => Number(c.divergenceCount ?? 0) > 0 || Number(c.findingCount ?? 0) > 0), [
4650
+ controls: pickFields(listed, [
4074
4651
  "controlId",
4075
4652
  "policyType",
4076
4653
  "text",
@@ -4079,8 +4656,7 @@ function registerMaintenanceTools(server, client) {
4079
4656
  "findingCount",
4080
4657
  "plannedCount"
4081
4658
  ]),
4082
- violations: pickFields(violations, VIOLATION_FIELDS),
4083
- resolvedViolations: resolved
4659
+ violations: pickFields(violations, VIOLATION_FIELDS)
4084
4660
  });
4085
4661
  }));
4086
4662
  if (!client.hasCapability("products:write")) return;
@@ -4096,7 +4672,7 @@ function registerMaintenanceTools(server, client) {
4096
4672
  }),
4097
4673
  annotations: OPERATE
4098
4674
  }, (params) => runTool(async () => {
4099
- if (params.ignored && params.reason === void 0) return toolError("VALIDATION_FAILED", "Say why the finding is ignored (reason).");
4675
+ if (params.ignored && params.reason === void 0) return toolError("VALIDATION_ERROR", "Say why the finding is ignored (reason).");
4100
4676
  const body = params.ignored ? {
4101
4677
  status: "ignored",
4102
4678
  ignoreReason: params.reason,
@@ -4138,11 +4714,11 @@ function registerMaintenanceTools(server, client) {
4138
4714
  inputSchema: z.object({
4139
4715
  productId,
4140
4716
  controlId: z.string().min(1).max(40).optional().describe("The control's identifier, e.g. SEC-3.4"),
4141
- violationId: z.string().uuid().optional().describe("One violation -- taiga_get_policy_standing gives it")
4717
+ violationId: idInput("violation", "taiga_get_policy_standing gives it").optional()
4142
4718
  }),
4143
4719
  annotations: OPERATE
4144
4720
  }, (params) => runTool(async () => {
4145
- if (params.controlId === void 0 === (params.violationId === void 0)) return toolError("VALIDATION_FAILED", "Name either a control (controlId) or one violation (violationId), not both or neither.");
4721
+ if (params.controlId === void 0 === (params.violationId === void 0)) return toolError("VALIDATION_ERROR", "Name either a control (controlId) or one violation (violationId), not both or neither.");
4146
4722
  return toolResult((params.controlId ? await client.remediatePolicyControl(params.productId, params.controlId) : await client.remediatePolicyViolation(params.productId, params.violationId)).data);
4147
4723
  }));
4148
4724
  }
@@ -4176,7 +4752,7 @@ const ENVIRONMENT_FIELDS = [
4176
4752
  "isProtected",
4177
4753
  "url"
4178
4754
  ];
4179
- const environmentId = z.string().uuid().describe("Environment ID -- taiga_get_deployments lists them");
4755
+ const environmentId = idInput("environment", "taiga_get_deployments lists them; a product ID is not one");
4180
4756
  const windowDays = (choices) => z.number().int().optional().describe(`Days to look back: ${choices} (default 30)`);
4181
4757
  /** Whether a failure is the API's (refused, missing, unreachable) rather than a bug here. */
4182
4758
  const isApiFailure = (error) => error instanceof TaigaApiError || isNetworkFailure(error);
@@ -4235,6 +4811,7 @@ function registerObserveTools(server, client) {
4235
4811
  part(() => client.getDeploymentSyncStatus(params.productId))
4236
4812
  ]);
4237
4813
  const now = current.data;
4814
+ const note = environments.rows.length === 0 ? "This product has no environments yet: an environment is defined with taiga_define_environment, and its deployments are mirrored from the provider once it is." : history.data.length === 0 && !params.cursor ? "No deployments recorded yet: none was mirrored from the provider (syncStatus says when it last synced), or nothing has been deployed." : null;
4238
4815
  return toolResult({
4239
4816
  environments: pickFields(environments.rows, ENVIRONMENT_FIELDS),
4240
4817
  ...environments.more ? { moreEnvironments: true } : {},
@@ -4244,7 +4821,8 @@ function registerObserveTools(server, client) {
4244
4821
  ..."value" in sync ? { syncStatus: sync.value.data } : {
4245
4822
  syncStatus: null,
4246
4823
  syncUnavailable: sync.unavailable
4247
- }
4824
+ },
4825
+ ...note ? { note } : {}
4248
4826
  });
4249
4827
  }));
4250
4828
  server.registerTool("taiga_get_delivery_metrics", {
@@ -4262,7 +4840,7 @@ function registerObserveTools(server, client) {
4262
4840
  30,
4263
4841
  60,
4264
4842
  90
4265
- ].includes(days)) return toolError("VALIDATION_FAILED", "windowDays is 30, 60 or 90.");
4843
+ ].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90.");
4266
4844
  return toolResult((await client.getDeliveryMetrics(params.productId, params.environmentId, days)).data);
4267
4845
  }));
4268
4846
  server.registerTool("taiga_get_monitoring", {
@@ -4276,22 +4854,32 @@ function registerObserveTools(server, client) {
4276
4854
  annotations: READ_ONLY
4277
4855
  }, (params) => runTool(async () => {
4278
4856
  const health = await client.getMonitoringHealth(params.productId);
4279
- if (!params.environmentId) return toolResult({ health: health.data });
4857
+ if (!params.environmentId) return toolResult({
4858
+ health: health.data,
4859
+ ...health.data.length === 0 ? { note: "No environment is monitored: monitoring starts when an environment has its App URL (taiga_define_environment, taiga_update_environment). No health is not good health." } : {}
4860
+ });
4280
4861
  const days = params.windowDays ?? 30;
4281
4862
  if (![
4282
4863
  30,
4283
4864
  60,
4284
4865
  90
4285
- ].includes(days)) return toolError("VALIDATION_FAILED", "windowDays is 30, 60 or 90, the windows the page offers.");
4866
+ ].includes(days)) return toolError("VALIDATION_ERROR", "windowDays is 30, 60 or 90, the windows the page offers.");
4286
4867
  const env = params.environmentId;
4287
4868
  const [uptime, experience, clientHealth] = await Promise.all([
4288
4869
  part(() => client.getMonitoringSignal(params.productId, "uptime", env, days)),
4289
4870
  part(() => client.getMonitoringSignal(params.productId, "rum", env, days)),
4290
4871
  part(() => client.getMonitoringSignal(params.productId, "client-health", env, days))
4291
4872
  ]);
4873
+ if ("unavailable" in uptime && [
4874
+ uptime,
4875
+ experience,
4876
+ clientHealth
4877
+ ].every((p) => "unavailable" in p && p.unavailable.startsWith("404"))) return toolError("NOT_FOUND", withNotFoundHint(uptime.unavailable.replace(/^404:\s*/, "") || "Environment not found"));
4292
4878
  const value = (p) => "value" in p ? p.value.data : { unavailable: p.unavailable };
4879
+ const envHealth = health.data.filter((row) => row.environmentId === env);
4293
4880
  return toolResult({
4294
- health: health.data.filter((row) => row.environmentId === env),
4881
+ health: envHealth,
4882
+ ...envHealth.length === 0 ? { note: "No health for this environment: it has no App URL to check yet (taiga_update_environment sets one). No health is not good health." } : {},
4295
4883
  windowDays: days,
4296
4884
  uptime: value(uptime),
4297
4885
  realUserExperience: value(experience),
@@ -4327,7 +4915,7 @@ function registerObserveTools(server, client) {
4327
4915
  annotations: OPERATE_SETTING
4328
4916
  }, (params) => runTool(async () => {
4329
4917
  const { environmentId: id, ...changes } = params;
4330
- if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_FAILED", "Change at least one of name, type or url.");
4918
+ if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_ERROR", "Change at least one of name, type or url.");
4331
4919
  const updated = (await client.updateEnvironment(id, changes)).data;
4332
4920
  return toolResult(pickFields([updated], ENVIRONMENT_FIELDS)[0]);
4333
4921
  }));
@@ -19051,7 +19639,7 @@ function registerPrompts(server, client) {
19051
19639
  if (client.hasCapability("backlog:read") && client.hasCapability("products:read")) server.registerPrompt("taiga_implement_backlog_item", {
19052
19640
  title: "Implement a backlog item",
19053
19641
  description: "Implement one initiative: its description and acceptance criteria, the architecture of the product from its own timeline, the factory's instructions and the policy sections to check against. taiga_list_backlog_items gives the item ID.",
19054
- argsSchema: z.object({ itemId: productId.describe("The backlog item ID") })
19642
+ argsSchema: z.object({ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it") })
19055
19643
  }, async (args) => {
19056
19644
  const item = (await served$1(client.getBacklogItem(args.itemId), "No backlog item with that ID the key can see; taiga_list_backlog_items shows what exists")).data;
19057
19645
  const productId = String(item.productId);
@@ -19146,7 +19734,7 @@ const GITHUB_REPOSITORY_FIELDS = [
19146
19734
  "description",
19147
19735
  "language"
19148
19736
  ];
19149
- const repositoryId = z.string().uuid().describe("The product's repository link ID -- taiga_list_product_repositories gives it");
19737
+ const repositoryId = idInput("repository link", "taiga_list_product_repositories gives it; a GitHub repository id is not one");
19150
19738
  function registerRepositoryTools(server, client) {
19151
19739
  if (client.hasCapability("products:read")) {
19152
19740
  server.registerTool("taiga_list_product_repositories", {
@@ -19178,7 +19766,7 @@ function registerRepositoryTools(server, client) {
19178
19766
  description: "The repositories the organization's GitHub connection can reach, to link one to a product (taiga_link_repository takes its id). Searches by name with query; without installationId it reads every connected GitHub account. No connection means GitHub is not connected yet: that is done once, in the web, by someone who can install the app.",
19179
19767
  inputSchema: z.object({
19180
19768
  query: z.string().trim().min(1).max(256).optional().describe("Part of the repository name"),
19181
- installationId: z.string().uuid().optional().describe("One GitHub account -- this tool lists them"),
19769
+ installationId: idInput("GitHub account", "this tool lists them").optional(),
19182
19770
  ...pagination
19183
19771
  }),
19184
19772
  annotations: READ_ONLY
@@ -19188,8 +19776,8 @@ function registerRepositoryTools(server, client) {
19188
19776
  if (installations.length === 0) return toolError("NOT_CONNECTED", "GitHub is not connected for this organization. It is connected once, in the web; then link.");
19189
19777
  const chosen = params.installationId ? installations.filter((i) => i.id === params.installationId) : installations;
19190
19778
  if (chosen.length === 0) return toolError("NOT_FOUND", "No such GitHub connection; these are the connected accounts.", { accounts });
19191
- if (params.cursor !== void 0 && params.query !== void 0) return toolError("VALIDATION_FAILED", "A search answers one page; narrow the query instead of paging it.");
19192
- if (chosen.length > 1 && params.cursor !== void 0) return toolError("VALIDATION_FAILED", "A cursor pages one account: name its installationId.", { accounts });
19779
+ if (params.cursor !== void 0 && params.query !== void 0) return toolError("VALIDATION_ERROR", SEARCH_WITH_CURSOR);
19780
+ if (chosen.length > 1 && params.cursor !== void 0) return toolError("VALIDATION_ERROR", "A cursor pages one account: name its installationId.", { accounts });
19193
19781
  const max = params.limit ?? 25;
19194
19782
  const rows = (data) => pickFields(data, GITHUB_REPOSITORY_FIELDS);
19195
19783
  if (chosen.length === 1) {
@@ -19223,7 +19811,7 @@ function registerRepositoryTools(server, client) {
19223
19811
  description: "Link a repository the GitHub connection can reach (taiga_list_linkable_repositories gives its id) to a product, or with replacesRepositoryId swap the one linked now for it in one step (naming the same repository there changes nothing; linking one already linked without it is refused). A link Taiga manages (isManaged) cannot be replaced or unlinked. Once linked, planning reads its code, runs open pull requests in it, sweeps scan it, and its deployment history is imported in the background. Ask the person first.",
19224
19812
  inputSchema: z.object({
19225
19813
  productId,
19226
- githubRepositoryId: z.string().uuid().describe("The repository id -- taiga_list_linkable_repositories gives it"),
19814
+ githubRepositoryId: idInput("GitHub repository", "taiga_list_linkable_repositories gives it"),
19227
19815
  replacesRepositoryId: repositoryId.optional().describe("The link this one replaces")
19228
19816
  }),
19229
19817
  annotations: OPERATE
@@ -19244,7 +19832,7 @@ function registerRepositoryTools(server, client) {
19244
19832
  }),
19245
19833
  annotations: OPERATE_SETTING
19246
19834
  }, (params) => runTool(async () => {
19247
- if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_FAILED", "Set baseBranch, label or both.");
19835
+ if (params.baseBranch === void 0 && params.label === void 0) return toolError("VALIDATION_ERROR", "Set baseBranch, label or both.");
19248
19836
  const updated = (await client.updateRepository(params.productId, params.repositoryId, {
19249
19837
  ...params.baseBranch !== void 0 ? { baseBranch: params.baseBranch } : {},
19250
19838
  ...params.label !== void 0 ? { label: params.label } : {}
@@ -19411,26 +19999,56 @@ const FACTORY_FIELDS = [
19411
19999
  "slug",
19412
20000
  "isDefault"
19413
20001
  ];
19414
- const factoryId = z.string().uuid().describe("Factory ID -- taiga_list_factories gives it");
20002
+ const factoryId = idInput("factory", "taiga_list_factories (query finds one by name) gives it");
19415
20003
  const buildScope = {
19416
20004
  ownsInfrastructure: z.boolean().nullable().optional().describe("Whether Taiga writes the infrastructure code; null follows the organization"),
19417
20005
  ownsDeployment: z.boolean().nullable().optional().describe("Whether Taiga writes the CI/CD pipelines; null follows the organization")
19418
20006
  };
19419
20007
  const deploymentNotes = z.string().trim().min(1).max(DEPLOYMENT_NOTES_MAX_LENGTH).describe("When a half is off: a sentence on how it is done today, which Taiga designs and plans against");
20008
+ /** How a missing required document comes to exist, said with a documents_incomplete refusal. */
20009
+ const MISSING_DOCUMENTS_HOW = "An imported product gets them from its import (taiga_start_import); a product started from scratch gets the specification from the Discovery conversation in the web and the rest from taiga_generate_product_documents. A draft is published with taiga_publish_product_document.";
19420
20010
  /** A product row the model can act on, saying how it started as the other product tools say it. */
19421
- function productRow(product) {
20011
+ async function productRow(client, product) {
19422
20012
  const { settings: _settings, ...rest } = pickFields([product], PRODUCT_FIELDS)[0];
20013
+ const nextStep = setupNextStep(await withDiscoveryStep(client, product));
19423
20014
  return {
19424
20015
  ...rest,
19425
- projectType: isImportedProduct(product) ? "brownfield" : "greenfield"
20016
+ projectType: isImportedProduct(product) ? "brownfield" : "greenfield",
20017
+ ...nextStep ? { nextStep } : {}
19426
20018
  };
19427
20019
  }
19428
20020
  /**
20021
+ * The product with its discoveryStep. The API derives it on reads only, not
20022
+ * on the answer to a write (products.ts), and a draft started from scratch
20023
+ * needs it to say what comes next; an imported one reads its import status
20024
+ * instead. Best effort: a failed read leaves the product without a nextStep
20025
+ * rather than failing the write that already happened.
20026
+ */
20027
+ async function withDiscoveryStep(client, product) {
20028
+ if (product.status !== "draft" || "discoveryStep" in product || isImportedProduct(product)) return product;
20029
+ try {
20030
+ return (await client.getProduct(String(product.id))).data;
20031
+ } catch (error) {
20032
+ if (error instanceof TaigaApiError || isNetworkFailure(error)) return product;
20033
+ throw error;
20034
+ }
20035
+ }
20036
+ /**
20037
+ * A half turned off with nothing said about how it is done today. Allowed (the
20038
+ * web asks for the sentence but does not insist), so a warning rather than a
20039
+ * refusal: plans then design against a gap.
20040
+ */
20041
+ function missingNotesWarning(product) {
20042
+ const off = product.ownsInfrastructure === false || product.ownsDeployment === false;
20043
+ const notes = typeof product.deploymentNotes === "string" ? product.deploymentNotes.trim() : "";
20044
+ return off && !notes ? ["A half Taiga does not build has no deploymentNotes: plans cannot design against how it is done today. Ask the person for a sentence and set it."] : [];
20045
+ }
20046
+ /**
19429
20047
  * The sentence is for a half Taiga does not build; the web asks for it only
19430
20048
  * then. When both halves are decided on here, a sentence has nothing to say.
19431
20049
  */
19432
20050
  function notesWithNothingToDescribe(params) {
19433
- return typeof params.deploymentNotes === "string" && params.ownsInfrastructure === true && params.ownsDeployment === true ? toolError("VALIDATION_FAILED", "deploymentNotes describes a half Taiga does not build; both are on, so leave it out.") : null;
20051
+ return typeof params.deploymentNotes === "string" && params.ownsInfrastructure === true && params.ownsDeployment === true ? toolError("VALIDATION_ERROR", "deploymentNotes describes a half Taiga does not build; both are on, so leave it out.") : null;
19434
20052
  }
19435
20053
  function registerSetupTools(server, client) {
19436
20054
  if (client.hasCapability("factories:write")) {
@@ -19473,7 +20091,7 @@ function registerSetupTools(server, client) {
19473
20091
  ...params.stopLine ? { stopLine: true } : {}
19474
20092
  });
19475
20093
  return toolResult({
19476
- product: productRow(moved.data),
20094
+ product: await productRow(client, moved.data),
19477
20095
  changed: moved.changed,
19478
20096
  previousFactoryId: moved.previousFactoryId,
19479
20097
  lineStopped: moved.lineStopped,
@@ -19504,7 +20122,7 @@ function registerSetupTools(server, client) {
19504
20122
  ...params.ownsDeployment !== void 0 ? { ownsDeployment: params.ownsDeployment } : {},
19505
20123
  ...params.deploymentNotes !== void 0 ? { deploymentNotes: params.deploymentNotes } : {}
19506
20124
  })).data;
19507
- return toolResult(productRow(product));
20125
+ return toolResult(await productRow(client, product));
19508
20126
  }));
19509
20127
  server.registerTool("taiga_update_product", {
19510
20128
  title: "Change a product's settings",
@@ -19519,10 +20137,15 @@ function registerSetupTools(server, client) {
19519
20137
  annotations: OPERATE_SETTING
19520
20138
  }, (params) => runTool(async () => {
19521
20139
  const { productId: id, ...changes } = params;
19522
- if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_FAILED", "Change at least one setting.");
20140
+ if (Object.values(changes).every((value) => value === void 0)) return toolError("VALIDATION_ERROR", "Change at least one setting.");
19523
20141
  const contradiction = notesWithNothingToDescribe(params);
19524
20142
  if (contradiction) return contradiction;
19525
- return toolResult(productRow((await client.updateProduct(id, changes)).data));
20143
+ const updated = (await client.updateProduct(id, changes)).data;
20144
+ const warnings = missingNotesWarning(updated);
20145
+ return toolResult({
20146
+ ...await productRow(client, updated),
20147
+ ...warnings.length > 0 ? { warnings } : {}
20148
+ });
19526
20149
  }));
19527
20150
  server.registerTool("taiga_refresh_reality_documents", {
19528
20151
  title: "Refresh a product's documents from its code",
@@ -19535,16 +20158,23 @@ function registerSetupTools(server, client) {
19535
20158
  }, (params) => runTool(async () => toolResult((await client.refreshRealityDocuments(params.productId, params.docTypes)).data)));
19536
20159
  server.registerTool("taiga_finish_discovery", {
19537
20160
  title: "Finish Discovery",
19538
- description: "Finish a product's Discovery. Refused (CONFLICT) with details.reason: documents_incomplete names the required documents still unpublished (details.missingDocuments); not_in_setup means it is already finished; a required check left unstamped (stamped in the web) is refused with its details too. It locks the document set and opens the rest of the product; for an imported product it is also when the first initiatives are planned. A locked document changes only after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.",
20161
+ description: "Finish a product's Discovery. Refused (CONFLICT) with details.reason: documents_incomplete names the required documents still unpublished (details.missingDocuments) and how they are made; not_in_setup means it is already finished; a required check left unstamped (stamped in the web) is refused with its details too. It locks the document set and opens the rest of the product; for an imported product it is also when the first initiatives are planned. A locked document changes only after taiga_reopen_discovery. Ask the person first: it is their decision that the documents are right.",
19539
20162
  inputSchema: z.object({ productId }),
19540
20163
  annotations: OPERATE
19541
- }, (params) => runTool(async () => toolResult(productRow((await client.completeSetup(params.productId)).data))));
20164
+ }, (params) => runTool(async () => {
20165
+ try {
20166
+ return toolResult(await productRow(client, (await client.completeSetup(params.productId)).data));
20167
+ } catch (error) {
20168
+ if (!(error instanceof TaigaApiError) || error.details?.reason !== "documents_incomplete") throw error;
20169
+ return toolError(error.code ?? "CONFLICT", `${withStop(error.apiMessage ?? "Required documents are not published yet")} ${MISSING_DOCUMENTS_HOW}`, error.details);
20170
+ }
20171
+ }));
19542
20172
  server.registerTool("taiga_reopen_discovery", {
19543
20173
  title: "Reopen Discovery",
19544
20174
  description: "Reopen a finished product's Discovery so its documents can be amended; finish it again afterwards. Nothing built from them is undone. Ask the person first.",
19545
20175
  inputSchema: z.object({ productId }),
19546
20176
  annotations: OPERATE
19547
- }, (params) => runTool(async () => toolResult(productRow((await client.reopenSetup(params.productId)).data))));
20177
+ }, (params) => runTool(async () => toolResult(await productRow(client, (await client.reopenSetup(params.productId)).data))));
19548
20178
  if (!client.hasCapability("products:read")) return;
19549
20179
  server.registerTool("taiga_start_import", {
19550
20180
  title: "Import a codebase into a product",
@@ -19824,7 +20454,7 @@ function registerWaitTools(server, client, options) {
19824
20454
  title: "Wait for an initiative",
19825
20455
  description: "Wait until an initiative reaches a point, instead of reading it again and again: changed (anything about where it is or its latest run), planned (its plan landed), your_turn (it waits on a person: a plan to review, a pull request, a flag), pr_open (a pull request open now; a merged or closed one does not count), run_settled (its latest run stopped moving) or done (finished; a canceled one stops the wait). Returns as soon as that holds (reached: true), early when it cannot hold without a person (a flag, a failed plan, a plan waiting for its verdict, a pull request waiting for a review, a cancelled initiative, or a landed plan whose review gate cannot be read; stoppedBecause says which), or at the timeout with reached: false: call again to keep waiting. Conditions are states, not events: an initiative whose latest run is already settled answers run_settled at once; item is null when not even the first read answered in time. Planning takes minutes and a build up to an hour.",
19826
20456
  inputSchema: z.object({
19827
- itemId: z.string().uuid().describe("Backlog item ID -- get from taiga_list_backlog_items"),
20457
+ itemId: idInput("backlog item", "taiga_list_backlog_items (query finds one by title) gives it"),
19828
20458
  until: z.enum(ITEM_CONDITIONS).describe("What to wait for"),
19829
20459
  timeoutSeconds
19830
20460
  }),
@@ -19857,7 +20487,7 @@ function registerWaitTools(server, client, options) {
19857
20487
  title: "Wait for a run",
19858
20488
  description: "Wait until a coding agent's run reaches a point: changed (status, steps or pull request), step_done (one more step finished), pr_open (open now, not merged or closed) or settled (it stopped moving: in review, completed, failed, stopped). Returns as soon as that holds (reached: true), early when the run settled without it (stoppedBecause; changed waits on, since a person resuming the run is a change), or at the timeout with reached: false: call again to keep waiting. Conditions are states: a run already settled answers settled at once; run is null when not even the first read answered in time.",
19859
20489
  inputSchema: z.object({
19860
- buildId: z.string().uuid().describe("Build run ID -- get from taiga_list_builds"),
20490
+ buildId: idInput("build run", "taiga_list_builds gives it"),
19861
20491
  until: z.enum(BUILD_CONDITIONS).describe("What to wait for"),
19862
20492
  timeoutSeconds
19863
20493
  }),
@@ -19878,7 +20508,7 @@ function registerWaitTools(server, client, options) {
19878
20508
  title: "Wait for an agent job",
19879
20509
  description: "Wait until an agent job finishes (completed, failed or cancelled): an initiative request, a document, a refresh. Returns the job as taiga_get_job reads it, with what came of an initiative request, as soon as it settles (reached: true), or at the timeout with reached: false: call again to keep waiting. Most jobs take a few minutes.",
19880
20510
  inputSchema: z.object({
19881
- jobId: z.string().uuid().describe("Agent job ID -- a request tool returns it"),
20511
+ jobId: idInput("job", "the tool that started the job returned it"),
19882
20512
  timeoutSeconds
19883
20513
  }),
19884
20514
  annotations: READ_ONLY
@@ -19897,7 +20527,8 @@ function registerWaitTools(server, client, options) {
19897
20527
  //#endregion
19898
20528
  //#region src/tools/index.ts
19899
20529
  /**
19900
- * Every tool refuses an argument it does not take. A plain object schema
20530
+ * Every tool refuses an argument it does not take, and every tool's 404 says
20531
+ * where the right IDs are (hints.ts). A plain object schema
19901
20532
  * strips one silently: a review note passed to a tool with no place for it
19902
20533
  * vanished and the call still succeeded, and a misspelt option quietly
19903
20534
  * became its default. Strict, the call fails with "Unrecognized key" and the
@@ -19909,10 +20540,12 @@ function withStrictInputs(server) {
19909
20540
  server.registerTool = ((name, config, callback) => {
19910
20541
  const schema = config.inputSchema;
19911
20542
  if (schema !== void 0 && !(schema instanceof z.ZodObject)) throw new Error(`Tool ${name}: inputSchema must be a z.object() so unknown arguments can be refused`);
20543
+ const handler = callback;
20544
+ const withHint = (args, ctx) => notFoundHints.run(notFoundHint(name, args), () => handler(args, ctx));
19912
20545
  return register(name, schema ? {
19913
20546
  ...config,
19914
20547
  inputSchema: schema.strict()
19915
- } : config, callback);
20548
+ } : config, withHint);
19916
20549
  });
19917
20550
  return server;
19918
20551
  }
@@ -19942,17 +20575,30 @@ function registerAllTools(server, client, options = {}) {
19942
20575
  registerResources(server, client);
19943
20576
  registerPrompts(server, client);
19944
20577
  }
19945
- //#endregion
19946
- //#region src/tools/instructions.ts
19947
- /**
19948
- * What the server tells a client at connect time: the knowledge base, the
19949
- * order to read it in, the two document timelines, what section metadata
19950
- * means, and when to write. Kept as data so a test can hold it to the
19951
- * catalogue the same way it holds the tool descriptions.
19952
- */
19953
20578
  const SERVER_INSTRUCTIONS = [
19954
- "You have access to Taiga, a policy-aware software development platform: the organization's",
19955
- "engineering knowledge base, its products, their backlog and what the coding agent has built.",
20579
+ [
20580
+ "You have access to Taiga, a policy-aware software development platform: the organization's",
20581
+ "engineering knowledge base, its products, their backlog and what the coding agent has built.",
20582
+ "",
20583
+ "**Start here.** Call taiga_get_product_context first, naming the product by its git remote URL inside a",
20584
+ "checkout (git remote get-url origin), else by slug or ID (taiga_list_products finds one by name with",
20585
+ "query). It returns the product (and, while it is in setup, nextStep: what it waits for), its factory",
20586
+ "instructions, its specification and architecture, every policy as its section ids, the open initiatives,",
20587
+ "the repositories and the latest runs. Then read only what you need: the policy sections with",
20588
+ "taiga_get_document, an initiative with taiga_get_backlog_item. Check Action Required",
20589
+ "(taiga_list_action_required) for what waits on a person.",
20590
+ "",
20591
+ "**Before you write.** Tell the person what you are about to do and wait for their yes before a tool that",
20592
+ "starts paid agent work (queueing, approving, resuming, requesting, importing, generating), interrupts it",
20593
+ "(stopping, pausing) or changes what others read (publishing, resolving, settings, repositories). A client",
20594
+ "that prompts on destructive tools does part of that for you.",
20595
+ "",
20596
+ "**Answers.** A refusal carries the API reason: say it as it is. NOT_FOUND names the tool that lists the",
20597
+ "right IDs; an ID is a UUID, never a name. An empty result says why when it has a reason.",
20598
+ "",
20599
+ "**Words.** A backlog item is an initiative; a build is a run, the record of one attempt at it; Action",
20600
+ "Required is the inbox; the line is the queue; a factory is the group that owns a set of products."
20601
+ ].join("\n"),
19956
20602
  "",
19957
20603
  "**Company documents** (taiga_list_documents / taiga_get_document):",
19958
20604
  `The ${POLICY_TYPES.length} policies (${named(POLICY_TYPES)}) carry the security, data, SDLC, operations and`,
@@ -20046,15 +20692,9 @@ const SERVER_INSTRUCTIONS = [
20046
20692
  "`priority: core` marks the sections every organization keeps. Neither changes what the section",
20047
20693
  "requires of you.",
20048
20694
  "",
20049
- "Workflow: start with taiga_get_product_context, naming the product by its git remote URL inside a checkout",
20050
- "(git remote get-url origin), else by slug or ID from taiga_list_products. It returns the product, the",
20051
- "factory instructions, the specification and architecture as the whole sections that fit (the rest named for",
20052
- "taiga_get_product_document to read by section), every policy as its section ids, the open backlog",
20053
- "items, the repositories and the latest runs in one call, and names any part it could not read. Then read",
20054
- "what you need in full: a policy with taiga_get_document, a backlog item with taiga_get_backlog_item.",
20055
- "Without it, use taiga_list_factories and taiga_list_products, then read the factory instructions,",
20056
- "the product's specification and architecture, and the backlog item you are working on. A document",
20057
- "read that finds nothing tells you what does exist and which tool reads or writes it.",
20695
+ "Without taiga_get_product_context, use taiga_list_factories and taiga_list_products, then read the",
20696
+ "factory instructions, the product's specification and architecture, and the initiative you are working",
20697
+ "on. A document read that finds nothing tells you what does exist and which tool reads or writes it.",
20058
20698
  "",
20059
20699
  "The same documents are resources (taiga://documents/{type}, taiga://products/{productId}/documents/{type},",
20060
20700
  "taiga://factories/{factoryId}/instructions, taiga://backlog/{itemId}) for clients that attach them, and three",