feat(planner): 编排器把 analyzing/speccing 也当可执行——派 planner run 自动拆解/写方案
修复"hard/medium 任务建后永远停在 analyzing/speccing"的缺口:编排器现在对 analyzing(hard) 派 planner-decompose、对 speccing(medium) 派 planner-spec(auto-approved 下;auto-easy 仍只执行 easy), planner 只读跑 CC(无 worktree/无写)产出 → daemon 落库 + 自动提交到 plan_review/spec_review 闸(仍人审)。 - protocol: JobSpec.runKind + spec-result/decompose-result 两型 outbox + DecomposeResult - runner: runPlanner(只读 Read/Glob/Grep 跑在 repo,opus 档)+ buildPlannerPrompt - pipeline: runKind 分支 + parseDecompose(取末尾 fenced JSON,非法→failed) - orchestrator: claimable 纳入 analyzing/speccing + 排除在途;并发/in-flight 从 countExecuting 泛化为 inflightTaskIds(有 started run 的任务,executor+planner 通用);claimOne 按状态分 executor/planner (planner 不转状态);reap 泛化(死 planner→failPlanAttempt) - ingest: ingestAll 覆盖所有 started run;spec-result→setSpec+spec_review;decompose-result→setPlan+建子任务+plan_review; failed 按 kind 分流(planner→failPlanAttempt 退避留态、超限→needs_attention) - store: inflightTaskIds / liveRunsWithTask / failPlanAttempt;reconcile 泛化到所有 started run - status: analyzing/speccing 加 →needs_attention(planner 失败超限升级) - models: planner 角色(opus);status: RunKind 已含 planner - 测试 +12(pipeline 5 / ingest 3 / orchestrator 4),194 全绿 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+45
-24
@@ -117,17 +117,21 @@ export function createOrchestrator(store: Store, log: OrchestratorLogger, deps:
|
||||
};
|
||||
|
||||
/**
|
||||
* 本项目可领取的任务:queued(重试/孤儿)+ ready 叶子且 deps 全 done;auto-easy 只挑 easy。
|
||||
* 退避读持久化的 task.nextEligibleAt(早于它不领)。按调度分降序返回(见 model/scoring.ts)。
|
||||
* 本项目可领取的任务(三类"可执行"状态):
|
||||
* ready/queued → executor(执行改动);analyzing → planner 拆解 Hard;speccing → planner 写方案 Medium。
|
||||
* 排除:已在途(inflight=有 started run 的任务)、非叶子容器、退避冷却中、deps 未全 done;
|
||||
* auto-easy 只挑 easy(故不做规划——analyzing/speccing 都是 hard/medium 被排除)。按调度分降序返回。
|
||||
*/
|
||||
function claimable(project: Project): Array<{ task: Task; score: number }> {
|
||||
function claimable(project: Project, inflight: ReadonlySet<string>): Array<{ task: Task; score: number }> {
|
||||
const nowMs = d.nowMs();
|
||||
const tasks = store.listTasks(project.id);
|
||||
const byId = new Map(tasks.map((t) => [t.id, t]));
|
||||
const parents = new Set(tasks.filter((t) => t.parentId).map((t) => t.parentId as string));
|
||||
const easyOnly = project.autonomy === 'auto-easy';
|
||||
const CLAIMABLE = new Set<string>(['ready', 'queued', 'analyzing', 'speccing']);
|
||||
const candidates = tasks.filter((t) => {
|
||||
if (t.status !== 'ready' && t.status !== 'queued') return false;
|
||||
if (!CLAIMABLE.has(t.status)) return false;
|
||||
if (inflight.has(t.id)) return false; // 已有在途 run(executor/planner)
|
||||
if (parents.has(t.id)) return false; // 非叶子(容器)跳过
|
||||
if (easyOnly && t.complexity !== 'easy') return false;
|
||||
if (t.nextEligibleAt && nowMs < Date.parse(t.nextEligibleAt)) return false; // 退避冷却中
|
||||
@@ -136,51 +140,66 @@ export function createOrchestrator(store: Store, log: OrchestratorLogger, deps:
|
||||
return rankByScore(candidates, tasks);
|
||||
}
|
||||
|
||||
/** 领取一个任务:建分支/worktree 路径 → executing → startRun → 写 job.json → spawn worker → 记 pid。 */
|
||||
/**
|
||||
* 领取一个任务并起 worker:
|
||||
* - ready/queued → executor:ready→queued→executing,startRun(executor, worktree),job.runKind=executor。
|
||||
* - analyzing → planner-decompose;speccing → planner-spec:【不转状态】(留 analyzing/speccing 作"规划中"),
|
||||
* startRun(planner),planner 只读跑在 repo(worktreeDir 仅记录用)。失败按 kind 走 failTaskAttempt/failPlanAttempt。
|
||||
*/
|
||||
function claimOne(project: Project, task: Task, score: number): void {
|
||||
const isPlanner = task.status === 'analyzing' || task.status === 'speccing';
|
||||
const runKind = task.status === 'analyzing' ? 'planner-decompose'
|
||||
: task.status === 'speccing' ? 'planner-spec' : 'executor';
|
||||
const dbKind = isPlanner ? 'planner' : 'executor';
|
||||
const role = isPlanner ? 'planner' : 'executor';
|
||||
let runId: string | null = null;
|
||||
try {
|
||||
if (task.status === 'ready') store.transition(task.id, 'queued', { by: 'orchestrator', score });
|
||||
|
||||
const branch = branchFor(task.id);
|
||||
const dir = worktreeDirFor(project.repoPath, task.id);
|
||||
|
||||
store.transition(task.id, 'executing', { by: 'orchestrator' });
|
||||
const run = store.startRun(task.id, 'executor', { worktree: dir, branch });
|
||||
if (!isPlanner) {
|
||||
if (task.status === 'ready') store.transition(task.id, 'queued', { by: 'orchestrator', score });
|
||||
store.transition(task.id, 'executing', { by: 'orchestrator' });
|
||||
}
|
||||
// planner run 只读跑在主仓,worktree 记 repoPath;executor 记隔离 worktree 路径
|
||||
const run = store.startRun(task.id, dbKind, { worktree: isPlanner ? project.repoPath : dir, branch });
|
||||
runId = run.id;
|
||||
|
||||
d.writeJobSpec({
|
||||
runId: run.id,
|
||||
task: { ...task, status: 'executing' },
|
||||
project,
|
||||
worktreeDir: dir,
|
||||
branch,
|
||||
task: { ...task, status: isPlanner ? task.status : 'executing' },
|
||||
project, worktreeDir: dir, branch, runKind,
|
||||
});
|
||||
|
||||
log.info(`领取任务 ${task.id}「${task.title}」score=${score} run=${run.id} model=${pickModel(task, project, 'executor')}`);
|
||||
log.info(`领取 ${runKind} ${task.id}「${task.title}」score=${score} run=${run.id} model=${pickModel(task, project, role)}`);
|
||||
const pid = d.spawnWorker(run.id);
|
||||
store.setWorkerPid(run.id, pid);
|
||||
log.info(`任务 ${task.id} 已起 worker pid=${pid} worktree=${dir}`);
|
||||
log.info(`任务 ${task.id} 已起 worker pid=${pid}(${runKind})`);
|
||||
} catch (e) {
|
||||
const msg = (e as Error).message;
|
||||
log.error(`任务 ${task.id} 领取/spawn 失败:${msg}`);
|
||||
try {
|
||||
store.failTaskAttempt(task.id, runId, msg);
|
||||
if (isPlanner) store.failPlanAttempt(task.id, runId, msg);
|
||||
else store.failTaskAttempt(task.id, runId, msg);
|
||||
} catch (e2) {
|
||||
log.error(`任务 ${task.id} 失败收尾出错:${(e2 as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 领取轮:对每个 active 且 autonomy≠manual 的项目,在 countExecuting<concurrency 时按 claimable 领新任务并 spawn worker。 */
|
||||
/**
|
||||
* 领取轮:对每个 active 且 autonomy≠manual 的项目,并发闸 = inflightTaskIds(有 started run 的任务,
|
||||
* executor + planner 通用)。在 active<concurrency 时按 claimable 领新任务并 spawn worker。
|
||||
*/
|
||||
function claimTick(): void {
|
||||
try {
|
||||
for (const p of store.listProjects()) {
|
||||
if (p.status !== 'active' || p.autonomy === 'manual') continue;
|
||||
let active = store.countExecuting(p.id);
|
||||
const inflight = store.inflightTaskIds(p.id);
|
||||
let active = inflight.size;
|
||||
if (active >= p.concurrency) continue;
|
||||
|
||||
for (const { task, score } of claimable(p)) {
|
||||
for (const { task, score } of claimable(p, inflight)) {
|
||||
if (active >= p.concurrency) break;
|
||||
claimOne(p, task, score);
|
||||
active++;
|
||||
@@ -200,19 +219,21 @@ export function createOrchestrator(store: Store, log: OrchestratorLogger, deps:
|
||||
}
|
||||
}
|
||||
|
||||
/** 回收:对每个 executing 任务判活,死 worker → failTaskAttempt(收尾 + 重试/needs_attention)。 */
|
||||
/** 回收:对每个在途 run(executor + planner)判活,死 worker → 按 kind 收尾(executor=failTaskAttempt / planner=failPlanAttempt)。 */
|
||||
function reap(): void {
|
||||
try {
|
||||
for (const { task, run } of store.executingWithLatestExecutorRun()) {
|
||||
const alive = run !== null && d.isWorkerAlive({
|
||||
for (const { task, run } of store.liveRunsWithTask()) {
|
||||
const alive = d.isWorkerAlive({
|
||||
pid: run.workerPid,
|
||||
heartbeatAgeMs: heartbeatAgeMs(run.id, d.nowMs()),
|
||||
startedAgeMs: d.nowMs() - Date.parse(run.startedAt),
|
||||
});
|
||||
if (alive) continue;
|
||||
log.error(`任务 ${task.id} worker 异常退出(run=${run?.id ?? '无'})→ 回收`);
|
||||
log.error(`任务 ${task.id} worker 异常退出(run=${run.id} ${run.kind})→ 回收`);
|
||||
try {
|
||||
store.failTaskAttempt(task.id, run?.id ?? null, 'worker 异常退出');
|
||||
if (run.kind === 'executor') store.failTaskAttempt(task.id, run.id, 'worker 异常退出');
|
||||
else if (run.kind === 'planner') store.failPlanAttempt(task.id, run.id, 'planner worker 异常退出');
|
||||
else store.finishRun(run.id, 'failed', { error: 'worker 异常退出' });
|
||||
} catch (e) {
|
||||
log.error(`任务 ${task.id} 回收收尾出错:${(e as Error).message}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user