Files
wangjia f18db021c3 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>
2026-06-13 02:46:37 +08:00

148 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 repomain + 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();