@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,247 lines (1,246 loc) • 70.8 kB
JavaScript
import { randomUUID } from 'crypto';
import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js';
/**
* Create a new product roadmap
*/
const createRoadmapTool = createTool({
name: 'create_roadmap',
description: 'Create a new product roadmap with vision and strategic timeline',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the product roadmap',
minLength: 1,
maxLength: 200
},
vision: {
type: 'string',
description: 'Product vision statement',
minLength: 1,
maxLength: 1000
},
timeHorizon: {
type: 'string',
enum: ['quarterly', 'annual', 'multi-year'],
description: 'Planning time horizon'
},
owner: {
type: 'string',
description: 'Product owner name',
minLength: 1,
maxLength: 100
},
stakeholders: {
type: 'array',
items: { type: 'string', maxLength: 100 },
description: 'List of stakeholder names',
maxItems: 20
}
},
required: ['name', 'vision', 'timeHorizon', 'owner'],
additionalProperties: false
},
async execute(input, context) {
try {
const roadmapId = `roadmap-${randomUUID()}`;
const now = Date.now();
// Check for duplicate roadmap names
const existingCheck = await context.db.get('SELECT id FROM product_roadmaps WHERE name = ? AND project_id = ?', [input.name, context.projectId || 'default']);
if (existingCheck.success && existingCheck.data) {
return createErrorResult({
code: 'DUPLICATE_RESOURCE',
message: 'A roadmap with this name already exists',
category: 'validation'
});
}
// Insert roadmap into database
const result = await context.db.run(`INSERT INTO product_roadmaps
(id, project_id, name, vision, time_horizon, status, owner, stakeholders, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
roadmapId,
context.projectId || 'default',
input.name,
input.vision,
input.timeHorizon,
'draft',
input.owner,
JSON.stringify(input.stakeholders || []),
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create roadmap',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
roadmap: {
id: roadmapId,
name: input.name,
vision: input.vision,
timeHorizon: input.timeHorizon,
status: 'draft',
owner: input.owner,
stakeholders: input.stakeholders || []
},
message: `Product roadmap "${input.name}" created successfully`,
nextSteps: [
'Add strategic themes using add_roadmap_theme',
'Create initiatives within themes',
'Add features to initiatives',
'Set up milestones and releases',
'Prioritize features using prioritize_features'
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create roadmap: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Add a strategic theme to a roadmap
*/
const addRoadmapThemeTool = createTool({
name: 'add_roadmap_theme',
description: 'Add a strategic theme to a product roadmap',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
name: {
type: 'string',
description: 'Theme name',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Theme description',
minLength: 1,
maxLength: 1000
},
objectives: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Strategic objectives',
minItems: 1,
maxItems: 10
},
priority: {
type: 'string',
enum: ['must-have', 'should-have', 'nice-to-have'],
description: 'Theme priority'
},
startQuarter: {
type: 'string',
description: 'Start quarter (e.g., Q1 2024)',
pattern: '^Q[1-4] \\d{4}$'
},
endQuarter: {
type: 'string',
description: 'End quarter',
pattern: '^Q[1-4] \\d{4}$'
}
},
required: ['roadmapId', 'name', 'description', 'objectives', 'priority', 'startQuarter', 'endQuarter'],
additionalProperties: false
},
async execute(input, context) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get('SELECT id FROM product_roadmaps WHERE id = ? AND project_id = ?', [input.roadmapId, context.projectId || 'default']);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
const themeId = `theme-${randomUUID()}`;
const now = Date.now();
// Insert theme
const result = await context.db.run(`INSERT INTO roadmap_themes
(id, roadmap_id, name, description, objectives, priority, start_quarter, end_quarter, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
themeId,
input.roadmapId,
input.name,
input.description,
JSON.stringify(input.objectives),
input.priority,
input.startQuarter,
input.endQuarter,
'planned',
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create theme',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
theme: {
id: themeId,
name: input.name,
description: input.description,
objectives: input.objectives,
priority: input.priority,
timeframe: {
startQuarter: input.startQuarter,
endQuarter: input.endQuarter
},
status: 'planned'
},
message: `Theme "${input.name}" added to roadmap`,
nextSteps: [
'Create initiatives within this theme using create_initiative',
'Track progress through the theme lifecycle'
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to add theme: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Create a product initiative
*/
const createInitiativeTool = createTool({
name: 'create_initiative',
description: 'Create a product initiative within a theme',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
themeId: {
type: 'string',
description: 'ID of the theme',
pattern: '^theme-[a-f0-9-]+$'
},
title: {
type: 'string',
description: 'Initiative title',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Initiative description',
minLength: 1,
maxLength: 2000
},
estimatedValue: {
type: 'object',
properties: {
userImpact: {
type: 'string',
enum: ['low', 'medium', 'high', 'critical']
},
revenueImpact: {
type: 'number',
description: 'Estimated revenue impact',
minimum: 0
},
costSavings: {
type: 'number',
description: 'Estimated cost savings',
minimum: 0
},
strategicValue: {
type: 'integer',
description: 'Strategic value (1-10)',
minimum: 1,
maximum: 10
},
customerSatisfaction: {
type: 'integer',
description: 'Projected NPS impact',
minimum: -100,
maximum: 100
}
},
required: ['userImpact', 'revenueImpact', 'costSavings', 'strategicValue', 'customerSatisfaction'],
additionalProperties: false
},
estimatedEffort: {
type: 'object',
properties: {
developmentWeeks: {
type: 'number',
minimum: 0
},
designWeeks: {
type: 'number',
minimum: 0
},
qaWeeks: {
type: 'number',
minimum: 0
},
confidence: {
type: 'string',
enum: ['low', 'medium', 'high']
}
},
required: ['developmentWeeks', 'designWeeks', 'qaWeeks', 'confidence'],
additionalProperties: false
},
risks: {
type: 'array',
items: {
type: 'object',
properties: {
description: {
type: 'string',
maxLength: 500
},
likelihood: {
type: 'string',
enum: ['low', 'medium', 'high']
},
impact: {
type: 'string',
enum: ['low', 'medium', 'high']
},
mitigation: {
type: 'string',
maxLength: 500
}
},
required: ['description', 'likelihood', 'impact', 'mitigation'],
additionalProperties: false
},
maxItems: 10
}
},
required: ['roadmapId', 'themeId', 'title', 'description', 'estimatedValue', 'estimatedEffort'],
additionalProperties: false
},
async execute(input, context) {
try {
// Verify theme exists
const themeCheck = await context.db.get(`SELECT t.id FROM roadmap_themes t
JOIN product_roadmaps r ON t.roadmap_id = r.id
WHERE t.id = ? AND t.roadmap_id = ? AND r.project_id = ?`, [input.themeId, input.roadmapId, context.projectId || 'default']);
if (!themeCheck.success || !themeCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Theme not found in specified roadmap',
details: { themeId: input.themeId, roadmapId: input.roadmapId },
category: 'validation'
});
}
const initiativeId = `initiative-${randomUUID()}`;
const now = Date.now();
// Insert initiative
const result = await context.db.run(`INSERT INTO roadmap_initiatives
(id, theme_id, roadmap_id, title, description, status,
user_impact, revenue_impact, cost_savings, strategic_value, customer_satisfaction,
development_weeks, design_weeks, qa_weeks, effort_confidence,
risks, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
initiativeId,
input.themeId,
input.roadmapId,
input.title,
input.description,
'ideation',
input.estimatedValue.userImpact,
input.estimatedValue.revenueImpact,
input.estimatedValue.costSavings,
input.estimatedValue.strategicValue,
input.estimatedValue.customerSatisfaction,
input.estimatedEffort.developmentWeeks,
input.estimatedEffort.designWeeks,
input.estimatedEffort.qaWeeks,
input.estimatedEffort.confidence,
JSON.stringify(input.risks || []),
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create initiative',
details: { error: result.error },
category: 'system'
});
}
const totalEffort = input.estimatedEffort.developmentWeeks +
input.estimatedEffort.designWeeks +
input.estimatedEffort.qaWeeks;
return createSuccessResult({
initiative: {
id: initiativeId,
title: input.title,
description: input.description,
status: 'ideation',
value: input.estimatedValue,
effort: input.estimatedEffort,
totalEffortWeeks: totalEffort,
risks: input.risks || []
},
message: `Initiative "${input.title}" created`,
nextSteps: [
'Add features to this initiative using add_feature',
'Validate the initiative with stakeholders',
'Link to agile epics for implementation'
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create initiative: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Add a feature to an initiative
*/
const addFeatureTool = createTool({
name: 'add_feature',
description: 'Add a feature to an initiative',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
initiativeId: {
type: 'string',
description: 'ID of the initiative',
pattern: '^initiative-[a-f0-9-]+$'
},
name: {
type: 'string',
description: 'Feature name',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Feature description',
minLength: 1,
maxLength: 1000
},
businessValue: {
type: 'object',
properties: {
score: {
type: 'integer',
description: 'Business value score (1-100)',
minimum: 1,
maximum: 100
},
rationale: {
type: 'string',
description: 'Value rationale',
maxLength: 1000
},
metrics: {
type: 'array',
items: { type: 'string', maxLength: 200 },
description: 'Success metrics',
minItems: 1,
maxItems: 10
}
},
required: ['score', 'rationale', 'metrics'],
additionalProperties: false
},
technicalComplexity: {
type: 'string',
enum: ['low', 'medium', 'high', 'very-high'],
description: 'Technical complexity'
},
targetRelease: {
type: 'string',
description: 'Target release ID (optional)',
pattern: '^release-[a-f0-9-]+$'
}
},
required: ['roadmapId', 'initiativeId', 'name', 'description', 'businessValue', 'technicalComplexity'],
additionalProperties: false
},
async execute(input, context) {
try {
// Verify initiative exists
const initiativeCheck = await context.db.get(`SELECT i.id FROM roadmap_initiatives i
JOIN product_roadmaps r ON i.roadmap_id = r.id
WHERE i.id = ? AND i.roadmap_id = ? AND r.project_id = ?`, [input.initiativeId, input.roadmapId, context.projectId || 'default']);
if (!initiativeCheck.success || !initiativeCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Initiative not found in specified roadmap',
details: { initiativeId: input.initiativeId, roadmapId: input.roadmapId },
category: 'validation'
});
}
const featureId = `feature-${randomUUID()}`;
const now = Date.now();
// Insert feature
const result = await context.db.run(`INSERT INTO roadmap_features
(id, initiative_id, roadmap_id, name, description, status, priority,
business_value_score, business_value_rationale, business_value_metrics,
technical_complexity, target_release, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
featureId,
input.initiativeId,
input.roadmapId,
input.name,
input.description,
'proposed',
input.businessValue.score, // Using score as priority
input.businessValue.score,
input.businessValue.rationale,
JSON.stringify(input.businessValue.metrics),
input.technicalComplexity,
input.targetRelease || null,
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to add feature',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
feature: {
id: featureId,
name: input.name,
description: input.description,
status: 'proposed',
businessValue: input.businessValue,
technicalComplexity: input.technicalComplexity,
targetRelease: input.targetRelease
},
message: `Feature "${input.name}" added`,
nextSteps: [
'Prioritize features using prioritize_features',
'Assign to a release using plan_release',
'Link to user stories for implementation'
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to add feature: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update feature status
*/
const updateFeatureStatusTool = createTool({
name: 'update_feature_status',
description: 'Update the status of a feature',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
featureId: {
type: 'string',
description: 'ID of the feature',
pattern: '^feature-[a-f0-9-]+$'
},
status: {
type: 'string',
enum: ['proposed', 'approved', 'in-progress', 'completed', 'cancelled'],
description: 'New status'
}
},
required: ['featureId', 'status'],
additionalProperties: false
},
async execute(input, context) {
try {
const result = await context.db.run(`UPDATE roadmap_features
SET status = ?, updated_at = ?
WHERE id = ? AND roadmap_id IN (
SELECT id FROM product_roadmaps WHERE project_id = ?
)`, [input.status, Date.now(), input.featureId, context.projectId || 'default']);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update feature status',
details: { error: result.error },
category: 'system'
});
}
if (!result.data || result.data.changes === 0) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Feature not found',
details: { featureId: input.featureId },
category: 'validation'
});
}
// Get feature details
const feature = await context.db.get('SELECT name FROM roadmap_features WHERE id = ?', [input.featureId]);
return createSuccessResult({
featureId: input.featureId,
featureName: feature.data?.name || 'Unknown',
status: input.status,
message: `Feature status updated to: ${input.status}`
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update feature status: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update initiative status
*/
const updateInitiativeStatusTool = createTool({
name: 'update_initiative_status',
description: 'Update the status of an initiative',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
initiativeId: {
type: 'string',
description: 'ID of the initiative',
pattern: '^initiative-[a-f0-9-]+$'
},
status: {
type: 'string',
enum: ['ideation', 'validated', 'scheduled', 'in-development', 'launched'],
description: 'New status'
}
},
required: ['initiativeId', 'status'],
additionalProperties: false
},
async execute(input, context) {
try {
const result = await context.db.run(`UPDATE roadmap_initiatives
SET status = ?, updated_at = ?
WHERE id = ? AND roadmap_id IN (
SELECT id FROM product_roadmaps WHERE project_id = ?
)`, [input.status, Date.now(), input.initiativeId, context.projectId || 'default']);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update initiative status',
details: { error: result.error },
category: 'system'
});
}
if (!result.data || result.data.changes === 0) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Initiative not found',
details: { initiativeId: input.initiativeId },
category: 'validation'
});
}
// Get initiative details
const initiative = await context.db.get('SELECT title FROM roadmap_initiatives WHERE id = ?', [input.initiativeId]);
return createSuccessResult({
initiativeId: input.initiativeId,
initiativeTitle: initiative.data?.title || 'Unknown',
status: input.status,
message: `Initiative status updated to: ${input.status}`
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update initiative status: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Create a milestone
*/
const createMilestoneTool = createTool({
name: 'create_milestone',
description: 'Create a milestone in the roadmap',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
name: {
type: 'string',
description: 'Milestone name',
minLength: 1,
maxLength: 200
},
date: {
type: 'string',
format: 'date',
description: 'Milestone date (ISO format)'
},
type: {
type: 'string',
enum: ['release', 'business', 'technical', 'regulatory'],
description: 'Type of milestone'
},
description: {
type: 'string',
description: 'Milestone description',
minLength: 1,
maxLength: 1000
},
deliverables: {
type: 'array',
items: { type: 'string', maxLength: 200 },
description: 'List of deliverables',
minItems: 1,
maxItems: 20
},
dependencies: {
type: 'array',
items: {
type: 'string',
pattern: '^milestone-[a-f0-9-]+$'
},
description: 'List of dependency IDs',
maxItems: 10
}
},
required: ['roadmapId', 'name', 'date', 'type', 'description', 'deliverables'],
additionalProperties: false
},
async execute(input, context) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get('SELECT id FROM product_roadmaps WHERE id = ? AND project_id = ?', [input.roadmapId, context.projectId || 'default']);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
const milestoneId = `milestone-${randomUUID()}`;
const milestoneDate = new Date(input.date);
const now = Date.now();
// Insert milestone
const result = await context.db.run(`INSERT INTO roadmap_milestones
(id, roadmap_id, name, date, type, description, deliverables, dependencies, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
milestoneId,
input.roadmapId,
input.name,
milestoneDate.getTime(),
input.type,
input.description,
JSON.stringify(input.deliverables),
JSON.stringify(input.dependencies || []),
'upcoming',
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create milestone',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
milestone: {
id: milestoneId,
name: input.name,
date: input.date,
type: input.type,
description: input.description,
deliverables: input.deliverables,
dependencies: input.dependencies || [],
status: 'upcoming'
},
message: `Milestone "${input.name}" created`,
nextSteps: [
'Track milestone progress',
'Update status as work progresses',
'Link features to milestone deliverables'
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create milestone: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Plan a release
*/
const planReleaseTool = createTool({
name: 'plan_release',
description: 'Plan a product release',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
version: {
type: 'string',
description: 'Release version',
minLength: 1,
maxLength: 50
},
name: {
type: 'string',
description: 'Release name',
minLength: 1,
maxLength: 200
},
date: {
type: 'string',
format: 'date',
description: 'Release date (ISO format)'
},
features: {
type: 'array',
items: {
type: 'string',
pattern: '^feature-[a-f0-9-]+$'
},
description: 'Feature IDs to include',
minItems: 1,
maxItems: 100
},
themes: {
type: 'array',
items: {
type: 'string',
pattern: '^theme-[a-f0-9-]+$'
},
description: 'Theme IDs addressed',
maxItems: 20
},
goals: {
type: 'array',
items: { type: 'string', maxLength: 500 },
description: 'Release goals',
minItems: 1,
maxItems: 10
},
notes: {
type: 'string',
description: 'Release notes',
maxLength: 5000
}
},
required: ['roadmapId', 'version', 'name', 'date', 'features', 'goals'],
additionalProperties: false
},
async execute(input, context) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get('SELECT id FROM product_roadmaps WHERE id = ? AND project_id = ?', [input.roadmapId, context.projectId || 'default']);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
// Verify all features exist
const featurePlaceholders = input.features.map(() => '?').join(',');
const featureCheck = await context.db.all(`SELECT id FROM roadmap_features
WHERE id IN (${featurePlaceholders}) AND roadmap_id = ?`, [...input.features, input.roadmapId]);
if (!featureCheck.success || featureCheck.data.length !== input.features.length) {
return createErrorResult({
code: 'VALIDATION_ERROR',
message: 'One or more features not found in this roadmap',
category: 'validation'
});
}
const releaseId = `release-${randomUUID()}`;
const releaseDate = new Date(input.date);
const now = Date.now();
// Insert release
const result = await context.db.run(`INSERT INTO roadmap_releases
(id, roadmap_id, version, name, date, features, themes, goals, status, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
releaseId,
input.roadmapId,
input.version,
input.name,
releaseDate.getTime(),
JSON.stringify(input.features),
JSON.stringify(input.themes || []),
JSON.stringify(input.goals),
'planning',
input.notes || null,
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to plan release',
details: { error: result.error },
category: 'system'
});
}
return createSuccessResult({
release: {
id: releaseId,
version: input.version,
name: input.name,
date: input.date,
featureCount: input.features.length,
themeCount: (input.themes || []).length,
goals: input.goals,
status: 'planning'
},
message: `Release v${input.version} planned`,
nextSteps: [
'Review release scope with stakeholders',
'Update feature statuses as development progresses',
'Prepare release notes and documentation'
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to plan release: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Prioritize features
*/
const prioritizeFeaturesTool = createTool({
name: 'prioritize_features',
description: 'Prioritize features using various methods (RICE, MoSCoW, Value-Effort, etc.)',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
method: {
type: 'string',
enum: ['rice', 'value-effort', 'moscow', 'kano', 'custom'],
description: 'Prioritization method'
},
weights: {
type: 'object',
properties: {
businessValue: { type: 'number', minimum: 0, maximum: 1 },
userImpact: { type: 'number', minimum: 0, maximum: 1 },
strategicAlignment: { type: 'number', minimum: 0, maximum: 1 },
technicalFeasibility: { type: 'number', minimum: 0, maximum: 1 },
risk: { type: 'number', minimum: 0, maximum: 1 }
},
description: 'Custom weights (only for custom method)',
additionalProperties: false
},
scope: {
type: 'string',
enum: ['all', 'theme', 'initiative', 'unscheduled'],
description: 'Scope of prioritization',
default: 'all'
},
scopeId: {
type: 'string',
description: 'ID of theme/initiative if scope is specific'
}
},
required: ['roadmapId', 'method'],
additionalProperties: false
},
async execute(input, context) {
try {
// Build query based on scope
let query = `
SELECT f.*, i.user_impact, i.strategic_value, i.development_weeks
FROM roadmap_features f
JOIN roadmap_initiatives i ON f.initiative_id = i.id
WHERE f.roadmap_id = ?
`;
const params = [input.roadmapId];
if (input.scope === 'theme' && input.scopeId) {
query += ' AND i.theme_id = ?';
params.push(input.scopeId);
}
else if (input.scope === 'initiative' && input.scopeId) {
query += ' AND f.initiative_id = ?';
params.push(input.scopeId);
}
else if (input.scope === 'unscheduled') {
query += ' AND f.target_release IS NULL';
}
const features = await context.db.all(query, params);
if (!features.success || features.data.length === 0) {
return createErrorResult({
code: 'NO_DATA',
message: 'No features found for prioritization',
category: 'validation'
});
}
// Calculate scores based on method
const scoredFeatures = features.data.map((feature) => {
let score = 0;
let rationale = '';
switch (input.method) {
case 'rice':
// RICE = (Reach * Impact * Confidence) / Effort
const reach = 100; // Default reach
const impact = feature.user_impact === 'critical' ? 3 :
feature.user_impact === 'high' ? 2 :
feature.user_impact === 'medium' ? 1 : 0.5;
const confidence = 0.8; // Default confidence
const effort = feature.development_weeks || 1;
score = (reach * impact * confidence) / effort;
rationale = `RICE Score: Reach(${reach}) × Impact(${impact}) × Confidence(${confidence}) ÷ Effort(${effort}w)`;
break;
case 'value-effort':
const value = feature.business_value_score || 50;
const effortScore = feature.technical_complexity === 'very-high' ? 4 :
feature.technical_complexity === 'high' ? 3 :
feature.technical_complexity === 'medium' ? 2 : 1;
score = value / effortScore;
rationale = `Value(${value}) / Effort(${effortScore})`;
break;
case 'moscow':
// Assign scores based on business value ranges
if (feature.business_value_score >= 80) {
score = 4;
rationale = 'Must Have (80+ value)';
}
else if (feature.business_value_score >= 60) {
score = 3;
rationale = 'Should Have (60-79 value)';
}
else if (feature.business_value_score >= 40) {
score = 2;
rationale = 'Could Have (40-59 value)';
}
else {
score = 1;
rationale = 'Won\'t Have (<40 value)';
}
break;
case 'custom':
if (!input.weights) {
score = feature.priority || 50;
rationale = 'Using default priority';
}
else {
// Apply custom weights
const bv = (feature.business_value_score || 50) * (input.weights.businessValue || 0);
const ui = (feature.user_impact === 'critical' ? 100 :
feature.user_impact === 'high' ? 75 :
feature.user_impact === 'medium' ? 50 : 25) * (input.weights.userImpact || 0);
const sa = (feature.strategic_value || 5) * 10 * (input.weights.strategicAlignment || 0);
const tf = (feature.technical_complexity === 'low' ? 100 :
feature.technical_complexity === 'medium' ? 75 :
feature.technical_complexity === 'high' ? 50 : 25) * (input.weights.technicalFeasibility || 0);
score = bv + ui + sa + tf;
rationale = `Custom: BV(${bv.toFixed(1)}) + UI(${ui.toFixed(1)}) + SA(${sa.toFixed(1)}) + TF(${tf.toFixed(1)})`;
}
break;
default:
score = feature.priority || 50;
rationale = 'Default priority';
}
return {
featureId: feature.id,
name: feature.name,
score,
rationale,
currentStatus: feature.status,
complexity: feature.technical_complexity
};
});
// Sort by score descending
scoredFeatures.sort((a, b) => b.score - a.score);
// Add rank
scoredFeatures.forEach((f, index) => {
f.rank = index + 1;
});
// Return top 10 for display
const topFeatures = scoredFeatures.slice(0, 10);
return createSuccessResult({
method: input.method,
scope: input.scope,
totalFeatures: scoredFeatures.length,
topFeatures,
message: `Prioritized ${scoredFeatures.length} features using ${input.method.toUpperCase()} method`,
insights: [
`Highest priority: "${topFeatures[0].name}" (score: ${topFeatures[0].score.toFixed(2)})`,
`${scoredFeatures.filter((f) => f.currentStatus === 'proposed').length} features awaiting approval`,
`Consider reviewing low-complexity, high-value features for quick wins`
]
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to prioritize features: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Generate roadmap timeline
*/
const generateTimelineTool = createTool({
name: 'generate_roadmap_timeline',
description: 'Generate timeline views of the roadmap',
category: 'product-roadmap',
inputSchema: {
type: 'object',
properties: {
roadmapId: {
type: 'string',
description: 'ID of the roadmap',
pattern: '^roadmap-[a-f0-9-]+$'
},
viewType: {
type: 'string',
enum: ['quarterly', 'monthly', 'release', 'now-next-later'],
description: 'Type of timeline view'
},
startPeriod: {
type: 'string',
description: 'Start quarter/month (for quarterly/monthly views)'
},
endPeriod: {
type: 'string',
description: 'End quarter/month (for quarterly view)'
},
months: {
type: 'integer',
description: 'Number of months (for monthly view)',
minimum: 1,
maximum: 24,
default: 6
}
},
required: ['roadmapId', 'viewType'],
additionalProperties: false
},
async execute(input, context) {
try {
// Verify roadmap exists
const roadmapCheck = await context.db.get('SELECT * FROM product_roadmaps WHERE id = ? AND project_id = ?', [input.roadmapId, context.projectId || 'default']);
if (!roadmapCheck.success || !roadmapCheck.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Roadmap not found',
details: { roadmapId: input.roadmapId },
category: 'validation'
});
}
const roadmap = roadmapCheck.data;
let timelineData = {};
switch (input.viewType) {
case 'quarterly':
// Get themes for quarterly view
const themes = await context.db.all(`SELECT * FROM roadmap_themes
WHERE roadmap_id = ?
ORDER BY start_quarter`, [input.roadmapId]);
if (themes.success && themes.data) {
const quarters = new Set();
themes.data.forEach((theme) => {
quarters.add(theme.start_quarter);
quarters.add(theme.end_quarter);
});
timelineData = {
type: 'quarterly',
quarters: Array.from(quarters).sort(),
items: themes.data.map((theme) => ({
id: theme.id,
type: 'theme',
name: theme.name,
startQuarter: theme.start_quarter,
endQuarter: theme.end_quarter,
priority: theme.priority,
status: theme.status
}))
};
}
break;
case 'release':
// Get releases
const releases = await context.db.all(`SELECT * FROM roadmap_releases
WHERE roadmap_id = ?
ORDER BY date`, [input.roadmapId]);
if (releases.success && releases.data) {
// Get feature counts for each release
const releaseFeatures = await Promise.all(releases.data.map(async (release) => {
const features = JSON.parse(release.features);
return {
...release,
featureCount: features.length,
date: new Date(release.date).toISOString()