@hasna/todos 0.9.34 → 0.9.35

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
@@ -283,6 +283,13 @@ var MIGRATIONS = [
283
283
  ALTER TABLE agents ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
284
284
  ALTER TABLE projects ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
285
285
  INSERT OR IGNORE INTO _migrations (id) VALUES (12);
286
+ `,
287
+ `
288
+ ALTER TABLE tasks ADD COLUMN recurrence_rule TEXT;
289
+ ALTER TABLE tasks ADD COLUMN recurrence_parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL;
290
+ CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id);
291
+ CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL;
292
+ INSERT OR IGNORE INTO _migrations (id) VALUES (13);
286
293
  `
287
294
  ];
288
295
  var _db = null;
@@ -409,6 +416,8 @@ function ensureSchema(db) {
409
416
  ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
410
417
  ensureColumn("tasks", "approved_by", "TEXT");
411
418
  ensureColumn("tasks", "approved_at", "TEXT");
419
+ ensureColumn("tasks", "recurrence_rule", "TEXT");
420
+ ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
412
421
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
413
422
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
414
423
  ensureColumn("agents", "reports_to", "TEXT");
@@ -433,6 +442,8 @@ function ensureSchema(db) {
433
442
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
434
443
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
435
444
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
445
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
446
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
436
447
  }
437
448
  function backfillTaskTags(db) {
438
449
  try {
@@ -533,6 +544,8 @@ class VersionConflictError extends Error {
533
544
  taskId;
534
545
  expectedVersion;
535
546
  actualVersion;
547
+ static code = "VERSION_CONFLICT";
548
+ static suggestion = "Fetch the task with get_task to get the current version before updating.";
536
549
  constructor(taskId, expectedVersion, actualVersion) {
537
550
  super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
538
551
  this.taskId = taskId;
@@ -544,6 +557,8 @@ class VersionConflictError extends Error {
544
557
 
545
558
  class TaskNotFoundError extends Error {
546
559
  taskId;
560
+ static code = "TASK_NOT_FOUND";
561
+ static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
547
562
  constructor(taskId) {
548
563
  super(`Task not found: ${taskId}`);
549
564
  this.taskId = taskId;
@@ -553,6 +568,8 @@ class TaskNotFoundError extends Error {
553
568
 
554
569
  class ProjectNotFoundError extends Error {
555
570
  projectId;
571
+ static code = "PROJECT_NOT_FOUND";
572
+ static suggestion = "Use list_projects to see available projects.";
556
573
  constructor(projectId) {
557
574
  super(`Project not found: ${projectId}`);
558
575
  this.projectId = projectId;
@@ -562,6 +579,8 @@ class ProjectNotFoundError extends Error {
562
579
 
563
580
  class PlanNotFoundError extends Error {
564
581
  planId;
582
+ static code = "PLAN_NOT_FOUND";
583
+ static suggestion = "Use list_plans to see available plans.";
565
584
  constructor(planId) {
566
585
  super(`Plan not found: ${planId}`);
567
586
  this.planId = planId;
@@ -572,6 +591,8 @@ class PlanNotFoundError extends Error {
572
591
  class LockError extends Error {
573
592
  taskId;
574
593
  lockedBy;
594
+ static code = "LOCK_ERROR";
595
+ static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
575
596
  constructor(taskId, lockedBy) {
576
597
  super(`Task ${taskId} is locked by ${lockedBy}`);
577
598
  this.taskId = taskId;
@@ -582,6 +603,8 @@ class LockError extends Error {
582
603
 
583
604
  class AgentNotFoundError extends Error {
584
605
  agentId;
606
+ static code = "AGENT_NOT_FOUND";
607
+ static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
585
608
  constructor(agentId) {
586
609
  super(`Agent not found: ${agentId}`);
587
610
  this.agentId = agentId;
@@ -591,6 +614,8 @@ class AgentNotFoundError extends Error {
591
614
 
592
615
  class TaskListNotFoundError extends Error {
593
616
  taskListId;
617
+ static code = "TASK_LIST_NOT_FOUND";
618
+ static suggestion = "Use list_task_lists to see available lists.";
594
619
  constructor(taskListId) {
595
620
  super(`Task list not found: ${taskListId}`);
596
621
  this.taskListId = taskListId;
@@ -601,6 +626,8 @@ class TaskListNotFoundError extends Error {
601
626
  class DependencyCycleError extends Error {
602
627
  taskId;
603
628
  dependsOn;
629
+ static code = "DEPENDENCY_CYCLE";
630
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
604
631
  constructor(taskId, dependsOn) {
605
632
  super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
606
633
  this.taskId = taskId;
@@ -612,6 +639,8 @@ class DependencyCycleError extends Error {
612
639
  class CompletionGuardError extends Error {
613
640
  reason;
614
641
  retryAfterSeconds;
642
+ static code = "COMPLETION_BLOCKED";
643
+ static suggestion = "Wait for the cooldown period, then retry.";
615
644
  constructor(reason, retryAfterSeconds) {
616
645
  super(reason);
617
646
  this.reason = reason;
@@ -891,6 +920,101 @@ function getRecentActivity(limit = 50, db) {
891
920
  return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
892
921
  }
893
922
 
923
+ // src/lib/recurrence.ts
924
+ var DAY_NAMES = {
925
+ sunday: 0,
926
+ sun: 0,
927
+ monday: 1,
928
+ mon: 1,
929
+ tuesday: 2,
930
+ tue: 2,
931
+ wednesday: 3,
932
+ wed: 3,
933
+ thursday: 4,
934
+ thu: 4,
935
+ friday: 5,
936
+ fri: 5,
937
+ saturday: 6,
938
+ sat: 6
939
+ };
940
+ function parseRecurrenceRule(rule) {
941
+ const normalized = rule.trim().toLowerCase();
942
+ if (normalized === "every weekday" || normalized === "every weekdays") {
943
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
944
+ }
945
+ if (normalized === "every day" || normalized === "daily") {
946
+ return { type: "interval", interval: 1, unit: "day" };
947
+ }
948
+ if (normalized === "every week" || normalized === "weekly") {
949
+ return { type: "interval", interval: 1, unit: "week" };
950
+ }
951
+ if (normalized === "every month" || normalized === "monthly") {
952
+ return { type: "interval", interval: 1, unit: "month" };
953
+ }
954
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
955
+ if (intervalMatch) {
956
+ return {
957
+ type: "interval",
958
+ interval: parseInt(intervalMatch[1], 10),
959
+ unit: intervalMatch[2]
960
+ };
961
+ }
962
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
963
+ if (daysMatch) {
964
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
965
+ const days = [];
966
+ for (const part of dayParts) {
967
+ const dayNum = DAY_NAMES[part];
968
+ if (dayNum !== undefined) {
969
+ days.push(dayNum);
970
+ }
971
+ }
972
+ if (days.length > 0) {
973
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
974
+ }
975
+ }
976
+ throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
977
+ }
978
+ function isValidRecurrenceRule(rule) {
979
+ try {
980
+ parseRecurrenceRule(rule);
981
+ return true;
982
+ } catch {
983
+ return false;
984
+ }
985
+ }
986
+ function nextOccurrence(rule, from) {
987
+ const parsed = parseRecurrenceRule(rule);
988
+ const base = from || new Date;
989
+ if (parsed.type === "interval") {
990
+ const next = new Date(base);
991
+ if (parsed.unit === "day") {
992
+ next.setDate(next.getDate() + parsed.interval);
993
+ } else if (parsed.unit === "week") {
994
+ next.setDate(next.getDate() + parsed.interval * 7);
995
+ } else if (parsed.unit === "month") {
996
+ next.setMonth(next.getMonth() + parsed.interval);
997
+ }
998
+ return next.toISOString();
999
+ }
1000
+ if (parsed.type === "specific_days") {
1001
+ const currentDay = base.getDay();
1002
+ const days = parsed.days;
1003
+ let daysToAdd = Infinity;
1004
+ for (const day of days) {
1005
+ let diff = day - currentDay;
1006
+ if (diff <= 0)
1007
+ diff += 7;
1008
+ if (diff < daysToAdd)
1009
+ daysToAdd = diff;
1010
+ }
1011
+ const next = new Date(base);
1012
+ next.setDate(next.getDate() + daysToAdd);
1013
+ return next.toISOString();
1014
+ }
1015
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
1016
+ }
1017
+
894
1018
  // src/db/tasks.ts
895
1019
  function rowToTask(row) {
896
1020
  return {
@@ -922,8 +1046,8 @@ function createTask(input, db) {
922
1046
  const tags = input.tags || [];
923
1047
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
924
1048
  const title = shortId ? `${shortId}: ${input.title}` : input.title;
925
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at)
926
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)`, [
1049
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id)
1050
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
927
1051
  id,
