feat(KB-635): add mission management CLI commands and engine integration
- Add comprehensive mission CLI commands: create, list, show, start, abort, delete, and next - Add Pi extension tools for mission operations: list_missions, show_mission, start_mission, abort_mission - Implement mission-aware scheduler with task start handling and autoAdvance support - Add sliceId to Task type and autoAdvance flag to Mission type for slice sequencing - Add MissionStore helper methods for mission lifecycle operations - Include comprehensive tests for CLI commands and Pi extension mission tools
This commit is contained in:
@@ -975,6 +975,445 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
},
|
||||
});
|
||||
|
||||
// ── Mission Tools ───────────────────────────────────────────────
|
||||
// Mission hierarchy management for multi-phase project planning
|
||||
|
||||
// ── kb_mission_create ───────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_mission_create",
|
||||
label: "KB: Create Mission",
|
||||
description:
|
||||
"Create a new mission — a high-level objective that can span multiple milestones. " +
|
||||
"Missions contain milestones that break down work into phases.",
|
||||
promptSnippet: "Create a new mission for high-level project planning",
|
||||
promptGuidelines: [
|
||||
"Use for high-level project objectives that span multiple work phases",
|
||||
"Missions are broken down into milestones → slices → features → tasks",
|
||||
"Be descriptive so the mission purpose is clear",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
title: Type.String({ description: "Mission title — brief but descriptive" }),
|
||||
description: Type.Optional(
|
||||
Type.String({ description: "Detailed mission objectives and context" })
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const mission = missionStore.createMission({
|
||||
title: params.title.trim(),
|
||||
description: params.description?.trim(),
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Created ${mission.id}: ${mission.title}\nStatus: ${mission.status}`,
|
||||
},
|
||||
],
|
||||
details: { missionId: mission.id, title: mission.title, status: mission.status },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_mission_list ──────────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_mission_list",
|
||||
label: "KB: List Missions",
|
||||
description: "List all missions with their current status.",
|
||||
promptSnippet: "List all missions",
|
||||
promptGuidelines: [
|
||||
"Use to see all missions and their current status",
|
||||
"Missions are grouped by status (active, planning, complete, etc.)",
|
||||
"Use before kb_mission_show to find a specific mission ID",
|
||||
],
|
||||
parameters: Type.Object({}),
|
||||
|
||||
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const missions = missionStore.listMissions();
|
||||
|
||||
if (missions.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No missions yet." }],
|
||||
details: { count: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`Missions (${missions.length}):\n`);
|
||||
|
||||
for (const mission of missions) {
|
||||
const statusIcon = mission.status === "complete" ? "✓" : mission.status === "active" ? "●" : "○";
|
||||
lines.push(` ${statusIcon} ${mission.id}: ${mission.title} (${mission.status})`);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { count: missions.length, missions: missions.map((m) => ({ id: m.id, title: m.title, status: m.status })) },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_mission_show ──────────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_mission_show",
|
||||
label: "KB: Show Mission",
|
||||
description: "Show mission details with full hierarchy: milestones → slices → features.",
|
||||
promptSnippet: "Show mission details with hierarchy",
|
||||
promptGuidelines: [
|
||||
"Use to see the full mission structure before planning work",
|
||||
"Shows milestones, slices, and features in hierarchical order",
|
||||
"Check slice status to see if features can be linked to tasks",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
id: Type.String({ description: "Mission ID (e.g., M-001)" }),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const mission = missionStore.getMissionWithHierarchy(params.id);
|
||||
if (!mission) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Mission ${params.id} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Mission not found" },
|
||||
};
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`${mission.id}: ${mission.title}`);
|
||||
lines.push(`Status: ${mission.status}`);
|
||||
if (mission.description) {
|
||||
lines.push(`Description: ${mission.description}`);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
if (mission.milestones.length === 0) {
|
||||
lines.push("No milestones yet.");
|
||||
} else {
|
||||
lines.push("Milestones:");
|
||||
for (const milestone of mission.milestones) {
|
||||
const mIcon = milestone.status === "complete" ? "✓" : milestone.status === "active" ? "●" : "○";
|
||||
lines.push(` ${mIcon} ${milestone.id}: ${milestone.title} (${milestone.status})`);
|
||||
|
||||
for (const slice of milestone.slices) {
|
||||
const sIcon = slice.status === "complete" ? "✓" : slice.status === "active" ? "●" : "○";
|
||||
lines.push(` ${sIcon} ${slice.id}: ${slice.title} (${slice.status})`);
|
||||
|
||||
for (const feature of slice.features) {
|
||||
const fIcon = feature.status === "done" ? "✓" : feature.status === "in-progress" ? "▸" : feature.status === "triaged" ? "●" : "○";
|
||||
const taskLink = feature.taskId ? ` → ${feature.taskId}` : "";
|
||||
lines.push(` ${fIcon} ${feature.id}: ${feature.title} (${feature.status})${taskLink}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
details: { mission },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_mission_delete ───────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_mission_delete",
|
||||
label: "KB: Delete Mission",
|
||||
description: "Delete a mission and all its milestones, slices, and features. Cannot be undone.",
|
||||
promptSnippet: "Delete a mission and all its contents",
|
||||
promptGuidelines: [
|
||||
"Use for cleaning up test missions or mistakenly created missions",
|
||||
"Permanently deletes all milestones, slices, and features within the mission",
|
||||
"Tasks linked to features are NOT deleted — only the feature links are removed",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
id: Type.String({ description: "Mission ID to delete (e.g., M-001)" }),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const mission = missionStore.getMission(params.id);
|
||||
if (!mission) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Mission ${params.id} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Mission not found" },
|
||||
};
|
||||
}
|
||||
|
||||
missionStore.deleteMission(params.id);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Deleted ${params.id}: "${mission.title}"` }],
|
||||
details: { missionId: params.id, title: mission.title },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_milestone_add ────────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_milestone_add",
|
||||
label: "KB: Add Milestone",
|
||||
description: "Add a milestone to a mission. Milestones represent phases of work.",
|
||||
promptSnippet: "Add a milestone to a mission",
|
||||
promptGuidelines: [
|
||||
"Use to break down a mission into manageable phases",
|
||||
"Milestones are ordered and contain slices (work units)",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
missionId: Type.String({ description: "Parent mission ID (e.g., M-001)" }),
|
||||
title: Type.String({ description: "Milestone title" }),
|
||||
description: Type.Optional(Type.String({ description: "Milestone description" })),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const mission = missionStore.getMission(params.missionId);
|
||||
if (!mission) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Mission ${params.missionId} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Mission not found" },
|
||||
};
|
||||
}
|
||||
|
||||
const milestone = missionStore.addMilestone(params.missionId, {
|
||||
title: params.title.trim(),
|
||||
description: params.description?.trim(),
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Added ${milestone.id}: "${milestone.title}" to ${params.missionId}` },
|
||||
],
|
||||
details: { milestoneId: milestone.id, missionId: params.missionId, title: milestone.title },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_slice_add ─────────────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_slice_add",
|
||||
label: "KB: Add Slice",
|
||||
description: "Add a slice to a milestone. Slices are work units that can be activated for implementation.",
|
||||
promptSnippet: "Add a work slice to a milestone",
|
||||
promptGuidelines: [
|
||||
"Slices represent work units within a milestone",
|
||||
"Slices are activated for implementation, linking features to tasks",
|
||||
"Order slices by priority — they execute in sequence",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
milestoneId: Type.String({ description: "Parent milestone ID (e.g., MS-001)" }),
|
||||
title: Type.String({ description: "Slice title" }),
|
||||
description: Type.Optional(Type.String({ description: "Slice description" })),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const milestone = missionStore.getMilestone(params.milestoneId);
|
||||
if (!milestone) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Milestone ${params.milestoneId} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Milestone not found" },
|
||||
};
|
||||
}
|
||||
|
||||
const slice = missionStore.addSlice(params.milestoneId, {
|
||||
title: params.title.trim(),
|
||||
description: params.description?.trim(),
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Added ${slice.id}: "${slice.title}" to ${params.milestoneId}` },
|
||||
],
|
||||
details: { sliceId: slice.id, milestoneId: params.milestoneId, title: slice.title },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_feature_add ────────────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_feature_add",
|
||||
label: "KB: Add Feature",
|
||||
description: "Add a feature to a slice. Features are deliverables that can be linked to tasks.",
|
||||
promptSnippet: "Add a feature to a slice",
|
||||
promptGuidelines: [
|
||||
"Features represent deliverables within a slice",
|
||||
"Features start as 'defined' and progress through 'triaged' → 'in-progress' → 'done'",
|
||||
"Link features to tasks using kb_feature_link_task",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
sliceId: Type.String({ description: "Parent slice ID (e.g., SL-001)" }),
|
||||
title: Type.String({ description: "Feature title" }),
|
||||
description: Type.Optional(Type.String({ description: "Feature description" })),
|
||||
acceptanceCriteria: Type.Optional(
|
||||
Type.String({ description: "Acceptance criteria for completing the feature" })
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const slice = missionStore.getSlice(params.sliceId);
|
||||
if (!slice) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Slice ${params.sliceId} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Slice not found" },
|
||||
};
|
||||
}
|
||||
|
||||
const feature = missionStore.addFeature(params.sliceId, {
|
||||
title: params.title.trim(),
|
||||
description: params.description?.trim(),
|
||||
acceptanceCriteria: params.acceptanceCriteria?.trim(),
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Added ${feature.id}: "${feature.title}" to ${params.sliceId}` },
|
||||
],
|
||||
details: { featureId: feature.id, sliceId: params.sliceId, title: feature.title },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_slice_activate ────────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_slice_activate",
|
||||
label: "KB: Activate Slice",
|
||||
description:
|
||||
"Activate a pending slice for implementation. " +
|
||||
"Sets status to 'active' and enables task linking for its features.",
|
||||
promptSnippet: "Activate a slice for implementation",
|
||||
promptGuidelines: [
|
||||
"Activating a slice allows its features to be linked to tasks",
|
||||
"Only pending slices can be activated",
|
||||
"Slice activation triggers auto-advance when linked tasks complete",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
id: Type.String({ description: "Slice ID to activate (e.g., SL-001)" }),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const slice = missionStore.getSlice(params.id);
|
||||
if (!slice) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Slice ${params.id} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Slice not found" },
|
||||
};
|
||||
}
|
||||
|
||||
if (slice.status !== "pending") {
|
||||
return {
|
||||
content: [{ type: "text", text: `Slice ${params.id} is not pending (status: ${slice.status})` }],
|
||||
isError: true,
|
||||
details: { error: "Slice not pending", currentStatus: slice.status },
|
||||
};
|
||||
}
|
||||
|
||||
const activated = missionStore.activateSlice(params.id);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Activated ${activated.id}: "${activated.title}"\nStatus: ${activated.status}`,
|
||||
},
|
||||
],
|
||||
details: { sliceId: activated.id, title: activated.title, status: activated.status },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── kb_feature_link_task ──────────────────────────────────────────
|
||||
|
||||
pi.registerTool({
|
||||
name: "kb_feature_link_task",
|
||||
label: "KB: Link Feature to Task",
|
||||
description:
|
||||
"Link a feature to a kb task for implementation. " +
|
||||
"Updates the feature status to 'triaged' and associates it with the task.",
|
||||
promptSnippet: "Link a feature to a task",
|
||||
promptGuidelines: [
|
||||
"Use when a feature is ready for implementation and has a corresponding task",
|
||||
"The feature's slice must be active to link tasks",
|
||||
"Linking updates the feature status to 'triaged'",
|
||||
"When the linked task moves to 'done', the feature status becomes 'done'",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
featureId: Type.String({ description: "Feature ID to link (e.g., F-001)" }),
|
||||
taskId: Type.String({ description: "Task ID to link to (e.g., KB-001)" }),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const store = await getStore(ctx.cwd);
|
||||
const missionStore = store.getMissionStore();
|
||||
|
||||
const feature = missionStore.getFeature(params.featureId);
|
||||
if (!feature) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Feature ${params.featureId} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Feature not found" },
|
||||
};
|
||||
}
|
||||
|
||||
// Check if task exists
|
||||
try {
|
||||
await store.getTask(params.taskId);
|
||||
} catch {
|
||||
return {
|
||||
content: [{ type: "text", text: `Task ${params.taskId} not found` }],
|
||||
isError: true,
|
||||
details: { error: "Task not found" },
|
||||
};
|
||||
}
|
||||
|
||||
const updated = missionStore.linkFeatureToTask(params.featureId, params.taskId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Linked ${updated.id}: "${updated.title}" → ${params.taskId}\nStatus: ${updated.status}`,
|
||||
},
|
||||
],
|
||||
details: { featureId: updated.id, taskId: params.taskId, title: updated.title, status: updated.status },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ── /fn command — start the dashboard + engine ───────────────────
|
||||
|
||||
let dashboardProcess: ChildProcess | null = null;
|
||||
|
||||
Reference in New Issue
Block a user