FN-5714: reject linking features to non-active tasks

Gracefully handle feature-to-task linking when the referenced task is archived, deleted, or missing.

- enforce active-board task validation in mission store linkage path with clear error text
- return surfaced validation errors from fn_feature_link_task instead of throwing opaque failures
- add regression coverage in core and CLI extension tests for archived/non-active task references
- add changeset and sync Fusion skill reference docs with the new fn_feature_link_task behavior

Files changed:
 .changeset/fn-5714-link-archived-task.md           |  5 +++
 .../cli/skill/fusion/references/extension-tools.md |  2 +-
 .../skill/fusion/references/fusion-capabilities.md |  2 +-
 packages/cli/src/__tests__/extension.test.ts       | 50 ++++++++++++++++++++++
 packages/cli/src/extension.ts                      | 35 +++++++++------
 packages/core/src/__tests__/mission-store.test.ts  | 17 ++++++++
 packages/core/src/mission-store.ts                 |  9 ++++
 7 files changed, 106 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-5714

Fusion-Task-Lineage: d84c45cf-0a0d-47a3-81c5-f99d16e03ecf
This commit is contained in:
gsxdsm
2026-05-30 07:39:59 -07:00
parent 3b594879a8
commit 033f74c458
7 changed files with 106 additions and 14 deletions

View File

@@ -1244,6 +1244,23 @@ describe("MissionStore", () => {
expect(taskRow.sliceId).toBe(slice.id);
});
it("throws a clear error when linking to a task not on the active board", () => {
const mission = store.createMission({ title: "Mission" });
const milestone = store.addMilestone(mission.id, { title: "Milestone" });
const slice = store.addSlice(milestone.id, { title: "Slice" });
const feature = store.addFeature(slice.id, { title: "Linkable" });
expect(() => store.linkFeatureToTask(feature.id, "FN-ARCHIVED")).toThrow(
`Cannot link feature ${feature.id} to task FN-ARCHIVED: task is not on the active board (it may be archived, deleted, or never existed). Only active tasks can be linked to features.`,
);
const unchanged = store.getFeature(feature.id)!;
expect(unchanged.taskId).toBeUndefined();
expect(unchanged.status).toBe("defined");
expect(unchanged.loopState).toBe("idle");
expect(unchanged.implementationAttemptCount).toBe(0);
});
it("emits feature:linked event", () => {
createTaskInDb(db, "FN-001");

View File

@@ -2085,6 +2085,15 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
throw new Error(`Feature ${featureId} not found`);
}
const liveTask = this.db
.prepare(`SELECT id FROM tasks WHERE id = ? AND "deletedAt" IS NULL`)
.get(taskId) as { id: string } | undefined;
if (!liveTask) {
throw new Error(
`Cannot link feature ${featureId} to task ${taskId}: task is not on the active board (it may be archived, deleted, or never existed). Only active tasks can be linked to features.`,
);
}
const linkage = this.resolveTaskLinkage(feature.sliceId);
// When first linking (loopState is idle or falsy), transition to implementing