@evomap/evolver-core 2.0.0-beta.8 → 2.0.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.
Files changed (147) hide show
  1. package/dist/algo/candidateAssembly.d.ts +7 -2
  2. package/dist/algo/candidateAssembly.js +81 -13
  3. package/dist/algo/capabilityCandidates.js +10 -0
  4. package/dist/algo/conversationSniffer.js +20 -7
  5. package/dist/algo/cycleEngine.d.ts +23 -2
  6. package/dist/algo/cycleEngine.js +236 -36
  7. package/dist/algo/exploration.d.ts +7 -0
  8. package/dist/algo/exploration.js +16 -3
  9. package/dist/algo/geneHealth.d.ts +2 -2
  10. package/dist/algo/geneHealth.js +5 -4
  11. package/dist/algo/geneSelection.d.ts +63 -5
  12. package/dist/algo/geneSelection.js +330 -30
  13. package/dist/algo/index.d.ts +2 -0
  14. package/dist/algo/index.js +2 -0
  15. package/dist/algo/orchestrator.d.ts +8 -1
  16. package/dist/algo/orchestrator.js +15 -3
  17. package/dist/algo/publishEligibility.d.ts +34 -0
  18. package/dist/algo/publishEligibility.js +52 -0
  19. package/dist/algo/ucb1.d.ts +53 -0
  20. package/dist/algo/ucb1.js +156 -0
  21. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  22. package/dist/assetstore/assetSidecarRecords.js +238 -0
  23. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  24. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  25. package/dist/assetstore/assetStoreHealth.js +56 -14
  26. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  27. package/dist/assetstore/assetStoreStorage.js +27 -9
  28. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  29. package/dist/assetstore/assetSyncLedger.js +683 -4
  30. package/dist/assetstore/index.d.ts +1 -0
  31. package/dist/assetstore/index.js +1 -0
  32. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  33. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  34. package/dist/assetstore/localJsonl.d.ts +4 -2
  35. package/dist/assetstore/localJsonl.js +91 -11
  36. package/dist/assetstore/provenance.d.ts +80 -4
  37. package/dist/assetstore/provenance.js +313 -5
  38. package/dist/assetstore/provider.d.ts +58 -1
  39. package/dist/assetstore/provider.js +97 -6
  40. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  41. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  42. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  43. package/dist/benchmark/antiGeneRollout.js +4 -3
  44. package/dist/benchmark/index.d.ts +2 -1
  45. package/dist/benchmark/index.js +2 -1
  46. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  47. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  48. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  49. package/dist/bootstrap/envFingerprint.js +5 -0
  50. package/dist/bootstrap/index.d.ts +2 -1
  51. package/dist/bootstrap/index.js +2 -1
  52. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  53. package/dist/bootstrap/v1EnvCompat.js +280 -0
  54. package/dist/events/eventArchive.d.ts +2 -0
  55. package/dist/events/eventArchive.js +13 -3
  56. package/dist/events/eventSchema.d.ts +7 -7
  57. package/dist/events/eventStore.d.ts +2 -0
  58. package/dist/events/eventStore.js +5 -1
  59. package/dist/events/ingest.d.ts +1 -0
  60. package/dist/events/ingest.js +1 -0
  61. package/dist/events/paths.d.ts +3 -1
  62. package/dist/events/paths.js +4 -0
  63. package/dist/events/public.d.ts +2 -2
  64. package/dist/events/public.js +2 -2
  65. package/dist/events/reports.d.ts +2 -0
  66. package/dist/events/reports.js +4 -0
  67. package/dist/exec/autoExec.d.ts +43 -2
  68. package/dist/exec/autoExec.js +76 -9
  69. package/dist/exec/autonomousCycle.d.ts +22 -4
  70. package/dist/exec/autonomousCycle.js +64 -13
  71. package/dist/exec/claudeBridge.d.ts +32 -7
  72. package/dist/exec/claudeBridge.js +281 -29
  73. package/dist/exec/prompt.js +5 -1
  74. package/dist/exec/runnerRegistry.d.ts +68 -26
  75. package/dist/exec/runnerRegistry.js +307 -72
  76. package/dist/exec/selfPr.js +1 -7
  77. package/dist/feedback/envelope.d.ts +61 -0
  78. package/dist/feedback/envelope.js +168 -0
  79. package/dist/feedback/index.d.ts +1 -0
  80. package/dist/feedback/index.js +1 -0
  81. package/dist/hooks/hooks.js +1 -0
  82. package/dist/hub/assetCallLog.d.ts +35 -1
  83. package/dist/hub/assetCallLog.js +124 -1
  84. package/dist/hub/bindings.d.ts +8 -1
  85. package/dist/hub/bindings.js +17 -6
  86. package/dist/hub/capability.d.ts +90 -3
  87. package/dist/hub/fake.d.ts +2 -2
  88. package/dist/hub/fake.js +1 -1
  89. package/dist/hub/questionGenerator.d.ts +5 -1
  90. package/dist/hub/questionGenerator.js +8 -6
  91. package/dist/hub/sanitize.js +3 -2
  92. package/dist/index.d.ts +3 -1
  93. package/dist/index.js +4 -1
  94. package/dist/mailbox/dispatch.d.ts +1 -1
  95. package/dist/mailbox/dispatch.js +22 -6
  96. package/dist/mailbox/envelope.d.ts +7 -1
  97. package/dist/mailbox/envelope.js +9 -2
  98. package/dist/mailbox/ipcServer.d.ts +10 -2
  99. package/dist/mailbox/ipcServer.js +163 -13
  100. package/dist/mailbox/store.d.ts +89 -3
  101. package/dist/mailbox/store.js +895 -41
  102. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  103. package/dist/ops/evolutionGraphProjection.js +315 -0
  104. package/dist/ops/index.d.ts +2 -1
  105. package/dist/ops/index.js +2 -1
  106. package/dist/ops/selfUpdate.d.ts +8 -0
  107. package/dist/ops/selfUpdate.js +24 -8
  108. package/dist/personality/schema.d.ts +16 -16
  109. package/dist/schema/evolutionGraph.d.ts +784 -0
  110. package/dist/schema/evolutionGraph.js +187 -0
  111. package/dist/schema/index.d.ts +1 -0
  112. package/dist/schema/index.js +1 -0
  113. package/dist/schema/signal.d.ts +3 -3
  114. package/dist/schema/signal.js +1 -1
  115. package/dist/signals/curriculum.d.ts +55 -0
  116. package/dist/signals/curriculum.js +202 -0
  117. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  118. package/dist/signals/expand.d.ts +15 -1
  119. package/dist/signals/expand.js +169 -1
  120. package/dist/signals/extractor.d.ts +2 -2
  121. package/dist/signals/extractor.js +31 -6
  122. package/dist/signals/index.d.ts +3 -1
  123. package/dist/signals/index.js +3 -1
  124. package/dist/signals/metaSignals.d.ts +4 -0
  125. package/dist/signals/metaSignals.js +42 -0
  126. package/dist/signals/signalGate.js +1 -1
  127. package/dist/signals/taskDomain.d.ts +22 -0
  128. package/dist/signals/taskDomain.js +43 -0
  129. package/dist/strategy/constraintAblation.d.ts +64 -0
  130. package/dist/strategy/constraintAblation.js +2820 -0
  131. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  132. package/dist/strategy/constraintAblationPredicates.js +339 -0
  133. package/dist/strategy/index.d.ts +2 -1
  134. package/dist/strategy/index.js +2 -1
  135. package/dist/trace/index.d.ts +3 -1
  136. package/dist/trace/index.js +3 -1
  137. package/dist/trace/learningTrace.d.ts +209 -0
  138. package/dist/trace/learningTrace.js +276 -0
  139. package/dist/trace/proxyTurns.d.ts +31 -0
  140. package/dist/trace/proxyTurns.js +137 -0
  141. package/dist/verify/sandboxRunner.d.ts +28 -0
  142. package/dist/verify/sandboxRunner.js +218 -19
  143. package/dist/verify/sandboxedValidation.d.ts +9 -0
  144. package/dist/verify/sandboxedValidation.js +113 -13
  145. package/dist/verify/validation.d.ts +11 -1
  146. package/dist/verify/validation.js +31 -0
  147. package/package.json +5 -2
