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:
+64
-15
@@ -7,6 +7,7 @@ import {
|
||||
import type { Project, Task, ApprovalRecord, Run, Event, TaskResult, Autonomy, EventType } from '../model/types.js';
|
||||
import { DEFAULT_MAX_DEPTH, HARD_MAX_DEPTH } from '../model/types.js';
|
||||
import type { Complexity } from '../model/complexity.js';
|
||||
import { rankByScore } from '../model/scoring.js';
|
||||
import {
|
||||
type TaskStatus, type GateKind, canTransition, initialNextStatus, gateOf, STATUS_LABEL,
|
||||
} from '../model/status.js';
|
||||
@@ -174,9 +175,15 @@ export class Store {
|
||||
}
|
||||
|
||||
if (input.priority !== undefined) assertPriority(input.priority);
|
||||
// deps 必须引用本项目已存在的任务(也因此依赖图天然无环:新任务不可能已被引用)
|
||||
const depsArr = input.deps ?? [];
|
||||
for (const d of depsArr) {
|
||||
const dep = this.getTaskRow(d);
|
||||
if (!dep) throw new StoreError(`依赖任务不存在: ${d}`);
|
||||
if (dep.project_id !== input.projectId) throw new StoreError(`依赖任务不在同一项目: ${d}`);
|
||||
}
|
||||
let status = initialNextStatus(input.complexity);
|
||||
// Easy 直达 ready,但有未完成依赖时落位 blocked
|
||||
const depsArr = input.deps ?? [];
|
||||
if (status === 'ready' && depsArr.length && !depsArr.every((d) => this.getTaskRow(d)?.status === 'done')) {
|
||||
status = 'blocked';
|
||||
}
|
||||
@@ -333,6 +340,26 @@ export class Store {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务变 done 后的连锁处理:
|
||||
* 1. 放行依赖它的 blocked 任务;
|
||||
* 2. 已拆解容器的子任务全部 done/取消 → 容器自动 done(“深度1整树完成”生命周期闭环),逐级向上。
|
||||
*/
|
||||
private afterDone(projectId: string, taskId: string): void {
|
||||
this.releaseDependents(projectId);
|
||||
let cur = this.getTaskRow(taskId);
|
||||
while (cur?.parent_id) {
|
||||
const parent = this.getTaskRow(cur.parent_id);
|
||||
if (!parent || parent.status !== 'decomposed') break;
|
||||
const kids = this.db.prepare(`SELECT status FROM tasks WHERE parent_id = ?`).all(parent.id) as Array<{ status: string }>;
|
||||
if (!kids.every((k) => k.status === 'done' || k.status === 'cancelled')) break;
|
||||
this.db.prepare(`UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?`).run(now(), parent.id);
|
||||
this.emit(parent.project_id, parent.id, 'status.changed', { from: 'decomposed', to: 'done', auto: 'children-done' });
|
||||
this.releaseDependents(projectId); // 容器本身可能是别人的依赖
|
||||
cur = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量依赖对账(幂等):ready 但依赖未满足 → blocked;blocked 但依赖已满足 → ready。
|
||||
* daemon 启动时调用,纠正存量数据。
|
||||
@@ -361,6 +388,22 @@ export class Store {
|
||||
return { blocked: nBlocked, released: nReleased };
|
||||
}
|
||||
|
||||
/**
|
||||
* daemon 启动时的中断现场恢复:上次进程退出时仍 started 的 run 标 failed,
|
||||
* 卡在 executing 的任务转 failed→queued 等编排器重新领取。
|
||||
* 注:中断产生的 failed run 会计入该任务的失败次数(多次中断+真失败可能提前转 needs_attention,可接受)。
|
||||
*/
|
||||
reconcileInterrupted(): { runs: number; tasks: number } {
|
||||
const runs = this.db.prepare(`SELECT * FROM runs WHERE status = 'started'`).all() as RunRow[];
|
||||
for (const r of runs) this.finishRun(r.id, 'failed', { error: 'daemon 重启,执行中断' });
|
||||
const rows = this.db.prepare(`SELECT * FROM tasks WHERE status = 'executing'`).all() as TaskRow[];
|
||||
for (const t of rows) {
|
||||
this.transition(t.id, 'failed', { auto: 'daemon-restart' });
|
||||
this.transition(t.id, 'queued', { auto: 'daemon-restart' });
|
||||
}
|
||||
return { runs: runs.length, tasks: rows.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅供导入器:旧系统中已完成的任务直接置 done(绕过执行链路与依赖落位——
|
||||
* 历史事实优先,避免已完成的工作被当作待执行复活),并放行依赖它的任务。
|
||||
@@ -372,7 +415,7 @@ export class Store {
|
||||
if (from === 'done') return this.getTask(taskId)!;
|
||||
this.db.prepare(`UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?`).run(now(), taskId);
|
||||
this.emit(row.project_id, taskId, 'status.changed', { from, to: 'done', auto: 'import-done', actor });
|
||||
this.releaseDependents(row.project_id);
|
||||
this.afterDone(row.project_id, taskId);
|
||||
return this.getTask(taskId)!;
|
||||
}
|
||||
|
||||
@@ -392,7 +435,7 @@ export class Store {
|
||||
this.emit(row.project_id, taskId, 'status.changed', {
|
||||
from, to: actual, ...(actual !== to ? { requested: to, auto: 'deps-unmet' } : {}), ...meta,
|
||||
});
|
||||
if (actual === 'done') this.releaseDependents(row.project_id);
|
||||
if (actual === 'done') this.afterDone(row.project_id, taskId);
|
||||
return this.getTask(taskId)!;
|
||||
}
|
||||
|
||||
@@ -444,7 +487,7 @@ export class Store {
|
||||
});
|
||||
txn();
|
||||
this.emit(row.project_id, taskId, action === 'accept' ? 'approval.granted' : 'approval.rejected', { gate, from, to, reason: reason ?? null });
|
||||
if (to === 'done') this.releaseDependents(row.project_id); // 完成 → 自动放行依赖它的任务
|
||||
if (to === 'done') this.afterDone(row.project_id, taskId); // 完成 → 放行依赖 + 容器自动收口
|
||||
return this.getTask(taskId)!;
|
||||
}
|
||||
|
||||
@@ -494,6 +537,14 @@ export class Store {
|
||||
}
|
||||
|
||||
// ---------- Events ----------
|
||||
/** 单任务全量事件(升序):状态流转/审批/运行历史,供归档详情时间线 */
|
||||
listTaskEvents(taskId: string): Event[] {
|
||||
const rows = this.db.prepare(
|
||||
`SELECT * FROM events WHERE task_id = ? ORDER BY at`,
|
||||
).all(taskId) as EventRow[];
|
||||
return rows.map(rowToEvent);
|
||||
}
|
||||
|
||||
listEvents(projectId: string, limit = 200): Event[] {
|
||||
const rows = this.db.prepare(
|
||||
`SELECT * FROM events WHERE project_id = ? ORDER BY at DESC LIMIT ?`,
|
||||
@@ -506,16 +557,14 @@ export class Store {
|
||||
* 被拆解的 Hard 容器任务不会是 ready(停在 decomposed),天然排除。
|
||||
*/
|
||||
nextExecutable(projectId: string): Task | null {
|
||||
const rows = this.db.prepare(
|
||||
`SELECT * FROM tasks WHERE project_id = ? AND status = 'ready' ORDER BY priority ASC, created_at`,
|
||||
).all(projectId) as TaskRow[];
|
||||
for (const r of rows) {
|
||||
if (this.childrenOf(r.id).length > 0) continue; // 非叶子跳过
|
||||
const deps = JSON.parse(r.deps) as string[];
|
||||
const depsDone = deps.every((d) => this.getTaskRow(d)?.status === 'done');
|
||||
if (!depsDone) continue;
|
||||
return rowToTask(r, this.listApprovals(r.id));
|
||||
}
|
||||
return null;
|
||||
const all = this.listTasks(projectId);
|
||||
const byId = new Map(all.map((t) => [t.id, t]));
|
||||
const parents = new Set(all.filter((t) => t.parentId).map((t) => t.parentId as string));
|
||||
const candidates = all.filter((t) =>
|
||||
t.status === 'ready' && !parents.has(t.id) &&
|
||||
t.deps.every((d) => byId.get(d)?.status === 'done'),
|
||||
);
|
||||
const ranked = rankByScore(candidates, all); // 与编排器同一打分(见 model/scoring.ts)
|
||||
return ranked[0]?.task ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user