fix(engine): skip task-completion validation for inactive missions
processTaskOutcome validated every completed feature-linked task with no mission-status check, unlike recoverActiveMissions (which already skips missions with status !== "active"). A parked mission (status=planning) kept minting validator runs — and "Fix:" features on failure — for tasks completed after parking, spiraling Fix^n features from a stale validator workspace. Gate processTaskOutcome on the resolved mission being active, before the needs_fix -> implementing transition so parked missions get zero feature state mutation. The skip logs a warning mission event (validation_skipped_mission_inactive). Features that don't resolve to a mission keep the current behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Honor mission branchStrategy when triage omits branchAssignment; skip validation for inactive missions.
|
||||||
|
category: fix
|
||||||
|
dev: resolveBranchAssignmentContext returns undefined for absent mode so triage falls back to mission.branchStrategy; processTaskOutcome gates on mission.status === "active" like recoverActiveMissions.
|
||||||
@@ -823,6 +823,56 @@ describe("MissionExecutionLoop", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips validation when the feature's mission is not active", async () => {
|
||||||
|
missionStore._setMission(createMockMission({ id: "M-TEST1", status: "planning" }));
|
||||||
|
const feature = createMockFeature({ loopState: "implementing", taskId: "FN-001" });
|
||||||
|
missionStore._setFeature(feature);
|
||||||
|
missionStore.getFeatureByTaskId = vi.fn().mockReturnValue(feature);
|
||||||
|
|
||||||
|
loop = new MissionExecutionLoop({
|
||||||
|
taskStore: taskStore as any,
|
||||||
|
missionStore: missionStore as any,
|
||||||
|
rootDir: "/tmp",
|
||||||
|
});
|
||||||
|
loop.start();
|
||||||
|
|
||||||
|
await loop.processTaskOutcome("FN-001");
|
||||||
|
|
||||||
|
expect(missionStore.startValidatorRun).not.toHaveBeenCalled();
|
||||||
|
expect(missionStore.logMissionEvent).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
"warning",
|
||||||
|
expect.stringContaining("Validation skipped"),
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "validation_skipped_mission_inactive",
|
||||||
|
featureId: "F-001",
|
||||||
|
taskId: "FN-001",
|
||||||
|
missionId: "M-TEST1",
|
||||||
|
missionStatus: "planning",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates when the feature's mission is active", async () => {
|
||||||
|
missionStore._setMission(createMockMission({ id: "M-TEST1", status: "active" }));
|
||||||
|
const feature = createMockFeature({ loopState: "implementing", taskId: "FN-001" });
|
||||||
|
missionStore._setFeature(feature);
|
||||||
|
missionStore.getFeatureByTaskId = vi.fn().mockReturnValue(feature);
|
||||||
|
taskStore._setTask({ id: "FN-001", title: "Test", description: "Test task", log: [] });
|
||||||
|
|
||||||
|
loop = new MissionExecutionLoop({
|
||||||
|
taskStore: taskStore as any,
|
||||||
|
missionStore: missionStore as any,
|
||||||
|
rootDir: "/tmp",
|
||||||
|
});
|
||||||
|
vi.spyOn(loop as any, "runValidation").mockResolvedValue({ status: "pass", summary: "ok" });
|
||||||
|
loop.start();
|
||||||
|
|
||||||
|
await loop.processTaskOutcome("FN-001");
|
||||||
|
|
||||||
|
expect(missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion");
|
||||||
|
});
|
||||||
|
|
||||||
it("requeues needs_fix features back through validation", async () => {
|
it("requeues needs_fix features back through validation", async () => {
|
||||||
const assertions = makeAssertions(1);
|
const assertions = makeAssertions(1);
|
||||||
const response = JSON.stringify({
|
const response = JSON.stringify({
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type {
|
|||||||
AgentStore,
|
AgentStore,
|
||||||
Settings,
|
Settings,
|
||||||
Milestone,
|
Milestone,
|
||||||
|
Mission,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import { normalizeMissionAssertionType } from "@fusion/core";
|
import { normalizeMissionAssertionType } from "@fusion/core";
|
||||||
import type { VerificationOutcome } from "./mission-verification.js";
|
import type { VerificationOutcome } from "./mission-verification.js";
|
||||||
@@ -417,6 +418,21 @@ export class MissionExecutionLoop extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only validate features of active missions — mirrors the
|
||||||
|
// recoverActiveMissions guard. A parked/blocked/completed mission must
|
||||||
|
// not keep minting validations (and Fix features) for completed tasks.
|
||||||
|
// Features that don't resolve to a mission keep the current behavior.
|
||||||
|
const mission = this.resolveFeatureMission(feature);
|
||||||
|
if (mission && mission.status !== "active") {
|
||||||
|
loopLog.log(`Feature ${feature.id} belongs to mission ${mission.id} with status "${mission.status}"; skipping validation`);
|
||||||
|
this.logFeatureWarningEvent(feature.id, "validation_skipped_mission_inactive", `Validation skipped: mission ${mission.id} status is "${mission.status}" (expected "active").`, {
|
||||||
|
taskId,
|
||||||
|
missionId: mission.id,
|
||||||
|
missionStatus: mission.status,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (feature.loopState === "needs_fix") {
|
if (feature.loopState === "needs_fix") {
|
||||||
this.missionStore.transitionLoopState(feature.id, "implementing");
|
this.missionStore.transitionLoopState(feature.id, "implementing");
|
||||||
feature.loopState = "implementing";
|
feature.loopState = "implementing";
|
||||||
@@ -1189,6 +1205,15 @@ ${taskContext ? `\n\nImplementation context:\n${taskContext}` : ""}`;
|
|||||||
return this.missionStore.getMilestone(slice.milestoneId);
|
return this.missionStore.getMilestone(slice.milestoneId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveFeatureMission(feature: MissionFeature): Mission | undefined {
|
||||||
|
const milestone = this.resolveFeatureMilestone(feature);
|
||||||
|
if (!milestone) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.missionStore.getMission(milestone.missionId);
|
||||||
|
}
|
||||||
|
|
||||||
private completeValidatorRunIfStillRunning(
|
private completeValidatorRunIfStillRunning(
|
||||||
runId: string | undefined,
|
runId: string | undefined,
|
||||||
status: "passed" | "failed" | "blocked" | "error",
|
status: "passed" | "failed" | "blocked" | "error",
|
||||||
|
|||||||
Reference in New Issue
Block a user