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

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 (75) hide show
  1. package/assets/gep/genes.jsonl +5 -0
  2. package/dist/algo/candidateAssembly.js +17 -11
  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.js +2 -0
  8. package/dist/algo/cycleFailureClassifier.js +13 -5
  9. package/dist/algo/geneIntake.d.ts +14 -4
  10. package/dist/algo/geneIntake.js +37 -9
  11. package/dist/algo/geneSelection.d.ts +6 -0
  12. package/dist/algo/geneSelection.js +19 -11
  13. package/dist/algo/index.d.ts +1 -0
  14. package/dist/algo/index.js +1 -0
  15. package/dist/assetstore/assetSyncLedger.d.ts +28 -0
  16. package/dist/assetstore/assetSyncLedger.js +86 -0
  17. package/dist/assetstore/index.d.ts +3 -1
  18. package/dist/assetstore/index.js +3 -1
  19. package/dist/assetstore/localJsonl.d.ts +9 -2
  20. package/dist/assetstore/localJsonl.js +75 -23
  21. package/dist/assetstore/pendingSignals.js +3 -1
  22. package/dist/assetstore/provenance.d.ts +7 -2
  23. package/dist/assetstore/provenance.js +85 -25
  24. package/dist/assetstore/reviewFilter.js +2 -1
  25. package/dist/assetstore/reviewLedger.d.ts +6 -0
  26. package/dist/assetstore/reviewLedger.js +9 -0
  27. package/dist/assetstore/seedGenes.d.ts +3 -0
  28. package/dist/assetstore/seedGenes.js +134 -0
  29. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  30. package/dist/benchmark/antiGeneBenchmark.js +11 -1
  31. package/dist/benchmark/antiGeneImpact.d.ts +16 -0
  32. package/dist/benchmark/antiGeneImpact.js +34 -0
  33. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  34. package/dist/benchmark/antiGeneRollout.js +13 -1
  35. package/dist/events/eventArchive.d.ts +65 -0
  36. package/dist/events/eventArchive.js +343 -0
  37. package/dist/events/eventStore.js +2 -12
  38. package/dist/events/public.d.ts +3 -1
  39. package/dist/events/public.js +2 -1
  40. package/dist/events/retention.d.ts +24 -1
  41. package/dist/events/retention.js +133 -37
  42. package/dist/exec/autoExec.js +3 -0
  43. package/dist/exec/claudeBridge.js +23 -0
  44. package/dist/exec/runnerRegistry.d.ts +8 -6
  45. package/dist/exec/runnerRegistry.js +71 -18
  46. package/dist/exec/selfPrObfuscation.d.ts +2 -1
  47. package/dist/exec/selfPrObfuscation.js +19 -6
  48. package/dist/hub/agentDirectory.d.ts +90 -0
  49. package/dist/hub/agentDirectory.js +104 -0
  50. package/dist/hub/bindings.js +13 -3
  51. package/dist/hub/capability.d.ts +14 -2
  52. package/dist/hub/fake.d.ts +4 -2
  53. package/dist/hub/fake.js +3 -2
  54. package/dist/hub/index.d.ts +1 -0
  55. package/dist/hub/index.js +1 -0
  56. package/dist/mailbox/catalog.js +3 -0
  57. package/dist/mailbox/ipcServer.js +2 -2
  58. package/dist/mailbox/store.d.ts +14 -0
  59. package/dist/mailbox/store.js +44 -2
  60. package/dist/material/consumer.js +9 -6
  61. package/dist/material/index.d.ts +1 -0
  62. package/dist/material/index.js +1 -0
  63. package/dist/material/materialArchive.d.ts +81 -0
  64. package/dist/material/materialArchive.js +466 -0
  65. package/dist/material/materialStore.d.ts +1 -0
  66. package/dist/material/materialStore.js +6 -13
  67. package/dist/schema/common.d.ts +1 -1
  68. package/dist/schema/common.js +1 -1
  69. package/dist/schema/material.d.ts +5 -5
  70. package/dist/trace/trajectoryExport.js +2 -2
  71. package/dist/wire/geneHints.d.ts +42 -1
  72. package/dist/wire/geneHints.js +52 -1
  73. package/dist/wire/index.d.ts +14 -2
  74. package/dist/wire/index.js +1 -1
  75. package/package.json +2 -1
@@ -1,24 +1,29 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { material } from '../schema/material.js';
4
+ import { inspectMaterialArchive, planMaterialArchive, readMaterialHistory, } from '../material/materialArchive.js';
4
5
  import { rootEvent } from './eventSchema.js';
6
+ import { ArchiveSegmentConflictError, inspectRootEventArchive, readRootEventHistory, RootEventHistoryGapError, ROOT_EVENT_ARCHIVE_DEFAULT_KEEP_EVENTS, validateRootEventHistory, } from './eventArchive.js';
5
7
  import { materialStorePath as defaultMaterialStorePath, rootEventsPath as defaultRootEventsPath } from './paths.js';
