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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -397,6 +397,7 @@ describe("Scheduler", () => {
|
||||
title: "Linked feature",
|
||||
status: "triaged",
|
||||
})),
|
||||
triageFeature: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -4487,6 +4488,160 @@ describe("Scheduler", () => {
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it("triages defined stranded features for active autopilot slices", async () => {
|
||||
const triagedFeature = {
|
||||
id: "F-001",
|
||||
sliceId: "SL-001",
|
||||
title: "Feature one",
|
||||
taskId: "FN-TRIAGED",
|
||||
status: "triaged",
|
||||
};
|
||||
const store = createMockStore({
|
||||
getTask: vi.fn().mockResolvedValue(createMockTask({ id: "FN-TRIAGED", column: "todo" })),
|
||||
});
|
||||
const mockMissionStore = createMockMissionStore({
|
||||
listMissions: vi.fn().mockReturnValue([{ id: "M-001", status: "active", autopilotEnabled: true }]),
|
||||
getMissionWithHierarchy: vi.fn().mockReturnValue({
|
||||
id: "M-001",
|
||||
status: "active",
|
||||
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features: [{ id: "F-001", sliceId: "SL-001", title: "Feature one", taskId: undefined, status: "defined" }] }] }],
|
||||
}),
|
||||
triageFeature: vi.fn().mockResolvedValue(triagedFeature),
|
||||
updateFeatureStatus: vi.fn(),
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
|
||||
const result = await scheduler.reconcileAllMissionFeatures();
|
||||
|
||||
expect(mockMissionStore.triageFeature).toHaveBeenCalledWith("F-001");
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it("does not triage stranded features for non-autopilot missions", async () => {
|
||||
const store = createMockStore({ getTask: vi.fn() });
|
||||
const mockMissionStore = createMockMissionStore({
|
||||
listMissions: vi.fn().mockReturnValue([{ id: "M-001", status: "active", autopilotEnabled: false, autoAdvance: false }]),
|
||||
getMissionWithHierarchy: vi.fn().mockReturnValue({
|
||||
id: "M-001",
|
||||
status: "active",
|
||||
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features: [{ id: "F-001", sliceId: "SL-001", title: "Feature one", taskId: undefined, status: "defined" }] }] }],
|
||||
}),
|
||||
triageFeature: vi.fn(),
|
||||
updateFeatureStatus: vi.fn(),
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
|
||||
const result = await scheduler.reconcileAllMissionFeatures();
|
||||
|
||||
expect(mockMissionStore.triageFeature).not.toHaveBeenCalled();
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("links title-matched stranded features instead of re-triaging", async () => {
|
||||
const matchedTask = createMockTask({
|
||||
id: "FN-1703",
|
||||
title: "Feature one",
|
||||
missionId: "M-001",
|
||||
sliceId: "SL-001",
|
||||
column: "todo",
|
||||
} as Partial<Task>);
|
||||
const store = createMockStore({
|
||||
listTasks: vi.fn().mockResolvedValue([matchedTask]),
|
||||
getTask: vi.fn().mockResolvedValue(matchedTask),
|
||||
});
|
||||
const mockMissionStore = createMockMissionStore({
|
||||
listMissions: vi.fn().mockReturnValue([{ id: "M-001", status: "active", autopilotEnabled: true }]),
|
||||
getMissionWithHierarchy: vi.fn().mockReturnValue({
|
||||
id: "M-001",
|
||||
status: "active",
|
||||
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features: [{ id: "F-001", sliceId: "SL-001", title: "Feature one", taskId: undefined, status: "triaged" }] }] }],
|
||||
}),
|
||||
triageFeature: vi.fn(),
|
||||
linkFeatureToTask: vi.fn().mockReturnValue({ id: "F-001", sliceId: "SL-001", title: "Feature one", taskId: "FN-1703", status: "triaged" }),
|
||||
updateFeatureStatus: vi.fn(),
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
|
||||
const result = await scheduler.reconcileAllMissionFeatures();
|
||||
|
||||
expect(mockMissionStore.linkFeatureToTask).toHaveBeenCalledWith("F-001", "FN-1703");
|
||||
expect(mockMissionStore.triageFeature).not.toHaveBeenCalled();
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it("skips inconsistent non-defined stranded features with no title match", async () => {
|
||||
const store = createMockStore({ getTask: vi.fn() });
|
||||
const mockMissionStore = createMockMissionStore({
|
||||
listMissions: vi.fn().mockReturnValue([{ id: "M-001", status: "active", autopilotEnabled: true }]),
|
||||
getMissionWithHierarchy: vi.fn().mockReturnValue({
|
||||
id: "M-001",
|
||||
status: "active",
|
||||
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features: [{ id: "F-001", sliceId: "SL-001", title: "Feature one", taskId: undefined, status: "triaged" }] }] }],
|
||||
}),
|
||||
triageFeature: vi.fn(),
|
||||
updateFeatureStatus: vi.fn(),
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
|
||||
const result = await scheduler.reconcileAllMissionFeatures();
|
||||
|
||||
expect(mockMissionStore.triageFeature).not.toHaveBeenCalled();
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("skips blocked stranded features", async () => {
|
||||
const store = createMockStore({ getTask: vi.fn() });
|
||||
const mockMissionStore = createMockMissionStore({
|
||||
listMissions: vi.fn().mockReturnValue([{ id: "M-001", status: "active", autopilotEnabled: true }]),
|
||||
getMissionWithHierarchy: vi.fn().mockReturnValue({
|
||||
id: "M-001",
|
||||
status: "active",
|
||||
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features: [{ id: "F-001", sliceId: "SL-001", title: "Feature one", taskId: undefined, status: "blocked" }] }] }],
|
||||
}),
|
||||
triageFeature: vi.fn(),
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
|
||||
const result = await scheduler.reconcileAllMissionFeatures();
|
||||
|
||||
expect(mockMissionStore.triageFeature).not.toHaveBeenCalled();
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("emits stranded-feature audit events for triaged features", async () => {
|
||||
const triagedFeature = {
|
||||
id: "F-001",
|
||||
sliceId: "SL-001",
|
||||
title: "Feature one",
|
||||
taskId: "FN-TRIAGED",
|
||||
status: "triaged",
|
||||
};
|
||||
const recordRunAuditEvent = vi.fn().mockResolvedValue(undefined);
|
||||
const store = createMockStore({
|
||||
recordRunAuditEvent,
|
||||
getTask: vi.fn().mockResolvedValue(createMockTask({ id: "FN-TRIAGED", column: "todo" })),
|
||||
});
|
||||
const mockMissionStore = createMockMissionStore({
|
||||
listMissions: vi.fn().mockReturnValue([{ id: "M-001", status: "active", autopilotEnabled: true }]),
|
||||
getMissionWithHierarchy: vi.fn().mockReturnValue({
|
||||
id: "M-001",
|
||||
status: "active",
|
||||
milestones: [{ id: "MS-001", slices: [{ id: "SL-001", status: "active", features: [{ id: "F-001", sliceId: "SL-001", title: "Feature one", taskId: undefined, status: "defined" }] }] }],
|
||||
}),
|
||||
triageFeature: vi.fn().mockResolvedValue(triagedFeature),
|
||||
});
|
||||
|
||||
const scheduler = new Scheduler(store, { missionStore: mockMissionStore as any });
|
||||
await scheduler.reconcileAllMissionFeatures();
|
||||
|
||||
expect(recordRunAuditEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
domain: "database",
|
||||
mutationType: "mission:stranded-feature-triaged",
|
||||
target: "F-001",
|
||||
metadata: expect.objectContaining({ missionId: "M-001", sliceId: "SL-001", featureId: "F-001", taskId: "FN-TRIAGED" }),
|
||||
}));
|
||||
});
|
||||
|
||||
it("skips features without taskId", async () => {
|
||||
const store = createMockStore({
|
||||
getTask: vi.fn(),
|
||||
|
||||
@@ -414,6 +414,7 @@ export type DatabaseMutationType =
|
||||
| "verification:followup-deduped"
|
||||
/** Metadata: { kind, parentTaskId, newTaskId, signature, supersedesTaskId } */
|
||||
| "verification:followup-created"
|
||||
| "mission:stranded-feature-triaged"
|
||||
| "task:auto-recover-branch-misbound"
|
||||
| "task:auto-recover-misrouted-foreign-commit"
|
||||
| "task:auto-recover-foreign-only-contamination"
|
||||
|
||||
@@ -732,6 +732,7 @@ export class InProcessRuntime
|
||||
getActiveMergeTaskId: () => this.activeMergeTaskIdProvider?.() ?? null,
|
||||
leaseManager: this.leaseManager,
|
||||
hasActiveAgentExecution: (agentId: string) => this.heartbeatMonitor?.getTrackedAgents().includes(agentId) ?? false,
|
||||
reconcileAllMissionFeatures: async () => this.scheduler.reconcileAllMissionFeatures(),
|
||||
chatStore: this.chatStore,
|
||||
messageStore: this.messageStore,
|
||||
restartDurableAgentHeartbeat: async (agentId: string, context: { reason: string; attempt: number }) => {
|
||||
|
||||
@@ -2033,10 +2033,11 @@ export class Scheduler {
|
||||
.filter((slice) => slice.status === "active");
|
||||
|
||||
for (const slice of activeSlices) {
|
||||
const missionAutoTriageEnabled = mission.autopilotEnabled === true || mission.autoAdvance === true;
|
||||
|
||||
for (const feature of slice.features) {
|
||||
let featureForReconciliation = feature;
|
||||
let task: Task | undefined;
|
||||
|
||||
if (feature.taskId) {
|
||||
task = await this.store.getTask(feature.taskId);
|
||||
} else {
|
||||
@@ -2050,6 +2051,36 @@ export class Scheduler {
|
||||
featureForReconciliation = missionStore.linkFeatureToTask(feature.id, matchedTask.id);
|
||||
task = matchedTask;
|
||||
totalFixed++;
|
||||
await this.emitStrandedFeatureTriageAudit(mission.id, slice.id, feature.id, matchedTask.id);
|
||||
} else if (
|
||||
missionAutoTriageEnabled
|
||||
&& feature.status !== "blocked"
|
||||
) {
|
||||
if (feature.status === "defined") {
|
||||
try {
|
||||
featureForReconciliation = await missionStore.triageFeature(feature.id);
|
||||
task = featureForReconciliation.taskId
|
||||
? await this.store.getTask(featureForReconciliation.taskId)
|
||||
: undefined;
|
||||
totalFixed++;
|
||||
if (featureForReconciliation.taskId) {
|
||||
await this.emitStrandedFeatureTriageAudit(
|
||||
mission.id,
|
||||
slice.id,
|
||||
featureForReconciliation.id,
|
||||
featureForReconciliation.taskId,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
schedulerLog.warn(
|
||||
`Failed to triage stranded feature ${feature.id} during reconciliation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
schedulerLog.warn(
|
||||
`Skipping stranded feature ${feature.id} with status ${feature.status}: no linked task and no title-matched task available`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2095,6 +2126,37 @@ export class Scheduler {
|
||||
return totalFixed;
|
||||
}
|
||||
|
||||
private async emitStrandedFeatureTriageAudit(
|
||||
missionId: string,
|
||||
sliceId: string,
|
||||
featureId: string,
|
||||
taskId: string,
|
||||
): Promise<void> {
|
||||
const auditor = createRunAuditor(this.store, {
|
||||
runId: generateSyntheticRunId("scheduler-mission-stranded-feature", featureId),
|
||||
agentId: "scheduler",
|
||||
taskId,
|
||||
phase: "mission-stranded-feature-reconcile",
|
||||
});
|
||||
|
||||
try {
|
||||
await auditor.database({
|
||||
type: "mission:stranded-feature-triaged",
|
||||
target: featureId,
|
||||
metadata: {
|
||||
missionId,
|
||||
sliceId,
|
||||
featureId,
|
||||
taskId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
schedulerLog.warn(
|
||||
`Feature ${featureId} failed to emit stranded-feature triage audit: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getMissionFeatureTitleKey(sliceId: string, title: string): string {
|
||||
return `${sliceId}\0${this.normalizeMissionFeatureTitle(title)}`;
|
||||
}
|
||||
|
||||
@@ -274,6 +274,8 @@ export interface SelfHealingOptions {
|
||||
/** Optional notifier for board-stall unrecovered alerts. */
|
||||
ntfyNotifier?: Pick<NtfyNotifier, "notifyBoardStallUnrecovered">;
|
||||
getProjectId?: () => string;
|
||||
/** Optional callback to reconcile active mission features during maintenance. */
|
||||
reconcileAllMissionFeatures?: () => Promise<number>;
|
||||
}
|
||||
|
||||
const APPROVED_TRIAGE_RECOVERY_GRACE_MS = 60_000;
|
||||
@@ -1468,6 +1470,15 @@ export class SelfHealingManager {
|
||||
} else {
|
||||
// Batch 2 — Task recovery (operations are independent of each other)
|
||||
const batch2Fns: Array<{ name: string; fn: () => Promise<unknown> }> = [
|
||||
{
|
||||
name: "reconcile-mission-features",
|
||||
fn: async () => {
|
||||
if (!this.options.reconcileAllMissionFeatures) {
|
||||
return;
|
||||
}
|
||||
await this.options.reconcileAllMissionFeatures();
|
||||
},
|
||||
},
|
||||
{ name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
|
||||
{ name: "recover-stranded-completed-todo", fn: () => this.recoverStrandedCompletedTodoTasks() },
|
||||
{ name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
|
||||
|
||||
Reference in New Issue
Block a user