@taiga-ai/mcp-server 0.18.0-rc.445 → 0.19.0-dev.451

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,10 @@ import { serveStdio } from '@modelcontextprotocol/server/stdio';
4
4
  import { TaigaClient } from './client.js';
5
5
  import { createServer } from './server.js';
6
6
  import type { WaitOptions } from './tools/wait.js';
7
+ import { inProductWords } from './tools/errors.js';
8
+
9
+ /** What a tool returns for an API value: its keys in the product's words (PRO-2800). */
10
+ const asSeen = <T>(value: T): T => inProductWords(value) as T;
7
11
 
8
12
  // ---------------------------------------------------------------------------
9
13
  // Helpers -- thin wrappers, not abstraction layers
@@ -42,15 +46,15 @@ const ALL_TOOL_NAMES = [
42
46
  'taiga_fix_finding',
43
47
  'taiga_fix_policy_violation',
44
48
  'taiga_generate_product_documents',
45
- 'taiga_get_backlog_item',
46
- 'taiga_get_build',
47
49
  'taiga_get_delivery_metrics',
48
50
  'taiga_get_deployments',
49
51
  'taiga_get_document',
50
52
  'taiga_get_factory_document',
51
53
  'taiga_get_finding',
54
+ 'taiga_get_initiative',
52
55
  'taiga_get_job',
53
56
  'taiga_get_knowledge',
57
+ 'taiga_get_line',
54
58
  'taiga_get_maintenance',
55
59
  'taiga_get_monitoring',
56
60
  'taiga_get_plan',
@@ -58,26 +62,26 @@ const ALL_TOOL_NAMES = [
58
62
  'taiga_get_product',
59
63
  'taiga_get_product_context',
60
64
  'taiga_get_product_document',
61
- 'taiga_get_queue',
65
+ 'taiga_get_run',
62
66
  'taiga_ignore_finding',
63
67
  'taiga_link_repository',
64
68
  'taiga_list_action_required',
65
- 'taiga_list_backlog_items',
66
- 'taiga_list_builds',
67
69
  'taiga_list_documents',
68
70
  'taiga_list_environments',
69
71
  'taiga_list_factories',
70
72
  'taiga_list_factory_documents',
71
73
  'taiga_list_findings',
74
+ 'taiga_list_initiatives',
72
75
  'taiga_list_knowledge',
73
76
  'taiga_list_linkable_repositories',
74
77
  'taiga_list_product_documents',
75
78
  'taiga_list_product_repositories',
76
79
  'taiga_list_products',
77
80
  'taiga_list_repository_branches',
81
+ 'taiga_list_runs',
78
82
  'taiga_list_setup_tasks',
79
83
  'taiga_mark_product_document_reviewed',
80
- 'taiga_move_backlog_item',
84
+ 'taiga_move_initiative',
81
85
  'taiga_move_product',
82
86
  'taiga_pause_line',
83
87
  'taiga_plan_again',
@@ -85,7 +89,7 @@ const ALL_TOOL_NAMES = [
85
89
  'taiga_publish_document',
86
90
  'taiga_publish_factory_document',
87
91
  'taiga_publish_product_document',
88
- 'taiga_queue_backlog_item',
92
+ 'taiga_queue_initiative',
89
93
  'taiga_refresh_reality_documents',
90
94
  'taiga_regenerate_product_document',
91
95
  'taiga_reject_plan',
@@ -94,24 +98,24 @@ const ALL_TOOL_NAMES = [
94
98
  'taiga_request_initiatives',
95
99
  'taiga_resolve_action',
96
100
  'taiga_resolve_product',
97
- 'taiga_resume_backlog_item',
101
+ 'taiga_resume_initiative',
98
102
  'taiga_retry_knowledge',
99
103
  'taiga_set_auto_build',
100
104
  'taiga_set_auto_merge',
101
105
  'taiga_set_product_autonomy',
102
106
  'taiga_set_repository_branch',
103
107
  'taiga_start_import',
104
- 'taiga_stop_build',
105
108
  'taiga_stop_generating_documents',
109
+ 'taiga_stop_run',
106
110
  'taiga_unlink_repository',
107
111
  'taiga_update_document',
108
112
  'taiga_update_environment',
109
113
  'taiga_update_factory_document',
110
114
  'taiga_update_product',
111
115
  'taiga_update_product_document',
112
- 'taiga_wait_for_backlog_item',
113
- 'taiga_wait_for_build',
116
+ 'taiga_wait_for_initiative',
114
117
  'taiga_wait_for_job',
118
+ 'taiga_wait_for_run',
115
119
  ];
116
120
 
117
121
  const PRODUCT_ID = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c';
@@ -267,7 +271,7 @@ function toolNames(tools: { name: string }[]) {
267
271
  }
268
272
 
269
273
  /** Extract parsed JSON from the first text content block of a tool call result. */
270
- function parseToolResult(result: Record<string, unknown>) {
274
+ function parseToolResult(result: { content?: unknown }) {
271
275
  const content = result.content as { type: string; text: string }[];
272
276
  return JSON.parse(content[0]!.text);
273
277
  }
@@ -434,10 +438,10 @@ describe('MCP server integration', () => {
434
438
  'taiga_resolve_product',
435
439
  'taiga_list_product_documents',
436
440
  'taiga_get_product_document',
437
- 'taiga_list_builds',
438
- 'taiga_get_build',
439
- 'taiga_get_queue',
440
- 'taiga_wait_for_build',
441
+ 'taiga_list_runs',
442
+ 'taiga_get_run',
443
+ 'taiga_get_line',
444
+ 'taiga_wait_for_run',
441
445
  'taiga_get_job',
442
446
  'taiga_wait_for_job',
443
447
  'taiga_get_maintenance',
@@ -461,12 +465,7 @@ describe('MCP server integration', () => {
461
465
  'taiga_move_product',
462
466
  ],
463
467
  'vcs:manage': ['taiga_link_repository', 'taiga_set_repository_branch', 'taiga_unlink_repository'],
464
- 'backlog:read': [
465
- 'taiga_list_backlog_items',
466
- 'taiga_get_backlog_item',
467
- 'taiga_get_plan',
468
- 'taiga_wait_for_backlog_item',
469
- ],
468
+ 'backlog:read': ['taiga_list_initiatives', 'taiga_get_initiative', 'taiga_get_plan', 'taiga_wait_for_initiative'],
470
469
  'factories:read': ['taiga_list_factories', 'taiga_list_factory_documents', 'taiga_get_factory_document'],
471
470
  'tenant:documents:read': ['taiga_list_documents', 'taiga_get_document'],
472
471
  'customDocuments:read': ['taiga_list_knowledge', 'taiga_get_knowledge'],
@@ -474,10 +473,10 @@ describe('MCP server integration', () => {
474
473
  const PAIRED_CONTRACT: Record<string, string[]> = {
475
474
  'tenant:documents:read tenant:documents:write': ['taiga_update_document', 'taiga_publish_document'],
476
475
  'factories:read factories:documents:write': ['taiga_update_factory_document', 'taiga_publish_factory_document'],
477
- 'backlog:write': ['taiga_move_backlog_item', 'taiga_set_auto_merge'],
478
- 'backlog:write products:write': ['taiga_queue_backlog_item'],
476
+ 'backlog:write': ['taiga_move_initiative', 'taiga_set_auto_merge'],
477
+ 'backlog:write products:write': ['taiga_queue_initiative'],
479
478
  'products:write': [
480
- 'taiga_resume_backlog_item',
479
+ 'taiga_resume_initiative',
481
480
  'taiga_set_auto_build',
482
481
  'taiga_request_initiatives',
483
482
  'taiga_set_product_autonomy',
@@ -495,7 +494,7 @@ describe('MCP server integration', () => {
495
494
  'backlog:read backlog:write products:write': ['taiga_plan_again'],
496
495
  'products:read products:write': [
497
496
  'taiga_start_import',
498
- 'taiga_stop_build',
497
+ 'taiga_stop_run',
499
498
  'taiga_pause_line',
500
499
  'taiga_ignore_finding',
501
500
  'taiga_fix_finding',
@@ -580,7 +579,7 @@ describe('MCP server integration', () => {
580
579
  it('should replace one section of the existing draft and keep the rest', async () => {
581
580
  const draft = policyDocument({ id: 'draft-1', version: null, isDraft: true });
582
581
  const saved = policyDocument({ id: 'draft-1', version: null, isDraft: true });
583
- (saved.data.sections as Record<string, { content: string }>).summary!.content = 'New summary';
582
+ saved.data.sections.summary.content = 'New summary';
584
583
 
585
584
  const { client, fetchSpy } = await bootMcpPair({
586
585
  capabilities: ALL_CAPABILITIES,
@@ -750,9 +749,9 @@ describe('MCP server integration', () => {
750
749
  const { tools } = await client.listTools();
751
750
  const byName = Object.fromEntries(tools.map((t) => [t.name, t.annotations]));
752
751
 
753
- expect(byName.taiga_publish_document).toMatchObject({ destructiveHint: true, idempotentHint: false });
754
- expect(byName.taiga_publish_factory_document).toMatchObject({ destructiveHint: true, idempotentHint: false });
755
- expect(byName.taiga_update_document).toMatchObject({ destructiveHint: false, idempotentHint: true });
752
+ expect(byName['taiga_publish_document']).toMatchObject({ destructiveHint: true, idempotentHint: false });
753
+ expect(byName['taiga_publish_factory_document']).toMatchObject({ destructiveHint: true, idempotentHint: false });
754
+ expect(byName['taiga_update_document']).toMatchObject({ destructiveHint: false, idempotentHint: true });
756
755
  });
757
756
 
758
757
  it('should hand the model the API message when the publish gate refuses the draft', async () => {
@@ -1136,7 +1135,7 @@ describe('MCP server integration', () => {
1136
1135
  });
1137
1136
 
1138
1137
  const result = await client.callTool({
1139
- name: 'taiga_list_backlog_items',
1138
+ name: 'taiga_list_initiatives',
1140
1139
  arguments: { productId: PRODUCT_ID, status: 'in_progress', category: 'feature', includeArchived: true },
1141
1140
  });
1142
1141
 
@@ -1156,7 +1155,7 @@ describe('MCP server integration', () => {
1156
1155
  routes: { [`/v1/backlog/${ITEM_ID}`]: { data: item } },
1157
1156
  });
1158
1157
 
1159
- const result = await client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } });
1158
+ const result = await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } });
1160
1159
 
1161
1160
  expect(parseToolResult(result)).toEqual(item);
1162
1161
  });
@@ -1168,8 +1167,8 @@ describe('MCP server integration', () => {
1168
1167
  });
1169
1168
 
1170
1169
  const result = await client.callTool({
1171
- name: 'taiga_list_builds',
1172
- arguments: { productId: PRODUCT_ID, backlogItemId: ITEM_ID },
1170
+ name: 'taiga_list_runs',
1171
+ arguments: { productId: PRODUCT_ID, initiativeId: ITEM_ID },
1173
1172
  });
1174
1173
 
1175
1174
  const [row] = parseToolResult(result).data;
@@ -1195,10 +1194,18 @@ describe('MCP server integration', () => {
1195
1194
  },
1196
1195
  });
1197
1196
 
1198
- const result = await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID, full: true } });
1197
+ const result = await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID, full: true } });
1199
1198
 
1200
1199
  expect(result.isError).toBeFalsy();
1201
- expect(parseToolResult(result)).toEqual({ ...run, steps: [step] });
1200
+ const { backlogItemId: _item, ...rest } = run;
1201
+ expect(parseToolResult(result)).toEqual({
1202
+ ...rest,
1203
+ initiativeId: ITEM_ID,
1204
+ // Completed with no merge recorded: an older run that settled on an open pull request.
1205
+ disposition: 'unmerged',
1206
+ outcome: 'Settled without merging: its pull request was left unmerged.',
1207
+ steps: [step],
1208
+ });
1202
1209
  });
1203
1210
 
1204
1211
  it('should drop tenantId from every response at the client, so a run, its steps, an item and a list row never carry it', async () => {
@@ -1214,14 +1221,22 @@ describe('MCP server integration', () => {
1214
1221
  });
1215
1222
 
1216
1223
  const build = parseToolResult(
1217
- await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID, full: true } })
1224
+ await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID, full: true } })
1218
1225
  );
1219
- expect(build).toEqual({ ...run, steps: [{ ...step, verification: { ok: true } }] });
1226
+ const { backlogItemId: _item, ...rest } = run;
1227
+ expect(build).toEqual({
1228
+ ...rest,
1229
+ initiativeId: ITEM_ID,
1230
+ disposition: 'unmerged',
1231
+ outcome: 'Settled without merging: its pull request was left unmerged.',
1232
+ steps: [{ ...step, verification: { ok: true } }],
1233
+ });
1220
1234
  expect(JSON.stringify(build)).not.toContain('tenantId');
1221
1235
 
1222
1236
  const got = parseToolResult(
1223
- await client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } })
1237
+ await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
1224
1238
  );
1239
+ // No lifecycle on this item, so no situation.
1225
1240
  expect(got).toEqual(item);
1226
1241
  });
1227
1242
 
