@hasna/todos 0.11.72 → 0.11.74

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/mcp/index.js CHANGED
@@ -1807,6 +1807,11 @@ function ensureSchema(db) {
1807
1807
  ensureColumn("tasks", "priority_score", "INTEGER");
1808
1808
  ensureColumn("tasks", "priority_reason", "TEXT");
1809
1809
  ensureColumn("tasks", "archived_at", "TEXT");
1810
+ ensureColumn("tasks", "runner_id", "TEXT");
1811
+ ensureColumn("tasks", "runner_started_at", "TEXT");
1812
+ ensureColumn("tasks", "runner_completed_at", "TEXT");
1813
+ ensureColumn("tasks", "current_step", "TEXT");
1814
+ ensureColumn("tasks", "total_steps", "INTEGER");
1810
1815
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
1811
1816
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
1812
1817
  ensureColumn("agents", "reports_to", "TEXT");
@@ -1814,6 +1819,10 @@ function ensureSchema(db) {
1814
1819
  ensureColumn("agents", "level", "TEXT");
1815
1820
  ensureColumn("agents", "org_id", "TEXT");
1816
1821
  ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
1822
+ ensureColumn("agents", "session_id", "TEXT");
1823
+ ensureColumn("agents", "working_dir", "TEXT");
1824
+ ensureColumn("agents", "active_project_id", "TEXT");
1825
+ ensureColumn("agents", "status", "TEXT NOT NULL DEFAULT 'active'");
1817
1826
  ensureColumn("projects", "org_id", "TEXT");
1818
1827
  ensureColumn("plans", "slug", "TEXT");
1819
1828
  ensureColumn("plans", "task_list_id", "TEXT");
@@ -2731,34 +2740,40 @@ function ensureDir(filePath) {
2731
2740
  mkdirSync(dir, { recursive: true });
2732
2741
  }
2733
2742
  }
2743
+ function openDatabase(path) {
2744
+ ensureDir(path);
2745
+ const db = new Database(path);
2746
+ db.run("PRAGMA journal_mode = WAL");
2747
+ db.run("PRAGMA busy_timeout = 5000");
2748
+ db.run("PRAGMA foreign_keys = ON");
2749
+ runMigrations(db);
2750
+ backfillTaskTags(db);
2751
+ backfillMachineId(db);
2752
+ return db;
2753
+ }
2734
2754
  function getDatabase(dbPath) {
2735
2755
  const path = dbPath || getDbPath();
2736
2756
  if (_db && _dbPath === path)
2737
2757
  return _db;
2738
- if (_db && _dbPath !== path) {
2739
- _db.close();
2740
- _db = null;
2741
- _dbPath = null;
2742
- }
2743
- ensureDir(path);
2744
- _db = new Database(path);
2758
+ _db = openDatabase(path);
2745
2759
  _dbPath = path;
2746
- _db.run("PRAGMA journal_mode = WAL");
2747
- _db.run("PRAGMA busy_timeout = 5000");
2748
- _db.run("PRAGMA foreign_keys = ON");
2749
- runMigrations(_db);
2750
- backfillTaskTags(_db);
2751
- backfillMachineId(_db);
2752
2760
  return _db;
2753
2761
  }
2754
2762
  function closeDatabase() {
2755
2763
  if (_db) {
2756
- _db.close();
2764
+ try {
2765
+ _db.close();
2766
+ } catch {}
2757
2767
  _db = null;
2758
2768
  _dbPath = null;
2759
2769
  }
2760
2770
  }
2761
2771
  function resetDatabase() {
2772
+ if (_db) {
2773
+ try {
2774
+ _db.close();
2775
+ } catch {}
2776
+ }
2762
2777
  _db = null;
2763
2778
  _dbPath = null;
2764
2779
  }
@@ -10385,1767 +10400,1858 @@ var init_checklists = __esm(() => {
10385
10400
  init_database();
10386
10401
  });
10387
10402
 
10388
- // src/db/task-crud.ts
10389
- function rowToTask(row) {
10403
+ // src/lib/recurrence.ts
10404
+ function parseRecurrenceRule(rule) {
10405
+ const normalized = rule.trim().toLowerCase();
10406
+ if (normalized === "every weekday" || normalized === "every weekdays") {
10407
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
10408
+ }
10409
+ if (normalized === "every day" || normalized === "daily") {
10410
+ return { type: "interval", interval: 1, unit: "day" };
10411
+ }
10412
+ if (normalized === "every week" || normalized === "weekly") {
10413
+ return { type: "interval", interval: 1, unit: "week" };
10414
+ }
10415
+ if (normalized === "every month" || normalized === "monthly") {
10416
+ return { type: "interval", interval: 1, unit: "month" };
10417
+ }
10418
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
10419
+ if (intervalMatch) {
10420
+ return {
10421
+ type: "interval",
10422
+ interval: parseInt(intervalMatch[1], 10),
10423
+ unit: intervalMatch[2]
10424
+ };
10425
+ }
10426
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
10427
+ if (daysMatch) {
10428
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
10429
+ const days = [];
10430
+ for (const part of dayParts) {
10431
+ const dayNum = DAY_NAMES[part];
10432
+ if (dayNum !== undefined) {
10433
+ days.push(dayNum);
10434
+ }
10435
+ }
10436
+ if (days.length > 0) {
10437
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
10438
+ }
10439
+ }
10440
+ 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"`);
10441
+ }
10442
+ function isValidRecurrenceRule(rule) {
10443
+ try {
10444
+ parseRecurrenceRule(rule);
10445
+ return true;
10446
+ } catch {
10447
+ return false;
10448
+ }
10449
+ }
10450
+ function nextOccurrence(rule, from) {
10451
+ const parsed = parseRecurrenceRule(rule);
10452
+ const base = from || new Date;
10453
+ if (parsed.type === "interval") {
10454
+ const next = new Date(base);
10455
+ if (parsed.unit === "day") {
10456
+ next.setDate(next.getDate() + parsed.interval);
10457
+ } else if (parsed.unit === "week") {
10458
+ next.setDate(next.getDate() + parsed.interval * 7);
10459
+ } else if (parsed.unit === "month") {
10460
+ next.setMonth(next.getMonth() + parsed.interval);
10461
+ }
10462
+ return next.toISOString();
10463
+ }
10464
+ if (parsed.type === "specific_days") {
10465
+ const currentDay = base.getDay();
10466
+ const days = parsed.days;
10467
+ let daysToAdd = Infinity;
10468
+ for (const day of days) {
10469
+ let diff = day - currentDay;
10470
+ if (diff <= 0)
10471
+ diff += 7;
10472
+ if (diff < daysToAdd)
10473
+ daysToAdd = diff;
10474
+ }
10475
+ const next = new Date(base);
10476
+ next.setDate(next.getDate() + daysToAdd);
10477
+ return next.toISOString();
10478
+ }
10479
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
10480
+ }
10481
+ var DAY_NAMES;
10482
+ var init_recurrence = __esm(() => {
10483
+ DAY_NAMES = {
10484
+ sunday: 0,
10485
+ sun: 0,
10486
+ monday: 1,
10487
+ mon: 1,
10488
+ tuesday: 2,
10489
+ tue: 2,
10490
+ wednesday: 3,
10491
+ wed: 3,
10492
+ thursday: 4,
10493
+ thu: 4,
10494
+ friday: 5,
10495
+ fri: 5,
10496
+ saturday: 6,
10497
+ sat: 6
10498
+ };
10499
+ });
10500
+
10501
+ // src/db/templates.ts
10502
+ var exports_templates = {};
10503
+ __export(exports_templates, {
10504
+ updateTemplate: () => updateTemplate,
10505
+ tasksFromTemplate: () => tasksFromTemplate,
10506
+ taskFromTemplate: () => taskFromTemplate,
10507
+ resolveVariables: () => resolveVariables,
10508
+ previewTemplate: () => previewTemplate,
10509
+ listTemplates: () => listTemplates,
10510
+ listTemplateVersions: () => listTemplateVersions,
10511
+ importTemplate: () => importTemplate,
10512
+ getTemplateWithTasks: () => getTemplateWithTasks,
10513
+ getTemplateVersion: () => getTemplateVersion,
10514
+ getTemplateTasks: () => getTemplateTasks,
10515
+ getTemplate: () => getTemplate,
10516
+ exportTemplate: () => exportTemplate,
10517
+ evaluateCondition: () => evaluateCondition,
10518
+ deleteTemplate: () => deleteTemplate,
10519
+ createTemplate: () => createTemplate,
10520
+ addTemplateTasks: () => addTemplateTasks
10521
+ });
10522
+ function rowToTemplate(row) {
10390
10523
  return {
10391
10524
  ...row,
10392
10525
  tags: JSON.parse(row.tags || "[]"),
10526
+ variables: JSON.parse(row.variables || "[]"),
10393
10527
  metadata: JSON.parse(row.metadata || "{}"),
10394
- status: row.status,
10395
- priority: row.priority,
10396
- requires_approval: !!row.requires_approval
10528
+ priority: row.priority || "medium",
10529
+ version: row.version ?? 1
10397
10530
  };
10398
10531
  }
10399
- function insertTaskTags(taskId, tags, db) {
10400
- if (tags.length === 0)
10401
- return;
10402
- const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
10403
- for (const tag of tags) {
10404
- if (tag)
10405
- stmt.run(taskId, tag);
10406
- }
10407
- }
10408
- function replaceTaskTags(taskId, tags, db) {
10409
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
10410
- insertTaskTags(taskId, tags, db);
10532
+ function rowToTemplateTask(row) {
10533
+ return {
10534
+ ...row,
10535
+ tags: JSON.parse(row.tags || "[]"),
10536
+ depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
10537
+ metadata: JSON.parse(row.metadata || "{}"),
10538
+ priority: row.priority || "medium",
10539
+ condition: row.condition ?? null,
10540
+ include_template_id: row.include_template_id ?? null
10541
+ };
10411
10542
  }
10412
- function addMetadataConditions(metadata, conditions, params) {
10413
- if (!metadata)
10414
- return;
10415
- for (const [key, value] of Object.entries(metadata)) {
10416
- if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
10417
- throw new Error(`Invalid metadata filter key: ${key}`);
10418
- }
10419
- conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
10420
- params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
10421
- }
10543
+ function resolveTemplateId(id, d) {
10544
+ return resolvePartialId(d, "task_templates", id);
10422
10545
  }
10423
- function createTask(input, db) {
10546
+ function createTemplate(input, db) {
10424
10547
  const d = db || getDatabase();
10425
- const timestamp = now();
10426
- const tags = input.tags || [];
10548
+ const id = uuid();
10427
10549
  const machineId = currentStorageMachineId(d);
10428
- const assignedBy = input.assigned_by || input.agent_id;
10429
- const assignedFromProject = input.assigned_from_project || null;
10430
- let id = uuid();
10431
- for (let attempt = 0;attempt < 3; attempt++) {
10432
- try {
10433
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type, machine_id)
10434
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10435
- id,
10436
- null,
10437
- input.project_id || null,
10438
- input.parent_id || null,
10439
- input.plan_id || null,
10440
- input.task_list_id || null,
10441
- input.cycle_id || null,
10442
- input.title,
10443
- input.description || null,
10444
- input.status || "pending",
10445
- input.priority || "medium",
10446
- input.agent_id || null,
10447
- input.assigned_to || null,
10448
- input.session_id || null,
10449
- input.working_dir || null,
10450
- JSON.stringify(tags),
10451
- JSON.stringify(input.metadata || {}),
10452
- timestamp,
10453
- timestamp,
10454
- input.due_at || null,
10455
- input.estimated_minutes || null,
10456
- input.sla_minutes ?? null,
10457
- input.confidence ?? null,
10458
- input.retry_count ?? 0,
10459
- input.max_retries ?? 3,
10460
- input.retry_after ?? null,
10461
- input.requires_approval ? 1 : 0,
10462
- null,
10463
- null,
10464
- input.recurrence_rule || null,
10465
- input.recurrence_parent_id || null,
10466
- input.spawns_template_id || null,
10467
- input.reason || null,
10468
- input.spawned_from_session || null,
10469
- assignedBy || null,
10470
- assignedFromProject || null,
10471
- input.task_type || null,
10472
- machineId
10473
- ]);
10474
- break;
10475
- } catch (e) {
10476
- if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
10477
- id = uuid();
10478
- continue;
10479
- }
10480
- throw e;
10481
- }
10482
- }
10483
- if (tags.length > 0) {
10484
- insertTaskTags(id, tags, d);
10550
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
10551
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10552
+ id,
10553
+ input.name,
10554
+ input.title_pattern,
10555
+ input.description || null,
10556
+ input.priority || "medium",
10557
+ JSON.stringify(input.tags || []),
10558
+ JSON.stringify(input.variables || []),
10559
+ input.project_id || null,
10560
+ input.plan_id || null,
10561
+ JSON.stringify(input.metadata || {}),
10562
+ now(),
10563
+ machineId
10564
+ ]);
10565
+ if (input.tasks && input.tasks.length > 0) {
10566
+ addTemplateTasks(id, input.tasks, d);
10485
10567
  }
10486
- const task = getTask(id, d);
10487
- const payload = taskEventData(task);
10488
- const databasePath = databasePathFromDatabase(d);
10489
- dispatchWebhook2("task.created", payload, d).catch(() => {});
10490
- emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
10491
- emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
10492
- return task;
10568
+ return getTemplate(id, d);
10493
10569
  }
