@evomap/evolver-core 2.0.0-beta.9 → 2.0.1

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 (201) hide show
  1. package/dist/algo/antiDistill.js +1 -167
  2. package/dist/algo/bans.js +1 -33
  3. package/dist/algo/candidateAssembly.d.ts +7 -2
  4. package/dist/algo/candidateAssembly.js +1 -205
  5. package/dist/algo/capabilityCandidates.js +1 -136
  6. package/dist/algo/confidence.js +1 -196
  7. package/dist/algo/conversationSniffer.js +1 -149
  8. package/dist/algo/cycleEngine.d.ts +23 -2
  9. package/dist/algo/cycleEngine.js +1 -425
  10. package/dist/algo/cycleFailureClassifier.js +1 -112
  11. package/dist/algo/epigenetics.js +1 -41
  12. package/dist/algo/evolutionEvent.js +1 -24
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +1 -55
  15. package/dist/algo/geneHealth.d.ts +2 -2
  16. package/dist/algo/geneHealth.js +1 -16
  17. package/dist/algo/geneIntake.js +1 -128
  18. package/dist/algo/genePromotion.js +1 -53
  19. package/dist/algo/geneSelection.d.ts +63 -5
  20. package/dist/algo/geneSelection.js +1 -246
  21. package/dist/algo/index.d.ts +2 -0
  22. package/dist/algo/index.js +1 -20
  23. package/dist/algo/memoryGraph.js +1 -86
  24. package/dist/algo/mutation.js +1 -22
  25. package/dist/algo/orchestrator.d.ts +8 -1
  26. package/dist/algo/orchestrator.js +1 -75
  27. package/dist/algo/publishEligibility.d.ts +34 -0
  28. package/dist/algo/publishEligibility.js +1 -0
  29. package/dist/algo/solidify.js +1 -63
  30. package/dist/algo/strategyPresets.js +1 -61
  31. package/dist/algo/ucb1.d.ts +53 -0
  32. package/dist/algo/ucb1.js +1 -0
  33. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  34. package/dist/assetstore/assetSidecarRecords.js +238 -0
  35. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  36. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  37. package/dist/assetstore/assetStoreHealth.js +56 -14
  38. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  39. package/dist/assetstore/assetStoreStorage.js +27 -9
  40. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  41. package/dist/assetstore/assetSyncLedger.js +683 -4
  42. package/dist/assetstore/index.d.ts +1 -0
  43. package/dist/assetstore/index.js +1 -0
  44. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  45. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  46. package/dist/assetstore/localJsonl.d.ts +4 -2
  47. package/dist/assetstore/localJsonl.js +89 -9
  48. package/dist/assetstore/provenance.d.ts +80 -4
  49. package/dist/assetstore/provenance.js +313 -5
  50. package/dist/assetstore/provider.d.ts +58 -1
  51. package/dist/assetstore/provider.js +97 -6
  52. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  53. package/dist/benchmark/antiGeneBenchmark.js +1 -251
  54. package/dist/benchmark/antiGeneImpact.js +1 -34
  55. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  56. package/dist/benchmark/antiGeneRollout.js +1 -268
  57. package/dist/benchmark/benchmark.js +1 -26
  58. package/dist/benchmark/evolutionThesisSolver.js +1 -41
  59. package/dist/benchmark/index.d.ts +2 -1
  60. package/dist/benchmark/index.js +1 -6
  61. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  62. package/dist/benchmark/selectionFlatAbstention.js +1 -0
  63. package/dist/benchmark/thesis.js +1 -150
  64. package/dist/benchmark/triggerShift.js +1 -106
  65. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  66. package/dist/bootstrap/envFingerprint.js +5 -0
  67. package/dist/bootstrap/index.d.ts +2 -1
  68. package/dist/bootstrap/index.js +2 -1
  69. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  70. package/dist/bootstrap/v1EnvCompat.js +280 -0
  71. package/dist/cycle/cycleTimeline.js +1 -74
  72. package/dist/cycle/index.js +1 -2
  73. package/dist/cycle/stateMachine.js +1 -25
  74. package/dist/events/eventArchive.d.ts +2 -0
  75. package/dist/events/eventArchive.js +13 -3
  76. package/dist/events/eventSchema.d.ts +7 -7
  77. package/dist/events/eventStore.d.ts +2 -0
  78. package/dist/events/eventStore.js +5 -1
  79. package/dist/events/ingest.d.ts +1 -0
  80. package/dist/events/ingest.js +1 -0
  81. package/dist/events/paths.d.ts +3 -1
  82. package/dist/events/paths.js +4 -0
  83. package/dist/events/public.d.ts +2 -2
  84. package/dist/events/public.js +2 -2
  85. package/dist/events/reports.d.ts +2 -0
  86. package/dist/events/reports.js +4 -0
  87. package/dist/exec/autoExec.d.ts +43 -2
  88. package/dist/exec/autoExec.js +76 -9
  89. package/dist/exec/autonomousCycle.d.ts +22 -4
  90. package/dist/exec/autonomousCycle.js +64 -13
  91. package/dist/exec/claudeBridge.d.ts +32 -7
  92. package/dist/exec/claudeBridge.js +281 -29
  93. package/dist/exec/prompt.js +5 -1
  94. package/dist/exec/runnerRegistry.d.ts +68 -26
  95. package/dist/exec/runnerRegistry.js +307 -72
  96. package/dist/exec/selfPr.js +1 -7
  97. package/dist/feedback/envelope.d.ts +61 -0
  98. package/dist/feedback/envelope.js +168 -0
  99. package/dist/feedback/index.d.ts +1 -0
  100. package/dist/feedback/index.js +1 -0
  101. package/dist/hooks/hooks.js +1 -0
  102. package/dist/hub/agentDirectory.js +1 -104
  103. package/dist/hub/assetCallLog.d.ts +35 -1
  104. package/dist/hub/assetCallLog.js +1 -72
  105. package/dist/hub/bindings.d.ts +8 -1
  106. package/dist/hub/bindings.js +1 -110
  107. package/dist/hub/capability.d.ts +90 -3
  108. package/dist/hub/capability.js +1 -1
  109. package/dist/hub/conversationDistiller.js +1 -264
  110. package/dist/hub/fake.d.ts +2 -2
  111. package/dist/hub/fake.js +1 -70
  112. package/dist/hub/hubReview.js +1 -106
  113. package/dist/hub/index.js +1 -11
  114. package/dist/hub/ingest.js +1 -15
  115. package/dist/hub/questionGenerator.d.ts +5 -1
  116. package/dist/hub/questionGenerator.js +1 -406
  117. package/dist/hub/reuseDecision.js +1 -127
  118. package/dist/hub/sanitize.js +1 -322
  119. package/dist/index.d.ts +3 -1
  120. package/dist/index.js +4 -1
  121. package/dist/mailbox/dispatch.d.ts +1 -1
  122. package/dist/mailbox/dispatch.js +22 -6
  123. package/dist/mailbox/envelope.d.ts +7 -1
  124. package/dist/mailbox/envelope.js +9 -2
  125. package/dist/mailbox/ipcServer.d.ts +10 -2
  126. package/dist/mailbox/ipcServer.js +163 -13
  127. package/dist/mailbox/store.d.ts +89 -3
  128. package/dist/mailbox/store.js +895 -41
  129. package/dist/material/boundary.js +1 -14
  130. package/dist/material/consumer.js +1 -55
  131. package/dist/material/emit.js +1 -52
  132. package/dist/material/factory.js +1 -25
  133. package/dist/material/index.js +1 -9
  134. package/dist/material/materialArchive.js +1 -466
  135. package/dist/material/materialStore.js +1 -69
  136. package/dist/material/sampling.js +1 -39
  137. package/dist/material/sources.js +1 -33
  138. package/dist/material/watermark.js +1 -76
  139. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  140. package/dist/ops/evolutionGraphProjection.js +315 -0
  141. package/dist/ops/index.d.ts +2 -1
  142. package/dist/ops/index.js +2 -1
  143. package/dist/ops/selfUpdate.d.ts +8 -0
  144. package/dist/ops/selfUpdate.js +24 -8
  145. package/dist/personality/drift.js +1 -106
  146. package/dist/personality/events.js +1 -37
  147. package/dist/personality/evolveOps.js +1 -93
  148. package/dist/personality/index.js +1 -11
  149. package/dist/personality/mutate.js +1 -39
  150. package/dist/personality/pivot.js +1 -22
  151. package/dist/personality/prompt.js +1 -62
  152. package/dist/personality/riskGate.js +1 -80
  153. package/dist/personality/schema.d.ts +16 -16
  154. package/dist/personality/schema.js +1 -119
  155. package/dist/personality/select.js +1 -71
  156. package/dist/personality/stats.js +1 -82
  157. package/dist/personality/store.js +1 -92
  158. package/dist/schema/common.js +1 -17
  159. package/dist/schema/evolutionGraph.d.ts +784 -0
  160. package/dist/schema/evolutionGraph.js +1 -0
  161. package/dist/schema/index.d.ts +1 -0
  162. package/dist/schema/index.js +1 -5
  163. package/dist/schema/material.js +1 -50
  164. package/dist/schema/problem.js +1 -45
  165. package/dist/schema/proofOfWork.js +1 -9
  166. package/dist/schema/signal.d.ts +3 -3
  167. package/dist/schema/signal.js +1 -19
  168. package/dist/signals/curriculum.d.ts +55 -0
  169. package/dist/signals/curriculum.js +1 -0
  170. package/dist/signals/cycleHistoryFromEvents.js +1 -96
  171. package/dist/signals/expand.d.ts +15 -1
  172. package/dist/signals/expand.js +1 -125
  173. package/dist/signals/extractor.d.ts +2 -2
  174. package/dist/signals/extractor.js +1 -57
  175. package/dist/signals/index.d.ts +3 -1
  176. package/dist/signals/index.js +1 -6
  177. package/dist/signals/metaSignals.d.ts +4 -0
  178. package/dist/signals/metaSignals.js +1 -158
  179. package/dist/signals/signalGate.js +1 -40
  180. package/dist/signals/taskDomain.d.ts +22 -0
  181. package/dist/signals/taskDomain.js +1 -0
  182. package/dist/signals/traceSignals.js +1 -123
  183. package/dist/strategy/constraintAblation.js +1 -3074
  184. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  185. package/dist/strategy/constraintAblationPredicates.js +1 -0
  186. package/dist/strategy/experiment.js +1 -63
  187. package/dist/strategy/index.js +1 -3
  188. package/dist/strategy/strategyPoint.js +1 -23
  189. package/dist/trace/index.d.ts +3 -1
  190. package/dist/trace/index.js +3 -1
  191. package/dist/trace/learningTrace.d.ts +209 -0
  192. package/dist/trace/learningTrace.js +276 -0
  193. package/dist/trace/proxyTurns.d.ts +31 -0
  194. package/dist/trace/proxyTurns.js +137 -0
  195. package/dist/verify/sandboxRunner.d.ts +28 -0
  196. package/dist/verify/sandboxRunner.js +218 -19
  197. package/dist/verify/sandboxedValidation.d.ts +9 -0
  198. package/dist/verify/sandboxedValidation.js +113 -13
  199. package/dist/verify/validation.d.ts +11 -1
  200. package/dist/verify/validation.js +31 -0
  201. package/package.json +5 -2
