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:
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user