feat(KB-636): add Missions system for large-scale project planning

- Fix TaskStore sliceId persistence and bidirectional linking between tasks and features
- Add comprehensive mission integration tests for hierarchy, status rollup, and events
- Enhance E2E tests with reorder, cascade delete, and error handling coverage
- Add scheduler integration tests for mission slice activation
- Fix duplicate mission commands in CLI help text
- Add changeset for missions launch
This commit is contained in:
gsxdsm
2026-04-01 08:04:51 -07:00
parent d8a591bd7f
commit 97ec4268a4
12 changed files with 477 additions and 12 deletions

View File

@@ -117,6 +117,8 @@ export type { CentralCoreEvents } from "./central-core.js";
export { CentralDatabase, createCentralDatabase } from "./central-db.js";
export type {
RegisteredProject,
/** @deprecated Use RegisteredProject instead */
ProjectInfo,
IsolationMode,
ProjectStatus,
ProjectHealth,

View File

@@ -186,6 +186,45 @@ describe("Mission Integration", () => {
expect(found).toBeDefined();
expect(found!.id).toBe(feature.id);
});
it("should set task.sliceId when linking feature to task", async () => {
const mission = missionStore.createMission({ title: "Test Mission" });
const milestone = missionStore.addMilestone(mission.id, { title: "Milestone 1" });
const slice = missionStore.addSlice(milestone.id, { title: "Slice 1" });
const feature = missionStore.addFeature(slice.id, { title: "Feature 1" });
const task = await taskStore.createTask({
description: "Implement feature",
title: "Feature implementation",
});
// Link feature to task
missionStore.linkFeatureToTask(feature.id, task.id);
// Reload task and verify sliceId was set
const reloaded = await taskStore.getTask(task.id);
expect(reloaded.sliceId).toBe(slice.id);
});
it("should clear task.sliceId when unlinking feature from task", async () => {
const mission = missionStore.createMission({ title: "Test Mission" });
const milestone = missionStore.addMilestone(mission.id, { title: "Milestone 1" });
const slice = missionStore.addSlice(milestone.id, { title: "Slice 1" });
const feature = missionStore.addFeature(slice.id, { title: "Feature 1" });
const task = await taskStore.createTask({
description: "Implement feature",
title: "Feature implementation",
});
// Link and then unlink
missionStore.linkFeatureToTask(feature.id, task.id);
missionStore.unlinkFeatureFromTask(feature.id);
// Reload task and verify sliceId was cleared
const reloaded = await taskStore.getTask(task.id);
expect(reloaded.sliceId).toBeUndefined();
});
});
describe("Status Rollup", () => {

View File

@@ -919,6 +919,11 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
status: "triaged",
});
// Also update the task's sliceId for bidirectional linking
this.db.prepare(`
UPDATE tasks SET sliceId = ? WHERE id = ?
`).run(feature.sliceId, taskId);
this.emit("feature:linked", { feature: updated, taskId });
// Recompute slice status
@@ -941,11 +946,21 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
throw new Error(`Feature ${featureId} not found`);
}
// Get the taskId before clearing it
const { taskId } = feature;
const updated = this.updateFeature(featureId, {
taskId: undefined,
status: "defined",
});
// Clear the task's sliceId
if (taskId) {
this.db.prepare(`
UPDATE tasks SET sliceId = NULL WHERE id = ?
`).run(taskId);
}
// Recompute slice status
this.recomputeSliceStatus(updated.sliceId);

View File

@@ -164,6 +164,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
breakIntoSubtasks: row.breakIntoSubtasks ? true : undefined,
enabledWorkflowSteps: (() => { const e = fromJson<string[]>(row.enabledWorkflowSteps); return e && e.length > 0 ? e : undefined; })(),
modifiedFiles: (() => { const m = fromJson<string[]>(row.modifiedFiles); return m && m.length > 0 ? m : undefined; })(),
sliceId: row.sliceId || undefined,
};
}
@@ -179,10 +180,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
summary, thinkingLevel, createdAt, updatedAt, columnMovedAt,
dependencies, steps, log, attachments,
comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`).run(
task.id,
@@ -223,6 +224,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.breakIntoSubtasks ? 1 : 0,
toJson(task.enabledWorkflowSteps || []),
toJson(task.modifiedFiles || []),
task.sliceId ?? null,
);
this.db.bumpLastModified();
}

View File

@@ -961,6 +961,9 @@ export interface RegisteredProject {
settings?: ProjectSettings;
}
/** @deprecated Alias for RegisteredProject - use RegisteredProject instead */
export type ProjectInfo = RegisteredProject;
/** Health metrics for a registered project */
export interface ProjectHealth {
/** Project ID reference */