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:
wangjia
2026-06-13 02:46:37 +08:00
parent fa472a06a7
commit f18db021c3
25 changed files with 2215 additions and 56 deletions
+47
View File
@@ -0,0 +1,47 @@
import type { Task } from './types.js';
/**
* 调度打分(在领取那一刻现算,不落库):
* score = 自身分 + Σ(已完成依赖的自身分) —— 链条惯性:前置投入越重越优先出活
* + Σ(直接等我解锁的 blocked 任务的自身分) —— 解锁效应:等我的人越多越优先
* 自身分按优先级:P0=3 / P1=2 / P2=1。
* 依赖与解锁均只算一层(不递归):deps 在创建时即固定且只能引用已存在任务,
* 因此依赖图天然无环,但递归会让深链分数膨胀失控,故不递归。
*/
export function baseScore(priority: number): number {
return Math.max(1, 3 - priority);
}
/** 反向索引:taskId → 直接依赖它的任务列表 */
export function buildDependentsIndex(tasks: Task[]): Map<string, Task[]> {
const idx = new Map<string, Task[]>();
for (const t of tasks) {
for (const d of t.deps) {
const list = idx.get(d) ?? [];
list.push(t);
idx.set(d, list);
}
}
return idx;
}
export function scoreTask(t: Task, byId: Map<string, Task>, dependents: Map<string, Task[]>): number {
let s = baseScore(t.priority);
for (const d of t.deps) {
const dt = byId.get(d);
if (dt?.status === 'done') s += baseScore(dt.priority); // 链条惯性
}
for (const w of dependents.get(t.id) ?? []) {
if (w.status === 'blocked') s += baseScore(w.priority); // 解锁效应
}
return s;
}
/** 把候选任务按 score 降序排(同分创建早的在前)。返回新数组,附带分数。 */
export function rankByScore(candidates: Task[], all: Task[]): Array<{ task: Task; score: number }> {
const byId = new Map(all.map((t) => [t.id, t]));
const dependents = buildDependentsIndex(all);
return candidates
.map((task) => ({ task, score: scoreTask(task, byId, dependents) }))
.sort((a, b) => b.score - a.score || a.task.createdAt.localeCompare(b.task.createdAt));
}
+1 -1
View File
@@ -27,7 +27,7 @@ export const STATUS_LABEL: Record<TaskStatus, string> = {
speccing: '写方案中',
spec_review: '待确认方案',
ready: '可执行',
blocked: '被依赖阻塞',
blocked: '被阻塞',
queued: '排队中',
executing: '执行中',
exec_review: '待审/合',
+7 -2
View File
@@ -32,12 +32,17 @@ export interface ApprovalRecord {
at: string;
}
/** 复审结论:approve=建议通过 / reject=建议拒绝 / null=未复审(复审失败或旧数据) */
export type ReviewVerdict = 'approve' | 'reject';
export interface TaskResult {
branch: string | null;
worktree: string | null;
diffSummary: string | null;
commits: string[];
prUrl: string | null;
summary: string | null; // 复审 summarymarkdown),旧数据缺省 null
verdict: ReviewVerdict | null; // 复审结论,解析不到 / 复审失败 = null
}
export interface Task {
@@ -61,13 +66,13 @@ export interface Task {
updatedAt: string;
}
export type RunKind = 'planner' | 'executor';
export type RunKind = 'planner' | 'executor' | 'reviewer';
export type RunStatus = 'started' | 'succeeded' | 'failed' | 'cancelled';
export interface Run {
id: Id;
taskId: Id;
kind: RunKind; // planner(产出拆解/方案)| executor(执行改动)
kind: RunKind; // planner(产出拆解/方案)| executor(执行改动)| reviewer(执行后自动复审)
worktree: string | null;
branch: string | null;
status: RunStatus;