From 82e40f0c218b183d5fa0a024ba3d414e5a1f8a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Wed, 13 May 2026 14:20:23 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20planner=20abort=20path=20=E2=80=94=20fa?= =?UTF-8?q?il=20fast=20when=20workspace=20info=20is=20missing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlannerMeta is now a discriminated union: planned | aborted - Moderator routes aborted planner → END (no coder invocation) - System prompt requires absolute workspace path, instructs abort if missing - extractRefs handles both variants - Test: 'planner aborted → END' Signed-off-by: 小橘 --- .../__tests__/develop-template.test.ts | 27 ++++++++++++++----- .../src/moderator.ts | 19 +++++++++++-- .../src/roles/planner.ts | 26 +++++++++++++----- 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/packages/workflow-template-develop/__tests__/develop-template.test.ts b/packages/workflow-template-develop/__tests__/develop-template.test.ts index 0013ea2..5813b60 100644 --- a/packages/workflow-template-develop/__tests__/develop-template.test.ts +++ b/packages/workflow-template-develop/__tests__/develop-template.test.ts @@ -9,7 +9,9 @@ import type { DevelopMeta } from "../src/roles.js"; const developModerator = tableToModerator(developTable); -const DEFAULT_PHASES: PlannerMeta["phases"] = [ +type PlannedMeta = Extract; + +const DEFAULT_PHASES: PlannedMeta["phases"] = [ { hash: "4KNMR2PX", title: "Do the work", @@ -36,11 +38,11 @@ function makeCtx(steps: ModeratorContext["steps"]): ModeratorContex }; } -function plannerStep(phases: PlannerMeta["phases"] = DEFAULT_PHASES): RoleStep { +function plannerStep(phases: PlannedMeta["phases"] = DEFAULT_PHASES): RoleStep { return { role: "planner", contentHash: "STUBHASHPLANNER001", - meta: { phases }, + meta: { status: "planned" as const, phases }, refs: phases.map((p) => p.hash), timestamp: 1, }; @@ -153,7 +155,7 @@ describe("developModerator", () => { }); test("multiple planner phases → coder until all complete, then reviewer", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "AA000001", title: "first phase" }, { hash: "AA000002", title: "second phase" }, ]; @@ -167,7 +169,7 @@ describe("developModerator", () => { }); test("one-shot coder reports only last phase hash → reviewer (moderator treats as all phases done)", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "BB000001", title: "setup branch" }, { hash: "BB000002", title: "write tests" }, { hash: "BB000003", title: "verify" }, @@ -179,7 +181,7 @@ describe("developModerator", () => { }); test("unrecognised completedPhase hash → coder retry when budget allows", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "CC000001", title: "first phase" }, { hash: "CC000002", title: "second phase" }, ]; @@ -187,7 +189,7 @@ describe("developModerator", () => { }); test("incomplete phases → coder retry (supervisor controls termination)", () => { - const phases: PlannerMeta["phases"] = [ + const phases: PlannedMeta["phases"] = [ { hash: "DD000001", title: "first phase" }, { hash: "DD000002", title: "second phase" }, ]; @@ -198,6 +200,17 @@ describe("developModerator", () => { expect(developModerator(makeCtx(steps))).toBe("coder"); }); + test("planner aborted → END", () => { + const abortedStep: RoleStep = { + role: "planner", + contentHash: "STUBHASHABORT001", + meta: { status: "aborted", reason: "No workspace path provided" }, + refs: [], + timestamp: 1, + }; + expect(developModerator(makeCtx([abortedStep]))).toBe("__end__"); + }); + test("committer → END for any committer meta status", () => { const committed = committerStep({ status: "committed", branch: "f", commitSha: "x" }); const recoverable = committerStep({ diff --git a/packages/workflow-template-develop/src/moderator.ts b/packages/workflow-template-develop/src/moderator.ts index a2c4d79..a8152be 100644 --- a/packages/workflow-template-develop/src/moderator.ts +++ b/packages/workflow-template-develop/src/moderator.ts @@ -30,6 +30,18 @@ function coderFinishedAllPlannedPhases( // ── Conditions ───────────────────────────────────────────────────── +const plannerAborted: ModeratorCondition = { + name: "plannerAborted", + description: "The planner aborted due to insufficient information", + check: (ctx) => { + const plannerStep = ctx.steps.find((s) => s.role === "planner"); + if (plannerStep === undefined) { + return false; + } + return plannerStep.meta.status === "aborted"; + }, +}; + const allPhasesComplete: ModeratorCondition = { name: "allPhasesComplete", description: "All planned phases have been completed by the coder", @@ -38,7 +50,7 @@ const allPhasesComplete: ModeratorCondition = { if (plannerStep === undefined) { return true; } - const phases = plannerStep.meta.phases; + const phases = plannerStep.meta.status === "planned" ? plannerStep.meta.phases : []; if (!Array.isArray(phases)) { return true; } @@ -71,7 +83,10 @@ const testsPassed: ModeratorCondition = { const table: ModeratorTable = { [START]: [{ condition: "FALLBACK", role: "planner" }], - planner: [{ condition: "FALLBACK", role: "coder" }], + planner: [ + { condition: plannerAborted, role: END }, + { condition: "FALLBACK", role: "coder" }, + ], coder: [ { condition: allPhasesComplete, role: "reviewer" }, { condition: "FALLBACK", role: "coder" }, diff --git a/packages/workflow-template-develop/src/roles/planner.ts b/packages/workflow-template-develop/src/roles/planner.ts index 635d68a..eec6bf0 100644 --- a/packages/workflow-template-develop/src/roles/planner.ts +++ b/packages/workflow-template-develop/src/roles/planner.ts @@ -6,16 +6,27 @@ export const phaseSchema = z.object({ title: z.string(), }); -export const plannerMetaSchema = z.object({ - phases: z.array(phaseSchema), -}); +export const plannerMetaSchema = z.discriminatedUnion("status", [ + z.object({ + status: z.literal("planned"), + phases: z.array(phaseSchema), + }), + z.object({ + status: z.literal("aborted"), + reason: z.string().describe("Why the task cannot proceed"), + }), +]); export type PlannerMeta = z.infer; -const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time. +const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time. **Abort** if the prompt lacks critical information (e.g. no project/workspace path, ambiguous target repo). Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide. +## Prerequisites — check FIRST + +The prompt MUST include an **absolute filesystem path** to the project workspace (e.g. \`/home/user/repos/my-project\`). If no workspace path is given and you cannot reliably infer one from context, **abort immediately** with a clear reason explaining what information is missing. Do NOT guess paths. + ## Storing phase details — MANDATORY For each phase, store its full detail text in CAS via \`uncaged-workflow cas put ''\`. The command prints a content-hash — use that as the phase identifier. @@ -37,7 +48,10 @@ Fewer phases is always better. Each phase must justify its existence — if two ## Output format After storing all phases via the CLI, output compact JSON only: - { "phases": [{ "hash": "", "title": "" }] } + { "status": "planned", "phases": [{ "hash": "", "title": "" }] } + +If aborting: + { "status": "aborted", "reason": "" } Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. @@ -49,5 +63,5 @@ export const plannerRole: RoleDefinition = { description: "Breaks the task into sequential phases for the coder.", systemPrompt: PLANNER_SYSTEM, schema: plannerMetaSchema, - extractRefs: (meta) => meta.phases.map((p) => p.hash), + extractRefs: (meta) => meta.status === "planned" ? meta.phases.map((p) => p.hash) : [], };