feat(FN-980): add mission autopilot for autonomous slice progression
- Add MissionAutopilot class with state machine (inactive → watching → activating → completing) - Add autopilot database schema: autopilotEnabled, autopilotState, lastAutopilotActivityAt columns - Integrate autopilot with scheduler: handleTaskCompletion() triggers slice activation - Add API routes: GET/PATCH /missions/:id/autopilot, POST start/stop endpoints - Add autopilot toggle UI in MissionManager dashboard component - Add retry logic with exponential backoff (up to 3 attempts) for slice activation - Add background poll (60s) to detect stale autopilot missions - Include changeset for @gsxdsm/fusion minor bump
This commit is contained in:
@@ -63,6 +63,8 @@ export interface SchedulerOptions {
|
||||
prMonitor?: PrMonitor;
|
||||
/** Optional MissionStore for slice activation and auto-advance */
|
||||
missionStore?: MissionStore;
|
||||
/** Optional MissionAutopilot for autonomous mission progression */
|
||||
missionAutopilot?: import("./mission-autopilot.js").MissionAutopilot;
|
||||
/**
|
||||
* Called when a task with a closed/merged PR moves out of in-review
|
||||
* and the PrMonitor has buffered actionable comments.
|
||||
@@ -281,6 +283,19 @@ export class Scheduler {
|
||||
this.pollInterval = setInterval(() => this.schedule(), interval);
|
||||
this.schedule();
|
||||
schedulerLog.log(`Started (poll interval: ${interval}ms)`);
|
||||
|
||||
// Wire up MissionAutopilot: set scheduler reference for lazy injection
|
||||
// and start watching all missions with autopilotEnabled: true
|
||||
if (this.options.missionAutopilot && this.options.missionStore) {
|
||||
this.options.missionAutopilot.setScheduler(this);
|
||||
const missions = this.options.missionStore.listMissions();
|
||||
for (const mission of missions) {
|
||||
if (mission.autopilotEnabled && mission.status !== "complete" && mission.status !== "archived") {
|
||||
this.options.missionAutopilot.watchMission(mission.id);
|
||||
}
|
||||
}
|
||||
this.options.missionAutopilot.start();
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
@@ -294,6 +309,10 @@ export class Scheduler {
|
||||
if (this.options.prMonitor) {
|
||||
this.options.prMonitor.stopAll();
|
||||
}
|
||||
// Stop MissionAutopilot when scheduler shuts down
|
||||
if (this.options.missionAutopilot) {
|
||||
this.options.missionAutopilot.stop();
|
||||
}
|
||||
schedulerLog.log("Stopped");
|
||||
}
|
||||
|
||||
@@ -681,7 +700,10 @@ export class Scheduler {
|
||||
* When a task moves to "done", update the linked feature status to "done".
|
||||
* updateFeatureStatus cascades via recomputeSliceStatus — if all features
|
||||
* in the slice are done the slice status becomes "complete" automatically.
|
||||
* We then call onSliceComplete to trigger auto-advance to the next slice.
|
||||
*
|
||||
* If MissionAutopilot is configured, delegate slice advancement to it
|
||||
* (which tracks autopilot state and handles retries). Otherwise fall back
|
||||
* to the legacy onSliceComplete() path for non-autopilot missions.
|
||||
*/
|
||||
private async handleMissionTaskCompletion(taskId: string, sliceId: string): Promise<void> {
|
||||
if (!this.options.missionStore) return;
|
||||
@@ -709,8 +731,18 @@ export class Scheduler {
|
||||
// Check if the slice became complete after the feature update
|
||||
const slice = missionStore.getSlice(sliceIdBeforeUpdate);
|
||||
if (slice && slice.status === "complete") {
|
||||
schedulerLog.log(`Slice ${slice.id} is complete — triggering auto-advance`);
|
||||
await this.onSliceComplete(slice);
|
||||
// If MissionAutopilot is available, delegate progression to it.
|
||||
// The autopilot handles: watching missions, autoAdvance guard,
|
||||
// retry logic, and state tracking. The autopilot will call back
|
||||
// into scheduler.activateNextPendingSlice() when appropriate.
|
||||
if (this.options.missionAutopilot) {
|
||||
schedulerLog.log(`Slice ${slice.id} is complete — delegating to autopilot`);
|
||||
await this.options.missionAutopilot.handleTaskCompletion(taskId);
|
||||
} else {
|
||||
// Legacy path for missions without autopilot
|
||||
schedulerLog.log(`Slice ${slice.id} is complete — triggering auto-advance`);
|
||||
await this.onSliceComplete(slice);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
schedulerLog.error(`Error handling mission task completion for ${taskId}:`, err);
|
||||
|
||||
Reference in New Issue
Block a user