928
1052
  shortId,
929
1053
  input.project_id || null,
@@ -946,7 +1070,9 @@ function createTask(input, db) {
946
1070
  input.estimated_minutes || null,
947
1071
  input.requires_approval ? 1 : 0,
948
1072
  null,
949
- null
1073
+ null,
1074
+ input.recurrence_rule || null,
1075
+ input.recurrence_parent_id || null
950
1076
  ]);
951
1077
  if (tags.length > 0) {
952
1078
  insertTaskTags(id, tags, d);
@@ -1046,6 +1172,11 @@ function listTasks(filter = {}, db) {
1046
1172
  conditions.push("task_list_id = ?");
1047
1173
  params.push(filter.task_list_id);
1048
1174
  }
1175
+ if (filter.has_recurrence === true) {
1176
+ conditions.push("recurrence_rule IS NOT NULL");
1177
+ } else if (filter.has_recurrence === false) {
1178
+ conditions.push("recurrence_rule IS NULL");
1179
+ }
1049
1180
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1050
1181
  let limitClause = "";
1051
1182
  if (filter.limit) {
@@ -1061,6 +1192,69 @@ function listTasks(filter = {}, db) {
1061
1192
  created_at DESC${limitClause}`).all(...params);
1062
1193
  return rows.map(rowToTask);
1063
1194
  }
1195
+ function countTasks(filter = {}, db) {
1196
+ const d = db || getDatabase();
1197
+ const conditions = [];
1198
+ const params = [];
1199
+ if (filter.project_id) {
1200
+ conditions.push("project_id = ?");
1201
+ params.push(filter.project_id);
1202
+ }
1203
+ if (filter.parent_id !== undefined) {
1204
+ if (filter.parent_id === null) {
1205
+ conditions.push("parent_id IS NULL");
1206
+ } else {
1207
+ conditions.push("parent_id = ?");
1208
+ params.push(filter.parent_id);
1209
+ }
1210
+ }
1211
+ if (filter.status) {
1212
+ if (Array.isArray(filter.status)) {
1213
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1214
+ params.push(...filter.status);
1215
+ } else {
1216
+ conditions.push("status = ?");
1217
+ params.push(filter.status);
1218
+ }
1219
+ }
1220
+ if (filter.priority) {
1221
+ if (Array.isArray(filter.priority)) {
1222
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
1223
+ params.push(...filter.priority);
1224
+ } else {
1225
+ conditions.push("priority = ?");
1226
+ params.push(filter.priority);
1227
+ }
1228
+ }
1229
+ if (filter.assigned_to) {
1230
+ conditions.push("assigned_to = ?");
1231
+ params.push(filter.assigned_to);
1232
+ }
1233
+ if (filter.agent_id) {
1234
+ conditions.push("agent_id = ?");
1235
+ params.push(filter.agent_id);
1236
+ }
1237
+ if (filter.session_id) {
1238
+ conditions.push("session_id = ?");
1239
+ params.push(filter.session_id);
1240
+ }
1241
+ if (filter.tags && filter.tags.length > 0) {
1242
+ const placeholders = filter.tags.map(() => "?").join(",");
1243
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1244
+ params.push(...filter.tags);
1245
+ }
1246
+ if (filter.plan_id) {
1247
+ conditions.push("plan_id = ?");
1248
+ params.push(filter.plan_id);
1249
+ }
1250
+ if (filter.task_list_id) {
1251
+ conditions.push("task_list_id = ?");
1252
+ params.push(filter.task_list_id);
1253
+ }
1254
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1255
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
1256
+ return row.count;
1257
+ }
1064
1258
  function updateTask(id, input, db) {
1065
1259
  const d = db || getDatabase();
1066
1260
  const task = getTask(id, d);
@@ -1132,6 +1326,10 @@ function updateTask(id, input, db) {
1132
1326
  sets.push("approved_at = ?");
1133
1327
  params.push(now());
1134
1328
  }
1329
+ if (input.recurrence_rule !== undefined) {
1330
+ sets.push("recurrence_rule = ?");
1331
+ params.push(input.recurrence_rule);
1332
+ }
1135
1333
  params.push(id, input.version);
1136
1334
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
1137
1335
  if (result.changes === 0) {
@@ -1205,7 +1403,7 @@ function startTask(id, agentId, db) {
1205
1403
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
1206
1404
  return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
1207
1405
  }
1208
- function completeTask(id, agentId, db, evidence) {
1406
+ function completeTask(id, agentId, db, options) {
1209
1407
  const d = db || getDatabase();
1210
1408
  const task = getTask(id, d);
1211
1409
  if (!task)
@@ -1214,7 +1412,9 @@ function completeTask(id, agentId, db, evidence) {
1214
1412
  throw new LockError(id, task.locked_by);
1215
1413
  }
1216
1414
  checkCompletionGuard(task, agentId || null, d);
1217
- if (evidence) {
1415
+ const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes } : undefined;
1416
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes);
1417
+ if (hasEvidence) {
1218
1418
  const meta2 = { ...task.metadata, _evidence: evidence };
1219
1419
  d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
1220
1420
  }
@@ -1222,7 +1422,14 @@ function completeTask(id, agentId, db, evidence) {
1222
1422
  d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
1223
1423
  WHERE id = ?`, [timestamp, timestamp, id]);
1224
1424
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
1225
- const meta = evidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
1425
+ let spawnedTask = null;
1426
+ if (task.recurrence_rule && !options?.skip_recurrence) {
1427
+ spawnedTask = spawnNextRecurrence(task, d);
1428
+ }
1429
+ const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
1430
+ if (spawnedTask) {
1431
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
1432
+ }
1226
1433
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
1227
1434
  }
1228
1435
  function lockTask(id, agentId, db) {
@@ -1289,6 +1496,266 @@ function getTaskDependents(taskId, db) {
1289
1496
  const d = db || getDatabase();
1290
1497
  return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
1291
1498
  }
1499
+ function cloneTask(taskId, overrides, db) {
1500
+ const d = db || getDatabase();
1501
+ const source = getTask(taskId, d);
1502
+ if (!source)
1503
+ throw new TaskNotFoundError(taskId);
1504
+ const input = {
1505
+ title: overrides?.title ?? source.title,
1506
+ description: overrides?.description ?? source.description ?? undefined,
1507
+ priority: overrides?.priority ?? source.priority,
1508
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
1509
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
1510
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
1511
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
1512
+ status: overrides?.status ?? "pending",
1513
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
1514
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
1515
+ tags: overrides?.tags ?? source.tags,
1516
+ metadata: overrides?.metadata ?? source.metadata,
1517
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
1518
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
1519
+ };
1520
+ return createTask(input, d);
1521
+ }
1522
+ function getTaskGraph(taskId, direction = "both", db) {
1523
+ const d = db || getDatabase();
1524
+ const task = getTask(taskId, d);
1525
+ if (!task)
1526
+ throw new TaskNotFoundError(taskId);
1527
+ function toNode(t) {
1528
+ const deps = getTaskDependencies(t.id, d);
1529
+ const hasUnfinishedDeps = deps.some((dep) => {
1530
+ const depTask = getTask(dep.depends_on, d);
1531
+ return depTask && depTask.status !== "completed";
1532
+ });
1533
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
1534
+ }
1535
+ function buildUp(id, visited) {
1536
+ if (visited.has(id))
1537
+ return [];
1538
+ visited.add(id);
1539
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
1540
+ return deps.map((dep) => {
1541
+ const depTask = getTask(dep.depends_on, d);
1542
+ if (!depTask)
1543
+ return null;
1544
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
1545
+ }).filter(Boolean);
1546
+ }
1547
+ function buildDown(id, visited) {
1548
+ if (visited.has(id))
1549
+ return [];
1550
+ visited.add(id);
1551
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
1552
+ return dependents.map((dep) => {
1553
+ const depTask = getTask(dep.task_id, d);
1554
+ if (!depTask)
1555
+ return null;
1556
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
1557
+ }).filter(Boolean);
1558
+ }
1559
+ const rootNode = toNode(task);
1560
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
1561
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
1562
+ return { task: rootNode, depends_on, blocks };
1563
+ }
1564
+ function moveTask(taskId, target, db) {
1565
+ const d = db || getDatabase();
1566
+ const task = getTask(taskId, d);
1567
+ if (!task)
1568
+ throw new TaskNotFoundError(taskId);
1569
+ const sets = ["updated_at = ?", "version = version + 1"];
1570
+ const params = [now()];
1571
+ if (target.task_list_id !== undefined) {
1572
+ sets.push("task_list_id = ?");
1573
+ params.push(target.task_list_id);
1574
+ }
1575
+ if (target.project_id !== undefined) {
1576
+ sets.push("project_id = ?");
1577
+ params.push(target.project_id);
1578
+ }
1579
+ if (target.plan_id !== undefined) {
1580
+ sets.push("plan_id = ?");
1581
+ params.push(target.plan_id);
1582
+ }
1583
+ params.push(taskId);
1584
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
1585
+ return getTask(taskId, d);
1586
+ }
1587
+ function spawnNextRecurrence(completedTask, db) {
1588
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
1589
+ let title = completedTask.title;
1590
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
1591
+ title = title.slice(completedTask.short_id.length + 2);
1592
+ }
1593
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
1594
+ return createTask({
1595
+ title,
1596
+ description: completedTask.description ?? undefined,
1597
+ priority: completedTask.priority,
1598
+ project_id: completedTask.project_id ?? undefined,
1599
+ task_list_id: completedTask.task_list_id ?? undefined,
1600
+ plan_id: completedTask.plan_id ?? undefined,
1601
+ assigned_to: completedTask.assigned_to ?? undefined,
1602
+ tags: completedTask.tags,
1603
+ metadata: completedTask.metadata,
1604
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
1605
+ recurrence_rule: completedTask.recurrence_rule,
1606
+ recurrence_parent_id: recurrenceParentId,
1607
+ due_at: dueAt
1608
+ }, db);
1609
+ }
1610
+ function claimNextTask(agentId, filters, db) {
1611
+ const d = db || getDatabase();
1612
+ const tx = d.transaction(() => {
1613
+ const task = getNextTask(agentId, filters, d);
1614
+ if (!task)
1615
+ return null;
1616
+ return startTask(task.id, agentId, d);
1617
+ });
1618
+ return tx();
1619
+ }
1620
+ function getNextTask(agentId, filters, db) {
1621
+ const d = db || getDatabase();
1622
+ clearExpiredLocks(d);
1623
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
1624
+ const params = [lockExpiryCutoff()];
1625
+ if (filters?.project_id) {
1626
+ conditions.push("project_id = ?");
1627
+ params.push(filters.project_id);
1628
+ }
1629
+ if (filters?.task_list_id) {
1630
+ conditions.push("task_list_id = ?");
1631
+ params.push(filters.task_list_id);
1632
+ }
1633
+ if (filters?.plan_id) {
1634
+ conditions.push("plan_id = ?");
1635
+ params.push(filters.plan_id);
1636
+ }
1637
+ if (filters?.tags && filters.tags.length > 0) {
1638
+ const placeholders = filters.tags.map(() => "?").join(",");
1639
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1640
+ params.push(...filters.tags);
1641
+ }
1642
+ conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
1643
+ const where = conditions.join(" AND ");
1644
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
1645
+ if (agentId) {
1646
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
1647
+ params.push(agentId);
1648
+ }
1649
+ sql += `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at ASC LIMIT 1`;
1650
+ const row = d.query(sql).get(...params);
1651
+ return row ? rowToTask(row) : null;
1652
+ }
1653
+ function getActiveWork(filters, db) {
1654
+ const d = db || getDatabase();
1655
+ clearExpiredLocks(d);
1656
+ const conditions = ["status = 'in_progress'"];
1657
+ const params = [];
1658
+ if (filters?.project_id) {
1659
+ conditions.push("project_id = ?");
1660
+ params.push(filters.project_id);
1661
+ }
1662
+ if (filters?.task_list_id) {
1663
+ conditions.push("task_list_id = ?");
1664
+ params.push(filters.task_list_id);
1665
+ }
1666
+ const where = conditions.join(" AND ");
1667
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
1668
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
1669
+ updated_at DESC`).all(...params);
1670
+ return rows;
1671
+ }
1672
+ function getTasksChangedSince(since, filters, db) {
1673
+ const d = db || getDatabase();
1674
+ const conditions = ["updated_at > ?"];
1675
+ const params = [since];
1676
+ if (filters?.project_id) {
1677
+ conditions.push("project_id = ?");
1678
+ params.push(filters.project_id);
1679
+ }
1680
+ if (filters?.task_list_id) {
1681
+ conditions.push("task_list_id = ?");
1682
+ params.push(filters.task_list_id);
1683
+ }
1684
+ const where = conditions.join(" AND ");
1685
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
1686
+ return rows.map(rowToTask);
1687
+ }
1688
+ function failTask(id, agentId, reason, options, db) {
1689
+ const d = db || getDatabase();
1690
+ const task = getTask(id, d);
1691
+ if (!task)
1692
+ throw new TaskNotFoundError(id);
1693
+ const meta = {
1694
+ ...task.metadata,
1695
+ _failure: {
1696
+ reason: reason || "Unknown failure",
1697
+ error_code: options?.error_code || null,
1698
+ failed_by: agentId || null,
1699
+ failed_at: now(),
1700
+ retry_requested: options?.retry || false
1701
+ }
1702
+ };
1703
+ const timestamp = now();
1704
+ d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
1705
+ WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
1706
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
1707
+ const failedTask = {
1708
+ ...task,
1709
+ status: "failed",
1710
+ locked_by: null,
1711
+ locked_at: null,
1712
+ metadata: meta,
1713
+ version: task.version + 1,
1714
+ updated_at: timestamp
1715
+ };
1716
+ let retryTask;
1717
+ if (options?.retry) {
1718
+ let title = task.title;
1719
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
1720
+ title = title.slice(task.short_id.length + 2);
1721
+ }
1722
+ retryTask = createTask({
1723
+ title,
1724
+ description: task.description ?? undefined,
1725
+ priority: task.priority,
1726
+ project_id: task.project_id ?? undefined,
1727
+ task_list_id: task.task_list_id ?? undefined,
1728
+ plan_id: task.plan_id ?? undefined,
1729
+ assigned_to: task.assigned_to ?? undefined,
1730
+ tags: task.tags,
1731
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
1732
+ estimated_minutes: task.estimated_minutes ?? undefined,
1733
+ recurrence_rule: task.recurrence_rule ?? undefined,
1734
+ due_at: options.retry_after || task.due_at || undefined
1735
+ }, d);
1736
+ }
1737
+ return { task: failedTask, retryTask };
1738
+ }
1739
+ function getStaleTasks(staleMinutes = 30, filters, db) {
1740
+ const d = db || getDatabase();
1741
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
1742
+ const conditions = [
1743
+ "status = 'in_progress'",
1744
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
1745
+ ];
1746
+ const params = [cutoff, cutoff];
1747
+ if (filters?.project_id) {
1748
+ conditions.push("project_id = ?");
1749
+ params.push(filters.project_id);
1750
+ }
1751
+ if (filters?.task_list_id) {
1752
+ conditions.push("task_list_id = ?");
1753
+ params.push(filters.task_list_id);
1754
+ }
1755
+ const where = conditions.join(" AND ");
1756
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
1757
+ return rows.map(rowToTask);
1758
+ }
1292
1759
  function wouldCreateCycle(taskId, dependsOn, db) {
1293
1760
  const visited = new Set;
1294
1761
  const queue = [dependsOn];
@@ -1306,6 +1773,91 @@ function wouldCreateCycle(taskId, dependsOn, db) {
1306
1773
  }
1307
1774
  return false;
1308
1775
  }
1776
+ function getTaskStats(filters, db) {
1777
+ const d = db || getDatabase();
1778
+ const conditions = [];
1779
+ const params = [];
1780
+ if (filters?.project_id) {
1781
+ conditions.push("project_id = ?");
1782
+ params.push(filters.project_id);
1783
+ }
1784
+ if (filters?.task_list_id) {
1785
+ conditions.push("task_list_id = ?");
1786
+ params.push(filters.task_list_id);
1787
+ }
1788
+ if (filters?.agent_id) {
1789
+ conditions.push("(agent_id = ? OR assigned_to = ?)");
1790
+ params.push(filters.agent_id, filters.agent_id);
1791
+ }
1792
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1793
+ const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
1794
+ const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
1795
+ const by_status = {};
1796
+ for (const r of statusRows)
1797
+ by_status[r.status] = r.count;
1798
+ const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
1799
+ const by_priority = {};
1800
+ for (const r of priorityRows)
1801
+ by_priority[r.priority] = r.count;
1802
+ const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
1803
+ const by_agent = {};
1804
+ for (const r of agentRows)
1805
+ by_agent[r.agent] = r.count;
1806
+ const completed = by_status["completed"] || 0;
1807
+ const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
1808
+ return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
1809
+ }
1810
+ function bulkCreateTasks(inputs, db) {
1811
+ const d = db || getDatabase();
1812
+ const tempIdToRealId = new Map;
1813
+ const created = [];
1814
+ const tx = d.transaction(() => {
1815
+ for (const input of inputs) {
1816
+ const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
1817
+ const task = createTask(createInput, d);
1818
+ if (temp_id)
1819
+ tempIdToRealId.set(temp_id, task.id);
1820
+ created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
1821
+ }
1822
+ for (const input of inputs) {
1823
+ if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
1824
+ const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
1825
+ if (!taskId)
1826
+ continue;
1827
+ for (const depTempId of input.depends_on_temp_ids) {
1828
+ const depRealId = tempIdToRealId.get(depTempId);
1829
+ if (depRealId) {
1830
+ addDependency(taskId, depRealId, d);
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+ });
1836
+ tx();
1837
+ return { created };
1838
+ }
1839
+ function bulkUpdateTasks(taskIds, updates, db) {
1840
+ const d = db || getDatabase();
1841
+ let updated = 0;
1842
+ const failed = [];
1843
+ const tx = d.transaction(() => {
1844
+ for (const id of taskIds) {
1845
+ try {
1846
+ const task = getTask(id, d);
1847
+ if (!task) {
1848
+ failed.push({ id, error: "Task not found" });
1849
+ continue;
1850
+ }
1851
+ updateTask(id, { ...updates, version: task.version }, d);
1852
+ updated++;
1853
+ } catch (e) {
1854
+ failed.push({ id, error: e instanceof Error ? e.message : String(e) });
1855
+ }
1856
+ }
1857
+ });
1858
+ tx();
1859
+ return { updated, failed };
1860
+ }
1309
1861
  // src/db/plans.ts
1310
1862
  function createPlan(input, db) {
1311
1863
  const d = db || getDatabase();
@@ -1811,19 +2363,64 @@ function rowToTask2(row) {
1811
2363
  requires_approval: Boolean(row.requires_approval)
1812
2364
  };
1813
2365
  }
1814
- function searchTasks(query, projectId, taskListId, db) {
2366
+ function searchTasks(options, projectId, taskListId, db) {
2367
+ const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
1815
2368
  const d = db || getDatabase();
1816
2369
  clearExpiredLocks(d);
1817
- const pattern = `%${query}%`;
2370
+ const pattern = `%${opts.query}%`;
1818
2371
  let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = tasks.id AND tag LIKE ?))`;
1819
2372
  const params = [pattern, pattern, pattern];
1820
- if (projectId) {
2373
+ if (opts.project_id) {
1821
2374
  sql += " AND project_id = ?";
1822
- params.push(projectId);
2375
+ params.push(opts.project_id);
1823
2376
  }
1824
- if (taskListId) {
2377
+ if (opts.task_list_id) {
1825
2378
  sql += " AND task_list_id = ?";
1826
- params.push(taskListId);
2379
+ params.push(opts.task_list_id);
2380
+ }
2381
+ if (opts.status) {
2382
+ if (Array.isArray(opts.status)) {
2383
+ sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
2384
+ params.push(...opts.status);
2385
+ } else {
2386
+ sql += " AND status = ?";
2387
+ params.push(opts.status);
2388
+ }
2389
+ }
2390
+ if (opts.priority) {
2391
+ if (Array.isArray(opts.priority)) {
2392
+ sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
2393
+ params.push(...opts.priority);
2394
+ } else {
2395
+ sql += " AND priority = ?";
2396
+ params.push(opts.priority);
2397
+ }
2398
+ }
2399
+ if (opts.assigned_to) {
2400
+ sql += " AND assigned_to = ?";
2401
+ params.push(opts.assigned_to);
2402
+ }
2403
+ if (opts.agent_id) {
2404
+ sql += " AND agent_id = ?";
2405
+ params.push(opts.agent_id);
2406
+ }
2407
+ if (opts.created_after) {
2408
+ sql += " AND created_at > ?";
2409
+ params.push(opts.created_after);
2410
+ }
2411
+ if (opts.updated_after) {
2412
+ sql += " AND updated_at > ?";
2413
+ params.push(opts.updated_after);
2414
+ }
2415
+ if (opts.has_dependencies === true) {
2416
+ sql += " AND id IN (SELECT task_id FROM task_dependencies)";
2417
+ } else if (opts.has_dependencies === false) {
2418
+ sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
2419
+ }
2420
+ if (opts.is_blocked === true) {
2421
+ sql += " AND id IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
2422
+ } else if (opts.is_blocked === false) {
2423
+ sql += " AND id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
1827
2424
  }
1828
2425
  sql += ` ORDER BY
1829
2426
  CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
@@ -2368,8 +2965,11 @@ export {
2368
2965
  resetDatabase,
2369
2966
  removeDependency,
2370
2967
  registerAgent,
2968
+ parseRecurrenceRule,
2371
2969
  now,
2372
2970
  nextTaskShortId,
2971
+ nextOccurrence,
2972
+ moveTask,
2373
2973
  logTaskChange,
2374
2974
  lockTask,
2375
2975
  loadConfig,
@@ -2383,15 +2983,20 @@ export {
2383
2983
  listOrgs,
2384
2984
  listComments,
2385
2985
  listAgents,
2986
+ isValidRecurrenceRule,
2386
2987
  getWebhook,
2387
2988
  getTemplate,
2989
+ getTasksChangedSince,
2388
2990
  getTaskWithRelations,
2991
+ getTaskStats,
2389
2992
  getTaskListBySlug,
2390
2993
  getTaskList,
2391
2994
  getTaskHistory,
2995
+ getTaskGraph,
2392
2996
  getTaskDependents,
2393
2997
  getTaskDependencies,
2394
2998
  getTask,
2999
+ getStaleTasks,
2395
3000
  getSession,
2396
3001
  getRecentActivity,
2397
3002
  getProjectByPath,
@@ -2400,6 +3005,7 @@ export {
2400
3005
  getOrgChart,
2401
3006
  getOrgByName,
2402
3007
  getOrg,
3008
+ getNextTask,
2403
3009
  getDirectReports,
2404
3010
  getDatabase,
2405
3011
  getCompletionGuardConfig,
@@ -2407,6 +3013,8 @@ export {
2407
3013
  getBlockingDeps,
2408
3014
  getAgentByName,
2409
3015
  getAgent,
3016
+ getActiveWork,
3017
+ failTask,
2410
3018
  ensureTaskList,
2411
3019
  ensureProject,
2412
3020
  dispatchWebhook,
@@ -2429,9 +3037,14 @@ export {
2429
3037
  createProject,
2430
3038
  createPlan,
2431
3039
  createOrg,
3040
+ countTasks,
2432
3041
  completeTask,
2433
3042
  closeDatabase,
3043
+ cloneTask,
3044
+ claimNextTask,
2434
3045
  checkCompletionGuard,
3046
+ bulkUpdateTasks,
3047
+ bulkCreateTasks,
2435
3048
  addDependency,
2436
3049
  addComment,
2437
3050
  VersionConflictError,