@hasna/todos 0.9.83 → 0.10.2

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 CHANGED
@@ -659,6 +659,44 @@ var MIGRATIONS = [
659
659
  ALTER TABLE tasks ADD COLUMN assigned_from_project TEXT;
660
660
  CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by);
661
661
  INSERT OR IGNORE INTO _migrations (id) VALUES (26);
662
+ `,
663
+ `
664
+ CREATE TABLE IF NOT EXISTS task_relationships (
665
+ id TEXT PRIMARY KEY,
666
+ source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
667
+ target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
668
+ relationship_type TEXT NOT NULL CHECK(relationship_type IN ('related_to', 'conflicts_with', 'similar_to', 'duplicates', 'supersedes', 'modifies_same_file')),
669
+ metadata TEXT DEFAULT '{}',
670
+ created_by TEXT,
671
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
672
+ CHECK (source_task_id != target_task_id)
673
+ );
674
+ CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id);
675
+ CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id);
676
+ CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type);
677
+ INSERT OR IGNORE INTO _migrations (id) VALUES (27);
678
+ `,
679
+ `
680
+ CREATE TABLE IF NOT EXISTS kg_edges (
681
+ id TEXT PRIMARY KEY,
682
+ source_id TEXT NOT NULL,
683
+ source_type TEXT NOT NULL,
684
+ target_id TEXT NOT NULL,
685
+ target_type TEXT NOT NULL,
686
+ relation_type TEXT NOT NULL,
687
+ weight REAL NOT NULL DEFAULT 1.0,
688
+ metadata TEXT DEFAULT '{}',
689
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
690
+ UNIQUE(source_id, source_type, target_id, target_type, relation_type)
691
+ );
692
+ CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type);
693
+ CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type);
694
+ CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type);
695
+ INSERT OR IGNORE INTO _migrations (id) VALUES (28);
696
+ `,
697
+ `
698
+ ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]';
699
+ INSERT OR IGNORE INTO _migrations (id) VALUES (29);
662
700
  `
663
701
  ];
664
702
  var _db = null;
@@ -796,6 +834,30 @@ function ensureSchema(db) {
796
834
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
797
835
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
798
836
  )`);
837
+ ensureTable("task_relationships", `
838
+ CREATE TABLE task_relationships (
839
+ id TEXT PRIMARY KEY,
840
+ source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
841
+ target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
842
+ relationship_type TEXT NOT NULL,
843
+ metadata TEXT DEFAULT '{}',
844
+ created_by TEXT,
845
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
846
+ CHECK (source_task_id != target_task_id)
847
+ )`);
848
+ ensureTable("kg_edges", `
849
+ CREATE TABLE kg_edges (
850
+ id TEXT PRIMARY KEY,
851
+ source_id TEXT NOT NULL,
852
+ source_type TEXT NOT NULL,
853
+ target_id TEXT NOT NULL,
854
+ target_type TEXT NOT NULL,
855
+ relation_type TEXT NOT NULL,
856
+ weight REAL NOT NULL DEFAULT 1.0,
857
+ metadata TEXT DEFAULT '{}',
858
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
859
+ UNIQUE(source_id, source_type, target_id, target_type, relation_type)
860
+ )`);
799
861
  ensureColumn("projects", "task_list_id", "TEXT");
800
862
  ensureColumn("projects", "task_prefix", "TEXT");
801
863
  ensureColumn("projects", "task_counter", "INTEGER NOT NULL DEFAULT 0");
@@ -820,6 +882,7 @@ function ensureSchema(db) {
820
882
  ensureColumn("agents", "title", "TEXT");
821
883
  ensureColumn("agents", "level", "TEXT");
822
884
  ensureColumn("agents", "org_id", "TEXT");
885
+ ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
823
886
  ensureColumn("projects", "org_id", "TEXT");
824
887
  ensureColumn("plans", "task_list_id", "TEXT");
825
888
  ensureColumn("plans", "agent_id", "TEXT");
@@ -846,6 +909,12 @@ function ensureSchema(db) {
846
909
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id)");
847
910
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type)");
848
911
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by)");
912
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id)");
913
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id)");
914
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type)");
915
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type)");
916
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type)");
917
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type)");
849
918
  }
