@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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/src/db/client.ts +34 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrate.ts +51 -0
- package/src/db/migrations/001_initial_schema.sql +160 -0
- package/src/domain/index.ts +1 -0
- package/src/domain/types.ts +225 -0
- package/src/index.ts +7 -0
- package/src/repos/base.ts +151 -0
- package/src/repos/dependencies.ts +356 -0
- package/src/repos/features.ts +507 -0
- package/src/repos/index.ts +4 -0
- package/src/repos/projects.ts +350 -0
- package/src/repos/sections.ts +505 -0
- package/src/repos/tags.example.ts +125 -0
- package/src/repos/tags.ts +175 -0
- package/src/repos/tasks.ts +581 -0
- package/src/repos/templates.ts +649 -0
- package/src/server.ts +121 -0
- package/src/services/index.ts +2 -0
- package/src/services/status-validator.ts +100 -0
- package/src/services/workflow.ts +104 -0
- package/src/tools/apply-template.ts +129 -0
- package/src/tools/get-blocked-tasks.ts +63 -0
- package/src/tools/get-next-status.ts +183 -0
- package/src/tools/get-next-task.ts +75 -0
- package/src/tools/get-tag-usage.ts +54 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-tags.ts +56 -0
- package/src/tools/manage-container.ts +333 -0
- package/src/tools/manage-dependency.ts +198 -0
- package/src/tools/manage-sections.ts +388 -0
- package/src/tools/manage-template.ts +313 -0
- package/src/tools/query-container.ts +296 -0
- package/src/tools/query-dependencies.ts +68 -0
- package/src/tools/query-sections.ts +70 -0
- package/src/tools/query-templates.ts +137 -0
- package/src/tools/query-workflow-state.ts +198 -0
- package/src/tools/registry.ts +180 -0
- package/src/tools/rename-tag.ts +64 -0
- package/src/tools/setup-project.ts +189 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { db, generateId, now } from '../db/client';
|
|
2
|
+
import type { Result } from '../domain/types';
|
|
3
|
+
import { NotFoundError, ValidationError, ConflictError } from '../domain/types';
|
|
4
|
+
|
|
5
|
+
// Re-export for convenience
|
|
6
|
+
export { db, generateId, now };
|
|
7
|
+
|
|
8
|
+
// --- Query helpers ---
|
|
9
|
+
|
|
10
|
+
/** Get a single row by query, or null */
|
|
11
|
+
export function queryOne<T>(sql: string, params: any[] = []): T | null {
|
|
12
|
+
return db.query<T, any[]>(sql).get(...params) ?? null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Get all rows matching query */
|
|
16
|
+
export function queryAll<T>(sql: string, params: any[] = []): T[] {
|
|
17
|
+
return db.query<T, any[]>(sql).all(...params);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Execute a write statement, return changes count */
|
|
21
|
+
export function execute(sql: string, params: any[] = []): number {
|
|
22
|
+
const result = db.run(sql, params);
|
|
23
|
+
return result.changes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- UUID helpers ---
|
|
27
|
+
|
|
28
|
+
/** Convert hex string ID to the format used in queries */
|
|
29
|
+
export function toId(hex: string): string {
|
|
30
|
+
return hex;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- Timestamp helpers ---
|
|
34
|
+
|
|
35
|
+
/** Parse ISO timestamp string to Date */
|
|
36
|
+
export function toDate(ts: string): Date {
|
|
37
|
+
return new Date(ts);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Format Date to ISO string for storage */
|
|
41
|
+
export function toTimestamp(date: Date): string {
|
|
42
|
+
return date.toISOString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Search vector builder ---
|
|
46
|
+
|
|
47
|
+
/** Build a search vector from text fields for LIKE-based searching */
|
|
48
|
+
export function buildSearchVector(...fields: (string | undefined | null)[]): string {
|
|
49
|
+
return fields
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.map(f => f!.toLowerCase())
|
|
52
|
+
.join(' ');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Tag helpers (entity_tags table) ---
|
|
56
|
+
|
|
57
|
+
/** Load tags for an entity */
|
|
58
|
+
export function loadTags(entityId: string, entityType: string): string[] {
|
|
59
|
+
const rows = queryAll<{ tag: string }>(
|
|
60
|
+
'SELECT tag FROM entity_tags WHERE entity_id = ? AND entity_type = ?',
|
|
61
|
+
[entityId, entityType]
|
|
62
|
+
);
|
|
63
|
+
return rows.map(r => r.tag);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Save tags for an entity (replaces existing) */
|
|
67
|
+
export function saveTags(entityId: string, entityType: string, tags: string[]): void {
|
|
68
|
+
execute('DELETE FROM entity_tags WHERE entity_id = ? AND entity_type = ?', [entityId, entityType]);
|
|
69
|
+
|
|
70
|
+
// Normalize and deduplicate tags
|
|
71
|
+
const uniqueTags = new Set<string>();
|
|
72
|
+
for (const tag of tags) {
|
|
73
|
+
const normalizedTag = tag.trim().toLowerCase();
|
|
74
|
+
if (normalizedTag) {
|
|
75
|
+
uniqueTags.add(normalizedTag);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Insert unique tags
|
|
80
|
+
for (const normalizedTag of uniqueTags) {
|
|
81
|
+
execute(
|
|
82
|
+
'INSERT INTO entity_tags (id, entity_id, entity_type, tag, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
83
|
+
[generateId(), entityId, entityType, normalizedTag, now()]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Delete all tags for an entity */
|
|
89
|
+
export function deleteTags(entityId: string, entityType: string): void {
|
|
90
|
+
execute('DELETE FROM entity_tags WHERE entity_id = ? AND entity_type = ?', [entityId, entityType]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Result helpers ---
|
|
94
|
+
|
|
95
|
+
/** Wrap a value in a success result */
|
|
96
|
+
export function ok<T>(data: T): Result<T> {
|
|
97
|
+
return { success: true, data };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Wrap an error in a failure result */
|
|
101
|
+
export function err<T>(error: string, code?: string): Result<T> {
|
|
102
|
+
return { success: false, error, code };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Pagination helper ---
|
|
106
|
+
|
|
107
|
+
export interface PaginationParams {
|
|
108
|
+
limit?: number;
|
|
109
|
+
offset?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildPaginationClause(params: PaginationParams): string {
|
|
113
|
+
const limit = params.limit ?? 20;
|
|
114
|
+
const offset = params.offset ?? 0;
|
|
115
|
+
return ` LIMIT ${limit} OFFSET ${offset}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Count helper ---
|
|
119
|
+
|
|
120
|
+
export interface TaskCounts {
|
|
121
|
+
total: number;
|
|
122
|
+
byStatus: Record<string, number>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function countTasksByFeature(featureId: string): TaskCounts {
|
|
126
|
+
const rows = queryAll<{ status: string; count: number }>(
|
|
127
|
+
'SELECT status, COUNT(*) as count FROM tasks WHERE feature_id = ? GROUP BY status',
|
|
128
|
+
[featureId]
|
|
129
|
+
);
|
|
130
|
+
const byStatus: Record<string, number> = {};
|
|
131
|
+
let total = 0;
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
byStatus[row.status] = row.count;
|
|
134
|
+
total += row.count;
|
|
135
|
+
}
|
|
136
|
+
return { total, byStatus };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function countTasksByProject(projectId: string): TaskCounts {
|
|
140
|
+
const rows = queryAll<{ status: string; count: number }>(
|
|
141
|
+
'SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status',
|
|
142
|
+
[projectId]
|
|
143
|
+
);
|
|
144
|
+
const byStatus: Record<string, number> = {};
|
|
145
|
+
let total = 0;
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
byStatus[row.status] = row.count;
|
|
148
|
+
total += row.count;
|
|
149
|
+
}
|
|
150
|
+
return { total, byStatus };
|
|
151
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
generateId,
|
|
4
|
+
now,
|
|
5
|
+
queryOne,
|
|
6
|
+
queryAll,
|
|
7
|
+
execute,
|
|
8
|
+
ok,
|
|
9
|
+
err,
|
|
10
|
+
toDate
|
|
11
|
+
} from './base';
|
|
12
|
+
import type { Result, Dependency, DependencyType, Task } from '../domain/types';
|
|
13
|
+
import { ValidationError, NotFoundError, ConflictError } from '../domain/types';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Helper: Circular Dependency Detection
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if adding a dependency would create a circular dependency.
|
|
21
|
+
* Uses BFS to traverse the dependency graph from toTaskId following BLOCKS dependencies.
|
|
22
|
+
* If we reach fromTaskId, it means adding fromTaskId -> toTaskId would create a cycle.
|
|
23
|
+
*/
|
|
24
|
+
function hasCircularDependency(fromTaskId: string, toTaskId: string): boolean {
|
|
25
|
+
const visited = new Set<string>();
|
|
26
|
+
const queue = [toTaskId];
|
|
27
|
+
|
|
28
|
+
while (queue.length > 0) {
|
|
29
|
+
const current = queue.shift()!;
|
|
30
|
+
|
|
31
|
+
if (current === fromTaskId) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (visited.has(current)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
visited.add(current);
|
|
40
|
+
|
|
41
|
+
// Follow BLOCKS dependencies from current task
|
|
42
|
+
const deps = queryAll<{ to_task_id: string }>(
|
|
43
|
+
"SELECT to_task_id FROM dependencies WHERE from_task_id = ? AND type = 'BLOCKS'",
|
|
44
|
+
[current]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
for (const dep of deps) {
|
|
48
|
+
queue.push(dep.to_task_id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Mapping Functions
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/** Map database row to Dependency domain object */
|
|
60
|
+
function mapRowToDependency(row: any): Dependency {
|
|
61
|
+
return {
|
|
62
|
+
id: row.id,
|
|
63
|
+
fromTaskId: row.from_task_id,
|
|
64
|
+
toTaskId: row.to_task_id,
|
|
65
|
+
type: row.type as DependencyType,
|
|
66
|
+
createdAt: toDate(row.created_at)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Map database row to Task domain object */
|
|
71
|
+
function mapRowToTask(row: any): Task {
|
|
72
|
+
return {
|
|
73
|
+
id: row.id,
|
|
74
|
+
projectId: row.project_id ?? undefined,
|
|
75
|
+
featureId: row.feature_id ?? undefined,
|
|
76
|
+
title: row.title,
|
|
77
|
+
summary: row.summary,
|
|
78
|
+
description: row.description ?? undefined,
|
|
79
|
+
status: row.status,
|
|
80
|
+
priority: row.priority,
|
|
81
|
+
complexity: row.complexity,
|
|
82
|
+
version: row.version,
|
|
83
|
+
lastModifiedBy: row.last_modified_by ?? undefined,
|
|
84
|
+
lockStatus: row.lock_status,
|
|
85
|
+
createdAt: toDate(row.created_at),
|
|
86
|
+
modifiedAt: toDate(row.modified_at)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Repository Functions
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a new dependency between two tasks.
|
|
96
|
+
*
|
|
97
|
+
* Validates:
|
|
98
|
+
* - fromTaskId != toTaskId (no self-dependency)
|
|
99
|
+
* - Both tasks exist
|
|
100
|
+
* - No circular dependencies (if A blocks B, B cannot block A)
|
|
101
|
+
* - No duplicate dependencies
|
|
102
|
+
*/
|
|
103
|
+
export function createDependency(params: {
|
|
104
|
+
fromTaskId: string;
|
|
105
|
+
toTaskId: string;
|
|
106
|
+
type: DependencyType;
|
|
107
|
+
}): Result<Dependency> {
|
|
108
|
+
const { fromTaskId, toTaskId, type } = params;
|
|
109
|
+
|
|
110
|
+
// Validate: no self-dependency
|
|
111
|
+
if (fromTaskId === toTaskId) {
|
|
112
|
+
return err('Cannot create a dependency from a task to itself', 'SELF_DEPENDENCY');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate: both tasks exist
|
|
116
|
+
const fromTask = queryOne<{ id: string }>(
|
|
117
|
+
'SELECT id FROM tasks WHERE id = ?',
|
|
118
|
+
[fromTaskId]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (!fromTask) {
|
|
122
|
+
return err(`Task not found: ${fromTaskId}`, 'NOT_FOUND');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const toTask = queryOne<{ id: string }>(
|
|
126
|
+
'SELECT id FROM tasks WHERE id = ?',
|
|
127
|
+
[toTaskId]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!toTask) {
|
|
131
|
+
return err(`Task not found: ${toTaskId}`, 'NOT_FOUND');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validate: no circular dependencies for BLOCKS type
|
|
135
|
+
if (type === 'BLOCKS' && hasCircularDependency(fromTaskId, toTaskId)) {
|
|
136
|
+
return err(
|
|
137
|
+
'Cannot create dependency: would create a circular dependency',
|
|
138
|
+
'CIRCULAR_DEPENDENCY'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for duplicate
|
|
143
|
+
const existing = queryOne<{ id: string }>(
|
|
144
|
+
'SELECT id FROM dependencies WHERE from_task_id = ? AND to_task_id = ? AND type = ?',
|
|
145
|
+
[fromTaskId, toTaskId, type]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (existing) {
|
|
149
|
+
return err(
|
|
150
|
+
'Dependency already exists between these tasks with this type',
|
|
151
|
+
'DUPLICATE_DEPENDENCY'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create dependency
|
|
156
|
+
const id = generateId();
|
|
157
|
+
const createdAt = now();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
execute(
|
|
161
|
+
'INSERT INTO dependencies (id, from_task_id, to_task_id, type, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
162
|
+
[id, fromTaskId, toTaskId, type, createdAt]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const dependency: Dependency = {
|
|
166
|
+
id,
|
|
167
|
+
fromTaskId,
|
|
168
|
+
toTaskId,
|
|
169
|
+
type,
|
|
170
|
+
createdAt: toDate(createdAt)
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return ok(dependency);
|
|
174
|
+
} catch (error: any) {
|
|
175
|
+
return err(`Failed to create dependency: ${error.message}`, 'CREATE_FAILED');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get dependencies for a task.
|
|
181
|
+
*
|
|
182
|
+
* @param taskId - The task ID to query
|
|
183
|
+
* @param direction - Filter by direction:
|
|
184
|
+
* - 'dependencies': tasks that this task depends on (from_task_id = taskId)
|
|
185
|
+
* - 'dependents': tasks that depend on this task (to_task_id = taskId)
|
|
186
|
+
* - 'both': union of above (default)
|
|
187
|
+
*/
|
|
188
|
+
export function getDependencies(
|
|
189
|
+
taskId: string,
|
|
190
|
+
direction: 'dependencies' | 'dependents' | 'both' = 'both'
|
|
191
|
+
): Result<Dependency[]> {
|
|
192
|
+
try {
|
|
193
|
+
let dependencies: Dependency[] = [];
|
|
194
|
+
|
|
195
|
+
if (direction === 'dependencies' || direction === 'both') {
|
|
196
|
+
const rows = queryAll<any>(
|
|
197
|
+
'SELECT * FROM dependencies WHERE from_task_id = ? ORDER BY created_at',
|
|
198
|
+
[taskId]
|
|
199
|
+
);
|
|
200
|
+
dependencies.push(...rows.map(mapRowToDependency));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (direction === 'dependents' || direction === 'both') {
|
|
204
|
+
const rows = queryAll<any>(
|
|
205
|
+
'SELECT * FROM dependencies WHERE to_task_id = ? ORDER BY created_at',
|
|
206
|
+
[taskId]
|
|
207
|
+
);
|
|
208
|
+
dependencies.push(...rows.map(mapRowToDependency));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return ok(dependencies);
|
|
212
|
+
} catch (error: any) {
|
|
213
|
+
return err(`Failed to get dependencies: ${error.message}`, 'QUERY_FAILED');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Delete a dependency by ID.
|
|
219
|
+
*/
|
|
220
|
+
export function deleteDependency(id: string): Result<boolean> {
|
|
221
|
+
try {
|
|
222
|
+
const changes = execute('DELETE FROM dependencies WHERE id = ?', [id]);
|
|
223
|
+
|
|
224
|
+
if (changes === 0) {
|
|
225
|
+
return err(`Dependency not found: ${id}`, 'NOT_FOUND');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return ok(true);
|
|
229
|
+
} catch (error: any) {
|
|
230
|
+
return err(`Failed to delete dependency: ${error.message}`, 'DELETE_FAILED');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get all blocked tasks.
|
|
236
|
+
*
|
|
237
|
+
* Returns tasks that either:
|
|
238
|
+
* - Have status = 'BLOCKED', OR
|
|
239
|
+
* - Have incomplete blocking dependencies (tasks that block them but are not completed)
|
|
240
|
+
*
|
|
241
|
+
* @param params - Optional filters for projectId and/or featureId
|
|
242
|
+
*/
|
|
243
|
+
export function getBlockedTasks(params?: {
|
|
244
|
+
projectId?: string;
|
|
245
|
+
featureId?: string;
|
|
246
|
+
}): Result<Task[]> {
|
|
247
|
+
try {
|
|
248
|
+
let sql = `
|
|
249
|
+
SELECT DISTINCT t.*
|
|
250
|
+
FROM tasks t
|
|
251
|
+
WHERE (
|
|
252
|
+
t.status = 'BLOCKED'
|
|
253
|
+
OR EXISTS (
|
|
254
|
+
SELECT 1
|
|
255
|
+
FROM dependencies d
|
|
256
|
+
JOIN tasks blocker ON blocker.id = d.from_task_id
|
|
257
|
+
WHERE d.to_task_id = t.id
|
|
258
|
+
AND d.type = 'BLOCKS'
|
|
259
|
+
AND blocker.status NOT IN ('COMPLETED', 'CANCELLED')
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
const sqlParams: string[] = [];
|
|
265
|
+
|
|
266
|
+
if (params?.projectId) {
|
|
267
|
+
sql += ' AND t.project_id = ?';
|
|
268
|
+
sqlParams.push(params.projectId);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (params?.featureId) {
|
|
272
|
+
sql += ' AND t.feature_id = ?';
|
|
273
|
+
sqlParams.push(params.featureId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
sql += ' ORDER BY t.priority DESC, t.created_at ASC';
|
|
277
|
+
|
|
278
|
+
const rows = queryAll<any>(sql, sqlParams);
|
|
279
|
+
const tasks = rows.map(mapRowToTask);
|
|
280
|
+
|
|
281
|
+
return ok(tasks);
|
|
282
|
+
} catch (error: any) {
|
|
283
|
+
return err(`Failed to get blocked tasks: ${error.message}`, 'QUERY_FAILED');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get the next task to work on.
|
|
289
|
+
*
|
|
290
|
+
* Returns the highest priority PENDING task that has no incomplete blocking dependencies.
|
|
291
|
+
* Considers priority and complexity.
|
|
292
|
+
*
|
|
293
|
+
* @param params - Optional filters and priority preference
|
|
294
|
+
*/
|
|
295
|
+
export function getNextTask(params?: {
|
|
296
|
+
projectId?: string;
|
|
297
|
+
featureId?: string;
|
|
298
|
+
priority?: string;
|
|
299
|
+
}): Result<Task | null> {
|
|
300
|
+
try {
|
|
301
|
+
let sql = `
|
|
302
|
+
SELECT t.*
|
|
303
|
+
FROM tasks t
|
|
304
|
+
WHERE t.status = 'PENDING'
|
|
305
|
+
AND NOT EXISTS (
|
|
306
|
+
SELECT 1
|
|
307
|
+
FROM dependencies d
|
|
308
|
+
JOIN tasks blocker ON blocker.id = d.from_task_id
|
|
309
|
+
WHERE d.to_task_id = t.id
|
|
310
|
+
AND d.type = 'BLOCKS'
|
|
311
|
+
AND blocker.status NOT IN ('COMPLETED', 'CANCELLED')
|
|
312
|
+
)
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
const sqlParams: string[] = [];
|
|
316
|
+
|
|
317
|
+
if (params?.projectId) {
|
|
318
|
+
sql += ' AND t.project_id = ?';
|
|
319
|
+
sqlParams.push(params.projectId);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (params?.featureId) {
|
|
323
|
+
sql += ' AND t.feature_id = ?';
|
|
324
|
+
sqlParams.push(params.featureId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (params?.priority) {
|
|
328
|
+
sql += ' AND t.priority = ?';
|
|
329
|
+
sqlParams.push(params.priority);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Order by priority (HIGH > MEDIUM > LOW), then by complexity (simpler first), then by creation time
|
|
333
|
+
sql += `
|
|
334
|
+
ORDER BY
|
|
335
|
+
CASE t.priority
|
|
336
|
+
WHEN 'HIGH' THEN 1
|
|
337
|
+
WHEN 'MEDIUM' THEN 2
|
|
338
|
+
WHEN 'LOW' THEN 3
|
|
339
|
+
END,
|
|
340
|
+
t.complexity ASC,
|
|
341
|
+
t.created_at ASC
|
|
342
|
+
LIMIT 1
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
const row = queryOne<any>(sql, sqlParams);
|
|
346
|
+
|
|
347
|
+
if (!row) {
|
|
348
|
+
return ok(null);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const task = mapRowToTask(row);
|
|
352
|
+
return ok(task);
|
|
353
|
+
} catch (error: any) {
|
|
354
|
+
return err(`Failed to get next task: ${error.message}`, 'QUERY_FAILED');
|
|
355
|
+
}
|
|
356
|
+
}
|