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:
Fusion
2026-05-02 07:34:34 -07:00
committed by gsxdsm
parent 9d1736fc64
commit 6ae7aefaf4
11 changed files with 185 additions and 1 deletions

View File

@@ -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,

View File

@@ -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[];

View File

@@ -181,6 +181,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
validatorFallbackModelId: undefined,
modelPresets: [],
autoSelectModelPreset: false,
completionDocumentationMode: "off",
defaultPresetBySize: {},
autoResolveConflicts: true,
smartConflictResolution: true,

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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";

View File

@@ -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}`;
}