@j0hanz/code-review-analyst-mcp 1.5.1 → 1.5.2
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 +75 -57
- 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,15 @@ function isRetryableUpstreamMessage(message) {
|
|
|
134
136
|
return (RETRYABLE_UPSTREAM_ERROR_PATTERN.test(message) ||
|
|
135
137
|
BUSY_ERROR_PATTERN.test(message));
|
|
136
138
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (typeof progressToken !== 'string' && typeof progressToken !== 'number') {
|
|
139
|
+
function ignoreProgressInput(value) {
|
|
140
|
+
if (value === null) {
|
|
140
141
|
return;
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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.
|
|
160
|
-
}
|
|
143
|
+
}
|
|
144
|
+
function sendTaskProgress(extra, payload) {
|
|
145
|
+
ignoreProgressInput(extra);
|
|
146
|
+
ignoreProgressInput(payload);
|
|
147
|
+
return Promise.resolve();
|
|
161
148
|
}
|
|
162
149
|
function createProgressReporter(extra) {
|
|
163
150
|
let lastCurrent = 0;
|
|
@@ -195,21 +182,26 @@ function normalizeProgressContext(context) {
|
|
|
195
182
|
return `${compact.slice(0, 77)}...`;
|
|
196
183
|
}
|
|
197
184
|
function formatProgressStep(toolName, context, metadata) {
|
|
198
|
-
|
|
199
|
-
return `${prefix} ${toolName}: ${context} [${metadata}]`;
|
|
185
|
+
return `${toolName}: ${context} [${metadata}]`;
|
|
200
186
|
}
|
|
201
187
|
function friendlyModelName(model) {
|
|
202
188
|
if (!model)
|
|
203
|
-
return 'model';
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
189
|
+
return 'calling model';
|
|
190
|
+
const normalized = model.toLowerCase();
|
|
191
|
+
if (normalized.includes('pro'))
|
|
192
|
+
return 'calling Pro';
|
|
193
|
+
if (normalized.includes('flash'))
|
|
194
|
+
return 'calling Flash';
|
|
195
|
+
return 'calling model';
|
|
196
|
+
}
|
|
197
|
+
function formatProgressCompletion(toolName, context, outcome) {
|
|
198
|
+
return `🗒 ${toolName}: ${context} • ${outcome}`;
|
|
209
199
|
}
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
|
|
200
|
+
function createFailureStatusMessage(outcome, errorMessage) {
|
|
201
|
+
if (outcome === 'cancelled') {
|
|
202
|
+
return `cancelled: ${errorMessage}`;
|
|
203
|
+
}
|
|
204
|
+
return errorMessage;
|
|
213
205
|
}
|
|
214
206
|
async function reportProgressStepUpdate(reportProgress, toolName, context, current, metadata) {
|
|
215
207
|
await reportProgress({
|
|
@@ -218,13 +210,21 @@ async function reportProgressStepUpdate(reportProgress, toolName, context, curre
|
|
|
218
210
|
message: formatProgressStep(toolName, context, metadata),
|
|
219
211
|
});
|
|
220
212
|
}
|
|
221
|
-
async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome
|
|
213
|
+
async function reportProgressCompletionUpdate(reportProgress, toolName, context, outcome) {
|
|
222
214
|
await reportProgress({
|
|
223
215
|
current: TASK_PROGRESS_TOTAL,
|
|
224
216
|
total: TASK_PROGRESS_TOTAL,
|
|
225
|
-
message: formatProgressCompletion(toolName, context, outcome
|
|
217
|
+
message: formatProgressCompletion(toolName, context, outcome),
|
|
226
218
|
});
|
|
227
219
|
}
|
|
220
|
+
async function reportSchemaRetryProgressBestEffort(reportProgress, toolName, context, retryCount, maxRetries) {
|
|
221
|
+
try {
|
|
222
|
+
await reportProgressStepUpdate(reportProgress, toolName, context, 3, `repairing schema retry ${retryCount}/${maxRetries}`);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Progress updates are best-effort and must not interrupt retries.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
228
|
function toLoggingLevel(level) {
|
|
229
229
|
switch (level) {
|
|
230
230
|
case 'debug':
|
|
@@ -277,21 +277,23 @@ export function wrapToolHandler(options, handler) {
|
|
|
277
277
|
const result = await handler(input, extra);
|
|
278
278
|
// End progress (1/1)
|
|
279
279
|
const outcome = result.isError ? 'failed' : 'completed';
|
|
280
|
-
const success = !result.isError;
|
|
281
280
|
await sendTaskProgress(extra, {
|
|
282
281
|
current: 1,
|
|
283
282
|
total: 1,
|
|
284
|
-
message: formatProgressCompletion(options.toolName, context, outcome
|
|
283
|
+
message: formatProgressCompletion(options.toolName, context, outcome),
|
|
285
284
|
});
|
|
286
285
|
return result;
|
|
287
286
|
}
|
|
288
287
|
catch (error) {
|
|
288
|
+
const errorMessage = getErrorMessage(error);
|
|
289
|
+
const failureMeta = classifyErrorMeta(error, errorMessage);
|
|
290
|
+
const outcome = failureMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
|
|
289
291
|
// Progress is best-effort; must never mask the original error.
|
|
290
292
|
try {
|
|
291
293
|
await sendTaskProgress(extra, {
|
|
292
294
|
current: 1,
|
|
293
295
|
total: 1,
|
|
294
|
-
message: formatProgressCompletion(options.toolName, context,
|
|
296
|
+
message: formatProgressCompletion(options.toolName, context, outcome),
|
|
295
297
|
});
|
|
296
298
|
}
|
|
297
299
|
catch {
|
|
@@ -301,6 +303,21 @@ export function wrapToolHandler(options, handler) {
|
|
|
301
303
|
}
|
|
302
304
|
};
|
|
303
305
|
}
|
|
306
|
+
async function validateRequest(config, inputRecord, ctx) {
|
|
307
|
+
if (config.requiresDiff) {
|
|
308
|
+
if (!ctx.diffSlot) {
|
|
309
|
+
return createNoDiffError();
|
|
310
|
+
}
|
|
311
|
+
const budgetError = validateDiffBudget(ctx.diffSlot.diff);
|
|
312
|
+
if (budgetError) {
|
|
313
|
+
return budgetError;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (config.validateInput) {
|
|
317
|
+
return await config.validateInput(inputRecord, ctx);
|
|
318
|
+
}
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
304
321
|
export function registerStructuredToolTask(server, config) {
|
|
305
322
|
const responseSchema = createGeminiResponseSchema({
|
|
306
323
|
geminiSchema: config.geminiSchema,
|
|
@@ -350,18 +367,16 @@ export function registerStructuredToolTask(server, config) {
|
|
|
350
367
|
// could replace the slot and silently bypass the budget check.
|
|
351
368
|
const ctx = { diffSlot: getDiff() };
|
|
352
369
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 0, 'starting');
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
370
|
+
const validationError = await validateRequest(config, inputRecord, ctx);
|
|
371
|
+
if (validationError) {
|
|
372
|
+
const validationMessage = validationError.structuredContent.error?.message ??
|
|
373
|
+
INPUT_VALIDATION_FAILED;
|
|
374
|
+
await updateStatusMessage(validationMessage);
|
|
375
|
+
await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, 'rejected');
|
|
376
|
+
await storeResultSafely('completed', validationError);
|
|
377
|
+
return;
|
|
363
378
|
}
|
|
364
|
-
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, '
|
|
379
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 1, 'building prompt');
|
|
365
380
|
const promptParts = config.buildPrompt(inputRecord, ctx);
|
|
366
381
|
const { prompt } = promptParts;
|
|
367
382
|
const { systemInstruction } = promptParts;
|
|
@@ -369,18 +384,18 @@ export function registerStructuredToolTask(server, config) {
|
|
|
369
384
|
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 2, modelLabel);
|
|
370
385
|
let parsed;
|
|
371
386
|
let retryPrompt = prompt;
|
|
372
|
-
|
|
387
|
+
const maxRetries = config.schemaRetries ?? geminiSchemaRetriesConfig.get();
|
|
388
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
373
389
|
try {
|
|
374
390
|
const raw = await generateStructuredJson(createGenerationRequest(config, { systemInstruction, prompt: retryPrompt }, responseSchema, onLog, extra.signal));
|
|
375
391
|
if (attempt === 0) {
|
|
376
|
-
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, '
|
|
392
|
+
await reportProgressStepUpdate(reportProgress, config.name, progressContext, 3, 'validating response');
|
|
377
393
|
}
|
|
378
394
|
parsed = config.resultSchema.parse(raw);
|
|
379
395
|
break;
|
|
380
396
|
}
|
|
381
397
|
catch (error) {
|
|
382
|
-
if (attempt >=
|
|
383
|
-
!(error instanceof z.ZodError)) {
|
|
398
|
+
if (attempt >= maxRetries || !(error instanceof z.ZodError)) {
|
|
384
399
|
throw error;
|
|
385
400
|
}
|
|
386
401
|
const errorMessage = getErrorMessage(error);
|
|
@@ -393,6 +408,8 @@ export function registerStructuredToolTask(server, config) {
|
|
|
393
408
|
originalChars: errorMessage.length,
|
|
394
409
|
},
|
|
395
410
|
});
|
|
411
|
+
const retryCount = attempt + 1;
|
|
412
|
+
await reportSchemaRetryProgressBestEffort(reportProgress, config.name, progressContext, retryCount, maxRetries);
|
|
396
413
|
retryPrompt = schemaRetryPrompt.prompt;
|
|
397
414
|
}
|
|
398
415
|
}
|
|
@@ -415,9 +432,10 @@ export function registerStructuredToolTask(server, config) {
|
|
|
415
432
|
catch (error) {
|
|
416
433
|
const errorMessage = getErrorMessage(error);
|
|
417
434
|
const errorMeta = classifyErrorMeta(error, errorMessage);
|
|
418
|
-
|
|
419
|
-
await updateStatusMessage(errorMessage);
|
|
435
|
+
const outcome = errorMeta.kind === 'cancelled' ? 'cancelled' : 'failed';
|
|
436
|
+
await updateStatusMessage(createFailureStatusMessage(outcome, errorMessage));
|
|
420
437
|
await storeResultSafely('failed', createErrorToolResponse(config.errorCode, errorMessage, undefined, errorMeta));
|
|
438
|
+
await reportProgressCompletionUpdate(reportProgress, config.name, progressContext, outcome);
|
|
421
439
|
}
|
|
422
440
|
};
|
|
423
441
|
queueMicrotask(() => {
|
|
@@ -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;
|