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:
wangjia
2026-06-13 16:59:30 +08:00
parent dcf610f6fd
commit 8392944877
12 changed files with 501 additions and 46 deletions
+66
View File
@@ -94,3 +94,69 @@ export async function runTask(task: Task, project: Project, worktree: WorktreeIn
}
return { ok: true, transcriptRef: cc.transcriptRef, sessionId: cc.sessionId, finalText: cc.finalText || null, modelUsed: cc.modelUsed };
}
// ───────────────────────── planner(拆解 Hard / 写方案 Medium)─────────────────────────
export type PlanKind = 'spec' | 'decompose';
export interface PlannerResult {
ok: boolean;
transcriptRef: string | null;
sessionId: string | null;
finalText: string | null; // CC 的最终文本:spec=方案正文;decompose=分析 + 末尾 fenced JSON
error?: string;
}
/** 测试注入点(pipeline 的 planner 分支用) */
export type PlannerFn = (task: Task, project: Project, kind: PlanKind, runId: string) => Promise<PlannerResult>;
/** planner 提示词:只读代码库,spec=写改动方案;decompose=拆子任务并在末尾输出 fenced JSON。 */
export function buildPlannerPrompt(task: Task, kind: PlanKind): string {
const head = [`# 任务:${task.title}`, `任务 ID${task.id}`];
if (kind === 'spec') {
if (task.spec) head.push('', '## 现有方案草稿(可改进/替换)', task.spec);
return [
...head, '',
'## 你的角色:方案作者(只读,不改任何文件)',
'只读这个代码库,为上述任务写一份「具体改动方案」。不要编辑文件、不要执行有副作用的命令。',
'## 输出(markdown 正文)',
'## 改动(要改哪些文件/模块、怎么改)',
'## 为什么(这么做的理由、取舍)',
'## 验收(怎么算完成、怎么验证)',
'这份方案将提交给人审;通过后才会有执行 agent 按它改代码。直接输出方案正文即可。',
].join('\n');
}
if (task.plan) head.push('', '## 现有分析草稿(可改进/替换)', task.plan);
return [
...head, '',
'## 你的角色:任务拆解者(只读,不改任何文件)',
'只读这个代码库,分析上述(复杂)任务,拆成 2–6 个更小、可独立交付的子任务。不要编辑文件、不要执行有副作用的命令。',
'每个子任务标注复杂度:easy(单文件机械改动/无设计)、medium(需方案、跨几处)、hard(仍需进一步拆解)。',
'## 输出(重要)',
'先写一段分析与拆解理由;然后在【最后】输出唯一一个 ```json 代码块,严格格式:',
'```json',
'{"plan":"一句话分析与拆解理由","subtasks":[{"title":"子任务标题","complexity":"easy"}]}',
'```',
'subtasks 至少 1 个;complexity 只能是 easy|medium|hard;除这个 JSON 块外不要再写其它 ``` 代码块。',
].join('\n');
}
/**
* 起 headless CC 跑一次 planner**只读**跑在 project.repoPath(不建 worktree、不改文件)。
* 模型用 planner 角色(默认 opus)。不抛错,失败折叠进 { ok:false, error }。
*/
export async function runPlanner(task: Task, project: Project, kind: PlanKind, runId: string): Promise<PlannerResult> {
const model = pickModel(task, project, 'planner');
const timeoutMs = project.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const cc = await runClaude({
prompt: buildPlannerPrompt(task, kind),
cwd: project.repoPath, // 只读跑在主仓(planner 不改文件,无需 worktree
model,
runId,
maxTurns: 60,
timeoutMs,
permissionMode: 'acceptEdits', // planner 只读,不会触发编辑;保持工具流不被 prompt 卡住
allowedTools: ['Read', 'Glob', 'Grep'], // 纯只读:无 Edit/Write/Bash,零副作用
});
return { ok: cc.ok, transcriptRef: cc.transcriptRef, sessionId: cc.sessionId, finalText: cc.finalText || null, error: cc.error };
}