feat(FN-1369): add mission planning context propagation to tasks
- Add buildEnrichedDescription() for hierarchical mission context during triage - Propagate mission → milestone → slice → feature context to task descriptions - Add planningNotes and verification fields to milestones and slices - Track planState on slices (not_started, planned, needs_update) - Add apply/skip interview endpoints for milestone and slice planning - Fix error mapping for rate limit and apply interview routes - Add comprehensive integration tests for context enrichment - Add AGENTS.md documentation for Mission Planning Context feature
This commit is contained in:
543
packages/core/src/mission-planning-context.integration.test.ts
Normal file
543
packages/core/src/mission-planning-context.integration.test.ts
Normal file
@@ -0,0 +1,543 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { TaskStore } from "./store.js";
|
||||
|
||||
function makeTmpDir(): string {
|
||||
return mkdtempSync(join(tmpdir(), "kb-mission-planning-"));
|
||||
}
|
||||
|
||||
/**
|
||||
* MissionStore planning context integration tests verify the enriched triage flow
|
||||
* that adds mission hierarchy context to task descriptions. These scenarios cover:
|
||||
* - Full hierarchy context enrichment in task descriptions
|
||||
* - Omission of empty hierarchy sections
|
||||
* - Custom description override bypassing enrichment
|
||||
* - Bulk triage with enrichment
|
||||
* - Enrichment after interview updates
|
||||
* - Plan state transitions
|
||||
*/
|
||||
describe("MissionStore planning context integration", () => {
|
||||
let rootDir: string;
|
||||
let taskStore: TaskStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-04-01T00:00:00.000Z"));
|
||||
|
||||
rootDir = makeTmpDir();
|
||||
taskStore = new TaskStore(rootDir);
|
||||
await taskStore.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await rm(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("buildEnrichedDescription", () => {
|
||||
it("enriches task description with full hierarchy context", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
// Create full hierarchy with rich context
|
||||
const mission = missionStore.createMission({
|
||||
title: "Launch Authentication",
|
||||
description: "Build a complete auth system",
|
||||
});
|
||||
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "Core Auth",
|
||||
description: "Implement core authentication",
|
||||
verification: "Users can log in and log out",
|
||||
planningNotes: "Decided on JWT strategy",
|
||||
});
|
||||
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Login Page",
|
||||
description: "Build the login UI",
|
||||
verification: "Login form accepts valid credentials",
|
||||
planningNotes: "Use existing design system",
|
||||
});
|
||||
|
||||
const feature = missionStore.addFeature(slice.id, {
|
||||
title: "Login Form",
|
||||
description: "Standard login form with email/password",
|
||||
acceptanceCriteria: "Form validates input and shows errors",
|
||||
});
|
||||
|
||||
// Build enriched description
|
||||
const enriched = missionStore.buildEnrichedDescription(feature.id);
|
||||
|
||||
expect(enriched).toBeDefined();
|
||||
// Mission context
|
||||
expect(enriched).toContain("Launch Authentication");
|
||||
expect(enriched).toContain("Build a complete auth system");
|
||||
// Milestone context
|
||||
expect(enriched).toContain("Core Auth");
|
||||
expect(enriched).toContain("Implement core authentication");
|
||||
expect(enriched).toContain("Users can log in and log out");
|
||||
expect(enriched).toContain("Decided on JWT strategy");
|
||||
// Slice context
|
||||
expect(enriched).toContain("Login Page");
|
||||
expect(enriched).toContain("Build the login UI");
|
||||
expect(enriched).toContain("Login form accepts valid credentials");
|
||||
expect(enriched).toContain("Use existing design system");
|
||||
// Feature context
|
||||
expect(enriched).toContain("Login Form");
|
||||
expect(enriched).toContain("Standard login form with email/password");
|
||||
expect(enriched).toContain("Form validates input and shows errors");
|
||||
});
|
||||
|
||||
it("omits empty hierarchy sections from enriched description", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
// Create minimal hierarchy
|
||||
const mission = missionStore.createMission({
|
||||
title: "Minimal Mission",
|
||||
description: "Just a title and description",
|
||||
});
|
||||
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "Minimal Milestone",
|
||||
// No description, verification, or planningNotes
|
||||
});
|
||||
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Minimal Slice",
|
||||
// No description, verification, or planningNotes
|
||||
});
|
||||
|
||||
const feature = missionStore.addFeature(slice.id, {
|
||||
title: "Minimal Feature",
|
||||
description: "Feature with description only",
|
||||
// No acceptance criteria
|
||||
});
|
||||
|
||||
const enriched = missionStore.buildEnrichedDescription(feature.id);
|
||||
|
||||
expect(enriched).toBeDefined();
|
||||
// Mission title should be present
|
||||
expect(enriched).toContain("Minimal Mission");
|
||||
expect(enriched).toContain("Just a title and description");
|
||||
// Milestone title should be present but description/verification/notes sections should not be empty
|
||||
expect(enriched).toContain("Minimal Milestone");
|
||||
// Should not have empty sections like "Description: undefined"
|
||||
expect(enriched).not.toMatch(/Description:\s*undefined/);
|
||||
expect(enriched).not.toMatch(/Verification:\s*undefined/);
|
||||
expect(enriched).not.toMatch(/Planning Notes:\s*undefined/);
|
||||
// Feature context
|
||||
expect(enriched).toContain("Minimal Feature");
|
||||
expect(enriched).toContain("Feature with description only");
|
||||
});
|
||||
|
||||
it("returns undefined for non-existent feature", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const enriched = missionStore.buildEnrichedDescription("non-existent-id");
|
||||
|
||||
expect(enriched).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when slice is not found", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({ title: "Test Mission" });
|
||||
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
||||
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
||||
const feature = missionStore.addFeature(slice.id, { title: "Test Feature" });
|
||||
|
||||
// Manually delete the slice to simulate orphan feature
|
||||
missionStore.deleteSlice(slice.id);
|
||||
|
||||
const enriched = missionStore.buildEnrichedDescription(feature.id);
|
||||
|
||||
expect(enriched).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("triageFeature with enrichment", () => {
|
||||
it("triageFeature enriches task description with full hierarchy context", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
// Create full hierarchy
|
||||
const mission = missionStore.createMission({
|
||||
title: "Authentication System",
|
||||
description: "Implement complete auth",
|
||||
});
|
||||
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "User Management",
|
||||
description: "Handle user accounts",
|
||||
verification: "Users can manage accounts",
|
||||
planningNotes: "Use PostgreSQL for user data",
|
||||
});
|
||||
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "User Registration",
|
||||
description: "Build registration flow",
|
||||
verification: "Users can register",
|
||||
planningNotes: "Add email verification",
|
||||
});
|
||||
|
||||
const feature = missionStore.addFeature(slice.id, {
|
||||
title: "Registration Form",
|
||||
description: "Create registration form",
|
||||
acceptanceCriteria: "Form submits successfully",
|
||||
});
|
||||
|
||||
// Triage the feature (no custom description override)
|
||||
await missionStore.triageFeature(feature.id);
|
||||
|
||||
// Get the linked task
|
||||
const updatedFeature = missionStore.getFeature(feature.id);
|
||||
expect(updatedFeature?.taskId).toBeDefined();
|
||||
|
||||
const task = await taskStore.getTask(updatedFeature!.taskId!);
|
||||
expect(task.description).toContain("Authentication System");
|
||||
expect(task.description).toContain("Implement complete auth");
|
||||
expect(task.description).toContain("User Management");
|
||||
expect(task.description).toContain("Handle user accounts");
|
||||
expect(task.description).toContain("Users can manage accounts");
|
||||
expect(task.description).toContain("Use PostgreSQL for user data");
|
||||
expect(task.description).toContain("User Registration");
|
||||
expect(task.description).toContain("Build registration flow");
|
||||
expect(task.description).toContain("Users can register");
|
||||
expect(task.description).toContain("Add email verification");
|
||||
expect(task.description).toContain("Registration Form");
|
||||
expect(task.description).toContain("Create registration form");
|
||||
expect(task.description).toContain("Form submits successfully");
|
||||
});
|
||||
|
||||
it("triageFeature with custom description override skips enrichment", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
// Create full hierarchy
|
||||
const mission = missionStore.createMission({
|
||||
title: "Full Mission",
|
||||
description: "Full mission description",
|
||||
});
|
||||
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "Full Milestone",
|
||||
description: "Full milestone description",
|
||||
verification: "Full verification",
|
||||
planningNotes: "Full notes",
|
||||
});
|
||||
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Full Slice",
|
||||
description: "Full slice description",
|
||||
verification: "Full slice verification",
|
||||
planningNotes: "Full slice notes",
|
||||
});
|
||||
|
||||
const feature = missionStore.addFeature(slice.id, {
|
||||
title: "Custom Feature",
|
||||
description: "Custom feature description",
|
||||
});
|
||||
|
||||
// Triage with custom description override
|
||||
await missionStore.triageFeature(
|
||||
feature.id,
|
||||
undefined, // title uses default
|
||||
"Custom description override", // description override
|
||||
);
|
||||
|
||||
const updatedFeature = missionStore.getFeature(feature.id);
|
||||
const task = await taskStore.getTask(updatedFeature!.taskId!);
|
||||
|
||||
// Custom description should be used exactly
|
||||
expect(task.description).toBe("Custom description override");
|
||||
// Mission context should NOT be present
|
||||
expect(task.description).not.toContain("Full Mission");
|
||||
expect(task.description).not.toContain("Full mission description");
|
||||
expect(task.description).not.toContain("Full Milestone");
|
||||
});
|
||||
|
||||
it("triageSlice enriches all feature tasks with hierarchy context", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
// Create hierarchy with multiple features
|
||||
const mission = missionStore.createMission({
|
||||
title: "Multi Feature Mission",
|
||||
description: "Testing multiple features",
|
||||
});
|
||||
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "Multi Feature Milestone",
|
||||
description: "Multiple features milestone",
|
||||
verification: "All features complete",
|
||||
planningNotes: "Coordinate development",
|
||||
});
|
||||
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Multi Feature Slice",
|
||||
description: "Multiple features slice",
|
||||
verification: "Slice verification",
|
||||
planningNotes: "Slice planning",
|
||||
});
|
||||
|
||||
// Add 3 features
|
||||
const feature1 = missionStore.addFeature(slice.id, {
|
||||
title: "Feature One",
|
||||
description: "First feature description",
|
||||
acceptanceCriteria: "First criterion",
|
||||
});
|
||||
|
||||
const feature2 = missionStore.addFeature(slice.id, {
|
||||
title: "Feature Two",
|
||||
description: "Second feature description",
|
||||
acceptanceCriteria: "Second criterion",
|
||||
});
|
||||
|
||||
const feature3 = missionStore.addFeature(slice.id, {
|
||||
title: "Feature Three",
|
||||
description: "Third feature description",
|
||||
acceptanceCriteria: "Third criterion",
|
||||
});
|
||||
|
||||
// Triage all features in the slice
|
||||
await missionStore.triageSlice(slice.id);
|
||||
|
||||
// Check all 3 tasks have enriched descriptions
|
||||
for (const feature of [feature1, feature2, feature3]) {
|
||||
const updatedFeature = missionStore.getFeature(feature.id);
|
||||
const task = await taskStore.getTask(updatedFeature!.taskId!);
|
||||
|
||||
// All tasks should have hierarchy context
|
||||
expect(task.description).toContain("Multi Feature Mission");
|
||||
expect(task.description).toContain("Multi Feature Milestone");
|
||||
expect(task.description).toContain("Multi Feature Slice");
|
||||
// Each task should have its own feature-specific content
|
||||
expect(task.description).toContain(feature.title);
|
||||
expect(task.description).toContain(feature.description!);
|
||||
expect(task.description).toContain(feature.acceptanceCriteria!);
|
||||
}
|
||||
});
|
||||
|
||||
it("enriched description reflects updates after interview", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
// Create initial hierarchy
|
||||
const mission = missionStore.createMission({
|
||||
title: "Evolving Mission",
|
||||
description: "Initial mission",
|
||||
});
|
||||
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "Evolving Milestone",
|
||||
description: "Initial milestone",
|
||||
planningNotes: "Initial notes",
|
||||
});
|
||||
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Evolving Slice",
|
||||
description: "Initial slice",
|
||||
planningNotes: "Initial slice notes",
|
||||
});
|
||||
|
||||
const feature1 = missionStore.addFeature(slice.id, {
|
||||
title: "Feature Alpha",
|
||||
description: "First feature",
|
||||
});
|
||||
|
||||
const feature2 = missionStore.addFeature(slice.id, {
|
||||
title: "Feature Beta",
|
||||
description: "Second feature",
|
||||
});
|
||||
|
||||
// Triage first feature
|
||||
await missionStore.triageFeature(feature1.id);
|
||||
const task1 = await taskStore.getTask(missionStore.getFeature(feature1.id)!.taskId!);
|
||||
|
||||
// Verify initial enrichment
|
||||
expect(task1.description).toContain("Initial notes");
|
||||
expect(task1.description).toContain("Initial slice notes");
|
||||
|
||||
// Update milestone and slice after "interview"
|
||||
missionStore.updateMilestone(milestone.id, {
|
||||
planningNotes: "Revised milestone planning: Use JWT tokens, add refresh token support",
|
||||
});
|
||||
|
||||
missionStore.updateSlice(slice.id, {
|
||||
planningNotes: "Revised slice planning: Use React Hook Form, add validation",
|
||||
});
|
||||
|
||||
// Triage second feature
|
||||
await missionStore.triageFeature(feature2.id);
|
||||
const task2 = await taskStore.getTask(missionStore.getFeature(feature2.id)!.taskId!);
|
||||
|
||||
// Second task should have updated planning notes
|
||||
expect(task2.description).toContain("Revised milestone planning");
|
||||
expect(task2.description).toContain("Revised slice planning");
|
||||
// First task should still have original notes (historical)
|
||||
expect(task1.description).toContain("Initial notes");
|
||||
});
|
||||
});
|
||||
|
||||
describe("planState transitions", () => {
|
||||
it("defaults planState to not_started for new slices", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({ title: "Plan State Test" });
|
||||
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
||||
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
||||
|
||||
expect(slice.planState).toBe("not_started");
|
||||
});
|
||||
|
||||
it("transitions planState to planned after interview", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({ title: "Plan State Test" });
|
||||
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
||||
const slice = missionStore.addSlice(milestone.id, { title: "Test Slice" });
|
||||
|
||||
// Simulate interview completion by updating planState
|
||||
const updated = missionStore.updateSlice(slice.id, {
|
||||
planState: "planned",
|
||||
planningNotes: "Interview completed with decisions documented",
|
||||
verification: "All acceptance criteria met",
|
||||
});
|
||||
|
||||
expect(updated.planState).toBe("planned");
|
||||
expect(updated.planningNotes).toBe("Interview completed with decisions documented");
|
||||
expect(updated.verification).toBe("All acceptance criteria met");
|
||||
});
|
||||
|
||||
it("transitions planState to needs_update when revisions needed", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({ title: "Plan State Test" });
|
||||
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Test Slice",
|
||||
});
|
||||
|
||||
// Slice should default to not_started
|
||||
expect(slice.planState).toBe("not_started");
|
||||
|
||||
// Simulate interview completion by updating planState
|
||||
let updated = missionStore.updateSlice(slice.id, {
|
||||
planState: "planned",
|
||||
planningNotes: "Interview completed with decisions documented",
|
||||
verification: "All acceptance criteria met",
|
||||
});
|
||||
|
||||
expect(updated.planState).toBe("planned");
|
||||
|
||||
// Simulate requesting updates
|
||||
updated = missionStore.updateSlice(slice.id, {
|
||||
planState: "needs_update",
|
||||
});
|
||||
|
||||
expect(updated.planState).toBe("needs_update");
|
||||
});
|
||||
|
||||
it("planState changes do not affect milestone or mission status", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({ title: "Status Test" });
|
||||
const milestone = missionStore.addMilestone(mission.id, { title: "Test Milestone" });
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Test Slice",
|
||||
});
|
||||
|
||||
// New missions are "planning" status
|
||||
expect(mission.status).toBe("planning");
|
||||
// New milestones are "planning" status
|
||||
expect(milestone.status).toBe("planning");
|
||||
expect(slice.status).toBe("pending");
|
||||
expect(slice.status).toBe("pending");
|
||||
|
||||
// Change planState multiple times
|
||||
missionStore.updateSlice(slice.id, { planState: "planned" });
|
||||
missionStore.updateSlice(slice.id, { planState: "needs_update" });
|
||||
missionStore.updateSlice(slice.id, { planState: "planned" });
|
||||
|
||||
// Status should remain unchanged
|
||||
const refreshedMission = missionStore.getMission(mission.id);
|
||||
const refreshedMilestone = missionStore.getMilestone(milestone.id);
|
||||
const refreshedSlice = missionStore.getSlice(slice.id);
|
||||
|
||||
expect(refreshedMission?.status).toBe("planning");
|
||||
expect(refreshedMilestone?.status).toBe("planning");
|
||||
expect(refreshedSlice?.status).toBe("pending");
|
||||
});
|
||||
});
|
||||
|
||||
describe("milestone interview state integration", () => {
|
||||
it("milestone interviewState transitions work correctly", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({ title: "Interview Test" });
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "Test Milestone",
|
||||
});
|
||||
|
||||
// interviewState defaults to not_started
|
||||
expect(milestone.interviewState).toBe("not_started");
|
||||
|
||||
// Transition to in_progress
|
||||
let updated = missionStore.updateMilestone(milestone.id, {
|
||||
interviewState: "in_progress",
|
||||
});
|
||||
expect(updated.interviewState).toBe("in_progress");
|
||||
|
||||
// Complete the interview
|
||||
updated = missionStore.updateMilestone(milestone.id, {
|
||||
interviewState: "completed",
|
||||
planningNotes: "Interview completed successfully",
|
||||
verification: "All requirements captured",
|
||||
});
|
||||
expect(updated.interviewState).toBe("completed");
|
||||
expect(updated.planningNotes).toBe("Interview completed successfully");
|
||||
expect(updated.verification).toBe("All requirements captured");
|
||||
|
||||
// Request update
|
||||
updated = missionStore.updateMilestone(milestone.id, {
|
||||
interviewState: "needs_update",
|
||||
});
|
||||
expect(updated.interviewState).toBe("needs_update");
|
||||
});
|
||||
|
||||
it("enriched description includes milestone interview state", async () => {
|
||||
const missionStore = taskStore.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({
|
||||
title: "Interview Context Test",
|
||||
description: "Mission with interview context",
|
||||
});
|
||||
|
||||
// First create milestone, then update with interview results
|
||||
const milestone = missionStore.addMilestone(mission.id, {
|
||||
title: "Interviewed Milestone",
|
||||
description: "Milestone after interview",
|
||||
});
|
||||
|
||||
// Simulate interview completion
|
||||
missionStore.updateMilestone(milestone.id, {
|
||||
interviewState: "completed",
|
||||
verification: "Verified criteria",
|
||||
planningNotes: "Key decisions from interview",
|
||||
});
|
||||
|
||||
const slice = missionStore.addSlice(milestone.id, {
|
||||
title: "Test Slice",
|
||||
});
|
||||
|
||||
const feature = missionStore.addFeature(slice.id, {
|
||||
title: "Test Feature",
|
||||
description: "Feature description",
|
||||
});
|
||||
|
||||
const enriched = missionStore.buildEnrichedDescription(feature.id);
|
||||
|
||||
expect(enriched).toContain("Interviewed Milestone");
|
||||
expect(enriched).toContain("Key decisions from interview");
|
||||
expect(enriched).toContain("Verified criteria");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -882,13 +882,15 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
orderIndex,
|
||||
interviewState: "not_started",
|
||||
dependencies: input.dependencies || [],
|
||||
planningNotes: input.planningNotes,
|
||||
verification: input.verification,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.db.prepare(`
|
||||
INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
milestone.id,
|
||||
milestone.missionId,
|
||||
@@ -898,6 +900,8 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
milestone.orderIndex,
|
||||
milestone.interviewState,
|
||||
toJson(milestone.dependencies),
|
||||
milestone.planningNotes ?? null,
|
||||
milestone.verification ?? null,
|
||||
milestone.createdAt,
|
||||
milestone.updatedAt,
|
||||
);
|
||||
@@ -1089,13 +1093,15 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
status: "pending",
|
||||
planState: "not_started",
|
||||
orderIndex,
|
||||
planningNotes: input.planningNotes,
|
||||
verification: input.verification,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.db.prepare(`
|
||||
INSERT INTO slices (id, milestoneId, title, description, status, orderIndex, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO slices (id, milestoneId, title, description, status, orderIndex, planState, planningNotes, verification, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
slice.id,
|
||||
slice.milestoneId,
|
||||
@@ -1103,6 +1109,9 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
slice.description ?? null,
|
||||
slice.status,
|
||||
slice.orderIndex,
|
||||
slice.planState,
|
||||
slice.planningNotes ?? null,
|
||||
slice.verification ?? null,
|
||||
slice.createdAt,
|
||||
slice.updatedAt,
|
||||
);
|
||||
@@ -1610,6 +1619,90 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
|
||||
// ── Triage Operations ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build an enriched task description that includes the full mission hierarchy context.
|
||||
*
|
||||
* When a feature is triaged to a task, this method constructs a structured markdown
|
||||
* description that includes context from all levels of the hierarchy:
|
||||
* - Mission: title and description
|
||||
* - Milestone: title, description, verification criteria, planning notes
|
||||
* - Slice: title, description, verification criteria, planning notes
|
||||
* - Feature: description and acceptance criteria
|
||||
*
|
||||
* Only non-empty fields are included in the output. This provides AI agents
|
||||
* with full context for making informed decisions during task implementation.
|
||||
*
|
||||
* @param featureId - Feature ID to build enriched description for
|
||||
* @returns The enriched description string, or undefined if feature not found
|
||||
*/
|
||||
buildEnrichedDescription(featureId: string): string | undefined {
|
||||
const feature = this.getFeature(featureId);
|
||||
if (!feature) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const slice = this.getSlice(feature.sliceId);
|
||||
if (!slice) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const milestone = this.getMilestone(slice.milestoneId);
|
||||
if (!milestone) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mission = this.getMission(milestone.missionId);
|
||||
if (!mission) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
// Mission context (always included)
|
||||
sections.push(`## Mission: ${mission.title}`);
|
||||
if (mission.description) {
|
||||
sections.push(mission.description);
|
||||
}
|
||||
|
||||
// Milestone context
|
||||
const milestoneSections: string[] = [`## Milestone: ${milestone.title}`];
|
||||
if (milestone.description) {
|
||||
milestoneSections.push(`**Description:** ${milestone.description}`);
|
||||
}
|
||||
if (milestone.verification) {
|
||||
milestoneSections.push(`**Verification:** ${milestone.verification}`);
|
||||
}
|
||||
if (milestone.planningNotes) {
|
||||
milestoneSections.push(`**Planning Notes:** ${milestone.planningNotes}`);
|
||||
}
|
||||
sections.push(milestoneSections.join("\n"));
|
||||
|
||||
// Slice context
|
||||
const sliceSections: string[] = [`## Slice: ${slice.title}`];
|
||||
if (slice.description) {
|
||||
sliceSections.push(`**Description:** ${slice.description}`);
|
||||
}
|
||||
if (slice.verification) {
|
||||
sliceSections.push(`**Verification:** ${slice.verification}`);
|
||||
}
|
||||
if (slice.planningNotes) {
|
||||
sliceSections.push(`**Planning Notes:** ${slice.planningNotes}`);
|
||||
}
|
||||
sections.push(sliceSections.join("\n"));
|
||||
|
||||
// Feature context
|
||||
const featureSections: string[] = [`## Feature: ${feature.title}`];
|
||||
if (feature.description) {
|
||||
featureSections.push(feature.description);
|
||||
}
|
||||
if (feature.acceptanceCriteria) {
|
||||
featureSections.push(`**Acceptance Criteria:**\n${feature.acceptanceCriteria}`);
|
||||
}
|
||||
sections.push(featureSections.join("\n"));
|
||||
|
||||
return sections.join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Triage a feature by creating a new task and linking it.
|
||||
*
|
||||
@@ -1617,11 +1710,14 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
* the feature to the newly created task using `linkFeatureToTask()`.
|
||||
* The feature status transitions from "defined" to "triaged".
|
||||
*
|
||||
* When no custom description is provided, the task description is enriched
|
||||
* with the full mission hierarchy context (mission → milestone → slice → feature).
|
||||
*
|
||||
* Requires MissionStore to have been constructed with a TaskStore reference.
|
||||
*
|
||||
* @param featureId - Feature ID to triage
|
||||
* @param taskTitle - Optional title override (defaults to feature title)
|
||||
* @param taskDescription - Optional description override (defaults to feature description + acceptance criteria)
|
||||
* @param taskDescription - Optional description override (skips enrichment if provided)
|
||||
* @returns The updated feature with taskId set
|
||||
* @throws Error if feature not found, already triaged, or TaskStore not available
|
||||
*/
|
||||
@@ -1643,11 +1739,16 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
throw new Error(`Feature ${featureId} is already ${feature.status} (status must be "defined" to triage)`);
|
||||
}
|
||||
|
||||
// Build description from feature + acceptance criteria
|
||||
const description = taskDescription || [
|
||||
feature.description,
|
||||
feature.acceptanceCriteria ? `\n**Acceptance Criteria:**\n${feature.acceptanceCriteria}` : "",
|
||||
].filter(Boolean).join("\n\n") || feature.title;
|
||||
// Build description: use custom description if provided, otherwise use enriched description
|
||||
let description: string;
|
||||
if (taskDescription) {
|
||||
// Custom description provided - skip enrichment
|
||||
description = taskDescription;
|
||||
} else {
|
||||
// Use enriched description with full hierarchy context
|
||||
const enriched = this.buildEnrichedDescription(featureId);
|
||||
description = enriched || feature.title;
|
||||
}
|
||||
|
||||
// Create the task
|
||||
const task = await this.taskStore.createTask({
|
||||
|
||||
Reference in New Issue
Block a user