@evomap/evolver-core 2.0.0-beta.0 → 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.
- package/assets/gep/genes.jsonl +5 -0
- package/dist/algo/candidateAssembly.js +17 -11
- 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.js +2 -0
- 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 +6 -0
- package/dist/algo/geneSelection.js +19 -11
- package/dist/algo/index.d.ts +1 -0
- package/dist/algo/index.js +1 -0
- package/dist/assetstore/assetSyncLedger.d.ts +28 -0
- package/dist/assetstore/assetSyncLedger.js +86 -0
- package/dist/assetstore/index.d.ts +3 -1
- package/dist/assetstore/index.js +3 -1
- package/dist/assetstore/localJsonl.d.ts +9 -2
- package/dist/assetstore/localJsonl.js +75 -23
- package/dist/assetstore/pendingSignals.js +3 -1
- package/dist/assetstore/provenance.d.ts +7 -2
- package/dist/assetstore/provenance.js +85 -25
- package/dist/assetstore/reviewFilter.js +2 -1
- package/dist/assetstore/reviewLedger.d.ts +6 -0
- package/dist/assetstore/reviewLedger.js +9 -0
- 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/events/eventArchive.d.ts +65 -0
- package/dist/events/eventArchive.js +343 -0
- package/dist/events/eventStore.js +2 -12
- 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.js +3 -0
- package/dist/exec/claudeBridge.js +23 -0
- package/dist/exec/runnerRegistry.d.ts +8 -6
- package/dist/exec/runnerRegistry.js +71 -18
- 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 +13 -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 +58 -6
- 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/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/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 +2 -1
package/dist/events/retention.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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,
|
|
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: '
|
|
94
|
+
reason: 'archive rotation preserves replay history; destructive archive deletion remains disabled',
|
|
63
95
|
};
|
|
64
96
|
}
|
|
65
|
-
function materialSnapshot(path,
|
|
97
|
+
function materialSnapshot(path, cursorPaths, thresholds) {
|
|
66
98
|
const stats = readJsonl(path, (value) => material.parse(value));
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
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:
|
|
79
|
-
lastCapturedAt:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
85
|
-
pending: Math.max(0,
|
|
136
|
+
consumedPrefix: effectiveMinimum,
|
|
137
|
+
pending: Math.max(0, historyRecords - effectiveCursor),
|
|
138
|
+
archiveRotationSupported: true,
|
|
139
|
+
archiveRotationSafe,
|
|
86
140
|
destructivePruneSafe: false,
|
|
87
|
-
reason: '
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
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;
|
|
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;
|
|
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
|
|
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)
|
|
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 >
|
|
162
|
-
out.push(`material: ${mat.consumedPrefix} consumed prefix record(s)
|
|
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;
|
package/dist/exec/autoExec.js
CHANGED
|
@@ -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
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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 (
|
|
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
|
-
|
|
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).
|
|
189
|
-
//
|
|
190
|
-
// resolveSpawnCommand
|
|
191
|
-
//
|
|
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], {
|
|
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.
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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;
|