Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions assets/gep/genes.seed.json
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,59 @@
"execing or deserializing anything from the payload"
],
"asset_id": "sha256:ac2a2f185390aef37996651ef21355f4beb437049e52a6ca3898619a8d648084"
},
{
"type": "Gene",
"id": "gene_shared_sentinel_arena_ci_gate",
"category": "optimize",
"signals_match": [
"sentinel_arena",
"shared_ci_gate",
"github_actions_composite_action",
"self_hosted_runner",
"open_pr_sweep",
"private_internal_action_access",
"对抗性流水线",
"共享sentinel",
"反向依赖",
"本地runner"
],
"strategy": [
"Extract the adversarial quality gate into a dedicated shared repository/action first; product repositories should keep only policy config and thin local wrappers",
"Make consumers reverse-depend on the shared action in CI and keep a fast verifier that asserts the workflow uses the shared action, uploads reports, and preserves fail-on-review policy",
"During migration, land narrow main-branch compatibility bridges before tightening consumer contracts, because pull_request_target enforcers execute from main while validating PR head files",
"For internal/private action repositories, set GitHub Actions repository access to organization; otherwise consumers fail during action resolution before the gate can run",
"Validate in layers: local contract verifier, shared action verifier, dry-run adversarial scan, PR matrix CI, self-hosted sentinel arena job, post-merge main CI, and open PR sweep",
"Treat local sandbox failures separately from runner authority: localhost bind/cache/DNS failures may be environmental, but remote self-hosted CI and uploaded reports are authoritative for merge decisions"
],
"validation": [
"node --version"
],
"constraints": {
"max_files": 30,
"forbidden_paths": [
".git",
"node_modules"
]
},
"preconditions": [
"a product repository needs an adversarial quality gate shared across multiple EvoMap repos",
"GitHub Actions runs on organization-owned self-hosted runners or internal/private action repos"
],
"summary": "Roll out a reusable lowercase sentinel arena CI gate: extract the runner into a shared action, make product repos reverse-depend on it, bridge pull_request_target enforcers during migration, enable internal action repository access, and verify via PR CI, main CI, and open PR sweep.",
"schema_version": "1.6.0",
"epigenetic_marks": [],
"learning_history": [],
"anti_patterns": [],
"routing_hint": null,
"tool_policy": null,
"avoid": [
"vendoring the same arena runner into each product repository",
"tightening PR-head contracts before main pull_request_target enforcers can accept the new shape",
"forgetting GitHub Actions access on the internal action repository, which fails before the job starts",
"treating local sandbox DNS/localhost/cache failures as product regressions without checking self-hosted runner evidence"
],
"asset_id": "sha256:253e9b8c5bf627aa807aed421952751c0354b64b5e87d7d3e1f766b00b915e6f"
}
]
}
36 changes: 18 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const { solidify } = require('./src/gep/solidify');
const path = require('path');
const os = require('os');
const { getRepoRoot } = require('./src/gep/paths');
const { resolveLoopBridgeMode } = require('./src/evolve/loopBridgeMode');
const fs = require('fs');
const { spawn } = require('child_process');

Expand Down Expand Up @@ -254,6 +255,14 @@ function parseBoolEnv(v, fallback) {
return fallback;
}

function classifyInvocation(args) {
const argv = Array.isArray(args) ? args : [];
const command = argv[0];
const isLoop = argv.includes('--loop') || argv.includes('--mad-dog');
const startsEvolution = !command || command === 'run' || command === '/evolve' || isLoop;
return { command, isLoop, startsEvolution };
}

