feat: preserve original description at top of generated PROMPT.md (#2129)
## Summary Generated PROMPT.md (after triage/planning — not the bootstrap stub) now keeps the operator's original task description near the top under `## Original Description`, so executors always see the source request even after Mission/Steps rewrites. - **AI-planned path:** planning templates (standard/fast/concise) require a verbatim `## Original Description` section; `buildSpecificationPrompt` instructs the planner; `finalizeApprovedTask` deterministically injects/rewrites it as hygiene. - **Non-AI path:** `generateSpecifiedPrompt` uses the same pure helper so direct creates into non-intake columns get the same contract. - **Description edits:** real specs keep `## Original Description` in sync when `task.description` changes. - **Unchanged:** bootstrap stubs and `isUnplannedSeedPrompt` equality detection. ## Surfaces | Surface | Change | |--------|--------| | `original-description-policy.ts` | Shared inject/rewrite helper | | `agent-prompts.ts` | Template + requirement text | | `triage.ts` finalize + `buildSpecificationPrompt` | Instructions + post-write pin | | `generateSpecifiedPromptImpl` | Non-AI specified PROMPT.md | | `task-update.ts` | Description sync on real specs | ## Test plan - [x] `pnpm --filter @fusion/core exec vitest run src/__tests__/original-description-policy.test.ts src/__tests__/agent-prompts.test.ts src/__tests__/mesh-task-replication.test.ts src/__tests__/store-create-intake-column.test.ts --silent=passed-only --reporter=dot` - [x] `pnpm --filter @fusion/engine exec vitest run src/__tests__/triage.test.ts -t "Original Description|injects ## Original" --silent=passed-only --reporter=dot` - [ ] CI gate (Lint / Typecheck / Build / Gate) ## How to verify manually 1. Create a task with a distinctive description, let triage plan it (or finalize a mock plan). 2. Open `.fusion/tasks/<id>/PROMPT.md` and confirm `## Original Description` appears after title/metadata with the raw description, before Mission / Before → After. 3. Direct-create into `todo` (non-intake) and confirm the non-AI generated prompt also has the section. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Generated `PROMPT.md` specifications now include an `## Original Description` section near the top. - Operator task descriptions are preserved verbatim for AI-planned and specified prompts. - Updated prompts remain synchronized when task descriptions change. - **Bug Fixes** - Replaced paraphrased original descriptions with the correct task description. - Preserved existing prompt content during review and retry workflows. - **Tests** - Added coverage for placement, formatting, replacement, and idempotent behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
7
.changeset/original-desc-in-prompt.md
Normal file
7
.changeset/original-desc-in-prompt.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Keep the operator's original task description at the top of generated PROMPT.md specs.
|
||||||
|
category: feature
|
||||||
|
dev: Deterministic `## Original Description` injection on AI-planned finalize and non-AI generateSpecifiedPrompt; planning templates instruct verbatim copy.
|
||||||
@@ -292,7 +292,9 @@ describe("resolveAgentPrompt", () => {
|
|||||||
expect(fastPrompt).toContain("Do not write bare `### Preflight` / `### Implementation` headings");
|
expect(fastPrompt).toContain("Do not write bare `### Preflight` / `### Implementation` headings");
|
||||||
expect(fastPrompt).not.toContain("## Review Level");
|
expect(fastPrompt).not.toContain("## Review Level");
|
||||||
expect(fastPrompt.length).toBeLessThan(standardPrompt.length / 3);
|
expect(fastPrompt.length).toBeLessThan(standardPrompt.length / 3);
|
||||||
expect(fastPrompt.length).toBeLessThan(6000);
|
// FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35: Original Description contract
|
||||||
|
// adds a few lines to fast planning; keep lean but allow the new mandatory section.
|
||||||
|
expect(fastPrompt.length).toBeLessThan(6500);
|
||||||
expect(fastPrompt.split("\n").length).toBeLessThan(120);
|
expect(fastPrompt.split("\n").length).toBeLessThan(120);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -333,6 +335,32 @@ describe("resolveAgentPrompt", () => {
|
|||||||
expect(fastTransformationIdx).toBeLessThan(fastMissionIdx);
|
expect(fastTransformationIdx).toBeLessThan(fastMissionIdx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Planning prompts must require ## Original Description (verbatim) near the top so
|
||||||
|
generated PROMPT.md preserves the operator source request after Mission rewrites.
|
||||||
|
*/
|
||||||
|
it("requires ## Original Description near the top of generated PROMPT.md across planning prompts", () => {
|
||||||
|
const standardPrompt = resolveAgentPrompt("triage");
|
||||||
|
const fastPrompt = builtinSeamPrompt("planning-fast");
|
||||||
|
const concise = resolveAgentPrompt("triage", {
|
||||||
|
roleAssignments: { triage: "concise-triage" },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const prompt of [standardPrompt, fastPrompt, concise]) {
|
||||||
|
expect(prompt).toContain("## Original Description");
|
||||||
|
expect(prompt.toLowerCase()).toMatch(/verbatim/);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template order: Original Description before Before → After and Mission
|
||||||
|
const originalIdx = standardPrompt.indexOf("## Original Description");
|
||||||
|
const transformIdx = standardPrompt.indexOf("## Before → After Transformation");
|
||||||
|
const missionIdx = standardPrompt.indexOf("## Mission");
|
||||||
|
expect(originalIdx).toBeGreaterThan(-1);
|
||||||
|
expect(originalIdx).toBeLessThan(transformIdx);
|
||||||
|
expect(originalIdx).toBeLessThan(missionIdx);
|
||||||
|
});
|
||||||
|
|
||||||
it("triage planning prompt is sourced from workflow IR without an engine duplicate", () => {
|
it("triage planning prompt is sourced from workflow IR without an engine duplicate", () => {
|
||||||
const corePrompt = resolveAgentPrompt("triage");
|
const corePrompt = resolveAgentPrompt("triage");
|
||||||
const planningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR);
|
const planningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR);
|
||||||
|
|||||||
@@ -7,11 +7,29 @@ database) and those functions were removed from mesh-task-replication.ts.
|
|||||||
Only buildBootstrapPrompt survives (task/comment PROMPT.md stub builder).
|
Only buildBootstrapPrompt survives (task/comment PROMPT.md stub builder).
|
||||||
*/
|
*/
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { buildBootstrapPrompt } from "../mesh-task-replication.js";
|
import { buildBootstrapPrompt, isUnplannedSeedPrompt } from "../mesh-task-replication.js";
|
||||||
|
import { applyOriginalDescription } from "../original-description-policy.js";
|
||||||
|
|
||||||
describe("mesh-task-replication", () => {
|
describe("mesh-task-replication", () => {
|
||||||
it("buildBootstrapPrompt matches task bootstrap format", () => {
|
it("buildBootstrapPrompt matches task bootstrap format", () => {
|
||||||
expect(buildBootstrapPrompt("FN-1", undefined, "desc")).toBe("# FN-1\n\ndesc\n");
|
expect(buildBootstrapPrompt("FN-1", undefined, "desc")).toBe("# FN-1\n\ndesc\n");
|
||||||
expect(buildBootstrapPrompt("FN-1", "Title", "desc")).toBe("# FN-1: Title\n\ndesc\n");
|
expect(buildBootstrapPrompt("FN-1", "Title", "desc")).toBe("# FN-1: Title\n\ndesc\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Planned-spec Original Description injection must not change bootstrap equality
|
||||||
|
used by isUnplannedSeedPrompt / hold-release unplanned detection.
|
||||||
|
*/
|
||||||
|
it("keeps bootstrap seed equality after original-description policy exists", () => {
|
||||||
|
const bootstrap = buildBootstrapPrompt("FN-1", "Title", "desc");
|
||||||
|
expect(isUnplannedSeedPrompt(bootstrap, "FN-1", "Title", "desc")).toBe(true);
|
||||||
|
// Applying original description to a *real* spec does not affect bootstrap detection.
|
||||||
|
const planned = applyOriginalDescription(
|
||||||
|
"# FN-1: Title\n\n**Created:** 2026-07-14\n\n## Mission\n\nPlanned work.\n",
|
||||||
|
"desc",
|
||||||
|
);
|
||||||
|
expect(isUnplannedSeedPrompt(planned, "FN-1", "Title", "desc")).toBe(false);
|
||||||
|
expect(planned).toContain("## Original Description");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
186
packages/core/src/__tests__/original-description-policy.test.ts
Normal file
186
packages/core/src/__tests__/original-description-policy.test.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Unit coverage for deterministic ## Original Description injection used by non-AI
|
||||||
|
generateSpecifiedPrompt and AI-planning finalize hygiene.
|
||||||
|
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-15-00:40:
|
||||||
|
Also covers embedded-H2 operator text so description updates cannot duplicate or
|
||||||
|
corrupt PROMPT.md when the raw request contains lines like `## Required behavior`.
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
ORIGINAL_DESCRIPTION_END_MARKER,
|
||||||
|
ORIGINAL_DESCRIPTION_HEADING,
|
||||||
|
ORIGINAL_DESCRIPTION_START_MARKER,
|
||||||
|
applyOriginalDescription,
|
||||||
|
buildOriginalDescriptionSection,
|
||||||
|
extractOriginalDescriptionBody,
|
||||||
|
} from "../original-description-policy.js";
|
||||||
|
|
||||||
|
const SAMPLE_DESC = "Fix the board blank state when autoMerge is off on mobile Android.";
|
||||||
|
|
||||||
|
function sampleSpec(opts?: { withOriginal?: boolean; originalBody?: string; marked?: boolean }): string {
|
||||||
|
let original = "";
|
||||||
|
if (opts?.withOriginal) {
|
||||||
|
const body = opts.originalBody ?? "paraphrased planner text";
|
||||||
|
if (opts.marked) {
|
||||||
|
original =
|
||||||
|
`${ORIGINAL_DESCRIPTION_HEADING}\n\n` +
|
||||||
|
`${ORIGINAL_DESCRIPTION_START_MARKER}\n` +
|
||||||
|
`${body}\n` +
|
||||||
|
`${ORIGINAL_DESCRIPTION_END_MARKER}\n\n`;
|
||||||
|
} else {
|
||||||
|
original = `${ORIGINAL_DESCRIPTION_HEADING}\n\n${body}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `# Task: FN-1000 - Fix blank board
|
||||||
|
|
||||||
|
**Created:** 2026-07-14
|
||||||
|
**Size:** M
|
||||||
|
|
||||||
|
${original}## Before → After Transformation
|
||||||
|
|
||||||
|
- **Before:** blank board
|
||||||
|
- **After:** board shows tasks
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
|
||||||
|
Implement the fix across desktop and mobile.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("original description policy", () => {
|
||||||
|
it("builds a marked section with a single trailing newline", () => {
|
||||||
|
const section = buildOriginalDescriptionSection(SAMPLE_DESC);
|
||||||
|
expect(section).toContain(ORIGINAL_DESCRIPTION_START_MARKER);
|
||||||
|
expect(section).toContain(ORIGINAL_DESCRIPTION_END_MARKER);
|
||||||
|
expect(section).toContain(SAMPLE_DESC);
|
||||||
|
expect(section.startsWith(`${ORIGINAL_DESCRIPTION_HEADING}\n\n`)).toBe(true);
|
||||||
|
expect(section.endsWith("\n")).toBe(true);
|
||||||
|
expect(section.endsWith("\n\n")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inserts ## Original Description after title/metadata and before other ## sections", () => {
|
||||||
|
const injected = applyOriginalDescription(sampleSpec(), SAMPLE_DESC);
|
||||||
|
|
||||||
|
const titleIdx = injected.indexOf("# Task: FN-1000");
|
||||||
|
const originalIdx = injected.indexOf(ORIGINAL_DESCRIPTION_HEADING);
|
||||||
|
const transformIdx = injected.indexOf("## Before → After Transformation");
|
||||||
|
const missionIdx = injected.indexOf("## Mission");
|
||||||
|
|
||||||
|
expect(titleIdx).toBeGreaterThan(-1);
|
||||||
|
expect(originalIdx).toBeGreaterThan(titleIdx);
|
||||||
|
expect(transformIdx).toBeGreaterThan(originalIdx);
|
||||||
|
expect(missionIdx).toBeGreaterThan(transformIdx);
|
||||||
|
expect(injected).toContain(SAMPLE_DESC);
|
||||||
|
expect(injected.match(/## Original Description/g)).toHaveLength(1);
|
||||||
|
expect(extractOriginalDescriptionBody(injected)).toBe(SAMPLE_DESC);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the operator description verbatim including multi-line and markdown-like text", () => {
|
||||||
|
const multi = [
|
||||||
|
"Please fix ## Mission drift.",
|
||||||
|
"",
|
||||||
|
"Also handle:",
|
||||||
|
"- empty state",
|
||||||
|
"- **Created:** in body text",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const injected = applyOriginalDescription(sampleSpec(), multi);
|
||||||
|
expect(extractOriginalDescriptionBody(injected)).toBe(multi);
|
||||||
|
// Verbatim body must not strip operator markdown-looking lines
|
||||||
|
expect(injected).toContain("Please fix ## Mission drift.");
|
||||||
|
expect(injected).toContain("- **Created:** in body text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces a paraphrased Original Description with the verbatim task description", () => {
|
||||||
|
const withParaphrase = sampleSpec({ withOriginal: true, originalBody: "planner rewrote this" });
|
||||||
|
const injected = applyOriginalDescription(withParaphrase, SAMPLE_DESC);
|
||||||
|
|
||||||
|
expect(extractOriginalDescriptionBody(injected)).toBe(SAMPLE_DESC);
|
||||||
|
expect(injected).not.toContain("planner rewrote this");
|
||||||
|
expect(injected.match(/## Original Description/g)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is idempotent when the section already matches with markers", () => {
|
||||||
|
const once = applyOriginalDescription(sampleSpec(), SAMPLE_DESC);
|
||||||
|
const twice = applyOriginalDescription(once, SAMPLE_DESC);
|
||||||
|
expect(twice).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends the section when the prompt has no ## headings", () => {
|
||||||
|
const bare = "# FN-1: Title\n\nSome body without sections.\n";
|
||||||
|
const injected = applyOriginalDescription(bare, SAMPLE_DESC);
|
||||||
|
expect(injected).toContain(ORIGINAL_DESCRIPTION_HEADING);
|
||||||
|
expect(extractOriginalDescriptionBody(injected)).toBe(SAMPLE_DESC);
|
||||||
|
expect(injected.indexOf(ORIGINAL_DESCRIPTION_HEADING)).toBeGreaterThan(injected.indexOf("# FN-1"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty input unchanged", () => {
|
||||||
|
expect(applyOriginalDescription("", SAMPLE_DESC)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-15-00:40:
|
||||||
|
Greptile P1: embedded H2 in the operator description must not end the section.
|
||||||
|
A description update must replace the full body without leaving a duplicated suffix.
|
||||||
|
*/
|
||||||
|
it("does not corrupt PROMPT.md when the description contains embedded ## headings", () => {
|
||||||
|
const withEmbeddedH2 = [
|
||||||
|
"Please keep this request intact.",
|
||||||
|
"",
|
||||||
|
"## Required behavior",
|
||||||
|
"",
|
||||||
|
"- blank board stays fixed",
|
||||||
|
"- mobile Android included",
|
||||||
|
"",
|
||||||
|
"## Mission",
|
||||||
|
"",
|
||||||
|
"Note: this H2 is operator prose, not the PROMPT Mission section.",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
// Planner-written section without markers, body already contains embedded H2s.
|
||||||
|
const plannerWritten = sampleSpec({
|
||||||
|
withOriginal: true,
|
||||||
|
originalBody: withEmbeddedH2,
|
||||||
|
marked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// First apply pins markers and full body (including embedded ## Mission prose).
|
||||||
|
const once = applyOriginalDescription(plannerWritten, withEmbeddedH2);
|
||||||
|
expect(extractOriginalDescriptionBody(once)).toBe(withEmbeddedH2);
|
||||||
|
expect(once).toContain(ORIGINAL_DESCRIPTION_START_MARKER);
|
||||||
|
expect(once.match(/## Original Description/g)).toHaveLength(1);
|
||||||
|
// One ## Mission inside the marked body + one structural PROMPT Mission section.
|
||||||
|
expect(once.match(/^## Mission\s*$/gm)?.length).toBe(2);
|
||||||
|
expect(once).toContain("## Before → After Transformation");
|
||||||
|
// Structural Mission is outside the markers.
|
||||||
|
const endMarkerIdx = once.indexOf(ORIGINAL_DESCRIPTION_END_MARKER);
|
||||||
|
expect(once.indexOf("Implement the fix across desktop and mobile", endMarkerIdx)).toBeGreaterThan(
|
||||||
|
endMarkerIdx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Description update (greptile corruption path): new text with more H2s.
|
||||||
|
const updated = [
|
||||||
|
withEmbeddedH2,
|
||||||
|
"",
|
||||||
|
"## Extra section from operator",
|
||||||
|
"more text",
|
||||||
|
].join("\n");
|
||||||
|
const twice = applyOriginalDescription(once, updated);
|
||||||
|
expect(extractOriginalDescriptionBody(twice)).toBe(updated);
|
||||||
|
expect(twice.match(/## Original Description/g)).toHaveLength(1);
|
||||||
|
expect(twice.match(/^## Mission\s*$/gm)?.length).toBe(2);
|
||||||
|
// No duplicated leftover suffix from the previous body.
|
||||||
|
expect(twice.split("## Required behavior").length - 1).toBe(1);
|
||||||
|
expect(twice.split("Note: this H2 is operator prose").length - 1).toBe(1);
|
||||||
|
expect(twice.split("## Extra section from operator").length - 1).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats unmarked planner sections ending at structural headings only", () => {
|
||||||
|
const bodyWithUnknownH2 = "Intro\n\n## Required behavior\n\n- do the thing";
|
||||||
|
const unmarked = sampleSpec({ withOriginal: true, originalBody: bodyWithUnknownH2, marked: false });
|
||||||
|
// Extract must include ## Required behavior (not a structural heading).
|
||||||
|
expect(extractOriginalDescriptionBody(unmarked)).toBe(bodyWithUnknownH2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -94,6 +94,14 @@ pgTest("createTask intake-column wiring (Coding (Ideas))", () => {
|
|||||||
);
|
);
|
||||||
// A direct todo create is NOT an intake column, so it must NOT get the bootstrap stub.
|
// A direct todo create is NOT an intake column, so it must NOT get the bootstrap stub.
|
||||||
expect(prompt).not.toBe(`# ${task.id}\n\n${task.description}\n`);
|
expect(prompt).not.toBe(`# ${task.id}\n\n${task.description}\n`);
|
||||||
|
// FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
// Non-AI specified prompts must surface the operator description near the top.
|
||||||
|
expect(prompt).toContain("## Original Description");
|
||||||
|
expect(prompt).toContain("direct todo create");
|
||||||
|
const originalIdx = prompt.indexOf("## Original Description");
|
||||||
|
const missionIdx = prompt.indexOf("## Mission");
|
||||||
|
expect(originalIdx).toBeGreaterThan(-1);
|
||||||
|
expect(missionIdx).toBeGreaterThan(originalIdx);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -259,6 +259,9 @@ Fast mode skips heavyweight planning ceremony, but every generated task still ne
|
|||||||
|
|
||||||
FNXC:FastPlanning 2026-07-05-12:00:
|
FNXC:FastPlanning 2026-07-05-12:00:
|
||||||
Per FN-7593, the transformation summary must sit at the top of the PROMPT.md (before Mission), matching the standard-mode placement, so operators get the same glance-first ordering in fast mode.
|
Per FN-7593, the transformation summary must sit at the top of the PROMPT.md (before Mission), matching the standard-mode placement, so operators get the same glance-first ordering in fast mode.
|
||||||
|
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Fast planning also requires \`## Original Description\` (verbatim operator text) immediately after title/metadata and before the transformation summary, same as standard planning.
|
||||||
*/
|
*/
|
||||||
const FAST_TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn". This task is running in **fast mode**.
|
const FAST_TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn". This task is running in **fast mode**.
|
||||||
|
|
||||||
@@ -273,7 +276,7 @@ Write a lean, executable PROMPT.md quickly. Preserve safety gates, but skip heav
|
|||||||
Before writing a spec, call \`fn_task_list\` for active work, then call \`fn_task_search\` with 2-4 targeted keyword phrases from the title/description, such as file paths, symptoms, and symbols. For any likely match in \`done\` or \`archived\`, call \`fn_task_show\` and inspect it before deciding. If an existing task covers the same work, do not write PROMPT.md; write exactly \`DUPLICATE: {existing-task-id}\`.
|
Before writing a spec, call \`fn_task_list\` for active work, then call \`fn_task_search\` with 2-4 targeted keyword phrases from the title/description, such as file paths, symptoms, and symbols. For any likely match in \`done\` or \`archived\`, call \`fn_task_show\` and inspect it before deciding. If an existing task covers the same work, do not write PROMPT.md; write exactly \`DUPLICATE: {existing-task-id}\`.
|
||||||
|
|
||||||
## Required PROMPT.md shape
|
## Required PROMPT.md shape
|
||||||
Write PROMPT.md with Before → After Transformation, Mission, Dependencies, Context to Read First, File Scope, Steps, Documentation Requirements, Completion Criteria, Git Commit Convention, and Do NOT. Put \`## Before → After Transformation\` at the top, before \`## Mission\`, with concise Before/After bullets: current state, target state, why it satisfies the user's request at a glance. In \`## Steps\`, every executable heading MUST use \`### Step N: <name>\` (e.g. \`### Step 1: Preflight\`). Do not write bare \`### Preflight\` / \`### Implementation\` headings, and do not add review-level, triage subtask, or proactive subtask headings.
|
Write PROMPT.md with Original Description, Before → After Transformation, Mission, Dependencies, Context to Read First, File Scope, Steps, Documentation Requirements, Completion Criteria, Git Commit Convention, and Do NOT. Put \`## Original Description\` immediately after the title/\`Created\`/\`Size\` metadata with the operator's original task description copied **verbatim** (do not paraphrase). Put \`## Before → After Transformation\` next, before \`## Mission\`, with concise Before/After bullets: current state, target state, why it satisfies the user's request at a glance. In \`## Steps\`, every executable heading MUST use \`### Step N: <name>\` (e.g. \`### Step 1: Preflight\`). Do not write bare \`### Preflight\` / \`### Implementation\` headings, and do not add review-level, triage subtask, or proactive subtask headings.
|
||||||
|
|
||||||
## Surface Enumeration
|
## Surface Enumeration
|
||||||
For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. The workflow Plan Review gate validates this before execution when plan review is enabled.
|
For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. The workflow Plan Review gate validates this before execution when plan review is enabled.
|
||||||
@@ -335,6 +338,10 @@ Follow this structure exactly:
|
|||||||
**Created:** {YYYY-MM-DD}
|
**Created:** {YYYY-MM-DD}
|
||||||
**Size:** {S | M | L}
|
**Size:** {S | M | L}
|
||||||
|
|
||||||
|
## Original Description
|
||||||
|
|
||||||
|
{Verbatim copy of the operator's original task description — do not paraphrase or summarize}
|
||||||
|
|
||||||
## Before → After Transformation
|
## Before → After Transformation
|
||||||
|
|
||||||
- **Before:** {Briefly describe the current state, missing capability, broken behavior, or operator pain point}
|
- **Before:** {Briefly describe the current state, missing capability, broken behavior, or operator pain point}
|
||||||
@@ -480,9 +487,21 @@ If this task REMOVES existing functionality (deleting modules, settings, API end
|
|||||||
- This is mandatory for any net-negative change (more deletions than additions to existing files)
|
- This is mandatory for any net-negative change (more deletions than additions to existing files)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
## Original description requirement
|
||||||
|
|
||||||
|
<!--
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Planning rewrites Mission/Steps into structured prose and used to drop the operator's
|
||||||
|
raw request. Generated PROMPT.md must keep that text under \`## Original Description\`
|
||||||
|
near the top (after title/metadata) so executors always see the source request.
|
||||||
|
Deterministic post-write injection also enforces this; the planner still writes it so
|
||||||
|
the on-disk draft is correct before finalize.
|
||||||
|
-->
|
||||||
|
Every generated PROMPT.md MUST include \`## Original Description\` immediately after the \`# Task\` title and \`Created\`/\`Size\` metadata, before \`## Before → After Transformation\`, \`## Review Level\`, and \`## Mission\`. Copy the operator's original task description **verbatim** — do not paraphrase, summarize, or omit details.
|
||||||
|
|
||||||
## Transformation summary requirement
|
## Transformation summary requirement
|
||||||
|
|
||||||
Every normal implementation, documentation, or decision task definition MUST include \`## Before → After Transformation\` at the top of the definition, immediately after the \`# Task\` title and \`Created\`/\`Size\` metadata, before \`## Review Level\` and \`## Mission\`. Keep it concise: use brief Before and After bullets (or equivalent short prose) that name the current state, the target state, and why that target satisfies the user's request at a glance.
|
Every normal implementation, documentation, or decision task definition MUST include \`## Before → After Transformation\` near the top of the definition, immediately after the \`# Task\` title, \`Created\`/\`Size\` metadata, and \`## Original Description\` section, before \`## Review Level\` and \`## Mission\`. Keep it concise: use brief Before and After bullets (or equivalent short prose) that name the current state, the target state, and why that target satisfies the user's request at a glance.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
FNXC:TriagePromptStructure 2026-07-04-16:20:
|
FNXC:TriagePromptStructure 2026-07-04-16:20:
|
||||||
@@ -1136,6 +1155,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise — avoi
|
|||||||
**Created:** {YYYY-MM-DD}
|
**Created:** {YYYY-MM-DD}
|
||||||
**Size:** {S | M | L}
|
**Size:** {S | M | L}
|
||||||
|
|
||||||
|
## Original Description
|
||||||
|
{Verbatim operator description — do not paraphrase}
|
||||||
|
|
||||||
## Review Level: {0-3} ({description})
|
## Review Level: {0-3} ({description})
|
||||||
|
|
||||||
**Assessment:** {1-2 sentences}
|
**Assessment:** {1-2 sentences}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export type { PlanApprovalMode } from "./plan-approval.js";
|
|||||||
export { isActiveNearDuplicateColumn, isNearDuplicateCanonicalInactive } from "./near-duplicate-canonical.js";
|
export { isActiveNearDuplicateColumn, isNearDuplicateCanonicalInactive } from "./near-duplicate-canonical.js";
|
||||||
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 "./file-scope-classification.js";
|
export * from "./file-scope-classification.js";
|
||||||
export {
|
export {
|
||||||
WAKE_DELTA_ASSIGNED_TASKS_CAP,
|
WAKE_DELTA_ASSIGNED_TASKS_CAP,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ types/policy for severity-routed notes before they reach steering inject.
|
|||||||
export * from "./overseer-advice.js";
|
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 "./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 {
|
||||||
|
|||||||
218
packages/core/src/original-description-policy.ts
Normal file
218
packages/core/src/original-description-policy.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Generated PROMPT.md (AI-planned and non-AI specified) must keep the operator's original
|
||||||
|
task description near the top so executors always see the source request after planning
|
||||||
|
rewrites Mission/Steps/etc. Bootstrap stubs (buildBootstrapPrompt) stay description-only
|
||||||
|
under the title — this helper is only for real specifications.
|
||||||
|
|
||||||
|
Placement: after the `#` title heading and optional Created/Size metadata lines, before
|
||||||
|
any other structural `##` section (including Before → After Transformation and Mission).
|
||||||
|
|
||||||
|
Idempotent: if `## Original Description` already exists, replace its body with the verbatim
|
||||||
|
description so paraphrased planner copies cannot stick. Empty descriptions still get a
|
||||||
|
section so the heading is a stable contract for executors and tests.
|
||||||
|
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-15-00:40:
|
||||||
|
Operator descriptions routinely contain markdown H2 lines (e.g. `## Required behavior`).
|
||||||
|
Naive "next `##` ends the section" parsing treated those as PROMPT structure and, on
|
||||||
|
description updates, replaced only a prefix while leaving the old suffix — duplicating and
|
||||||
|
corrupting PROMPT.md. Section bounds use HTML markers when present, else only known
|
||||||
|
structural PROMPT headings (Mission, File Scope, Steps, …), so embedded H2s stay inside
|
||||||
|
the Original Description body.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ORIGINAL_DESCRIPTION_HEADING = "## Original Description";
|
||||||
|
|
||||||
|
/** Markers delimit the verbatim body so embedded `##` lines cannot end the section. */
|
||||||
|
export const ORIGINAL_DESCRIPTION_START_MARKER = "<!-- fusion-original-description:start -->";
|
||||||
|
export const ORIGINAL_DESCRIPTION_END_MARKER = "<!-- fusion-original-description:end -->";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When markers are absent (planner-written plain section), end Original Description at the
|
||||||
|
* first *preferred following* structural heading that appears in the file — not the first
|
||||||
|
* arbitrary `##` line. Preferred order matters: operator text may contain `## Mission` as
|
||||||
|
* prose; we still bind to a later `## Before → After Transformation` / `## Review Level`
|
||||||
|
* when those exist (standard/concise templates). Unknown H2s never end the section.
|
||||||
|
*/
|
||||||
|
const PREFERRED_SECTION_TERMINATORS: RegExp[] = [
|
||||||
|
/^##\s+Before\s*→\s*After Transformation\s*$/im,
|
||||||
|
/^##\s+Review Level(?:\s*:.*)?\s*$/im,
|
||||||
|
/^##\s+Mission\s*$/im,
|
||||||
|
/^##\s+Surface Enumeration\s*$/im,
|
||||||
|
/^##\s+Symptom Verification\s*$/im,
|
||||||
|
/^##\s+Dependencies\s*$/im,
|
||||||
|
/^##\s+Context to Read First\s*$/im,
|
||||||
|
/^##\s+File Scope\s*$/im,
|
||||||
|
/^##\s+Steps\s*$/im,
|
||||||
|
/^##\s+Documentation Requirements\s*$/im,
|
||||||
|
/^##\s+Completion Criteria\s*$/im,
|
||||||
|
/^##\s+Git Commit Convention\s*$/im,
|
||||||
|
/^##\s+Do NOT\s*$/im,
|
||||||
|
/^##\s+Changeset Requirements\s*$/im,
|
||||||
|
/^##\s+Frontend UX Criteria\s*$/im,
|
||||||
|
/^##\s+Acceptance Criteria\s*$/im,
|
||||||
|
/^##\s+Notifications\s*$/im,
|
||||||
|
/^##\s+External Integration Evidence\s*$/im,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the `## Original Description` section body (heading + marked verbatim text).
|
||||||
|
* Ends with exactly one trailing newline so insertion is predictable.
|
||||||
|
*/
|
||||||
|
export function buildOriginalDescriptionSection(originalDescription: string): string {
|
||||||
|
const body = (originalDescription ?? "").trimEnd();
|
||||||
|
return (
|
||||||
|
`${ORIGINAL_DESCRIPTION_HEADING}\n\n` +
|
||||||
|
`${ORIGINAL_DESCRIPTION_START_MARKER}\n` +
|
||||||
|
`${body}\n` +
|
||||||
|
`${ORIGINAL_DESCRIPTION_END_MARKER}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure `promptMarkdown` includes a top-of-spec `## Original Description` section with the
|
||||||
|
* operator text verbatim. Safe to call repeatedly; never inspects bootstrap-stub equality
|
||||||
|
* (callers only apply this to planned/specified prompts).
|
||||||
|
*/
|
||||||
|
export function applyOriginalDescription(
|
||||||
|
promptMarkdown: string,
|
||||||
|
originalDescription: string,
|
||||||
|
): string {
|
||||||
|
if (!promptMarkdown) {
|
||||||
|
return promptMarkdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wantedBody = (originalDescription ?? "").trimEnd();
|
||||||
|
const existingBody = extractOriginalDescriptionBody(promptMarkdown);
|
||||||
|
// Idempotent when the section already carries the exact operator text.
|
||||||
|
if (existingBody !== null && existingBody.trimEnd() === wantedBody) {
|
||||||
|
// Still rewrite when markers are missing so later updates stay H2-safe.
|
||||||
|
if (hasOriginalDescriptionMarkers(promptMarkdown)) {
|
||||||
|
return promptMarkdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = buildOriginalDescriptionSection(originalDescription);
|
||||||
|
if (existingBody !== null || hasOriginalDescriptionHeading(promptMarkdown)) {
|
||||||
|
return replaceOriginalDescriptionSection(promptMarkdown, section);
|
||||||
|
}
|
||||||
|
return insertOriginalDescriptionNearTop(promptMarkdown, section);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the body under `## Original Description`, or null when the section is absent. */
|
||||||
|
export function extractOriginalDescriptionBody(content: string): string | null {
|
||||||
|
const range = findOriginalDescriptionRange(content);
|
||||||
|
if (!range) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return range.body.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOriginalDescriptionHeading(content: string): boolean {
|
||||||
|
return /^##\s+Original Description\s*$/m.test(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOriginalDescriptionMarkers(content: string): boolean {
|
||||||
|
return (
|
||||||
|
content.includes(ORIGINAL_DESCRIPTION_START_MARKER) &&
|
||||||
|
content.includes(ORIGINAL_DESCRIPTION_END_MARKER)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolute [start, end) range of the Original Description section and its body text.
|
||||||
|
* Prefer HTML markers; fall back to the next known structural PROMPT heading.
|
||||||
|
*/
|
||||||
|
function findOriginalDescriptionRange(
|
||||||
|
content: string,
|
||||||
|
): { sectionStart: number; sectionEnd: number; body: string } | null {
|
||||||
|
const match = content.match(/^##\s+Original Description\s*$/m);
|
||||||
|
if (!match || match.index === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionStart = match.index;
|
||||||
|
const headerEnd = match.index + match[0].length;
|
||||||
|
const afterHeader = content.slice(headerEnd);
|
||||||
|
|
||||||
|
// Marker-bounded body (preferred — safe for any embedded markdown).
|
||||||
|
const startMarkerIdx = afterHeader.indexOf(ORIGINAL_DESCRIPTION_START_MARKER);
|
||||||
|
const endMarkerIdx = afterHeader.indexOf(ORIGINAL_DESCRIPTION_END_MARKER);
|
||||||
|
if (
|
||||||
|
startMarkerIdx !== -1 &&
|
||||||
|
endMarkerIdx !== -1 &&
|
||||||
|
endMarkerIdx > startMarkerIdx
|
||||||
|
) {
|
||||||
|
const bodyStart = startMarkerIdx + ORIGINAL_DESCRIPTION_START_MARKER.length;
|
||||||
|
const body = afterHeader.slice(bodyStart, endMarkerIdx).replace(/^\n/, "").replace(/\n$/, "");
|
||||||
|
const sectionEnd =
|
||||||
|
headerEnd + endMarkerIdx + ORIGINAL_DESCRIPTION_END_MARKER.length;
|
||||||
|
// Consume a single trailing newline after the end marker when present.
|
||||||
|
const absoluteEnd =
|
||||||
|
content[sectionEnd] === "\n" ? sectionEnd + 1 : sectionEnd;
|
||||||
|
return { sectionStart, sectionEnd: absoluteEnd, body };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarked (planner-written): end at preferred following structural heading.
|
||||||
|
const structuralOffset = findPreferredSectionTerminatorOffset(afterHeader);
|
||||||
|
const sectionEnd =
|
||||||
|
structuralOffset === -1 ? content.length : headerEnd + structuralOffset;
|
||||||
|
const body = afterHeader
|
||||||
|
.slice(0, structuralOffset === -1 ? undefined : structuralOffset)
|
||||||
|
.replace(/^\n+/, "")
|
||||||
|
.trimEnd();
|
||||||
|
return { sectionStart, sectionEnd, body };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset of the preferred section terminator within `text`, or -1.
|
||||||
|
* Walks preferred following headings in template order and returns the first that exists
|
||||||
|
* (even if a lower-priority structural heading like Mission appears earlier in the body).
|
||||||
|
*/
|
||||||
|
function findPreferredSectionTerminatorOffset(text: string): number {
|
||||||
|
for (const re of PREFERRED_SECTION_TERMINATORS) {
|
||||||
|
// Fresh regex instance so global/sticky flags never retain lastIndex.
|
||||||
|
const match = new RegExp(re.source, re.flags).exec(text);
|
||||||
|
if (match) {
|
||||||
|
return match.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceOriginalDescriptionSection(content: string, section: string): string {
|
||||||
|
const range = findOriginalDescriptionRange(content);
|
||||||
|
if (!range) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = content.slice(0, range.sectionStart).trimEnd();
|
||||||
|
let after = content.slice(range.sectionEnd);
|
||||||
|
// Drop a leading blank line on after so we don't triple-space before the next section.
|
||||||
|
after = after.replace(/^\n*/, "\n\n");
|
||||||
|
if (!after.trim()) {
|
||||||
|
return `${before}\n\n${section.trimEnd()}\n`;
|
||||||
|
}
|
||||||
|
return `${before}\n\n${section.trimEnd()}${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert before the preferred following structural section so the block sits under
|
||||||
|
* title/metadata. Unknown H2s are ignored. Falls back to the first H2, then append.
|
||||||
|
*/
|
||||||
|
function insertOriginalDescriptionNearTop(content: string, section: string): string {
|
||||||
|
const structuralOffset = findPreferredSectionTerminatorOffset(content);
|
||||||
|
if (structuralOffset !== -1) {
|
||||||
|
const before = content.slice(0, structuralOffset).trimEnd();
|
||||||
|
const after = content.slice(structuralOffset);
|
||||||
|
return `${before}\n\n${section.trimEnd()}\n\n${after}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstH2 = content.search(/^##\s+/m);
|
||||||
|
if (firstH2 !== -1) {
|
||||||
|
const before = content.slice(0, firstH2).trimEnd();
|
||||||
|
const after = content.slice(firstH2);
|
||||||
|
return `${before}\n\n${section.trimEnd()}\n\n${after}`;
|
||||||
|
}
|
||||||
|
return `${content.trimEnd()}\n\n${section.trimEnd()}\n`;
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ import {nextWorkflowDefinitionIdAsyncImpl} from "../task-store/remaining-ops-8.j
|
|||||||
import {upsertTaskRowInTransaction, buildTaskInsertValues} from "../task-store/async-persistence.js";
|
import {upsertTaskRowInTransaction, buildTaskInsertValues} from "../task-store/async-persistence.js";
|
||||||
import {readTaskRowInTransaction} from "../task-store/async-persistence.js";
|
import {readTaskRowInTransaction} from "../task-store/async-persistence.js";
|
||||||
import {recordActivityLogEntry as recordActivityLogEntryAsync} from "../task-store/async-audit.js";
|
import {recordActivityLogEntry as recordActivityLogEntryAsync} from "../task-store/async-audit.js";
|
||||||
|
import {applyOriginalDescription} from "../original-description-policy.js";
|
||||||
import {recordRunAuditEvent as recordRunAuditEventAsync} from "../postgres/data-layer.js";
|
import {recordRunAuditEvent as recordRunAuditEventAsync} from "../postgres/data-layer.js";
|
||||||
import {listGoalCitations as listGoalCitationsAsync} from "../task-store/async-events.js";
|
import {listGoalCitations as listGoalCitationsAsync} from "../task-store/async-events.js";
|
||||||
import type {GoalCitationRow, RunAuditEventRow} from "../task-store/row-types.js";
|
import type {GoalCitationRow, RunAuditEventRow} from "../task-store/row-types.js";
|
||||||
@@ -1064,6 +1065,12 @@ export async function countActiveInCapacitySlotAsyncImpl(store: TaskStore, param
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateSpecifiedPromptImpl(store: TaskStore, task: Task): string {
|
export function generateSpecifiedPromptImpl(store: TaskStore, task: Task): string {
|
||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Non-AI specified PROMPT.md (direct create into non-intake columns) must include the
|
||||||
|
operator's original description near the top, same contract as AI-planned specs.
|
||||||
|
Bootstrap stubs use buildBootstrapPrompt and intentionally skip this path.
|
||||||
|
*/
|
||||||
const deps =
|
const deps =
|
||||||
task.dependencies.length > 0
|
task.dependencies.length > 0
|
||||||
? task.dependencies.map((d) => `- **Task:** ${d}`).join("\n")
|
? task.dependencies.map((d) => `- **Task:** ${d}`).join("\n")
|
||||||
@@ -1077,7 +1084,7 @@ export function generateSpecifiedPromptImpl(store: TaskStore, task: Task): strin
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const heading = task.title ? `${task.id}: ${task.title}` : task.id;
|
const heading = task.title ? `${task.id}: ${task.title}` : task.id;
|
||||||
return `# ${heading}
|
const base = `# ${heading}
|
||||||
|
|
||||||
**Created:** ${task.createdAt.split("T")[0]}
|
**Created:** ${task.createdAt.split("T")[0]}
|
||||||
**Size:** M
|
**Size:** M
|
||||||
@@ -1113,6 +1120,7 @@ ${deps}
|
|||||||
- [ ] All steps complete
|
- [ ] All steps complete
|
||||||
- [ ] All tests passing
|
- [ ] All tests passing
|
||||||
${notificationsSection}`;
|
${notificationsSection}`;
|
||||||
|
return applyOriginalDescription(base, task.description ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recordActivityImpl(store: TaskStore, entry: Omit<ActivityLogEntry, "id" | "timestamp">): Promise<ActivityLogEntry> {
|
export async function recordActivityImpl(store: TaskStore, entry: Omit<ActivityLogEntry, "id" | "timestamp">): Promise<ActivityLogEntry> {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {extractTaskIdTokens, normalizeTitleForTaskId} from "../task-title-id-dri
|
|||||||
import {buildBootstrapPrompt} from "../mesh-task-replication.js";
|
import {buildBootstrapPrompt} from "../mesh-task-replication.js";
|
||||||
import {validateFileScopeInPromptContent} from "../task-store/file-scope.js";
|
import {validateFileScopeInPromptContent} from "../task-store/file-scope.js";
|
||||||
import {__setTaskActivityLogLimitsForTesting, isBootstrapPromptStub, rewriteHeadingLine, rewriteMissionSection} from "../task-store/comments.js";
|
import {__setTaskActivityLogLimitsForTesting, isBootstrapPromptStub, rewriteHeadingLine, rewriteMissionSection} from "../task-store/comments.js";
|
||||||
|
import {applyOriginalDescription} from "../original-description-policy.js";
|
||||||
import {normalizeTaskReviewState} from "../task-store/review-state.js";
|
import {normalizeTaskReviewState} from "../task-store/review-state.js";
|
||||||
|
|
||||||
export async function updateTaskUnlockedImpl(store: TaskStore, id: string, updates: Parameters<TaskStore["updateTask"]>[1], runContext?: RunMutationContext,): Promise<Task> {
|
export async function updateTaskUnlockedImpl(store: TaskStore, id: string, updates: Parameters<TaskStore["updateTask"]>[1], runContext?: RunMutationContext,): Promise<Task> {
|
||||||
@@ -756,7 +757,11 @@ export async function updateTaskUnlockedImpl(store: TaskStore, id: string, updat
|
|||||||
next = rewriteHeadingLine(next, heading);
|
next = rewriteHeadingLine(next, heading);
|
||||||
}
|
}
|
||||||
if (updates.description !== undefined) {
|
if (updates.description !== undefined) {
|
||||||
|
// FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
// Keep ## Mission and ## Original Description in sync with task.description
|
||||||
|
// on real specs so operator edits stay visible at the top of PROMPT.md.
|
||||||
next = rewriteMissionSection(next, task.description);
|
next = rewriteMissionSection(next, task.description);
|
||||||
|
next = applyOriginalDescription(next, task.description ?? "");
|
||||||
}
|
}
|
||||||
if (next !== existingPrompt) {
|
if (next !== existingPrompt) {
|
||||||
await writeFile(promptPath, next);
|
await writeFile(promptPath, next);
|
||||||
|
|||||||
@@ -195,6 +195,22 @@ describe("buildSpecificationPrompt", () => {
|
|||||||
expect(prompt).toContain(".fusion/tasks/KB-001/PROMPT.md");
|
expect(prompt).toContain(".fusion/tasks/KB-001/PROMPT.md");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Planner instructions must require ## Original Description with the operator text
|
||||||
|
verbatim so AI-planned PROMPT.md preserves the source request.
|
||||||
|
*/
|
||||||
|
it("instructs the planner to include ## Original Description verbatim", () => {
|
||||||
|
const prompt = buildSpecificationPrompt(
|
||||||
|
baseTask,
|
||||||
|
".fusion/tasks/KB-001/PROMPT.md",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(prompt).toContain("## Original Description");
|
||||||
|
expect(prompt).toContain("verbatim");
|
||||||
|
expect(prompt).toContain("Test task description");
|
||||||
|
});
|
||||||
|
|
||||||
it("includes project commands when provided", () => {
|
it("includes project commands when provided", () => {
|
||||||
const settings: Settings = {
|
const settings: Settings = {
|
||||||
maxConcurrent: 2,
|
maxConcurrent: 2,
|
||||||
@@ -1393,6 +1409,66 @@ describe("TriageProcessor", () => {
|
|||||||
expect(processor).toBeInstanceOf(TriageProcessor);
|
expect(processor).toBeInstanceOf(TriageProcessor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
finalizeApprovedTask must inject ## Original Description with the task description
|
||||||
|
verbatim near the top of the planner-written PROMPT.md (deterministic hygiene).
|
||||||
|
*/
|
||||||
|
it("injects ## Original Description into PROMPT.md on finalize", async () => {
|
||||||
|
const originalDesc = "Operator raw request: blank board on mobile when autoMerge is off.";
|
||||||
|
const task = createTriageTask({
|
||||||
|
id: "FN-ORIG-DESC",
|
||||||
|
title: "Preserve original description",
|
||||||
|
description: originalDesc,
|
||||||
|
status: "planning",
|
||||||
|
});
|
||||||
|
const tempRoot = await mkdtemp(join(tmpdir(), "fusion-orig-desc-"));
|
||||||
|
try {
|
||||||
|
const taskDir = join(tempRoot, ".fusion", "tasks", task.id);
|
||||||
|
await mkdir(taskDir, { recursive: true });
|
||||||
|
const plannerWritten = `# Task: ${task.id} - Preserve original description
|
||||||
|
|
||||||
|
**Created:** 2026-07-14
|
||||||
|
**Size:** M
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
|
||||||
|
Planner rewrote mission without the raw request.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Implement
|
||||||
|
|
||||||
|
- [ ] Do the work
|
||||||
|
`;
|
||||||
|
await writeFile(join(taskDir, "PROMPT.md"), plannerWritten, "utf-8");
|
||||||
|
|
||||||
|
const localStore = createMockStore({
|
||||||
|
getTask: vi.fn().mockResolvedValue(task),
|
||||||
|
});
|
||||||
|
const localProcessor = new TriageProcessor(localStore, tempRoot);
|
||||||
|
|
||||||
|
await (localProcessor as unknown as {
|
||||||
|
finalizeApprovedTask(task: Task, writtenInput: string, settings: Settings): Promise<void>;
|
||||||
|
}).finalizeApprovedTask(
|
||||||
|
task,
|
||||||
|
plannerWritten,
|
||||||
|
{ requirePlanApproval: false } as Settings,
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDisk = readFileSync(join(taskDir, "PROMPT.md"), "utf-8");
|
||||||
|
expect(onDisk).toContain("## Original Description");
|
||||||
|
expect(onDisk).toContain(originalDesc);
|
||||||
|
expect(onDisk).not.toMatch(/## Original Description\s*\n\s*Planner rewrote/);
|
||||||
|
const originalIdx = onDisk.indexOf("## Original Description");
|
||||||
|
const missionIdx = onDisk.indexOf("## Mission");
|
||||||
|
expect(originalIdx).toBeGreaterThan(-1);
|
||||||
|
expect(missionIdx).toBeGreaterThan(originalIdx);
|
||||||
|
} finally {
|
||||||
|
await cleanupTriageFixtureRoot(tempRoot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("runs enabled Plan Review in triage before moving to todo", async () => {
|
it("runs enabled Plan Review in triage before moving to todo", async () => {
|
||||||
const task = createTriageTask({
|
const task = createTriageTask({
|
||||||
id: "FN-PLAN-APPROVE",
|
id: "FN-PLAN-APPROVE",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
isNearDuplicateCanonicalInactive,
|
isNearDuplicateCanonicalInactive,
|
||||||
detectImageMimeFromBytes,
|
detectImageMimeFromBytes,
|
||||||
applyFrontendUxCriteria,
|
applyFrontendUxCriteria,
|
||||||
|
applyOriginalDescription,
|
||||||
extractEffectiveWriteScopeFromPrompt,
|
extractEffectiveWriteScopeFromPrompt,
|
||||||
MAX_TASK_LIST_TEXT_CHARS,
|
MAX_TASK_LIST_TEXT_CHARS,
|
||||||
upsertWorkflowStepResult,
|
upsertWorkflowStepResult,
|
||||||
@@ -2459,15 +2460,23 @@ export class TriageProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!options.preservePromptContent) {
|
if (!options.preservePromptContent) {
|
||||||
const promptWithFrontendUxCriteria = applyFrontendUxCriteria(written, parsedFileScope);
|
/*
|
||||||
if (promptWithFrontendUxCriteria !== written) {
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
After the planner writes PROMPT.md, inject the operator's original description near
|
||||||
|
the top (verbatim) so Mission/Steps rewrites never hide the source request. Runs
|
||||||
|
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 ?? "");
|
||||||
|
nextPrompt = applyFrontendUxCriteria(nextPrompt, parsedFileScope);
|
||||||
|
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");
|
||||||
try {
|
try {
|
||||||
await writeFile(promptPath, promptWithFrontendUxCriteria, "utf-8");
|
await writeFile(promptPath, nextPrompt, "utf-8");
|
||||||
written = promptWithFrontendUxCriteria;
|
written = nextPrompt;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
planLog.warn(`${task.id}: failed to write Frontend UX Criteria to PROMPT.md (${message})`);
|
planLog.warn(`${task.id}: failed to write prompt hygiene sections to PROMPT.md (${message})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3102,6 +3111,12 @@ The user did not explicitly request subtask breakdown. Default to keeping the ta
|
|||||||
- If size is uncertain at first, make a quick assessment from the available context before deciding.`;
|
- If size is uncertain at first, make a quick assessment from the available context before deciding.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:OriginalDescriptionInPrompt 2026-07-14-23:35:
|
||||||
|
Planning instructions require a top-of-PROMPT `## Original Description` with the
|
||||||
|
operator description verbatim. Deterministic finalize injection enforces the same
|
||||||
|
contract if the planner omits or rewrites it.
|
||||||
|
*/
|
||||||
return `${isRevision ? "Revise" : isFreshRespecification ? "Re-specify" : "Specify"} this task and write the result to \`${promptPath}\`.
|
return `${isRevision ? "Revise" : isFreshRespecification ? "Re-specify" : "Specify"} this task and write the result to \`${promptPath}\`.
|
||||||
|
|
||||||
## Task
|
## Task
|
||||||
@@ -3112,7 +3127,7 @@ ${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. Review the existing specification and user feedback carefully\n2. Revise the PROMPT.md to address the feedback while maintaining the structure\n3. Ensure the specification is 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. Address the user feedback without carrying forward stale assumptions from the old spec\n5. 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. The specification must be detailed enough for an autonomous AI agent to implement without asking questions\n4. Name actual files, functions, and patterns from the codebase — be specific"}
|
${isRevision ? "1. Review the existing specification and user feedback carefully\n2. Revise the PROMPT.md to address the feedback while maintaining the structure\n3. Keep `## Original Description` at the top (after title/metadata) with the operator description **verbatim**\n4. Ensure the specification is 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 user feedback without carrying forward stale assumptions from the old spec\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"}
|
||||||
|
|
||||||
Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${attachmentsSection}${userCommentsSection}`;
|
Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${attachmentsSection}${userCommentsSection}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user