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:
@@ -89,7 +89,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getSchemaVersion()).toBe(10);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -112,7 +112,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getSchemaVersion()).toBe(10);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -719,7 +719,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 5 (includes v1→v2, v2→v3, v3→v4, and v4→v5 migrations)
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getSchemaVersion()).toBe(10);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -744,11 +744,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getSchemaVersion()).toBe(10);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getSchemaVersion()).toBe(10);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -843,7 +843,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 5
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getSchemaVersion()).toBe(10);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1053,7 +1053,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getSchemaVersion()).toBe(10);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -450,6 +450,14 @@ export class Database {
|
||||
|
||||
// Future migrations go here:
|
||||
// if (version < 10) { this.applyMigration(10, () => { ... }); }
|
||||
|
||||
if (version < 10) {
|
||||
this.applyMigration(10, () => {
|
||||
this.addColumnIfMissing("missions", "autopilotEnabled", "INTEGER DEFAULT 0");
|
||||
this.addColumnIfMissing("missions", "autopilotState", "TEXT DEFAULT 'inactive'");
|
||||
this.addColumnIfMissing("missions", "lastAutopilotActivityAt", "TEXT");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -80,6 +80,7 @@ export {
|
||||
SLICE_STATUSES,
|
||||
FEATURE_STATUSES,
|
||||
INTERVIEW_STATES,
|
||||
AUTOPILOT_STATES,
|
||||
} from "./mission-types.js";
|
||||
export type {
|
||||
MissionStatus,
|
||||
@@ -87,6 +88,8 @@ export type {
|
||||
SliceStatus,
|
||||
FeatureStatus,
|
||||
InterviewState,
|
||||
AutopilotState,
|
||||
AutopilotStatus,
|
||||
Mission,
|
||||
Milestone,
|
||||
Slice,
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
SliceStatus,
|
||||
FeatureStatus,
|
||||
InterviewState,
|
||||
AutopilotState,
|
||||
} from "./mission-types.js";
|
||||
|
||||
// ── Mission Summary Type ─────────────────────────────────────────────
|
||||
@@ -112,6 +113,9 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
status: row.status as MissionStatus,
|
||||
interviewState: row.interviewState as InterviewState,
|
||||
autoAdvance: Boolean(row.autoAdvance),
|
||||
autopilotEnabled: Boolean(row.autopilotEnabled),
|
||||
autopilotState: (row.autopilotState as AutopilotState) || "inactive",
|
||||
lastAutopilotActivityAt: row.lastAutopilotActivityAt || undefined,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
@@ -178,7 +182,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
* @param input - Mission creation input
|
||||
* @returns The created mission
|
||||
*/
|
||||
createMission(input: MissionCreateInput): Mission {
|
||||
createMission(input: MissionCreateInput & { autopilotEnabled?: boolean }): Mission {
|
||||
const now = new Date().toISOString();
|
||||
const id = this.generateMissionId();
|
||||
|
||||
@@ -189,13 +193,15 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
status: "planning",
|
||||
interviewState: "not_started",
|
||||
autoAdvance: false,
|
||||
autopilotEnabled: input.autopilotEnabled ?? false,
|
||||
autopilotState: "inactive",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.db.prepare(`
|
||||
INSERT INTO missions (id, title, description, status, interviewState, autoAdvance, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO missions (id, title, description, status, interviewState, autoAdvance, autopilotEnabled, autopilotState, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
mission.id,
|
||||
mission.title,
|
||||
@@ -203,6 +209,8 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
mission.status,
|
||||
mission.interviewState,
|
||||
mission.autoAdvance ? 1 : 0,
|
||||
mission.autopilotEnabled ? 1 : 0,
|
||||
mission.autopilotState ?? "inactive",
|
||||
mission.createdAt,
|
||||
mission.updatedAt,
|
||||
);
|
||||
@@ -337,6 +345,9 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
status = ?,
|
||||
interviewState = ?,
|
||||
autoAdvance = ?,
|
||||
autopilotEnabled = ?,
|
||||
autopilotState = ?,
|
||||
lastAutopilotActivityAt = ?,
|
||||
updatedAt = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
@@ -345,6 +356,9 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
updated.status,
|
||||
updated.interviewState,
|
||||
updated.autoAdvance ? 1 : 0,
|
||||
updated.autopilotEnabled ? 1 : 0,
|
||||
updated.autopilotState ?? "inactive",
|
||||
updated.lastAutopilotActivityAt ?? null,
|
||||
updated.updatedAt,
|
||||
updated.id,
|
||||
);
|
||||
|
||||
@@ -31,6 +31,19 @@ export type FeatureStatus = (typeof FEATURE_STATUSES)[number];
|
||||
export const INTERVIEW_STATES = ["not_started", "in_progress", "completed", "needs_update"] as const;
|
||||
export type InterviewState = (typeof INTERVIEW_STATES)[number];
|
||||
|
||||
/** Autopilot state values for mission autonomous progression */
|
||||
export const AUTOPILOT_STATES = ["inactive", "watching", "activating", "completing"] as const;
|
||||
export type AutopilotState = (typeof AUTOPILOT_STATES)[number];
|
||||
|
||||
/** Autopilot status for a mission */
|
||||
export interface AutopilotStatus {
|
||||
enabled: boolean;
|
||||
state: AutopilotState;
|
||||
watched: boolean;
|
||||
lastActivityAt?: string;
|
||||
nextScheduledCheck?: string;
|
||||
}
|
||||
|
||||
// ── Core Entity Types ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -50,6 +63,12 @@ export interface Mission {
|
||||
interviewState: InterviewState;
|
||||
/** When true, automatically activate the next pending slice when current slice completes */
|
||||
autoAdvance?: boolean;
|
||||
/** When true, enable autopilot monitoring system for this mission */
|
||||
autopilotEnabled?: boolean;
|
||||
/** Current autopilot runtime state */
|
||||
autopilotState?: AutopilotState;
|
||||
/** ISO-8601 timestamp of last autopilot activity (only populated when active) */
|
||||
lastAutopilotActivityAt?: string;
|
||||
/** ISO-8601 timestamp of creation */
|
||||
createdAt: string;
|
||||
/** ISO-8601 timestamp of last update */
|
||||
|
||||
Reference in New Issue
Block a user