f18db021c3
调度: - model/scoring.ts: score = 自身分(P0=3/P1=2/P2=1) + 已完成依赖分(链条惯性) + 等待解锁的 blocked 任务分(解锁加权),编排器与 nextExecutable 同一打分 - createTask 校验 deps 存在且同项目(依赖图天然无环) - daemon 重启中断自愈: executing 任务标 failed run 后重新入队(reconcileInterrupted) 执行管线: - executor/cc.ts: 公共 headless CC 执行器(转录/超时/模型回退重试) - executor/reviewer.ts: 执行后自动复审(只读 CC 审 diff),固定模板 summary (做了什么/怎么做/测试/CodeReview/安全Review/结论) + VERDICT 解析 - executor/models.ts: 按复杂度选模型(easy→sonnet/medium→opus/hard→fable5), env 可覆盖、project.model 最优先、不可用自动回退链 - runner: 测试/构建命令白名单(npm/go/shellcheck/make/pytest),prompt 要求实跑测试 - TaskResult 加 summary/verdict; RunKind 加 reviewer - 容器收口: 已拆解 Hard 子任务全 done → 容器自动 done(afterDone 逐级向上) 看板: - 五徽章组(待审批/待执行/执行中/被阻塞/总量,hover 展开,均不含已完成) - 归档区: 深度1整树完成沉底,时间倒序分页(10/20/50/100 chip 选择) - 归档详情对话框: 全属性/执行历史与时长/审批记录/状态流转时间线(含相关人或事) - Agent 面板显示调度模式 + 各复杂度实际模型 - 结果闸展示复审 summary + 建议通过/拒绝徽章 - 筛选修复(组选与单选分离、已拆解移出进行中)、同步按钮收进配置面板、 保存配置自动收起、预览全宽、被依赖阻塞→被阻塞 - API: GET /api/tasks/:id/events(任务级事件时间线)、/api/agents 带 scheduling/models 测试: 49/49(新增 scoring/复审/模型/容器收口/deps 校验/中断恢复) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
6.1 KiB
JavaScript
148 lines
6.1 KiB
JavaScript
#!/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/<taskId> 分支上有 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();
|