agents-task-assigning 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 +311 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1672 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1672 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/server.ts
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// src/db/connection.ts
|
|
12
|
+
import Database from "better-sqlite3";
|
|
13
|
+
import { mkdirSync } from "fs";
|
|
14
|
+
import { dirname, resolve } from "path";
|
|
15
|
+
|
|
16
|
+
// src/db/schema.ts
|
|
17
|
+
function initializeSchema(db) {
|
|
18
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS task_groups (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
title TEXT NOT NULL,
|
|
22
|
+
description TEXT NOT NULL,
|
|
23
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
group_id TEXT NOT NULL REFERENCES task_groups(id),
|
|
30
|
+
sequence INTEGER NOT NULL,
|
|
31
|
+
title TEXT NOT NULL,
|
|
32
|
+
description TEXT NOT NULL,
|
|
33
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
34
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
35
|
+
assigned_to TEXT,
|
|
36
|
+
branch_name TEXT,
|
|
37
|
+
worktree_path TEXT,
|
|
38
|
+
progress INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
progress_note TEXT,
|
|
40
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
41
|
+
started_at TEXT,
|
|
42
|
+
completed_at TEXT,
|
|
43
|
+
merged_at TEXT
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
47
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
48
|
+
depends_on TEXT NOT NULL REFERENCES tasks(id),
|
|
49
|
+
PRIMARY KEY (task_id, depends_on)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS task_file_ownership (
|
|
53
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
54
|
+
file_pattern TEXT NOT NULL,
|
|
55
|
+
ownership_type TEXT NOT NULL DEFAULT 'exclusive',
|
|
56
|
+
PRIMARY KEY (task_id, file_pattern)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS progress_logs (
|
|
60
|
+
id TEXT PRIMARY KEY,
|
|
61
|
+
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
62
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
63
|
+
event TEXT NOT NULL,
|
|
64
|
+
message TEXT NOT NULL,
|
|
65
|
+
metadata TEXT
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_group ON tasks(group_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_deps_task ON task_dependencies(task_id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_deps_depends ON task_dependencies(depends_on);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_logs_task ON progress_logs(task_id);
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/db/connection.ts
|
|
77
|
+
var instances = /* @__PURE__ */ new Map();
|
|
78
|
+
function resolveDefaultPath() {
|
|
79
|
+
const envPath = process.env.TASK_DB_PATH;
|
|
80
|
+
if (envPath) {
|
|
81
|
+
return resolve(envPath);
|
|
82
|
+
}
|
|
83
|
+
return resolve(process.cwd(), ".tasks", "tasks.db");
|
|
84
|
+
}
|
|
85
|
+
function getDb(dbPath) {
|
|
86
|
+
const resolvedPath = dbPath ? resolve(dbPath) : resolveDefaultPath();
|
|
87
|
+
const cached = instances.get(resolvedPath);
|
|
88
|
+
if (cached) {
|
|
89
|
+
return cached;
|
|
90
|
+
}
|
|
91
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
92
|
+
const db = new Database(resolvedPath);
|
|
93
|
+
db.pragma("journal_mode = WAL");
|
|
94
|
+
db.pragma("foreign_keys = ON");
|
|
95
|
+
initializeSchema(db);
|
|
96
|
+
instances.set(resolvedPath, db);
|
|
97
|
+
return db;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/db/queries.ts
|
|
101
|
+
var TaskQueries = class {
|
|
102
|
+
db;
|
|
103
|
+
constructor(db) {
|
|
104
|
+
this.db = db;
|
|
105
|
+
}
|
|
106
|
+
// ── Task Groups ──────────────────────────────────────────────────
|
|
107
|
+
createGroup(group) {
|
|
108
|
+
const stmt = this.db.prepare(`
|
|
109
|
+
INSERT INTO task_groups (id, title, description, status)
|
|
110
|
+
VALUES (@id, @title, @description, @status)
|
|
111
|
+
`);
|
|
112
|
+
stmt.run(group);
|
|
113
|
+
return this.getGroup(group.id);
|
|
114
|
+
}
|
|
115
|
+
getGroup(id) {
|
|
116
|
+
const stmt = this.db.prepare(`
|
|
117
|
+
SELECT id, title, description, status, created_at
|
|
118
|
+
FROM task_groups WHERE id = ?
|
|
119
|
+
`);
|
|
120
|
+
return stmt.get(id);
|
|
121
|
+
}
|
|
122
|
+
// ── Tasks ────────────────────────────────────────────────────────
|
|
123
|
+
createTask(task) {
|
|
124
|
+
const stmt = this.db.prepare(`
|
|
125
|
+
INSERT INTO tasks (id, group_id, sequence, title, description, status, priority,
|
|
126
|
+
assigned_to, branch_name, worktree_path, progress, progress_note)
|
|
127
|
+
VALUES (@id, @group_id, @sequence, @title, @description, @status, @priority,
|
|
128
|
+
@assigned_to, @branch_name, @worktree_path, @progress, @progress_note)
|
|
129
|
+
`);
|
|
130
|
+
stmt.run({
|
|
131
|
+
id: task.id,
|
|
132
|
+
group_id: task.group_id,
|
|
133
|
+
sequence: task.sequence,
|
|
134
|
+
title: task.title,
|
|
135
|
+
description: task.description,
|
|
136
|
+
status: task.status,
|
|
137
|
+
priority: task.priority,
|
|
138
|
+
assigned_to: task.assigned_to ?? null,
|
|
139
|
+
branch_name: task.branch_name ?? null,
|
|
140
|
+
worktree_path: task.worktree_path ?? null,
|
|
141
|
+
progress: task.progress,
|
|
142
|
+
progress_note: task.progress_note ?? null
|
|
143
|
+
});
|
|
144
|
+
return this.getTask(task.id);
|
|
145
|
+
}
|
|
146
|
+
getTask(id) {
|
|
147
|
+
const stmt = this.db.prepare(`
|
|
148
|
+
SELECT id, group_id, sequence, title, description, status, priority,
|
|
149
|
+
assigned_to, branch_name, worktree_path, progress, progress_note,
|
|
150
|
+
created_at, started_at, completed_at, merged_at
|
|
151
|
+
FROM tasks WHERE id = ?
|
|
152
|
+
`);
|
|
153
|
+
return stmt.get(id);
|
|
154
|
+
}
|
|
155
|
+
getTaskBySequenceAndGroup(groupId, sequence) {
|
|
156
|
+
const stmt = this.db.prepare(`
|
|
157
|
+
SELECT id, group_id, sequence, title, description, status, priority,
|
|
158
|
+
assigned_to, branch_name, worktree_path, progress, progress_note,
|
|
159
|
+
created_at, started_at, completed_at, merged_at
|
|
160
|
+
FROM tasks WHERE group_id = ? AND sequence = ?
|
|
161
|
+
`);
|
|
162
|
+
return stmt.get(groupId, sequence);
|
|
163
|
+
}
|
|
164
|
+
listTasks(opts) {
|
|
165
|
+
let sql = `
|
|
166
|
+
SELECT id, group_id, sequence, title, description, status, priority,
|
|
167
|
+
assigned_to, branch_name, worktree_path, progress, progress_note,
|
|
168
|
+
created_at, started_at, completed_at, merged_at
|
|
169
|
+
FROM tasks
|
|
170
|
+
`;
|
|
171
|
+
const conditions = [];
|
|
172
|
+
const params = [];
|
|
173
|
+
if (opts?.group_id) {
|
|
174
|
+
conditions.push("group_id = ?");
|
|
175
|
+
params.push(opts.group_id);
|
|
176
|
+
}
|
|
177
|
+
if (opts?.status && opts.status.length > 0) {
|
|
178
|
+
const placeholders = opts.status.map(() => "?").join(", ");
|
|
179
|
+
conditions.push(`status IN (${placeholders})`);
|
|
180
|
+
params.push(...opts.status);
|
|
181
|
+
}
|
|
182
|
+
if (conditions.length > 0) {
|
|
183
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
184
|
+
}
|
|
185
|
+
sql += " ORDER BY sequence ASC";
|
|
186
|
+
const stmt = this.db.prepare(sql);
|
|
187
|
+
return stmt.all(...params);
|
|
188
|
+
}
|
|
189
|
+
updateTask(id, updates) {
|
|
190
|
+
const keys = Object.keys(updates).filter(
|
|
191
|
+
(k) => updates[k] !== void 0
|
|
192
|
+
);
|
|
193
|
+
if (keys.length === 0) {
|
|
194
|
+
return this.getTask(id);
|
|
195
|
+
}
|
|
196
|
+
const setClauses = keys.map((k) => `${k} = @${k}`).join(", ");
|
|
197
|
+
const sql = `UPDATE tasks SET ${setClauses} WHERE id = @id`;
|
|
198
|
+
const stmt = this.db.prepare(sql);
|
|
199
|
+
stmt.run({ id, ...updates });
|
|
200
|
+
return this.getTask(id);
|
|
201
|
+
}
|
|
202
|
+
// ── Dependencies ─────────────────────────────────────────────────
|
|
203
|
+
addDependency(taskId, dependsOn) {
|
|
204
|
+
const stmt = this.db.prepare(`
|
|
205
|
+
INSERT OR IGNORE INTO task_dependencies (task_id, depends_on)
|
|
206
|
+
VALUES (?, ?)
|
|
207
|
+
`);
|
|
208
|
+
stmt.run(taskId, dependsOn);
|
|
209
|
+
}
|
|
210
|
+
getDependencies(taskId) {
|
|
211
|
+
const stmt = this.db.prepare(`
|
|
212
|
+
SELECT t.id, t.group_id, t.sequence, t.title, t.description, t.status,
|
|
213
|
+
t.priority, t.assigned_to, t.branch_name, t.worktree_path,
|
|
214
|
+
t.progress, t.progress_note, t.created_at, t.started_at,
|
|
215
|
+
t.completed_at, t.merged_at
|
|
216
|
+
FROM task_dependencies d
|
|
217
|
+
JOIN tasks t ON t.id = d.depends_on
|
|
218
|
+
WHERE d.task_id = ?
|
|
219
|
+
ORDER BY t.sequence ASC
|
|
220
|
+
`);
|
|
221
|
+
return stmt.all(taskId);
|
|
222
|
+
}
|
|
223
|
+
getDependents(taskId) {
|
|
224
|
+
const stmt = this.db.prepare(`
|
|
225
|
+
SELECT t.id, t.group_id, t.sequence, t.title, t.description, t.status,
|
|
226
|
+
t.priority, t.assigned_to, t.branch_name, t.worktree_path,
|
|
227
|
+
t.progress, t.progress_note, t.created_at, t.started_at,
|
|
228
|
+
t.completed_at, t.merged_at
|
|
229
|
+
FROM task_dependencies d
|
|
230
|
+
JOIN tasks t ON t.id = d.task_id
|
|
231
|
+
WHERE d.depends_on = ?
|
|
232
|
+
ORDER BY t.sequence ASC
|
|
233
|
+
`);
|
|
234
|
+
return stmt.all(taskId);
|
|
235
|
+
}
|
|
236
|
+
// ── File Ownership ───────────────────────────────────────────────
|
|
237
|
+
addFileOwnership(ownership) {
|
|
238
|
+
const stmt = this.db.prepare(`
|
|
239
|
+
INSERT OR REPLACE INTO task_file_ownership (task_id, file_pattern, ownership_type)
|
|
240
|
+
VALUES (@task_id, @file_pattern, @ownership_type)
|
|
241
|
+
`);
|
|
242
|
+
stmt.run(ownership);
|
|
243
|
+
}
|
|
244
|
+
getFileOwnership(taskId) {
|
|
245
|
+
const stmt = this.db.prepare(`
|
|
246
|
+
SELECT task_id, file_pattern, ownership_type
|
|
247
|
+
FROM task_file_ownership WHERE task_id = ?
|
|
248
|
+
`);
|
|
249
|
+
return stmt.all(taskId);
|
|
250
|
+
}
|
|
251
|
+
getFileOwnershipConflicts(taskId) {
|
|
252
|
+
const stmt = this.db.prepare(`
|
|
253
|
+
SELECT t.id, t.group_id, t.sequence, t.title, t.description, t.status,
|
|
254
|
+
t.priority, t.assigned_to, t.branch_name, t.worktree_path,
|
|
255
|
+
t.progress, t.progress_note, t.created_at, t.started_at,
|
|
256
|
+
t.completed_at, t.merged_at,
|
|
257
|
+
other_fo.file_pattern AS pattern,
|
|
258
|
+
other_fo.ownership_type AS ownership_type
|
|
259
|
+
FROM task_file_ownership my_fo
|
|
260
|
+
JOIN task_file_ownership other_fo ON my_fo.file_pattern = other_fo.file_pattern
|
|
261
|
+
JOIN tasks t ON t.id = other_fo.task_id
|
|
262
|
+
WHERE my_fo.task_id = ?
|
|
263
|
+
AND other_fo.task_id != ?
|
|
264
|
+
AND t.status = 'in_progress'
|
|
265
|
+
`);
|
|
266
|
+
const rows = stmt.all(taskId, taskId);
|
|
267
|
+
return rows.map((row) => {
|
|
268
|
+
const { pattern, ownership_type, ...taskFields } = row;
|
|
269
|
+
return {
|
|
270
|
+
task: taskFields,
|
|
271
|
+
pattern,
|
|
272
|
+
ownership_type
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// ── Progress Logs ────────────────────────────────────────────────
|
|
277
|
+
addProgressLog(log) {
|
|
278
|
+
const stmt = this.db.prepare(`
|
|
279
|
+
INSERT INTO progress_logs (id, task_id, event, message, metadata)
|
|
280
|
+
VALUES (@id, @task_id, @event, @message, @metadata)
|
|
281
|
+
`);
|
|
282
|
+
stmt.run({
|
|
283
|
+
id: log.id,
|
|
284
|
+
task_id: log.task_id,
|
|
285
|
+
event: log.event,
|
|
286
|
+
message: log.message,
|
|
287
|
+
metadata: log.metadata ? JSON.stringify(log.metadata) : null
|
|
288
|
+
});
|
|
289
|
+
return this.getProgressLog(log.id);
|
|
290
|
+
}
|
|
291
|
+
getProgressLogs(taskId) {
|
|
292
|
+
const stmt = this.db.prepare(`
|
|
293
|
+
SELECT id, task_id, timestamp, event, message, metadata
|
|
294
|
+
FROM progress_logs WHERE task_id = ?
|
|
295
|
+
ORDER BY timestamp ASC
|
|
296
|
+
`);
|
|
297
|
+
const rows = stmt.all(taskId);
|
|
298
|
+
return rows.map((row) => ({
|
|
299
|
+
...row,
|
|
300
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
304
|
+
getProgressLog(id) {
|
|
305
|
+
const stmt = this.db.prepare(`
|
|
306
|
+
SELECT id, task_id, timestamp, event, message, metadata
|
|
307
|
+
FROM progress_logs WHERE id = ?
|
|
308
|
+
`);
|
|
309
|
+
const row = stmt.get(id);
|
|
310
|
+
if (!row) return void 0;
|
|
311
|
+
return {
|
|
312
|
+
...row,
|
|
313
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/services/dag-service.ts
|
|
319
|
+
var DagService = class {
|
|
320
|
+
/**
|
|
321
|
+
* Validate that adding dependencies won't create a cycle.
|
|
322
|
+
* Uses DFS-based cycle detection.
|
|
323
|
+
*/
|
|
324
|
+
validateNoCycles(dependencies) {
|
|
325
|
+
const WHITE = 0;
|
|
326
|
+
const GRAY = 1;
|
|
327
|
+
const BLACK = 2;
|
|
328
|
+
const color = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const [node, deps] of dependencies) {
|
|
330
|
+
color.set(node, WHITE);
|
|
331
|
+
for (const dep of deps) {
|
|
332
|
+
if (!color.has(dep)) {
|
|
333
|
+
color.set(dep, WHITE);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const parent = /* @__PURE__ */ new Map();
|
|
338
|
+
const dfs = (node) => {
|
|
339
|
+
color.set(node, GRAY);
|
|
340
|
+
const neighbors = dependencies.get(node) ?? [];
|
|
341
|
+
for (const neighbor of neighbors) {
|
|
342
|
+
const neighborColor = color.get(neighbor) ?? WHITE;
|
|
343
|
+
if (neighborColor === GRAY) {
|
|
344
|
+
const cycle = [neighbor, node];
|
|
345
|
+
let current = node;
|
|
346
|
+
while (parent.get(current) !== null && parent.get(current) !== neighbor) {
|
|
347
|
+
current = parent.get(current);
|
|
348
|
+
cycle.push(current);
|
|
349
|
+
}
|
|
350
|
+
cycle.reverse();
|
|
351
|
+
return cycle;
|
|
352
|
+
}
|
|
353
|
+
if (neighborColor === WHITE) {
|
|
354
|
+
parent.set(neighbor, node);
|
|
355
|
+
const result = dfs(neighbor);
|
|
356
|
+
if (result) return result;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
color.set(node, BLACK);
|
|
360
|
+
return null;
|
|
361
|
+
};
|
|
362
|
+
for (const node of color.keys()) {
|
|
363
|
+
if (color.get(node) === WHITE) {
|
|
364
|
+
parent.set(node, null);
|
|
365
|
+
const cycle = dfs(node);
|
|
366
|
+
if (cycle) {
|
|
367
|
+
return { valid: false, cycle };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { valid: true };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get topological order of tasks using Kahn's algorithm.
|
|
375
|
+
*/
|
|
376
|
+
topologicalSort(dependencies) {
|
|
377
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
378
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const [node, deps] of dependencies) {
|
|
380
|
+
if (!inDegree.has(node)) {
|
|
381
|
+
inDegree.set(node, 0);
|
|
382
|
+
}
|
|
383
|
+
if (!adjacency.has(node)) {
|
|
384
|
+
adjacency.set(node, []);
|
|
385
|
+
}
|
|
386
|
+
for (const dep of deps) {
|
|
387
|
+
if (!inDegree.has(dep)) {
|
|
388
|
+
inDegree.set(dep, 0);
|
|
389
|
+
}
|
|
390
|
+
if (!adjacency.has(dep)) {
|
|
391
|
+
adjacency.set(dep, []);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (const [node, deps] of dependencies) {
|
|
396
|
+
inDegree.set(node, (inDegree.get(node) ?? 0) + deps.length);
|
|
397
|
+
for (const dep of deps) {
|
|
398
|
+
const adj = adjacency.get(dep) ?? [];
|
|
399
|
+
adj.push(node);
|
|
400
|
+
adjacency.set(dep, adj);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const queue = [];
|
|
404
|
+
for (const [node, degree] of inDegree) {
|
|
405
|
+
if (degree === 0) {
|
|
406
|
+
queue.push(node);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const result = [];
|
|
410
|
+
while (queue.length > 0) {
|
|
411
|
+
const node = queue.shift();
|
|
412
|
+
result.push(node);
|
|
413
|
+
const neighbors = adjacency.get(node) ?? [];
|
|
414
|
+
for (const neighbor of neighbors) {
|
|
415
|
+
const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
|
|
416
|
+
inDegree.set(neighbor, newDegree);
|
|
417
|
+
if (newDegree === 0) {
|
|
418
|
+
queue.push(neighbor);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (result.length !== inDegree.size) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"Cannot perform topological sort: graph contains a cycle"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Check if a task can start (all dependencies completed).
|
|
431
|
+
*/
|
|
432
|
+
canStart(taskId, dependencies, completedTasks) {
|
|
433
|
+
const deps = dependencies.get(taskId) ?? [];
|
|
434
|
+
return deps.every((dep) => completedTasks.has(dep));
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get all tasks that would be unlocked if a given task is completed.
|
|
438
|
+
* A task is unlocked when ALL of its dependencies are in the completed set.
|
|
439
|
+
*/
|
|
440
|
+
getUnlockedTasks(completedTaskId, allDependencies, completedTasks) {
|
|
441
|
+
const newCompleted = new Set(completedTasks);
|
|
442
|
+
newCompleted.add(completedTaskId);
|
|
443
|
+
const unlocked = [];
|
|
444
|
+
for (const [taskId, deps] of allDependencies) {
|
|
445
|
+
if (newCompleted.has(taskId)) continue;
|
|
446
|
+
if (!deps.includes(completedTaskId)) continue;
|
|
447
|
+
if (deps.every((dep) => newCompleted.has(dep))) {
|
|
448
|
+
unlocked.push(taskId);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return unlocked;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
var dagService = new DagService();
|
|
455
|
+
|
|
456
|
+
// src/services/git-service.ts
|
|
457
|
+
import { execSync } from "child_process";
|
|
458
|
+
var GitService = class {
|
|
459
|
+
constructor(repoRoot) {
|
|
460
|
+
this.repoRoot = repoRoot;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Get the repo root directory.
|
|
464
|
+
*/
|
|
465
|
+
static getRepoRoot() {
|
|
466
|
+
try {
|
|
467
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
468
|
+
encoding: "utf-8",
|
|
469
|
+
stdio: "pipe"
|
|
470
|
+
}).trim();
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`Failed to determine git repo root: ${error instanceof Error ? error.message : String(error)}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Create a new worktree with a new branch.
|
|
479
|
+
*/
|
|
480
|
+
createWorktree(worktreePath, branchName) {
|
|
481
|
+
try {
|
|
482
|
+
execSync(`git worktree add "${worktreePath}" -b "${branchName}"`, {
|
|
483
|
+
cwd: this.repoRoot,
|
|
484
|
+
encoding: "utf-8",
|
|
485
|
+
stdio: "pipe"
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Failed to create worktree at ${worktreePath} with branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Remove a worktree.
|
|
495
|
+
*/
|
|
496
|
+
removeWorktree(worktreePath) {
|
|
497
|
+
try {
|
|
498
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
499
|
+
cwd: this.repoRoot,
|
|
500
|
+
encoding: "utf-8",
|
|
501
|
+
stdio: "pipe"
|
|
502
|
+
});
|
|
503
|
+
} catch (error) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
`Failed to remove worktree at ${worktreePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Delete a branch.
|
|
511
|
+
*/
|
|
512
|
+
deleteBranch(branchName) {
|
|
513
|
+
try {
|
|
514
|
+
execSync(`git branch -D "${branchName}"`, {
|
|
515
|
+
cwd: this.repoRoot,
|
|
516
|
+
encoding: "utf-8",
|
|
517
|
+
stdio: "pipe"
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Failed to delete branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get current branch name.
|
|
527
|
+
*/
|
|
528
|
+
getCurrentBranch() {
|
|
529
|
+
try {
|
|
530
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
531
|
+
cwd: this.repoRoot,
|
|
532
|
+
encoding: "utf-8",
|
|
533
|
+
stdio: "pipe"
|
|
534
|
+
}).trim();
|
|
535
|
+
} catch (error) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Failed to get current branch: ${error instanceof Error ? error.message : String(error)}`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Check if we're on the main/master branch.
|
|
543
|
+
*/
|
|
544
|
+
isOnMainBranch() {
|
|
545
|
+
const branch = this.getCurrentBranch();
|
|
546
|
+
return branch === "main" || branch === "master";
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get the latest commit hash on a branch.
|
|
550
|
+
*/
|
|
551
|
+
getLatestCommit(branch) {
|
|
552
|
+
try {
|
|
553
|
+
const ref = branch ?? "HEAD";
|
|
554
|
+
return execSync(`git rev-parse "${ref}"`, {
|
|
555
|
+
cwd: this.repoRoot,
|
|
556
|
+
encoding: "utf-8",
|
|
557
|
+
stdio: "pipe"
|
|
558
|
+
}).trim();
|
|
559
|
+
} catch (error) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Failed to get latest commit${branch ? ` for branch ${branch}` : ""}: ${error instanceof Error ? error.message : String(error)}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Check if a worktree path exists in the list of worktrees.
|
|
567
|
+
*/
|
|
568
|
+
worktreeExists(worktreePath) {
|
|
569
|
+
try {
|
|
570
|
+
const output = execSync("git worktree list --porcelain", {
|
|
571
|
+
cwd: this.repoRoot,
|
|
572
|
+
encoding: "utf-8",
|
|
573
|
+
stdio: "pipe"
|
|
574
|
+
});
|
|
575
|
+
return output.includes(worktreePath);
|
|
576
|
+
} catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Merge a branch (squash or regular).
|
|
582
|
+
* Returns success status and any conflicted files.
|
|
583
|
+
*/
|
|
584
|
+
mergeBranch(branchName, strategy) {
|
|
585
|
+
try {
|
|
586
|
+
const cmd = strategy === "squash" ? `git merge --squash "${branchName}"` : `git merge "${branchName}" --no-edit`;
|
|
587
|
+
execSync(cmd, {
|
|
588
|
+
cwd: this.repoRoot,
|
|
589
|
+
encoding: "utf-8",
|
|
590
|
+
stdio: "pipe"
|
|
591
|
+
});
|
|
592
|
+
return { success: true, conflicts: [] };
|
|
593
|
+
} catch {
|
|
594
|
+
const conflicts = this.getConflictedFiles();
|
|
595
|
+
if (conflicts.length > 0) {
|
|
596
|
+
return { success: false, conflicts };
|
|
597
|
+
}
|
|
598
|
+
throw new Error(`Failed to merge branch ${branchName}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Abort a merge in progress.
|
|
603
|
+
*/
|
|
604
|
+
abortMerge() {
|
|
605
|
+
try {
|
|
606
|
+
execSync("git merge --abort", {
|
|
607
|
+
cwd: this.repoRoot,
|
|
608
|
+
encoding: "utf-8",
|
|
609
|
+
stdio: "pipe"
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Failed to abort merge: ${error instanceof Error ? error.message : String(error)}`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Get list of conflicted files.
|
|
619
|
+
*/
|
|
620
|
+
getConflictedFiles() {
|
|
621
|
+
try {
|
|
622
|
+
const output = execSync("git diff --name-only --diff-filter=U", {
|
|
623
|
+
cwd: this.repoRoot,
|
|
624
|
+
encoding: "utf-8",
|
|
625
|
+
stdio: "pipe"
|
|
626
|
+
});
|
|
627
|
+
return output.trim().split("\n").filter((f) => f.length > 0);
|
|
628
|
+
} catch {
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Check if main branch has new commits since a given commit hash.
|
|
634
|
+
*/
|
|
635
|
+
hasNewCommitsSince(commitHash) {
|
|
636
|
+
try {
|
|
637
|
+
let mainBranch = "main";
|
|
638
|
+
try {
|
|
639
|
+
execSync("git rev-parse --verify main", {
|
|
640
|
+
cwd: this.repoRoot,
|
|
641
|
+
encoding: "utf-8",
|
|
642
|
+
stdio: "pipe"
|
|
643
|
+
});
|
|
644
|
+
} catch {
|
|
645
|
+
mainBranch = "master";
|
|
646
|
+
}
|
|
647
|
+
const output = execSync(
|
|
648
|
+
`git log --oneline "${commitHash}..${mainBranch}"`,
|
|
649
|
+
{
|
|
650
|
+
cwd: this.repoRoot,
|
|
651
|
+
encoding: "utf-8",
|
|
652
|
+
stdio: "pipe"
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
return output.trim().length > 0;
|
|
656
|
+
} catch {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
function createGitService(repoRoot) {
|
|
662
|
+
const root = repoRoot ?? GitService.getRepoRoot();
|
|
663
|
+
return new GitService(root);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/services/conflict-service.ts
|
|
667
|
+
var ConflictService = class {
|
|
668
|
+
/**
|
|
669
|
+
* Check if two glob patterns could overlap.
|
|
670
|
+
* Simple heuristic: check if one pattern is a prefix of another
|
|
671
|
+
* after removing glob suffixes like **, *.
|
|
672
|
+
*/
|
|
673
|
+
patternsOverlap(pattern1, pattern2) {
|
|
674
|
+
const normalize = (p) => p.replace(/\*\*\/?/g, "").replace(/\*/g, "").replace(/\/+$/, "");
|
|
675
|
+
const base1 = normalize(pattern1);
|
|
676
|
+
const base2 = normalize(pattern2);
|
|
677
|
+
if (base1.length === 0 || base2.length === 0) {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
return base1.startsWith(base2) || base2.startsWith(base1);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Find potential conflicts between a task's file patterns and other in-progress tasks.
|
|
684
|
+
* Only exclusive patterns cause conflicts.
|
|
685
|
+
*/
|
|
686
|
+
findConflicts(taskPatterns, otherTasks) {
|
|
687
|
+
const conflicts = [];
|
|
688
|
+
for (const myOwnership of taskPatterns) {
|
|
689
|
+
for (const other of otherTasks) {
|
|
690
|
+
for (const otherOwnership of other.patterns) {
|
|
691
|
+
if ((myOwnership.ownership_type === "exclusive" || otherOwnership.ownership_type === "exclusive") && this.patternsOverlap(
|
|
692
|
+
myOwnership.file_pattern,
|
|
693
|
+
otherOwnership.file_pattern
|
|
694
|
+
)) {
|
|
695
|
+
conflicts.push({
|
|
696
|
+
task: other.task,
|
|
697
|
+
conflicting_pattern: otherOwnership.file_pattern,
|
|
698
|
+
ownership_type: otherOwnership.ownership_type
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return conflicts;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Check if a specific file matches any of the given patterns.
|
|
708
|
+
* Uses simple string matching: startsWith for directory patterns (ending with ** or *).
|
|
709
|
+
*/
|
|
710
|
+
fileMatchesPatterns(filePath, patterns) {
|
|
711
|
+
for (const pattern of patterns) {
|
|
712
|
+
const base = pattern.replace(/\*\*\/?$/, "").replace(/\*$/, "");
|
|
713
|
+
if (base.length === 0) {
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
if (filePath.startsWith(base)) {
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
if (filePath === pattern) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Check changed files against other tasks' exclusive patterns.
|
|
727
|
+
* Returns warning messages for any files that conflict.
|
|
728
|
+
*/
|
|
729
|
+
checkFileConflicts(changedFiles, otherTasks) {
|
|
730
|
+
const warnings = [];
|
|
731
|
+
for (const file of changedFiles) {
|
|
732
|
+
for (const other of otherTasks) {
|
|
733
|
+
const exclusivePatterns = other.patterns.filter((p) => p.ownership_type === "exclusive").map((p) => p.file_pattern);
|
|
734
|
+
if (this.fileMatchesPatterns(file, exclusivePatterns)) {
|
|
735
|
+
warnings.push(
|
|
736
|
+
`File "${file}" conflicts with task #${other.task.sequence} "${other.task.title}" which has exclusive ownership`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return warnings;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
var conflictService = new ConflictService();
|
|
745
|
+
|
|
746
|
+
// src/services/task-service.ts
|
|
747
|
+
import { v4 as uuidv4 } from "uuid";
|
|
748
|
+
import slugify from "slugify";
|
|
749
|
+
import { resolve as resolve2 } from "path";
|
|
750
|
+
var TaskService = class {
|
|
751
|
+
queries;
|
|
752
|
+
dagService;
|
|
753
|
+
gitService;
|
|
754
|
+
conflictService;
|
|
755
|
+
constructor(db, gitRepoRoot) {
|
|
756
|
+
this.queries = new TaskQueries(db);
|
|
757
|
+
this.dagService = dagService;
|
|
758
|
+
this.conflictService = conflictService;
|
|
759
|
+
if (gitRepoRoot) {
|
|
760
|
+
this.gitService = new GitService(gitRepoRoot);
|
|
761
|
+
} else {
|
|
762
|
+
try {
|
|
763
|
+
this.gitService = createGitService();
|
|
764
|
+
} catch {
|
|
765
|
+
this.gitService = new GitService(process.cwd());
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// === Task Management ===
|
|
770
|
+
/**
|
|
771
|
+
* Create a task group with tasks, dependencies, and file ownership.
|
|
772
|
+
*/
|
|
773
|
+
createTasks(input) {
|
|
774
|
+
const groupId = uuidv4();
|
|
775
|
+
const warnings = [];
|
|
776
|
+
this.queries.createGroup({
|
|
777
|
+
id: groupId,
|
|
778
|
+
title: input.group_title,
|
|
779
|
+
description: input.group_description,
|
|
780
|
+
status: "active"
|
|
781
|
+
});
|
|
782
|
+
const sequenceToIdMap = /* @__PURE__ */ new Map();
|
|
783
|
+
const createdTasks = [];
|
|
784
|
+
for (let i = 0; i < input.tasks.length; i++) {
|
|
785
|
+
const taskInput = input.tasks[i];
|
|
786
|
+
const sequence = i + 1;
|
|
787
|
+
const taskId = uuidv4();
|
|
788
|
+
sequenceToIdMap.set(sequence, taskId);
|
|
789
|
+
const task = this.queries.createTask({
|
|
790
|
+
id: taskId,
|
|
791
|
+
group_id: groupId,
|
|
792
|
+
sequence,
|
|
793
|
+
title: taskInput.title,
|
|
794
|
+
description: taskInput.description,
|
|
795
|
+
status: "pending",
|
|
796
|
+
// Will be updated after dependency analysis
|
|
797
|
+
priority: taskInput.priority ?? "medium",
|
|
798
|
+
assigned_to: null,
|
|
799
|
+
branch_name: null,
|
|
800
|
+
worktree_path: null,
|
|
801
|
+
progress: 0,
|
|
802
|
+
progress_note: null
|
|
803
|
+
});
|
|
804
|
+
createdTasks.push(task);
|
|
805
|
+
}
|
|
806
|
+
const dependencyMap = /* @__PURE__ */ new Map();
|
|
807
|
+
for (let i = 0; i < input.tasks.length; i++) {
|
|
808
|
+
const taskInput = input.tasks[i];
|
|
809
|
+
const sequence = i + 1;
|
|
810
|
+
const taskId = sequenceToIdMap.get(sequence);
|
|
811
|
+
const deps = [];
|
|
812
|
+
if (taskInput.depends_on && taskInput.depends_on.length > 0) {
|
|
813
|
+
for (const depSequence of taskInput.depends_on) {
|
|
814
|
+
const depId = sequenceToIdMap.get(depSequence);
|
|
815
|
+
if (depId) {
|
|
816
|
+
this.queries.addDependency(taskId, depId);
|
|
817
|
+
deps.push(depId);
|
|
818
|
+
} else {
|
|
819
|
+
warnings.push(
|
|
820
|
+
`Task #${sequence} "${taskInput.title}" references invalid dependency sequence #${depSequence}`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
dependencyMap.set(taskId, deps);
|
|
826
|
+
}
|
|
827
|
+
for (let i = 0; i < input.tasks.length; i++) {
|
|
828
|
+
const taskInput = input.tasks[i];
|
|
829
|
+
const sequence = i + 1;
|
|
830
|
+
const taskId = sequenceToIdMap.get(sequence);
|
|
831
|
+
if (taskInput.file_patterns) {
|
|
832
|
+
for (const fp of taskInput.file_patterns) {
|
|
833
|
+
this.queries.addFileOwnership({
|
|
834
|
+
task_id: taskId,
|
|
835
|
+
file_pattern: fp.pattern,
|
|
836
|
+
ownership_type: fp.ownership_type
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const validation = this.dagService.validateNoCycles(dependencyMap);
|
|
842
|
+
if (!validation.valid) {
|
|
843
|
+
warnings.push(
|
|
844
|
+
`Dependency cycle detected: ${validation.cycle?.join(" -> ")}`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
for (let i = 0; i < input.tasks.length; i++) {
|
|
848
|
+
const taskInputI = input.tasks[i];
|
|
849
|
+
const seqI = i + 1;
|
|
850
|
+
const taskIdI = sequenceToIdMap.get(seqI);
|
|
851
|
+
if (!taskInputI.file_patterns) continue;
|
|
852
|
+
for (let j = i + 1; j < input.tasks.length; j++) {
|
|
853
|
+
const taskInputJ = input.tasks[j];
|
|
854
|
+
const seqJ = j + 1;
|
|
855
|
+
if (!taskInputJ.file_patterns) continue;
|
|
856
|
+
for (const fpI of taskInputI.file_patterns) {
|
|
857
|
+
for (const fpJ of taskInputJ.file_patterns) {
|
|
858
|
+
if ((fpI.ownership_type === "exclusive" || fpJ.ownership_type === "exclusive") && this.conflictService.patternsOverlap(fpI.pattern, fpJ.pattern)) {
|
|
859
|
+
warnings.push(
|
|
860
|
+
`File pattern overlap: task #${seqI} "${taskInputI.title}" (${fpI.pattern}) and task #${seqJ} "${taskInputJ.title}" (${fpJ.pattern})`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const completedTasks = /* @__PURE__ */ new Set();
|
|
868
|
+
const outputTasks = [];
|
|
869
|
+
for (let i = 0; i < createdTasks.length; i++) {
|
|
870
|
+
const sequence = i + 1;
|
|
871
|
+
const taskId = sequenceToIdMap.get(sequence);
|
|
872
|
+
const deps = dependencyMap.get(taskId) ?? [];
|
|
873
|
+
const hasDeps = deps.length > 0;
|
|
874
|
+
if (hasDeps) {
|
|
875
|
+
this.queries.updateTask(taskId, { status: "blocked" });
|
|
876
|
+
}
|
|
877
|
+
const canStart = this.dagService.canStart(
|
|
878
|
+
taskId,
|
|
879
|
+
dependencyMap,
|
|
880
|
+
completedTasks
|
|
881
|
+
);
|
|
882
|
+
outputTasks.push({
|
|
883
|
+
id: taskId,
|
|
884
|
+
sequence,
|
|
885
|
+
title: createdTasks[i].title,
|
|
886
|
+
status: hasDeps ? "blocked" : "pending",
|
|
887
|
+
can_start: canStart
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
group_id: groupId,
|
|
892
|
+
tasks: outputTasks,
|
|
893
|
+
warnings
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* List tasks with computed can_start and summary.
|
|
898
|
+
*/
|
|
899
|
+
listTasks(input) {
|
|
900
|
+
const tasks = this.queries.listTasks({
|
|
901
|
+
group_id: input.group_id,
|
|
902
|
+
status: input.status
|
|
903
|
+
});
|
|
904
|
+
const dependencyMap = /* @__PURE__ */ new Map();
|
|
905
|
+
const completedTasks = /* @__PURE__ */ new Set();
|
|
906
|
+
for (const task of tasks) {
|
|
907
|
+
const deps = this.queries.getDependencies(task.id);
|
|
908
|
+
dependencyMap.set(
|
|
909
|
+
task.id,
|
|
910
|
+
deps.map((d) => d.id)
|
|
911
|
+
);
|
|
912
|
+
if (task.status === "completed") {
|
|
913
|
+
completedTasks.add(task.id);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const outputTasks = tasks.map((task) => {
|
|
917
|
+
const deps = this.queries.getDependencies(task.id);
|
|
918
|
+
const canStart = task.status === "pending" && this.dagService.canStart(task.id, dependencyMap, completedTasks);
|
|
919
|
+
return {
|
|
920
|
+
id: task.id,
|
|
921
|
+
sequence: task.sequence,
|
|
922
|
+
title: task.title,
|
|
923
|
+
status: task.status,
|
|
924
|
+
progress: task.progress,
|
|
925
|
+
progress_note: task.progress_note,
|
|
926
|
+
assigned_to: task.assigned_to,
|
|
927
|
+
branch_name: task.branch_name,
|
|
928
|
+
worktree_path: task.worktree_path,
|
|
929
|
+
dependencies: deps.map((d) => ({
|
|
930
|
+
sequence: d.sequence,
|
|
931
|
+
title: d.title,
|
|
932
|
+
status: d.status
|
|
933
|
+
})),
|
|
934
|
+
can_start: canStart
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
const summary = {
|
|
938
|
+
total: tasks.length,
|
|
939
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
940
|
+
in_progress: tasks.filter((t) => t.status === "in_progress").length,
|
|
941
|
+
in_review: tasks.filter((t) => t.status === "in_review").length,
|
|
942
|
+
completed: tasks.filter((t) => t.status === "completed").length,
|
|
943
|
+
blocked: tasks.filter((t) => t.status === "blocked").length
|
|
944
|
+
};
|
|
945
|
+
return { tasks: outputTasks, summary };
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Get a task with all related data.
|
|
949
|
+
*/
|
|
950
|
+
getTask(input) {
|
|
951
|
+
const task = this.queries.getTask(input.task_id);
|
|
952
|
+
if (!task) {
|
|
953
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
954
|
+
}
|
|
955
|
+
const deps = this.queries.getDependencies(task.id);
|
|
956
|
+
const fileOwnership = this.queries.getFileOwnership(task.id);
|
|
957
|
+
const progressLogs = this.queries.getProgressLogs(task.id);
|
|
958
|
+
return {
|
|
959
|
+
task,
|
|
960
|
+
dependencies: deps.map((d) => ({
|
|
961
|
+
sequence: d.sequence,
|
|
962
|
+
title: d.title,
|
|
963
|
+
status: d.status
|
|
964
|
+
})),
|
|
965
|
+
file_ownership: fileOwnership,
|
|
966
|
+
progress_logs: progressLogs
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
// === Agent Workflow ===
|
|
970
|
+
/**
|
|
971
|
+
* Claim a task for an agent. Uses a transaction for atomicity.
|
|
972
|
+
*/
|
|
973
|
+
claimTask(input) {
|
|
974
|
+
const task = this.queries.getTask(input.task_id);
|
|
975
|
+
if (!task) {
|
|
976
|
+
return {
|
|
977
|
+
success: false,
|
|
978
|
+
task: null,
|
|
979
|
+
error: `Task not found: ${input.task_id}`
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
if (task.status !== "pending") {
|
|
983
|
+
return {
|
|
984
|
+
success: false,
|
|
985
|
+
task,
|
|
986
|
+
error: `Task is not pending (current status: ${task.status})`
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const deps = this.queries.getDependencies(task.id);
|
|
990
|
+
const unmetDeps = deps.filter((d) => d.status !== "completed");
|
|
991
|
+
if (unmetDeps.length > 0) {
|
|
992
|
+
return {
|
|
993
|
+
success: false,
|
|
994
|
+
task,
|
|
995
|
+
error: `Unmet dependencies: ${unmetDeps.map((d) => `#${d.sequence} "${d.title}" (${d.status})`).join(", ")}`
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const conflicts = this.queries.getFileOwnershipConflicts(task.id);
|
|
999
|
+
if (conflicts.length > 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
success: false,
|
|
1002
|
+
task,
|
|
1003
|
+
error: `File ownership conflicts with in-progress tasks: ${conflicts.map((c) => `#${c.task.sequence} "${c.task.title}" on pattern "${c.pattern}"`).join(", ")}`
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
const agentId = input.agent_id ?? `agent-${uuidv4().slice(0, 8)}`;
|
|
1007
|
+
const updatedTask = this.queries.updateTask(task.id, {
|
|
1008
|
+
status: "assigned",
|
|
1009
|
+
assigned_to: agentId
|
|
1010
|
+
});
|
|
1011
|
+
this.queries.addProgressLog({
|
|
1012
|
+
id: uuidv4(),
|
|
1013
|
+
task_id: task.id,
|
|
1014
|
+
event: "claimed",
|
|
1015
|
+
message: `Task claimed by ${agentId}`,
|
|
1016
|
+
metadata: { agent_id: agentId }
|
|
1017
|
+
});
|
|
1018
|
+
return {
|
|
1019
|
+
success: true,
|
|
1020
|
+
task: updatedTask
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Start a task: create git worktree, update task state.
|
|
1025
|
+
*/
|
|
1026
|
+
startTask(input) {
|
|
1027
|
+
const task = this.queries.getTask(input.task_id);
|
|
1028
|
+
if (!task) {
|
|
1029
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (task.status !== "assigned") {
|
|
1032
|
+
throw new Error(
|
|
1033
|
+
`Task must be 'assigned' to start (current status: ${task.status})`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
const slugifiedTitle = slugify(task.title, {
|
|
1037
|
+
lower: true,
|
|
1038
|
+
strict: true
|
|
1039
|
+
}).slice(0, 30);
|
|
1040
|
+
const branchName = `task/task-${task.sequence}-${slugifiedTitle}`;
|
|
1041
|
+
const worktreePath = resolve2(
|
|
1042
|
+
this.gitService["repoRoot"],
|
|
1043
|
+
".worktrees",
|
|
1044
|
+
`task-${task.sequence}-${slugifiedTitle}`
|
|
1045
|
+
);
|
|
1046
|
+
this.gitService.createWorktree(worktreePath, branchName);
|
|
1047
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1048
|
+
const updatedTask = this.queries.updateTask(task.id, {
|
|
1049
|
+
status: "in_progress",
|
|
1050
|
+
branch_name: branchName,
|
|
1051
|
+
worktree_path: worktreePath,
|
|
1052
|
+
started_at: now
|
|
1053
|
+
});
|
|
1054
|
+
this.queries.addProgressLog({
|
|
1055
|
+
id: uuidv4(),
|
|
1056
|
+
task_id: task.id,
|
|
1057
|
+
event: "started",
|
|
1058
|
+
message: `Task started with branch ${branchName}`,
|
|
1059
|
+
metadata: {
|
|
1060
|
+
branch_name: branchName,
|
|
1061
|
+
worktree_path: worktreePath
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
const deps = this.queries.getDependencies(task.id);
|
|
1065
|
+
const fileOwnership = this.queries.getFileOwnership(task.id);
|
|
1066
|
+
return {
|
|
1067
|
+
success: true,
|
|
1068
|
+
worktree_path: worktreePath,
|
|
1069
|
+
branch_name: branchName,
|
|
1070
|
+
task: updatedTask,
|
|
1071
|
+
context: {
|
|
1072
|
+
description: task.description,
|
|
1073
|
+
file_patterns: fileOwnership.map((fo) => fo.file_pattern),
|
|
1074
|
+
dependencies_completed: deps.filter((d) => d.status === "completed").map((d) => ({
|
|
1075
|
+
title: d.title,
|
|
1076
|
+
branch_name: d.branch_name ?? ""
|
|
1077
|
+
}))
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Update progress on a task.
|
|
1083
|
+
*/
|
|
1084
|
+
updateProgress(input) {
|
|
1085
|
+
const task = this.queries.getTask(input.task_id);
|
|
1086
|
+
if (!task) {
|
|
1087
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
1088
|
+
}
|
|
1089
|
+
this.queries.updateTask(task.id, {
|
|
1090
|
+
progress: input.progress,
|
|
1091
|
+
progress_note: input.note
|
|
1092
|
+
});
|
|
1093
|
+
let conflictWarnings = [];
|
|
1094
|
+
if (input.files_changed && input.files_changed.length > 0) {
|
|
1095
|
+
const allTasks = this.queries.listTasks({
|
|
1096
|
+
group_id: task.group_id,
|
|
1097
|
+
status: ["in_progress"]
|
|
1098
|
+
});
|
|
1099
|
+
const otherTasks = allTasks.filter((t) => t.id !== task.id).map((t) => ({
|
|
1100
|
+
task: t,
|
|
1101
|
+
patterns: this.queries.getFileOwnership(t.id)
|
|
1102
|
+
}));
|
|
1103
|
+
conflictWarnings = this.conflictService.checkFileConflicts(
|
|
1104
|
+
input.files_changed,
|
|
1105
|
+
otherTasks
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
let rebaseRecommended = false;
|
|
1109
|
+
if (task.branch_name) {
|
|
1110
|
+
try {
|
|
1111
|
+
const mainCommit = this.gitService.getLatestCommit("HEAD");
|
|
1112
|
+
rebaseRecommended = this.gitService.hasNewCommitsSince(mainCommit);
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
this.queries.addProgressLog({
|
|
1117
|
+
id: uuidv4(),
|
|
1118
|
+
task_id: task.id,
|
|
1119
|
+
event: "progress_update",
|
|
1120
|
+
message: input.note,
|
|
1121
|
+
metadata: {
|
|
1122
|
+
progress: input.progress,
|
|
1123
|
+
files_changed: input.files_changed ?? [],
|
|
1124
|
+
conflict_warnings: conflictWarnings
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
return {
|
|
1128
|
+
success: true,
|
|
1129
|
+
conflict_warnings: conflictWarnings,
|
|
1130
|
+
rebase_recommended: rebaseRecommended
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Mark a task as complete and find unlocked downstream tasks.
|
|
1135
|
+
*/
|
|
1136
|
+
completeTask(input) {
|
|
1137
|
+
const task = this.queries.getTask(input.task_id);
|
|
1138
|
+
if (!task) {
|
|
1139
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
1140
|
+
}
|
|
1141
|
+
if (task.status !== "in_progress") {
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
`Task must be 'in_progress' to complete (current status: ${task.status})`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1147
|
+
const updatedTask = this.queries.updateTask(task.id, {
|
|
1148
|
+
status: "in_review",
|
|
1149
|
+
completed_at: now,
|
|
1150
|
+
progress: 100,
|
|
1151
|
+
progress_note: input.summary
|
|
1152
|
+
});
|
|
1153
|
+
const allGroupTasks = this.queries.listTasks({ group_id: task.group_id });
|
|
1154
|
+
const completedTasks = new Set(
|
|
1155
|
+
allGroupTasks.filter((t) => t.status === "completed" || t.status === "in_review").map((t) => t.id)
|
|
1156
|
+
);
|
|
1157
|
+
completedTasks.add(task.id);
|
|
1158
|
+
const dependencyMap = /* @__PURE__ */ new Map();
|
|
1159
|
+
for (const t of allGroupTasks) {
|
|
1160
|
+
const deps = this.queries.getDependencies(t.id);
|
|
1161
|
+
dependencyMap.set(
|
|
1162
|
+
t.id,
|
|
1163
|
+
deps.map((d) => d.id)
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
const unlockedIds = this.dagService.getUnlockedTasks(
|
|
1167
|
+
task.id,
|
|
1168
|
+
dependencyMap,
|
|
1169
|
+
completedTasks
|
|
1170
|
+
);
|
|
1171
|
+
const unlockedTasks = unlockedIds.map((id) => allGroupTasks.find((t) => t.id === id)).filter((t) => t !== void 0).map((t) => ({
|
|
1172
|
+
sequence: t.sequence,
|
|
1173
|
+
title: t.title
|
|
1174
|
+
}));
|
|
1175
|
+
for (const id of unlockedIds) {
|
|
1176
|
+
const t = allGroupTasks.find((at) => at.id === id);
|
|
1177
|
+
if (t && t.status === "blocked") {
|
|
1178
|
+
this.queries.updateTask(id, { status: "pending" });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
this.queries.addProgressLog({
|
|
1182
|
+
id: uuidv4(),
|
|
1183
|
+
task_id: task.id,
|
|
1184
|
+
event: "completed",
|
|
1185
|
+
message: input.summary,
|
|
1186
|
+
metadata: {
|
|
1187
|
+
files_changed: input.files_changed,
|
|
1188
|
+
unlocked_tasks: unlockedTasks
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
return {
|
|
1192
|
+
success: true,
|
|
1193
|
+
task: updatedTask,
|
|
1194
|
+
unlocked_tasks: unlockedTasks
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
// === Integration ===
|
|
1198
|
+
/**
|
|
1199
|
+
* Merge a task's branch into the main branch.
|
|
1200
|
+
*/
|
|
1201
|
+
mergeTask(input) {
|
|
1202
|
+
if (!this.gitService.isOnMainBranch()) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
"Must be on main/master branch to merge. Current branch: " + this.gitService.getCurrentBranch()
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
const task = this.queries.getTask(input.task_id);
|
|
1208
|
+
if (!task) {
|
|
1209
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
1210
|
+
}
|
|
1211
|
+
if (task.status !== "in_review") {
|
|
1212
|
+
throw new Error(
|
|
1213
|
+
`Task must be 'in_review' to merge (current status: ${task.status})`
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
if (!task.branch_name) {
|
|
1217
|
+
throw new Error("Task has no branch to merge");
|
|
1218
|
+
}
|
|
1219
|
+
const strategy = input.strategy ?? "squash";
|
|
1220
|
+
const mergeResult = this.gitService.mergeBranch(task.branch_name, strategy);
|
|
1221
|
+
if (mergeResult.success) {
|
|
1222
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1223
|
+
this.queries.updateTask(task.id, {
|
|
1224
|
+
status: "completed",
|
|
1225
|
+
merged_at: now
|
|
1226
|
+
});
|
|
1227
|
+
if (task.worktree_path) {
|
|
1228
|
+
try {
|
|
1229
|
+
if (this.gitService.worktreeExists(task.worktree_path)) {
|
|
1230
|
+
this.gitService.removeWorktree(task.worktree_path);
|
|
1231
|
+
}
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
try {
|
|
1236
|
+
this.gitService.deleteBranch(task.branch_name);
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
const allGroupTasks = this.queries.listTasks({
|
|
1240
|
+
group_id: task.group_id
|
|
1241
|
+
});
|
|
1242
|
+
const completedTasks = new Set(
|
|
1243
|
+
allGroupTasks.filter((t) => t.status === "completed").map((t) => t.id)
|
|
1244
|
+
);
|
|
1245
|
+
const dependencyMap = /* @__PURE__ */ new Map();
|
|
1246
|
+
for (const t of allGroupTasks) {
|
|
1247
|
+
const deps = this.queries.getDependencies(t.id);
|
|
1248
|
+
dependencyMap.set(
|
|
1249
|
+
t.id,
|
|
1250
|
+
deps.map((d) => d.id)
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const unlockedIds = this.dagService.getUnlockedTasks(
|
|
1254
|
+
task.id,
|
|
1255
|
+
dependencyMap,
|
|
1256
|
+
completedTasks
|
|
1257
|
+
);
|
|
1258
|
+
const unlockedTasks = unlockedIds.map((id) => allGroupTasks.find((t) => t.id === id)).filter((t) => t !== void 0).map((t) => {
|
|
1259
|
+
const canStart = this.dagService.canStart(
|
|
1260
|
+
t.id,
|
|
1261
|
+
dependencyMap,
|
|
1262
|
+
completedTasks
|
|
1263
|
+
);
|
|
1264
|
+
return {
|
|
1265
|
+
sequence: t.sequence,
|
|
1266
|
+
title: t.title,
|
|
1267
|
+
can_start: canStart
|
|
1268
|
+
};
|
|
1269
|
+
});
|
|
1270
|
+
for (const id of unlockedIds) {
|
|
1271
|
+
const t = allGroupTasks.find((at) => at.id === id);
|
|
1272
|
+
if (t && t.status === "blocked") {
|
|
1273
|
+
this.queries.updateTask(id, { status: "pending" });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
this.queries.addProgressLog({
|
|
1277
|
+
id: uuidv4(),
|
|
1278
|
+
task_id: task.id,
|
|
1279
|
+
event: "merged",
|
|
1280
|
+
message: `Task merged via ${strategy} strategy`,
|
|
1281
|
+
metadata: {
|
|
1282
|
+
strategy,
|
|
1283
|
+
unlocked_tasks: unlockedTasks
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
return {
|
|
1287
|
+
success: true,
|
|
1288
|
+
merge_result: "clean",
|
|
1289
|
+
unlocked_tasks: unlockedTasks
|
|
1290
|
+
};
|
|
1291
|
+
} else {
|
|
1292
|
+
const conflicts = mergeResult.conflicts.map((file) => ({
|
|
1293
|
+
file,
|
|
1294
|
+
description: `Merge conflict in ${file}`,
|
|
1295
|
+
auto_resolvable: false,
|
|
1296
|
+
suggestion: `Manually resolve conflicts in ${file} and commit the result`
|
|
1297
|
+
}));
|
|
1298
|
+
this.queries.addProgressLog({
|
|
1299
|
+
id: uuidv4(),
|
|
1300
|
+
task_id: task.id,
|
|
1301
|
+
event: "conflict_detected",
|
|
1302
|
+
message: `Merge conflicts detected in ${mergeResult.conflicts.length} file(s)`,
|
|
1303
|
+
metadata: {
|
|
1304
|
+
conflicted_files: mergeResult.conflicts,
|
|
1305
|
+
strategy
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
return {
|
|
1309
|
+
success: false,
|
|
1310
|
+
merge_result: "conflict",
|
|
1311
|
+
conflicts,
|
|
1312
|
+
unlocked_tasks: []
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Clean up a task: remove worktree, delete branch, mark as failed.
|
|
1318
|
+
*/
|
|
1319
|
+
cleanupTask(input) {
|
|
1320
|
+
const task = this.queries.getTask(input.task_id);
|
|
1321
|
+
if (!task) {
|
|
1322
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
1323
|
+
}
|
|
1324
|
+
let worktreeRemoved = false;
|
|
1325
|
+
let branchRemoved = false;
|
|
1326
|
+
if (task.worktree_path) {
|
|
1327
|
+
try {
|
|
1328
|
+
if (this.gitService.worktreeExists(task.worktree_path)) {
|
|
1329
|
+
this.gitService.removeWorktree(task.worktree_path);
|
|
1330
|
+
worktreeRemoved = true;
|
|
1331
|
+
}
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (task.branch_name) {
|
|
1336
|
+
try {
|
|
1337
|
+
this.gitService.deleteBranch(task.branch_name);
|
|
1338
|
+
branchRemoved = true;
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
this.queries.updateTask(task.id, {
|
|
1343
|
+
status: "failed"
|
|
1344
|
+
});
|
|
1345
|
+
this.queries.addProgressLog({
|
|
1346
|
+
id: uuidv4(),
|
|
1347
|
+
task_id: task.id,
|
|
1348
|
+
event: "failed",
|
|
1349
|
+
message: input.reason ?? "Task cleaned up and marked as failed",
|
|
1350
|
+
metadata: {
|
|
1351
|
+
reason: input.reason,
|
|
1352
|
+
worktree_removed: worktreeRemoved,
|
|
1353
|
+
branch_removed: branchRemoved
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
return {
|
|
1357
|
+
success: true,
|
|
1358
|
+
cleaned: {
|
|
1359
|
+
worktree_removed: worktreeRemoved,
|
|
1360
|
+
branch_removed: branchRemoved
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
// src/server.ts
|
|
1367
|
+
var taskService = null;
|
|
1368
|
+
function getTaskService() {
|
|
1369
|
+
if (!taskService) {
|
|
1370
|
+
const db = getDb();
|
|
1371
|
+
taskService = new TaskService(db);
|
|
1372
|
+
}
|
|
1373
|
+
return taskService;
|
|
1374
|
+
}
|
|
1375
|
+
function createServer() {
|
|
1376
|
+
const server2 = new McpServer({
|
|
1377
|
+
name: "task-manager",
|
|
1378
|
+
version: "0.1.0"
|
|
1379
|
+
});
|
|
1380
|
+
server2.tool(
|
|
1381
|
+
"create_tasks",
|
|
1382
|
+
"Create a group of tasks from a high-level requirement. Analyzes dependencies and file ownership.",
|
|
1383
|
+
{
|
|
1384
|
+
group_title: z.string().describe("Title for the task group"),
|
|
1385
|
+
group_description: z.string().describe("Description of the overall requirement"),
|
|
1386
|
+
tasks: z.array(
|
|
1387
|
+
z.object({
|
|
1388
|
+
title: z.string(),
|
|
1389
|
+
description: z.string(),
|
|
1390
|
+
priority: z.enum(["high", "medium", "low"]).optional().default("medium"),
|
|
1391
|
+
depends_on: z.array(z.number()).optional().default([]),
|
|
1392
|
+
file_patterns: z.array(
|
|
1393
|
+
z.object({
|
|
1394
|
+
pattern: z.string(),
|
|
1395
|
+
ownership_type: z.enum(["exclusive", "shared"])
|
|
1396
|
+
})
|
|
1397
|
+
).optional().default([])
|
|
1398
|
+
})
|
|
1399
|
+
)
|
|
1400
|
+
},
|
|
1401
|
+
async (params) => {
|
|
1402
|
+
try {
|
|
1403
|
+
const result = getTaskService().createTasks(params);
|
|
1404
|
+
return {
|
|
1405
|
+
content: [
|
|
1406
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1407
|
+
]
|
|
1408
|
+
};
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
return {
|
|
1411
|
+
content: [
|
|
1412
|
+
{
|
|
1413
|
+
type: "text",
|
|
1414
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1415
|
+
}
|
|
1416
|
+
],
|
|
1417
|
+
isError: true
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
);
|
|
1422
|
+
server2.tool(
|
|
1423
|
+
"list_tasks",
|
|
1424
|
+
"List tasks with optional filtering by group and status. Can include progress details.",
|
|
1425
|
+
{
|
|
1426
|
+
group_id: z.string().optional(),
|
|
1427
|
+
status: z.array(
|
|
1428
|
+
z.enum([
|
|
1429
|
+
"pending",
|
|
1430
|
+
"assigned",
|
|
1431
|
+
"in_progress",
|
|
1432
|
+
"in_review",
|
|
1433
|
+
"completed",
|
|
1434
|
+
"failed",
|
|
1435
|
+
"blocked"
|
|
1436
|
+
])
|
|
1437
|
+
).optional(),
|
|
1438
|
+
include_progress: z.boolean().optional().default(false)
|
|
1439
|
+
},
|
|
1440
|
+
async (params) => {
|
|
1441
|
+
try {
|
|
1442
|
+
const result = getTaskService().listTasks(params);
|
|
1443
|
+
return {
|
|
1444
|
+
content: [
|
|
1445
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1446
|
+
]
|
|
1447
|
+
};
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
return {
|
|
1450
|
+
content: [
|
|
1451
|
+
{
|
|
1452
|
+
type: "text",
|
|
1453
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1454
|
+
}
|
|
1455
|
+
],
|
|
1456
|
+
isError: true
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1461
|
+
server2.tool(
|
|
1462
|
+
"get_task",
|
|
1463
|
+
"Get detailed information about a specific task including dependencies, file ownership, and progress logs.",
|
|
1464
|
+
{
|
|
1465
|
+
task_id: z.string()
|
|
1466
|
+
},
|
|
1467
|
+
async (params) => {
|
|
1468
|
+
try {
|
|
1469
|
+
const result = getTaskService().getTask(params);
|
|
1470
|
+
return {
|
|
1471
|
+
content: [
|
|
1472
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1473
|
+
]
|
|
1474
|
+
};
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
return {
|
|
1477
|
+
content: [
|
|
1478
|
+
{
|
|
1479
|
+
type: "text",
|
|
1480
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1481
|
+
}
|
|
1482
|
+
],
|
|
1483
|
+
isError: true
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
);
|
|
1488
|
+
server2.tool(
|
|
1489
|
+
"claim_task",
|
|
1490
|
+
"Claim a task for an agent. Assigns the task and checks for dependency and file ownership conflicts.",
|
|
1491
|
+
{
|
|
1492
|
+
task_id: z.string(),
|
|
1493
|
+
agent_id: z.string().optional()
|
|
1494
|
+
},
|
|
1495
|
+
async (params) => {
|
|
1496
|
+
try {
|
|
1497
|
+
const result = getTaskService().claimTask(params);
|
|
1498
|
+
return {
|
|
1499
|
+
content: [
|
|
1500
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1501
|
+
]
|
|
1502
|
+
};
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
return {
|
|
1505
|
+
content: [
|
|
1506
|
+
{
|
|
1507
|
+
type: "text",
|
|
1508
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1509
|
+
}
|
|
1510
|
+
],
|
|
1511
|
+
isError: true
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
);
|
|
1516
|
+
server2.tool(
|
|
1517
|
+
"start_task",
|
|
1518
|
+
"Start working on a claimed task. Creates a git worktree and branch for isolated work.",
|
|
1519
|
+
{
|
|
1520
|
+
task_id: z.string()
|
|
1521
|
+
},
|
|
1522
|
+
async (params) => {
|
|
1523
|
+
try {
|
|
1524
|
+
const result = getTaskService().startTask(params);
|
|
1525
|
+
return {
|
|
1526
|
+
content: [
|
|
1527
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1528
|
+
]
|
|
1529
|
+
};
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
return {
|
|
1532
|
+
content: [
|
|
1533
|
+
{
|
|
1534
|
+
type: "text",
|
|
1535
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1536
|
+
}
|
|
1537
|
+
],
|
|
1538
|
+
isError: true
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
);
|
|
1543
|
+
server2.tool(
|
|
1544
|
+
"update_progress",
|
|
1545
|
+
"Update progress on an in-progress task. Reports percentage complete and checks for file conflicts.",
|
|
1546
|
+
{
|
|
1547
|
+
task_id: z.string(),
|
|
1548
|
+
progress: z.number().min(0).max(100),
|
|
1549
|
+
note: z.string(),
|
|
1550
|
+
files_changed: z.array(z.string()).optional()
|
|
1551
|
+
},
|
|
1552
|
+
async (params) => {
|
|
1553
|
+
try {
|
|
1554
|
+
const result = getTaskService().updateProgress(params);
|
|
1555
|
+
return {
|
|
1556
|
+
content: [
|
|
1557
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1558
|
+
]
|
|
1559
|
+
};
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
return {
|
|
1562
|
+
content: [
|
|
1563
|
+
{
|
|
1564
|
+
type: "text",
|
|
1565
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1566
|
+
}
|
|
1567
|
+
],
|
|
1568
|
+
isError: true
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
);
|
|
1573
|
+
server2.tool(
|
|
1574
|
+
"complete_task",
|
|
1575
|
+
"Mark a task as completed with a summary and list of changed files. Moves task to in_review status.",
|
|
1576
|
+
{
|
|
1577
|
+
task_id: z.string(),
|
|
1578
|
+
summary: z.string(),
|
|
1579
|
+
files_changed: z.array(z.string())
|
|
1580
|
+
},
|
|
1581
|
+
async (params) => {
|
|
1582
|
+
try {
|
|
1583
|
+
const result = getTaskService().completeTask(params);
|
|
1584
|
+
return {
|
|
1585
|
+
content: [
|
|
1586
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1587
|
+
]
|
|
1588
|
+
};
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
return {
|
|
1591
|
+
content: [
|
|
1592
|
+
{
|
|
1593
|
+
type: "text",
|
|
1594
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1595
|
+
}
|
|
1596
|
+
],
|
|
1597
|
+
isError: true
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
);
|
|
1602
|
+
server2.tool(
|
|
1603
|
+
"merge_task",
|
|
1604
|
+
"Merge a completed task branch back into the main branch. Supports merge and squash strategies.",
|
|
1605
|
+
{
|
|
1606
|
+
task_id: z.string(),
|
|
1607
|
+
strategy: z.enum(["merge", "squash"]).optional().default("squash")
|
|
1608
|
+
},
|
|
1609
|
+
async (params) => {
|
|
1610
|
+
try {
|
|
1611
|
+
const result = getTaskService().mergeTask(params);
|
|
1612
|
+
return {
|
|
1613
|
+
content: [
|
|
1614
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1615
|
+
]
|
|
1616
|
+
};
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
return {
|
|
1619
|
+
content: [
|
|
1620
|
+
{
|
|
1621
|
+
type: "text",
|
|
1622
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1623
|
+
}
|
|
1624
|
+
],
|
|
1625
|
+
isError: true
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
server2.tool(
|
|
1631
|
+
"cleanup_task",
|
|
1632
|
+
"Clean up a task by removing its worktree and branch. Used after merging or to abandon a task.",
|
|
1633
|
+
{
|
|
1634
|
+
task_id: z.string(),
|
|
1635
|
+
reason: z.string().optional()
|
|
1636
|
+
},
|
|
1637
|
+
async (params) => {
|
|
1638
|
+
try {
|
|
1639
|
+
const result = getTaskService().cleanupTask(params);
|
|
1640
|
+
return {
|
|
1641
|
+
content: [
|
|
1642
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1643
|
+
]
|
|
1644
|
+
};
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
return {
|
|
1647
|
+
content: [
|
|
1648
|
+
{
|
|
1649
|
+
type: "text",
|
|
1650
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
1651
|
+
}
|
|
1652
|
+
],
|
|
1653
|
+
isError: true
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
return server2;
|
|
1659
|
+
}
|
|
1660
|
+
var server = createServer();
|
|
1661
|
+
|
|
1662
|
+
// src/index.ts
|
|
1663
|
+
async function main() {
|
|
1664
|
+
const server2 = createServer();
|
|
1665
|
+
const transport = new StdioServerTransport();
|
|
1666
|
+
await server2.connect(transport);
|
|
1667
|
+
}
|
|
1668
|
+
main().catch((error) => {
|
|
1669
|
+
console.error("Fatal error:", error);
|
|
1670
|
+
process.exit(1);
|
|
1671
|
+
});
|
|
1672
|
+
//# sourceMappingURL=index.js.map
|