@@ -5,11 +5,35 @@
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 { closeSync, existsSync, fstatSync, openSync, readFileSync, readdirSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
9
10
  export const DEFAULT_TIMEOUT_MS = 600_000;
11
+ export const MAX_AGENT_SESSION_ID_CHARS = 128;
12
+ const NATIVE_SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10
13
  /** Per-stream stdout/stderr capture ceiling. A child can emit indefinitely without growing the parent heap. */
11
14
  export const DEFAULT_MAX_CAPTURE_BYTES = 1_048_576;
12
15
  const MIN_MAX_CAPTURE_BYTES = 256;
16
+ export class AgentSessionResumeError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.code = code;
21
+ this.name = 'AgentSessionResumeError';
22
+ }
23
+ }
24
+ /** Validate before spawn so malformed or cross-harness session targets always fail closed. */
25
+ export function validateAgentSessionResume(resume, expectedRunner) {
26
+ if (resume.runner !== expectedRunner) {
27
+ throw new AgentSessionResumeError('runner_mismatch', `session resume runner '${resume.runner}' does not match selected runner '${expectedRunner}'`);
28
+ }
29
+ if (expectedRunner !== 'claude' && expectedRunner !== 'cursor') {
30
+ throw new AgentSessionResumeError('unsupported_runner', `runner '${expectedRunner}' does not support native session resume`);
31
+ }
32
+ if (!NATIVE_SESSION_ID_PATTERN.test(resume.sessionId)) {
33
+ throw new AgentSessionResumeError('invalid_session_id', `session resume identifier must be ${MAX_AGENT_SESSION_ID_CHARS} characters or fewer and use only letters, numbers, '.', '_', or '-'`);
34
+ }
35
+ return resume;
36
+ }
13
37
  /** Thrown when permission bypass is requested without bounding the agent's tools (would be an unbounded autonomous agent). */
