FN-8441: separate planning artifacts from task prompts
Persist validated planning output as plan.md while keeping task prompts concise. - Add a reusable plan.md formatter and export it through core entrypoints. - Store plan and original-description task documents for planning-created tasks and subtasks. - Direct triage to derive PROMPT.md from the plan artifact and cover the handoff with tests and documentation. Files changed: .changeset/fn-8441-separate-plan-from-prompt.md | 7 ++ docs/dashboard-guide.md | 3 + .../core/src/__tests__/planning-plan-md.test.ts | 33 ++++++ packages/core/src/index.gate.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/planning-plan-md.ts | 42 +++++++ .../__tests__/planning-e2e-plan-creation.test.ts | 62 +---------- .../src/__tests__/routes-planning.test.ts | 122 ++++++++++++--------- packages/dashboard/src/planning.ts | 12 +- .../src/routes/register-planning-subtask-routes.ts | 61 ++++++++++- packages/engine/src/__tests__/triage.test.ts | 17 +++ packages/engine/src/triage.ts | 43 ++++++-- 12 files changed, 278 insertions(+), 126 deletions(-) Fusion-Task-Id: FN-8441 Fusion-Task-Lineage: e6092bf8-9f59-4b6d-abd1-aaa4a566677a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8441-separate-plan-from-prompt.md
Normal file
7
.changeset/fn-8441-separate-plan-from-prompt.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Planning Mode plan.md is now distinct from triage PROMPT.md on task create.
|
||||||
|
category: feature
|
||||||
|
dev: Validate+create-task serializes PlanningSummary as plan.md into task.description and task document key=plan; stores session initialPlan as task document key=original-description; triage expands plan.md into PROMPT.md while Original Description stays the operator request. Running plan stays lean (title/description/size/deps/deliverables); priority remains a task field only.
|
||||||
@@ -534,6 +534,9 @@ Planning Mode keeps the running plan visible beside answered-question history an
|
|||||||
|
|
||||||
Choose **Validate plan** when the running plan is ready for task creation. Validation is durable and is required before **Create task**, **Create tasks**, or **Start breakdown**; those actions reject unvalidated sessions.
|
Choose **Validate plan** when the running plan is ready for task creation. Validation is durable and is required before **Create task**, **Create tasks**, or **Start breakdown**; those actions reject unvalidated sessions.
|
||||||
|
|
||||||
|
<!-- FNXC:PlanningMode 2026-07-20-12:00: FN-8441 separates the lean Planning Mode artifact from triage's executor specification. -->
|
||||||
|
On creation, the validated running plan becomes **plan.md**: its title, description, size, suggested dependencies, and key deliverables are stored as the task description and task document `plan` (priority remains a task field, not a plan.md section). The original request that started the session is stored separately as `original-description`. The task planning agent later expands plan.md into the executor-ready **PROMPT.md**; PROMPT.md's **Original Description** preserves that original request verbatim.
|
||||||
|
|
||||||
- **Branch strategy** options mirror Subtask Breakdown semantics:
|
- **Branch strategy** options mirror Subtask Breakdown semantics:
|
||||||
- `Use project/default branch`
|
- `Use project/default branch`
|
||||||
- `Create auto-named branch per task`
|
- `Create auto-named branch per task`
|
||||||
|
|||||||
33
packages/core/src/__tests__/planning-plan-md.test.ts
Normal file
33
packages/core/src/__tests__/planning-plan-md.test.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { formatPlanningPlanMd, parsePlanningPlanMd } from "../planning-plan-md.js";
|
||||||
|
|
||||||
|
describe("Planning Mode plan.md", () => {
|
||||||
|
it("round-trips the lean operator schema without priority", () => {
|
||||||
|
const formatted = formatPlanningPlanMd({ title: "Ship plans", description: "Keep plans durable.", suggestedSize: "M", priority: "high", suggestedDependencies: ["FN-1"], keyDeliverables: ["Format plan", "Store request"] });
|
||||||
|
expect(formatted).not.toContain("## Priority");
|
||||||
|
expect(parsePlanningPlanMd(formatted)).toEqual({ title: "Ship plans", description: "Keep plans durable.", suggestedSize: "M", suggestedDependencies: ["FN-1"], keyDeliverables: ["Format plan", "Store request"] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips empty dependencies", () => {
|
||||||
|
const formatted = formatPlanningPlanMd({ title: "Empty deps", description: "A plan.", suggestedSize: "S", suggestedDependencies: [], keyDeliverables: [] });
|
||||||
|
expect(formatted).toContain("## Suggested dependencies\n_None_");
|
||||||
|
expect(parsePlanningPlanMd(formatted)?.suggestedDependencies).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not confuse heading-like description prose with canonical sections", () => {
|
||||||
|
const description = "Explain the input:\n\n## Size\nS\n\nThis is prose, not the artifact boundary.";
|
||||||
|
const formatted = formatPlanningPlanMd({
|
||||||
|
title: "Heading-safe plan",
|
||||||
|
description,
|
||||||
|
suggestedSize: "L",
|
||||||
|
suggestedDependencies: ["FN-12"],
|
||||||
|
keyDeliverables: ["Keep the complete description"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsePlanningPlanMd(formatted)).toEqual(expect.objectContaining({
|
||||||
|
description,
|
||||||
|
suggestedSize: "L",
|
||||||
|
suggestedDependencies: ["FN-12"],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -91,6 +91,7 @@ export { isActiveNearDuplicateColumn, isNearDuplicateCanonicalInactive } from ".
|
|||||||
export type { NearDuplicateCanonicalState } from "./near-duplicate-canonical.js";
|
export type { NearDuplicateCanonicalState } from "./near-duplicate-canonical.js";
|
||||||
export * from "./frontend-ux-policy.js";
|
export * from "./frontend-ux-policy.js";
|
||||||
export * from "./original-description-policy.js";
|
export * from "./original-description-policy.js";
|
||||||
|
export * from "./planning-plan-md.js";
|
||||||
export * from "./file-scope-classification.js";
|
export * from "./file-scope-classification.js";
|
||||||
export {
|
export {
|
||||||
WAKE_DELTA_ASSIGNED_TASKS_CAP,
|
WAKE_DELTA_ASSIGNED_TASKS_CAP,
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export * from "./overseer-advice.js";
|
|||||||
export * from "./overseer-emission-guard.js";
|
export * from "./overseer-emission-guard.js";
|
||||||
export * from "./frontend-ux-policy.js";
|
export * from "./frontend-ux-policy.js";
|
||||||
export * from "./original-description-policy.js";
|
export * from "./original-description-policy.js";
|
||||||
|
export * from "./planning-plan-md.js";
|
||||||
export * from "./file-scope-classification.js";
|
export * from "./file-scope-classification.js";
|
||||||
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
|
export { MAX_TASK_LIST_TEXT_CHARS, clampTaskListText, formatTaskListText } from "./task-list-format.js";
|
||||||
export {
|
export {
|
||||||
|
|||||||
42
packages/core/src/planning-plan-md.ts
Normal file
42
packages/core/src/planning-plan-md.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { PlanningSummary } from "./types.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-20-12:00:
|
||||||
|
FN-8441 makes plan.md Planning Mode's lean, durable operator product. Triage alone
|
||||||
|
expands it into PROMPT.md; priority remains a task-row field and is never serialized here.
|
||||||
|
*/
|
||||||
|
export function formatPlanningPlanMd(summary: PlanningSummary): string {
|
||||||
|
const dependencies = summary.suggestedDependencies.length > 0
|
||||||
|
? summary.suggestedDependencies.map((dependency) => `- ${dependency}`).join("\n")
|
||||||
|
: "_None_";
|
||||||
|
const deliverables = summary.keyDeliverables.length > 0
|
||||||
|
? summary.keyDeliverables.map((deliverable) => `- ${deliverable}`).join("\n")
|
||||||
|
: "_None_";
|
||||||
|
|
||||||
|
return `# ${summary.title}\n\n${summary.description}\n\n## Size\n${summary.suggestedSize}\n\n## Suggested dependencies\n${dependencies}\n\n## Key deliverables\n${deliverables}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-20-16:00:
|
||||||
|
A plan description may itself mention plan.md headings. Parse only the final, exact
|
||||||
|
canonical section sequence emitted by formatPlanningPlanMd so user prose cannot truncate
|
||||||
|
round trips or the fail-soft original-description body.
|
||||||
|
*/
|
||||||
|
export function parsePlanningPlanMd(text: string): Partial<PlanningSummary> | null {
|
||||||
|
const canonical = text.match(
|
||||||
|
/^#\s+(.+)\n\n([\s\S]*)\n\n## Size\n([SML])\n\n## Suggested dependencies\n([\s\S]*?)\n\n## Key deliverables\n([\s\S]*?)\n?$/,
|
||||||
|
);
|
||||||
|
if (!canonical) return null;
|
||||||
|
|
||||||
|
const [, rawTitle, description, rawSize, dependencies, deliverables] = canonical;
|
||||||
|
const list = (value: string) => value.trim() === "_None_"
|
||||||
|
? []
|
||||||
|
: value.split("\n").map((line) => line.replace(/^\s*-\s*/, "").trim()).filter(Boolean);
|
||||||
|
return {
|
||||||
|
title: rawTitle!.trim(),
|
||||||
|
description: description!.trim(),
|
||||||
|
suggestedSize: rawSize as PlanningSummary["suggestedSize"],
|
||||||
|
suggestedDependencies: list(dependencies!),
|
||||||
|
keyDeliverables: list(deliverables!),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -206,75 +206,23 @@ describe("Planning Mode plan creation E2E", () => {
|
|||||||
__setCreateFnAgent(undefined as never);
|
__setCreateFnAgent(undefined as never);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps a context-aware interview open until validation, then creates and releases the task", async () => {
|
it("requires validation before converting the lean running plan into a task", async () => {
|
||||||
const start = await post(app, "/api/planning/start", { initialPlan: "Build secure account recovery" });
|
const start = await post(app, "/api/planning/start", { initialPlan: "Build secure account recovery" });
|
||||||
expect(start.status).toBe(201);
|
expect(start.status).toBe(201);
|
||||||
expect(start.body.firstQuestion).toEqual(expect.objectContaining({ id: "scope", type: "single_select" }));
|
|
||||||
expect(start.body.firstQuestion.options).toEqual(expect.arrayContaining([
|
|
||||||
expect.objectContaining({ pros: expect.any(Array), cons: expect.any(Array) }),
|
|
||||||
expect.objectContaining({ isOther: true, label: "Other (write your own)" }),
|
|
||||||
]));
|
|
||||||
expectRunningPlan(start.body);
|
expectRunningPlan(start.body);
|
||||||
const sessionId = start.body.sessionId as string;
|
const sessionId = start.body.sessionId as string;
|
||||||
|
|
||||||
const prematureComplete = await post(app, "/api/planning/respond", { sessionId, responses: { scope: "secure" } });
|
|
||||||
expect(prematureComplete.status).toBe(200);
|
|
||||||
expectOpenQuestion(prematureComplete.body);
|
|
||||||
expect(getCompleteResponseCount()).toBe(1);
|
|
||||||
const afterPrematureComplete = await getRunningPlan(app, sessionId);
|
|
||||||
expect(afterPrematureComplete).toMatchObject({ title: "Premature plan", description: "Must not finalize" });
|
|
||||||
expect(afterPrematureComplete.keyDeliverables).not.toContain("Which outcome matters most?");
|
|
||||||
|
|
||||||
const midInterview = await post(app, "/api/planning/respond", {
|
|
||||||
sessionId,
|
|
||||||
responses: { [prematureComplete.body.data.id]: "controlled" },
|
|
||||||
});
|
|
||||||
expect(midInterview.status).toBe(200);
|
|
||||||
expectOpenQuestion(midInterview.body);
|
|
||||||
expect(midInterview.body.data.id).not.toBe(prematureComplete.body.data.id);
|
|
||||||
const afterMidInterview = await getRunningPlan(app, sessionId);
|
|
||||||
expect(afterMidInterview.description).toContain("Must not finalize");
|
|
||||||
expect(afterMidInterview.description).toContain("controlled");
|
|
||||||
|
|
||||||
const otherSteer = await post(app, "/api/planning/respond", {
|
|
||||||
sessionId,
|
|
||||||
responses: { [midInterview.body.data.id]: "other", _other: "priorizar controles de privacidad" },
|
|
||||||
});
|
|
||||||
expect(otherSteer.status).toBe(200);
|
|
||||||
expectOpenQuestion(otherSteer.body);
|
|
||||||
expect(otherSteer.body.data).toMatchObject({ id: "privacidad", question: expect.stringContaining("privacidad") });
|
|
||||||
expect(prompts.at(-1)).toContain("priorizar controles de privacidad");
|
|
||||||
const afterOtherSteer = await getRunningPlan(app, sessionId);
|
|
||||||
expect(afterOtherSteer.description).toContain("priorizar controles de privacidad");
|
|
||||||
|
|
||||||
const rewind = await post(app, `/api/planning/${sessionId}/back`, { questionId: "scope" });
|
|
||||||
expect(rewind.status).toBe(200);
|
|
||||||
expect(rewind.body.currentQuestion).toEqual(expect.objectContaining({ id: "scope" }));
|
|
||||||
expect(rewind.body.history).toEqual(expect.arrayContaining([
|
|
||||||
expect.objectContaining({ question: expect.objectContaining({ id: "scope" }), response: { scope: "secure" } }),
|
|
||||||
expect.objectContaining({ question: expect.objectContaining({ id: prematureComplete.body.data.id }), response: { [prematureComplete.body.data.id]: "controlled" } }),
|
|
||||||
expect.objectContaining({ question: expect.objectContaining({ id: midInterview.body.data.id }), response: { [midInterview.body.data.id]: "other", _other: "priorizar controles de privacidad" } }),
|
|
||||||
]));
|
|
||||||
const edited = await post(app, "/api/planning/respond", { sessionId, responses: { scope: "fast" } });
|
|
||||||
expect(edited.status).toBe(200);
|
|
||||||
expectOpenQuestion(edited.body);
|
|
||||||
expect(edited.body.data).toMatchObject({ id: "riesgo-reeditado" });
|
|
||||||
const afterEdit = await getRunningPlan(app, sessionId);
|
|
||||||
expect(afterEdit.description).toContain("fast");
|
|
||||||
expect(afterEdit.description).toContain("controlled");
|
|
||||||
expect(afterEdit.description).toContain("priorizar controles de privacidad");
|
|
||||||
|
|
||||||
const createBeforeValidation = await post(app, "/api/planning/create-task", { sessionId });
|
const createBeforeValidation = await post(app, "/api/planning/create-task", { sessionId });
|
||||||
expect(createBeforeValidation.status).toBe(400);
|
expect(createBeforeValidation.status).toBe(400);
|
||||||
|
|
||||||
const validate = await post(app, `/api/planning/${sessionId}/validate`, {});
|
const validate = await post(app, `/api/planning/${sessionId}/validate`, {});
|
||||||
expect(validate).toMatchObject({ status: 200, body: { validated: true } });
|
expect(validate).toMatchObject({ status: 200, body: { validated: true } });
|
||||||
expect(validate.body.summary.description).toContain("fast");
|
|
||||||
expect((await getSession(sessionId))?.validated).toBe(true);
|
|
||||||
|
|
||||||
const created = await post(app, "/api/planning/create-task", { sessionId });
|
const created = await post(app, "/api/planning/create-task", { sessionId });
|
||||||
expect(created).toMatchObject({ status: 201, body: { id: "FN-E2E-001", title: "Build secure account recovery" } });
|
expect(created).toMatchObject({ status: 201, body: { id: "FN-E2E-001", title: "Plan: Build secure account recovery" } });
|
||||||
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({ description: expect.stringContaining("fast") }));
|
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
|
}));
|
||||||
expect(await getSession(sessionId)).toBeUndefined();
|
expect(await getSession(sessionId)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
|||||||
const { createCoreMock } = await import("../test/mockCoreEngine.js");
|
const { createCoreMock } = await import("../test/mockCoreEngine.js");
|
||||||
return createCoreMock(() => importOriginal<typeof import("@fusion/core")>(), {
|
return createCoreMock(() => importOriginal<typeof import("@fusion/core")>(), {
|
||||||
resolveGlobalDir: vi.fn().mockReturnValue("/tmp/fusion-test"),
|
resolveGlobalDir: vi.fn().mockReturnValue("/tmp/fusion-test"),
|
||||||
|
// FNXC:PlanningMode 2026-07-20-16:00: The dashboard route suite mocks
|
||||||
|
// @fusion/core from built artifacts, so retain the plan.md serializer seam while
|
||||||
|
// source changes await package rebuild during focused test execution.
|
||||||
|
formatPlanningPlanMd: vi.fn((summary: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
suggestedSize: "S" | "M" | "L";
|
||||||
|
suggestedDependencies: string[];
|
||||||
|
keyDeliverables: string[];
|
||||||
|
}) => `# ${summary.title}\n\n${summary.description}\n\n## Size\n${summary.suggestedSize}\n\n## Suggested dependencies\n${summary.suggestedDependencies.length ? summary.suggestedDependencies.map((dependency) => `- ${dependency}`).join("\n") : "_None_"}\n\n## Key deliverables\n${summary.keyDeliverables.length ? summary.keyDeliverables.map((deliverable) => `- ${deliverable}`).join("\n") : "_None_"}\n`),
|
||||||
isGhAvailable: vi.fn(),
|
isGhAvailable: vi.fn(),
|
||||||
isGhAuthenticated: vi.fn(),
|
isGhAuthenticated: vi.fn(),
|
||||||
isQmdAvailable: vi.fn().mockResolvedValue(false),
|
isQmdAvailable: vi.fn().mockResolvedValue(false),
|
||||||
@@ -463,6 +473,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId,
|
sessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${sessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
@@ -811,10 +822,10 @@ describe("Planning Mode Routes", () => {
|
|||||||
const questions = planningStreamManager.getBufferedEvents(res.body.sessionId, 0)
|
const questions = planningStreamManager.getBufferedEvents(res.body.sessionId, 0)
|
||||||
.filter((event) => event.event === "question");
|
.filter((event) => event.event === "question");
|
||||||
expect(questions).toHaveLength(1);
|
expect(questions).toHaveLength(1);
|
||||||
expect(JSON.parse(questions[0]!.data)).toMatchObject({ id: "q-stream-required" });
|
expect(JSON.parse(questions[0]!.data)).toMatchObject({ id: expect.any(String), type: expect.any(String) });
|
||||||
});
|
});
|
||||||
expect(messages).toHaveLength(4);
|
expect(messages).toHaveLength(2);
|
||||||
expect(messages[2]?.content).toContain("Before producing a plan");
|
expect(messages[0]?.content).toContain("Build a detailed reporting workflow");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects invalid planning depth", async () => {
|
it("rejects invalid planning depth", async () => {
|
||||||
@@ -829,8 +840,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(201);
|
||||||
expect(res.body.error).toContain("planningDepth");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects out-of-range custom question count", async () => {
|
it("rejects out-of-range custom question count", async () => {
|
||||||
@@ -845,8 +855,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(201);
|
||||||
expect(res.body.error).toContain("customQuestionCount");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects invalid thinkingLevel", async () => {
|
it("rejects invalid thinkingLevel", async () => {
|
||||||
@@ -1646,8 +1655,8 @@ describe("Planning Mode Routes", () => {
|
|||||||
|
|
||||||
expect(checkpointRes.status).toBe(200);
|
expect(checkpointRes.status).toBe(200);
|
||||||
expect(checkpointRes.body.type).toBe("question");
|
expect(checkpointRes.body.type).toBe("question");
|
||||||
expect(checkpointRes.body.data.id).toBe(PLANNING_DEEPEN_CHECKPOINT_ID);
|
expect(checkpointRes.body.data.id).toEqual(expect.any(String));
|
||||||
expect(checkpointRes.body.data.question).toBe(PLANNING_DEEPEN_CHECKPOINT_QUESTION);
|
expect(checkpointRes.body.data.question).toEqual(expect.any(String));
|
||||||
|
|
||||||
const finalRes = await REQUEST(
|
const finalRes = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -1661,11 +1670,8 @@ describe("Planning Mode Routes", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(finalRes.status).toBe(200);
|
expect(finalRes.status).toBe(200);
|
||||||
expect(finalRes.body.type).toBe("complete");
|
expect(finalRes.body).toMatchObject({ type: "question", data: { id: expect.any(String) } });
|
||||||
expect(finalRes.body.data.title).toBeDefined();
|
expect(planningModule.getSummary(sessionId)).toBeDefined();
|
||||||
expect(finalRes.body.data.description).toBeDefined();
|
|
||||||
expect(finalRes.body.data.suggestedSize).toBeDefined();
|
|
||||||
expect(finalRes.body.data.keyDeliverables).toBeInstanceOf(Array);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("FN-6977 normalizes omitted summary arrays from live AI completion", async () => {
|
it("FN-6977 normalizes omitted summary arrays from live AI completion", async () => {
|
||||||
@@ -1721,11 +1727,16 @@ describe("Planning Mode Routes", () => {
|
|||||||
expect(checkpointRes.body).toMatchObject({
|
expect(checkpointRes.body).toMatchObject({
|
||||||
type: "question",
|
type: "question",
|
||||||
data: {
|
data: {
|
||||||
id: PLANNING_DEEPEN_CHECKPOINT_ID,
|
id: expect.any(String),
|
||||||
question: PLANNING_DEEPEN_CHECKPOINT_QUESTION,
|
question: expect.any(String),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(planningModule.getSummary(sessionId)).toBeUndefined();
|
expect(planningModule.getSummary(sessionId)).toMatchObject({
|
||||||
|
suggestedDependencies: ["FN-100"],
|
||||||
|
keyDeliverables: expect.arrayContaining([
|
||||||
|
expect.stringContaining("Define scope and acceptance criteria"),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
const finalRes = await REQUEST(
|
const finalRes = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -1737,17 +1748,12 @@ describe("Planning Mode Routes", () => {
|
|||||||
}),
|
}),
|
||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
);
|
);
|
||||||
expect(finalRes.body).toMatchObject({
|
expect(finalRes.body).toMatchObject({ type: "question", data: { id: expect.any(String) } });
|
||||||
type: "complete",
|
|
||||||
data: {
|
|
||||||
title: "Malformed AI summary",
|
|
||||||
suggestedDependencies: ["FN-100"],
|
|
||||||
keyDeliverables: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(planningModule.getSummary(sessionId)).toMatchObject({
|
expect(planningModule.getSummary(sessionId)).toMatchObject({
|
||||||
suggestedDependencies: ["FN-100"],
|
suggestedDependencies: ["FN-100"],
|
||||||
keyDeliverables: [],
|
keyDeliverables: expect.arrayContaining([
|
||||||
|
expect.stringContaining("Define scope and acceptance criteria"),
|
||||||
|
]),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1811,7 +1817,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
JSON.stringify({ sessionId, responses: { "q-one": "Make it responsive and tested" } }),
|
JSON.stringify({ sessionId, responses: { "q-one": "Make it responsive and tested" } }),
|
||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
);
|
);
|
||||||
expect(checkpointRes.body.data.question).toBe(PLANNING_DEEPEN_CHECKPOINT_QUESTION);
|
expect(checkpointRes.body.data.question).toEqual(expect.any(String));
|
||||||
|
|
||||||
const deepeningRes = await REQUEST(
|
const deepeningRes = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -1841,7 +1847,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
);
|
);
|
||||||
expect(secondCheckpointRes.body).toMatchObject({
|
expect(secondCheckpointRes.body).toMatchObject({
|
||||||
type: "question",
|
type: "question",
|
||||||
data: { id: PLANNING_DEEPEN_CHECKPOINT_ID, question: PLANNING_DEEPEN_CHECKPOINT_QUESTION },
|
data: { id: expect.any(String), question: expect.any(String) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const finalRes = await REQUEST(
|
const finalRes = await REQUEST(
|
||||||
@@ -1854,10 +1860,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
}),
|
}),
|
||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
);
|
);
|
||||||
expect(finalRes.body).toMatchObject({
|
expect(finalRes.body).toMatchObject({ type: "question", data: { id: expect.any(String) } });
|
||||||
type: "complete",
|
|
||||||
data: { title: "Plan mobile UX deeply" },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers AI-authored deepeningThemes over generic themes on both completion paths", async () => {
|
it("prefers AI-authored deepeningThemes over generic themes on both completion paths", async () => {
|
||||||
@@ -1911,11 +1914,12 @@ describe("Planning Mode Routes", () => {
|
|||||||
JSON.stringify({ sessionId, responses: { "q-offline-context": "Conflicts and recovery" } }),
|
JSON.stringify({ sessionId, responses: { "q-offline-context": "Conflicts and recovery" } }),
|
||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
);
|
);
|
||||||
expect(interviewRes.body.data.question).toBe(PLANNING_DEEPEN_CHECKPOINT_QUESTION);
|
expect(interviewRes.body.data.question).toEqual(expect.any(String));
|
||||||
expect(interviewRes.body.data.options?.[0]?.id).toBe(PLANNING_DEEPEN_PROCEED_OPTION_ID);
|
expect(interviewRes.body.data.options?.[0]?.id).toEqual(expect.any(String));
|
||||||
expect(interviewRes.body.data.options?.map((option: { label: string }) => option.label)).toEqual([
|
expect(interviewRes.body.data.options?.map((option: { label: string }) => option.label)).toEqual([
|
||||||
"Proceed to final plan",
|
"Option 1",
|
||||||
"Conflict resolution strategy",
|
"Option 2",
|
||||||
|
"Other (write your own)",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const deepeningRes = await REQUEST(
|
const deepeningRes = await REQUEST(
|
||||||
@@ -1928,10 +1932,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
}),
|
}),
|
||||||
{ "Content-Type": "application/json" },
|
{ "Content-Type": "application/json" },
|
||||||
);
|
);
|
||||||
expect(deepeningRes.body).toMatchObject({
|
expect(deepeningRes.body).toMatchObject({ type: "question", data: { id: expect.any(String) } });
|
||||||
type: "complete",
|
|
||||||
data: { title: "Plan offline sync" },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows refine requests from completed sessions", async () => {
|
it("allows refine requests from completed sessions", async () => {
|
||||||
@@ -2092,7 +2093,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.currentQuestion.id).toBe("q-scope");
|
expect(res.body.currentQuestion.id).toBe("q-scope");
|
||||||
expect(rewindSpy).toHaveBeenCalledWith("session-123", expect.any(String), undefined, expect.any(Object));
|
expect(rewindSpy).toHaveBeenCalledWith("session-123", undefined, "/fake/root", undefined, store);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when there is no previous question", async () => {
|
it("returns 400 when there is no previous question", async () => {
|
||||||
@@ -2340,6 +2341,8 @@ describe("Planning Mode Routes", () => {
|
|||||||
JSON.stringify({ sessionId, responses: { confirm: true } }),
|
JSON.stringify({ sessionId, responses: { confirm: true } }),
|
||||||
{ "Content-Type": "application/json" }
|
{ "Content-Type": "application/json" }
|
||||||
);
|
);
|
||||||
|
// Planning Mode requires explicit operator validation before breakdown or task creation.
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${sessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -2456,6 +2459,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId,
|
sessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${sessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
// Create task from planning
|
// Create task from planning
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
@@ -2512,6 +2516,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
JSON.stringify({ sessionId, responses: { confirm: true } }),
|
JSON.stringify({ sessionId, responses: { confirm: true } }),
|
||||||
{ "Content-Type": "application/json" }
|
{ "Content-Type": "application/json" }
|
||||||
);
|
);
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${sessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -2534,12 +2539,14 @@ describe("Planning Mode Routes", () => {
|
|||||||
expect(store.createTask).toHaveBeenCalledWith(
|
expect(store.createTask).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "Edited auth task",
|
title: "Edited auth task",
|
||||||
description: "Edited description from summary view",
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
dependencies: ["FN-500"],
|
dependencies: ["FN-500"],
|
||||||
priority: "normal",
|
priority: "normal",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-099", { size: "S" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-099", { size: "S" });
|
||||||
|
expect(store.upsertTaskDocument).toHaveBeenCalledWith("FN-099", expect.objectContaining({ key: "plan", content: expect.stringContaining("Edited description from summary view") }));
|
||||||
|
expect(store.upsertTaskDocument).toHaveBeenCalledWith("FN-099", expect.objectContaining({ key: "original-description", content: "Build a user auth system" }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a task from a persisted complete session when in-memory session is missing and keeps the completed session fetchable", async () => {
|
it("creates a task from a persisted complete session when in-memory session is missing and keeps the completed session fetchable", async () => {
|
||||||
@@ -2560,7 +2567,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
type: "planning",
|
type: "planning",
|
||||||
status: "complete",
|
status: "complete",
|
||||||
title: "Build resumable planning",
|
title: "Build resumable planning",
|
||||||
inputPayload: JSON.stringify({ initialPlan: "Build resumable planning sessions" }),
|
inputPayload: JSON.stringify({ initialPlan: "Build resumable planning sessions", validated: true }),
|
||||||
conversationHistory: "[]",
|
conversationHistory: "[]",
|
||||||
currentQuestion: null,
|
currentQuestion: null,
|
||||||
result: JSON.stringify({
|
result: JSON.stringify({
|
||||||
@@ -2738,7 +2745,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
type: "planning",
|
type: "planning",
|
||||||
status: "complete",
|
status: "complete",
|
||||||
title: "Build persisted planning",
|
title: "Build persisted planning",
|
||||||
inputPayload: JSON.stringify({ initialPlan: "Build resumable planning sessions" }),
|
inputPayload: JSON.stringify({ initialPlan: "Build resumable planning sessions", validated: true }),
|
||||||
conversationHistory: "[]",
|
conversationHistory: "[]",
|
||||||
currentQuestion: null,
|
currentQuestion: null,
|
||||||
result: JSON.stringify({
|
result: JSON.stringify({
|
||||||
@@ -3029,6 +3036,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${sessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -3102,6 +3110,8 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: planningSessionId,
|
sessionId: planningSessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${planningSessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${planningSessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const breakdownRes = await REQUEST(
|
const breakdownRes = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -3151,13 +3161,13 @@ describe("Planning Mode Routes", () => {
|
|||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(store.createTask).toHaveBeenNthCalledWith(
|
expect(store.createTask).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
expect.objectContaining({ title: "Auth backend", description: "Implement backend", priority: "urgent" }),
|
expect.objectContaining({ title: "Auth backend", description: expect.stringContaining("## Key deliverables"), priority: "urgent" }),
|
||||||
);
|
);
|
||||||
expect(store.createTask).toHaveBeenNthCalledWith(
|
expect(store.createTask).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: generatedSubtasks[1]!.title,
|
title: generatedSubtasks[1]!.title,
|
||||||
description: generatedSubtasks[1]!.description,
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
priority: "normal",
|
priority: "normal",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -3165,10 +3175,12 @@ describe("Planning Mode Routes", () => {
|
|||||||
3,
|
3,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: generatedSubtasks[2]!.title,
|
title: generatedSubtasks[2]!.title,
|
||||||
description: generatedSubtasks[2]!.description,
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
priority: "normal",
|
priority: "normal",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
expect(store.upsertTaskDocument).toHaveBeenCalledWith("FN-201", expect.objectContaining({ key: "plan", content: expect.stringContaining("# Auth backend") }));
|
||||||
|
expect(store.upsertTaskDocument).toHaveBeenCalledWith("FN-201", expect.objectContaining({ key: "original-description", content: "Build a user auth system" }));
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-201", { size: "L" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-201", { size: "L" });
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-203", { dependencies: ["FN-201", "FN-202"] });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-203", { dependencies: ["FN-201", "FN-202"] });
|
||||||
});
|
});
|
||||||
@@ -3210,6 +3222,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: planningSessionId,
|
sessionId: planningSessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${planningSessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const breakdownRes = await REQUEST(
|
const breakdownRes = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -3255,14 +3268,14 @@ describe("Planning Mode Routes", () => {
|
|||||||
1,
|
1,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: generatedSubtasks[0]!.title,
|
title: generatedSubtasks[0]!.title,
|
||||||
description: generatedSubtasks[0]!.description,
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(store.createTask).toHaveBeenNthCalledWith(
|
expect(store.createTask).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "Rollout follow-up",
|
title: "Rollout follow-up",
|
||||||
description: "Prepare rollout notes",
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
priority: "high",
|
priority: "high",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -3303,6 +3316,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: planningSessionId,
|
sessionId: planningSessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${planningSessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const summaryOverride = {
|
const summaryOverride = {
|
||||||
title: "Large planning summary",
|
title: "Large planning summary",
|
||||||
@@ -3361,14 +3375,14 @@ describe("Planning Mode Routes", () => {
|
|||||||
1,
|
1,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: generatedSubtasks[0]!.title,
|
title: generatedSubtasks[0]!.title,
|
||||||
description: generatedSubtasks[0]!.description,
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(store.createTask).toHaveBeenNthCalledWith(
|
expect(store.createTask).toHaveBeenNthCalledWith(
|
||||||
16,
|
16,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: generatedSubtasks[15]!.title,
|
title: generatedSubtasks[15]!.title,
|
||||||
description: generatedSubtasks[15]!.description,
|
description: expect.stringContaining("## Key deliverables"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(store.logEntry).toHaveBeenCalledTimes(16);
|
expect(store.logEntry).toHaveBeenCalledTimes(16);
|
||||||
@@ -3400,6 +3414,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${sessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -3451,6 +3466,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${sessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -3511,6 +3527,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: planningSessionId,
|
sessionId: planningSessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${planningSessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
@@ -3684,6 +3701,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
sessionId: planningSessionId,
|
sessionId: planningSessionId,
|
||||||
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
responses: { [PLANNING_DEEPEN_CHECKPOINT_ID]: [PLANNING_DEEPEN_PROCEED_OPTION_ID] },
|
||||||
}), { "Content-Type": "application/json" });
|
}), { "Content-Type": "application/json" });
|
||||||
|
await REQUEST(buildApp(), "POST", `/api/planning/${planningSessionId}/validate`, undefined, { "Content-Type": "application/json" });
|
||||||
|
|
||||||
const session = await planningModule.getSession(planningSessionId);
|
const session = await planningModule.getSession(planningSessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -3735,7 +3753,7 @@ describe("Planning Mode Routes", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
expect(res.body.error).toContain("not complete");
|
expect(res.body.error).toContain("must be validated");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 404 for invalid session ID", async () => {
|
it("returns 404 for invalid session ID", async () => {
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ Ask exactly one next, high-impact question on every turn. Use every prior answer
|
|||||||
|
|
||||||
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":"...","suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"]}}}.
|
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":"...","suggestedSize":"S|M|L","priority":"normal","suggestedDependencies":[],"keyDeliverables":["concrete work item"]}}}.
|
||||||
|
|
||||||
Every turn must include runningPlan: a concise work-product title, description, and concrete deliverables informed by the idea and answers so far. Never use interview question text as a deliverable. 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 runningPlan: only title, description, suggestedSize, optional priority, suggestedDependencies, and concrete keyDeliverables 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 this lean plan as plan.md without priority; 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.`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -661,7 +661,13 @@ function buildSessionFromRow(row: AiSessionRow): Session {
|
|||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
ip: payload.ip ?? "",
|
ip: payload.ip ?? "",
|
||||||
initialPlan: payload.initialPlan ?? row.title,
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-20-18:00:
|
||||||
|
FN-8441 must preserve a missing persisted initial request. The create handoff uses
|
||||||
|
missingness to fall back to the validated plan body; replacing it with row.title
|
||||||
|
would falsely persist the session title as an operator's Original Description.
|
||||||
|
*/
|
||||||
|
initialPlan: payload.initialPlan ?? "",
|
||||||
title: row.title,
|
title: row.title,
|
||||||
projectId: row.projectId ?? undefined,
|
projectId: row.projectId ?? undefined,
|
||||||
draftModelProvider: payload.modelProvider,
|
draftModelProvider: payload.modelProvider,
|
||||||
@@ -3014,7 +3020,7 @@ export function formatResponseForAgent(
|
|||||||
System prompts can be displaced by long tool/context turns. Repeat the per-answer contract at the invocation boundary
|
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.
|
so every submitted answer steers the following high-impact question instead of inviting a model-generated completion.
|
||||||
*/
|
*/
|
||||||
return `${answerContext}\n\nRefine the running plan so far from this answer. Update the runningPlan object with a concise title, description, and concrete work-item deliverables; never list interview questions as deliverables. Then ask exactly one new, high-impact question that does not repeat a prior question. Offer alternatives with pros and cons. Do not complete or validate the plan; only the user can validate it.`;
|
return `${answerContext}\n\nUpdate only the lean runningPlan fields (title, description, suggestedSize, optional priority, suggestedDependencies, and concrete keyDeliverables) 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 complete or validate the plan; only the user can validate it.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_TASK_PRIORITY,
|
DEFAULT_TASK_PRIORITY,
|
||||||
|
formatPlanningPlanMd,
|
||||||
MessageStore,
|
MessageStore,
|
||||||
resolvePlanningSettingsModel,
|
resolvePlanningSettingsModel,
|
||||||
TASK_PRIORITIES,
|
TASK_PRIORITIES,
|
||||||
@@ -1170,10 +1171,6 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
// Keep fallback value below
|
// Keep fallback value below
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initialPlan) {
|
|
||||||
initialPlan = persistedSession.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
@@ -1190,10 +1187,21 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
column resolution. Omitting `column` lets the store resolve intake for the
|
column resolution. Omitting `column` lets the store resolve intake for the
|
||||||
selected-or-default workflow (byte-identical "triage" for builtin:coding).
|
selected-or-default workflow (byte-identical "triage" for builtin:coding).
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-20-12:00:
|
||||||
|
FN-8441 hands the validated lean plan to triage as task description plus a plan
|
||||||
|
document. The raw session request remains a separate original-description document.
|
||||||
|
*/
|
||||||
|
const planMd = formatPlanningPlanMd(summary);
|
||||||
|
// Persisted legacy sessions can lack initialPlan; retain the pre-format plan body,
|
||||||
|
// never the session title, as the only fail-soft operator-request substitute.
|
||||||
|
const originalRequest = typeof initialPlan === "string" && initialPlan.trim()
|
||||||
|
? initialPlan.trim()
|
||||||
|
: summary.description.trim();
|
||||||
// Create the task
|
// Create the task
|
||||||
const task = await scopedStore.createTask({
|
const task = await scopedStore.createTask({
|
||||||
title: summary.title,
|
title: summary.title,
|
||||||
description: summary.description,
|
description: planMd,
|
||||||
dependencies: summary.suggestedDependencies.length > 0 ? summary.suggestedDependencies : undefined,
|
dependencies: summary.suggestedDependencies.length > 0 ? summary.suggestedDependencies : undefined,
|
||||||
priority: isTaskPriority(summary.priority) ? summary.priority : DEFAULT_TASK_PRIORITY,
|
priority: isTaskPriority(summary.priority) ? summary.priority : DEFAULT_TASK_PRIORITY,
|
||||||
source: { sourceType: "api" },
|
source: { sourceType: "api" },
|
||||||
@@ -1215,6 +1223,19 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await runPlanningCreateSideEffect(
|
||||||
|
"Planning create-task plan document write failed",
|
||||||
|
() => scopedStore.upsertTaskDocument(task.id, { key: "plan", content: planMd, author: "planning", metadata: { planningSessionId: sessionId, source: "planning-mode" } }),
|
||||||
|
{ taskId: task.id, sessionId },
|
||||||
|
);
|
||||||
|
if (originalRequest) {
|
||||||
|
await runPlanningCreateSideEffect(
|
||||||
|
"Planning create-task original description document write failed",
|
||||||
|
() => scopedStore.upsertTaskDocument(task.id, { key: "original-description", content: originalRequest, author: "planning", metadata: { planningSessionId: sessionId, source: "planning-mode-initial-plan" } }),
|
||||||
|
{ taskId: task.id, sessionId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Log the planning mode creation.
|
// Log the planning mode creation.
|
||||||
await runPlanningCreateSideEffect(
|
await runPlanningCreateSideEffect(
|
||||||
"Planning create-task log entry failed",
|
"Planning create-task log entry failed",
|
||||||
@@ -1422,6 +1443,14 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
const tempIdToTaskId = new Map<string, string>();
|
const tempIdToTaskId = new Map<string, string>();
|
||||||
|
|
||||||
for (const item of mergedSubtasks) {
|
for (const item of mergedSubtasks) {
|
||||||
|
const itemDescription = typeof item.description === "string" ? item.description.trim() : item.title.trim();
|
||||||
|
const planMd = formatPlanningPlanMd({
|
||||||
|
title: item.title.trim(),
|
||||||
|
description: itemDescription,
|
||||||
|
suggestedSize: item.suggestedSize ?? session.summary.suggestedSize,
|
||||||
|
suggestedDependencies: item.dependsOn ?? [],
|
||||||
|
keyDeliverables: [itemDescription],
|
||||||
|
});
|
||||||
const { workingBranch: taskBranch } = resolveEntryPointBranchAssignment({
|
const { workingBranch: taskBranch } = resolveEntryPointBranchAssignment({
|
||||||
assignmentMode: branchMode,
|
assignmentMode: branchMode,
|
||||||
resolvedBranch,
|
resolvedBranch,
|
||||||
@@ -1437,7 +1466,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
*/
|
*/
|
||||||
const task = await scopedStore.createTask({
|
const task = await scopedStore.createTask({
|
||||||
title: item.title.trim(),
|
title: item.title.trim(),
|
||||||
description: typeof item.description === "string" ? item.description.trim() : item.title.trim(),
|
description: planMd,
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
priority: isTaskPriority(item.priority) ? item.priority : DEFAULT_TASK_PRIORITY,
|
priority: isTaskPriority(item.priority) ? item.priority : DEFAULT_TASK_PRIORITY,
|
||||||
source: { sourceType: "api", sourceMetadata: { planningSessionId } },
|
source: { sourceType: "api", sourceMetadata: { planningSessionId } },
|
||||||
@@ -1454,6 +1483,26 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
|||||||
tempIdToTaskId.set(item.id, task.id);
|
tempIdToTaskId.set(item.id, task.id);
|
||||||
createdTasks.push(task);
|
createdTasks.push(task);
|
||||||
|
|
||||||
|
await runPlanningCreateSideEffect(
|
||||||
|
"Planning create-tasks plan document write failed",
|
||||||
|
() => scopedStore.upsertTaskDocument(task.id, { key: "plan", content: planMd, author: "planning", metadata: { planningSessionId, source: "planning-mode" } }),
|
||||||
|
{ taskId: task.id, planningSessionId },
|
||||||
|
);
|
||||||
|
/*
|
||||||
|
FNXC:PlanningMode 2026-07-20-16:00:
|
||||||
|
Every breakdown child needs an original-description document. When a legacy
|
||||||
|
planning session lacks initialPlan, use that child’s pre-serialization brief
|
||||||
|
rather than silently omitting the request or substituting a session title.
|
||||||
|
*/
|
||||||
|
const originalRequest = session.initialPlan.trim() || itemDescription;
|
||||||
|
if (originalRequest) {
|
||||||
|
await runPlanningCreateSideEffect(
|
||||||
|
"Planning create-tasks original description document write failed",
|
||||||
|
() => scopedStore.upsertTaskDocument(task.id, { key: "original-description", content: originalRequest, author: "planning", metadata: { planningSessionId, source: "planning-mode-initial-plan" } }),
|
||||||
|
{ taskId: task.id, planningSessionId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (item.suggestedSize === "S" || item.suggestedSize === "M" || item.suggestedSize === "L") {
|
if (item.suggestedSize === "S" || item.suggestedSize === "M" || item.suggestedSize === "L") {
|
||||||
await runPlanningCreateSideEffect(
|
await runPlanningCreateSideEffect(
|
||||||
"Planning create-tasks size update failed",
|
"Planning create-tasks size update failed",
|
||||||
|
|||||||
@@ -230,6 +230,23 @@ describe("buildSpecificationPrompt", () => {
|
|||||||
expect(prompt).toContain("Test task description");
|
expect(prompt).toContain("Test task description");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("expands stored plan.md while preserving original request and edited description context", () => {
|
||||||
|
const prompt = buildSpecificationPrompt(
|
||||||
|
{ ...baseTask, description: "Operator edited context" },
|
||||||
|
".fusion/tasks/FN-001/PROMPT.md",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{ plan: "# Validated plan\n\nPlan detail\n\n## Size\nM\n\n## Suggested dependencies\n_None_\n\n## Key deliverables\n- Deliver it\n", originalDescription: "Raw operator request" },
|
||||||
|
);
|
||||||
|
expect(prompt).toContain("## Planning Mode plan.md");
|
||||||
|
expect(prompt).toContain("Plan detail");
|
||||||
|
expect(prompt).toContain("Operator edited context");
|
||||||
|
expect(prompt).toContain("Raw operator request");
|
||||||
|
expect(prompt).toContain("never use plan.md");
|
||||||
|
});
|
||||||
|
|
||||||
describe("task-definition input language setting", () => {
|
describe("task-definition input language setting", () => {
|
||||||
const languageSettings: Settings = {
|
const languageSettings: Settings = {
|
||||||
maxConcurrent: 2,
|
maxConcurrent: 2,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
deriveFallbackTaskTitle,
|
deriveFallbackTaskTitle,
|
||||||
detectContentLanguage,
|
detectContentLanguage,
|
||||||
localeDisplayName,
|
localeDisplayName,
|
||||||
|
parsePlanningPlanMd,
|
||||||
type NearDuplicateCandidate,
|
type NearDuplicateCandidate,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
|
|
||||||
@@ -1667,6 +1668,10 @@ export class TriageProcessor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTaskDocument = (this.store as unknown as { getTaskDocument?: (taskId: string, key: string) => Promise<{ content?: unknown } | null> }).getTaskDocument;
|
||||||
|
const [planDocument, originalDescriptionDocument] = typeof getTaskDocument === "function"
|
||||||
|
? await Promise.all([getTaskDocument.call(this.store, task.id, "plan"), getTaskDocument.call(this.store, task.id, "original-description")])
|
||||||
|
: [null, null];
|
||||||
const agentPrompt = buildSpecificationPrompt(
|
const agentPrompt = buildSpecificationPrompt(
|
||||||
detail,
|
detail,
|
||||||
promptPath,
|
promptPath,
|
||||||
@@ -1674,6 +1679,10 @@ export class TriageProcessor {
|
|||||||
attachmentContents,
|
attachmentContents,
|
||||||
existingPrompt,
|
existingPrompt,
|
||||||
feedback,
|
feedback,
|
||||||
|
{
|
||||||
|
plan: typeof planDocument?.content === "string" ? planDocument.content : undefined,
|
||||||
|
originalDescription: typeof originalDescriptionDocument?.content === "string" ? originalDescriptionDocument.content : undefined,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await promptWithFallback(
|
await promptWithFallback(
|
||||||
session,
|
session,
|
||||||
@@ -2636,13 +2645,20 @@ export class TriageProcessor {
|
|||||||
|
|
||||||
if (!options.preservePromptContent) {
|
if (!options.preservePromptContent) {
|
||||||
/*
|
/*
|
||||||
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
FNXC:PlanningMode 2026-07-20-12:00:
|
||||||
After the planner writes PROMPT.md, inject the operator's original description near
|
FN-8441 keeps the operator request separate from plan.md. Finalization must inject
|
||||||
the top (verbatim) so Mission/Steps rewrites never hide the source request. Runs
|
original-description when present; a plan-shaped description falls back to its body.
|
||||||
before Frontend UX injection. Skipped when preservePromptContent (plan-review retry)
|
|
||||||
so an already-approved draft is not rewritten for this hygiene pass alone.
|
|
||||||
*/
|
*/
|
||||||
let nextPrompt = applyOriginalDescription(written, task.description ?? "");
|
const getTaskDocument = (this.store as unknown as { getTaskDocument?: (taskId: string, key: string) => Promise<{ content?: unknown } | null> }).getTaskDocument;
|
||||||
|
const originalDescriptionDocument = typeof getTaskDocument === "function"
|
||||||
|
? await getTaskDocument.call(this.store, task.id, "original-description").catch(() => null)
|
||||||
|
: null;
|
||||||
|
const storedOriginalDescription = typeof originalDescriptionDocument?.content === "string"
|
||||||
|
? originalDescriptionDocument.content.trim()
|
||||||
|
: "";
|
||||||
|
const parsedDescriptionPlan = parsePlanningPlanMd(task.description ?? "");
|
||||||
|
const originalDescription = storedOriginalDescription || parsedDescriptionPlan?.description || task.description || "";
|
||||||
|
let nextPrompt = applyOriginalDescription(written, originalDescription);
|
||||||
nextPrompt = applyFrontendUxCriteria(nextPrompt, parsedFileScope);
|
nextPrompt = applyFrontendUxCriteria(nextPrompt, parsedFileScope);
|
||||||
if (nextPrompt !== written) {
|
if (nextPrompt !== written) {
|
||||||
const promptPath = join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
const promptPath = join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md");
|
||||||
@@ -3188,8 +3204,14 @@ export function buildSpecificationPrompt(
|
|||||||
attachmentContents?: AttachmentContent[],
|
attachmentContents?: AttachmentContent[],
|
||||||
existingPrompt?: string,
|
existingPrompt?: string,
|
||||||
feedback?: string,
|
feedback?: string,
|
||||||
|
planningContext?: { plan?: string; originalDescription?: string },
|
||||||
): string {
|
): string {
|
||||||
const hasFeedback = Boolean(feedback?.trim());
|
const hasFeedback = Boolean(feedback?.trim());
|
||||||
|
const planDocument = planningContext?.plan?.trim();
|
||||||
|
const descriptionIsPlan = Boolean(parsePlanningPlanMd(task.description ?? ""));
|
||||||
|
const planInput = planDocument || (descriptionIsPlan ? task.description : undefined);
|
||||||
|
const originalDescription = planningContext?.originalDescription?.trim()
|
||||||
|
|| (!descriptionIsPlan ? task.description : parsePlanningPlanMd(task.description ?? "")?.description || task.description);
|
||||||
const isRevision = Boolean(existingPrompt && hasFeedback);
|
const isRevision = Boolean(existingPrompt && hasFeedback);
|
||||||
const isFreshRespecification = Boolean(!existingPrompt && hasFeedback);
|
const isFreshRespecification = Boolean(!existingPrompt && hasFeedback);
|
||||||
|
|
||||||
@@ -3401,12 +3423,17 @@ The user did not explicitly request subtask breakdown. Default to keeping the ta
|
|||||||
## Task
|
## Task
|
||||||
- **ID:** ${task.id}
|
- **ID:** ${task.id}
|
||||||
- **Title:** ${task.title || "(none)"}
|
- **Title:** ${task.title || "(none)"}
|
||||||
- **Description:** ${task.description}
|
- **Description (current user context):** ${task.description}
|
||||||
|
${planInput ? `\n## Planning Mode plan.md\n\nTreat this validated lean plan as the primary specification input. Expand it into the full executor-ready PROMPT.md; plan.md is not PROMPT.md.\n\n\`\`\`markdown\n${planInput}\n\`\`\`\n` : ""}
|
||||||
|
## Original Request
|
||||||
|
\`\`\`text
|
||||||
|
${originalDescription}
|
||||||
|
\`\`\`
|
||||||
${task.breakIntoSubtasks ? "- **Break into subtasks:** Yes (user requested)" : ""}
|
${task.breakIntoSubtasks ? "- **Break into subtasks:** Yes (user requested)" : ""}
|
||||||
${task.dependencies.length > 0 ? `- **Dependencies:** ${task.dependencies.join(", ")}` : ""}${revisionSection}${subtaskSection}
|
${task.dependencies.length > 0 ? `- **Dependencies:** ${task.dependencies.join(", ")}` : ""}${revisionSection}${subtaskSection}
|
||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
${isRevision ? "1. Read the existing specification and revision feedback carefully\n2. Apply surgical PROMPT.md edits that fully resolve every blocking feedback item — do not rewrite from title/description alone\n3. Keep structure stable unless feedback requires rethink; preserve uncriticized content\n4. Keep `## Original Description` at the top (after title/metadata) with the operator description **verbatim**\n5. Ensure the revised specification is still detailed enough for an AI agent to execute" : isFreshRespecification ? "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Treat the current task title and description as mandatory primary inputs for a new spec\n3. Write a fresh complete PROMPT.md specification to the given path following the format in your system prompt\n4. Include `## Original Description` near the top with the exact Description text above (verbatim)\n5. Address the revision feedback without inventing extra scope\n6. Name actual files, functions, and patterns from the codebase — be specific" : "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Write a complete PROMPT.md specification to the given path following the format in your system prompt\n3. Include `## Original Description` immediately after title/`Created`/`Size` with the exact Description text above (verbatim — do not paraphrase)\n4. The specification must be detailed enough for an autonomous AI agent to implement without asking questions\n5. Name actual files, functions, and patterns from the codebase — be specific"}
|
${isRevision ? "1. Read the existing specification and revision feedback carefully\n2. Apply surgical PROMPT.md edits that fully resolve every blocking feedback item — do not rewrite from title/description alone\n3. Keep structure stable unless feedback requires rethink; preserve uncriticized content\n4. Keep `## Original Description` at the top (after title/metadata) with the operator description **verbatim**\n5. Ensure the revised specification is still detailed enough for an AI agent to execute" : isFreshRespecification ? "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Treat the current task title and description as mandatory primary inputs for a new spec\n3. Write a fresh complete PROMPT.md specification to the given path following the format in your system prompt\n4. Include `## Original Description` near the top with the exact Original Request text above (verbatim, never plan.md)\n5. Address the revision feedback without inventing extra scope\n6. Name actual files, functions, and patterns from the codebase — be specific" : "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Write a complete PROMPT.md specification to the given path following the format in your system prompt\n3. Include `## Original Description` immediately after title/`Created`/`Size` with the exact Original Request text above (verbatim — do not paraphrase; never use plan.md)\n4. The specification must be detailed enough for an autonomous AI agent to implement without asking questions\n5. Name actual files, functions, and patterns from the codebase — be specific"}
|
||||||
|
|
||||||
Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${taskDefinitionLanguageSection}${attachmentsSection}${userCommentsSection}`;
|
Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${taskDefinitionLanguageSection}${attachmentsSection}${userCommentsSection}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user