From 546ef162d962db23483ac90860449eda0af3d8a7 Mon Sep 17 00:00:00 2001 From: fusion-merge-train Date: Sun, 5 Jul 2026 04:27:17 +0000 Subject: [PATCH] fix(engine): skip task-completion validation for inactive missions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...e-branch-strategy-and-parked-validation.md | 7 +++ .../__tests__/mission-execution-loop.test.ts | 50 +++++++++++++++++++ packages/engine/src/mission-execution-loop.ts | 25 ++++++++++ 3 files changed, 82 insertions(+) create mode 100644 .changeset/mission-triage-branch-strategy-and-parked-validation.md diff --git a/.changeset/mission-triage-branch-strategy-and-parked-validation.md b/.changeset/mission-triage-branch-strategy-and-parked-validation.md new file mode 100644 index 0000000000..0966318fa7 --- /dev/null +++ b/.changeset/mission-triage-branch-strategy-and-parked-validation.md @@ -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. diff --git a/packages/engine/src/__tests__/mission-execution-loop.test.ts b/packages/engine/src/__tests__/mission-execution-loop.test.ts index 3108d554c9..427e7f576c 100644 --- a/packages/engine/src/__tests__/mission-execution-loop.test.ts +++ b/packages/engine/src/__tests__/mission-execution-loop.test.ts @@ -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({ diff --git a/packages/engine/src/mission-execution-loop.ts b/packages/engine/src/mission-execution-loop.ts index 49c39bc5cd..763781e2d4 100644 --- a/packages/engine/src/mission-execution-loop.ts +++ b/packages/engine/src/mission-execution-loop.ts @@ -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",