6
- import { NARRATIVE_MAX_LIMIT } from './reports.js';
7
8
  export const RETENTION_DEFAULT_MAX_ROOT_EVENTS = 100_000;
8
9
  export const RETENTION_DEFAULT_MAX_ROOT_BYTES = 64 * 1024 * 1024;
9
10
  export const RETENTION_DEFAULT_MAX_MATERIAL_RECORDS = 10_000;
10
11
  export const RETENTION_DEFAULT_MAX_MATERIAL_BYTES = 64 * 1024 * 1024;
11
12
  export const RETENTION_DEFAULT_WATCH_RATIO = 0.8;
12
- export const RETENTION_DEFAULT_ROOT_TAIL_EVENTS = Math.max(1_000, NARRATIVE_MAX_LIMIT);
13
+ export const RETENTION_DEFAULT_ROOT_TAIL_EVENTS = ROOT_EVENT_ARCHIVE_DEFAULT_KEEP_EVENTS;
13
14
  export function defaultMaterialCursorPath(path = defaultMaterialStorePath()) {
14
15
  return join(dirname(path), 'cycle-consumer.json');
15
16
  }
17
+ export function defaultMaterialCursorPaths(path = defaultMaterialStorePath()) {
18
+ return [defaultMaterialCursorPath(path), join(dirname(path), 'distill-consumer.json')];
19
+ }
16
20
  export function buildRetentionReport(opts = {}) {
17
21
  const now = new Date((opts.now ?? Date.now)()).toISOString();
18
22
  const watchRatio = ratioOf(opts.watchRatio, RETENTION_DEFAULT_WATCH_RATIO);
19
23
  const rootPath = opts.rootEventsPath ?? defaultRootEventsPath();
20
24
  const materialPath = opts.materialStorePath ?? defaultMaterialStorePath();
21
- const cursorPath = opts.materialCursorPath ?? defaultMaterialCursorPath(materialPath);
25
+ const cursorPaths = opts.materialCursorPaths
26
+ ?? (opts.materialCursorPath !== undefined ? [opts.materialCursorPath] : defaultMaterialCursorPaths(materialPath));
22
27
  const rootThresholds = {
23
28
  maxRecords: positiveInt(opts.maxRootEvents, RETENTION_DEFAULT_MAX_ROOT_EVENTS),
24
29
  maxBytes: positiveInt(opts.maxRootBytes, RETENTION_DEFAULT_MAX_ROOT_BYTES),
@@ -30,7 +35,7 @@ export function buildRetentionReport(opts = {}) {
30
35
  watchRatio,
31
36
  };
32
37
  const rootEvents = rootSnapshot(rootPath, rootThresholds, positiveInt(opts.protectRootTailEvents, RETENTION_DEFAULT_ROOT_TAIL_EVENTS));
33
- const material = materialSnapshot(materialPath, cursorPath, materialThresholds);
38
+ const material = materialSnapshot(materialPath, cursorPaths, materialThresholds);
34
39
  const warnings = buildWarnings(rootEvents, material);
35
40
  const nextActions = buildNextActions(rootEvents, material);
36
41
  return {
@@ -45,6 +50,22 @@ export function buildRetentionReport(opts = {}) {
45
50
  }
46
51
  function rootSnapshot(path, thresholds, protectTailEvents) {
47
52
  const stats = readJsonl(path, (value) => rootEvent.parse(value));
53
+ const archive = inspectRootEventArchive(path);
54
+ const historyRecords = readRootEventHistory(path).length;
55
+ let historyConflicts = 0;
56
+ let historyGaps = 0;
57
+ let historyIntegrityErrors = 0;
58
+ try {
59
+ validateRootEventHistory(path);
60
+ }
61
+ catch (error) {
62
+ if (error instanceof ArchiveSegmentConflictError)
63
+ historyConflicts = 1;
64
+ else if (error instanceof RootEventHistoryGapError)
65
+ historyGaps = 1;
66
+ else
67
+ historyIntegrityErrors = 1;
68
+ }
48
69
  return {
49
70
  name: 'root_events',
50
71
  exists: stats.exists,
@@ -57,34 +78,67 @@ function rootSnapshot(path, thresholds, protectTailEvents) {
57
78
  lastSeq: stats.last?.seq ?? null,
58
79
  firstTs: stats.first?.ts ?? null,
59
80
  lastTs: stats.last?.ts ?? null,
81
+ archiveSegments: archive.segments,
82
+ archiveRecords: archive.records,
83
+ archiveBytes: archive.bytes,
84
+ archiveInvalidLines: archive.invalidLines,
85
+ historyRecords,
86
+ historyBytes: archive.bytes + stats.bytes,
87
+ historyConflicts,
88
+ historyGaps,
89
+ historyIntegrityErrors,
90
+ archiveRotationSupported: true,
91
+ archiveRotationSafe: stats.invalidLines === 0 && archive.invalidLines === 0 && historyConflicts === 0 && historyGaps === 0 && historyIntegrityErrors === 0,
60
92
  protectTailEvents: Math.min(stats.records, protectTailEvents),
61
93
  destructivePruneSafe: false,
62
- reason: 'root_events is the replay and audit source; archive readers must exist before destructive prune is safe',
94
+ reason: 'archive rotation preserves replay history; destructive archive deletion remains disabled',
63
95
  };
64
96
  }
65
- function materialSnapshot(path, cursorPath, thresholds) {
97
+ function materialSnapshot(path, cursorPaths, thresholds) {
66
98
  const stats = readJsonl(path, (value) => material.parse(value));
67
- const cursor = readCycleCursor(cursorPath);
68
- const cursorInRange = cursor.value >= 0 && cursor.value <= stats.records;
69
- const effectiveCursor = cursorInRange ? cursor.value : clampCursor(cursor.value, stats.records);
99
+ const archive = inspectMaterialArchive(path);
100
+ const history = readMaterialHistory(path);
101
+ const historyRecords = history.length;
102
+ const cursors = readMaterialCursors(cursorPaths, historyRecords, archive.records);
103
+ const effectiveCursor = cursors.inRange ? cursors.cycle : clampCursor(cursors.cycle, historyRecords);
104
+ const effectiveMinimum = cursors.inRange ? cursors.min : clampCursor(cursors.min, historyRecords);
105
+ let archiveRotationSafe = stats.invalidLines === 0 && archive.invalidLines === 0 && cursors.valid && cursors.inRange;
106
+ if (archiveRotationSafe) {
107
+ try {
108
+ planMaterialArchive({ path, cursorPaths, keepRecords: 1 });
109
+ }
110
+ catch {
111
+ archiveRotationSafe = false;
112
+ }
113
+ }
70
114
  return {
71
115
  name: 'material',
72
- exists: stats.exists,
116
+ exists: stats.exists || archive.exists,
73
117
  bytes: stats.bytes,
74
118
  records: stats.records,
75
119
  invalidLines: stats.invalidLines,
76
120
  state: retentionState(stats.records, stats.bytes, thresholds),
77
121
  thresholds,
78
- firstCapturedAt: stats.first?.capturedAt ?? null,
79
- lastCapturedAt: stats.last?.capturedAt ?? null,
80
- cursorValid: cursor.valid,
81
- cursorInRange,
82
- cursor: cursor.value,
122
+ firstCapturedAt: history[0]?.capturedAt ?? null,
123
+ lastCapturedAt: history[history.length - 1]?.capturedAt ?? null,
124
+ archiveSegments: archive.segments,
125
+ archiveRecords: archive.records,
126
+ archiveBytes: archive.bytes,
127
+ archiveInvalidLines: archive.invalidLines,
128
+ historyRecords,
129
+ historyBytes: archive.bytes + stats.bytes,
130
+ cursorCount: cursors.count,
131
+ minCursor: effectiveMinimum,
132
+ cursorValid: cursors.valid,
133
+ cursorInRange: cursors.inRange,
134
+ cursor: cursors.cycle,
83
135
  effectiveCursor,
84
- consumedPrefix: effectiveCursor,
85
- pending: Math.max(0, stats.records - effectiveCursor),
136
+ consumedPrefix: effectiveMinimum,
137
+ pending: Math.max(0, historyRecords - effectiveCursor),
138
+ archiveRotationSupported: true,
139
+ archiveRotationSafe,
86
140
  destructivePruneSafe: false,
87
- reason: 'material cursor is index-based; prune requires atomic archive plus cursor rewrite',
141
+ reason: 'archive rotation preserves absolute consumer cursors and full material history; destructive archive deletion remains disabled',
88
142
  };
89
143
  }
90
144
  function readJsonl(path, parse) {
@@ -111,19 +165,49 @@ function readJsonl(path, parse) {
111
165
  }
112
166
  return { exists: true, bytes, records, invalidLines, first, last };
113
167
  }
114
- function readCycleCursor(path) {
115
- if (!existsSync(path))
116
- return { exists: false, valid: true, value: 0 };
117
- try {
118
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
119
- if (typeof parsed.cycle === 'number' && Number.isFinite(parsed.cycle)) {
120
- return { exists: true, valid: true, value: Math.floor(parsed.cycle) };
168
+ function readMaterialCursors(paths, historyRecords, archiveRecords) {
169
+ let valid = true;
170
+ let inRange = true;
171
+ let cycle = 0;
172
+ let cycleFound = false;
173
+ const values = [];
174
+ for (const path of paths) {
175
+ if (!existsSync(path))
176
+ continue;
177
+ let parsed;
178
+ try {
179
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
180
+ }
181
+ catch {
182
+ valid = false;
183
+ continue;
184
+ }
185
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
186
+ valid = false;
187
+ continue;
188
+ }
189
+ for (const [group, value] of Object.entries(parsed)) {
190
+ if (!Number.isSafeInteger(value) || value < 0) {
191
+ valid = false;
192
+ continue;
193
+ }
194
+ const position = value;
195
+ values.push(position);
196
+ if (position > historyRecords)
197
+ inRange = false;
198
+ if (group === 'cycle' && !cycleFound) {
199
+ cycle = position;
200
+ cycleFound = true;
201
+ }
121
202
  }
122
- return { exists: true, valid: false, value: 0 };
123
- }
124
- catch {
125
- return { exists: true, valid: false, value: 0 };
126
203
  }
204
+ return {
205
+ valid,
206
+ inRange,
207
+ count: values.length,
208
+ cycle,
209
+ min: values.length === 0 ? archiveRecords : Math.min(...values),
210
+ };
127
211
  }
128
212
  function retentionState(records, bytes, thresholds) {
129
213
  if (records > thresholds.maxRecords || bytes > thresholds.maxBytes)
@@ -136,30 +220,42 @@ function buildWarnings(root, mat) {
136
220
  const out = [];
137
221
  if (root.invalidLines > 0)
138
222
  out.push(`root_events has ${root.invalidLines} invalid jsonl line(s); recover before rotating`);
223
+ if (root.archiveInvalidLines > 0)
224
+ out.push(`root_events archive has ${root.archiveInvalidLines} invalid jsonl line(s); repair archive integrity before rotating again`);
225
+ if (root.historyConflicts > 0)
226
+ out.push('root_events active/archive history has conflicting seq records; repair audit integrity before rotating');
227
+ if (root.historyGaps > 0)
228
+ out.push('root_events active/archive history has a seq gap; restore the missing audit segment before rotating');
229
+ if (root.historyIntegrityErrors > 0)
230
+ out.push('root_events history fails structural integrity checks; repair event ordering before rotating');
139
231
  if (mat.invalidLines > 0)
140
232
  out.push(`material store has ${mat.invalidLines} invalid jsonl line(s); inspect before rotating`);
141
233
  if (!mat.cursorValid)
142
234
  out.push('material cursor is unreadable; retention uses effectiveCursor=0 until the cursor is repaired');
143
235
  if (mat.cursorValid && !mat.cursorInRange)
144
- out.push(`material cursor ${mat.cursor} is outside material record range 0..${mat.records}; retention uses effectiveCursor=${mat.effectiveCursor}`);
236
+ out.push(`material cursor ${mat.cursor} is outside material record range 0..${mat.historyRecords}; retention uses effectiveCursor=${mat.effectiveCursor}`);
145
237
  if (root.state === 'over_limit')
146
- out.push('root_events exceeds retention thresholds; plan archive support before deleting audit lines');
238
+ out.push('root_events exceeds retention thresholds for the active log; preview archive rotation before writing');
239
+ if (mat.archiveInvalidLines > 0)
240
+ out.push(`material archive has ${mat.archiveInvalidLines} invalid jsonl line(s); repair archive integrity before rotating again`);
241
+ if (!mat.archiveRotationSafe)
242
+ out.push('material active/archive/cursor state is not safe for rotation; run archive-material preview after repairing integrity');
147
243
  if (mat.state === 'over_limit')
148
- out.push('material store exceeds retention thresholds; archive consumed prefix only with cursor rewrite');
244
+ out.push('material active store exceeds retention thresholds; preview absolute-cursor archive rotation before writing');
149
245
  if (mat.pending > 0)
150
246
  out.push(`material store has ${mat.pending} pending record(s); do not prune pending material`);
151
247
  return out;
152
248
  }
153
249
  function buildNextActions(root, mat) {
154
250
  const out = [
155
- 'keep destructive prune disabled until archive readers are wired into reports, replay and cycle recovery',
251
+ 'archive rotation is available; keep destructive prune disabled for archive deletion',
156
252
  ];
157
253
  if (root.state !== 'ok')
158
- out.push(`root_events ${root.state}: keep at least the last ${root.protectTailEvents} event(s) active after future archive`);
254
+ out.push(`root_events ${root.state}: run retention archive-root preview, then keep at least the last ${root.protectTailEvents} active event(s)`);
159
255
  if (!mat.cursorValid || !mat.cursorInRange)
160
- out.push('material: repair the cycle cursor before archiving consumed material');
161
- if (mat.consumedPrefix > 0)
162
- out.push(`material: ${mat.consumedPrefix} consumed prefix record(s) are archive candidates after atomic cursor rewrite exists`);
256
+ out.push('material: repair the cycle cursor and any other durable consumer cursor before archiving consumed material');
257
+ if (mat.consumedPrefix > mat.archiveRecords)
258
+ out.push(`material: ${mat.consumedPrefix - mat.archiveRecords} consumed prefix record(s) may be archived with archive-material without rewriting cursors`);
163
259
  if (mat.pending > 0)
164
260
  out.push('material: process or inspect pending records before any future prune');
165
261
  return out;
@@ -122,6 +122,9 @@ export async function runAutoExecTask(deps, rawTask, safety) {
122
122
  const gi = intakeGene({
123
123
  category: 'repair', signals_match: cycleSignals, strategy: [...task.strategy],
124
124
  summary: task.expectedEffect.slice(0, 80), ...(task.validationCmds ? { validation: [...task.validationCmds] } : {}),
125
+ // A task's seeded strategy is an execution-entry seed (the operator/daemon supplied a learned strategy to
126
+ // run/evolve), not a skill/session transcription → `evolved` per V1 #302 classifyProvenance.
127
+ generation_meta: { source: 'evolved' },
125
128
  });
126
129
  if (gi.ok && gi.gene) {
127
130
  await deps.store.put(gi.gene);
@@ -96,9 +96,32 @@ export function scrubAgentEnv(env, opts = {}) {
96
96
  for (const [k, v] of Object.entries(env)) {
97
97
  if (v === undefined)
98
98
  continue;
99
+ // Windows treats env keys case-insensitively, but plain JS objects can contain both PATH and Path. Package
100
+ // runners such as pnpm may prepend their bins to PATH while the inherited user executables remain in Path.
101
+ // Canonicalize those aliases below so the whitelist does not silently discard either half.
102
+ if (process.platform === 'win32' && k.toLowerCase() === 'path')
103
+ continue;
99
104
  if (allowKeys.has(k) || allowPrefixes.some((p) => k.startsWith(p)))
100
105
  out[k] = v;
101
106
  }
107
+ if (process.platform === 'win32') {
108
+ const pathValues = Object.entries(env)
109
+ .filter(([key, value]) => key.toLowerCase() === 'path' && value !== undefined)
110
+ .sort(([a], [b]) => a === 'PATH' ? -1 : b === 'PATH' ? 1 : 0)
111
+ .map(([, value]) => value);
112
+ const seen = new Set();
113
+ const merged = pathValues.flatMap((value) => value.split(';')).filter((entry) => {
114
+ if (!entry)
115
+ return false;
116
+ const key = entry.toLowerCase();
117
+ if (seen.has(key))
118
+ return false;
119
+ seen.add(key);
120
+ return true;
121
+ });
122
+ if (merged.length > 0)
123
+ out['PATH'] = merged.join(';');
124
+ }
102
125
  return out;
103
126
  }
104
127
  /** Default git runner: spawn `git <args>` in cwd, return stdout (empty string on error). Env scrubbed — git never needs evolver/hub secrets. */
@@ -20,20 +20,21 @@ export declare class UnboundedSkipPermissionsError extends Error {
20
20
  export declare class UnsupportedCursorSkipPermissionsError extends Error {
21
21
  constructor();
22
22
  }
23
- /** Thrown when Cursor's built-in runner is requested on Windows before its launcher path is run-verified. */
23
+ /** Thrown when Cursor's Windows installation cannot be reduced to a shell-free node.exe + index.js launch. */
24
24
  export declare class UnsupportedCursorWindowsRunnerError extends Error {
25
25
  constructor();
26
26
  }
27
- export declare function assertCursorRunnerPlatformSupported(platform?: NodeJS.Platform): void;
27
+ export declare function assertCursorRunnerPlatformSupported(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): void;
28
28
  /**
29
29
  * Make a bare command name spawnable shell-free on Windows. `spawn(shell:false)` cannot execute an npm CLI
30
30
  * shim (a `.cmd`/`.bat`), and routing through a shell would expose the prompt arg to cmd.exe quoting
31
31
  * (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.
32
+ * run `node <entry.js>` directly. Cursor's installer uses a PowerShell shim around a bundled
33
+ * `versions/<version>/node.exe + index.js`; that known layout is resolved directly too, without invoking
34
+ * cmd.exe or PowerShell. A native `.exe` (claude) resolves to itself. No-op on POSIX and for any command that
35
+ * is already a path or has an extension. Surfaced by codex/cursor-agent on Windows (#66).
35
36
  */
36
- export declare function resolveSpawnCommand(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv): {
37
+ export declare function resolveSpawnCommand(cmd: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): {
37
38
  cmd: string;
38
39
  args: string[];
39
40
  };
@@ -49,6 +50,7 @@ export declare function spawnCapture(cmd: string, args: readonly string[], opts:
49
50
  timeoutMs: number;
50
51
  input?: string;
51
52
  env?: NodeJS.ProcessEnv;
53
+ resolvePlatform?: NodeJS.Platform;
52
54
  }): Promise<{
53
55
  code: number | null;
54
56
  stdout: string;
@@ -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,34 @@ 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 Cursor's Windows installation cannot be reduced to a shell-free node.exe + index.js launch. */
25
25
  export class UnsupportedCursorWindowsRunnerError extends Error {
26
26
  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');
27
+ 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
28
  this.name = 'UnsupportedCursorWindowsRunnerError';
29
29
  }
30
30
  }
31
- export function assertCursorRunnerPlatformSupported(platform = process.platform) {
32
- if (platform === 'win32')
31
+ export function assertCursorRunnerPlatformSupported(platform = process.platform, env = process.env) {
32
+ if (platform !== 'win32')
33
+ return;
34
+ if (resolveSpawnCommand('cursor-agent', [], env, platform).cmd === 'cursor-agent') {
33
35
  throw new UnsupportedCursorWindowsRunnerError();
36
+ }
34
37
  }
35
38
  /**
36
39
  * Make a bare command name spawnable shell-free on Windows. `spawn(shell:false)` cannot execute an npm CLI
37
40
  * shim (a `.cmd`/`.bat`), and routing through a shell would expose the prompt arg to cmd.exe quoting
38
41
  * (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.
42
+ * run `node <entry.js>` directly. Cursor's installer uses a PowerShell shim around a bundled
43
+ * `versions/<version>/node.exe + index.js`; that known layout is resolved directly too, without invoking
44
+ * cmd.exe or PowerShell. A native `.exe` (claude) resolves to itself. No-op on POSIX and for any command that
45
+ * is already a path or has an extension. Surfaced by codex/cursor-agent on Windows (#66).
42
46
  */
43
- export function resolveSpawnCommand(cmd, args, env = process.env) {
44
- if (process.platform !== 'win32' || /[\\/]/.test(cmd) || /\.[a-z0-9]+$/i.test(cmd))
47
+ export function resolveSpawnCommand(cmd, args, env = process.env, platform = process.platform) {
48
+ if (platform !== 'win32' || /[\\/]/.test(cmd) || /\.[a-z0-9]+$/i.test(cmd))
45
49
  return { cmd, args: [...args] };
46
- for (const dir of (env['PATH'] ?? '').split(pathDelimiter).filter(Boolean)) {
50
+ const delimiter = platform === 'win32' ? ';' : pathDelimiter;
51
+ for (const dir of (env['PATH'] ?? '').split(delimiter).filter(Boolean)) {
47
52
  for (const ext of ['.exe', '.cmd', '.bat']) {
48
53
  const full = joinPath(dir, cmd + ext);
49
54
  if (!existsSync(full))
@@ -58,6 +63,11 @@ export function resolveSpawnCommand(cmd, args, env = process.env) {
58
63
  if (existsSync(js))
59
64
  return { cmd: process.execPath, args: [js, ...args] };
60
65
  }
66
+ if (cmd.toLowerCase() === 'cursor-agent' && /cursor-agent\.ps1/i.test(shim)) {
67
+ const cursor = resolveCursorAgentBundle(dir, args);
68
+ if (cursor)
69
+ return cursor;
70
+ }
61
71
  }
62
72
  catch { /* unreadable shim — keep searching */ }
63
73
  // A .cmd/.bat whose node entry we can't extract is NOT runnable shell-free — DON'T return it (that would
@@ -66,6 +76,44 @@ export function resolveSpawnCommand(cmd, args, env = process.env) {
66
76
  }
67
77
  return { cmd, args: [...args] };
68
78
  }
79
+ function resolveCursorAgentBundle(dir, args) {
80
+ const direct = cursorBundleAt(dir, args);
81
+ if (direct)
82
+ return direct;
83
+ let versions;
84
+ try {
85
+ versions = readdirSync(joinPath(dir, 'versions'), { withFileTypes: true })
86
+ .filter((entry) => entry.isDirectory() && cursorVersionKey(entry.name) !== undefined)
87
+ .map((entry) => entry.name)
88
+ .sort((a, b) => cursorVersionKey(b).localeCompare(cursorVersionKey(a)));
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ for (const version of versions) {
94
+ const resolved = cursorBundleAt(joinPath(dir, 'versions', version), args);
95
+ if (resolved)
96
+ return resolved;
97
+ }
98
+ return undefined;
99
+ }
100
+ function cursorBundleAt(dir, args) {
101
+ const node = joinPath(dir, 'node.exe');
102
+ const entry = joinPath(dir, 'index.js');
103
+ return existsSync(node) && existsSync(entry) ? { cmd: node, args: [entry, ...args] } : undefined;
104
+ }
105
+ function cursorVersionKey(version) {
106
+ 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);
107
+ if (!match)
108
+ return undefined;
109
+ const year = match[1];
110
+ const month = match[2];
111
+ const day = match[3];
112
+ const hour = match[4] ?? '0';
113
+ const minute = match[5] ?? '0';
114
+ const second = match[6] ?? '0';
115
+ return [year, month, day, hour, minute, second].map((part, index) => index === 0 ? part : part.padStart(2, '0')).join('');
116
+ }
69
117
  /**
70
118
  * Promise wrapper over spawn (shell:false). Optionally writes `input` to stdin; resolves with stdout/exit.
71
119
  * On timeout the WHOLE process group is killed, not just the direct child (finding #39.5): an agent spawns
@@ -76,7 +124,7 @@ export function resolveSpawnCommand(cmd, args, env = process.env) {
76
124
  export function spawnCapture(cmd, args, opts) {
77
125
  return new Promise((resolve, reject) => {
78
126
  const detached = process.platform !== 'win32';
79
- const r = resolveSpawnCommand(cmd, args, opts.env);
127
+ const r = resolveSpawnCommand(cmd, args, opts.env, opts.resolvePlatform ?? process.platform);
80
128
  const child = spawn(r.cmd, r.args, { cwd: opts.cwd, shell: false, detached, ...(opts.env ? { env: opts.env } : {}) });
81
129
  let stdout = '';
82
130
  let stderr = '';
@@ -185,10 +233,10 @@ export function makeCodexHeadlessRunner(opts = {}) {
185
233
  // - `--model <model>` exists (e.g. gpt-5, sonnet-4, sonnet-4-thinking); `--list-models` enumerates.
186
234
  // - auth: `CURSOR_API_KEY` or `--api-key` (the spec's envAllow prefix is CURSOR_).
187
235
  // - 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).
236
+ // STILL NOT run-verified end to end (needs an authed cursor-agent; claude/codex were each run-verified). The
237
+ // Windows launcher is a PowerShell shim and its own version regex rejects the current timestamped version-dir
238
+ // shape. resolveSpawnCommand therefore bypasses both scripts and runs the newest verified node.exe + index.js
239
+ // bundle directly, shell-free. If that known bundle layout cannot be found, the runner still fail-fasts.
192
240
  //
193
241
  // SAFETY: `-p --force --trust` auto-approves shell+write with no verified per-run allowlist/sandbox mapping.
194
242
  // Until Cursor can really map agentOptions into per-run permissions, skipPermissions is refused outright. The
@@ -215,9 +263,14 @@ export function cursorRunnerArgs(opts = {}) {
215
263
  export function makeCursorHeadlessRunner(opts = {}, platform = process.platform) {
216
264
  const args = cursorRunnerArgs(opts);
217
265
  return async (prompt, ctx) => {
218
- assertCursorRunnerPlatformSupported(platform);
266
+ assertCursorRunnerPlatformSupported(platform, ctx.env ?? process.env);
219
267
  try {
220
- const r = await spawnCapture('cursor-agent', [...args, prompt], { cwd: ctx.cwd, timeoutMs: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS, ...(ctx.env ? { env: ctx.env } : {}) });
268
+ const r = await spawnCapture('cursor-agent', [...args, prompt], {
269
+ cwd: ctx.cwd,
270
+ timeoutMs: ctx.timeoutMs ?? DEFAULT_TIMEOUT_MS,
271
+ resolvePlatform: platform,
272
+ ...(ctx.env ? { env: ctx.env } : {}),
273
+ });
221
274
  return r.code === 0 ? { ok: true, output: r.stdout } : { ok: false, output: r.stdout, error: r.stderr || `exit ${r.code}` };
222
275
  }
223
276
  catch (e) {
@@ -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
  /**
@@ -28,8 +28,10 @@ export function defaultReadManifest(repoRoot) {
28
28
  }
29
29
  };
30
30
  }
31
+ const MANIFEST_TRANSIENT_RETRY_MS = 5 * 60 * 1000;
31
32
  // Resettable cache keyed by the reader identity, so distinct readers (e.g. per test) don't bleed into each other.
32
- // `null` = a load was attempted and FAILED (fail-closed); a Set = the loaded obfuscate paths.
33
+ // Success is cached permanently for the reader. Failures remain fail-closed, but transient reads retry for a
34
+ // short window before stabilizing to null for the process lifetime.
33
35
  const cache = new WeakMap();
34
36
  /** Test/maintenance hook: drop any cached load for a reader (or all readers) so the load path re-runs. */
35
37
  export function resetObfuscatedCache(reader) {
@@ -40,11 +42,15 @@ export function resetObfuscatedCache(reader) {
40
42
  /**
41
43
  * Load the obfuscated-file set from the manifest. Returns a normalized Set, or null when the manifest is
42
44
  * missing / unreadable / structurally invalid (NOT an `obfuscate` array of literal string paths). null is the
43
- * fail-closed signal: the caller must then reject all files. Result is cached per reader.
45
+ * fail-closed signal: the caller must then reject all files. Success is cached per reader; failures retry briefly
46
+ * so startup-time transient filesystem errors do not disable self-PR until process restart.
44
47
  */
45
48
  export function loadObfuscatedFiles(readManifest) {
46
- if (cache.has(readManifest))
47
- return cache.get(readManifest) ?? null;
49
+ const cached = cache.get(readManifest);
50
+ if (cached?.kind === 'loaded')
51
+ return cached.files;
52
+ if (cached?.kind === 'failed' && cached.stable)
53
+ return null;
48
54
  let result;
49
55
  try {
50
56
  const raw = readManifest();
@@ -63,9 +69,16 @@ export function loadObfuscatedFiles(readManifest) {
63
69
  result = new Set(manifest.obfuscate.map((f) => normalizeManifestPath(f)).filter(Boolean));
64
70
  }
65
71
  catch {
66
- result = null; // fail-closed: an unreadable/invalid manifest rejects everything
72
+ const now = Date.now();
73
+ const firstFailedAt = cached?.kind === 'failed' ? cached.firstFailedAt : now;
74
+ cache.set(readManifest, {
75
+ kind: 'failed',
76
+ firstFailedAt,
77
+ stable: now - firstFailedAt >= MANIFEST_TRANSIENT_RETRY_MS,
78
+ });
79
+ return null; // fail-closed: an unreadable/invalid manifest rejects everything
67
80
  }
68
- cache.set(readManifest, result);
81
+ cache.set(readManifest, { kind: 'loaded', files: result });
69
82
  return result;
70
83
  }
71
84
  /**
@@ -0,0 +1,90 @@
1
+ export declare const AGENT_DIRECTORY_DEFAULT_LIMIT = 20;
2
+ export declare const AGENT_DIRECTORY_MAX_LIMIT = 50;
3
+ export declare const AGENT_DIRECTORY_DEFAULT_TIMEOUT_MS = 8000;
4
+ export declare const AGENT_DIRECTORY_MAX_TIMEOUT_MS = 30000;
5
+ export declare const AGENT_DIRECTORY_MAX_QUERY_LENGTH = 500;
6
+ export declare const AGENT_DIRECTORY_MAX_SIGNAL_COUNT = 20;
7
+ export declare const AGENT_DIRECTORY_MAX_SIGNAL_LENGTH = 64;
8
+ export declare const AGENT_DIRECTORY_MAX_CURSOR_LENGTH = 256;
9
+ export declare const AGENT_DIRECTORY_MAX_AGENT_ID_LENGTH = 128;
10
+ export type AgentAvailability = 'online' | 'busy' | 'offline' | 'unknown';
11
+ export type AgentDirectorySort = 'relevance' | 'reputation' | 'recent' | 'availability';
12
+ export type SortOrder = 'asc' | 'desc';
13
+ export interface AgentSearchRequest {
14
+ query?: string;
15
+ signals?: readonly string[];
16
+ availability?: AgentAvailability;
17
+ sort?: AgentDirectorySort;
18
+ order?: SortOrder;
19
+ cursor?: string;
20
+ limit?: number;
21
+ timeoutMs?: number;
22
+ }
23
+ export interface AgentTaskDiscoveryRequest {
24
+ title: string;
25
+ description?: string;
26
+ signals?: readonly string[];
27
+ availability?: AgentAvailability;
28
+ sort?: AgentDirectorySort;
29
+ order?: SortOrder;
30
+ cursor?: string;
31
+ limit?: number;
32
+ timeoutMs?: number;
33
+ }
34
+ export interface AgentDirectoryEntry {
35
+ agentId: string;
36
+ displayName?: string;
37
+ summary?: string;
38
+ capabilities?: string[];
39
+ domains?: string[];
40
+ score?: number;
41
+ reputation?: number;
42
+ completedTaskCount?: number;
43
+ availability?: AgentAvailability;
44
+ lastSeenAt?: number;
45
+ }
46
+ export interface AgentProfile {
47
+ agentId: string;
48
+ displayName?: string;
49
+ summary?: string;
50
+ capabilities?: string[];
51
+ domains?: string[];
52
+ reputation?: number;
53
+ completedTaskCount?: number;
54
+ availability?: AgentAvailability;
55
+ lastSeenAt?: number;
56
+ }
57
+ export interface AgentDirectoryPage {
58
+ items: AgentDirectoryEntry[];
59
+ nextCursor?: string;
60
+ hasMore: boolean;
61
+ }
62
+ export type AgentDirectoryFailureCode = 'capability_unavailable' | 'permission_denied' | 'hub_unavailable' | 'timeout' | 'invalid_request' | 'invalid_response';
63
+ export interface AgentDirectoryFailure {
64
+ code: AgentDirectoryFailureCode;
65
+ retryable: boolean;
66
+ message?: string;
67
+ }
68
+ export type AgentDirectoryResult<T> = {
69
+ ok: true;
70
+ value: T;
71
+ } | {
72
+ ok: false;
73
+ error: AgentDirectoryFailure;
74
+ };
75
+ export interface AgentDirectoryCapability {
76
+ search(request: AgentSearchRequest): Promise<AgentDirectoryResult<AgentDirectoryPage>>;
77
+ getProfile(agentId: string, options?: {
78
+ timeoutMs?: number;
79
+ }): Promise<AgentDirectoryResult<AgentProfile | null>>;
80
+ discoverForTask(request: AgentTaskDiscoveryRequest): Promise<AgentDirectoryResult<AgentDirectoryPage>>;
81
+ }
82
+ export declare class AgentDirectoryInputError extends Error {
83
+ readonly code = "invalid_request";
84
+ }
85
+ export declare function normalizeAgentSearchRequest(input: AgentSearchRequest): Required<Pick<AgentSearchRequest, 'limit' | 'timeoutMs' | 'sort' | 'order'>> & AgentSearchRequest;
86
+ export declare function normalizeAgentTaskDiscoveryRequest(input: AgentTaskDiscoveryRequest): AgentTaskDiscoveryRequest & Required<Pick<AgentTaskDiscoveryRequest, 'limit' | 'timeoutMs' | 'sort' | 'order'>>;
87
+ export declare function normalizeAgentId(value: string): string;
88
+ export declare function normalizeAgentDirectoryTimeout(value: number | undefined): number;
89
+ export declare function capabilityUnavailable(message?: string): AgentDirectoryResult<never>;
90
+ export declare function unsupportedAgentDirectoryCapability(message?: string): AgentDirectoryCapability;