@allpepper/task-orchestrator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/src/db/client.ts +34 -0
  4. package/src/db/index.ts +1 -0
  5. package/src/db/migrate.ts +51 -0
  6. package/src/db/migrations/001_initial_schema.sql +160 -0
  7. package/src/domain/index.ts +1 -0
  8. package/src/domain/types.ts +225 -0
  9. package/src/index.ts +7 -0
  10. package/src/repos/base.ts +151 -0
  11. package/src/repos/dependencies.ts +356 -0
  12. package/src/repos/features.ts +507 -0
  13. package/src/repos/index.ts +4 -0
  14. package/src/repos/projects.ts +350 -0
  15. package/src/repos/sections.ts +505 -0
  16. package/src/repos/tags.example.ts +125 -0
  17. package/src/repos/tags.ts +175 -0
  18. package/src/repos/tasks.ts +581 -0
  19. package/src/repos/templates.ts +649 -0
  20. package/src/server.ts +121 -0
  21. package/src/services/index.ts +2 -0
  22. package/src/services/status-validator.ts +100 -0
  23. package/src/services/workflow.ts +104 -0
  24. package/src/tools/apply-template.ts +129 -0
  25. package/src/tools/get-blocked-tasks.ts +63 -0
  26. package/src/tools/get-next-status.ts +183 -0
  27. package/src/tools/get-next-task.ts +75 -0
  28. package/src/tools/get-tag-usage.ts +54 -0
  29. package/src/tools/index.ts +30 -0
  30. package/src/tools/list-tags.ts +56 -0
  31. package/src/tools/manage-container.ts +333 -0
  32. package/src/tools/manage-dependency.ts +198 -0
  33. package/src/tools/manage-sections.ts +388 -0
  34. package/src/tools/manage-template.ts +313 -0
  35. package/src/tools/query-container.ts +296 -0
  36. package/src/tools/query-dependencies.ts +68 -0
  37. package/src/tools/query-sections.ts +70 -0
  38. package/src/tools/query-templates.ts +137 -0
  39. package/src/tools/query-workflow-state.ts +198 -0
  40. package/src/tools/registry.ts +180 -0
  41. package/src/tools/rename-tag.ts +64 -0
  42. package/src/tools/setup-project.ts +189 -0
