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 () => {
|
||||
const assertions = makeAssertions(1);
|
||||
const response = JSON.stringify({
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user