FN-5679: default new missions to stopped state

Ensure mission creation always starts in a stopped planning state with autopilot off.

- Force MissionStore createMission to ignore create-time autopilotEnabled and persist stopped defaults
- Update mission POST route behavior to stop auto-watching on create and keep optional create-time updates scoped
- Add/adjust core and dashboard tests to assert stopped-by-default creation semantics
- Document mission autopilot creation behavior and add a patch changeset for @runfusion/fusion

Files changed:
 .changeset/fn-5679-missions-default-stopped.md         |  7 +++++++
 docs/missions.md                                       |  4 +++-
 .../mission-factory-parity.integration.test.ts         |  6 +++---
 packages/core/src/__tests__/mission-store.test.ts      | 18 ++++++++++++++++++
 packages/core/src/mission-store.ts                     |  5 +++--
 packages/dashboard/src/__tests__/mission-e2e.test.ts   |  8 +++++---
 packages/dashboard/src/mission-routes.ts               |  9 +--------
 7 files changed, 40 insertions(+), 17 deletions(-)

Fusion-Task-Id: FN-5679

Fusion-Task-Lineage: 05eff3cb-07d8-4b52-ac7f-065979e78816
This commit is contained in:
gsxdsm
2026-05-29 15:45:20 -07:00
parent 91f568a9b0
commit 668e3a5099
7 changed files with 40 additions and 17 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
Mission creation now always returns a stopped mission. `POST /api/missions` and the mission store ignore create-time `autopilotEnabled` input, forcing new missions to `status: "planning"` with autopilot disabled and inactive.
Autopilot remains a post-creation action via explicit mission start/update flows.

View File

@@ -176,6 +176,8 @@ Manual activation is available through `fn mission activate-slice <slice-id>`.
## Mission Autopilot
Missions are always created stopped (`status: "planning"`, `autopilotEnabled: false`, `autoAdvance: false`).
Autopilot must be enabled explicitly after creation (for example via start/update actions).
When `autopilotEnabled` is on, Fusion can watch completion events and progress missions automatically.
State machine:
@@ -187,7 +189,7 @@ State machine:
Typical flow:
1. Mission is watched (missions created/updated with `autopilotEnabled: true` are watched immediately)
1. Mission is watched (missions updated with `autopilotEnabled: true` or explicitly started are watched)
2. Task completion updates feature status
3. If a slice is complete, autopilot activates next pending slice
4. When milestones are all complete, mission transitions to complete

View File

@@ -377,7 +377,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
});
describe("Parity Matrix: Autopilot Configuration", () => {
it("autopilotEnabled persists across restart", async () => {
it("missions created with autopilotEnabled start disabled across restart", async () => {
const missionStore = taskStore.getMissionStore();
const mission = missionStore.createMission({
@@ -387,7 +387,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Verify initial state
let retrieved = missionStore.getMission(mission.id);
expect(retrieved!.autopilotEnabled).toBe(true);
expect(retrieved!.autopilotEnabled).toBe(false);
// Restart
taskStore.close();
@@ -397,7 +397,7 @@ describe("MissionFactory Parity: Core MissionStore", () => {
// Verify persistence
retrieved = missionStore2.getMission(mission.id);
expect(retrieved!.autopilotEnabled).toBe(true);
expect(retrieved!.autopilotEnabled).toBe(false);
});
it("autopilotEnabled can be toggled", async () => {

View File

@@ -68,6 +68,24 @@ describe("MissionStore", () => {
expect(mission.updatedAt).toBeTruthy();
});
it("ignores autopilotEnabled on create and persists stopped defaults", () => {
const mission = store.createMission({
title: "Stopped by default",
autopilotEnabled: true,
});
expect(mission.autopilotEnabled).toBe(false);
expect(mission.autoAdvance).toBe(false);
expect(mission.status).toBe("planning");
expect(mission.autopilotState).toBe("inactive");
const persisted = store.getMission(mission.id);
expect(persisted?.autopilotEnabled).toBe(false);
expect(persisted?.autoAdvance).toBe(false);
expect(persisted?.status).toBe("planning");
expect(persisted?.autopilotState).toBe("inactive");
});
it("gets a mission by id", () => {
const created = store.createMission({ title: "Get Test" });
const retrieved = store.getMission(created.id);

View File

@@ -567,7 +567,8 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
/**
* Create a new mission.
* The mission starts in "planning" status with "not_started" interview state.
* Missions are always created stopped (status `planning`, autopilot disabled).
* Autopilot is only enabled via an explicit start/update after creation.
*
* @param input - Mission creation input
* @returns The created mission
@@ -586,7 +587,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
branchStrategy: input.branchStrategy,
autoMerge: input.autoMerge,
autoAdvance: false,
autopilotEnabled: input.autopilotEnabled ?? false,
autopilotEnabled: false,
autopilotState: "inactive",
createdAt: now,
updatedAt: now,

View File

@@ -793,7 +793,7 @@ describe("Mission API", () => {
expect(missionStore.updateMission).toHaveBeenCalledWith(res.body.id, { autoAdvance: true });
});
it("watches mission immediately when created with autopilotEnabled true", async () => {
it("creates missions stopped even when autopilotEnabled is passed", async () => {
const missionAutopilot = createMockMissionAutopilot();
const { app } = buildApp({ missionAutopilot });
@@ -806,8 +806,10 @@ describe("Mission API", () => {
);
expect(res.status).toBe(201);
expect(missionAutopilot.watchMission).toHaveBeenCalledTimes(1);
expect(missionAutopilot.watchMission).toHaveBeenCalledWith(res.body.id);
expect(res.body.status).toBe("planning");
expect(res.body.autopilotEnabled).toBe(false);
expect(res.body.autoAdvance).toBe(false);
expect(missionAutopilot.watchMission).not.toHaveBeenCalled();
});
});

View File

@@ -332,7 +332,7 @@ export function createMissionRouter(
router.post(
"/",
catchTypedHandler(async (req, res) => {
const { title, description, autoAdvance, autopilotEnabled, baseBranch, branchStrategy } = req.body;
const { title, description, autoAdvance, baseBranch, branchStrategy } = req.body;
const validatedTitle = validateTitle(title);
const validatedDescription = validateDescription(description);
@@ -350,15 +350,8 @@ export function createMissionRouter(
if (autoAdvance !== undefined) {
updates.autoAdvance = validateBoolean(autoAdvance, "autoAdvance");
}
if (autopilotEnabled !== undefined) {
updates.autopilotEnabled = validateBoolean(autopilotEnabled, "autopilotEnabled");
}
if (Object.keys(updates).length > 0) {
const updatedMission = missionStore.updateMission(mission.id, updates);
if (missionAutopilot && updatedMission.autopilotEnabled) {
missionAutopilot.watchMission(updatedMission.id);
}
res.status(201).json(updatedMission);
return;
}