@@ -1230,8 +1245,8 @@ describe('MCP server integration', () => {
1230
1245
  const before = fetchSpy.mock.calls.length;
1231
1246
 
1232
1247
  for (const [name, args] of [
1233
- ['taiga_list_backlog_items', { productId: PRODUCT_ID, limit: 2.5 }],
1234
- ['taiga_list_builds', { productId: PRODUCT_ID, limit: 2.5 }],
1248
+ ['taiga_list_initiatives', { productId: PRODUCT_ID, limit: 2.5 }],
1249
+ ['taiga_list_runs', { productId: PRODUCT_ID, limit: 2.5 }],
1235
1250
  ['taiga_list_products', { limit: 2.5 }],
1236
1251
  ] as const) {
1237
1252
  const result = await client.callTool({ name, arguments: { ...args } });
@@ -1245,8 +1260,8 @@ describe('MCP server integration', () => {
1245
1260
 
1246
1261
  const names = toolNames((await client.listTools()).tools);
1247
1262
 
1248
- expect(names).toContain('taiga_list_builds');
1249
- expect(names).not.toContain('taiga_list_backlog_items');
1263
+ expect(names).toContain('taiga_list_runs');
1264
+ expect(names).not.toContain('taiga_list_initiatives');
1250
1265
  });
1251
1266
  });
1252
1267
 
@@ -1949,6 +1964,34 @@ describe('MCP server integration', () => {
1949
1964
  },
1950
1965
  [`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [repo], nextCursor: null },
1951
1966
  [`GET /v1/products/${PRODUCT_ID}/builds?limit=10`]: { data: [run], nextCursor: null },
1967
+ [`GET /v1/products/${PRODUCT_ID}/inbox`]: {
1968
+ data: [
1969
+ { id: 'i1', kind: 'review_pr', status: 'open' },
1970
+ { id: 'i2', kind: 'review_pr', status: 'open' },
1971
+ { id: 'i3', kind: 'setup_secret', status: 'snoozed', snoozedUntil: '2999-01-01T00:00:00.000Z' },
1972
+ ],
1973
+ nextCursor: null,
1974
+ totalCount: 3,
1975
+ },
1976
+ [`GET /v1/products/${PRODUCT_ID}/maintenance/summary`]: {
1977
+ data: {
1978
+ openFindings: 1,
1979
+ severityCounts: { critical: 0, high: 1 },
1980
+ lastRun: { id: 'sweep-1', status: 'completed', completedAt: '2026-10-01T00:00:00.000Z', findingsCount: 1 },
1981
+ },
1982
+ },
1983
+ [`GET /v1/products/${PRODUCT_ID}/maintenance/policies`]: {
1984
+ // The API's own shape: the lists, no summary.
1985
+ data: {
1986
+ controls: [
1987
+ { controlId: 'SEC-1', divergenceCount: 2, findingCount: 0 },
1988
+ { controlId: 'SEC-2', divergenceCount: 0, findingCount: 1 },
1989
+ { controlId: 'SEC-3', divergenceCount: 0, findingCount: 0 },
1990
+ ],
1991
+ divergences: [{ severity: 'critical' }, { severity: 'high' }, { severity: 'high' }],
1992
+ resolvedDivergences: [],
1993
+ },
1994
+ },
1952
1995
  };
1953
1996
 
1954
1997
  it('should assemble every part in one call and name the one document that is not published', async () => {
@@ -2003,7 +2046,24 @@ describe('MCP server integration', () => {
2003
2046
  ],
2004
2047
  truncated: false,
2005
2048
  });
2006
- expect(bundle.builds).toEqual([{ id: BUILD_ID, runNumber: 1, status: 'completed', targetTitle: 'Add login' }]);
2049
+ expect(bundle.builds).toEqual([
2050
+ {
2051
+ id: BUILD_ID,
2052
+ runNumber: 1,
2053
+ status: 'completed',
2054
+ targetTitle: 'Add login',
2055
+ disposition: 'unmerged',
2056
+ outcome: 'Settled without a merged pull request.',
2057
+ },
2058
+ ]);
2059
+ // PRO-2800: "how does it stand" in the same call, as counts.
2060
+ expect(bundle.attention).toEqual({ open: 3, byKind: { review_pr: 2, setup_secret: 1 }, snoozed: 1 });
2061
+ expect(bundle.maintenance).toEqual({
2062
+ openFindings: 1,
2063
+ bySeverity: { critical: 0, high: 1 },
2064
+ lastSweep: { status: 'completed', completedAt: '2026-10-01T00:00:00.000Z' },
2065
+ });
2066
+ expect(bundle.standing).toEqual({ openViolations: 3, bySeverity: { critical: 1, high: 2 }, controlsViolated: 2 });
2007
2067
  expect(bundle.notGranted).toEqual([]);
2008
2068
  expect(bundle.unavailable).toEqual({});
2009
2069
  });
@@ -2328,8 +2388,8 @@ describe('MCP server integration', () => {
2328
2388
  `/v1/factories/${FACTORY_ID}/documents`,
2329
2389
  ['factories:read'],
2330
2390
  ],
2331
- ['taiga_list_backlog_items', { productId: PRODUCT_ID }, `/v1/products/${PRODUCT_ID}/backlog`, ['backlog:read']],
2332
- ['taiga_list_builds', { productId: PRODUCT_ID }, `/v1/products/${PRODUCT_ID}/builds`, ['products:read']],
2391
+ ['taiga_list_initiatives', { productId: PRODUCT_ID }, `/v1/products/${PRODUCT_ID}/backlog`, ['backlog:read']],
2392
+ ['taiga_list_runs', { productId: PRODUCT_ID }, `/v1/products/${PRODUCT_ID}/builds`, ['products:read']],
2333
2393
  ] as const)('%s passes the cursor on and returns where the next page starts', async (name, args, path, caps) => {
2334
2394
  const { client, fetchSpy } = await bootMcpPair({
2335
2395
  capabilities: [...caps],
@@ -2385,9 +2445,9 @@ describe('MCP server integration', () => {
2385
2445
  mimeType: 'text/markdown',
2386
2446
  });
2387
2447
  expect(resourceTemplates.map((t) => t.uriTemplate).sort()).toEqual([
2388
- 'taiga://backlog/{itemId}',
2389
2448
  'taiga://documents/{type}',
2390
2449
  'taiga://factories/{factoryId}/instructions',
2450
+ 'taiga://initiatives/{initiativeId}',
2391
2451
  'taiga://products/{productId}/documents/{type}',
2392
2452
  'taiga://products/{productId}/documents/{type}{?origin}',
2393
2453
  ]);
@@ -2398,7 +2458,7 @@ describe('MCP server integration', () => {
2398
2458
 
2399
2459
  const { resourceTemplates } = await client.listResourceTemplates();
2400
2460
 
2401
- expect(resourceTemplates.map((t) => t.uriTemplate)).toEqual(['taiga://backlog/{itemId}']);
2461
+ expect(resourceTemplates.map((t) => t.uriTemplate)).toEqual(['taiga://initiatives/{initiativeId}']);
2402
2462
  });
2403
2463
 
2404
2464
  it('should read a policy as Markdown with its sections as headings and no provenance', async () => {
@@ -2463,7 +2523,7 @@ describe('MCP server integration', () => {
2463
2523
  });
2464
2524
 
2465
2525
  const instructions = await client.readResource({ uri: `taiga://factories/${FACTORY_ID}/instructions` });
2466
- const backlog = await client.readResource({ uri: `taiga://backlog/${ITEM_ID}` });
2526
+ const backlog = await client.readResource({ uri: `taiga://initiatives/${ITEM_ID}` });
2467
2527
 
2468
2528
  expect(textOf(instructions.contents)).toContain('## instructions\n\nUse pnpm.');
2469
2529
  expect(backlog.contents[0]!.mimeType).toBe('application/json');
@@ -2475,7 +2535,7 @@ describe('MCP server integration', () => {
2475
2535
  const before = fetchSpy.mock.calls.length;
2476
2536
 
2477
2537
  await expect(client.readResource({ uri: 'taiga://documents/policy_ip' })).rejects.toThrow(/not one of/);
2478
- await expect(client.readResource({ uri: 'taiga://backlog/not-an-id' })).rejects.toThrow(/not an ID/);
2538
+ await expect(client.readResource({ uri: 'taiga://initiatives/not-an-id' })).rejects.toThrow(/not an ID/);
2479
2539
  await expect(client.readResource({ uri: 'taiga://nothing/here' })).rejects.toThrow(/not found/);
2480
2540
  await expect(
2481
2541
  client.readResource({ uri: `taiga://products/${PRODUCT_ID}/documents/architecture?origin=` })
@@ -2601,7 +2661,7 @@ describe('MCP server integration', () => {
2601
2661
  const docsOnly = await bootMcpPair({ capabilities: ['tenant:documents:read'] });
2602
2662
 
2603
2663
  expect((await full.client.listPrompts()).prompts.map((p) => p.name).sort()).toEqual([
2604
- 'taiga_implement_backlog_item',
2664
+ 'taiga_implement_initiative',
2605
2665
  'taiga_review_against_policies',
2606
2666
  'taiga_start_session',
2607
2667
  ]);
@@ -2643,7 +2703,10 @@ describe('MCP server integration', () => {
2643
2703
  routes: { ...routes, [`GET /v1/products/${PRODUCT_ID}`]: { data: named } },
2644
2704
  });
2645
2705
 
2646
- const result = await client.getPrompt({ name: 'taiga_implement_backlog_item', arguments: { itemId: ITEM_ID } });
2706
+ const result = await client.getPrompt({
2707
+ name: 'taiga_implement_initiative',
2708
+ arguments: { initiativeId: ITEM_ID },
2709
+ });
2647
2710
 
2648
2711
  const text = (result.messages[0]!.content as { text: string }).text;
2649
2712
  expect(text.split('## Initiative')[0]).not.toContain('(none apply)');
@@ -2720,25 +2783,28 @@ describe('MCP server integration', () => {
2720
2783
  const before = fetchSpy.mock.calls.length;
2721
2784
 
2722
2785
  const notAnId = await client
2723
- .getPrompt({ name: 'taiga_implement_backlog_item', arguments: { itemId: 'PRO-123' } })
2786
+ .getPrompt({ name: 'taiga_implement_initiative', arguments: { initiativeId: 'PRO-123' } })
2724
2787
  .catch((e: unknown) => e);
2725
2788
  expect(notAnId).toMatchObject({ code: -32602 });
2726
2789
  expect(fetchSpy.mock.calls.length).toBe(before);
2727
2790
 
2728
2791
  const unseen = await client
2729
2792
  .getPrompt({
2730
- name: 'taiga_implement_backlog_item',
2731
- arguments: { itemId: '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a99' },
2793
+ name: 'taiga_implement_initiative',
2794
+ arguments: { initiativeId: '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a99' },
2732
2795
  })
2733
2796
  .catch((e: unknown) => e);
2734
2797
  expect(unseen).toMatchObject({ code: -32602 });
2735
- expect(String((unseen as Error).message)).toContain('taiga_list_backlog_items shows what exists');
2798
+ expect(String((unseen as Error).message)).toContain('taiga_list_initiatives shows what exists');
2736
2799
  });
2737
2800
 
2738
2801
  it('should assemble an initiative with its criteria, the architecture, the instructions and the policy sections', async () => {
2739
2802
  const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES, routes });
2740
2803
 
2741
- const result = await client.getPrompt({ name: 'taiga_implement_backlog_item', arguments: { itemId: ITEM_ID } });
2804
+ const result = await client.getPrompt({
2805
+ name: 'taiga_implement_initiative',
2806
+ arguments: { initiativeId: ITEM_ID },
2807
+ });
2742
2808
 
2743
2809
  const text = (result.messages[0]!.content as { text: string }).text;
2744
2810
  expect(text).toContain('Users can log in');
@@ -2750,7 +2816,10 @@ describe('MCP server integration', () => {
2750
2816
  it('should say which parts a narrower key could not read rather than drop them', async () => {
2751
2817
  const { client } = await bootMcpPair({ capabilities: ['products:read', 'backlog:read'], routes });
2752
2818
 
2753
- const result = await client.getPrompt({ name: 'taiga_implement_backlog_item', arguments: { itemId: ITEM_ID } });
2819
+ const result = await client.getPrompt({
2820
+ name: 'taiga_implement_initiative',
2821
+ arguments: { initiativeId: ITEM_ID },
2822
+ });
2754
2823
 
2755
2824
  const text = (result.messages[0]!.content as { text: string }).text;
2756
2825
  expect(text).toContain('(The factory instructions need factories:read, which this key lacks.)');
@@ -2766,7 +2835,10 @@ describe('MCP server integration', () => {
2766
2835
  return { ok: false, status: 403, json: async () => body, text: async () => JSON.stringify(body) };
2767
2836
  });
2768
2837
 
2769
- const result = await client.getPrompt({ name: 'taiga_implement_backlog_item', arguments: { itemId: ITEM_ID } });
2838
+ const result = await client.getPrompt({
2839
+ name: 'taiga_implement_initiative',
2840
+ arguments: { initiativeId: ITEM_ID },
2841
+ });
2770
2842
 
2771
2843
  const text = (result.messages[0]!.content as { text: string }).text;
2772
2844
  expect(text).toContain(
@@ -2878,12 +2950,12 @@ describe('MCP server integration', () => {
2878
2950
  const byName = Object.fromEntries(tools.map((t) => [t.name, t.annotations]));
2879
2951
 
2880
2952
  for (const name of [
2881
- 'taiga_queue_backlog_item',
2882
- 'taiga_move_backlog_item',
2883
- 'taiga_resume_backlog_item',
2953
+ 'taiga_queue_initiative',
2954
+ 'taiga_move_initiative',
2955
+ 'taiga_resume_initiative',
2884
2956
  'taiga_approve_plan',
2885
2957
  'taiga_reject_plan',
2886
- 'taiga_stop_build',
2958
+ 'taiga_stop_run',
2887
2959
  'taiga_request_initiatives',
2888
2960
  'taiga_plan_again',
2889
2961
  'taiga_define_environment',
@@ -2904,8 +2976,9 @@ describe('MCP server integration', () => {
2904
2976
  }
2905
2977
  // A section id points at the read tool of its own scope, which lists the valid ids.
2906
2978
  const sectionHelp = (name: string) =>
2907
- (tools.find((t) => t.name === name)!.inputSchema.properties as Record<string, { description?: string }>)
2908
- .sectionId!.description;
2979
+ (tools.find((t) => t.name === name)!.inputSchema.properties as Record<string, { description?: string }>)[
2980
+ 'sectionId'
2981
+ ]!.description;
2909
2982
  expect(sectionHelp('taiga_update_document')).toContain('taiga_get_document,');
2910
2983
  expect(sectionHelp('taiga_update_factory_document')).toContain('taiga_get_factory_document');
2911
2984
  expect(sectionHelp('taiga_update_product_document')).toContain('taiga_get_product_document');
@@ -3079,7 +3152,7 @@ describe('MCP server integration', () => {
3079
3152
  routes: { [`PUT /v1/backlog/${ITEM_ID}/state`]: { data: queued } },
3080
3153
  });
3081
3154
 
3082
- const result = await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID } });
3155
+ const result = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
3083
3156
 
3084
3157
  expect(result.isError).toBeFalsy();
3085
3158
  // One write to the item, no item read around it: the route answers with the item (the reads
@@ -3096,12 +3169,15 @@ describe('MCP server integration', () => {
3096
3169
  routes: { [`PUT /v1/backlog/${ITEM_ID}/state`]: { data: queued } },
3097
3170
  });
3098
3171
 
3099
- await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID, afterId: OTHER_ITEM } });
3172
+ await client.callTool({
3173
+ name: 'taiga_queue_initiative',
3174
+ arguments: { initiativeId: ITEM_ID, afterId: OTHER_ITEM },
3175
+ });
3100
3176
  expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'queue', afterId: OTHER_ITEM });
3101
3177
 
3102
3178
  fetchSpy.mockClear();
3103
3179
  vi.stubGlobal('fetch', fetchSpy);
3104
- await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID, afterId: null } });
3180
+ await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID, afterId: null } });
3105
3181
  expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'queue', afterId: null });
3106
3182
  });
