feat(FN-5584): merge fusion/fn-5584

This commit is contained in:
gsxdsm
2026-05-25 15:27:06 -07:00
parent 06a107dcba
commit 5eacd79b43
29 changed files with 329 additions and 57 deletions

View File

@@ -132,6 +132,7 @@ Create a task via AI-guided planning mode — interactive conversation to refine
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `description` | string | — | Initial plan description (optional) — the AI will ask clarifying questions if not provided |
| `baseBranch` | string | — | Optional base branch for the task created from this planning session |
## GitHub Tools
@@ -177,6 +178,7 @@ Create a new mission — a high-level objective that can span multiple milestone
| `title` | string | ✓ | Mission title — brief but descriptive |
| `description` | string | — | Detailed mission objectives and context |
| `autoAdvance` | boolean | — | Automatically activate the next pending slice when the current slice completes |
| `baseBranch` | string | — | Optional integration base branch for tasks triaged from this mission |
### fn_mission_list

View File

@@ -511,6 +511,7 @@ describe("bin command routing and fallbacks", () => {
"Test Mission",
"Detailed mission description",
"demo",
undefined,
);
});

View File

@@ -268,7 +268,7 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
makeCtx(tmpDir),
);
expect(runTaskPlan).toHaveBeenCalledWith("Plan a project task", true);
expect(runTaskPlan).toHaveBeenCalledWith("Plan a project task", true, undefined, undefined);
expect(result.details.taskId).toBe("PROJ-042");
expect(result.content[0].text).toContain("Task PROJ-042");
});
@@ -1031,7 +1031,7 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
const tool = api.tools.get("fn_mission_create")!;
const result = await tool.execute(
"call-1",
{ title: "Test Mission", description: "Test description", autoAdvance: true },
{ title: "Test Mission", description: "Test description", autoAdvance: true, baseBranch: "develop" },
undefined,
undefined,
makeCtx(tmpDir),
@@ -1043,6 +1043,11 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
expect(result.content[0].text).toContain("Created");
expect(result.content[0].text).toContain("Test Mission");
expect(result.content[0].text).toContain("Auto-advance: enabled");
const store = new TaskStore(tmpDir);
await store.init();
const mission = store.getMissionStore().getMission(result.details.missionId);
expect(mission?.baseBranch).toBe("develop");
});
});

View File

