feat: Phase2 完整管线——score 调度 + 自动复审 + 模型分级 + 归档与详情
调度: - 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>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user