@vibescope/mcp-server 0.1.0 → 0.2.0

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/api-client.d.ts +56 -1
  3. package/dist/api-client.js +17 -2
  4. package/dist/handlers/bodies-of-work.js +3 -2
  5. package/dist/handlers/deployment.js +3 -2
  6. package/dist/handlers/discovery.d.ts +3 -0
  7. package/dist/handlers/discovery.js +20 -652
  8. package/dist/handlers/fallback.js +18 -9
  9. package/dist/handlers/findings.d.ts +8 -1
  10. package/dist/handlers/findings.js +24 -3
  11. package/dist/handlers/session.js +23 -8
  12. package/dist/handlers/sprints.js +3 -2
  13. package/dist/handlers/tasks.js +22 -1
  14. package/dist/handlers/tool-docs.d.ts +4 -3
  15. package/dist/handlers/tool-docs.js +252 -5
  16. package/dist/handlers/validation.js +13 -1
  17. package/dist/index.js +25 -7
  18. package/dist/tools.js +30 -4
  19. package/package.json +1 -1
  20. package/src/api-client.ts +72 -2
  21. package/src/handlers/__test-setup__.ts +5 -0
  22. package/src/handlers/bodies-of-work.ts +27 -11
  23. package/src/handlers/deployment.ts +4 -2
  24. package/src/handlers/discovery.ts +23 -740
  25. package/src/handlers/fallback.test.ts +78 -0
  26. package/src/handlers/fallback.ts +20 -9
  27. package/src/handlers/findings.test.ts +129 -2
  28. package/src/handlers/findings.ts +32 -3
  29. package/src/handlers/session.test.ts +37 -2
  30. package/src/handlers/session.ts +29 -8
  31. package/src/handlers/sprints.ts +19 -6
  32. package/src/handlers/tasks.test.ts +61 -0
  33. package/src/handlers/tasks.ts +26 -1
  34. package/src/handlers/tool-docs.ts +1024 -0
  35. package/src/handlers/validation.test.ts +52 -0
  36. package/src/handlers/validation.ts +14 -1
  37. package/src/index.ts +25 -7
  38. package/src/tools.ts +30 -4
  39. package/src/knowledge.ts +0 -230
@@ -145,6 +145,84 @@ describe('startFallbackActivity', () => {
145
145
  }, ctx)
146
146
  ).rejects.toThrow('Failed to start fallback activity');
147
147
  });
148
+
149
+ it('should pass through worktree guidance when API returns it', async () => {
150
+ mockApiClient.startFallbackActivity.mockResolvedValue({
151
+ ok: true,
152
+ data: {
153
+ success: true,
154
+ activity: 'code_review',
155
+ message: 'Started code_review',
156
+ git_workflow: {
157
+ workflow: 'git-flow',
158
+ base_branch: 'develop',
159
+ worktree_recommended: true,
160
+ note: 'Fallback activities use the base branch directly (read-only).',
161
+ },
162
+ worktree_setup: {
163
+ message: 'RECOMMENDED: Create a worktree to avoid conflicts.',
164
+ commands: [
165
+ 'git checkout develop',
166
+ 'git pull origin develop',
167
+ 'git worktree add ../Project-code-review develop',
168
+ 'cd ../Project-code-review',
169
+ ],
170
+ worktree_path: '../Project-code-review',
171
+ branch_name: 'develop',
172
+ cleanup_command: 'git worktree remove ../Project-code-review',
173
+ report_worktree: 'heartbeat(current_worktree_path: "../Project-code-review")',
174
+ },
175
+ next_step: 'After setting up worktree: call heartbeat to report your location.',
176
+ },
177
+ });
178
+ const ctx = createMockContext();
179
+
180
+ const result = await startFallbackActivity(
181
+ {
182
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
183
+ activity: 'code_review',
184
+ },
185
+ ctx
186
+ );
187
+
188
+ expect(result.result).toMatchObject({
189
+ success: true,
190
+ activity: 'code_review',
191
+ });
192
+ expect((result.result as { git_workflow?: unknown }).git_workflow).toBeDefined();
193
+ expect((result.result as { git_workflow: { workflow: string } }).git_workflow.workflow).toBe('git-flow');
194
+ expect((result.result as { worktree_setup?: unknown }).worktree_setup).toBeDefined();
195
+ expect((result.result as { worktree_setup: { worktree_path: string } }).worktree_setup.worktree_path).toBe('../Project-code-review');
196
+ expect((result.result as { next_step?: string }).next_step).toContain('heartbeat');
197
+ });
198
+
199
+ it('should not include worktree guidance when API does not return it', async () => {
200
+ mockApiClient.startFallbackActivity.mockResolvedValue({
201
+ ok: true,
202
+ data: {
203
+ success: true,
204
+ activity: 'code_review',
205
+ message: 'Started code_review',
206
+ },
207
+ });
208
+ const ctx = createMockContext();
209
+
210
+ const result = await startFallbackActivity(
211
+ {
212
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
213
+ activity: 'code_review',
214
+ },
215
+ ctx
216
+ );
217
+
218
+ expect(result.result).toMatchObject({
219
+ success: true,
220
+ activity: 'code_review',
221
+ });
222
+ expect((result.result as { git_workflow?: unknown }).git_workflow).toBeUndefined();
223
+ expect((result.result as { worktree_setup?: unknown }).worktree_setup).toBeUndefined();
224
+ expect((result.result as { next_step?: string }).next_step).toBeUndefined();
225
+ });
148
226
  });
