From 3780f1c0646d779441f4af7bc9e27675d5c41ef4 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 29 Jun 2026 10:57:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(orchestrator):=20=E6=8B=86=E8=A7=A3?= =?UTF-8?q?=E6=9C=AA=E6=89=B9=E5=87=86=E5=89=8D=E5=86=BB=E7=BB=93=E5=AD=90?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E2=80=94=E2=80=94=E7=88=B6=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=20plan=5Freview/analyzing=20=E6=9C=9F=E9=97=B4=E5=AD=90?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E4=B8=8D=E5=8F=AF=E9=A2=86=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 bug:auto-approved 项目里 planner 拆解后子任务立即被领取执行,而父任务还在 plan_review 等人审批,导致拆解若被驳回则子任务已白跑。claimable 加闸:子任务的父任务 处于 plan_review(待审拆解)或 analyzing(拆解中)时冻结,批准(decomposed)后放行。 仅冻结这两态,不影响 todo-sync 等其它带 parentId 的流程。 Co-Authored-By: Claude Opus 4.8 --- src/daemon/orchestrator.ts | 5 +++++ test/orchestrator.test.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/daemon/orchestrator.ts b/src/daemon/orchestrator.ts index 4408886..d9cae82 100644 --- a/src/daemon/orchestrator.ts +++ b/src/daemon/orchestrator.ts @@ -133,6 +133,11 @@ export function createOrchestrator(store: Store, log: OrchestratorLogger, deps: if (!CLAIMABLE.has(t.status)) return false; if (inflight.has(t.id)) return false; // 已有在途 run(executor/planner) if (parents.has(t.id)) return false; // 非叶子(容器)跳过 + // 父任务拆解待审批 / 拆解中 → 冻结子任务,避免拆解被驳回时子任务已白跑(仅冻结这两态,不影响 sync 等流程) + if (t.parentId) { + const ps = byId.get(t.parentId)?.status; + if (ps === 'plan_review' || ps === 'analyzing') return false; + } if (easyOnly && t.complexity !== 'easy') return false; if (t.nextEligibleAt && nowMs < Date.parse(t.nextEligibleAt)) return false; // 退避冷却中 return t.deps.every((dep) => byId.get(dep)?.status === 'done'); diff --git a/test/orchestrator.test.ts b/test/orchestrator.test.ts index 4530090..d2ad749 100644 --- a/test/orchestrator.test.ts +++ b/test/orchestrator.test.ts @@ -415,6 +415,23 @@ test('claimable 纳入 analyzing:hard 任务派 planner-decompose', () => { store.close(); }); +test('子任务在父任务 plan_review 期间不可领取,拆解批准(decomposed)后放行', () => { + const { store, projectId } = setup('auto-approved'); + const parent = store.createTask({ projectId, title: 'epic', complexity: 'hard' }); // → analyzing + const child = store.createTask({ projectId, parentId: parent.id, title: 'sub', complexity: 'easy' }); // → ready + store.transition(parent.id, 'plan_review', { by: 'test' }); // 模拟拆解完成待审 + const state = freshState(); + const { deps } = mockDeps(state); + const orch = createOrchestrator(store, noopLog, deps); + orch.claimTick(); + assert.equal(state.spawned.length, 0, '父任务 plan_review(拆解待审)期间,子任务不应被领取'); + store.transition(parent.id, 'decomposed', { by: 'test' }); // 批准拆解 + orch.claimTick(); + assert.equal(state.spawned.length, 1, '拆解批准后子任务放行可领取'); + assert.equal(store.listRuns(child.id).length, 1, '被领取的是该子任务'); + store.close(); +}); + test('auto-easy 不做规划:medium(speccing) 不被领取', () => { const { store, projectId } = setup('auto-easy'); const t = store.createTask({ projectId, title: 'feat', complexity: 'medium' }); // → speccing