10494
- function getTask(id, db) {
10570
+ function getTemplate(id, db) {
10495
10571
  const d = db || getDatabase();
10496
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
10497
- if (!row)
10572
+ const resolved = resolveTemplateId(id, d);
10573
+ if (!resolved)
10498
10574
  return null;
10499
- return rowToTask(row);
10575
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
10576
+ return row ? rowToTemplate(row) : null;
10500
10577
  }
10501
- function getTaskWithRelations(id, db) {
10578
+ function listTemplates(db) {
10502
10579
  const d = db || getDatabase();
10503
- const task = getTask(id, d);
10504
- if (!task)
10505
- return null;
10506
- const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
10507
- const subtasks = subtaskRows.map(rowToTask);
10508
- const depRows = d.query(`SELECT t.* FROM tasks t
10509
- JOIN task_dependencies td ON td.depends_on = t.id
10510
- WHERE td.task_id = ?`).all(id);
10511
- const dependencies = depRows.map(rowToTask);
10512
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
10513
- JOIN task_dependencies td ON td.task_id = t.id
10514
- WHERE td.depends_on = ?`).all(id);
10515
- const blocked_by = blockedByRows.map(rowToTask);
10516
- const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
10517
- const parent = task.parent_id ? getTask(task.parent_id, d) : null;
10518
- const checklist = getChecklist(id, d);
10519
- return {
10520
- ...task,
10521
- subtasks,
10522
- dependencies,
10523
- blocked_by,
10524
- comments,
10525
- parent,
10526
- checklist
10527
- };
10580
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
10528
10581
  }
10529
- function listTasks(filter = {}, db) {
10582
+ function deleteTemplate(id, db) {
10530
10583
  const d = db || getDatabase();
10531
- const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
10532
- clearExpiredLocks2(d);
10533
- const conditions = [];
10534
- const params = [];
10535
- if (filter.project_id) {
10536
- conditions.push("project_id = ?");
10537
- params.push(filter.project_id);
10538
- }
10539
- if (filter.ids && filter.ids.length > 0) {
10540
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
10541
- params.push(...filter.ids);
10542
- }
10543
- if (filter.parent_id !== undefined) {
10544
- if (filter.parent_id === null) {
10545
- conditions.push("parent_id IS NULL");
10546
- } else {
10547
- conditions.push("parent_id = ?");
10548
- params.push(filter.parent_id);
10549
- }
10550
- }
10551
- if (filter.status) {
10552
- if (Array.isArray(filter.status)) {
10553
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
10554
- params.push(...filter.status);
10555
- } else {
10556
- conditions.push("status = ?");
10557
- params.push(filter.status);
10558
- }
10559
- }
10560
- if (filter.priority) {
10561
- if (Array.isArray(filter.priority)) {
10562
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
10563
- params.push(...filter.priority);
10564
- } else {
10565
- conditions.push("priority = ?");
10566
- params.push(filter.priority);
10567
- }
10568
- }
10569
- if (filter.assigned_to) {
10570
- conditions.push("assigned_to = ?");
10571
- params.push(filter.assigned_to);
10572
- }
10573
- if (filter.agent_id) {
10574
- conditions.push("agent_id = ?");
10575
- params.push(filter.agent_id);
10584
+ const resolved = resolveTemplateId(id, d);
10585
+ if (!resolved)
10586
+ return false;
10587
+ const template = getTemplate(resolved, d);
10588
+ if (!template)
10589
+ return false;
10590
+ recordStorageTombstone({
10591
+ object_type: "templates",
10592
+ object_id: resolved,
10593
+ payload: template,
10594
+ version: template.version
10595
+ }, d);
10596
+ return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
10597
+ }
10598
+ function updateTemplate(id, updates, db) {
10599
+ const d = db || getDatabase();
10600
+ const resolved = resolveTemplateId(id, d);
10601
+ if (!resolved)
10602
+ return null;
10603
+ const current = getTemplateWithTasks(resolved, d);
10604
+ if (current) {
10605
+ const snapshot = JSON.stringify({
10606
+ name: current.name,
10607
+ title_pattern: current.title_pattern,
10608
+ description: current.description,
10609
+ priority: current.priority,
10610
+ tags: current.tags,
10611
+ variables: current.variables,
10612
+ project_id: current.project_id,
10613
+ plan_id: current.plan_id,
10614
+ metadata: current.metadata,
10615
+ tasks: current.tasks
10616
+ });
10617
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
10576
10618
  }
10577
- if (filter.session_id) {
10578
- conditions.push("session_id = ?");
10579
- params.push(filter.session_id);
10619
+ const sets = ["version = version + 1"];
10620
+ const values = [];
10621
+ if (updates.name !== undefined) {
10622
+ sets.push("name = ?");
10623
+ values.push(updates.name);
10580
10624
  }
10581
- if (filter.tags && filter.tags.length > 0) {
10582
- const placeholders = filter.tags.map(() => "?").join(",");
10583
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
10584
- params.push(...filter.tags);
10625
+ if (updates.title_pattern !== undefined) {
10626
+ sets.push("title_pattern = ?");
10627
+ values.push(updates.title_pattern);
10585
10628
  }
10586
- if (filter.plan_id) {
10587
- conditions.push("plan_id = ?");
10588
- params.push(filter.plan_id);
10629
+ if (updates.description !== undefined) {
10630
+ sets.push("description = ?");
10631
+ values.push(updates.description);
10589
10632
  }
10590
- if (filter.task_list_id) {
10591
- conditions.push("task_list_id = ?");
10592
- params.push(filter.task_list_id);
10633
+ if (updates.priority !== undefined) {
10634
+ sets.push("priority = ?");
10635
+ values.push(updates.priority);
10593
10636
  }
10594
- if (filter.has_recurrence === true) {
10595
- conditions.push("recurrence_rule IS NOT NULL");
10596
- } else if (filter.has_recurrence === false) {
10597
- conditions.push("recurrence_rule IS NULL");
10637
+ if (updates.tags !== undefined) {
10638
+ sets.push("tags = ?");
10639
+ values.push(JSON.stringify(updates.tags));
10598
10640
  }
10599
- if (filter.task_type) {
10600
- if (Array.isArray(filter.task_type)) {
10601
- conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
10602
- params.push(...filter.task_type);
10603
- } else {
10604
- conditions.push("task_type = ?");
10605
- params.push(filter.task_type);
10606
- }
10641
+ if (updates.variables !== undefined) {
10642
+ sets.push("variables = ?");
10643
+ values.push(JSON.stringify(updates.variables));
10607
10644
  }
10608
- addMetadataConditions(filter.metadata, conditions, params);
10609
- const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
10610
- if (filter.cursor) {
10611
- try {
10612
- const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
10613
- conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
10614
- params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
10615
- } catch {}
10645
+ if (updates.project_id !== undefined) {
10646
+ sets.push("project_id = ?");
10647
+ values.push(updates.project_id);
10616
10648
  }
10617
- if (!filter.include_archived) {
10618
- conditions.push("archived_at IS NULL");
10649
+ if (updates.plan_id !== undefined) {
10650
+ sets.push("plan_id = ?");
10651
+ values.push(updates.plan_id);
10619
10652
  }
10620
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10621
- let limitClause = "";
10622
- if (filter.limit) {
10623
- limitClause = " LIMIT ?";
10624
- params.push(filter.limit);
10625
- if (!filter.cursor && filter.offset) {
10626
- limitClause += " OFFSET ?";
10627
- params.push(filter.offset);
10628
- }
10653
+ if (updates.metadata !== undefined) {
10654
+ sets.push("metadata = ?");
10655
+ values.push(JSON.stringify(updates.metadata));
10629
10656
  }
10630
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
10631
- return rows.map(rowToTask);
10632
- }
10633
- function getTaskByFingerprint(fingerprint, db) {
10634
- const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
10635
- return tasks[0] ?? null;
10657
+ values.push(resolved);
10658
+ d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
10659
+ return getTemplate(resolved, d);
10636
10660
  }
10637
- function mergeTaskMetadata(current, next, fingerprint) {
10661
+ function taskFromTemplate(templateId, overrides = {}, db) {
10662
+ const t = getTemplate(templateId, db);
10663
+ if (!t)
10664
+ throw new Error(`Template not found: ${templateId}`);
10665
+ const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
10638
10666
  return {
10639
- ...current,
10640
- ...next ?? {},
10641
- fingerprint
10667
+ title: cleanOverrides.title || t.title_pattern,
10668
+ description: cleanOverrides.description ?? t.description ?? undefined,
10669
+ priority: cleanOverrides.priority ?? t.priority,
10670
+ tags: cleanOverrides.tags ?? t.tags,
10671
+ project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
10672
+ plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
10673
+ metadata: cleanOverrides.metadata ?? t.metadata,
10674
+ ...cleanOverrides
10642
10675
  };
10643
10676
  }
10644
- function upsertTaskByFingerprint(input, db) {
10677
+ function addTemplateTasks(templateId, tasks, db) {
10645
10678
  const d = db || getDatabase();
10646
- const fingerprint = input.fingerprint.trim();
10647
- if (!fingerprint)
10648
- throw new Error("fingerprint is required");
10649
- const existing = getTaskByFingerprint(fingerprint, d);
10650
- const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
10651
- if (!existing) {
10652
- const task2 = createTask({ ...input, metadata }, d);
10653
- return { task: task2, created: true };
10679
+ const template = getTemplate(templateId, d);
10680
+ if (!template)
10681
+ throw new Error(`Template not found: ${templateId}`);
10682
+ d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
10683
+ const results = [];
10684
+ for (let i = 0;i < tasks.length; i++) {
10685
+ const task = tasks[i];
10686
+ const id = uuid();
10687
+ d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
10688
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10689
+ id,
10690
+ templateId,
10691
+ i,
10692
+ task.title_pattern,
10693
+ task.description || null,
10694
+ task.priority || "medium",
10695
+ JSON.stringify(task.tags || []),
10696
+ task.task_type || null,
10697
+ task.condition || null,
10698
+ task.include_template_id || null,
10699
+ JSON.stringify(task.depends_on || []),
10700
+ JSON.stringify(task.metadata || {}),
10701
+ now()
10702
+ ]);
10703
+ const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
10704
+ if (row)
10705
+ results.push(rowToTemplateTask(row));
10654
10706
  }
10655
- const task = updateTask(existing.id, {
10656
- version: existing.version,
10657
- title: input.title,
10658
- description: input.description,
10659
- status: input.status,
10660
- priority: input.priority,
10661
- project_id: input.project_id,
10662
- assigned_to: input.assigned_to,
10663
- working_dir: input.working_dir,
10664
- plan_id: input.plan_id,
10665
- task_list_id: input.task_list_id,
10666
- tags: input.tags,
10667
- metadata,
10668
- due_at: input.due_at,
10669
- estimated_minutes: input.estimated_minutes,
10670
- sla_minutes: input.sla_minutes,
10671
- confidence: input.confidence,
10672
- retry_count: input.retry_count,
10673
- max_retries: input.max_retries,
10674
- retry_after: input.retry_after,
10675
- requires_approval: input.requires_approval,
10676
- recurrence_rule: input.recurrence_rule,
10677
- task_type: input.task_type
10678
- }, d);
10679
- return { task, created: false };
10707
+ return results;
10680
10708
  }
10681
- function countTasks(filter = {}, db) {
10709
+ function getTemplateWithTasks(id, db) {
10682
10710
  const d = db || getDatabase();
10683
- const conditions = [];
10684
- const params = [];
10685
- if (filter.project_id) {
10686
- conditions.push("project_id = ?");
10687
- params.push(filter.project_id);
10711
+ const template = getTemplate(id, d);
10712
+ if (!template)
10713
+ return null;
10714
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
10715
+ const tasks = rows.map(rowToTemplateTask);
10716
+ return { ...template, tasks };
10717
+ }
10718
+ function getTemplateTasks(templateId, db) {
10719
+ const d = db || getDatabase();
10720
+ const resolved = resolveTemplateId(templateId, d);
10721
+ if (!resolved)
10722
+ return [];
10723
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
10724
+ return rows.map(rowToTemplateTask);
10725
+ }
10726
+ function evaluateCondition(condition, variables) {
10727
+ if (!condition || condition.trim() === "")
10728
+ return true;
10729
+ const trimmed = condition.trim();
10730
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
10731
+ if (eqMatch) {
10732
+ const varName = eqMatch[1];
10733
+ const expected = eqMatch[2].trim();
10734
+ return (variables[varName] ?? "") === expected;
10688
10735
  }
10689
- if (filter.ids && filter.ids.length > 0) {
10690
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
10691
- params.push(...filter.ids);
10736
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
10737
+ if (neqMatch) {
10738
+ const varName = neqMatch[1];
10739
+ const expected = neqMatch[2].trim();
10740
+ return (variables[varName] ?? "") !== expected;
10692
10741
  }
10693
- if (filter.parent_id !== undefined) {
10694
- if (filter.parent_id === null) {
10695
- conditions.push("parent_id IS NULL");
10696
- } else {
10697
- conditions.push("parent_id = ?");
10698
- params.push(filter.parent_id);
10699
- }
10742
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
10743
+ if (falsyMatch) {
10744
+ const varName = falsyMatch[1];
10745
+ const val = variables[varName];
10746
+ return !val || val === "" || val === "false";
10700
10747
  }
10701
- if (filter.status) {
10702
- if (Array.isArray(filter.status)) {
10703
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
10704
- params.push(...filter.status);
10705
- } else {
10706
- conditions.push("status = ?");
10707
- params.push(filter.status);
10708
- }
10748
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
10749
+ if (truthyMatch) {
10750
+ const varName = truthyMatch[1];
10751
+ const val = variables[varName];
10752
+ return !!val && val !== "" && val !== "false";
10709
10753
  }
10710
- if (filter.priority) {
10711
- if (Array.isArray(filter.priority)) {
10712
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
10713
- params.push(...filter.priority);
10714
- } else {
10715
- conditions.push("priority = ?");
10716
- params.push(filter.priority);
10754
+ return true;
10755
+ }
10756
+ function exportTemplate(id, db) {
10757
+ const d = db || getDatabase();
10758
+ const template = getTemplateWithTasks(id, d);
10759
+ if (!template)
10760
+ throw new Error(`Template not found: ${id}`);
10761
+ return {
10762
+ name: template.name,
10763
+ title_pattern: template.title_pattern,
10764
+ description: template.description,
10765
+ priority: template.priority,
10766
+ tags: template.tags,
10767
+ variables: template.variables,
10768
+ project_id: template.project_id,
10769
+ plan_id: template.plan_id,
10770
+ metadata: template.metadata,
10771
+ tasks: template.tasks.map((t) => ({
10772
+ position: t.position,
10773
+ title_pattern: t.title_pattern,
10774
+ description: t.description,
10775
+ priority: t.priority,
10776
+ tags: t.tags,
10777
+ task_type: t.task_type,
10778
+ condition: t.condition,
10779
+ include_template_id: t.include_template_id,
10780
+ depends_on_positions: t.depends_on_positions,
10781
+ metadata: t.metadata
10782
+ }))
10783
+ };
10784
+ }
10785
+ function importTemplate(json, db) {
10786
+ const d = db || getDatabase();
10787
+ const taskInputs = (json.tasks || []).map((t) => ({
10788
+ title_pattern: t.title_pattern,
10789
+ description: t.description ?? undefined,
10790
+ priority: t.priority,
10791
+ tags: t.tags,
10792
+ task_type: t.task_type ?? undefined,
10793
+ condition: t.condition ?? undefined,
10794
+ include_template_id: t.include_template_id ?? undefined,
10795
+ depends_on: t.depends_on_positions,
10796
+ metadata: t.metadata
10797
+ }));
10798
+ return createTemplate({
10799
+ name: json.name,
10800
+ title_pattern: json.title_pattern,
10801
+ description: json.description ?? undefined,
10802
+ priority: json.priority,
10803
+ tags: json.tags,
10804
+ variables: json.variables,
10805
+ project_id: json.project_id ?? undefined,
10806
+ plan_id: json.plan_id ?? undefined,
10807
+ metadata: json.metadata,
10808
+ tasks: taskInputs
10809
+ }, d);
10810
+ }
10811
+ function getTemplateVersion(id, version, db) {
10812
+ const d = db || getDatabase();
10813
+ const resolved = resolveTemplateId(id, d);
10814
+ if (!resolved)
10815
+ return null;
10816
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
10817
+ return row || null;
10818
+ }
10819
+ function listTemplateVersions(id, db) {
10820
+ const d = db || getDatabase();
10821
+ const resolved = resolveTemplateId(id, d);
10822
+ if (!resolved)
10823
+ return [];
10824
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
10825
+ }
10826
+ function resolveVariables(templateVars, provided) {
10827
+ const merged = { ...provided };
10828
+ for (const v of templateVars) {
10829
+ if (merged[v.name] === undefined && v.default !== undefined) {
10830
+ merged[v.name] = v.default;
10717
10831
  }
10718
10832
  }
10719
- if (filter.assigned_to) {
10720
- conditions.push("assigned_to = ?");
10721
- params.push(filter.assigned_to);
10722
- }
10723
- if (filter.agent_id) {
10724
- conditions.push("agent_id = ?");
10725
- params.push(filter.agent_id);
10726
- }
10727
- if (filter.session_id) {
10728
- conditions.push("session_id = ?");
10729
- params.push(filter.session_id);
10730
- }
10731
- if (filter.tags && filter.tags.length > 0) {
10732
- const placeholders = filter.tags.map(() => "?").join(",");
10733
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
10734
- params.push(...filter.tags);
10735
- }
10736
- if (filter.plan_id) {
10737
- conditions.push("plan_id = ?");
10738
- params.push(filter.plan_id);
10833
+ const missing = [];
10834
+ for (const v of templateVars) {
10835
+ if (v.required && merged[v.name] === undefined) {
10836
+ missing.push(v.name);
10837
+ }
10739
10838
  }
10740
- if (filter.task_list_id) {
10741
- conditions.push("task_list_id = ?");
10742
- params.push(filter.task_list_id);
10839
+ if (missing.length > 0) {
10840
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
10743
10841
  }
10744
- addMetadataConditions(filter.metadata, conditions, params);
10745
- if (!filter.include_archived) {
10746
- conditions.push("archived_at IS NULL");
10842
+ return merged;
10843
+ }
10844
+ function substituteVars(text, variables) {
10845
+ let result = text;
10846
+ for (const [key, val] of Object.entries(variables)) {
10847
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
10747
10848
  }
10748
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10749
- const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
10750
- return row.count;
10849
+ return result;
10751
10850
  }
10752
- function updateTask(id, input, db) {
10851
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
10753
10852
  const d = db || getDatabase();
10754
- const task = getTask(id, d);
10755
- if (!task)
10756
- throw new TaskNotFoundError(id);
10757
- if (task.version !== input.version) {
10758
- throw new VersionConflictError(id, input.version, task.version);
10759
- }
10760
- const timestamp = now();
10761
- const completionTimestamp = input.completed_at ?? timestamp;
10762
- const sets = ["version = version + 1", "updated_at = ?"];
10763
- const params = [timestamp];
10764
- if (input.title !== undefined) {
10765
- sets.push("title = ?");
10766
- params.push(input.title);
10853
+ const template = getTemplateWithTasks(templateId, d);
10854
+ if (!template)
10855
+ throw new Error(`Template not found: ${templateId}`);
10856
+ const visited = _visitedTemplateIds || new Set;
10857
+ if (visited.has(template.id)) {
10858
+ throw new Error(`Circular template reference detected: ${template.id}`);
10767
10859
  }
10768
- if (input.description !== undefined) {
10769
- sets.push("description = ?");
10770
- params.push(input.description);
10860
+ visited.add(template.id);
10861
+ const resolved = resolveVariables(template.variables, variables);
10862
+ if (template.tasks.length === 0) {
10863
+ const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
10864
+ const task = createTask(input, d);
10865
+ return [task];
10771
10866
  }
10772
- if (input.status !== undefined) {
10773
- if (input.status === "completed") {
10774
- checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
10867
+ const createdTasks = [];
10868
+ const positionToId = new Map;
10869
+ const skippedPositions = new Set;
10870
+ for (const tt of template.tasks) {
10871
+ if (tt.include_template_id) {
10872
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
10873
+ createdTasks.push(...includedTasks);
10874
+ if (includedTasks.length > 0) {
10875
+ positionToId.set(tt.position, includedTasks[0].id);
10876
+ } else {
10877
+ skippedPositions.add(tt.position);
10878
+ }
10879
+ continue;
10775
10880
  }
10776
- sets.push("status = ?");
10777
- params.push(input.status);
10778
- if (input.status === "completed") {
10779
- sets.push("completed_at = ?");
10780
- params.push(completionTimestamp);
10881
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
10882
+ skippedPositions.add(tt.position);
10883
+ continue;
10781
10884
  }
10885
+ let title = tt.title_pattern;
10886
+ let desc = tt.description;
10887
+ title = substituteVars(title, resolved);
10888
+ if (desc)
10889
+ desc = substituteVars(desc, resolved);
10890
+ const task = createTask({
10891
+ title,
10892
+ description: desc ?? undefined,
10893
+ priority: tt.priority,
10894
+ tags: tt.tags,
10895
+ task_type: tt.task_type ?? undefined,
10896
+ project_id: projectId,
10897
+ task_list_id: taskListId,
10898
+ metadata: tt.metadata
10899
+ }, d);
10900
+ createdTasks.push(task);
10901
+ positionToId.set(tt.position, task.id);
10782
10902
  }
10783
- if (input.priority !== undefined) {
10784
- sets.push("priority = ?");
10785
- params.push(input.priority);
10786
- }
10787
- if (input.project_id !== undefined) {
10788
- sets.push("project_id = ?");
10789
- params.push(input.project_id);
10903
+ for (const tt of template.tasks) {
10904
+ if (skippedPositions.has(tt.position))
10905
+ continue;
10906
+ if (tt.include_template_id)
10907
+ continue;
10908
+ const deps = tt.depends_on_positions;
10909
+ for (const depPos of deps) {
10910
+ if (skippedPositions.has(depPos))
10911
+ continue;
10912
+ const taskId = positionToId.get(tt.position);
10913
+ const depId = positionToId.get(depPos);
10914
+ if (taskId && depId) {
10915
+ addDependency(taskId, depId, d);
10916
+ }
10917
+ }
10790
10918
  }
10791
- if (input.assigned_to !== undefined) {
10792
- sets.push("assigned_to = ?");
10793
- params.push(input.assigned_to);
10919
+ return createdTasks;
10920
+ }
10921
+ function previewTemplate(templateId, variables, db) {
10922
+ const d = db || getDatabase();
10923
+ const template = getTemplateWithTasks(templateId, d);
10924
+ if (!template)
10925
+ throw new Error(`Template not found: ${templateId}`);
10926
+ const resolved = resolveVariables(template.variables, variables);
10927
+ const tasks = [];
10928
+ if (template.tasks.length === 0) {
10929
+ tasks.push({
10930
+ position: 0,
10931
+ title: substituteVars(template.title_pattern, resolved),
10932
+ description: template.description ? substituteVars(template.description, resolved) : null,
10933
+ priority: template.priority,
10934
+ tags: template.tags,
10935
+ task_type: null,
10936
+ depends_on_positions: []
10937
+ });
10938
+ } else {
10939
+ for (const tt of template.tasks) {
10940
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
10941
+ continue;
10942
+ tasks.push({
10943
+ position: tt.position,
10944
+ title: substituteVars(tt.title_pattern, resolved),
10945
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
10946
+ priority: tt.priority,
10947
+ tags: tt.tags,
10948
+ task_type: tt.task_type,
10949
+ depends_on_positions: tt.depends_on_positions
10950
+ });
10951
+ }
10794
10952
  }
10795
- if (input.working_dir !== undefined) {
10796
- sets.push("working_dir = ?");
10797
- params.push(input.working_dir);
10953
+ return {
10954
+ template_id: template.id,
10955
+ template_name: template.name,
10956
+ description: template.description,
10957
+ variables: template.variables,
10958
+ resolved_variables: resolved,
10959
+ tasks
10960
+ };
10961
+ }
10962
+ var init_templates = __esm(() => {
10963
+ init_database();
10964
+ init_tasks();
10965
+ init_storage_tombstones();
10966
+ });
10967
+
10968
+ // src/db/task-graph.ts
10969
+ function addDependency(taskId, dependsOn, db) {
10970
+ const d = db || getDatabase();
10971
+ if (!getTask(taskId, d))
10972
+ throw new TaskNotFoundError(taskId);
10973
+ if (!getTask(dependsOn, d))
10974
+ throw new TaskNotFoundError(dependsOn);
10975
+ if (wouldCreateCycle(taskId, dependsOn, d)) {
10976
+ throw new DependencyCycleError(taskId, dependsOn);
10798
10977
  }
10799
- if (input.tags !== undefined) {
10800
- sets.push("tags = ?");
10801
- params.push(JSON.stringify(input.tags));
10978
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
10979
+ }
10980
+ function removeDependency(taskId, dependsOn, db) {
10981
+ const d = db || getDatabase();
10982
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
10983
+ return result.changes > 0;
10984
+ }
10985
+ function getTaskDependencies(taskId, db) {
10986
+ const d = db || getDatabase();
10987
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
10988
+ }
10989
+ function getTaskDependents(taskId, db) {
10990
+ const d = db || getDatabase();
10991
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
10992
+ }
10993
+ function cloneTask(taskId, overrides, db) {
10994
+ const d = db || getDatabase();
10995
+ const source = getTask(taskId, d);
10996
+ if (!source)
10997
+ throw new TaskNotFoundError(taskId);
10998
+ const input = {
10999
+ title: overrides?.title ?? source.title,
11000
+ description: overrides?.description ?? source.description ?? undefined,
11001
+ priority: overrides?.priority ?? source.priority,
11002
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
11003
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
11004
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
11005
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
11006
+ status: overrides?.status ?? "pending",
11007
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
11008
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
11009
+ tags: overrides?.tags ?? source.tags,
11010
+ metadata: overrides?.metadata ?? source.metadata,
11011
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
11012
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
11013
+ };
11014
+ return createTask(input, d);
11015
+ }
11016
+ function getTaskGraph(taskId, direction = "both", db) {
11017
+ const d = db || getDatabase();
11018
+ const task = getTask(taskId, d);
11019
+ if (!task)
11020
+ throw new TaskNotFoundError(taskId);
11021
+ function toNode(t) {
11022
+ const deps = getTaskDependencies(t.id, d);
11023
+ const hasUnfinishedDeps = deps.some((dep) => {
11024
+ const depTask = getTask(dep.depends_on, d);
11025
+ return depTask && depTask.status !== "completed";
11026
+ });
11027
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
10802
11028
  }
10803
- if (input.metadata !== undefined) {
10804
- sets.push("metadata = ?");
10805
- params.push(JSON.stringify(input.metadata));
11029
+ function buildUp(id, visited) {
11030
+ if (visited.has(id))
11031
+ return [];
11032
+ visited.add(id);
11033
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
11034
+ return deps.map((dep) => {
11035
+ const depTask = getTask(dep.depends_on, d);
11036
+ if (!depTask)
11037
+ return null;
11038
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
11039
+ }).filter(Boolean);
10806
11040
  }
10807
- if (input.plan_id !== undefined) {
10808
- sets.push("plan_id = ?");
10809
- params.push(input.plan_id);
11041
+ function buildDown(id, visited) {
11042
+ if (visited.has(id))
11043
+ return [];
11044
+ visited.add(id);
11045
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
11046
+ return dependents.map((dep) => {
11047
+ const depTask = getTask(dep.task_id, d);
11048
+ if (!depTask)
11049
+ return null;
11050
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
11051
+ }).filter(Boolean);
10810
11052
  }
10811
- if (input.task_list_id !== undefined) {
11053
+ const rootNode = toNode(task);
11054
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
11055
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
11056
+ return { task: rootNode, depends_on, blocks };
11057
+ }
11058
+ function moveTask(taskId, target, db) {
11059
+ const d = db || getDatabase();
11060
+ const task = getTask(taskId, d);
11061
+ if (!task)
11062
+ throw new TaskNotFoundError(taskId);
11063
+ const sets = ["updated_at = ?", "version = version + 1"];
11064
+ const params = [now()];
11065
+ if (target.task_list_id !== undefined) {
10812
11066
  sets.push("task_list_id = ?");
10813
- params.push(input.task_list_id);
10814
- }
10815
- if (input.due_at !== undefined) {
10816
- sets.push("due_at = ?");
10817
- params.push(input.due_at);
10818
- }
10819
- if (input.estimated_minutes !== undefined) {
10820
- sets.push("estimated_minutes = ?");
10821
- params.push(input.estimated_minutes);
10822
- }
10823
- if (input.sla_minutes !== undefined) {
10824
- sets.push("sla_minutes = ?");
10825
- params.push(input.sla_minutes);
10826
- }
10827
- if (input.actual_minutes !== undefined) {
10828
- sets.push("actual_minutes = ?");
10829
- params.push(input.actual_minutes);
10830
- }
10831
- if (input.completed_at !== undefined && input.status !== "completed") {
10832
- sets.push("completed_at = ?");
10833
- params.push(input.completed_at);
10834
- }
10835
- if (input.confidence !== undefined) {
10836
- sets.push("confidence = ?");
10837
- params.push(input.confidence);
10838
- }
10839
- if (input.retry_count !== undefined) {
10840
- sets.push("retry_count = ?");
10841
- params.push(input.retry_count);
11067
+ params.push(target.task_list_id);
10842
11068
  }
10843
- if (input.max_retries !== undefined) {
10844
- sets.push("max_retries = ?");
10845
- params.push(input.max_retries);
11069
+ if (target.project_id !== undefined) {
11070
+ sets.push("project_id = ?");
11071
+ params.push(target.project_id);
10846
11072
  }
10847
- if (input.retry_after !== undefined) {
10848
- sets.push("retry_after = ?");
10849
- params.push(input.retry_after);
11073
+ if (target.plan_id !== undefined) {
11074
+ sets.push("plan_id = ?");
11075
+ params.push(target.plan_id);
10850
11076
  }
10851
- if (input.requires_approval !== undefined) {
10852
- sets.push("requires_approval = ?");
10853
- params.push(input.requires_approval ? 1 : 0);
11077
+ params.push(taskId);
11078
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
11079
+ return getTask(taskId, d);
11080
+ }
11081
+ function wouldCreateCycle(taskId, dependsOn, db) {
11082
+ const visited = new Set;
11083
+ const queue = [dependsOn];
11084
+ while (queue.length > 0) {
11085
+ const current = queue.shift();
11086
+ if (current === taskId)
11087
+ return true;
11088
+ if (visited.has(current))
11089
+ continue;
11090
+ visited.add(current);
11091
+ const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
11092
+ for (const dep of deps) {
11093
+ queue.push(dep.depends_on);
11094
+ }
10854
11095
  }
10855
- if (input.approved_by !== undefined) {
10856
- sets.push("approved_by = ?");
10857
- params.push(input.approved_by);
10858
- sets.push("approved_at = ?");
10859
- params.push(now());
10860
- }
10861
- if (input.recurrence_rule !== undefined) {
10862
- sets.push("recurrence_rule = ?");
10863
- params.push(input.recurrence_rule);
11096
+ return false;
11097
+ }
11098
+ var init_task_graph = __esm(() => {
11099
+ init_types();
11100
+ init_database();
11101
+ init_task_crud();
11102
+ });
11103
+
11104
+ // src/db/task-lifecycle.ts
11105
+ var exports_task_lifecycle = {};
11106
+ __export(exports_task_lifecycle, {
11107
+ unlockTask: () => unlockTask,
11108
+ stealTask: () => stealTask,
11109
+ startTask: () => startTask,
11110
+ spawnNextRecurrence: () => spawnNextRecurrence,
11111
+ lockTask: () => lockTask,
11112
+ getTasksChangedSince: () => getTasksChangedSince,
11113
+ getTaskLockStatus: () => getTaskLockStatus,
11114
+ getStaleTasks: () => getStaleTasks,
11115
+ getNextTask: () => getNextTask,
11116
+ getBlockingDeps: () => getBlockingDeps,
11117
+ getActiveWork: () => getActiveWork,
11118
+ failTask: () => failTask,
11119
+ completeTask: () => completeTask,
11120
+ claimOrSteal: () => claimOrSteal,
11121
+ claimNextTask: () => claimNextTask
11122
+ });
11123
+ function lockExpiresAt(lockedAt) {
11124
+ if (!lockedAt)
11125
+ return null;
11126
+ return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
11127
+ }
11128
+ function assertStartable(task, agentId) {
11129
+ if (task.status === "pending")
11130
+ return;
11131
+ if (task.status === "in_progress")
11132
+ return;
11133
+ throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
11134
+ }
11135
+ function getBlockingDeps(id, db) {
11136
+ const d = db || getDatabase();
11137
+ const deps = getTaskDependencies(id, d);
11138
+ if (deps.length === 0)
11139
+ return [];
11140
+ const blocking = [];
11141
+ for (const dep of deps) {
11142
+ const task = getTask(dep.depends_on, d);
11143
+ if (task && task.status !== "completed")
11144
+ blocking.push(task);
10864
11145
  }
10865
- if (input.task_type !== undefined) {
10866
- sets.push("task_type = ?");
10867
- params.push(input.task_type ?? null);
11146
+ return blocking;
11147
+ }
11148
+ function startTask(id, agentId, db) {
11149
+ const d = db || getDatabase();
11150
+ const databasePath = databasePathFromDatabase(d);
11151
+ const task = getTask(id, d);
11152
+ if (!task)
11153
+ throw new TaskNotFoundError(id);
11154
+ assertStartable(task, agentId);
11155
+ const blocking = getBlockingDeps(id, d);
11156
+ if (blocking.length > 0) {
11157
+ const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
11158
+ emitLocalEventHooksQuiet({
11159
+ type: "task.blocked",
11160
+ payload: {
11161
+ id,
11162
+ agent_id: agentId,
11163
+ title: task.title,
11164
+ blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
11165
+ },
11166
+ databasePath
11167
+ });
11168
+ throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
10868
11169
  }
10869
- params.push(id, input.version);
10870
- const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
11170
+ const cutoff = lockExpiryCutoff();
11171
+ const timestamp = now();
11172
+ const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
11173
+ WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
10871
11174
  if (result.changes === 0) {
10872
11175
  const current = getTask(id, d);
10873
- throw new VersionConflictError(id, input.version, current?.version ?? -1);
10874
- }
10875
- if (input.tags !== undefined) {
10876
- replaceTaskTags(id, input.tags, d);
11176
+ if (!current)
11177
+ throw new TaskNotFoundError(id);
11178
+ assertStartable(current, agentId);
11179
+ if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
11180
+ throw new LockError(id, current.locked_by);
11181
+ }
11182
+ throw new Error(`Task ${id} could not be started because it changed during claim`);
10877
11183
  }
10878
- const agentId = task.assigned_to || task.agent_id || null;
10879
- if (input.status !== undefined && input.status !== task.status)
10880
- logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
10881
- if (input.priority !== undefined && input.priority !== task.priority)
10882
- logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
10883
- if (input.title !== undefined && input.title !== task.title)
10884
- logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
10885
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
10886
- logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
10887
- if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
10888
- logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
10889
- if (input.approved_by !== undefined)
10890
- logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
10891
- const updatedTask = {
10892
- ...task,
10893
- ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
10894
- tags: input.tags ?? task.tags,
10895
- metadata: input.metadata ?? task.metadata,
10896
- version: task.version + 1,
10897
- updated_at: timestamp,
10898
- completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
10899
- sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
10900
- actual_minutes: input.actual_minutes ?? task.actual_minutes,
10901
- confidence: input.confidence !== undefined ? input.confidence : task.confidence,
10902
- retry_count: input.retry_count ?? task.retry_count,
10903
- max_retries: input.max_retries ?? task.max_retries,
10904
- retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
10905
- requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
10906
- approved_by: input.approved_by ?? task.approved_by,
10907
- approved_at: input.approved_by ? timestamp : task.approved_at
10908
- };
11184
+ logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
11185
+ const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
11186
+ const payload = taskEventData(startedTask, { agent_id: agentId });
11187
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
11188
+ emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
11189
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
11190
+ return startedTask;
11191
+ }
11192
+ function completeTask(id, agentId, db, options) {
11193
+ const d = db || getDatabase();
10909
11194
  const databasePath = databasePathFromDatabase(d);
10910
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
10911
- const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
10912
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
10913
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
10914
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
11195
+ const task = getTask(id, d);
11196
+ if (!task)
11197
+ throw new TaskNotFoundError(id);
11198
+ if (task.status === "completed") {
11199
+ return task;
10915
11200
  }
10916
- if (input.status !== undefined && input.status !== task.status) {
10917
- const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
10918
- dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
10919
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
10920
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
11201
+ if (task.status === "cancelled") {
11202
+ throw new Error(`Task ${id} is cancelled and cannot be completed`);
10921
11203
  }
10922
- if (input.approved_by !== undefined) {
10923
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
11204
+ if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
11205
+ throw new LockError(id, task.locked_by);
10924
11206
  }
10925
- const updatePayload = taskEventData(updatedTask);
10926
- dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
10927
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
10928
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
10929
- return updatedTask;
10930
- }
10931
- function deleteTask(id, db) {
10932
- const d = db || getDatabase();
10933
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
10934
- if (!row)
10935
- return false;
10936
- recordStorageTombstone({
10937
- object_type: "tasks",
10938
- object_id: id,
10939
- payload: rowToTask(row),
10940
- version: row.version
10941
- }, d);
10942
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
10943
- return result.changes > 0;
10944
- }
10945
- var init_task_crud = __esm(() => {
10946
- init_types();
10947
- init_database();
10948
- init_completion_guard();
10949
- init_event_emission_safety();
10950
- init_event_hooks();
10951
- init_shared_events();
10952
- init_audit();
10953
- init_webhooks();
10954
- init_checklists();
10955
- init_storage_tombstones();
10956
- });
10957
-
10958
- // src/lib/recurrence.ts
10959
- function parseRecurrenceRule(rule) {
10960
- const normalized = rule.trim().toLowerCase();
10961
- if (normalized === "every weekday" || normalized === "every weekdays") {
10962
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
11207
+ checkCompletionGuard(task, agentId || null, d);
11208
+ const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes, attachment_ids: options.attachment_ids } : undefined;
11209
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
11210
+ const completionMeta = {};
11211
+ if (hasEvidence)
11212
+ completionMeta._evidence = evidence;
11213
+ if (options?.confidence !== undefined) {
11214
+ completionMeta._completion = { confidence: options.confidence };
10963
11215
  }
10964
- if (normalized === "every day" || normalized === "daily") {
10965
- return { type: "interval", interval: 1, unit: "day" };
11216
+ const hasMeta = Object.keys(completionMeta).length > 0;
11217
+ const timestamp = options?.completed_at || now();
11218
+ const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
11219
+ const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
11220
+ const finalVersion = versionBeforeStatus + 1;
11221
+ const tx = d.transaction(() => {
11222
+ if (hasMeta) {
11223
+ const meta2 = { ...task.metadata, ...completionMeta };
11224
+ const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
11225
+ if (metaResult.changes === 0) {
11226
+ const current = getTask(id, d);
11227
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
11228
+ }
11229
+ }
11230
+ const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
11231
+ WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
11232
+ if (statusResult.changes === 0) {
11233
+ const current = getTask(id, d);
11234
+ throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
11235
+ }
11236
+ });
11237
+ tx();
11238
+ logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
11239
+ const completedTaskForEvent = {
11240
+ ...task,
11241
+ status: "completed",
11242
+ locked_by: null,
11243
+ locked_at: null,
11244
+ completed_at: timestamp,
11245
+ confidence,
11246
+ version: finalVersion,
11247
+ updated_at: timestamp,
11248
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
11249
+ };
11250
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
11251
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
11252
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
11253
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
11254
+ let spawnedTask = null;
11255
+ if (task.recurrence_rule && !options?.skip_recurrence) {
11256
+ try {
11257
+ spawnedTask = spawnNextRecurrence(task, d, timestamp);
11258
+ } catch (e) {
11259
+ spawnedTask = null;
11260
+ console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
11261
+ }
10966
11262
  }
10967
- if (normalized === "every week" || normalized === "weekly") {
10968
- return { type: "interval", interval: 1, unit: "week" };
11263
+ let spawnedFromTemplate = null;
11264
+ if (task.spawns_template_id) {
11265
+ const spawnDepth = task.metadata?._spawn_depth || 0;
11266
+ if (spawnDepth >= MAX_SPAWN_DEPTH) {
11267
+ console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
11268
+ } else {
11269
+ try {
11270
+ const input = taskFromTemplate(task.spawns_template_id, {
11271
+ project_id: task.project_id ?? undefined,
11272
+ plan_id: task.plan_id ?? undefined,
11273
+ task_list_id: task.task_list_id ?? undefined,
11274
+ assigned_to: task.assigned_to ?? undefined
11275
+ }, d);
11276
+ input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
11277
+ spawnedFromTemplate = createTask(input, d);
11278
+ } catch {}
11279
+ }
10969
11280
  }
10970
- if (normalized === "every month" || normalized === "monthly") {
10971
- return { type: "interval", interval: 1, unit: "month" };
11281
+ const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
11282
+ if (spawnedTask) {
11283
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
10972
11284
  }
10973
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
10974
- if (intervalMatch) {
10975
- return {
10976
- type: "interval",
10977
- interval: parseInt(intervalMatch[1], 10),
10978
- unit: intervalMatch[2]
10979
- };
11285
+ if (spawnedFromTemplate) {
11286
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
10980
11287
  }
10981
- const daysMatch = normalized.match(/^every\s+(.+)$/);
10982
- if (daysMatch) {
10983
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
10984
- const days = [];
10985
- for (const part of dayParts) {
10986
- const dayNum = DAY_NAMES[part];
10987
- if (dayNum !== undefined) {
10988
- days.push(dayNum);
10989
- }
10990
- }
10991
- if (days.length > 0) {
10992
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
11288
+ const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
11289
+ JOIN task_dependencies td ON td.task_id = t.id
11290
+ WHERE td.depends_on = ? AND t.status = 'pending'
11291
+ AND NOT EXISTS (
11292
+ SELECT 1 FROM task_dependencies td2
11293
+ JOIN tasks dep2 ON dep2.id = td2.depends_on
11294
+ WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
11295
+ )`).all(id, id);
11296
+ if (unblockedDeps.length > 0) {
11297
+ meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
11298
+ for (const dep of unblockedDeps) {
11299
+ const depTask = getTask(dep.id, d);
11300
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
11301
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
11302
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
11303
+ if (depTask)
11304
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
10993
11305
  }
10994
11306
  }
10995
- 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"`);
11307
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
10996
11308
  }
10997
- function isValidRecurrenceRule(rule) {
10998
- try {
10999
- parseRecurrenceRule(rule);
11000
- return true;
11001
- } catch {
11002
- return false;
11309
+ function lockTask(id, agentId, db) {
11310
+ const d = db || getDatabase();
11311
+ const task = getTask(id, d);
11312
+ if (!task)
11313
+ throw new TaskNotFoundError(id);
11314
+ if (task.status === "completed" || task.status === "cancelled") {
11315
+ return {
11316
+ success: false,
11317
+ error: `Task is ${task.status} and cannot be locked`
11318
+ };
11003
11319
  }
11004
- }
11005
- function nextOccurrence(rule, from) {
11006
- const parsed = parseRecurrenceRule(rule);
11007
- const base = from || new Date;
11008
- if (parsed.type === "interval") {
11009
- const next = new Date(base);
11010
- if (parsed.unit === "day") {
11011
- next.setDate(next.getDate() + parsed.interval);
11012
- } else if (parsed.unit === "week") {
11013
- next.setDate(next.getDate() + parsed.interval * 7);
11014
- } else if (parsed.unit === "month") {
11015
- next.setMonth(next.getMonth() + parsed.interval);
11016
- }
11017
- return next.toISOString();
11320
+ if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
11321
+ const timestamp2 = now();
11322
+ d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
11323
+ logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
11324
+ return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
11018
11325
  }
11019
- if (parsed.type === "specific_days") {
11020
- const currentDay = base.getDay();
11021
- const days = parsed.days;
11022
- let daysToAdd = Infinity;
11023
- for (const day of days) {
11024
- let diff = day - currentDay;
11025
- if (diff <= 0)
11026
- diff += 7;
11027
- if (diff < daysToAdd)
11028
- daysToAdd = diff;
11326
+ const cutoff = lockExpiryCutoff();
11327
+ const timestamp = now();
11328
+ const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
11329
+ WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
11330
+ if (result.changes === 0) {
11331
+ const current = getTask(id, d);
11332
+ if (!current)
11333
+ throw new TaskNotFoundError(id);
11334
+ if (current.status === "completed" || current.status === "cancelled") {
11335
+ return {
11336
+ success: false,
11337
+ error: `Task is ${current.status} and cannot be locked`
11338
+ };
11029
11339
  }
11030
- const next = new Date(base);
11031
- next.setDate(next.getDate() + daysToAdd);
11032
- return next.toISOString();
11340
+ if (current.locked_by && !isLockExpired(current.locked_at)) {
11341
+ return {
11342
+ success: false,
11343
+ locked_by: current.locked_by,
11344
+ locked_at: current.locked_at,
11345
+ error: `Task is locked by ${current.locked_by}`
11346
+ };
11347
+ }
11348
+ return {
11349
+ success: false,
11350
+ error: `Task ${id} could not be locked because it changed during lock acquisition`
11351
+ };
11033
11352
  }
11034
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
11035
- }
11036
- var DAY_NAMES;
11037
- var init_recurrence = __esm(() => {
11038
- DAY_NAMES = {
11039
- sunday: 0,
11040
- sun: 0,
11041
- monday: 1,
11042
- mon: 1,
11043
- tuesday: 2,
11044
- tue: 2,
11045
- wednesday: 3,
11046
- wed: 3,
11047
- thursday: 4,
11048
- thu: 4,
11049
- friday: 5,
11050
- fri: 5,
11051
- saturday: 6,
11052
- sat: 6
11053
- };
11054
- });
11055
-
11056
- // src/db/templates.ts
11057
- var exports_templates = {};
11058
- __export(exports_templates, {
11059
- updateTemplate: () => updateTemplate,
11060
- tasksFromTemplate: () => tasksFromTemplate,
11061
- taskFromTemplate: () => taskFromTemplate,
11062
- resolveVariables: () => resolveVariables,
11063
- previewTemplate: () => previewTemplate,
11064
- listTemplates: () => listTemplates,
11065
- listTemplateVersions: () => listTemplateVersions,
11066
- importTemplate: () => importTemplate,
11067
- getTemplateWithTasks: () => getTemplateWithTasks,
11068
- getTemplateVersion: () => getTemplateVersion,
11069
- getTemplateTasks: () => getTemplateTasks,
11070
- getTemplate: () => getTemplate,
11071
- exportTemplate: () => exportTemplate,
11072
- evaluateCondition: () => evaluateCondition,
11073
- deleteTemplate: () => deleteTemplate,
11074
- createTemplate: () => createTemplate,
11075
- addTemplateTasks: () => addTemplateTasks
11076
- });
11077
- function rowToTemplate(row) {
11078
- return {
11079
- ...row,
11080
- tags: JSON.parse(row.tags || "[]"),
11081
- variables: JSON.parse(row.variables || "[]"),
11082
- metadata: JSON.parse(row.metadata || "{}"),
11083
- priority: row.priority || "medium",
11084
- version: row.version ?? 1
11085
- };
11086
- }
11087
- function rowToTemplateTask(row) {
11088
- return {
11089
- ...row,
11090
- tags: JSON.parse(row.tags || "[]"),
11091
- depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
11092
- metadata: JSON.parse(row.metadata || "{}"),
11093
- priority: row.priority || "medium",
11094
- condition: row.condition ?? null,
11095
- include_template_id: row.include_template_id ?? null
11096
- };
11097
- }
11098
- function resolveTemplateId(id, d) {
11099
- return resolvePartialId(d, "task_templates", id);
11353
+ logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
11354
+ return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
11100
11355
  }
11101
- function createTemplate(input, db) {
11356
+ function unlockTask(id, agentId, db) {
11102
11357
  const d = db || getDatabase();
11103
- const id = uuid();
11104
- const machineId = currentStorageMachineId(d);
11105
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
11106
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
11107
- id,
11108
- input.name,
11109
- input.title_pattern,
11110
- input.description || null,
11111
- input.priority || "medium",
11112
- JSON.stringify(input.tags || []),
11113
- JSON.stringify(input.variables || []),
11114
- input.project_id || null,
11115
- input.plan_id || null,
11116
- JSON.stringify(input.metadata || {}),
11117
- now(),
11118
- machineId
11119
- ]);
11120
- if (input.tasks && input.tasks.length > 0) {
11121
- addTemplateTasks(id, input.tasks, d);
11358
+ const task = getTask(id, d);
11359
+ if (!task)
11360
+ throw new TaskNotFoundError(id);
11361
+ if (agentId && task.locked_by && task.locked_by !== agentId) {
11362
+ throw new LockError(id, task.locked_by);
11122
11363
  }
11123
- return getTemplate(id, d);
11364
+ const timestamp = now();
11365
+ d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
11366
+ WHERE id = ?`, [timestamp, id]);
11367
+ return true;
11124
11368
  }
11125
- function getTemplate(id, db) {
11369
+ function getTaskLockStatus(id, db) {
11126
11370
  const d = db || getDatabase();
11127
- const resolved = resolveTemplateId(id, d);
11128
- if (!resolved)
11129
- return null;
11130
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
11131
- return row ? rowToTemplate(row) : null;
11371
+ const task = getTask(id, d);
11372
+ if (!task)
11373
+ throw new TaskNotFoundError(id);
11374
+ const expired = isLockExpired(task.locked_at);
11375
+ return {
11376
+ task_id: id,
11377
+ locked: !!task.locked_by && !expired,
11378
+ locked_by: task.locked_by,
11379
+ locked_at: task.locked_at,
11380
+ expires_at: lockExpiresAt(task.locked_at),
11381
+ expired
11382
+ };
11132
11383
  }
11133
- function listTemplates(db) {
11384
+ function claimNextTask(agentId, filters, db) {
11134
11385
  const d = db || getDatabase();
11135
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
11386
+ const MAX_ATTEMPTS = 25;
11387
+ const tried = new Set;
11388
+ for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
11389
+ const outcome = d.transaction(() => {
11390
+ const task = getNextTask(agentId, filters, d);
11391
+ if (!task)
11392
+ return { done: true, task: null };
11393
+ if (tried.has(task.id))
11394
+ return { done: true, task: null };
11395
+ tried.add(task.id);
11396
+ try {
11397
+ return { done: true, task: startTask(task.id, agentId, d) };
11398
+ } catch {
11399
+ return { done: false, task: null };
11400
+ }
11401
+ })();
11402
+ if (outcome.done)
11403
+ return outcome.task;
11404
+ }
11405
+ return null;
11136
11406
  }
11137
- function deleteTemplate(id, db) {
11407
+ function getNextTask(agentId, filters, db) {
11138
11408
  const d = db || getDatabase();
11139
- const resolved = resolveTemplateId(id, d);
11140
- if (!resolved)
11141
- return false;
11142
- const template = getTemplate(resolved, d);
11143
- if (!template)
11144
- return false;
11145
- recordStorageTombstone({
11146
- object_type: "templates",
11147
- object_id: resolved,
11148
- payload: template,
11149
- version: template.version
11150
- }, d);
11151
- return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
11152
- }
11153
- function updateTemplate(id, updates, db) {
11154
- const d = db || getDatabase();
11155
- const resolved = resolveTemplateId(id, d);
11156
- if (!resolved)
11157
- return null;
11158
- const current = getTemplateWithTasks(resolved, d);
11159
- if (current) {
11160
- const snapshot = JSON.stringify({
11161
- name: current.name,
11162
- title_pattern: current.title_pattern,
11163
- description: current.description,
11164
- priority: current.priority,
11165
- tags: current.tags,
11166
- variables: current.variables,
11167
- project_id: current.project_id,
11168
- plan_id: current.plan_id,
11169
- metadata: current.metadata,
11170
- tasks: current.tasks
11171
- });
11172
- d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
11173
- }
11174
- const sets = ["version = version + 1"];
11175
- const values = [];
11176
- if (updates.name !== undefined) {
11177
- sets.push("name = ?");
11178
- values.push(updates.name);
11179
- }
11180
- if (updates.title_pattern !== undefined) {
11181
- sets.push("title_pattern = ?");
11182
- values.push(updates.title_pattern);
11183
- }
11184
- if (updates.description !== undefined) {
11185
- sets.push("description = ?");
11186
- values.push(updates.description);
11409
+ clearExpiredLocks(d);
11410
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
11411
+ const params = [lockExpiryCutoff()];
11412
+ if (filters?.project_id) {
11413
+ conditions.push("project_id = ?");
11414
+ params.push(filters.project_id);
11187
11415
  }
11188
- if (updates.priority !== undefined) {
11189
- sets.push("priority = ?");
11190
- values.push(updates.priority);
11416
+ if (filters?.task_list_id) {
11417
+ conditions.push("task_list_id = ?");
11418
+ params.push(filters.task_list_id);
11191
11419
  }
11192
- if (updates.tags !== undefined) {
11193
- sets.push("tags = ?");
11194
- values.push(JSON.stringify(updates.tags));
11420
+ if (filters?.plan_id) {
11421
+ conditions.push("plan_id = ?");
11422
+ params.push(filters.plan_id);
11195
11423
  }
11196
- if (updates.variables !== undefined) {
11197
- sets.push("variables = ?");
11198
- values.push(JSON.stringify(updates.variables));
11424
+ if (filters?.tags && filters.tags.length > 0) {
11425
+ const placeholders = filters.tags.map(() => "?").join(",");
11426
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
11427
+ params.push(...filters.tags);
11199
11428
  }
11200
- if (updates.project_id !== undefined) {
11201
- sets.push("project_id = ?");
11202
- values.push(updates.project_id);
11429
+ 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')");
11430
+ const where = conditions.join(" AND ");
11431
+ let recentProjectIds = [];
11432
+ if (agentId) {
11433
+ const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
11434
+ recentProjectIds = recentRows.map((r) => r.project_id);
11203
11435
  }
11204
- if (updates.plan_id !== undefined) {
11205
- sets.push("plan_id = ?");
11206
- values.push(updates.plan_id);
11436
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
11437
+ if (agentId) {
11438
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
11439
+ params.push(agentId);
11207
11440
  }
11208
- if (updates.metadata !== undefined) {
11209
- sets.push("metadata = ?");
11210
- values.push(JSON.stringify(updates.metadata));
11441
+ if (recentProjectIds.length > 0) {
11442
+ const placeholders = recentProjectIds.map(() => "?").join(",");
11443
+ sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
11444
+ params.push(...recentProjectIds);
11211
11445
  }
11212
- values.push(resolved);
11213
- d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
11214
- return getTemplate(resolved, d);
11215
- }
11216
- function taskFromTemplate(templateId, overrides = {}, db) {
11217
- const t = getTemplate(templateId, db);
11218
- if (!t)
11219
- throw new Error(`Template not found: ${templateId}`);
11220
- const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
11221
- return {
11222
- title: cleanOverrides.title || t.title_pattern,
11223
- description: cleanOverrides.description ?? t.description ?? undefined,
11224
- priority: cleanOverrides.priority ?? t.priority,
11225
- tags: cleanOverrides.tags ?? t.tags,
11226
- project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
11227
- plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
11228
- metadata: cleanOverrides.metadata ?? t.metadata,
11229
- ...cleanOverrides
11230
- };
11446
+ 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`;
11447
+ const row = d.query(sql).get(...params);
11448
+ return row ? rowToTask(row) : null;
11231
11449
  }
11232
- function addTemplateTasks(templateId, tasks, db) {
11450
+ function getActiveWork(filters, db) {
11233
11451
  const d = db || getDatabase();
11234
- const template = getTemplate(templateId, d);
11235
- if (!template)
11236
- throw new Error(`Template not found: ${templateId}`);
11237
- d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
11238
- const results = [];
11239
- for (let i = 0;i < tasks.length; i++) {
11240
- const task = tasks[i];
11241
- const id = uuid();
11242
- d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
11243
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
11244
- id,
11245
- templateId,
11246
- i,
11247
- task.title_pattern,
11248
- task.description || null,
11249
- task.priority || "medium",
11250
- JSON.stringify(task.tags || []),
11251
- task.task_type || null,
11252
- task.condition || null,
11253
- task.include_template_id || null,
11254
- JSON.stringify(task.depends_on || []),
11255
- JSON.stringify(task.metadata || {}),
11256
- now()
11257
- ]);
11258
- const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
11259
- if (row)
11260
- results.push(rowToTemplateTask(row));
11452
+ clearExpiredLocks(d);
11453
+ const conditions = ["status = 'in_progress'"];
11454
+ const params = [];
11455
+ if (filters?.project_id) {
11456
+ conditions.push("project_id = ?");
11457
+ params.push(filters.project_id);
11261
11458
  }
11262
- return results;
11263
- }
11264
- function getTemplateWithTasks(id, db) {
11265
- const d = db || getDatabase();
11266
- const template = getTemplate(id, d);
11267
- if (!template)
11268
- return null;
11269
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
11270
- const tasks = rows.map(rowToTemplateTask);
11271
- return { ...template, tasks };
11459
+ if (filters?.task_list_id) {
11460
+ conditions.push("task_list_id = ?");
11461
+ params.push(filters.task_list_id);
11462
+ }
11463
+ const where = conditions.join(" AND ");
11464
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
11465
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
11466
+ updated_at DESC`).all(...params);
11467
+ return rows;
11272
11468
  }
11273
- function getTemplateTasks(templateId, db) {
11469
+ function getTasksChangedSince(since, filters, db) {
11274
11470
  const d = db || getDatabase();
11275
- const resolved = resolveTemplateId(templateId, d);
11276
- if (!resolved)
11277
- return [];
11278
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
11279
- return rows.map(rowToTemplateTask);
11280
- }
11281
- function evaluateCondition(condition, variables) {
11282
- if (!condition || condition.trim() === "")
11283
- return true;
11284
- const trimmed = condition.trim();
11285
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
11286
- if (eqMatch) {
11287
- const varName = eqMatch[1];
11288
- const expected = eqMatch[2].trim();
11289
- return (variables[varName] ?? "") === expected;
11290
- }
11291
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
11292
- if (neqMatch) {
11293
- const varName = neqMatch[1];
11294
- const expected = neqMatch[2].trim();
11295
- return (variables[varName] ?? "") !== expected;
11296
- }
11297
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
11298
- if (falsyMatch) {
11299
- const varName = falsyMatch[1];
11300
- const val = variables[varName];
11301
- return !val || val === "" || val === "false";
11471
+ const conditions = ["updated_at > ?"];
11472
+ const params = [since];
11473
+ if (filters?.project_id) {
11474
+ conditions.push("project_id = ?");
11475
+ params.push(filters.project_id);
11302
11476
  }
11303
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
11304
- if (truthyMatch) {
11305
- const varName = truthyMatch[1];
11306
- const val = variables[varName];
11307
- return !!val && val !== "" && val !== "false";
11477
+ if (filters?.task_list_id) {
11478
+ conditions.push("task_list_id = ?");
11479
+ params.push(filters.task_list_id);
11308
11480
  }
11309
- return true;
11481
+ const where = conditions.join(" AND ");
11482
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
11483
+ return rows.map(rowToTask);
11310
11484
  }
11311
- function exportTemplate(id, db) {
11485
+ function failTask(id, agentId, reason, options, db) {
11312
11486
  const d = db || getDatabase();
11313
- const template = getTemplateWithTasks(id, d);
11314
- if (!template)
11315
- throw new Error(`Template not found: ${id}`);
11316
- return {
11317
- name: template.name,
11318
- title_pattern: template.title_pattern,
11319
- description: template.description,
11320
- priority: template.priority,
11321
- tags: template.tags,
11322
- variables: template.variables,
11323
- project_id: template.project_id,
11324
- plan_id: template.plan_id,
11325
- metadata: template.metadata,
11326
- tasks: template.tasks.map((t) => ({
11327
- position: t.position,
11328
- title_pattern: t.title_pattern,
11329
- description: t.description,
11330
- priority: t.priority,
11331
- tags: t.tags,
11332
- task_type: t.task_type,
11333
- condition: t.condition,
11334
- include_template_id: t.include_template_id,
11335
- depends_on_positions: t.depends_on_positions,
11336
- metadata: t.metadata
11337
- }))
11487
+ const databasePath = databasePathFromDatabase(d);
11488
+ const task = getTask(id, d);
11489
+ if (!task)
11490
+ throw new TaskNotFoundError(id);
11491
+ const meta = {
11492
+ ...task.metadata,
11493
+ _failure: {
11494
+ reason: reason || "Unknown failure",
11495
+ error_code: options?.error_code || null,
11496
+ failed_by: agentId || null,
11497
+ failed_at: now(),
11498
+ retry_requested: options?.retry || false
11499
+ }
11500
+ };
11501
+ const timestamp = now();
11502
+ const failTx = d.transaction(() => {
11503
+ const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
11504
+ WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
11505
+ if (res.changes === 0) {
11506
+ const current = getTask(id, d);
11507
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
11508
+ }
11509
+ });
11510
+ failTx();
11511
+ const failedTask = {
11512
+ ...task,
11513
+ status: "failed",
11514
+ locked_by: null,
11515
+ locked_at: null,
11516
+ metadata: meta,
11517
+ version: task.version + 1,
11518
+ updated_at: timestamp
11338
11519
  };
11520
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
11521
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
11522
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
11523
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
11524
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
11525
+ let retryTask;
11526
+ if (options?.retry) {
11527
+ const retryCount = (task.retry_count || 0) + 1;
11528
+ const maxRetries = task.max_retries || 3;
11529
+ if (retryCount > maxRetries) {
11530
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
11531
+ JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
11532
+ id
11533
+ ]);
11534
+ } else {
11535
+ const backoffMinutes = Math.pow(5, retryCount - 1);
11536
+ const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
11537
+ let title = task.title;
11538
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
11539
+ title = title.slice(task.short_id.length + 2);
11540
+ }
11541
+ retryTask = createTask({
11542
+ title,
11543
+ description: task.description ?? undefined,
11544
+ priority: task.priority,
11545
+ project_id: task.project_id ?? undefined,
11546
+ task_list_id: task.task_list_id ?? undefined,
11547
+ plan_id: task.plan_id ?? undefined,
11548
+ assigned_to: task.assigned_to ?? undefined,
11549
+ tags: task.tags,
11550
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
11551
+ estimated_minutes: task.estimated_minutes ?? undefined,
11552
+ recurrence_rule: task.recurrence_rule ?? undefined,
11553
+ due_at: retryAfter
11554
+ }, d);
11555
+ d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
11556
+ }
11557
+ }
11558
+ return { task: failedTask, retryTask };
11339
11559
  }
11340
- function importTemplate(json, db) {
11560
+ function getStaleTasks(staleQuery = 30, filters, db) {
11341
11561
  const d = db || getDatabase();
11342
- const taskInputs = (json.tasks || []).map((t) => ({
11343
- title_pattern: t.title_pattern,
11344
- description: t.description ?? undefined,
11345
- priority: t.priority,
11346
- tags: t.tags,
11347
- task_type: t.task_type ?? undefined,
11348
- condition: t.condition ?? undefined,
11349
- include_template_id: t.include_template_id ?? undefined,
11350
- depends_on: t.depends_on_positions,
11351
- metadata: t.metadata
11352
- }));
11353
- return createTemplate({
11354
- name: json.name,
11355
- title_pattern: json.title_pattern,
11356
- description: json.description ?? undefined,
11357
- priority: json.priority,
11358
- tags: json.tags,
11359
- variables: json.variables,
11360
- project_id: json.project_id ?? undefined,
11361
- plan_id: json.plan_id ?? undefined,
11362
- metadata: json.metadata,
11363
- tasks: taskInputs
11364
- }, d);
11562
+ const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
11563
+ const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
11564
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
11565
+ const conditions = [
11566
+ "status = 'in_progress'",
11567
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
11568
+ ];
11569
+ const params = [cutoff, cutoff];
11570
+ if (effectiveFilters?.project_id) {
11571
+ conditions.push("project_id = ?");
11572
+ params.push(effectiveFilters.project_id);
11573
+ }
11574
+ if (effectiveFilters?.task_list_id) {
11575
+ conditions.push("task_list_id = ?");
11576
+ params.push(effectiveFilters.task_list_id);
11577
+ }
11578
+ const where = conditions.join(" AND ");
11579
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
11580
+ return rows.map(rowToTask);
11365
11581
  }
11366
- function getTemplateVersion(id, version, db) {
11582
+ function stealTask(agentId, opts, db) {
11367
11583
  const d = db || getDatabase();
11368
- const resolved = resolveTemplateId(id, d);
11369
- if (!resolved)
11584
+ const databasePath = databasePathFromDatabase(d);
11585
+ const staleMinutes = opts?.stale_minutes ?? 30;
11586
+ const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
11587
+ if (staleTasks.length === 0)
11370
11588
  return null;
11371
- const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
11372
- return row || null;
11589
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
11590
+ staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
11591
+ const target = staleTasks[0];
11592
+ const timestamp = now();
11593
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
11594
+ const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
11595
+ WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
11596
+ if (result.changes === 0)
11597
+ return null;
11598
+ logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
11599
+ logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
11600
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
11601
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
11602
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
11603
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
11604
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
11605
+ return stolenTask;
11373
11606
  }
11374
- function listTemplateVersions(id, db) {
11607
+ function claimOrSteal(agentId, filters, db) {
11375
11608
  const d = db || getDatabase();
11376
- const resolved = resolveTemplateId(id, d);
11377
- if (!resolved)
11378
- return [];
11379
- return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
11380
- }
11381
- function resolveVariables(templateVars, provided) {
11382
- const merged = { ...provided };
11383
- for (const v of templateVars) {
11384
- if (merged[v.name] === undefined && v.default !== undefined) {
11385
- merged[v.name] = v.default;
11386
- }
11387
- }
11388
- const missing = [];
11389
- for (const v of templateVars) {
11390
- if (v.required && merged[v.name] === undefined) {
11391
- missing.push(v.name);
11609
+ const tx = d.transaction(() => {
11610
+ const next = getNextTask(agentId, filters, d);
11611
+ if (next) {
11612
+ const started = startTask(next.id, agentId, d);
11613
+ return { task: started, stolen: false };
11392
11614
  }
11393
- }
11394
- if (missing.length > 0) {
11395
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
11396
- }
11397
- return merged;
11615
+ const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
11616
+ if (stolen)
11617
+ return { task: stolen, stolen: true };
11618
+ return null;
11619
+ });
11620
+ return tx();
11398
11621
  }
11399
- function substituteVars(text, variables) {
11400
- let result = text;
11401
- for (const [key, val] of Object.entries(variables)) {
11402
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
11622
+ function spawnNextRecurrence(completedTask, db, completedAt) {
11623
+ const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
11624
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
11625
+ let title = completedTask.title;
11626
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
11627
+ title = title.slice(completedTask.short_id.length + 2);
11403
11628
  }
11404
- return result;
11629
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
11630
+ return createTask({
11631
+ title,
11632
+ description: completedTask.description ?? undefined,
11633
+ priority: completedTask.priority,
11634
+ project_id: completedTask.project_id ?? undefined,
11635
+ task_list_id: completedTask.task_list_id ?? undefined,
11636
+ plan_id: completedTask.plan_id ?? undefined,
11637
+ assigned_to: completedTask.assigned_to ?? undefined,
11638
+ tags: completedTask.tags,
11639
+ metadata: completedTask.metadata,
11640
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
11641
+ sla_minutes: completedTask.sla_minutes ?? undefined,
11642
+ recurrence_rule: completedTask.recurrence_rule,
11643
+ recurrence_parent_id: recurrenceParentId,
11644
+ due_at: dueAt
11645
+ }, db);
11405
11646
  }
11406
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
11407
- const d = db || getDatabase();
11408
- const template = getTemplateWithTasks(templateId, d);
11409
- if (!template)
11410
- throw new Error(`Template not found: ${templateId}`);
11411
- const visited = _visitedTemplateIds || new Set;
11412
- if (visited.has(template.id)) {
11413
- throw new Error(`Circular template reference detected: ${template.id}`);
11414
- }
11415
- visited.add(template.id);
11416
- const resolved = resolveVariables(template.variables, variables);
11417
- if (template.tasks.length === 0) {
11418
- const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
11419
- const task = createTask(input, d);
11420
- return [task];
11421
- }
11422
- const createdTasks = [];
11423
- const positionToId = new Map;
11424
- const skippedPositions = new Set;
11425
- for (const tt of template.tasks) {
11426
- if (tt.include_template_id) {
11427
- const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
11428
- createdTasks.push(...includedTasks);
11429
- if (includedTasks.length > 0) {
11430
- positionToId.set(tt.position, includedTasks[0].id);
11431
- } else {
11432
- skippedPositions.add(tt.position);
11433
- }
11434
- continue;
11435
- }
11436
- if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
11437
- skippedPositions.add(tt.position);
11438
- continue;
11439
- }
11440
- let title = tt.title_pattern;
11441
- let desc = tt.description;
11442
- title = substituteVars(title, resolved);
11443
- if (desc)
11444
- desc = substituteVars(desc, resolved);
11445
- const task = createTask({
11446
- title,
11447
- description: desc ?? undefined,
11448
- priority: tt.priority,
11449
- tags: tt.tags,
11450
- task_type: tt.task_type ?? undefined,
11451
- project_id: projectId,
11452
- task_list_id: taskListId,
11453
- metadata: tt.metadata
11454
- }, d);
11455
- createdTasks.push(task);
11456
- positionToId.set(tt.position, task.id);
11647
+ var MAX_SPAWN_DEPTH = 10;
11648
+ var init_task_lifecycle = __esm(() => {
11649
+ init_types();
11650
+ init_database();
11651
+ init_completion_guard();
11652
+ init_event_emission_safety();
11653
+ init_event_hooks();
11654
+ init_shared_events();
11655
+ init_audit();
11656
+ init_recurrence();
11657
+ init_webhooks();
11658
+ init_templates();
11659
+ init_task_crud();
11660
+ init_task_graph();
11661
+ });
11662
+
11663
+ // src/db/task-crud.ts
11664
+ function rowToTask(row) {
11665
+ return {
11666
+ ...row,
11667
+ tags: JSON.parse(row.tags || "[]"),
11668
+ metadata: JSON.parse(row.metadata || "{}"),
11669
+ status: row.status,
11670
+ priority: row.priority,
11671
+ requires_approval: !!row.requires_approval
11672
+ };
11673
+ }
11674
+ function insertTaskTags(taskId, tags, db) {
11675
+ if (tags.length === 0)
11676
+ return;
11677
+ const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
11678
+ for (const tag of tags) {
11679
+ if (tag)
11680
+ stmt.run(taskId, tag);
11457
11681
  }
11458
- for (const tt of template.tasks) {
11459
- if (skippedPositions.has(tt.position))
11460
- continue;
11461
- if (tt.include_template_id)
11462
- continue;
11463
- const deps = tt.depends_on_positions;
11464
- for (const depPos of deps) {
11465
- if (skippedPositions.has(depPos))
11466
- continue;
11467
- const taskId = positionToId.get(tt.position);
11468
- const depId = positionToId.get(depPos);
11469
- if (taskId && depId) {
11470
- addDependency(taskId, depId, d);
11471
- }
11682
+ }
11683
+ function replaceTaskTags(taskId, tags, db) {
11684
+ db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
11685
+ insertTaskTags(taskId, tags, db);
11686
+ }
11687
+ function addMetadataConditions(metadata, conditions, params) {
11688
+ if (!metadata)
11689
+ return;
11690
+ for (const [key, value] of Object.entries(metadata)) {
11691
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
11692
+ throw new Error(`Invalid metadata filter key: ${key}`);
11472
11693
  }
11694
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
11695
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
11473
11696
  }
11474
- return createdTasks;
11475
11697
  }
11476
- function previewTemplate(templateId, variables, db) {
11698
+ function createTask(input, db) {
11477
11699
  const d = db || getDatabase();
11478
- const template = getTemplateWithTasks(templateId, d);
11479
- if (!template)
11480
- throw new Error(`Template not found: ${templateId}`);
11481
- const resolved = resolveVariables(template.variables, variables);
11482
- const tasks = [];
11483
- if (template.tasks.length === 0) {
11484
- tasks.push({
11485
- position: 0,
11486
- title: substituteVars(template.title_pattern, resolved),
11487
- description: template.description ? substituteVars(template.description, resolved) : null,
11488
- priority: template.priority,
11489
- tags: template.tags,
11490
- task_type: null,
11491
- depends_on_positions: []
11492
- });
11493
- } else {
11494
- for (const tt of template.tasks) {
11495
- if (tt.condition && !evaluateCondition(tt.condition, resolved))
11700
+ const timestamp = now();
11701
+ const tags = input.tags || [];
11702
+ const machineId = currentStorageMachineId(d);
11703
+ const assignedBy = input.assigned_by || input.agent_id;
11704
+ const assignedFromProject = input.assigned_from_project || null;
11705
+ let id = uuid();
11706
+ for (let attempt = 0;attempt < 3; attempt++) {
11707
+ try {
11708
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type, machine_id)
11709
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
11710
+ id,
11711
+ null,
11712
+ input.project_id || null,
11713
+ input.parent_id || null,
11714
+ input.plan_id || null,
11715
+ input.task_list_id || null,
11716
+ input.cycle_id || null,
11717
+ input.title,
11718
+ input.description || null,
11719
+ input.status || "pending",
11720
+ input.priority || "medium",
11721
+ input.agent_id || null,
11722
+ input.assigned_to || null,
11723
+ input.session_id || null,
11724
+ input.working_dir || null,
11725
+ JSON.stringify(tags),
11726
+ JSON.stringify(input.metadata || {}),
11727
+ timestamp,
11728
+ timestamp,
11729
+ input.due_at || null,
11730
+ input.estimated_minutes || null,
11731
+ input.sla_minutes ?? null,
11732
+ input.confidence ?? null,
11733
+ input.retry_count ?? 0,
11734
+ input.max_retries ?? 3,
11735
+ input.retry_after ?? null,
11736
+ input.requires_approval ? 1 : 0,
11737
+ null,
11738
+ null,
11739
+ input.recurrence_rule || null,
11740
+ input.recurrence_parent_id || null,
11741
+ input.spawns_template_id || null,
11742
+ input.reason || null,
11743
+ input.spawned_from_session || null,
11744
+ assignedBy || null,
11745
+ assignedFromProject || null,
11746
+ input.task_type || null,
11747
+ machineId
11748
+ ]);
11749
+ break;
11750
+ } catch (e) {
11751
+ if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
11752
+ id = uuid();
11496
11753
  continue;
11497
- tasks.push({
11498
- position: tt.position,
11499
- title: substituteVars(tt.title_pattern, resolved),
11500
- description: tt.description ? substituteVars(tt.description, resolved) : null,
11501
- priority: tt.priority,
11502
- tags: tt.tags,
11503
- task_type: tt.task_type,
11504
- depends_on_positions: tt.depends_on_positions
11505
- });
11754
+ }
11755
+ throw e;
11506
11756
  }
11507
11757
  }
11508
- return {
11509
- template_id: template.id,
11510
- template_name: template.name,
11511
- description: template.description,
11512
- variables: template.variables,
11513
- resolved_variables: resolved,
11514
- tasks
11515
- };
11516
- }
11517
- var init_templates = __esm(() => {
11518
- init_database();
11519
- init_tasks();
11520
- init_storage_tombstones();
11521
- });
11522
-
11523
- // src/db/task-graph.ts
11524
- function addDependency(taskId, dependsOn, db) {
11525
- const d = db || getDatabase();
11526
- if (!getTask(taskId, d))
11527
- throw new TaskNotFoundError(taskId);
11528
- if (!getTask(dependsOn, d))
11529
- throw new TaskNotFoundError(dependsOn);
11530
- if (wouldCreateCycle(taskId, dependsOn, d)) {
11531
- throw new DependencyCycleError(taskId, dependsOn);
11758
+ if (tags.length > 0) {
11759
+ insertTaskTags(id, tags, d);
11532
11760
  }
11533
- d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
11534
- }
11535
- function removeDependency(taskId, dependsOn, db) {
11536
- const d = db || getDatabase();
11537
- const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
11538
- return result.changes > 0;
11539
- }
11540
- function getTaskDependencies(taskId, db) {
11541
- const d = db || getDatabase();
11542
- return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
11761
+ const task = getTask(id, d);
11762
+ const payload = taskEventData(task);
11763
+ const databasePath = databasePathFromDatabase(d);
11764
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
11765
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
11766
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
11767
+ return task;
11543
11768
  }
11544
- function getTaskDependents(taskId, db) {
11769
+ function getTask(id, db) {
11545
11770
  const d = db || getDatabase();
11546
- return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
11771
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
11772
+ if (!row)
11773
+ return null;
11774
+ return rowToTask(row);
11547
11775
  }
11548
- function cloneTask(taskId, overrides, db) {
11776
+ function getTaskWithRelations(id, db) {
11549
11777
  const d = db || getDatabase();
11550
- const source = getTask(taskId, d);
11551
- if (!source)
11552
- throw new TaskNotFoundError(taskId);
11553
- const input = {
11554
- title: overrides?.title ?? source.title,
11555
- description: overrides?.description ?? source.description ?? undefined,
11556
- priority: overrides?.priority ?? source.priority,
11557
- project_id: overrides?.project_id ?? source.project_id ?? undefined,
11558
- parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
11559
- plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
11560
- task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
11561
- status: overrides?.status ?? "pending",
11562
- agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
11563
- assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
11564
- tags: overrides?.tags ?? source.tags,
11565
- metadata: overrides?.metadata ?? source.metadata,
11566
- estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
11567
- recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
11778
+ const task = getTask(id, d);
11779
+ if (!task)
11780
+ return null;
11781
+ const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
11782
+ const subtasks = subtaskRows.map(rowToTask);
11783
+ const depRows = d.query(`SELECT t.* FROM tasks t
11784
+ JOIN task_dependencies td ON td.depends_on = t.id
11785
+ WHERE td.task_id = ?`).all(id);
11786
+ const dependencies = depRows.map(rowToTask);
11787
+ const blockedByRows = d.query(`SELECT t.* FROM tasks t
11788
+ JOIN task_dependencies td ON td.task_id = t.id
11789
+ WHERE td.depends_on = ?`).all(id);
11790
+ const blocked_by = blockedByRows.map(rowToTask);
11791
+ const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
11792
+ const parent = task.parent_id ? getTask(task.parent_id, d) : null;
11793
+ const checklist = getChecklist(id, d);
11794
+ return {
11795
+ ...task,
11796
+ subtasks,
11797
+ dependencies,
11798
+ blocked_by,
11799
+ comments,
11800
+ parent,
11801
+ checklist
11568
11802
  };
11569
- return createTask(input, d);
11570
11803
  }
11571
- function getTaskGraph(taskId, direction = "both", db) {
11804
+ function listTasks(filter = {}, db) {
11572
11805
  const d = db || getDatabase();
11573
- const task = getTask(taskId, d);
11574
- if (!task)
11575
- throw new TaskNotFoundError(taskId);
11576
- function toNode(t) {
11577
- const deps = getTaskDependencies(t.id, d);
11578
- const hasUnfinishedDeps = deps.some((dep) => {
11579
- const depTask = getTask(dep.depends_on, d);
11580
- return depTask && depTask.status !== "completed";
11581
- });
11582
- return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
11806
+ const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
11807
+ clearExpiredLocks2(d);
11808
+ const conditions = [];
11809
+ const params = [];
11810
+ if (filter.project_id) {
11811
+ conditions.push("project_id = ?");
11812
+ params.push(filter.project_id);
11583
11813
  }
11584
- function buildUp(id, visited) {
11585
- if (visited.has(id))
11586
- return [];
11587
- visited.add(id);
11588
- const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
11589
- return deps.map((dep) => {
11590
- const depTask = getTask(dep.depends_on, d);
11591
- if (!depTask)
11592
- return null;
11593
- return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
11594
- }).filter(Boolean);
11814
+ if (filter.ids && filter.ids.length > 0) {
11815
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
11816
+ params.push(...filter.ids);
11595
11817
  }
11596
- function buildDown(id, visited) {
11597
- if (visited.has(id))
11598
- return [];
11599
- visited.add(id);
11600
- const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
11601
- return dependents.map((dep) => {
11602
- const depTask = getTask(dep.task_id, d);
11603
- if (!depTask)
11604
- return null;
11605
- return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
11606
- }).filter(Boolean);
11818
+ if (filter.parent_id !== undefined) {
11819
+ if (filter.parent_id === null) {
11820
+ conditions.push("parent_id IS NULL");
11821
+ } else {
11822
+ conditions.push("parent_id = ?");
11823
+ params.push(filter.parent_id);
11824
+ }
11607
11825
  }
11608
- const rootNode = toNode(task);
11609
- const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
11610
- const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
11611
- return { task: rootNode, depends_on, blocks };
11612
- }
11613
- function moveTask(taskId, target, db) {
11614
- const d = db || getDatabase();
11615
- const task = getTask(taskId, d);
11616
- if (!task)
11617
- throw new TaskNotFoundError(taskId);
11618
- const sets = ["updated_at = ?", "version = version + 1"];
11619
- const params = [now()];
11620
- if (target.task_list_id !== undefined) {
11621
- sets.push("task_list_id = ?");
11622
- params.push(target.task_list_id);
11826
+ if (filter.status) {
11827
+ if (Array.isArray(filter.status)) {
11828
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
11829
+ params.push(...filter.status);
11830
+ } else {
11831
+ conditions.push("status = ?");
11832
+ params.push(filter.status);
11833
+ }
11623
11834
  }
11624
- if (target.project_id !== undefined) {
11625
- sets.push("project_id = ?");
11626
- params.push(target.project_id);
11835
+ if (filter.priority) {
11836
+ if (Array.isArray(filter.priority)) {
11837
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
11838
+ params.push(...filter.priority);
11839
+ } else {
11840
+ conditions.push("priority = ?");
11841
+ params.push(filter.priority);
11842
+ }
11627
11843
  }
11628
- if (target.plan_id !== undefined) {
11629
- sets.push("plan_id = ?");
11630
- params.push(target.plan_id);
11844
+ if (filter.assigned_to) {
11845
+ conditions.push("assigned_to = ?");
11846
+ params.push(filter.assigned_to);
11631
11847
  }
11632
- params.push(taskId);
11633
- d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
11634
- return getTask(taskId, d);
11635
- }
11636
- function wouldCreateCycle(taskId, dependsOn, db) {
11637
- const visited = new Set;
11638
- const queue = [dependsOn];
11639
- while (queue.length > 0) {
11640
- const current = queue.shift();
11641
- if (current === taskId)
11642
- return true;
11643
- if (visited.has(current))
11644
- continue;
11645
- visited.add(current);
11646
- const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
11647
- for (const dep of deps) {
11648
- queue.push(dep.depends_on);
11848
+ if (filter.agent_id) {
11849
+ conditions.push("agent_id = ?");
11850
+ params.push(filter.agent_id);
11851
+ }
11852
+ if (filter.session_id) {
11853
+ conditions.push("session_id = ?");
11854
+ params.push(filter.session_id);
11855
+ }
11856
+ if (filter.tags && filter.tags.length > 0) {
11857
+ const placeholders = filter.tags.map(() => "?").join(",");
11858
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
11859
+ params.push(...filter.tags);
11860
+ }
11861
+ if (filter.plan_id) {
11862
+ conditions.push("plan_id = ?");
11863
+ params.push(filter.plan_id);
11864
+ }
11865
+ if (filter.task_list_id) {
11866
+ conditions.push("task_list_id = ?");
11867
+ params.push(filter.task_list_id);
11868
+ }
11869
+ if (filter.has_recurrence === true) {
11870
+ conditions.push("recurrence_rule IS NOT NULL");
11871
+ } else if (filter.has_recurrence === false) {
11872
+ conditions.push("recurrence_rule IS NULL");
11873
+ }
11874
+ if (filter.task_type) {
11875
+ if (Array.isArray(filter.task_type)) {
11876
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
11877
+ params.push(...filter.task_type);
11878
+ } else {
11879
+ conditions.push("task_type = ?");
11880
+ params.push(filter.task_type);
11649
11881
  }
11650
11882
  }
11651
- return false;
11883
+ addMetadataConditions(filter.metadata, conditions, params);
11884
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
11885
+ if (filter.cursor) {
11886
+ try {
11887
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
11888
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
11889
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
11890
+ } catch {}
11891
+ }
11892
+ if (!filter.include_archived) {
11893
+ conditions.push("archived_at IS NULL");
11894
+ }
11895
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
11896
+ let limitClause = "";
11897
+ if (filter.limit) {
11898
+ limitClause = " LIMIT ?";
11899
+ params.push(filter.limit);
11900
+ if (!filter.cursor && filter.offset) {
11901
+ limitClause += " OFFSET ?";
11902
+ params.push(filter.offset);
11903
+ }
11904
+ }
11905
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
11906
+ return rows.map(rowToTask);
11652
11907
  }
11653
- var init_task_graph = __esm(() => {
11654
- init_types();
11655
- init_database();
11656
- init_task_crud();
11657
- });
11658
-
11659
- // src/db/task-lifecycle.ts
11660
- function lockExpiresAt(lockedAt) {
11661
- if (!lockedAt)
11662
- return null;
11663
- return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
11908
+ function getTaskByFingerprint(fingerprint, db) {
11909
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
11910
+ return tasks[0] ?? null;
11664
11911
  }
11665
- function assertStartable(task, agentId) {
11666
- if (task.status === "pending")
11667
- return;
11668
- if (task.status === "in_progress")
11669
- return;
11670
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
11912
+ function mergeTaskMetadata(current, next, fingerprint) {
11913
+ return {
11914
+ ...current,
11915
+ ...next ?? {},
11916
+ fingerprint
11917
+ };
11671
11918
  }
11672
- function getBlockingDeps(id, db) {
11919
+ function upsertTaskByFingerprint(input, db) {
11673
11920
  const d = db || getDatabase();
11674
- const deps = getTaskDependencies(id, d);
11675
- if (deps.length === 0)
11676
- return [];
11677
- const blocking = [];
11678
- for (const dep of deps) {
11679
- const task = getTask(dep.depends_on, d);
11680
- if (task && task.status !== "completed")
11681
- blocking.push(task);
11682
- }
11683
- return blocking;
11921
+ const fingerprint = input.fingerprint.trim();
11922
+ if (!fingerprint)
11923
+ throw new Error("fingerprint is required");
11924
+ const tx = d.transaction(() => {
11925
+ const existing = getTaskByFingerprint(fingerprint, d);
11926
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
11927
+ if (!existing) {
11928
+ const task2 = createTask({ ...input, metadata }, d);
11929
+ return { task: task2, created: true };
11930
+ }
11931
+ const task = updateTask(existing.id, {
11932
+ version: existing.version,
11933
+ title: input.title,
11934
+ description: input.description,
11935
+ status: input.status,
11936
+ priority: input.priority,
11937
+ project_id: input.project_id,
11938
+ assigned_to: input.assigned_to,
11939
+ working_dir: input.working_dir,
11940
+ plan_id: input.plan_id,
11941
+ task_list_id: input.task_list_id,
11942
+ tags: input.tags,
11943
+ metadata,
11944
+ due_at: input.due_at,
11945
+ estimated_minutes: input.estimated_minutes,
11946
+ sla_minutes: input.sla_minutes,
11947
+ confidence: input.confidence,
11948
+ retry_count: input.retry_count,
11949
+ max_retries: input.max_retries,
11950
+ retry_after: input.retry_after,
11951
+ requires_approval: input.requires_approval,
11952
+ recurrence_rule: input.recurrence_rule,
11953
+ task_type: input.task_type
11954
+ }, d);
11955
+ return { task, created: false };
11956
+ });
11957
+ return tx();
11684
11958
  }
11685
- function startTask(id, agentId, db) {
11959
+ function countTasks(filter = {}, db) {
11686
11960
  const d = db || getDatabase();
11687
- const databasePath = databasePathFromDatabase(d);
11688
- const task = getTask(id, d);
11689
- if (!task)
11690
- throw new TaskNotFoundError(id);
11691
- assertStartable(task, agentId);
11692
- const blocking = getBlockingDeps(id, d);
11693
- if (blocking.length > 0) {
11694
- const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
11695
- emitLocalEventHooksQuiet({
11696
- type: "task.blocked",
11697
- payload: {
11698
- id,
11699
- agent_id: agentId,
11700
- title: task.title,
11701
- blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
11702
- },
11703
- databasePath
11704
- });
11705
- throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
11706
- }
11707
- const cutoff = lockExpiryCutoff();
11708
- const timestamp = now();
11709
- const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
11710
- WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
11711
- if (result.changes === 0) {
11712
- const current = getTask(id, d);
11713
- if (!current)
11714
- throw new TaskNotFoundError(id);
11715
- assertStartable(current, agentId);
11716
- if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
11717
- throw new LockError(id, current.locked_by);
11718
- }
11719
- throw new Error(`Task ${id} could not be started because it changed during claim`);
11961
+ const conditions = [];
11962
+ const params = [];
11963
+ if (filter.project_id) {
11964
+ conditions.push("project_id = ?");
11965
+ params.push(filter.project_id);
11720
11966
  }
11721
- logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
11722
- const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
11723
- const payload = taskEventData(startedTask, { agent_id: agentId });
11724
- dispatchWebhook2("task.started", payload, d).catch(() => {});
11725
- emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
11726
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
11727
- return startedTask;
11728
- }
11729
- function completeTask(id, agentId, db, options) {
11730
- const d = db || getDatabase();
11731
- const databasePath = databasePathFromDatabase(d);
11732
- const task = getTask(id, d);
11733
- if (!task)
11734
- throw new TaskNotFoundError(id);
11735
- if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
11736
- throw new LockError(id, task.locked_by);
11967
+ if (filter.ids && filter.ids.length > 0) {
11968
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
11969
+ params.push(...filter.ids);
11737
11970
  }
11738
- checkCompletionGuard(task, agentId || null, d);
11739
- const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes, attachment_ids: options.attachment_ids } : undefined;
11740
- const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
11741
- const completionMeta = {};
11742
- if (hasEvidence)
11743
- completionMeta._evidence = evidence;
11744
- if (options?.confidence !== undefined) {
11745
- completionMeta._completion = { confidence: options.confidence };
11971
+ if (filter.parent_id !== undefined) {
11972
+ if (filter.parent_id === null) {
11973
+ conditions.push("parent_id IS NULL");
11974
+ } else {
11975
+ conditions.push("parent_id = ?");
11976
+ params.push(filter.parent_id);
11977
+ }
11746
11978
  }
11747
- const hasMeta = Object.keys(completionMeta).length > 0;
11748
- const timestamp = options?.completed_at || now();
11749
- const confidence = options?.confidence !== undefined ? options.confidence : null;
11750
- const tx = d.transaction(() => {
11751
- if (hasMeta) {
11752
- const meta2 = { ...task.metadata, ...completionMeta };
11753
- const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
11754
- if (metaResult.changes === 0) {
11755
- const current = getTask(id, d);
11756
- throw new VersionConflictError(id, task.version, current?.version ?? -1);
11757
- }
11979
+ if (filter.status) {
11980
+ if (Array.isArray(filter.status)) {
11981
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
11982
+ params.push(...filter.status);
11983
+ } else {
11984
+ conditions.push("status = ?");
11985
+ params.push(filter.status);
11758
11986
  }
11759
- d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
11760
- WHERE id = ?`, [timestamp, confidence, timestamp, id]);
11761
- });
11762
- tx();
11763
- logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
11764
- const completedTaskForEvent = {
11765
- ...task,
11766
- status: "completed",
11767
- locked_by: null,
11768
- locked_at: null,
11769
- completed_at: timestamp,
11770
- confidence,
11771
- version: task.version + 1,
11772
- updated_at: timestamp,
11773
- metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
11774
- };
11775
- const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
11776
- dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
11777
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
11778
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
11779
- let spawnedTask = null;
11780
- if (task.recurrence_rule && !options?.skip_recurrence) {
11781
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
11782
11987
  }
11783
- let spawnedFromTemplate = null;
11784
- if (task.spawns_template_id) {
11785
- const spawnDepth = task.metadata?._spawn_depth || 0;
11786
- if (spawnDepth >= MAX_SPAWN_DEPTH) {
11787
- console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
11988
+ if (filter.priority) {
11989
+ if (Array.isArray(filter.priority)) {
11990
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
11991
+ params.push(...filter.priority);
11788
11992
  } else {
11789
- try {
11790
- const input = taskFromTemplate(task.spawns_template_id, {
11791
- project_id: task.project_id ?? undefined,
11792
- plan_id: task.plan_id ?? undefined,
11793
- task_list_id: task.task_list_id ?? undefined,
11794
- assigned_to: task.assigned_to ?? undefined
11795
- }, d);
11796
- input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
11797
- spawnedFromTemplate = createTask(input, d);
11798
- } catch {}
11993
+ conditions.push("priority = ?");
11994
+ params.push(filter.priority);
11799
11995
  }
11800
11996
  }
11801
- const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
11802
- if (spawnedTask) {
11803
- meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
11997
+ if (filter.assigned_to) {
11998
+ conditions.push("assigned_to = ?");
11999
+ params.push(filter.assigned_to);
11804
12000
  }
11805
- if (spawnedFromTemplate) {
11806
- meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
12001
+ if (filter.agent_id) {
12002
+ conditions.push("agent_id = ?");
12003
+ params.push(filter.agent_id);
11807
12004
  }
11808
- const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
11809
- JOIN task_dependencies td ON td.task_id = t.id
11810
- WHERE td.depends_on = ? AND t.status = 'pending'
11811
- AND NOT EXISTS (
11812
- SELECT 1 FROM task_dependencies td2
11813
- JOIN tasks dep2 ON dep2.id = td2.depends_on
11814
- WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
11815
- )`).all(id, id);
11816
- if (unblockedDeps.length > 0) {
11817
- meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
11818
- for (const dep of unblockedDeps) {
11819
- const depTask = getTask(dep.id, d);
11820
- const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
11821
- dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
11822
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
11823
- if (depTask)
11824
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
12005
+ if (filter.session_id) {
12006
+ conditions.push("session_id = ?");
12007
+ params.push(filter.session_id);
12008
+ }
12009
+ if (filter.tags && filter.tags.length > 0) {
12010
+ const placeholders = filter.tags.map(() => "?").join(",");
12011
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
12012
+ params.push(...filter.tags);
12013
+ }
12014
+ if (filter.plan_id) {
12015
+ conditions.push("plan_id = ?");
12016
+ params.push(filter.plan_id);
12017
+ }
12018
+ if (filter.task_list_id) {
12019
+ conditions.push("task_list_id = ?");
12020
+ params.push(filter.task_list_id);
12021
+ }
12022
+ if (filter.has_recurrence === true) {
12023
+ conditions.push("recurrence_rule IS NOT NULL");
12024
+ } else if (filter.has_recurrence === false) {
12025
+ conditions.push("recurrence_rule IS NULL");
12026
+ }
12027
+ if (filter.task_type) {
12028
+ if (Array.isArray(filter.task_type)) {
12029
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
12030
+ params.push(...filter.task_type);
12031
+ } else {
12032
+ conditions.push("task_type = ?");
12033
+ params.push(filter.task_type);
11825
12034
  }
11826
12035
  }
11827
- return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
12036
+ addMetadataConditions(filter.metadata, conditions, params);
12037
+ if (!filter.include_archived) {
12038
+ conditions.push("archived_at IS NULL");
12039
+ }
12040
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
12041
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
12042
+ return row.count;
11828
12043
  }
11829
- function lockTask(id, agentId, db) {
12044
+ function updateTask(id, input, db) {
11830
12045
  const d = db || getDatabase();
11831
12046
  const task = getTask(id, d);
11832
12047
  if (!task)
11833
12048
  throw new TaskNotFoundError(id);
11834
- if (task.status === "completed" || task.status === "cancelled") {
11835
- return {
11836
- success: false,
11837
- error: `Task is ${task.status} and cannot be locked`
11838
- };
11839
- }
11840
- if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
11841
- const timestamp2 = now();
11842
- d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
11843
- logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
11844
- return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
12049
+ if (task.version !== input.version) {
12050
+ throw new VersionConflictError(id, input.version, task.version);
11845
12051
  }
11846
- const cutoff = lockExpiryCutoff();
11847
12052
  const timestamp = now();
11848
- const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
11849
- WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
11850
- if (result.changes === 0) {
11851
- const current = getTask(id, d);
11852
- if (!current)
11853
- throw new TaskNotFoundError(id);
11854
- if (current.status === "completed" || current.status === "cancelled") {
11855
- return {
11856
- success: false,
11857
- error: `Task is ${current.status} and cannot be locked`
11858
- };
12053
+ const completionTimestamp = input.completed_at ?? timestamp;
12054
+ const sets = ["version = version + 1", "updated_at = ?"];
12055
+ const params = [timestamp];
12056
+ if (input.title !== undefined) {
12057
+ sets.push("title = ?");
12058
+ params.push(input.title);
12059
+ }
12060
+ if (input.description !== undefined) {
12061
+ sets.push("description = ?");
12062
+ params.push(input.description);
12063
+ }
12064
+ if (input.status !== undefined) {
12065
+ if (input.status === "completed") {
12066
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
11859
12067
  }
11860
- if (current.locked_by && !isLockExpired(current.locked_at)) {
11861
- return {
11862
- success: false,
11863
- locked_by: current.locked_by,
11864
- locked_at: current.locked_at,
11865
- error: `Task is locked by ${current.locked_by}`
11866
- };
12068
+ sets.push("status = ?");
12069
+ params.push(input.status);
12070
+ if (input.status === "completed") {
12071
+ sets.push("completed_at = ?");
12072
+ params.push(completionTimestamp);
12073
+ sets.push("locked_by = NULL");
12074
+ sets.push("locked_at = NULL");
12075
+ } else if (task.status === "completed" && input.completed_at === undefined) {
12076
+ sets.push("completed_at = NULL");
11867
12077
  }
11868
- return {
11869
- success: false,
11870
- error: `Task ${id} could not be locked because it changed during lock acquisition`
11871
- };
11872
12078
  }
11873
- logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
11874
- return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
11875
- }
11876
- function unlockTask(id, agentId, db) {
11877
- const d = db || getDatabase();
11878
- const task = getTask(id, d);
11879
- if (!task)
11880
- throw new TaskNotFoundError(id);
11881
- if (agentId && task.locked_by && task.locked_by !== agentId) {
11882
- throw new LockError(id, task.locked_by);
12079
+ if (input.priority !== undefined) {
12080
+ sets.push("priority = ?");
12081
+ params.push(input.priority);
11883
12082
  }
11884
- const timestamp = now();
11885
- d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
11886
- WHERE id = ?`, [timestamp, id]);
11887
- return true;
11888
- }
11889
- function getTaskLockStatus(id, db) {
11890
- const d = db || getDatabase();
11891
- const task = getTask(id, d);
11892
- if (!task)
11893
- throw new TaskNotFoundError(id);
11894
- const expired = isLockExpired(task.locked_at);
11895
- return {
11896
- task_id: id,
11897
- locked: !!task.locked_by && !expired,
11898
- locked_by: task.locked_by,
11899
- locked_at: task.locked_at,
11900
- expires_at: lockExpiresAt(task.locked_at),
11901
- expired
11902
- };
11903
- }
11904
- function claimNextTask(agentId, filters, db) {
11905
- const d = db || getDatabase();
11906
- const tx = d.transaction(() => {
11907
- const task = getNextTask(agentId, filters, d);
11908
- if (!task)
11909
- return null;
11910
- return startTask(task.id, agentId, d);
11911
- });
11912
- return tx();
11913
- }
11914
- function getNextTask(agentId, filters, db) {
11915
- const d = db || getDatabase();
11916
- clearExpiredLocks(d);
11917
- const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
11918
- const params = [lockExpiryCutoff()];
11919
- if (filters?.project_id) {
11920
- conditions.push("project_id = ?");
11921
- params.push(filters.project_id);
12083
+ if (input.project_id !== undefined) {
12084
+ sets.push("project_id = ?");
12085
+ params.push(input.project_id);
11922
12086
  }
11923
- if (filters?.task_list_id) {
11924
- conditions.push("task_list_id = ?");
11925
- params.push(filters.task_list_id);
12087
+ if (input.assigned_to !== undefined) {
12088
+ sets.push("assigned_to = ?");
12089
+ params.push(input.assigned_to);
11926
12090
  }
11927
- if (filters?.plan_id) {
11928
- conditions.push("plan_id = ?");
11929
- params.push(filters.plan_id);
12091
+ if (input.working_dir !== undefined) {
12092
+ sets.push("working_dir = ?");
12093
+ params.push(input.working_dir);
11930
12094
  }
11931
- if (filters?.tags && filters.tags.length > 0) {
11932
- const placeholders = filters.tags.map(() => "?").join(",");
11933
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
11934
- params.push(...filters.tags);
12095
+ if (input.tags !== undefined) {
12096
+ sets.push("tags = ?");
12097
+ params.push(JSON.stringify(input.tags));
11935
12098
  }
11936
- 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')");
11937
- const where = conditions.join(" AND ");
11938
- let recentProjectIds = [];
11939
- if (agentId) {
11940
- const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
11941
- recentProjectIds = recentRows.map((r) => r.project_id);
12099
+ if (input.metadata !== undefined) {
12100
+ sets.push("metadata = ?");
12101
+ params.push(JSON.stringify(input.metadata));
11942
12102
  }
11943
- let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
11944
- if (agentId) {
11945
- sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
11946
- params.push(agentId);
12103
+ if (input.plan_id !== undefined) {
12104
+ sets.push("plan_id = ?");
12105
+ params.push(input.plan_id);
11947
12106
  }
11948
- if (recentProjectIds.length > 0) {
11949
- const placeholders = recentProjectIds.map(() => "?").join(",");
11950
- sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
11951
- params.push(...recentProjectIds);
12107
+ if (input.task_list_id !== undefined) {
12108
+ sets.push("task_list_id = ?");
12109
+ params.push(input.task_list_id);
11952
12110
  }
11953
- 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`;
11954
- const row = d.query(sql).get(...params);
11955
- return row ? rowToTask(row) : null;
11956
- }
11957
- function getActiveWork(filters, db) {
11958
- const d = db || getDatabase();
11959
- clearExpiredLocks(d);
11960
- const conditions = ["status = 'in_progress'"];
11961
- const params = [];
11962
- if (filters?.project_id) {
11963
- conditions.push("project_id = ?");
11964
- params.push(filters.project_id);
12111
+ if (input.due_at !== undefined) {
12112
+ sets.push("due_at = ?");
12113
+ params.push(input.due_at);
11965
12114
  }
11966
- if (filters?.task_list_id) {
11967
- conditions.push("task_list_id = ?");
11968
- params.push(filters.task_list_id);
12115
+ if (input.estimated_minutes !== undefined) {
12116
+ sets.push("estimated_minutes = ?");
12117
+ params.push(input.estimated_minutes);
11969
12118
  }
11970
- const where = conditions.join(" AND ");
11971
- const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
11972
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
11973
- updated_at DESC`).all(...params);
11974
- return rows;
11975
- }
11976
- function getTasksChangedSince(since, filters, db) {
11977
- const d = db || getDatabase();
11978
- const conditions = ["updated_at > ?"];
11979
- const params = [since];
11980
- if (filters?.project_id) {
11981
- conditions.push("project_id = ?");
11982
- params.push(filters.project_id);
12119
+ if (input.sla_minutes !== undefined) {
12120
+ sets.push("sla_minutes = ?");
12121
+ params.push(input.sla_minutes);
11983
12122
  }
11984
- if (filters?.task_list_id) {
11985
- conditions.push("task_list_id = ?");
11986
- params.push(filters.task_list_id);
12123
+ if (input.actual_minutes !== undefined) {
12124
+ sets.push("actual_minutes = ?");
12125
+ params.push(input.actual_minutes);
11987
12126
  }
11988
- const where = conditions.join(" AND ");
11989
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
11990
- return rows.map(rowToTask);
11991
- }
11992
- function failTask(id, agentId, reason, options, db) {
11993
- const d = db || getDatabase();
11994
- const databasePath = databasePathFromDatabase(d);
11995
- const task = getTask(id, d);
11996
- if (!task)
11997
- throw new TaskNotFoundError(id);
11998
- const meta = {
11999
- ...task.metadata,
12000
- _failure: {
12001
- reason: reason || "Unknown failure",
12002
- error_code: options?.error_code || null,
12003
- failed_by: agentId || null,
12004
- failed_at: now(),
12005
- retry_requested: options?.retry || false
12127
+ if (input.completed_at !== undefined && input.status !== "completed") {
12128
+ sets.push("completed_at = ?");
12129
+ params.push(input.completed_at);
12130
+ }
12131
+ if (input.confidence !== undefined) {
12132
+ sets.push("confidence = ?");
12133
+ params.push(input.confidence);
12134
+ }
12135
+ if (input.retry_count !== undefined) {
12136
+ sets.push("retry_count = ?");
12137
+ params.push(input.retry_count);
12138
+ }
12139
+ if (input.max_retries !== undefined) {
12140
+ sets.push("max_retries = ?");
12141
+ params.push(input.max_retries);
12142
+ }
12143
+ if (input.retry_after !== undefined) {
12144
+ sets.push("retry_after = ?");
12145
+ params.push(input.retry_after);
12146
+ }
12147
+ if (input.requires_approval !== undefined) {
12148
+ sets.push("requires_approval = ?");
12149
+ params.push(input.requires_approval ? 1 : 0);
12150
+ }
12151
+ if (input.approved_by !== undefined) {
12152
+ sets.push("approved_by = ?");
12153
+ params.push(input.approved_by);
12154
+ sets.push("approved_at = ?");
12155
+ params.push(now());
12156
+ }
12157
+ if (input.recurrence_rule !== undefined) {
12158
+ sets.push("recurrence_rule = ?");
12159
+ params.push(input.recurrence_rule);
12160
+ }
12161
+ if (input.task_type !== undefined) {
12162
+ sets.push("task_type = ?");
12163
+ params.push(input.task_type ?? null);
12164
+ }
12165
+ params.push(id, input.version);
12166
+ const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
12167
+ if (result.changes === 0) {
12168
+ const current = getTask(id, d);
12169
+ throw new VersionConflictError(id, input.version, current?.version ?? -1);
12170
+ }
12171
+ if (input.tags !== undefined) {
12172
+ replaceTaskTags(id, input.tags, d);
12173
+ }
12174
+ const transitionedToCompleted = input.status === "completed" && task.status !== "completed";
12175
+ if (transitionedToCompleted && task.recurrence_rule) {
12176
+ try {
12177
+ const { spawnNextRecurrence: spawnNextRecurrence2 } = (init_task_lifecycle(), __toCommonJS(exports_task_lifecycle));
12178
+ spawnNextRecurrence2(task, d, completionTimestamp);
12179
+ } catch (e) {
12180
+ console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
12006
12181
  }
12007
- };
12008
- const timestamp = now();
12009
- d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
12010
- WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
12011
- const failedTask = {
12182
+ }
12183
+ const agentId = task.assigned_to || task.agent_id || null;
12184
+ if (input.status !== undefined && input.status !== task.status)
12185
+ logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
12186
+ if (input.priority !== undefined && input.priority !== task.priority)
12187
+ logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
12188
+ if (input.title !== undefined && input.title !== task.title)
12189
+ logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
12190
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
12191
+ logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
12192
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
12193
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
12194
+ if (input.approved_by !== undefined)
12195
+ logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
12196
+ const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
12197
+ const completedNow = input.status === "completed";
12198
+ const updatedTask = {
12012
12199
  ...task,
12013
- status: "failed",
12014
- locked_by: null,
12015
- locked_at: null,
12016
- metadata: meta,
12200
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
12201
+ tags: input.tags ?? task.tags,
12202
+ metadata: input.metadata ?? task.metadata,
12017
12203
  version: task.version + 1,
12018
- updated_at: timestamp
12204
+ updated_at: timestamp,
12205
+ locked_by: completedNow ? null : task.locked_by,
12206
+ locked_at: completedNow ? null : task.locked_at,
12207
+ completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
12208
+ sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
12209
+ actual_minutes: input.actual_minutes ?? task.actual_minutes,
12210
+ confidence: input.confidence !== undefined ? input.confidence : task.confidence,
12211
+ retry_count: input.retry_count ?? task.retry_count,
12212
+ max_retries: input.max_retries ?? task.max_retries,
12213
+ retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
12214
+ requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
12215
+ approved_by: input.approved_by ?? task.approved_by,
12216
+ approved_at: input.approved_by ? timestamp : task.approved_at
12019
12217
  };
12020
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
12021
- const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
12022
- dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
12023
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
12024
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
12025
- let retryTask;
12026
- if (options?.retry) {
12027
- const retryCount = (task.retry_count || 0) + 1;
12028
- const maxRetries = task.max_retries || 3;
12029
- if (retryCount > maxRetries) {
12030
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
12031
- JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
12032
- id
12033
- ]);
12034
- } else {
12035
- const backoffMinutes = Math.pow(5, retryCount - 1);
12036
- const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
12037
- let title = task.title;
12038
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
12039
- title = title.slice(task.short_id.length + 2);
12040
- }
12041
- retryTask = createTask({
12042
- title,
12043
- description: task.description ?? undefined,
12044
- priority: task.priority,
12045
- project_id: task.project_id ?? undefined,
12046
- task_list_id: task.task_list_id ?? undefined,
12047
- plan_id: task.plan_id ?? undefined,
12048
- assigned_to: task.assigned_to ?? undefined,
12049
- tags: task.tags,
12050
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
12051
- estimated_minutes: task.estimated_minutes ?? undefined,
12052
- recurrence_rule: task.recurrence_rule ?? undefined,
12053
- due_at: retryAfter
12054
- }, d);
12055
- d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
12056
- }
12218
+ const databasePath = databasePathFromDatabase(d);
12219
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
12220
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
12221
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
12222
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
12223
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
12057
12224
  }
12058
- return { task: failedTask, retryTask };
12059
- }
12060
- function getStaleTasks(staleQuery = 30, filters, db) {
12061
- const d = db || getDatabase();
12062
- const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
12063
- const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
12064
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
12065
- const conditions = [
12066
- "status = 'in_progress'",
12067
- "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
12068
- ];
12069
- const params = [cutoff, cutoff];
12070
- if (effectiveFilters?.project_id) {
12071
- conditions.push("project_id = ?");
12072
- params.push(effectiveFilters.project_id);
12225
+ if (input.status !== undefined && input.status !== task.status) {
12226
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
12227
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
12228
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
12229
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
12073
12230
  }
12074
- if (effectiveFilters?.task_list_id) {
12075
- conditions.push("task_list_id = ?");
12076
- params.push(effectiveFilters.task_list_id);
12231
+ if (input.approved_by !== undefined) {
12232
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
12077
12233
  }
12078
- const where = conditions.join(" AND ");
12079
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
12080
- return rows.map(rowToTask);
12081
- }
12082
- function stealTask(agentId, opts, db) {
12083
- const d = db || getDatabase();
12084
- const databasePath = databasePathFromDatabase(d);
12085
- const staleMinutes = opts?.stale_minutes ?? 30;
12086
- const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
12087
- if (staleTasks.length === 0)
12088
- return null;
12089
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
12090
- staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
12091
- const target = staleTasks[0];
12092
- const timestamp = now();
12093
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
12094
- const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
12095
- WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
12096
- if (result.changes === 0)
12097
- return null;
12098
- logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
12099
- logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
12100
- const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
12101
- const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
12102
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
12103
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
12104
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
12105
- return stolenTask;
12234
+ const updatePayload = taskEventData(updatedTask);
12235
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
12236
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
12237
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
12238
+ return updatedTask;
12106
12239
  }
12107
- function claimOrSteal(agentId, filters, db) {
12240
+ function deleteTask(id, db) {
12108
12241
  const d = db || getDatabase();
12109
- const tx = d.transaction(() => {
12110
- const next = getNextTask(agentId, filters, d);
12111
- if (next) {
12112
- const started = startTask(next.id, agentId, d);
12113
- return { task: started, stolen: false };
12114
- }
12115
- const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
12116
- if (stolen)
12117
- return { task: stolen, stolen: true };
12118
- return null;
12119
- });
12120
- return tx();
12121
- }
12122
- function spawnNextRecurrence(completedTask, db, completedAt) {
12123
- const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
12124
- const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
12125
- let title = completedTask.title;
12126
- if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
12127
- title = title.slice(completedTask.short_id.length + 2);
12128
- }
12129
- const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
12130
- return createTask({
12131
- title,
12132
- description: completedTask.description ?? undefined,
12133
- priority: completedTask.priority,
12134
- project_id: completedTask.project_id ?? undefined,
12135
- task_list_id: completedTask.task_list_id ?? undefined,
12136
- plan_id: completedTask.plan_id ?? undefined,
12137
- assigned_to: completedTask.assigned_to ?? undefined,
12138
- tags: completedTask.tags,
12139
- metadata: completedTask.metadata,
12140
- estimated_minutes: completedTask.estimated_minutes ?? undefined,
12141
- sla_minutes: completedTask.sla_minutes ?? undefined,
12142
- recurrence_rule: completedTask.recurrence_rule,
12143
- recurrence_parent_id: recurrenceParentId,
12144
- due_at: dueAt
12145
- }, db);
12242
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
12243
+ if (!row)
12244
+ return false;
12245
+ recordStorageTombstone({
12246
+ object_type: "tasks",
12247
+ object_id: id,
12248
+ payload: rowToTask(row),
12249
+ version: row.version
12250
+ }, d);
12251
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
12252
+ return result.changes > 0;
12146
12253
  }
12147
- var MAX_SPAWN_DEPTH = 10;
12148
- var init_task_lifecycle = __esm(() => {
12254
+ var init_task_crud = __esm(() => {
12149
12255
  init_types();
12150
12256
  init_database();
12151
12257
  init_completion_guard();
@@ -12153,11 +12259,9 @@ var init_task_lifecycle = __esm(() => {
12153
12259
  init_event_hooks();
12154
12260
  init_shared_events();
12155
12261
  init_audit();
12156
- init_recurrence();
12157
12262
  init_webhooks();
12158
- init_templates();
12159
- init_task_crud();
12160
- init_task_graph();
12263
+ init_checklists();
12264
+ init_storage_tombstones();
12161
12265
  });
12162
12266
 
12163
12267
  // src/db/task-status.ts
@@ -12247,6 +12351,15 @@ function setTaskStatus(id, status, _agentId, db) {
12247
12351
  throw new TaskNotFoundError(id);
12248
12352
  if (task.status === status)
12249
12353
  return task;
12354
+ if (status === "completed") {
12355
+ try {
12356
+ return completeTask(id, _agentId, d);
12357
+ } catch (e) {
12358
+ if (e instanceof VersionConflictError && attempt < 2)
12359
+ continue;
12360
+ throw e;
12361
+ }
12362
+ }
12250
12363
  try {
12251
12364
  return updateTask(id, { status, version: task.version }, d);
12252
12365
  } catch (e) {
@@ -15632,10 +15745,8 @@ var init_token_utils = __esm(() => {
15632
15745
  "cancel_task",
15633
15746
  "check_task_done_contract",
15634
15747
  "claim_task",
15635
- "clone_task",
15636
15748
  "delete_task",
15637
15749
  "extend_task",
15638
- "get_active_work",
15639
15750
  "get_archived_tasks",
15640
15751
  "get_blocked_tasks",
15641
15752
  "get_blocking_tasks",
@@ -15671,7 +15782,8 @@ var init_token_utils = __esm(() => {
15671
15782
  "task_context",
15672
15783
  "unlock_task",
15673
15784
  "unarchive_task",
15674
- "update_task"
15785
+ "update_task",
15786
+ "upsert_task"
15675
15787
  ],
15676
15788
  projects: [
15677
15789
  "bootstrap_project",
@@ -15930,12 +16042,8 @@ var init_token_utils = __esm(() => {
15930
16042
  "delete_tag",
15931
16043
  "get_label",
15932
16044
  "get_activity_timeline",
15933
- "get_recent_activity",
15934
16045
  "get_tag",
15935
16046
  "get_task_fields",
15936
- "get_task_graph",
15937
- "get_task_history",
15938
- "get_task_stats",
15939
16047
  "list_workflow_states",
15940
16048
  "list_labels",
15941
16049
  "list_tags",
@@ -15946,6 +16054,10 @@ var init_token_utils = __esm(() => {
15946
16054
  "describe_tools",
15947
16055
  "set_task_workflow_state",
15948
16056
  "set_task_fields",
16057
+ "assign_label_to_task",
16058
+ "create_custom_field",
16059
+ "set_task_custom_field",
16060
+ "set_task_priority_meta",
15949
16061
  "update_label",
15950
16062
  "update_tag"
15951
16063
  ],
@@ -15971,7 +16083,6 @@ var init_token_utils = __esm(() => {
15971
16083
  "update_template",
15972
16084
  "write_template_library"
15973
16085
  ],
15974
- webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
15975
16086
  machines: [
15976
16087
  "machines_archive",
15977
16088
  "machines_delete",
@@ -38545,6 +38656,11 @@ function safeEqualHex(a, b) {
38545
38656
  return false;
38546
38657
  return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
38547
38658
  }
38659
+ function safeEqualStrings(a, b) {
38660
+ const ah = createHash12("sha256").update(a, "utf8").digest();
38661
+ const bh = createHash12("sha256").update(b, "utf8").digest();
38662
+ return timingSafeEqual3(ah, bh);
38663
+ }
38548
38664
  function hasActiveApiKeys(db) {
38549
38665
  const d = db || getDatabase();
38550
38666
  const row = d.query("SELECT COUNT(*) AS count FROM api_keys WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)").get(now());
@@ -38717,6 +38833,37 @@ function parseBoundedLimit(value, fallback, max) {
38717
38833
  return fallback;
38718
38834
  return Math.min(parsed, max);
38719
38835
  }
38836
+ function mapTaskError(e, json2) {
38837
+ if (e instanceof VersionConflictError) {
38838
+ return json2({
38839
+ error: e.message,
38840
+ code: VersionConflictError.code,
38841
+ expected_version: e.expectedVersion,
38842
+ current_version: e.actualVersion
38843
+ }, 409);
38844
+ }
38845
+ if (e instanceof TaskNotFoundError) {
38846
+ return json2({ error: e.message, code: TaskNotFoundError.code }, 404);
38847
+ }
38848
+ if (e instanceof LockError) {
38849
+ return json2({ error: e.message, code: LockError.code }, 409);
38850
+ }
38851
+ if (e instanceof CompletionGuardError) {
38852
+ return json2({
38853
+ error: e.message,
38854
+ code: CompletionGuardError.code,
38855
+ retry_after: e.retryAfterSeconds ?? null
38856
+ }, 409);
38857
+ }
38858
+ if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
38859
+ return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
38860
+ }
38861
+ return null;
38862
+ }
38863
+ function countRecurringTasks() {
38864
+ const row = getDatabase().query("SELECT COUNT(*) as count FROM tasks WHERE recurrence_rule IS NOT NULL AND recurrence_rule != '' AND archived_at IS NULL").get();
38865
+ return row?.count ?? 0;
38866
+ }
38720
38867
  function handleSseEvents(_req, url, ctx) {
38721
38868
  const agentId = url.searchParams.get("agent_id") || undefined;
38722
38869
  const projectId = url.searchParams.get("project_id") || undefined;
@@ -38795,35 +38942,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
38795
38942
  });
38796
38943
  }
38797
38944
  function handleHealth(_ctx, json2) {
38798
- const all = listTasks({ limit: 1e4 });
38799
- const stale = all.filter((t) => t.status === "in_progress" && new Date(t.updated_at).getTime() < Date.now() - 30 * 60 * 1000);
38800
- const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < new Date().toISOString());
38801
- return json2({ status: stale.length === 0 && overdue.length === 0 ? "ok" : "warn", tasks: all.length, stale: stale.length, overdue_recurring: overdue.length, timestamp: new Date().toISOString() });
38945
+ const stats2 = getTaskStats();
38946
+ const staleCount = getStaleTasks(30).length;
38947
+ const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
38948
+ return json2({
38949
+ status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
38950
+ tasks: stats2.total,
38951
+ stale: staleCount,
38952
+ overdue_recurring: overdueRecurring,
38953
+ timestamp: new Date().toISOString()
38954
+ });
38802
38955
  }
38803
38956
  function handleHeadlessBoundary(_ctx, json2) {
38804
38957
  const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
38805
38958
  return json2(getHeadlessBoundaryManifest2());
38806
38959
  }
38807
38960
  function handleStats(_ctx, json2) {
38808
- const all = listTasks({ limit: 1e4 });
38961
+ const stats2 = getTaskStats();
38962
+ const byStatus = stats2.by_status;
38809
38963
  const projects = listProjects();
38810
38964
  const agents = listAgents();
38811
- const staleItems = getStaleTasks(30);
38812
- const nowStr = new Date().toISOString();
38813
- const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
38814
- const recurringTasks = all.filter((t) => t.recurrence_rule).length;
38965
+ const staleCount = getStaleTasks(30).length;
38966
+ const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
38815
38967
  return json2({
38816
- total_tasks: all.length,
38817
- pending: all.filter((t) => t.status === "pending").length,
38818
- in_progress: all.filter((t) => t.status === "in_progress").length,
38819
- completed: all.filter((t) => t.status === "completed").length,
38820
- failed: all.filter((t) => t.status === "failed").length,
38821
- cancelled: all.filter((t) => t.status === "cancelled").length,
38968
+ total_tasks: stats2.total,
38969
+ pending: byStatus["pending"] ?? 0,
38970
+ in_progress: byStatus["in_progress"] ?? 0,
38971
+ completed: byStatus["completed"] ?? 0,
38972
+ failed: byStatus["failed"] ?? 0,
38973
+ cancelled: byStatus["cancelled"] ?? 0,
38822
38974
  projects: projects.length,
38823
38975
  agents: agents.length,
38824
- stale_count: staleItems.length,
38976
+ stale_count: staleCount,
38825
38977
  overdue_recurring: overdueRecurring,
38826
- recurring_tasks: recurringTasks
38978
+ recurring_tasks: countRecurringTasks()
38827
38979
  });
38828
38980
  }
38829
38981
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
@@ -38902,27 +39054,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
38902
39054
  const summaries = tasks.map((t) => taskToSummary2(t));
38903
39055
  if (format === "csv") {
38904
39056
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
38905
- const rows = summaries.map((t) => headers.map((h) => {
38906
- const val = t[h];
39057
+ const csvCell = (val) => {
38907
39058
  if (val === null || val === undefined)
38908
39059
  return "";
38909
- const str = String(val);
38910
- return str.includes(",") || str.includes('"') || str.includes(`
38911
- `) ? `"${str.replace(/"/g, '""')}"` : str;
38912
- }).join(","));
39060
+ let str = String(val);
39061
+ if (/^[=+\-@\t\r]/.test(str))
39062
+ str = `'${str}`;
39063
+ if (str.includes(",") || str.includes('"') || str.includes(`
39064
+ `) || str.includes("\r")) {
39065
+ str = `"${str.replace(/"/g, '""')}"`;
39066
+ }
39067
+ return str;
39068
+ };
39069
+ const rows = summaries.map((t) => headers.map((h) => csvCell(t[h])).join(","));
38913
39070
  const csv = [headers.join(","), ...rows].join(`
38914
39071
  `);
38915
39072
  return new Response(csv, {
38916
39073
  headers: {
38917
39074
  "Content-Type": "text/csv",
38918
- "Content-Disposition": "attachment; filename=tasks.csv"
39075
+ "Content-Disposition": "attachment; filename=tasks.csv",
39076
+ ...SECURITY_HEADERS
38919
39077
  }
38920
39078
  });
38921
39079
  }
38922
39080
  return new Response(JSON.stringify(summaries, null, 2), {
38923
39081
  headers: {
38924
39082
  "Content-Type": "application/json",
38925
- "Content-Disposition": "attachment; filename=tasks.json"
39083
+ "Content-Disposition": "attachment; filename=tasks.json",
39084
+ ...SECURITY_HEADERS
38926
39085
  }
38927
39086
  });
38928
39087
  }
@@ -39096,12 +39255,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
39096
39255
  if (ALLOWED.has(key))
39097
39256
  safeBody[key] = value;
39098
39257
  }
39258
+ const clientVersion = typeof body["version"] === "number" ? body["version"] : task2.version;
39099
39259
  const updated = updateTask(id, {
39100
39260
  ...safeBody,
39101
- version: task2.version
39261
+ version: clientVersion
39102
39262
  });
39103
39263
  return json2(taskToSummary2(updated));
39104
39264
  } catch (e) {
39265
+ const mapped = mapTaskError(e, json2);
39266
+ if (mapped)
39267
+ return mapped;
39105
39268
  return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
39106
39269
  }
39107
39270
  }
@@ -39117,6 +39280,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
39117
39280
  ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "started", agent_id: "dashboard", project_id: task2.project_id });
39118
39281
  return json2(taskToSummary2(task2));
39119
39282
  } catch (e) {
39283
+ const mapped = mapTaskError(e, json2);
39284
+ if (mapped)
39285
+ return mapped;
39120
39286
  return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
39121
39287
  }
39122
39288
  }
@@ -39136,6 +39302,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
39136
39302
  ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "completed", agent_id: "dashboard", project_id: task2.project_id });
39137
39303
  return json2(taskToSummary2(task2));
39138
39304
  } catch (e) {
39305
+ const mapped = mapTaskError(e, json2);
39306
+ if (mapped)
39307
+ return mapped;
39139
39308
  return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
39140
39309
  }
39141
39310
  }
@@ -39460,6 +39629,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
39460
39629
  }
39461
39630
  var init_routes = __esm(() => {
39462
39631
  init_tasks();
39632
+ init_database();
39633
+ init_types();
39463
39634
  init_projects();
39464
39635
  init_agents();
39465
39636
  init_plans();
@@ -39517,7 +39688,7 @@ function checkAuth(req, apiKey) {
39517
39688
  if (!apiKey && !generatedKeysEnabled)
39518
39689
  return null;
39519
39690
  const provided = getProvidedApiKey(req);
39520
- const matchesEnvKey = Boolean(apiKey && provided && provided === apiKey);
39691
+ const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
39521
39692
  const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
39522
39693
  if (!matchesEnvKey && !matchesGeneratedKey) {
39523
39694
  return new Response(JSON.stringify({ error: "Unauthorized" }), {
@@ -39527,6 +39698,15 @@ function checkAuth(req, apiKey) {
39527
39698
  }
39528
39699
  return null;
39529
39700
  }
39701
+ function resolveClientIp(req, server) {
39702
+ const trustProxy = process.env["TODOS_TRUST_PROXY"] === "1" || process.env["TODOS_TRUST_PROXY"] === "true";
39703
+ if (trustProxy) {
39704
+ const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim();
39705
+ if (forwarded)
39706
+ return forwarded;
39707
+ }
39708
+ return server.requestIP(req)?.address || "unknown";
39709
+ }
39530
39710
  function checkRateLimit(ip) {
39531
39711
  const now4 = Date.now();
39532
39712
  const entry = rateLimitMap.get(ip);
@@ -39654,7 +39834,7 @@ Dashboard not found at: ${dashboardDir}`);
39654
39834
  const server = Bun.serve({
39655
39835
  port,
39656
39836
  hostname: hostname3,
39657
- async fetch(req) {
39837
+ async fetch(req, server2) {
39658
39838
  const url = new URL(req.url);
39659
39839
  const path = url.pathname;
39660
39840
  const method = req.method;
@@ -39666,15 +39846,6 @@ Dashboard not found at: ${dashboardDir}`);
39666
39846
  Vary: "Origin"
39667
39847
  } : undefined;
39668
39848
  const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
39669
- if (path === "/health" && method === "GET") {
39670
- const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
39671
- return healthResponse2("todos");
39672
- }
39673
- if (path === "/mcp") {
39674
- const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
39675
- const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
39676
- return handleMcpHttpRequest2(req, buildServer2);
39677
- }
39678
39849
  if (method === "OPTIONS") {
39679
39850
  return new Response(null, {
39680
39851
  headers: corsHeaders || {
@@ -39682,7 +39853,7 @@ Dashboard not found at: ${dashboardDir}`);
39682
39853
  }
39683
39854
  });
39684
39855
  }
39685
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
39856
+ const ip = resolveClientIp(req, server2);
39686
39857
  const rl = checkRateLimit(ip);
39687
39858
  if (!rl.allowed) {
39688
39859
  return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
@@ -39690,6 +39861,18 @@ Dashboard not found at: ${dashboardDir}`);
39690
39861
  headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
39691
39862
  });
39692
39863
  }
39864
+ if (path === "/health" && method === "GET") {
39865
+ const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
39866
+ return healthResponse2("todos");
39867
+ }
39868
+ if (path === "/mcp") {
39869
+ const authError = checkAuth(req, apiKey);
39870
+ if (authError)
39871
+ return authError;
39872
+ const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
39873
+ const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
39874
+ return handleMcpHttpRequest2(req, buildServer2);
39875
+ }
39693
39876
  if (path.startsWith("/api/")) {
39694
39877
  const authError = checkAuth(req, apiKey);
39695
39878
  if (authError)
@@ -39969,14 +40152,16 @@ function printHelp() {
39969
40152
  Start the @hasna/todos MCP server.
39970
40153
 
39971
40154
  Options:
39972
- --stdio Use stdio transport
39973
- --port <port> Use Streamable HTTP on the given port
40155
+ --stdio Use stdio transport (default)
40156
+ --http Use Streamable HTTP transport
40157
+ --port <port> Use Streamable HTTP on the given port (implies --http)
39974
40158
  -V, --version output the version number
39975
40159
  -h, --help display help for command
39976
40160
 
39977
40161
  Environment:
39978
- TODOS_MCP_STDIO=true Force stdio transport
39979
- TODOS_MCP_PORT=<port> HTTP port when not using stdio
40162
+ MCP_STDIO=1 Force stdio transport
40163
+ MCP_HTTP=1 Use Streamable HTTP transport
40164
+ MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
39980
40165
  TODOS_PROFILE=<profile> Tool profile filter
39981
40166
  TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
39982
40167
  }
@@ -40065,8 +40250,22 @@ function formatError(error) {
40065
40250
  function resolveId(partialId, table = "tasks") {
40066
40251
  const db = getDatabase();
40067
40252
  const id = resolvePartialId(db, table, partialId);
40068
- if (!id)
40069
- throw new Error(`Could not resolve ID: ${partialId}`);
40253
+ if (!id) {
40254
+ switch (table) {
40255
+ case "tasks":
40256
+ throw new TaskNotFoundError(partialId);
40257
+ case "projects":
40258
+ throw new ProjectNotFoundError(partialId);
40259
+ case "plans":
40260
+ throw new PlanNotFoundError(partialId);
40261
+ case "task_lists":
40262
+ throw new TaskListNotFoundError(partialId);
40263
+ case "agents":
40264
+ throw new AgentNotFoundError(partialId);
40265
+ default:
40266
+ throw new TaskNotFoundError(partialId);
40267
+ }
40268
+ }
40070
40269
  return id;
40071
40270
  }
40072
40271
  function formatTask(task2) {
@@ -40155,8 +40354,9 @@ function buildServer() {
40155
40354
  return server;
40156
40355
  }
40157
40356
  async function main() {
40158
- const { isStdioMode: isStdioMode2, resolveHttpPort: resolveHttpPort2 } = await Promise.resolve().then(() => (init_http(), exports_http));
40159
- if (isStdioMode2()) {
40357
+ const { isHttpMode: isHttpMode2, resolveHttpPort: resolveHttpPort2 } = await Promise.resolve().then(() => (init_http(), exports_http));
40358
+ const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
40359
+ if (!isHttpMode2() && !portRequested) {
40160
40360
  const server = buildServer();
40161
40361
  const transport = new StdioServerTransport;
40162
40362
  await server.connect(transport);