@vibescope/mcp-server 0.2.1 → 0.2.3

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.
Files changed (93) hide show
  1. package/README.md +63 -38
  2. package/dist/api-client.d.ts +187 -0
  3. package/dist/api-client.js +53 -1
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +14 -14
  6. package/dist/handlers/connectors.d.ts +45 -0
  7. package/dist/handlers/connectors.js +183 -0
  8. package/dist/handlers/cost.d.ts +10 -0
  9. package/dist/handlers/cost.js +54 -0
  10. package/dist/handlers/decisions.js +3 -3
  11. package/dist/handlers/deployment.js +35 -19
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +61 -2
  14. package/dist/handlers/fallback.js +5 -4
  15. package/dist/handlers/file-checkouts.d.ts +2 -0
  16. package/dist/handlers/file-checkouts.js +38 -6
  17. package/dist/handlers/findings.js +13 -12
  18. package/dist/handlers/git-issues.js +4 -4
  19. package/dist/handlers/ideas.js +5 -5
  20. package/dist/handlers/index.d.ts +1 -0
  21. package/dist/handlers/index.js +3 -0
  22. package/dist/handlers/milestones.js +5 -5
  23. package/dist/handlers/organizations.js +13 -13
  24. package/dist/handlers/progress.js +2 -2
  25. package/dist/handlers/project.js +6 -6
  26. package/dist/handlers/requests.js +3 -3
  27. package/dist/handlers/session.js +28 -9
  28. package/dist/handlers/sprints.js +17 -17
  29. package/dist/handlers/tasks.d.ts +2 -0
  30. package/dist/handlers/tasks.js +78 -20
  31. package/dist/handlers/types.d.ts +64 -2
  32. package/dist/handlers/types.js +48 -1
  33. package/dist/handlers/validation.js +3 -3
  34. package/dist/index.js +7 -2716
  35. package/dist/token-tracking.d.ts +74 -0
  36. package/dist/token-tracking.js +122 -0
  37. package/dist/tools.js +298 -9
  38. package/dist/utils.d.ts +5 -0
  39. package/dist/utils.js +17 -0
  40. package/docs/TOOLS.md +2053 -0
  41. package/package.json +4 -1
  42. package/scripts/generate-docs.ts +212 -0
  43. package/src/api-client.test.ts +723 -0
  44. package/src/api-client.ts +236 -1
  45. package/src/handlers/__test-setup__.ts +9 -0
  46. package/src/handlers/blockers.test.ts +31 -19
  47. package/src/handlers/blockers.ts +9 -8
  48. package/src/handlers/bodies-of-work.test.ts +55 -32
  49. package/src/handlers/bodies-of-work.ts +14 -14
  50. package/src/handlers/connectors.test.ts +834 -0
  51. package/src/handlers/connectors.ts +229 -0
  52. package/src/handlers/cost.ts +66 -0
  53. package/src/handlers/decisions.test.ts +34 -25
  54. package/src/handlers/decisions.ts +3 -3
  55. package/src/handlers/deployment.ts +39 -19
  56. package/src/handlers/discovery.ts +61 -2
  57. package/src/handlers/fallback.test.ts +26 -22
  58. package/src/handlers/fallback.ts +5 -4
  59. package/src/handlers/file-checkouts.test.ts +242 -49
  60. package/src/handlers/file-checkouts.ts +44 -6
  61. package/src/handlers/findings.test.ts +38 -24
  62. package/src/handlers/findings.ts +13 -12
  63. package/src/handlers/git-issues.test.ts +51 -43
  64. package/src/handlers/git-issues.ts +4 -4
  65. package/src/handlers/ideas.test.ts +28 -23
  66. package/src/handlers/ideas.ts +5 -5
  67. package/src/handlers/index.ts +3 -0
  68. package/src/handlers/milestones.test.ts +33 -28
  69. package/src/handlers/milestones.ts +5 -5
  70. package/src/handlers/organizations.test.ts +104 -83
  71. package/src/handlers/organizations.ts +13 -13
  72. package/src/handlers/progress.test.ts +20 -14
  73. package/src/handlers/progress.ts +2 -2
  74. package/src/handlers/project.test.ts +34 -27
  75. package/src/handlers/project.ts +6 -6
  76. package/src/handlers/requests.test.ts +27 -18
  77. package/src/handlers/requests.ts +3 -3
  78. package/src/handlers/session.test.ts +47 -0
  79. package/src/handlers/session.ts +26 -9
  80. package/src/handlers/sprints.test.ts +71 -50
  81. package/src/handlers/sprints.ts +17 -17
  82. package/src/handlers/tasks.test.ts +77 -15
  83. package/src/handlers/tasks.ts +90 -21
  84. package/src/handlers/tool-categories.test.ts +66 -0
  85. package/src/handlers/types.ts +81 -2
  86. package/src/handlers/validation.test.ts +78 -45
  87. package/src/handlers/validation.ts +3 -3
  88. package/src/index.ts +12 -2732
  89. package/src/token-tracking.test.ts +453 -0
  90. package/src/token-tracking.ts +164 -0
  91. package/src/tools.ts +298 -9
  92. package/src/utils.test.ts +2 -2
  93. package/src/utils.ts +17 -0