3107
3183
 
@@ -3113,7 +3189,7 @@ describe('MCP server integration', () => {
3113
3189
  },
3114
3190
  });
3115
3191
 
3116
- const result = await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID } });
3192
+ const result = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
3117
3193
 
3118
3194
  expect(result.isError).toBe(true);
3119
3195
  expect(outcomeOf(result)).toEqual({ error: 'CONFLICT', message: 'Stop the running agent before re-planning' });
@@ -3122,7 +3198,7 @@ describe('MCP server integration', () => {
3122
3198
  it('should report an item the key cannot see as a miss', async () => {
3123
3199
  const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
3124
3200
 
3125
- const result = await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID } });
3201
+ const result = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
3126
3202
 
3127
3203
  expect(result.isError).toBe(true);
3128
3204
  expect(outcomeOf(result).error).toBe('NOT_FOUND');
@@ -3133,7 +3209,7 @@ describe('MCP server integration', () => {
3133
3209
  const operatorOnly = await bootMcpPair({ capabilities: ['products:write'] });
3134
3210
 
3135
3211
  expect(toolNames((await backlogOnly.client.listTools()).tools)).toEqual([
3136
- 'taiga_move_backlog_item',
3212
+ 'taiga_move_initiative',
3137
3213
  'taiga_set_auto_merge',
3138
3214
  ]);
3139
3215
  // A request, the resume and the review gate take a product or an item ID a request or a queue hands
@@ -3147,7 +3223,7 @@ describe('MCP server integration', () => {
3147
3223
  'taiga_refresh_reality_documents',
3148
3224
  'taiga_reopen_discovery',
3149
3225
  'taiga_request_initiatives',
3150
- 'taiga_resume_backlog_item',
3226
+ 'taiga_resume_initiative',
3151
3227
  'taiga_set_auto_build',
3152
3228
  'taiga_set_product_autonomy',
3153
3229
  'taiga_stop_generating_documents',
@@ -3160,8 +3236,8 @@ describe('MCP server integration', () => {
3160
3236
  const before = narrow.fetchSpy.mock.calls.length;
3161
3237
 
3162
3238
  const result = await narrow.client.callTool({
3163
- name: 'taiga_move_backlog_item',
3164
- arguments: { itemId: ITEM_ID, state: 'queue' },
3239
+ name: 'taiga_move_initiative',
3240
+ arguments: { initiativeId: ITEM_ID, state: 'queue' },
3165
3241
  });
3166
3242
 
3167
3243
  expect(result.isError).toBe(true);
@@ -3181,7 +3257,10 @@ describe('MCP server integration', () => {
3181
3257
  [`PUT /v1/backlog/${ITEM_ID}/state`]: { data: { ...atStation, planningStatus: 'planning' } },
3182
3258
  },
3183
3259
  });
3184
- const replanned = await station.client.callTool({ name: 'taiga_plan_again', arguments: { itemId: ITEM_ID } });
3260
+ const replanned = await station.client.callTool({
3261
+ name: 'taiga_plan_again',
3262
+ arguments: { initiativeId: ITEM_ID },
3263
+ });
3185
3264
  expect(replanned.isError).toBeFalsy();
3186
3265
  expect(sentBody(station.fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'plan' });
3187
3266
  expect(outcomeOf(replanned)).toMatchObject({ state: 'plan', planningStatus: 'planning' });
@@ -3190,7 +3269,7 @@ describe('MCP server integration', () => {
3190
3269
  capabilities: ALL_CAPABILITIES,
3191
3270
  routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: item } },
3192
3271
  });
3193
- const refused = await idle.client.callTool({ name: 'taiga_plan_again', arguments: { itemId: ITEM_ID } });
3272
+ const refused = await idle.client.callTool({ name: 'taiga_plan_again', arguments: { initiativeId: ITEM_ID } });
3194
3273
  expect(refused.isError).toBe(true);
3195
3274
  expect(outcomeOf(refused).message).toMatch(/not at the Build station\. The queue is the only way to start work/);
3196
3275
  // Refused on the read: no move was sent.
@@ -3207,7 +3286,7 @@ describe('MCP server integration', () => {
3207
3286
  },
3208
3287
  });
3209
3288
 
3210
- await client.callTool({ name: 'taiga_plan_again', arguments: { itemId: ITEM_ID, force: true } });
3289
+ await client.callTool({ name: 'taiga_plan_again', arguments: { initiativeId: ITEM_ID, force: true } });
3211
3290
 
3212
3291
  expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'plan', force: true });
3213
3292
  });
@@ -3234,8 +3313,8 @@ describe('MCP server integration', () => {
3234
3313
  'Initiatives are already being generated for this product',
3235
3314
  ],
3236
3315
  [
3237
- 'taiga_resume_backlog_item',
3238
- { itemId: ITEM_ID },
3316
+ 'taiga_resume_initiative',
3317
+ { initiativeId: ITEM_ID },
3239
3318
  'CONFLICT',
3240
3319
  'This initiative is not stopped, so there is nothing to resume.',
3241
3320
  ],
@@ -3255,8 +3334,8 @@ describe('MCP server integration', () => {
3255
3334
  });
3256
3335
 
3257
3336
  const result = await client.callTool({
3258
- name: 'taiga_move_backlog_item',
3259
- arguments: { itemId: ITEM_ID, state: 'todo', afterId: null },
3337
+ name: 'taiga_move_initiative',
3338
+ arguments: { initiativeId: ITEM_ID, state: 'todo', afterId: null },
3260
3339
  });
3261
3340
 
3262
3341
  expect(result.isError).toBeFalsy();
@@ -3275,8 +3354,8 @@ describe('MCP server integration', () => {
3275
3354
  });
3276
3355
 
3277
3356
  const filed = await client.callTool({
3278
- name: 'taiga_move_backlog_item',
3279
- arguments: { itemId: ITEM_ID, state: 'canceled', archived: true },
3357
+ name: 'taiga_move_initiative',
3358
+ arguments: { initiativeId: ITEM_ID, state: 'canceled', archived: true },
3280
3359
  });
3281
3360
 
3282
3361
  expect(filed.isError).toBeFalsy();
@@ -3286,8 +3365,8 @@ describe('MCP server integration', () => {
3286
3365
  fetchSpy.mockClear();
3287
3366
  vi.stubGlobal('fetch', fetchSpy);
3288
3367
  await client.callTool({
3289
- name: 'taiga_move_backlog_item',
3290
- arguments: { itemId: ITEM_ID, state: 'todo', archived: false },
3368
+ name: 'taiga_move_initiative',
3369
+ arguments: { initiativeId: ITEM_ID, state: 'todo', archived: false },
3291
3370
  });
3292
3371
  expect(sentBody(fetchSpy, 'PUT', `/backlog/${ITEM_ID}/state`)).toEqual({ state: 'todo', archived: false });
3293
3372
  });
@@ -3305,8 +3384,8 @@ describe('MCP server integration', () => {
3305
3384
  });
3306
3385
 
3307
3386
  const result = await client.callTool({
3308
- name: 'taiga_move_backlog_item',
3309
- arguments: { itemId: ITEM_ID, state: 'done', archived: true },
3387
+ name: 'taiga_move_initiative',
3388
+ arguments: { initiativeId: ITEM_ID, state: 'done', archived: true },
3310
3389
  });
3311
3390
 
3312
3391
  expect(result.isError).toBe(true);
@@ -3320,16 +3399,16 @@ describe('MCP server integration', () => {
3320
3399
  const { client, fetchSpy } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
3321
3400
 
3322
3401
  const { tools } = await client.listTools();
3323
- const move = tools.find((t) => t.name === 'taiga_move_backlog_item')!;
3402
+ const move = tools.find((t) => t.name === 'taiga_move_initiative')!;
3324
3403
  const props = move.inputSchema.properties as Record<string, { enum?: string[] }>;
3325
3404
  // The queue is the only way to start work: no "plan this now", nothing dropped into Build.
3326
- expect(props.state!.enum).toEqual(['backlog', 'todo', 'queue', 'done', 'canceled']);
3405
+ expect(props['state']!.enum).toEqual(['backlog', 'todo', 'queue', 'done', 'canceled']);
3327
3406
 
3328
3407
  const before = fetchSpy.mock.calls.length;
3329
3408
  for (const state of ['build', 'plan']) {
3330
3409
  const result = await client.callTool({
3331
- name: 'taiga_move_backlog_item',
3332
- arguments: { itemId: ITEM_ID, state },
3410
+ name: 'taiga_move_initiative',
3411
+ arguments: { initiativeId: ITEM_ID, state },
3333
3412
  });
3334
3413
  expect(result.isError, state).toBe(true);
3335
3414
  }
@@ -3345,8 +3424,8 @@ describe('MCP server integration', () => {
3345
3424
  });
3346
3425
 
3347
3426
  const result = await client.callTool({
3348
- name: 'taiga_move_backlog_item',
3349
- arguments: { itemId: ITEM_ID, state: 'todo' },
3427
+ name: 'taiga_move_initiative',
3428
+ arguments: { initiativeId: ITEM_ID, state: 'todo' },
3350
3429
  });
3351
3430
 
3352
3431
  expect(result.isError).toBe(true);
@@ -3360,7 +3439,7 @@ describe('MCP server integration', () => {
3360
3439
  routes: { [`POST /v1/backlog/${ITEM_ID}/resume`]: { data: resumed } },
3361
3440
  });
3362
3441
 
3363
- const result = await client.callTool({ name: 'taiga_resume_backlog_item', arguments: { itemId: ITEM_ID } });
3442
+ const result = await client.callTool({ name: 'taiga_resume_initiative', arguments: { initiativeId: ITEM_ID } });
3364
3443
 
3365
3444
  expect(result.isError).toBeFalsy();
3366
3445
  const call = fetchSpy.mock.calls.find((c: unknown[]) => (c[0] as string).endsWith('/resume'))!;
@@ -3378,11 +3457,11 @@ describe('MCP server integration', () => {
3378
3457
  },
3379
3458
  });
3380
3459
 
3381
- const found = await client.callTool({ name: 'taiga_get_plan', arguments: { itemId: ITEM_ID } });
3460
+ const found = await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: ITEM_ID } });
3382
3461
  // A summary by default: the step's prose, timestamps and planId stay behind.
3383
3462
  expect(outcomeOf(found)).toEqual({
3384
3463
  id: PLAN_ID,
3385
- backlogItemId: ITEM_ID,
3464
+ initiativeId: ITEM_ID,
3386
3465
  version: 1,
3387
3466
  status: 'active',
3388
3467
  steps: [
@@ -3399,19 +3478,21 @@ describe('MCP server integration', () => {
3399
3478
  ],
3400
3479
  });
3401
3480
 
3402
- const full = await client.callTool({ name: 'taiga_get_plan', arguments: { itemId: ITEM_ID, full: true } });
3481
+ const full = await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: ITEM_ID, full: true } });
3403
3482
  const { tenantId: _t, ...planWithoutTenant } = plan;
3404
- expect(outcomeOf(full)).toEqual({
3405
- ...planWithoutTenant,
3406
- steps: plan.steps.map(({ tenantId: _s, ...step }) => step),
3407
- });
3483
+ expect(outcomeOf(full)).toEqual(
3484
+ asSeen({
3485
+ ...planWithoutTenant,
3486
+ steps: plan.steps.map(({ tenantId: _s, ...step }) => step),
3487
+ })
3488
+ );
3408
3489
 
3409
- const none = await client.callTool({ name: 'taiga_get_plan', arguments: { itemId: OTHER_ITEM } });
3490
+ const none = await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: OTHER_ITEM } });
3410
3491
  expect(none.isError).toBe(true);
3411
3492
  expect(outcomeOf(none)).toEqual({
3412
3493
  error: 'NOT_FOUND',
3413
3494
  message:
3414
- 'No active plan found. taiga_get_backlog_item says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one.',
3495
+ 'No active plan found. taiga_get_initiative says whether one is being generated (planningStatus planning) or failed, and taiga_plan_again asks for a new one.',
3415
3496
  });
3416
3497
  });
3417
3498
 
@@ -3472,18 +3553,20 @@ describe('MCP server integration', () => {
3472
3553
  },
3473
3554
  });
3474
3555
 
3475
- const stop = await client.callTool({ name: 'taiga_stop_build', arguments: { buildId: BUILD_ID } });
3556
+ const stop = await client.callTool({ name: 'taiga_stop_run', arguments: { runId: BUILD_ID } });
3476
3557
  expect(stop.isError).toBeFalsy();
3477
3558
  expect(outcomeOf(stop)).toEqual({
3478
3559
  id: BUILD_ID,
3479
3560
  runNumber: 1,
3480
- backlogItemId: ITEM_ID,
3561
+ initiativeId: ITEM_ID,
3481
3562
  status: 'stopped',
3482
3563
  failedReason: 'user_stopped',
3483
3564
  completedSteps: 1,
3484
3565
  totalSteps: 3,
3485
3566
  prUrl: null,
3486
3567
  actions: ['resume', 'start_over'],
3568
+ disposition: 'stopped',
3569
+ outcome: 'Stopped before it finished (1 of 3 steps done).',
3487
3570
  });
3488
3571
  const call = fetchSpy.mock.calls.find((c: unknown[]) => (c[0] as string).endsWith('/stop'))!;
3489
3572
  expect((call[1] as RequestInit).body).toBeNull();
@@ -3497,7 +3580,7 @@ describe('MCP server integration', () => {
3497
3580
  },
3498
3581
  });
3499
3582
 
3500
- const result = await client.callTool({ name: 'taiga_stop_build', arguments: { buildId: BUILD_ID } });
3583
+ const result = await client.callTool({ name: 'taiga_stop_run', arguments: { runId: BUILD_ID } });
3501
3584
 
3502
3585
  expect(result.isError).toBe(true);
3503
3586
  expect(outcomeOf(result)).toEqual({ error: 'CONFLICT', message: 'Cannot stop a run that is completed' });
@@ -3519,7 +3602,7 @@ describe('MCP server integration', () => {
3519
3602
  },
3520
3603
  });
3521
3604
 
