@allpepper/task-orchestrator 1.1.3 → 1.2.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.
@@ -355,8 +355,8 @@ export function deleteTask(id: string): Result<boolean> {
355
355
  db.run('BEGIN TRANSACTION');
356
356
 
357
357
  try {
358
- // Delete related dependencies
359
- execute('DELETE FROM dependencies WHERE from_task_id = ? OR to_task_id = ?', [id, id]);
358
+ // Delete related dependencies (scoped to task entity type)
359
+ execute('DELETE FROM dependencies WHERE (from_entity_id = ? OR to_entity_id = ?) AND entity_type = ?', [id, id, 'task']);
360
360
 
361
361
  // Delete related sections
362
362
  execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.TASK, id]);
package/src/server.ts CHANGED
@@ -18,6 +18,8 @@ import {
18
18
  registerRenameTagTool,
19
19
  registerGetNextTaskTool,
20
20
  registerGetBlockedTasksTool,
21
+ registerGetNextFeatureTool,
22
+ registerGetBlockedFeaturesTool,
21
23
  registerGetNextStatusTool,
22
24
  registerQueryWorkflowStateTool,
23
25
  registerSetupProjectTool,
@@ -47,6 +49,8 @@ registerGetTagUsageTool(server);
47
49
  registerRenameTagTool(server);
48
50
  registerGetNextTaskTool(server);
49
51
  registerGetBlockedTasksTool(server);
52
+ registerGetNextFeatureTool(server);
53
+ registerGetBlockedFeaturesTool(server);
50
54
  registerGetNextStatusTool(server);
51
55
  registerQueryWorkflowStateTool(server);
52
56
  registerSetupProjectTool(server);
@@ -105,6 +109,8 @@ if (useHttp) {
105
109
  registerRenameTagTool(sessionServer);
106
110
  registerGetNextTaskTool(sessionServer);
107
111
  registerGetBlockedTasksTool(sessionServer);
112
+ registerGetNextFeatureTool(sessionServer);
113
+ registerGetBlockedFeaturesTool(sessionServer);
108
114
  registerGetNextStatusTool(sessionServer);
109
115
  registerQueryWorkflowStateTool(sessionServer);
110
116
  registerSetupProjectTool(sessionServer);
@@ -1,5 +1,7 @@
1
1
  import { ProjectStatus, FeatureStatus, TaskStatus } from '../domain/types';
2
2
 
3
+ type TransitionMap = Record<string, string[]>;
4
+
3
5
  // Valid statuses per container type
4
6
  const PROJECT_STATUSES = Object.values(ProjectStatus);
5
7
  const FEATURE_STATUSES = Object.values(FeatureStatus);
@@ -12,44 +14,44 @@ const TASK_STATUSES = Object.values(TaskStatus);
12
14
  * They allow transitions back to earlier workflow stages (BACKLOG/PENDING for tasks,
13
15
  * PLANNING for projects) to support reinstating cancelled or deferred work.
14
16
  */
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: [],
17
+ export const PROJECT_TRANSITIONS: Record<ProjectStatus, ProjectStatus[]> = {
18
+ [ProjectStatus.PLANNING]: [ProjectStatus.IN_DEVELOPMENT, ProjectStatus.ON_HOLD, ProjectStatus.CANCELLED],
19
+ [ProjectStatus.IN_DEVELOPMENT]: [ProjectStatus.COMPLETED, ProjectStatus.ON_HOLD, ProjectStatus.CANCELLED],
20
+ [ProjectStatus.ON_HOLD]: [ProjectStatus.PLANNING, ProjectStatus.IN_DEVELOPMENT, ProjectStatus.CANCELLED],
21
+ [ProjectStatus.COMPLETED]: [ProjectStatus.ARCHIVED],
22
+ [ProjectStatus.CANCELLED]: [ProjectStatus.PLANNING], // Non-terminal: allows reinstating cancelled projects
23
+ [ProjectStatus.ARCHIVED]: [],
22
24
  };
23
25
 
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: [],
26
+ export const FEATURE_TRANSITIONS: Record<FeatureStatus, FeatureStatus[]> = {
27
+ [FeatureStatus.DRAFT]: [FeatureStatus.PLANNING],
28
+ [FeatureStatus.PLANNING]: [FeatureStatus.IN_DEVELOPMENT, FeatureStatus.ON_HOLD],
29
+ [FeatureStatus.IN_DEVELOPMENT]: [FeatureStatus.TESTING, FeatureStatus.BLOCKED, FeatureStatus.ON_HOLD],
30
+ [FeatureStatus.TESTING]: [FeatureStatus.VALIDATING, FeatureStatus.IN_DEVELOPMENT],
31
+ [FeatureStatus.VALIDATING]: [FeatureStatus.PENDING_REVIEW, FeatureStatus.IN_DEVELOPMENT],
32
+ [FeatureStatus.PENDING_REVIEW]: [FeatureStatus.DEPLOYED, FeatureStatus.IN_DEVELOPMENT],
33
+ [FeatureStatus.BLOCKED]: [FeatureStatus.IN_DEVELOPMENT, FeatureStatus.ON_HOLD],
34
+ [FeatureStatus.ON_HOLD]: [FeatureStatus.PLANNING, FeatureStatus.IN_DEVELOPMENT],
35
+ [FeatureStatus.DEPLOYED]: [FeatureStatus.COMPLETED],
36
+ [FeatureStatus.COMPLETED]: [FeatureStatus.ARCHIVED],
37
+ [FeatureStatus.ARCHIVED]: [],
36
38
  };
37
39
 
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
40
+ export const TASK_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
41
+ [TaskStatus.BACKLOG]: [TaskStatus.PENDING],
42
+ [TaskStatus.PENDING]: [TaskStatus.IN_PROGRESS, TaskStatus.BLOCKED, TaskStatus.ON_HOLD, TaskStatus.CANCELLED, TaskStatus.DEFERRED],
43
+ [TaskStatus.IN_PROGRESS]: [TaskStatus.IN_REVIEW, TaskStatus.TESTING, TaskStatus.BLOCKED, TaskStatus.ON_HOLD, TaskStatus.COMPLETED],
44
+ [TaskStatus.IN_REVIEW]: [TaskStatus.CHANGES_REQUESTED, TaskStatus.COMPLETED],
45
+ [TaskStatus.CHANGES_REQUESTED]: [TaskStatus.IN_PROGRESS],
46
+ [TaskStatus.TESTING]: [TaskStatus.READY_FOR_QA, TaskStatus.IN_PROGRESS],
47
+ [TaskStatus.READY_FOR_QA]: [TaskStatus.INVESTIGATING, TaskStatus.DEPLOYED, TaskStatus.COMPLETED],
48
+ [TaskStatus.INVESTIGATING]: [TaskStatus.IN_PROGRESS, TaskStatus.BLOCKED],
49
+ [TaskStatus.BLOCKED]: [TaskStatus.PENDING, TaskStatus.IN_PROGRESS],
50
+ [TaskStatus.ON_HOLD]: [TaskStatus.PENDING, TaskStatus.IN_PROGRESS],
51
+ [TaskStatus.DEPLOYED]: [TaskStatus.COMPLETED],
52
+ [TaskStatus.COMPLETED]: [], // Terminal: no transitions allowed
53
+ [TaskStatus.CANCELLED]: [TaskStatus.BACKLOG, TaskStatus.PENDING], // Non-terminal: allows reinstating cancelled tasks
54
+ [TaskStatus.DEFERRED]: [TaskStatus.BACKLOG, TaskStatus.PENDING], // Non-terminal: allows resuming deferred tasks
53
55
  };
54
56
 
55
57
  // Terminal statuses (no transitions out)
@@ -77,7 +79,7 @@ export function getValidStatuses(containerType: ContainerType): string[] {
77
79
  }
78
80
  }
