@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.
@@ -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 { readFileSync, existsSync, readdirSync } from 'node:fs';
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({ code: null, stdout: '', stderr: '', termination: 'cancelled' });
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
- const child = spawn(r.cmd, r.args, { cwd: opts.cwd, shell: false, detached, ...(opts.env ? { env: opts.env } : {}) });
181
- let stdout = '';
182
- let stderr = '';
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
- finish();
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) => { stdout += d.toString(); });
259
- child.stderr?.on('data', (d) => { stderr += d.toString(); });
260
- child.on('error', (e) => { void settle(() => reject(e)); });
261
- child.on('close', (code) => { void settle(() => resolve({ code, stdout, stderr, termination })); });
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: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS,
712
+ timeoutMs,
420
713
  ...(ctx.env ? { env: ctx.env } : {}),
421
714
  ...(ctx.signal ? { signal: ctx.signal } : {}),
422
715
  });
423
- if (result.termination === 'timeout') {
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;