3522
- const result = await client.callTool({ name: 'taiga_list_builds', arguments: { active: true, limit: 10 } });
3605
+ const result = await client.callTool({ name: 'taiga_list_runs', arguments: { active: true, limit: 10 } });
3523
3606
 
3524
3607
  expect(result.isError).toBeFalsy();
3525
3608
  const { data, nextCursor } = outcomeOf(result);
@@ -3540,11 +3623,11 @@ describe('MCP server integration', () => {
3540
3623
  expect(sentUrl(fetchSpy, '/products').searchParams.get('limit')).toBe('100');
3541
3624
  });
3542
3625
 
3543
- it('should refuse backlogItemId without productId before any request rather than answer with every run', async () => {
3626
+ it('should refuse initiativeId without productId before any request rather than answer with every run', async () => {
3544
3627
  const { client, fetchSpy } = await bootMcpPair({ capabilities: ['products:read'] });
3545
3628
  const before = fetchSpy.mock.calls.length;
3546
3629
 
3547
- const result = await client.callTool({ name: 'taiga_list_builds', arguments: { backlogItemId: ITEM_ID } });
3630
+ const result = await client.callTool({ name: 'taiga_list_runs', arguments: { initiativeId: ITEM_ID } });
3548
3631
 
3549
3632
  expect(result.isError).toBe(true);
3550
3633
  expect(outcomeOf(result)).toMatchObject({ error: 'VALIDATION_ERROR' });
@@ -3560,7 +3643,7 @@ describe('MCP server integration', () => {
3560
3643
  },
3561
3644
  });
3562
3645
 
3563
- const result = await client.callTool({ name: 'taiga_list_builds', arguments: {} });
3646
+ const result = await client.callTool({ name: 'taiga_list_runs', arguments: {} });
3564
3647
 
3565
3648
  const { data, note } = outcomeOf(result);
3566
3649
  expect(data[0]).not.toHaveProperty('product');
@@ -3576,7 +3659,7 @@ describe('MCP server integration', () => {
3576
3659
  },
3577
3660
  });
3578
3661
 
3579
- const { data, note } = outcomeOf(await client.callTool({ name: 'taiga_list_builds', arguments: {} }));
3662
+ const { data, note } = outcomeOf(await client.callTool({ name: 'taiga_list_runs', arguments: {} }));
3580
3663
 
3581
3664
  expect(data[0]).not.toHaveProperty('product');
3582
3665
  expect(note).toMatch(/cannot list \(deleted since, or not visible/);
@@ -3592,7 +3675,7 @@ describe('MCP server integration', () => {
3592
3675
  },
3593
3676
  });
3594
3677
 
3595
- const result = outcomeOf(await client.callTool({ name: 'taiga_list_builds', arguments: {} }));
3678
+ const result = outcomeOf(await client.callTool({ name: 'taiga_list_runs', arguments: {} }));
3596
3679
 
3597
3680
  expect(result.data[0].product).toEqual({ name: 'Web', slug: 'web' });
3598
3681
  expect(result).not.toHaveProperty('note');
@@ -3608,7 +3691,7 @@ describe('MCP server integration', () => {
3608
3691
  },
3609
3692
  },
3610
3693
  });
3611
- const result = outcomeOf(await offline.client.callTool({ name: 'taiga_list_builds', arguments: {} }));
3694
+ const result = outcomeOf(await offline.client.callTool({ name: 'taiga_list_runs', arguments: {} }));
3612
3695
  expect(result.data[0]).not.toHaveProperty('product');
3613
3696
  expect(result.note).toMatch(/products could not be read/);
3614
3697
 
@@ -3621,7 +3704,7 @@ describe('MCP server integration', () => {
3621
3704
  },
3622
3705
  },
3623
3706
  });
3624
- const failure = await buggy.client.callTool({ name: 'taiga_list_builds', arguments: {} });
3707
+ const failure = await buggy.client.callTool({ name: 'taiga_list_runs', arguments: {} });
3625
3708
  expect(failure.isError).toBe(true);
3626
3709
  expect(JSON.stringify(failure.content)).toContain('a bug in the products path');
3627
3710
  expect(JSON.stringify(failure.content)).not.toContain('could not be read');
@@ -3636,7 +3719,7 @@ describe('MCP server integration', () => {
3636
3719
  },
3637
3720
  });
3638
3721
 
3639
- const result = await client.callTool({ name: 'taiga_list_builds', arguments: {} });
3722
+ const result = await client.callTool({ name: 'taiga_list_runs', arguments: {} });
3640
3723
 
3641
3724
  expect(result.isError).toBeFalsy();
3642
3725
  const { data, note } = outcomeOf(result);
@@ -3655,7 +3738,7 @@ describe('MCP server integration', () => {
3655
3738
  },
3656
3739
  });
3657
3740
 
3658
- expect(outcomeOf(await client.callTool({ name: 'taiga_list_builds', arguments: {} }))).not.toHaveProperty('note');
3741
+ expect(outcomeOf(await client.callTool({ name: 'taiga_list_runs', arguments: {} }))).not.toHaveProperty('note');
3659
3742
  });
3660
3743
 
3661
3744
  it('should refuse active with productId before any request rather than drop the filter', async () => {
@@ -3663,7 +3746,7 @@ describe('MCP server integration', () => {
3663
3746
  const before = fetchSpy.mock.calls.length;
3664
3747
 
3665
3748
  const result = await client.callTool({
3666
- name: 'taiga_list_builds',
3749
+ name: 'taiga_list_runs',
3667
3750
  arguments: { productId: PRODUCT_ID, active: true },
3668
3751
  });
3669
3752
 
@@ -3678,7 +3761,7 @@ describe('MCP server integration', () => {
3678
3761
  routes: { [`/v1/products/${PRODUCT_ID}/builds`]: { data: [run], nextCursor: null } },
3679
3762
  });
3680
3763
 
3681
- const result = await client.callTool({ name: 'taiga_list_builds', arguments: { productId: PRODUCT_ID } });
3764
+ const result = await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } });
3682
3765
 
3683
3766
  expect(result.isError).toBeFalsy();
3684
3767
  expect(fetchSpy.mock.calls.some((c: unknown[]) => (c[0] as string).endsWith('/v1/products?limit=100'))).toBe(
@@ -3757,7 +3840,7 @@ describe('MCP server integration', () => {
3757
3840
  });
3758
3841
 
3759
3842
  const rows = parseToolResult(
3760
- await client.callTool({ name: 'taiga_list_backlog_items', arguments: { productId: PRODUCT_ID } })
3843
+ await client.callTool({ name: 'taiga_list_initiatives', arguments: { productId: PRODUCT_ID } })
3761
3844
  ).data;
3762
3845
 
3763
3846
  expect(rows[0].criteria).toEqual({ met: 1, total: 2 });
@@ -3790,9 +3873,7 @@ describe('MCP server integration', () => {
3790
3873
  routes: { [`/v1/builds/${BUILD_ID}`]: { data: run }, [`/v1/builds/${BUILD_ID}/steps`]: { data: steps } },
3791
3874
  });
3792
3875
 
3793
- const result = parseToolResult(
3794
- await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
3795
- );
3876
+ const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
3796
3877
 
3797
3878
  expect(result).toMatchObject(run);
3798
3879
  expect(result.steps[0]).toEqual({
@@ -3827,7 +3908,7 @@ describe('MCP server integration', () => {
3827
3908
  });
3828
3909
 
3829
3910
  const queue = parseToolResult(
3830
- await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
3911
+ await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
3831
3912
  );
3832
3913
 
3833
3914
  expect(queue.entries.map((row: { title: string }) => row.title)).toEqual(['ARC-3.3 OpenAPI', 'README']);
@@ -3859,10 +3940,10 @@ describe('MCP server integration', () => {
3859
3940
  });
3860
3941
 
3861
3942
  const queue = parseToolResult(
3862
- await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
3943
+ await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
3863
3944
  );
3864
3945
 
3865
- expect(queue.entries[0]).toMatchObject({ buildId: BUILD_ID, prUrl: 'https://github.com/acme/web/pull/22' });
3946
+ expect(queue.entries[0]).toMatchObject({ runId: BUILD_ID, prUrl: 'https://github.com/acme/web/pull/22' });
3866
3947
  expect(queue.waitingOn).toBe(
3867
3948
  '"ARC-3.3 OpenAPI" is waiting on a person (review_pr) (https://github.com/acme/web/pull/22); every other queued initiative waits until its pull request merges.'
3868
3949
  );
@@ -3886,12 +3967,12 @@ describe('MCP server integration', () => {
3886
3967
  });
3887
3968
 
3888
3969
  const queue = parseToolResult(
3889
- await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
3970
+ await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
3890
3971
  );
3891
3972
  expect(queue.waitingOn).toMatch(/^"ARC-3.3 OpenAPI" is waiting on a person/);
3892
3973
 
3893
3974
  fetchSpy.mockClear();
3894
- const queued = await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID } });
3975
+ const queued = await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } });
3895
3976
  expect(queued.isError).toBeFalsy();
3896
3977
  // Placing an item names only the entry the line waits on: the active one.
3897
3978
  const itemReads = fetchSpy.mock.calls
@@ -3914,7 +3995,7 @@ describe('MCP server integration', () => {
3914
3995
  });
3915
3996
 
3916
3997
  const queue = parseToolResult(
3917
- await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
3998
+ await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
3918
3999
  );
3919
4000
 
3920
4001
  expect(queue.waitingOn).toContain('holding the entries behind it');
@@ -3936,7 +4017,7 @@ describe('MCP server integration', () => {
3936
4017
  });
3937
4018
 
3938
4019
  const queue = parseToolResult(
3939
- await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
4020
+ await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
3940
4021
  );
3941
4022
 
