1450 lines
39 KiB
Markdown
1450 lines
39 KiB
Markdown
# Directus Task Management Suite - Implementation Guides
|
|
|
|
## Implementation Overview
|
|
|
|
This document provides detailed technical implementation guides for building the Directus Task Management Suite, organized by development phase with specific code examples, configuration steps, and best practices.
|
|
|
|
## Phase 1: Foundation Implementation Guide
|
|
|
|
### 1.1 Directus Collections Setup
|
|
|
|
#### Core Collections Schema Implementation
|
|
|
|
**Step 1: Create Projects Collection**
|
|
```sql
|
|
-- Execute via Directus API or admin interface
|
|
{
|
|
"collection": "projects",
|
|
"schema": {
|
|
"id": {
|
|
"type": "uuid",
|
|
"default": "$GENERATE_UUID",
|
|
"primary_key": true
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"required": true,
|
|
"max_length": 255
|
|
},
|
|
"description": {
|
|
"type": "text",
|
|
"nullable": true
|
|
},
|
|
"status": {
|
|
"type": "string",
|
|
"default": "active",
|
|
"options": {
|
|
"choices": [
|
|
{"text": "Active", "value": "active"},
|
|
{"text": "On Hold", "value": "on_hold"},
|
|
{"text": "Completed", "value": "completed"},
|
|
{"text": "Archived", "value": "archived"}
|
|
]
|
|
}
|
|
},
|
|
"parent_project": {
|
|
"type": "uuid",
|
|
"foreign_key": "projects.id",
|
|
"nullable": true
|
|
},
|
|
"repository_url": {
|
|
"type": "string",
|
|
"nullable": true
|
|
},
|
|
"bmad_workflow_type": {
|
|
"type": "string",
|
|
"options": {
|
|
"choices": [
|
|
{"text": "Greenfield", "value": "greenfield"},
|
|
{"text": "Brownfield", "value": "brownfield"},
|
|
{"text": "Maintenance", "value": "maintenance"}
|
|
]
|
|
}
|
|
},
|
|
"task_master_project_id": {
|
|
"type": "string",
|
|
"nullable": true
|
|
},
|
|
"priority": {
|
|
"type": "string",
|
|
"default": "medium",
|
|
"options": {
|
|
"choices": [
|
|
{"text": "Low", "value": "low"},
|
|
{"text": "Medium", "value": "medium"},
|
|
{"text": "High", "value": "high"},
|
|
{"text": "Critical", "value": "critical"}
|
|
]
|
|
}
|
|
},
|
|
"start_date": {
|
|
"type": "date",
|
|
"nullable": true
|
|
},
|
|
"due_date": {
|
|
"type": "date",
|
|
"nullable": true
|
|
},
|
|
"completion_percentage": {
|
|
"type": "integer",
|
|
"default": 0,
|
|
"validation": {
|
|
"min": 0,
|
|
"max": 100
|
|
}
|
|
},
|
|
"created_by": {
|
|
"type": "uuid",
|
|
"foreign_key": "directus_users.id"
|
|
},
|
|
"assigned_to": {
|
|
"type": "uuid",
|
|
"foreign_key": "directus_users.id",
|
|
"nullable": true
|
|
},
|
|
"metadata": {
|
|
"type": "json",
|
|
"nullable": true
|
|
},
|
|
"created_at": {
|
|
"type": "timestamp",
|
|
"auto_create": true
|
|
},
|
|
"updated_at": {
|
|
"type": "timestamp",
|
|
"auto_update": true
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Create Task Statuses Collection**
|
|
```sql
|
|
{
|
|
"collection": "task_statuses",
|
|
"schema": {
|
|
"id": {
|
|
"type": "uuid",
|
|
"default": "$GENERATE_UUID",
|
|
"primary_key": true
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"required": true,
|
|
"unique": true
|
|
},
|
|
"slug": {
|
|
"type": "string",
|
|
"required": true,
|
|
"unique": true
|
|
},
|
|
"description": {
|
|
"type": "text",
|
|
"nullable": true
|
|
},
|
|
"color": {
|
|
"type": "string",
|
|
"default": "#6366f1",
|
|
"validation": {
|
|
"pattern": "^#[0-9A-Fa-f]{6}$"
|
|
}
|
|
},
|
|
"icon": {
|
|
"type": "string",
|
|
"default": "circle"
|
|
},
|
|
"category": {
|
|
"type": "string",
|
|
"required": true,
|
|
"options": {
|
|
"choices": [
|
|
{"text": "To Do", "value": "todo"},
|
|
{"text": "In Progress", "value": "in_progress"},
|
|
{"text": "Review", "value": "review"},
|
|
{"text": "Done", "value": "done"},
|
|
{"text": "Blocked", "value": "blocked"}
|
|
]
|
|
}
|
|
},
|
|
"is_final": {
|
|
"type": "boolean",
|
|
"default": false
|
|
},
|
|
"auto_transition_rules": {
|
|
"type": "json",
|
|
"nullable": true
|
|
},
|
|
"workflow_type": {
|
|
"type": "string",
|
|
"options": {
|
|
"choices": [
|
|
{"text": "BMad", "value": "bmad"},
|
|
{"text": "Task Master", "value": "task_master"},
|
|
{"text": "GitHub", "value": "github"},
|
|
{"text": "Custom", "value": "custom"}
|
|
]
|
|
}
|
|
},
|
|
"sort_order": {
|
|
"type": "integer",
|
|
"default": 0
|
|
},
|
|
"active": {
|
|
"type": "boolean",
|
|
"default": true
|
|
},
|
|
"created_at": {
|
|
"type": "timestamp",
|
|
"auto_create": true
|
|
},
|
|
"updated_at": {
|
|
"type": "timestamp",
|
|
"auto_update": true
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: Create Tasks Collection**
|
|
```sql
|
|
{
|
|
"collection": "tasks",
|
|
"schema": {
|
|
"id": {
|
|
"type": "uuid",
|
|
"default": "$GENERATE_UUID",
|
|
"primary_key": true
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"required": true,
|
|
"max_length": 255
|
|
},
|
|
"description": {
|
|
"type": "text",
|
|
"nullable": true
|
|
},
|
|
"status": {
|
|
"type": "uuid",
|
|
"foreign_key": "task_statuses.id",
|
|
"required": true
|
|
},
|
|
"priority": {
|
|
"type": "string",
|
|
"default": "medium",
|
|
"options": {
|
|
"choices": [
|
|
{"text": "Lowest", "value": "lowest"},
|
|
{"text": "Low", "value": "low"},
|
|
{"text": "Medium", "value": "medium"},
|
|
{"text": "High", "value": "high"},
|
|
{"text": "Highest", "value": "highest"}
|
|
]
|
|
}
|
|
},
|
|
"task_type": {
|
|
"type": "string",
|
|
"default": "feature",
|
|
"options": {
|
|
"choices": [
|
|
{"text": "Feature", "value": "feature"},
|
|
{"text": "Bug", "value": "bug"},
|
|
{"text": "Enhancement", "value": "enhancement"},
|
|
{"text": "Research", "value": "research"},
|
|
{"text": "Maintenance", "value": "maintenance"}
|
|
]
|
|
}
|
|
},
|
|
"complexity": {
|
|
"type": "string",
|
|
"options": {
|
|
"choices": [
|
|
{"text": "Trivial", "value": "trivial"},
|
|
{"text": "Minor", "value": "minor"},
|
|
{"text": "Major", "value": "major"},
|
|
{"text": "Critical", "value": "critical"}
|
|
]
|
|
}
|
|
},
|
|
"story_points": {
|
|
"type": "integer",
|
|
"nullable": true,
|
|
"validation": {
|
|
"min": 1,
|
|
"max": 21
|
|
}
|
|
},
|
|
"project": {
|
|
"type": "uuid",
|
|
"foreign_key": "projects.id",
|
|
"required": true
|
|
},
|
|
"parent_task": {
|
|
"type": "uuid",
|
|
"foreign_key": "tasks.id",
|
|
"nullable": true
|
|
},
|
|
"epic": {
|
|
"type": "uuid",
|
|
"foreign_key": "tasks.id",
|
|
"nullable": true
|
|
},
|
|
"created_by": {
|
|
"type": "uuid",
|
|
"foreign_key": "directus_users.id"
|
|
},
|
|
"assigned_to": {
|
|
"type": "uuid",
|
|
"foreign_key": "directus_users.id",
|
|
"nullable": true
|
|
},
|
|
"ai_agent_assigned": {
|
|
"type": "string",
|
|
"nullable": true
|
|
},
|
|
"reviewer": {
|
|
"type": "uuid",
|
|
"foreign_key": "directus_users.id",
|
|
"nullable": true
|
|
},
|
|
"estimated_hours": {
|
|
"type": "decimal",
|
|
"precision": 5,
|
|
"scale": 2,
|
|
"nullable": true
|
|
},
|
|
"actual_hours": {
|
|
"type": "decimal",
|
|
"precision": 5,
|
|
"scale": 2,
|
|
"computed": "SUM(task_time_entries.hours)"
|
|
},
|
|
"progress_percentage": {
|
|
"type": "integer",
|
|
"default": 0,
|
|
"validation": {
|
|
"min": 0,
|
|
"max": 100
|
|
}
|
|
},
|
|
"start_date": {
|
|
"type": "date",
|
|
"nullable": true
|
|
},
|
|
"due_date": {
|
|
"type": "date",
|
|
"nullable": true
|
|
},
|
|
"completed_at": {
|
|
"type": "timestamp",
|
|
"nullable": true
|
|
},
|
|
"task_master_id": {
|
|
"type": "string",
|
|
"nullable": true,
|
|
"unique": true
|
|
},
|
|
"github_issue_url": {
|
|
"type": "string",
|
|
"nullable": true
|
|
},
|
|
"bmad_story_id": {
|
|
"type": "string",
|
|
"nullable": true
|
|
},
|
|
"external_refs": {
|
|
"type": "json",
|
|
"nullable": true
|
|
},
|
|
"ai_generated": {
|
|
"type": "boolean",
|
|
"default": false
|
|
},
|
|
"ai_context": {
|
|
"type": "json",
|
|
"nullable": true
|
|
},
|
|
"auto_status_updates": {
|
|
"type": "boolean",
|
|
"default": false
|
|
},
|
|
"acceptance_criteria": {
|
|
"type": "text",
|
|
"nullable": true
|
|
},
|
|
"definition_of_done": {
|
|
"type": "text",
|
|
"nullable": true
|
|
},
|
|
"notes": {
|
|
"type": "text",
|
|
"nullable": true
|
|
},
|
|
"created_at": {
|
|
"type": "timestamp",
|
|
"auto_create": true
|
|
},
|
|
"updated_at": {
|
|
"type": "timestamp",
|
|
"auto_update": true
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Database Optimization Setup
|
|
|
|
**Step 4: Create Strategic Indexes**
|
|
```sql
|
|
-- Composite indexes for common query patterns
|
|
CREATE INDEX idx_tasks_project_status ON tasks(project, status);
|
|
CREATE INDEX idx_tasks_assigned_status ON tasks(assigned_to, status) WHERE assigned_to IS NOT NULL;
|
|
CREATE INDEX idx_tasks_due_date ON tasks(due_date) WHERE due_date IS NOT NULL;
|
|
|
|
-- Partial indexes for active data
|
|
CREATE INDEX idx_active_tasks ON tasks(project, status)
|
|
WHERE status NOT IN (
|
|
SELECT id FROM task_statuses WHERE is_final = true
|
|
);
|
|
|
|
-- Full-text search optimization
|
|
CREATE INDEX idx_tasks_search ON tasks
|
|
USING gin(to_tsvector('english', title || ' ' || COALESCE(description, '')));
|
|
|
|
-- Project hierarchy optimization
|
|
CREATE INDEX idx_projects_parent ON projects(parent_project) WHERE parent_project IS NOT NULL;
|
|
CREATE INDEX idx_tasks_parent ON tasks(parent_task) WHERE parent_task IS NOT NULL;
|
|
```
|
|
|
|
### 1.2 MCP Tools Implementation
|
|
|
|
#### MCP Server Extension Setup
|
|
|
|
**Step 1: Project Structure Setup**
|
|
```bash
|
|
# Create MCP extension directory
|
|
mkdir -p tools/directus-task-management-mcp
|
|
cd tools/directus-task-management-mcp
|
|
|
|
# Initialize package.json
|
|
npm init -y
|
|
npm install @modelcontextprotocol/sdk-typescript
|
|
npm install @types/node typescript --save-dev
|
|
|
|
# Create source structure
|
|
mkdir -p src/{tools,types,utils}
|
|
```
|
|
|
|
**Step 2: Core MCP Tools Implementation**
|
|
|
|
**`src/tools/task-crud.ts`**
|
|
```typescript
|
|
import { Tool } from "@modelcontextprotocol/sdk-typescript";
|
|
import { DirectusClient } from "../utils/directus-client";
|
|
import { Task, TaskCreateData, TaskUpdateData, TaskFilters } from "../types/task-types";
|
|
|
|
export const createTaskTool: Tool = {
|
|
name: "mcp__directus__create_task",
|
|
description: "Create a new task in Directus with full metadata support",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
data: {
|
|
type: "object",
|
|
properties: {
|
|
title: { type: "string" },
|
|
description: { type: "string" },
|
|
project: { type: "string", format: "uuid" },
|
|
priority: { type: "string", enum: ["lowest", "low", "medium", "high", "highest"] },
|
|
task_type: { type: "string", enum: ["feature", "bug", "enhancement", "research", "maintenance"] },
|
|
complexity: { type: "string", enum: ["trivial", "minor", "major", "critical"] },
|
|
assigned_to: { type: "string", format: "uuid" },
|
|
estimated_hours: { type: "number" },
|
|
due_date: { type: "string", format: "date" },
|
|
acceptance_criteria: { type: "string" },
|
|
definition_of_done: { type: "string" },
|
|
tags: { type: "array", items: { type: "string" } }
|
|
},
|
|
required: ["title", "project"]
|
|
}
|
|
},
|
|
required: ["data"]
|
|
}
|
|
};
|
|
|
|
export async function handleCreateTask(args: any): Promise<Task> {
|
|
const client = new DirectusClient();
|
|
|
|
try {
|
|
// Validate project exists
|
|
await client.validateProject(args.data.project);
|
|
|
|
// Get default status for new tasks
|
|
const defaultStatus = await client.getDefaultTaskStatus();
|
|
|
|
// Prepare task data with defaults
|
|
const taskData: TaskCreateData = {
|
|
...args.data,
|
|
status: defaultStatus.id,
|
|
progress_percentage: 0,
|
|
ai_generated: args.data.ai_context ? true : false,
|
|
created_at: new Date().toISOString()
|
|
};
|
|
|
|
// Create task
|
|
const task = await client.createItem('tasks', taskData);
|
|
|
|
// Handle tags if provided
|
|
if (args.data.tags?.length > 0) {
|
|
await client.manageTags(task.id, args.data.tags);
|
|
}
|
|
|
|
return task;
|
|
|
|
} catch (error) {
|
|
throw new Error(`Failed to create task: ${error.message}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
**`src/tools/task-queries.ts`**
|
|
```typescript
|
|
export const getTaskTool: Tool = {
|
|
name: "mcp__directus__get_task",
|
|
description: "Get detailed task information with optional relations",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "string", format: "uuid" },
|
|
include_relations: { type: "boolean", default: false }
|
|
},
|
|
required: ["id"]
|
|
}
|
|
};
|
|
|
|
export async function handleGetTask(args: any): Promise<Task> {
|
|
const client = new DirectusClient();
|
|
|
|
const fields = args.include_relations ? [
|
|
'*',
|
|
'status.*',
|
|
'project.*',
|
|
'assigned_to.*',
|
|
'parent_task.*',
|
|
'dependencies.dependency_task.*',
|
|
'time_entries.*',
|
|
'ai_contexts.*'
|
|
] : ['*', 'status.*', 'project.name', 'assigned_to.first_name', 'assigned_to.last_name'];
|
|
|
|
try {
|
|
const task = await client.readItem('tasks', args.id, { fields });
|
|
|
|
if (!task) {
|
|
throw new Error(`Task with ID ${args.id} not found`);
|
|
}
|
|
|
|
return task;
|
|
|
|
} catch (error) {
|
|
throw new Error(`Failed to get task: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
export const listTasksTool: Tool = {
|
|
name: "mcp__directus__list_tasks",
|
|
description: "List tasks with filtering, sorting, and pagination",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
filters: {
|
|
type: "object",
|
|
properties: {
|
|
project: { type: "string", format: "uuid" },
|
|
status: { type: "string" },
|
|
assigned_to: { type: "string", format: "uuid" },
|
|
priority: { type: "string" },
|
|
task_type: { type: "string" },
|
|
search: { type: "string" },
|
|
due_before: { type: "string", format: "date" },
|
|
created_after: { type: "string", format: "date-time" }
|
|
}
|
|
},
|
|
pagination: {
|
|
type: "object",
|
|
properties: {
|
|
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
|
|
offset: { type: "integer", minimum: 0, default: 0 },
|
|
page: { type: "integer", minimum: 1 }
|
|
}
|
|
},
|
|
sort: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
default: ["-created_at"]
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
export async function handleListTasks(args: any): Promise<{ data: Task[], meta: any }> {
|
|
const client = new DirectusClient();
|
|
|
|
const query: any = {
|
|
fields: ['*', 'status.*', 'project.name', 'assigned_to.first_name', 'assigned_to.last_name'],
|
|
limit: args.pagination?.limit || 25,
|
|
sort: args.sort || ['-created_at']
|
|
};
|
|
|
|
// Handle pagination
|
|
if (args.pagination?.offset) {
|
|
query.offset = args.pagination.offset;
|
|
} else if (args.pagination?.page) {
|
|
query.offset = (args.pagination.page - 1) * query.limit;
|
|
}
|
|
|
|
// Build filter conditions
|
|
const filter: any = {};
|
|
|
|
if (args.filters) {
|
|
if (args.filters.project) filter.project = { _eq: args.filters.project };
|
|
if (args.filters.status) filter.status = { _eq: args.filters.status };
|
|
if (args.filters.assigned_to) filter.assigned_to = { _eq: args.filters.assigned_to };
|
|
if (args.filters.priority) filter.priority = { _eq: args.filters.priority };
|
|
if (args.filters.task_type) filter.task_type = { _eq: args.filters.task_type };
|
|
if (args.filters.due_before) filter.due_date = { _lte: args.filters.due_before };
|
|
if (args.filters.created_after) filter.created_at = { _gte: args.filters.created_after };
|
|
|
|
// Full-text search
|
|
if (args.filters.search) {
|
|
filter._or = [
|
|
{ title: { _icontains: args.filters.search } },
|
|
{ description: { _icontains: args.filters.search } }
|
|
];
|
|
}
|
|
}
|
|
|
|
if (Object.keys(filter).length > 0) {
|
|
query.filter = filter;
|
|
}
|
|
|
|
try {
|
|
const result = await client.readItems('tasks', query);
|
|
return result;
|
|
|
|
} catch (error) {
|
|
throw new Error(`Failed to list tasks: ${error.message}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 3: AI Integration Tools**
|
|
|
|
**`src/tools/ai-integration.ts`**
|
|
```typescript
|
|
export const createAITaskTool: Tool = {
|
|
name: "mcp__directus__create_ai_task",
|
|
description: "Create a task using AI prompt processing with context awareness",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
prompt: { type: "string", minLength: 10 },
|
|
context: {
|
|
type: "object",
|
|
properties: {
|
|
project_id: { type: "string", format: "uuid" },
|
|
current_tasks: { type: "array", items: { type: "string" } },
|
|
agent_type: { type: "string" },
|
|
workflow_stage: { type: "string" }
|
|
}
|
|
}
|
|
},
|
|
required: ["prompt"]
|
|
}
|
|
};
|
|
|
|
export async function handleCreateAITask(args: any): Promise<Task> {
|
|
const client = new DirectusClient();
|
|
const aiService = new AITaskProcessor();
|
|
|
|
try {
|
|
// Get context information
|
|
let projectContext = null;
|
|
if (args.context?.project_id) {
|
|
projectContext = await client.readItem('projects', args.context.project_id);
|
|
}
|
|
|
|
// Process prompt with AI
|
|
const taskData = await aiService.processTaskPrompt(args.prompt, {
|
|
project: projectContext,
|
|
existingTasks: args.context?.current_tasks || [],
|
|
agentType: args.context?.agent_type,
|
|
workflowStage: args.context?.workflow_stage
|
|
});
|
|
|
|
// Enhance with AI context
|
|
taskData.ai_generated = true;
|
|
taskData.ai_context = {
|
|
original_prompt: args.prompt,
|
|
processing_timestamp: new Date().toISOString(),
|
|
agent_type: args.context?.agent_type,
|
|
confidence_score: taskData.confidence_score || 0.8
|
|
};
|
|
|
|
// Create task using standard creation flow
|
|
return await handleCreateTask({ data: taskData });
|
|
|
|
} catch (error) {
|
|
throw new Error(`Failed to create AI task: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
export const updateTaskAIContextTool: Tool = {
|
|
name: "mcp__directus__update_task_ai_context",
|
|
description: "Update task AI context with agent activity and results",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
task_id: { type: "string", format: "uuid" },
|
|
context_data: {
|
|
type: "object",
|
|
properties: {
|
|
agent_type: { type: "string" },
|
|
operation: { type: "string" },
|
|
result: { type: "string" },
|
|
files_modified: { type: "array", items: { type: "string" } },
|
|
time_spent: { type: "number" },
|
|
success: { type: "boolean" },
|
|
next_steps: { type: "string" }
|
|
},
|
|
required: ["agent_type", "operation", "success"]
|
|
}
|
|
},
|
|
required: ["task_id", "context_data"]
|
|
}
|
|
};
|
|
|
|
export async function handleUpdateTaskAIContext(args: any): Promise<void> {
|
|
const client = new DirectusClient();
|
|
|
|
try {
|
|
// Create AI context entry
|
|
await client.createItem('task_ai_contexts', {
|
|
task: args.task_id,
|
|
ai_agent_type: args.context_data.agent_type,
|
|
context_type: 'feedback',
|
|
context_data: args.context_data,
|
|
execution_timestamp: new Date().toISOString(),
|
|
success: args.context_data.success
|
|
});
|
|
|
|
// Update task progress if specified
|
|
if (args.context_data.progress_update) {
|
|
await client.updateItem('tasks', args.task_id, {
|
|
progress_percentage: args.context_data.progress_update,
|
|
updated_at: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
throw new Error(`Failed to update task AI context: ${error.message}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 1.3 Frontend Interface Setup
|
|
|
|
#### Directus Admin Interface Customization
|
|
|
|
**Step 1: Collection Interface Configuration**
|
|
|
|
**`collections/tasks.yaml`** (via API or admin interface)
|
|
```yaml
|
|
collection: tasks
|
|
meta:
|
|
icon: check_box
|
|
note: "Task management with full project integration"
|
|
display_template: "{{title}} - {{project.name}}"
|
|
|
|
fields:
|
|
title:
|
|
interface: input
|
|
options:
|
|
placeholder: "Enter task title..."
|
|
display_options:
|
|
tabular: true
|
|
required: true
|
|
|
|
description:
|
|
interface: input-rich-text-html
|
|
options:
|
|
toolbar: ['bold', 'italic', 'underline', 'link', 'code']
|
|
|
|
status:
|
|
interface: select-dropdown-m2o
|
|
options:
|
|
template: "{{name}}"
|
|
display: labels
|
|
display_options:
|
|
choices:
|
|
- background: "{{color}}"
|
|
foreground: "#ffffff"
|
|
value: "{{id}}"
|
|
text: "{{name}}"
|
|
|
|
priority:
|
|
interface: select-dropdown
|
|
display: labels
|
|
display_options:
|
|
choices:
|
|
- { text: "Lowest", value: "lowest", background: "#64748b" }
|
|
- { text: "Low", value: "low", background: "#10b981" }
|
|
- { text: "Medium", value: "medium", background: "#f59e0b" }
|
|
- { text: "High", value: "high", background: "#ef4444" }
|
|
- { text: "Highest", value: "highest", background: "#dc2626" }
|
|
|
|
project:
|
|
interface: select-dropdown-m2o
|
|
options:
|
|
template: "{{name}}"
|
|
required: true
|
|
|
|
assigned_to:
|
|
interface: select-dropdown-m2o
|
|
options:
|
|
template: "{{first_name}} {{last_name}}"
|
|
|
|
progress_percentage:
|
|
interface: slider
|
|
options:
|
|
min: 0
|
|
max: 100
|
|
step: 5
|
|
display: progress-bar
|
|
|
|
estimated_hours:
|
|
interface: input
|
|
options:
|
|
type: number
|
|
step: 0.25
|
|
min: 0
|
|
|
|
due_date:
|
|
interface: datetime
|
|
options:
|
|
type: date
|
|
```
|
|
|
|
**Step 2: Custom Layout Configuration**
|
|
|
|
**`layouts/tasks-detail.vue`**
|
|
```vue
|
|
<template>
|
|
<div class="task-detail-layout">
|
|
<!-- Header Section -->
|
|
<div class="task-header">
|
|
<v-card class="task-title-card">
|
|
<v-card-title>
|
|
<div class="task-title-section">
|
|
<v-field v-model="values.title" :field="titleField" />
|
|
<div class="task-meta">
|
|
<v-field v-model="values.status" :field="statusField" />
|
|
<v-field v-model="values.priority" :field="priorityField" />
|
|
</div>
|
|
</div>
|
|
</v-card-title>
|
|
</v-card>
|
|
</div>
|
|
|
|
<!-- Main Content Grid -->
|
|
<div class="task-content-grid">
|
|
<!-- Left Column: Task Details -->
|
|
<div class="task-details">
|
|
<v-card>
|
|
<v-card-title>Task Details</v-card-title>
|
|
<v-card-text>
|
|
<v-field v-model="values.description" :field="descriptionField" />
|
|
<v-field v-model="values.acceptance_criteria" :field="acceptanceCriteriaField" />
|
|
<v-field v-model="values.definition_of_done" :field="definitionOfDoneField" />
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
|
|
<!-- Right Column: Metadata -->
|
|
<div class="task-metadata">
|
|
<v-card class="mb-4">
|
|
<v-card-title>Assignment</v-card-title>
|
|
<v-card-text>
|
|
<v-field v-model="values.project" :field="projectField" />
|
|
<v-field v-model="values.assigned_to" :field="assignedToField" />
|
|
<v-field v-model="values.ai_agent_assigned" :field="aiAgentField" />
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<v-card class="mb-4">
|
|
<v-card-title>Planning</v-card-title>
|
|
<v-card-text>
|
|
<v-field v-model="values.estimated_hours" :field="estimatedHoursField" />
|
|
<v-field v-model="values.story_points" :field="storyPointsField" />
|
|
<v-field v-model="values.complexity" :field="complexityField" />
|
|
<v-field v-model="values.due_date" :field="dueDateField" />
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<v-card>
|
|
<v-card-title>Progress</v-card-title>
|
|
<v-card-text>
|
|
<v-field v-model="values.progress_percentage" :field="progressField" />
|
|
<div class="progress-stats">
|
|
<div>Estimated: {{ values.estimated_hours }}h</div>
|
|
<div>Actual: {{ values.actual_hours }}h</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: 'TaskDetailLayout',
|
|
props: {
|
|
collection: String,
|
|
primaryKey: [String, Number],
|
|
item: Object,
|
|
fields: Array
|
|
},
|
|
|
|
computed: {
|
|
values() {
|
|
return this.item || {};
|
|
},
|
|
|
|
titleField() {
|
|
return this.fields.find(f => f.field === 'title');
|
|
},
|
|
|
|
statusField() {
|
|
return this.fields.find(f => f.field === 'status');
|
|
},
|
|
|
|
// ... other field getters
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.task-detail-layout {
|
|
padding: 20px;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.task-header {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.task-content-grid {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 24px;
|
|
}
|
|
|
|
.task-title-section {
|
|
width: 100%;
|
|
}
|
|
|
|
.task-meta {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.progress-stats {
|
|
margin-top: 8px;
|
|
font-size: 14px;
|
|
color: var(--foreground-subdued);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.task-content-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## Phase 2: Integration Implementation Guide
|
|
|
|
### 2.1 Task Master Sync Integration
|
|
|
|
#### Bidirectional Sync Service
|
|
|
|
**`src/services/task-master-sync.ts`**
|
|
```typescript
|
|
import { EventEmitter } from 'events';
|
|
import { spawn } from 'child_process';
|
|
import { DirectusClient } from '../utils/directus-client';
|
|
|
|
export class TaskMasterSyncService extends EventEmitter {
|
|
private directus: DirectusClient;
|
|
private syncInterval: NodeJS.Timer | null = null;
|
|
private isRunning: boolean = false;
|
|
|
|
constructor() {
|
|
super();
|
|
this.directus = new DirectusClient();
|
|
}
|
|
|
|
async startSync(intervalMinutes: number = 5): Promise<void> {
|
|
if (this.isRunning) return;
|
|
|
|
this.isRunning = true;
|
|
console.log('Starting Task Master sync service...');
|
|
|
|
// Initial sync
|
|
await this.performSync();
|
|
|
|
// Set up recurring sync
|
|
this.syncInterval = setInterval(async () => {
|
|
try {
|
|
await this.performSync();
|
|
} catch (error) {
|
|
console.error('Sync error:', error);
|
|
this.emit('sync-error', error);
|
|
}
|
|
}, intervalMinutes * 60 * 1000);
|
|
|
|
this.emit('sync-started');
|
|
}
|
|
|
|
async stopSync(): Promise<void> {
|
|
if (this.syncInterval) {
|
|
clearInterval(this.syncInterval);
|
|
this.syncInterval = null;
|
|
}
|
|
this.isRunning = false;
|
|
this.emit('sync-stopped');
|
|
}
|
|
|
|
async performSync(): Promise<void> {
|
|
console.log('Starting bidirectional sync...');
|
|
const syncResult = {
|
|
timestamp: new Date().toISOString(),
|
|
taskMasterToDirectus: 0,
|
|
directusToTaskMaster: 0,
|
|
conflicts: 0,
|
|
errors: []
|
|
};
|
|
|
|
try {
|
|
// Phase 1: Task Master → Directus
|
|
const tmUpdates = await this.syncFromTaskMaster();
|
|
syncResult.taskMasterToDirectus = tmUpdates.length;
|
|
|
|
// Phase 2: Directus → Task Master
|
|
const directusUpdates = await this.syncToTaskMaster();
|
|
syncResult.directusToTaskMaster = directusUpdates.length;
|
|
|
|
// Phase 3: Conflict Resolution
|
|
const conflicts = await this.resolveConflicts();
|
|
syncResult.conflicts = conflicts.length;
|
|
|
|
console.log('Sync completed:', syncResult);
|
|
this.emit('sync-completed', syncResult);
|
|
|
|
} catch (error) {
|
|
syncResult.errors.push(error.message);
|
|
this.emit('sync-error', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async syncFromTaskMaster(): Promise<any[]> {
|
|
// Export from Task Master
|
|
const tmData = await this.executeTaskMasterCommand(['export', '--format=json', '--since-sync']);
|
|
|
|
if (!tmData.tasks || tmData.tasks.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const updates: any[] = [];
|
|
|
|
for (const tmTask of tmData.tasks) {
|
|
try {
|
|
// Check if task exists in Directus
|
|
const existingTask = await this.directus.readItems('tasks', {
|
|
filter: { task_master_id: { _eq: tmTask.id } },
|
|
limit: 1
|
|
});
|
|
|
|
if (existingTask.data.length > 0) {
|
|
// Update existing task
|
|
const directusTask = existingTask.data[0];
|
|
const updateData = this.mapTaskMasterToDirectus(tmTask);
|
|
|
|
// Check for conflicts
|
|
if (this.hasConflict(directusTask, updateData, tmTask)) {
|
|
await this.recordConflict(directusTask.id, tmTask, updateData);
|
|
continue;
|
|
}
|
|
|
|
await this.directus.updateItem('tasks', directusTask.id, updateData);
|
|
updates.push({ type: 'update', taskMasterId: tmTask.id, directusId: directusTask.id });
|
|
|
|
} else {
|
|
// Create new task
|
|
const newTaskData = this.mapTaskMasterToDirectus(tmTask);
|
|
newTaskData.task_master_id = tmTask.id;
|
|
|
|
const newTask = await this.directus.createItem('tasks', newTaskData);
|
|
updates.push({ type: 'create', taskMasterId: tmTask.id, directusId: newTask.id });
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Error syncing task ${tmTask.id}:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
private async syncToTaskMaster(): Promise<any[]> {
|
|
// Get tasks modified since last sync
|
|
const lastSync = await this.getLastSyncTimestamp();
|
|
const modifiedTasks = await this.directus.readItems('tasks', {
|
|
filter: {
|
|
_and: [
|
|
{ updated_at: { _gt: lastSync } },
|
|
{ task_master_id: { _nnull: true } }
|
|
]
|
|
}
|
|
});
|
|
|
|
const updates: any[] = [];
|
|
|
|
for (const directusTask of modifiedTasks.data) {
|
|
try {
|
|
const tmData = this.mapDirectusToTaskMaster(directusTask);
|
|
|
|
// Update in Task Master
|
|
await this.executeTaskMasterCommand([
|
|
'update-task',
|
|
'--id', directusTask.task_master_id,
|
|
'--data', JSON.stringify(tmData)
|
|
]);
|
|
|
|
updates.push({
|
|
type: 'update',
|
|
directusId: directusTask.id,
|
|
taskMasterId: directusTask.task_master_id
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(`Error syncing to Task Master ${directusTask.task_master_id}:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
private mapTaskMasterToDirectus(tmTask: any): any {
|
|
return {
|
|
title: tmTask.title,
|
|
description: tmTask.description,
|
|
// Map Task Master status to Directus status
|
|
status: this.mapStatus(tmTask.status, 'tm_to_directus'),
|
|
priority: this.mapPriority(tmTask.priority, 'tm_to_directus'),
|
|
estimated_hours: tmTask.estimated_hours,
|
|
progress_percentage: tmTask.progress || 0,
|
|
due_date: tmTask.due_date,
|
|
acceptance_criteria: tmTask.acceptance_criteria,
|
|
definition_of_done: tmTask.definition_of_done,
|
|
updated_at: new Date().toISOString(),
|
|
// Preserve sync metadata
|
|
external_refs: {
|
|
...tmTask.external_refs,
|
|
last_tm_sync: new Date().toISOString()
|
|
}
|
|
};
|
|
}
|
|
|
|
private mapDirectusToTaskMaster(directusTask: any): any {
|
|
return {
|
|
title: directusTask.title,
|
|
description: directusTask.description,
|
|
status: this.mapStatus(directusTask.status.slug, 'directus_to_tm'),
|
|
priority: this.mapPriority(directusTask.priority, 'directus_to_tm'),
|
|
estimated_hours: directusTask.estimated_hours,
|
|
progress: directusTask.progress_percentage,
|
|
due_date: directusTask.due_date,
|
|
acceptance_criteria: directusTask.acceptance_criteria,
|
|
definition_of_done: directusTask.definition_of_done
|
|
};
|
|
}
|
|
|
|
private async executeTaskMasterCommand(args: string[]): Promise<any> {
|
|
return new Promise((resolve, reject) => {
|
|
const process = spawn('task-master', args, { stdio: 'pipe' });
|
|
let output = '';
|
|
|
|
process.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
process.on('close', (code) => {
|
|
if (code === 0) {
|
|
try {
|
|
resolve(JSON.parse(output));
|
|
} catch (error) {
|
|
resolve({ output });
|
|
}
|
|
} else {
|
|
reject(new Error(`Task Master command failed with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private hasConflict(directusTask: any, updateData: any, tmTask: any): boolean {
|
|
// Simple timestamp-based conflict detection
|
|
const directusModified = new Date(directusTask.updated_at);
|
|
const tmModified = new Date(tmTask.updated_at);
|
|
const lastSync = new Date(directusTask.external_refs?.last_tm_sync || 0);
|
|
|
|
// Conflict if both have been modified since last sync
|
|
return directusModified > lastSync && tmModified > lastSync;
|
|
}
|
|
|
|
private async recordConflict(directusTaskId: string, tmTask: any, updateData: any): Promise<void> {
|
|
await this.directus.createItem('task_sync_conflicts', {
|
|
task: directusTaskId,
|
|
conflict_type: 'bidirectional_update',
|
|
directus_data: updateData,
|
|
task_master_data: tmTask,
|
|
detected_at: new Date().toISOString(),
|
|
resolution_status: 'pending'
|
|
});
|
|
}
|
|
|
|
private mapStatus(status: string, direction: 'tm_to_directus' | 'directus_to_tm'): string {
|
|
const statusMap = {
|
|
tm_to_directus: {
|
|
'pending': 'todo',
|
|
'in-progress': 'in_progress',
|
|
'done': 'completed',
|
|
'blocked': 'blocked'
|
|
},
|
|
directus_to_tm: {
|
|
'todo': 'pending',
|
|
'in_progress': 'in-progress',
|
|
'completed': 'done',
|
|
'blocked': 'blocked'
|
|
}
|
|
};
|
|
|
|
return statusMap[direction][status] || status;
|
|
}
|
|
|
|
private async getLastSyncTimestamp(): Promise<string> {
|
|
const syncRecord = await this.directus.readItems('sync_metadata', {
|
|
filter: { sync_type: { _eq: 'task_master' } },
|
|
sort: ['-created_at'],
|
|
limit: 1
|
|
});
|
|
|
|
return syncRecord.data[0]?.last_sync_time || new Date(0).toISOString();
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.2 BMad Integration Implementation
|
|
|
|
#### BMad Workflow Templates
|
|
|
|
**`src/services/bmad-integration.ts`**
|
|
```typescript
|
|
export class BMadIntegrationService {
|
|
private directus: DirectusClient;
|
|
|
|
constructor() {
|
|
this.directus = new DirectusClient();
|
|
}
|
|
|
|
async createBMadEpic(epicData: BMadEpicData): Promise<Task> {
|
|
// Create epic task
|
|
const epic = await this.directus.createItem('tasks', {
|
|
title: epicData.title,
|
|
description: epicData.description,
|
|
task_type: 'feature',
|
|
priority: 'high',
|
|
complexity: 'major',
|
|
project: epicData.project_id,
|
|
bmad_story_id: epicData.bmad_id,
|
|
ai_generated: true,
|
|
ai_context: {
|
|
bmad_workflow: 'epic_creation',
|
|
planning_phase: epicData.planning_phase,
|
|
methodology: 'bmad'
|
|
}
|
|
});
|
|
|
|
// Create workflow tracking entries
|
|
await this.initializeBMadWorkflow(epic.id, epicData.workflow_type);
|
|
|
|
return epic;
|
|
}
|
|
|
|
async generateUserStories(epicId: string, storyCount: number = 3): Promise<Task[]> {
|
|
const epic = await this.directus.readItem('tasks', epicId);
|
|
if (!epic) throw new Error('Epic not found');
|
|
|
|
const stories: Task[] = [];
|
|
|
|
for (let i = 1; i <= storyCount; i++) {
|
|
const story = await this.directus.createItem('tasks', {
|
|
title: `${epic.title} - User Story ${i}`,
|
|
description: `User story derived from epic: ${epic.title}`,
|
|
task_type: 'feature',
|
|
priority: epic.priority,
|
|
complexity: 'minor',
|
|
project: epic.project,
|
|
parent_task: epic.id,
|
|
epic: epic.id,
|
|
bmad_story_id: `${epic.bmad_story_id}-story-${i}`,
|
|
ai_generated: true,
|
|
ai_context: {
|
|
bmad_workflow: 'story_generation',
|
|
parent_epic: epic.id,
|
|
story_number: i
|
|
}
|
|
});
|
|
|
|
stories.push(story);
|
|
}
|
|
|
|
return stories;
|
|
}
|
|
|
|
private async initializeBMadWorkflow(taskId: string, workflowType: string): Promise<void> {
|
|
const workflows = this.getBMadWorkflowSteps(workflowType);
|
|
|
|
for (const [index, step] of workflows.entries()) {
|
|
await this.directus.createItem('task_bmad_workflows', {
|
|
task: taskId,
|
|
bmad_phase: step.phase,
|
|
bmad_agent: step.agent,
|
|
workflow_step: step.step,
|
|
step_status: 'pending',
|
|
execution_order: index + 1,
|
|
step_data: step.default_data || {}
|
|
});
|
|
}
|
|
}
|
|
|
|
private getBMadWorkflowSteps(workflowType: string): BMadWorkflowStep[] {
|
|
const workflows = {
|
|
greenfield: [
|
|
{
|
|
phase: 'analysis',
|
|
agent: 'pm',
|
|
step: 'requirements_gathering',
|
|
default_data: { templates: ['prd', 'user_stories'] }
|
|
},
|
|
{
|
|
phase: 'analysis',
|
|
agent: 'architect',
|
|
step: 'system_design',
|
|
default_data: { deliverables: ['architecture_diagram', 'tech_stack'] }
|
|
},
|
|
{
|
|
phase: 'planning',
|
|
agent: 'analyst',
|
|
step: 'research_analysis',
|
|
default_data: { research_areas: ['market', 'technical', 'competitive'] }
|
|
},
|
|
{
|
|
phase: 'development',
|
|
agent: 'dev',
|
|
step: 'implementation',
|
|
default_data: { coding_standards: true, tdd_required: true }
|
|
},
|
|
{
|
|
phase: 'review',
|
|
agent: 'qa',
|
|
step: 'quality_assurance',
|
|
default_data: { test_coverage_min: 80, security_review: true }
|
|
}
|
|
],
|
|
|
|
brownfield: [
|
|
{
|
|
phase: 'analysis',
|
|
agent: 'architect',
|
|
step: 'system_assessment',
|
|
default_data: { legacy_analysis: true, impact_assessment: true }
|
|
},
|
|
{
|
|
phase: 'planning',
|
|
agent: 'pm',
|
|
step: 'enhancement_planning',
|
|
default_data: { risk_assessment: true, rollback_plan: true }
|
|
},
|
|
{
|
|
phase: 'development',
|
|
agent: 'dev',
|
|
step: 'incremental_implementation',
|
|
default_data: { backwards_compatibility: true, feature_flags: true }
|
|
},
|
|
{
|
|
phase: 'review',
|
|
agent: 'qa',
|
|
step: 'regression_testing',
|
|
default_data: { full_regression: true, performance_testing: true }
|
|
}
|
|
]
|
|
};
|
|
|
|
return workflows[workflowType] || workflows.greenfield;
|
|
}
|
|
|
|
async updateWorkflowStep(taskId: string, stepId: string, stepData: any): Promise<void> {
|
|
await this.directus.updateItem('task_bmad_workflows', stepId, {
|
|
step_status: stepData.status,
|
|
step_data: { ...stepData.data },
|
|
completed_at: stepData.status === 'completed' ? new Date().toISOString() : null
|
|
});
|
|
|
|
// Check if all steps are complete
|
|
if (stepData.status === 'completed') {
|
|
const allSteps = await this.directus.readItems('task_bmad_workflows', {
|
|
filter: { task: { _eq: taskId } }
|
|
});
|
|
|
|
const allComplete = allSteps.data.every(step => step.step_status === 'completed');
|
|
|
|
if (allComplete) {
|
|
await this.directus.updateItem('tasks', taskId, {
|
|
status: await this.getStatusId('completed'),
|
|
progress_percentage: 100,
|
|
completed_at: new Date().toISOString()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
This implementation guide provides comprehensive code examples and setup instructions for the first two phases of the Directus Task Management Suite. The remaining phases would follow similar patterns with additional features like analytics dashboards, advanced AI integration, and performance optimizations.
|
|
|
|
The key to successful implementation is:
|
|
|
|
1. **Start with solid foundations** - proper database design and API patterns
|
|
2. **Build incrementally** - each phase delivers working functionality
|
|
3. **Test thoroughly** - especially integration points with existing systems
|
|
4. **Document extensively** - enable future maintenance and enhancement
|
|
5. **Monitor performance** - ensure scalability as usage grows
|
|
|
|
Each code example includes error handling, validation, and integration patterns that align with the existing project architecture and established development practices. |