@@ -1029,16 +1029,20 @@ async function main() {
case "plan": {
const planArgs = args.slice(2);
const yesFlag = planArgs.includes("--yes");
let baseBranch: string | undefined;
const descParts: string[] = [];
for (let i = 0; i < planArgs.length; i++) {
if (planArgs[i] === "--yes") {
continue; // skip flag
} else if (planArgs[i] === "--base-branch" && i + 1 < planArgs.length) {
baseBranch = planArgs[i + 1];
i++;
} else {
descParts.push(planArgs[i]);
}
}
const initialPlan = descParts.join(" ");
await runTaskPlan(initialPlan || undefined, yesFlag, projectName);
await runTaskPlan(initialPlan || undefined, yesFlag, projectName, baseBranch);
break;
}
case "list":
@@ -1283,9 +1287,20 @@ async function main() {
const subcommand = args[1];
switch (subcommand) {
case "create": {
const title = args[2];
const description = args.length > 3 ? args.slice(3).join(" ") : undefined;
await runMissionCreate(title, description, projectName);
const createArgs = args.slice(2);
let baseBranch: string | undefined;
const positional: string[] = [];
for (let i = 0; i < createArgs.length; i++) {
if (createArgs[i] === "--base-branch" && i + 1 < createArgs.length) {
baseBranch = createArgs[i + 1];
i++;
} else {
positional.push(createArgs[i]);
}
}
const title = positional[0];
const description = positional.length > 1 ? positional.slice(1).join(" ") : undefined;
await runMissionCreate(title, description, projectName, baseBranch);
break;
}
case "list":

View File

@@ -199,6 +199,7 @@ describe("mission commands", () => {
expect(mockMissionStore.createMission).toHaveBeenCalledWith({
title: "Test Mission",
description: "Test description",
baseBranch: undefined,
});
expect(consoleCapture.logs).toContain(" ✓ Created M-001: Test Mission");
} finally {
@@ -206,6 +207,27 @@ describe("mission commands", () => {
}
});
it("passes baseBranch when provided", async () => {
const mockMissionStore = createMockMissionStore();
vi.mocked(getStore).mockResolvedValue({
getMissionStore: () => mockMissionStore,
} as any);
const consoleCapture = captureConsole();
try {
await runMissionCreate("Test Mission", "Test description", undefined, "develop");
expect(mockMissionStore.createMission).toHaveBeenCalledWith({
title: "Test Mission",
description: "Test description",
baseBranch: "develop",
});
} finally {
consoleCapture.restore();
}
});
it("creates mission with title only (no description)", async () => {
const mockMissionStore = createMockMissionStore();
vi.mocked(getStore).mockResolvedValue({
@@ -220,6 +242,7 @@ describe("mission commands", () => {
expect(mockMissionStore.createMission).toHaveBeenCalledWith({
title: "Test Mission",
description: undefined,
baseBranch: undefined,
});
} finally {
consoleCapture.restore();
@@ -250,6 +273,7 @@ describe("mission commands", () => {
expect(mockMissionStore.createMission).toHaveBeenCalledWith({
title: "Interactive Title",
description: "Interactive Description",
baseBranch: undefined,
});
} finally {
consoleCapture.restore();

View File

@@ -67,7 +67,12 @@ async function promptForTitleAndDescription(
* Create a new mission with optional title and description.
* If arguments are omitted, prompts interactively.
*/
export async function runMissionCreate(titleArg?: string, descriptionArg?: string, projectName?: string) {
export async function runMissionCreate(
titleArg?: string,
descriptionArg?: string,
projectName?: string,
baseBranch?: string,
) {
const store = await getStore({ project: projectName });
const missionStore = store.getMissionStore();
@@ -82,6 +87,7 @@ export async function runMissionCreate(titleArg?: string, descriptionArg?: strin
const mission = missionStore.createMission({
title,
description,
baseBranch: baseBranch?.trim() || undefined,
});
console.log();

View File

@@ -1815,7 +1815,12 @@ function wrapText(text: string, width: number): string[] {
}
/** Run the planning mode */
export async function runTaskPlan(initialPlanArg?: string, yesFlag = false, projectName?: string): Promise<string | undefined> {
export async function runTaskPlan(
initialPlanArg?: string,
yesFlag = false,
projectName?: string,
baseBranch?: string,
): Promise<string | undefined> {
let initialPlan = initialPlanArg;
// If no initial plan, prompt interactively
@@ -1954,6 +1959,7 @@ export async function runTaskPlan(initialPlanArg?: string, yesFlag = false, proj
description: result.data.description,
column: "triage",
dependencies: result.data.suggestedDependencies,
baseBranch: baseBranch?.trim() || undefined,
source: { sourceType: "cli" },
});

View File

@@ -1502,6 +1502,7 @@ export default function kbExtension(pi: ExtensionAPI) {
description: "Initial plan description (optional) — the AI will ask clarifying questions if not provided",
})
),
baseBranch: Type.Optional(Type.String({ description: "Optional base branch for the task created from this planning session" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
@@ -1526,7 +1527,7 @@ export default function kbExtension(pi: ExtensionAPI) {
let taskId: string | undefined;
try {
taskId = await runTaskPlan(params.description, true); // Use --yes flag for non-interactive
taskId = await runTaskPlan(params.description, true, undefined, params.baseBranch); // Use --yes flag for non-interactive
} catch (err) {
console.error = originalError;
console.log = originalLog;
@@ -2258,6 +2259,7 @@ export default function kbExtension(pi: ExtensionAPI) {
autoAdvance: Type.Optional(
Type.Boolean({ description: "Automatically activate the next pending slice when the current slice completes" })
),
baseBranch: Type.Optional(Type.String({ description: "Optional integration base branch for tasks triaged from this mission" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -2267,6 +2269,7 @@ export default function kbExtension(pi: ExtensionAPI) {
const mission = missionStore.createMission({
title: params.title.trim(),
description: params.description?.trim(),
baseBranch: params.baseBranch?.trim() || undefined,
});
if (params.autoAdvance !== undefined) {