@hasna/todos 0.9.34 → 0.9.37

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,141 @@ 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
+
1018
+ // src/db/webhooks.ts
1019
+ function rowToWebhook(row) {
1020
+ return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
1021
+ }
1022
+ function createWebhook(input, db) {
1023
+ const d = db || getDatabase();
1024
+ const id = uuid();
1025
+ d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
1026
+ return getWebhook(id, d);
1027
+ }
1028
+ function getWebhook(id, db) {
1029
+ const d = db || getDatabase();
1030
+ const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
1031
+ return row ? rowToWebhook(row) : null;
1032
+ }
1033
+ function listWebhooks(db) {
1034
+ const d = db || getDatabase();
1035
+ return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
1036
+ }
1037
+ function deleteWebhook(id, db) {
1038
+ const d = db || getDatabase();
1039
+ return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
1040
+ }
1041
+ async function dispatchWebhook(event, payload, db) {
1042
+ const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
1043
+ for (const wh of webhooks) {
1044
+ try {
1045
+ const body = JSON.stringify({ event, payload, timestamp: now() });
1046
+ const headers = { "Content-Type": "application/json" };
1047
+ if (wh.secret) {
1048
+ const encoder = new TextEncoder;
1049
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1050
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
1051
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1052
+ }
1053
+ fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
1054
+ } catch {}
1055
+ }
1056
+ }
1057
+
894
1058
  // src/db/tasks.ts
895
1059
  function rowToTask(row) {
896
1060
  return {
@@ -922,8 +1086,8 @@ function createTask(input, db) {
922
1086
  const tags = input.tags || [];
923
1087
  const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
924
1088
  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, ?, ?, ?, ?, ?, ?, ?)`, [
1089
+ 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)
1090
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
927
1091
  id,
928
1092
  shortId,
929
1093
  input.project_id || null,
@@ -946,12 +1110,16 @@ function createTask(input, db) {
946
1110
  input.estimated_minutes || null,
947
1111
  input.requires_approval ? 1 : 0,
948
1112
  null,
949
- null
1113
+ null,
1114
+ input.recurrence_rule || null,
1115
+ input.recurrence_parent_id || null
950
1116
  ]);
951
1117
  if (tags.length > 0) {
952
1118
  insertTaskTags(id, tags, d);
953
1119
  }
954
- return getTask(id, d);
1120
+ const task = getTask(id, d);
1121
+ dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
1122
+ return task;
955
1123
  }
956
1124
  function getTask(id, db) {
957
1125
  const d = db || getDatabase();
@@ -1046,6 +1214,11 @@ function listTasks(filter = {}, db) {
1046
1214
  conditions.push("task_list_id = ?");
1047
1215
  params.push(filter.task_list_id);
1048
1216
  }
1217
+ if (filter.has_recurrence === true) {
1218
+ conditions.push("recurrence_rule IS NOT NULL");
1219
+ } else if (filter.has_recurrence === false) {
1220
+ conditions.push("recurrence_rule IS NULL");
1221
+ }
1049
1222
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1050
1223
  let limitClause = "";
1051
1224
  if (filter.limit) {
@@ -1061,6 +1234,69 @@ function listTasks(filter = {}, db) {
1061
1234
  created_at DESC${limitClause}`).all(...params);
1062
1235
  return rows.map(rowToTask);
1063
1236
  }
