FN-5754: re-triage stranded features in active mission slices

Ensure active mission slices recover stranded features by linking or auto-triaging them during scheduler and self-healing reconciliation.

- Extend mission feature reconciliation to auto-triage defined features in active slices when mission autopilot/auto-advance is enabled and no linked task can be found.
- Emit a new mission:stranded-feature-triaged audit event whenever stranded features are linked or triaged, and wire reconciliation into self-healing maintenance runs.
- Add regression coverage for scheduler and reliability interactions, and document the new backstop in AGENTS and mission docs.

Files changed:
 AGENTS.md                                          |   1 +
 docs/missions-completion-contract.md               |   1 +
 docs/missions.md                                   |   1 +
 .../mission-stranded-feature-retriage.test.ts      | 160 +++++++++++++++++++++
 packages/engine/src/__tests__/scheduler.test.ts    | 155 ++++++++++++++++++++
 packages/engine/src/run-audit.ts                   |   1 +
 packages/engine/src/runtimes/in-process-runtime.ts |   1 +
 packages/engine/src/scheduler.ts                   |  64 ++++++++-
 packages/engine/src/self-healing.ts                |  11 ++
 9 files changed, 394 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-5754

Fusion-Task-Lineage: 6b29754a-2fdf-4528-aa43-af294c4210df
This commit is contained in:
gsxdsm
2026-05-30 16:35:41 -07:00
parent 76e3cde532
commit 0e0b17f3c4
9 changed files with 394 additions and 1 deletions

View File

