@evomap/evolver-core 2.0.0-beta.0
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/dist/algo/antiDistill.d.ts +35 -0
- package/dist/algo/antiDistill.js +167 -0
- package/dist/algo/bans.d.ts +6 -0
- package/dist/algo/bans.js +33 -0
- package/dist/algo/candidateAssembly.d.ts +65 -0
- package/dist/algo/candidateAssembly.js +196 -0
- package/dist/algo/capabilityCandidates.d.ts +38 -0
- package/dist/algo/capabilityCandidates.js +131 -0
- package/dist/algo/confidence.d.ts +106 -0
- package/dist/algo/confidence.js +196 -0
- package/dist/algo/cycleEngine.d.ts +143 -0
- package/dist/algo/cycleEngine.js +417 -0
- package/dist/algo/cycleFailureClassifier.d.ts +52 -0
- package/dist/algo/cycleFailureClassifier.js +104 -0
- package/dist/algo/epigenetics.d.ts +14 -0
- package/dist/algo/epigenetics.js +41 -0
- package/dist/algo/evolutionEvent.d.ts +23 -0
- package/dist/algo/evolutionEvent.js +24 -0
- package/dist/algo/exploration.d.ts +34 -0
- package/dist/algo/exploration.js +55 -0
- package/dist/algo/geneHealth.d.ts +24 -0
- package/dist/algo/geneHealth.js +16 -0
- package/dist/algo/geneIntake.d.ts +37 -0
- package/dist/algo/geneIntake.js +100 -0
- package/dist/algo/genePromotion.d.ts +48 -0
- package/dist/algo/genePromotion.js +53 -0
- package/dist/algo/geneSelection.d.ts +128 -0
- package/dist/algo/geneSelection.js +222 -0
- package/dist/algo/index.d.ts +18 -0
- package/dist/algo/index.js +18 -0
- package/dist/algo/mutation.d.ts +12 -0
- package/dist/algo/mutation.js +22 -0
- package/dist/algo/orchestrator.d.ts +65 -0
- package/dist/algo/orchestrator.js +63 -0
- package/dist/algo/solidify.d.ts +43 -0
- package/dist/algo/solidify.js +63 -0
- package/dist/algo/strategyPresets.d.ts +32 -0
- package/dist/algo/strategyPresets.js +61 -0
- package/dist/assetstore/index.d.ts +8 -0
- package/dist/assetstore/index.js +8 -0
- package/dist/assetstore/learningHistory.d.ts +25 -0
- package/dist/assetstore/learningHistory.js +38 -0
- package/dist/assetstore/localJsonl.d.ts +34 -0
- package/dist/assetstore/localJsonl.js +182 -0
- package/dist/assetstore/pendingSignals.d.ts +22 -0
- package/dist/assetstore/pendingSignals.js +171 -0
- package/dist/assetstore/provenance.d.ts +39 -0
- package/dist/assetstore/provenance.js +109 -0
- package/dist/assetstore/provider.d.ts +48 -0
- package/dist/assetstore/provider.js +36 -0
- package/dist/assetstore/remoteStub.d.ts +32 -0
- package/dist/assetstore/remoteStub.js +36 -0
- package/dist/assetstore/reviewFilter.d.ts +20 -0
- package/dist/assetstore/reviewFilter.js +44 -0
- package/dist/assetstore/reviewLedger.d.ts +55 -0
- package/dist/assetstore/reviewLedger.js +121 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +79 -0
- package/dist/benchmark/antiGeneBenchmark.js +241 -0
- package/dist/benchmark/antiGeneRollout.d.ts +80 -0
- package/dist/benchmark/antiGeneRollout.js +256 -0
- package/dist/benchmark/benchmark.d.ts +32 -0
- package/dist/benchmark/benchmark.js +26 -0
- package/dist/benchmark/evolutionThesisSolver.d.ts +25 -0
- package/dist/benchmark/evolutionThesisSolver.js +41 -0
- package/dist/benchmark/index.d.ts +5 -0
- package/dist/benchmark/index.js +5 -0
- package/dist/benchmark/thesis.d.ts +85 -0
- package/dist/benchmark/thesis.js +150 -0
- package/dist/bootstrap/envFingerprint.d.ts +45 -0
- package/dist/bootstrap/envFingerprint.js +72 -0
- package/dist/bootstrap/index.d.ts +2 -0
- package/dist/bootstrap/index.js +2 -0
- package/dist/bootstrap/selfEvolve.d.ts +36 -0
- package/dist/bootstrap/selfEvolve.js +26 -0
- package/dist/cycle/cycleTimeline.d.ts +35 -0
- package/dist/cycle/cycleTimeline.js +74 -0
- package/dist/cycle/index.d.ts +2 -0
- package/dist/cycle/index.js +2 -0
- package/dist/cycle/stateMachine.d.ts +6 -0
- package/dist/cycle/stateMachine.js +25 -0
- package/dist/daemon/cycleContext.d.ts +12 -0
- package/dist/daemon/cycleContext.js +13 -0
- package/dist/daemon/drain.d.ts +11 -0
- package/dist/daemon/drain.js +16 -0
- package/dist/daemon/guards.d.ts +87 -0
- package/dist/daemon/guards.js +142 -0
- package/dist/daemon/idempotency.d.ts +11 -0
- package/dist/daemon/idempotency.js +36 -0
- package/dist/daemon/idleScheduler.d.ts +55 -0
- package/dist/daemon/idleScheduler.js +181 -0
- package/dist/daemon/index.d.ts +8 -0
- package/dist/daemon/index.js +8 -0
- package/dist/daemon/stepRunner.d.ts +12 -0
- package/dist/daemon/stepRunner.js +31 -0
- package/dist/daemon/unhandledRejectionWindow.d.ts +21 -0
- package/dist/daemon/unhandledRejectionWindow.js +52 -0
- package/dist/daemon/watchdog.d.ts +11 -0
- package/dist/daemon/watchdog.js +14 -0
- package/dist/events/eventSchema.d.ts +132 -0
- package/dist/events/eventSchema.js +30 -0
- package/dist/events/eventStore.d.ts +32 -0
- package/dist/events/eventStore.js +137 -0
- package/dist/events/ingest.d.ts +34 -0
- package/dist/events/ingest.js +61 -0
- package/dist/events/paths.d.ts +22 -0
- package/dist/events/paths.js +44 -0
- package/dist/events/projectors.d.ts +9 -0
- package/dist/events/projectors.js +11 -0
- package/dist/events/public.d.ts +14 -0
- package/dist/events/public.js +8 -0
- package/dist/events/replayer.d.ts +26 -0
- package/dist/events/replayer.js +67 -0
- package/dist/events/reports.d.ts +90 -0
- package/dist/events/reports.js +244 -0
- package/dist/events/retention.d.ts +70 -0
- package/dist/events/retention.js +177 -0
- package/dist/events/sink.d.ts +5 -0
- package/dist/events/sink.js +1 -0
- package/dist/exec/autoExec.d.ts +137 -0
- package/dist/exec/autoExec.js +221 -0
- package/dist/exec/autonomousCycle.d.ts +48 -0
- package/dist/exec/autonomousCycle.js +78 -0
- package/dist/exec/claudeBridge.d.ts +112 -0
- package/dist/exec/claudeBridge.js +219 -0
- package/dist/exec/index.d.ts +9 -0
- package/dist/exec/index.js +11 -0
- package/dist/exec/openPrRegistry.d.ts +60 -0
- package/dist/exec/openPrRegistry.js +130 -0
- package/dist/exec/policy/blastRadius.d.ts +31 -0
- package/dist/exec/policy/blastRadius.js +67 -0
- package/dist/exec/policy/constraints.d.ts +26 -0
- package/dist/exec/policy/constraints.js +62 -0
- package/dist/exec/policy/destructive.d.ts +45 -0
- package/dist/exec/policy/destructive.js +108 -0
- package/dist/exec/policy/failureMode.d.ts +33 -0
- package/dist/exec/policy/failureMode.js +49 -0
- package/dist/exec/policy/index.d.ts +24 -0
- package/dist/exec/policy/index.js +30 -0
- package/dist/exec/policy/protectedPaths.d.ts +17 -0
- package/dist/exec/policy/protectedPaths.js +65 -0
- package/dist/exec/policyCheck.d.ts +2 -0
- package/dist/exec/policyCheck.js +7 -0
- package/dist/exec/prompt.d.ts +34 -0
- package/dist/exec/prompt.js +108 -0
- package/dist/exec/proofOfWork.d.ts +9 -0
- package/dist/exec/proofOfWork.js +11 -0
- package/dist/exec/runnerRegistry.d.ts +123 -0
- package/dist/exec/runnerRegistry.js +237 -0
- package/dist/exec/selfPr.d.ts +83 -0
- package/dist/exec/selfPr.js +104 -0
- package/dist/exec/selfPrObfuscation.d.ts +21 -0
- package/dist/exec/selfPrObfuscation.js +85 -0
- package/dist/hooks/hooks.d.ts +117 -0
- package/dist/hooks/hooks.js +89 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hub/assetCallLog.d.ts +43 -0
- package/dist/hub/assetCallLog.js +72 -0
- package/dist/hub/bindings.d.ts +28 -0
- package/dist/hub/bindings.js +90 -0
- package/dist/hub/capability.d.ts +278 -0
- package/dist/hub/capability.js +1 -0
- package/dist/hub/conversationDistiller.d.ts +88 -0
- package/dist/hub/conversationDistiller.js +264 -0
- package/dist/hub/fake.d.ts +45 -0
- package/dist/hub/fake.js +69 -0
- package/dist/hub/hubReview.d.ts +91 -0
- package/dist/hub/hubReview.js +106 -0
- package/dist/hub/index.d.ts +10 -0
- package/dist/hub/index.js +10 -0
- package/dist/hub/ingest.d.ts +15 -0
- package/dist/hub/ingest.js +15 -0
- package/dist/hub/questionGenerator.d.ts +59 -0
- package/dist/hub/questionGenerator.js +406 -0
- package/dist/hub/reuseDecision.d.ts +87 -0
- package/dist/hub/reuseDecision.js +127 -0
- package/dist/hub/sanitize.d.ts +52 -0
- package/dist/hub/sanitize.js +321 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/mailbox/catalog.d.ts +17 -0
- package/dist/mailbox/catalog.js +53 -0
- package/dist/mailbox/daemon.d.ts +48 -0
- package/dist/mailbox/daemon.js +61 -0
- package/dist/mailbox/dispatch.d.ts +46 -0
- package/dist/mailbox/dispatch.js +62 -0
- package/dist/mailbox/envelope.d.ts +43 -0
- package/dist/mailbox/envelope.js +52 -0
- package/dist/mailbox/index.d.ts +8 -0
- package/dist/mailbox/index.js +8 -0
- package/dist/mailbox/ipcServer.d.ts +46 -0
- package/dist/mailbox/ipcServer.js +168 -0
- package/dist/mailbox/requestReply.d.ts +40 -0
- package/dist/mailbox/requestReply.js +77 -0
- package/dist/mailbox/store.d.ts +74 -0
- package/dist/mailbox/store.js +308 -0
- package/dist/mailbox/wake.d.ts +45 -0
- package/dist/mailbox/wake.js +60 -0
- package/dist/material/boundary.d.ts +7 -0
- package/dist/material/boundary.js +14 -0
- package/dist/material/consumer.d.ts +21 -0
- package/dist/material/consumer.js +52 -0
- package/dist/material/emit.d.ts +55 -0
- package/dist/material/emit.js +52 -0
- package/dist/material/factory.d.ts +27 -0
- package/dist/material/factory.js +25 -0
- package/dist/material/index.d.ts +8 -0
- package/dist/material/index.js +8 -0
- package/dist/material/materialStore.d.ts +24 -0
- package/dist/material/materialStore.js +76 -0
- package/dist/material/sampling.d.ts +21 -0
- package/dist/material/sampling.js +39 -0
- package/dist/material/sources.d.ts +25 -0
- package/dist/material/sources.js +33 -0
- package/dist/material/watermark.d.ts +21 -0
- package/dist/material/watermark.js +76 -0
- package/dist/observers/cursorRewriteObserver.d.ts +46 -0
- package/dist/observers/cursorRewriteObserver.js +103 -0
- package/dist/observers/distillObserver.d.ts +31 -0
- package/dist/observers/distillObserver.js +93 -0
- package/dist/observers/index.d.ts +5 -0
- package/dist/observers/index.js +5 -0
- package/dist/observers/observerBus.d.ts +44 -0
- package/dist/observers/observerBus.js +73 -0
- package/dist/observers/reflectionObserver.d.ts +31 -0
- package/dist/observers/reflectionObserver.js +124 -0
- package/dist/observers/valueDigestObserver.d.ts +50 -0
- package/dist/observers/valueDigestObserver.js +70 -0
- package/dist/ops/cleanup.d.ts +23 -0
- package/dist/ops/cleanup.js +68 -0
- package/dist/ops/health.d.ts +37 -0
- package/dist/ops/health.js +96 -0
- package/dist/ops/index.d.ts +7 -0
- package/dist/ops/index.js +7 -0
- package/dist/ops/recall.d.ts +65 -0
- package/dist/ops/recall.js +152 -0
- package/dist/ops/reuseOutcomes.d.ts +68 -0
- package/dist/ops/reuseOutcomes.js +126 -0
- package/dist/ops/savingsCore.d.ts +47 -0
- package/dist/ops/savingsCore.js +112 -0
- package/dist/ops/selfUpdate.d.ts +106 -0
- package/dist/ops/selfUpdate.js +294 -0
- package/dist/ops/valueLedger.d.ts +205 -0
- package/dist/ops/valueLedger.js +238 -0
- package/dist/ops/valueOutreach.d.ts +70 -0
- package/dist/ops/valueOutreach.js +244 -0
- package/dist/personality/drift.d.ts +45 -0
- package/dist/personality/drift.js +106 -0
- package/dist/personality/events.d.ts +25 -0
- package/dist/personality/events.js +37 -0
- package/dist/personality/evolveOps.d.ts +20 -0
- package/dist/personality/evolveOps.js +93 -0
- package/dist/personality/index.d.ts +11 -0
- package/dist/personality/index.js +11 -0
- package/dist/personality/mutate.d.ts +21 -0
- package/dist/personality/mutate.js +39 -0
- package/dist/personality/pivot.d.ts +23 -0
- package/dist/personality/pivot.js +22 -0
- package/dist/personality/prompt.d.ts +6 -0
- package/dist/personality/prompt.js +62 -0
- package/dist/personality/riskGate.d.ts +42 -0
- package/dist/personality/riskGate.js +80 -0
- package/dist/personality/schema.d.ts +255 -0
- package/dist/personality/schema.js +119 -0
- package/dist/personality/select.d.ts +46 -0
- package/dist/personality/select.js +71 -0
- package/dist/personality/stats.d.ts +46 -0
- package/dist/personality/stats.js +82 -0
- package/dist/personality/store.d.ts +35 -0
- package/dist/personality/store.js +92 -0
- package/dist/schema/common.d.ts +28 -0
- package/dist/schema/common.js +17 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/material.d.ts +201 -0
- package/dist/schema/material.js +50 -0
- package/dist/schema/problem.d.ts +160 -0
- package/dist/schema/problem.js +45 -0
- package/dist/schema/proofOfWork.d.ts +97 -0
- package/dist/schema/proofOfWork.js +9 -0
- package/dist/schema/signal.d.ts +41 -0
- package/dist/schema/signal.js +19 -0
- package/dist/shadow/index.d.ts +6 -0
- package/dist/shadow/index.js +6 -0
- package/dist/shadow/jsonlSink.d.ts +17 -0
- package/dist/shadow/jsonlSink.js +27 -0
- package/dist/shadow/redact.d.ts +11 -0
- package/dist/shadow/redact.js +18 -0
- package/dist/shadow/shadowHub.d.ts +12 -0
- package/dist/shadow/shadowHub.js +42 -0
- package/dist/shadow/shadowMailbox.d.ts +17 -0
- package/dist/shadow/shadowMailbox.js +25 -0
- package/dist/shadow/shadowStore.d.ts +18 -0
- package/dist/shadow/shadowStore.js +55 -0
- package/dist/shadow/sink.d.ts +37 -0
- package/dist/shadow/sink.js +18 -0
- package/dist/signals/cycleHistoryFromEvents.d.ts +11 -0
- package/dist/signals/cycleHistoryFromEvents.js +96 -0
- package/dist/signals/expand.d.ts +34 -0
- package/dist/signals/expand.js +125 -0
- package/dist/signals/extractor.d.ts +25 -0
- package/dist/signals/extractor.js +57 -0
- package/dist/signals/index.d.ts +6 -0
- package/dist/signals/index.js +6 -0
- package/dist/signals/metaSignals.d.ts +57 -0
- package/dist/signals/metaSignals.js +158 -0
- package/dist/signals/signalGate.d.ts +15 -0
- package/dist/signals/signalGate.js +40 -0
- package/dist/signals/traceSignals.d.ts +44 -0
- package/dist/signals/traceSignals.js +123 -0
- package/dist/signatures/classifier.d.ts +16 -0
- package/dist/signatures/classifier.js +29 -0
- package/dist/signatures/index.d.ts +2 -0
- package/dist/signatures/index.js +2 -0
- package/dist/signatures/signatures.d.ts +22 -0
- package/dist/signatures/signatures.js +42 -0
- package/dist/strategy/experiment.d.ts +48 -0
- package/dist/strategy/experiment.js +63 -0
- package/dist/strategy/index.d.ts +2 -0
- package/dist/strategy/index.js +2 -0
- package/dist/strategy/strategyPoint.d.ts +30 -0
- package/dist/strategy/strategyPoint.js +23 -0
- package/dist/trace/index.d.ts +2 -0
- package/dist/trace/index.js +2 -0
- package/dist/trace/trajectory.d.ts +82 -0
- package/dist/trace/trajectory.js +117 -0
- package/dist/trace/trajectoryExport.d.ts +237 -0
- package/dist/trace/trajectoryExport.js +1818 -0
- package/dist/trigger/budget.d.ts +16 -0
- package/dist/trigger/budget.js +20 -0
- package/dist/trigger/engine.d.ts +12 -0
- package/dist/trigger/engine.js +23 -0
- package/dist/trigger/index.d.ts +6 -0
- package/dist/trigger/index.js +6 -0
- package/dist/trigger/problemBuilder.d.ts +24 -0
- package/dist/trigger/problemBuilder.js +32 -0
- package/dist/trigger/trigger.d.ts +22 -0
- package/dist/trigger/trigger.js +27 -0
- package/dist/trigger/valueModel.d.ts +26 -0
- package/dist/trigger/valueModel.js +25 -0
- package/dist/trigger/wfq.d.ts +13 -0
- package/dist/trigger/wfq.js +25 -0
- package/dist/util/fileLock.d.ts +24 -0
- package/dist/util/fileLock.js +227 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/index.js +2 -0
- package/dist/util/version.d.ts +12 -0
- package/dist/util/version.js +51 -0
- package/dist/verify/index.d.ts +4 -0
- package/dist/verify/index.js +4 -0
- package/dist/verify/rollback.d.ts +22 -0
- package/dist/verify/rollback.js +33 -0
- package/dist/verify/sandboxRunner.d.ts +39 -0
- package/dist/verify/sandboxRunner.js +116 -0
- package/dist/verify/sandboxedValidation.d.ts +35 -0
- package/dist/verify/sandboxedValidation.js +56 -0
- package/dist/verify/validation.d.ts +41 -0
- package/dist/verify/validation.js +85 -0
- package/dist/wire/canonicalize.golden.d.ts +23 -0
- package/dist/wire/canonicalize.golden.js +46 -0
- package/dist/wire/geneHints.d.ts +64 -0
- package/dist/wire/geneHints.js +100 -0
- package/dist/wire/index.d.ts +91 -0
- package/dist/wire/index.js +3 -0
- package/dist/wire/schemaGate.d.ts +11 -0
- package/dist/wire/schemaGate.js +49 -0
- package/dist/workflow/dsl.d.ts +54 -0
- package/dist/workflow/dsl.js +15 -0
- package/dist/workflow/engine.d.ts +30 -0
- package/dist/workflow/engine.js +94 -0
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.js +2 -0
- package/package.json +29 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// The Claude Code execution bridge — the REAL implementation of CycleEngine's `execute` seam (ported from
|
|
2
|
+
// v1's exec bridge, EVOLVE_EXEC_BRIDGE-gated). It renders the mutation into an instruction, runs a coding
|
|
3
|
+
// agent against a working directory, measures proof-of-work from the git diff, and (optionally) runs a
|
|
4
|
+
// validation hook to decide success. Everything external (the agent, git, validation) is an injected seam,
|
|
5
|
+
// so the whole bridge is unit-testable with fakes and NEVER spawns a real agent in tests.
|
|
6
|
+
//
|
|
7
|
+
// The runner layer (how to spawn a CLI shell-free, the per-runner argv/factories, the runner registry) lives
|
|
8
|
+
// in ./runnerRegistry.ts (#91 item 6); this file is the bridge orchestration around it. Env scrubbing stays
|
|
9
|
+
// here — it is a bridge-side security control, not a runner concern.
|
|
10
|
+
//
|
|
11
|
+
// SAFETY: default-OFF. The factory throws ExecBridgeDisabledError unless explicitly enabled (opts.enabled)
|
|
12
|
+
// or EVOLVE_EXEC_BRIDGE === '1'. Wiring it in by accident must fail loudly rather than silently spawn an
|
|
13
|
+
// autonomous agent.
|
|
14
|
+
import { resolve as resolvePath, sep, join as joinPath } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { writeFileSync } from 'node:fs';
|
|
17
|
+
import { renderExecPrompt } from './prompt.js';
|
|
18
|
+
import { parseGitShortstat, gitDiffProof } from './proofOfWork.js';
|
|
19
|
+
// Policy enforcement core (#107): checkPolicy runs the always-on global guards (blast hard cap +
|
|
20
|
+
// protected paths + destructive deletes) on EVERY exec — gene or not — plus the per-gene constraints when a
|
|
21
|
+
// gene supplies them. It supersedes the old gene-gated `checkChangeConstraints` call (the no-gene-no-guard hole).
|
|
22
|
+
import { checkPolicy, summarizeViolations } from './policy/index.js';
|
|
23
|
+
import { spawnCapture, getRunnerSpec, DEFAULT_TIMEOUT_MS } from './runnerRegistry.js';
|
|
24
|
+
// Re-export the runner layer so existing importers of ./claudeBridge.js (and the `exec` namespace) keep their
|
|
25
|
+
// surface after the #91-6 split — the registry simply has a clearer home now.
|
|
26
|
+
export { resolveSpawnCommand, spawnCapture, UnboundedSkipPermissionsError, UnsupportedCursorSkipPermissionsError, claudeRunnerArgs, makeClaudeHeadlessRunner, claudeHeadlessRunner, codexRunnerArgs, makeCodexHeadlessRunner, cursorRunnerArgs, makeCursorHeadlessRunner, getRunnerSpec, } from './runnerRegistry.js';
|
|
27
|
+
export class ExecBridgeDisabledError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super('exec bridge is disabled — set EVOLVE_EXEC_BRIDGE=1 or pass { enabled: true } to enable agent execution');
|
|
30
|
+
this.name = 'ExecBridgeDisabledError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Thrown when the agent's working directory is outside the configured allowedRoots (deny-by-default guardrail). */
|
|
34
|
+
export class ExecBridgeForbiddenError extends Error {
|
|
35
|
+
constructor(cwd) {
|
|
36
|
+
super(`exec bridge refused: cwd ${cwd} is not within any allowedRoots — an autonomous agent may only edit allowlisted repos`);
|
|
37
|
+
this.name = 'ExecBridgeForbiddenError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Thrown when a FULL-ACCESS (or unverified) agent run is requested without worktree isolation. The throwaway
|
|
42
|
+
* worktree is the containment WE control — allowedRoots gates the cwd but cannot stop an auto-approved or
|
|
43
|
+
* unsandboxed process writing/running outside it. Gated runners:
|
|
44
|
+
* - codex with skipPermissions → `--sandbox danger-full-access` (no inner OS sandbox). Without skip it stays
|
|
45
|
+
* workspace-write, so only the skip path is gated.
|
|
46
|
+
* - cursor default → gated UNCONDITIONALLY. cursor-agent base `-p` already documents write+shell access, cursor
|
|
47
|
+
* skipPermissions is rejected by the runner layer, and the scaffold remains unverified (#66/#181) — so default
|
|
48
|
+
* cursor still needs the wrapper worktree rather than risk mutating the real tree.
|
|
49
|
+
* claude is exempt: its skip is bounded by --allowedTools (finding #80).
|
|
50
|
+
*/
|
|
51
|
+
export class UnsandboxedFullAccessRequiresIsolationError extends Error {
|
|
52
|
+
constructor() {
|
|
53
|
+
super('a full-access or unverified agent run (codex --sandbox danger-full-access, or any cursor scaffold run) can auto-approve shell+write and requires isolation: "worktree" — refusing to run it against the real working tree');
|
|
54
|
+
this.name = 'UnsandboxedFullAccessRequiresIsolationError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Whether `child` is the same as, or nested under, `root` (both resolved to absolute paths). */
|
|
58
|
+
function isWithinRoot(child, root) {
|
|
59
|
+
const c = resolvePath(child);
|
|
60
|
+
const r = resolvePath(root);
|
|
61
|
+
return c === r || c.startsWith(r.endsWith(sep) ? r : r + sep);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sensitive env keys to strip before spawning an agent/tool (finding #39.2): evolver/hub/cloud secrets must
|
|
65
|
+
* not be inherited by an autonomous agent (it could exfiltrate them via its tools). The base keeps only NEUTRAL
|
|
66
|
+
* system env — vendor auth is NOT in the base; each runner contributes its OWN via envAllow (#66), so one
|
|
67
|
+
* runner's key (e.g. ANTHROPIC_) is never handed to another agent (e.g. codex).
|
|
68
|
+
*/
|
|
69
|
+
// WHITELIST, fail-safe (#39.2 / #46): default-DENY — only explicitly-allowed env vars reach the agent. A
|
|
70
|
+
// denylist of secret-shaped names can miss a future sensitive var; a whitelist can't. The base allow-set is
|
|
71
|
+
// ONLY NEUTRAL system env (no vendor auth) — per-runner auth (ANTHROPIC_/CLAUDE_ for claude, OPENAI_/CODEX_ for
|
|
72
|
+
// codex) is contributed by each runner's envAllow (#66), so one runner's key is never handed to another agent.
|
|
73
|
+
// Callers extend via opts.allowKeys / allowPrefixes when an agent legitimately needs more.
|
|
74
|
+
const ALLOW_ENV_PREFIXES = ['XDG_', 'LC_', 'NPM_CONFIG_'];
|
|
75
|
+
const ALLOW_ENV_KEYS = new Set([
|
|
76
|
+
'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LANGUAGE', 'TERM', 'TZ', 'PWD', 'HOSTNAME',
|
|
77
|
+
'TMPDIR', 'TMP', 'TEMP', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
|
|
78
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
79
|
+
// Windows OS essentials (absent on POSIX → no-op there): a process can't run, resolve PATHEXT shims, or find
|
|
80
|
+
// its own per-user config/auth dir without these. They are OS runtime vars, NOT app secrets — the secret
|
|
81
|
+
// env (NODE_SECRET/AWS_*/GH_TOKEN/vendor keys) is still denied by the whitelist. Their absence is why an
|
|
82
|
+
// npm-installed agent (codex) failed on Windows while the native claude.exe happened to run (#66).
|
|
83
|
+
'SystemRoot', 'SystemDrive', 'windir', 'ComSpec', 'PATHEXT', 'NUMBER_OF_PROCESSORS',
|
|
84
|
+
'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER', 'PROCESSOR_LEVEL', 'PROCESSOR_REVISION',
|
|
85
|
+
'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* Whitelist-filter `env` for a spawned agent/tool: keep ONLY the minimal runtime env + the caller-declared
|
|
89
|
+
* extras (the runner's own auth via allowPrefixes/allowKeys); drop everything else. Fail-safe by construction —
|
|
90
|
+
* an unlisted var never leaks.
|
|
91
|
+
*/
|
|
92
|
+
export function scrubAgentEnv(env, opts = {}) {
|
|
93
|
+
const allowKeys = new Set([...ALLOW_ENV_KEYS, ...(opts.allowKeys ?? [])]);
|
|
94
|
+
const allowPrefixes = [...ALLOW_ENV_PREFIXES, ...(opts.allowPrefixes ?? [])];
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const [k, v] of Object.entries(env)) {
|
|
97
|
+
if (v === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
if (allowKeys.has(k) || allowPrefixes.some((p) => k.startsWith(p)))
|
|
100
|
+
out[k] = v;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** Default git runner: spawn `git <args>` in cwd, return stdout (empty string on error). Env scrubbed — git never needs evolver/hub secrets. */
|
|
105
|
+
export const defaultGitRunner = async (args, cwd) => {
|
|
106
|
+
try {
|
|
107
|
+
const r = await spawnCapture('git', args, { cwd, timeoutMs: 30_000, env: scrubAgentEnv(process.env) });
|
|
108
|
+
return r.stdout;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Build the `execute` function CycleEngine/runEvolutionCycle consume. Default-off: throws
|
|
116
|
+
* ExecBridgeDisabledError on first call unless enabled.
|
|
117
|
+
*/
|
|
118
|
+
export function makeClaudeExecBridge(opts) {
|
|
119
|
+
const enabled = opts.enabled ?? (process.env['EVOLVE_EXEC_BRIDGE'] === '1');
|
|
120
|
+
const spec = getRunnerSpec(opts.runner); // #66: claude (default, byte-identical) | codex
|
|
121
|
+
const agent = opts.agent ?? spec.makeRunner(opts.agentOptions);
|
|
122
|
+
const git = opts.git ?? defaultGitRunner;
|
|
123
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
124
|
+
// secure by default; only THIS runner's auth env reaches it (claude never gets OPENAI_, codex never ANTHROPIC_, #66)
|
|
125
|
+
const agentEnv = opts.scrubEnv === false ? undefined : scrubAgentEnv(process.env, { allowPrefixes: spec.envAllow.prefixes, ...(spec.envAllow.keys ? { allowKeys: spec.envAllow.keys } : {}) });
|
|
126
|
+
// Fail-fast when a run that can write/execute against the tree with no inner sandbox we control is requested
|
|
127
|
+
// without the throwaway worktree as containment (allowedRoots gates the cwd but can't stop a write outside it).
|
|
128
|
+
// Only when WE build the runner — an injected `agent` bypasses the built-in runner:
|
|
129
|
+
// - codex: only its skip path emits --sandbox danger-full-access; without skip it stays workspace-write.
|
|
130
|
+
// - cursor: gated UNCONDITIONALLY. cursor-agent base `-p` already has write+shell access, its skipPermissions
|
|
131
|
+
// path is refused by runnerRegistry, and the runner is an unverified scaffold (#66/#181), so we do not let
|
|
132
|
+
// default cursor touch the real tree until run-verified. (Bugbot High #181)
|
|
133
|
+
// claude is exempt — its skip is bounded by --allowedTools (finding #80).
|
|
134
|
+
const needsIsolation = opts.runner === 'cursor'
|
|
135
|
+
|| (opts.runner === 'codex' && opts.agentOptions?.skipPermissions === true);
|
|
136
|
+
if (!opts.agent && needsIsolation && opts.isolation !== 'worktree') {
|
|
137
|
+
throw new UnsandboxedFullAccessRequiresIsolationError();
|
|
138
|
+
}
|
|
139
|
+
return async (mutation, decision) => {
|
|
140
|
+
if (!enabled)
|
|
141
|
+
throw new ExecBridgeDisabledError();
|
|
142
|
+
// Deny-by-default guardrail: an autonomous agent may only edit allowlisted repos. Checked before the
|
|
143
|
+
// agent is ever spawned, so a forbidden cwd never runs anything.
|
|
144
|
+
if (opts.allowedRoots !== undefined && !opts.allowedRoots.some((root) => isWithinRoot(opts.cwd, root))) {
|
|
145
|
+
throw new ExecBridgeForbiddenError(opts.cwd);
|
|
146
|
+
}
|
|
147
|
+
const resolveId = decision.selectedAssetId ?? decision.selectedGeneId;
|
|
148
|
+
const resolved = resolveId && opts.resolveGene ? await opts.resolveGene(resolveId) : null;
|
|
149
|
+
// Trust gate (finding #39.3): for unattended runs, only embed a gene we trust — an untrusted gene's strategy
|
|
150
|
+
// is dropped so a poisoned strategy can't drive the autonomous agent (the run falls back to innovate).
|
|
151
|
+
const gene = resolved && (!opts.requireTrustedGene || resolved.trusted === true) ? resolved : null;
|
|
152
|
+
const prompt = renderExecPrompt({
|
|
153
|
+
mutation,
|
|
154
|
+
decision,
|
|
155
|
+
...(gene ? { gene } : {}),
|
|
156
|
+
...(opts.validationCmds ? { validationCmds: opts.validationCmds } : {}),
|
|
157
|
+
// use-case ①: inject the personality style block from the state applySelectForRun just persisted.
|
|
158
|
+
...(opts.personality ? { personality: opts.personality.currentState() } : {}),
|
|
159
|
+
});
|
|
160
|
+
// Isolation: run in a throwaway git worktree so the agent's edits never touch the real working tree.
|
|
161
|
+
const isolate = opts.isolation === 'worktree';
|
|
162
|
+
const workDir = isolate ? joinPath(tmpdir(), `evolver-wt-${mutation.id}`) : opts.cwd;
|
|
163
|
+
if (isolate)
|
|
164
|
+
await git(['worktree', 'add', '--detach', workDir, 'HEAD'], opts.cwd);
|
|
165
|
+
try {
|
|
166
|
+
const run = await agent(prompt, { cwd: workDir, timeoutMs, ...(agentEnv ? { env: agentEnv } : {}) });
|
|
167
|
+
const stat = parseGitShortstat(await git(['diff', '--shortstat'], workDir));
|
|
168
|
+
// ENFORCE policy against the ACTUAL diff (finding: prompt.ts only ADVISES the agent "touch at most N
|
|
169
|
+
// file(s) / never modify X"; this is the hard gate). checkPolicy ALWAYS runs the global guards — the
|
|
170
|
+
// system blast hard cap (EVOLVER_HARD_CAP_FILES/LINES), the critical-protected paths (.env, MEMORY.md,
|
|
171
|
+
// package.json, the evolver skill, …), and destructive deletes of those paths — so a no-gene / no-
|
|
172
|
+
// constraints run is no longer un-guarded. The gene's max_files/max_lines/forbidden_paths layer on top.
|
|
173
|
+
// Any violation fails the cycle no matter what the agent did — even when validation would pass.
|
|
174
|
+
const changedFiles = (await git(['diff', '--name-only'], workDir)).split('\n').map((s) => s.trim()).filter(Boolean);
|
|
175
|
+
const numstat = await git(['diff', '--numstat'], workDir);
|
|
176
|
+
const violations = checkPolicy({ stat, changedFiles, numstat, ...(gene?.constraints ? { constraints: gene.constraints } : {}) });
|
|
177
|
+
let patchRef;
|
|
178
|
+
if (isolate && stat.files > 0) {
|
|
179
|
+
// preserve the isolated edits as a patch (the worktree itself is removed); the real repo is untouched
|
|
180
|
+
patchRef = joinPath(tmpdir(), `evolver-patch-${mutation.id}.diff`);
|
|
181
|
+
writeFileSync(patchRef, await git(['diff'], workDir));
|
|
182
|
+
}
|
|
183
|
+
const proof = gitDiffProof(stat, patchRef);
|
|
184
|
+
// Success: prefer the authoritative validation hook; otherwise "agent succeeded AND produced a diff".
|
|
185
|
+
let passed = run.ok && stat.files > 0;
|
|
186
|
+
let score = passed ? 0.7 : run.ok ? 0.4 : 0.1; // ran-but-no-change is weak, not a clean failure
|
|
187
|
+
// Only validate a change that already respects the constraints — a constraint-violating diff is never
|
|
188
|
+
// a success regardless of what its tests say.
|
|
189
|
+
if (run.ok && opts.validate && violations.length === 0) {
|
|
190
|
+
const v = await opts.validate(mutation, decision, workDir);
|
|
191
|
+
passed = v.passed;
|
|
192
|
+
score = v.score ?? (v.passed ? 0.9 : 0.2);
|
|
193
|
+
}
|
|
194
|
+
let reason;
|
|
195
|
+
if (violations.length > 0) {
|
|
196
|
+
passed = false;
|
|
197
|
+
score = Math.min(score, 0.1);
|
|
198
|
+
reason = summarizeViolations(violations);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
outcome: { status: passed ? 'success' : 'failed', score, ...(reason ? { reason } : {}) },
|
|
202
|
+
proofOfWork: proof,
|
|
203
|
+
strongEvidence: passed && stat.files > 0,
|
|
204
|
+
// On a FAILED outcome, hand the agent transcript (stdout + stderr) to the cycle engine as host-side
|
|
205
|
+
// triage context (#279): an empty transcript -> host_no_transcript, a provider-error string ->
|
|
206
|
+
// host_provider_error. Omitted on success (failure-only context; never persisted).
|
|
207
|
+
...(passed ? {} : { sessionLog: run.error ? `${run.output}\n${run.error}` : run.output }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
if (isolate) {
|
|
212
|
+
try {
|
|
213
|
+
await git(['worktree', 'remove', '--force', workDir], opts.cwd);
|
|
214
|
+
}
|
|
215
|
+
catch { /* best-effort cleanup */ }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './prompt.js';
|
|
2
|
+
export * from './proofOfWork.js';
|
|
3
|
+
export * from './policy/index.js';
|
|
4
|
+
export * from './openPrRegistry.js';
|
|
5
|
+
export * from './claudeBridge.js';
|
|
6
|
+
export * from './autonomousCycle.js';
|
|
7
|
+
export * from './autoExec.js';
|
|
8
|
+
export * from './selfPr.js';
|
|
9
|
+
export * from './selfPrObfuscation.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './prompt.js';
|
|
2
|
+
export * from './proofOfWork.js';
|
|
3
|
+
// The policy enforcement core (checkPolicy + the split guards). policyCheck.js is a back-compat shim that
|
|
4
|
+
// re-exports the per-gene checker from here, so it is intentionally NOT re-exported again (would duplicate names).
|
|
5
|
+
export * from './policy/index.js';
|
|
6
|
+
export * from './openPrRegistry.js';
|
|
7
|
+
export * from './claudeBridge.js';
|
|
8
|
+
export * from './autonomousCycle.js';
|
|
9
|
+
export * from './autoExec.js';
|
|
10
|
+
export * from './selfPr.js';
|
|
11
|
+
export * from './selfPrObfuscation.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface OpenPr {
|
|
2
|
+
number: number;
|
|
3
|
+
title: string;
|
|
4
|
+
headRefName: string;
|
|
5
|
+
files: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface FileOverlap {
|
|
8
|
+
overlap: true;
|
|
9
|
+
prNumber: number;
|
|
10
|
+
prTitle: string;
|
|
11
|
+
headRefName: string;
|
|
12
|
+
/** |shared| / |changedFiles| — what fraction of MY change is the PR already doing. */
|
|
13
|
+
overlapRatio: number;
|
|
14
|
+
sharedFiles: string[];
|
|
15
|
+
}
|
|
16
|
+
export type FileOverlapResult = FileOverlap | {
|
|
17
|
+
overlap: false;
|
|
18
|
+
reason: string;
|
|
19
|
+
};
|
|
20
|
+
export interface SignalHint {
|
|
21
|
+
number: number;
|
|
22
|
+
title: string;
|
|
23
|
+
headRefName: string;
|
|
24
|
+
files: readonly string[];
|
|
25
|
+
/** |shared signal tokens| / |signal tokens| — how much of the task's vocabulary the PR already covers. */
|
|
26
|
+
tokenOverlap: number;
|
|
27
|
+
}
|
|
28
|
+
/** List the repo's open PRs. Default = `gh pr list`; inject a fake in tests. cwd selects the repo. */
|
|
29
|
+
export type OpenPrLister = (cwd?: string) => Promise<OpenPr[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Strongest file overlap by ratio = |shared| / |changedFiles|. We use the CYCLE's changed-file count as the
|
|
32
|
+
* denominator (not the PR's): the question is "is this cycle re-doing work the PR already does?" — so a daemon
|
|
33
|
+
* that changes 4 files, 3 of which are in a PR's 11-file diff, scores 0.75 (re-doing most of its own work)
|
|
34
|
+
* even though that's only 0.27 of the PR. Returns { overlap:false, reason } when there's nothing to compare.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findFileOverlap(changedFiles: readonly string[], prs: readonly OpenPr[]): FileOverlapResult;
|
|
37
|
+
/**
|
|
38
|
+
* Token overlap between a task's signals and each PR's title + branch name — the pre-exec dedup signal, used
|
|
39
|
+
* before any files exist. Returns the top-N PRs whose token overlap meets `threshold` (default 0.5), strongest
|
|
40
|
+
* first. Denominator is the signal-token set, so this asks "how much of what I'm about to work on does this PR
|
|
41
|
+
* already mention?".
|
|
42
|
+
*/
|
|
43
|
+
export declare function findSignalHints(signals: readonly string[], prs: readonly OpenPr[], opts?: {
|
|
44
|
+
threshold?: number;
|
|
45
|
+
topN?: number;
|
|
46
|
+
}): SignalHint[];
|
|
47
|
+
/**
|
|
48
|
+
* Default lister: `gh pr list --state=open --json number,title,headRefName,files --limit 50`. Graceful —
|
|
49
|
+
* returns [] on any failure (gh missing, unauthenticated, timeout, bad JSON) so dedup just turns off. gh is a
|
|
50
|
+
* trusted infra tool, so its own auth (GH_TOKEN/GITHUB_TOKEN, or the gh config under $HOME) is passed through.
|
|
51
|
+
*/
|
|
52
|
+
export declare function makeGhPrLister(): OpenPrLister;
|
|
53
|
+
/**
|
|
54
|
+
* Wrap a lister with a TTL cache + single-flight: a resident daemon polls often, but the open-PR set changes
|
|
55
|
+
* slowly — don't shell `gh` every tick. Concurrent callers within the window share one in-flight fetch.
|
|
56
|
+
*/
|
|
57
|
+
export declare function makeCachedPrLister(lister: OpenPrLister, opts?: {
|
|
58
|
+
ttlMs?: number;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}): OpenPrLister;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Open-PR dedup (ported from v1 src/gep/openPRRegistry.js). An autonomous loop has been observed to
|
|
2
|
+
// independently re-implement work already in flight on an open PR — wasting a full agent run and dirtying
|
|
3
|
+
// the tree with a near-duplicate diff. This module lets the daemon ask, cheaply and BEFORE spawning an
|
|
4
|
+
// agent, "is an open PR already doing this?" (token overlap of the task's signals against PR titles/branches),
|
|
5
|
+
// and AFTER a diff, "does my change substantially overlap an open PR's files?" (file-set overlap).
|
|
6
|
+
//
|
|
7
|
+
// Seam-based like the rest of exec: the PR list is an injected OpenPrLister (default shells `gh pr list`).
|
|
8
|
+
// Graceful + opt-in: the default lister returns [] if gh is missing/unauthenticated/errors, so dedup simply
|
|
9
|
+
// turns itself off rather than failing a run; the daemon only consults it when a lister is wired in.
|
|
10
|
+
import { spawnCapture, scrubAgentEnv } from './claudeBridge.js';
|
|
11
|
+
// ── pure logic (fully testable, no subprocess) ─────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Strongest file overlap by ratio = |shared| / |changedFiles|. We use the CYCLE's changed-file count as the
|
|
14
|
+
* denominator (not the PR's): the question is "is this cycle re-doing work the PR already does?" — so a daemon
|
|
15
|
+
* that changes 4 files, 3 of which are in a PR's 11-file diff, scores 0.75 (re-doing most of its own work)
|
|
16
|
+
* even though that's only 0.27 of the PR. Returns { overlap:false, reason } when there's nothing to compare.
|
|
17
|
+
*/
|
|
18
|
+
export function findFileOverlap(changedFiles, prs) {
|
|
19
|
+
const files = changedFiles.map(String).filter(Boolean);
|
|
20
|
+
if (files.length === 0)
|
|
21
|
+
return { overlap: false, reason: 'no_changed_files' };
|
|
22
|
+
if (prs.length === 0)
|
|
23
|
+
return { overlap: false, reason: 'no_open_prs' };
|
|
24
|
+
const changedSet = new Set(files);
|
|
25
|
+
let best = null;
|
|
26
|
+
for (const pr of prs) {
|
|
27
|
+
if (!pr.files?.length)
|
|
28
|
+
continue;
|
|
29
|
+
const prSet = new Set(pr.files.map(String));
|
|
30
|
+
const shared = [...changedSet].filter((f) => prSet.has(f));
|
|
31
|
+
if (shared.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
const ratio = shared.length / files.length;
|
|
34
|
+
if (!best || ratio > best.overlapRatio) {
|
|
35
|
+
best = { overlap: true, prNumber: pr.number, prTitle: pr.title, headRefName: pr.headRefName, overlapRatio: ratio, sharedFiles: shared };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return best ?? { overlap: false, reason: 'no_intersection' };
|
|
39
|
+
}
|
|
40
|
+
function tokenize(s) {
|
|
41
|
+
return String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').split(/\s+/).filter((t) => t.length >= 3);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Token overlap between a task's signals and each PR's title + branch name — the pre-exec dedup signal, used
|
|
45
|
+
* before any files exist. Returns the top-N PRs whose token overlap meets `threshold` (default 0.5), strongest
|
|
46
|
+
* first. Denominator is the signal-token set, so this asks "how much of what I'm about to work on does this PR
|
|
47
|
+
* already mention?".
|
|
48
|
+
*/
|
|
49
|
+
export function findSignalHints(signals, prs, opts = {}) {
|
|
50
|
+
const threshold = opts.threshold ?? 0.5;
|
|
51
|
+
const topN = opts.topN ?? 3;
|
|
52
|
+
const sigTokens = new Set();
|
|
53
|
+
for (const s of signals)
|
|
54
|
+
for (const t of tokenize(s))
|
|
55
|
+
sigTokens.add(t);
|
|
56
|
+
if (sigTokens.size === 0 || prs.length === 0)
|
|
57
|
+
return [];
|
|
58
|
+
const hits = [];
|
|
59
|
+
for (const pr of prs) {
|
|
60
|
+
const prTokens = new Set();
|
|
61
|
+
for (const t of tokenize(pr.title))
|
|
62
|
+
prTokens.add(t);
|
|
63
|
+
for (const t of tokenize(pr.headRefName))
|
|
64
|
+
prTokens.add(t);
|
|
65
|
+
if (prTokens.size === 0)
|
|
66
|
+
continue;
|
|
67
|
+
let common = 0;
|
|
68
|
+
sigTokens.forEach((t) => { if (prTokens.has(t))
|
|
69
|
+
common++; });
|
|
70
|
+
const ratio = common / sigTokens.size;
|
|
71
|
+
if (ratio >= threshold) {
|
|
72
|
+
hits.push({ number: pr.number, title: pr.title, headRefName: pr.headRefName, files: (pr.files ?? []).slice(0, 5), tokenOverlap: ratio });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
hits.sort((a, b) => b.tokenOverlap - a.tokenOverlap);
|
|
76
|
+
return hits.slice(0, topN);
|
|
77
|
+
}
|
|
78
|
+
// ── gh lister seam + TTL cache ─────────────────────────────────────────────
|
|
79
|
+
const GH_TIMEOUT_MS = 5000;
|
|
80
|
+
/**
|
|
81
|
+
* Default lister: `gh pr list --state=open --json number,title,headRefName,files --limit 50`. Graceful —
|
|
82
|
+
* returns [] on any failure (gh missing, unauthenticated, timeout, bad JSON) so dedup just turns off. gh is a
|
|
83
|
+
* trusted infra tool, so its own auth (GH_TOKEN/GITHUB_TOKEN, or the gh config under $HOME) is passed through.
|
|
84
|
+
*/
|
|
85
|
+
export function makeGhPrLister() {
|
|
86
|
+
return async (cwd) => {
|
|
87
|
+
try {
|
|
88
|
+
const r = await spawnCapture('gh', ['pr', 'list', '--state=open', '--json', 'number,title,headRefName,files', '--limit', '50'], {
|
|
89
|
+
cwd: cwd ?? process.cwd(),
|
|
90
|
+
timeoutMs: GH_TIMEOUT_MS,
|
|
91
|
+
env: scrubAgentEnv(process.env, { allowKeys: ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_HOST', 'GH_CONFIG_DIR'] }),
|
|
92
|
+
});
|
|
93
|
+
if (r.code !== 0)
|
|
94
|
+
return [];
|
|
95
|
+
const arr = JSON.parse(r.stdout || '[]');
|
|
96
|
+
if (!Array.isArray(arr))
|
|
97
|
+
return [];
|
|
98
|
+
return arr.map((pr) => ({
|
|
99
|
+
number: Number(pr.number),
|
|
100
|
+
title: String(pr.title ?? ''),
|
|
101
|
+
headRefName: String(pr.headRefName ?? ''),
|
|
102
|
+
files: Array.isArray(pr.files) ? pr.files.map((f) => String(f.path ?? '')).filter(Boolean) : [],
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Wrap a lister with a TTL cache + single-flight: a resident daemon polls often, but the open-PR set changes
|
|
112
|
+
* slowly — don't shell `gh` every tick. Concurrent callers within the window share one in-flight fetch.
|
|
113
|
+
*/
|
|
114
|
+
export function makeCachedPrLister(lister, opts = {}) {
|
|
115
|
+
const ttlMs = opts.ttlMs ?? 60_000;
|
|
116
|
+
const now = opts.now ?? (() => Date.now());
|
|
117
|
+
let cache = null;
|
|
118
|
+
let cacheAt = 0;
|
|
119
|
+
let inflight = null;
|
|
120
|
+
return (cwd) => {
|
|
121
|
+
if (cache && now() - cacheAt < ttlMs)
|
|
122
|
+
return Promise.resolve(cache);
|
|
123
|
+
if (inflight)
|
|
124
|
+
return inflight; // share the in-flight fetch rather than spawning a second gh
|
|
125
|
+
inflight = lister(cwd)
|
|
126
|
+
.then((fresh) => { cache = fresh; cacheAt = now(); return fresh; })
|
|
127
|
+
.finally(() => { inflight = null; });
|
|
128
|
+
return inflight;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DiffStat } from '../proofOfWork.js';
|
|
2
|
+
import type { ChangeConstraints, PolicyViolation } from './constraints.js';
|
|
3
|
+
/** System-wide blast caps. Env-overridable (EVOLVER_HARD_CAP_FILES / EVOLVER_HARD_CAP_LINES), same names/defaults as v1. */
|
|
4
|
+
export declare function globalHardCaps(): {
|
|
5
|
+
files: number;
|
|
6
|
+
lines: number;
|
|
7
|
+
};
|
|
8
|
+
export type BlastSeverity = 'hard_cap_breach' | 'critical_overrun' | 'exceeded' | 'approaching_limit' | 'within_limit';
|
|
9
|
+
/**
|
|
10
|
+
* Classify a diff's blast radius (ported from v1 classifyBlastSeverity). The global hard cap is checked first
|
|
11
|
+
* and is independent of any per-gene maxFiles (so it fires even when maxFiles is undefined). When a per-gene
|
|
12
|
+
* maxFiles is given, the over-limit / 2x-critical / approaching bands apply on top.
|
|
13
|
+
*/
|
|
14
|
+
export declare function classifyBlastSeverity(stat: DiffStat, maxFiles?: number, caps?: {
|
|
15
|
+
files: number;
|
|
16
|
+
lines: number;
|
|
17
|
+
}): {
|
|
18
|
+
severity: BlastSeverity;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* GLOBAL blast-radius guard — ALWAYS enforced. Returns a violation when the diff breaches the system hard cap
|
|
23
|
+
* (files or lines), regardless of whether a gene/constraints are present. The per-gene `critical_overrun` band
|
|
24
|
+
* (2x the gene's own max_files) is also surfaced as a violation when a gene cap is supplied; the softer
|
|
25
|
+
* `approaching_limit` band is a warning, not a violation. Per-gene `exceeded` (files > max_files) is handled by
|
|
26
|
+
* constraints.ts so the two layers don't double-report; here we only escalate the global cap + the 2x overrun.
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkBlastRadius(stat: DiffStat, constraints?: ChangeConstraints, caps?: {
|
|
29
|
+
files: number;
|
|
30
|
+
lines: number;
|
|
31
|
+
}): PolicyViolation[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Read a positive integer env override, falling back to a default (mirrors v1 config.js envInt). */
|
|
2
|
+
function envInt(name, fallback) {
|
|
3
|
+
const raw = process.env[name];
|
|
4
|
+
if (raw === undefined || raw === '')
|
|
5
|
+
return fallback;
|
|
6
|
+
const n = Number.parseInt(raw, 10);
|
|
7
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
8
|
+
}
|
|
9
|
+
/** System-wide blast caps. Env-overridable (EVOLVER_HARD_CAP_FILES / EVOLVER_HARD_CAP_LINES), same names/defaults as v1. */
|
|
10
|
+
export function globalHardCaps() {
|
|
11
|
+
return {
|
|
12
|
+
files: envInt('EVOLVER_HARD_CAP_FILES', 60),
|
|
13
|
+
lines: envInt('EVOLVER_HARD_CAP_LINES', 20000),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
// Severity ratio bands ported verbatim from v1 policyCheck.js.
|
|
17
|
+
const BLAST_WARN_RATIO = 0.8;
|
|
18
|
+
const BLAST_CRITICAL_RATIO = 2.0;
|
|
19
|
+
/**
|
|
20
|
+
* Classify a diff's blast radius (ported from v1 classifyBlastSeverity). The global hard cap is checked first
|
|
21
|
+
* and is independent of any per-gene maxFiles (so it fires even when maxFiles is undefined). When a per-gene
|
|
22
|
+
* maxFiles is given, the over-limit / 2x-critical / approaching bands apply on top.
|
|
23
|
+
*/
|
|
24
|
+
export function classifyBlastSeverity(stat, maxFiles, caps = globalHardCaps()) {
|
|
25
|
+
const files = Number(stat.files) || 0;
|
|
26
|
+
const lines = Number(stat.lines) || 0;
|
|
27
|
+
if (files > caps.files || lines > caps.lines) {
|
|
28
|
+
return {
|
|
29
|
+
severity: 'hard_cap_breach',
|
|
30
|
+
message: `HARD CAP BREACH: ${files} files / ${lines} lines exceeds system limit (${caps.files} files / ${caps.lines} lines)`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (maxFiles === undefined || !Number.isFinite(maxFiles) || maxFiles <= 0) {
|
|
34
|
+
return { severity: 'within_limit', message: 'no max_files constraint defined' };
|
|
35
|
+
}
|
|
36
|
+
if (files > maxFiles * BLAST_CRITICAL_RATIO) {
|
|
37
|
+
return {
|
|
38
|
+
severity: 'critical_overrun',
|
|
39
|
+
message: `CRITICAL OVERRUN: ${files} files > ${maxFiles * BLAST_CRITICAL_RATIO} (${BLAST_CRITICAL_RATIO}x limit of ${maxFiles}). Agent likely performed bulk/unintended operation.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (files > maxFiles) {
|
|
43
|
+
return { severity: 'exceeded', message: `max_files exceeded: ${files} > ${maxFiles}` };
|
|
44
|
+
}
|
|
45
|
+
if (files > maxFiles * BLAST_WARN_RATIO) {
|
|
46
|
+
return {
|
|
47
|
+
severity: 'approaching_limit',
|
|
48
|
+
message: `approaching limit: ${files} / ${maxFiles} files (${Math.round((files / maxFiles) * 100)}%)`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { severity: 'within_limit', message: `${files} / ${maxFiles} files` };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* GLOBAL blast-radius guard — ALWAYS enforced. Returns a violation when the diff breaches the system hard cap
|
|
55
|
+
* (files or lines), regardless of whether a gene/constraints are present. The per-gene `critical_overrun` band
|
|
56
|
+
* (2x the gene's own max_files) is also surfaced as a violation when a gene cap is supplied; the softer
|
|
57
|
+
* `approaching_limit` band is a warning, not a violation. Per-gene `exceeded` (files > max_files) is handled by
|
|
58
|
+
* constraints.ts so the two layers don't double-report; here we only escalate the global cap + the 2x overrun.
|
|
59
|
+
*/
|
|
60
|
+
export function checkBlastRadius(stat, constraints, caps = globalHardCaps()) {
|
|
61
|
+
const violations = [];
|
|
62
|
+
const sev = classifyBlastSeverity(stat, constraints?.max_files, caps);
|
|
63
|
+
if (sev.severity === 'hard_cap_breach' || sev.severity === 'critical_overrun') {
|
|
64
|
+
violations.push({ kind: 'blast_radius', detail: sev.message });
|
|
65
|
+
}
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ChangeConstraints {
|
|
2
|
+
max_files?: number;
|
|
3
|
+
/** Per-gene line-churn cap (insertions+deletions). Layers UNDER the global hard cap. */
|
|
4
|
+
max_lines?: number;
|
|
5
|
+
forbidden_paths?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export interface PolicyViolation {
|
|
8
|
+
kind: 'blast_radius' | 'forbidden_path' | 'protected_path' | 'destructive';
|
|
9
|
+
detail: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Does a changed path fall under a forbidden path? Matches on path SEGMENTS, never raw substrings, so
|
|
13
|
+
* "migrations" hits "migrations/001.sql" and "db/migrations/x.sql" but NOT "migrations_helper.ts":
|
|
14
|
+
* - exact match: forbidden "src/x.ts" === changed "src/x.ts"
|
|
15
|
+
* - directory prefix: forbidden "migrations/" / "db/migrations" contains "db/migrations/001.sql"
|
|
16
|
+
* - bare segment (no slash, e.g. "node_modules", ".env"): matches any path containing that exact segment
|
|
17
|
+
*/
|
|
18
|
+
export declare function pathIsForbidden(changed: string, forbidden: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Check the agent's actual changed files (and optional line churn) against the gene's constraints.
|
|
21
|
+
* Undefined/empty constraints = no policy (returns []). Returns every violation (one per offending file for
|
|
22
|
+
* forbidden paths) so the caller can surface a complete reason.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkChangeConstraints(changedFiles: readonly string[], constraints?: ChangeConstraints, lines?: number): PolicyViolation[];
|
|
25
|
+
/** One-line human summary of a violation set (for an outcome reason / audit log). */
|
|
26
|
+
export declare function summarizeViolations(violations: readonly PolicyViolation[]): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Per-gene change-constraint enforcement (ported from v1 src/gep/policyCheck.js checkConstraints — the
|
|
2
|
+
// forbidden_paths + max_files slice). renderExecPrompt only TELLS the agent "touch at most N file(s) / never
|
|
3
|
+
// modify X" — that is advisory. A misbehaving, confused, or prompt-injected agent can ignore it. This module
|
|
4
|
+
// ENFORCES the same limits against the ACTUAL git diff after the agent runs. These checks ONLY apply when a
|
|
5
|
+
// gene with constraints is present; the always-on global guards (blast hard cap + protected paths +
|
|
6
|
+
// destructive) live in their own modules and run regardless. Pure + deterministic.
|
|
7
|
+
const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
8
|
+
/**
|
|
9
|
+
* Does a changed path fall under a forbidden path? Matches on path SEGMENTS, never raw substrings, so
|
|
10
|
+
* "migrations" hits "migrations/001.sql" and "db/migrations/x.sql" but NOT "migrations_helper.ts":
|
|
11
|
+
* - exact match: forbidden "src/x.ts" === changed "src/x.ts"
|
|
12
|
+
* - directory prefix: forbidden "migrations/" / "db/migrations" contains "db/migrations/001.sql"
|
|
13
|
+
* - bare segment (no slash, e.g. "node_modules", ".env"): matches any path containing that exact segment
|
|
14
|
+
*/
|
|
15
|
+
export function pathIsForbidden(changed, forbidden) {
|
|
16
|
+
const c = norm(changed);
|
|
17
|
+
const f = norm(forbidden);
|
|
18
|
+
if (!f)
|
|
19
|
+
return false;
|
|
20
|
+
if (c === f)
|
|
21
|
+
return true;
|
|
22
|
+
if (c.startsWith(`${f}/`))
|
|
23
|
+
return true; // forbidden dir is a path prefix at a segment boundary
|
|
24
|
+
if (!f.includes('/'))
|
|
25
|
+
return c.split('/').includes(f); // bare segment anywhere in the path
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check the agent's actual changed files (and optional line churn) against the gene's constraints.
|
|
30
|
+
* Undefined/empty constraints = no policy (returns []). Returns every violation (one per offending file for
|
|
31
|
+
* forbidden paths) so the caller can surface a complete reason.
|
|
32
|
+
*/
|
|
33
|
+
export function checkChangeConstraints(changedFiles, constraints, lines) {
|
|
34
|
+
const violations = [];
|
|
35
|
+
if (!constraints)
|
|
36
|
+
return violations;
|
|
37
|
+
const files = changedFiles.map(norm).filter(Boolean);
|
|
38
|
+
if (constraints.max_files !== undefined && files.length > constraints.max_files) {
|
|
39
|
+
violations.push({
|
|
40
|
+
kind: 'blast_radius',
|
|
41
|
+
detail: `changed ${files.length} file(s) > max_files ${constraints.max_files}`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (constraints.max_lines !== undefined && lines !== undefined && lines > constraints.max_lines) {
|
|
45
|
+
violations.push({
|
|
46
|
+
kind: 'blast_radius',
|
|
47
|
+
detail: `changed ${lines} line(s) > max_lines ${constraints.max_lines}`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (constraints.forbidden_paths?.length) {
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const hit = constraints.forbidden_paths.find((fp) => pathIsForbidden(file, fp));
|
|
53
|
+
if (hit)
|
|
54
|
+
violations.push({ kind: 'forbidden_path', detail: `${file} matches forbidden path "${hit}"` });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return violations;
|
|
58
|
+
}
|
|
59
|
+
/** One-line human summary of a violation set (for an outcome reason / audit log). */
|
|
60
|
+
export function summarizeViolations(violations) {
|
|
61
|
+
return `constraint violation: ${violations.map((v) => v.detail).join('; ')}`;
|
|
62
|
+
}
|