14
38
  export class UnboundedSkipPermissionsError extends Error {
15
39
  constructor() {
@@ -17,6 +41,13 @@ export class UnboundedSkipPermissionsError extends Error {
17
41
  this.name = 'UnboundedSkipPermissionsError';
18
42
  }
19
43
  }
44
+ /** Thrown when Codex permission options cannot be enforced by its CLI. */
45
+ export class UnsupportedCodexPermissionOptionsError extends Error {
46
+ constructor() {
47
+ super('codex runner does not support skipPermissions or allowedTools: the bypass is danger-full-access and Codex has no per-tool allowlist');
48
+ this.name = 'UnsupportedCodexPermissionOptionsError';
49
+ }
50
+ }
20
51
  /** Thrown when Cursor skipPermissions is requested before the runner can enforce per-run permissions. */
21
52
  export class UnsupportedCursorSkipPermissionsError extends Error {
22
53
  constructor() {
@@ -24,6 +55,13 @@ export class UnsupportedCursorSkipPermissionsError extends Error {
24
55
  this.name = 'UnsupportedCursorSkipPermissionsError';
25
56
  }
26
57
  }
58
+ /** Thrown when Cursor workspace trust is requested without verified host containment. */
59
+ export class UnsupportedCursorWorkspaceTrustError extends Error {
60
+ constructor() {
61
+ super('cursor runner does not support workspaceTrust: --trust grants host filesystem and network access that a Git worktree cannot contain');
62
+ this.name = 'UnsupportedCursorWorkspaceTrustError';
63
+ }
64
+ }
27
65
  /** Thrown when Gemini permission options cannot be mapped to a verified bounded CLI contract. */
28
66
  export class UnsupportedGeminiPermissionOptionsError extends Error {
29
67
  constructor() {
@@ -164,6 +202,13 @@ export function killWindowsProcessTree(pid, spawnCommand = spawn, timeoutMs = WI
164
202
  }
165
203
  });
166
204
  }
205
+ /** Thrown when Cursor allowedTools are requested without a verified per-tool CLI allowlist. */
206
+ export class UnsupportedCursorAllowedToolsError extends Error {
207
+ constructor() {
208
+ super('cursor runner does not support allowedTools: cursor-agent has no verified per-run tool allowlist');
209
+ this.name = 'UnsupportedCursorAllowedToolsError';
210
+ }
211
+ }
167
212
  /** A redirected stdout artifact could not be finalized; the subprocess outcome remains available for classification. */
168
213
  export class SpawnCaptureFinalizeError extends Error {
169
214
  result;
@@ -353,7 +398,7 @@ export function spawnCapture(cmd, args, opts) {
353
398
  }
354
399
  let child;
355
400
  try {
356
- child = spawn(r.cmd, r.args, {
401
+ child = (opts.spawnCommand ?? spawn)(r.cmd, r.args, {
357
402
  cwd: opts.cwd,
358
403
  shell: false,
359
404
  detached,
@@ -474,6 +519,43 @@ export function spawnCapture(cmd, args, opts) {
474
519
  }
475
520
  child.stdout?.on('data', (d) => { stdoutCapture.append(d); });
476
521
  child.stderr?.on('data', (d) => { stderrCapture.append(d); });
522
+ const stdinCompletion = opts.input === undefined
523
+ ? Promise.resolve(undefined)
524
+ : new Promise((resolveInput) => {
525
+ const stdin = child.stdin;
526
+ if (!stdin) {
527
+ resolveInput(new Error('runner stdin is unavailable'));
528
+ void killTree();
529
+ return;
530
+ }
531
+ let completed = false;
532
+ const complete = (error) => {
533
+ if (completed)
534
+ return false;
535
+ completed = true;
536
+ resolveInput(error);
537
+ return true;
538
+ };
539
+ stdin.on('error', (error) => {
540
+ if (complete(error instanceof Error ? error : new Error(String(error)))) {
541
+ void killTree();
542
+ }
543
+ });
544
+ stdin.on('finish', () => complete());
545
+ stdin.on('close', () => {
546
+ if (complete(new Error('runner stdin closed before prompt delivery'))) {
547
+ void killTree();
548
+ }
549
+ });
550
+ try {
551
+ stdin.end(opts.input);
552
+ }
553
+ catch (error) {
554
+ if (complete(error instanceof Error ? error : new Error(String(error)))) {
555
+ void killTree();
556
+ }
557
+ }
558
+ });
477
559
  child.on('error', (e) => {
478
560
  void settle(() => {
479
561
  cleanupOwnedStdoutFile();
@@ -481,40 +563,43 @@ export function spawnCapture(cmd, args, opts) {
481
563
  });
482
564
  });
483
565
  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);
566
+ void stdinCompletion.then((inputError) => {
567
+ void settle((stdoutFileError) => {
568
+ const stdout = stdoutCapture.result();
569
+ const stderr = stderrCapture.result();
570
+ const result = {
571
+ code,
572
+ stdout: stdout.text,
573
+ stderr: stderr.text,
574
+ termination,
575
+ stdoutBytes: redirectedStdoutBytes ?? stdout.bytes,
576
+ stderrBytes: stderr.bytes,
577
+ stdoutTruncated: stdout.truncated,
578
+ stderrTruncated: stderr.truncated,
579
+ ...(opts.stdoutFile ? { stdoutRedirected: true } : {}),
580
+ };
581
+ if (stdoutFileError !== undefined) {
582
+ cleanupOwnedStdoutFile();
583
+ reject(new SpawnCaptureFinalizeError(result, stdoutFileError));
584
+ return;
585
+ }
586
+ if (inputError && termination === 'exit' && code === 0) {
587
+ cleanupOwnedStdoutFile();
588
+ reject(inputError);
589
+ return;
590
+ }
591
+ resolve(result);
592
+ });
504
593
  });
505
594
  });
506
- if (opts.input !== undefined) {
507
- child.stdin?.write(opts.input);
508
- child.stdin?.end();
509
- }
510
595
  });
511
596
  }
512
597
  /** Map the shared process result into the failure taxonomy used by plain-text runners. */
513
- export function classifyBasicRunnerResult(runner, result, timeoutMs) {
598
+ export function classifyBasicRunnerResult(runner, result, timeoutMs, resume) {
514
599
  if (result.termination === 'timeout') {
515
600
  return {
516
601
  ok: false,
517
- output: result.stdout,
602
+ output: resume ? '' : result.stdout,
518
603
  error: `${runner} timed out after ${timeoutMs}ms`,
519
604
  failureKind: 'timeout',
520
605
  exitCode: result.code,
@@ -523,17 +608,45 @@ export function classifyBasicRunnerResult(runner, result, timeoutMs) {
523
608
  if (result.termination === 'cancelled') {
524
609
  return {
525
610
  ok: false,
526
- output: result.stdout,
611
+ output: resume ? '' : result.stdout,
527
612
  error: `${runner} execution cancelled`,
528
613
  failureKind: 'cancelled',
529
614
  exitCode: result.code,
530
615
  };
531
616
  }
617
+ // Runner stdout is agent content and may legitimately discuss these errors, including on a later failure.
618
+ const diagnostic = result.stderr;
619
+ if (result.code !== 0 && /permission denied|access denied|not authorized|unauthorized|authentication required|please (?:log|sign) in|login required/i.test(diagnostic)) {
620
+ return {
621
+ ok: false,
622
+ output: resume ? '' : result.stdout,
623
+ error: `${runner} permission denied while executing${resume ? ' resumed session' : ''}`,
624
+ failureKind: 'permission_denied',
625
+ exitCode: result.code,
626
+ };
627
+ }
628
+ const escapedResumeId = resume?.sessionId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
629
+ const contextualMissingSession = escapedResumeId
630
+ ? new RegExp(`(?:session|conversation|chat)\\s+(?:(?:with\\s+)?id[:=]?\\s+)?["']?${escapedResumeId}["']?\\s+(?:was\\s+)?(?:not found|does not exist)`, 'i').test(diagnostic)
631
+ : false;
632
+ if (resume && (contextualMissingSession || /(?:session|conversation|chat) (?:was )?not found|no (?:conversation|session|chat) found|invalid session(?: id)?|unable to resume|cannot resume|(?:session|conversation|chat) does not exist|expired session/i.test(diagnostic))) {
633
+ return {
634
+ ok: false,
635
+ output: '',
636
+ error: `${runner} resume session is missing, stale, or unavailable`,
637
+ failureKind: 'runtime_error',
638
+ exitCode: result.code,
639
+ };
640
+ }
532
641
  if (result.code !== 0) {
642
+ const error = result.stderr || `${runner} exited with code ${String(result.code)}`;
643
+ const safeError = resume
644
+ ? error.replaceAll(resume.sessionId, '[session-id]')
645
+ : error;
533
646
  return {
534
647
  ok: false,
535
648
  output: result.stdout,
536
- error: result.stderr || `${runner} exited with code ${String(result.code)}`,
649
+ error: safeError,
537
650
  failureKind: 'non_zero_exit',
538
651
  exitCode: result.code,
539
652
  };
@@ -549,12 +662,22 @@ function spawnFailureResult(error) {
549
662
  exitCode: null,
550
663
  };
551
664
  }
665
+ export const CLAUDE_SAFE_AUTONOMOUS_TOOLS = ['Read', 'Edit', 'Write', 'Glob', 'Grep'];
666
+ const CLAUDE_SAFE_AUTONOMOUS_TOOL_SET = new Set(CLAUDE_SAFE_AUTONOMOUS_TOOLS);
667
+ export function hasBoundedClaudeFileAccess(opts) {
668
+ return opts?.permissionMode === 'acceptEdits'
669
+ && opts.skipPermissions !== true
670
+ && (opts.allowedTools?.length ?? 0) === 0
671
+ && Array.isArray(opts.tools)
672
+ && opts.tools.length > 0
673
+ && opts.tools.every((tool) => CLAUDE_SAFE_AUTONOMOUS_TOOL_SET.has(tool));
674
+ }
552
675
  /**
553
- * Build the `claude -p` argv for the given options (pure testable without spawning).
676
+ * Build the `claude -p` argv for the given options (pure and testable without spawning).
554
677
  * Safety invariant: skipPermissions (bypassing prompts) is only allowed together with a non-empty
555
- * allowedTools otherwise it would be an unattended agent with full tools and no gate; refuse loudly.
678
+ * allowedTools; otherwise it would be an unattended agent with full tools and no gate; refuse loudly.
556
679
  */
557
- export function claudeRunnerArgs(opts = {}) {
680
+ export function claudeRunnerArgs(opts = {}, resume) {
558
681
  const bounded = !!(opts.allowedTools && opts.allowedTools.length > 0);
559
682
  if (opts.skipPermissions && !bounded)
560
683
  throw new UnboundedSkipPermissionsError();
@@ -563,22 +686,33 @@ export function claudeRunnerArgs(opts = {}) {
563
686
  args.push('--dangerously-skip-permissions');
564
687
  if (opts.allowedTools && opts.allowedTools.length > 0)
565
688
  args.push('--allowedTools', ...opts.allowedTools);
689
+ if (opts.permissionMode)
690
+ args.push('--permission-mode', opts.permissionMode);
691
+ if (opts.tools && opts.tools.length > 0)
692
+ args.push('--tools', opts.tools.join(','));
693
+ if (opts.permissionMode === 'acceptEdits') {
694
+ args.push('--strict-mcp-config', '--disable-slash-commands', '--setting-sources', '');
695
+ }
566
696
  if (opts.model)
567
697
  args.push('--model', opts.model);
698
+ if (resume) {
699
+ validateAgentSessionResume(resume, 'claude');
700
+ args.push('--resume', resume.sessionId);
701
+ }
568
702
  return args;
569
703
  }
570
704
  /**
571
- * Build a headless `claude -p` agent runner. Prompt fed via stdin (no shell, no argv length limit). For
572
- * unattended evolution set { skipPermissions: true, allowedTools: ['Read','Edit','Write'] } bypass the
573
- * permission prompts but bound the agent to file edits. Validated end to end against a real agent.
705
+ * Build a headless `claude -p` agent runner. Prompt is fed via stdin (no shell, no argv length limit).
706
+ * For unattended edits, prefer permissionMode: 'acceptEdits' with the bounded file/search tool list.
574
707
  */
575
708
  export function makeClaudeHeadlessRunner(opts = {}) {
576
709
  const args = claudeRunnerArgs(opts);
577
710
  return async (prompt, ctx) => {
578
711
  const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
712
+ const runArgs = ctx.resume ? claudeRunnerArgs(opts, ctx.resume) : args;
579
713
  try {
580
- const result = await spawnCapture('claude', args, { cwd: ctx.cwd, timeoutMs, input: prompt, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
581
- return classifyBasicRunnerResult('claude', result, timeoutMs);
714
+ const result = await spawnCapture('claude', runArgs, { cwd: ctx.cwd, timeoutMs, input: prompt, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
715
+ return classifyBasicRunnerResult('claude', result, timeoutMs, ctx.resume);
582
716
  }
583
717
  catch (e) {
584
718
  return spawnFailureResult(e);
@@ -589,34 +723,30 @@ export function makeClaudeHeadlessRunner(opts = {}) {
589
723
  export const claudeHeadlessRunner = makeClaudeHeadlessRunner();
590
724
  // --- Codex runner (#66 multi-harness) ---
591
725
  /**
592
- * Build the `codex exec` argv (pure). Verified live against codex-cli 0.137.0:
593
- * - sandboxed default → `exec --sandbox workspace-write`: edits the workspace non-interactively (read-only,
594
- * the codex default, cannot write). The wrapper's worktree + allowedRoots are the outer containment.
595
- * - skipPermissions (bounded) `exec --dangerously-bypass-approvals-and-sandbox`: full bypass, intended for
596
- * an already-externally-sandboxed run (our throwaway worktree). The `skip⇒bounded` invariant is the explicit
597
- * acknowledgement guard, same shape as claude.
726
+ * Build the `codex exec` argv (pure). Verified live against codex-cli 0.144.6:
727
+ * - sandboxed default → `--ask-for-approval never exec --sandbox workspace-write`: edits the workspace
728
+ * without waiting for interactive approval. The wrapper's worktree + allowedRoots are the outer containment.
729
+ * - permission overrides fail closed: Codex has no per-tool allowlist, and a Git worktree does not contain
730
+ * danger-full-access host filesystem or network access.
598
731
  */
599
732
  export function codexRunnerArgs(opts = {}) {
600
- const bounded = !!(opts.allowedTools && opts.allowedTools.length > 0);
601
- if (opts.skipPermissions && !bounded)
602
- throw new UnboundedSkipPermissionsError(); // same invariant as claude skip⇒allowedTools
603
- const args = ['exec'];
604
- // codex bounds via sandbox mode (not a per-tool allowlist): workspace-write is the safe autonomous default;
605
- // the explicit bypass removes the inner sandbox for a run the wrapper already isolates.
606
- args.push('--sandbox', opts.skipPermissions ? 'danger-full-access' : 'workspace-write');
607
- if (opts.skipPermissions)
608
- args.push('--dangerously-bypass-approvals-and-sandbox');
733
+ if (opts.skipPermissions || opts.allowedTools !== undefined) {
734
+ throw new UnsupportedCodexPermissionOptionsError();
735
+ }
736
+ const args = ['--ask-for-approval', 'never', 'exec'];
737
+ args.push('--sandbox', 'workspace-write');
738
+ args.push('--ephemeral');
609
739
  if (opts.model)
610
740
  args.push('--model', opts.model);
611
741
  return args;
612
742
  }
613
- /** Headless `codex exec` runner. Working root pinned with `--cd`; prompt is the trailing positional arg (shell:false). */
614
- export function makeCodexHeadlessRunner(opts = {}) {
743
+ /** Headless `codex exec` runner. Working root pinned with `--cd`; prompt is sent over stdin. */
744
+ export function makeCodexHeadlessRunner(opts = {}, spawnCaptureFn = spawnCapture) {
615
745
  const args = codexRunnerArgs(opts);
616
746
  return async (prompt, ctx) => {
617
747
  const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
618
748
  try {
619
- const result = await spawnCapture('codex', [...args, '--cd', ctx.cwd, prompt], { cwd: ctx.cwd, timeoutMs, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
749
+ const result = await spawnCaptureFn('codex', [...args, '--cd', ctx.cwd, '-'], { cwd: ctx.cwd, timeoutMs, input: prompt, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
620
750
  return classifyBasicRunnerResult('codex', result, timeoutMs);
621
751
  }
622
752
  catch (e) {
@@ -696,21 +826,96 @@ export function classifyGeminiRunnerResult(result, timeoutMs) {
696
826
  export function geminiRunnerArgs(opts = {}) {
697
827
  if (opts.skipPermissions || (opts.allowedTools?.length ?? 0) > 0)
698
828
  throw new UnsupportedGeminiPermissionOptionsError();
699
- const args = ['--output-format', 'json', '--approval-mode', 'auto_edit', '--skip-trust'];
829
+ const args = [
830
+ '--output-format', 'json',
831
+ '--approval-mode', 'auto_edit',
832
+ '--skip-trust',
833
+ '--extensions', 'none',
834
+ '--allowed-mcp-server-names', '__evolver_no_mcp__',
835
+ ];
700
836
  if (opts.model)
701
837
  args.push('--model', opts.model);
702
838
  return args;
703
839
  }
840
+ const GEMINI_AUTH_FILE_MAX_BYTES = 1_048_576;
841
+ function copyGeminiAuthFile(sourceDir, targetDir, name) {
842
+ const source = joinPath(sourceDir, name);
843
+ try {
844
+ const stat = lstatSync(source);
845
+ if (!stat.isFile() || stat.size > GEMINI_AUTH_FILE_MAX_BYTES)
846
+ return;
847
+ const target = joinPath(targetDir, name);
848
+ copyFileSync(source, target);
849
+ chmodSync(target, 0o600);
850
+ }
851
+ catch {
852
+ // Missing or unreadable optional auth state must fail closed in Gemini itself.
853
+ }
854
+ }
855
+ function sanitizedGeminiAuthSettings(sourceDir) {
856
+ try {
857
+ const parsed = JSON.parse(readFileSync(joinPath(sourceDir, 'settings.json'), 'utf8'));
858
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
859
+ return {};
860
+ const security = parsed['security'];
861
+ if (!security || typeof security !== 'object' || Array.isArray(security))
862
+ return {};
863
+ const auth = security['auth'];
864
+ if (!auth || typeof auth !== 'object' || Array.isArray(auth))
865
+ return {};
866
+ const selectedType = auth['selectedType'];
867
+ return typeof selectedType === 'string' && selectedType.length <= 128
868
+ ? { security: { auth: { selectedType } } }
869
+ : {};
870
+ }
871
+ catch {
872
+ return {};
873
+ }
874
+ }
875
+ function isolatedGeminiEnv(env, removeTempDir) {
876
+ const root = mkdtempSync(joinPath(tmpdir(), 'evolver-gemini-'));
877
+ const globalDir = joinPath(root, '.gemini');
878
+ mkdirSync(globalDir, { mode: 0o700 });
879
+ const sourceHome = env['GEMINI_CLI_HOME'] || env['HOME'];
880
+ const sourceDir = sourceHome ? joinPath(sourceHome, '.gemini') : undefined;
881
+ if (sourceDir) {
882
+ copyGeminiAuthFile(sourceDir, globalDir, 'oauth_creds.json');
883
+ copyGeminiAuthFile(sourceDir, globalDir, 'google_accounts.json');
884
+ }
885
+ const authSettings = sourceDir ? sanitizedGeminiAuthSettings(sourceDir) : {};
886
+ writeFileSync(joinPath(globalDir, 'settings.json'), `${JSON.stringify(authSettings)}\n`, { mode: 0o600 });
887
+ const systemSettings = joinPath(root, 'system-settings.json');
888
+ const systemDefaults = joinPath(root, 'system-defaults.json');
889
+ writeFileSync(systemDefaults, '{}\n', { mode: 0o600 });
890
+ writeFileSync(systemSettings, '{"hooksConfig":{"enabled":false},"admin":{"mcp":{"enabled":false}}}\n', { mode: 0o600 });
891
+ const isolatedEnv = { ...env };
892
+ for (const name of Object.keys(isolatedEnv)) {
893
+ if (name.startsWith('GEMINI_CLI_') || name.startsWith('XDG_'))
894
+ delete isolatedEnv[name];
895
+ }
896
+ return {
897
+ env: {
898
+ ...isolatedEnv,
899
+ GEMINI_CLI_HOME: root,
900
+ GEMINI_CLI_SYSTEM_DEFAULTS_PATH: systemDefaults,
901
+ GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettings,
902
+ },
903
+ cleanup: () => removeTempDir(root),
904
+ };
905
+ }
704
906
  /** Headless Gemini runner with structured failure classification; stdout text alone never proves execution success. */
705
- export function makeGeminiHeadlessRunner(opts = {}) {
907
+ export function makeGeminiHeadlessRunner(opts = {}, removeTempDir = (path) => rmSync(path, { recursive: true, force: true })) {
706
908
  const args = geminiRunnerArgs(opts);
707
909
  return async (prompt, ctx) => {
910
+ let cleanup = () => { };
708
911
  try {
709
912
  const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
913
+ const isolated = isolatedGeminiEnv({ ...(ctx.env ?? process.env) }, removeTempDir);
914
+ cleanup = isolated.cleanup;
710
915
  const result = await spawnCapture('gemini', [...args, '--prompt', prompt], {
711
916
  cwd: ctx.cwd,
712
917
  timeoutMs,
713
- ...(ctx.env ? { env: ctx.env } : {}),
918
+ env: isolated.env,
714
919
  ...(ctx.signal ? { signal: ctx.signal } : {}),
715
920
  });
716
921
  return classifyGeminiRunnerResult(result, timeoutMs);
@@ -718,6 +923,14 @@ export function makeGeminiHeadlessRunner(opts = {}) {
718
923
  catch (error) {
719
924
  return { ok: false, output: '', error: error instanceof Error ? error.message : String(error), failureKind: 'spawn_failed', exitCode: null };
720
925
  }
926
+ finally {
927
+ try {
928
+ cleanup();
929
+ }
930
+ catch {
931
+ // Cleanup is best-effort and must not replace the subprocess result.
932
+ }
933
+ }
721
934
  };
722
935
  }
723
936
  // --- Cursor runner (#66 multi-harness) ---
@@ -731,47 +944,69 @@ export function makeGeminiHeadlessRunner(opts = {}) {
731
944
  // - `--model <model>` exists (e.g. gpt-5, sonnet-4, sonnet-4-thinking); `--list-models` enumerates.
732
945
  // - auth: `CURSOR_API_KEY` or `--api-key` (the spec's envAllow prefix is CURSOR_).
733
946
  // - cursor has its OWN `--sandbox enabled|disabled` and `-w/--worktree`; we still wrap with our git worktree.
734
- // STILL NOT run-verified end to end (needs an authed cursor-agent; claude/codex were each run-verified). The
735
- // Windows launcher is a PowerShell shim and its own version regex rejects the current timestamped version-dir
736
- // shape. resolveSpawnCommand therefore bypasses both scripts and runs the newest verified node.exe + index.js
737
- // bundle directly, shell-free. If that known bundle layout cannot be found, the runner still fail-fasts.
947
+ // Bundle and auth preflight verified on Windows with Cursor Agent 2026.06.15. The Windows launcher is a
948
+ // PowerShell shim and its own version regex rejects the current timestamped version-dir shape. resolveSpawnCommand
949
+ // therefore bypasses both scripts and runs the newest verified node.exe + index.js bundle directly, shell-free.
950
+ // If that known bundle layout cannot be found, the runner still fail-fasts.
738
951
  //
739
- // SAFETY: `-p --force --trust` auto-approves shell+write with no verified per-run allowlist/sandbox mapping.
740
- // Until Cursor can really map agentOptions into per-run permissions, skipPermissions is refused outright. The
741
- // wrapper worktree still contains default cursor runs, but it is not a permissions/sandbox substitute for skip.
742
- // Safe default keeps skip OFF (CURSOR_DEFAULT_AGENT_OPTIONS), so the wiring is exercised by tests with fakes.
952
+ // SAFETY: `--trust`, `--force`, permission bypass, and per-tool allowlists remain fail-closed. The worktree is a
953
+ // measurement/patch-containment boundary, not an OS/network sandbox.
743
954
  /**
744
955
  * Build the `cursor-agent` argv (pure). Ground-truth from `cursor-agent --help` (#66): base `-p --output-format
745
956
  * text` (headless, write+shell access). `--model` is a real flag. skipPermissions is rejected until Cursor has a
746
957
  * verified per-run allowlist/sandbox mapping; allowedTools is not emitted because cursor has no per-tool allowlist.
747
958
  */
748
- export function cursorRunnerArgs(opts = {}) {
959
+ export function cursorRunnerArgs(opts = {}, resume, managedWorktreeName) {
749
960
  if (opts.skipPermissions)
750
961
  throw new UnsupportedCursorSkipPermissionsError();
962
+ if (opts.workspaceTrust !== undefined)
963
+ throw new UnsupportedCursorWorkspaceTrustError();
964
+ if (opts.allowedTools !== undefined) {
965
+ throw new UnsupportedCursorAllowedToolsError();
966
+ }
751
967
  const args = ['-p', '--output-format', 'text'];
752
968
  if (opts.model)
753
969
  args.push('--model', opts.model);
970
+ if (resume) {
971
+ validateAgentSessionResume(resume, 'cursor');
972
+ args.push('--resume', resume.sessionId);
973
+ }
974
+ if (managedWorktreeName) {
975
+ if (!NATIVE_SESSION_ID_PATTERN.test(managedWorktreeName)) {
976
+ throw new AgentSessionResumeError('invalid_session_id', 'managed worktree name is invalid');
977
+ }
978
+ args.push('--worktree', managedWorktreeName, '--skip-worktree-setup');
979
+ }
754
980
  return args;
755
981
  }
982
+ function cursorManagedWorktreePath(stdout) {
983
+ const match = /^Using worktree: (.+)$/m.exec(stdout);
984
+ return match?.[1]?.trim();
985
+ }
756
986
  /**
757
987
  * Headless `cursor-agent` runner. Prompt passed as the trailing positional arg (shell:false, no injection risk;
758
- * docs show `cursor-agent -p "<prompt>"`). cwd is set via spawn. SCAFFOLD run-verify against a real
759
- * cursor-agent before autonomous use (see the block comment above for what is doc-confirmed vs unverified).
988
+ * docs show `cursor-agent -p "<prompt>"`). cwd is set via spawn. Workspace trust must be certified by the
989
+ * bridge refuses built-in autonomous Cursor until host containment is verified.
760
990
  */
761
991
  export function makeCursorHeadlessRunner(opts = {}, platform = process.platform) {
762
992
  const args = cursorRunnerArgs(opts);
763
993
  return async (prompt, ctx) => {
994
+ const runArgs = ctx.resume || ctx.managedWorktreeName
995
+ ? cursorRunnerArgs(opts, ctx.resume, ctx.managedWorktreeName)
996
+ : args;
764
997
  assertCursorRunnerPlatformSupported(platform, ctx.env ?? process.env);
765
998
  const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
766
999
  try {
767
- const result = await spawnCapture('cursor-agent', [...args, prompt], {
1000
+ const result = await spawnCapture('cursor-agent', [...runArgs, prompt], {
768
1001
  cwd: ctx.cwd,
769
1002
  timeoutMs,
770
1003
  resolvePlatform: platform,
771
1004
  ...(ctx.env ? { env: ctx.env } : {}),
772
1005
  ...(ctx.signal ? { signal: ctx.signal } : {}),
773
1006
  });
774
- return classifyBasicRunnerResult('cursor', result, timeoutMs);
1007
+ const classified = classifyBasicRunnerResult('cursor', result, timeoutMs, ctx.resume);
1008
+ const managedWorktreePath = cursorManagedWorktreePath(`${result.stdout}\n${result.stderr}`);
1009
+ return managedWorktreePath ? { ...classified, managedWorktreePath } : classified;
775
1010
  }
776
1011
  catch (e) {
777
1012
  return spawnFailureResult(e);
@@ -6,15 +6,9 @@
6
6
  // 3. leak scan of the diff before any push 6. cooldown + diff dedup
7
7
  // evaluateSelfPr is PURE (decision only); the `gh` call is an injected GhRunner seam so a test never creates a
8
8
  // real PR. Nothing here runs unless the caller has wired enabled:true AND a non-empty allowedRoots.
9
- import { resolve as resolvePath, sep } from 'node:path';
9
+ import { isWithinRoot } from './claudeBridge.js';
10
10
  import { defaultReadManifest, isObfuscatedFile } from './selfPrObfuscation.js';
11
11
  export const DEFAULT_SELF_PR_GATES = { minScore: 0.9, minStreak: 2, maxFiles: 5, maxLines: 200, cooldownMs: 24 * 60 * 60 * 1000 };
12
- /** Whether `child` is the same as or nested under `root` (both resolved absolute). */
13
- function isWithinRoot(child, root) {
14
- const c = resolvePath(child);
15
- const r = resolvePath(root);
16
- return c === r || c.startsWith(r.endsWith(sep) ? r : r + sep);
17
- }
18
12
  /**
19
13
  * Decide whether a self-PR may be created. Pure — combines every gate and returns the FIRST failing reason, so
20
14
  * the default (disabled, empty allowlist) is always a clean refusal that runs nothing.
@@ -0,0 +1,61 @@
1
+ export declare const PRIORITY_AXES: readonly ["task_success", "user_preference", "quality", "safety", "cost", "latency", "other"];
2
+ export declare const LABELS: readonly ["positive", "negative", "mixed", "neutral"];
3
+ export declare const ATTENTION_LEVELS: readonly ["full", "limited", "skimmed", "unknown"];
4
+ export declare const EVIDENCE_KINDS: readonly ["evolution_event", "evolution_outcome", "user_override", "review", "turn", "external"];
5
+ export type FeedbackPriorityAxis = typeof PRIORITY_AXES[number];
6
+ export type FeedbackLabel = typeof LABELS[number];
7
+ export type EvaluatorAttentionLevel = typeof ATTENTION_LEVELS[number];
8
+ export type FeedbackEvidenceKind = typeof EVIDENCE_KINDS[number];
9
+ export interface EvaluatorAttention {
10
+ level: EvaluatorAttentionLevel;
11
+ observed_items?: number;
12
+ elapsed_ms?: number;
13
+ }
14
+ export interface FeedbackEvidenceRef {
15
+ kind: FeedbackEvidenceKind;
16
+ id: string;
17
+ summary?: string;
18
+ }
19
+ export interface FeedbackEnvelope {
20
+ priority_axis: FeedbackPriorityAxis;
21
+ label: FeedbackLabel;
22
+ scalar: number;
23
+ indecision: boolean;
24
+ conflict: boolean;
25
+ evaluator_attention: EvaluatorAttention;
26
+ evidence_ref: FeedbackEvidenceRef;
27
+ uncertainty: number;
28
+ }
29
+ export interface FeedbackAggregate {
30
+ dominant_label: 'positive' | 'negative' | null;
31
+ uncertainty: number;
32
+ sample_count: number;
33
+ }
34
+ export interface FeedbackEnvelopeInput {
35
+ priority_axis?: unknown;
36
+ priorityAxis?: unknown;
37
+ scalar?: unknown;
38
+ indecision?: unknown;
39
+ conflict?: unknown;
40
+ evaluator_attention?: unknown;
41
+ evaluatorAttention?: unknown;
42
+ evidence_ref?: unknown;
43
+ evidenceRef?: unknown;
44
+ }
45
+ export interface FeedbackOutcomeLike {
46
+ score?: unknown;
47
+ user_override?: unknown;
48
+ }
49
+ export declare function clamp01(value: unknown): number;
50
+ export declare function labelFromScalar(value: unknown): FeedbackLabel;
51
+ export declare function normalizeAttention(input: unknown): EvaluatorAttention;
52
+ export declare function evidenceRef(kind: unknown, id: unknown, options?: {
53
+ summary?: unknown;
54
+ }): FeedbackEvidenceRef;
55
+ export declare function normalizeEvidenceRef(input: unknown): FeedbackEvidenceRef;
56
+ export declare function envelopeUncertainty(scalar: unknown, attentionLevel: EvaluatorAttentionLevel, indecision: boolean, conflict: boolean): number;
57
+ export declare function fromScalarFeedback(options?: FeedbackEnvelopeInput | null): FeedbackEnvelope;
58
+ export declare function fromOutcomeScalar(outcome: unknown, options?: Omit<FeedbackEnvelopeInput, 'scalar'>): FeedbackEnvelope | null;
59
+ export declare function withConflict(envelope: FeedbackEnvelope): FeedbackEnvelope;
60
+ export declare function withIndecision(envelope: FeedbackEnvelope): FeedbackEnvelope;
61
+ export declare function aggregateFeedbackEnvelopes(envelopes: readonly FeedbackEnvelope[] | null | undefined): FeedbackAggregate;