3942
4023
  expect(queue.waitingOn).toBe(
@@ -3963,7 +4044,7 @@ describe('MCP server integration', () => {
3963
4044
  expect(pause).toEqual({ productId: PRODUCT_ID, linePaused: true, lineStoppedAt: '2026-10-08T20:00:00.000Z' });
3964
4045
 
3965
4046
  const queue = parseToolResult(
3966
- await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
4047
+ await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
3967
4048
  );
3968
4049
  expect(queue.linePaused).toBe(true);
3969
4050
 
@@ -3988,7 +4069,7 @@ describe('MCP server integration', () => {
3988
4069
  // One pair at a time: each boot stubs the global fetch the previous client reads through.
3989
4070
  const noBacklog = await bootMcpPair({ capabilities: ['products:read'], routes: queueRoute });
3990
4071
  const without = parseToolResult(
3991
- await noBacklog.client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
4072
+ await noBacklog.client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
3992
4073
  );
3993
4074
  expect(noBacklog.fetchSpy.mock.calls.some((c: unknown[]) => (c[0] as string).includes('/backlog/'))).toBe(false);
3994
4075
  const failing = await bootMcpPair({
@@ -3996,10 +4077,10 @@ describe('MCP server integration', () => {
3996
4077
  routes: { ...queueRoute, [`/v1/backlog/${HEAD_ITEM}`]: refusal(500, 'INTERNAL', 'boom') },
3997
4078
  });
3998
4079
  const failed = parseToolResult(
3999
- await failing.client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
4080
+ await failing.client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
4000
4081
  );
4001
4082
 
4002
- expect(without.entries[0]).toEqual({ position: 1, backlogItemId: HEAD_ITEM, entryState: 'active' });
4083
+ expect(without.entries[0]).toEqual({ position: 1, initiativeId: HEAD_ITEM, entryState: 'active' });
4003
4084
  expect(without.note).toContain('backlog:read');
4004
4085
  expect(failed.entries[0]).not.toHaveProperty('title');
4005
4086
  expect(failed.note).toContain('could not be read');
@@ -4019,7 +4100,10 @@ describe('MCP server integration', () => {
4019
4100
  });
4020
4101
 
4021
4102
  const result = parseToolResult(
4022
- await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID, autoBuild: false } })
4103
+ await client.callTool({
4104
+ name: 'taiga_queue_initiative',
4105
+ arguments: { initiativeId: ITEM_ID, autoBuild: false },
4106
+ })
4023
4107
  );
4024
4108
 
4025
4109
  // To place one item and say what the line waits on, only the blocking entry is read.
@@ -4055,14 +4139,14 @@ describe('MCP server integration', () => {
4055
4139
  });
4056
4140
 
4057
4141
  const result = parseToolResult(
4058
- await client.callTool({ name: 'taiga_queue_backlog_item', arguments: { itemId: ITEM_ID } })
4142
+ await client.callTool({ name: 'taiga_queue_initiative', arguments: { initiativeId: ITEM_ID } })
4059
4143
  );
4060
4144
 
4061
4145
  expect(fetchSpy.mock.calls.some((c: unknown[]) => (c[0] as string).includes('/auto-build'))).toBe(false);
4062
4146
  expect(result.reviewGate).toMatchObject({ buildsWithoutReview: true, decidedBy: 'product' });
4063
4147
  // The queue read missed (no route); the outcome says so rather than failing the write it reports.
4064
4148
  expect(result.queue).toBeNull();
4065
- expect(result.note).toContain('taiga_get_queue');
4149
+ expect(result.note).toContain('taiga_get_line');
4066
4150
  });
4067
4151
 
4068
4152
  it('should set an initiative back to the product default with null and report the gate that now applies', async () => {
@@ -4075,7 +4159,7 @@ describe('MCP server integration', () => {
4075
4159
  });
4076
4160
 
4077
4161
  const result = parseToolResult(
4078
- await client.callTool({ name: 'taiga_set_auto_build', arguments: { itemId: ITEM_ID, autoBuild: null } })
4162
+ await client.callTool({ name: 'taiga_set_auto_build', arguments: { initiativeId: ITEM_ID, autoBuild: null } })
4079
4163
  );
4080
4164
 
4081
4165
  expect(sentBody(fetchSpy, 'PUT', '/auto-build')).toEqual({ autoBuild: null });
@@ -4097,8 +4181,8 @@ describe('MCP server integration', () => {
4097
4181
 
4098
4182
  const result = parseToolResult(
4099
4183
  await client.callTool({
4100
- name: 'taiga_wait_for_backlog_item',
4101
- arguments: { itemId: ITEM_ID, until: 'planned', timeoutSeconds: 30 },
4184
+ name: 'taiga_wait_for_initiative',
4185
+ arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 30 },
4102
4186
  })
4103
4187
  );
4104
4188
 
@@ -4122,7 +4206,10 @@ describe('MCP server integration', () => {
4122
4206
  });
4123
4207
 
4124
4208
  const result = parseToolResult(
4125
- await client.callTool({ name: 'taiga_wait_for_backlog_item', arguments: { itemId: ITEM_ID, until: 'pr_open' } })
4209
+ await client.callTool({
4210
+ name: 'taiga_wait_for_initiative',
4211
+ arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
4212
+ })
4126
4213
  );
4127
4214
 
4128
4215
  expect(result).toMatchObject({ reached: false, stoppedBecause: 'needs a person: planning_failed' });
@@ -4142,8 +4229,8 @@ describe('MCP server integration', () => {
4142
4229
  });
4143
4230
  const plan = parseToolResult(
4144
4231
  await verdict.client.callTool({
4145
- name: 'taiga_wait_for_backlog_item',
4146
- arguments: { itemId: ITEM_ID, until: 'pr_open' },
4232
+ name: 'taiga_wait_for_initiative',
4233
+ arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
4147
4234
  })
4148
4235
  );
4149
4236
  expect(plan).toMatchObject({ reached: false, stoppedBecause: 'waits on a person: start_build' });
@@ -4155,8 +4242,8 @@ describe('MCP server integration', () => {
4155
4242
  });
4156
4243
  const unsure = parseToolResult(
4157
4244
  await unknown.client.callTool({
4158
- name: 'taiga_wait_for_backlog_item',
4159
- arguments: { itemId: ITEM_ID, until: 'your_turn' },
4245
+ name: 'taiga_wait_for_initiative',
4246
+ arguments: { initiativeId: ITEM_ID, until: 'your_turn' },
4160
4247
  })
4161
4248
  );
4162
4249
  expect(unsure.reached).toBe(false);
@@ -4168,8 +4255,8 @@ describe('MCP server integration', () => {
4168
4255
  });
4169
4256
  const pr = parseToolResult(
4170
4257
  await review.client.callTool({
4171
- name: 'taiga_wait_for_backlog_item',
4172
- arguments: { itemId: ITEM_ID, until: 'done' },
4258
+ name: 'taiga_wait_for_initiative',
4259
+ arguments: { initiativeId: ITEM_ID, until: 'done' },
4173
4260
  })
4174
4261
  );
4175
4262
  expect(pr).toMatchObject({ reached: false, stoppedBecause: 'waits on a person: review_pr' });
@@ -4195,8 +4282,8 @@ describe('MCP server integration', () => {
4195
4282
  const started = Date.now();
4196
4283
  const result = parseToolResult(
4197
4284
  await client.callTool({
4198
- name: 'taiga_wait_for_backlog_item',
4199
- arguments: { itemId: ITEM_ID, until: 'planned', timeoutSeconds: 1 },
4285
+ name: 'taiga_wait_for_initiative',
4286
+ arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 1 },
4200
4287
  })
4201
4288
  );
4202
4289
 
@@ -4217,8 +4304,8 @@ describe('MCP server integration', () => {
4217
4304
  });
4218
4305
 
4219
4306
  const result = await client.callTool({
4220
- name: 'taiga_wait_for_backlog_item',
4221
- arguments: { itemId: ITEM_ID, until: 'planned', timeoutSeconds: 1 },
4307
+ name: 'taiga_wait_for_initiative',
4308
+ arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 1 },
4222
4309
  });
4223
4310
 
4224
4311
  expect(result.isError).toBeFalsy();
@@ -4243,7 +4330,10 @@ describe('MCP server integration', () => {
4243
4330
  });
4244
4331
 
4245
4332
  const result = parseToolResult(
4246
- await client.callTool({ name: 'taiga_wait_for_backlog_item', arguments: { itemId: ITEM_ID, until: 'changed' } })
4333
+ await client.callTool({
4334
+ name: 'taiga_wait_for_initiative',
4335
+ arguments: { initiativeId: ITEM_ID, until: 'changed' },
4336
+ })
4247
4337
  );
4248
4338
 
4249
4339
  expect(result).toMatchObject({ reached: true, item: { latestBuild: { prState: 'closed' } } });
@@ -4267,7 +4357,10 @@ describe('MCP server integration', () => {
4267
4357
  const controller = new AbortController();
4268
4358
 
4269
4359
  const pending = client.callTool(
4270
- { name: 'taiga_wait_for_backlog_item', arguments: { itemId: ITEM_ID, until: 'planned', timeoutSeconds: 60 } },
4360
+ {
4361
+ name: 'taiga_wait_for_initiative',
4362
+ arguments: { initiativeId: ITEM_ID, until: 'planned', timeoutSeconds: 60 },
4363
+ },
4271
4364
  { signal: controller.signal }
4272
4365
  );
4273
4366
  await vi.waitFor(() => expect(hung).toBeDefined());
@@ -4300,8 +4393,8 @@ describe('MCP server integration', () => {
4300
4393
  });
4301
4394
  const pr = parseToolResult(
4302
4395
  await shipped.client.callTool({
4303
- name: 'taiga_wait_for_backlog_item',
4304
- arguments: { itemId: ITEM_ID, until: 'pr_open' },
4396
+ name: 'taiga_wait_for_initiative',
4397
+ arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
4305
4398
  })
4306
4399
  );
4307
4400
  expect(pr).toMatchObject({ reached: false, stoppedBecause: 'the initiative is done' });
@@ -4317,8 +4410,8 @@ describe('MCP server integration', () => {
4317
4410
  });
4318
4411
  const done = parseToolResult(
4319
4412
  await dropped.client.callTool({
4320
- name: 'taiga_wait_for_backlog_item',
4321
- arguments: { itemId: ITEM_ID, until: 'done' },
4413
+ name: 'taiga_wait_for_initiative',
4414
+ arguments: { initiativeId: ITEM_ID, until: 'done' },
4322
4415
  })
4323
4416
  );
4324
4417
  expect(done).toMatchObject({ reached: false, stoppedBecause: 'the initiative is canceled' });
@@ -4337,8 +4430,8 @@ describe('MCP server integration', () => {
4337
4430
  });
4338
4431
  const runPr = parseToolResult(
4339
4432
  await finished.client.callTool({
4340
- name: 'taiga_wait_for_build',
4341
- arguments: { buildId: BUILD_ID, until: 'pr_open' },
4433
+ name: 'taiga_wait_for_run',
4434
+ arguments: { runId: BUILD_ID, until: 'pr_open' },
4342
4435
  })
4343
4436
  );
4344
4437
  expect(runPr).toMatchObject({ reached: false, stoppedBecause: 'the run is completed' });
@@ -4373,7 +4466,10 @@ describe('MCP server integration', () => {
4373
4466
  });
4374
4467
 
4375
4468
  const result = parseToolResult(
4376
- await client.callTool({ name: 'taiga_wait_for_backlog_item', arguments: { itemId: ITEM_ID, until: 'pr_open' } })
4469
+ await client.callTool({
4470
+ name: 'taiga_wait_for_initiative',
4471
+ arguments: { initiativeId: ITEM_ID, until: 'pr_open' },
4472
+ })
4377
4473
  );
4378
4474
 
4379
4475
  // Not stopped at the landed plan: the open gate builds it without anyone.
@@ -4401,8 +4497,8 @@ describe('MCP server integration', () => {
4401
4497
 
4402
4498
  const result = parseToolResult(
4403
4499
  await client.callTool({
4404
- name: 'taiga_wait_for_backlog_item',
4405
- arguments: { itemId: ITEM_ID, until: 'your_turn' },
4500
+ name: 'taiga_wait_for_initiative',
4501
+ arguments: { initiativeId: ITEM_ID, until: 'your_turn' },
4406
4502
  })
4407
4503
  );
4408
4504
 
@@ -4435,8 +4531,8 @@ describe('MCP server integration', () => {
4435
4531
 
4436
4532
  const result = parseToolResult(
4437
4533
  await client.callTool({
4438
- name: 'taiga_wait_for_backlog_item',
4439
- arguments: { itemId: ITEM_ID, until: 'your_turn' },
4534
+ name: 'taiga_wait_for_initiative',
4535
+ arguments: { initiativeId: ITEM_ID, until: 'your_turn' },
4440
4536
  })
4441
4537
  );
4442
4538
 
@@ -4459,7 +4555,10 @@ describe('MCP server integration', () => {
4459
4555
  });
4460
4556
 
4461
4557
  const result = parseToolResult(
4462
- await client.callTool({ name: 'taiga_wait_for_backlog_item', arguments: { itemId: ITEM_ID, until: 'changed' } })
4558
+ await client.callTool({
4559
+ name: 'taiga_wait_for_initiative',
4560
+ arguments: { initiativeId: ITEM_ID, until: 'changed' },
4561
+ })
4463
4562
  );
4464
4563
 
4465
4564
  expect(result).toMatchObject({ reached: true, item: { autoBuild: false } });
@@ -4473,8 +4572,8 @@ describe('MCP server integration', () => {
4473
4572
 
4474
4573
  const result = parseToolResult(
4475
4574
  await client.callTool({
4476
- name: 'taiga_wait_for_backlog_item',
4477
- arguments: { itemId: ITEM_ID, until: 'changed', timeoutSeconds: 1 },
4575
+ name: 'taiga_wait_for_initiative',
4576
+ arguments: { initiativeId: ITEM_ID, until: 'changed', timeoutSeconds: 1 },
4478
4577
  })
4479
4578
  );
4480
4579
 
@@ -4491,7 +4590,7 @@ describe('MCP server integration', () => {
4491
4590
  const onprogress = vi.fn();
4492
4591
 
4493
4592
  const result = await client.callTool(
4494
- { name: 'taiga_wait_for_backlog_item', arguments: { itemId: ITEM_ID, until: 'changed' } },
4593
+ { name: 'taiga_wait_for_initiative', arguments: { initiativeId: ITEM_ID, until: 'changed' } },
4495
4594
  { onprogress }
4496
4595
  );
4497
4596
 
@@ -4508,8 +4607,8 @@ describe('MCP server integration', () => {
4508
4607
  });
4509
4608
  const step = parseToolResult(
4510
4609
  await stepped.client.callTool({
4511
- name: 'taiga_wait_for_build',
4512
- arguments: { buildId: BUILD_ID, until: 'step_done' },
4610
+ name: 'taiga_wait_for_run',
4611
+ arguments: { runId: BUILD_ID, until: 'step_done' },
4513
4612
  })
4514
4613
  );
4515
4614
  const failed = await bootMcpPair({
@@ -4518,8 +4617,8 @@ describe('MCP server integration', () => {
4518
4617
  });
4519
4618
  const settled = parseToolResult(
4520
4619
  await failed.client.callTool({
4521
- name: 'taiga_wait_for_build',
4522
- arguments: { buildId: BUILD_ID, until: 'pr_open' },
4620
+ name: 'taiga_wait_for_run',
4621
+ arguments: { runId: BUILD_ID, until: 'pr_open' },
4523
4622
  })
4524
4623
  );
4525
4624
 
@@ -4538,8 +4637,8 @@ describe('MCP server integration', () => {
4538
4637
  });
4539
4638
  const result = parseToolResult(
4540
4639
  await recovered.client.callTool({
4541
- name: 'taiga_wait_for_backlog_item',
4542
- arguments: { itemId: ITEM_ID, until: 'planned' },
4640
+ name: 'taiga_wait_for_initiative',
4641
+ arguments: { initiativeId: ITEM_ID, until: 'planned' },
4543
4642
  })
4544
4643
  );
4545
4644
  expect(result.reached).toBe(true);
@@ -4550,8 +4649,8 @@ describe('MCP server integration', () => {
4550
4649
  routes: { [`/v1/backlog/${ITEM_ID}`]: () => (calls++ === 0 ? answer({ data: item }) : failing) },
4551
4650
  });
4552
4651
  const failed = await broken.client.callTool({
4553
- name: 'taiga_wait_for_backlog_item',
4554
- arguments: { itemId: ITEM_ID, until: 'planned' },
4652
+ name: 'taiga_wait_for_initiative',
4653
+ arguments: { initiativeId: ITEM_ID, until: 'planned' },
4555
4654
  });
4556
4655
  expect(failed.isError).toBe(true);
4557
4656
  expect(calls).toBe(4);
@@ -4565,8 +4664,8 @@ describe('MCP server integration', () => {
4565
4664
  });
4566
4665
  const stopped = parseToolResult(
4567
4666
  await planning.client.callTool({
4568
- name: 'taiga_wait_for_backlog_item',
4569
- arguments: { itemId: ITEM_ID, until: 'planned' },
4667
+ name: 'taiga_wait_for_initiative',
4668
+ arguments: { initiativeId: ITEM_ID, until: 'planned' },
4570
4669
  })
4571
4670
  );
4572
4671
  expect(stopped).toMatchObject({ reached: false, stoppedBecause: 'planning failed' });
@@ -4578,8 +4677,8 @@ describe('MCP server integration', () => {
4578
4677
  });
4579
4678
  const changed = parseToolResult(
4580
4679
  await waiting.client.callTool({
4581
- name: 'taiga_wait_for_backlog_item',
4582
- arguments: { itemId: ITEM_ID, until: 'changed' },
4680
+ name: 'taiga_wait_for_initiative',
4681
+ arguments: { initiativeId: ITEM_ID, until: 'changed' },
4583
4682
  })
4584
4683
  );
4585
4684
  expect(changed).toMatchObject({ reached: true, item: { state: 'plan', needsHumanCause: null } });
@@ -4595,8 +4694,8 @@ describe('MCP server integration', () => {
4595
4694
  });
4596
4695
 
4597
4696
  const result = await client.callTool({
4598
- name: 'taiga_queue_backlog_item',
4599
- arguments: { itemId: ITEM_ID, autoBuild: false },
4697
+ name: 'taiga_queue_initiative',
4698
+ arguments: { initiativeId: ITEM_ID, autoBuild: false },
4600
4699
  });
4601
4700
 
4602
4701
  expect(result.isError).toBe(true);
@@ -4614,7 +4713,7 @@ describe('MCP server integration', () => {
4614
4713
  });
4615
4714
 
4616
4715
  const result = parseToolResult(
4617
- await client.callTool({ name: 'taiga_set_auto_build', arguments: { itemId: ITEM_ID, autoBuild: null } })
4716
+ await client.callTool({ name: 'taiga_set_auto_build', arguments: { initiativeId: ITEM_ID, autoBuild: null } })
4618
4717
  );
4619
4718
 
4620
4719
  expect(result.reviewGate).toBeNull();
@@ -4628,7 +4727,7 @@ describe('MCP server integration', () => {
4628
4727
  });
4629
4728
 
4630
4729
  const result = parseToolResult(
4631
- await client.callTool({ name: 'taiga_set_auto_build', arguments: { itemId: ITEM_ID, autoBuild: false } })
4730
+ await client.callTool({ name: 'taiga_set_auto_build', arguments: { initiativeId: ITEM_ID, autoBuild: false } })
4632
4731
  );
4633
4732
 
4634
4733
  expect(result.reviewGate).toMatchObject({ buildsWithoutReview: false, decidedBy: 'initiative' });
@@ -4653,17 +4752,17 @@ describe('MCP server integration', () => {
4653
4752
  });
4654
4753
 
4655
4754
  const [failed, refused, unknown] = parseToolResult(
4656
- await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } })
4755
+ await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } })
4657
4756
  ).entries;
4658
4757
 
4659
4758
  expect(failed.halt.nextStep).toMatch(/^It clears when the run is retried/);
4660
4759
  expect(refused.halt).toMatchObject({
4661
4760
  reason: 'No seats',
4662
- nextStep: expect.stringMatching(/^taiga_resume_backlog_item lifts it/),
4761
+ nextStep: expect.stringMatching(/^taiga_resume_initiative lifts it/),
4663
4762
  });
4664
4763
  expect(unknown.halt).toMatchObject({
4665
4764
  reason: 'Stored prose',
4666
- nextStep: expect.stringContaining('taiga_get_backlog_item'),
4765
+ nextStep: expect.stringContaining('taiga_get_initiative'),
4667
4766
  });
4668
4767
  });
4669
4768
 
@@ -4678,7 +4777,7 @@ describe('MCP server integration', () => {
4678
4777
  },
4679
4778
  });
4680
4779
 
4681
- const result = await client.callTool({ name: 'taiga_get_queue', arguments: { productId: PRODUCT_ID } });
4780
+ const result = await client.callTool({ name: 'taiga_get_line', arguments: { productId: PRODUCT_ID } });
4682
4781
 
4683
4782
  expect(result.isError).toBe(true);
4684
4783
  expect((result.content as { text: string }[])[0]!.text).toContain('a bug, not the API');
@@ -4815,14 +4914,10 @@ describe('MCP server integration', () => {
4815
4914
  // One shape whatever matched (PRO-2778): none right still answers, listing every real id with its size.
4816
4915
  expect(miss.isError).toBeFalsy();
4817
4916
  const missed = parseToolResult(miss);
4818
- expect(missed.data.sections).toEqual({});
4917
+ // No text matched, so no empty data.sections beside the ids (PRO-2800).
4918
+ expect(missed).not.toHaveProperty('data');
4819
4919
  expect(missed.unknownSections).toEqual(['nope']);
4820
- expect(missed.omittedSections.map((s: { id: string }) => s.id)).toEqual([
4821
- 'overview',
4822
- 'features',
4823
- 'flows',
4824
- 'risks',
4825
- ]);
4920
+ expect(missed.outline.map((s: { id: string }) => s.id)).toEqual(['overview', 'features', 'flows', 'risks']);
4826
4921
  });
4827
4922
 
4828
4923
  it('should read a product document as its outline with sections: []', async () => {
@@ -4835,8 +4930,8 @@ describe('MCP server integration', () => {
4835
4930
  })
4836
4931
  );
4837
4932
 
4838
- expect(outline.data.sections).toEqual({});
4839
- expect(outline.omittedSections).toEqual([
4933
+ expect(outline).not.toHaveProperty('data');
4934
+ expect(outline.outline).toEqual([
4840
4935
  { id: 'overview', characters: 5_000 },
4841
4936
  { id: 'features', characters: expect.any(Number) },
4842
4937
  { id: 'flows', characters: 5_000 },
@@ -4999,7 +5094,7 @@ describe('MCP server integration', () => {
4999
5094
  arguments: { productId: PRODUCT_ID, findingId: FINDING_ID },
5000
5095
  })
5001
5096
  );
5002
- expect(one).toMatchObject({ backlogItemId: ITEM_ID, finding: { id: FINDING_ID, status: 'remediating' } });
5097
+ expect(one).toMatchObject({ initiativeId: ITEM_ID, finding: { id: FINDING_ID, status: 'remediating' } });
5003
5098
  expect(one.finding).not.toHaveProperty('fingerprint');
5004
5099
 
5005
5100
  const group = parseToolResult(
@@ -5015,7 +5110,7 @@ describe('MCP server integration', () => {
5015
5110
  currentVersion: '4.17.20',
5016
5111
  productRepositoryId: repoId,
5017
5112
  });
5018
- expect(group).toEqual({ backlogItemId: ITEM_ID, findingIds: [FINDING_ID] });
5113
+ expect(group).toEqual({ initiativeId: ITEM_ID, findingIds: [FINDING_ID] });
5019
5114
 
5020
5115
  // A finding of another product is not fixed as a group of this one.
5021
5116
  const before = callsTo(fetchSpy, 'POST').length;
@@ -5121,7 +5216,7 @@ describe('MCP server integration', () => {
5121
5216
  arguments: { productId: PRODUCT_ID, controlId: 'SEC-3.4', violationId: VIOLATION_ID },
5122
5217
  });
5123
5218
 
5124
- expect(control).toEqual({ backlogItemId: ITEM_ID, divergenceIds: [VIOLATION_ID], created: true });
5219
+ expect(control).toEqual({ initiativeId: ITEM_ID, divergenceIds: [VIOLATION_ID], created: true });
5125
5220
  expect(callsTo(fetchSpy, 'POST').map((u) => u.pathname)).toEqual([
5126
5221
  `/v1/products/${PRODUCT_ID}/maintenance/policies/controls/SEC-3.4/remediate`,
5127
5222
  `/v1/products/${PRODUCT_ID}/maintenance/policies/divergences/${VIOLATION_ID}/remediate`,
@@ -5179,17 +5274,17 @@ describe('MCP server integration', () => {
5179
5274
  expect(product.searchParams.has('resolved')).toBe(false);
5180
5275
 
5181
5276
  const done = parseToolResult(
5182
- await client.callTool({ name: 'taiga_resolve_action', arguments: { itemId: INBOX_ID, status: 'done' } })
5277
+ await client.callTool({ name: 'taiga_resolve_action', arguments: { actionId: INBOX_ID, status: 'done' } })
5183
5278
  );
5184
5279
  expect(done.status).toBe('done');
5185
5280
  expect(sentBody(fetchSpy, 'PATCH', '/inbox/')).toEqual({ status: 'done' });
5186
5281
 
5187
5282
  fetchSpy.mockClear();
5188
5283
  vi.stubGlobal('fetch', fetchSpy);
5189
- await client.callTool({ name: 'taiga_resolve_action', arguments: { itemId: INBOX_ID, snoozedUntil: null } });
5284
+ await client.callTool({ name: 'taiga_resolve_action', arguments: { actionId: INBOX_ID, snoozedUntil: null } });
5190
5285
  expect(sentBody(fetchSpy, 'PATCH', '/inbox/')).toEqual({ snoozedUntil: null });
5191
5286
 
5192
- const nothing = await client.callTool({ name: 'taiga_resolve_action', arguments: { itemId: INBOX_ID } });
5287
+ const nothing = await client.callTool({ name: 'taiga_resolve_action', arguments: { actionId: INBOX_ID } });
5193
5288
  expect(nothing.isError).toBe(true);
5194
5289
  });
5195
5290
 
@@ -5201,7 +5296,7 @@ describe('MCP server integration', () => {
5201
5296
  commitSha: 'abc',
5202
5297
  commitSubject: 'Merge #1',
5203
5298
  artifacts: {},
5204
- metadata: { big: true },
5299
+ metadata: { big: true, repositoryFullName: 'acme/api' },
5205
5300
  };
5206
5301
  const { client, fetchSpy } = await bootMcpPair({
5207
5302
  capabilities: ['products:read'],
@@ -5246,8 +5341,11 @@ describe('MCP server integration', () => {
5246
5341
  });
5247
5342
  expect(result.environments.map((e: { id: string }) => e.id)).toEqual([ENV_ID, ENV_ID_2]);
5248
5343
  expect(result).not.toHaveProperty('moreEnvironments');
5249
- expect(result.running[0]).toMatchObject({ id: 'd1', commitSubject: 'Merge #1' });
5344
+ // Named by the repository that deployed it: each runs its own in an
5345
+ // environment (PRO-2528). The rest of the metadata stays behind.
5346
+ expect(result.running[0]).toMatchObject({ id: 'd1', commitSubject: 'Merge #1', repository: 'acme/api' });
5250
5347
  expect(result.running[0]).not.toHaveProperty('metadata');
5348
+ expect(result.history.data[0]).toMatchObject({ id: 'd1', repository: 'acme/api' });
5251
5349
  expect(result.history).toMatchObject({ nextCursor: 'more' });
5252
5350
  expect(result.syncStatus).toBeNull();
5253
5351
  expect(result.syncUnavailable).toContain('500');
@@ -5507,7 +5605,7 @@ describe('MCP server integration', () => {
5507
5605
  expect(product).not.toHaveProperty('settings');
5508
5606
 
5509
5607
  const item = parseToolResult(
5510
- await client.callTool({ name: 'taiga_set_auto_merge', arguments: { itemId: ITEM_ID, autoMerge: false } })
5608
+ await client.callTool({ name: 'taiga_set_auto_merge', arguments: { initiativeId: ITEM_ID, autoMerge: false } })
5511
5609
  );
5512
5610
  expect(sentBody(fetchSpy, 'PATCH', `/backlog/${ITEM_ID}`)).toEqual({ autoMerge: false });
5513
5611
  expect(item).not.toHaveProperty('description');
@@ -6328,11 +6426,11 @@ describe('what the agent review found (PRO-2759)', () => {
6328
6426
  },
6329
6427
  });
6330
6428
 
6331
- const item = await client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } });
6429
+ const item = await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } });
6332
6430
  expect(parseToolResult(item)).toEqual({
6333
6431
  error: 'NOT_FOUND',
6334
6432
  message:
6335
- "Backlog item not found. taiga_list_backlog_items lists a product's initiatives with their IDs; a run or " +
6433
+ "Backlog item not found. taiga_list_initiatives lists a product's initiatives with their IDs; a run or " +
6336
6434
  'plan ID is not one.',
6337
6435
  });
6338
6436
 
@@ -6340,10 +6438,34 @@ describe('what the agent review found (PRO-2759)', () => {
6340
6438
  name: 'taiga_list_repository_branches',
6341
6439
  arguments: { productId: PRODUCT_ID, repositoryId: LINK_ID },
6342
6440
  });
6441
+ // The API names the link, so only the link is hinted: the product the call
6442
+ // also named was right, and its hint read as blaming it (PRO-2800).
6343
6443
  expect(parseToolResult(branches).message).toBe(
6344
6444
  "Repository link not found. taiga_list_product_repositories lists the product's repository links; a GitHub " +
6345
- 'repository id is not a link ID. taiga_list_products lists the products the key can see; a name is not ' +
6346
- 'an ID (taiga_get_product takes a slug).'
6445
+ 'repository id is not a link ID.'
6446
+ );
6447
+ });
6448
+
6449
+ it('hints every ID the call named when the 404 does not say which one is missing (PRO-2800)', async () => {
6450
+ const { client } = await bootMcpPair({
6451
+ capabilities: ALL_CAPABILITIES,
6452
+ routes: {
6453
+ [`GET /v1/products/${PRODUCT_ID}/repositories/${LINK_ID}/branches`]: refusal(
6454
+ 404,
6455
+ 'NOT_FOUND',
6456
+ 'The requested resource does not exist'
6457
+ ),
6458
+ },
6459
+ });
6460
+
6461
+ const branches = await client.callTool({
6462
+ name: 'taiga_list_repository_branches',
6463
+ arguments: { productId: PRODUCT_ID, repositoryId: LINK_ID },
6464
+ });
6465
+ expect(parseToolResult(branches).message).toBe(
6466
+ "The requested resource does not exist. taiga_list_product_repositories lists the product's repository " +
6467
+ 'links; a GitHub repository id is not a link ID. taiga_list_products lists the products the key can see; ' +
6468
+ 'a name is not an ID (taiga_get_product takes a slug).'
6347
6469
  );
6348
6470
  });
6349
6471
 
@@ -6357,12 +6479,16 @@ describe('what the agent review found (PRO-2759)', () => {
6357
6479
  },
6358
6480
  });
6359
6481
 
6360
- const noPlan = parseToolResult(await client.callTool({ name: 'taiga_get_plan', arguments: { itemId: ITEM_ID } }));
6361
- const noItem = parseToolResult(await client.callTool({ name: 'taiga_get_plan', arguments: { itemId: OTHER } }));
6482
+ const noPlan = parseToolResult(
6483
+ await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: ITEM_ID } })
6484
+ );
6485
+ const noItem = parseToolResult(
6486
+ await client.callTool({ name: 'taiga_get_plan', arguments: { initiativeId: OTHER } })
6487
+ );
6362
6488
 
6363
6489
  expect(noPlan.message).toContain('taiga_plan_again asks for a new one');
6364
6490
  expect(noItem.message).toBe(
6365
- "Backlog item not found. taiga_list_backlog_items lists a product's initiatives with their IDs; a run or " +
6491
+ "Backlog item not found. taiga_list_initiatives lists a product's initiatives with their IDs; a run or " +
6366
6492
  'plan ID is not one.'
6367
6493
  );
6368
6494
  });
@@ -6376,9 +6502,7 @@ describe('what the agent review found (PRO-2759)', () => {
6376
6502
  },
6377
6503
  });
6378
6504
 
6379
- const result = parseToolResult(
6380
- await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
6381
- );
6505
+ const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
6382
6506
 
6383
6507
  expect(result.stoppedIn).toMatch(/^before the first step started/);
6384
6508
  });
@@ -6386,7 +6510,7 @@ describe('what the agent review found (PRO-2759)', () => {
6386
6510
  it('says where a product ID comes from when a name is passed for one', async () => {
6387
6511
  const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
6388
6512
 
6389
- const result = await client.callTool({ name: 'taiga_list_backlog_items', arguments: { productId: 'Web shop' } });
6513
+ const result = await client.callTool({ name: 'taiga_list_initiatives', arguments: { productId: 'Web shop' } });
6390
6514
 
6391
6515
  expect(result.isError).toBe(true);
6392
6516
  expect((result.content as { text: string }[])[0]!.text).toContain(
@@ -6452,12 +6576,12 @@ describe('what the agent review found (PRO-2759)', () => {
6452
6576
  // One shape whatever matched (PRO-2778): every real id with its size, the miss named.
6453
6577
  expect(none.isError).toBeFalsy();
6454
6578
  const missed = parseToolResult(none);
6455
- expect(missed.data.sections).toEqual({});
6579
+ expect(missed).not.toHaveProperty('data');
6456
6580
  expect(missed.unknownSections).toEqual(['secrets']);
6457
- expect(missed.omittedSections.map((s: { id: string }) => s.id)).toEqual(['summary', 'scope']);
6581
+ expect(missed.outline.map((s: { id: string }) => s.id)).toEqual(['summary', 'scope']);
6458
6582
  });
6459
6583
 
6460
- it('says what a product in setup waits for, and warns of a half turned off with nothing said about it', async () => {
6584
+ it('says what a product in setup still needs, from what is linked and what the API would refuse on (PRO-2800)', async () => {
6461
6585
  const draft = {
6462
6586
  id: PRODUCT_ID,
6463
6587
  name: 'Web',
@@ -6469,29 +6593,46 @@ describe('what the agent review found (PRO-2759)', () => {
6469
6593
  ownsInfrastructure: false,
6470
6594
  deploymentNotes: null,
6471
6595
  };
6596
+ const blockers = { data: { inSetup: true, missingDocuments: ['specification', 'architecture'], openChecks: [] } };
6597
+ let links: unknown[] = [];
6472
6598
  const { client } = await bootMcpPair({
6473
6599
  capabilities: ALL_CAPABILITIES,
6474
6600
  routes: {
6475
6601
  [`GET /v1/products/${PRODUCT_ID}`]: { data: draft },
6476
6602
  [`PATCH /v1/products/${PRODUCT_ID}`]: { data: draft },
6603
+ [`GET /v1/products/${PRODUCT_ID}/setup`]: blockers,
6604
+ [`GET /v1/products/${PRODUCT_ID}/repositories`]: () => ({
6605
+ ok: true,
6606
+ status: 200,
6607
+ json: async () => ({ data: links, nextCursor: null }),
6608
+ text: async () => JSON.stringify({ data: links, nextCursor: null }),
6609
+ }),
6477
6610
  },
6478
6611
  });
6479
6612
 
6480
6613
  const product = parseToolResult(
6481
6614
  await client.callTool({ name: 'taiga_get_product', arguments: { productId: PRODUCT_ID } })
6482
6615
  );
6483
- expect(product.nextStep).toBe(
6484
- 'Link its repository if it has none (taiga_list_linkable_repositories, taiga_link_repository), then ' +
6485
- 'taiga_start_import: the import derives the documents from the code and runs by itself.'
6486
- );
6487
-
6616
+ expect(product.readiness).toEqual({
6617
+ ready: false,
6618
+ repository: { linked: false },
6619
+ import: 'not started',
6620
+ missingDocuments: ['Specification', 'Architecture'],
6621
+ openChecks: [],
6622
+ nextStep:
6623
+ 'Link its repository (taiga_list_linkable_repositories, then taiga_link_repository), then taiga_start_import.',
6624
+ });
6625
+
6626
+ // Linked: the next move follows, which the step alone never said.
6627
+ links = [{ id: LINK_ID, repositoryName: 'acme/web', effectiveBranch: 'main' }];
6488
6628
  const updated = parseToolResult(
6489
6629
  await client.callTool({
6490
6630
  name: 'taiga_update_product',
6491
6631
  arguments: { productId: PRODUCT_ID, ownsInfrastructure: false },
6492
6632
  })
6493
6633
  );
6494
- expect(updated.nextStep).toBe(product.nextStep);
6634
+ expect(updated.readiness.repository).toEqual({ linked: true, name: 'acme/web', branch: 'main' });
6635
+ expect(updated.readiness.nextStep).toMatch(/^taiga_start_import derives the documents from the code/);
6495
6636
  expect(updated.warnings).toEqual([expect.stringContaining('has no deploymentNotes')]);
6496
6637
  });
6497
6638
 
@@ -6506,16 +6647,56 @@ describe('what the agent review found (PRO-2759)', () => {
6506
6647
  },
6507
6648
  });
6508
6649
 
6650
+ // An API without the setup read (404 here): the step is what there is to go on.
6509
6651
  const read = parseToolResult(
6510
6652
  await client.callTool({ name: 'taiga_update_product', arguments: { productId: PRODUCT_ID, name: 'Web' } })
6511
6653
  );
6512
- // The product's read is unreachable here (the mock answers 404): no nextStep, not the conversation's.
6654
+ // The product's read is unreachable too: no next step, not the conversation's.
6513
6655
  const unread = parseToolResult(
6514
6656
  await client.callTool({ name: 'taiga_update_product', arguments: { productId: PRODUCT_ID_2, name: 'Web' } })
6515
6657
  );
6516
6658
 
6517
- expect(read.nextStep).toMatch(/^The specification is done/);
6518
- expect(unread).not.toHaveProperty('nextStep');
6659
+ expect(read.readiness.ready).toBeNull();
6660
+ expect(read.readiness.nextStep).toMatch(/^The specification is done/);
6661
+ expect(unread.readiness.nextStep).toBeNull();
6662
+ expect(unread.readiness.repository).toBeNull();
6663
+ });
6664
+
6665
+ it('says what a scratch product still needs, from the Discovery conversation to the last document (PRO-2800)', async () => {
6666
+ const draft = {
6667
+ id: PRODUCT_ID,
6668
+ name: 'Web',
6669
+ slug: 'web',
6670
+ status: 'draft',
6671
+ factoryId: FACTORY_ID,
6672
+ discoveryStep: 'documents',
6673
+ settings: {},
6674
+ };
6675
+ let missing = ['specification', 'architecture'];
6676
+ const { client } = await bootMcpPair({
6677
+ capabilities: ALL_CAPABILITIES,
6678
+ routes: {
6679
+ [`GET /v1/products/${PRODUCT_ID}`]: { data: draft },
6680
+ [`GET /v1/products/${PRODUCT_ID}/repositories`]: { data: [], nextCursor: null },
6681
+ [`GET /v1/products/${PRODUCT_ID}/setup`]: () => {
6682
+ const body = { data: { inSetup: true, missingDocuments: missing, openChecks: [] } };
6683
+ return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) };
6684
+ },
6685
+ },
6686
+ });
6687
+ const read = async () =>
6688
+ parseToolResult(await client.callTool({ name: 'taiga_get_product', arguments: { productId: PRODUCT_ID } }))
6689
+ .readiness;
6690
+
6691
+ expect((await read()).nextStep).toMatch(
6692
+ /^The specification comes from the Discovery conversation, held in the web/
6693
+ );
6694
+ missing = ['architecture'];
6695
+ expect((await read()).nextStep).toMatch(/^Still to publish: Architecture\. taiga_generate_product_documents/);
6696
+ missing = [];
6697
+ const done = await read();
6698
+ expect(done.ready).toBe(true);
6699
+ expect(done.nextStep).toMatch(/^Every required document is published: taiga_finish_discovery/);
6519
6700
  });
6520
6701
 
6521
6702
  it('says nothing of where a merged run stopped, whatever its stale status says', async () => {
@@ -6529,9 +6710,7 @@ describe('what the agent review found (PRO-2759)', () => {
6529
6710
  },
6530
6711
  });
6531
6712
 
6532
- const result = parseToolResult(
6533
- await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
6534
- );
6713
+ const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
6535
6714
 
6536
6715
  expect(result).not.toHaveProperty('stoppedIn');
6537
6716
  });
@@ -6568,9 +6747,7 @@ describe('what the agent review found (PRO-2759)', () => {
6568
6747
  },
6569
6748
  });
6570
6749
 
6571
- const result = parseToolResult(
6572
- await client.callTool({ name: 'taiga_get_build', arguments: { buildId: BUILD_ID } })
6573
- );
6750
+ const result = parseToolResult(await client.callTool({ name: 'taiga_get_run', arguments: { runId: BUILD_ID } }));
6574
6751
 
6575
6752
  expect(result.stoppedIn).toMatch(/^after the last step, in hardening/);
6576
6753
  });
@@ -6704,7 +6881,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6704
6881
  });
