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:
fusion-merge-train
2026-07-05 04:27:17 +00:00
parent 95f410018a
commit 546ef162d9
3 changed files with 82 additions and 0 deletions

View File

@@ -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.

View File

@@ -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 () => {
const assertions = makeAssertions(1);
const response = JSON.stringify({

View File

@@ -21,6 +21,7 @@ import type {
AgentStore,
Settings,
Milestone,
Mission,
} from "@fusion/core";
import { normalizeMissionAssertionType } from "@fusion/core";
import type { VerificationOutcome } from "./mission-verification.js";
@@ -417,6 +418,21 @@ export class MissionExecutionLoop extends EventEmitter {
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") {
this.missionStore.transitionLoopState(feature.id, "implementing");
feature.loopState = "implementing";
@@ -1189,6 +1205,15 @@ ${taskContext ? `\n\nImplementation context:\n${taskContext}` : ""}`;
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(
runId: string | undefined,
status: "passed" | "failed" | "blocked" | "error",