@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.
- package/assets/gep/genes.jsonl +5 -0
- package/dist/algo/candidateAssembly.js +23 -14
- package/dist/algo/capabilityCandidates.d.ts +2 -0
- package/dist/algo/capabilityCandidates.js +5 -0
- package/dist/algo/conversationSniffer.d.ts +18 -0
- package/dist/algo/conversationSniffer.js +132 -0
- package/dist/algo/cycleEngine.d.ts +11 -0
- package/dist/algo/cycleEngine.js +14 -6
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- package/dist/algo/cycleFailureClassifier.js +13 -5
- package/dist/algo/geneIntake.d.ts +14 -4
- package/dist/algo/geneIntake.js +37 -9
- package/dist/algo/geneSelection.d.ts +18 -1
- package/dist/algo/geneSelection.js +42 -18
- package/dist/algo/index.d.ts +2 -0
- package/dist/algo/index.js +2 -0
- package/dist/algo/memoryGraph.d.ts +62 -0
- package/dist/algo/memoryGraph.js +86 -0
- package/dist/algo/orchestrator.d.ts +3 -0
- package/dist/algo/orchestrator.js +14 -2
- package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
- package/dist/assetstore/assetSidecarRecords.js +142 -0
- package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
- package/dist/assetstore/assetSidecarRecovery.js +288 -0
- package/dist/assetstore/assetStoreHealth.d.ts +75 -0
- package/dist/assetstore/assetStoreHealth.js +277 -0
- package/dist/assetstore/assetStoreLayout.d.ts +2 -0
- package/dist/assetstore/assetStoreLayout.js +6 -0
- package/dist/assetstore/assetStoreStorage.d.ts +42 -0
- package/dist/assetstore/assetStoreStorage.js +318 -0
- package/dist/assetstore/assetSyncLedger.d.ts +32 -0
- package/dist/assetstore/assetSyncLedger.js +66 -0
- package/dist/assetstore/index.d.ts +5 -1
- package/dist/assetstore/index.js +5 -1
- package/dist/assetstore/localJsonl.d.ts +10 -2
- package/dist/assetstore/localJsonl.js +93 -37
- package/dist/assetstore/pendingSignals.js +3 -1
- package/dist/assetstore/provenance.d.ts +20 -2
- package/dist/assetstore/provenance.js +92 -56
- package/dist/assetstore/provider.d.ts +2 -0
- package/dist/assetstore/reviewFilter.js +5 -2
- package/dist/assetstore/reviewLedger.d.ts +14 -2
- package/dist/assetstore/reviewLedger.js +78 -43
- package/dist/assetstore/seedGenes.d.ts +3 -0
- package/dist/assetstore/seedGenes.js +134 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +11 -1
- package/dist/benchmark/antiGeneImpact.d.ts +16 -0
- package/dist/benchmark/antiGeneImpact.js +34 -0
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +13 -1
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/triggerShift.d.ts +62 -0
- package/dist/benchmark/triggerShift.js +106 -0
- package/dist/events/eventArchive.d.ts +65 -0
- package/dist/events/eventArchive.js +343 -0
- package/dist/events/eventStore.js +2 -12
- package/dist/events/ingest.d.ts +1 -1
- package/dist/events/ingest.js +2 -0
- package/dist/events/paths.d.ts +1 -1
- package/dist/events/paths.js +2 -2
- package/dist/events/public.d.ts +3 -1
- package/dist/events/public.js +2 -1
- package/dist/events/retention.d.ts +24 -1
- package/dist/events/retention.js +133 -37
- package/dist/exec/autoExec.d.ts +6 -1
- package/dist/exec/autoExec.js +34 -0
- package/dist/exec/autonomousCycle.d.ts +2 -0
- package/dist/exec/autonomousCycle.js +5 -0
- package/dist/exec/claudeBridge.d.ts +7 -2
- package/dist/exec/claudeBridge.js +115 -14
- package/dist/exec/prompt.js +9 -0
- package/dist/exec/runnerRegistry.d.ts +64 -18
- package/dist/exec/runnerRegistry.js +341 -38
- package/dist/exec/selfPrObfuscation.d.ts +2 -1
- package/dist/exec/selfPrObfuscation.js +19 -6
- package/dist/hub/agentDirectory.d.ts +90 -0
- package/dist/hub/agentDirectory.js +104 -0
- package/dist/hub/bindings.js +23 -3
- package/dist/hub/capability.d.ts +14 -2
- package/dist/hub/fake.d.ts +4 -2
- package/dist/hub/fake.js +3 -2
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/mailbox/catalog.js +3 -0
- package/dist/mailbox/ipcServer.js +2 -2
- package/dist/mailbox/store.d.ts +14 -0
- package/dist/mailbox/store.js +44 -2
- package/dist/material/consumer.js +9 -6
- package/dist/material/index.d.ts +1 -0
- package/dist/material/index.js +1 -0
- package/dist/material/materialArchive.d.ts +81 -0
- package/dist/material/materialArchive.js +466 -0
- package/dist/material/materialStore.d.ts +1 -0
- package/dist/material/materialStore.js +6 -13
- package/dist/ops/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +10 -1
- package/dist/ops/selfUpdate.js +64 -15
- package/dist/schema/common.d.ts +1 -1
- package/dist/schema/common.js +1 -1
- package/dist/schema/material.d.ts +5 -5
- package/dist/trace/trajectoryExport.js +2 -2
- package/dist/util/fileLock.d.ts +19 -2
- package/dist/util/fileLock.js +166 -31
- package/dist/wire/geneHints.d.ts +42 -1
- package/dist/wire/geneHints.js +52 -1
- package/dist/wire/index.d.ts +14 -2
- package/dist/wire/index.js +1 -1
- 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
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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
|
|
74
|
-
*
|
|
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
|
-
|
|
79
|
-
|
|
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 (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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) => {
|
|
97
|
-
child.on('close', (code) => {
|
|
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
|
|
132
|
-
return
|
|
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
|
|
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
|
|
170
|
-
return
|
|
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
|
|
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).
|
|
189
|
-
//
|
|
190
|
-
// resolveSpawnCommand
|
|
191
|
-
//
|
|
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
|
|
221
|
-
|
|
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
|
|
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.
|
|
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
|
/**
|