6705
6882
 
6706
6883
  const item = parseToolResult(
6707
- await client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } })
6884
+ await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
6708
6885
  );
6709
6886
  expect(item.planFailure).toEqual({
6710
6887
  cause: 'cancelled',
@@ -6721,7 +6898,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6721
6898
  },
6722
6899
  });
6723
6900
  const bare = parseToolResult(
6724
- await refused.client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } })
6901
+ await refused.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
6725
6902
  );
6726
6903
  expect(bare).toEqual(failed);
6727
6904
 
@@ -6734,7 +6911,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6734
6911
  },
6735
6912
  });
6736
6913
  const staleItem = parseToolResult(
6737
- await stale.client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } })
6914
+ await stale.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
6738
6915
  );
6739
6916
  expect(staleItem.planFailure).toEqual({
6740
6917
  cause: 'worker_lost',
@@ -6763,7 +6940,9 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6763
6940
  },
6764
6941
  });
6765
6942
  expect(
6766
- parseToolResult(await retried.client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } }))
6943
+ parseToolResult(
6944
+ await retried.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
6945
+ )
6767
6946
  ).toEqual(failed);
6768
6947
 
6769
6948
  // No failure in the history, or no history reachable: the item as it is.
@@ -6775,7 +6954,9 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6775
6954
  },
