FN-5762: add mission hierarchy delete tools with linked-task guards

Add mission delete tooling across core, dashboard routes, and CLI extension surfaces.

- Add `fn_feature_delete`, `fn_slice_delete`, and `fn_milestone_delete` tools to the CLI extension with `force` override support.
- Enforce live-task linkage guards for feature/slice/milestone deletes in mission store logic and expose conflict behavior through mission routes.
- Extend coverage and docs for delete behavior, allowlist/gating metadata, and published Fusion skill/tool references.

Files changed:
 .changeset/fn-5762-feature-delete.md               |   7 +
 docs/missions.md                                   |  15 +++
 packages/cli/skill/fusion/SKILL.md                 |   2 +-
 .../cli/skill/fusion/references/extension-tools.md |  27 ++++
 .../skill/fusion/references/fusion-capabilities.md |   3 +
 packages/cli/src/__tests__/extension.test.ts       |  34 +++++
 packages/cli/src/extension.ts                      | 106 +++++++++++++++
 packages/core/src/__tests__/mission-store.test.ts  | 146 ++++++++++++++++++++-
 packages/core/src/mission-store.ts                 |  98 ++++++++++++--
 .../dashboard/src/__tests__/mission-e2e.test.ts    |  62 +++++++--
 packages/dashboard/src/mission-routes.ts           |  42 +++++-
 .../workflow-step-readonly-allowlist.test.ts       |   3 +
 packages/engine/src/gating-classifications.ts      |   3 +
 13 files changed, 514 insertions(+), 34 deletions(-)

Fusion-Task-Id: FN-5762
Fusion-Task-Lineage: 9c6eceff-dcb4-4a2b-90c6-59cb0228ca6d
This commit is contained in:
gsxdsm
2026-05-30 23:55:05 -07:00
parent 93d3a58cb8
commit 0ffe7f0604
13 changed files with 516 additions and 36 deletions

View File

@@ -220,6 +220,9 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
"fn_milestone_add",
"fn_slice_add",
"fn_feature_add",
"fn_feature_delete",
"fn_slice_delete",
"fn_milestone_delete",
"fn_slice_activate",
"fn_feature_link_task",
"fn_feature_update",
@@ -1334,6 +1337,37 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
});
});
describe("fn_feature_delete", () => {
it("deletes feature, guards linked task, and handles missing feature", async () => {
const missionTool = api.tools.get("fn_mission_create")!;
const milestoneTool = api.tools.get("fn_milestone_add")!;
const sliceTool = api.tools.get("fn_slice_add")!;
const featureTool = api.tools.get("fn_feature_add")!;
const createTaskTool = api.tools.get("fn_task_create")!;
const linkTool = api.tools.get("fn_feature_link_task")!;
const deleteTool = api.tools.get("fn_feature_delete")!;
const mission = await missionTool.execute("m1", { title: "Mission" }, undefined, undefined, makeCtx(tmpDir));
const milestone = await milestoneTool.execute("ms1", { missionId: mission.details.missionId, title: "Milestone" }, undefined, undefined, makeCtx(tmpDir));
const slice = await sliceTool.execute("sl1", { milestoneId: milestone.details.milestoneId, title: "Slice" }, undefined, undefined, makeCtx(tmpDir));
const feature = await featureTool.execute("f1", { sliceId: slice.details.sliceId, title: "Feature" }, undefined, undefined, makeCtx(tmpDir));
const task = await createTaskTool.execute("t1", { description: "Task for feature" }, undefined, undefined, makeCtx(tmpDir));
await linkTool.execute("l1", { featureId: feature.details.featureId, taskId: task.details.taskId }, undefined, undefined, makeCtx(tmpDir));
const guarded = await deleteTool.execute("d1", { featureId: feature.details.featureId }, undefined, undefined, makeCtx(tmpDir));
expect(guarded.isError).toBe(true);
expect(guarded.content[0].text).toContain("linked to task");
const forced = await deleteTool.execute("d2", { featureId: feature.details.featureId, force: true }, undefined, undefined, makeCtx(tmpDir));
expect(forced.isError).not.toBe(true);
expect(forced.content[0].text).toContain("Deleted");
const missing = await deleteTool.execute("d3", { featureId: feature.details.featureId }, undefined, undefined, makeCtx(tmpDir));
expect(missing.isError).toBe(true);
expect(missing.content[0].text).toContain("not found");
});
});
describe("fn_slice_activate", () => {
it("returns error when slice is already active", async () => {
const missionTool = api.tools.get("fn_mission_create")!;

View File

@@ -2919,6 +2919,112 @@ export default function kbExtension(pi: ExtensionAPI) {
},
});
// ── fn_feature_delete ────────────────────────────────────────────
pi.registerTool({
name: "fn_feature_delete",
label: "fn: Delete Feature",
description: "Delete a feature. Rejects deletion when linked to a live task unless force=true.",
promptSnippet: "Delete a mission feature",
promptGuidelines: [
"Use force=true only when intentionally overriding linked live-task guards",
"Deleting a feature is permanent and cannot be undone",
],
parameters: Type.Object({
featureId: Type.String({ description: "Feature ID to delete (e.g., F-001)" }),
force: Type.Optional(Type.Boolean({ description: "Override linked-task guard" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const store = await getStore(ctx.cwd);
const missionStore = store.getMissionStore();
try {
missionStore.deleteFeature(params.featureId, params.force === true);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: message }],
isError: true,
details: { error: message },
};
}
return {
content: [{ type: "text", text: `Deleted ${params.featureId}` }],
details: { featureId: params.featureId, force: params.force === true },
};
},
});
// ── fn_slice_delete ────────────────────────────────────────────
pi.registerTool({
name: "fn_slice_delete",
label: "fn: Delete Slice",
description: "Delete a slice and its features. Rejects deletion when child features link to live tasks unless force=true.",
promptSnippet: "Delete a mission slice",
parameters: Type.Object({
sliceId: Type.String({ description: "Slice ID to delete (e.g., SL-001)" }),
force: Type.Optional(Type.Boolean({ description: "Override linked-task guard" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const store = await getStore(ctx.cwd);
const missionStore = store.getMissionStore();
try {
missionStore.deleteSlice(params.sliceId, params.force === true);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: message }],
isError: true,
details: { error: message },
};
}
return {
content: [{ type: "text", text: `Deleted ${params.sliceId}` }],
details: { sliceId: params.sliceId, force: params.force === true },
};
},
});
// ── fn_milestone_delete ────────────────────────────────────────────
pi.registerTool({
name: "fn_milestone_delete",
label: "fn: Delete Milestone",
description: "Delete a milestone and all descendant slices/features. Rejects deletion when child features link to live tasks unless force=true.",
promptSnippet: "Delete a mission milestone",
parameters: Type.Object({
milestoneId: Type.String({ description: "Milestone ID to delete (e.g., MS-001)" }),
force: Type.Optional(Type.Boolean({ description: "Override linked-task guard" })),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const store = await getStore(ctx.cwd);
const missionStore = store.getMissionStore();
try {
missionStore.deleteMilestone(params.milestoneId, params.force === true);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: message }],
isError: true,
details: { error: message },
};
}
return {
content: [{ type: "text", text: `Deleted ${params.milestoneId}` }],
details: { milestoneId: params.milestoneId, force: params.force === true },
};
},
});
// ── fn_slice_activate ────────────────────────────────────────────
pi.registerTool({