feat(FN-3076): add automatic completion documentation mode for tasks
Merged FN-3076 introducing an auto completion-doc mode that automates task completion documentation. The feature adds a new setting to the settings schema and types, surfaces it in the dashboard Settings UI, provides triage-stage guidance to suggest completion documentation, and is documented in the Fusion-Task-Id: FN-3076
This commit is contained in:
7
.changeset/fn-3076-auto-completion-doc-mode.md
Normal file
7
.changeset/fn-3076-auto-completion-doc-mode.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
Add a project-level `completionDocumentationMode` setting (`off`, `changeset`, `changelog`) and use it during triage prompt generation so new task specs automatically require the appropriate completion release-note artifact.
|
||||
|
||||
Also expose the setting in Dashboard → Settings → Project → General and document it in the settings reference.
|
||||
@@ -98,6 +98,7 @@ This will trigger `release.yml` to build binaries and create a GitHub Release. N
|
||||
- You can add multiple changesets per PR if you're making changes to multiple packages
|
||||
- Changeset files are automatically deleted when versions are bumped
|
||||
- CI verifies binary compilation on every push/PR to catch build regressions early
|
||||
- If your project enables `completionDocumentationMode: "changeset"`, triage specs will explicitly require `.changeset/*.md` completion artifacts for relevant tasks; keep this aligned with your repo's release convention.
|
||||
|
||||
## Internal packages
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `verificationFixRetries` | `number` | `3` | Auto-fix retry attempts when verification fails during merge. |
|
||||
| `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). |
|
||||
| `requirePlanApproval` | `boolean` | `false` | Require manual approval before planning → todo. |
|
||||
| `completionDocumentationMode` | `"off" \| "changeset" \| "changelog"` | `"off"` | Controls triage prompt injection for release-note artifacts in future task specs. `"changeset"` requires `.changeset/*.md` workflow guidance; `"changelog"` requires updating an existing changelog file (without inventing a new one); `"off"` disables this automation. |
|
||||
| `specStalenessEnabled` | `boolean` | `false` | Enforce automatic re-planning for stale plans. |
|
||||
| `specStalenessMaxAgeMs` | `number` | `21600000` | Spec staleness threshold in ms (6 hours). |
|
||||
| `taskStuckTimeoutMs` | `number` | `undefined` | Inactivity timeout for stuck-task recovery. |
|
||||
|
||||
@@ -237,6 +237,14 @@ describe("settings-export", () => {
|
||||
const result = await exportSettings(store, { source: "my-laptop" });
|
||||
expect(result.source).toBe("my-laptop");
|
||||
});
|
||||
|
||||
it("should export completionDocumentationMode in project scope", async () => {
|
||||
await store.updateSettings({ completionDocumentationMode: "changelog" });
|
||||
|
||||
const result = await exportSettings(store, { scope: "project" });
|
||||
|
||||
expect(result.project?.completionDocumentationMode).toBe("changelog");
|
||||
});
|
||||
});
|
||||
|
||||
describe("importSettings", () => {
|
||||
@@ -284,6 +292,24 @@ describe("settings-export", () => {
|
||||
expect(settings.maxWorktrees).toBe(10);
|
||||
});
|
||||
|
||||
it("should import completionDocumentationMode in project merge mode", async () => {
|
||||
await store.updateSettings({ completionDocumentationMode: "off" });
|
||||
|
||||
const importData: SettingsExportData = {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
project: { completionDocumentationMode: "changeset" },
|
||||
};
|
||||
|
||||
const result = await importSettings(store, importData, { scope: "project", merge: true });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.projectCount).toBe(1);
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.completionDocumentationMode).toBe("changeset");
|
||||
});
|
||||
|
||||
it("should import both scopes", async () => {
|
||||
const importData: SettingsExportData = {
|
||||
version: 1,
|
||||
|
||||
@@ -50,6 +50,7 @@ describe("settings key parity", () => {
|
||||
expect(isGlobalSettingsKey("maxConcurrent")).toBe(false);
|
||||
expect(isProjectSettingsKey("maxConcurrent")).toBe(true);
|
||||
expect(isProjectSettingsKey("heartbeatMultiplier")).toBe(true);
|
||||
expect(isProjectSettingsKey("completionDocumentationMode")).toBe(true);
|
||||
expect(isProjectSettingsKey("remoteAccess")).toBe(false);
|
||||
expect(isProjectSettingsKey("researchSettings")).toBe(true);
|
||||
expect(isGlobalSettingsKey("researchGlobalDefaults")).toBe(true);
|
||||
@@ -64,6 +65,10 @@ describe("settings key parity", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.heartbeatMultiplier).toBe(1);
|
||||
});
|
||||
|
||||
it("defaults completionDocumentationMode to off", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.completionDocumentationMode).toBe("off");
|
||||
});
|
||||
|
||||
it("keeps remoteAccess scoped to global settings only", () => {
|
||||
const globalKeys = GLOBAL_SETTINGS_KEYS as readonly string[];
|
||||
const projectKeys = PROJECT_SETTINGS_KEYS as readonly string[];
|
||||
|
||||
@@ -181,6 +181,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
validatorFallbackModelId: undefined,
|
||||
modelPresets: [],
|
||||
autoSelectModelPreset: false,
|
||||
completionDocumentationMode: "off",
|
||||
defaultPresetBySize: {},
|
||||
autoResolveConflicts: true,
|
||||
smartConflictResolution: true,
|
||||
|
||||
@@ -27,6 +27,10 @@ export type ExecutionMode = (typeof EXECUTION_MODES)[number];
|
||||
/** Default execution mode for new tasks */
|
||||
export const DEFAULT_EXECUTION_MODE: ExecutionMode = "standard";
|
||||
|
||||
/** Controls whether triage should require completion documentation artifacts in task specs. */
|
||||
export const COMPLETION_DOCUMENTATION_MODES = ["off", "changeset", "changelog"] as const;
|
||||
export type CompletionDocumentationMode = (typeof COMPLETION_DOCUMENTATION_MODES)[number];
|
||||
|
||||
/** Theme mode for light/dark/system preference */
|
||||
export const THEME_MODES = ["dark", "light", "system"] as const;
|
||||
export type ThemeMode = (typeof THEME_MODES)[number];
|
||||
@@ -1736,6 +1740,12 @@ export interface ProjectSettings {
|
||||
modelPresets?: ModelPreset[];
|
||||
/** When true, task creation UIs automatically recommend/apply a preset based on task size. */
|
||||
autoSelectModelPreset?: boolean;
|
||||
/** Controls whether planning specs should require release documentation artifacts on completion.
|
||||
* - "off": do not inject any release-documentation requirement
|
||||
* - "changeset": require a `.changeset/*.md` entry when relevant
|
||||
* - "changelog": require updating an existing changelog file (do not invent a new one)
|
||||
* Default: "off" */
|
||||
completionDocumentationMode?: CompletionDocumentationMode;
|
||||
/** Mapping of task sizes to preset IDs used for auto-selection during task creation. */
|
||||
defaultPresetBySize?: { S?: string; M?: string; L?: string };
|
||||
/** When true, auto-merge will automatically resolve common conflict patterns
|
||||
|
||||
@@ -1824,6 +1824,27 @@ export function SettingsModal({
|
||||
</label>
|
||||
<small>When enabled, AI-generated task specifications require manual approval before moving to Todo</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="completionDocumentationMode">Completion Documentation Automation</label>
|
||||
<select
|
||||
id="completionDocumentationMode"
|
||||
value={form.completionDocumentationMode || "off"}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
completionDocumentationMode: e.target.value as "off" | "changeset" | "changelog",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="off">Off</option>
|
||||
<option value="changeset">Require changeset (.changeset/*.md)</option>
|
||||
<option value="changelog">Require changelog update (existing changelog)</option>
|
||||
</select>
|
||||
<small>
|
||||
Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow
|
||||
<code>.changeset</code> workflows, or changelog mode when contributors should update an existing changelog file.
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="showQuickChatFAB" className="checkbox-label">
|
||||
<input
|
||||
|
||||
@@ -474,6 +474,42 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Project General", () => {
|
||||
it("renders completion documentation automation control", async () => {
|
||||
renderModal({ initialSection: "general" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const select = screen.getByLabelText("Completion Documentation Automation") as HTMLSelectElement;
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select.value).toBe("off");
|
||||
expect(screen.getByRole("option", { name: "Require changeset (.changeset/*.md)" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "Require changelog update (existing changelog)" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves completion documentation mode through project settings", async () => {
|
||||
renderModal({ initialSection: "general" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText("Completion Documentation Automation"),
|
||||
"changeset",
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const projectPayload = mockUpdateSettings.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(projectPayload.completionDocumentationMode).toBe("changeset");
|
||||
|
||||
if (mockUpdateGlobalSettings.mock.calls.length > 0) {
|
||||
const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect(globalPayload.completionDocumentationMode).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Appearance", () => {
|
||||
it("renders dashboard font size options with saved value", async () => {
|
||||
const onDashboardFontScaleChange = vi.fn();
|
||||
|
||||
@@ -159,6 +159,70 @@ describe("buildSpecificationPrompt", () => {
|
||||
expect(prompt).toContain("pnpm build");
|
||||
});
|
||||
|
||||
describe("completionDocumentationMode setting", () => {
|
||||
it("omits completion documentation guidance when mode is off", () => {
|
||||
const settings: Settings = {
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
completionDocumentationMode: "off",
|
||||
};
|
||||
|
||||
const prompt = buildSpecificationPrompt(
|
||||
baseTask,
|
||||
".fusion/tasks/KB-001/PROMPT.md",
|
||||
settings,
|
||||
);
|
||||
|
||||
expect(prompt).not.toContain("## Completion Documentation Preference");
|
||||
});
|
||||
|
||||
it("includes changeset guidance when mode is changeset", () => {
|
||||
const settings: Settings = {
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
completionDocumentationMode: "changeset",
|
||||
};
|
||||
|
||||
const prompt = buildSpecificationPrompt(
|
||||
baseTask,
|
||||
".fusion/tasks/KB-001/PROMPT.md",
|
||||
settings,
|
||||
);
|
||||
|
||||
expect(prompt).toContain("## Completion Documentation Preference");
|
||||
expect(prompt).toContain("`completionDocumentationMode` is set to `changeset`");
|
||||
expect(prompt).toContain("`.changeset/*.md`");
|
||||
expect(prompt).toContain("completion documentation/delivery expectations");
|
||||
});
|
||||
|
||||
it("includes changelog guidance when mode is changelog", () => {
|
||||
const settings: Settings = {
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
completionDocumentationMode: "changelog",
|
||||
};
|
||||
|
||||
const prompt = buildSpecificationPrompt(
|
||||
baseTask,
|
||||
".fusion/tasks/KB-001/PROMPT.md",
|
||||
settings,
|
||||
);
|
||||
|
||||
expect(prompt).toContain("`completionDocumentationMode` is set to `changelog`");
|
||||
expect(prompt).toContain("updating an existing changelog file");
|
||||
expect(prompt).toContain("do not invent a new changelog file");
|
||||
});
|
||||
});
|
||||
|
||||
it("generates revision prompt when existingPrompt and feedback provided", () => {
|
||||
const existingPrompt = "# Original Spec\n\nOriginal content.";
|
||||
const feedback = "Add more details about error handling";
|
||||
|
||||
@@ -2129,6 +2129,18 @@ export function buildSpecificationPrompt(
|
||||
commandsSection = "\n\n" + lines.join("\n");
|
||||
}
|
||||
|
||||
const completionDocumentationMode = settings?.completionDocumentationMode ?? "off";
|
||||
let completionDocumentationSection = "";
|
||||
if (completionDocumentationMode !== "off") {
|
||||
const instruction = completionDocumentationMode === "changeset"
|
||||
? "If the task changes published-package behavior, require a `.changeset/*.md` entry and call out the repository's changeset workflow."
|
||||
: "Require updating an existing changelog file as part of completion; do not invent a new changelog file when none exists.";
|
||||
completionDocumentationSection = `\n\n## Completion Documentation Preference\nProject setting \`completionDocumentationMode\` is set to \`${completionDocumentationMode}\`.
|
||||
|
||||
When writing PROMPT.md, add this as an explicit requirement under completion documentation/delivery expectations (not a side note):
|
||||
- ${instruction}`;
|
||||
}
|
||||
|
||||
// Build project memory section from settings.
|
||||
// When enabled, agents consult project memory for durable project learnings.
|
||||
// Backend-aware: instructions branch based on memoryBackendType (file, readonly, qmd)
|
||||
@@ -2277,5 +2289,5 @@ ${task.dependencies.length > 0 ? `- **Dependencies:** ${task.dependencies.join("
|
||||
## 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. Write a fresh complete PROMPT.md specification to the given path following the format in your system prompt\n3. Address the user feedback without carrying forward stale assumptions from the old spec\n4. 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"}
|
||||
|
||||
Use the write tool to write the specification file.${commandsSection}${memorySection}${attachmentsSection}${userCommentsSection}`;
|
||||
Use the write tool to write the specification file.${commandsSection}${completionDocumentationSection}${memorySection}${attachmentsSection}${userCommentsSection}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user