@evomap/evolver-core 2.0.0-beta.1 → 2.0.0-beta.3

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 (110) hide show
  1. package/assets/gep/genes.jsonl +5 -0
  2. package/dist/algo/candidateAssembly.js +23 -14
  3. package/dist/algo/capabilityCandidates.d.ts +2 -0
  4. package/dist/algo/capabilityCandidates.js +5 -0
  5. package/dist/algo/conversationSniffer.d.ts +18 -0
  6. package/dist/algo/conversationSniffer.js +132 -0
  7. package/dist/algo/cycleEngine.d.ts +11 -0
  8. package/dist/algo/cycleEngine.js +14 -6
  9. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  10. package/dist/algo/cycleFailureClassifier.js +13 -5
  11. package/dist/algo/geneIntake.d.ts +14 -4
  12. package/dist/algo/geneIntake.js +37 -9
  13. package/dist/algo/geneSelection.d.ts +18 -1
  14. package/dist/algo/geneSelection.js +42 -18
  15. package/dist/algo/index.d.ts +2 -0
  16. package/dist/algo/index.js +2 -0
  17. package/dist/algo/memoryGraph.d.ts +62 -0
  18. package/dist/algo/memoryGraph.js +86 -0
  19. package/dist/algo/orchestrator.d.ts +3 -0
  20. package/dist/algo/orchestrator.js +14 -2
  21. package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
  22. package/dist/assetstore/assetSidecarRecords.js +142 -0
  23. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  24. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  25. package/dist/assetstore/assetStoreHealth.d.ts +75 -0
  26. package/dist/assetstore/assetStoreHealth.js +277 -0
  27. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  28. package/dist/assetstore/assetStoreLayout.js +6 -0
  29. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  30. package/dist/assetstore/assetStoreStorage.js +318 -0
  31. package/dist/assetstore/assetSyncLedger.d.ts +32 -0
  32. package/dist/assetstore/assetSyncLedger.js +66 -0
  33. package/dist/assetstore/index.d.ts +5 -1
  34. package/dist/assetstore/index.js +5 -1
  35. package/dist/assetstore/localJsonl.d.ts +10 -2
  36. package/dist/assetstore/localJsonl.js +93 -37
  37. package/dist/assetstore/pendingSignals.js +3 -1
  38. package/dist/assetstore/provenance.d.ts +20 -2
  39. package/dist/assetstore/provenance.js +92 -56
  40. package/dist/assetstore/provider.d.ts +2 -0
  41. package/dist/assetstore/reviewFilter.js +5 -2
  42. package/dist/assetstore/reviewLedger.d.ts +14 -2
  43. package/dist/assetstore/reviewLedger.js +78 -43
  44. package/dist/assetstore/seedGenes.d.ts +3 -0
  45. package/dist/assetstore/seedGenes.js +134 -0
  46. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  47. package/dist/benchmark/antiGeneBenchmark.js +11 -1
  48. package/dist/benchmark/antiGeneImpact.d.ts +16 -0
  49. package/dist/benchmark/antiGeneImpact.js +34 -0
  50. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  51. package/dist/benchmark/antiGeneRollout.js +13 -1
  52. package/dist/benchmark/index.d.ts +2 -1
  53. package/dist/benchmark/index.js +2 -1
  54. package/dist/benchmark/triggerShift.d.ts +62 -0
  55. package/dist/benchmark/triggerShift.js +106 -0
  56. package/dist/events/eventArchive.d.ts +65 -0
  57. package/dist/events/eventArchive.js +343 -0
  58. package/dist/events/eventStore.js +2 -12
  59. package/dist/events/ingest.d.ts +1 -1
  60. package/dist/events/ingest.js +2 -0
  61. package/dist/events/paths.d.ts +1 -1
  62. package/dist/events/paths.js +2 -2
  63. package/dist/events/public.d.ts +3 -1
  64. package/dist/events/public.js +2 -1
  65. package/dist/events/retention.d.ts +24 -1
  66. package/dist/events/retention.js +133 -37
  67. package/dist/exec/autoExec.d.ts +6 -1
  68. package/dist/exec/autoExec.js +34 -0
  69. package/dist/exec/autonomousCycle.d.ts +2 -0
  70. package/dist/exec/autonomousCycle.js +5 -0
  71. package/dist/exec/claudeBridge.d.ts +7 -2
  72. package/dist/exec/claudeBridge.js +115 -14
  73. package/dist/exec/prompt.js +9 -0
  74. package/dist/exec/runnerRegistry.d.ts +64 -18
  75. package/dist/exec/runnerRegistry.js +341 -38
  76. package/dist/exec/selfPrObfuscation.d.ts +2 -1
  77. package/dist/exec/selfPrObfuscation.js +19 -6
  78. package/dist/hub/agentDirectory.d.ts +90 -0
  79. package/dist/hub/agentDirectory.js +104 -0
  80. package/dist/hub/bindings.js +23 -3
  81. package/dist/hub/capability.d.ts +14 -2
  82. package/dist/hub/fake.d.ts +4 -2
  83. package/dist/hub/fake.js +3 -2
  84. package/dist/hub/index.d.ts +1 -0
  85. package/dist/hub/index.js +1 -0
  86. package/dist/mailbox/catalog.js +3 -0
  87. package/dist/mailbox/ipcServer.js +2 -2
  88. package/dist/mailbox/store.d.ts +14 -0
  89. package/dist/mailbox/store.js +44 -2
  90. package/dist/material/consumer.js +9 -6
  91. package/dist/material/index.d.ts +1 -0
  92. package/dist/material/index.js +1 -0
  93. package/dist/material/materialArchive.d.ts +81 -0
  94. package/dist/material/materialArchive.js +466 -0
  95. package/dist/material/materialStore.d.ts +1 -0
  96. package/dist/material/materialStore.js +6 -13
  97. package/dist/ops/savingsCore.js +1 -2
  98. package/dist/ops/selfUpdate.d.ts +10 -1
  99. package/dist/ops/selfUpdate.js +64 -15
  100. package/dist/schema/common.d.ts +1 -1
  101. package/dist/schema/common.js +1 -1
  102. package/dist/schema/material.d.ts +5 -5
  103. package/dist/trace/trajectoryExport.js +2 -2
  104. package/dist/util/fileLock.d.ts +19 -2
  105. package/dist/util/fileLock.js +166 -31
  106. package/dist/wire/geneHints.d.ts +42 -1
  107. package/dist/wire/geneHints.js +52 -1
  108. package/dist/wire/index.d.ts +14 -2
  109. package/dist/wire/index.js +1 -1
  110. package/package.json +6 -1
