@evomap/evolver-core 2.0.0-beta.5 → 2.0.0-beta.6
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/events/ingest.js +7 -0
- package/dist/exec/claudeBridge.d.ts +12 -2
- package/dist/exec/claudeBridge.js +200 -29
- package/dist/exec/openPrRegistry.d.ts +8 -2
- package/dist/exec/openPrRegistry.js +32 -22
- package/dist/exec/runnerRegistry.d.ts +26 -0
- package/dist/exec/runnerRegistry.js +305 -47
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/issueReporter/index.d.ts +156 -0
- package/dist/issueReporter/index.js +1668 -0
- package/dist/personality/schema.d.ts +12 -12
- package/dist/util/fetchPort.d.ts +1 -0
- package/dist/util/fetchPort.js +11 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/package.json +1 -1
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
// nothing here spawns a real agent in tests except through spawnCapture, which the bridge injects fakes around.
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { join as joinPath, delimiter as pathDelimiter } from 'node:path';
|
|
8
|
-
import {
|
|
8
|
+
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
9
9
|
export const DEFAULT_TIMEOUT_MS = 600_000;
|
|
10
|
+
/** Per-stream stdout/stderr capture ceiling. A child can emit indefinitely without growing the parent heap. */
|
|
11
|
+
export const DEFAULT_MAX_CAPTURE_BYTES = 1_048_576;
|
|
12
|
+
const MIN_MAX_CAPTURE_BYTES = 256;
|
|
10
13
|
/** Thrown when permission bypass is requested without bounding the agent's tools (would be an unbounded autonomous agent). */
|
|
11
14
|
export class UnboundedSkipPermissionsError extends Error {
|
|
12
15
|
constructor() {
|
|
@@ -161,6 +164,132 @@ export function killWindowsProcessTree(pid, spawnCommand = spawn, timeoutMs = WI
|
|
|
161
164
|
}
|
|
162
165
|
});
|
|
163
166
|
}
|
|
167
|
+
/** A redirected stdout artifact could not be finalized; the subprocess outcome remains available for classification. */
|
|
168
|
+
export class SpawnCaptureFinalizeError extends Error {
|
|
169
|
+
result;
|
|
170
|
+
constructor(result, cause) {
|
|
171
|
+
const detail = cause instanceof Error ? `: ${cause.message}` : '';
|
|
172
|
+
super(`redirected stdout finalization failed${detail}`, { cause });
|
|
173
|
+
this.name = 'SpawnCaptureFinalizeError';
|
|
174
|
+
this.result = result;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Retain a bounded prefix and suffix while counting every byte received. Keeping raw buffers until rendering
|
|
179
|
+
* avoids corrupting multi-byte UTF-8 characters when Node splits a character across stream chunks.
|
|
180
|
+
*/
|
|
181
|
+
class BoundedStreamCapture {
|
|
182
|
+
maxBytes;
|
|
183
|
+
headCapacity;
|
|
184
|
+
tailCapacity;
|
|
185
|
+
head;
|
|
186
|
+
tail;
|
|
187
|
+
headLength = 0;
|
|
188
|
+
tailLength = 0;
|
|
189
|
+
tailWriteOffset = 0;
|
|
190
|
+
totalBytes = 0;
|
|
191
|
+
constructor(maxBytes) {
|
|
192
|
+
this.maxBytes = maxBytes;
|
|
193
|
+
this.headCapacity = Math.ceil(maxBytes / 2);
|
|
194
|
+
this.tailCapacity = maxBytes - this.headCapacity;
|
|
195
|
+
}
|
|
196
|
+
append(value) {
|
|
197
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
198
|
+
this.totalBytes += chunk.length;
|
|
199
|
+
let offset = 0;
|
|
200
|
+
if (this.headLength < this.headCapacity) {
|
|
201
|
+
const take = Math.min(this.headCapacity - this.headLength, chunk.length);
|
|
202
|
+
const head = this.ensureHeadCapacity(this.headLength + take);
|
|
203
|
+
chunk.copy(head, this.headLength, 0, take);
|
|
204
|
+
this.headLength += take;
|
|
205
|
+
offset = take;
|
|
206
|
+
}
|
|
207
|
+
if (offset < chunk.length)
|
|
208
|
+
this.appendTail(chunk.subarray(offset));
|
|
209
|
+
}
|
|
210
|
+
result() {
|
|
211
|
+
const head = this.head?.subarray(0, this.headLength) ?? Buffer.alloc(0);
|
|
212
|
+
const tail = this.orderedTail();
|
|
213
|
+
if (this.totalBytes <= this.maxBytes) {
|
|
214
|
+
return {
|
|
215
|
+
text: Buffer.concat([head, tail]).toString('utf8'),
|
|
216
|
+
bytes: this.totalBytes,
|
|
217
|
+
truncated: false,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const marker = Buffer.from(`\n...[evolver output truncated; total_bytes=${this.totalBytes}]...\n`);
|
|
221
|
+
const retainedBudget = this.maxBytes - marker.length;
|
|
222
|
+
const headBudget = Math.ceil(retainedBudget / 2);
|
|
223
|
+
const tailBudget = retainedBudget - headBudget;
|
|
224
|
+
const retainedHead = trimIncompleteUtf8Suffix(head.subarray(0, headBudget));
|
|
225
|
+
const tailStart = Math.max(0, tail.length - tailBudget);
|
|
226
|
+
const retainedTail = trimUtf8ContinuationPrefix(tail.subarray(tailStart));
|
|
227
|
+
return {
|
|
228
|
+
text: Buffer.concat([retainedHead, marker, retainedTail]).toString('utf8'),
|
|
229
|
+
bytes: this.totalBytes,
|
|
230
|
+
truncated: true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
ensureHeadCapacity(required) {
|
|
234
|
+
const current = this.head;
|
|
235
|
+
if (current && current.length >= required)
|
|
236
|
+
return current;
|
|
237
|
+
let capacity = current?.length ?? Math.min(4_096, this.headCapacity);
|
|
238
|
+
while (capacity < required)
|
|
239
|
+
capacity = Math.min(this.headCapacity, capacity * 2);
|
|
240
|
+
const next = Buffer.allocUnsafe(capacity);
|
|
241
|
+
if (current)
|
|
242
|
+
current.copy(next, 0, 0, this.headLength);
|
|
243
|
+
this.head = next;
|
|
244
|
+
return next;
|
|
245
|
+
}
|
|
246
|
+
appendTail(incoming) {
|
|
247
|
+
const tail = this.tail ??= Buffer.allocUnsafe(this.tailCapacity);
|
|
248
|
+
if (incoming.length >= this.tailCapacity) {
|
|
249
|
+
incoming.copy(tail, 0, incoming.length - this.tailCapacity);
|
|
250
|
+
this.tailLength = this.tailCapacity;
|
|
251
|
+
this.tailWriteOffset = 0;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const first = Math.min(incoming.length, this.tailCapacity - this.tailWriteOffset);
|
|
255
|
+
incoming.copy(tail, this.tailWriteOffset, 0, first);
|
|
256
|
+
if (first < incoming.length)
|
|
257
|
+
incoming.copy(tail, 0, first);
|
|
258
|
+
this.tailWriteOffset = (this.tailWriteOffset + incoming.length) % this.tailCapacity;
|
|
259
|
+
this.tailLength = Math.min(this.tailCapacity, this.tailLength + incoming.length);
|
|
260
|
+
}
|
|
261
|
+
orderedTail() {
|
|
262
|
+
const tail = this.tail;
|
|
263
|
+
if (!tail || this.tailLength === 0)
|
|
264
|
+
return Buffer.alloc(0);
|
|
265
|
+
if (this.tailLength < this.tailCapacity)
|
|
266
|
+
return tail.subarray(0, this.tailLength);
|
|
267
|
+
if (this.tailWriteOffset === 0)
|
|
268
|
+
return tail;
|
|
269
|
+
return Buffer.concat([
|
|
270
|
+
tail.subarray(this.tailWriteOffset),
|
|
271
|
+
tail.subarray(0, this.tailWriteOffset),
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function trimIncompleteUtf8Suffix(value) {
|
|
276
|
+
if (value.length === 0)
|
|
277
|
+
return value;
|
|
278
|
+
let lead = value.length - 1;
|
|
279
|
+
while (lead >= 0 && (value[lead] & 0xc0) === 0x80)
|
|
280
|
+
lead -= 1;
|
|
281
|
+
if (lead < 0)
|
|
282
|
+
return Buffer.alloc(0);
|
|
283
|
+
const first = value[lead];
|
|
284
|
+
const expected = first < 0x80 ? 1 : first >= 0xf0 ? 4 : first >= 0xe0 ? 3 : first >= 0xc0 ? 2 : 1;
|
|
285
|
+
return value.length - lead < expected ? value.subarray(0, lead) : value;
|
|
286
|
+
}
|
|
287
|
+
function trimUtf8ContinuationPrefix(value) {
|
|
288
|
+
let offset = 0;
|
|
289
|
+
while (offset < value.length && (value[offset] & 0xc0) === 0x80)
|
|
290
|
+
offset += 1;
|
|
291
|
+
return value.subarray(offset);
|
|
292
|
+
}
|
|
164
293
|
/**
|
|
165
294
|
* Promise wrapper over spawn (shell:false). Optionally writes `input` to stdin; resolves with stdout/exit.
|
|
166
295
|
* On timeout the WHOLE process group is killed, not just the direct child (finding #39.5): an agent spawns
|
|
@@ -169,20 +298,87 @@ export function killWindowsProcessTree(pid, spawnCommand = spawn, timeoutMs = WI
|
|
|
169
298
|
* `taskkill.exe /PID <pid> /T /F` without a shell and waits for that command before resolving.
|
|
170
299
|
*/
|
|
171
300
|
export function spawnCapture(cmd, args, opts) {
|
|
301
|
+
const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_CAPTURE_BYTES;
|
|
302
|
+
if (!Number.isSafeInteger(maxOutputBytes)
|
|
303
|
+
|| maxOutputBytes < MIN_MAX_CAPTURE_BYTES
|
|
304
|
+
|| maxOutputBytes > DEFAULT_MAX_CAPTURE_BYTES) {
|
|
305
|
+
throw new RangeError(`maxOutputBytes must be an integer between ${MIN_MAX_CAPTURE_BYTES} and ${DEFAULT_MAX_CAPTURE_BYTES}`);
|
|
306
|
+
}
|
|
172
307
|
return new Promise((resolve, reject) => {
|
|
173
308
|
if (opts.signal?.aborted) {
|
|
174
|
-
resolve({
|
|
309
|
+
resolve({
|
|
310
|
+
code: null,
|
|
311
|
+
stdout: '',
|
|
312
|
+
stderr: '',
|
|
313
|
+
termination: 'cancelled',
|
|
314
|
+
stdoutBytes: 0,
|
|
315
|
+
stderrBytes: 0,
|
|
316
|
+
stdoutTruncated: false,
|
|
317
|
+
stderrTruncated: false,
|
|
318
|
+
});
|
|
175
319
|
return;
|
|
176
320
|
}
|
|
177
321
|
const platform = opts.processPlatform ?? process.platform;
|
|
178
322
|
const detached = platform !== 'win32';
|
|
179
323
|
const r = resolveSpawnCommand(cmd, args, opts.env, opts.resolvePlatform ?? process.platform);
|
|
180
|
-
|
|
181
|
-
let
|
|
182
|
-
|
|
324
|
+
let stdoutFd;
|
|
325
|
+
let ownsStdoutFile = false;
|
|
326
|
+
const cleanupOwnedStdoutFile = () => {
|
|
327
|
+
if (!ownsStdoutFile || !opts.stdoutFile)
|
|
328
|
+
return;
|
|
329
|
+
try {
|
|
330
|
+
rmSync(opts.stdoutFile, { force: true });
|
|
331
|
+
ownsStdoutFile = false;
|
|
332
|
+
}
|
|
333
|
+
catch { /* best-effort; a caller ownership hook can retry */ }
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
if (opts.stdoutFile) {
|
|
337
|
+
stdoutFd = openSync(opts.stdoutFile, 'wx', 0o600);
|
|
338
|
+
ownsStdoutFile = true;
|
|
339
|
+
opts.onStdoutFileOpened?.(opts.stdoutFile);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (stdoutFd !== undefined) {
|
|
344
|
+
try {
|
|
345
|
+
closeSync(stdoutFd);
|
|
346
|
+
}
|
|
347
|
+
catch { /* best-effort cleanup before the child exists */ }
|
|
348
|
+
stdoutFd = undefined;
|
|
349
|
+
}
|
|
350
|
+
cleanupOwnedStdoutFile();
|
|
351
|
+
reject(error);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let child;
|
|
355
|
+
try {
|
|
356
|
+
child = spawn(r.cmd, r.args, {
|
|
357
|
+
cwd: opts.cwd,
|
|
358
|
+
shell: false,
|
|
359
|
+
detached,
|
|
360
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
361
|
+
...(stdoutFd !== undefined ? { stdio: ['pipe', stdoutFd, 'pipe'] } : {}),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (stdoutFd !== undefined) {
|
|
366
|
+
try {
|
|
367
|
+
closeSync(stdoutFd);
|
|
368
|
+
}
|
|
369
|
+
catch { /* best-effort cleanup before rejection */ }
|
|
370
|
+
stdoutFd = undefined;
|
|
371
|
+
}
|
|
372
|
+
cleanupOwnedStdoutFile();
|
|
373
|
+
reject(error);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const stdoutCapture = new BoundedStreamCapture(maxOutputBytes);
|
|
377
|
+
const stderrCapture = new BoundedStreamCapture(maxOutputBytes);
|
|
183
378
|
let termination = 'exit';
|
|
184
379
|
let killPromise;
|
|
185
380
|
let settled = false;
|
|
381
|
+
let redirectedStdoutBytes;
|
|
186
382
|
const killTree = () => {
|
|
187
383
|
if (killPromise)
|
|
188
384
|
return killPromise;
|
|
@@ -241,7 +437,28 @@ export function spawnCapture(cmd, args, opts) {
|
|
|
241
437
|
if (killPromise)
|
|
242
438
|
await killPromise;
|
|
243
439
|
cleanup();
|
|
244
|
-
|
|
440
|
+
let stdoutFileError;
|
|
441
|
+
if (stdoutFd !== undefined) {
|
|
442
|
+
const fd = stdoutFd;
|
|
443
|
+
stdoutFd = undefined;
|
|
444
|
+
try {
|
|
445
|
+
redirectedStdoutBytes = opts.stdoutFileOps?.size(fd) ?? fstatSync(fd).size;
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
stdoutFileError = error;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
(opts.stdoutFileOps?.close ?? closeSync)(fd);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
stdoutFileError ??= error;
|
|
455
|
+
try {
|
|
456
|
+
closeSync(fd);
|
|
457
|
+
}
|
|
458
|
+
catch { /* retry a failed/injected close before removing our artifact */ }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
finish(stdoutFileError);
|
|
245
462
|
};
|
|
246
463
|
const timer = setTimeout(timeout, opts.timeoutMs);
|
|
247
464
|
opts.signal?.addEventListener('abort', cancel, { once: true });
|
|
@@ -255,10 +472,37 @@ export function spawnCapture(cmd, args, opts) {
|
|
|
255
472
|
process.once('SIGINT', cancel);
|
|
256
473
|
process.once('SIGTERM', cancel);
|
|
257
474
|
}
|
|
258
|
-
child.stdout?.on('data', (d) => {
|
|
259
|
-
child.stderr?.on('data', (d) => {
|
|
260
|
-
child.on('error', (e) => {
|
|
261
|
-
|
|
475
|
+
child.stdout?.on('data', (d) => { stdoutCapture.append(d); });
|
|
476
|
+
child.stderr?.on('data', (d) => { stderrCapture.append(d); });
|
|
477
|
+
child.on('error', (e) => {
|
|
478
|
+
void settle(() => {
|
|
479
|
+
cleanupOwnedStdoutFile();
|
|
480
|
+
reject(e);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
child.on('close', (code) => {
|
|
484
|
+
void settle((stdoutFileError) => {
|
|
485
|
+
const stdout = stdoutCapture.result();
|
|
486
|
+
const stderr = stderrCapture.result();
|
|
487
|
+
const result = {
|
|
488
|
+
code,
|
|
489
|
+
stdout: stdout.text,
|
|
490
|
+
stderr: stderr.text,
|
|
491
|
+
termination,
|
|
492
|
+
stdoutBytes: redirectedStdoutBytes ?? stdout.bytes,
|
|
493
|
+
stderrBytes: stderr.bytes,
|
|
494
|
+
stdoutTruncated: stdout.truncated,
|
|
495
|
+
stderrTruncated: stderr.truncated,
|
|
496
|
+
...(opts.stdoutFile ? { stdoutRedirected: true } : {}),
|
|
497
|
+
};
|
|
498
|
+
if (stdoutFileError !== undefined) {
|
|
499
|
+
cleanupOwnedStdoutFile();
|
|
500
|
+
reject(new SpawnCaptureFinalizeError(result, stdoutFileError));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
resolve(result);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
262
506
|
if (opts.input !== undefined) {
|
|
263
507
|
child.stdin?.write(opts.input);
|
|
264
508
|
child.stdin?.end();
|
|
@@ -400,6 +644,54 @@ function geminiMessage(value) {
|
|
|
400
644
|
function geminiWarnings(value) {
|
|
401
645
|
return Array.isArray(value) ? value.map(geminiMessage).filter(Boolean) : [];
|
|
402
646
|
}
|
|
647
|
+
/** Interpret one bounded Gemini subprocess result. Structured output and diagnostics require complete capture. */
|
|
648
|
+
export function classifyGeminiRunnerResult(result, timeoutMs) {
|
|
649
|
+
if (result.termination === 'timeout') {
|
|
650
|
+
return { ok: false, output: result.stdout, error: `gemini timed out after ${timeoutMs}ms`, failureKind: 'timeout', exitCode: result.code };
|
|
651
|
+
}
|
|
652
|
+
if (result.termination === 'cancelled') {
|
|
653
|
+
return { ok: false, output: result.stdout, error: 'gemini execution cancelled', failureKind: 'cancelled', exitCode: result.code };
|
|
654
|
+
}
|
|
655
|
+
if (result.stdoutTruncated || result.stderrTruncated) {
|
|
656
|
+
return {
|
|
657
|
+
ok: false,
|
|
658
|
+
output: result.stdout,
|
|
659
|
+
error: 'gemini output exceeded the capture limit',
|
|
660
|
+
failureKind: 'invalid_output',
|
|
661
|
+
exitCode: result.code,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
let envelope;
|
|
665
|
+
try {
|
|
666
|
+
const parsed = JSON.parse(result.stdout);
|
|
667
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
668
|
+
throw new Error('JSON object required');
|
|
669
|
+
envelope = parsed;
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
const error = result.stderr || (result.code === 0 ? 'gemini returned invalid JSON output' : `gemini exited with code ${String(result.code)}`);
|
|
673
|
+
return { ok: false, output: result.stdout, error, failureKind: result.code === 0 ? 'invalid_output' : 'non_zero_exit', exitCode: result.code };
|
|
674
|
+
}
|
|
675
|
+
const structuredError = geminiMessage(envelope.error);
|
|
676
|
+
const warnings = geminiWarnings(envelope.warnings);
|
|
677
|
+
const terminationError = result.code === 0
|
|
678
|
+
? GEMINI_TERMINATION_WARNINGS.find(({ pattern }) => warnings.some((warning) => pattern.test(warning)))?.error
|
|
679
|
+
: undefined;
|
|
680
|
+
if (terminationError) {
|
|
681
|
+
return { ok: false, output: geminiMessage(envelope.response), error: terminationError, failureKind: 'runtime_error', exitCode: result.code };
|
|
682
|
+
}
|
|
683
|
+
const denial = [structuredError, ...warnings, result.stderr].find((message) => GEMINI_PERMISSION_DENIAL_RE.test(message));
|
|
684
|
+
if (denial) {
|
|
685
|
+
return { ok: false, output: geminiMessage(envelope.response), error: denial, failureKind: 'permission_denied', exitCode: result.code };
|
|
686
|
+
}
|
|
687
|
+
if (result.code !== 0) {
|
|
688
|
+
return { ok: false, output: geminiMessage(envelope.response), error: structuredError || result.stderr || `gemini exited with code ${String(result.code)}`, failureKind: 'non_zero_exit', exitCode: result.code };
|
|
689
|
+
}
|
|
690
|
+
if (structuredError) {
|
|
691
|
+
return { ok: false, output: geminiMessage(envelope.response), error: structuredError, failureKind: 'runtime_error', exitCode: result.code };
|
|
692
|
+
}
|
|
693
|
+
return { ok: true, output: geminiMessage(envelope.response), exitCode: result.code };
|
|
694
|
+
}
|
|
403
695
|
/** Build verified Gemini CLI argv. The prompt is appended separately as one argv element with shell:false. */
|
|
404
696
|
export function geminiRunnerArgs(opts = {}) {
|
|
405
697
|
if (opts.skipPermissions || (opts.allowedTools?.length ?? 0) > 0)
|
|
@@ -414,48 +706,14 @@ export function makeGeminiHeadlessRunner(opts = {}) {
|
|
|
414
706
|
const args = geminiRunnerArgs(opts);
|
|
415
707
|
return async (prompt, ctx) => {
|
|
416
708
|
try {
|
|
709
|
+
const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
417
710
|
const result = await spawnCapture('gemini', [...args, '--prompt', prompt], {
|
|
418
711
|
cwd: ctx.cwd,
|
|
419
|
-
timeoutMs
|
|
712
|
+
timeoutMs,
|
|
420
713
|
...(ctx.env ? { env: ctx.env } : {}),
|
|
421
714
|
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
422
715
|
});
|
|
423
|
-
|
|
424
|
-
return { ok: false, output: result.stdout, error: `gemini timed out after ${ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, failureKind: 'timeout', exitCode: result.code };
|
|
425
|
-
}
|
|
426
|
-
if (result.termination === 'cancelled') {
|
|
427
|
-
return { ok: false, output: result.stdout, error: 'gemini execution cancelled', failureKind: 'cancelled', exitCode: result.code };
|
|
428
|
-
}
|
|
429
|
-
let envelope;
|
|
430
|
-
try {
|
|
431
|
-
const parsed = JSON.parse(result.stdout);
|
|
432
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
433
|
-
throw new Error('JSON object required');
|
|
434
|
-
envelope = parsed;
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
const error = result.stderr || (result.code === 0 ? 'gemini returned invalid JSON output' : `gemini exited with code ${String(result.code)}`);
|
|
438
|
-
return { ok: false, output: result.stdout, error, failureKind: result.code === 0 ? 'invalid_output' : 'non_zero_exit', exitCode: result.code };
|
|
439
|
-
}
|
|
440
|
-
const structuredError = geminiMessage(envelope.error);
|
|
441
|
-
const warnings = geminiWarnings(envelope.warnings);
|
|
442
|
-
const terminationError = result.code === 0
|
|
443
|
-
? GEMINI_TERMINATION_WARNINGS.find(({ pattern }) => warnings.some((warning) => pattern.test(warning)))?.error
|
|
444
|
-
: undefined;
|
|
445
|
-
if (terminationError) {
|
|
446
|
-
return { ok: false, output: geminiMessage(envelope.response), error: terminationError, failureKind: 'runtime_error', exitCode: result.code };
|
|
447
|
-
}
|
|
448
|
-
const denial = [structuredError, ...warnings, result.stderr].find((message) => GEMINI_PERMISSION_DENIAL_RE.test(message));
|
|
449
|
-
if (denial) {
|
|
450
|
-
return { ok: false, output: geminiMessage(envelope.response), error: denial, failureKind: 'permission_denied', exitCode: result.code };
|
|
451
|
-
}
|
|
452
|
-
if (result.code !== 0) {
|
|
453
|
-
return { ok: false, output: geminiMessage(envelope.response), error: structuredError || result.stderr || `gemini exited with code ${String(result.code)}`, failureKind: 'non_zero_exit', exitCode: result.code };
|
|
454
|
-
}
|
|
455
|
-
if (structuredError) {
|
|
456
|
-
return { ok: false, output: geminiMessage(envelope.response), error: structuredError, failureKind: 'runtime_error', exitCode: result.code };
|
|
457
|
-
}
|
|
458
|
-
return { ok: true, output: geminiMessage(envelope.response), exitCode: result.code };
|
|
716
|
+
return classifyGeminiRunnerResult(result, timeoutMs);
|
|
459
717
|
}
|
|
460
718
|
catch (error) {
|
|
461
719
|
return { ok: false, output: '', error: error instanceof Error ? error.message : String(error), failureKind: 'spawn_failed', exitCode: null };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,4 +24,5 @@ export * as hub from './hub/index.js';
|
|
|
24
24
|
export * as shadow from './shadow/index.js';
|
|
25
25
|
export * as ops from './ops/index.js';
|
|
26
26
|
export * as util from './util/index.js';
|
|
27
|
-
export * as trace from './trace/index.js';
|
|
27
|
+
export * as trace from './trace/index.js';
|
|
28
|
+
export * as issueReporter from './issueReporter/index.js';
|
package/dist/index.js
CHANGED
|
@@ -24,4 +24,5 @@ export * as hub from './hub/index.js';
|
|
|
24
24
|
export * as shadow from './shadow/index.js';
|
|
25
25
|
export * as ops from './ops/index.js';
|
|
26
26
|
export * as util from './util/index.js';
|
|
27
|
-
export * as trace from './trace/index.js';
|
|
27
|
+
export * as trace from './trace/index.js';
|
|
28
|
+
export * as issueReporter from './issueReporter/index.js';
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type EnvFingerprint } from '../bootstrap/envFingerprint.js';
|
|
2
|
+
export type IssueReportSource = 'cycle_failure' | 'doctor' | 'review' | 'event';
|
|
3
|
+
export type IssueDraftStatus = 'draft' | 'rejected' | 'submitted';
|
|
4
|
+
export declare function isIssueReportSource(value: unknown): value is IssueReportSource;
|
|
5
|
+
export interface IssueReportInput {
|
|
6
|
+
source: IssueReportSource;
|
|
7
|
+
errorClass: string;
|
|
8
|
+
cycleIds?: readonly string[];
|
|
9
|
+
eventIds?: readonly string[];
|
|
10
|
+
traceIds?: readonly string[];
|
|
11
|
+
reproductionSteps?: readonly string[];
|
|
12
|
+
diagnosticCodes?: readonly string[];
|
|
13
|
+
failureClass?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
platform?: string;
|
|
16
|
+
arch?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IssueDraft {
|
|
19
|
+
schemaVersion: 1;
|
|
20
|
+
fingerprint: string;
|
|
21
|
+
status: IssueDraftStatus;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
title: string;
|
|
25
|
+
body: string;
|
|
26
|
+
source: IssueReportSource;
|
|
27
|
+
errorClass: string;
|
|
28
|
+
cycleIds: string[];
|
|
29
|
+
eventIds: string[];
|
|
30
|
+
traceRefs: string[];
|
|
31
|
+
github?: {
|
|
32
|
+
issueNumber: number;
|
|
33
|
+
url: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface IssueReporterOptions {
|
|
37
|
+
rootDir: string;
|
|
38
|
+
workspaceScope?: string;
|
|
39
|
+
now?: () => Date;
|
|
40
|
+
env?: Record<string, string | undefined>;
|
|
41
|
+
envFingerprint?: Partial<EnvFingerprint>;
|
|
42
|
+
dedupWindowMs?: number;
|
|
43
|
+
rateLimitWindowMs?: number;
|
|
44
|
+
maxSubmissionsPerWindow?: number;
|
|
45
|
+
logger?: (entry: IssueReporterLogEntry) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface IssueReporterLogEntry {
|
|
48
|
+
fingerprint: string;
|
|
49
|
+
status: 'draft_created' | 'duplicate' | 'rejected' | 'submitted' | 'submission_failed' | 'rate_limited';
|
|
50
|
+
errorClass?: 'approval_required' | 'rate_limited' | 'transport_error' | 'invalid_response' | 'unsafe_draft' | 'local_finalize_error' | 'quota_persistence_error';
|
|
51
|
+
}
|
|
52
|
+
export interface GithubIssueTransport {
|
|
53
|
+
listOpenIssues(input: {
|
|
54
|
+
repo: string;
|
|
55
|
+
page: number;
|
|
56
|
+
perPage: number;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
issues: Array<{
|
|
59
|
+
number: number;
|
|
60
|
+
url: string;
|
|
61
|
+
body: string;
|
|
62
|
+
isPullRequest: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
hasNextPage: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
createIssue(input: {
|
|
67
|
+
repo: string;
|
|
68
|
+
title: string;
|
|
69
|
+
body: string;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
number: number;
|
|
72
|
+
url: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
export declare class GithubIssueTransportError extends Error {
|
|
76
|
+
readonly outcome: 'not_created' | 'ambiguous';
|
|
77
|
+
constructor(outcome: 'not_created' | 'ambiguous');
|
|
78
|
+
}
|
|
79
|
+
export interface SubmitIssueOptions {
|
|
80
|
+
repo: string;
|
|
81
|
+
approved: boolean;
|
|
82
|
+
approvalSource?: 'human' | 'operator_policy';
|
|
83
|
+
transport: GithubIssueTransport;
|
|
84
|
+
}
|
|
85
|
+
export type DraftResult = {
|
|
86
|
+
status: 'created';
|
|
87
|
+
draft: IssueDraft;
|
|
88
|
+
} | {
|
|
89
|
+
status: 'duplicate';
|
|
90
|
+
draft: IssueDraft;
|
|
91
|
+
};
|
|
92
|
+
export type SubmitResult = {
|
|
93
|
+
status: 'submitted';
|
|
94
|
+
draft: IssueDraft;
|
|
95
|
+
errorClass?: 'local_finalize_error' | 'quota_persistence_error';
|
|
96
|
+
} | {
|
|
97
|
+
status: 'already_submitted';
|
|
98
|
+
draft: IssueDraft;
|
|
99
|
+
} | {
|
|
100
|
+
status: 'approval_required';
|
|
101
|
+
draft: IssueDraft;
|
|
102
|
+
} | {
|
|
103
|
+
status: 'rejected';
|
|
104
|
+
draft: IssueDraft;
|
|
105
|
+
} | {
|
|
106
|
+
status: 'rate_limited';
|
|
107
|
+
draft: IssueDraft;
|
|
108
|
+
errorClass?: 'issue_report_submission_in_flight' | 'issue_report_submission_ambiguous';
|
|
109
|
+
} | {
|
|
110
|
+
status: 'failed';
|
|
111
|
+
draft: IssueDraft;
|
|
112
|
+
errorClass: 'transport_error' | 'invalid_response' | 'unsafe_draft' | 'local_finalize_error';
|
|
113
|
+
};
|
|
114
|
+
export type IssueDraftConflictErrorClass = 'issue_report_submission_in_flight' | 'issue_report_submission_ambiguous';
|
|
115
|
+
export declare class IssueDraftConflictError extends Error {
|
|
116
|
+
readonly errorClass: IssueDraftConflictErrorClass;
|
|
117
|
+
constructor(errorClass: IssueDraftConflictErrorClass);
|
|
118
|
+
}
|
|
119
|
+
export declare function createIssueDraft(input: IssueReportInput, options: IssueReporterOptions): DraftResult;
|
|
120
|
+
export declare function rejectIssueDraft(draft: IssueDraft, options: IssueReporterOptions): IssueDraft;
|
|
121
|
+
export declare function submitIssueDraft(draft: IssueDraft, submit: SubmitIssueOptions, options: IssueReporterOptions): Promise<SubmitResult>;
|
|
122
|
+
export type IssueDraftLookupResult = {
|
|
123
|
+
status: 'found';
|
|
124
|
+
draft: IssueDraft;
|
|
125
|
+
} | {
|
|
126
|
+
status: 'missing' | 'invalid';
|
|
127
|
+
};
|
|
128
|
+
export declare function lookupIssueDraft(rootDir: string, fingerprint: string): IssueDraftLookupResult;
|
|
129
|
+
export declare function loadIssueDraft(rootDir: string, fingerprint: string): IssueDraft | null;
|
|
130
|
+
export type IssueSubmissionResolution = {
|
|
131
|
+
outcome: 'not_created' | 'abandoned';
|
|
132
|
+
} | {
|
|
133
|
+
outcome: 'submitted';
|
|
134
|
+
issueNumber: number;
|
|
135
|
+
url: string;
|
|
136
|
+
repo: string;
|
|
137
|
+
};
|
|
138
|
+
export declare function resolveIssueSubmission(fingerprint: string, resolution: IssueSubmissionResolution, options: IssueReporterOptions): IssueDraft;
|
|
139
|
+
export interface IssueReportEvent {
|
|
140
|
+
type: string;
|
|
141
|
+
eventId?: string;
|
|
142
|
+
payload?: Record<string, unknown>;
|
|
143
|
+
}
|
|
144
|
+
export interface IssueReportDoctorCheck {
|
|
145
|
+
name: string;
|
|
146
|
+
status: 'pass' | 'warn' | 'fail';
|
|
147
|
+
}
|
|
148
|
+
export interface IssueReportReviewRecord {
|
|
149
|
+
assetId: string;
|
|
150
|
+
state: 'quarantined' | 'approved' | 'rejected';
|
|
151
|
+
}
|
|
152
|
+
export declare function issueReportInputFromCycle(events: readonly IssueReportEvent[], cycleId: string): IssueReportInput | null;
|
|
153
|
+
export declare function issueReportInputFromDoctor(checks: readonly IssueReportDoctorCheck[]): IssueReportInput | null;
|
|
154
|
+
export declare function issueReportInputFromReview(record: IssueReportReviewRecord): IssueReportInput | null;
|
|
155
|
+
export declare function issueReportInputFromEvent(event: IssueReportEvent): IssueReportInput | null;
|
|
156
|
+
export declare function createIssueDraftForEventBestEffort(event: IssueReportEvent, options: IssueReporterOptions): DraftResult | null;
|