From 96fc3e220a7f7efc92c325367f31e9a500d00700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 7 May 2026 03:13:44 +0000 Subject: [PATCH] fix(solve-issue): handle one-shot coder completing all phases at once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The moderator now treats completedPhase matching the last planned phase name as full completion. Also recognizes sentinel values (all-done, all_done, complete) for robustness. Fixes #21 小橘 🍊(NEKO Team) --- packages/workflow-role-coder/src/coder.ts | 4 +-- .../__tests__/solve-issue-template.test.ts | 22 +++++++++++++ .../src/moderator.ts | 32 ++++++++++++++++--- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/workflow-role-coder/src/coder.ts b/packages/workflow-role-coder/src/coder.ts index 37f732c..fe3dc1a 100644 --- a/packages/workflow-role-coder/src/coder.ts +++ b/packages/workflow-role-coder/src/coder.ts @@ -10,13 +10,13 @@ export const coderMetaSchema = z.object({ export type CoderMeta = z.infer; const CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only. -Report which phase you completed. List the files you changed and summarize what you did.`; +Report which phase you completed using the planner's exact phase name. If you legitimately finish every remaining phase in this single turn, set completedPhase to the last phase name in the plan (the workflow treats that as full completion). List the files you changed and summarize what you did.`; export const coderRole: RoleDefinition = { description: "Implements the next incomplete planner phase and reports structured completion metadata.", systemPrompt: CODER_SYSTEM, extractPrompt: - "Extract which phase was completed, which files were changed, and a summary of the work done.", + "Extract completedPhase: the planner phase name finished this round (exact string from the plan). If multiple phases were finished in one round, use the last finished phase name. Extract filesChanged and a summary of the work.", schema: coderMetaSchema, }; diff --git a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts index 919721f..6fbdd47 100644 --- a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts +++ b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts @@ -189,6 +189,28 @@ describe("solveIssueModerator", () => { ).toBe("reviewer"); }); + test("one-shot coder reports only last phase name → reviewer (moderator treats as all phases done)", () => { + const phases: PlannerMeta["phases"] = [ + { name: "setup-branch", description: "branch", acceptance: "branch exists" }, + { name: "write-tests", description: "tests", acceptance: "tests pass" }, + { name: "verify", description: "verify", acceptance: "ok" }, + { name: "commit-and-pr", description: "pr", acceptance: "pr open" }, + ]; + expect( + solveIssueModerator(makeCtx(20, [plannerStep(phases), coderStep("commit-and-pr")])), + ).toBe("reviewer"); + }); + + test("completedPhase sentinel when not a planned name → reviewer", () => { + const phases: PlannerMeta["phases"] = [ + { name: "p1", description: "first", acceptance: "a1" }, + { name: "p2", description: "second", acceptance: "a2" }, + ]; + expect(solveIssueModerator(makeCtx(20, [plannerStep(phases), coderStep("all-done")]))).toBe( + "reviewer", + ); + }); + test("incomplete phases → END when max rounds exhausted", () => { const phases: PlannerMeta["phases"] = [ { name: "p1", description: "first", acceptance: "a1" }, diff --git a/packages/workflow-template-solve-issue/src/moderator.ts b/packages/workflow-template-solve-issue/src/moderator.ts index 20b41ab..3c73f02 100644 --- a/packages/workflow-template-solve-issue/src/moderator.ts +++ b/packages/workflow-template-solve-issue/src/moderator.ts @@ -3,6 +3,30 @@ import { END } from "@uncaged/workflow"; import type { SolveIssueMeta } from "./roles.js"; +const COMPLETED_PHASE_SENTINELS = new Set(["all-done", "all_done", "complete"]); + +function coderFinishedAllPlannedPhases( + phases: ReadonlyArray<{ name: string }>, + coderCompletedPhases: ReadonlyArray, +): boolean { + if (phases.length === 0) { + return true; + } + const plannedNames = new Set(phases.map((p) => p.name)); + const lastName = phases[phases.length - 1].name; + const explicit = new Set(coderCompletedPhases.filter((name) => plannedNames.has(name))); + if (phases.every((p) => explicit.has(p.name))) { + return true; + } + // One-shot runs often report only the final phase; treat that as the full plan done. + if (coderCompletedPhases.some((name) => name === lastName)) { + return true; + } + return coderCompletedPhases.some( + (name) => !plannedNames.has(name) && COMPLETED_PHASE_SENTINELS.has(name), + ); +} + function nextAfterCoder( ctx: ModeratorContext, maxRounds: number, @@ -12,10 +36,10 @@ function nextAfterCoder( return "reviewer"; } const phases = plannerStep.meta.phases; - const completedPhases = new Set( - ctx.steps.filter((s) => s.role === "coder").map((s) => s.meta.completedPhase), - ); - const allDone = phases.every((p) => completedPhases.has(p.name)); + const coderCompletedPhases = ctx.steps + .filter((s) => s.role === "coder") + .map((s) => s.meta.completedPhase); + const allDone = coderFinishedAllPlannedPhases(phases, coderCompletedPhases); if (allDone) { return "reviewer"; }