#!/usr/bin/env node /** * PoC:最小执行闭环(DESIGN.md §14.1) * * 临时 git repo + 临时 daemon(:4528, 独立 MAESTRO_DATA_DIR) + autonomy=auto-easy * + 一个 Easy 任务(operations=「在 README.md 追加一行 hello maestro」) * → 等编排器在 worktree 真起 headless Claude Code 跑完 * → 断言:任务到 exec_review、maestro/ 分支上有 commit、README 真的被改了。 * * 前提:本机有 claude 凭证(claude CLI 已登录或 ANTHROPIC_API_KEY),网络可用。 * 不碰 4517 线上 daemon 与 ~/.maestro(数据目录走临时 MAESTRO_DATA_DIR)。 * * 用法:node scripts/poc-exec.mjs (在 maestro 仓库根目录) */ import { execFileSync, spawn } from 'node:child_process'; import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = dirname(dirname(fileURLToPath(import.meta.url))); const PORT = 4528; const BASE = `http://127.0.0.1:${PORT}`; const ORCH_INTERVAL = 3; // 秒 const EXEC_TIMEOUT_MS = 8 * 60_000; const log = (m) => console.log(`[poc] ${m}`); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function git(cwd, args) { return execFileSync('git', args, { cwd, encoding: 'utf8' }); } async function api(method, path, body) { const res = await fetch(`${BASE}${path}`, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await res.text(); if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${text}`); return text ? JSON.parse(text) : null; } async function main() { // 0) 临时目录 const dataDir = mkdtempSync(join(tmpdir(), 'maestro-poc-data-')); const repo = mkdtempSync(join(tmpdir(), 'maestro-poc-repo-')); // 1) 临时 git repo(main + README) git(repo, ['init', '-b', 'main']); git(repo, ['config', 'user.name', 'maestro-poc']); git(repo, ['config', 'user.email', 'poc@maestro.local']); writeFileSync(join(repo, 'README.md'), '# poc demo\n'); git(repo, ['add', '-A']); git(repo, ['commit', '-m', 'init']); log(`临时 repo:${repo}`); log(`临时数据目录:${dataDir}`); // 2) 起临时 daemon(:4528,编排器 3s 一轮,定时同步关闭) const daemon = spawn('npx', ['tsx', 'src/daemon/index.ts'], { cwd: ROOT, env: { ...process.env, MAESTRO_DATA_DIR: dataDir, MAESTRO_PORT: String(PORT), MAESTRO_ORCH_INTERVAL: String(ORCH_INTERVAL), MAESTRO_SYNC_INTERVAL: '0', }, stdio: ['ignore', 'pipe', 'pipe'], }); daemon.stdout.on('data', (d) => process.stdout.write(`[daemon] ${d}`)); daemon.stderr.on('data', (d) => process.stderr.write(`[daemon!] ${d}`)); const cleanup = (keep) => { daemon.kill('SIGTERM'); if (!keep) { rmSync(dataDir, { recursive: true, force: true }); rmSync(repo, { recursive: true, force: true }); } else { log(`保留现场:repo=${repo} dataDir=${dataDir}`); } }; try { // 等 daemon 就绪 let up = false; for (let i = 0; i < 40; i++) { try { await api('GET', '/api/projects'); up = true; break; } catch { await sleep(500); } } if (!up) throw new Error('daemon 未就绪(:4528)'); log('daemon 就绪'); // 3) 注册项目(auto-easy)+ Easy 任务 + operations const project = await api('POST', '/api/projects', { name: 'poc', repoPath: repo, defaultBranch: 'main', autonomy: 'auto-easy', concurrency: 1, }); const task = await api('POST', `/api/projects/${project.id}/tasks`, { title: '在 README.md 追加一行 hello maestro', complexity: 'easy', }); await api('POST', `/api/tasks/${task.id}/operations`, { operations: '在仓库根目录的 README.md 末尾追加一行文本「hello maestro」。不要做其他任何改动。', }); log(`任务已建:${task.id}(ready,等编排器领取)`); // 4) 轮询直到 exec_review / needs_attention / 超时 const t0 = Date.now(); let cur = null; for (;;) { await sleep(3000); cur = await api('GET', `/api/tasks/${task.id}`); log(`任务状态:${cur.status}`); if (cur.status === 'exec_review' || cur.status === 'needs_attention') break; if (Date.now() - t0 > EXEC_TIMEOUT_MS) throw new Error(`超时(>${EXEC_TIMEOUT_MS / 1000}s),最后状态:${cur.status}`); } if (cur.status !== 'exec_review') { const runs = await api('GET', `/api/tasks/${task.id}/runs`); throw new Error(`任务进入 ${cur.status}(非 exec_review)。runs=${JSON.stringify(runs, null, 2)}`); } // 5) 断言:result 落库、分支有 commit、文件真的改了、main 未被动过 const branch = `maestro/${task.id}`; if (cur.result?.branch !== branch) throw new Error(`result.branch 异常:${JSON.stringify(cur.result)}`); const commits = git(repo, ['log', '--format=%h %s', `main..${branch}`]).trim().split('\n').filter(Boolean); if (commits.length < 1) throw new Error(`分支 ${branch} 上没有 commit`); const readmeOnBranch = git(repo, ['show', `${branch}:README.md`]); if (!readmeOnBranch.includes('hello maestro')) throw new Error(`分支上的 README 未包含「hello maestro」:\n${readmeOnBranch}`); const readmeOnMain = readFileSync(join(repo, 'README.md'), 'utf8'); if (readmeOnMain.includes('hello maestro')) throw new Error('main 的 README 被改了(不应自动合并)'); if (cur.result.worktree && !existsSync(cur.result.worktree)) throw new Error('worktree 现场不存在'); log('================ PoC 通过 ✅ ================'); log(`任务 → exec_review;分支 ${branch} 共 ${commits.length} 个 commit:`); for (const c of commits) log(` ${c}`); log(`diffSummary: ${cur.result.diffSummary}`); log('main 未被自动合并(符合「绝不自动合并」)'); cleanup(false); process.exit(0); } catch (e) { console.error(`[poc] 失败:${e.message}`); cleanup(true); process.exit(1); } } main();