package/src/api-client.ts CHANGED
@@ -123,6 +123,23 @@ export class VibescopeApiClient {
123
123
  priority: number;
124
124
  estimated_minutes?: number;
125
125
  } | null;
126
+ pending_requests?: Array<{
127
+ id: string;
128
+ request_type: string;
129
+ message: string;
130
+ created_at: string;
131
+ }>;
132
+ pending_requests_count?: number;
133
+ URGENT_QUESTIONS?: {
134
+ count: number;
135
+ oldest_waiting_minutes: number;
136
+ action_required: string;
137
+ requests: Array<{
138
+ id: string;
139
+ message: string;
140
+ waiting_minutes: number;
141
+ }>;
142
+ };
126
143
  directive?: string;
127
144
  blockers_count?: number;
128
145
  validation_count?: number;
@@ -382,6 +399,7 @@ export class VibescopeApiClient {
382
399
  progress_note?: string;
383
400
  estimated_minutes?: number;
384
401
  git_branch?: string;
402
+ worktree_path?: string;
385
403
  session_id?: string;
386
404
  }): Promise<ApiResponse<{
387
405
  success: boolean;
@@ -426,7 +444,11 @@ export class VibescopeApiClient {
426
444
  next_action: string;
427
445
  warnings?: string[];
428
446
  }>> {
429
- return this.request('POST', `/api/mcp/tasks/${taskId}/complete`, params);
447
+ // Use proxy endpoint for consistency - direct endpoint had routing issues on Vercel
448
+ return this.proxy('complete_task', {
449
+ task_id: taskId,
450
+ summary: params.summary,
451
+ }, params.session_id ? { session_id: params.session_id, persona: null, instance_id: '' } : undefined);
430
452
  }
431
453
 
432
454
  async deleteTask(taskId: string): Promise<ApiResponse<{
@@ -1609,6 +1631,63 @@ export class VibescopeApiClient {
1609
1631
  });
1610
1632
  }
1611
1633
 
