agent-gauntlet 0.10.0 → 0.11.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 +25 -23
- package/dist/index.js +9226 -0
- package/dist/index.js.map +65 -0
- package/dist/scripts/status.js +280 -0
- package/dist/scripts/status.js.map +10 -0
- package/package.json +22 -8
- package/src/built-in-reviews/code-quality.md +0 -25
- package/src/built-in-reviews/index.ts +0 -28
- package/src/bun-plugins.d.ts +0 -4
- package/src/cli-adapters/claude.ts +0 -327
- package/src/cli-adapters/codex.ts +0 -290
- package/src/cli-adapters/cursor.ts +0 -128
- package/src/cli-adapters/gemini.ts +0 -510
- package/src/cli-adapters/github-copilot.ts +0 -141
- package/src/cli-adapters/index.ts +0 -250
- package/src/cli-adapters/thinking-budget.ts +0 -23
- package/src/commands/check.ts +0 -311
- package/src/commands/ci/index.ts +0 -15
- package/src/commands/ci/init.ts +0 -96
- package/src/commands/ci/list-jobs.ts +0 -90
- package/src/commands/clean.ts +0 -54
- package/src/commands/detect.ts +0 -173
- package/src/commands/health.ts +0 -169
- package/src/commands/help.ts +0 -34
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -1878
- package/src/commands/list.ts +0 -33
- package/src/commands/review.ts +0 -311
- package/src/commands/run.ts +0 -29
- package/src/commands/shared.ts +0 -267
- package/src/commands/stop-hook.ts +0 -567
- package/src/commands/validate.ts +0 -20
- package/src/commands/wait-ci.ts +0 -518
- package/src/config/ci-loader.ts +0 -33
- package/src/config/ci-schema.ts +0 -28
- package/src/config/global.ts +0 -87
- package/src/config/loader.ts +0 -301
- package/src/config/schema.ts +0 -165
- package/src/config/stop-hook-config.ts +0 -130
- package/src/config/types.ts +0 -65
- package/src/config/validator.ts +0 -592
- package/src/core/change-detector.ts +0 -137
- package/src/core/diff-stats.ts +0 -442
- package/src/core/entry-point.ts +0 -190
- package/src/core/job.ts +0 -96
- package/src/core/run-executor.ts +0 -621
- package/src/core/runner.ts +0 -290
- package/src/gates/check.ts +0 -118
- package/src/gates/resolve-check-command.ts +0 -21
- package/src/gates/result.ts +0 -54
- package/src/gates/review.ts +0 -1333
- package/src/hooks/adapters/claude-stop-hook.ts +0 -99
- package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
- package/src/hooks/adapters/types.ts +0 -94
- package/src/hooks/stop-hook-handler.ts +0 -748
- package/src/index.ts +0 -47
- package/src/output/app-logger.ts +0 -214
- package/src/output/console-log.ts +0 -168
- package/src/output/console.ts +0 -359
- package/src/output/logger.ts +0 -126
- package/src/output/sinks/console-sink.ts +0 -59
- package/src/output/sinks/file-sink.ts +0 -110
- package/src/scripts/status.ts +0 -433
- package/src/templates/workflow.yml +0 -79
- package/src/types/gauntlet-status.ts +0 -79
- package/src/utils/debug-log.ts +0 -392
- package/src/utils/diff-parser.ts +0 -103
- package/src/utils/execution-state.ts +0 -472
- package/src/utils/log-parser.ts +0 -696
- package/src/utils/sanitizer.ts +0 -3
- package/src/utils/session-ref.ts +0 -91
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { type CLIAdapter, runStreamingCommand } from "./index.js";
|
|
7
|
-
|
|
8
|
-
const execAsync = promisify(exec);
|
|
9
|
-
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
10
|
-
|
|
11
|
-
export class CursorAdapter implements CLIAdapter {
|
|
12
|
-
name = "cursor";
|
|
13
|
-
|
|
14
|
-
async isAvailable(): Promise<boolean> {
|
|
15
|
-
try {
|
|
16
|
-
// Note: Cursor's CLI binary is named "agent", not "cursor"
|
|
17
|
-
await execAsync("which agent");
|
|
18
|
-
return true;
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async checkHealth(): Promise<{
|
|
25
|
-
available: boolean;
|
|
26
|
-
status: "healthy" | "missing" | "unhealthy";
|
|
27
|
-
message?: string;
|
|
28
|
-
}> {
|
|
29
|
-
const available = await this.isAvailable();
|
|
30
|
-
if (!available) {
|
|
31
|
-
return {
|
|
32
|
-
available: false,
|
|
33
|
-
status: "missing",
|
|
34
|
-
message: "Command not found",
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { available: true, status: "healthy", message: "Ready" };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
getProjectCommandDir(): string | null {
|
|
42
|
-
// Cursor does not support custom commands
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
getUserCommandDir(): string | null {
|
|
47
|
-
// Cursor does not support custom commands
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getProjectSkillDir(): string | null {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
getUserSkillDir(): string | null {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
getCommandExtension(): string {
|
|
60
|
-
return ".md";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
canUseSymlink(): boolean {
|
|
64
|
-
// Not applicable - no command directory support
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
transformCommand(markdownContent: string): string {
|
|
69
|
-
// Not applicable - no command directory support
|
|
70
|
-
return markdownContent;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async execute(opts: {
|
|
74
|
-
prompt: string;
|
|
75
|
-
diff: string;
|
|
76
|
-
model?: string;
|
|
77
|
-
timeoutMs?: number;
|
|
78
|
-
onOutput?: (chunk: string) => void;
|
|
79
|
-
}): Promise<string> {
|
|
80
|
-
const fullContent = `${opts.prompt}\n\n--- DIFF ---\n${opts.diff}`;
|
|
81
|
-
|
|
82
|
-
const tmpDir = os.tmpdir();
|
|
83
|
-
// Include process.pid for uniqueness across concurrent processes
|
|
84
|
-
const tmpFile = path.join(
|
|
85
|
-
tmpDir,
|
|
86
|
-
`gauntlet-cursor-${process.pid}-${Date.now()}.txt`,
|
|
87
|
-
);
|
|
88
|
-
await fs.writeFile(tmpFile, fullContent);
|
|
89
|
-
|
|
90
|
-
// Cursor agent command reads from stdin
|
|
91
|
-
// Note: As of the current version, the Cursor 'agent' CLI does not expose
|
|
92
|
-
// flags for restricting tools or enforcing read-only mode (unlike claude's --allowedTools
|
|
93
|
-
// or codex's --sandbox read-only). The agent is assumed to be repo-scoped and
|
|
94
|
-
// safe for code review use. If Cursor adds such flags in the future, they should
|
|
95
|
-
// be added here for defense-in-depth.
|
|
96
|
-
|
|
97
|
-
const cleanup = () => fs.unlink(tmpFile).catch(() => {});
|
|
98
|
-
|
|
99
|
-
// If onOutput callback is provided, use spawn for real-time streaming
|
|
100
|
-
if (opts.onOutput) {
|
|
101
|
-
return runStreamingCommand({
|
|
102
|
-
command: "agent",
|
|
103
|
-
args: [],
|
|
104
|
-
tmpFile,
|
|
105
|
-
timeoutMs: opts.timeoutMs,
|
|
106
|
-
onOutput: opts.onOutput,
|
|
107
|
-
cleanup,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Otherwise use exec for buffered output
|
|
112
|
-
// Shell command construction: We use exec() with shell piping
|
|
113
|
-
// because the agent requires stdin input. The tmpFile path is system-controlled
|
|
114
|
-
// (os.tmpdir() + Date.now() + process.pid), not user-supplied, eliminating injection risk.
|
|
115
|
-
// Double quotes handle paths with spaces.
|
|
116
|
-
try {
|
|
117
|
-
const cmd = `cat "${tmpFile}" | agent`;
|
|
118
|
-
const { stdout } = await execAsync(cmd, {
|
|
119
|
-
timeout: opts.timeoutMs,
|
|
120
|
-
maxBuffer: MAX_BUFFER_BYTES,
|
|
121
|
-
});
|
|
122
|
-
return stdout;
|
|
123
|
-
} finally {
|
|
124
|
-
// Cleanup errors are intentionally ignored - the tmp file will be cleaned up by OS
|
|
125
|
-
await cleanup();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { getDebugLogger } from "../utils/debug-log.js";
|
|
7
|
-
import { type CLIAdapter, runStreamingCommand } from "./index.js";
|
|
8
|
-
import { GEMINI_THINKING_BUDGET } from "./thinking-budget.js";
|
|
9
|
-
|
|
10
|
-
const execAsync = promisify(exec);
|
|
11
|
-
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
12
|
-
|
|
13
|
-
interface GeminiTelemetryUsage {
|
|
14
|
-
inputTokens?: number;
|
|
15
|
-
outputTokens?: number;
|
|
16
|
-
thoughtTokens?: number;
|
|
17
|
-
cacheTokens?: number;
|
|
18
|
-
toolTokens?: number;
|
|
19
|
-
toolCalls?: number;
|
|
20
|
-
toolContentChars?: number;
|
|
21
|
-
apiRequests?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type TokenType = "input" | "output" | "thought" | "cache" | "tool";
|
|
25
|
-
const TOKEN_TYPE_MAP: Record<TokenType, keyof GeminiTelemetryUsage> = {
|
|
26
|
-
input: "inputTokens",
|
|
27
|
-
output: "outputTokens",
|
|
28
|
-
thought: "thoughtTokens",
|
|
29
|
-
cache: "cacheTokens",
|
|
30
|
-
tool: "toolTokens",
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Gemini CLI telemetry file contains pretty-printed JSON objects (not OTLP).
|
|
34
|
-
// The metric object has { resource, scopeMetrics: [{ scope, metrics }] }.
|
|
35
|
-
// Each metric has { descriptor: { name, type }, dataPoints: [{ value, attributes }] }.
|
|
36
|
-
|
|
37
|
-
interface SdkDataPoint {
|
|
38
|
-
value: number | { sum?: number };
|
|
39
|
-
attributes?: Record<string, string | number | boolean>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface SdkMetric {
|
|
43
|
-
descriptor?: { name?: string };
|
|
44
|
-
dataPoints?: SdkDataPoint[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface SdkScopeMetrics {
|
|
48
|
-
metrics?: SdkMetric[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function extractTokenType(dp: SdkDataPoint): TokenType | null {
|
|
52
|
-
const type =
|
|
53
|
-
dp.attributes?.type ?? dp.attributes?.["gen_ai.token.type"] ?? null;
|
|
54
|
-
return typeof type === "string" && type in TOKEN_TYPE_MAP
|
|
55
|
-
? (type as TokenType)
|
|
56
|
-
: null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function extractValue(dp: SdkDataPoint): number {
|
|
60
|
-
if (typeof dp.value === "number") return dp.value;
|
|
61
|
-
return dp.value?.sum ?? 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function processDataPoints(
|
|
65
|
-
dataPoints: SdkDataPoint[],
|
|
66
|
-
usage: GeminiTelemetryUsage,
|
|
67
|
-
): void {
|
|
68
|
-
for (const dp of dataPoints) {
|
|
69
|
-
const tokenType = extractTokenType(dp);
|
|
70
|
-
if (!tokenType) continue;
|
|
71
|
-
const key = TOKEN_TYPE_MAP[tokenType];
|
|
72
|
-
usage[key] = (usage[key] || 0) + extractValue(dp);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const TOKEN_METRIC_NAMES = new Set([
|
|
77
|
-
"gemini_cli.token.usage",
|
|
78
|
-
"gen_ai.client.token.usage",
|
|
79
|
-
]);
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Find [start, end) index pairs for each top-level `{…}` in a string
|
|
83
|
-
* whose JSON string literals have already been blanked out.
|
|
84
|
-
*/
|
|
85
|
-
function handleOpen(i: number, depth: number, start: number): [number, number] {
|
|
86
|
-
return [depth + 1, depth === 0 ? i : start];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function handleClose(
|
|
90
|
-
i: number,
|
|
91
|
-
depth: number,
|
|
92
|
-
start: number,
|
|
93
|
-
ranges: Array<[number, number]>,
|
|
94
|
-
): [number, number] {
|
|
95
|
-
const next = depth - 1;
|
|
96
|
-
if (next === 0 && start >= 0) {
|
|
97
|
-
ranges.push([start, i + 1]);
|
|
98
|
-
return [next, -1];
|
|
99
|
-
}
|
|
100
|
-
return [next, start];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function findObjectBoundaries(stripped: string): Array<[number, number]> {
|
|
104
|
-
const ranges: Array<[number, number]> = [];
|
|
105
|
-
let depth = 0;
|
|
106
|
-
let start = -1;
|
|
107
|
-
for (let i = 0; i < stripped.length; i++) {
|
|
108
|
-
const ch = stripped[i];
|
|
109
|
-
if (ch === "{") [depth, start] = handleOpen(i, depth, start);
|
|
110
|
-
else if (ch === "}") [depth, start] = handleClose(i, depth, start, ranges);
|
|
111
|
-
}
|
|
112
|
-
return ranges;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Parse top-level JSON objects from telemetry content.
|
|
117
|
-
* Blanks string literals (preserving length) before brace-counting
|
|
118
|
-
* so braces inside strings are ignored.
|
|
119
|
-
*/
|
|
120
|
-
function parseJsonObjects(content: string): unknown[] {
|
|
121
|
-
const stripped = content.replace(/"(?:[^"\\]|\\.)*"/g, (m) =>
|
|
122
|
-
" ".repeat(m.length),
|
|
123
|
-
);
|
|
124
|
-
const objects: unknown[] = [];
|
|
125
|
-
for (const [s, e] of findObjectBoundaries(stripped)) {
|
|
126
|
-
try {
|
|
127
|
-
objects.push(JSON.parse(content.slice(s, e)));
|
|
128
|
-
} catch {
|
|
129
|
-
// Skip malformed objects
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return objects;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Sum all data point values for a counter metric. */
|
|
136
|
-
function sumDataPoints(dataPoints: SdkDataPoint[]): number {
|
|
137
|
-
let total = 0;
|
|
138
|
-
for (const dp of dataPoints) {
|
|
139
|
-
total += extractValue(dp);
|
|
140
|
-
}
|
|
141
|
-
return total;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Route a single metric to the appropriate usage field. */
|
|
145
|
-
function processMetric(metric: SdkMetric, usage: GeminiTelemetryUsage): void {
|
|
146
|
-
const name = metric.descriptor?.name;
|
|
147
|
-
if (!name) return;
|
|
148
|
-
const dataPoints = metric.dataPoints ?? [];
|
|
149
|
-
|
|
150
|
-
if (TOKEN_METRIC_NAMES.has(name)) {
|
|
151
|
-
processDataPoints(dataPoints, usage);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (name === "gemini_cli.tool.call.count") {
|
|
155
|
-
usage.toolCalls = (usage.toolCalls || 0) + sumDataPoints(dataPoints);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (name === "gemini_cli.api.request.count") {
|
|
159
|
-
usage.apiRequests = (usage.apiRequests || 0) + sumDataPoints(dataPoints);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function processMetricObject(data: unknown, usage: GeminiTelemetryUsage): void {
|
|
164
|
-
const obj = data as { scopeMetrics?: SdkScopeMetrics[] };
|
|
165
|
-
if (!obj?.scopeMetrics) return;
|
|
166
|
-
for (const sm of obj.scopeMetrics) {
|
|
167
|
-
for (const metric of sm.metrics ?? []) {
|
|
168
|
-
processMetric(metric, usage);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Extract tool call details from OTel log records.
|
|
175
|
-
* The Gemini CLI emits `gemini_cli.tool_call` log events with
|
|
176
|
-
* content_length attributes that reveal how much data tools read.
|
|
177
|
-
*/
|
|
178
|
-
interface SdkLogRecord {
|
|
179
|
-
body?: { stringValue?: string };
|
|
180
|
-
attributes?: Array<{
|
|
181
|
-
key: string;
|
|
182
|
-
value: { intValue?: string; stringValue?: string };
|
|
183
|
-
}>;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
interface SdkScopeLogs {
|
|
187
|
-
logRecords?: SdkLogRecord[];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Extract content_length from a tool_call log record, or 0 if absent. */
|
|
191
|
-
function extractToolContentLength(record: SdkLogRecord): number {
|
|
192
|
-
const attr = record.attributes?.find((a) => a.key === "content_length");
|
|
193
|
-
if (!attr?.value?.intValue) return 0;
|
|
194
|
-
const len = parseInt(attr.value.intValue, 10);
|
|
195
|
-
return Number.isNaN(len) ? 0 : len;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Sum content_length across all tool_call log records in a scope. */
|
|
199
|
-
function sumToolContentChars(records: SdkLogRecord[]): number {
|
|
200
|
-
let total = 0;
|
|
201
|
-
for (const record of records) {
|
|
202
|
-
if (record.body?.stringValue !== "gemini_cli.tool_call") continue;
|
|
203
|
-
total += extractToolContentLength(record);
|
|
204
|
-
}
|
|
205
|
-
return total;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function processLogObject(data: unknown, usage: GeminiTelemetryUsage): void {
|
|
209
|
-
const obj = data as { scopeLogs?: SdkScopeLogs[] };
|
|
210
|
-
if (!obj?.scopeLogs) return;
|
|
211
|
-
for (const sl of obj.scopeLogs) {
|
|
212
|
-
const chars = sumToolContentChars(sl.logRecords ?? []);
|
|
213
|
-
if (chars > 0) {
|
|
214
|
-
usage.toolContentChars = (usage.toolContentChars || 0) + chars;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function parseGeminiTelemetry(
|
|
220
|
-
filePath: string,
|
|
221
|
-
): Promise<GeminiTelemetryUsage> {
|
|
222
|
-
const usage: GeminiTelemetryUsage = {};
|
|
223
|
-
let content: string;
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
content = await fs.readFile(filePath, "utf-8");
|
|
227
|
-
} catch {
|
|
228
|
-
return usage;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
for (const obj of parseJsonObjects(content)) {
|
|
232
|
-
processMetricObject(obj, usage);
|
|
233
|
-
processLogObject(obj, usage);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return usage;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const SUMMARY_FIELDS: Array<[keyof GeminiTelemetryUsage, string]> = [
|
|
240
|
-
["inputTokens", "in"],
|
|
241
|
-
["outputTokens", "out"],
|
|
242
|
-
["thoughtTokens", "thought"],
|
|
243
|
-
["cacheTokens", "cache"],
|
|
244
|
-
["toolTokens", "tool"],
|
|
245
|
-
["toolCalls", "tool_calls"],
|
|
246
|
-
["toolContentChars", "tool_content_chars"],
|
|
247
|
-
["apiRequests", "api_requests"],
|
|
248
|
-
];
|
|
249
|
-
|
|
250
|
-
function formatGeminiSummary(usage: GeminiTelemetryUsage): string | null {
|
|
251
|
-
const parts = SUMMARY_FIELDS.filter(([key]) => usage[key] !== undefined).map(
|
|
252
|
-
([key, label]) => `${label}=${usage[key]}`,
|
|
253
|
-
);
|
|
254
|
-
return parts.length > 0 ? `[telemetry] ${parts.join(" ")}` : null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function logTelemetryToStderr(telemetryFile: string): Promise<void> {
|
|
258
|
-
if (process.env.GEMINI_TELEMETRY_OUTFILE) return;
|
|
259
|
-
const usage = await parseGeminiTelemetry(telemetryFile);
|
|
260
|
-
const summary = formatGeminiSummary(usage);
|
|
261
|
-
if (summary) {
|
|
262
|
-
process.stderr.write(`${summary}\n`);
|
|
263
|
-
getDebugLogger()?.logTelemetry({ adapter: "gemini", summary });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export class GeminiAdapter implements CLIAdapter {
|
|
268
|
-
name = "gemini";
|
|
269
|
-
|
|
270
|
-
async isAvailable(): Promise<boolean> {
|
|
271
|
-
try {
|
|
272
|
-
await execAsync("which gemini");
|
|
273
|
-
return true;
|
|
274
|
-
} catch {
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async checkHealth(): Promise<{
|
|
280
|
-
available: boolean;
|
|
281
|
-
status: "healthy" | "missing" | "unhealthy";
|
|
282
|
-
message?: string;
|
|
283
|
-
}> {
|
|
284
|
-
const available = await this.isAvailable();
|
|
285
|
-
if (!available) {
|
|
286
|
-
return {
|
|
287
|
-
available: false,
|
|
288
|
-
status: "missing",
|
|
289
|
-
message: "Command not found",
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return { available: true, status: "healthy", message: "Installed" };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
getProjectCommandDir(): string | null {
|
|
297
|
-
return ".gemini/commands";
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
getUserCommandDir(): string | null {
|
|
301
|
-
return path.join(os.homedir(), ".gemini", "commands");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
getProjectSkillDir(): string | null {
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
getUserSkillDir(): string | null {
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
getCommandExtension(): string {
|
|
313
|
-
return ".toml";
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
canUseSymlink(): boolean {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
transformCommand(markdownContent: string): string {
|
|
321
|
-
const fmMatch = markdownContent.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
322
|
-
let description = "Run the gauntlet verification suite";
|
|
323
|
-
const body = fmMatch ? (fmMatch[2] ?? "") : markdownContent;
|
|
324
|
-
|
|
325
|
-
if (fmMatch) {
|
|
326
|
-
for (const line of (fmMatch[1] ?? "").split("\n")) {
|
|
327
|
-
const kv = line.match(/^description:\s*(.*)$/);
|
|
328
|
-
if (kv?.[1]) description = kv[1].trim();
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return `description = ${JSON.stringify(description)}
|
|
333
|
-
prompt = """
|
|
334
|
-
${body.trim()}
|
|
335
|
-
"""
|
|
336
|
-
`;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private async logTelemetry(
|
|
340
|
-
telemetryFile: string,
|
|
341
|
-
onOutput: (chunk: string) => void,
|
|
342
|
-
): Promise<void> {
|
|
343
|
-
if (process.env.GEMINI_TELEMETRY_OUTFILE) return;
|
|
344
|
-
const usage = await parseGeminiTelemetry(telemetryFile);
|
|
345
|
-
const summary = formatGeminiSummary(usage);
|
|
346
|
-
if (summary) {
|
|
347
|
-
onOutput(`\n${summary}\n`);
|
|
348
|
-
process.stderr.write(`${summary}\n`);
|
|
349
|
-
getDebugLogger()?.logTelemetry({ adapter: "gemini", summary });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Serialize access to .gemini/settings.json across concurrent Gemini instances.
|
|
355
|
-
* When multiple Gemini adapters run in parallel (num_reviews > 1 with
|
|
356
|
-
* cli_preference: ["gemini"]), each waits for the previous to finish
|
|
357
|
-
* before writing its settings.
|
|
358
|
-
*/
|
|
359
|
-
private static settingsLock: Promise<void> = Promise.resolve();
|
|
360
|
-
|
|
361
|
-
private async applyThinkingSettings(
|
|
362
|
-
budget: number,
|
|
363
|
-
): Promise<() => Promise<void>> {
|
|
364
|
-
let releaseLock = () => {};
|
|
365
|
-
const prev = GeminiAdapter.settingsLock;
|
|
366
|
-
GeminiAdapter.settingsLock = new Promise((resolve) => {
|
|
367
|
-
releaseLock = resolve;
|
|
368
|
-
});
|
|
369
|
-
await prev;
|
|
370
|
-
|
|
371
|
-
const settingsPath = path.join(process.cwd(), ".gemini", "settings.json");
|
|
372
|
-
let backup: string | null = null;
|
|
373
|
-
let existed = false;
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
try {
|
|
377
|
-
backup = await fs.readFile(settingsPath, "utf-8");
|
|
378
|
-
existed = true;
|
|
379
|
-
} catch {
|
|
380
|
-
// No existing file
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const existing = backup ? JSON.parse(backup) : {};
|
|
384
|
-
const merged = {
|
|
385
|
-
...existing,
|
|
386
|
-
thinkingConfig: { ...existing.thinkingConfig, thinkingBudget: budget },
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
390
|
-
await fs.writeFile(settingsPath, JSON.stringify(merged, null, 2));
|
|
391
|
-
} catch (err) {
|
|
392
|
-
releaseLock();
|
|
393
|
-
throw err;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return async () => {
|
|
397
|
-
try {
|
|
398
|
-
if (existed && backup !== null) {
|
|
399
|
-
await fs.writeFile(settingsPath, backup);
|
|
400
|
-
} else {
|
|
401
|
-
await fs.unlink(settingsPath).catch(() => {});
|
|
402
|
-
}
|
|
403
|
-
} finally {
|
|
404
|
-
releaseLock();
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private buildTelemetryEnv(telemetryFile: string): Record<string, string> {
|
|
410
|
-
const env: Record<string, string> = {};
|
|
411
|
-
if (!process.env.GEMINI_TELEMETRY_ENABLED) {
|
|
412
|
-
env.GEMINI_TELEMETRY_ENABLED = "true";
|
|
413
|
-
}
|
|
414
|
-
if (!process.env.GEMINI_TELEMETRY_TARGET) {
|
|
415
|
-
env.GEMINI_TELEMETRY_TARGET = "local";
|
|
416
|
-
}
|
|
417
|
-
if (!process.env.GEMINI_TELEMETRY_OUTFILE) {
|
|
418
|
-
env.GEMINI_TELEMETRY_OUTFILE = telemetryFile;
|
|
419
|
-
}
|
|
420
|
-
return env;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
private buildArgs(allowToolUse?: boolean): string[] {
|
|
424
|
-
const args = ["--sandbox"];
|
|
425
|
-
if (allowToolUse !== false) {
|
|
426
|
-
args.push(
|
|
427
|
-
"--allowed-tools",
|
|
428
|
-
"read_file,list_directory,glob,search_file_content",
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
args.push("--output-format", "text");
|
|
432
|
-
return args;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
private async maybeApplyThinking(
|
|
436
|
-
level?: string,
|
|
437
|
-
): Promise<(() => Promise<void>) | undefined> {
|
|
438
|
-
if (!level || !(level in GEMINI_THINKING_BUDGET)) return undefined;
|
|
439
|
-
return this.applyThinkingSettings(GEMINI_THINKING_BUDGET[level] as number);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
async execute(opts: {
|
|
443
|
-
prompt: string;
|
|
444
|
-
diff: string;
|
|
445
|
-
model?: string;
|
|
446
|
-
timeoutMs?: number;
|
|
447
|
-
onOutput?: (chunk: string) => void;
|
|
448
|
-
allowToolUse?: boolean;
|
|
449
|
-
thinkingBudget?: string;
|
|
450
|
-
}): Promise<string> {
|
|
451
|
-
const fullContent = `${opts.prompt}\n\n--- DIFF ---\n${opts.diff}`;
|
|
452
|
-
|
|
453
|
-
const tmpDir = os.tmpdir();
|
|
454
|
-
const tmpFile = path.join(
|
|
455
|
-
tmpDir,
|
|
456
|
-
`gauntlet-gemini-${process.pid}-${Date.now()}.txt`,
|
|
457
|
-
);
|
|
458
|
-
await fs.writeFile(tmpFile, fullContent);
|
|
459
|
-
|
|
460
|
-
// Use cwd for telemetry file — Gemini's --sandbox restricts writes
|
|
461
|
-
// to the project directory, so os.tmpdir() would fail with EPERM.
|
|
462
|
-
const telemetryFile = path.join(
|
|
463
|
-
process.cwd(),
|
|
464
|
-
`.gauntlet-gemini-telemetry-${process.pid}-${Date.now()}.log`,
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
const telemetryEnv = this.buildTelemetryEnv(telemetryFile);
|
|
468
|
-
const args = this.buildArgs(opts.allowToolUse);
|
|
469
|
-
const cleanupThinking = await this.maybeApplyThinking(opts.thinkingBudget);
|
|
470
|
-
|
|
471
|
-
const cleanup = () => fs.unlink(tmpFile).catch(() => {});
|
|
472
|
-
const cleanupTelemetry = () => fs.unlink(telemetryFile).catch(() => {});
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
if (opts.onOutput) {
|
|
476
|
-
try {
|
|
477
|
-
const result = await runStreamingCommand({
|
|
478
|
-
command: "gemini",
|
|
479
|
-
args,
|
|
480
|
-
tmpFile,
|
|
481
|
-
timeoutMs: opts.timeoutMs,
|
|
482
|
-
onOutput: opts.onOutput,
|
|
483
|
-
cleanup,
|
|
484
|
-
env: { ...process.env, ...telemetryEnv },
|
|
485
|
-
});
|
|
486
|
-
await this.logTelemetry(telemetryFile, opts.onOutput);
|
|
487
|
-
return result;
|
|
488
|
-
} finally {
|
|
489
|
-
await cleanupTelemetry();
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
const cmd = `gemini ${args.join(" ")} < "${tmpFile}"`;
|
|
495
|
-
const { stdout } = await execAsync(cmd, {
|
|
496
|
-
timeout: opts.timeoutMs,
|
|
497
|
-
maxBuffer: MAX_BUFFER_BYTES,
|
|
498
|
-
env: { ...process.env, ...telemetryEnv },
|
|
499
|
-
});
|
|
500
|
-
await logTelemetryToStderr(telemetryFile);
|
|
501
|
-
return stdout;
|
|
502
|
-
} finally {
|
|
503
|
-
await cleanup();
|
|
504
|
-
await cleanupTelemetry();
|
|
505
|
-
}
|
|
506
|
-
} finally {
|
|
507
|
-
await cleanupThinking?.();
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|