149
227
 
150
228
  // ============================================================================
@@ -51,16 +51,27 @@ export const startFallbackActivity: Handler = async (args, ctx) => {
51
51
  // Get the activity details for the response
52
52
  const activityInfo = FALLBACK_ACTIVITIES.find((a) => a.activity === activity);
53
53
 
54
- return {
55
- result: {
56
- success: true,
57
- activity,
58
- title: activityInfo?.title || activity,
59
- description: activityInfo?.description || '',
60
- prompt: activityInfo?.prompt || '',
61
- message: response.data?.message || `Started fallback activity: ${activityInfo?.title || activity}`,
62
- },
54
+ const result: Record<string, unknown> = {
55
+ success: true,
56
+ activity,
57
+ title: activityInfo?.title || activity,
58
+ description: activityInfo?.description || '',
59
+ prompt: activityInfo?.prompt || '',
60
+ message: response.data?.message || `Started fallback activity: ${activityInfo?.title || activity}`,
63
61
  };
62
+
63
+ // Pass through worktree guidance if provided
64
+ if (response.data?.git_workflow) {
65
+ result.git_workflow = response.data.git_workflow;
66
+ }
67
+ if (response.data?.worktree_setup) {
68
+ result.worktree_setup = response.data.worktree_setup;
69
+ }
70
+ if (response.data?.next_step) {
71
+ result.next_step = response.data.next_step;
72
+ }
73
+
74
+ return { result };
64
75
  };
65
76
 
66
77
  export const stopFallbackActivity: Handler = async (args, ctx) => {
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import {
3
3
  addFinding,
4
4
  getFindings,
5
+ getFindingsStats,
5
6
  updateFinding,
6
7
  deleteFinding,
7
8
  } from './findings.js';
@@ -171,12 +172,74 @@ describe('getFindings', () => {
171
172
 
172
173
  expect(mockApiClient.getFindings).toHaveBeenCalledWith(
173
174
  VALID_UUID,
174
- {
175
+ expect.objectContaining({
175
176
  category: 'security',
176
177
  severity: 'critical',
177
178
  status: 'open',
178
179
  limit: 10,
179
- }
180
+ })
181
+ );
182
+ });
183
+
184
+ it('should pass summary_only parameter to API client', async () => {
185
+ mockApiClient.getFindings.mockResolvedValue({
186
+ ok: true,
187
+ data: { findings: [], total_count: 0, has_more: false },
188
+ });
189
+ const ctx = createMockContext();
190
+
191
+ await getFindings({
192
+ project_id: VALID_UUID,
193
+ summary_only: true
194
+ }, ctx);
195
+
196
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
197
+ VALID_UUID,
198
+ expect.objectContaining({
199
+ summary_only: true,
200
+ })
201
+ );
202
+ });
203
+
204
+ it('should pass search_query parameter to API client', async () => {
205
+ mockApiClient.getFindings.mockResolvedValue({
206
+ ok: true,
207
+ data: { findings: [], total_count: 0, has_more: false },
208
+ });
209
+ const ctx = createMockContext();
210
+
211
+ await getFindings({
212
+ project_id: VALID_UUID,
213
+ search_query: 'security'
214
+ }, ctx);
215
+
216
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
217
+ VALID_UUID,
218
+ expect.objectContaining({
219
+ search_query: 'security',
220
+ })
221
+ );
222
+ });
223
+
224
+ it('should pass offset parameter to API client', async () => {
225
+ mockApiClient.getFindings.mockResolvedValue({
226
+ ok: true,
227
+ data: { findings: [], total_count: 100, has_more: true },
228
+ });
229
+ const ctx = createMockContext();
230
+
231
+ await getFindings({
232
+ project_id: VALID_UUID,
233
+ offset: 50,
234
+ limit: 25
235
+ }, ctx);
236
+
237
+ expect(mockApiClient.getFindings).toHaveBeenCalledWith(
238
+ VALID_UUID,
239
+ expect.objectContaining({
240
+ offset: 50,
241
+ limit: 25,
242
+ })
180
243
  );
181
244
  });