1634
+ async getBodyOfWorkCosts(params: {
1635
+ body_of_work_id?: string;
1636
+ project_id?: string;
1637
+ }): Promise<ApiResponse<{
1638
+ bodies_of_work: Array<{
1639
+ body_of_work_id: string;
1640
+ title: string;
1641
+ project_id: string;
1642
+ status: string;
1643
+ task_count: number;
1644
+ total_cost_usd: number;
1645
+ total_tokens: number;
1646
+ pre_phase_cost_usd: number;
1647
+ core_phase_cost_usd: number;
1648
+ post_phase_cost_usd: number;
1649
+ model_breakdown: Record<string, { input: number; output: number }>;
1650
+ }>;
1651
+ count: number;
1652
+ totals: {
1653
+ total_cost_usd: number;
1654
+ total_tokens: number;
1655
+ total_tasks: number;
1656
+ };
1657
+ }>> {
1658
+ return this.proxy('get_body_of_work_costs', params);
1659
+ }
1660
+
1661
+ async getSprintCosts(params: {
1662
+ sprint_id?: string;
1663
+ project_id?: string;
1664
+ }): Promise<ApiResponse<{
1665
+ sprints: Array<{
1666
+ sprint_id: string;
1667
+ title: string;
1668
+ project_id: string;
1669
+ status: string;
1670
+ sprint_number: number;
1671
+ task_count: number;
1672
+ total_cost_usd: number;
1673
+ total_tokens: number;
1674
+ cost_per_story_point: number | null;
1675
+ committed_points: number;
1676
+ velocity_points: number;
1677
+ model_breakdown: Record<string, { input: number; output: number }>;
1678
+ }>;
1679
+ count: number;
1680
+ totals: {
1681
+ total_cost_usd: number;
1682
+ total_tokens: number;
1683
+ total_tasks: number;
1684
+ total_velocity_points: number;
1685
+ avg_cost_per_point: number | null;
1686
+ };
1687
+ }>> {
1688
+ return this.proxy('get_sprint_costs', params);
1689
+ }
1690
+
1612
1691
  async getTokenUsage(): Promise<ApiResponse<{
1613
1692
  session_tokens: number;
1614
1693
  estimated_cost: number;
@@ -1695,6 +1774,7 @@ export class VibescopeApiClient {
1695
1774
  environment?: string;
1696
1775
  version_bump?: string;
1697
1776
  auto_trigger?: boolean;
1777
+ hours_interval?: number;
1698
1778
  notes?: string;
1699
1779
  git_ref?: string;
1700
1780
  }): Promise<ApiResponse<{
@@ -1712,6 +1792,7 @@ export class VibescopeApiClient {
1712
1792
  id: string;
1713
1793
  scheduled_at: string;
1714
1794
  schedule_type: string;
1795
+ hours_interval: number;
1715
1796
  environment: string;
1716
1797
  version_bump: string;
1717
1798
  auto_trigger: boolean;
@@ -1731,6 +1812,7 @@ export class VibescopeApiClient {
1731
1812
  async updateScheduledDeployment(scheduleId: string, updates: {
1732
1813
  scheduled_at?: string;
1733
1814
  schedule_type?: string;
1815
+ hours_interval?: number;
1734
1816
  environment?: string;
1735
1817
  version_bump?: string;
1736
1818
  auto_trigger?: boolean;
@@ -1883,6 +1965,159 @@ export class VibescopeApiClient {
1883
1965
  }>> {
1884
1966
  return this.proxy('abandon_checkout', params);
1885
1967
  }
1968
+
1969
+ // ============================================================================
1970
+ // Worktree Management
1971
+ // ============================================================================
1972
+
1973
+ async getStaleWorktrees(projectId: string): Promise<ApiResponse<{
1974
+ project_id: string;
1975
+ project_name: string;
1976
+ stale_worktrees: Array<{
1977
+ task_id: string;
1978
+ task_title: string;
1979
+ worktree_path: string;
1980
+ git_branch: string | null;
1981
+ status: string;
1982
+ completed_at: string | null;
1983
+ updated_at: string;
1984
+ pr_url: string | null;
1985
+ stale_reason: 'task_finished' | 'potentially_abandoned';
1986
+ }>;
1987
+ count: number;
1988
+ cleanup_instructions: string[] | null;
1989
+ }>> {
1990
+ return this.request('GET', `/api/mcp/worktrees/stale?project_id=${projectId}`);
1991
+ }
1992
+
1993
+ async clearWorktreePath(taskId: string): Promise<ApiResponse<{
1994
+ success: boolean;
1995
+ task_id: string;
1996
+ }>> {
1997
+ return this.request('PATCH', `/api/mcp/tasks/${taskId}`, { worktree_path: null });
1998
+ }
1999
+
2000
+ // ============================================================================
2001
+ // Connector endpoints
2002
+ // ============================================================================
2003
+
2004
+ async getConnectors(projectId: string, params?: {
2005
+ type?: string;
2006
+ status?: string;
2007
+ limit?: number;
2008
+ offset?: number;
2009
+ }): Promise<ApiResponse<{
2010
+ connectors: Array<{
2011
+ id: string;
2012
+ name: string;
2013
+ type: string;
2014
+ description?: string;
2015
+ status: string;
2016
+ events: Record<string, boolean>;
2017
+ events_sent: number;
2018
+ last_triggered_at?: string;
2019
+ last_error?: string;
2020
+ last_error_at?: string;
2021
+ created_at: string;
2022
+ }>;
2023
+ total_count: number;
2024
+ has_more: boolean;
2025
+ }>> {
2026
+ return this.proxy('get_connectors', {
2027
+ project_id: projectId,
2028
+ ...params
2029
+ });
2030
+ }
2031
+
2032
+ async getConnector(connectorId: string): Promise<ApiResponse<{
2033
+ connector: {
2034
+ id: string;
2035
+ name: string;
2036
+ type: string;
2037
+ description?: string;
2038
+ config: Record<string, unknown>;
2039
+ events: Record<string, boolean>;
2040
+ status: string;
2041
+ events_sent: number;
2042
+ last_triggered_at?: string;
2043
+ last_error?: string;
2044
+ last_error_at?: string;
2045
+ created_at: string;
2046
+ };
2047
+ }>> {
2048
+ return this.proxy('get_connector', { connector_id: connectorId });
2049
+ }
2050
+
2051
+ async addConnector(projectId: string, params: {
2052
+ name: string;
2053
+ type: string;
2054
+ description?: string;
2055
+ config?: Record<string, unknown>;
2056
+ events?: Record<string, boolean>;
2057
+ }): Promise<ApiResponse<{
2058
+ success: boolean;
2059
+ connector_id: string;
2060
+ }>> {
2061
+ return this.proxy('add_connector', {
2062
+ project_id: projectId,
2063
+ ...params
2064
+ });
2065
+ }
2066
+
2067
+ async updateConnector(connectorId: string, updates: {
2068
+ name?: string;
2069
+ description?: string;
2070
+ config?: Record<string, unknown>;
2071
+ events?: Record<string, boolean>;
2072
+ status?: string;
2073
+ }): Promise<ApiResponse<{
2074
+ success: boolean;
2075
+ connector_id: string;
2076
+ }>> {
2077
+ return this.proxy('update_connector', {
2078
+ connector_id: connectorId,
2079
+ ...updates
2080
+ });
2081
+ }
2082
+
2083
+ async deleteConnector(connectorId: string): Promise<ApiResponse<{
2084
+ success: boolean;
2085
+ }>> {
2086
+ return this.proxy('delete_connector', { connector_id: connectorId });
2087
+ }
2088
+
2089
+ async testConnector(connectorId: string): Promise<ApiResponse<{
2090
+ success: boolean;
2091
+ event_id: string;
2092
+ status?: number;
2093
+ error?: string;
2094
+ }>> {
2095
+ return this.proxy('test_connector', { connector_id: connectorId });
2096
+ }
2097
+
2098
+ async getConnectorEvents(params: {
2099
+ connector_id?: string;
2100
+ project_id?: string;
2101
+ status?: string;
2102
+ limit?: number;
2103
+ offset?: number;
2104
+ }): Promise<ApiResponse<{
2105
+ events: Array<{
2106
+ id: string;
2107
+ connector_id: string;
2108
+ event_type: string;
2109
+ status: string;
2110
+ response_status?: number;
2111
+ error_message?: string;
2112
+ attempts: number;
2113
+ created_at: string;
2114
+ sent_at?: string;
2115
+ }>;
2116
+ total_count: number;
2117
+ has_more: boolean;
2118
+ }>> {
2119
+ return this.proxy('get_connector_events', params);
2120
+ }
1886
2121
  }
1887
2122
 
1888
2123
  // Singleton instance
@@ -176,6 +176,15 @@ export const mockApiClient = {
176
176
  getFileCheckouts: vi.fn(),
177
177
  abandonCheckout: vi.fn(),
178
178
 
179
+ // Connectors
180
+ getConnectors: vi.fn(),
181
+ getConnector: vi.fn(),
182
+ addConnector: vi.fn(),
183
+ updateConnector: vi.fn(),
184
+ deleteConnector: vi.fn(),
185
+ testConnector: vi.fn(),
186
+ getConnectorEvents: vi.fn(),
187
+
179
188
  // Proxy (generic)
180
189
  proxy: vi.fn(),
181
190
  };
@@ -83,19 +83,22 @@ describe('addBlocker', () => {
83
83
  );
84
84
  });
85
85
 
86
- it('should throw error when API call fails', async () => {
86
+ it('should return error when API call fails', async () => {
87
87
  mockApiClient.addBlocker.mockResolvedValue({
88
88
  ok: false,
89
89
  error: 'Insert failed',
90
90
  });
91
91
  const ctx = createMockContext();
92
92
 
93
- await expect(
94
- addBlocker({
95
- project_id: '123e4567-e89b-12d3-a456-426614174000',
96
- description: 'Test blocker',
97
- }, ctx)
98
- ).rejects.toThrow('Insert failed');
93
+ const result = await addBlocker({
94
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
95
+ description: 'Test blocker',
96
+ }, ctx);
97
+
98
+ expect(result.isError).toBe(true);
99
+ expect(result.result).toMatchObject({
100
+ error: 'Insert failed',
101
+ });
99
102
  });
100
103
  });
101
104
 
@@ -177,16 +180,19 @@ describe('resolveBlocker', () => {
177
180
  );
178
181
  });
179
182
 
180
- it('should throw error when API call fails', async () => {
183
+ it('should return error when API call fails', async () => {
181
184
  mockApiClient.resolveBlocker.mockResolvedValue({
182
185
  ok: false,
183
186
  error: 'Update failed',
184
187
  });
185
188
  const ctx = createMockContext();
186
189
 
187
- await expect(
188
- resolveBlocker({ blocker_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
189
- ).rejects.toThrow('Update failed');
190
+ const result = await resolveBlocker({ blocker_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
191
+
192
+ expect(result.isError).toBe(true);
193
+ expect(result.result).toMatchObject({
194
+ error: 'Update failed',
195
+ });
190
196
  });
191
197
  });
192
198
 
@@ -298,16 +304,19 @@ describe('getBlockers', () => {
298
304
  expect(mockApiClient.getBlockers).toHaveBeenCalled();
299
305
  });
300
306
 
301
- it('should throw error when API call fails', async () => {
307
+ it('should return error when API call fails', async () => {
302
308
  mockApiClient.getBlockers.mockResolvedValue({
303
309
  ok: false,
304
310
  error: 'Query failed',
305
311
  });
306
312
  const ctx = createMockContext();
307
313
 
308
- await expect(
309
- getBlockers({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
310
- ).rejects.toThrow('Query failed');
314
+ const result = await getBlockers({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
315
+
316
+ expect(result.isError).toBe(true);
317
+ expect(result.result).toMatchObject({
318
+ error: 'Query failed',
319
+ });
311
320
  });
312
321
  });
313
322
 
@@ -366,15 +375,18 @@ describe('deleteBlocker', () => {
366
375
  );
367
376
  });
368
377
 
369
- it('should throw error when API call fails', async () => {
378
+ it('should return error when API call fails', async () => {
370
379
  mockApiClient.deleteBlocker.mockResolvedValue({
371
380
  ok: false,
372
381
  error: 'Delete failed',
373
382
  });
374
383
  const ctx = createMockContext();
375
384
 
376
- await expect(
377
- deleteBlocker({ blocker_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
378
- ).rejects.toThrow('Delete failed');
385
+ const result = await deleteBlocker({ blocker_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
386
+
387
+ expect(result.isError).toBe(true);
388
+ expect(result.result).toMatchObject({
389
+ error: 'Delete failed',
390
+ });
379
391
  });
380
392
  });
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { Handler, HandlerRegistry } from './types.js';
12
+ import { success, error } from './types.js';
12
13
  import {
13
14
  parseArgs,
14
15
  uuidValidator,
@@ -47,10 +48,10 @@ export const addBlocker: Handler = async (args, ctx) => {
47
48
  const response = await apiClient.addBlocker(project_id, description, ctx.session.currentSessionId || undefined);
48
49
 
49
50
  if (!response.ok) {
50
- throw new Error(response.error || 'Failed to add blocker');
51
+ return error(response.error || 'Failed to add blocker');
51
52
  }
52
53
 
53
- return { result: response.data };
54
+ return success(response.data);
54
55
  };
55
56
 
56
57
  export const resolveBlocker: Handler = async (args, _ctx) => {
@@ -60,10 +61,10 @@ export const resolveBlocker: Handler = async (args, _ctx) => {
60
61
  const response = await apiClient.resolveBlocker(blocker_id, resolution_note);
61
62
 
62
63
  if (!response.ok) {
63
- throw new Error(response.error || 'Failed to resolve blocker');
64
+ return error(response.error || 'Failed to resolve blocker');
64
65
  }
65
66
 
66
- return { result: response.data };
67
+ return success(response.data);
67
68
  };
68
69
 
69
70
  export const getBlockers: Handler = async (args, _ctx) => {
@@ -78,10 +79,10 @@ export const getBlockers: Handler = async (args, _ctx) => {
78
79
  });
79
80
 
80
81
  if (!response.ok) {
81
- throw new Error(response.error || 'Failed to fetch blockers');
82
+ return error(response.error || 'Failed to fetch blockers');
82
83
  }
83
84
 
84
- return { result: response.data };
85
+ return success(response.data);
85
86
  };
86
87
 
87
88
  export const deleteBlocker: Handler = async (args, _ctx) => {
@@ -91,10 +92,10 @@ export const deleteBlocker: Handler = async (args, _ctx) => {
91
92
  const response = await apiClient.deleteBlocker(blocker_id);
92
93
 
93
94
  if (!response.ok) {
94
- throw new Error(response.error || 'Failed to delete blocker');
95
+ return error(response.error || 'Failed to delete blocker');
95
96
  }
96
97
 
97
- return { result: response.data };
98
+ return success(response.data);
98
99
  };
99
100
 
100
101
  /**
@@ -109,16 +109,19 @@ describe('createBodyOfWork', () => {
109
109
  );
110
110
  });
111
111
 
112
- it('should throw error when API call fails', async () => {
112
+ it('should return error when API call fails', async () => {
113
113
  const ctx = createMockContext();
114
114
  mockApiClient.proxy.mockResolvedValue({
115
115
  ok: false,
116
116
  error: 'Insert failed',
117
117
  });
118
118
 
119
- await expect(
120
- createBodyOfWork({ project_id: VALID_UUID, title: 'Test' }, ctx)
121
- ).rejects.toThrow('Failed to create body of work');
119
+ const result = await createBodyOfWork({ project_id: VALID_UUID, title: 'Test' }, ctx);
120
+
121
+ expect(result.isError).toBe(true);
122
+ expect(result.result).toMatchObject({
123
+ error: 'Insert failed',
124
+ });
122
125
  });
123
126
  });
124
127
 
@@ -201,16 +204,19 @@ describe('getBodyOfWork', () => {
201
204
  await expect(getBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
202
205
  });
203
206
 
204
- it('should throw error when body of work not found', async () => {
207
+ it('should return error when body of work not found', async () => {
205
208
  const ctx = createMockContext();
206
209
  mockApiClient.proxy.mockResolvedValue({
207
210
  ok: false,
208
211
  error: 'Not found',
209
212
  });
210
213
 
211
- await expect(
212
- getBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
213
- ).rejects.toThrow('Failed to get body of work');
214
+ const result = await getBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
215
+
216
+ expect(result.isError).toBe(true);
217
+ expect(result.result).toMatchObject({
218
+ error: 'Not found',
219
+ });
214
220
  });
215
221
 
216
222
  it('should return body of work with tasks organized by phase', async () => {
@@ -331,16 +337,19 @@ describe('addTaskToBodyOfWork', () => {
331
337
  ).rejects.toThrow(ValidationError);
332
338
  });
333
339
 
334
- it('should throw error when API returns error', async () => {
340
+ it('should return error when API returns error', async () => {
335
341
  const ctx = createMockContext();
336
342
  mockApiClient.proxy.mockResolvedValue({
337
343
  ok: false,
338
344
  error: 'Body of work not found',
339
345
  });
340
346
 
341
- await expect(
342
- addTaskToBodyOfWork({ body_of_work_id: VALID_UUID, task_id: VALID_UUID_2 }, ctx)
343
- ).rejects.toThrow('Failed to add task to body of work');
347
+ const result = await addTaskToBodyOfWork({ body_of_work_id: VALID_UUID, task_id: VALID_UUID_2 }, ctx);
348
+
349
+ expect(result.isError).toBe(true);
350
+ expect(result.result).toMatchObject({
351
+ error: 'Body of work not found',
352
+ });
344
353
  });
345
354
 
346
355
  it('should add task with default phase "core"', async () => {
@@ -435,16 +444,19 @@ describe('activateBodyOfWork', () => {
435
444
  await expect(activateBodyOfWork({}, ctx)).rejects.toThrow(ValidationError);
436
445
  });
437
446
 
438
- it('should throw error when API returns error', async () => {
447
+ it('should return error when API returns error', async () => {
439
448
  const ctx = createMockContext();
440
449
  mockApiClient.proxy.mockResolvedValue({
441
450
  ok: false,
442
451
  error: 'Body of work not found',
443
452
  });
444
453
 
445
- await expect(
446
- activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx)
447
- ).rejects.toThrow('Failed to activate body of work');
454
+ const result = await activateBodyOfWork({ body_of_work_id: VALID_UUID }, ctx);
455
+
456
+ expect(result.isError).toBe(true);
457
+ expect(result.result).toMatchObject({
458
+ error: 'Body of work not found',
459
+ });
448
460
  });
449
461
 
450
462
  it('should activate body of work successfully', async () => {
@@ -484,15 +496,19 @@ describe('addTaskDependency', () => {
484
496
  ).rejects.toThrow(ValidationError);
485
497
  });
486
498
 
487
- it('should throw error when task depends on itself', async () => {
499
+ it('should return error when task depends on itself', async () => {
488
500
  const ctx = createMockContext();
489
- await expect(
490
- addTaskDependency({
491
- body_of_work_id: VALID_UUID,
492
- task_id: VALID_UUID_2,
493
- depends_on_task_id: VALID_UUID_2,
494
- }, ctx)
495
- ).rejects.toThrow('A task cannot depend on itself');
501
+
502
+ const result = await addTaskDependency({
503
+ body_of_work_id: VALID_UUID,
504
+ task_id: VALID_UUID_2,
505
+ depends_on_task_id: VALID_UUID_2,
506
+ }, ctx);
507
+
508
+ expect(result.isError).toBe(true);
509
+ expect(result.result).toMatchObject({
510
+ error: 'A task cannot depend on itself',
511
+ });
496
512
  });
497
513
 
498
514
  it('should add dependency successfully', async () => {
@@ -572,11 +588,15 @@ describe('removeTaskDependency', () => {
572
588
  describe('getTaskDependencies', () => {
573
589
  beforeEach(() => vi.clearAllMocks());
574
590
 
575
- it('should throw error when neither body_of_work_id nor task_id provided', async () => {
591
+ it('should return error when neither body_of_work_id nor task_id provided', async () => {
576
592
  const ctx = createMockContext();
577
- await expect(getTaskDependencies({}, ctx)).rejects.toThrow(
578
- 'Either body_of_work_id or task_id is required'
579
- );
593
+
594
+ const result = await getTaskDependencies({}, ctx);
595
+
596
+ expect(result.isError).toBe(true);
597
+ expect(result.result).toMatchObject({
598
+ error: 'Either body_of_work_id or task_id is required',
599
+ });
580
600
  });
581
601
 
582
602
  it('should return dependencies filtered by body_of_work_id', async () => {
@@ -626,16 +646,19 @@ describe('getNextBodyOfWorkTask', () => {
626
646
  await expect(getNextBodyOfWorkTask({}, ctx)).rejects.toThrow(ValidationError);
627
647
  });
628
648
 
629
- it('should throw error when API returns error', async () => {
649
+ it('should return error when API returns error', async () => {
630
650
  const ctx = createMockContext();
631
651
  mockApiClient.proxy.mockResolvedValue({
632
652
  ok: false,
633
653
  error: 'Body of work not found',
634
654
  });
635
655
 
636
- await expect(
637
- getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx)
638
- ).rejects.toThrow('Failed to get next body of work task');
656
+ const result = await getNextBodyOfWorkTask({ body_of_work_id: VALID_UUID }, ctx);
657
+
658
+ expect(result.isError).toBe(true);
659
+ expect(result.result).toMatchObject({
660
+ error: 'Body of work not found',
661
+ });
639
662
  });
640
663
 
641
664
  it('should return null when no tasks available', async () => {