import { nanoid } from 'nanoid'; import { openDb, type DB } from './db.js'; import { rowToProject, rowToTask, rowToApproval, rowToRun, rowToEvent, type ProjectRow, type TaskRow, type ApprovalRow, type RunRow, type EventRow, } from './mappers.js'; 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'; const now = (): string => new Date().toISOString(); const id = (prefix: string): string => `${prefix}_${nanoid(12)}`; export class StoreError extends Error {} export interface CreateProjectInput { name: string; repoPath: string; defaultBranch?: string; verifyCmd?: string | null; autonomy?: Autonomy; model?: string | null; concurrency?: number; } export interface CreateTaskInput { projectId: string; title: string; complexity: Complexity; parentId?: string | null; priority?: number; deps?: string[]; } export interface PatchProjectInput { autonomy?: Autonomy; concurrency?: number; verifyCmd?: string | null; model?: string | null; status?: 'active' | 'paused'; } export interface PatchTaskInput { title?: string; priority?: number; complexity?: Complexity; } /** 执行中的 run(联 tasks 取标题/项目),供 GET /api/agents 汇总 */ export interface ActiveRun { runId: string; taskId: string; taskTitle: string; kind: string; startedAt: string; projectId: string; } const AUTONOMY_VALUES: readonly Autonomy[] = ['manual', 'auto-easy', 'auto-approved']; /** 优先级取值:P0 最高 / P1 中(默认)/ P2 最低 */ function assertPriority(p: number): void { if (!Number.isInteger(p) || p < 0 || p > 2) { throw new StoreError('priority 必须是 0/1/2(P0 最高,P1 中,P2 最低)'); } } /** 允许修改 complexity 的状态:尚未进入执行/收尾链路 */ const COMPLEXITY_EDITABLE: ReadonlySet = new Set([ 'init', 'analyzing', 'speccing', 'ready', 'plan_review', 'spec_review', 'blocked', ]); type EventListener = (e: Event) => void; /** * 任务系统的唯一数据出入口。所有状态变更都经 transition() 守卫(canTransition), * 审批 reject 强制带改进意见,每次变更追加 Event 并广播给监听者(供 WS 推送)。 */ export class Store { readonly db: DB; private listeners = new Set(); constructor(file: string) { this.db = openDb(file); } close(): void { this.db.close(); } /** 订阅事件流(看板 WS)。返回取消订阅函数。 */ subscribe(fn: EventListener): () => void { this.listeners.add(fn); return () => this.listeners.delete(fn); } private emit(projectId: string, taskId: string | null, type: EventType, payload: Record = {}): Event { const row: EventRow = { id: id('evt'), project_id: projectId, task_id: taskId, type, payload: JSON.stringify(payload), at: now(), }; this.db.prepare( `INSERT INTO events (id, project_id, task_id, type, payload, at) VALUES (@id,@project_id,@task_id,@type,@payload,@at)`, ).run(row); const evt = rowToEvent(row); for (const l of this.listeners) { try { l(evt); } catch { /* listener 错误不影响主流程 */ } } return evt; } // ---------- Projects ---------- createProject(input: CreateProjectInput): Project { const row: ProjectRow = { id: id('prj'), name: input.name, repo_path: input.repoPath, default_branch: input.defaultBranch ?? 'main', verify_cmd: input.verifyCmd ?? null, autonomy: input.autonomy ?? 'manual', model: input.model ?? null, concurrency: input.concurrency ?? 1, status: 'active', created_at: now(), last_sync_at: null, }; this.db.prepare( `INSERT INTO projects (id,name,repo_path,default_branch,verify_cmd,autonomy,model,concurrency,status,created_at,last_sync_at) VALUES (@id,@name,@repo_path,@default_branch,@verify_cmd,@autonomy,@model,@concurrency,@status,@created_at,@last_sync_at)`, ).run(row); this.emit(row.id, null, 'task.created', { kind: 'project', name: row.name }); return rowToProject(row); } listProjects(): Project[] { const rows = this.db.prepare(`SELECT * FROM projects ORDER BY created_at`).all() as ProjectRow[]; return rows.map(rowToProject); } getProject(projectId: string): Project | null { const row = this.db.prepare(`SELECT * FROM projects WHERE id = ?`).get(projectId) as ProjectRow | undefined; return row ? rowToProject(row) : null; } /** 部分更新项目配置(autonomy/concurrency/verifyCmd/model/status),带合法性校验。 */ patchProject(projectId: string, patch: PatchProjectInput): Project { const cur = this.getProject(projectId); if (!cur) throw new StoreError(`项目不存在: ${projectId}`); const sets: string[] = []; const args: Record = { id: projectId }; if (patch.autonomy !== undefined) { if (!AUTONOMY_VALUES.includes(patch.autonomy)) { throw new StoreError('autonomy 必须是 manual | auto-easy | auto-approved'); } sets.push('autonomy = @autonomy'); args.autonomy = patch.autonomy; } if (patch.concurrency !== undefined) { if (!Number.isInteger(patch.concurrency) || patch.concurrency < 1) { throw new StoreError('concurrency 必须是 >=1 的整数'); } sets.push('concurrency = @concurrency'); args.concurrency = patch.concurrency; } if (patch.status !== undefined) { if (patch.status !== 'active' && patch.status !== 'paused') { throw new StoreError('status 必须是 active | paused'); } sets.push('status = @status'); args.status = patch.status; } if (patch.verifyCmd !== undefined) { sets.push('verify_cmd = @verify_cmd'); args.verify_cmd = patch.verifyCmd; } if (patch.model !== undefined) { sets.push('model = @model'); args.model = patch.model; } if (sets.length > 0) { this.db.prepare(`UPDATE projects SET ${sets.join(', ')} WHERE id = @id`).run(args); this.emit(projectId, null, 'task.updated', { kind: 'project', fields: Object.keys(patch) }); } return this.getProject(projectId)!; } /** 标记项目完成一次 todo.json 同步:写 last_sync_at 并广播 project.synced(payload=同步统计)。 */ markSynced(projectId: string, payload: Record = {}): string { const cur = this.getProject(projectId); if (!cur) throw new StoreError(`项目不存在: ${projectId}`); const at = now(); this.db.prepare(`UPDATE projects SET last_sync_at = ? WHERE id = ?`).run(at, projectId); this.emit(projectId, null, 'project.synced', { ...payload, lastSyncAt: at }); return at; } // ---------- Tasks ---------- createTask(input: CreateTaskInput): Task { const project = this.getProject(input.projectId); if (!project) throw new StoreError(`项目不存在: ${input.projectId}`); let depth = 1; if (input.parentId) { const parent = this.getTaskRow(input.parentId); if (!parent) throw new StoreError(`父任务不存在: ${input.parentId}`); if (parent.project_id !== input.projectId) throw new StoreError('子任务必须与父任务同项目'); depth = parent.depth + 1; const max = parent.complexity === 'hard' ? HARD_MAX_DEPTH : DEFAULT_MAX_DEPTH; if (depth > max) throw new StoreError(`层级超限:最多 ${max} 层(父任务 ${parent.complexity})`); } 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 if (status === 'ready' && depsArr.length && !depsArr.every((d) => this.getTaskRow(d)?.status === 'done')) { status = 'blocked'; } const row: TaskRow = { id: id('tsk'), project_id: input.projectId, parent_id: input.parentId ?? null, depth, title: input.title, complexity: input.complexity, status, priority: input.priority ?? 1, deps: JSON.stringify(input.deps ?? []), plan: null, spec: null, operations: null, result: null, assignee: null, created_at: now(), updated_at: now(), source_ref: null, }; this.db.prepare( `INSERT INTO tasks (id,project_id,parent_id,depth,title,complexity,status,priority,deps,plan,spec,operations,result,assignee,created_at,updated_at,source_ref) VALUES (@id,@project_id,@parent_id,@depth,@title,@complexity,@status,@priority,@deps,@plan,@spec,@operations,@result,@assignee,@created_at,@updated_at,@source_ref)`, ).run(row); this.emit(input.projectId, row.id, 'task.created', { title: row.title, complexity: row.complexity, status }); return rowToTask(row); } private getTaskRow(taskId: string): TaskRow | undefined { return this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId) as TaskRow | undefined; } getTask(taskId: string): Task | null { const row = this.getTaskRow(taskId); if (!row) return null; return rowToTask(row, this.listApprovals(taskId)); } listTasks(projectId: string): Task[] { const rows = this.db.prepare( `SELECT * FROM tasks WHERE project_id = ? ORDER BY depth, priority ASC, created_at`, ).all(projectId) as TaskRow[]; return rows.map((r) => rowToTask(r, this.listApprovals(r.id))); } childrenOf(taskId: string): Task[] { const rows = this.db.prepare( `SELECT * FROM tasks WHERE parent_id = ? ORDER BY priority ASC, created_at`, ).all(taskId) as TaskRow[]; return rows.map((r) => rowToTask(r, this.listApprovals(r.id))); } /** 写产出:Hard→plan / Medium→spec / Easy→operations。仅更新对应字段。 */ setPlan(taskId: string, plan: string): Task { return this.patchField(taskId, 'plan', plan); } setSpec(taskId: string, spec: string): Task { return this.patchField(taskId, 'spec', spec); } setOperations(taskId: string, ops: string): Task { return this.patchField(taskId, 'operations', ops); } private patchField(taskId: string, field: 'plan' | 'spec' | 'operations', value: string): Task { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); this.db.prepare(`UPDATE tasks SET ${field} = ?, updated_at = ? WHERE id = ?`).run(value, now(), taskId); this.emit(row.project_id, taskId, 'task.updated', { field }); return this.getTask(taskId)!; } setResult(taskId: string, result: TaskResult): Task { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); this.db.prepare(`UPDATE tasks SET result = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(result), now(), taskId); this.emit(row.project_id, taskId, 'task.updated', { field: 'result' }); return this.getTask(taskId)!; } /** * 部分更新任务(title/priority/complexity)。 * complexity 修改仅允许尚未进入执行链路的状态(init/analyzing/speccing/ready/plan_review/spec_review/blocked); * 改后 status 重置为新复杂度的初始态(initialNextStatus)并广播 status.changed。 */ patchTask(taskId: string, patch: PatchTaskInput): Task { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); const fields: string[] = []; if (patch.title !== undefined) { if (!patch.title.trim()) throw new StoreError('title 不能为空'); this.db.prepare(`UPDATE tasks SET title = ?, updated_at = ? WHERE id = ?`).run(patch.title, now(), taskId); fields.push('title'); } if (patch.priority !== undefined) { assertPriority(patch.priority); this.db.prepare(`UPDATE tasks SET priority = ?, updated_at = ? WHERE id = ?`).run(patch.priority, now(), taskId); fields.push('priority'); } let statusChange: { from: TaskStatus; to: TaskStatus } | null = null; if (patch.complexity !== undefined && patch.complexity !== row.complexity) { const cur = row.status as TaskStatus; if (!COMPLEXITY_EDITABLE.has(cur)) { throw new StoreError( `当前状态 ${STATUS_LABEL[cur]}(${cur}) 不允许修改复杂度(仅限 init/analyzing/speccing/ready/plan_review/spec_review/blocked)`, ); } const to = this.resolveReady(row, initialNextStatus(patch.complexity)); // ready 落位时考虑依赖 this.db.prepare(`UPDATE tasks SET complexity = ?, status = ?, updated_at = ? WHERE id = ?`) .run(patch.complexity, to, now(), taskId); fields.push('complexity'); if (to !== cur) statusChange = { from: cur, to }; } if (fields.length > 0) { this.emit(row.project_id, taskId, 'task.updated', { fields }); if (statusChange) { this.emit(row.project_id, taskId, 'status.changed', { ...statusChange, reason: 'complexity.changed' }); } } return this.getTask(taskId)!; } // ---------- 旧 todo 来源映射(sync 引擎用) ---------- setSourceRef(taskId: string, ref: string): void { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); this.db.prepare(`UPDATE tasks SET source_ref = ? WHERE id = ?`).run(ref, taskId); } getTaskBySourceRef(projectId: string, ref: string): Task | null { const row = this.db.prepare( `SELECT * FROM tasks WHERE project_id = ? AND source_ref = ?`, ).get(projectId, ref) as TaskRow | undefined; return row ? rowToTask(row, this.listApprovals(row.id)) : null; } /** 项目内全部已映射任务:source_ref → Task */ sourceRefMap(projectId: string): Map { const rows = this.db.prepare( `SELECT * FROM tasks WHERE project_id = ? AND source_ref IS NOT NULL`, ).all(projectId) as TaskRow[]; const map = new Map(); for (const r of rows) map.set(r.source_ref!, rowToTask(r)); return map; } // ---------- 依赖驱动的 ready/blocked 自动管理 ---------- /** 依赖是否全部 done(未知 id 视为未满足) */ private depsMetRow(row: TaskRow): boolean { const deps = JSON.parse(row.deps) as string[]; return deps.every((d) => this.getTaskRow(d)?.status === 'done'); } /** 意图是 ready 时按依赖落位:未满足 → blocked(系统自动管理,不可手动绕过) */ private resolveReady(row: TaskRow, to: TaskStatus): TaskStatus { return to === 'ready' && !this.depsMetRow(row) ? 'blocked' : to; } /** 某任务 done 后:把同项目内依赖已全部满足的 blocked 任务自动放行为 ready */ private releaseDependents(projectId: string): void { const rows = this.db.prepare( `SELECT * FROM tasks WHERE project_id = ? AND status = 'blocked'`, ).all(projectId) as TaskRow[]; for (const r of rows) { if (!this.depsMetRow(r)) continue; this.db.prepare(`UPDATE tasks SET status = 'ready', updated_at = ? WHERE id = ?`).run(now(), r.id); this.emit(projectId, r.id, 'status.changed', { from: 'blocked', to: 'ready', auto: 'deps-met' }); } } /** * 任务变 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 启动时调用,纠正存量数据。 */ reconcileDeps(projectId?: string): { blocked: number; released: number } { const pids = projectId ? [projectId] : this.listProjects().map((p) => p.id); let nBlocked = 0; let nReleased = 0; for (const pid of pids) { const rows = this.db.prepare( `SELECT * FROM tasks WHERE project_id = ? AND status IN ('ready','blocked')`, ).all(pid) as TaskRow[]; for (const r of rows) { const met = this.depsMetRow(r); if (r.status === 'ready' && !met) { this.db.prepare(`UPDATE tasks SET status = 'blocked', updated_at = ? WHERE id = ?`).run(now(), r.id); this.emit(pid, r.id, 'status.changed', { from: 'ready', to: 'blocked', auto: 'deps-reconcile' }); nBlocked++; } else if (r.status === 'blocked' && met) { this.db.prepare(`UPDATE tasks SET status = 'ready', updated_at = ? WHERE id = ?`).run(now(), r.id); this.emit(pid, r.id, 'status.changed', { from: 'blocked', to: 'ready', auto: 'deps-reconcile' }); nReleased++; } } } 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(绕过执行链路与依赖落位—— * 历史事实优先,避免已完成的工作被当作待执行复活),并放行依赖它的任务。 */ forceDone(taskId: string, actor: string): Task { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); const from = row.status as TaskStatus; 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.afterDone(row.project_id, taskId); return this.getTask(taskId)!; } /** 受守卫的状态变更:非法流转抛错;记录 status.changed 事件。 */ transition(taskId: string, to: TaskStatus, meta: Record = {}): Task { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); const from = row.status as TaskStatus; if (from === to) return this.getTask(taskId)!; if (!canTransition(from, to)) { throw new StoreError(`非法状态流转:${STATUS_LABEL[from]}(${from}) → ${STATUS_LABEL[to]}(${to})`); } // 意图 ready 但依赖未满足 → 系统落位 blocked const actual = this.resolveReady(row, to); if (from === actual) return this.getTask(taskId)!; this.db.prepare(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`).run(actual, now(), taskId); this.emit(row.project_id, taskId, 'status.changed', { from, to: actual, ...(actual !== to ? { requested: to, auto: 'deps-unmet' } : {}), ...meta, }); if (actual === 'done') this.afterDone(row.project_id, taskId); return this.getTask(taskId)!; } // ---------- Approvals(审批闸) ---------- private listApprovals(taskId: string): ApprovalRecord[] { const rows = this.db.prepare(`SELECT * FROM approvals WHERE task_id = ? ORDER BY at`).all(taskId) as ApprovalRow[]; return rows.map(rowToApproval); } /** 列出所有处于审批闸状态(plan_review/spec_review/exec_review)的任务。 */ pendingApprovals(projectId?: string): Task[] { const sql = projectId ? `SELECT * FROM tasks WHERE project_id = ? AND status IN ('plan_review','spec_review','exec_review') ORDER BY updated_at` : `SELECT * FROM tasks WHERE status IN ('plan_review','spec_review','exec_review') ORDER BY updated_at`; const rows = (projectId ? this.db.prepare(sql).all(projectId) : this.db.prepare(sql).all()) as TaskRow[]; return rows.map((r) => rowToTask(r, this.listApprovals(r.id))); } /** * 处理一次审批。accept/reject 必须发生在闸状态上;reject 必须带 reason(改进意见)。 * accept:plan_review→decomposed, spec_review→ready, exec_review→done。 * reject:plan_review→analyzing, spec_review→speccing, exec_review→ready(返工)。 */ decide(taskId: string, action: 'accept' | 'reject', actor: string, reason?: string | null): Task { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); const from = row.status as TaskStatus; const gate: GateKind | null = gateOf(from); if (!gate) throw new StoreError(`任务不在审批闸状态:当前 ${STATUS_LABEL[from]}(${from})`); if (action === 'reject' && !reason?.trim()) { throw new StoreError('reject 必须填写改进意见'); } const intended: TaskStatus = action === 'accept' ? ({ plan: 'decomposed', spec: 'ready', exec: 'done' } as const)[gate] : ({ plan: 'analyzing', spec: 'speccing', exec: 'ready' } as const)[gate]; const to = this.resolveReady(row, intended); // spec accept / exec reject → ready 时按依赖落位 const txn = this.db.transaction(() => { const ap: ApprovalRow = { id: id('apr'), task_id: taskId, gate, action, actor, reason: reason ?? null, at: now(), }; this.db.prepare( `INSERT INTO approvals (id,task_id,gate,action,actor,reason,at) VALUES (@id,@task_id,@gate,@action,@actor,@reason,@at)`, ).run(ap); this.db.prepare(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`).run(to, now(), taskId); }); txn(); this.emit(row.project_id, taskId, action === 'accept' ? 'approval.granted' : 'approval.rejected', { gate, from, to, reason: reason ?? null }); if (to === 'done') this.afterDone(row.project_id, taskId); // 完成 → 放行依赖 + 容器自动收口 return this.getTask(taskId)!; } // ---------- Runs ---------- startRun(taskId: string, kind: Run['kind'], fields: Partial> = {}): Run { const row = this.getTaskRow(taskId); if (!row) throw new StoreError(`任务不存在: ${taskId}`); const rr: RunRow = { id: id('run'), task_id: taskId, kind, worktree: fields.worktree ?? null, branch: fields.branch ?? null, status: 'started', started_at: now(), ended_at: null, transcript_ref: null, claude_session_id: null, error: null, }; this.db.prepare( `INSERT INTO runs (id,task_id,kind,worktree,branch,status,started_at,ended_at,transcript_ref,claude_session_id,error) VALUES (@id,@task_id,@kind,@worktree,@branch,@status,@started_at,@ended_at,@transcript_ref,@claude_session_id,@error)`, ).run(rr); this.emit(row.project_id, taskId, 'run.started', { runId: rr.id, kind }); return rowToRun(rr); } finishRun(runId: string, status: Run['status'], fields: Partial> = {}): Run { const row = this.db.prepare(`SELECT * FROM runs WHERE id = ?`).get(runId) as RunRow | undefined; if (!row) throw new StoreError(`run 不存在: ${runId}`); this.db.prepare( `UPDATE runs SET status = ?, ended_at = ?, transcript_ref = ?, claude_session_id = ?, error = ? WHERE id = ?`, ).run(status, now(), fields.transcriptRef ?? row.transcript_ref, fields.claudeSessionId ?? row.claude_session_id, fields.error ?? row.error, runId); const task = this.getTaskRow(row.task_id); if (task) this.emit(task.project_id, row.task_id, 'run.finished', { runId, status }); return rowToRun(this.db.prepare(`SELECT * FROM runs WHERE id = ?`).get(runId) as RunRow); } /** 所有进行中的 run(status='started'),联 tasks 取任务标题与项目。 */ activeRuns(): ActiveRun[] { const rows = this.db.prepare( `SELECT r.id AS run_id, r.task_id, r.kind, r.started_at, t.title, t.project_id FROM runs r JOIN tasks t ON t.id = r.task_id WHERE r.status = 'started' ORDER BY r.started_at`, ).all() as Array<{ run_id: string; task_id: string; kind: string; started_at: string; title: string; project_id: string }>; return rows.map((r) => ({ runId: r.run_id, taskId: r.task_id, taskTitle: r.title, kind: r.kind, startedAt: r.started_at, projectId: r.project_id, })); } listRuns(taskId: string): Run[] { const rows = this.db.prepare(`SELECT * FROM runs WHERE task_id = ? ORDER BY started_at`).all(taskId) as RunRow[]; return rows.map(rowToRun); } // ---------- 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 ?`, ).all(projectId, limit) as EventRow[]; return rows.map(rowToEvent).reverse(); } /** * 取下一个可执行任务(叶子、ready、依赖全部 done)。供编排器领取。 * 被拆解的 Hard 容器任务不会是 ready(停在 decomposed),天然排除。 */ nextExecutable(projectId: string): Task | 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; } }