6776
6955
  });
6777
6956
  expect(
6778
- parseToolResult(await silent.client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } }))
6957
+ parseToolResult(
6958
+ await silent.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
6959
+ )
6779
6960
  ).toEqual(failed);
6780
6961
  const offline = await bootMcpPair({
6781
6962
  capabilities: ALL_CAPABILITIES,
@@ -6786,7 +6967,9 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6786
6967
  },
6787
6968
  });
6788
6969
  expect(
6789
- parseToolResult(await offline.client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } }))
6970
+ parseToolResult(
6971
+ await offline.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
6972
+ )
6790
6973
  ).toEqual(failed);
6791
6974
 
6792
6975
  // A plan that did not fail reads no history.
@@ -6794,7 +6977,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6794
6977
  capabilities: ALL_CAPABILITIES,
6795
6978
  routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: { ...failed, planningStatus: 'planned' } } },
6796
6979
  });
6797
- await planned.client.callTool({ name: 'taiga_get_backlog_item', arguments: { itemId: ITEM_ID } });
6980
+ await planned.client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } });
6798
6981
  expect(planned.fetchSpy.mock.calls.some((c: unknown[]) => String(c[0]).includes('/activity'))).toBe(false);
6799
6982
  });
6800
6983
 
@@ -6846,7 +7029,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6846
7029
  const { tools } = await client.listTools();
6847
7030
  const metrics = tools.find((t) => t.name === 'taiga_get_delivery_metrics')!;
6848
7031
  const properties = metrics.inputSchema.properties as Record<string, { enum?: unknown }>;
6849
- expect(properties.windowDays!.enum).toEqual([30, 60, 90]);
7032
+ expect(properties['windowDays']!.enum).toEqual([30, 60, 90]);
6850
7033
  });
6851
7034
 
6852
7035
  it('opens every writing tool with what it does, and leaves the reads as they were', async () => {
@@ -6861,7 +7044,7 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6861
7044
  expect(description, tool.name).toMatch(/^Acts on paid or shared work: ask the person first\. /);
6862
7045
  else expect(description, tool.name).toMatch(/^Writes\. /);
6863
7046
  }
6864
- expect(tools.find((t) => t.name === 'taiga_queue_backlog_item')!.description).toMatch(/^Acts on paid/);
7047
+ expect(tools.find((t) => t.name === 'taiga_queue_initiative')!.description).toMatch(/^Acts on paid/);
6865
7048
  expect(tools.find((t) => t.name === 'taiga_create_factory')!.description).toMatch(/^Writes\. /);
6866
7049
  });
6867
7050
 
@@ -6900,3 +7083,359 @@ describe('what the second agent review found (PRO-2778, PRO-2780)', () => {
6900
7083
  expect(standing.controls[0].findingsByTier).toEqual(tiers);
6901
7084
  });
6902
7085
  });
