Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { createAutomationPlannerService } from "./services/automations/automatio
import { createCiService } from "./services/ci/ciService";
import { createRestackSuggestionService } from "./services/lanes/restackSuggestionService";
import { createAutoRebaseService } from "./services/lanes/autoRebaseService";
import { createMissionService } from "./services/missions/missionService";

function getRendererUrl(): string {
const devUrl = process.env.VITE_DEV_SERVER_URL;
Expand Down Expand Up @@ -413,6 +414,12 @@ app.whenReady().then(async () => {
onEvent: (event) => broadcast(IPC.automationsEvent, event)
});

const missionService = createMissionService({
db,
projectId,
onEvent: (event) => broadcast(IPC.missionsEvent, event)
});

const automationPlannerService = createAutomationPlannerService({
logger,
projectRoot,
Expand Down Expand Up @@ -535,6 +542,7 @@ app.whenReady().then(async () => {
jobEngine,
automationService,
automationPlannerService,
missionService,
ciService,
packService,
projectConfigService,
Expand Down
62 changes: 61 additions & 1 deletion apps/desktop/src/main/services/ipc/registerIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import type {
AutomationSaveDraftResult,
AutomationSimulateRequest,
AutomationSimulateResult,
AddMissionArtifactArgs,
AddMissionInterventionArgs,
ConflictProposal,
ConflictExternalResolverRunSummary,
ConflictProposalPreview,
Expand Down Expand Up @@ -113,6 +115,7 @@ import type {
ListOperationsArgs,
ListOverlapsArgs,
ListLanesArgs,
ListMissionsArgs,
ListSessionsArgs,
ListTestRunsArgs,
MergeSimulationArgs,
Expand Down Expand Up @@ -175,10 +178,19 @@ import type {
TerminalProfilesSnapshot,
TerminalSessionSummary,
UpdateSessionMetaArgs,
UpdateMissionArgs,
UpdateMissionStepArgs,
TestRunSummary,
TestSuiteDefinition,
UpdateLaneAppearanceArgs,
WriteTextAtomicArgs
WriteTextAtomicArgs,
MissionDetail,
MissionIntervention,
MissionArtifact,
MissionStep,
MissionSummary,
ResolveMissionInterventionArgs,
CreateMissionArgs
} from "../../../shared/types";
import type { Logger } from "../logging/logger";
import type { AdeDb } from "../state/kvDb";
Expand Down Expand Up @@ -210,6 +222,7 @@ import type { createOnboardingService } from "../onboarding/onboardingService";
import type { createCiService } from "../ci/ciService";
import type { createAutomationService } from "../automations/automationService";
import type { createAutomationPlannerService } from "../automations/automationPlannerService";
import type { createMissionService } from "../missions/missionService";
import { redactSecrets } from "../../utils/redaction";

export type AppContext = {
Expand Down Expand Up @@ -242,6 +255,7 @@ export type AppContext = {
jobEngine: ReturnType<typeof createJobEngine>;
automationService: ReturnType<typeof createAutomationService>;
automationPlannerService: ReturnType<typeof createAutomationPlannerService>;
missionService: ReturnType<typeof createMissionService>;
packService: ReturnType<typeof createPackService>;
projectConfigService: ReturnType<typeof createProjectConfigService>;
processService: ReturnType<typeof createProcessService>;
Expand Down Expand Up @@ -584,6 +598,52 @@ export function registerIpc({
return ctx.automationPlannerService.simulate(arg);
});

ipcMain.handle(IPC.missionsList, async (_event, arg: ListMissionsArgs = {}): Promise<MissionSummary[]> => {
const ctx = getCtx();
return ctx.missionService.list(arg);
});

ipcMain.handle(IPC.missionsGet, async (_event, arg: { missionId: string }): Promise<MissionDetail | null> => {
const ctx = getCtx();
return ctx.missionService.get(arg?.missionId ?? "");
});

ipcMain.handle(IPC.missionsCreate, async (_event, arg: CreateMissionArgs): Promise<MissionDetail> => {
const ctx = getCtx();
return ctx.missionService.create(arg);
});

ipcMain.handle(IPC.missionsUpdate, async (_event, arg: UpdateMissionArgs): Promise<MissionDetail> => {
const ctx = getCtx();
return ctx.missionService.update(arg);
});

ipcMain.handle(IPC.missionsUpdateStep, async (_event, arg: UpdateMissionStepArgs): Promise<MissionStep> => {
const ctx = getCtx();
return ctx.missionService.updateStep(arg);
});

ipcMain.handle(IPC.missionsAddArtifact, async (_event, arg: AddMissionArtifactArgs): Promise<MissionArtifact> => {
const ctx = getCtx();
return ctx.missionService.addArtifact(arg);
});

ipcMain.handle(
IPC.missionsAddIntervention,
async (_event, arg: AddMissionInterventionArgs): Promise<MissionIntervention> => {
const ctx = getCtx();
return ctx.missionService.addIntervention(arg);
}
);

ipcMain.handle(
IPC.missionsResolveIntervention,
async (_event, arg: ResolveMissionInterventionArgs): Promise<MissionIntervention> => {
const ctx = getCtx();
return ctx.missionService.resolveIntervention(arg);
}
);

ipcMain.handle(IPC.layoutGet, async (_event, arg: { layoutId: string }): Promise<DockLayout | null> => {
const ctx = getCtx();
const key = `dock_layout:${arg.layoutId}`;
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/services/jobs/jobEngine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe("jobEngine narrative payload propagation", () => {
clipReason: null,
omittedSections: []
})),
getPeerLanesContext: vi.fn(async () => null),
applyHostedNarrative,
recordEvent
} as any,
Expand Down
205 changes: 205 additions & 0 deletions apps/desktop/src/main/services/missions/missionService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { openKvDb } from "../state/kvDb";
import { createMissionService } from "./missionService";

function createLogger() {
return {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {}
} as any;
}

async function createDbWithProjectAndLane() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ade-mission-service-"));
const dbPath = path.join(root, "ade.db");
const db = await openKvDb(dbPath, createLogger());

const projectId = "proj-1";
const laneId = "lane-1";
const now = "2026-02-18T00:00:00.000Z";

db.run(
`
insert into projects(id, root_path, display_name, default_base_ref, created_at, last_opened_at)
values (?, ?, ?, ?, ?, ?)
`,
[projectId, root, "ADE", "main", now, now]
);

db.run(
`
insert into lanes(
id,
project_id,
name,
description,
lane_type,
base_ref,
branch_ref,
worktree_path,
attached_root_path,
is_edit_protected,
parent_lane_id,
color,
icon,
tags_json,
status,
created_at,
archived_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
[
laneId,
projectId,
"Lane 1",
null,
"worktree",
"main",
"feature/lane-1",
root,
null,
0,
null,
null,
null,
null,
"active",
now,
null
]
);

return {
db,
projectId,
laneId,
dispose: () => db.close()
};
}

describe("missionService lifecycle", () => {
it("supports valid mission lifecycle transitions", async () => {
const { db, projectId, laneId, dispose } = await createDbWithProjectAndLane();
const service = createMissionService({ db, projectId });

const created = service.create({
prompt: "Implement profile tab improvements and prepare PR summary.",
laneId
});

expect(created.status).toBe("queued");
expect(created.steps.length).toBeGreaterThan(0);

const started = service.update({ missionId: created.id, status: "in_progress" });
expect(started.status).toBe("in_progress");

const intervention = service.addIntervention({
missionId: created.id,
interventionType: "manual_input",
title: "Need design confirmation",
body: "Confirm whether we should preserve the old spacing scale."
});

expect(intervention.status).toBe("open");

const paused = service.get(created.id);
expect(paused?.status).toBe("intervention_required");
expect(paused?.openInterventions).toBe(1);

service.resolveIntervention({
missionId: created.id,
interventionId: intervention.id,
status: "resolved",
note: "Proceed with the new spacing scale."
});

const resumed = service.get(created.id);
expect(resumed?.status).toBe("in_progress");
expect(resumed?.openInterventions).toBe(0);

const completed = service.update({
missionId: created.id,
status: "completed",
outcomeSummary: "Updated profile layout and linked PR #123."
});

expect(completed.status).toBe("completed");
expect(completed.outcomeSummary).toContain("PR #123");
expect(completed.artifacts.some((artifact) => artifact.artifactType === "summary")).toBe(true);

dispose();
});

it("rejects invalid mission and step transitions", async () => {
const { db, projectId, laneId, dispose } = await createDbWithProjectAndLane();
const service = createMissionService({ db, projectId });

const created = service.create({
prompt: "Write migration notes and close release lane.",
laneId
});

expect(() => service.update({ missionId: created.id, status: "completed" })).toThrow(/Invalid mission transition/i);

const firstStep = service.get(created.id)?.steps[0];
expect(firstStep).toBeTruthy();
if (!firstStep) {
dispose();
throw new Error("Expected first step");
}

expect(() =>
service.updateStep({
missionId: created.id,
stepId: firstStep.id,
status: "succeeded"
})
).toThrow(/Invalid mission step transition/i);

dispose();
});

it("creates intervention-required status when a running step fails", async () => {
const { db, projectId, laneId, dispose } = await createDbWithProjectAndLane();
const service = createMissionService({ db, projectId });

const created = service.create({
prompt: "Refactor auth checks and run regression tests.",
laneId
});

service.update({ missionId: created.id, status: "in_progress" });

const firstStep = service.get(created.id)?.steps[0];
expect(firstStep).toBeTruthy();
if (!firstStep) {
dispose();
throw new Error("Expected first step");
}

service.updateStep({
missionId: created.id,
stepId: firstStep.id,
status: "running"
});

service.updateStep({
missionId: created.id,
stepId: firstStep.id,
status: "failed",
note: "Unit tests failed in CI parity suite."
});

const detail = service.get(created.id);
expect(detail?.status).toBe("intervention_required");
expect(detail?.openInterventions).toBeGreaterThan(0);
expect(detail?.lastError).toContain("Unit tests failed");

dispose();
});
});
Loading