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:
@@ -375,3 +375,68 @@ test(`reaper 反复回收:净失败 ${DEFAULT_MAX_RETRIES} 次后 → needs_at
|
||||
assert.equal(failed.length, DEFAULT_MAX_RETRIES + 1);
|
||||
store.close();
|
||||
});
|
||||
|
||||
// ───────────────────────── planner:analyzing / speccing 也被领取 ─────────────────────────
|
||||
|
||||
test('claimable 纳入 speccing:medium 任务派 planner-spec(不转状态、startRun=planner、job.runKind=planner-spec)', () => {
|
||||
const { store, projectId } = setup('auto-approved');
|
||||
const t = store.createTask({ projectId, title: 'feat', complexity: 'medium' }); // → speccing
|
||||
assert.equal(store.getTask(t.id)!.status, 'speccing');
|
||||
const state = freshState();
|
||||
const { deps } = mockDeps(state);
|
||||
const orch = createOrchestrator(store, noopLog, deps);
|
||||
|
||||
orch.claimTick();
|
||||
assert.equal(state.spawned.length, 1, '派了一个 planner worker');
|
||||
assert.equal(store.getTask(t.id)!.status, 'speccing', 'planner 不转任务状态(留 speccing 作规划中)');
|
||||
const runs = store.listRuns(t.id);
|
||||
assert.equal(runs.length, 1);
|
||||
assert.equal(runs[0].kind, 'planner', 'run kind=planner');
|
||||
assert.equal(runs[0].status, 'started');
|
||||
assert.equal(state.jobs[0].runKind, 'planner-spec');
|
||||
|
||||
// 已在途(有 started planner run)→ 下一轮不重领
|
||||
orch.claimTick();
|
||||
assert.equal(state.spawned.length, 1, 'inflight 任务不被重复领取');
|
||||
store.close();
|
||||
});
|
||||
|
||||
test('claimable 纳入 analyzing:hard 任务派 planner-decompose', () => {
|
||||
const { store, projectId } = setup('auto-approved');
|
||||
const t = store.createTask({ projectId, title: 'epic', complexity: 'hard' }); // → analyzing
|
||||
assert.equal(store.getTask(t.id)!.status, 'analyzing');
|
||||
const state = freshState();
|
||||
const { deps } = mockDeps(state);
|
||||
createOrchestrator(store, noopLog, deps).claimTick();
|
||||
assert.equal(state.spawned.length, 1);
|
||||
assert.equal(store.getTask(t.id)!.status, 'analyzing', 'planner 不转状态');
|
||||
assert.equal(store.listRuns(t.id)[0].kind, 'planner');
|
||||
assert.equal(state.jobs[0].runKind, 'planner-decompose');
|
||||
store.close();
|
||||
});
|
||||
|
||||
test('auto-easy 不做规划:medium(speccing) 不被领取', () => {
|
||||
const { store, projectId } = setup('auto-easy');
|
||||
const t = store.createTask({ projectId, title: 'feat', complexity: 'medium' }); // → speccing
|
||||
const state = freshState();
|
||||
const { deps } = mockDeps(state);
|
||||
createOrchestrator(store, noopLog, deps).claimTick();
|
||||
assert.equal(state.spawned.length, 0, 'auto-easy 只做 easy 执行,不规划 medium/hard');
|
||||
assert.equal(store.getTask(t.id)!.status, 'speccing');
|
||||
store.close();
|
||||
});
|
||||
|
||||
test('并发闸用 inflight:1 个 executing + 1 个 planner 占满 concurrency=2', () => {
|
||||
const { store, projectId } = setup('auto-approved', 2);
|
||||
const e = store.createTask({ projectId, title: 'easy', complexity: 'easy' }); // → ready
|
||||
const m = store.createTask({ projectId, title: 'med', complexity: 'medium' }); // → speccing
|
||||
store.createTask({ projectId, title: 'easy2', complexity: 'easy' }); // → ready(第三个)
|
||||
const state = freshState();
|
||||
const { deps } = mockDeps(state);
|
||||
createOrchestrator(store, noopLog, deps).claimTick();
|
||||
// concurrency=2:一个 executor(easy) + 一个 planner(medium) 占满,第三个不领
|
||||
assert.equal(state.spawned.length, 2);
|
||||
assert.equal(store.getTask(e.id)!.status, 'executing');
|
||||
assert.equal(store.getTask(m.id)!.status, 'speccing');
|
||||
store.close();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user