1237
+ function countTasks(filter = {}, db) {
1238
+ const d = db || getDatabase();
1239
+ const conditions = [];
1240
+ const params = [];
1241
+ if (filter.project_id) {
1242
+ conditions.push("project_id = ?");
1243
+ params.push(filter.project_id);
1244
+ }
1245
+ if (filter.parent_id !== undefined) {
1246
+ if (filter.parent_id === null) {
1247
+ conditions.push("parent_id IS NULL");
1248
+ } else {
1249
+ conditions.push("parent_id = ?");
1250
+ params.push(filter.parent_id);
1251
+ }
1252
+ }
1253
+ if (filter.status) {
1254
+ if (Array.isArray(filter.status)) {
1255
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1256
+ params.push(...filter.status);
1257
+ } else {
1258
+ conditions.push("status = ?");
1259
+ params.push(filter.status);
1260
+ }
1261
+ }
1262
+ if (filter.priority) {
1263
+ if (Array.isArray(filter.priority)) {
1264
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
1265
+ params.push(...filter.priority);
1266
+ } else {
1267
+ conditions.push("priority = ?");
1268
+ params.push(filter.priority);
1269
+ }
1270
+ }
1271
+ if (filter.assigned_to) {
1272
+ conditions.push("assigned_to = ?");
1273
+ params.push(filter.assigned_to);
1274
+ }
1275
+ if (filter.agent_id) {
1276
+ conditions.push("agent_id = ?");
1277
+ params.push(filter.agent_id);
1278
+ }
1279
+ if (filter.session_id) {
1280
+ conditions.push("session_id = ?");
1281
+ params.push(filter.session_id);
1282
+ }
1283
+ if (filter.tags && filter.tags.length > 0) {
1284
+ const placeholders = filter.tags.map(() => "?").join(",");
1285
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1286
+ params.push(...filter.tags);
1287
+ }
1288
+ if (filter.plan_id) {
1289
+ conditions.push("plan_id = ?");
1290
+ params.push(filter.plan_id);
1291
+ }
1292
+ if (filter.task_list_id) {
1293
+ conditions.push("task_list_id = ?");
1294
+ params.push(filter.task_list_id);
1295
+ }
1296
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1297
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
1298
+ return row.count;
1299
+ }
1064
1300
  function updateTask(id, input, db) {
1065
1301
  const d = db || getDatabase();
1066
1302
  const task = getTask(id, d);
@@ -1132,6 +1368,10 @@ function updateTask(id, input, db) {
1132
1368
  sets.push("approved_at = ?");
1133
1369
  params.push(now());
1134
1370
  }
1371
+ if (input.recurrence_rule !== undefined) {
1372
+ sets.push("recurrence_rule = ?");
1373
+ params.push(input.recurrence_rule);
1374
+ }
1135
1375
  params.push(id, input.version);
1136
1376
  const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
1137
1377
  if (result.changes === 0) {
@@ -1152,6 +1392,12 @@ function updateTask(id, input, db) {
1152
1392
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
1153
1393
  if (input.approved_by !== undefined)
1154
1394
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
1395
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
1396
+ dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
1397
+ }
1398
+ if (input.status !== undefined && input.status !== task.status) {
1399
+ dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
1400
+ }
1155
1401
  return {
1156
1402
  ...task,
1157
1403
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
@@ -1203,9 +1449,10 @@ function startTask(id, agentId, db) {
1203
1449
  }
1204
1450
  }
1205
1451
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
1452
+ dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
1206
1453
  return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
1207
1454
  }
1208
- function completeTask(id, agentId, db, evidence) {
1455
+ function completeTask(id, agentId, db, options) {
1209
1456
  const d = db || getDatabase();
1210
1457
  const task = getTask(id, d);
1211
1458
  if (!task)
@@ -1214,7 +1461,9 @@ function completeTask(id, agentId, db, evidence) {
1214
1461
  throw new LockError(id, task.locked_by);
1215
1462
  }
1216
1463
  checkCompletionGuard(task, agentId || null, d);
1217
- if (evidence) {
1464
+ const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes } : undefined;
1465
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes);
1466
+ if (hasEvidence) {
1218
1467
  const meta2 = { ...task.metadata, _evidence: evidence };
1219
1468
  d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
1220
1469
  }
@@ -1222,7 +1471,15 @@ function completeTask(id, agentId, db, evidence) {
1222
1471
  d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
1223
1472
  WHERE id = ?`, [timestamp, timestamp, id]);
1224
1473
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
1225
- const meta = evidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
1474
+ dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
1475
+ let spawnedTask = null;
1476
+ if (task.recurrence_rule && !options?.skip_recurrence) {
1477
+ spawnedTask = spawnNextRecurrence(task, d);
1478
+ }
1479
+ const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
1480
+ if (spawnedTask) {
1481
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
1482
+ }
1226
1483
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
1227
1484
  }
1228
1485
  function lockTask(id, agentId, db) {
@@ -1289,6 +1546,298 @@ function getTaskDependents(taskId, db) {
1289
1546
  const d = db || getDatabase();
1290
1547
  return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
1291
1548
  }
1549
+ function cloneTask(taskId, overrides, db) {
1550
+ const d = db || getDatabase();
1551
+ const source = getTask(taskId, d);
1552
+ if (!source)
1553
+ throw new TaskNotFoundError(taskId);
1554
+ const input = {
1555
+ title: overrides?.title ?? source.title,
1556
+ description: overrides?.description ?? source.description ?? undefined,
1557
+ priority: overrides?.priority ?? source.priority,
1558
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
1559
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
1560
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
1561
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
1562
+ status: overrides?.status ?? "pending",
1563
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
1564
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
1565
+ tags: overrides?.tags ?? source.tags,
1566
+ metadata: overrides?.metadata ?? source.metadata,
1567
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
1568
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
1569
+ };
1570
+ return createTask(input, d);
1571
+ }
1572
+ function getTaskGraph(taskId, direction = "both", db) {
1573
+ const d = db || getDatabase();
1574
+ const task = getTask(taskId, d);
1575
+ if (!task)
1576
+ throw new TaskNotFoundError(taskId);
1577
+ function toNode(t) {
1578
+ const deps = getTaskDependencies(t.id, d);
1579
+ const hasUnfinishedDeps = deps.some((dep) => {
1580
+ const depTask = getTask(dep.depends_on, d);
1581
+ return depTask && depTask.status !== "completed";
1582
+ });
1583
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
1584
+ }
1585
+ function buildUp(id, visited) {
1586
+ if (visited.has(id))
1587
+ return [];
1588
+ visited.add(id);
1589
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
1590
+ return deps.map((dep) => {
1591
+ const depTask = getTask(dep.depends_on, d);
1592
+ if (!depTask)
1593
+ return null;
1594
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
1595
+ }).filter(Boolean);
1596
+ }
1597
+ function buildDown(id, visited) {
1598
+ if (visited.has(id))
1599
+ return [];
1600
+ visited.add(id);
1601
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
1602
+ return dependents.map((dep) => {
1603
+ const depTask = getTask(dep.task_id, d);
1604
+ if (!depTask)
1605
+ return null;
1606
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
1607
+ }).filter(Boolean);
1608
+ }
1609
+ const rootNode = toNode(task);
1610
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
1611
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
1612
+ return { task: rootNode, depends_on, blocks };
1613
+ }
1614
+ function moveTask(taskId, target, db) {
1615
+ const d = db || getDatabase();
1616
+ const task = getTask(taskId, d);
1617
+ if (!task)
1618
+ throw new TaskNotFoundError(taskId);
1619
+ const sets = ["updated_at = ?", "version = version + 1"];
1620
+ const params = [now()];
1621
+ if (target.task_list_id !== undefined) {
1622
+ sets.push("task_list_id = ?");
1623
+ params.push(target.task_list_id);
1624
+ }
1625
+ if (target.project_id !== undefined) {
1626
+ sets.push("project_id = ?");
1627
+ params.push(target.project_id);
1628
+ }
1629
+ if (target.plan_id !== undefined) {
1630
+ sets.push("plan_id = ?");
1631
+ params.push(target.plan_id);
1632
+ }
1633
+ params.push(taskId);
1634
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
1635
+ return getTask(taskId, d);
1636
+ }
1637
+ function spawnNextRecurrence(completedTask, db) {
1638
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
1639
+ let title = completedTask.title;
1640
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
1641
+ title = title.slice(completedTask.short_id.length + 2);
1642
+ }
1643
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
1644
+ return createTask({
1645
+ title,
1646
+ description: completedTask.description ?? undefined,
1647
+ priority: completedTask.priority,
1648
+ project_id: completedTask.project_id ?? undefined,
1649
+ task_list_id: completedTask.task_list_id ?? undefined,
1650
+ plan_id: completedTask.plan_id ?? undefined,
1651
+ assigned_to: completedTask.assigned_to ?? undefined,
1652
+ tags: completedTask.tags,
1653
+ metadata: completedTask.metadata,
1654
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
1655
+ recurrence_rule: completedTask.recurrence_rule,
1656
+ recurrence_parent_id: recurrenceParentId,
1657
+ due_at: dueAt
1658
+ }, db);
1659
+ }
1660
+ function claimNextTask(agentId, filters, db) {
1661
+ const d = db || getDatabase();
1662
+ const tx = d.transaction(() => {
1663
+ const task = getNextTask(agentId, filters, d);
1664
+ if (!task)
1665
+ return null;
1666
+ return startTask(task.id, agentId, d);
1667
+ });
1668
+ return tx();
1669
+ }
1670
+ function getNextTask(agentId, filters, db) {
1671
+ const d = db || getDatabase();
1672
+ clearExpiredLocks(d);
1673
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
1674
+ const params = [lockExpiryCutoff()];
1675
+ if (filters?.project_id) {
1676
+ conditions.push("project_id = ?");
1677
+ params.push(filters.project_id);
1678
+ }
1679
+ if (filters?.task_list_id) {
1680
+ conditions.push("task_list_id = ?");
1681
+ params.push(filters.task_list_id);
1682
+ }
1683
+ if (filters?.plan_id) {
1684
+ conditions.push("plan_id = ?");
1685
+ params.push(filters.plan_id);
1686
+ }
1687
+ if (filters?.tags && filters.tags.length > 0) {
1688
+ const placeholders = filters.tags.map(() => "?").join(",");
1689
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1690
+ params.push(...filters.tags);
1691
+ }
1692
+ 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')");
1693
+ const where = conditions.join(" AND ");
1694
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
1695
+ if (agentId) {
1696
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
1697
+ params.push(agentId);
1698
+ }
1699
+ 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`;
1700
+ const row = d.query(sql).get(...params);
1701
+ return row ? rowToTask(row) : null;
1702
+ }
1703
+ function getActiveWork(filters, db) {
1704
+ const d = db || getDatabase();
1705
+ clearExpiredLocks(d);
1706
+ const conditions = ["status = 'in_progress'"];
1707
+ const params = [];
1708
+ if (filters?.project_id) {
1709
+ conditions.push("project_id = ?");
1710
+ params.push(filters.project_id);
1711
+ }
1712
+ if (filters?.task_list_id) {
1713
+ conditions.push("task_list_id = ?");
1714
+ params.push(filters.task_list_id);
1715
+ }
1716
+ const where = conditions.join(" AND ");
1717
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
1718
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
1719
+ updated_at DESC`).all(...params);
1720
+ return rows;
1721
+ }
1722
+ function getTasksChangedSince(since, filters, db) {
1723
+ const d = db || getDatabase();
1724
+ const conditions = ["updated_at > ?"];
1725
+ const params = [since];
1726
+ if (filters?.project_id) {
1727
+ conditions.push("project_id = ?");
1728
+ params.push(filters.project_id);
1729
+ }
1730
+ if (filters?.task_list_id) {
1731
+ conditions.push("task_list_id = ?");
1732
+ params.push(filters.task_list_id);
1733
+ }
1734
+ const where = conditions.join(" AND ");
1735
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
1736
+ return rows.map(rowToTask);
1737
+ }
1738
+ function failTask(id, agentId, reason, options, db) {
1739
+ const d = db || getDatabase();
1740
+ const task = getTask(id, d);
1741
+ if (!task)
1742
+ throw new TaskNotFoundError(id);
1743
+ const meta = {
1744
+ ...task.metadata,
1745
+ _failure: {
1746
+ reason: reason || "Unknown failure",
1747
+ error_code: options?.error_code || null,
1748
+ failed_by: agentId || null,
1749
+ failed_at: now(),
1750
+ retry_requested: options?.retry || false
1751
+ }
1752
+ };
1753
+ const timestamp = now();
1754
+ d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
1755
+ WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
1756
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
1757
+ dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
1758
+ const failedTask = {
1759
+ ...task,
1760
+ status: "failed",
1761
+ locked_by: null,
1762
+ locked_at: null,
1763
+ metadata: meta,
1764
+ version: task.version + 1,
1765
+ updated_at: timestamp
1766
+ };
1767
+ let retryTask;
1768
+ if (options?.retry) {
1769
+ let title = task.title;
1770
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
1771
+ title = title.slice(task.short_id.length + 2);
1772
+ }
1773
+ retryTask = createTask({
1774
+ title,
1775
+ description: task.description ?? undefined,
1776
+ priority: task.priority,
1777
+ project_id: task.project_id ?? undefined,
1778
+ task_list_id: task.task_list_id ?? undefined,
1779
+ plan_id: task.plan_id ?? undefined,
1780
+ assigned_to: task.assigned_to ?? undefined,
1781
+ tags: task.tags,
1782
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
1783
+ estimated_minutes: task.estimated_minutes ?? undefined,
1784
+ recurrence_rule: task.recurrence_rule ?? undefined,
1785
+ due_at: options.retry_after || task.due_at || undefined
1786
+ }, d);
1787
+ }
1788
+ return { task: failedTask, retryTask };
1789
+ }
1790
+ function getStaleTasks(staleMinutes = 30, filters, db) {
1791
+ const d = db || getDatabase();
1792
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
1793
+ const conditions = [
1794
+ "status = 'in_progress'",
1795
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
1796
+ ];
1797
+ const params = [cutoff, cutoff];
1798
+ if (filters?.project_id) {
1799
+ conditions.push("project_id = ?");
1800
+ params.push(filters.project_id);
1801
+ }
1802
+ if (filters?.task_list_id) {
1803
+ conditions.push("task_list_id = ?");
1804
+ params.push(filters.task_list_id);
1805
+ }
1806
+ const where = conditions.join(" AND ");
1807
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
1808
+ return rows.map(rowToTask);
1809
+ }
1810
+ function getStatus(filters, agentId, db) {
1811
+ const d = db || getDatabase();
1812
+ const pending = countTasks({ ...filters, status: "pending" }, d);
1813
+ const in_progress = countTasks({ ...filters, status: "in_progress" }, d);
1814
+ const completed = countTasks({ ...filters, status: "completed" }, d);
1815
+ const total = countTasks(filters || {}, d);
1816
+ const active_work = getActiveWork(filters, d);
1817
+ const next_task = getNextTask(agentId, filters, d);
1818
+ const stale = getStaleTasks(30, filters, d);
1819
+ const conditions = ["recurrence_rule IS NOT NULL", "status = 'pending'", "due_at < ?"];
1820
+ const params = [now()];
1821
+ if (filters?.project_id) {
1822
+ conditions.push("project_id = ?");
1823
+ params.push(filters.project_id);
1824
+ }
1825
+ if (filters?.task_list_id) {
1826
+ conditions.push("task_list_id = ?");
1827
+ params.push(filters.task_list_id);
1828
+ }
1829
+ const overdueRow = d.query(`SELECT COUNT(*) as count FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params);
1830
+ return {
1831
+ pending,
1832
+ in_progress,
1833
+ completed,
1834
+ total,
1835
+ active_work,
1836
+ next_task,
1837
+ stale_count: stale.length,
1838
+ overdue_recurring: overdueRow.count
1839
+ };
1840
+ }
1292
1841
  function wouldCreateCycle(taskId, dependsOn, db) {
1293
1842
  const visited = new Set;
1294
1843
  const queue = [dependsOn];
@@ -1306,6 +1855,91 @@ function wouldCreateCycle(taskId, dependsOn, db) {
1306
1855
  }
1307
1856
  return false;
1308
1857
  }
1858
+ function getTaskStats(filters, db) {
1859
+ const d = db || getDatabase();
1860
+ const conditions = [];
1861
+ const params = [];
1862
+ if (filters?.project_id) {
1863
+ conditions.push("project_id = ?");
1864
+ params.push(filters.project_id);
1865
+ }
1866
+ if (filters?.task_list_id) {
1867
+ conditions.push("task_list_id = ?");
1868
+ params.push(filters.task_list_id);
1869
+ }
1870
+ if (filters?.agent_id) {
1871
+ conditions.push("(agent_id = ? OR assigned_to = ?)");
1872
+ params.push(filters.agent_id, filters.agent_id);
1873
+ }
1874
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1875
+ const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
1876
+ const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
1877
+ const by_status = {};
1878
+ for (const r of statusRows)
1879
+ by_status[r.status] = r.count;
1880
+ const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
1881
+ const by_priority = {};
1882
+ for (const r of priorityRows)
1883
+ by_priority[r.priority] = r.count;
1884
+ const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
1885
+ const by_agent = {};
1886
+ for (const r of agentRows)
1887
+ by_agent[r.agent] = r.count;
1888
+ const completed = by_status["completed"] || 0;
1889
+ const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
1890
+ return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
1891
+ }
1892
+ function bulkCreateTasks(inputs, db) {
1893
+ const d = db || getDatabase();
1894
+ const tempIdToRealId = new Map;
1895
+ const created = [];
1896
+ const tx = d.transaction(() => {
1897
+ for (const input of inputs) {
1898
+ const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
1899
+ const task = createTask(createInput, d);
1900
+ if (temp_id)
1901
+ tempIdToRealId.set(temp_id, task.id);
1902
+ created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
1903
+ }
1904
+ for (const input of inputs) {
1905
+ if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
1906
+ const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
1907
+ if (!taskId)
1908
+ continue;
1909
+ for (const depTempId of input.depends_on_temp_ids) {
1910
+ const depRealId = tempIdToRealId.get(depTempId);
1911
+ if (depRealId) {
1912
+ addDependency(taskId, depRealId, d);
1913
+ }
1914
+ }
1915
+ }
1916
+ }
1917
+ });
1918
+ tx();
1919
+ return { created };
1920
+ }
1921
+ function bulkUpdateTasks(taskIds, updates, db) {
1922
+ const d = db || getDatabase();
1923
+ let updated = 0;
1924
+ const failed = [];
1925
+ const tx = d.transaction(() => {
1926
+ for (const id of taskIds) {
1927
+ try {
1928
+ const task = getTask(id, d);
1929
+ if (!task) {
1930
+ failed.push({ id, error: "Task not found" });
1931
+ continue;
1932
+ }
1933
+ updateTask(id, { ...updates, version: task.version }, d);
1934
+ updated++;
1935
+ } catch (e) {
1936
+ failed.push({ id, error: e instanceof Error ? e.message : String(e) });
1937
+ }
1938
+ }
1939
+ });
1940
+ tx();
1941
+ return { updated, failed };
1942
+ }
1309
1943
  // src/db/plans.ts