182
245
 
@@ -345,3 +408,67 @@ describe('deleteFinding', () => {
345
408
  ).rejects.toThrow('Delete failed');
346
409
  });
347
410
  });
411
+
412
+ // ============================================================================
413
+ // getFindingsStats Tests
414
+ // ============================================================================
415
+
416
+ describe('getFindingsStats', () => {
417
+ beforeEach(() => vi.clearAllMocks());
418
+
419
+ it('should throw error for missing project_id', async () => {
420
+ const ctx = createMockContext();
421
+
422
+ await expect(getFindingsStats({}, ctx)).rejects.toThrow(ValidationError);
423
+ });
424
+
425
+ it('should throw error for invalid project_id UUID', async () => {
426
+ const ctx = createMockContext();
427
+
428
+ await expect(
429
+ getFindingsStats({ project_id: 'invalid' }, ctx)
430
+ ).rejects.toThrow(ValidationError);
431
+ });
432
+
433
+ it('should return findings stats for project', async () => {
434
+ const mockStats = {
435
+ total: 10,
436
+ by_status: { open: 5, addressed: 3, dismissed: 2 },
437
+ by_severity: { critical: 1, high: 3, medium: 4, low: 2 },
438
+ by_category: { security: 3, performance: 4, code_quality: 3 },
439
+ };
440
+ mockApiClient.getFindingsStats.mockResolvedValue({
441
+ ok: true,
442
+ data: mockStats,
443
+ });
444
+ const ctx = createMockContext();
445
+
446
+ const result = await getFindingsStats({ project_id: VALID_UUID }, ctx);
447
+
448
+ expect(result.result).toMatchObject(mockStats);
449
+ });
450
+
451
+ it('should call API client getFindingsStats with project_id', async () => {
452
+ mockApiClient.getFindingsStats.mockResolvedValue({
453
+ ok: true,
454
+ data: { total: 0, by_status: {}, by_severity: {}, by_category: {} },
455
+ });
456
+ const ctx = createMockContext();
457
+
458
+ await getFindingsStats({ project_id: VALID_UUID }, ctx);
459
+
460
+ expect(mockApiClient.getFindingsStats).toHaveBeenCalledWith(VALID_UUID);
461
+ });
462
+
463
+ it('should throw error when API call fails', async () => {
464
+ mockApiClient.getFindingsStats.mockResolvedValue({
465
+ ok: false,
466
+ error: 'Query failed',
467
+ });
468
+ const ctx = createMockContext();
469
+
470
+ await expect(
471
+ getFindingsStats({ project_id: VALID_UUID }, ctx)
472
+ ).rejects.toThrow('Query failed');
473
+ });
474
+ });
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Handles audit findings and knowledge base:
5
5
  * - add_finding