7086
+
7087
+ describe('what the third agent review found (PRO-2800)', () => {
7088
+ beforeEach(() => {
7089
+ vi.restoreAllMocks();
7090
+ });
7091
+
7092
+ it('loads the three entry tools upfront in Claude Code, and only those', async () => {
7093
+ const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
7094
+
7095
+ const { tools } = await client.listTools();
7096
+ const upfront = tools.filter((tool) => tool._meta?.['anthropic/alwaysLoad'] === true).map((tool) => tool.name);
7097
+
7098
+ expect(upfront.sort()).toEqual(['taiga_get_product_context', 'taiga_list_action_required', 'taiga_list_products']);
7099
+ });
7100
+
7101
+ it('opens Action Required with what is open by product and kind, rows without their metadata unless asked', async () => {
7102
+ const created = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString();
7103
+ const rows = [
7104
+ {
7105
+ id: 'i1',
7106
+ productId: PRODUCT_ID,
7107
+ productName: 'Web',
7108
+ kind: 'review_pr',
7109
+ status: 'open',
7110
+ title: 'Review & merge PR #9',
7111
+ createdAt: created,
7112
+ metadata: { prUrl: 'https://github.com/acme/web/pull/9', prNumber: 9 },
7113
+ },
7114
+ {
7115
+ id: 'i2',
7116
+ productId: PRODUCT_ID,
7117
+ productName: 'Web',
7118
+ kind: 'review_doc',
7119
+ status: 'open',
7120
+ createdAt: created,
7121
+ metadata: { decision: { detail: 'x'.repeat(2_000) } },
7122
+ },
7123
+ { id: 'i3', productId: PRODUCT_ID_2, productName: 'Api', kind: 'review_pr', status: 'open', createdAt: created },
7124
+ ];
7125
+ // The API pages: a short page says there is more, so the summary reads up to 100 itself.
7126
+ const inbox = (url: string) => {
7127
+ const limit = Number(new URL(url).searchParams.get('limit') ?? 25);
7128
+ const body = { data: rows.slice(0, limit), nextCursor: limit < rows.length ? 'more' : null, totalCount: 3 };
7129
+ return { ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) };
7130
+ };
7131
+ const { client, fetchSpy } = await bootMcpPair({
7132
+ capabilities: ['products:read'],
7133
+ routes: { 'GET /v1/inbox': inbox },
7134
+ });
7135
+
7136
+ const list = parseToolResult(
7137
+ await client.callTool({ name: 'taiga_list_action_required', arguments: { limit: 2 } })
7138
+ );
7139
+ const full = parseToolResult(
7140
+ await client.callTool({ name: 'taiga_list_action_required', arguments: { full: true } })
7141
+ );
7142
+
7143
+ expect(list.summary).toEqual([
7144
+ { productId: PRODUCT_ID, product: 'Web', open: 2, byKind: { review_pr: 1, review_doc: 1 } },
7145
+ { productId: PRODUCT_ID_2, product: 'Api', open: 1, byKind: { review_pr: 1 } },
7146
+ ]);
7147
+ expect(list.totalCount).toBe(3);
7148
+ expect(list.data[0]).toMatchObject({ id: 'i1', prUrl: 'https://github.com/acme/web/pull/9', waitingDays: 3 });
7149
+ expect(JSON.stringify(list.data)).not.toContain('metadata');
7150
+ expect(full.data[1].metadata).toEqual(rows[1]!.metadata);
7151
+ // The page asked for and the summary's own read, nothing on a later page.
7152
+ const limits = fetchSpy.mock.calls
7153
+ .map((call: unknown[]) => new URL(call[0] as string))
7154
+ .filter((url: URL) => url.pathname === '/v1/inbox')
7155
+ .map((url: URL) => url.searchParams.get('limit'));
7156
+ // The short page and the summary's read; the full page held everything, so it read once.
7157
+ expect(limits).toEqual(['2', '100', null]);
7158
+ });
7159
+
7160
+ it("counts a resolved item's wait up to its resolution", async () => {
7161
+ const day = 24 * 60 * 60 * 1000;
7162
+ const { client } = await bootMcpPair({
7163
+ capabilities: ['products:read'],
7164
+ routes: {
7165
+ 'GET /v1/inbox': {
7166
+ data: [
7167
+ {
7168
+ id: 'r1',
7169
+ productId: PRODUCT_ID,
7170
+ kind: 'review_pr',
7171
+ status: 'done',
7172
+ createdAt: new Date(Date.now() - 40 * day).toISOString(),
7173
+ resolvedAt: new Date(Date.now() - 38 * day).toISOString(),
7174
+ },
7175
+ ],
7176
+ nextCursor: null,
7177
+ },
7178
+ },
7179
+ });
7180
+
7181
+ const archive = parseToolResult(
7182
+ await client.callTool({ name: 'taiga_list_action_required', arguments: { resolved: true } })
7183
+ );
7184
+
7185
+ expect(archive.data[0].waitingDays).toBe(2);
7186
+ });
7187
+
7188
+ it('lists a variable filed under two categories once, naming the other record', async () => {
7189
+ const task = (id: string, category: string, key: string | null) => ({
7190
+ id,
7191
+ productId: PRODUCT_ID,
7192
+ category,
7193
+ key,
7194
+ title: `Set ${key ?? 'something'}`,
7195
+ });
7196
+ const { client } = await bootMcpPair({
7197
+ capabilities: ['products:read'],
7198
+ routes: {
7199
+ 'GET /v1/setup-tasks': {
7200
+ data: [
7201
+ task('t1', 'secret', 'JWT_SECRET'),
7202
+ task('t2', 'manual_config', null),
7203
+ task('t3', 'env_var', 'JWT_SECRET'),
7204
+ ],
7205
+ nextCursor: null,
7206
+ },
7207
+ },
7208
+ });
7209
+
7210
+ const tasks = parseToolResult(await client.callTool({ name: 'taiga_list_setup_tasks', arguments: {} }));
7211
+
7212
+ expect(tasks.data.map((row: { id: string }) => row.id)).toEqual(['t1', 't2']);
7213
+ expect(tasks.data[0].alsoFiledAs).toEqual([{ id: 't3', category: 'env_var', title: 'Set JWT_SECRET' }]);
7214
+ });
7215
+
7216
+ it('leads a variable with its open record when an older one is done', async () => {
7217
+ const { client } = await bootMcpPair({
7218
+ capabilities: ['products:read'],
7219
+ routes: {
7220
+ 'GET /v1/setup-tasks': {
7221
+ data: [
7222
+ { id: 't1', productId: PRODUCT_ID, category: 'secret', key: 'K', title: 'Old', inboxItemStatus: 'done' },
7223
+ { id: 't2', productId: PRODUCT_ID, category: 'env_var', key: 'K', title: 'New', inboxItemStatus: 'open' },
7224
+ ],
7225
+ nextCursor: null,
7226
+ },
7227
+ },
7228
+ });
7229
+
7230
+ const tasks = parseToolResult(
7231
+ await client.callTool({ name: 'taiga_list_setup_tasks', arguments: { includeDone: true } })
7232
+ );
7233
+
7234
+ expect(tasks.data).toHaveLength(1);
7235
+ expect(tasks.data[0]).toMatchObject({ id: 't2', inboxItemStatus: 'open' });
7236
+ expect(tasks.data[0].alsoFiledAs).toEqual([
7237
+ { id: 't1', category: 'secret', title: 'Old', inboxItemStatus: 'done' },
7238
+ ]);
7239
+ });
7240
+
7241
+ it("names a finding's sweep sweepId, so runId stays what taiga_get_run takes", async () => {
7242
+ const FINDING = '0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4d01';
7243
+ const { client } = await bootMcpPair({
7244
+ capabilities: ['products:read'],
7245
+ routes: {
7246
+ [`GET /v1/maintenance/findings/${FINDING}`]: {
7247
+ data: { id: FINDING, runId: 'sweep-1', remediation: { buildRunId: BUILD_ID } },
7248
+ },
7249
+ },
7250
+ });
7251
+
7252
+ const finding = parseToolResult(
7253
+ await client.callTool({ name: 'taiga_get_finding', arguments: { findingId: FINDING } })
7254
+ );
7255
+
7256
+ expect(finding).toEqual({ sweepId: 'sweep-1', id: FINDING, remediation: { runId: BUILD_ID } });
7257
+ });
7258
+
7259
+ it('says how each run stands in a sentence, and says nothing of a draft where there is no pull request', async () => {
7260
+ const base = { productId: PRODUCT_ID, completedSteps: 6, totalSteps: 6, prDraft: true };
7261
+ const { client } = await bootMcpPair({
7262
+ capabilities: ['products:read'],
7263
+ routes: {
7264
+ [`/v1/products/${PRODUCT_ID}/builds`]: {
7265
+ data: [
7266
+ { ...base, id: 'r1', status: 'closed', prNumber: 28, prUrl: 'https://x/pull/28', prDraft: false },
7267
+ {
7268
+ ...base,
7269
+ id: 'r2',
7270
+ status: 'completed',
7271
+ prMerged: true,
7272
+ prNumber: 7,
7273
+ prUrl: 'https://x/7',
7274
+ baseBranch: 'main',
7275
+ },
7276
+ { ...base, id: 'r3', status: 'failed', failedReason: 'agent_error', completedSteps: 2, prUrl: null },
7277
+ { ...base, id: 'r4', status: 'needs_review', prNumber: 9, prUrl: null },
7278
+ { ...base, id: 'r5', status: 'hardening', prUrl: null },
7279
+ // Merged, but the completion write after the merge failed: the latch decides.
7280
+ {
7281
+ ...base,
7282
+ id: 'r6',
7283
+ status: 'failed',
7284
+ prMerged: true,
7285
+ prNumber: 8,
7286
+ prUrl: 'https://x/8',
7287
+ baseBranch: 'main',
7288
+ },
7289
+ ],
7290
+ nextCursor: null,
7291
+ },
7292
+ },
7293
+ });
7294
+
7295
+ const runs = parseToolResult(
7296
+ await client.callTool({ name: 'taiga_list_runs', arguments: { productId: PRODUCT_ID } })
7297
+ ).data as Record<string, unknown>[];
7298
+
7299
+ expect(runs.map((run) => [run['disposition'], run['outcome']])).toEqual([
7300
+ ['rejected', 'PR #28 was closed without merging, so the work did not land.'],
7301
+ ['succeeded', 'Merged: PR #7 is in main.'],
7302
+ ['failed', 'Failed: agent_error (2 of 6 steps done).'],
7303
+ ['gated', 'Waiting for a person to review and merge PR #9.'],
7304
+ ['in_flight', 'Running: hardening: running the security scanners and fixing what they find (6 of 6 steps done).'],
7305
+ ['succeeded', 'Merged: PR #8 is in main.'],
7306
+ ]);
7307
+ expect(runs[0]).toHaveProperty('prDraft', false);
7308
+ expect(runs[2]).not.toHaveProperty('prDraft');
7309
+ // A recorded PR number with no URL still has a pull request, and its draft state.
7310
+ expect(runs[3]).toHaveProperty('prDraft', true);
7311
+ });
7312
+
7313
+ it('says where an initiative stands and the call that moves it on, in one sentence', async () => {
7314
+ const item = {
7315
+ id: ITEM_ID,
7316
+ title: 'Add login',
7317
+ status: 'in_progress',
7318
+ state: 'build',
7319
+ needsHumanCause: null,
7320
+ lifecycle: {
7321
+ turn: 'you',
7322
+ stage: 'building',
7323
+ cta: 'retry_build',
7324
+ planFailureCause: null,
7325
+ latestBuild: { status: 'closed', prNumber: 28 },
7326
+ },
7327
+ };
7328
+ const { client } = await bootMcpPair({
7329
+ capabilities: ['backlog:read'],
7330
+ routes: {
7331
+ [`GET /v1/backlog/${ITEM_ID}`]: { data: item },
7332
+ [`/v1/products/${PRODUCT_ID}/backlog`]: { data: [item], nextCursor: null },
7333
+ },
7334
+ });
7335
+
7336
+ const got = parseToolResult(
7337
+ await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
7338
+ );
7339
+ const listed = parseToolResult(
7340
+ await client.callTool({ name: 'taiga_list_initiatives', arguments: { productId: PRODUCT_ID } })
7341
+ );
7342
+
7343
+ const said =
7344
+ "Being built; its latest run had its pull request closed without merging (PR #28). A person's turn: a new " +
7345
+ 'run starts over from the web; taiga_move_initiative takes it off the line.';
7346
+ expect(got.situation).toBe(said);
7347
+ expect(listed.data[0].situation).toBe(said);
7348
+ });
7349
+
7350
+ it("keeps the run's own reason beside the halt that stopped the line", async () => {
7351
+ const item = {
7352
+ id: ITEM_ID,
7353
+ title: 'Add login',
7354
+ needsHumanCause: 'pr_closed',
7355
+ needsHumanDetail: 'The pull request was closed by alice.',
7356
+ lifecycle: { turn: 'you', stage: 'building', cta: 'retry_build', planFailureCause: null, latestBuild: null },
7357
+ };
7358
+ const { client } = await bootMcpPair({
7359
+ capabilities: ['backlog:read'],
7360
+ routes: { [`GET /v1/backlog/${ITEM_ID}`]: { data: item } },
7361
+ });
7362
+
7363
+ const got = parseToolResult(
7364
+ await client.callTool({ name: 'taiga_get_initiative', arguments: { initiativeId: ITEM_ID } })
7365
+ );
7366
+
7367
+ expect(got.situation).toContain('The pull request was closed by alice.');
7368
+ expect(got.situation).toMatch(/^Being built\. The line stopped on it: .+\. The pull request was closed by alice\./);
7369
+ });
7370
+
7371
+ it("leaves the keys of a document's sections as their writers named them", async () => {
7372
+ const { client } = await bootMcpPair({
7373
+ capabilities: ALL_CAPABILITIES,
7374
+ routes: {
7375
+ 'GET /v1/tenants/current/documents/policy_security': {
7376
+ data: {
7377
+ id: 'doc-1',
7378
+ type: 'policy_security',
7379
+ version: 1,
7380
+ isDraft: false,
7381
+ data: { version: '1.0.0', sections: { backlogItemId: { content: 'a section by that name' } } },
7382
+ },
7383
+ },
7384
+ },
7385
+ });
7386
+
7387
+ const doc = parseToolResult(
7388
+ await client.callTool({ name: 'taiga_get_document', arguments: { type: 'policy_security' } })
7389
+ );
7390
+
7391
+ expect(Object.keys(doc.data.sections)).toEqual(['backlogItemId']);
7392
+ });
7393
+
7394
+ it("names IDs in refusal details in the product's words", async () => {
7395
+ const { client } = await bootMcpPair({
7396
+ capabilities: ALL_CAPABILITIES,
7397
+ routes: {
7398
+ [`POST /v1/builds/${BUILD_ID}/stop`]: refusal(409, 'CONFLICT', 'Not running', { buildRunId: BUILD_ID }),
7399
+ },
7400
+ });
7401
+
7402
+ const result = await client.callTool({ name: 'taiga_stop_run', arguments: { runId: BUILD_ID } });
7403
+
7404
+ expect(parseToolResult(result).details).toEqual({ runId: BUILD_ID });
7405
+ });
7406
+
7407
+ it('leaves stopping the line to the person when a move meets work in flight', async () => {
7408
+ const { client } = await bootMcpPair({
7409
+ capabilities: ALL_CAPABILITIES,
7410
+ routes: {
7411
+ [`PUT /v1/products/${PRODUCT_ID}/factory`]: refusal(
7412
+ 409,
7413
+ 'WORK_IN_FLIGHT',
7414
+ 'This product is planning right now. Stop the line, then move the product.'
7415
+ ),
7416
+ },
7417
+ });
7418
+
7419
+ const result = await client.callTool({
7420
+ name: 'taiga_move_product',
7421
+ arguments: { productId: PRODUCT_ID, factoryId: FACTORY_ID },
7422
+ });
7423
+
7424
+ expect(result.isError).toBe(true);
7425
+ const said = parseToolResult(result);
7426
+ expect(said.error).toBe('WORK_IN_FLIGHT');
7427
+ expect(said.message).toMatch(/^This product is planning right now\. Moving it now means stopping the line/);
7428
+ expect(said.message).toMatch(/ask the person, and pass stopLine: true only if they say so\.$/);
7429
+ expect(said.message).not.toContain('Stop the line, then');
7430
+ });
7431
+
7432
+ it('fits the server instructions inside what Claude Code keeps', async () => {
7433
+ const { client } = await bootMcpPair({ capabilities: ALL_CAPABILITIES });
7434
+
7435
+ const instructions = client.getInstructions() ?? '';
7436
+
7437
+ expect(instructions.length).toBeGreaterThan(500);
7438
+ expect(instructions.length).toBeLessThanOrEqual(2048);
7439
+ expect(instructions).toContain('taiga_get_product_context');
7440
+ });
7441
+ });