@@ -2,6 +2,8 @@ export declare const DEFAULT_TIMEOUT_MS = 600000;
2
2
  export interface AgentRunContext {
3
3
  cwd: string;
4
4
  timeoutMs?: number;
5
+ /** Cooperative cancellation. The runner kills the whole spawned process tree when aborted. */
6
+ signal?: AbortSignal;
5
7
  /** Environment for the spawned agent. The bridge passes a scrubbed env here (see scrubAgentEnv); undefined → inherit. */
6
8
  env?: NodeJS.ProcessEnv;
7
9
  }
@@ -9,6 +11,8 @@ export interface AgentRunResult {
9
11
  ok: boolean;
10
12
  output: string;
11
13
  error?: string;
14
+ failureKind?: 'spawn_failed' | 'timeout' | 'cancelled' | 'permission_denied' | 'non_zero_exit' | 'invalid_output' | 'runtime_error';
15
+ exitCode?: number | null;
12
16
  }
13
17
  /** Run a coding agent against a working directory with the given instruction. */
14
18
  export type AgentRunner = (prompt: string, ctx: AgentRunContext) => Promise<AgentRunResult>;
@@ -20,40 +24,77 @@ export declare class UnboundedSkipPermissionsError extends Error {
20
24
  export declare class UnsupportedCursorSkipPermissionsError extends Error {
21
25
  constructor();
22
26
  }
23
- /** Thrown when Cursor's built-in runner is requested on Windows before its launcher path is run-verified. */
27
+ /** Thrown when Gemini permission options cannot be mapped to a verified bounded CLI contract. */
28
+ export declare class UnsupportedGeminiPermissionOptionsError extends Error {
29
+ constructor();
30
+ }
31
+ /** Thrown when Cursor's Windows installation cannot be reduced to a shell-free node.exe + index.js launch. */
24
32
  export declare class UnsupportedCursorWindowsRunnerError extends Error {
25
33
  constructor();
26
34
  }
27
- export declare function assertCursorRunnerPlatformSupported(platform?: NodeJS.Platform): void;
35
+ export declare function assertCursorRunnerPlatformSupported(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): void;
28
36
  /**
29
37
  * Make a bare command name spawnable shell-free on Windows. `spawn(shell:false)` cannot execute an npm CLI
30
38
  * shim (a `.cmd`/`.bat`), and routing through a shell would expose the prompt arg to cmd.exe quoting
31
39
  * (injection). npm shims are node wrappers, so we resolve the bare name on PATH and, when it's a node shim,
32
- * run `node <entry.js>` directly (shell-free, args passed safely). A native `.exe` (claude) resolves to itself.
33
- * No-op on POSIX and for any command that is already a path or has an extension. Surfaced by codex on Windows
34
- * (codex is `codex.cmd`, while claude is `claude.exe`), #66.
40
+ * run `node <entry.js>` directly. Cursor's installer uses a PowerShell shim around a bundled
41
+ * `versions/<version>/node.exe + index.js`; that known layout is resolved directly too, without invoking
42
+ * cmd.exe or PowerShell. A native `.exe` (claude) resolves to itself. No-op on POSIX and for any command that
43
+ * is already a path or has an extension. Surfaced by codex/cursor-agent on Windows (#66).
35
44
  */
36
- export declare function resolveSpawnCommand(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv): {
45
+ export declare function resolveSpawnCommand(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): {
37
46
  cmd: string;
38
47
  args: string[];
39
48
  };
40
- /**
41
- * Promise wrapper over spawn (shell:false). Optionally writes `input` to stdin; resolves with stdout/exit.
42
- * On timeout the WHOLE process group is killed, not just the direct child (finding #39.5): an agent spawns
43
- * tool subprocesses (grandchildren) that would otherwise orphan and leak. On POSIX we spawn detached (the
44
- * child becomes its own group leader) and SIGKILL the group via the negative pid; Windows falls back to a
45
- * direct kill (different process-group semantics).
46
- */
47
- export declare function spawnCapture(cmd: string, args: readonly string[], opts: {
49
+ export interface WindowsTreeKillCommand {
50
+ command: 'taskkill.exe';
51
+ args: ['/PID', string, '/T', '/F'];
52
+ }
53
+ export interface WindowsTreeKillChild {
54
+ once(event: 'error', listener: (error: Error) => void): this;
55
+ once(event: 'close', listener: (code: number | null) => void): this;
56
+ kill?(signal?: NodeJS.Signals | number): boolean;
57
+ }
58
+ export type WindowsTreeKillSpawn = (command: string, args: readonly string[], options: {
59
+ shell: false;
60
+ windowsHide: true;
61
+ stdio: 'ignore';
62
+ }) => WindowsTreeKillChild;
63
+ type WindowsProcessTreeKiller = (pid: number) => Promise<boolean>;
64
+ /** Build the shell-free taskkill invocation used for Windows process-tree termination. */
65
+ export declare function windowsTreeKillCommand(pid: number): WindowsTreeKillCommand;
66
+ /** Run taskkill and report whether Windows accepted the process-tree termination request. */
67
+ export declare function killWindowsProcessTree(pid: number, spawnCommand?: WindowsTreeKillSpawn, timeoutMs?: number): Promise<boolean>;
68
+ export interface SpawnCaptureOptions {
48
69
  cwd: string;
49
70
  timeoutMs: number;
50
71
  input?: string;
51
72
  env?: NodeJS.ProcessEnv;
52
- }): Promise<{
73
+ signal?: AbortSignal;
74
+ /** Cleanup subprocesses can shield themselves from repeated SIGINT/SIGTERM instead of cancelling. */
75
+ processSignalMode?: 'cancel' | 'ignore';
76
+ resolvePlatform?: NodeJS.Platform;
77
+ /** Test seam for Windows process behavior; production callers should use the default. */
78
+ processPlatform?: NodeJS.Platform;
79
+ /** Test seam for the shell-free Windows taskkill invocation. */
80
+ windowsProcessTreeKiller?: WindowsProcessTreeKiller;
81
+ }
82
+ export interface SpawnCaptureResult {
53
83
  code: number | null;
54
84
  stdout: string;
55
85
  stderr: string;
56
- }>;
86
+ termination: 'exit' | 'timeout' | 'cancelled';
87
+ }
88
+ /**
89
+ * Promise wrapper over spawn (shell:false). Optionally writes `input` to stdin; resolves with stdout/exit.
90
+ * On timeout the WHOLE process group is killed, not just the direct child (finding #39.5): an agent spawns
91
+ * tool subprocesses (grandchildren) that would otherwise orphan and leak. On POSIX we spawn detached (the
92
+ * child becomes its own group leader) and SIGKILL the group via the negative pid. Windows runs
93
+ * `taskkill.exe /PID <pid> /T /F` without a shell and waits for that command before resolving.
94
+ */
95
+ export declare function spawnCapture(cmd: string, args: readonly string[], opts: SpawnCaptureOptions): Promise<SpawnCaptureResult>;
96
+ /** Map the shared process result into the failure taxonomy used by plain-text runners. */
97
+ export declare function classifyBasicRunnerResult(runner: 'claude' | 'codex' | 'cursor', result: SpawnCaptureResult, timeoutMs: number): AgentRunResult;
57
98
  /** Options for a built-in headless runner (claude / codex share the shape and the skip⇒bounded invariant). */
58
99
  export interface AgentRunnerOptions {
59
100
  /** Bypass permission prompts so the agent can edit autonomously (required for unattended use). Default off.
@@ -95,6 +136,10 @@ export declare const claudeHeadlessRunner: AgentRunner;
95
136
  export declare function codexRunnerArgs(opts?: AgentRunnerOptions): string[];
96
137
  /** Headless `codex exec` runner. Working root pinned with `--cd`; prompt is the trailing positional arg (shell:false). */
97
138
  export declare function makeCodexHeadlessRunner(opts?: AgentRunnerOptions): AgentRunner;
139
+ /** Build verified Gemini CLI argv. The prompt is appended separately as one argv element with shell:false. */
140
+ export declare function geminiRunnerArgs(opts?: AgentRunnerOptions): string[];
141
+ /** Headless Gemini runner with structured failure classification; stdout text alone never proves execution success. */
142
+ export declare function makeGeminiHeadlessRunner(opts?: AgentRunnerOptions): AgentRunner;
98
143
  /**
99
144
  * Build the `cursor-agent` argv (pure). Ground-truth from `cursor-agent --help` (#66): base `-p --output-format
100
145
  * text` (headless, write+shell access). `--model` is a real flag. skipPermissions is rejected until Cursor has a
@@ -108,7 +153,7 @@ export declare function cursorRunnerArgs(opts?: AgentRunnerOptions): string[];
108
153
  */
109
154
  export declare function makeCursorHeadlessRunner(opts?: AgentRunnerOptions, platform?: NodeJS.Platform): AgentRunner;
110
155
  /** A built-in coding-agent harness (#66). cursor is a SCAFFOLD — its runner is unverified (see cursorRunnerArgs). */
111
- export type RunnerName = 'claude' | 'codex' | 'cursor';
156
+ export type RunnerName = 'claude' | 'codex' | 'cursor' | 'gemini';
112
157
  /** A harness runner: how to launch it + which env auth prefixes it (and ONLY it) may keep (#66). */
113
158
  export interface AgentRunnerSpec {
114
159
  name: RunnerName;
@@ -120,4 +165,5 @@ export interface AgentRunnerSpec {
120
165
  };
121
166
  }
122
167
  /** Resolve a runner spec by name (default 'claude' — byte-identical to the pre-registry behavior). */
123
- export declare function getRunnerSpec(name?: RunnerName): AgentRunnerSpec;
168
+ export declare function getRunnerSpec(name?: RunnerName): AgentRunnerSpec;
169
+ export {};
@@ -5,7 +5,7 @@
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 } from 'node:fs';
8
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
9
9
  export const DEFAULT_TIMEOUT_MS = 600_000;
10
10
  /** Thrown when permission bypass is requested without bounding the agent's tools (would be an unbounded autonomous agent). */
11
11
  export class UnboundedSkipPermissionsError extends Error {
@@ -21,29 +21,41 @@ export class UnsupportedCursorSkipPermissionsError extends Error {
21
21
  this.name = 'UnsupportedCursorSkipPermissionsError';
22
22
  }
23
23
  }
24
- /** Thrown when Cursor's built-in runner is requested on Windows before its launcher path is run-verified. */
24
+ /** Thrown when Gemini permission options cannot be mapped to a verified bounded CLI contract. */
25
+ export class UnsupportedGeminiPermissionOptionsError extends Error {
26
+ constructor() {
27
+ super('gemini runner does not support skipPermissions or allowedTools: --yolo is unbounded and --allowed-tools is deprecated; the verified runner uses --approval-mode auto_edit only');
28
+ this.name = 'UnsupportedGeminiPermissionOptionsError';
29
+ }
30
+ }
31
+ /** Thrown when Cursor's Windows installation cannot be reduced to a shell-free node.exe + index.js launch. */
25
32
  export class UnsupportedCursorWindowsRunnerError extends Error {
26
33
  constructor() {
27
- super('cursor runner on Windows is not yet supported/run-verified due to cursor-agent shim spawn issue; use claude/codex or an injected cursor runner until the Windows launcher is run-verified');
34
+ super('cursor runner on Windows could not resolve the installed cursor-agent bundle shell-free; reinstall/update cursor-agent or use claude/codex until node.exe + index.js are available');
28
35
  this.name = 'UnsupportedCursorWindowsRunnerError';
29
36
  }
30
37
  }
31
- export function assertCursorRunnerPlatformSupported(platform = process.platform) {
32
- if (platform === 'win32')
38
+ export function assertCursorRunnerPlatformSupported(platform = process.platform, env = process.env) {
39
+ if (platform !== 'win32')
40
+ return;
41
+ if (resolveSpawnCommand('cursor-agent', [], env, platform).cmd === 'cursor-agent') {
33
42
  throw new UnsupportedCursorWindowsRunnerError();
43
+ }
34
44
  }
35
45
  /**
36
46
  * Make a bare command name spawnable shell-free on Windows. `spawn(shell:false)` cannot execute an npm CLI
37
47
  * shim (a `.cmd`/`.bat`), and routing through a shell would expose the prompt arg to cmd.exe quoting
38
48
  * (injection). npm shims are node wrappers, so we resolve the bare name on PATH and, when it's a node shim,
39
- * run `node <entry.js>` directly (shell-free, args passed safely). A native `.exe` (claude) resolves to itself.
40
- * No-op on POSIX and for any command that is already a path or has an extension. Surfaced by codex on Windows
41
- * (codex is `codex.cmd`, while claude is `claude.exe`), #66.
49
+ * run `node <entry.js>` directly. Cursor's installer uses a PowerShell shim around a bundled
50
+ * `versions/<version>/node.exe + index.js`; that known layout is resolved directly too, without invoking
51
+ * cmd.exe or PowerShell. A native `.exe` (claude) resolves to itself. No-op on POSIX and for any command that
52
+ * is already a path or has an extension. Surfaced by codex/cursor-agent on Windows (#66).
42
53
  */
43
- export function resolveSpawnCommand(cmd, args, env = process.env) {
44
- if (process.platform !== 'win32' || /[\\/]/.test(cmd) || /\.[a-z0-9]+$/i.test(cmd))
54
+ export function resolveSpawnCommand(cmd, args, env = process.env, platform = process.platform) {
55
+ if (platform !== 'win32' || /[\\/]/.test(cmd) || /\.[a-z0-9]+$/i.test(cmd))
45
56
  return { cmd, args: [...args] };
46
- for (const dir of (env['PATH'] ?? '').split(pathDelimiter).filter(Boolean)) {
57
+ const delimiter = platform === 'win32' ? ';' : pathDelimiter;
58
+ for (const dir of (env['PATH'] ?? '').split(delimiter).filter(Boolean)) {
47
59
  for (const ext of ['.exe', '.cmd', '.bat']) {
48
60
  const full = joinPath(dir, cmd + ext);
49
61
  if (!existsSync(full))
@@ -58,6 +70,11 @@ export function resolveSpawnCommand(cmd, args, env = process.env) {
58
70
  if (existsSync(js))
59
71
  return { cmd: process.execPath, args: [js, ...args] };
60
72
  }
73
+ if (cmd.toLowerCase() === 'cursor-agent' && /cursor-agent\.ps1/i.test(shim)) {
74
+ const cursor = resolveCursorAgentBundle(dir, args);
75
+ if (cursor)
76
+ return cursor;
77
+ }
61
78
  }
62
79
  catch { /* unreadable shim — keep searching */ }
63
80
  // A .cmd/.bat whose node entry we can't extract is NOT runnable shell-free — DON'T return it (that would
@@ -66,41 +83,228 @@ export function resolveSpawnCommand(cmd, args, env = process.env) {
66
83
  }
67
84
  return { cmd, args: [...args] };
68
85
  }
86
+ function resolveCursorAgentBundle(dir, args) {
87
+ const direct = cursorBundleAt(dir, args);
88
+ if (direct)
89
+ return direct;
90
+ let versions;
91
+ try {
92
+ versions = readdirSync(joinPath(dir, 'versions'), { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory() && cursorVersionKey(entry.name) !== undefined)
94
+ .map((entry) => entry.name)
95
+ .sort((a, b) => cursorVersionKey(b).localeCompare(cursorVersionKey(a)));
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ for (const version of versions) {
101
+ const resolved = cursorBundleAt(joinPath(dir, 'versions', version), args);
102
+ if (resolved)
103
+ return resolved;
104
+ }
105
+ return undefined;
106
+ }
107
+ function cursorBundleAt(dir, args) {
108
+ const node = joinPath(dir, 'node.exe');
109
+ const entry = joinPath(dir, 'index.js');
110
+ return existsSync(node) && existsSync(entry) ? { cmd: node, args: [entry, ...args] } : undefined;
111
+ }
112
+ function cursorVersionKey(version) {
113
+ const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:-(\d{1,2})-(\d{1,2})-(\d{1,2}))?-[a-f0-9]+$/i.exec(version);
114
+ if (!match)
115
+ return undefined;
116
+ const year = match[1];
117
+ const month = match[2];
118
+ const day = match[3];
119
+ const hour = match[4] ?? '0';
120
+ const minute = match[5] ?? '0';
121
+ const second = match[6] ?? '0';
122
+ return [year, month, day, hour, minute, second].map((part, index) => index === 0 ? part : part.padStart(2, '0')).join('');
123
+ }
124
+ const WINDOWS_TREE_KILL_TIMEOUT_MS = 5_000;
125
+ /** Build the shell-free taskkill invocation used for Windows process-tree termination. */
126
+ export function windowsTreeKillCommand(pid) {
127
+ if (!Number.isSafeInteger(pid) || pid <= 0)
128
+ throw new RangeError(`invalid process id: ${pid}`);
129
+ return { command: 'taskkill.exe', args: ['/PID', String(pid), '/T', '/F'] };
130
+ }
131
+ /** Run taskkill and report whether Windows accepted the process-tree termination request. */
132
+ export function killWindowsProcessTree(pid, spawnCommand = spawn, timeoutMs = WINDOWS_TREE_KILL_TIMEOUT_MS) {
133
+ const { command, args } = windowsTreeKillCommand(pid);
134
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
135
+ throw new RangeError(`invalid taskkill timeout: ${timeoutMs}`);
136
+ return new Promise((resolve) => {
137
+ let settled = false;
138
+ let killer;
139
+ const finish = (ok, terminateKiller = false) => {
140
+ if (settled)
141
+ return;
142
+ settled = true;
143
+ clearTimeout(timer);
144
+ if (terminateKiller) {
145
+ try {
146
+ killer?.kill?.('SIGKILL');
147
+ }
148
+ catch { /* best-effort watchdog cleanup */ }
149
+ }
150
+ resolve(ok);
151
+ };
152
+ const timer = setTimeout(() => finish(false, true), timeoutMs);
153
+ timer.unref?.();
154
+ try {
155
+ killer = spawnCommand(command, args, { shell: false, windowsHide: true, stdio: 'ignore' });
156
+ killer.once('error', () => finish(false));
157
+ killer.once('close', (code) => finish(code === 0));
158
+ }
159
+ catch {
160
+ finish(false);
161
+ }
162
+ });
163
+ }
69
164
  /**
70
165
  * Promise wrapper over spawn (shell:false). Optionally writes `input` to stdin; resolves with stdout/exit.
71
166
  * On timeout the WHOLE process group is killed, not just the direct child (finding #39.5): an agent spawns
72
167
  * tool subprocesses (grandchildren) that would otherwise orphan and leak. On POSIX we spawn detached (the
73
- * child becomes its own group leader) and SIGKILL the group via the negative pid; Windows falls back to a
74
- * direct kill (different process-group semantics).
168
+ * child becomes its own group leader) and SIGKILL the group via the negative pid. Windows runs
169
+ * `taskkill.exe /PID <pid> /T /F` without a shell and waits for that command before resolving.
75
170
  */
76
171
  export function spawnCapture(cmd, args, opts) {
77
172
  return new Promise((resolve, reject) => {
78
- const detached = process.platform !== 'win32';
79
- const r = resolveSpawnCommand(cmd, args, opts.env);
173
+ if (opts.signal?.aborted) {
174
+ resolve({ code: null, stdout: '', stderr: '', termination: 'cancelled' });
175
+ return;
176
+ }
177
+ const platform = opts.processPlatform ?? process.platform;
178
+ const detached = platform !== 'win32';
179
+ const r = resolveSpawnCommand(cmd, args, opts.env, opts.resolvePlatform ?? process.platform);
80
180
  const child = spawn(r.cmd, r.args, { cwd: opts.cwd, shell: false, detached, ...(opts.env ? { env: opts.env } : {}) });
81
181
  let stdout = '';
82
182
  let stderr = '';
183
+ let termination = 'exit';
184
+ let killPromise;
185
+ let settled = false;
83
186
  const killTree = () => {
84
- if (detached && typeof child.pid === 'number') {
85
- try {
86
- process.kill(-child.pid, 'SIGKILL');
87
- return;
187
+ if (killPromise)
188
+ return killPromise;
189
+ killPromise = (async () => {
190
+ if (platform === 'win32' && typeof child.pid === 'number') {
191
+ let killed = false;
192
+ try {
193
+ killed = await (opts.windowsProcessTreeKiller ?? killWindowsProcessTree)(child.pid);
194
+ }
195
+ catch {
196
+ // Treat an injected/custom killer rejection like taskkill failure and fall back to the direct child.
197
+ }
198
+ if (killed)
199
+ return;
200
+ }
201
+ if (detached && typeof child.pid === 'number') {
202
+ try {
203
+ process.kill(-child.pid, 'SIGKILL');
204
+ return;
205
+ }
206
+ catch { /* group gone; fall back */ }
88
207
  }
89
- catch { /* group gone; fall back */ }
208
+ child.kill('SIGKILL');
209
+ })();
210
+ return killPromise;
211
+ };
212
+ const cancel = () => {
213
+ if (termination !== 'exit')
214
+ return;
215
+ termination = 'cancelled';
216
+ void killTree();
217
+ };
218
+ const timeout = () => {
219
+ if (termination !== 'exit')
220
+ return;
221
+ termination = 'timeout';
222
+ void killTree();
223
+ };
224
+ const ignoreProcessSignal = () => { };
225
+ const cleanup = () => {
226
+ clearTimeout(timer);
227
+ opts.signal?.removeEventListener('abort', cancel);
228
+ if (opts.processSignalMode === 'ignore') {
229
+ process.removeListener('SIGINT', ignoreProcessSignal);
230
+ process.removeListener('SIGTERM', ignoreProcessSignal);
231
+ }
232
+ else {
233
+ process.removeListener('SIGINT', cancel);
234
+ process.removeListener('SIGTERM', cancel);
90
235
  }
91
- child.kill('SIGKILL');
92
236
  };
93
- const timer = setTimeout(killTree, opts.timeoutMs);
237
+ const settle = async (finish) => {
238
+ if (settled)
239
+ return;
240
+ settled = true;
241
+ if (killPromise)
242
+ await killPromise;
243
+ cleanup();
244
+ finish();
245
+ };
246
+ const timer = setTimeout(timeout, opts.timeoutMs);
247
+ opts.signal?.addEventListener('abort', cancel, { once: true });
248
+ // A detached POSIX child would otherwise survive Ctrl-C/SIGTERM. Cancel first so the bridge can clean its
249
+ // worktree and return a failure instead of leaking an agent or tool subprocess.
250
+ if (opts.processSignalMode === 'ignore') {
251
+ process.on('SIGINT', ignoreProcessSignal);
252
+ process.on('SIGTERM', ignoreProcessSignal);
253
+ }
254
+ else {
255
+ process.once('SIGINT', cancel);
256
+ process.once('SIGTERM', cancel);
257
+ }
94
258
  child.stdout?.on('data', (d) => { stdout += d.toString(); });
95
259
  child.stderr?.on('data', (d) => { stderr += d.toString(); });
96
- child.on('error', (e) => { clearTimeout(timer); reject(e); });
97
- child.on('close', (code) => { clearTimeout(timer); resolve({ code, stdout, stderr }); });
260
+ child.on('error', (e) => { void settle(() => reject(e)); });
261
+ child.on('close', (code) => { void settle(() => resolve({ code, stdout, stderr, termination })); });
98
262
  if (opts.input !== undefined) {
99
263
  child.stdin?.write(opts.input);
100
264
  child.stdin?.end();
101
265
  }
102
266
  });
103
267
  }
268
+ /** Map the shared process result into the failure taxonomy used by plain-text runners. */
269
+ export function classifyBasicRunnerResult(runner, result, timeoutMs) {
270
+ if (result.termination === 'timeout') {
271
+ return {
272
+ ok: false,
273
+ output: result.stdout,
274
+ error: `${runner} timed out after ${timeoutMs}ms`,
275
+ failureKind: 'timeout',
276
+ exitCode: result.code,
277
+ };
278
+ }
279
+ if (result.termination === 'cancelled') {
280
+ return {
281
+ ok: false,
282
+ output: result.stdout,
283
+ error: `${runner} execution cancelled`,
284
+ failureKind: 'cancelled',
285
+ exitCode: result.code,
286
+ };
287
+ }
288
+ if (result.code !== 0) {
289
+ return {
290
+ ok: false,
291
+ output: result.stdout,
292
+ error: result.stderr || `${runner} exited with code ${String(result.code)}`,
293
+ failureKind: 'non_zero_exit',
294
+ exitCode: result.code,
295
+ };
296
+ }
297
+ return { ok: true, output: result.stdout };
298
+ }
299
+ function spawnFailureResult(error) {
300
+ return {
301
+ ok: false,
302
+ output: '',
303
+ error: error instanceof Error ? error.message : String(error),
304
+ failureKind: 'spawn_failed',
305
+ exitCode: null,
306
+ };
307
+ }
104
308
  /**
105
309
  * Build the `claude -p` argv for the given options (pure — testable without spawning).
106
310
  * Safety invariant: skipPermissions (bypassing prompts) is only allowed together with a non-empty
@@ -127,12 +331,13 @@ export function claudeRunnerArgs(opts = {}) {
127
331
  export function makeClaudeHeadlessRunner(opts = {}) {
128
332
  const args = claudeRunnerArgs(opts);
129
333
  return async (prompt, ctx) => {
334
+ const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
130
335
  try {
131
- const r = await spawnCapture('claude', args, { cwd: ctx.cwd, timeoutMs: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS, input: prompt, ...(ctx.env ? { env: ctx.env } : {}) });
132
- return r.code === 0 ? { ok: true, output: r.stdout } : { ok: false, output: r.stdout, error: r.stderr || `exit ${r.code}` };
336
+ const result = await spawnCapture('claude', args, { cwd: ctx.cwd, timeoutMs, input: prompt, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
337
+ return classifyBasicRunnerResult('claude', result, timeoutMs);
133
338
  }
134
339
  catch (e) {
135
- return { ok: false, output: '', error: e instanceof Error ? e.message : String(e) };
340
+ return spawnFailureResult(e);
136
341
  }
137
342
  };
138
343
  }
@@ -165,12 +370,95 @@ export function codexRunnerArgs(opts = {}) {
165
370
  export function makeCodexHeadlessRunner(opts = {}) {
166
371
  const args = codexRunnerArgs(opts);
167
372
  return async (prompt, ctx) => {
373
+ const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
168
374
  try {
169
- const r = await spawnCapture('codex', [...args, '--cd', ctx.cwd, prompt], { cwd: ctx.cwd, timeoutMs: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS, ...(ctx.env ? { env: ctx.env } : {}) });
170
- return r.code === 0 ? { ok: true, output: r.stdout } : { ok: false, output: r.stdout, error: r.stderr || `exit ${r.code}` };
375
+ const result = await spawnCapture('codex', [...args, '--cd', ctx.cwd, prompt], { cwd: ctx.cwd, timeoutMs, ...(ctx.env ? { env: ctx.env } : {}), ...(ctx.signal ? { signal: ctx.signal } : {}) });
376
+ return classifyBasicRunnerResult('codex', result, timeoutMs);
171
377
  }
172
378
  catch (e) {
173
- return { ok: false, output: '', error: e instanceof Error ? e.message : String(e) };
379
+ return spawnFailureResult(e);
380
+ }
381
+ };
382
+ }
383
+ const GEMINI_PERMISSION_DENIAL_RE = /agent execution blocked|permission denied|approval required|not approved|denied by (?:policy|user|admin)|tool (?:call )?(?:was )?denied/i;
384
+ const GEMINI_TERMINATION_WARNINGS = [
385
+ { pattern: /^(?:[^:\r\n]{1,80}:\s*)?Agent execution stopped\b/i, error: 'gemini agent execution stopped' },
386
+ { pattern: /^(?:[^:\r\n]{1,80}:\s*)?Loop detected\b/i, error: 'gemini loop detected' },
387
+ { pattern: /^(?:[^:\r\n]{1,80}:\s*)?Maximum session turns exceeded\b/i, error: 'gemini maximum session turns exceeded' },
388
+ ];
389
+ function geminiMessage(value) {
390
+ if (typeof value === 'string')
391
+ return value;
392
+ if (value && typeof value === 'object') {
393
+ const record = value;
394
+ const type = typeof record['type'] === 'string' ? record['type'] : '';
395
+ const message = typeof record['message'] === 'string' ? record['message'] : '';
396
+ return [type, message].filter(Boolean).join(': ');
397
+ }
398
+ return value === undefined ? '' : String(value);
399
+ }
400
+ function geminiWarnings(value) {
401
+ return Array.isArray(value) ? value.map(geminiMessage).filter(Boolean) : [];
402
+ }
403
+ /** Build verified Gemini CLI argv. The prompt is appended separately as one argv element with shell:false. */
404
+ export function geminiRunnerArgs(opts = {}) {
405
+ if (opts.skipPermissions || (opts.allowedTools?.length ?? 0) > 0)
406
+ throw new UnsupportedGeminiPermissionOptionsError();
407
+ const args = ['--output-format', 'json', '--approval-mode', 'auto_edit', '--skip-trust'];
408
+ if (opts.model)
409
+ args.push('--model', opts.model);
410
+ return args;
411
+ }
412
+ /** Headless Gemini runner with structured failure classification; stdout text alone never proves execution success. */
413
+ export function makeGeminiHeadlessRunner(opts = {}) {
414
+ const args = geminiRunnerArgs(opts);
415
+ return async (prompt, ctx) => {
416
+ try {
417
+ const result = await spawnCapture('gemini', [...args, '--prompt', prompt], {
418
+ cwd: ctx.cwd,
419
+ timeoutMs: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS,
420
+ ...(ctx.env ? { env: ctx.env } : {}),
421
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
422
+ });
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 };
459
+ }
460
+ catch (error) {
461
+ return { ok: false, output: '', error: error instanceof Error ? error.message : String(error), failureKind: 'spawn_failed', exitCode: null };
174
462
  }
175
463
  };
176
464
  }
@@ -185,10 +473,10 @@ export function makeCodexHeadlessRunner(opts = {}) {
185
473
  // - `--model <model>` exists (e.g. gpt-5, sonnet-4, sonnet-4-thinking); `--list-models` enumerates.
186
474
  // - auth: `CURSOR_API_KEY` or `--api-key` (the spec's envAllow prefix is CURSOR_).
187
475
  // - cursor has its OWN `--sandbox enabled|disabled` and `-w/--worktree`; we still wrap with our git worktree.
188
- // STILL NOT run-verified end to end (needs an authed cursor-agent; claude/codex were each run-verified). Two known
189
- // gaps surfaced while testing: (1) on Windows `cursor-agent.cmd` is a powershell shim, NOT an npm node-shim, so
190
- // resolveSpawnCommand cannot rewrite it; the built-in runner fail-fasts there instead of surfacing ENOENT;
191
- // (2) the installed launcher's version-discovery regex rejects the real version dir name (Cursor's own bug).
476
+ // STILL NOT run-verified end to end (needs an authed cursor-agent; claude/codex were each run-verified). The
477
+ // Windows launcher is a PowerShell shim and its own version regex rejects the current timestamped version-dir
478
+ // shape. resolveSpawnCommand therefore bypasses both scripts and runs the newest verified node.exe + index.js
479
+ // bundle directly, shell-free. If that known bundle layout cannot be found, the runner still fail-fasts.
192
480
  //
193
481
  // SAFETY: `-p --force --trust` auto-approves shell+write with no verified per-run allowlist/sandbox mapping.
194
482
  // Until Cursor can really map agentOptions into per-run permissions, skipPermissions is refused outright. The
@@ -215,13 +503,20 @@ export function cursorRunnerArgs(opts = {}) {
215
503
  export function makeCursorHeadlessRunner(opts = {}, platform = process.platform) {
216
504
  const args = cursorRunnerArgs(opts);
217
505
  return async (prompt, ctx) => {
218
- assertCursorRunnerPlatformSupported(platform);
506
+ assertCursorRunnerPlatformSupported(platform, ctx.env ?? process.env);
507
+ const timeoutMs = ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS;
219
508
  try {
220
- const r = await spawnCapture('cursor-agent', [...args, prompt], { cwd: ctx.cwd, timeoutMs: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS, ...(ctx.env ? { env: ctx.env } : {}) });
221
- return r.code === 0 ? { ok: true, output: r.stdout } : { ok: false, output: r.stdout, error: r.stderr || `exit ${r.code}` };
509
+ const result = await spawnCapture('cursor-agent', [...args, prompt], {
510
+ cwd: ctx.cwd,
511
+ timeoutMs,
512
+ resolvePlatform: platform,
513
+ ...(ctx.env ? { env: ctx.env } : {}),
514
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
515
+ });
516
+ return classifyBasicRunnerResult('cursor', result, timeoutMs);
222
517
  }
223
518
  catch (e) {
224
- return { ok: false, output: '', error: e instanceof Error ? e.message : String(e) };
519
+ return spawnFailureResult(e);
225
520
  }
226
521
  };
227
522
  }
@@ -230,6 +525,14 @@ const RUNNER_SPECS = {
230
525
  codex: { name: 'codex', makeRunner: makeCodexHeadlessRunner, envAllow: { prefixes: ['OPENAI_', 'CODEX_'] } },
231
526
  // cursor keeps only its OWN auth env (CURSOR_); like every runner it never inherits another's vendor key.
232
527
  cursor: { name: 'cursor', makeRunner: makeCursorHeadlessRunner, envAllow: { prefixes: ['CURSOR_'] } },
528
+ gemini: {
529
+ name: 'gemini',
530
+ makeRunner: makeGeminiHeadlessRunner,
531
+ envAllow: {
532
+ prefixes: ['GEMINI_'],
533
+ keys: ['GOOGLE_API_KEY', 'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION', 'GOOGLE_GENAI_USE_VERTEXAI'],
534
+ },
535
+ },
233
536
  };
234
537
  /** Resolve a runner spec by name (default 'claude' — byte-identical to the pre-registry behavior). */
235
538
  export function getRunnerSpec(name = 'claude') {
@@ -9,7 +9,8 @@ export declare function resetObfuscatedCache(reader?: ReadManifest): void;
9
9
  /**
10
10
  * Load the obfuscated-file set from the manifest. Returns a normalized Set, or null when the manifest is
11
11
  * missing / unreadable / structurally invalid (NOT an `obfuscate` array of literal string paths). null is the
12
- * fail-closed signal: the caller must then reject all files. Result is cached per reader.
12
+ * fail-closed signal: the caller must then reject all files. Success is cached per reader; failures retry briefly
13
+ * so startup-time transient filesystem errors do not disable self-PR until process restart.
13
14
  */
14
15
  export declare function loadObfuscatedFiles(readManifest: ReadManifest): Set<string> | null;
15
16
  /**