850
919
  function backfillTaskTags(db) {
851
920
  try {
@@ -2771,30 +2840,13 @@ function rowToAgent(row) {
2771
2840
  return {
2772
2841
  ...row,
2773
2842
  permissions: JSON.parse(row.permissions || '["*"]'),
2843
+ capabilities: JSON.parse(row.capabilities || "[]"),
2774
2844
  metadata: JSON.parse(row.metadata || "{}")
2775
2845
  };
2776
2846
  }
2777
2847
  function registerAgent(input, db) {
2778
2848
  const d = db || getDatabase();
2779
2849
  const normalizedName = input.name.trim().toLowerCase();
2780
- if (input.pool && input.pool.length > 0) {
2781
- const poolLower = input.pool.map((n) => n.toLowerCase());
2782
- if (!poolLower.includes(normalizedName)) {
2783
- const available = getAvailableNamesFromPool(input.pool, d);
2784
- const suggestion = available.length > 0 ? available[0] : null;
2785
- return {
2786
- conflict: true,
2787
- pool_violation: true,
2788
- existing_id: "",
2789
- existing_name: normalizedName,
2790
- last_seen_at: "",
2791
- session_hint: null,
2792
- working_dir: input.working_dir || null,
2793
- suggestions: available.slice(0, 5),
2794
- message: `"${normalizedName}" is not in this project's agent pool [${input.pool.join(", ")}]. ${available.length > 0 ? `Try: ${available.slice(0, 3).join(", ")}` : "No names are currently available \u2014 wait for an active agent to go stale."}${suggestion ? ` Suggested: ${suggestion}` : ""}`
2795
- };
2796
- }
2797
- }
2798
2850
  const existing = getAgentByName(normalizedName, d);
2799
2851
  if (existing) {
2800
2852
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
@@ -2835,8 +2887,8 @@ function registerAgent(input, db) {
2835
2887
  }
2836
2888
  const id = shortUuid();
2837
2889
  const timestamp = now();
2838
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir)
2839
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2890
+ d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir)
2891
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2840
2892
  id,
2841
2893
  normalizedName,
2842
2894
  input.description || null,
@@ -2844,6 +2896,7 @@ function registerAgent(input, db) {
2844
2896
  input.title || null,
2845
2897
  input.level || null,
2846
2898
  JSON.stringify(input.permissions || ["*"]),
2899
+ JSON.stringify(input.capabilities || []),
2847
2900
  input.reports_to || null,
2848
2901
  input.org_id || null,
2849
2902
  JSON.stringify(input.metadata || {}),
@@ -2899,6 +2952,10 @@ function updateAgent(id, input, db) {
2899
2952
  sets.push("permissions = ?");
2900
2953
  params.push(JSON.stringify(input.permissions));
2901
2954
  }
2955
+ if (input.capabilities !== undefined) {
2956
+ sets.push("capabilities = ?");
2957
+ params.push(JSON.stringify(input.capabilities));
2958
+ }
2902
2959
  if (input.title !== undefined) {
2903
2960
  sets.push("title = ?");
2904
2961
  params.push(input.title);
@@ -2946,6 +3003,28 @@ function getOrgChart(db) {
2946
3003
  }
2947
3004
  return buildTree(null);
2948
3005
  }
3006
+ function matchCapabilities(agentCapabilities, requiredCapabilities) {
3007
+ if (requiredCapabilities.length === 0)
3008
+ return 1;
3009
+ if (agentCapabilities.length === 0)
3010
+ return 0;
3011
+ const agentSet = new Set(agentCapabilities.map((c) => c.toLowerCase()));
3012
+ let matches = 0;
3013
+ for (const req of requiredCapabilities) {
3014
+ if (agentSet.has(req.toLowerCase()))
3015
+ matches++;
3016
+ }
3017
+ return matches / requiredCapabilities.length;
3018
+ }
3019
+ function getCapableAgents(capabilities, opts, db) {
3020
+ const agents = listAgents(db);
3021
+ const minScore = opts?.min_score ?? 0.1;
3022
+ const scored = agents.map((agent) => ({
3023
+ agent,
3024
+ score: matchCapabilities(agent.capabilities, capabilities)
3025
+ })).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
3026
+ return opts?.limit ? scored.slice(0, opts.limit) : scored;
3027
+ }
2949
3028
  // src/db/task-lists.ts
2950
3029
  function rowToTaskList(row) {
2951
3030
  return {
@@ -3266,8 +3345,537 @@ function deleteOrg(id, db) {
3266
3345
  const d = db || getDatabase();
3267
3346
  return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
3268
3347
  }
3269
- // src/lib/search.ts
3348
+ // src/db/task-relationships.ts
3349
+ var RELATIONSHIP_TYPES = [
3350
+ "related_to",
3351
+ "conflicts_with",
3352
+ "similar_to",
3353
+ "duplicates",
3354
+ "supersedes",
3355
+ "modifies_same_file"
3356
+ ];
3357
+ function rowToRelationship(row) {
3358
+ return {
3359
+ ...row,
3360
+ relationship_type: row.relationship_type,
3361
+ metadata: JSON.parse(row.metadata || "{}")
3362
+ };
3363
+ }
3364
+ function addTaskRelationship(input, db) {
3365
+ const d = db || getDatabase();
3366
+ const id = uuid();
3367
+ const timestamp = now();
3368
+ if (input.source_task_id === input.target_task_id) {
3369
+ throw new Error("Cannot create a relationship between a task and itself");
3370
+ }
3371
+ const symmetric = ["related_to", "conflicts_with", "similar_to", "modifies_same_file"];
3372
+ if (symmetric.includes(input.relationship_type)) {
3373
+ const existing = d.query(`SELECT id FROM task_relationships
3374
+ WHERE relationship_type = ?
3375
+ AND ((source_task_id = ? AND target_task_id = ?) OR (source_task_id = ? AND target_task_id = ?))`).get(input.relationship_type, input.source_task_id, input.target_task_id, input.target_task_id, input.source_task_id);
3376
+ if (existing) {
3377
+ return getTaskRelationship(existing.id, d);
3378
+ }
3379
+ } else {
3380
+ const existing = d.query("SELECT id FROM task_relationships WHERE source_task_id = ? AND target_task_id = ? AND relationship_type = ?").get(input.source_task_id, input.target_task_id, input.relationship_type);
3381
+ if (existing) {
3382
+ return getTaskRelationship(existing.id, d);
3383
+ }
3384
+ }
3385
+ d.run(`INSERT INTO task_relationships (id, source_task_id, target_task_id, relationship_type, metadata, created_by, created_at)
3386
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
3387
+ id,
3388
+ input.source_task_id,
3389
+ input.target_task_id,
3390
+ input.relationship_type,
3391
+ JSON.stringify(input.metadata || {}),
3392
+ input.created_by || null,
3393
+ timestamp
3394
+ ]);
3395
+ return getTaskRelationship(id, d);
3396
+ }
3397
+ function getTaskRelationship(id, db) {
3398
+ const d = db || getDatabase();
3399
+ const row = d.query("SELECT * FROM task_relationships WHERE id = ?").get(id);
3400
+ return row ? rowToRelationship(row) : null;
3401
+ }
3402
+ function removeTaskRelationship(id, db) {
3403
+ const d = db || getDatabase();
3404
+ return d.run("DELETE FROM task_relationships WHERE id = ?", [id]).changes > 0;
3405
+ }
3406
+ function removeTaskRelationshipByPair(sourceTaskId, targetTaskId, relationshipType, db) {
3407
+ const d = db || getDatabase();
3408
+ const symmetric = ["related_to", "conflicts_with", "similar_to", "modifies_same_file"];
3409
+ if (symmetric.includes(relationshipType)) {
3410
+ return d.run(`DELETE FROM task_relationships
3411
+ WHERE relationship_type = ?
3412
+ AND ((source_task_id = ? AND target_task_id = ?) OR (source_task_id = ? AND target_task_id = ?))`, [relationshipType, sourceTaskId, targetTaskId, targetTaskId, sourceTaskId]).changes > 0;
3413
+ }
3414
+ return d.run("DELETE FROM task_relationships WHERE source_task_id = ? AND target_task_id = ? AND relationship_type = ?", [sourceTaskId, targetTaskId, relationshipType]).changes > 0;
3415
+ }
3416
+ function getTaskRelationships(taskId, relationshipType, db) {
3417
+ const d = db || getDatabase();
3418
+ let sql = "SELECT * FROM task_relationships WHERE (source_task_id = ? OR target_task_id = ?)";
3419
+ const params = [taskId, taskId];
3420
+ if (relationshipType) {
3421
+ sql += " AND relationship_type = ?";
3422
+ params.push(relationshipType);
3423
+ }
3424
+ sql += " ORDER BY created_at DESC";
3425
+ return d.query(sql).all(...params).map(rowToRelationship);
3426
+ }
3427
+ function findRelatedTaskIds(taskId, relationshipType, db) {
3428
+ const rels = getTaskRelationships(taskId, relationshipType, db);
3429
+ const ids = new Set;
3430
+ for (const rel of rels) {
3431
+ if (rel.source_task_id === taskId)
3432
+ ids.add(rel.target_task_id);
3433
+ else
3434
+ ids.add(rel.source_task_id);
3435
+ }
3436
+ return [...ids];
3437
+ }
3438
+ function autoDetectFileRelationships(taskId, db) {
3439
+ const d = db || getDatabase();
3440
+ const files = d.query("SELECT path FROM task_files WHERE task_id = ? AND status != 'removed'").all(taskId);
3441
+ const created = [];
3442
+ for (const file of files) {
3443
+ const others = d.query("SELECT DISTINCT task_id FROM task_files WHERE path = ? AND task_id != ? AND status != 'removed'").all(file.path, taskId);
3444
+ for (const other of others) {
3445
+ try {
3446
+ const rel = addTaskRelationship({
3447
+ source_task_id: taskId,
3448
+ target_task_id: other.task_id,
3449
+ relationship_type: "modifies_same_file",
3450
+ metadata: { shared_file: file.path }
3451
+ }, d);
3452
+ created.push(rel);
3453
+ } catch {}
3454
+ }
3455
+ }
3456
+ return created;
3457
+ }
3458
+ // src/db/kg.ts
3459
+ function rowToEdge(row) {
3460
+ return {
3461
+ ...row,
3462
+ metadata: JSON.parse(row.metadata || "{}")
3463
+ };
3464
+ }
3465
+ function syncKgEdges(db) {
3466
+ const d = db || getDatabase();
3467
+ let synced = 0;
3468
+ const tx = d.transaction(() => {
3469
+ const deps = d.query("SELECT task_id, depends_on FROM task_dependencies").all();
3470
+ for (const dep of deps) {
3471
+ synced += upsertEdge(d, dep.task_id, "task", dep.depends_on, "task", "depends_on");
3472
+ }
3473
+ const assignments = d.query("SELECT id, assigned_to FROM tasks WHERE assigned_to IS NOT NULL").all();
3474
+ for (const a of assignments) {
3475
+ synced += upsertEdge(d, a.id, "task", a.assigned_to, "agent", "assigned_to");
3476
+ }
3477
+ const agents = d.query("SELECT id, reports_to FROM agents WHERE reports_to IS NOT NULL").all();
3478
+ for (const a of agents) {
3479
+ synced += upsertEdge(d, a.id, "agent", a.reports_to, "agent", "reports_to");
3480
+ }
3481
+ const files = d.query("SELECT task_id, path FROM task_files WHERE status != 'removed'").all();
3482
+ for (const f of files) {
3483
+ synced += upsertEdge(d, f.task_id, "task", f.path, "file", "references_file");
3484
+ }
3485
+ const taskProjects = d.query("SELECT id, project_id FROM tasks WHERE project_id IS NOT NULL").all();
3486
+ for (const tp of taskProjects) {
3487
+ synced += upsertEdge(d, tp.id, "task", tp.project_id, "project", "in_project");
3488
+ }
3489
+ const taskPlans = d.query("SELECT id, plan_id FROM tasks WHERE plan_id IS NOT NULL").all();
3490
+ for (const tp of taskPlans) {
3491
+ synced += upsertEdge(d, tp.id, "task", tp.plan_id, "plan", "in_plan");
3492
+ }
3493
+ try {
3494
+ const rels = d.query("SELECT source_task_id, target_task_id, relationship_type FROM task_relationships").all();
3495
+ for (const r of rels) {
3496
+ synced += upsertEdge(d, r.source_task_id, "task", r.target_task_id, "task", r.relationship_type);
3497
+ }
3498
+ } catch {}
3499
+ });
3500
+ tx();
3501
+ return { synced };
3502
+ }
3503
+ function upsertEdge(d, sourceId, sourceType, targetId, targetType, relationType, weight = 1) {
3504
+ try {
3505
+ d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
3506
+ VALUES (?, ?, ?, ?, ?, ?, ?, '{}', ?)`, [uuid(), sourceId, sourceType, targetId, targetType, relationType, weight, now()]);
3507
+ return 1;
3508
+ } catch {
3509
+ return 0;
3510
+ }
3511
+ }
3512
+ function getRelated(entityId, opts, db) {
3513
+ const d = db || getDatabase();
3514
+ const direction = opts?.direction || "both";
3515
+ const conditions = [];
3516
+ const params = [];
3517
+ if (direction === "outgoing" || direction === "both") {
3518
+ conditions.push("source_id = ?");
3519
+ params.push(entityId);
3520
+ }
3521
+ if (direction === "incoming" || direction === "both") {
3522
+ conditions.push("target_id = ?");
3523
+ params.push(entityId);
3524
+ }
3525
+ let sql = `SELECT * FROM kg_edges WHERE (${conditions.join(" OR ")})`;
3526
+ if (opts?.relation_type) {
3527
+ sql += " AND relation_type = ?";
3528
+ params.push(opts.relation_type);
3529
+ }
3530
+ if (opts?.entity_type) {
3531
+ sql += " AND (source_type = ? OR target_type = ?)";
3532
+ params.push(opts.entity_type, opts.entity_type);
3533
+ }
3534
+ sql += " ORDER BY weight DESC, created_at DESC";
3535
+ if (opts?.limit) {
3536
+ sql += " LIMIT ?";
3537
+ params.push(opts.limit);
3538
+ }
3539
+ return d.query(sql).all(...params).map(rowToEdge);
3540
+ }
3541
+ function findPath(sourceId, targetId, opts, db) {
3542
+ const d = db || getDatabase();
3543
+ const maxDepth = opts?.max_depth || 5;
3544
+ const visited = new Set;
3545
+ const queue = [{ id: sourceId, path: [] }];
3546
+ const results = [];
3547
+ visited.add(sourceId);
3548
+ while (queue.length > 0) {
3549
+ const current = queue.shift();
3550
+ if (current.path.length >= maxDepth)
3551
+ continue;
3552
+ let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
3553
+ const params = [current.id];
3554
+ if (opts?.relation_types && opts.relation_types.length > 0) {
3555
+ const placeholders = opts.relation_types.map(() => "?").join(",");
3556
+ sql += ` AND relation_type IN (${placeholders})`;
3557
+ params.push(...opts.relation_types);
3558
+ }
3559
+ const edges = d.query(sql).all(...params).map(rowToEdge);
3560
+ for (const edge of edges) {
3561
+ const nextId = edge.target_id;
3562
+ const newPath = [...current.path, edge];
3563
+ if (nextId === targetId) {
3564
+ results.push(newPath);
3565
+ if (results.length >= 3)
3566
+ return results;
3567
+ continue;
3568
+ }
3569
+ if (!visited.has(nextId)) {
3570
+ visited.add(nextId);
3571
+ queue.push({ id: nextId, path: newPath });
3572
+ }
3573
+ }
3574
+ }
3575
+ return results;
3576
+ }
3577
+ function getImpactAnalysis(entityId, opts, db) {
3578
+ const d = db || getDatabase();
3579
+ const maxDepth = opts?.max_depth || 3;
3580
+ const results = [];
3581
+ const visited = new Set;
3582
+ visited.add(entityId);
3583
+ const queue = [{ id: entityId, depth: 0 }];
3584
+ while (queue.length > 0) {
3585
+ const current = queue.shift();
3586
+ if (current.depth >= maxDepth)
3587
+ continue;
3588
+ let sql = "SELECT * FROM kg_edges WHERE source_id = ?";
3589
+ const params = [current.id];
3590
+ if (opts?.relation_types && opts.relation_types.length > 0) {
3591
+ const placeholders = opts.relation_types.map(() => "?").join(",");
3592
+ sql += ` AND relation_type IN (${placeholders})`;
3593
+ params.push(...opts.relation_types);
3594
+ }
3595
+ const edges = d.query(sql).all(...params).map(rowToEdge);
3596
+ for (const edge of edges) {
3597
+ if (!visited.has(edge.target_id)) {
3598
+ visited.add(edge.target_id);
3599
+ results.push({
3600
+ entity_id: edge.target_id,
3601
+ entity_type: edge.target_type,
3602
+ depth: current.depth + 1,
3603
+ relation: edge.relation_type
3604
+ });
3605
+ queue.push({ id: edge.target_id, depth: current.depth + 1 });
3606
+ }
3607
+ }
3608
+ }
3609
+ return results;
3610
+ }
3611
+ function getCriticalPath(opts, db) {
3612
+ const d = db || getDatabase();
3613
+ let sql = `SELECT source_id, target_id FROM kg_edges WHERE relation_type = 'depends_on'`;
3614
+ const params = [];
3615
+ if (opts?.project_id) {
3616
+ sql += ` AND source_id IN (SELECT id FROM tasks WHERE project_id = ?)`;
3617
+ params.push(opts.project_id);
3618
+ }
3619
+ const edges = d.query(sql).all(...params);
3620
+ const blocks = new Map;
3621
+ for (const e of edges) {
3622
+ if (!blocks.has(e.target_id))
3623
+ blocks.set(e.target_id, new Set);
3624
+ blocks.get(e.target_id).add(e.source_id);
3625
+ }
3626
+ const results = [];
3627
+ for (const [taskId] of blocks) {
3628
+ const visited = new Set;
3629
+ const q = [taskId];
3630
+ let maxDepth = 0;
3631
+ let depth = 0;
3632
+ let levelSize = q.length;
3633
+ while (q.length > 0) {
3634
+ const node = q.shift();
3635
+ levelSize--;
3636
+ const downstream = blocks.get(node);
3637
+ if (downstream) {
3638
+ for (const d2 of downstream) {
3639
+ if (!visited.has(d2)) {
3640
+ visited.add(d2);
3641
+ q.push(d2);
3642
+ }
3643
+ }
3644
+ }
3645
+ if (levelSize === 0) {
3646
+ depth++;
3647
+ maxDepth = Math.max(maxDepth, depth);
3648
+ levelSize = q.length;
3649
+ }
3650
+ }
3651
+ if (visited.size > 0) {
3652
+ results.push({ task_id: taskId, blocking_count: visited.size, depth: maxDepth });
3653
+ }
3654
+ }
3655
+ results.sort((a, b) => b.blocking_count - a.blocking_count);
3656
+ return results.slice(0, opts?.limit || 20);
3657
+ }
3658
+ function addKgEdge(sourceId, sourceType, targetId, targetType, relationType, weight = 1, metadata, db) {
3659
+ const d = db || getDatabase();
3660
+ const id = uuid();
3661
+ const timestamp = now();
3662
+ d.run(`INSERT OR IGNORE INTO kg_edges (id, source_id, source_type, target_id, target_type, relation_type, weight, metadata, created_at)
3663
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, sourceId, sourceType, targetId, targetType, relationType, weight, JSON.stringify(metadata || {}), timestamp]);
3664
+ return { id, source_id: sourceId, source_type: sourceType, target_id: targetId, target_type: targetType, relation_type: relationType, weight, metadata: metadata || {}, created_at: timestamp };
3665
+ }
3666
+ function removeKgEdges(sourceId, targetId, relationType, db) {
3667
+ const d = db || getDatabase();
3668
+ if (relationType) {
3669
+ return d.run("DELETE FROM kg_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?", [sourceId, targetId, relationType]).changes;
3670
+ }
3671
+ return d.run("DELETE FROM kg_edges WHERE source_id = ? AND target_id = ?", [sourceId, targetId]).changes;
3672
+ }
3673
+ // src/db/patrol.ts
3270
3674
  function rowToTask2(row) {
3675
+ return {
3676
+ ...row,
3677
+ status: row.status,
3678
+ priority: row.priority,
3679
+ tags: JSON.parse(row.tags || "[]"),
3680
+ metadata: JSON.parse(row.metadata || "{}"),
3681
+ requires_approval: Boolean(row.requires_approval)
3682
+ };
3683
+ }
3684
+ function patrolTasks(opts, db) {
3685
+ const d = db || getDatabase();
3686
+ const stuckMinutes = opts?.stuck_minutes || 60;
3687
+ const confidenceThreshold = opts?.confidence_threshold || 0.5;
3688
+ const issues = [];
3689
+ let projectFilter = "";
3690
+ const projectParams = [];
3691
+ if (opts?.project_id) {
3692
+ projectFilter = " AND project_id = ?";
3693
+ projectParams.push(opts.project_id);
3694
+ }
3695
+ const stuckCutoff = new Date(Date.now() - stuckMinutes * 60 * 1000).toISOString();
3696
+ const stuckRows = d.query(`SELECT * FROM tasks WHERE status = 'in_progress' AND updated_at < ?${projectFilter} ORDER BY updated_at ASC`).all(stuckCutoff, ...projectParams);
3697
+ for (const row of stuckRows) {
3698
+ const task = rowToTask2(row);
3699
+ const minutesStuck = Math.round((Date.now() - new Date(task.updated_at).getTime()) / 60000);
3700
+ issues.push({
3701
+ type: "stuck",
3702
+ task_id: task.id,
3703
+ task_title: task.title,
3704
+ severity: minutesStuck > 480 ? "critical" : minutesStuck > 120 ? "high" : "medium",
3705
+ detail: `In progress for ${minutesStuck} minutes without update`
3706
+ });
3707
+ }
3708
+ const lowConfRows = d.query(`SELECT * FROM tasks WHERE status = 'completed' AND confidence IS NOT NULL AND confidence < ?${projectFilter} ORDER BY confidence ASC`).all(confidenceThreshold, ...projectParams);
3709
+ for (const row of lowConfRows) {
3710
+ const task = rowToTask2(row);
3711
+ issues.push({
3712
+ type: "low_confidence",
3713
+ task_id: task.id,
3714
+ task_title: task.title,
3715
+ severity: (task.confidence ?? 0) < 0.3 ? "high" : "medium",
3716
+ detail: `Completed with confidence ${task.confidence} (threshold: ${confidenceThreshold})`
3717
+ });
3718
+ }
3719
+ const orphanedRows = d.query(`SELECT * FROM tasks WHERE status = 'pending' AND project_id IS NULL AND agent_id IS NULL AND assigned_to IS NULL ORDER BY created_at ASC`).all();
3720
+ for (const row of orphanedRows) {
3721
+ const task = rowToTask2(row);
3722
+ issues.push({
3723
+ type: "orphaned",
3724
+ task_id: task.id,
3725
+ task_title: task.title,
3726
+ severity: "low",
3727
+ detail: "Pending task with no project, no agent, and no assignee"
3728
+ });
3729
+ }
3730
+ const needsReviewRows = d.query(`SELECT * FROM tasks WHERE status = 'completed' AND requires_approval = 1 AND approved_by IS NULL${projectFilter} ORDER BY completed_at DESC`).all(...projectParams);
3731
+ for (const row of needsReviewRows) {
3732
+ const task = rowToTask2(row);
3733
+ issues.push({
3734
+ type: "needs_review",
3735
+ task_id: task.id,
3736
+ task_title: task.title,
3737
+ severity: "medium",
3738
+ detail: "Completed but requires approval \u2014 not yet reviewed"
3739
+ });
3740
+ }
3741
+ const pendingWithDeps = d.query(`SELECT t.* FROM tasks t
3742
+ WHERE t.status = 'pending'${projectFilter}
3743
+ AND t.id IN (SELECT task_id FROM task_dependencies)`).all(...projectParams);
3744
+ for (const row of pendingWithDeps) {
3745
+ const deps = d.query(`SELECT d.depends_on, t.status FROM task_dependencies d
3746
+ JOIN tasks t ON t.id = d.depends_on
3747
+ WHERE d.task_id = ?`).all(row.id);
3748
+ if (deps.length > 0 && deps.every((dep) => dep.status === "failed" || dep.status === "cancelled")) {
3749
+ const task = rowToTask2(row);
3750
+ issues.push({
3751
+ type: "zombie_blocked",
3752
+ task_id: task.id,
3753
+ task_title: task.title,
3754
+ severity: "high",
3755
+ detail: `Blocked by ${deps.length} task(s) that are all failed/cancelled \u2014 will never unblock`
3756
+ });
3757
+ }
3758
+ }
3759
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
3760
+ issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
3761
+ return {
3762
+ issues,
3763
+ total_issues: issues.length,
3764
+ scanned_at: new Date().toISOString()
3765
+ };
3766
+ }
3767
+ function getReviewQueue(opts, db) {
3768
+ const d = db || getDatabase();
3769
+ let sql = `SELECT * FROM tasks WHERE status = 'completed' AND (
3770
+ (requires_approval = 1 AND approved_by IS NULL)
3771
+ OR (confidence IS NOT NULL AND confidence < 0.5)
3772
+ )`;
3773
+ const params = [];
3774
+ if (opts?.project_id) {
3775
+ sql += " AND project_id = ?";
3776
+ params.push(opts.project_id);
3777
+ }
3778
+ sql += " ORDER BY CASE WHEN requires_approval = 1 AND approved_by IS NULL THEN 0 ELSE 1 END, confidence ASC";
3779
+ if (opts?.limit) {
3780
+ sql += " LIMIT ?";
3781
+ params.push(opts.limit);
3782
+ }
3783
+ return d.query(sql).all(...params).map(rowToTask2);
3784
+ }
3785
+ // src/db/agent-metrics.ts
3786
+ function getAgentMetrics(agentId, opts, db) {
3787
+ const d = db || getDatabase();
3788
+ const agent = d.query("SELECT id, name FROM agents WHERE id = ? OR LOWER(name) = LOWER(?)").get(agentId, agentId);
3789
+ if (!agent)
3790
+ return null;
3791
+ let projectFilter = "";
3792
+ const params = [agent.id, agent.id];
3793
+ if (opts?.project_id) {
3794
+ projectFilter = " AND project_id = ?";
3795
+ params.push(opts.project_id);
3796
+ }
3797
+ const completed = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed'${projectFilter}`).get(...params).count;
3798
+ const failed = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'failed'${projectFilter}`).get(...params).count;
3799
+ const inProgress = d.query(`SELECT COUNT(*) as count FROM tasks WHERE (agent_id = ? OR assigned_to = ?) AND status = 'in_progress'${projectFilter}`).get(...params).count;
3800
+ const total = completed + failed;
3801
+ const completionRate = total > 0 ? completed / total : 0;
3802
+ const avgTime = d.query(`SELECT AVG(
3803
+ (julianday(completed_at) - julianday(created_at)) * 24 * 60
3804
+ ) as avg_minutes
3805
+ FROM tasks
3806
+ WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed' AND completed_at IS NOT NULL${projectFilter}`).get(...params);
3807
+ const avgConf = d.query(`SELECT AVG(confidence) as avg_confidence
3808
+ FROM tasks
3809
+ WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed' AND confidence IS NOT NULL${projectFilter}`).get(...params);
3810
+ const reviewTasks = d.query(`SELECT metadata FROM tasks
3811
+ WHERE (agent_id = ? OR assigned_to = ?) AND status = 'completed'${projectFilter}
3812
+ AND metadata LIKE '%_review_score%'`).all(...params);
3813
+ let reviewScoreAvg = null;
3814
+ if (reviewTasks.length > 0) {
3815
+ let total2 = 0;
3816
+ let count = 0;
3817
+ for (const row of reviewTasks) {
3818
+ try {
3819
+ const meta = JSON.parse(row.metadata);
3820
+ if (typeof meta._review_score === "number") {
3821
+ total2 += meta._review_score;
3822
+ count++;
3823
+ }
3824
+ } catch {}
3825
+ }
3826
+ if (count > 0)
3827
+ reviewScoreAvg = total2 / count;
3828
+ }
3829
+ const speedScore = avgTime?.avg_minutes != null ? Math.max(0, 1 - avgTime.avg_minutes / (60 * 24)) : 0.5;
3830
+ const confidenceScore = avgConf?.avg_confidence ?? 0.5;
3831
+ const volumeScore = Math.min(1, completed / 50);
3832
+ const compositeScore = completionRate * 0.3 + speedScore * 0.2 + confidenceScore * 0.3 + volumeScore * 0.2;
3833
+ return {
3834
+ agent_id: agent.id,
3835
+ agent_name: agent.name,
3836
+ tasks_completed: completed,
3837
+ tasks_failed: failed,
3838
+ tasks_in_progress: inProgress,
3839
+ completion_rate: Math.round(completionRate * 1000) / 1000,
3840
+ avg_completion_minutes: avgTime?.avg_minutes != null ? Math.round(avgTime.avg_minutes * 10) / 10 : null,
3841
+ avg_confidence: avgConf?.avg_confidence != null ? Math.round(avgConf.avg_confidence * 1000) / 1000 : null,
3842
+ review_score_avg: reviewScoreAvg != null ? Math.round(reviewScoreAvg * 1000) / 1000 : null,
3843
+ composite_score: Math.round(compositeScore * 1000) / 1000
3844
+ };
3845
+ }
3846
+ function getLeaderboard(opts, db) {
3847
+ const d = db || getDatabase();
3848
+ const agents = d.query("SELECT id FROM agents ORDER BY name").all();
3849
+ const entries = [];
3850
+ for (const agent of agents) {
3851
+ const metrics = getAgentMetrics(agent.id, { project_id: opts?.project_id }, d);
3852
+ if (metrics && (metrics.tasks_completed > 0 || metrics.tasks_failed > 0 || metrics.tasks_in_progress > 0)) {
3853
+ entries.push(metrics);
3854
+ }
3855
+ }
3856
+ entries.sort((a, b) => b.composite_score - a.composite_score);
3857
+ const limit = opts?.limit || 20;
3858
+ return entries.slice(0, limit).map((entry, idx) => ({
3859
+ ...entry,
3860
+ rank: idx + 1
3861
+ }));
3862
+ }
3863
+ function scoreTask(taskId, score, reviewerId, db) {
3864
+ const d = db || getDatabase();
3865
+ if (score < 0 || score > 1)
3866
+ throw new Error("Score must be between 0 and 1");
3867
+ const task = d.query("SELECT metadata FROM tasks WHERE id = ?").get(taskId);
3868
+ if (!task)
3869
+ throw new Error(`Task not found: ${taskId}`);
3870
+ const metadata = JSON.parse(task.metadata || "{}");
3871
+ metadata._review_score = score;
3872
+ if (reviewerId)
3873
+ metadata._reviewed_by = reviewerId;
3874
+ metadata._reviewed_at = now();
3875
+ d.run("UPDATE tasks SET metadata = ?, updated_at = ? WHERE id = ?", [JSON.stringify(metadata), now(), taskId]);
3876
+ }
3877
+ // src/lib/search.ts
3878
+ function rowToTask3(row) {
3271
3879
  return {
3272
3880
  ...row,
3273
3881
  tags: JSON.parse(row.tags || "[]"),
@@ -3367,7 +3975,7 @@ function searchTasks(options, projectId, taskListId, db) {
3367
3975
  t.created_at DESC`;
3368
3976
  }
3369
3977
  const rows = d.query(sql).all(...params);
3370
- return rows.map(rowToTask2);
3978
+ return rows.map(rowToTask3);
3371
3979
  }
3372
3980
  // src/lib/claude-tasks.ts
3373
3981
  import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -3901,25 +4509,32 @@ export {
3901
4509
  taskFromTemplate,
3902
4510
  syncWithAgents,
3903
4511
  syncWithAgent,
4512
+ syncKgEdges,
3904
4513
  startTask,
3905
4514
  slugify,
3906
4515
  setTaskStatus,
3907
4516
  setTaskPriority,
3908
4517
  searchTasks,
4518
+ scoreTask,
3909
4519
  resolvePartialId,
3910
4520
  resetDatabase,
4521
+ removeTaskRelationshipByPair,
4522
+ removeTaskRelationship,
3911
4523
  removeTaskFile,
3912
4524
  removeProjectSource,
4525
+ removeKgEdges,
3913
4526
  removeDependency,
3914
4527
  removeChecklistItem,
3915
4528
  releaseLock,
3916
4529
  registerAgent,
3917
4530
  redistributeStaleTasks,
4531
+ patrolTasks,
3918
4532
  parseRecurrenceRule,
3919
4533
  now,
3920
4534
  nextTaskShortId,
3921
4535
  nextOccurrence,
3922
4536
  moveTask,
4537
+ matchCapabilities,
3923
4538
  logTaskChange,
3924
4539
  logProgress,
3925
4540
  lockTask,
@@ -3944,6 +4559,8 @@ export {
3944
4559
  getTasksChangedSince,
3945
4560
  getTaskWithRelations,
3946
4561
  getTaskStats,
4562
+ getTaskRelationships,
4563
+ getTaskRelationship,
3947
4564
  getTaskListBySlug,
3948
4565
  getTaskList,
3949
4566
  getTaskHistory,
@@ -3955,6 +4572,8 @@ export {
3955
4572
  getStatus,
3956
4573
  getStaleTasks,
3957
4574
  getSession,
4575
+ getReviewQueue,
4576
+ getRelated,
3958
4577
  getRecentActivity,
3959
4578
  getProjectWithSources,
3960
4579
  getProjectByPath,
@@ -3965,18 +4584,25 @@ export {
3965
4584
  getOrgByName,
3966
4585
  getOrg,
3967
4586
  getNextTask,
4587
+ getLeaderboard,
3968
4588
  getLatestHandoff,
4589
+ getImpactAnalysis,
3969
4590
  getDirectReports,
3970
4591
  getDatabase,
4592
+ getCriticalPath,
3971
4593
  getCompletionGuardConfig,
3972
4594
  getComment,
3973
4595
  getChecklistStats,
3974
4596
  getChecklist,
4597
+ getCapableAgents,
3975
4598
  getBlockingDeps,
4599
+ getAgentMetrics,
3976
4600
  getAgentByName,
3977
4601
  getAgent,
3978
4602
  getActiveWork,
3979
4603
  findTasksByFile,
4604
+ findRelatedTaskIds,
4605
+ findPath,
3980
4606
  failTask,
3981
4607
  ensureTaskList,
3982
4608
  ensureProject,
@@ -4016,8 +4642,11 @@ export {
4016
4642
  bulkUpdateTasks,
4017
4643
  bulkCreateTasks,
4018
4644
  bulkAddTaskFiles,
4645
+ autoDetectFileRelationships,
4646
+ addTaskRelationship,
4019
4647
  addTaskFile,
4020
4648
  addProjectSource,
4649
+ addKgEdge,
4021
4650
  addDependency,
4022
4651
  addComment,
4023
4652
  addChecklistItem,
@@ -4028,6 +4657,7 @@ export {
4028
4657
  TaskListNotFoundError,
4029
4658
  TASK_STATUSES,
4030
4659
  TASK_PRIORITIES,
4660
+ RELATIONSHIP_TYPES,
4031
4661
  ProjectNotFoundError,
4032
4662
  PlanNotFoundError,
4033
4663
  PLAN_STATUSES,