()
| 831 | } |
| 832 | |
| 833 | async function main() { |
| 834 | const args = process.argv.slice(2); |
| 835 | const command = args[0]; |
| 836 | // Solo mode (--solo): the "constrained wild" profile. Implies loop (a solo |
| 837 | // run IS a Mad Dog loop) but hard-cuts the network + ATP, snapshots the |
| 838 | // target repo each cycle and rolls back on failure, and circuit-breaks on |
| 839 | // repeated failures instead of blind-respawning. It is a NEW side path — the |
| 840 | // wild --mad-dog / controlled-daemon behavior below is untouched when isSolo |
| 841 | // is false. |
| 842 | const isSolo = args.includes('--solo'); |
| 843 | const isLoop = args.includes('--loop') || args.includes('--mad-dog') || isSolo; |
| 844 | // Solo lockdown: hard-cut network + autonomous spend at the SOURCE, in-process, |
| 845 | // before any module reads these envs. The block-level `if (!isSolo)` gates |
| 846 | // below are belt-and-suspenders for the startup path; THIS is what also closes |
| 847 | // the in-cycle paths (src/evolve/guards.js starts autoBuyer/autoDeliver from |
| 848 | // inside evolve.run()) and the validator daemon (network + staked credits). |
| 849 | // These are overwrites, not defaults — a user cannot re-enable ATP/hub/validator |
| 850 | // under --solo by setting the env themselves. That is the "no escape valve" the |
| 851 | // boss pinned. Numeric knobs (e.g. EVOLVER_SOLO_MAX_FAILS) stay tunable; the |
| 852 | // four safety cuts do not. |
| 853 | if (isSolo) { |
| 854 | process.env.A2A_HUB_URL = ''; |
| 855 | process.env.EVOMAP_HUB_URL = ''; |
| 856 | process.env.EVOLVER_ATP = 'off'; // getAtpMode() → 'off' |
| 857 | process.env.EVOLVER_ATP_AUTOBUY = 'off'; // getConsent() → disabled (env wins over ack) |
| 858 | process.env.EVOLVER_ATP_AUTODELIVER = 'off'; |
| 859 | process.env.EVOLVER_VALIDATOR_ENABLED = 'false'; |
| 860 | process.env.EVOMAP_PROXY = '0'; |
| 861 | } |
| 862 | const isVerbose = args.includes('--verbose') || args.includes('-v') || |
| 863 | String(process.env.EVOLVER_VERBOSE || '').toLowerCase() === 'true'; |
| 864 | if (isVerbose) process.env.EVOLVER_VERBOSE = 'true'; |
| 865 | |
| 866 | if (!command || command === 'run' || command === '/evolve' || isLoop) { |
| 867 | if (isLoop) { |
| 868 | // EPIPE protection. The daemon may outlive the controlling |
| 869 | // terminal (user closes the iTerm tab, ssh session drops, parent |
| 870 | // shell exits). The SIGHUP handler below covers the signal side, |
| 871 | // but the underlying pty fd is gone and the FIRST subsequent |
| 872 | // console.log writes to a closed pipe -> stdout emits 'error' |
| 873 | // with EPIPE. Without a listener attached, Node escalates EPIPE |
| 874 | // to uncaughtException, which our handler then turns into |
| 875 | // process.exit(1). Net result: daemon silently dies the next |
| 876 | // time it tries to log, with no useful trace. Swallow EPIPE |
| 877 | // explicitly so the daemon stays alive when its terminal goes |
| 878 | // away (matching standard daemonization practice). |
| 879 | try { |
| 880 | // EPIPE: swallow (daemon must outlive its controlling terminal). |
| 881 | // Non-EPIPE (EIO, ENOSPC on redirected log, etc.): the listener |
| 882 | // already prevents 'error' from escalating to uncaughtException, |
| 883 | // so write a one-line trace to the *other* stream so operators |
| 884 | // can see the failure mode instead of finding a silent daemon. |
| 885 | process.stdout.on('error', function (err) { |
| 886 | if (err && err.code === 'EPIPE') return; |
| 887 | try { process.stderr.write('[evolver] stdout error: ' + (err && (err.code || err.message) || err) + '\n'); } catch (_) {} |
| 888 | }); |
| 889 | process.stderr.on('error', function (err) { |
| 890 | if (err && err.code === 'EPIPE') return; |
no test coverage detected