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:
@@ -203,3 +203,68 @@ test('ingest 续读:先 ingest 半截(started/phase),再追加 result,
|
||||
store.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ───────────────────────── planner run 摄取 ─────────────────────────
|
||||
|
||||
/** 真 Store + 一个 medium(speccing) 或 hard(analyzing) 任务 + 一条 planner run(模拟已领取规划)。 */
|
||||
function setupPlanning(complexity: 'medium' | 'hard'): { store: Store; taskId: string; runId: string } {
|
||||
const store = new Store(':memory:');
|
||||
const p = store.createProject({ name: 'plan', repoPath: '/tmp/plan-' + Math.random(), autonomy: 'auto-approved' });
|
||||
const t = store.createTask({ projectId: p.id, title: '大任务', complexity });
|
||||
// 建任务即落 speccing(medium)/analyzing(hard),不转状态,直接起 planner run
|
||||
const run = store.startRun(t.id, 'planner', { worktree: p.repoPath, branch: 'n/a' });
|
||||
store.setWorkerPid(run.id, 4243);
|
||||
return { store, taskId: t.id, runId: run.id };
|
||||
}
|
||||
|
||||
test('ingest planner-spec:setSpec + 转 spec_review + 收尾 planner run', () => {
|
||||
withTmpDataDir(() => {
|
||||
const { store, taskId, runId } = setupPlanning('medium');
|
||||
assert.equal(store.getTask(taskId)!.status, 'speccing');
|
||||
appendOutbox(runId, { type: 'started', pid: 4243, worktree: '/r', branch: 'n/a', model: 'opus' });
|
||||
appendOutbox(runId, { type: 'spec-result', spec: '## 改动\n改 a.ts', transcriptRef: '/p.jsonl', sessionId: 's' });
|
||||
appendOutbox(runId, { type: 'done' });
|
||||
ingestRun(store, noopLog, runId);
|
||||
const t = store.getTask(taskId)!;
|
||||
assert.equal(t.status, 'spec_review');
|
||||
assert.equal(t.spec, '## 改动\n改 a.ts');
|
||||
assert.equal(store.getRun(runId)!.status, 'succeeded');
|
||||
store.close();
|
||||
});
|
||||
});
|
||||
|
||||
test('ingest planner-decompose:setPlan + 建子任务 + 转 plan_review', () => {
|
||||
withTmpDataDir(() => {
|
||||
const { store, taskId, runId } = setupPlanning('hard');
|
||||
assert.equal(store.getTask(taskId)!.status, 'analyzing');
|
||||
appendOutbox(runId, {
|
||||
type: 'decompose-result', plan: '拆成两步',
|
||||
subtasks: [{ title: '建骨架', complexity: 'easy' }, { title: '实现', complexity: 'medium' }],
|
||||
transcriptRef: null, sessionId: null,
|
||||
});
|
||||
appendOutbox(runId, { type: 'done' });
|
||||
ingestRun(store, noopLog, runId);
|
||||
const t = store.getTask(taskId)!;
|
||||
assert.equal(t.status, 'plan_review');
|
||||
assert.equal(t.plan, '拆成两步');
|
||||
const kids = store.listTasks(t.projectId).filter((x) => x.parentId === taskId);
|
||||
assert.equal(kids.length, 2, '建了 2 个子任务');
|
||||
assert.deepEqual(kids.map((k) => k.complexity).sort(), ['easy', 'medium']);
|
||||
assert.equal(store.getRun(runId)!.status, 'succeeded');
|
||||
store.close();
|
||||
});
|
||||
});
|
||||
|
||||
test('ingest planner failed:走 failPlanAttempt(退避,任务留 speccing,不进 failed)', () => {
|
||||
withTmpDataDir(() => {
|
||||
const { store, taskId, runId } = setupPlanning('medium');
|
||||
appendOutbox(runId, { type: 'failed', error: 'planner 崩了', transcriptRef: null, sessionId: null });
|
||||
appendOutbox(runId, { type: 'done' });
|
||||
ingestRun(store, noopLog, runId);
|
||||
const t = store.getTask(taskId)!;
|
||||
assert.equal(t.status, 'speccing', '默认 maxRetries=2,第一次失败留 speccing 退避重试');
|
||||
assert.ok(t.nextEligibleAt, '设了退避 nextEligibleAt');
|
||||
assert.equal(store.getRun(runId)!.status, 'failed');
|
||||
store.close();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user