@@ -12,19 +12,19 @@
12
12
  // or EVOLVE_EXEC_BRIDGE === '1'. Wiring it in by accident must fail loudly rather than silently spawn an
13
13
  // autonomous agent.
14
14
  import { randomUUID } from 'node:crypto';
15
- import { resolve as resolvePath, sep, join as joinPath } from 'node:path';
16
- import { tmpdir } from 'node:os';
17
- import { closeSync, lstatSync, mkdtempSync, openSync, realpathSync, rmdirSync, rmSync, writeFileSync, } from 'node:fs';
15
+ import { basename, dirname, resolve as resolvePath, sep, join as joinPath } from 'node:path';
16
+ import { homedir, tmpdir } from 'node:os';
17
+ import { closeSync, existsSync, lstatSync, mkdtempSync, openSync, readdirSync, realpathSync, rmdirSync, rmSync, writeFileSync, } from 'node:fs';
18
18
  import { renderExecPrompt } from './prompt.js';
19
19
  import { parseGitShortstat, gitDiffProof } from './proofOfWork.js';
20
20
  // Policy enforcement core (#107): checkPolicy runs the always-on global guards (blast hard cap +
21
21
  // protected paths + destructive deletes) on EVERY exec — gene or not — plus the per-gene constraints when a
22
22
  // gene supplies them. It supersedes the old gene-gated `checkChangeConstraints` call (the no-gene-no-guard hole).