@@ -0,0 +1,160 @@
import { describe, expect, it, vi } from "vitest";
import type { MissionFeature, TaskStore } from "@fusion/core";
import { Scheduler } from "../../scheduler.js";
function createTaskStore(tasks: any[] = []): TaskStore {
return {
listTasks: vi.fn(async () => tasks),
getTask: vi.fn(async (taskId: string) => tasks.find((task) => task.id === taskId)),
recordRunAuditEvent: vi.fn(async () => undefined),
getSettings: vi.fn(async () => ({})),
getRootDir: vi.fn(() => "/test/project"),
on: vi.fn(),
off: vi.fn(),
} as unknown as TaskStore;
}
function feature(overrides: Partial<MissionFeature>): MissionFeature {
return {
id: "F-001",
sliceId: "SL-001",
title: "Feature one",
status: "defined",
loopState: "idle",
implementationAttemptCount: 0,
validatorAttemptCount: 0,
taskId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as MissionFeature;
}
describe("FN-5754 reliability: mission stranded feature retriage", () => {
it("triages stranded features in active autopilot slices and is idempotent", async () => {
const features = [feature({ id: "F-001" })];
const tasks: any[] = [];
const missionStore = {
listMissions: vi.fn(() => [{ id: "M-001", status: "active", autopilotEnabled: true }]),
getMissionWithHierarchy: vi.fn(() => ({
id: "M-001",
status: "active",
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features }] }],
})),
triageFeature: vi.fn(async (featureId: string) => {
const taskId = `FN-${featureId}`;
tasks.push({ id: taskId, title: "Feature one", missionId: "M-001", sliceId: "SL-001", column: "todo", status: "queued" });
features[0] = { ...features[0], taskId, status: "triaged" };
return features[0];
}),
linkFeatureToTask: vi.fn(),
updateFeatureStatus: vi.fn(),
listAssertionsForFeature: vi.fn(() => []),
};
const store = createTaskStore(tasks);
const scheduler = new Scheduler(store, { missionStore: missionStore as any });
await scheduler.reconcileAllMissionFeatures();
await scheduler.reconcileAllMissionFeatures();
expect(missionStore.triageFeature).toHaveBeenCalledTimes(1);
expect(features[0].taskId).toBe("FN-F-001");
});
it("links title-matched existing tasks without recreating", async () => {
const tasks = [{ id: "FN-001", title: "Feature one", missionId: "M-001", sliceId: "SL-001", column: "todo", status: "queued" }];
const features = [feature({ id: "F-001", taskId: undefined, status: "triaged" })];
const missionStore = {
listMissions: vi.fn(() => [{ id: "M-001", status: "active", autopilotEnabled: true }]),
getMissionWithHierarchy: vi.fn(() => ({
id: "M-001",
status: "active",
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features }] }],
})),
triageFeature: vi.fn(),
linkFeatureToTask: vi.fn((featureId: string, taskId: string) => {
features[0] = { ...features[0], id: featureId, taskId };
return features[0];
}),
updateFeatureStatus: vi.fn(),
listAssertionsForFeature: vi.fn(() => []),
};
const scheduler = new Scheduler(createTaskStore(tasks), { missionStore: missionStore as any });
await scheduler.reconcileAllMissionFeatures();
expect(missionStore.linkFeatureToTask).toHaveBeenCalledWith("F-001", "FN-001");
expect(missionStore.triageFeature).not.toHaveBeenCalled();
});
it("skips inconsistent non-defined stranded features without title match", async () => {
const features = [feature({ id: "F-001", status: "triaged", taskId: undefined })];
const missionStore = {
listMissions: vi.fn(() => [{ id: "M-001", status: "active", autopilotEnabled: true }]),
getMissionWithHierarchy: vi.fn(() => ({
id: "M-001",
status: "active",
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features }] }],
})),
triageFeature: vi.fn(),
linkFeatureToTask: vi.fn(),
updateFeatureStatus: vi.fn(),
};
const scheduler = new Scheduler(createTaskStore([]), { missionStore: missionStore as any });
await scheduler.reconcileAllMissionFeatures();
expect(missionStore.triageFeature).not.toHaveBeenCalled();
expect(missionStore.linkFeatureToTask).not.toHaveBeenCalled();
});
it("leaves non-autopilot and blocked features untouched", async () => {
const autopilotOffFeature = feature({ id: "F-001", status: "defined", taskId: undefined });
const blockedFeature = feature({ id: "F-002", status: "blocked", taskId: undefined, title: "Blocked feature" });
const missionStore = {
listMissions: vi.fn(() => [
{ id: "M-001", status: "active", autopilotEnabled: false, autoAdvance: false },
{ id: "M-002", status: "active", autopilotEnabled: true },
]),
getMissionWithHierarchy: vi.fn((missionId: string) => missionId === "M-001"
? { id: missionId, status: "active", milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features: [autopilotOffFeature] }] }] }
: { id: missionId, status: "active", milestones: [{ id: "MS-002", slices: [{ id: "SL-002", status: "active", features: [blockedFeature] }] }] }),
triageFeature: vi.fn(),
linkFeatureToTask: vi.fn(),
updateFeatureStatus: vi.fn(),
};
const scheduler = new Scheduler(createTaskStore([]), { missionStore: missionStore as any });
await scheduler.reconcileAllMissionFeatures();
expect(missionStore.triageFeature).not.toHaveBeenCalled();
expect(missionStore.linkFeatureToTask).not.toHaveBeenCalled();
});
it("emits mission:stranded-feature-triaged audit entries", async () => {
const features = [feature({ id: "F-001" })];
const tasks: any[] = [];
const store = createTaskStore(tasks);
const missionStore = {
listMissions: vi.fn(() => [{ id: "M-001", status: "active", autopilotEnabled: true }]),
getMissionWithHierarchy: vi.fn(() => ({
id: "M-001",
status: "active",
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features }] }],
})),
triageFeature: vi.fn(async () => {
features[0] = { ...features[0], taskId: "FN-001", status: "triaged" };
tasks.push({ id: "FN-001", title: "Feature one", missionId: "M-001", sliceId: "SL-001", column: "todo", status: "queued" });
return features[0];
}),
linkFeatureToTask: vi.fn(),
updateFeatureStatus: vi.fn(),
listAssertionsForFeature: vi.fn(() => []),
};
const scheduler = new Scheduler(store, { missionStore: missionStore as any });
await scheduler.reconcileAllMissionFeatures();
expect((store.recordRunAuditEvent as any).mock.calls.some(([event]: any[]) => event.mutationType === "mission:stranded-feature-triaged" && event.metadata?.featureId === "F-001" && event.metadata?.taskId === "FN-001")).toBe(true);
});
});