fix(planning): show all refinement categories

Preserve every valid suggested refinement through prompt generation, server normalization, and desktop/mobile rendering instead of truncating the list to three.
This commit is contained in:
gsxdsm
2026-07-20 16:39:07 -07:00
parent ba08d90574
commit d008d66e01
5 changed files with 55 additions and 7 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Show every suggested Planning Mode refinement category instead of limiting choices to three.
category: fix
dev: Removes prompt, server-normalization, and client-normalization caps on suggested refinements.

View File

@@ -199,7 +199,7 @@ function normalizePlanningSummary(summary: PlanningSummary): PlanningSummary {
priority: normalizeTaskPriority(summary.priority),
suggestedDependencies: normalizeStringArray(raw.suggestedDependencies),
keyDeliverables: normalizeStringArray(raw.keyDeliverables),
suggestedRefinements: normalizeStringArray(raw.suggestedRefinements).slice(0, 3),
suggestedRefinements: normalizeStringArray(raw.suggestedRefinements),
};
}

View File

@@ -26,7 +26,7 @@ const summaryWithRefinements = {
...mockSummary,
proposedChanges: ["Change the authentication API", "Add durable session recovery"],
acceptanceCriteria: ["Refresh preserves generation", "The plan is reviewable before questions"],
suggestedRefinements: ["Security boundaries", "Rollout strategy", "Failure recovery"],
suggestedRefinements: ["Security boundaries", "Rollout strategy", "Failure recovery", "Accessibility", "Observability"],
};
describe("PlanningModeModal sequential flow", () => {
@@ -51,6 +51,8 @@ describe("PlanningModeModal sequential flow", () => {
expect(screen.getByRole("radio", { name: "Security boundaries" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Rollout strategy" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Failure recovery" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Accessibility" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Observability" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Write your own focus" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Refine" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Validate" })).toBeInTheDocument();
@@ -100,6 +102,7 @@ describe("PlanningModeModal sequential flow", () => {
expect(await screen.findByText("What to change")).toBeInTheDocument();
expect(screen.getByText("Acceptance criteria")).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Security boundaries" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Observability" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Write your own focus" })).toBeInTheDocument();
});
it("restores a validated unlinked session to create-only retry", async () => {

View File

@@ -18,8 +18,12 @@ import {
__setCreateFnAgent,
createSession,
createSessionWithAgent,
formatInitialRunningPlanRequestForAgent,
formatResponseForAgent,
getSession,
normalizePlanningSummaryPayload,
normalizePlanningQuestion,
PLANNING_SYSTEM_PROMPT,
planningStreamManager,
rewindSession,
submitResponse,
@@ -94,6 +98,40 @@ describe("reactive Planning Mode question contract", () => {
__resetPlanningState();
});
it("preserves every valid suggested refinement category", () => {
const refinementCategories = [
"Security boundaries",
"Rollout strategy",
"Failure recovery",
"Accessibility",
"Observability",
];
const summary = normalizePlanningSummaryPayload({
title: "A plan",
description: "A description",
suggestedSize: "M",
suggestedDependencies: [],
keyDeliverables: [],
suggestedRefinements: refinementCategories,
});
expect(summary.suggestedRefinements).toEqual(refinementCategories);
});
it("asks the model for all high-value categories without a three-category cap", () => {
const prompts = [
PLANNING_SYSTEM_PROMPT,
formatInitialRunningPlanRequestForAgent("Build secure accounts"),
formatResponseForAgent(FIRST_QUESTION, { scope: "secure" }),
];
for (const prompt of prompts) {
expect(prompt).not.toMatch(/exactly three/i);
expect(prompt).toMatch(/do not cap[^.]*three/i);
}
});
it("repairs malformed select options and appends one localized Other option", () => {
const question = normalizePlanningQuestion({
id: "security",

View File

@@ -242,9 +242,9 @@ First analyze the codebase and active board with the available readonly tools, f
Start by producing a concrete initial plan for review without asking a question. Ask exactly one next, high-impact question only when the user explicitly requests a refine turn. After an answer, update the running plan and do not ask a question. A JSON response with type "complete" means only that the current plan update is ready for review; it never validates or terminates the session. Only the user can validate the plan.
For a refine turn respond only with JSON: {"type":"question","data":{"id":"unique-id","type":"single_select|multi_select","question":"...","description":"...","options":[{"id":"option-a","label":"...","description":"...","pros":["..."],"cons":["..."]},{"id":"option-b","label":"...","description":"...","pros":["..."],"cons":["..."]},{"id":"other","label":"...","isOther":true}],"runningPlan":{"title":"...","description":"...","proposedChanges":["specific change"],"acceptanceCriteria":["observable outcome"],"suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"],"suggestedRefinements":["next focus 1","next focus 2","next focus 3"]}}}. For an initial-plan or answer-update turn respond only with {"type":"complete","data":{"title":"...","description":"...","proposedChanges":["specific change"],"acceptanceCriteria":["observable outcome"],"suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"],"suggestedRefinements":["next focus 1","next focus 2","next focus 3"]}}.
For a refine turn respond only with JSON: {"type":"question","data":{"id":"unique-id","type":"single_select|multi_select","question":"...","description":"...","options":[{"id":"option-a","label":"...","description":"...","pros":["..."],"cons":["..."]},{"id":"option-b","label":"...","description":"...","pros":["..."],"cons":["..."]},{"id":"other","label":"...","isOther":true}],"runningPlan":{"title":"...","description":"...","proposedChanges":["specific change"],"acceptanceCriteria":["observable outcome"],"suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"],"suggestedRefinements":["next focus 1","next focus 2"]}}}. For an initial-plan or answer-update turn respond only with {"type":"complete","data":{"title":"...","description":"...","proposedChanges":["specific change"],"acceptanceCriteria":["observable outcome"],"suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"],"suggestedRefinements":["next focus 1","next focus 2"]}}.
Every turn must include the running-plan fields: only title, description, concrete proposedChanges, observable acceptanceCriteria, suggestedSize, optional priority, suggestedDependencies, concrete keyDeliverables, and exactly three concise suggestedRefinements informed by the idea and answers so far. Never use interview question text as a deliverable. Do not put PROMPT.md sections (Mission, Before → After, Steps, File Scope, Review Level, Completion Criteria, or Do NOT) in runningPlan or free text: triage writes PROMPT.md only after Validate. Validate serializes the plan as plan.md without priority or suggestedRefinements; priority remains a task field. Every question must provide at least two alternatives, each with non-empty pros and cons, plus exactly one Other/write-your-own option. Write every label, option, and Other label in the language of the user's original input. Incorporate free-text Other answers verbatim as steering context for the following question.`;
Every turn must include the running-plan fields: only title, description, concrete proposedChanges, observable acceptanceCriteria, suggestedSize, optional priority, suggestedDependencies, concrete keyDeliverables, and concise suggestedRefinements informed by the idea and answers so far. Include every distinct, high-value unresolved refinement area; do not cap the list at three. Never use interview question text as a deliverable. Do not put PROMPT.md sections (Mission, Before → After, Steps, File Scope, Review Level, Completion Criteria, or Do NOT) in runningPlan or free text: triage writes PROMPT.md only after Validate. Validate serializes the plan as plan.md without priority or suggestedRefinements; priority remains a task field. Every question must provide at least two alternatives, each with non-empty pros and cons, plus exactly one Other/write-your-own option. Write every label, option, and Other label in the language of the user's original input. Incorporate free-text Other answers verbatim as steering context for the following question.`;
/*
FNXC:PlanningMode 2026-07-20-14:30:
@@ -515,7 +515,7 @@ export function normalizePlanningSummaryPayload(
priority: isTaskPriority(summary.priority) ? summary.priority : DEFAULT_TASK_PRIORITY,
suggestedDependencies: normalizeStringArray(summary.suggestedDependencies),
keyDeliverables: normalizeStringArray(summary.keyDeliverables),
suggestedRefinements: normalizeStringArray(summary.suggestedRefinements).slice(0, 3),
suggestedRefinements: normalizeStringArray(summary.suggestedRefinements),
};
}
@@ -2161,7 +2161,7 @@ export function formatInitialRunningPlanRequestForAgent(initialPlan: string): st
return [
"Create a concrete initial implementation plan from this operator idea.",
"Inspect the relevant codebase and active-board context before drafting it. Make the description specific about the affected behavior and intended outcome. Provide concrete proposedChanges that name what behavior, component, interface, data, or configuration should change, and acceptanceCriteria stated as observable pass/fail outcomes. Make every key deliverable an actionable work item rather than generic planning advice.",
"Also propose exactly three short suggestedRefinements representing the highest-value unresolved areas the operator could explore next.",
"Also propose concise suggestedRefinements covering every distinct, high-value unresolved area the operator could explore next; do not cap the list at three.",
"Return only type:\"complete\" JSON with title, description, proposedChanges, acceptanceCriteria, suggestedSize, priority, suggestedDependencies, keyDeliverables, and suggestedRefinements. Do not ask a question yet and do not validate the plan; the operator will review it and explicitly choose Refine or Validate.",
"Operator idea:",
initialPlan,
@@ -3146,7 +3146,7 @@ export function formatResponseForAgent(
System prompts can be displaced by long tool/context turns. Repeat the per-answer contract at the invocation boundary
so every submitted answer steers the following high-impact question instead of inviting a model-generated completion.
*/
return `${answerContext}\n\nUpdate only the runningPlan fields (title, description, concrete proposedChanges, observable acceptanceCriteria, suggestedSize, optional priority, suggestedDependencies, concrete keyDeliverables, and three suggestedRefinements) informed by this answer; never list interview questions as deliverables or PROMPT.md sections such as Mission, Steps, File Scope, Review Level, Completion Criteria, or Do NOT. Then ask exactly one new, high-impact question that does not repeat a prior question. Offer alternatives with pros and cons. Do not validate the plan; only the user can validate it.`;
return `${answerContext}\n\nUpdate only the runningPlan fields (title, description, concrete proposedChanges, observable acceptanceCriteria, suggestedSize, optional priority, suggestedDependencies, concrete keyDeliverables, and all distinct high-value suggestedRefinements) informed by this answer; do not cap suggestedRefinements at three. Never list interview questions as deliverables or PROMPT.md sections such as Mission, Steps, File Scope, Review Level, Completion Criteria, or Do NOT. Then ask exactly one new, high-impact question that does not repeat a prior question. Offer alternatives with pros and cons. Do not validate the plan; only the user can validate it.`;
}
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {