@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.
- package/README.md +1 -1
- package/dist/api-client.d.ts +56 -1
- package/dist/api-client.js +17 -2
- package/dist/handlers/bodies-of-work.js +3 -2
- package/dist/handlers/deployment.js +3 -2
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +20 -652
- package/dist/handlers/fallback.js +18 -9
- package/dist/handlers/findings.d.ts +8 -1
- package/dist/handlers/findings.js +24 -3
- package/dist/handlers/session.js +23 -8
- package/dist/handlers/sprints.js +3 -2
- package/dist/handlers/tasks.js +22 -1
- package/dist/handlers/tool-docs.d.ts +4 -3
- package/dist/handlers/tool-docs.js +252 -5
- package/dist/handlers/validation.js +13 -1
- package/dist/index.js +25 -7
- package/dist/tools.js +30 -4
- package/package.json +1 -1
- package/src/api-client.ts +72 -2
- package/src/handlers/__test-setup__.ts +5 -0
- package/src/handlers/bodies-of-work.ts +27 -11
- package/src/handlers/deployment.ts +4 -2
- package/src/handlers/discovery.ts +23 -740
- package/src/handlers/fallback.test.ts +78 -0
- package/src/handlers/fallback.ts +20 -9
- package/src/handlers/findings.test.ts +129 -2
- package/src/handlers/findings.ts +32 -3
- package/src/handlers/session.test.ts +37 -2
- package/src/handlers/session.ts +29 -8
- package/src/handlers/sprints.ts +19 -6
- package/src/handlers/tasks.test.ts +61 -0
- package/src/handlers/tasks.ts +26 -1
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/validation.test.ts +52 -0
- package/src/handlers/validation.ts +14 -1
- package/src/index.ts +25 -7
- package/src/tools.ts +30 -4
- 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
|
// ============================================================================
|
package/src/handlers/fallback.ts
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
});
|
package/src/handlers/findings.ts
CHANGED
|
@@ -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
|
|
package/src/handlers/session.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
253
|
-
|
|
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
|
|
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)
|
package/src/handlers/sprints.ts
CHANGED
|
@@ -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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
// ============================================================================
|
package/src/handlers/tasks.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|