23
23
  import { checkPolicy, summarizeViolations } from './policy/index.js';
24
- import { spawnCapture, SpawnCaptureFinalizeError, getRunnerSpec, DEFAULT_TIMEOUT_MS } from './runnerRegistry.js';
24
+ import { spawnCapture, SpawnCaptureFinalizeError, getRunnerSpec, validateAgentSessionResume, DEFAULT_TIMEOUT_MS } from './runnerRegistry.js';
25
25
  // Re-export the runner layer so existing importers of ./claudeBridge.js (and the `exec` namespace) keep their
26
26
  // surface after the #91-6 split — the registry simply has a clearer home now.
27
- export { resolveSpawnCommand, spawnCapture, DEFAULT_MAX_CAPTURE_BYTES, UnboundedSkipPermissionsError, UnsupportedCursorSkipPermissionsError, UnsupportedGeminiPermissionOptionsError, claudeRunnerArgs, makeClaudeHeadlessRunner, claudeHeadlessRunner, codexRunnerArgs, makeCodexHeadlessRunner, cursorRunnerArgs, makeCursorHeadlessRunner, getRunnerSpec, geminiRunnerArgs, makeGeminiHeadlessRunner, classifyGeminiRunnerResult, } from './runnerRegistry.js';
27
+ export { resolveSpawnCommand, spawnCapture, DEFAULT_MAX_CAPTURE_BYTES, MAX_AGENT_SESSION_ID_CHARS, AgentSessionResumeError, validateAgentSessionResume, UnboundedSkipPermissionsError, UnsupportedCodexPermissionOptionsError, UnsupportedCursorAllowedToolsError, UnsupportedCursorSkipPermissionsError, UnsupportedCursorWorkspaceTrustError, UnsupportedGeminiPermissionOptionsError, claudeRunnerArgs, makeClaudeHeadlessRunner, claudeHeadlessRunner, codexRunnerArgs, makeCodexHeadlessRunner, cursorRunnerArgs, makeCursorHeadlessRunner, getRunnerSpec, hasBoundedClaudeFileAccess, CLAUDE_SAFE_AUTONOMOUS_TOOLS, geminiRunnerArgs, makeGeminiHeadlessRunner, classifyGeminiRunnerResult, } from './runnerRegistry.js';
28
28
  export class ExecBridgeDisabledError extends Error {
29
29
  constructor() {
30
30
  super('exec bridge is disabled — set EVOLVE_EXEC_BRIDGE=1 or pass { enabled: true } to enable agent execution');
@@ -42,12 +42,11 @@ export class ExecBridgeForbiddenError extends Error {
42
42
  * Thrown when a FULL-ACCESS (or unverified) agent run is requested without worktree isolation. The throwaway
43
43
  * worktree is the containment WE control — allowedRoots gates the cwd but cannot stop an auto-approved or
44
44
  * unsandboxed process writing/running outside it. Gated runners:
45
- * - codex with skipPermissions `--sandbox danger-full-access` (no inner OS sandbox). Without skip it stays
46
- * workspace-write, so only the skip path is gated.
45
+ * - codex permission overrides are refused by the runner because it has no enforceable per-tool allowlist.
47
46
  * - cursor default → gated UNCONDITIONALLY. cursor-agent base `-p` already documents write+shell access, cursor
48
47
  * skipPermissions is rejected by the runner layer, and the scaffold remains unverified (#66/#181) — so default
49
48
  * cursor still needs the wrapper worktree rather than risk mutating the real tree.
50
- * claude is exempt: its skip is bounded by --allowedTools (finding #80).
49
+ * Claude is fail-closed below because a tool-name allowlist does not constrain absolute filesystem paths.
51
50
  */
52
51
  export class UnsandboxedFullAccessRequiresIsolationError extends Error {
53
52
  constructor() {
@@ -55,6 +54,31 @@ export class UnsandboxedFullAccessRequiresIsolationError extends Error {
55
54
  this.name = 'UnsandboxedFullAccessRequiresIsolationError';
56
55
  }
57
56
  }
57
+ export class WorkspaceTrustRequiresIsolationError extends Error {
58
+ code = 'WORKSPACE_TRUST_REQUIRES_ISOLATION';
59
+ constructor() {
60
+ super('workspaceTrust=isolated-worktree requires isolation=worktree');
61
+ this.name = 'WorkspaceTrustRequiresIsolationError';
62
+ }
63
+ }
64
+ export class UnsupportedCursorBuiltInRunnerError extends Error {
65
+ constructor() {
66
+ super('built-in cursor autonomous execution is unsupported until host filesystem and network containment is verified');
67
+ this.name = 'UnsupportedCursorBuiltInRunnerError';
68
+ }
69
+ }
70
+ export class UnsupportedClaudeBuiltInRunnerError extends Error {
71
+ constructor() {
72
+ super('built-in claude autonomous execution is unsupported until host filesystem and network containment is verified');
73
+ this.name = 'UnsupportedClaudeBuiltInRunnerError';
74
+ }
75
+ }
76
+ export class UnsupportedCodexBuiltInRunnerError extends Error {
77
+ constructor() {
78
+ super('built-in codex autonomous execution is unsupported because workspace-write does not contain host filesystem reads');
79
+ this.name = 'UnsupportedCodexBuiltInRunnerError';
80
+ }
81
+ }
58
82
  export class UnsafeWorktreePathError extends Error {
59
83
  constructor(reason) {
60
84
  super(`worktree isolation refused: ${reason}`);
@@ -118,6 +142,80 @@ function worktreePathStillOwned(reservation, identity) {
118
142
  return false;
119
143
  }
120
144
  }
145
+ async function verifyManagedCursorWorktree(path, expectedName, repoCwd, git, managedRoot) {
146
+ const resolvedPath = resolvePath(path);
147
+ const cursorRoot = realpathSync(managedRoot);
148
+ if (resolvedPath !== path) {
149
+ throw new UnsafeWorktreePathError('Cursor reported a non-canonical worktree path');
150
+ }
151
+ const realPath = realpathSync(resolvedPath);
152
+ if (!isWithinRoot(realPath, cursorRoot)) {
153
+ throw new UnsafeWorktreePathError('Cursor reported a worktree outside its managed root');
154
+ }
155
+ if (basename(realPath) !== expectedName) {
156
+ throw new UnsafeWorktreePathError('Cursor reported an unexpected worktree name');
157
+ }
158
+ const stat = lstatSync(resolvedPath);
159
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
160
+ throw new UnsafeWorktreePathError('Cursor worktree is not a real directory');
161
+ }
162
+ const [repoCommonDir, worktreeCommonDir] = await Promise.all([
163
+ git(['rev-parse', '--path-format=absolute', '--git-common-dir'], repoCwd),
164
+ git(['rev-parse', '--path-format=absolute', '--git-common-dir'], realPath),
165
+ ]);
166
+ if (realpathSync(repoCommonDir.trim()) !== realpathSync(worktreeCommonDir.trim())) {
167
+ throw new UnsafeWorktreePathError('Cursor worktree belongs to a different repository');
168
+ }
169
+ return { path: realPath, dev: stat.dev, ino: stat.ino };
170
+ }
171
+ async function cleanupManagedCursorWorktree(identity, git, repoCwd) {
172
+ const stat = lstatSync(identity.path);
173
+ if (stat.isSymbolicLink() || !stat.isDirectory() || stat.dev !== identity.dev || stat.ino !== identity.ino) {
174
+ throw new UnsafeWorktreePathError('verified Cursor worktree path changed before cleanup');
175
+ }
176
+ await git(['worktree', 'remove', '--force', identity.path], repoCwd, undefined, { processSignalMode: 'ignore' });
177
+ assertPathAbsent(identity.path);
178
+ }
179
+ async function discoverManagedCursorWorktree(expectedName, repoCwd, git, managedRoot) {
180
+ const cursorRoot = managedRoot;
181
+ if (!existsSync(cursorRoot))
182
+ return undefined;
183
+ const verified = [];
184
+ let validationError;
185
+ for (const entry of readdirSync(cursorRoot, { withFileTypes: true })) {
186
+ if (!entry.isDirectory())
187
+ continue;
188
+ const candidate = joinPath(cursorRoot, entry.name, expectedName);
189
+ if (!existsSync(candidate))
190
+ continue;
191
+ try {
192
+ verified.push(await verifyManagedCursorWorktree(candidate, expectedName, repoCwd, git, cursorRoot));
193
+ }
194
+ catch (error) {
195
+ // Unverified same-name paths are never read or removed.
196
+ validationError ??= error;
197
+ }
198
+ }
199
+ if (verified.length > 1) {
200
+ throw new UnsafeWorktreePathError('multiple verified Cursor worktrees matched the generated name');
201
+ }
202
+ if (verified.length === 0 && validationError)
203
+ throw validationError;
204
+ return verified[0];
205
+ }
206
+ async function resolveManagedCursorWorktree(reportedPath, expectedName, repoCwd, git, managedRoot) {
207
+ if (!reportedPath)
208
+ return discoverManagedCursorWorktree(expectedName, repoCwd, git, managedRoot);
209
+ try {
210
+ return await verifyManagedCursorWorktree(reportedPath, expectedName, repoCwd, git, managedRoot);
211
+ }
212
+ catch (reportedPathError) {
213
+ const discovered = await discoverManagedCursorWorktree(expectedName, repoCwd, git, managedRoot);
214
+ if (discovered)
215
+ return discovered;
216
+ throw reportedPathError;
217
+ }
218
+ }
121
219
  function removeEmptyReservation(reservation) {
122
220
  try {
123
221
  const stat = lstatSync(reservation.container);
@@ -161,11 +259,38 @@ async function cleanupWorktreeReservation(reservation, identity, git, repoCwd) {
161
259
  }
162
260
  removeEmptyReservation(reservation);
163
261
  }
164
- /** Whether `child` is the same as, or nested under, `root` (both resolved to absolute paths). */
165
- function isWithinRoot(child, root) {
166
- const c = resolvePath(child);
167
- const r = resolvePath(root);
168
- return c === r || c.startsWith(r.endsWith(sep) ? r : r + sep);
262
+ /** Resolve existing path components so an allowlisted symlink cannot redirect execution outside its root. */
263
+ function canonicalPath(path) {
264
+ let current = resolvePath(path);
265
+ const missing = [];
266
+ while (true) {
267
+ try {
268
+ return resolvePath(realpathSync(current), ...missing.reverse());
269
+ }
270
+ catch (error) {
271
+ const code = error.code;
272
+ if (code !== 'ENOENT' && code !== 'ENOTDIR')
273
+ return undefined;
274
+ const parent = dirname(current);
275
+ if (parent === current)
276
+ return undefined;
277
+ missing.push(basename(current));
278
+ current = parent;
279
+ }
280
+ }
281
+ }
282
+ /** Whether `child` is the same as, or nested under, `root`, including filesystem symlink resolution. */
283
+ export function isWithinRoot(child, root) {
284
+ return canonicalPathWithinRoots(child, [root]) !== undefined;
285
+ }
286
+ function canonicalPathWithinRoots(child, roots) {
287
+ const c = canonicalPath(child);
288
+ if (!c)
289
+ return undefined;
290
+ return roots.some((root) => {
291
+ const r = canonicalPath(root);
292
+ return Boolean(r && (c === r || c.startsWith(r.endsWith(sep) ? r : r + sep)));
293
+ }) ? c : undefined;
169
294
  }
170
295
  /**
171
296
  * Sensitive env keys to strip before spawning an agent/tool (finding #39.2): evolver/hub/cloud secrets must
@@ -237,6 +362,12 @@ class ExecBridgeRunCancelledError extends Error {
237
362
  this.name = 'ExecBridgeRunCancelledError';
238
363
  }
239
364
  }
365
+ class AgentRunBeforeManagedWorktreeError extends Error {
366
+ constructor() {
367
+ super('agent run failed before a managed worktree was created');
368
+ this.name = 'AgentRunBeforeManagedWorktreeError';
369
+ }
370
+ }
240
371
  class GitProofError extends Error {
241
372
  constructor(message) {
242
373
  super(message);
@@ -268,6 +399,15 @@ function failedProofExecutionResult(run, error, proofOfWork) {
268
399
  sessionLog: run.error ? `${run.output}\n${run.error}` : run.output,
269
400
  };
270
401
  }
402
+ function failedAgentRunResult(run) {
403
+ return {
404
+ outcome: { status: 'failed', score: 0.1, reason: run.error ?? run.failureKind ?? 'agent run failed' },
405
+ strongEvidence: false,
406
+ ...(run.failureKind !== undefined ? { failureKind: run.failureKind } : {}),
407
+ ...(run.exitCode !== undefined ? { exitCode: run.exitCode } : {}),
408
+ sessionLog: run.error ? `${run.output}\n${run.error}` : run.output,
409
+ };
410
+ }
271
411
  /** Default git runner: every incomplete command result fails closed. */
272
412
  export const defaultGitRunner = async (args, cwd, signal, options) => {
273
413
  let result;
@@ -341,10 +481,29 @@ function writePrivatePatchFile(path, patch) {
341
481
  * Build the `execute` function CycleEngine/runEvolutionCycle consume. Default-off: throws
342
482
  * ExecBridgeDisabledError on first call unless enabled.
343
483
  */
344
- export function makeClaudeExecBridge(opts) {
484
+ export function makeClaudeExecBridge(opts, internal) {
485
+ if (internal?.cursorWorktreeRoot && !opts.agent) {
486
+ throw new Error('custom Cursor worktree root requires an injected agent');
487
+ }
488
+ const cursorWorktreeRoot = internal?.cursorWorktreeRoot ?? joinPath(homedir(), '.cursor', 'worktrees');
345
489
  const enabled = opts.enabled ?? (process.env['EVOLVE_EXEC_BRIDGE'] === '1');
346
- const spec = getRunnerSpec(opts.runner); // #66: claude (default, byte-identical) | codex
347
- const agent = opts.agent ?? spec.makeRunner(opts.agentOptions);
490
+ if (opts.agentOptions?.workspaceTrust === 'isolated-worktree' && opts.isolation !== 'worktree') {
491
+ throw new WorkspaceTrustRequiresIsolationError();
492
+ }
493
+ const runnerName = opts.runner ?? 'claude';
494
+ const unsupportedBuiltInClaude = runnerName === 'claude' && !opts.agent;
495
+ const unsupportedBuiltInCodex = runnerName === 'codex' && !opts.agent;
496
+ const spec = getRunnerSpec(runnerName); // #66: provider-neutral runner registry
497
+ const resume = opts.resume ? validateAgentSessionResume(opts.resume, spec.name) : undefined;
498
+ // Do not construct uncontained built-in runners. Keep the bridge factory side-effect free so
499
+ // default-OFF callers retain the documented first-call failure semantics.
500
+ const agent = opts.agent ?? (unsupportedBuiltInClaude || unsupportedBuiltInCodex
501
+ ? (() => {
502
+ if (unsupportedBuiltInCodex)
503
+ throw new UnsupportedCodexBuiltInRunnerError();
504
+ throw new UnsupportedClaudeBuiltInRunnerError();
505
+ })
506
+ : spec.makeRunner(opts.agentOptions));
348
507
  const git = opts.git ?? defaultGitRunner;
349
508
  const gitPatchWriter = opts.gitPatchWriter ?? (git === defaultGitRunner ? defaultGitPatchWriter : undefined);
350
509
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -353,24 +512,39 @@ export function makeClaudeExecBridge(opts) {
353
512
  // Fail-fast when a run that can write/execute against the tree with no inner sandbox we control is requested
354
513
  // without the throwaway worktree as containment (allowedRoots gates the cwd but can't stop a write outside it).
355
514
  // Only when WE build the runner — an injected `agent` bypasses the built-in runner:
356
- // - codex: only its skip path emits --sandbox danger-full-access; without skip it stays workspace-write.
515
+ // - codex: fail-closed above because workspace-write does not contain host reads.
357
516
  // - cursor: gated UNCONDITIONALLY. cursor-agent base `-p` already has write+shell access, its skipPermissions
358
517
  // path is refused by runnerRegistry, and the runner is an unverified scaffold (#66/#181), so we do not let
359
518
  // default cursor touch the real tree until run-verified. (Bugbot High #181)
360
- // claude is exempt its skip is bounded by --allowedTools (finding #80).
361
- const needsIsolation = opts.runner === 'cursor' || opts.runner === 'gemini'
362
- || (opts.runner === 'codex' && opts.agentOptions?.skipPermissions === true);
519
+ // Built-in Claude and Codex are fail-closed above.
520
+ const needsIsolation = opts.runner === 'cursor' || opts.runner === 'gemini';
363
521
  if (!opts.agent && needsIsolation && opts.isolation !== 'worktree') {
364
522
  throw new UnsandboxedFullAccessRequiresIsolationError();
365
523
  }
524
+ if (opts.runner === 'cursor' && !opts.agent) {
525
+ throw new UnsupportedCursorBuiltInRunnerError();
526
+ }
366
527
  return async (mutation, decision) => {
367
528
  if (!enabled)
368
529
  throw new ExecBridgeDisabledError();
369
530
  // Deny-by-default guardrail: an autonomous agent may only edit allowlisted repos. Checked before the
370
531
  // agent is ever spawned, so a forbidden cwd never runs anything.
371
- if (opts.allowedRoots !== undefined && !opts.allowedRoots.some((root) => isWithinRoot(opts.cwd, root))) {
532
+ const repoCwd = opts.allowedRoots === undefined
533
+ ? opts.cwd
534
+ : canonicalPathWithinRoots(opts.cwd, opts.allowedRoots);
535
+ if (!repoCwd) {
372
536
  throw new ExecBridgeForbiddenError(opts.cwd);
373
537
  }
538
+ if (unsupportedBuiltInClaude)
539
+ throw new UnsupportedClaudeBuiltInRunnerError();
540
+ if (unsupportedBuiltInCodex)
541
+ throw new UnsupportedCodexBuiltInRunnerError();
542
+ const assertRepoCwdStillAllowed = () => {
543
+ if (opts.allowedRoots !== undefined
544
+ && canonicalPathWithinRoots(repoCwd, opts.allowedRoots) !== repoCwd) {
545
+ throw new ExecBridgeForbiddenError(opts.cwd);
546
+ }
547
+ };
374
548
  const resolveId = decision.selectedAssetId ?? decision.selectedGeneId;
375
549
  const resolved = resolveId && opts.resolveGene ? await opts.resolveGene(resolveId) : null;
376
550
  // Trust gate (finding #39.3): for unattended runs, only embed a gene we trust — an untrusted gene's strategy
@@ -384,14 +558,18 @@ export function makeClaudeExecBridge(opts) {
384
558
  // use-case ①: inject the personality style block from the state applySelectForRun just persisted.
385
559
  ...(opts.personality ? { personality: opts.personality.currentState() } : {}),
386
560
  });
561
+ assertRepoCwdStillAllowed();
387
562
  // Isolation: atomically reserve an unpredictable private temp container, then let git create the absent
388
563
  // worktree path inside it. Cleanup is armed only after git succeeds and the resulting directory is verified.
389
564
  const isolate = opts.isolation === 'worktree';
565
+ const managedCursorIsolation = isolate && opts.runner === 'cursor' && resume?.runner === 'cursor';
390
566
  if (opts.signal?.aborted)
391
567
  return cancelledExecutionResult(undefined);
392
568
  const reservation = isolate ? reserveWorktreePath() : undefined;
393
- const workDir = reservation?.workDir ?? opts.cwd;
569
+ let workDir = reservation?.workDir ?? repoCwd;
394
570
  let worktreeIdentity;
571
+ let managedCursorIdentity;
572
+ const managedWorktreeName = managedCursorIsolation ? `evolver-${randomUUID()}` : undefined;
395
573
  let result;
396
574
  let observedRun;
397
575
  let patchRef;
@@ -401,6 +579,8 @@ export function makeClaudeExecBridge(opts) {
401
579
  const proofGit = async (args, cwd, checkCancellationAfter = true) => {
402
580
  if (opts.signal?.aborted)
403
581
  throw new ExecBridgeRunCancelledError();
582
+ if (cwd === repoCwd)
583
+ assertRepoCwdStillAllowed();
404
584
  try {
405
585
  const output = await git(args, cwd, opts.signal);
406
586
  if (checkCancellationAfter && opts.signal?.aborted)
@@ -417,18 +597,63 @@ export function makeClaudeExecBridge(opts) {
417
597
  };
418
598
  try {
419
599
  if (reservation) {
420
- await proofGit(['worktree', 'add', '--detach', workDir, 'HEAD'], opts.cwd, false);
600
+ await proofGit(['worktree', 'add', '--detach', workDir, 'HEAD'], repoCwd, false);
421
601
  worktreeIdentity = verifyWorktreePath(reservation);
422
602
  if (opts.signal?.aborted)
423
603
  throw new ExecBridgeRunCancelledError();
424
604
  }
605
+ if (!reservation)
606
+ assertRepoCwdStillAllowed();
425
607
  const run = await agent(prompt, {
426
608
  cwd: workDir,
427
609
  timeoutMs,
428
610
  ...(agentEnv ? { env: agentEnv } : {}),
429
611
  ...(opts.signal ? { signal: opts.signal } : {}),
612
+ ...(resume ? { resume } : {}),
613
+ ...(managedWorktreeName ? { managedWorktreeName } : {}),
430
614
  });
431
615
  observedRun = run;
616
+ if (managedWorktreeName) {
617
+ try {
618
+ assertRepoCwdStillAllowed();
619
+ managedCursorIdentity = await resolveManagedCursorWorktree(run.managedWorktreePath, managedWorktreeName, repoCwd, git, cursorWorktreeRoot);
620
+ }
621
+ catch (error) {
622
+ if (run.failureKind === 'cancelled' || opts.signal?.aborted)
623
+ throw new ExecBridgeRunCancelledError();
624
+ if (!run.ok)
625
+ throw new AgentRunBeforeManagedWorktreeError();
626
+ if (error instanceof UnsafeWorktreePathError) {
627
+ throw new GitProofError(`Cursor managed worktree verification failed: ${error.message}`);
628
+ }
629
+ throw error;
630
+ }
631
+ if (!managedCursorIdentity) {
632
+ if (run.failureKind === 'cancelled' || opts.signal?.aborted)
633
+ throw new ExecBridgeRunCancelledError();
634
+ if (!run.ok)
635
+ throw new AgentRunBeforeManagedWorktreeError();
636
+ throw new GitProofError('Cursor managed worktree could not be verified');
637
+ }
638
+ workDir = managedCursorIdentity.path;
639
+ }
640
+ // Learning trace (slice 2): one model.called per agent spawn — the headless runner is one opaque
641
+ // model-driven turn from the bridge's viewpoint (per-request fidelity arrives via the proxy's llm_turn
642
+ // records; recordLlmTurn folds those when a caller has them). Best-effort: never affects the result.
643
+ try {
644
+ opts.traceRecorder?.modelCalled({
645
+ ...(opts.runner !== undefined ? { provider: opts.runner } : { provider: 'claude' }),
646
+ ...(opts.agentOptions?.model !== undefined ? { model: opts.agentOptions.model } : {}),
647
+ ...(run.exitCode !== undefined && run.exitCode !== null ? { stopReason: `exit_${run.exitCode}` } : {}),
648
+ });
649
+ if (!run.ok) {
650
+ opts.traceRecorder?.toolFailed({
651
+ toolName: 'agent_runner',
652
+ error: run.error ?? run.failureKind ?? 'agent run failed',
653
+ });
654
+ }
655
+ }
656
+ catch { /* trace emission is observability only */ }
432
657
  if (run.failureKind === 'cancelled' || opts.signal?.aborted)
433
658
  throw new ExecBridgeRunCancelledError();
434
659
  // A worktree can contain three independent change surfaces after the agent exits: staged tracked changes,
@@ -456,6 +681,8 @@ export function makeClaudeExecBridge(opts) {
456
681
  numstat = await proofGit(['diff', '--numstat', 'HEAD'], workDir);
457
682
  if (isolate && stat.files > 0) {
458
683
  if (gitPatchWriter) {
684
+ if (workDir === repoCwd)
685
+ assertRepoCwdStillAllowed();
459
686
  patchRef = joinPath(tmpdir(), `evolver-patch-${randomUUID()}.diff`);
460
687
  try {
461
688
  await gitPatchWriter(['diff', '--binary', '--full-index', 'HEAD'], workDir, patchRef, opts.signal, () => { ownsPatchRef = true; });
@@ -567,13 +794,26 @@ export function makeClaudeExecBridge(opts) {
567
794
  if (error instanceof ExecBridgeRunCancelledError || opts.signal?.aborted) {
568
795
  result = cancelledExecutionResult(observedRun);
569
796
  }
797
+ else if (error instanceof AgentRunBeforeManagedWorktreeError && observedRun) {
798
+ result = failedAgentRunResult(observedRun);
799
+ }
570
800
  else if (error instanceof GitProofError && observedRun) {
571
801
  result = failedProofExecutionResult(observedRun, error, failedProof);
572
802
  }
573
803
  else {
804
+ if (managedCursorIdentity) {
805
+ try {
806
+ assertRepoCwdStillAllowed();
807
+ await cleanupManagedCursorWorktree(managedCursorIdentity, git, repoCwd);
808
+ }
809
+ catch {
810
+ // Preserve the primary execution error; the verified worktree remains diagnosable.
811
+ }
812
+ }
574
813
  if (reservation) {
575
814
  try {
576
- await cleanupWorktreeReservation(reservation, worktreeIdentity, git, opts.cwd);
815
+ assertRepoCwdStillAllowed();
816
+ await cleanupWorktreeReservation(reservation, worktreeIdentity, git, repoCwd);
577
817
  }
578
818
  catch {
579
819
  // Preserve the primary execution error. The abandoned reservation remains private and diagnosable.
@@ -590,17 +830,29 @@ export function makeClaudeExecBridge(opts) {
590
830
  catch { /* best-effort cleanup must not replace the execution result or its primary failure */ }
591
831
  }
592
832
  }
833
+ let cleanupError;
834
+ if (managedCursorIdentity) {
835
+ try {
836
+ assertRepoCwdStillAllowed();
837
+ await cleanupManagedCursorWorktree(managedCursorIdentity, git, repoCwd);
838
+ }
839
+ catch (error) {
840
+ cleanupError = error;
841
+ }
842
+ }
593
843
  if (reservation) {
594
844
  try {
595
- await cleanupWorktreeReservation(reservation, worktreeIdentity, git, opts.cwd);
845
+ assertRepoCwdStillAllowed();
846
+ await cleanupWorktreeReservation(reservation, worktreeIdentity, git, repoCwd);
596
847
  }
597
848
  catch (error) {
598
- // A successful run must not hide abandoned edits/resources. Failed and cancelled results retain their
599
- // primary classification; the private reservation remains available for diagnosis.
600
- if (result.outcome.status === 'success')
601
- throw error;
849
+ cleanupError ??= error;
602
850
  }
603
851
  }
852
+ // A successful run must not hide abandoned edits/resources. Both cleanup paths are attempted first so one
853
+ // failure cannot strand the other worktree. Failed and cancelled results retain their primary classification.
854
+ if (cleanupError && result.outcome.status === 'success')
855
+ throw cleanupError;
604
856
  return result;
605
857
  };
606
858
  }
@@ -5,6 +5,7 @@ import { renderPersonalityBlock } from '../personality/prompt.js';
5
5
  // trust gate (only embed trusted gene strategies); this just blunts the obvious "ignore your instructions" /
6
6
  // fake-role-tag attacks in whatever content does flow through. Kept tight to avoid redacting real strategy text.
7
7
  const INJECTION_PATTERNS = [
8
+ /(?<![a-z0-9])(?:ignore|disregard|forget)[_-]+(?:all[_-]+)?(?:previous|above|prior|preceding|earlier)[_-]+(?:instructions?|prompts?|context|rules?|messages?)(?:[_-]+[a-z0-9]+){0,12}/gi,
8
9
  /\b(ignore|disregard|forget)\b[^.\n]{0,40}\b(previous|above|prior|preceding|earlier|all)\b[^.\n]{0,30}\b(instruction|prompt|context|rule|message)/gi,
9
10
  /\b(new|updated|real|actual)\b[^.\n]{0,20}\b(instruction|system prompt|task|directive)s?\s*:/gi,
10
11
  /<\/?\s*(system|user|assistant|instructions?|im_start|im_end)\s*>/gi,
@@ -42,6 +43,9 @@ function renderAntiWarning(warning, index, sanitize) {
42
43
  export function renderExecPrompt(input) {
43
44
  const { mutation: m, decision: d, gene, validationCmds, personality } = input;
44
45
  const s = sanitizeInjection; // every embedded (potentially untrusted) field is sanitized (finding #39.3)
46
+ // Hub capability gaps may steer curriculum selection, but they are control-plane data rather than executable
47
+ // task instructions. Keep their raw values out of the real-agent prompt while retaining ordinary/local signals.
48
+ const promptSignals = m.trigger_signals.filter((signal) => !signal.toLowerCase().startsWith('curriculum_target:gap:'));
45
49
  const lines = [
46
50
  'You are an autonomous coding agent applying ONE focused, minimal change.',
47
51
  '',
@@ -51,7 +55,7 @@ export function renderExecPrompt(input) {
51
55
  '## Target',
52
56
  `Area/file: ${s(m.target)}`,
53
57
  `Category: ${m.category} Risk: ${m.risk_level}`,
54
- `Triggering signals: ${s(m.trigger_signals.join(', ')) || '(none)'}`,
58
+ `Triggering signals: ${s(promptSignals.join(', ')) || '(none)'}`,
55
59
  ];
56
60
  if (d.selectedGeneId && gene) {
57
61
  lines.push('', `## Strategy (learned gene ${d.selectedGeneId})`);