package/src/server.ts ADDED
@@ -0,0 +1,121 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
4
+ import { runMigrations } from './db/migrate';
5
+ import {
6
+ registerQueryContainerTool,
7
+ registerManageContainerTool,
8
+ registerQuerySectionsTool,
9
+ registerManageSectionsTool,
10
+ registerQueryTemplatesTool,
11
+ registerManageTemplateTool,
12
+ registerApplyTemplateTool,
13
+ registerQueryDependenciesTool,
14
+ registerManageDependencyTool,
15
+ registerListTagsTool,
16
+ registerGetTagUsageTool,
17
+ registerRenameTagTool,
18
+ registerGetNextTaskTool,
19
+ registerGetBlockedTasksTool,
20
+ registerGetNextStatusTool,
21
+ registerQueryWorkflowStateTool,
22
+ registerSetupProjectTool,
23
+ } from './tools';
24
+
25
+ // Initialize database and run migrations
26
+ runMigrations();
27
+
28
+ // Create MCP server
29
+ const server = new McpServer({
30
+ name: 'task-orchestrator',
31
+ version: '2.0.0',
32
+ });
33
+
34
+ // Register all tools
35
+ registerQueryContainerTool(server);
36
+ registerManageContainerTool(server);
37
+ registerQuerySectionsTool(server);
38
+ registerManageSectionsTool(server);
39
+ registerQueryTemplatesTool(server);
40
+ registerManageTemplateTool(server);
41
+ registerApplyTemplateTool(server);
42
+ registerQueryDependenciesTool(server);
43
+ registerManageDependencyTool(server);
44
+ registerListTagsTool(server);
45
+ registerGetTagUsageTool(server);
46
+ registerRenameTagTool(server);
47
+ registerGetNextTaskTool(server);
48
+ registerGetBlockedTasksTool(server);
49
+ registerGetNextStatusTool(server);
50
+ registerQueryWorkflowStateTool(server);
51
+ registerSetupProjectTool(server);
52
+
53
+ // Determine transport mode from CLI args or env
54
+ const useHttp = process.argv.includes('--http') || process.env.TRANSPORT === 'http';
55
+
56
+ if (useHttp) {
57
+ const port = parseInt(process.env.PORT || '3100', 10);
58
+
59
+ // Map of session ID -> transport for stateful sessions
60
+ const sessions = new Map<string, WebStandardStreamableHTTPServerTransport>();
61
+
62
+ Bun.serve({
63
+ port,
64
+ fetch: async (req: Request) => {
65
+ const url = new URL(req.url);
66
+
67
+ if (url.pathname !== '/mcp') {
68
+ return new Response('Not Found', { status: 404 });
69
+ }
70
+
71
+ // Check for existing session
72
+ const sessionId = req.headers.get('mcp-session-id');
73
+ if (sessionId && sessions.has(sessionId)) {
74
+ return sessions.get(sessionId)!.handleRequest(req);
75
+ }
76
+
77
+ // New session — create transport and connect a fresh server instance
78
+ const transport = new WebStandardStreamableHTTPServerTransport({
79
+ sessionIdGenerator: () => crypto.randomUUID(),
80
+ onsessioninitialized: (id) => {
81
+ sessions.set(id, transport);
82
+ },
83
+ onsessionclosed: (id) => {
84
+ sessions.delete(id);
85
+ },
86
+ });
87
+
88
+ const sessionServer = new McpServer({
89
+ name: 'task-orchestrator',
90
+ version: '2.0.0',
91
+ });
92
+
93
+ registerQueryContainerTool(sessionServer);
94
+ registerManageContainerTool(sessionServer);
95
+ registerQuerySectionsTool(sessionServer);
96
+ registerManageSectionsTool(sessionServer);
97
+ registerQueryTemplatesTool(sessionServer);
98
+ registerManageTemplateTool(sessionServer);
99
+ registerApplyTemplateTool(sessionServer);
100
+ registerQueryDependenciesTool(sessionServer);
101
+ registerManageDependencyTool(sessionServer);
102
+ registerListTagsTool(sessionServer);
103
+ registerGetTagUsageTool(sessionServer);
104
+ registerRenameTagTool(sessionServer);
105
+ registerGetNextTaskTool(sessionServer);
106
+ registerGetBlockedTasksTool(sessionServer);
107
+ registerGetNextStatusTool(sessionServer);
108
+ registerQueryWorkflowStateTool(sessionServer);
109
+ registerSetupProjectTool(sessionServer);
110
+
111
+ await sessionServer.connect(transport);
112
+ return transport.handleRequest(req);
113
+ },
114
+ });
115
+
116
+ console.error(`Task Orchestrator MCP listening on http://localhost:${port}/mcp`);
117
+ } else {
118
+ // Default: stdio transport
119
+ const transport = new StdioServerTransport();
120
+ await server.connect(transport);
121
+ }
@@ -0,0 +1,2 @@
1
+ export * from './status-validator';
2
+ export * from './workflow';
@@ -0,0 +1,100 @@
1
+ import { ProjectStatus, FeatureStatus, TaskStatus } from '../domain/types';
2
+
3
+ // Valid statuses per container type
4
+ const PROJECT_STATUSES = Object.values(ProjectStatus);
5
+ const FEATURE_STATUSES = Object.values(FeatureStatus);
6
+ const TASK_STATUSES = Object.values(TaskStatus);
7
+
8
+ /**
9
+ * Status transition maps
10
+ *
11
+ * Note: CANCELLED and DEFERRED are intentionally non-terminal statuses.
12
+ * They allow transitions back to earlier workflow stages (BACKLOG/PENDING for tasks,
13
+ * PLANNING for projects) to support reinstating cancelled or deferred work.
14
+ */
15
+ const PROJECT_TRANSITIONS: Record<string, string[]> = {
16
+ PLANNING: ['IN_DEVELOPMENT', 'ON_HOLD', 'CANCELLED'],
17
+ IN_DEVELOPMENT: ['COMPLETED', 'ON_HOLD', 'CANCELLED'],
18
+ ON_HOLD: ['PLANNING', 'IN_DEVELOPMENT', 'CANCELLED'],
19
+ COMPLETED: ['ARCHIVED'],
20
+ CANCELLED: ['PLANNING'], // Non-terminal: allows reinstating cancelled projects
21
+ ARCHIVED: [],
22
+ };
23
+
24
+ const FEATURE_TRANSITIONS: Record<string, string[]> = {
25
+ DRAFT: ['PLANNING'],
26
+ PLANNING: ['IN_DEVELOPMENT', 'ON_HOLD'],
27
+ IN_DEVELOPMENT: ['TESTING', 'BLOCKED', 'ON_HOLD'],
28
+ TESTING: ['VALIDATING', 'IN_DEVELOPMENT'],
29
+ VALIDATING: ['PENDING_REVIEW', 'IN_DEVELOPMENT'],
30
+ PENDING_REVIEW: ['DEPLOYED', 'IN_DEVELOPMENT'],
31
+ BLOCKED: ['IN_DEVELOPMENT', 'ON_HOLD'],
32
+ ON_HOLD: ['PLANNING', 'IN_DEVELOPMENT'],
33
+ DEPLOYED: ['COMPLETED'],
34
+ COMPLETED: ['ARCHIVED'],
35
+ ARCHIVED: [],
36
+ };
37
+
38
+ const TASK_TRANSITIONS: Record<string, string[]> = {
39
+ BACKLOG: ['PENDING'],
40
+ PENDING: ['IN_PROGRESS', 'BLOCKED', 'ON_HOLD', 'CANCELLED', 'DEFERRED'],
41
+ IN_PROGRESS: ['IN_REVIEW', 'TESTING', 'BLOCKED', 'ON_HOLD', 'COMPLETED'],
42
+ IN_REVIEW: ['CHANGES_REQUESTED', 'COMPLETED'],
43
+ CHANGES_REQUESTED: ['IN_PROGRESS'],
44
+ TESTING: ['READY_FOR_QA', 'IN_PROGRESS'],
45
+ READY_FOR_QA: ['INVESTIGATING', 'DEPLOYED', 'COMPLETED'],
46
+ INVESTIGATING: ['IN_PROGRESS', 'BLOCKED'],
47
+ BLOCKED: ['PENDING', 'IN_PROGRESS'],
48
+ ON_HOLD: ['PENDING', 'IN_PROGRESS'],
49
+ DEPLOYED: ['COMPLETED'],
50
+ COMPLETED: [], // Terminal: no transitions allowed
51
+ CANCELLED: ['BACKLOG', 'PENDING'], // Non-terminal: allows reinstating cancelled tasks
52
+ DEFERRED: ['BACKLOG', 'PENDING'], // Non-terminal: allows resuming deferred tasks
53
+ };
54
+
55
+ // Terminal statuses (no transitions out)
56
+ const TERMINAL_STATUSES: Record<string, string[]> = {
57
+ project: ['ARCHIVED'],
58
+ feature: ['ARCHIVED'],
59
+ task: ['COMPLETED'],
60
+ };
61
+
62
+ export type ContainerType = 'project' | 'feature' | 'task';
63
+
64
+ export function isValidStatus(containerType: ContainerType, status: string): boolean {
65
+ switch (containerType) {
66
+ case 'project': return PROJECT_STATUSES.includes(status as ProjectStatus);
67
+ case 'feature': return FEATURE_STATUSES.includes(status as FeatureStatus);
68
+ case 'task': return TASK_STATUSES.includes(status as TaskStatus);
69
+ }
70
+ }
71
+
72
+ export function getValidStatuses(containerType: ContainerType): string[] {
73
+ switch (containerType) {
74
+ case 'project': return [...PROJECT_STATUSES];
75
+ case 'feature': return [...FEATURE_STATUSES];
76
+ case 'task': return [...TASK_STATUSES];
77
+ }
78
+ }
79
+
80
+ export function getTransitions(containerType: ContainerType): Record<string, string[]> {
81
+ switch (containerType) {
82
+ case 'project': return PROJECT_TRANSITIONS;
83
+ case 'feature': return FEATURE_TRANSITIONS;
84
+ case 'task': return TASK_TRANSITIONS;
85
+ }
86
+ }
87
+
88
+ export function getAllowedTransitions(containerType: ContainerType, currentStatus: string): string[] {
89
+ const transitions = getTransitions(containerType);
90
+ return transitions[currentStatus] || [];
91
+ }
92
+
93
+ export function isValidTransition(containerType: ContainerType, from: string, to: string): boolean {
94
+ const allowed = getAllowedTransitions(containerType, from);
95
+ return allowed.includes(to);
96
+ }
97
+
98
+ export function isTerminalStatus(containerType: ContainerType, status: string): boolean {
99
+ return TERMINAL_STATUSES[containerType]?.includes(status) ?? false;
100
+ }
@@ -0,0 +1,104 @@
1
+ import type { ContainerType } from './status-validator';
2
+ import { getAllowedTransitions, isTerminalStatus, isValidStatus } from './status-validator';
3
+ import { getProject } from '../repos/projects';
4
+ import { getFeature } from '../repos/features';
5
+ import { getTask } from '../repos/tasks';
6
+ import { getDependencies } from '../repos/dependencies';
7
+ import { searchTasks } from '../repos/tasks';
8
+ import type { Result } from '../domain/types';
9
+ import { ok, err } from '../repos/base';
10
+
11
+ export interface WorkflowState {
12
+ containerType: ContainerType;
13
+ id: string;
14
+ currentStatus: string;
15
+ allowedTransitions: string[];
16
+ isTerminal: boolean;
17
+ cascadeEvents?: string[];
18
+ blockingDependencies?: Array<{
19
+ taskId: string;
20
+ taskTitle: string;
21
+ status: string;
22
+ }>;
23
+ }
24
+
25
+ export function getWorkflowState(containerType: ContainerType, id: string): Result<WorkflowState> {
26
+ // 1. Get entity
27
+ let currentStatus: string;
28
+
29
+ switch (containerType) {
30
+ case 'project': {
31
+ const result = getProject(id);
32
+ if (!result.success) return err(result.error, result.code);
33
+ currentStatus = result.data.status;
34
+ break;
35
+ }
36
+ case 'feature': {
37
+ const result = getFeature(id);
38
+ if (!result.success) return err(result.error, result.code);
39
+ currentStatus = result.data.status;
40
+ break;
41
+ }
42
+ case 'task': {
43
+ const result = getTask(id);
44
+ if (!result.success) return err(result.error, result.code);
45
+ currentStatus = result.data.status;
46
+ break;
47
+ }
48
+ }
49
+
50
+ // 2. Get allowed transitions
51
+ const allowedTransitions = getAllowedTransitions(containerType, currentStatus);
52
+ const isTerminal = isTerminalStatus(containerType, currentStatus);
53
+
54
+ // 3. Build state
55
+ const state: WorkflowState = {
56
+ containerType,
57
+ id,
58
+ currentStatus,
59
+ allowedTransitions,
60
+ isTerminal,
61
+ };
62
+
63
+ // 4. For tasks, check blocking dependencies
64
+ if (containerType === 'task') {
65
+ const depsResult = getDependencies(id, 'dependents');
66
+ if (depsResult.success) {
67
+ const blockers = depsResult.data
68
+ .filter(d => d.type === 'BLOCKS')
69
+ .map(d => {
70
+ const taskResult = getTask(d.fromTaskId);
71
+ if (taskResult.success) {
72
+ return {
73
+ taskId: d.fromTaskId,
74
+ taskTitle: taskResult.data.title,
75
+ status: taskResult.data.status,
76
+ };
77
+ }
78
+ return { taskId: d.fromTaskId, taskTitle: 'Unknown', status: 'UNKNOWN' };
79
+ })
80
+ .filter(b => b.status !== 'COMPLETED' && b.status !== 'CANCELLED');
81
+
82
+ if (blockers.length > 0) {
83
+ state.blockingDependencies = blockers;
84
+ }
85
+ }
86
+ }
87
+
88
+ // 5. Detect cascade events for features/projects
89
+ if (containerType === 'feature') {
90
+ const tasksResult = searchTasks({ featureId: id });
91
+ if (tasksResult.success && tasksResult.data.length > 0) {
92
+ const events: string[] = [];
93
+ const allCompleted = tasksResult.data.every(t => t.status === 'COMPLETED' || t.status === 'CANCELLED');
94
+ const anyStarted = tasksResult.data.some(t => t.status !== 'PENDING' && t.status !== 'BACKLOG');
95
+
96
+ if (allCompleted) events.push('all_tasks_complete');
97
+ if (anyStarted) events.push('first_task_started');
98
+
99
+ if (events.length > 0) state.cascadeEvents = events;
100
+ }
101
+ }
102
+
103
+ return ok(state);
104
+ }
@@ -0,0 +1,129 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, uuidSchema } from './registry';
4
+ import { applyTemplate } from '../repos/templates';
5
+
6
+ /**
7
+ * Registers the `apply_template` MCP tool.
8
+ *
9
+ * Applies a template to an entity (PROJECT, FEATURE, or TASK),
10
+ * creating sections from the template's section definitions.
11
+ */
12
+ export function registerApplyTemplateTool(server: McpServer): void {
13
+ server.tool(
14
+ 'apply_template',
15
+ 'Apply a template to an entity (PROJECT, FEATURE, or TASK), creating sections from the template',
16
+ {
17
+ templateId: uuidSchema,
18
+ entityType: z.enum(['PROJECT', 'FEATURE', 'TASK']),
19
+ entityId: uuidSchema
20
+ },
21
+ async (params: any) => {
22
+ try {
23
+ // Validate required parameters
24
+ if (!params.templateId) {
25
+ return {
26
+ content: [{
27
+ type: 'text' as const,
28
+ text: JSON.stringify(
29
+ createErrorResponse('Template ID is required'),
30
+ null,
31
+ 2
32
+ )
33
+ }]
34
+ };
35
+ }
36
+
37
+ if (!params.entityType) {
38
+ return {
39
+ content: [{
40
+ type: 'text' as const,
41
+ text: JSON.stringify(
42
+ createErrorResponse('Entity type is required'),
43
+ null,
44
+ 2
45
+ )
46
+ }]
47
+ };
48
+ }
49
+
50
+ if (!params.entityId) {
51
+ return {
52
+ content: [{
53
+ type: 'text' as const,
54
+ text: JSON.stringify(
55
+ createErrorResponse('Entity ID is required'),
56
+ null,
57
+ 2
58
+ )
59
+ }]
60
+ };
61
+ }
62
+
63
+ // Validate entity type
64
+ const validEntityTypes = ['PROJECT', 'FEATURE', 'TASK'];
65
+ if (!validEntityTypes.includes(params.entityType)) {
66
+ return {
67
+ content: [{
68
+ type: 'text' as const,
69
+ text: JSON.stringify(
70
+ createErrorResponse(
71
+ `Invalid entity type. Must be one of: ${validEntityTypes.join(', ')}`
72
+ ),
73
+ null,
74
+ 2
75
+ )
76
+ }]
77
+ };
78
+ }
79
+
80
+ const result = applyTemplate(
81
+ params.templateId,
82
+ params.entityType,
83
+ params.entityId
84
+ );
85
+
86
+ if (!result.success) {
87
+ return {
88
+ content: [{
89
+ type: 'text' as const,
90
+ text: JSON.stringify(
91
+ createErrorResponse(result.error || 'Failed to apply template'),
92
+ null,
93
+ 2
94
+ )
95
+ }]
96
+ };
97
+ }
98
+
99
+ return {
100
+ content: [{
101
+ type: 'text' as const,
102
+ text: JSON.stringify(
103
+ createSuccessResponse(
104
+ `Template applied successfully. Created ${result.data.length} section(s)`,
105
+ result.data
106
+ ),
107
+ null,
108
+ 2
109
+ )
110
+ }]
111
+ };
112
+ } catch (error: any) {
113
+ return {
114
+ content: [{
115
+ type: 'text' as const,
116
+ text: JSON.stringify(
117
+ createErrorResponse(
118
+ 'Internal error',
119
+ error.message || 'Unknown error occurred'
120
+ ),
121
+ null,
122
+ 2
123
+ )
124
+ }]
125
+ };
126
+ }
127
+ }
128
+ );
129
+ }
@@ -0,0 +1,63 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, optionalUuidSchema } from './registry';
4
+ import { getBlockedTasks } from '../repos/dependencies';
5
+
6
+ /**
7
+ * Register the get_blocked_tasks MCP tool.
8
+ *
9
+ * Returns all blocked tasks, either with status 'BLOCKED' or tasks
10
+ * that have incomplete blocking dependencies.
11
+ */
12
+ export function registerGetBlockedTasksTool(server: McpServer): void {
13
+ server.tool(
14
+ 'get_blocked_tasks',
15
+ 'Get all blocked tasks. Returns tasks that either have status BLOCKED or have incomplete blocking dependencies (tasks that block them but are not completed). Results are sorted by priority (descending) then creation time (ascending).',
16
+ {
17
+ projectId: optionalUuidSchema.describe('Filter by project ID'),
18
+ featureId: optionalUuidSchema.describe('Filter by feature ID')
19
+ },
20
+ async (params: any) => {
21
+ try {
22
+ const result = getBlockedTasks({
23
+ projectId: params.projectId,
24
+ featureId: params.featureId
25
+ });
26
+
27
+ if (!result.success) {
28
+ return {
29
+ content: [{
30
+ type: 'text' as const,
31
+ text: JSON.stringify(createErrorResponse(result.error, result.code), null, 2)
32
+ }]
33
+ };
34
+ }
35
+
36
+ return {
37
+ content: [{
38
+ type: 'text' as const,
39
+ text: JSON.stringify(
40
+ createSuccessResponse(
41
+ `Found ${result.data.length} blocked task(s)`,
42
+ result.data
43
+ ),
44
+ null,
45
+ 2
46
+ )
47
+ }]
48
+ };
49
+ } catch (error: any) {
50
+ return {
51
+ content: [{
52
+ type: 'text' as const,
53
+ text: JSON.stringify(
54
+ createErrorResponse('Failed to get blocked tasks', error.message),
55
+ null,
56
+ 2
57
+ )
58
+ }]
59
+ };
60
+ }
61
+ }
62
+ );
63
+ }