class CycleTimeoutError extends Error {
constructor(timeoutMs, phase, cycleNum) {
super('Cycle hard-timeout exceeded after ' + timeoutMs + 'ms (cycle=' + cycleNum + ', phase=' + phase + ')');
Expand Down Expand Up @@ -624,13 +633,14 @@ function refuseHelloIfDaemonRunning(toolLabel) {

async function main() {
const args = process.argv.slice(2);
const command = args[0];
const isLoop = args.includes('--loop') || args.includes('--mad-dog');
const invocation = classifyInvocation(args);
const command = invocation.command;
const isLoop = invocation.isLoop;
const isVerbose = args.includes('--verbose') || args.includes('-v') ||
String(process.env.EVOLVER_VERBOSE || '').toLowerCase() === 'true';
if (isVerbose) process.env.EVOLVER_VERBOSE = 'true';

if (!command || command === 'run' || command === '/evolve' || isLoop) {
if (invocation.startsEvolution) {
if (isLoop) {
// EPIPE protection. The daemon may outlive the controlling
// terminal (user closes the iTerm tab, ssh session drops, parent
Expand Down Expand Up @@ -1097,21 +1107,9 @@ async function main() {
// by default since v1.81.0): the daemon's changes get pushed to a
// stash entry the user can recover with `git stash pop`.
// Set EVOLVE_BRIDGE=false explicitly to opt back into observe-only.
if (!process.env.EVOLVE_BRIDGE) {
process.env.EVOLVE_BRIDGE = 'true';
}
const bridgeEnabled = String(process.env.EVOLVE_BRIDGE).toLowerCase() !== 'false';
console.log(`Loop mode enabled (internal daemon, bridge=${process.env.EVOLVE_BRIDGE}, verbose=${isVerbose}).`);
if (bridgeEnabled) {
console.warn('[Daemon] EVOLVE_BRIDGE=true (default since v1.85.0).');
console.warn('[Daemon] evolver may modify your working tree.');
console.warn('[Daemon] Failed cycles auto-stash via "git stash push --include-untracked".');
console.warn('[Daemon] Recover: git stash list | grep evolver-rollback');
console.warn('[Daemon] Set EVOLVE_BRIDGE=false to opt out (observe-only mode).');
} else {
console.warn('[Daemon] EVOLVE_BRIDGE=false: evolver will NOT modify your working tree (observe-only).');
console.warn('[Daemon] To enable real evolution: unset EVOLVE_BRIDGE or set it to "true".');
}
const bridgeMode = resolveLoopBridgeMode(process.env);
console.log(`Loop mode enabled (internal daemon, bridge=${bridgeMode.value}, verbose=${isVerbose}).`);
for (const line of bridgeMode.banner) console.warn(line);

// Startup diagnostic: in daemon mode evolver consumes its own stdout
// instead of handing `sessions_spawn(...)` directives to a host
Expand Down Expand Up @@ -3136,6 +3134,8 @@ module.exports = {
rejectPendingRun,
isPendingSolidify,
parseBoolEnv,
classifyInvocation,
resolveLoopBridgeMode,
CycleTimeoutError,
writeCycleProgressAtomic,
spawnReplacementProcess,
Expand Down
23 changes: 23 additions & 0 deletions scripts/com.evomap.evolver.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.evomap.evolver</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/usr/local/lib/node_modules/@evomap/evolver/index.js</string>
<string>--loop</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/evomap-evolver.out.log</string>
<key>StandardErrorPath</key>
<string>/tmp/evomap-evolver.err.log</string>
</dict>
</plist>
21 changes: 21 additions & 0 deletions scripts/evolver.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[Unit]
Description=EvoMap Evolver daemon
After=network-online.target

[Service]
Type=notify
Environment=EVOMAP_PROXY=1
Environment=EVOMAP_PROXY_TRACE=metadata
Environment=EVOMAP_PROXY_TRACE_FILE=%h/.local/state/evomap/proxy-traces.jsonl
WorkingDirectory=%h
ExecStart=/usr/bin/env node %h/.npm-global/lib/node_modules/@evomap/evolver/index.js --loop
Restart=on-failure
RestartSec=5
WatchdogSec=60
StandardOutput=journal
StandardError=journal
# proxy_trace_failed_once is emitted by the proxy trace writer when local trace
# persistence is unavailable; journald captures it for diagnostics.

[Install]
WantedBy=default.target
31 changes: 31 additions & 0 deletions scripts/install-evolver-windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
param(
[ValidateSet('metadata', 'full', 'off')]
[string]$TraceMode = 'metadata',
[string]$TraceFile = "$env:LOCALAPPDATA\EvoMap\proxy-traces.jsonl"
)

$launcherDir = Join-Path $env:LOCALAPPDATA 'EvoMap'
$launcherDir = Join-Path $launcherDir 'Evolver'
New-Item -ItemType Directory -Force -Path $launcherDir | Out-Null
$launcherPath = Join-Path $launcherDir 'evolver-loop.vbs'

$traceModeEsc = $TraceMode.Replace('"', '""')
$traceFileEsc = $TraceFile.Replace('"', '""')
$node = (Get-Command node).Source.Replace('"', '""')
$index = "$PSScriptRoot\..\index.js".Replace('"', '""')

$launcherBody = @"
Set WshShell = CreateObject("WScript.Shell")
Set env = WshShell.Environment("PROCESS")
env("EVOMAP_PROXY") = "1"
env("EVOMAP_PROXY_TRACE") = "$traceModeEsc"
env("EVOMAP_PROXY_TRACE_FILE") = "$traceFileEsc"
cmd = """$node"" ""$index"" --loop"
result = WshShell.Run(cmd, 0, True)
"@
Set-Content -Path $launcherPath -Value $launcherBody -Encoding Unicode

$action = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument "`"$launcherPath`""
$settings = New-ScheduledTaskSettingsSet -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName 'EvoMap Evolver' -Action $action -Settings $settings -Force | Out-Null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows task lacks startup trigger

Medium Severity

Register-ScheduledTask registers the EvoMap Evolver action and restart settings but no -Trigger (for example at logon). The task is created yet never starts automatically, unlike the macOS plist RunAtLoad and typical daemon install expectations after running the installer.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d6fcba. Configure here.

Write-Host "Installed EvoMap Evolver scheduled task."
33 changes: 33 additions & 0 deletions scripts/internal-proxy-env.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
param(
[string]$Settings = "$HOME\.evolver\settings.json",
[switch]$PrintSensitiveEnv
)

function Test-NonEmptyString($Value) {
return ($Value -is [string]) -and (-not [string]::IsNullOrWhiteSpace($Value))
}

function Warn-ExistingAnthropicApiKey {
if (Test-NonEmptyString $env:ANTHROPIC_API_KEY) {
Write-Warning 'ANTHROPIC_API_KEY is already set in this PowerShell session; internal-proxy-env.ps1 does not overwrite it.'
}
}

$settingsJson = Get-Content -Raw -Path $Settings | ConvertFrom-Json
$proxy = $settingsJson.proxy
if ((-not (Test-NonEmptyString $proxy.url)) -or (-not (Test-NonEmptyString $proxy.token))) {
throw 'no active string proxy.url/proxy.token in settings'
}

Warn-ExistingAnthropicApiKey
$proxyUrl = $proxy.url.TrimEnd('/')
$proxyToken = $proxy.token
$env:ANTHROPIC_BASE_URL = "$proxyUrl/v1"
$env:ANTHROPIC_AUTH_TOKEN = $proxyToken

if ($PrintSensitiveEnv) {
Write-Output "`$env:ANTHROPIC_BASE_URL = '$($env:ANTHROPIC_BASE_URL)'"
Write-Output "`$env:ANTHROPIC_AUTH_TOKEN = '$proxyToken'"
} else {
Write-Host "EvoMap Proxy environment applied for $proxyUrl"
}
57 changes: 57 additions & 0 deletions scripts/internal-proxy-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail

settings_file=""
codex_config=0

while [ "$#" -gt 0 ]; do
case "$1" in
--settings)
if [ "$#" -lt 2 ]; then
echo "internal-proxy-env: missing value for --settings" >&2
exit 2
fi
settings_file="$2"
shift 2
;;
--codex-config)
codex_config=1
shift
;;
*)
echo "internal-proxy-env: unknown argument: $1" >&2
exit 2
;;
esac
done

node_bin="${NODE:-node}"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
index_js="$repo_root/index.js"

url="$("$node_bin" -e '
const fs = require("fs");
const file = process.argv[1];
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
const url = parsed && parsed.proxy && parsed.proxy.url;
if (typeof url !== "string" || !url.trim()) process.exit(1);
process.stdout.write(url.replace(/\/+$/, ""));
' "$settings_file")"

if [ "$codex_config" -eq 1 ]; then
printf '[model_providers.evomap_proxy]\n'
printf 'name = "EvoMap Proxy"\n'
printf 'base_url = "%s/v1"\n' "$url"
printf 'wire_api = "responses"\n'
printf 'env_key = "ANTHROPIC_AUTH_TOKEN"\n'
printf 'env_key_command = { command = %s, args = [%s, %s, %s] }\n' \
"$(node -p 'JSON.stringify(process.execPath)')" \
"$(node -p 'JSON.stringify(process.argv[1])' "$index_js")" \
"$(node -p 'JSON.stringify("proxy-token")')" \
"$(node -p 'JSON.stringify("--settings")'), $(node -p 'JSON.stringify(process.argv[1])' "$settings_file")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex env_key_command args malformed

Medium Severity

The --codex-config branch builds env_key_command with only three args placeholders but four CLI tokens are needed (index.js, proxy-token, --settings, settings path). The fourth printf argument merges --settings and the settings path into one invalid TOML fragment, so Codex cannot run the proxy-token helper correctly.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d6fcba. Configure here.

exit 0
fi

printf 'export ANTHROPIC_BASE_URL=%q\n' "$url/v1"
printf 'export ANTHROPIC_AUTH_TOKEN="$("%q" "%q" proxy-token --settings "%q")"\n' "$node_bin" "$index_js" "$settings_file"
12 changes: 12 additions & 0 deletions src/evolve/bridgeMode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

function determineBridgeEnabled(env) {
const source = env || process.env;
const raw = source.EVOLVE_BRIDGE;
if (raw !== undefined && String(raw) !== '') {
return String(raw).toLowerCase() !== 'false';
}
return !!String(source.OPENCLAW_WORKSPACE || '').trim();
}

module.exports = { determineBridgeEnabled };
28 changes: 28 additions & 0 deletions src/evolve/loopBridgeMode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

function resolveLoopBridgeMode(env) {
const source = env || process.env;
if (!source.EVOLVE_BRIDGE) {
source.EVOLVE_BRIDGE = 'true';
}
const value = String(source.EVOLVE_BRIDGE);
const enabled = value.toLowerCase() !== 'false';
return {
value,
enabled,
banner: enabled
? [
'[Daemon] EVOLVE_BRIDGE=true (default since v1.85.0).',
'[Daemon] evolver may modify your working tree.',
'[Daemon] Failed cycles auto-stash via "git stash push --include-untracked".',
'[Daemon] Recover: git stash list | grep evolver-rollback',
'[Daemon] Set EVOLVE_BRIDGE=false to opt out (observe-only mode).',
]
: [
'[Daemon] EVOLVE_BRIDGE=false: evolver will NOT modify your working tree (observe-only).',
'[Daemon] To enable real evolution: unset EVOLVE_BRIDGE or set it to "true".',
],
};
}

module.exports = { resolveLoopBridgeMode };
1 change: 1 addition & 0 deletions src/gep/gitOps.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function isGitRepo(dir) {
execSync('git rev-parse --git-dir', {
cwd: dir, encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000, maxBuffer: MAX_EXEC_BUFFER,
windowsHide: true,
});
return true;
} catch (_) {
Expand Down
Loading
Loading