6
- * - get_findings
6
+ * - get_findings (supports summary_only for reduced tokens)
7
+ * - get_findings_stats (aggregate counts for minimal tokens)
7
8
  * - update_finding
8
9
  * - delete_finding
9
10
  */
@@ -52,7 +53,7 @@ export const addFinding: Handler = async (args, ctx) => {
52
53
  };
53
54
 
54
55
  export const getFindings: Handler = async (args, ctx) => {
55
- const { project_id, category, severity, status, limit = 50, offset = 0, search_query } = args as {
56
+ const { project_id, category, severity, status, limit = 50, offset = 0, search_query, summary_only = false } = args as {
56
57
  project_id: string;
57
58
  category?: FindingCategory;
58
59
  severity?: FindingSeverity;
@@ -60,6 +61,7 @@ export const getFindings: Handler = async (args, ctx) => {
60
61
  limit?: number;
61
62
  offset?: number;
62
63
  search_query?: string;
64
+ summary_only?: boolean;
63
65
  };
64
66
 
65
67
  validateRequired(project_id, 'project_id');
@@ -70,7 +72,10 @@ export const getFindings: Handler = async (args, ctx) => {
70
72
  category,
71
73
  severity,
72
74
  status,
73
- limit
75
+ limit,
76
+ offset,
77
+ search_query,
78
+ summary_only
74
79
  });
75
80
 
76
81
  if (!response.ok) {
@@ -80,6 +85,29 @@ export const getFindings: Handler = async (args, ctx) => {
80
85
  return { result: response.data };
81
86
  };
82
87
 
88
+ /**
89
+ * Get aggregate statistics about findings for a project.
90
+ * Returns counts by category, severity, and status without the actual finding data.
91
+ * This is much more token-efficient than get_findings for understanding the overall state.
92
+ */
93
+ export const getFindingsStats: Handler = async (args, ctx) => {
94
+ const { project_id } = args as {
95
+ project_id: string;
96
+ };
97
+
98
+ validateRequired(project_id, 'project_id');
99
+ validateUUID(project_id, 'project_id');
100
+
101
+ const apiClient = getApiClient();
102
+ const response = await apiClient.getFindingsStats(project_id);
103
+
104
+ if (!response.ok) {
105
+ throw new Error(response.error || 'Failed to get findings stats');
106
+ }
107
+
108
+ return { result: response.data };
109
+ };
110
+
83
111
  export const updateFinding: Handler = async (args, ctx) => {
84
112
  const { finding_id, status, resolution_note, title, description, severity } = args as {
85
113
  finding_id: string;
@@ -131,6 +159,7 @@ export const deleteFinding: Handler = async (args, ctx) => {
131
159
  export const findingHandlers: HandlerRegistry = {
132
160
  add_finding: addFinding,
133
161
  get_findings: getFindings,
162
+ get_findings_stats: getFindingsStats,
134
163
  update_finding: updateFinding,
135
164
  delete_finding: deleteFinding,
136
165
  };
@@ -31,7 +31,7 @@ describe('heartbeat', () => {
31
31
  session_id: 'session-123',
32
32
  });
33
33
  expect(result.result).toHaveProperty('timestamp');
34
- expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123');
34
+ expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123', { current_worktree_path: undefined });
35
35
  });
36
36
 
37
37
  it('should use provided session_id over current session', async () => {
@@ -48,7 +48,20 @@ describe('heartbeat', () => {
48
48
  success: true,
49
49
  session_id: 'other-session-456',
50
50
  });
51
- expect(mockApiClient.heartbeat).toHaveBeenCalledWith('other-session-456');
51
+ expect(mockApiClient.heartbeat).toHaveBeenCalledWith('other-session-456', { current_worktree_path: undefined });
52
+ });
53
+
54
+ it('should pass worktree_path to API', async () => {
55
+ const ctx = createMockContext();
56
+ mockApiClient.heartbeat.mockResolvedValue({
57
+ ok: true,
58
+ data: { timestamp: '2026-01-14T10:00:00Z' },
59
+ });
60
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
61
+
62
+ await heartbeat({ current_worktree_path: '../project-task-abc123' }, ctx);
63
+
64
+ expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123', { current_worktree_path: '../project-task-abc123' });
52
65
  });
53
66
 
54
67
  it('should return error when no active session', async () => {
@@ -101,6 +114,10 @@ describe('getHelp', () => {
101
114
 
102
115
  it('should return help content for valid topic', async () => {
103
116
  const ctx = createMockContext();
117
+ mockApiClient.getHelpTopic.mockResolvedValue({
118
+ ok: true,
119
+ data: { slug: 'tasks', title: 'Task Workflow', content: '# Task Workflow\nTest content' },
120
+ });
104
121
 
105
122
  const result = await getHelp({ topic: 'tasks' }, ctx);
106
123
 
@@ -110,6 +127,10 @@ describe('getHelp', () => {
110
127
 
111
128
  it('should return getting_started help', async () => {
112
129
  const ctx = createMockContext();
130
+ mockApiClient.getHelpTopic.mockResolvedValue({
131
+ ok: true,
132
+ data: { slug: 'getting_started', title: 'Getting Started', content: '# Getting Started\nTest content' },
133
+ });
113
134
 
114
135
  const result = await getHelp({ topic: 'getting_started' }, ctx);
115
136
 
@@ -119,6 +140,11 @@ describe('getHelp', () => {
119
140
 
120
141
  it('should return error for unknown topic', async () => {
121
142
  const ctx = createMockContext();
143
+ mockApiClient.getHelpTopic.mockResolvedValue({ ok: true, data: null });
144
+ mockApiClient.getHelpTopics.mockResolvedValue({
145
+ ok: true,
146
+ data: [{ slug: 'tasks', title: 'Tasks' }, { slug: 'getting_started', title: 'Getting Started' }],
147
+ });
122
148
 
123
149
  const result = await getHelp({ topic: 'unknown_topic' }, ctx);
124
150
 
@@ -130,6 +156,15 @@ describe('getHelp', () => {
130
156
 
131
157
  it('should list available topics for unknown topic', async () => {
132
158
  const ctx = createMockContext();
159
+ mockApiClient.getHelpTopic.mockResolvedValue({ ok: true, data: null });
160
+ mockApiClient.getHelpTopics.mockResolvedValue({
161
+ ok: true,
162
+ data: [
163
+ { slug: 'tasks', title: 'Tasks' },
164
+ { slug: 'getting_started', title: 'Getting Started' },
165
+ { slug: 'validation', title: 'Validation' },
166
+ ],
167
+ });
133
168
 
134
169
  const result = await getHelp({ topic: 'nonexistent' }, ctx);
135
170
 
@@ -10,7 +10,6 @@
10
10
  */
11
11
 
12
12
  import type { Handler, HandlerRegistry, TokenUsage } from './types.js';
13
- import { KNOWLEDGE_BASE } from '../knowledge.js';
14
13
  import { getApiClient } from '../api-client.js';
15
14
 
16
15
  export const startWorkSession: Handler = async (args, ctx) => {
@@ -140,7 +139,10 @@ export const startWorkSession: Handler = async (args, ctx) => {
140
139
  };
141
140
 
142
141
  export const heartbeat: Handler = async (args, ctx) => {
143
- const { session_id } = args as { session_id?: string };
142
+ const { session_id, current_worktree_path } = args as {
143
+ session_id?: string;
144
+ current_worktree_path?: string | null;
145
+ };
144
146
  const { session } = ctx;
145
147
  const targetSession = session_id || session.currentSessionId;
146
148
 
@@ -154,8 +156,10 @@ export const heartbeat: Handler = async (args, ctx) => {
154
156
 
155
157
  const apiClient = getApiClient();
156
158
 
157
- // Send heartbeat
158
- const heartbeatResponse = await apiClient.heartbeat(targetSession);
159
+ // Send heartbeat with optional worktree path
160
+ const heartbeatResponse = await apiClient.heartbeat(targetSession, {
161
+ current_worktree_path,
162
+ });
159
163
 
160
164
  if (!heartbeatResponse.ok) {
161
165
  return {
@@ -249,17 +253,34 @@ export const endWorkSession: Handler = async (args, ctx) => {
249
253
  export const getHelp: Handler = async (args, _ctx) => {
250
254
  const { topic } = args as { topic: string };
251
255
 
252
- const content = KNOWLEDGE_BASE[topic];
253
- if (!content) {
256
+ const apiClient = getApiClient();
257
+ const response = await apiClient.getHelpTopic(topic);
258
+
259
+ if (!response.ok) {
260
+ // If database fetch fails, return error
261
+ return {
262
+ result: {
263
+ error: response.error || `Failed to fetch help topic: ${topic}`,
264
+ },
265
+ };
266
+ }
267
+
268
+ if (!response.data) {
269
+ // Topic not found - fetch available topics
270
+ const topicsResponse = await apiClient.getHelpTopics();
271
+ const available = topicsResponse.ok && topicsResponse.data
272
+ ? topicsResponse.data.map(t => t.slug)
273
+ : ['getting_started', 'tasks', 'validation', 'deployment', 'git', 'blockers', 'milestones', 'fallback', 'session', 'tokens', 'sprints', 'topics'];
274
+
254
275
  return {
255
276
  result: {
256
277
  error: `Unknown topic: ${topic}`,
257
- available: Object.keys(KNOWLEDGE_BASE),
278
+ available,
258
279
  },
259
280
  };
260
281
  }
261
282
 
262
- return { result: { topic, content } };
283
+ return { result: { topic, content: response.data.content } };
263
284
  };
264
285
 
265
286
  // Model pricing rates (USD per 1M tokens)
@@ -182,13 +182,14 @@ export const updateSprint: Handler = async (args, ctx) => {
182
182
  };
183
183
 
184
184
  export const getSprint: Handler = async (args, ctx) => {
185
- const { sprint_id } = args as { sprint_id: string };
185
+ const { sprint_id, summary_only = false } = args as { sprint_id: string; summary_only?: boolean };
186
186
 
187
187
  validateRequired(sprint_id, 'sprint_id');
188
188
  validateUUID(sprint_id, 'sprint_id');
189
189
 
190
190
  const apiClient = getApiClient();
191
191
 
192
+ // Response type varies based on summary_only
192
193
  const response = await apiClient.proxy<{
193
194
  id: string;
194
195
  title: string;
@@ -200,11 +201,23 @@ export const getSprint: Handler = async (args, ctx) => {
200
201
  progress_percentage: number;
201
202
  velocity_points: number;
202
203
  committed_points: number;
203
- pre_tasks: unknown[];
204
- core_tasks: unknown[];
205
- post_tasks: unknown[];
206
- total_tasks: number;
207
- }>('get_sprint', { sprint_id });
204
+ // Full response includes task arrays
205
+ pre_tasks?: unknown[];
206
+ core_tasks?: unknown[];
207
+ post_tasks?: unknown[];
208
+ total_tasks?: number;
209
+ // Summary response includes counts and next task
210
+ task_counts?: {
211
+ pre: { total: number; completed: number };
212
+ core: { total: number; completed: number };
213
+ post: { total: number; completed: number };
214
+ total: number;
215
+ completed: number;
216
+ in_progress: number;
217
+ };
218
+ current_task?: { id: string; title: string } | null;
219
+ next_task?: { id: string; title: string; priority: number } | null;
220
+ }>('get_sprint', { sprint_id, summary_only });
208
221
 
209
222
  if (!response.ok) {
210
223
  throw new Error(`Failed to get sprint: ${response.error}`);
@@ -352,6 +352,67 @@ describe('updateTask', () => {
352
352
  error: 'task_claimed',
353
353
  });
354
354
  });
355
+
356
+ it('should warn when setting in_progress without git_branch', async () => {
357
+ mockApiClient.updateTask.mockResolvedValue({
358
+ ok: true,
359
+ data: { success: true },
360
+ });
361
+
362
+ const ctx = createMockContext();
363
+ const result = await updateTask(
364
+ {
365
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
366
+ status: 'in_progress',
367
+ },
368
+ ctx
369
+ );
370
+
371
+ expect(result.result).toMatchObject({
372
+ success: true,
373
+ warning: expect.stringContaining('git_branch not set'),
374
+ hint: expect.stringContaining('update_task again'),
375
+ });
376
+ });
377
+
378
+ it('should not warn when setting in_progress with git_branch', async () => {
379
+ mockApiClient.updateTask.mockResolvedValue({
380
+ ok: true,
381
+ data: { success: true },
382
+ });
383
+
384
+ const ctx = createMockContext();
385
+ const result = await updateTask(
386
+ {
387
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
388
+ status: 'in_progress',
389
+ git_branch: 'feature/my-task',
390
+ },
391
+ ctx
392
+ );
393
+
394
+ expect(result.result).toMatchObject({ success: true });
395
+ expect(result.result).not.toHaveProperty('warning');
396
+ });
397
+
398
+ it('should not warn when updating status other than in_progress', async () => {
399
+ mockApiClient.updateTask.mockResolvedValue({
400
+ ok: true,
401
+ data: { success: true },
402
+ });
403
+
404
+ const ctx = createMockContext();
405
+ const result = await updateTask(
406
+ {
407
+ task_id: '123e4567-e89b-12d3-a456-426614174000',
408
+ status: 'completed',
409
+ },
410
+ ctx
411
+ );
412
+
413
+ expect(result.result).toMatchObject({ success: true });
414
+ expect(result.result).not.toHaveProperty('warning');
415
+ });
355
416
  });
356
417
 
357
418
  // ============================================================================
@@ -313,7 +313,27 @@ export const updateTask: Handler = async (args, ctx) => {
313
313
  throw new Error(`Failed to update task: ${response.error}`);
314
314
  }
315
315
 
316
- return { result: { success: true, task_id } };
316
+ // Build result - include git workflow info when transitioning to in_progress
317
+ const data = response.data;
318
+ const result: Record<string, unknown> = { success: true, task_id };
319
+
320
+ if (data?.git_workflow) {
321
+ result.git_workflow = data.git_workflow;
322
+ }
323
+ if (data?.worktree_setup) {
324
+ result.worktree_setup = data.worktree_setup;
325
+ }
326
+ if (data?.next_step) {
327
+ result.next_step = data.next_step;
328
+ }
329
+
330
+ // Warn if transitioning to in_progress without git_branch
331
+ if (updates.status === 'in_progress' && !updates.git_branch) {
332
+ result.warning = 'git_branch not set. For multi-agent collaboration, set git_branch when marking in_progress to track your worktree.';
333
+ result.hint = 'Call update_task again with git_branch parameter after creating your worktree.';
334
+ }
335
+
336
+ return { result };
317
337
  };
318
338
 
319
339
  export const completeTask: Handler = async (args, ctx) => {
@@ -353,6 +373,11 @@ export const completeTask: Handler = async (args, ctx) => {
353
373
  result.context = data.context;
354
374
  }
355
375
 
376
+ // Pass through warnings (e.g., missing git_branch)
377
+ if (data.warnings) {
378
+ result.warnings = data.warnings;
379
+ }
380
+
356
381
  // Git workflow instructions are already in API response but we need to fetch
357
382
  // task details if we want to include them (API should return these)
358
383
  result.next_action = data.next_action;