1310
1944
  function createPlan(input, db) {
1311
1945
  const d = db || getDatabase();
@@ -1654,45 +2288,6 @@ function deleteSession(id, db) {
1654
2288
  const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
1655
2289
  return result.changes > 0;
1656
2290
  }
1657
- // src/db/webhooks.ts
1658
- function rowToWebhook(row) {
1659
- return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
1660
- }
1661
- function createWebhook(input, db) {
1662
- const d = db || getDatabase();
1663
- const id = uuid();
1664
- d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
1665
- return getWebhook(id, d);
1666
- }
1667
- function getWebhook(id, db) {
1668
- const d = db || getDatabase();
1669
- const row = d.query("SELECT * FROM webhooks WHERE id = ?").get(id);
1670
- return row ? rowToWebhook(row) : null;
1671
- }
1672
- function listWebhooks(db) {
1673
- const d = db || getDatabase();
1674
- return d.query("SELECT * FROM webhooks ORDER BY created_at DESC").all().map(rowToWebhook);
1675
- }
1676
- function deleteWebhook(id, db) {
1677
- const d = db || getDatabase();
1678
- return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
1679
- }
1680
- async function dispatchWebhook(event, payload, db) {
1681
- const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
1682
- for (const wh of webhooks) {
1683
- try {
1684
- const body = JSON.stringify({ event, payload, timestamp: now() });
1685
- const headers = { "Content-Type": "application/json" };
1686
- if (wh.secret) {
1687
- const encoder = new TextEncoder;
1688
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1689
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
1690
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1691
- }
1692
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
1693
- } catch {}
1694
- }
1695
- }
1696
2291
  // src/db/templates.ts
1697
2292
  function rowToTemplate(row) {
1698
2293
  return {
@@ -1811,19 +2406,64 @@ function rowToTask2(row) {
1811
2406
  requires_approval: Boolean(row.requires_approval)
1812
2407
  };
1813
2408
  }
1814
- function searchTasks(query, projectId, taskListId, db) {
2409
+ function searchTasks(options, projectId, taskListId, db) {
2410
+ const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
1815
2411
  const d = db || getDatabase();
1816
2412
  clearExpiredLocks(d);
1817
- const pattern = `%${query}%`;
2413
+ const pattern = `%${opts.query}%`;
1818
2414
  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
2415
  const params = [pattern, pattern, pattern];
1820
- if (projectId) {
2416
+ if (opts.project_id) {
1821
2417
  sql += " AND project_id = ?";
1822
- params.push(projectId);
2418
+ params.push(opts.project_id);
1823
2419
  }
1824
- if (taskListId) {
2420
+ if (opts.task_list_id) {
1825
2421
  sql += " AND task_list_id = ?";
1826
- params.push(taskListId);
2422
+ params.push(opts.task_list_id);
2423
+ }
2424
+ if (opts.status) {
2425
+ if (Array.isArray(opts.status)) {
2426
+ sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
2427
+ params.push(...opts.status);
2428
+ } else {
2429
+ sql += " AND status = ?";
2430
+ params.push(opts.status);
2431
+ }
2432
+ }
2433
+ if (opts.priority) {
2434
+ if (Array.isArray(opts.priority)) {
2435
+ sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
2436
+ params.push(...opts.priority);
2437
+ } else {
2438
+ sql += " AND priority = ?";
2439
+ params.push(opts.priority);
2440
+ }
2441
+ }
2442
+ if (opts.assigned_to) {
2443
+ sql += " AND assigned_to = ?";
2444
+ params.push(opts.assigned_to);
2445
+ }
2446
+ if (opts.agent_id) {
2447
+ sql += " AND agent_id = ?";
2448
+ params.push(opts.agent_id);
2449
+ }
2450
+ if (opts.created_after) {
2451
+ sql += " AND created_at > ?";
2452
+ params.push(opts.created_after);
2453
+ }
2454
+ if (opts.updated_after) {
2455
+ sql += " AND updated_at > ?";
2456
+ params.push(opts.updated_after);
2457
+ }
2458
+ if (opts.has_dependencies === true) {
2459
+ sql += " AND id IN (SELECT task_id FROM task_dependencies)";
2460
+ } else if (opts.has_dependencies === false) {
2461
+ sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
2462
+ }
2463
+ if (opts.is_blocked === true) {
2464
+ 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')";
2465
+ } else if (opts.is_blocked === false) {
2466
+ 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
2467
  }
1828
2468
  sql += ` ORDER BY
1829
2469
  CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
@@ -2368,8 +3008,11 @@ export {
2368
3008
  resetDatabase,
2369
3009
  removeDependency,
2370
3010
  registerAgent,
3011
+ parseRecurrenceRule,
2371
3012
  now,
2372
3013
  nextTaskShortId,
3014
+ nextOccurrence,
3015
+ moveTask,
2373
3016
  logTaskChange,
2374
3017
  lockTask,
2375
3018
  loadConfig,
@@ -2383,15 +3026,21 @@ export {
2383
3026
  listOrgs,
2384
3027
  listComments,
2385
3028
  listAgents,
3029
+ isValidRecurrenceRule,
2386
3030
  getWebhook,
2387
3031
  getTemplate,
3032
+ getTasksChangedSince,
2388
3033
  getTaskWithRelations,
3034
+ getTaskStats,
2389
3035
  getTaskListBySlug,
2390
3036
  getTaskList,
2391
3037
  getTaskHistory,
3038
+ getTaskGraph,
2392
3039
  getTaskDependents,
2393
3040
  getTaskDependencies,
2394
3041
  getTask,
3042
+ getStatus,
3043
+ getStaleTasks,
2395
3044
  getSession,
2396
3045
  getRecentActivity,
2397
3046
  getProjectByPath,
@@ -2400,6 +3049,7 @@ export {
2400
3049
  getOrgChart,
2401
3050
  getOrgByName,
2402
3051
  getOrg,
3052
+ getNextTask,
2403
3053
  getDirectReports,
2404
3054
  getDatabase,
2405
3055
  getCompletionGuardConfig,
@@ -2407,6 +3057,8 @@ export {
2407
3057
  getBlockingDeps,
2408
3058
  getAgentByName,
2409
3059
  getAgent,
3060
+ getActiveWork,
3061
+ failTask,
2410
3062
  ensureTaskList,
2411
3063
  ensureProject,
2412
3064
  dispatchWebhook,
@@ -2429,9 +3081,14 @@ export {
2429
3081
  createProject,
2430
3082
  createPlan,
2431
3083
  createOrg,
3084
+ countTasks,
2432
3085
  completeTask,
2433
3086
  closeDatabase,
3087
+ cloneTask,
3088
+ claimNextTask,
2434
3089
  checkCompletionGuard,
3090
+ bulkUpdateTasks,
3091
+ bulkCreateTasks,
2435
3092
  addDependency,
2436
3093
  addComment,
2437
3094
  VersionConflictError,