Files
ozone-tech_owl_prime/agent/cli.mjs
Даня Архипов b5985f33bb feat: ship production deploy, Playwright e2e, and agent implementer
Add Docker atomic deploy tooling, CI, Playwright smoke/visual suite,
shared industrial theme, physics invariants, quality-mode wiring,
and a worktree-isolated agent implementer MVP with hard safety limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 17:14:01 +00:00

1147 lines
33 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Autonomous improvement agent — Implementer MVP.
*
* Modes: dry-run | run-once | implement | start | status | stop | resume | pause | report
* NEVER merges to main, NEVER deploys production, NEVER pushes.
* No external LLM required — deterministic local demo-task path.
*/
import { spawnSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
appendFileSync,
unlinkSync,
rmSync,
symlinkSync,
lstatSync,
} from 'node:fs';
import { cpus, freemem, totalmem, loadavg } from 'node:os';
import { dirname, join, resolve, relative, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
const STATE_DIR = join(ROOT, 'agent', 'state');
const REPORTS_DIR = join(ROOT, 'agent', 'reports');
const TASKS_DIR = join(ROOT, 'agent', 'tasks');
const WORKTREES_DIR = join(ROOT, '.agent', 'worktrees');
const KILL_SWITCH = join(STATE_DIR, 'KILL');
const LOCK_FILE = join(STATE_DIR, 'agent.lock');
const STATUS_FILE = join(STATE_DIR, 'status.json');
const AUDIT_LOG = join(STATE_DIR, 'audit.jsonl');
const LIMITS = {
maxCycleMinutes: 25,
maxChangedFiles: 12,
maxDiffLines: 800,
maxParallelWorkers: 1,
dailyLlmBudgetUsd: 5,
forbidMergeToMain: true,
forbidProductionDeploy: true,
forbidSecretAccess: true,
requireTestsPass: true,
requireBuildPass: true,
minScoreDelta: 0,
maxDiskUsagePercent: 85,
minMemAvailableGiB: 1,
};
/** Policy allowlist for Implementer patches (no LLM needed). */
const POLICY_ALLOWLIST = [
'docs',
'tests',
'data-testid',
'a11y',
'small-ui',
'logging-scripts',
];
const FORBIDDEN_PATH_PREFIXES = [
'.env',
'docker',
'nginx',
'deploy',
'releases/',
];
const DEFAULT_TASK_PATH = join(TASKS_DIR, 'demo-docs.json');
const DEMO_DOCS_CONTENT = `# Agent Cycle Demo
This file was created by the Implementer MVP as a **safe, docs-only** demonstration patch.
- Worktree-isolated (never applied to production automatically)
- Policy category: \`docs\`
- No merge, no deploy, no push
Generated at: {{TIMESTAMP}}
Run id: {{RUN_ID}}
## Manual review
1. Inspect the worktree and branch named in the cycle report.
2. If acceptable, copy or cherry-pick into your feature branch yourself.
3. Never let the agent merge to \`main\` or deploy.
`;
function ensureDirs() {
mkdirSync(STATE_DIR, { recursive: true });
mkdirSync(REPORTS_DIR, { recursive: true });
mkdirSync(TASKS_DIR, { recursive: true });
mkdirSync(WORKTREES_DIR, { recursive: true });
}
function nowIso() {
return new Date().toISOString();
}
function audit(event, payload = {}) {
ensureDirs();
appendFileSync(AUDIT_LOG, `${JSON.stringify({ ts: nowIso(), event, ...payload })}\n`);
}
function writeStatus(status) {
ensureDirs();
writeFileSync(STATUS_FILE, JSON.stringify({ ...status, updatedAt: nowIso() }, null, 2));
}
function readStatus() {
if (!existsSync(STATUS_FILE)) return { state: 'idle' };
return JSON.parse(readFileSync(STATUS_FILE, 'utf8'));
}
function isKilled() {
return existsSync(KILL_SWITCH);
}
function assertNotKilled() {
if (isKilled()) {
console.error('Kill switch active. Remove agent/state/KILL (or run: resume) to continue.');
process.exit(2);
}
}
function run(cmd, args, opts = {}) {
const cwd = opts.cwd ?? ROOT;
const timeout = opts.timeout ?? 15 * 60 * 1000;
const result = spawnSync(cmd, args, {
cwd,
encoding: 'utf8',
timeout,
env: { ...process.env, AGENT_MODE: '1' },
});
return {
code: result.status ?? (result.error ? 1 : 0),
stdout: result.stdout ?? '',
stderr: result.stderr ?? '',
error: result.error ? String(result.error.message || result.error) : null,
signal: result.signal ?? null,
};
}
function git(args, opts = {}) {
return run('git', args, opts);
}
function slugify(value) {
return String(value)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 48) || 'task';
}
/* ─── Watchdog ─────────────────────────────────────────────── */
function readMemAvailableBytes() {
try {
const meminfo = readFileSync('/proc/meminfo', 'utf8');
const m = meminfo.match(/^MemAvailable:\s+(\d+)\s+kB/m);
if (m) return Number(m[1]) * 1024;
} catch {
/* fall through */
}
return freemem();
}
function diskUsagePercent(mount = '/') {
const result = run('df', ['-P', mount]);
if (result.code !== 0) {
return { ok: false, reason: `df failed: ${result.stderr || result.error}` };
}
const lines = result.stdout.trim().split('\n');
const data = lines[lines.length - 1]?.trim().split(/\s+/);
// df -P: Filesystem 1024-blocks Used Available Capacity Mounted
const capacity = data?.[4];
if (!capacity) return { ok: false, reason: 'could not parse df output' };
const pct = Number(String(capacity).replace('%', ''));
if (Number.isNaN(pct)) return { ok: false, reason: `invalid capacity: ${capacity}` };
return { ok: true, percent: pct, mount };
}
function watchdogChecks() {
const failures = [];
if (isKilled()) {
failures.push('kill switch active (agent/state/KILL)');
}
const disk = diskUsagePercent('/');
if (!disk.ok) {
failures.push(`disk check failed: ${disk.reason}`);
} else if (disk.percent > LIMITS.maxDiskUsagePercent) {
failures.push(`disk usage ${disk.percent}% > ${LIMITS.maxDiskUsagePercent}% on ${disk.mount}`);
}
const nproc = Math.max(cpus().length, 1);
const load1 = loadavg()[0];
if (load1 > nproc) {
failures.push(`loadavg 1m ${load1.toFixed(2)} > nproc ${nproc}`);
}
const memAvail = readMemAvailableBytes();
const minBytes = LIMITS.minMemAvailableGiB * 1024 ** 3;
if (memAvail < minBytes) {
const availGiB = (memAvail / 1024 ** 3).toFixed(2);
failures.push(`MemAvailable ${availGiB} GiB < ${LIMITS.minMemAvailableGiB} GiB`);
}
return {
ok: failures.length === 0,
failures,
snapshot: {
diskPercent: disk.ok ? disk.percent : null,
load1,
nproc,
memAvailableBytes: memAvail,
memAvailableGiB: Number((memAvail / 1024 ** 3).toFixed(3)),
totalMemGiB: Number((totalmem() / 1024 ** 3).toFixed(3)),
killSwitch: isKilled(),
},
};
}
function pidAlive(pid) {
if (!pid || !Number.isFinite(pid)) return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function acquireLock(runId) {
ensureDirs();
if (existsSync(LOCK_FILE)) {
let existing = null;
try {
existing = JSON.parse(readFileSync(LOCK_FILE, 'utf8'));
} catch {
existing = null;
}
if (existing?.pid && pidAlive(existing.pid)) {
console.error(
`[agent] refuse: another agent is running (pid ${existing.pid}, runId ${existing.runId ?? '?'}).`,
);
console.error(`[agent] lock: ${LOCK_FILE}`);
process.exit(1);
}
console.warn('[agent] stale lock found — taking over');
audit('lock_stale_takeover', { previous: existing });
}
const lock = {
pid: process.pid,
runId,
startedAt: nowIso(),
cwd: ROOT,
};
writeFileSync(LOCK_FILE, JSON.stringify(lock, null, 2));
audit('lock_acquired', lock);
return lock;
}
function releaseLock() {
if (!existsSync(LOCK_FILE)) return;
try {
const existing = JSON.parse(readFileSync(LOCK_FILE, 'utf8'));
if (existing.pid && existing.pid !== process.pid && pidAlive(existing.pid)) {
console.warn('[agent] lock owned by another pid — not releasing');
return;
}
} catch {
/* remove anyway if unreadable and ours */
}
try {
unlinkSync(LOCK_FILE);
audit('lock_released', { pid: process.pid });
} catch {
/* ignore */
}
}
/* ─── Baseline / scoring (shared with dry-run / run-once) ───── */
function collectBaseline(cwd = ROOT) {
const tests = run('npm', ['test'], { cwd });
const build = run('npm', ['run', 'build'], { cwd });
return {
testsPassed: tests.code === 0,
buildPassed: build.code === 0,
testOutputTail: (tests.stdout || tests.stderr).split('\n').slice(-20).join('\n'),
buildOutputTail: (build.stdout || build.stderr).split('\n').slice(-20).join('\n'),
};
}
function proposeTasks(baseline) {
const backlog = [
{
id: 'unify-classifier-proof',
title: 'Keep classifyItem as single decision source on main demo',
impact: 9,
risk: 2,
},
{
id: 'safety-playlist',
title: 'Jam / E-stop visible on main playlist',
impact: 8,
risk: 3,
},
{
id: 'demo-hotkeys',
title: 'Presenter seek/speed/presentation hotkeys',
impact: 7,
risk: 2,
},
{
id: 'perf-quality-modes',
title: 'Adaptive quality modes for demo FPS',
impact: 6,
risk: 2,
},
];
if (!baseline.testsPassed || !baseline.buildPassed) {
return {
id: 'stabilize-baseline',
title: 'Restore green tests/build before further changes',
impact: 10,
risk: 1,
};
}
return backlog.sort((a, b) => b.impact - a.impact)[0];
}
function scoreCategories(baseline, notes) {
const categories = {
visualRealism: 72,
physicsFidelity: 68,
demoClarity: 80,
technicalStability: baseline.testsPassed && baseline.buildPassed ? 88 : 40,
performance: 78,
faultTolerance: 75,
architecture: 76,
testCoverage: 82,
launchConvenience: 84,
logging: 74,
security: 90,
hardwareFit: 86,
};
const values = Object.values(categories);
const total = Math.round(values.reduce((a, b) => a + b, 0) / values.length);
return { categories, total, notes };
}
function writeReport(runId, report) {
ensureDirs();
const path = join(REPORTS_DIR, `${runId}.json`);
writeFileSync(path, JSON.stringify(report, null, 2));
writeFileSync(join(REPORTS_DIR, 'latest.json'), JSON.stringify(report, null, 2));
return path;
}
/* ─── Task loading & policy ─────────────────────────────────── */
function loadTaskFile(taskPath) {
const abs = resolve(ROOT, taskPath);
if (!existsSync(abs)) {
throw new Error(`Task file not found: ${abs}`);
}
const task = JSON.parse(readFileSync(abs, 'utf8'));
if (!task.id || !task.title) {
throw new Error('Task file must include id and title');
}
task.slug = task.slug || slugify(task.id);
task.policyCategory = task.policyCategory || task.category || 'docs';
task._sourcePath = abs;
return task;
}
function defaultDemoTask() {
if (existsSync(DEFAULT_TASK_PATH)) {
return loadTaskFile(DEFAULT_TASK_PATH);
}
return {
id: 'demo-docs',
title: 'Add agent cycle demo documentation',
slug: 'demo-docs',
policyCategory: 'docs',
description: 'Safe docs-only demo patch for Implementer MVP (no LLM).',
allowPaths: ['docs/AGENT_CYCLE_DEMO.md'],
patch: {
type: 'create-file',
path: 'docs/AGENT_CYCLE_DEMO.md',
contentTemplate: 'builtin-demo-docs',
},
};
}
function validateTaskPolicy(task) {
const errors = [];
const category = task.policyCategory || task.category;
if (!POLICY_ALLOWLIST.includes(category)) {
errors.push(
`policy category "${category}" not in allowlist: ${POLICY_ALLOWLIST.join(', ')}`,
);
}
const paths = [];
if (Array.isArray(task.allowPaths)) paths.push(...task.allowPaths);
if (task.patch?.path) paths.push(task.patch.path);
if (Array.isArray(task.patch?.files)) {
for (const f of task.patch.files) {
if (f.path) paths.push(f.path);
}
}
for (const p of paths) {
const norm = p.replace(/\\/g, '/');
if (norm.includes('..')) {
errors.push(`path escapes not allowed: ${p}`);
continue;
}
for (const bad of FORBIDDEN_PATH_PREFIXES) {
if (norm === bad || norm.startsWith(bad) || basename(norm) === '.env') {
errors.push(`forbidden path: ${p}`);
}
}
if (category === 'docs' && !norm.startsWith('docs/')) {
errors.push(`docs policy requires path under docs/: ${p}`);
}
if (category === 'tests' && !(norm.startsWith('src/') || norm.includes('.test.') || norm.includes('.spec.') || norm.startsWith('tests/'))) {
errors.push(`tests policy path looks unsafe: ${p}`);
}
if (category === 'logging-scripts' && !norm.startsWith('scripts/')) {
errors.push(`logging-scripts policy requires scripts/: ${p}`);
}
}
if (task.requiresLlm) {
errors.push('tasks requiring external LLM are not supported in this MVP');
}
return { ok: errors.length === 0, errors, category, paths };
}
function resolvePatchContent(task, runId) {
const patch = task.patch || {};
if (patch.content) return patch.content;
if (patch.contentTemplate === 'builtin-demo-docs' || !patch.content) {
return DEMO_DOCS_CONTENT.replaceAll('{{TIMESTAMP}}', nowIso()).replaceAll(
'{{RUN_ID}}',
runId,
);
}
return String(patch.content);
}
/* ─── Worktree + patch ──────────────────────────────────────── */
function ensureNodeModulesLink(worktreePath) {
const target = join(worktreePath, 'node_modules');
const source = join(ROOT, 'node_modules');
if (!existsSync(source)) {
throw new Error('Root node_modules missing — run npm install in repo root first');
}
if (existsSync(target)) {
try {
const st = lstatSync(target);
if (st.isSymbolicLink() || st.isDirectory()) return;
} catch {
/* recreate below */
}
}
symlinkSync(source, target, 'dir');
}
function createWorktree(runId, taskSlug) {
mkdirSync(WORKTREES_DIR, { recursive: true });
const worktreePath = join(WORKTREES_DIR, runId);
const branch = `agent/${runId}/${taskSlug}`;
if (existsSync(worktreePath)) {
throw new Error(`Worktree path already exists: ${worktreePath}`);
}
// Drop leftover branch name if present (safe local delete only).
const branchCheck = git(['rev-parse', '--verify', branch]);
if (branchCheck.code === 0) {
git(['branch', '-D', branch]);
}
const add = git(['worktree', 'add', '-b', branch, worktreePath, 'HEAD']);
if (add.code !== 0) {
throw new Error(`git worktree add failed: ${add.stderr || add.stdout || add.error}`);
}
ensureNodeModulesLink(worktreePath);
return { worktreePath, branch };
}
function applyBoundedPatch(worktreePath, task, runId) {
const patch = task.patch || { type: 'create-file', path: 'docs/AGENT_CYCLE_DEMO.md' };
const type = patch.type || 'create-file';
const changed = [];
if (type === 'create-file' || type === 'write-file') {
const rel = patch.path || 'docs/AGENT_CYCLE_DEMO.md';
const abs = join(worktreePath, rel);
mkdirSync(dirname(abs), { recursive: true });
const content = resolvePatchContent(task, runId);
writeFileSync(abs, content);
changed.push(rel);
} else if (type === 'multi' && Array.isArray(patch.files)) {
for (const f of patch.files) {
const abs = join(worktreePath, f.path);
mkdirSync(dirname(abs), { recursive: true });
writeFileSync(abs, f.content ?? '');
changed.push(f.path);
}
} else {
throw new Error(`Unsupported patch type: ${type}`);
}
const add = git(['add', '--', ...changed], { cwd: worktreePath });
if (add.code !== 0) {
throw new Error(`git add failed: ${add.stderr || add.stdout}`);
}
const commit = git(
['commit', '-m', `agent(${runId}): ${task.id} — safe implementer demo`],
{ cwd: worktreePath },
);
if (commit.code !== 0) {
throw new Error(`git commit failed: ${commit.stderr || commit.stdout}`);
}
return changed;
}
function measureDiff(worktreePath, baseRef = 'HEAD~1') {
const nameOnly = git(['diff', '--name-only', baseRef, 'HEAD'], { cwd: worktreePath });
const files = nameOnly.stdout
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
const numstat = git(['diff', '--numstat', baseRef, 'HEAD'], { cwd: worktreePath });
let added = 0;
let deleted = 0;
for (const line of numstat.stdout.split('\n')) {
const parts = line.trim().split(/\s+/);
if (parts.length < 3) continue;
const a = parts[0] === '-' ? 0 : Number(parts[0]);
const d = parts[1] === '-' ? 0 : Number(parts[1]);
if (!Number.isNaN(a)) added += a;
if (!Number.isNaN(d)) deleted += d;
}
return {
files,
fileCount: files.length,
diffLines: added + deleted,
added,
deleted,
};
}
function enforceDiffLimits(diff) {
const errors = [];
if (diff.fileCount > LIMITS.maxChangedFiles) {
errors.push(`changed files ${diff.fileCount} > max ${LIMITS.maxChangedFiles}`);
}
if (diff.diffLines > LIMITS.maxDiffLines) {
errors.push(`diff lines ${diff.diffLines} > max ${LIMITS.maxDiffLines}`);
}
return { ok: errors.length === 0, errors };
}
function removeWorktreeAndBranch(worktreePath, branch) {
if (worktreePath && existsSync(worktreePath)) {
const rm = git(['worktree', 'remove', '--force', worktreePath]);
if (rm.code !== 0) {
try {
rmSync(worktreePath, { recursive: true, force: true });
git(['worktree', 'prune']);
} catch (e) {
console.warn(`[agent] worktree cleanup warning: ${e.message}`);
}
}
}
if (branch) {
git(['branch', '-D', branch]);
}
}
function cycleTimedOut(startedAt) {
const elapsedMs = Date.now() - startedAt;
return elapsedMs > LIMITS.maxCycleMinutes * 60 * 1000;
}
function remainingTimeoutMs(startedAt) {
const budget = LIMITS.maxCycleMinutes * 60 * 1000;
return Math.max(30_000, budget - (Date.now() - startedAt));
}
/* ─── Commands ──────────────────────────────────────────────── */
function cmdDryRun() {
ensureDirs();
assertNotKilled();
const runId = `dry-${Date.now()}`;
writeStatus({ state: 'dry-run', runId });
audit('dry_run_start', { runId });
console.log(`[agent] dry-run ${runId}`);
console.log('[agent] collecting baseline (tests + build)...');
const baseline = collectBaseline();
const task = proposeTasks(baseline);
const score = scoreCategories(baseline, 'dry-run — no code changes');
const report = {
runId,
mode: 'dry-run',
hypothesis: `If we execute "${task.title}", demo jury clarity improves without regressing FPS.`,
task,
limits: LIMITS,
baseline,
score,
decision: 'PLAN_ONLY',
reason: 'dry-run does not apply patches',
safety: {
wouldTouchProduction: false,
wouldMergeMain: false,
killSwitch: false,
},
};
const path = writeReport(runId, report);
writeStatus({ state: 'idle', lastRunId: runId, lastMode: 'dry-run' });
audit('dry_run_complete', { runId, path, taskId: task.id });
console.log(`[agent] planned task: ${task.id} — ${task.title}`);
console.log(`[agent] score: ${score.total}/100`);
console.log(`[agent] report: ${path}`);
}
function cmdRunOnce() {
ensureDirs();
assertNotKilled();
const runId = `once-${Date.now()}`;
writeStatus({ state: 'run-once', runId });
audit('run_once_start', { runId });
console.log(`[agent] run-once ${runId}`);
console.log('[agent] verify-only: baseline tests/build — no patches.');
const baseline = collectBaseline();
const task = proposeTasks(baseline);
const score = scoreCategories(baseline, 'run-once verification');
const decision =
baseline.testsPassed && baseline.buildPassed ? 'ACCEPT_BASELINE' : 'REJECT_BASELINE';
const report = {
runId,
mode: 'run-once',
task,
limits: LIMITS,
baseline,
score,
decision,
reason:
decision === 'ACCEPT_BASELINE'
? 'Tests and build green. Use `implement` for worktree-isolated safe patches.'
: 'Baseline red — agent refuses patches until fixed.',
nextSafeActions: [
'node agent/cli.mjs implement --task agent/tasks/demo-docs.json',
'Never merge to main automatically',
'Use agent dry-run before long autonomous sessions',
],
};
const path = writeReport(runId, report);
writeStatus({ state: 'idle', lastRunId: runId, lastMode: 'run-once', decision });
audit('run_once_complete', { runId, decision });
console.log(`[agent] decision: ${decision}`);
console.log(`[agent] report: ${path}`);
process.exit(decision === 'ACCEPT_BASELINE' ? 0 : 1);
}
function parseImplementArgs(argv) {
let taskPath = null;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--task' && argv[i + 1]) {
taskPath = argv[i + 1];
i++;
}
}
return { taskPath };
}
function cmdImplement(argv = []) {
ensureDirs();
assertNotKilled();
const { taskPath } = parseImplementArgs(argv);
const runId = `impl-${Date.now()}`;
const startedAt = Date.now();
let worktreePath = null;
let branch = null;
let reportPath = null;
acquireLock(runId);
const finish = (exitCode) => {
releaseLock();
process.exit(exitCode);
};
try {
writeStatus({ state: 'implement', runId });
audit('implement_start', { runId, taskPath });
console.log(`[agent] implement ${runId}`);
console.log('[agent] watchdog checks...');
const watchdog = watchdogChecks();
if (!watchdog.ok) {
const report = {
runId,
mode: 'implement',
decision: 'REJECT',
reason: 'watchdog failed',
watchdog,
limits: LIMITS,
safety: { merged: false, deployed: false, pushed: false },
};
reportPath = writeReport(runId, report);
writeStatus({ state: 'idle', lastRunId: runId, lastMode: 'implement', decision: 'REJECT' });
console.error(`[agent] REJECT — watchdog: ${watchdog.failures.join('; ')}`);
console.error(`[agent] report: ${reportPath}`);
finish(1);
}
// 1. Plan
console.log('[agent] plan...');
const task = taskPath ? loadTaskFile(taskPath) : defaultDemoTask();
console.log(`[agent] task: ${task.id} — ${task.title}`);
if (cycleTimedOut(startedAt)) {
throw new Error(`cycle timeout before patch (${LIMITS.maxCycleMinutes} min)`);
}
// 2. Validate policy
console.log('[agent] validate task policy...');
const policy = validateTaskPolicy(task);
if (!policy.ok) {
const report = {
runId,
mode: 'implement',
task,
decision: 'REJECT',
reason: 'task policy validation failed',
policyErrors: policy.errors,
watchdog,
limits: LIMITS,
safety: { merged: false, deployed: false, pushed: false },
};
reportPath = writeReport(runId, report);
writeStatus({ state: 'idle', lastRunId: runId, lastMode: 'implement', decision: 'REJECT' });
console.error(`[agent] REJECT — policy: ${policy.errors.join('; ')}`);
console.error(`[agent] report: ${reportPath}`);
finish(1);
}
// 3. Worktree
console.log('[agent] create git worktree...');
({ worktreePath, branch } = createWorktree(runId, task.slug || slugify(task.id)));
console.log(`[agent] worktree: ${worktreePath}`);
console.log(`[agent] branch: ${branch}`);
// 4. Bounded patch
console.log('[agent] apply bounded patch...');
const changedFiles = applyBoundedPatch(worktreePath, task, runId);
const diff = measureDiff(worktreePath);
const limitCheck = enforceDiffLimits(diff);
if (!limitCheck.ok) {
removeWorktreeAndBranch(worktreePath, branch);
worktreePath = null;
branch = null;
const report = {
runId,
mode: 'implement',
task,
decision: 'REJECT',
reason: 'diff limits exceeded',
limitErrors: limitCheck.errors,
diff,
changedFiles,
watchdog,
limits: LIMITS,
safety: { merged: false, deployed: false, pushed: false },
};
reportPath = writeReport(runId, report);
writeStatus({ state: 'idle', lastRunId: runId, lastMode: 'implement', decision: 'REJECT' });
console.error(`[agent] REJECT — limits: ${limitCheck.errors.join('; ')}`);
console.error(`[agent] report: ${reportPath}`);
finish(1);
}
if (isKilled() || cycleTimedOut(startedAt)) {
removeWorktreeAndBranch(worktreePath, branch);
worktreePath = null;
branch = null;
const why = isKilled() ? 'kill switch' : 'cycle timeout';
const report = {
runId,
mode: 'implement',
task,
decision: 'REJECT',
reason: why,
diff,
watchdog,
limits: LIMITS,
safety: { merged: false, deployed: false, pushed: false },
};
reportPath = writeReport(runId, report);
writeStatus({ state: 'idle', lastRunId: runId, lastMode: 'implement', decision: 'REJECT' });
console.error(`[agent] REJECT — ${why}`);
finish(1);
}
// 5. Tests + build in worktree
console.log('[agent] npm test && npm run build (worktree)...');
const timeout = remainingTimeoutMs(startedAt);
const tests = run('npm', ['test'], { cwd: worktreePath, timeout });
const buildTimeout = remainingTimeoutMs(startedAt);
const build = run('npm', ['run', 'build'], { cwd: worktreePath, timeout: buildTimeout });
const verification = {
testsPassed: tests.code === 0,
buildPassed: build.code === 0,
testOutputTail: (tests.stdout || tests.stderr).split('\n').slice(-20).join('\n'),
buildOutputTail: (build.stdout || build.stderr).split('\n').slice(-20).join('\n'),
};
// 6. Score + decide
const score = scoreCategories(
verification,
'implementer worktree candidate (deterministic, no LLM)',
);
const gatesOk =
verification.testsPassed &&
verification.buildPassed &&
limitCheck.ok &&
!isKilled() &&
!cycleTimedOut(startedAt);
const decision = gatesOk ? 'ACCEPT_CANDIDATE' : 'REJECT';
let reason;
if (decision === 'ACCEPT_CANDIDATE') {
reason =
'Tests and build passed in isolated worktree within limits. Candidate kept for manual review — NOT merged, NOT deployed, NOT pushed.';
} else if (!verification.testsPassed || !verification.buildPassed) {
reason = 'tests or build failed in worktree';
} else if (isKilled()) {
reason = 'kill switch during cycle';
} else if (cycleTimedOut(startedAt)) {
reason = 'cycle timeout';
} else {
reason = 'gates failed';
}
if (decision === 'REJECT') {
console.log('[agent] REJECT — removing worktree and branch...');
removeWorktreeAndBranch(worktreePath, branch);
const removedBranch = branch;
const removedWt = worktreePath;
worktreePath = null;
branch = null;
const report = {
runId,
mode: 'implement',
task,
policy,
diff,
changedFiles,
verification,
score,
decision,
reason,
watchdog,
limits: LIMITS,
cleanup: { worktreeRemoved: removedWt, branchDeleted: removedBranch },
safety: {
merged: false,
deployed: false,
pushed: false,
wouldTouchProduction: false,
},
elapsedMs: Date.now() - startedAt,
};
reportPath = writeReport(runId, report);
writeStatus({
state: 'idle',
lastRunId: runId,
lastMode: 'implement',
decision,
});
audit('implement_complete', { runId, decision, reportPath });
console.error(`[agent] decision: ${decision}`);
console.error(`[agent] reason: ${reason}`);
console.error(`[agent] report: ${reportPath}`);
finish(1);
}
// ACCEPT — keep worktree
const manualReview = {
instructions: [
`Inspect worktree: ${worktreePath}`,
`Inspect branch: ${branch}`,
`Review report: agent/reports/${runId}.json`,
'Diff: git -C <worktree> show HEAD',
'If good: cherry-pick or merge the agent/* branch yourself into your feature branch.',
'NEVER auto-merge to main. NEVER deploy. NEVER push from the agent.',
'To discard: git worktree remove --force <worktree> && git branch -D <branch>',
],
worktreePath,
branch,
relativeWorktree: relative(ROOT, worktreePath),
};
const report = {
runId,
mode: 'implement',
task,
policy,
diff,
changedFiles,
verification,
score,
decision,
reason,
watchdog,
limits: LIMITS,
worktreePath,
branch,
manualReview,
safety: {
merged: false,
deployed: false,
pushed: false,
wouldTouchProduction: false,
wouldMergeMain: false,
},
elapsedMs: Date.now() - startedAt,
};
reportPath = writeReport(runId, report);
writeStatus({
state: 'idle',
lastRunId: runId,
lastMode: 'implement',
decision,
worktreePath,
branch,
reportPath,
});
audit('implement_complete', { runId, decision, reportPath, branch });
console.log(`[agent] decision: ${decision}`);
console.log(`[agent] score: ${score.total}/100`);
console.log(`[agent] worktree: ${worktreePath}`);
console.log(`[agent] branch: ${branch}`);
console.log(`[agent] report: ${reportPath}`);
console.log('[agent] manual review required — agent will NOT merge/deploy/push');
for (const line of manualReview.instructions) {
console.log(` → ${line}`);
}
finish(0);
} catch (err) {
const message = err?.message || String(err);
console.error(`[agent] implement error: ${message}`);
if (worktreePath || branch) {
try {
removeWorktreeAndBranch(worktreePath, branch);
} catch (cleanupErr) {
console.warn(`[agent] cleanup failed: ${cleanupErr.message}`);
}
}
const report = {
runId,
mode: 'implement',
decision: 'REJECT',
reason: message,
limits: LIMITS,
safety: { merged: false, deployed: false, pushed: false },
elapsedMs: Date.now() - startedAt,
};
try {
reportPath = writeReport(runId, report);
console.error(`[agent] report: ${reportPath}`);
} catch {
/* ignore */
}
writeStatus({ state: 'idle', lastRunId: runId, lastMode: 'implement', decision: 'REJECT' });
audit('implement_error', { runId, message });
finish(1);
}
}
function cmdStart() {
ensureDirs();
if (process.env.AUTONOMOUS_AGENT_ENABLED !== 'true') {
console.error(
'[agent] start refused: set AUTONOMOUS_AGENT_ENABLED=true to enable (default off).',
);
console.error('[agent] safer entrypoints: dry-run | run-once | implement');
process.exit(1);
}
assertNotKilled();
audit('start_requested', { pid: process.pid });
console.log('[agent] start enabled via AUTONOMOUS_AGENT_ENABLED=true');
console.log('[agent] running one implement cycle (MVP; no continuous loop flood)');
cmdImplement(process.argv.slice(3));
}
function cmdStatus() {
ensureDirs();
let lock = null;
if (existsSync(LOCK_FILE)) {
try {
lock = JSON.parse(readFileSync(LOCK_FILE, 'utf8'));
lock.alive = pidAlive(lock.pid);
} catch {
lock = { unreadable: true };
}
}
const watchdog = watchdogChecks();
console.log(
JSON.stringify(
{
...readStatus(),
killSwitch: isKilled(),
lock,
watchdog,
limits: LIMITS,
policyAllowlist: POLICY_ALLOWLIST,
},
null,
2,
),
);
}
function cmdStop() {
ensureDirs();
writeFileSync(KILL_SWITCH, `stopped at ${nowIso()}\n`);
writeStatus({ state: 'stopped' });
audit('kill_switch_on');
console.log('[agent] stop requested — kill switch written');
}
function cmdResume() {
ensureDirs();
if (existsSync(KILL_SWITCH)) unlinkSync(KILL_SWITCH);
writeStatus({ state: 'idle' });
audit('kill_switch_off');
console.log('[agent] resumed');
}
function cmdPause() {
writeStatus({ ...readStatus(), state: 'paused' });
audit('paused');
console.log('[agent] paused');
}
function cmdReport() {
ensureDirs();
const latest = join(REPORTS_DIR, 'latest.json');
if (!existsSync(latest)) {
console.error('No report yet. Run: node agent/cli.mjs dry-run');
process.exit(1);
}
console.log(readFileSync(latest, 'utf8'));
}
function usage() {
console.log(`Usage: node agent/cli.mjs <command> [options]
Commands:
dry-run Plan only (baseline + propose task)
run-once Verify baseline only (no patches)
implement [--task <path>]
Worktree-isolated Implementer cycle (deterministic demo if no LLM)
start Requires AUTONOMOUS_AGENT_ENABLED=true; runs implement once
status Status + lock + watchdog snapshot
stop Set kill switch (agent/state/KILL)
resume Clear kill switch
pause Mark status paused
report Print latest report JSON
Safety: NEVER merges to main, NEVER deploys, NEVER pushes.
`);
}
const cmd = process.argv[2] ?? 'status';
switch (cmd) {
case 'dry-run':
cmdDryRun();
break;
case 'run-once':
cmdRunOnce();
break;
case 'implement':
cmdImplement(process.argv.slice(3));
break;
case 'start':
cmdStart();
break;
case 'status':
cmdStatus();
break;
case 'stop':
cmdStop();
break;
case 'resume':
cmdResume();
break;
case 'pause':
cmdPause();
break;
case 'report':
cmdReport();
break;
case 'help':
case '--help':
case '-h':
usage();
break;
default:
usage();
process.exit(1);
}