@j0hanz/code-review-analyst-mcp 1.5.1 → 1.5.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.
- package/dist/lib/diff-store.d.ts +4 -4
- package/dist/lib/diff-store.js +15 -10
- package/dist/lib/gemini.js +16 -8
- package/dist/lib/model-config.d.ts +1 -1
- package/dist/lib/model-config.js +1 -1
- package/dist/lib/tool-contracts.d.ts +2 -4
- package/dist/lib/tool-factory.d.ts +5 -3
- package/dist/lib/tool-factory.js +101 -64
- package/dist/tools/analyze-complexity.js +1 -8
- package/dist/tools/analyze-pr-impact.js +1 -8
- package/dist/tools/detect-api-breaking.js +1 -8
- package/dist/tools/generate-diff.js +39 -35
- package/dist/tools/generate-review-summary.js +1 -8
- package/dist/tools/generate-test-plan.js +1 -8
- package/dist/tools/inspect-code-quality.js +3 -9
- package/dist/tools/suggest-search-replace.js +1 -8
- package/package.json +1 -1
package/dist/lib/diff-store.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ export interface DiffSlot {
|
|
|
16
16
|
}
|
|
17
17
|
/** Call once during server setup so the store can emit resource-updated notifications. */
|
|
18
18
|
export declare function initDiffStore(server: McpServer): void;
|
|
19
|
-
export declare function storeDiff(data: DiffSlot): void;
|
|
20
|
-
export declare function getDiff(): DiffSlot | undefined;
|
|
21
|
-
export declare function hasDiff(): boolean;
|
|
19
|
+
export declare function storeDiff(data: DiffSlot, key?: string): void;
|
|
20
|
+
export declare function getDiff(key?: string): DiffSlot | undefined;
|
|
21
|
+
export declare function hasDiff(key?: string): boolean;
|
|
22
22
|
/** Test-only: directly set or clear the diff slot without emitting resource-updated. */
|
|
23
|
-
export declare function setDiffForTesting(data: DiffSlot | undefined): void;
|
|
23
|
+
export declare function setDiffForTesting(data: DiffSlot | undefined, key?: string): void;
|
|
24
24
|
export declare function createNoDiffError(): ReturnType<typeof createErrorToolResponse>;
|
package/dist/lib/diff-store.js
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import { createErrorToolResponse } from './tool-response.js';
|
|
2
2
|
export const DIFF_RESOURCE_URI = 'diff://current';
|
|
3
|
-
|
|
3
|
+
const diffSlots = new Map();
|
|
4
4
|
let sendResourceUpdated;
|
|
5
5
|
/** Call once during server setup so the store can emit resource-updated notifications. */
|
|
6
6
|
export function initDiffStore(server) {
|
|
7
7
|
const inner = server.server;
|
|
8
8
|
sendResourceUpdated = inner.sendResourceUpdated.bind(inner);
|
|
9
9
|
}
|
|
10
|
-
export function storeDiff(data) {
|
|
11
|
-
|
|
10
|
+
export function storeDiff(data, key = process.cwd()) {
|
|
11
|
+
diffSlots.set(key, data);
|
|
12
12
|
void sendResourceUpdated?.({ uri: DIFF_RESOURCE_URI }).catch(() => {
|
|
13
|
-
//
|
|
13
|
+
// Ignore errors sending resource-updated, which can happen if the server is not fully initialized yet.
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
export function getDiff() {
|
|
17
|
-
return
|
|
16
|
+
export function getDiff(key = process.cwd()) {
|
|
17
|
+
return diffSlots.get(key);
|
|
18
18
|
}
|
|
19
|
-
export function hasDiff() {
|
|
20
|
-
return
|
|
19
|
+
export function hasDiff(key = process.cwd()) {
|
|
20
|
+
return diffSlots.has(key);
|
|
21
21
|
}
|
|
22
22
|
/** Test-only: directly set or clear the diff slot without emitting resource-updated. */
|
|
23
|
-
export function setDiffForTesting(data) {
|
|
24
|
-
|
|
23
|
+
export function setDiffForTesting(data, key = process.cwd()) {
|
|
24
|
+
if (data) {
|
|
25
|
+
diffSlots.set(key, data);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
diffSlots.delete(key);
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
export function createNoDiffError() {
|
|
27
32
|
return createErrorToolResponse('E_NO_DIFF', 'No diff cached. You must call the generate_diff tool before using any review tool. Run generate_diff with mode="unstaged" or mode="staged" to capture the current branch changes, then retry this tool.', undefined, { retryable: false, kind: 'validation' });
|
package/dist/lib/gemini.js
CHANGED
|
@@ -352,8 +352,6 @@ function buildGenerationConfig(request, abortSignal) {
|
|
|
352
352
|
responseMimeType: 'application/json',
|
|
353
353
|
responseSchema: applyResponseKeyOrdering(request.responseSchema, request.responseKeyOrdering),
|
|
354
354
|
safetySettings: getSafetySettings(getSafetyThreshold()),
|
|
355
|
-
topP: 0.95,
|
|
356
|
-
topK: 40,
|
|
357
355
|
abortSignal,
|
|
358
356
|
};
|
|
359
357
|
if (request.systemInstruction) {
|
|
@@ -416,6 +414,14 @@ async function executeAttempt(request, model, timeoutMs, attempt, onLog) {
|
|
|
416
414
|
const response = await generateContentWithTimeout(request, model, timeoutMs);
|
|
417
415
|
const latencyMs = Math.round(performance.now() - startedAt);
|
|
418
416
|
const finishReason = response.candidates?.[0]?.finishReason;
|
|
417
|
+
let thoughts;
|
|
418
|
+
const parts = response.candidates?.[0]?.content?.parts;
|
|
419
|
+
if (Array.isArray(parts)) {
|
|
420
|
+
const thoughtParts = parts.filter((p) => p.thought === true && typeof p.text === 'string');
|
|
421
|
+
if (thoughtParts.length > 0) {
|
|
422
|
+
thoughts = thoughtParts.map((p) => p.text).join('\n\n');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
419
425
|
await emitGeminiLog(onLog, 'info', {
|
|
420
426
|
event: 'gemini_call',
|
|
421
427
|
details: {
|
|
@@ -423,6 +429,7 @@ async function executeAttempt(request, model, timeoutMs, attempt, onLog) {
|
|
|
423
429
|
latencyMs,
|
|
424
430
|
finishReason: finishReason ?? null,
|
|
425
431
|
usageMetadata: response.usageMetadata ?? null,
|
|
432
|
+
...(thoughts ? { thoughts } : {}),
|
|
426
433
|
},
|
|
427
434
|
});
|
|
428
435
|
if (finishReason === FinishReason.MAX_TOKENS) {
|
|
@@ -457,33 +464,34 @@ async function waitBeforeRetry(attempt, error, onLog, requestSignal) {
|
|
|
457
464
|
throw sleepError;
|
|
458
465
|
}
|
|
459
466
|
}
|
|
460
|
-
async function throwGeminiFailure(
|
|
461
|
-
const attempts = maxRetries + 1;
|
|
467
|
+
async function throwGeminiFailure(attemptsMade, lastError, onLog) {
|
|
462
468
|
const message = getErrorMessage(lastError);
|
|
463
469
|
await emitGeminiLog(onLog, 'error', {
|
|
464
470
|
event: 'gemini_failure',
|
|
465
471
|
details: {
|
|
466
472
|
error: message,
|
|
467
|
-
attempts,
|
|
473
|
+
attempts: attemptsMade,
|
|
468
474
|
},
|
|
469
475
|
});
|
|
470
|
-
throw new Error(`Gemini request failed after ${
|
|
476
|
+
throw new Error(`Gemini request failed after ${attemptsMade} attempts: ${message}`, { cause: lastError });
|
|
471
477
|
}
|
|
472
478
|
async function runWithRetries(request, model, timeoutMs, maxRetries, onLog) {
|
|
473
479
|
let lastError;
|
|
474
|
-
|
|
480
|
+
let attempt = 0;
|
|
481
|
+
for (; attempt <= maxRetries; attempt += 1) {
|
|
475
482
|
try {
|
|
476
483
|
return await executeAttempt(request, model, timeoutMs, attempt, onLog);
|
|
477
484
|
}
|
|
478
485
|
catch (error) {
|
|
479
486
|
lastError = error;
|
|
480
487
|
if (!canRetryAttempt(attempt, maxRetries, error)) {
|
|
488
|
+
attempt += 1; // Count this attempt before breaking
|
|
481
489
|
break;
|
|
482
490
|
}
|
|
483
491
|
await waitBeforeRetry(attempt, error, onLog, request.signal);
|
|
484
492
|
}
|
|
485
493
|
}
|
|
486
|
-
return throwGeminiFailure(
|
|
494
|
+
return throwGeminiFailure(attempt, lastError, onLog);
|
|
487
495
|
}
|
|
488
496
|
function canRetryAttempt(attempt, maxRetries, error) {
|
|
489
497
|
return attempt < maxRetries && shouldRetry(error);
|
|
@@ -20,7 +20,7 @@ export declare const PRO_THINKING_LEVEL: "high";
|
|
|
20
20
|
/** Output cap for Flash API breaking-change detection. */
|
|
21
21
|
export declare const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS: 4096;
|
|
22
22
|
/** Output cap for Flash complexity analysis. */
|
|
23
|
-
export declare const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS:
|
|
23
|
+
export declare const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS: 4096;
|
|
24
24
|
/** Output cap for Flash test-plan generation. */
|
|
25
25
|
export declare const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS: 8192;
|
|
26
26
|
/** Output cap for Flash triage tools. */
|
package/dist/lib/model-config.js
CHANGED
|
@@ -28,7 +28,7 @@ const THINKING_LEVELS = {
|
|
|
28
28
|
// Thinking budget in tokens for Flash and Pro tools. Note that these are not hard limits, but rather guidelines to encourage concise responses and manage latency/cost.
|
|
29
29
|
const OUTPUT_TOKEN_BUDGET = {
|
|
30
30
|
flashApiBreaking: 4_096,
|
|
31
|
-
flashComplexity:
|
|
31
|
+
flashComplexity: 4_096,
|
|
32
32
|
flashTestPlan: 8_192,
|
|
33
33
|
flashTriage: 4_096,
|
|
34
34
|
proPatch: 8_192,
|
|
@@ -18,9 +18,7 @@ export interface ToolContract {
|
|
|
18
18
|
maxOutputTokens: number;
|
|
19
19
|
/**
|
|
20
20
|
* Sampling temperature for the Gemini call.
|
|
21
|
-
*
|
|
22
|
-
* higher values (0.2) add diversity for creative synthesis tasks.
|
|
23
|
-
* Omit to use the global default (0.2).
|
|
21
|
+
* Gemini 3 recommends 1.0 for all tasks.
|
|
24
22
|
*/
|
|
25
23
|
temperature?: number;
|
|
26
24
|
/** Enables deterministic JSON guidance and schema key ordering. */
|
|
@@ -210,7 +208,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
210
208
|
readonly model: "gemini-3-flash-preview";
|
|
211
209
|
readonly timeoutMs: 90000;
|
|
212
210
|
readonly thinkingLevel: "medium";
|
|
213
|
-
readonly maxOutputTokens:
|
|
211
|
+
readonly maxOutputTokens: 4096;
|
|
214
212
|
readonly temperature: 1;
|
|
215
213
|
readonly deterministicJson: true;
|
|
216
214
|
readonly params: readonly [{
|
|
@@ -55,6 +55,10 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
55
55
|
transformResult?: (input: TInput, result: TResult, ctx: ToolExecutionContext) => TFinal;
|
|
56
56
|
/** Optional validation hook for input parameters. */
|
|
57
57
|
validateInput?: (input: TInput, ctx: ToolExecutionContext) => Promise<ReturnType<typeof createErrorToolResponse> | undefined> | ReturnType<typeof createErrorToolResponse> | undefined;
|
|
58
|
+
/** Optional flag to enforce diff presence and budget check before tool execution. */
|
|
59
|
+
requiresDiff?: boolean;
|
|
60
|
+
/** Optional override for schema validation retries. Defaults to GEMINI_SCHEMA_RETRIES env var. */
|
|
61
|
+
schemaRetries?: number;
|
|
58
62
|
/** Optional Gemini model to use (e.g. 'gemini-3-pro-preview'). */
|
|
59
63
|
model?: string;
|
|
60
64
|
/** Optional thinking level. */
|
|
@@ -65,9 +69,7 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
65
69
|
maxOutputTokens?: number;
|
|
66
70
|
/**
|
|
67
71
|
* Optional sampling temperature for this tool's Gemini call.
|
|
68
|
-
*
|
|
69
|
-
* higher values (0.2) add useful diversity for creative synthesis tasks.
|
|
70
|
-
* Falls back to the global default (0.2) when omitted.
|
|
72
|
+
* Gemini 3 recommends 1.0 for all tasks.
|
|
71
73
|
*/
|
|
72
74
|
temperature?: number;
|
|
73
75
|
/** Optional opt-in to Gemini thought output. Defaults to false. */
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
3
|
-
import {
|
|
3
|
+
import { validateDiffBudget } from './diff-budget.js';
|
|
4
|
+
import { createNoDiffError, getDiff } from './diff-store.js';
|
|
4
5
|
import { createCachedEnvInt } from './env-config.js';
|
|
5
6
|
import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
|
|
6
7
|
import { stripJsonSchemaConstraints } from './gemini-schema.js';
|
|
@@ -14,7 +15,8 @@ const CANCELLED_ERROR_PATTERN = /cancelled|canceled/i;
|
|
|
14
15
|
const TIMEOUT_ERROR_PATTERN = /timed out|timeout/i;
|
|
15
16
|
const BUDGET_ERROR_PATTERN = /exceeds limit|max allowed size|input too large/i;
|
|
16
17
|
const BUSY_ERROR_PATTERN = /too many concurrent/i;
|
|
17
|
-
const
|
|
18
|
+
const DEFAULT_SCHEMA_RETRIES = 1;
|
|
19
|
+
const geminiSchemaRetriesConfig = createCachedEnvInt('GEMINI_SCHEMA_RETRIES', DEFAULT_SCHEMA_RETRIES);
|
|
18
20
|
const DEFAULT_SCHEMA_RETRY_ERROR_CHARS = 1_500;
|
|
19
21
|
const schemaRetryErrorCharsConfig = createCachedEnvInt('MAX_SCHEMA_RETRY_ERROR_CHARS', DEFAULT_SCHEMA_RETRY_ERROR_CHARS);
|
|
20
22
|
const DETERMINISTIC_JSON_RETRY_NOTE = 'Deterministic JSON mode: keep key names exactly as schema-defined and preserve stable field ordering.';
|
|
@@ -134,30 +136,22 @@ function isRetryableUpstreamMessage(message) {
|
|
|
134
136
|
return (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message) ||
|
|
135
137
|
BUSY_ERROR_PATTERN.test(message));
|
|
136
138
|
}
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
if (typeof
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
const params = {
|
|
144
|
-
progressToken,
|
|
145
|
-
progress: payload.current,
|
|
146
|
-
};
|
|
147
|
-
if (payload.total !== undefined) {
|
|
148
|
-
params.total = payload.total;
|
|
149
|
-
}
|
|
150
|
-
if (payload.message !== undefined) {
|
|
151
|
-
params.message = payload.message;
|
|
152
|
-
}
|
|
153
|
-
await extra.sendNotification({
|
|
154
|
-
method: 'notifications/progress',
|
|
155
|
-
params,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Progress is best-effort; never fail the tool call.
|
|
139
|
+
function sendTaskProgress(extra, payload) {
|
|
140
|
+
const rawToken = extra._meta?.progressToken;
|
|
141
|
+
if (typeof rawToken !== 'string' && typeof rawToken !== 'number') {
|
|
142
|
+
return Promise.resolve();
|
|
160
143
|
}
|
|
144
|
+
const params = {
|
|
145
|
+
progressToken: rawToken,
|
|
146
|
+
progress: payload.current,
|
|
147
|
+
...(payload.total !== undefined ? { total: payload.total } : {}),
|
|
148
|
+
...(payload.message !== undefined ? { message: payload.message } : {}),
|
|
149
|
+
};
|
|
150
|
+
return extra
|
|
151
|
+
.sendNotification({ method: 'notifications/progress', params })
|
|
152
|
+
.catch(() => {
|
|
153
|
+
// Progress notifications are best-effort; never fail tool execution.
|
|
154
|
+
});
|
|
161
155
|
}
|
|
162
156
|
function createProgressReporter(extra) {
|
|
163
157
|
let lastCurrent = 0;
|
|
@@ -195,21 +189,26 @@ function normalizeProgressContext(context) {
|
|
|
195
189
|
return `${compact.slice(0, 77)}...`;
|
|
196
190
|
}
|
|
197
191
|
function formatProgressStep(toolName, context, metadata) {
|
|
198
|
-
|
|
199
|
-
return `${prefix} ${toolName}: ${context} [${metadata}]`;
|
|
192
|
+
return `${toolName}: ${context} [${metadata}]`;
|
|
200
193
|
}
|
|
201
194
|
function friendlyModelName(model) {
|
|
202
195
|
if (!model)
|
|
203
|
-
return 'model';
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
196
|
+
return 'calling model';
|
|
197
|
+
const normalized = model.toLowerCase();
|
|
198
|
+
if (normalized.includes('pro'))
|
|
199
|
+
return 'calling Pro';
|
|
200
|
+
if (normalized.includes('flash'))
|
|
201
|
+
return 'calling Flash';
|
|
202
|
+
return 'calling model';
|
|
209
203
|
}
|
|
210
|
-
function formatProgressCompletion(toolName, context, outcome
|
|
211
|
-
|
|
212
|
-
|
|
204
|
+
function formatProgressCompletion(toolName, context, outcome) {
|
|
205
|
+
return `🗒 ${toolName}: ${context} • ${outcome}`;
|
|
206
|
+
}
|
|
207
|
+
function createFailureStatusMessage(outcome, errorMessage) {
|
|
208
|
+
if (outcome === 'cancelled') {
|
|
209
|
+
return `cancelled: ${errorMessage}`;
|
|
210
|
+
}
|
|
211
|
+
return errorMessage;
|
|
213
212
|
}
|
|
214
213
|
async function reportProgressStepUpdate(reportProgress, toolName, context, current, metadata) {
|
|
215
214
|
await reportProgress({
|
|
@@ -218,13 +217,21 @@ async function reportProgressStepUpdate(reportProgress, toolName, context, curre
|
|
|
218
217
|
message: formatProgressStep(toolName, context, metadata),
|
|
219
218
|
});
|
|
220
219
|
}
|
|
221
|
-
async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome
|
|
220
|
+
async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome) {
|
|
222
221
|
await reportProgress({
|
|
223
222
|
current: TASK_PROGRESS_TOTAL,
|
|
224
223
|
total: TASK_PROGRESS_TOTAL,
|
|
225
|
-
message: formatProgressCompletion(toolName, context, outcome
|
|
224
|
+
message: formatProgressCompletion(toolName, context, outcome),
|
|
226
225
|
});
|
|
227
226
|
}
|
|
227
|
+
async function reportSchemaRetryProgressBestEffort(reportProgress, toolName, context, retryCount, maxRetries) {
|
|
228
|
+
try {
|
|
229
|
+
await reportProgressStepUpdate(reportProgress, toolName, context, 3, `repairing schema retry ${retryCount}/${maxRetries}`);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Progress updates are best-effort and must not interrupt retries.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
228
235
|
function toLoggingLevel(level) {
|
|
229
236
|
switch (level) {
|
|
230
237
|
case 'debug':
|
|
@@ -277,21 +284,23 @@ export function wrapToolHandler(options, handler) {
|
|
|
277
284
|
const result = await handler(input, extra);
|
|
278
285
|
// End progress (1/1)
|
|
279
286
|
const outcome = result.isError ? 'failed' : 'completed';
|
|
280
|
-
const success = !result.isError;
|
|
281
287
|
await sendTaskProgress(extra, {
|
|
282
288
|
current: 1,
|
|
283
289
|
total: 1,
|
|
284
|
-
message: formatProgressCompletion(options.toolName, context, outcome
|
|
290
|
+
message: formatProgressCompletion(options.toolName, context, outcome),
|
|
285
291
|
});
|
|
286
292
|
return result;
|
|
287
293
|
}
|
|
288
294
|
catch (error) {
|
|
295
|
+
const errorMessage = getErrorMessage(error);
|
|
296
|
+
const failureMeta = classifyErrorMeta(error, errorMessage);
|
|
297
|
+
const outcome = failureMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
|
|
289
298
|
// Progress is best-effort; must never mask the original error.
|
|
290
299
|
try {
|
|
291
300
|
await sendTaskProgress(extra, {
|
|
292
301
|
current: 1,
|
|
293
302
|
total: 1,
|
|
294
|
-
message: formatProgressCompletion(options.toolName, context,
|
|
303
|
+
message: formatProgressCompletion(options.toolName, context, outcome),
|
|
295
304
|
});
|
|
296
305
|
}
|
|
297
306
|
catch {
|
|
@@ -301,6 +310,21 @@ export function wrapToolHandler(options, handler) {
|
|
|
301
310
|
}
|
|
302
311
|
};
|
|
303
312
|
}
|
|
313
|
+
async function validateRequest(config, inputRecord, ctx) {
|
|
314
|
+
if (config.requiresDiff) {
|
|
315
|
+
if (!ctx.diffSlot) {
|
|
316
|
+
return createNoDiffError();
|
|
317
|
+
}
|
|
318
|
+
const budgetError = validateDiffBudget(ctx.diffSlot.diff);
|
|
319
|
+
if (budgetError) {
|
|
320
|
+
return budgetError;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (config.validateInput) {
|
|
324
|
+
return await config.validateInput(inputRecord, ctx);
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
304
328
|
export function registerStructuredToolTask(server, config) {
|
|
305
329
|
const responseSchema = createGeminiResponseSchema({
|
|
306
330
|
geminiSchema: config.geminiSchema,
|
|
@@ -332,16 +356,19 @@ export function registerStructuredToolTask(server, config) {
|
|
|
332
356
|
// statusMessage is best-effort; task may already be terminal.
|
|
333
357
|
}
|
|
334
358
|
};
|
|
359
|
+
const onLog = createGeminiLogger(server, task.taskId);
|
|
335
360
|
const storeResultSafely = async (status, result) => {
|
|
336
361
|
try {
|
|
337
362
|
await extra.taskStore.storeTaskResult(task.taskId, status, result);
|
|
338
363
|
}
|
|
339
|
-
catch {
|
|
340
|
-
|
|
364
|
+
catch (storeErr) {
|
|
365
|
+
await onLog('error', {
|
|
366
|
+
event: 'store_result_failed',
|
|
367
|
+
error: getErrorMessage(storeErr),
|
|
368
|
+
});
|
|
341
369
|
}
|
|
342
370
|
};
|
|
343
371
|
try {
|
|
344
|
-
const onLog = createGeminiLogger(server, task.taskId);
|
|
345
372
|
const inputRecord = parseToolInput(input, config.fullInputSchema);
|
|
346
373
|
progressContext = normalizeProgressContext(config.progressContext?.(inputRecord));
|
|
347
374
|
// Snapshot the diff slot ONCE before any async work so that
|
|
@@ -350,18 +377,16 @@ export function registerStructuredToolTask(server, config) {
|
|
|
350
377
|
// could replace the slot and silently bypass the budget check.
|
|
351
378
|
const ctx = { diffSlot: getDiff() };
|
|
352
379
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
380
|
+
const validationError = await validateRequest(config, inputRecord, ctx);
|
|
381
|
+
if (validationError) {
|
|
382
|
+
const validationMessage = validationError.structuredContent.error?.message ??
|
|
383
|
+
INPUT_VALIDATION_FAILED;
|
|
384
|
+
await updateStatusMessage(validationMessage);
|
|
385
|
+
await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'rejected');
|
|
386
|
+
await storeResultSafely('completed', validationError);
|
|
387
|
+
return;
|
|
363
388
|
}
|
|
364
|
-
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, '
|
|
389
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'building prompt');
|
|
365
390
|
const promptParts = config.buildPrompt(inputRecord, ctx);
|
|
366
391
|
const { prompt } = promptParts;
|
|
367
392
|
const { systemInstruction } = promptParts;
|
|
@@ -369,18 +394,18 @@ export function registerStructuredToolTask(server, config) {
|
|
|
369
394
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 2, modelLabel);
|
|
370
395
|
let parsed;
|
|
371
396
|
let retryPrompt = prompt;
|
|
372
|
-
|
|
397
|
+
const maxRetries = config.schemaRetries ?? geminiSchemaRetriesConfig.get();
|
|
398
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
373
399
|
try {
|
|
374
400
|
const raw = await generateStructuredJson(createGenerationRequest(config, { systemInstruction, prompt: retryPrompt }, responseSchema, onLog, extra.signal));
|
|
375
401
|
if (attempt === 0) {
|
|
376
|
-
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, '
|
|
402
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, 'validating response');
|
|
377
403
|
}
|
|
378
404
|
parsed = config.resultSchema.parse(raw);
|
|
379
405
|
break;
|
|
380
406
|
}
|
|
381
407
|
catch (error) {
|
|
382
|
-
if (attempt >=
|
|
383
|
-
!(error instanceof z.ZodError)) {
|
|
408
|
+
if (attempt >= maxRetries || !(error instanceof z.ZodError)) {
|
|
384
409
|
throw error;
|
|
385
410
|
}
|
|
386
411
|
const errorMessage = getErrorMessage(error);
|
|
@@ -393,6 +418,8 @@ export function registerStructuredToolTask(server, config) {
|
|
|
393
418
|
originalChars: errorMessage.length,
|
|
394
419
|
},
|
|
395
420
|
});
|
|
421
|
+
const retryCount = attempt + 1;
|
|
422
|
+
await reportSchemaRetryProgressBestEffort(reportProgress, config.name, progressContext, retryCount, maxRetries);
|
|
396
423
|
retryPrompt = schemaRetryPrompt.prompt;
|
|
397
424
|
}
|
|
398
425
|
}
|
|
@@ -415,14 +442,24 @@ export function registerStructuredToolTask(server, config) {
|
|
|
415
442
|
catch (error) {
|
|
416
443
|
const errorMessage = getErrorMessage(error);
|
|
417
444
|
const errorMeta = classifyErrorMeta(error, errorMessage);
|
|
418
|
-
|
|
419
|
-
await updateStatusMessage(errorMessage);
|
|
445
|
+
const outcome = errorMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
|
|
446
|
+
await updateStatusMessage(createFailureStatusMessage(outcome, errorMessage));
|
|
420
447
|
await storeResultSafely('failed', createErrorToolResponse(config.errorCode, errorMessage, undefined, errorMeta));
|
|
448
|
+
await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, outcome);
|
|
421
449
|
}
|
|
422
450
|
};
|
|
423
|
-
|
|
424
|
-
void runTask().catch((error) => {
|
|
425
|
-
|
|
451
|
+
setImmediate(() => {
|
|
452
|
+
void runTask().catch(async (error) => {
|
|
453
|
+
try {
|
|
454
|
+
await server.sendLoggingMessage({
|
|
455
|
+
level: 'error',
|
|
456
|
+
logger: 'task-runner',
|
|
457
|
+
data: { task: config.name, error: getErrorMessage(error) },
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
console.error(`[task-runner:${config.name}] ${getErrorMessage(error)}`);
|
|
462
|
+
}
|
|
426
463
|
});
|
|
427
464
|
});
|
|
428
465
|
return { task };
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { createNoDiffError } from '../lib/diff-store.js';
|
|
3
1
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
4
2
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
3
|
import { AnalyzeComplexityInputSchema } from '../schemas/inputs.js';
|
|
@@ -32,12 +30,7 @@ export function registerAnalyzeComplexityTool(server) {
|
|
|
32
30
|
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
33
31
|
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
34
32
|
: undefined),
|
|
35
|
-
|
|
36
|
-
const slot = ctx.diffSlot;
|
|
37
|
-
if (!slot)
|
|
38
|
-
return createNoDiffError();
|
|
39
|
-
return validateDiffBudget(slot.diff);
|
|
40
|
-
},
|
|
33
|
+
requiresDiff: true,
|
|
41
34
|
formatOutcome: (result) => result.isDegradation
|
|
42
35
|
? 'Performance degradation detected'
|
|
43
36
|
: 'No degradation',
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
1
|
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff-parser.js';
|
|
3
|
-
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
2
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
3
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
6
4
|
import { AnalyzePrImpactInputSchema } from '../schemas/inputs.js';
|
|
@@ -36,12 +34,7 @@ export function registerAnalyzePrImpactTool(server) {
|
|
|
36
34
|
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
37
35
|
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
38
36
|
: undefined),
|
|
39
|
-
|
|
40
|
-
const slot = ctx.diffSlot;
|
|
41
|
-
if (!slot)
|
|
42
|
-
return createNoDiffError();
|
|
43
|
-
return validateDiffBudget(slot.diff);
|
|
44
|
-
},
|
|
37
|
+
requiresDiff: true,
|
|
45
38
|
formatOutcome: (result) => `severity: ${result.severity}`,
|
|
46
39
|
formatOutput: (result) => `Impact Analysis (${result.severity}): ${result.summary}`,
|
|
47
40
|
buildPrompt: (input, ctx) => {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { createNoDiffError } from '../lib/diff-store.js';
|
|
3
1
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
4
2
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
5
3
|
import { DetectApiBreakingInputSchema } from '../schemas/inputs.js';
|
|
@@ -32,12 +30,7 @@ export function registerDetectApiBreakingTool(server) {
|
|
|
32
30
|
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
33
31
|
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
34
32
|
: undefined),
|
|
35
|
-
|
|
36
|
-
const slot = ctx.diffSlot;
|
|
37
|
-
if (!slot)
|
|
38
|
-
return createNoDiffError();
|
|
39
|
-
return validateDiffBudget(slot.diff);
|
|
40
|
-
},
|
|
33
|
+
requiresDiff: true,
|
|
41
34
|
formatOutcome: (result) => `${result.breakingChanges.length} breaking change(s) found`,
|
|
42
35
|
formatOutput: (result) => result.hasBreakingChanges
|
|
43
36
|
? `API Breaking Changes: ${result.breakingChanges.length} found.`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { cleanDiff, isEmptyDiff, NOISY_EXCLUDE_PATHSPECS, } from '../lib/diff-cleaner.js';
|
|
4
5
|
import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
@@ -7,6 +8,7 @@ import { wrapToolHandler } from '../lib/tool-factory.js';
|
|
|
7
8
|
import { createErrorToolResponse, createToolResponse, } from '../lib/tool-response.js';
|
|
8
9
|
const GIT_TIMEOUT_MS = 30_000;
|
|
9
10
|
const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
10
12
|
function buildGitArgs(mode) {
|
|
11
13
|
const args = ['diff', '--no-color', '--no-ext-diff'];
|
|
12
14
|
if (mode === 'staged') {
|
|
@@ -33,43 +35,45 @@ export function registerGenerateDiffTool(server) {
|
|
|
33
35
|
}, wrapToolHandler({
|
|
34
36
|
toolName: 'generate_diff',
|
|
35
37
|
progressContext: (input) => input.mode,
|
|
36
|
-
}, (input) => {
|
|
38
|
+
}, async (input) => {
|
|
37
39
|
const { mode } = input;
|
|
38
40
|
const args = buildGitArgs(mode);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
try {
|
|
42
|
+
// execFileAsync with an explicit args array — no shell, no interpolation.
|
|
43
|
+
// 'git' is resolved via PATH which is controlled by the server environment.
|
|
44
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
45
|
+
cwd: process.cwd(),
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
48
|
+
timeout: GIT_TIMEOUT_MS,
|
|
49
|
+
});
|
|
50
|
+
const cleaned = cleanDiff(stdout);
|
|
51
|
+
if (isEmptyDiff(cleaned)) {
|
|
52
|
+
return createErrorToolResponse('E_NO_CHANGES', `No ${mode} changes found in the current branch. Make sure you have changes that are ${describeModeHint(mode)}.`, undefined, { retryable: false, kind: 'validation' });
|
|
53
|
+
}
|
|
54
|
+
const parsedFiles = parseDiffFiles(cleaned);
|
|
55
|
+
const stats = computeDiffStatsFromFiles(parsedFiles);
|
|
56
|
+
const generatedAt = new Date().toISOString();
|
|
57
|
+
storeDiff({ diff: cleaned, parsedFiles, stats, generatedAt, mode });
|
|
58
|
+
const summary = `Diff cached at ${DIFF_RESOURCE_URI} — ${stats.files} file(s), +${stats.added} -${stats.deleted}. All review tools are now ready.`;
|
|
59
|
+
return createToolResponse({
|
|
60
|
+
ok: true,
|
|
61
|
+
result: {
|
|
62
|
+
diffRef: DIFF_RESOURCE_URI,
|
|
63
|
+
stats,
|
|
64
|
+
generatedAt,
|
|
65
|
+
mode,
|
|
66
|
+
message: summary,
|
|
67
|
+
},
|
|
68
|
+
}, summary);
|
|
50
69
|
}
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
70
|
+
catch (error) {
|
|
71
|
+
const err = error;
|
|
72
|
+
if (err.code && typeof err.code === 'number') {
|
|
73
|
+
const stderr = err.stderr ? err.stderr.trim() : '';
|
|
74
|
+
return createErrorToolResponse('E_GENERATE_DIFF', `git exited with code ${String(err.code)}: ${stderr || 'unknown error'}. Ensure the working directory is a git repository.`, undefined, { retryable: false, kind: 'internal' });
|
|
75
|
+
}
|
|
76
|
+
return createErrorToolResponse('E_GENERATE_DIFF', `Failed to run git: ${err.message}. Ensure git is installed and the working directory is a git repository.`, undefined, { retryable: false, kind: 'internal' });
|
|
54
77
|
}
|
|
55
|
-
const cleaned = cleanDiff(result.stdout);
|
|
56
|
-
if (isEmptyDiff(cleaned)) {
|
|
57
|
-
return createErrorToolResponse('E_NO_CHANGES', `No ${mode} changes found in the current branch. Make sure you have changes that are ${describeModeHint(mode)}.`, undefined, { retryable: false, kind: 'validation' });
|
|
58
|
-
}
|
|
59
|
-
const parsedFiles = parseDiffFiles(cleaned);
|
|
60
|
-
const stats = computeDiffStatsFromFiles(parsedFiles);
|
|
61
|
-
const generatedAt = new Date().toISOString();
|
|
62
|
-
storeDiff({ diff: cleaned, parsedFiles, stats, generatedAt, mode });
|
|
63
|
-
const summary = `Diff cached at ${DIFF_RESOURCE_URI} — ${stats.files} file(s), +${stats.added} -${stats.deleted}. All review tools are now ready.`;
|
|
64
|
-
return createToolResponse({
|
|
65
|
-
ok: true,
|
|
66
|
-
result: {
|
|
67
|
-
diffRef: DIFF_RESOURCE_URI,
|
|
68
|
-
stats,
|
|
69
|
-
generatedAt,
|
|
70
|
-
mode,
|
|
71
|
-
message: summary,
|
|
72
|
-
},
|
|
73
|
-
}, summary);
|
|
74
78
|
}));
|
|
75
79
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { createNoDiffError } from '../lib/diff-store.js';
|
|
3
1
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
4
2
|
import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
5
3
|
import { GenerateReviewSummaryInputSchema } from '../schemas/inputs.js';
|
|
@@ -49,12 +47,7 @@ export function registerGenerateReviewSummaryTool(server) {
|
|
|
49
47
|
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
50
48
|
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
51
49
|
: undefined),
|
|
52
|
-
|
|
53
|
-
const slot = ctx.diffSlot;
|
|
54
|
-
if (!slot)
|
|
55
|
-
return createNoDiffError();
|
|
56
|
-
return validateDiffBudget(slot.diff);
|
|
57
|
-
},
|
|
50
|
+
requiresDiff: true,
|
|
58
51
|
formatOutcome: (result) => `risk: ${result.overallRisk}`,
|
|
59
52
|
transformResult: (_input, result, ctx) => {
|
|
60
53
|
const { files, added, deleted } = getDiffStats(ctx);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
1
|
import { computeDiffStatsAndPathsFromFiles } from '../lib/diff-parser.js';
|
|
3
|
-
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
2
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
3
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
6
4
|
import { GenerateTestPlanInputSchema } from '../schemas/inputs.js';
|
|
@@ -36,12 +34,7 @@ export function registerGenerateTestPlanTool(server) {
|
|
|
36
34
|
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
37
35
|
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
38
36
|
: undefined),
|
|
39
|
-
|
|
40
|
-
const slot = ctx.diffSlot;
|
|
41
|
-
if (!slot)
|
|
42
|
-
return createNoDiffError();
|
|
43
|
-
return validateDiffBudget(slot.diff);
|
|
44
|
-
},
|
|
37
|
+
requiresDiff: true,
|
|
45
38
|
formatOutcome: (result) => `${result.testCases.length} test cases`,
|
|
46
39
|
formatOutput: (result) => `Test Plan: ${result.summary}\n${result.testCases.length} cases proposed.`,
|
|
47
40
|
transformResult: (input, result) => {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { validateContextBudget } from '../lib/context-budget.js';
|
|
2
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
3
2
|
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff-parser.js';
|
|
4
|
-
import { createNoDiffError } from '../lib/diff-store.js';
|
|
5
3
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
6
4
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
7
5
|
import { InspectCodeQualityInputSchema } from '../schemas/inputs.js';
|
|
@@ -76,14 +74,10 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
76
74
|
const fileCount = input.files?.length;
|
|
77
75
|
return fileCount ? `+${fileCount} files` : '';
|
|
78
76
|
},
|
|
77
|
+
requiresDiff: true,
|
|
79
78
|
validateInput: (input, ctx) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return createNoDiffError();
|
|
83
|
-
const diffError = validateDiffBudget(slot.diff);
|
|
84
|
-
if (diffError)
|
|
85
|
-
return diffError;
|
|
86
|
-
return validateContextBudget(slot.diff, input.files);
|
|
79
|
+
// Diff presence and budget checked by requiresDiff: true
|
|
80
|
+
return validateContextBudget(ctx.diffSlot?.diff ?? '', input.files);
|
|
87
81
|
},
|
|
88
82
|
formatOutcome: (result) => `${result.findings.length} findings, risk: ${result.overallRisk}`,
|
|
89
83
|
formatOutput: (result) => {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
1
|
import { extractChangedPathsFromFiles } from '../lib/diff-parser.js';
|
|
3
|
-
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
2
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
3
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
6
4
|
import { SuggestSearchReplaceInputSchema } from '../schemas/inputs.js';
|
|
@@ -36,12 +34,7 @@ export function registerSuggestSearchReplaceTool(server) {
|
|
|
36
34
|
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
37
35
|
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
38
36
|
: undefined),
|
|
39
|
-
|
|
40
|
-
const slot = ctx.diffSlot;
|
|
41
|
-
if (!slot)
|
|
42
|
-
return createNoDiffError();
|
|
43
|
-
return validateDiffBudget(slot.diff);
|
|
44
|
-
},
|
|
37
|
+
requiresDiff: true,
|
|
45
38
|
formatOutcome: (result) => formatPatchCount(result.blocks.length),
|
|
46
39
|
formatOutput: (result) => {
|
|
47
40
|
const count = result.blocks.length;
|