79
81
 
80
- export function getTransitions(containerType: ContainerType): Record<string, string[]> {
82
+ export function getTransitions(containerType: ContainerType): TransitionMap {
81
83
  switch (containerType) {
82
84
  case 'project': return PROJECT_TRANSITIONS;
83
85
  case 'feature': return FEATURE_TRANSITIONS;
@@ -6,6 +6,7 @@ import { getTask } from '../repos/tasks';
6
6
  import { getDependencies } from '../repos/dependencies';
7
7
  import { searchTasks } from '../repos/tasks';
8
8
  import type { Result } from '../domain/types';
9
+ import { DependencyEntityType } from '../domain/types';
9
10
  import { ok, err } from '../repos/base';
10
11
 
11
12
  export interface WorkflowState {
@@ -16,8 +17,8 @@ export interface WorkflowState {
16
17
  isTerminal: boolean;
17
18
  cascadeEvents?: string[];
18
19
  blockingDependencies?: Array<{
19
- taskId: string;
20
- taskTitle: string;
20
+ entityId: string;
21
+ entityName: string;
21
22
  status: string;
22
23
  }>;
23
24
  }
@@ -60,22 +61,23 @@ export function getWorkflowState(containerType: ContainerType, id: string): Resu
60
61
  isTerminal,
61
62
  };
62
63
 
63
- // 4. For tasks, check blocking dependencies
64
+ // 4. For tasks and features, check blocking dependencies
64
65
  if (containerType === 'task') {
65
- const depsResult = getDependencies(id, 'dependents');
66
+ // 'dependents' finds deps where to_entity_id = id, i.e. deps where this entity is the blocked one
67
+ const depsResult = getDependencies(id, 'dependents', DependencyEntityType.TASK);
66
68
  if (depsResult.success) {
67
69
  const blockers = depsResult.data
68
70
  .filter(d => d.type === 'BLOCKS')
69
71
  .map(d => {
70
- const taskResult = getTask(d.fromTaskId);
72
+ const taskResult = getTask(d.fromEntityId);
71
73
  if (taskResult.success) {
72
74
  return {
73
- taskId: d.fromTaskId,
74
- taskTitle: taskResult.data.title,
75
+ entityId: d.fromEntityId,
76
+ entityName: taskResult.data.title,
75
77
  status: taskResult.data.status,
76
78
  };
77
79
  }
78
- return { taskId: d.fromTaskId, taskTitle: 'Unknown', status: 'UNKNOWN' };
80
+ return { entityId: d.fromEntityId, entityName: 'Unknown', status: 'UNKNOWN' };
79
81
  })
80
82
  .filter(b => b.status !== 'COMPLETED' && b.status !== 'CANCELLED');
81
83
 
@@ -85,6 +87,31 @@ export function getWorkflowState(containerType: ContainerType, id: string): Resu
85
87
  }
86
88
  }
87
89
 
90
+ if (containerType === 'feature') {
91
+ // 'dependents' finds deps where to_entity_id = id, i.e. deps where this entity is the blocked one
92
+ const depsResult = getDependencies(id, 'dependents', DependencyEntityType.FEATURE);
93
+ if (depsResult.success) {
94
+ const blockers = depsResult.data
95
+ .filter(d => d.type === 'BLOCKS')
96
+ .map(d => {
97
+ const featureResult = getFeature(d.fromEntityId);
98
+ if (featureResult.success) {
99
+ return {
100
+ entityId: d.fromEntityId,
101
+ entityName: featureResult.data.name,
102
+ status: featureResult.data.status,
103
+ };
104
+ }
105
+ return { entityId: d.fromEntityId, entityName: 'Unknown', status: 'UNKNOWN' };
106
+ })
107
+ .filter(b => b.status !== 'COMPLETED' && b.status !== 'ARCHIVED');
108
+
109
+ if (blockers.length > 0) {
110
+ state.blockingDependencies = blockers;
111
+ }
112
+ }
113
+ }
114
+
88
115
  // 5. Detect cascade events for features/projects
89
116
  if (containerType === 'feature') {
90
117
  const tasksResult = searchTasks({ featureId: id });
@@ -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 { getBlocked } from '../repos/dependencies';
5
+ import { DependencyEntityType } from '../domain/types';
6
+
7
+ /**
8
+ * Register the get_blocked_features MCP tool.
9
+ *
10
+ * Returns all blocked features, either with status 'BLOCKED' or features
11
+ * that have incomplete blocking dependencies.
12
+ */
13
+ export function registerGetBlockedFeaturesTool(server: McpServer): void {
14
+ server.tool(
15
+ 'get_blocked_features',
16
+ 'Get all blocked features. Returns features that either have status BLOCKED or have incomplete blocking dependencies (features that block them but are not completed/archived). Results are sorted by priority (descending) then creation time (ascending).',
17
+ {
18
+ projectId: optionalUuidSchema.describe('Filter by project ID')
19
+ },
20
+ async (params: any) => {
21
+ try {
22
+ const result = getBlocked({
23
+ entityType: DependencyEntityType.FEATURE,
24
+ projectId: params.projectId
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 feature(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 features', error.message),
55
+ null,
56
+ 2
57
+ )
58
+ }]
59
+ };
60
+ }
61
+ }
62
+ );
63
+ }
@@ -1,7 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { createSuccessResponse, createErrorResponse, optionalUuidSchema } from './registry';
4
- import { getBlockedTasks } from '../repos/dependencies';
4
+ import { getBlocked } from '../repos/dependencies';
5
+ import { DependencyEntityType } from '../domain/types';
5
6
 
6
7
  /**
7
8
  * Register the get_blocked_tasks MCP tool.
@@ -19,7 +20,8 @@ export function registerGetBlockedTasksTool(server: McpServer): void {
19
20
  },
20
21
  async (params: any) => {
21
22
  try {
22
- const result = getBlockedTasks({
23
+ const result = getBlocked({
24
+ entityType: DependencyEntityType.TASK,
23
25
  projectId: params.projectId,
24
26
  featureId: params.featureId
25
27
  });
@@ -0,0 +1,75 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createSuccessResponse, createErrorResponse, optionalUuidSchema } from './registry';
4
+ import { getNext } from '../repos/dependencies';
5
+ import { DependencyEntityType } from '../domain/types';
6
+
7
+ /**
8
+ * Register the get_next_feature MCP tool.
9
+ *
10
+ * Recommends the next feature to work on by priority,
11
+ * excluding blocked features.
12
+ */
13
+ export function registerGetNextFeatureTool(server: McpServer): void {
14
+ server.tool(
15
+ 'get_next_feature',
16
+ 'Get the next recommended feature to work on. Returns the highest priority DRAFT or PLANNING feature that has no incomplete blocking dependencies. Features are prioritized by priority (HIGH > MEDIUM > LOW), then by creation time.',
17
+ {
18
+ projectId: optionalUuidSchema.describe('Filter by project ID'),
19
+ priority: z.string().optional().describe('Filter by priority (HIGH, MEDIUM, LOW)')
20
+ },
21
+ async (params: any) => {
22
+ try {
23
+ const result = getNext({
24
+ entityType: DependencyEntityType.FEATURE,
25
+ projectId: params.projectId,
26
+ priority: params.priority
27
+ });
28
+
29
+ if (!result.success) {
30
+ return {
31
+ content: [{
32
+ type: 'text' as const,
33
+ text: JSON.stringify(createErrorResponse(result.error, result.code), null, 2)
34
+ }]
35
+ };
36
+ }
37
+
38
+ if (result.data === null) {
39
+ return {
40
+ content: [{
41
+ type: 'text' as const,
42
+ text: JSON.stringify(
43
+ createSuccessResponse('No available features found matching the criteria', null),
44
+ null,
45
+ 2
46
+ )
47
+ }]
48
+ };
49
+ }
50
+
51
+ return {
52
+ content: [{
53
+ type: 'text' as const,
54
+ text: JSON.stringify(
55
+ createSuccessResponse('Next feature retrieved successfully', result.data),
56
+ null,
57
+ 2
58
+ )
59
+ }]
60
+ };
61
+ } catch (error: any) {
62
+ return {
63
+ content: [{
64
+ type: 'text' as const,
65
+ text: JSON.stringify(
66
+ createErrorResponse('Failed to get next feature', error.message),
67
+ null,
68
+ 2
69
+ )
70
+ }]
71
+ };
72
+ }
73
+ }
74
+ );
75
+ }
@@ -2,53 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { createSuccessResponse, createErrorResponse } from './registry';
4
4
  import { ProjectStatus, FeatureStatus, TaskStatus } from '../domain/types';
5
-
6
- /**
7
- * Status transition maps for each container type
8
- *
9
- * Note: CANCELLED and DEFERRED are intentionally non-terminal statuses.
10
- * They allow transitions back to earlier workflow stages (BACKLOG/PENDING for tasks,
11
- * PLANNING for projects) to support reinstating cancelled or deferred work.
12
- */
13
- const PROJECT_TRANSITIONS: Record<ProjectStatus, ProjectStatus[]> = {
14
- [ProjectStatus.PLANNING]: [ProjectStatus.IN_DEVELOPMENT, ProjectStatus.ON_HOLD, ProjectStatus.CANCELLED],
15
- [ProjectStatus.IN_DEVELOPMENT]: [ProjectStatus.COMPLETED, ProjectStatus.ON_HOLD, ProjectStatus.CANCELLED],
16
- [ProjectStatus.ON_HOLD]: [ProjectStatus.PLANNING, ProjectStatus.IN_DEVELOPMENT, ProjectStatus.CANCELLED],
17
- [ProjectStatus.COMPLETED]: [ProjectStatus.ARCHIVED],
18
- [ProjectStatus.CANCELLED]: [ProjectStatus.PLANNING], // Non-terminal: allows reinstating cancelled projects
19
- [ProjectStatus.ARCHIVED]: []
20
- };
21
-
22
- const FEATURE_TRANSITIONS: Record<FeatureStatus, FeatureStatus[]> = {
23
- [FeatureStatus.DRAFT]: [FeatureStatus.PLANNING],
24
- [FeatureStatus.PLANNING]: [FeatureStatus.IN_DEVELOPMENT, FeatureStatus.ON_HOLD],
25
- [FeatureStatus.IN_DEVELOPMENT]: [FeatureStatus.TESTING, FeatureStatus.BLOCKED, FeatureStatus.ON_HOLD],
26
- [FeatureStatus.TESTING]: [FeatureStatus.VALIDATING, FeatureStatus.IN_DEVELOPMENT],
27
- [FeatureStatus.VALIDATING]: [FeatureStatus.PENDING_REVIEW, FeatureStatus.IN_DEVELOPMENT],
28
- [FeatureStatus.PENDING_REVIEW]: [FeatureStatus.DEPLOYED, FeatureStatus.IN_DEVELOPMENT],
29
- [FeatureStatus.BLOCKED]: [FeatureStatus.IN_DEVELOPMENT, FeatureStatus.ON_HOLD],
30
- [FeatureStatus.ON_HOLD]: [FeatureStatus.PLANNING, FeatureStatus.IN_DEVELOPMENT],
31
- [FeatureStatus.DEPLOYED]: [FeatureStatus.COMPLETED],
32
- [FeatureStatus.COMPLETED]: [FeatureStatus.ARCHIVED],
33
- [FeatureStatus.ARCHIVED]: []
34
- };
35
-
36
- const TASK_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
37
- [TaskStatus.BACKLOG]: [TaskStatus.PENDING],
38
- [TaskStatus.PENDING]: [TaskStatus.IN_PROGRESS, TaskStatus.BLOCKED, TaskStatus.ON_HOLD, TaskStatus.CANCELLED, TaskStatus.DEFERRED],
39
- [TaskStatus.IN_PROGRESS]: [TaskStatus.IN_REVIEW, TaskStatus.TESTING, TaskStatus.BLOCKED, TaskStatus.ON_HOLD, TaskStatus.COMPLETED],
40
- [TaskStatus.IN_REVIEW]: [TaskStatus.CHANGES_REQUESTED, TaskStatus.COMPLETED],
41
- [TaskStatus.CHANGES_REQUESTED]: [TaskStatus.IN_PROGRESS],
42
- [TaskStatus.TESTING]: [TaskStatus.READY_FOR_QA, TaskStatus.IN_PROGRESS],
43
- [TaskStatus.READY_FOR_QA]: [TaskStatus.INVESTIGATING, TaskStatus.DEPLOYED, TaskStatus.COMPLETED],
44
- [TaskStatus.INVESTIGATING]: [TaskStatus.IN_PROGRESS, TaskStatus.BLOCKED],
45
- [TaskStatus.BLOCKED]: [TaskStatus.PENDING, TaskStatus.IN_PROGRESS],
46
- [TaskStatus.ON_HOLD]: [TaskStatus.PENDING, TaskStatus.IN_PROGRESS],
47
- [TaskStatus.DEPLOYED]: [TaskStatus.COMPLETED],
48
- [TaskStatus.COMPLETED]: [], // Terminal: no transitions allowed
49
- [TaskStatus.CANCELLED]: [TaskStatus.BACKLOG, TaskStatus.PENDING], // Non-terminal: allows reinstating cancelled tasks
50
- [TaskStatus.DEFERRED]: [TaskStatus.BACKLOG, TaskStatus.PENDING] // Non-terminal: allows resuming deferred tasks
51
- };
5
+ import { PROJECT_TRANSITIONS, FEATURE_TRANSITIONS, TASK_TRANSITIONS } from '../services/status-validator';
52
6
 
53
7
  /**
54
8
  * Register the get_next_status MCP tool.
@@ -1,7 +1,8 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { createSuccessResponse, createErrorResponse, optionalUuidSchema } from './registry';
4
- import { getNextTask } from '../repos/dependencies';
4
+ import { getNext } from '../repos/dependencies';
5
+ import { DependencyEntityType } from '../domain/types';
5
6
 
6
7
  /**
7
8
  * Register the get_next_task MCP tool.
@@ -20,7 +21,8 @@ export function registerGetNextTaskTool(server: McpServer): void {
20
21
  },
21
22
  async (params: any) => {
22
23
  try {
23
- const result = getNextTask({
24
+ const result = getNext({
25
+ entityType: DependencyEntityType.TASK,
24
26
  projectId: params.projectId,
25
27
  featureId: params.featureId,
26
28
  priority: params.priority
@@ -23,6 +23,8 @@ export { registerRenameTagTool } from './rename-tag';
23
23
  // Workflow tools
24
24
  export { registerGetNextTaskTool } from './get-next-task';
25
25
  export { registerGetBlockedTasksTool } from './get-blocked-tasks';
26
+ export { registerGetNextFeatureTool } from './get-next-feature';
27
+ export { registerGetBlockedFeaturesTool } from './get-blocked-features';
26
28
  export { registerGetNextStatusTool } from './get-next-status';
27
29
  export { registerQueryWorkflowStateTool } from './query-workflow-state';
28
30
 
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { createSuccessResponse, createErrorResponse, uuidSchema, optionalUuidSchema } from './registry';
4
4
  import { createDependency, deleteDependency } from '../repos/dependencies';
5
- import { DependencyType } from '../domain/types';
5
+ import { DependencyType, DependencyEntityType } from '../domain/types';
6
6
 
7
7
  /**
8
8
  * Register the manage_dependency MCP tool.
@@ -14,17 +14,21 @@ import { DependencyType } from '../domain/types';
14
14
  export function registerManageDependencyTool(server: McpServer): void {
15
15
  server.tool(
16
16
  'manage_dependency',
17
- 'Manage dependencies between tasks (create, delete)',
17
+ 'Manage dependencies between entities of the same type (create, delete). Supports task-to-task and feature-to-feature dependencies.',
18
18
  {
19
19
  operation: z
20
20
  .enum(['create', 'delete'])
21
21
  .describe('Operation to perform: create or delete'),
22
22
  id: optionalUuidSchema
23
23
  .describe('Dependency ID (required for delete operation)'),
24
- fromTaskId: optionalUuidSchema
25
- .describe('Source task ID (required for create operation)'),
26
- toTaskId: optionalUuidSchema
27
- .describe('Target task ID (required for create operation)'),
24
+ fromId: optionalUuidSchema
25
+ .describe('Source entity ID (required for create operation)'),
26
+ toId: optionalUuidSchema
27
+ .describe('Target entity ID (required for create operation)'),
28
+ containerType: z
29
+ .enum(['task', 'feature'])
30
+ .optional()
31
+ .describe('Entity type for the dependency (required for create operation)'),
28
32
  type: z
29
33
  .enum(['BLOCKS', 'IS_BLOCKED_BY', 'RELATES_TO'])
30
34
  .optional()
@@ -36,16 +40,31 @@ export function registerManageDependencyTool(server: McpServer): void {
36
40
 
37
41
  // Handle create operation
38
42
  if (operation === 'create') {
39
- const { fromTaskId, toTaskId, type } = params;
43
+ const { fromId, toId, type, containerType } = params;
40
44
 
41
45
  // Validate required parameters for create
42
- if (!fromTaskId) {
46
+ if (!fromId) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text' as const,
51
+ text: JSON.stringify(
52
+ createErrorResponse('fromId is required for create operation'),
53
+ null,
54
+ 2
55
+ ),
56
+ },
57
+ ],
58
+ };
59
+ }
60
+
61
+ if (!toId) {
43
62
  return {
44
63
  content: [
45
64
  {
46
65
  type: 'text' as const,
47
66
  text: JSON.stringify(
48
- createErrorResponse('fromTaskId is required for create operation'),
67
+ createErrorResponse('toId is required for create operation'),
49
68
  null,
50
69
  2
51
70
  ),
@@ -54,13 +73,13 @@ export function registerManageDependencyTool(server: McpServer): void {
54
73
  };
55
74
  }
56
75
 
57
- if (!toTaskId) {
76
+ if (!containerType) {
58
77
  return {
59
78
  content: [
60
79
  {
61
80
  type: 'text' as const,
62
81
  text: JSON.stringify(
63
- createErrorResponse('toTaskId is required for create operation'),
82
+ createErrorResponse('containerType is required for create operation'),
64
83
  null,
65
84
  2
66
85
  ),
@@ -85,9 +104,10 @@ export function registerManageDependencyTool(server: McpServer): void {
85
104
  }
86
105
 
87
106
  const result = createDependency({
88
- fromTaskId,
89
- toTaskId,
107
+ fromEntityId: fromId,
108
+ toEntityId: toId,
90
109
  type: type as DependencyType,
110
+ entityType: containerType as DependencyEntityType,
91
111
  });
92
112
 
93
113
  if (result.success === false) {
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { createSuccessResponse, createErrorResponse, uuidSchema } from './registry';
4
4
  import { getDependencies } from '../repos/dependencies';
5
+ import { DependencyEntityType } from '../domain/types';
5
6
 
6
7
  /**
7
8
  * Register the query_dependencies MCP tool.
@@ -13,9 +14,12 @@ import { getDependencies } from '../repos/dependencies';
13
14
  export function registerQueryDependenciesTool(server: McpServer): void {
14
15
  server.tool(
15
16
  'query_dependencies',
16
- 'Query dependencies for a task',
17
+ 'Query dependencies for an entity (task or feature)',
17
18
  {
18
- taskId: uuidSchema.describe('Task ID'),
19
+ id: uuidSchema.describe('Entity ID (task or feature)'),
20
+ containerType: z
21
+ .enum(['task', 'feature'])
22
+ .describe('Entity type to query dependencies for'),
19
23
  direction: z
20
24
  .enum(['dependencies', 'dependents', 'both'])
21
25
  .optional()
@@ -24,7 +28,7 @@ export function registerQueryDependenciesTool(server: McpServer): void {
24
28
  },
25
29
  async (params) => {
26
30
  try {
27
- const result = getDependencies(params.taskId, params.direction);
31
+ const result = getDependencies(params.id, params.direction, params.containerType as DependencyEntityType);
28
32
 
29
33
  if (result.success === false) {
30
34
  return {