@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.
- package/package.json +1 -1
- package/src/db/migrate.ts +1 -1
- package/src/db/migrations/002_generalize_dependencies.sql +27 -0
- package/src/domain/types.ts +8 -2
- package/src/repos/dependencies.ts +269 -148
- package/src/repos/features.ts +4 -1
- package/src/repos/projects.ts +3 -2
- package/src/repos/tasks.ts +2 -2
- package/src/server.ts +6 -0
- package/src/services/status-validator.ts +37 -35
- package/src/services/workflow.ts +35 -8
- package/src/tools/get-blocked-features.ts +63 -0
- package/src/tools/get-blocked-tasks.ts +4 -2
- package/src/tools/get-next-feature.ts +75 -0
- package/src/tools/get-next-status.ts +1 -47
- package/src/tools/get-next-task.ts +4 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/manage-dependency.ts +33 -13
- package/src/tools/query-dependencies.ts +7 -3
- package/src/tools/query-workflow-state.ts +54 -129
- package/src/tools/registry.ts +3 -0
package/src/repos/tasks.ts
CHANGED
|
@@ -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
|
|
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<
|
|
16
|
-
PLANNING: [
|
|
17
|
-
IN_DEVELOPMENT: [
|
|
18
|
-
ON_HOLD: [
|
|
19
|
-
COMPLETED: [
|
|
20
|
-
CANCELLED: [
|
|
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<
|
|
25
|
-
DRAFT: [
|
|
26
|
-
PLANNING: [
|
|
27
|
-
IN_DEVELOPMENT: [
|
|
28
|
-
TESTING: [
|
|
29
|
-
VALIDATING: [
|
|
30
|
-
PENDING_REVIEW: [
|
|
31
|
-
BLOCKED: [
|
|
32
|
-
ON_HOLD: [
|
|
33
|
-
DEPLOYED: [
|
|
34
|
-
COMPLETED: [
|
|
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<
|
|
39
|
-
BACKLOG: [
|
|
40
|
-
PENDING: [
|
|
41
|
-
IN_PROGRESS: [
|
|
42
|
-
IN_REVIEW: [
|
|
43
|
-
CHANGES_REQUESTED: [
|
|
44
|
-
TESTING: [
|
|
45
|
-
READY_FOR_QA: [
|
|
46
|
-
INVESTIGATING: [
|
|
47
|
-
BLOCKED: [
|
|
48
|
-
ON_HOLD: [
|
|
49
|
-
DEPLOYED: [
|
|
50
|
-
COMPLETED: [], // Terminal: no transitions allowed
|
|
51
|
-
CANCELLED: [
|
|
52
|
-
DEFERRED: [
|
|
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):
|
|
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;
|
package/src/services/workflow.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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.
|
|
72
|
+
const taskResult = getTask(d.fromEntityId);
|
|
71
73
|
if (taskResult.success) {
|
|
72
74
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
entityId: d.fromEntityId,
|
|
76
|
+
entityName: taskResult.data.title,
|
|
75
77
|
status: taskResult.data.status,
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
|
-
return {
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
24
|
+
const result = getNext({
|
|
25
|
+
entityType: DependencyEntityType.TASK,
|
|
24
26
|
projectId: params.projectId,
|
|
25
27
|
featureId: params.featureId,
|
|
26
28
|
priority: params.priority
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
25
|
-
.describe('Source
|
|
26
|
-
|
|
27
|
-
.describe('Target
|
|
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 {
|
|
43
|
+
const { fromId, toId, type, containerType } = params;
|
|
40
44
|
|
|
41
45
|
// Validate required parameters for create
|
|
42
|
-
if (!
|
|
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('
|
|
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 (!
|
|
76
|
+
if (!containerType) {
|
|
58
77
|
return {
|
|
59
78
|
content: [
|
|
60
79
|
{
|
|
61
80
|
type: 'text' as const,
|
|
62
81
|
text: JSON.stringify(
|
|
63
|
-
createErrorResponse('
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
17
|
+
'Query dependencies for an entity (task or feature)',
|
|
17
18
|
{
|
|
18
|
-
|
|
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.
|
|
31
|
+
const result = getDependencies(params.id, params.direction, params.containerType as DependencyEntityType);
|
|
28
32
|
|
|
29
33
|
if (result.success === false) {
|
|
30
34
|
return {
|