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/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