@hasna/todos 0.11.71 → 0.11.73

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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/index.js +2162 -1450
  7. package/dist/cli-mcp-parity.d.ts.map +1 -1
  8. package/dist/contracts.js +6526 -6258
  9. package/dist/db/api-keys.d.ts +9 -0
  10. package/dist/db/api-keys.d.ts.map +1 -1
  11. package/dist/db/database.d.ts.map +1 -1
  12. package/dist/db/schema.d.ts.map +1 -1
  13. package/dist/db/task-crud.d.ts.map +1 -1
  14. package/dist/db/task-lifecycle.d.ts +1 -0
  15. package/dist/db/task-lifecycle.d.ts.map +1 -1
  16. package/dist/db/task-status.d.ts.map +1 -1
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +10121 -9282
  20. package/dist/lib/db-backup.d.ts.map +1 -1
  21. package/dist/lib/shared-events.d.ts.map +1 -1
  22. package/dist/lib/task-route-contract.d.ts +2 -1
  23. package/dist/lib/task-route-contract.d.ts.map +1 -1
  24. package/dist/lib/task-route-sources.d.ts +68 -0
  25. package/dist/lib/task-route-sources.d.ts.map +1 -0
  26. package/dist/lib/task-routing.d.ts.map +1 -1
  27. package/dist/mcp/index.d.ts.map +1 -1
  28. package/dist/mcp/index.js +1869 -1650
  29. package/dist/mcp/token-utils.d.ts.map +1 -1
  30. package/dist/mcp.js +6 -8
  31. package/dist/registry.js +6526 -6258
  32. package/dist/release-provenance.json +3 -3
  33. package/dist/sdk/client.d.ts.map +1 -1
  34. package/dist/sdk/index.js +6 -4
  35. package/dist/server/index.js +1871 -1652
  36. package/dist/server/routes.d.ts +2 -1
  37. package/dist/server/routes.d.ts.map +1 -1
  38. package/dist/server/serve.d.ts.map +1 -1
  39. package/dist/storage/postgres-sync.d.ts.map +1 -1
  40. package/dist/storage.js +3324 -3029
  41. package/package.json +1 -1
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
  }
@@ -9706,8 +9721,6 @@ function routeEnabledForTask(task, taskList) {
9706
9721
  const explicit = booleanField(task.metadata.route_enabled);
9707
9722
  if (explicit !== undefined)
9708
9723
  return explicit;
9709
- if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
9710
- return true;
9711
9724
  const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
9712
9725
  if (taskListDefault !== undefined)
9713
9726
  return taskListDefault;
@@ -9726,8 +9739,26 @@ function workflowPointersFromMetadata(metadata) {
9726
9739
  workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
9727
9740
  };
9728
9741
  }
9729
- function classifyProjectKind(path) {
9730
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
9742
+ function metadataStringField(record, keys) {
9743
+ if (!record)
9744
+ return;
9745
+ for (const key of keys) {
9746
+ const value = record[key];
9747
+ if (typeof value === "string" && value.trim())
9748
+ return value.trim();
9749
+ }
9750
+ return;
9751
+ }
9752
+ function projectKindFromMetadata(...records) {
9753
+ for (const record of records) {
9754
+ const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
9755
+ if (value)
9756
+ return value;
9757
+ }
9758
+ return null;
9759
+ }
9760
+ function classifyProjectKind(_path, metadata) {
9761
+ return projectKindFromMetadata(metadata);
9731
9762
  }
9732
9763
  function isWorktreePath(path) {
9733
9764
  return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
@@ -9800,7 +9831,6 @@ function taskEventMetadata(task) {
9800
9831
  metadata.project_canonical_path = projectPath;
9801
9832
  }
9802
9833
  if (projectPath) {
9803
- metadata.project_kind = classifyProjectKind(projectPath);
9804
9834
  metadata.project_is_worktree = isWorktreePath(projectPath);
9805
9835
  metadata.working_dir = task.working_dir ?? projectPath;
9806
9836
  }
@@ -9812,6 +9842,10 @@ function taskEventMetadata(task) {
9812
9842
  metadata.task_list_project_id = taskList.project_id;
9813
9843
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
9814
9844
  }
9845
+ const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
9846
+ if (projectKind) {
9847
+ metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
9848
+ }
9815
9849
  const routeEnabled = routeEnabledForTask(task, taskList);
9816
9850
  if (routeEnabled !== undefined) {
9817
9851
  metadata.route_enabled = routeEnabled;
@@ -10366,1767 +10400,1858 @@ var init_checklists = __esm(() => {
10366
10400
  init_database();
10367
10401
  });
10368
10402
 
10369
- // src/db/task-crud.ts
10370
- 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) {
10371
10523
  return {
10372
10524
  ...row,
10373
10525
  tags: JSON.parse(row.tags || "[]"),
10526
+ variables: JSON.parse(row.variables || "[]"),
10374
10527
  metadata: JSON.parse(row.metadata || "{}"),
10375
- status: row.status,
10376
- priority: row.priority,
10377
- requires_approval: !!row.requires_approval
10528
+ priority: row.priority || "medium",
10529
+ version: row.version ?? 1
10378
10530
  };
10379
10531
  }
10380
- function insertTaskTags(taskId, tags, db) {
10381
- if (tags.length === 0)
10382
- return;
10383
- const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
10384
- for (const tag of tags) {
10385
- if (tag)
10386
- stmt.run(taskId, tag);
10387
- }
10388
- }
10389
- function replaceTaskTags(taskId, tags, db) {
10390
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
10391
- 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
+ };
10392
10542
  }
10393
- function addMetadataConditions(metadata, conditions, params) {
10394
- if (!metadata)
10395
- return;
10396
- for (const [key, value] of Object.entries(metadata)) {
10397
- if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
10398
- throw new Error(`Invalid metadata filter key: ${key}`);
10399
- }
10400
- conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
10401
- params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
10402
- }
10543
+ function resolveTemplateId(id, d) {
10544
+ return resolvePartialId(d, "task_templates", id);
10403
10545
  }
10404
- function createTask(input, db) {
10546
+ function createTemplate(input, db) {
10405
10547
  const d = db || getDatabase();
10406
- const timestamp = now();
10407
- const tags = input.tags || [];
10548
+ const id = uuid();
10408
10549
  const machineId = currentStorageMachineId(d);
10409
- const assignedBy = input.assigned_by || input.agent_id;
10410
- const assignedFromProject = input.assigned_from_project || null;
10411
- let id = uuid();
10412
- for (let attempt = 0;attempt < 3; attempt++) {
10413
- try {
10414
- 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)
10415
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10416
- id,
10417
- null,
10418
- input.project_id || null,
10419
- input.parent_id || null,
10420
- input.plan_id || null,
10421
- input.task_list_id || null,
10422
- input.cycle_id || null,
10423
- input.title,
10424
- input.description || null,
10425
- input.status || "pending",
10426
- input.priority || "medium",
10427
- input.agent_id || null,
10428
- input.assigned_to || null,
10429
- input.session_id || null,
10430
- input.working_dir || null,
10431
- JSON.stringify(tags),
10432
- JSON.stringify(input.metadata || {}),
10433
- timestamp,
10434
- timestamp,
10435
- input.due_at || null,
10436
- input.estimated_minutes || null,
10437
- input.sla_minutes ?? null,
10438
- input.confidence ?? null,
10439
- input.retry_count ?? 0,
10440
- input.max_retries ?? 3,
10441
- input.retry_after ?? null,
10442
- input.requires_approval ? 1 : 0,
10443
- null,
10444
- null,
10445
- input.recurrence_rule || null,
10446
- input.recurrence_parent_id || null,
10447
- input.spawns_template_id || null,
10448
- input.reason || null,
10449
- input.spawned_from_session || null,
10450
- assignedBy || null,
10451
- assignedFromProject || null,
10452
- input.task_type || null,
10453
- machineId
10454
- ]);
10455
- break;
10456
- } catch (e) {
10457
- if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
10458
- id = uuid();
10459
- continue;
10460
- }
10461
- throw e;
10462
- }
10463
- }
10464
- if (tags.length > 0) {
10465
- 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);
10466
10567
  }
10467
- const task = getTask(id, d);
10468
- const payload = taskEventData(task);
10469
- const databasePath = databasePathFromDatabase(d);
10470
- dispatchWebhook2("task.created", payload, d).catch(() => {});
10471
- emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
10472
- emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
10473
- return task;
10568
+ return getTemplate(id, d);
10474
10569
  }
10475
- function getTask(id, db) {
10570
+ function getTemplate(id, db) {
10476
10571
  const d = db || getDatabase();
10477
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
10478
- if (!row)
10572
+ const resolved = resolveTemplateId(id, d);
10573
+ if (!resolved)
10479
10574
  return null;
10480
- return rowToTask(row);
10575
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
10576
+ return row ? rowToTemplate(row) : null;
10481
10577
  }
10482
- function getTaskWithRelations(id, db) {
10578
+ function listTemplates(db) {
10483
10579
  const d = db || getDatabase();
10484
- const task = getTask(id, d);
10485
- if (!task)
10486
- return null;
10487
- const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
10488
- const subtasks = subtaskRows.map(rowToTask);
10489
- const depRows = d.query(`SELECT t.* FROM tasks t
10490
- JOIN task_dependencies td ON td.depends_on = t.id
10491
- WHERE td.task_id = ?`).all(id);
10492
- const dependencies = depRows.map(rowToTask);
10493
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
10494
- JOIN task_dependencies td ON td.task_id = t.id
10495
- WHERE td.depends_on = ?`).all(id);
10496
- const blocked_by = blockedByRows.map(rowToTask);
10497
- const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
10498
- const parent = task.parent_id ? getTask(task.parent_id, d) : null;
10499
- const checklist = getChecklist(id, d);
10500
- return {
10501
- ...task,
10502
- subtasks,
10503
- dependencies,
10504
- blocked_by,
10505
- comments,
10506
- parent,
10507
- checklist
10508
- };
10580
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
10509
10581
  }
10510
- function listTasks(filter = {}, db) {
10582
+ function deleteTemplate(id, db) {
10511
10583
  const d = db || getDatabase();
10512
- const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
10513
- clearExpiredLocks2(d);
10514
- const conditions = [];
10515
- const params = [];
10516
- if (filter.project_id) {
10517
- conditions.push("project_id = ?");
10518
- params.push(filter.project_id);
10519
- }
10520
- if (filter.ids && filter.ids.length > 0) {
10521
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
10522
- params.push(...filter.ids);
10523
- }
10524
- if (filter.parent_id !== undefined) {
10525
- if (filter.parent_id === null) {
10526
- conditions.push("parent_id IS NULL");
10527
- } else {
10528
- conditions.push("parent_id = ?");
10529
- params.push(filter.parent_id);
10530
- }
10531
- }
10532
- if (filter.status) {
10533
- if (Array.isArray(filter.status)) {
10534
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
10535
- params.push(...filter.status);
10536
- } else {
10537
- conditions.push("status = ?");
10538
- params.push(filter.status);
10539
- }
10540
- }
10541
- if (filter.priority) {
10542
- if (Array.isArray(filter.priority)) {
10543
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
10544
- params.push(...filter.priority);
10545
- } else {
10546
- conditions.push("priority = ?");
10547
- params.push(filter.priority);
10548
- }
10549
- }
10550
- if (filter.assigned_to) {
10551
- conditions.push("assigned_to = ?");
10552
- params.push(filter.assigned_to);
10553
- }
10554
- if (filter.agent_id) {
10555
- conditions.push("agent_id = ?");
10556
- 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()]);
10557
10618
  }
10558
- if (filter.session_id) {
10559
- conditions.push("session_id = ?");
10560
- 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);
10561
10624
  }
10562
- if (filter.tags && filter.tags.length > 0) {
10563
- const placeholders = filter.tags.map(() => "?").join(",");
10564
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
10565
- params.push(...filter.tags);
10625
+ if (updates.title_pattern !== undefined) {
10626
+ sets.push("title_pattern = ?");
10627
+ values.push(updates.title_pattern);
10566
10628
  }
10567
- if (filter.plan_id) {
10568
- conditions.push("plan_id = ?");
10569
- params.push(filter.plan_id);
10629
+ if (updates.description !== undefined) {
10630
+ sets.push("description = ?");
10631
+ values.push(updates.description);
10570
10632
  }
10571
- if (filter.task_list_id) {
10572
- conditions.push("task_list_id = ?");
10573
- params.push(filter.task_list_id);
10633
+ if (updates.priority !== undefined) {
10634
+ sets.push("priority = ?");
10635
+ values.push(updates.priority);
10574
10636
  }
10575
- if (filter.has_recurrence === true) {
10576
- conditions.push("recurrence_rule IS NOT NULL");
10577
- } else if (filter.has_recurrence === false) {
10578
- conditions.push("recurrence_rule IS NULL");
10637
+ if (updates.tags !== undefined) {
10638
+ sets.push("tags = ?");
10639
+ values.push(JSON.stringify(updates.tags));
10579
10640
  }
10580
- if (filter.task_type) {
10581
- if (Array.isArray(filter.task_type)) {
10582
- conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
10583
- params.push(...filter.task_type);
10584
- } else {
10585
- conditions.push("task_type = ?");
10586
- params.push(filter.task_type);
10587
- }
10641
+ if (updates.variables !== undefined) {
10642
+ sets.push("variables = ?");
10643
+ values.push(JSON.stringify(updates.variables));
10588
10644
  }
10589
- addMetadataConditions(filter.metadata, conditions, params);
10590
- const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
10591
- if (filter.cursor) {
10592
- try {
10593
- const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
10594
- conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
10595
- params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
10596
- } catch {}
10645
+ if (updates.project_id !== undefined) {
10646
+ sets.push("project_id = ?");
10647
+ values.push(updates.project_id);
10597
10648
  }
10598
- if (!filter.include_archived) {
10599
- conditions.push("archived_at IS NULL");
10649
+ if (updates.plan_id !== undefined) {
10650
+ sets.push("plan_id = ?");
10651
+ values.push(updates.plan_id);
10600
10652
  }
10601
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10602
- let limitClause = "";
10603
- if (filter.limit) {
10604
- limitClause = " LIMIT ?";
10605
- params.push(filter.limit);
10606
- if (!filter.cursor && filter.offset) {
10607
- limitClause += " OFFSET ?";
10608
- params.push(filter.offset);
10609
- }
10653
+ if (updates.metadata !== undefined) {
10654
+ sets.push("metadata = ?");
10655
+ values.push(JSON.stringify(updates.metadata));
10610
10656
  }
10611
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
10612
- return rows.map(rowToTask);
10613
- }
10614
- function getTaskByFingerprint(fingerprint, db) {
10615
- const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
10616
- 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);
10617
10660
  }
10618
- 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));
10619
10666
  return {
10620
- ...current,
10621
- ...next ?? {},
10622
- 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
10623
10675
  };
10624
10676
  }
10625
- function upsertTaskByFingerprint(input, db) {
10626
- const d = db || getDatabase();
10627
- const fingerprint = input.fingerprint.trim();
10628
- if (!fingerprint)
10629
- throw new Error("fingerprint is required");
10630
- const existing = getTaskByFingerprint(fingerprint, d);
10631
- const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
10632
- if (!existing) {
10633
- const task2 = createTask({ ...input, metadata }, d);
10634
- return { task: task2, created: true };
10635
- }
10636
- const task = updateTask(existing.id, {
10637
- version: existing.version,
10638
- title: input.title,
10639
- description: input.description,
10640
- status: input.status,
10641
- priority: input.priority,
10642
- project_id: input.project_id,
10643
- assigned_to: input.assigned_to,
10644
- working_dir: input.working_dir,
10645
- plan_id: input.plan_id,
10646
- task_list_id: input.task_list_id,
10647
- tags: input.tags,
10648
- metadata,
10649
- due_at: input.due_at,
10650
- estimated_minutes: input.estimated_minutes,
10651
- sla_minutes: input.sla_minutes,
10652
- confidence: input.confidence,
10653
- retry_count: input.retry_count,
10654
- max_retries: input.max_retries,
10655
- retry_after: input.retry_after,
10656
- requires_approval: input.requires_approval,
10657
- recurrence_rule: input.recurrence_rule,
10658
- task_type: input.task_type
10659
- }, d);
10660
- return { task, created: false };
10661
- }
10662
- function countTasks(filter = {}, db) {
10677
+ function addTemplateTasks(templateId, tasks, db) {
10663
10678
  const d = db || getDatabase();
10664
- const conditions = [];
10665
- const params = [];
10666
- if (filter.project_id) {
10667
- conditions.push("project_id = ?");
10668
- params.push(filter.project_id);
10669
- }
10670
- if (filter.ids && filter.ids.length > 0) {
10671
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
10672
- params.push(...filter.ids);
10673
- }
10674
- if (filter.parent_id !== undefined) {
10675
- if (filter.parent_id === null) {
10676
- conditions.push("parent_id IS NULL");
10677
- } else {
10678
- conditions.push("parent_id = ?");
10679
- params.push(filter.parent_id);
10680
- }
10681
- }
10682
- if (filter.status) {
10683
- if (Array.isArray(filter.status)) {
10684
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
10685
- params.push(...filter.status);
10686
- } else {
10687
- conditions.push("status = ?");
10688
- params.push(filter.status);
10689
- }
10690
- }
10691
- if (filter.priority) {
10692
- if (Array.isArray(filter.priority)) {
10693
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
10694
- params.push(...filter.priority);
10695
- } else {
10696
- conditions.push("priority = ?");
10697
- params.push(filter.priority);
10698
- }
10699
- }
10700
- if (filter.assigned_to) {
10701
- conditions.push("assigned_to = ?");
10702
- params.push(filter.assigned_to);
10703
- }
10704
- if (filter.agent_id) {
10705
- conditions.push("agent_id = ?");
10706
- params.push(filter.agent_id);
10707
- }
10708
- if (filter.session_id) {
10709
- conditions.push("session_id = ?");
10710
- params.push(filter.session_id);
10711
- }
10712
- if (filter.tags && filter.tags.length > 0) {
10713
- const placeholders = filter.tags.map(() => "?").join(",");
10714
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
10715
- params.push(...filter.tags);
10716
- }
10717
- if (filter.plan_id) {
10718
- conditions.push("plan_id = ?");
10719
- params.push(filter.plan_id);
10720
- }
10721
- if (filter.task_list_id) {
10722
- conditions.push("task_list_id = ?");
10723
- params.push(filter.task_list_id);
10724
- }
10725
- addMetadataConditions(filter.metadata, conditions, params);
10726
- if (!filter.include_archived) {
10727
- conditions.push("archived_at IS NULL");
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));
10728
10706
  }
10729
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10730
- const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
10731
- return row.count;
10707
+ return results;
10732
10708
  }
10733
- function updateTask(id, input, db) {
10709
+ function getTemplateWithTasks(id, db) {
10734
10710
  const d = db || getDatabase();
10735
- const task = getTask(id, d);
10736
- if (!task)
10737
- throw new TaskNotFoundError(id);
10738
- if (task.version !== input.version) {
10739
- throw new VersionConflictError(id, input.version, task.version);
10740
- }
10741
- const timestamp = now();
10742
- const completionTimestamp = input.completed_at ?? timestamp;
10743
- const sets = ["version = version + 1", "updated_at = ?"];
10744
- const params = [timestamp];
10745
- if (input.title !== undefined) {
10746
- sets.push("title = ?");
10747
- params.push(input.title);
10748
- }
10749
- if (input.description !== undefined) {
10750
- sets.push("description = ?");
10751
- params.push(input.description);
10752
- }
10753
- if (input.status !== undefined) {
10754
- if (input.status === "completed") {
10755
- checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
10756
- }
10757
- sets.push("status = ?");
10758
- params.push(input.status);
10759
- if (input.status === "completed") {
10760
- sets.push("completed_at = ?");
10761
- params.push(completionTimestamp);
10762
- }
10763
- }
10764
- if (input.priority !== undefined) {
10765
- sets.push("priority = ?");
10766
- params.push(input.priority);
10767
- }
10768
- if (input.project_id !== undefined) {
10769
- sets.push("project_id = ?");
10770
- params.push(input.project_id);
10771
- }
10772
- if (input.assigned_to !== undefined) {
10773
- sets.push("assigned_to = ?");
10774
- params.push(input.assigned_to);
10775
- }
10776
- if (input.working_dir !== undefined) {
10777
- sets.push("working_dir = ?");
10778
- params.push(input.working_dir);
10779
- }
10780
- if (input.tags !== undefined) {
10781
- sets.push("tags = ?");
10782
- params.push(JSON.stringify(input.tags));
10783
- }
10784
- if (input.metadata !== undefined) {
10785
- sets.push("metadata = ?");
10786
- params.push(JSON.stringify(input.metadata));
10787
- }
10788
- if (input.plan_id !== undefined) {
10789
- sets.push("plan_id = ?");
10790
- params.push(input.plan_id);
10791
- }
10792
- if (input.task_list_id !== undefined) {
10793
- sets.push("task_list_id = ?");
10794
- params.push(input.task_list_id);
10795
- }
10796
- if (input.due_at !== undefined) {
10797
- sets.push("due_at = ?");
10798
- params.push(input.due_at);
10799
- }
10800
- if (input.estimated_minutes !== undefined) {
10801
- sets.push("estimated_minutes = ?");
10802
- params.push(input.estimated_minutes);
10803
- }
10804
- if (input.sla_minutes !== undefined) {
10805
- sets.push("sla_minutes = ?");
10806
- params.push(input.sla_minutes);
10807
- }
10808
- if (input.actual_minutes !== undefined) {
10809
- sets.push("actual_minutes = ?");
10810
- params.push(input.actual_minutes);
10811
- }
10812
- if (input.completed_at !== undefined && input.status !== "completed") {
10813
- sets.push("completed_at = ?");
10814
- params.push(input.completed_at);
10815
- }
10816
- if (input.confidence !== undefined) {
10817
- sets.push("confidence = ?");
10818
- params.push(input.confidence);
10819
- }
10820
- if (input.retry_count !== undefined) {
10821
- sets.push("retry_count = ?");
10822
- params.push(input.retry_count);
10823
- }
10824
- if (input.max_retries !== undefined) {
10825
- sets.push("max_retries = ?");
10826
- params.push(input.max_retries);
10827
- }
10828
- if (input.retry_after !== undefined) {
10829
- sets.push("retry_after = ?");
10830
- params.push(input.retry_after);
10831
- }
10832
- if (input.requires_approval !== undefined) {
10833
- sets.push("requires_approval = ?");
10834
- params.push(input.requires_approval ? 1 : 0);
10835
- }
10836
- if (input.approved_by !== undefined) {
10837
- sets.push("approved_by = ?");
10838
- params.push(input.approved_by);
10839
- sets.push("approved_at = ?");
10840
- params.push(now());
10841
- }
10842
- if (input.recurrence_rule !== undefined) {
10843
- sets.push("recurrence_rule = ?");
10844
- params.push(input.recurrence_rule);
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;
10845
10735
  }
10846
- if (input.task_type !== undefined) {
10847
- sets.push("task_type = ?");
10848
- params.push(input.task_type ?? null);
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;
10849
10741
  }
10850
- params.push(id, input.version);
10851
- const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
10852
- if (result.changes === 0) {
10853
- const current = getTask(id, d);
10854
- throw new VersionConflictError(id, input.version, current?.version ?? -1);
10742
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
10743
+ if (falsyMatch) {
10744
+ const varName = falsyMatch[1];
10745
+ const val = variables[varName];
10746
+ return !val || val === "" || val === "false";
10855
10747
  }
10856
- if (input.tags !== undefined) {
10857
- replaceTaskTags(id, input.tags, d);
10748
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
10749
+ if (truthyMatch) {
10750
+ const varName = truthyMatch[1];
10751
+ const val = variables[varName];
10752
+ return !!val && val !== "" && val !== "false";
10858
10753
  }
10859
- const agentId = task.assigned_to || task.agent_id || null;
10860
- if (input.status !== undefined && input.status !== task.status)
10861
- logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
10862
- if (input.priority !== undefined && input.priority !== task.priority)
10863
- logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
10864
- if (input.title !== undefined && input.title !== task.title)
10865
- logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
10866
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
10867
- logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
10868
- if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
10869
- logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
10870
- if (input.approved_by !== undefined)
10871
- logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
10872
- const updatedTask = {
10873
- ...task,
10874
- ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
10875
- tags: input.tags ?? task.tags,
10876
- metadata: input.metadata ?? task.metadata,
10877
- version: task.version + 1,
10878
- updated_at: timestamp,
10879
- completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
10880
- sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
10881
- actual_minutes: input.actual_minutes ?? task.actual_minutes,
10882
- confidence: input.confidence !== undefined ? input.confidence : task.confidence,
10883
- retry_count: input.retry_count ?? task.retry_count,
10884
- max_retries: input.max_retries ?? task.max_retries,
10885
- retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
10886
- requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
10887
- approved_by: input.approved_by ?? task.approved_by,
10888
- approved_at: input.approved_by ? timestamp : task.approved_at
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
+ }))
10889
10783
  };
10890
- const databasePath = databasePathFromDatabase(d);
10891
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
10892
- const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
10893
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
10894
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
10895
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
10896
- }
10897
- if (input.status !== undefined && input.status !== task.status) {
10898
- const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
10899
- dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
10900
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
10901
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
10902
- }
10903
- if (input.approved_by !== undefined) {
10904
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
10905
- }
10906
- const updatePayload = taskEventData(updatedTask);
10907
- dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
10908
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
10909
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
10910
- return updatedTask;
10911
10784
  }
10912
- function deleteTask(id, db) {
10785
+ function importTemplate(json, db) {
10913
10786
  const d = db || getDatabase();
10914
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
10915
- if (!row)
10916
- return false;
10917
- recordStorageTombstone({
10918
- object_type: "tasks",
10919
- object_id: id,
10920
- payload: rowToTask(row),
10921
- version: row.version
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
10922
10809
  }, d);
10923
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
10924
- return result.changes > 0;
10925
10810
  }
10926
- var init_task_crud = __esm(() => {
10927
- init_types();
10928
- init_database();
10929
- init_completion_guard();
10930
- init_event_emission_safety();
10931
- init_event_hooks();
10932
- init_shared_events();
10933
- init_audit();
10934
- init_webhooks();
10935
- init_checklists();
10936
- init_storage_tombstones();
10937
- });
10938
-
10939
- // src/lib/recurrence.ts
10940
- function parseRecurrenceRule(rule) {
10941
- const normalized = rule.trim().toLowerCase();
10942
- if (normalized === "every weekday" || normalized === "every weekdays") {
10943
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
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;
10831
+ }
10944
10832
  }
10945
- if (normalized === "every day" || normalized === "daily") {
10946
- return { type: "interval", interval: 1, unit: "day" };
10833
+ const missing = [];
10834
+ for (const v of templateVars) {
10835
+ if (v.required && merged[v.name] === undefined) {
10836
+ missing.push(v.name);
10837
+ }
10947
10838
  }
10948
- if (normalized === "every week" || normalized === "weekly") {
10949
- return { type: "interval", interval: 1, unit: "week" };
10839
+ if (missing.length > 0) {
10840
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
10950
10841
  }
10951
- if (normalized === "every month" || normalized === "monthly") {
10952
- return { type: "interval", interval: 1, unit: "month" };
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);
10953
10848
  }
10954
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
10955
- if (intervalMatch) {
10956
- return {
10957
- type: "interval",
10958
- interval: parseInt(intervalMatch[1], 10),
10959
- unit: intervalMatch[2]
10960
- };
10849
+ return result;
10850
+ }
10851
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
10852
+ const d = db || getDatabase();
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}`);
10961
10859
  }
10962
- const daysMatch = normalized.match(/^every\s+(.+)$/);
10963
- if (daysMatch) {
10964
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
10965
- const days = [];
10966
- for (const part of dayParts) {
10967
- const dayNum = DAY_NAMES[part];
10968
- if (dayNum !== undefined) {
10969
- days.push(dayNum);
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];
10866
+ }
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);
10970
10878
  }
10879
+ continue;
10971
10880
  }
10972
- if (days.length > 0) {
10973
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
10881
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
10882
+ skippedPositions.add(tt.position);
10883
+ continue;
10974
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);
10975
10902
  }
10976
- 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"`);
10977
- }
10978
- function isValidRecurrenceRule(rule) {
10979
- try {
10980
- parseRecurrenceRule(rule);
10981
- return true;
10982
- } catch {
10983
- return false;
10984
- }
10985
- }
10986
- function nextOccurrence(rule, from) {
10987
- const parsed = parseRecurrenceRule(rule);
10988
- const base = from || new Date;
10989
- if (parsed.type === "interval") {
10990
- const next = new Date(base);
10991
- if (parsed.unit === "day") {
10992
- next.setDate(next.getDate() + parsed.interval);
10993
- } else if (parsed.unit === "week") {
10994
- next.setDate(next.getDate() + parsed.interval * 7);
10995
- } else if (parsed.unit === "month") {
10996
- next.setMonth(next.getMonth() + parsed.interval);
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
+ }
10997
10917
  }
10998
- return next.toISOString();
10999
10918
  }
11000
- if (parsed.type === "specific_days") {
11001
- const currentDay = base.getDay();
11002
- const days = parsed.days;
11003
- let daysToAdd = Infinity;
11004
- for (const day of days) {
11005
- let diff = day - currentDay;
11006
- if (diff <= 0)
11007
- diff += 7;
11008
- if (diff < daysToAdd)
11009
- daysToAdd = diff;
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
+ });
11010
10951
  }
11011
- const next = new Date(base);
11012
- next.setDate(next.getDate() + daysToAdd);
11013
- return next.toISOString();
11014
10952
  }
11015
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
11016
- }
11017
- var DAY_NAMES;
11018
- var init_recurrence = __esm(() => {
11019
- DAY_NAMES = {
11020
- sunday: 0,
11021
- sun: 0,
11022
- monday: 1,
11023
- mon: 1,
11024
- tuesday: 2,
11025
- tue: 2,
11026
- wednesday: 3,
11027
- wed: 3,
11028
- thursday: 4,
11029
- thu: 4,
11030
- friday: 5,
11031
- fri: 5,
11032
- saturday: 6,
11033
- sat: 6
11034
- };
11035
- });
11036
-
11037
- // src/db/templates.ts
11038
- var exports_templates = {};
11039
- __export(exports_templates, {
11040
- updateTemplate: () => updateTemplate,
11041
- tasksFromTemplate: () => tasksFromTemplate,
11042
- taskFromTemplate: () => taskFromTemplate,
11043
- resolveVariables: () => resolveVariables,
11044
- previewTemplate: () => previewTemplate,
11045
- listTemplates: () => listTemplates,
11046
- listTemplateVersions: () => listTemplateVersions,
11047
- importTemplate: () => importTemplate,
11048
- getTemplateWithTasks: () => getTemplateWithTasks,
11049
- getTemplateVersion: () => getTemplateVersion,
11050
- getTemplateTasks: () => getTemplateTasks,
11051
- getTemplate: () => getTemplate,
11052
- exportTemplate: () => exportTemplate,
11053
- evaluateCondition: () => evaluateCondition,
11054
- deleteTemplate: () => deleteTemplate,
11055
- createTemplate: () => createTemplate,
11056
- addTemplateTasks: () => addTemplateTasks
11057
- });
11058
- function rowToTemplate(row) {
11059
- return {
11060
- ...row,
11061
- tags: JSON.parse(row.tags || "[]"),
11062
- variables: JSON.parse(row.variables || "[]"),
11063
- metadata: JSON.parse(row.metadata || "{}"),
11064
- priority: row.priority || "medium",
11065
- version: row.version ?? 1
11066
- };
11067
- }
11068
- function rowToTemplateTask(row) {
11069
10953
  return {
11070
- ...row,
11071
- tags: JSON.parse(row.tags || "[]"),
11072
- depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
11073
- metadata: JSON.parse(row.metadata || "{}"),
11074
- priority: row.priority || "medium",
11075
- condition: row.condition ?? null,
11076
- include_template_id: row.include_template_id ?? null
10954
+ template_id: template.id,
10955
+ template_name: template.name,
10956
+ description: template.description,
10957
+ variables: template.variables,
10958
+ resolved_variables: resolved,
10959
+ tasks
11077
10960
  };
11078
10961
  }
11079
- function resolveTemplateId(id, d) {
11080
- return resolvePartialId(d, "task_templates", id);
11081
- }
11082
- function createTemplate(input, db) {
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) {
11083
10970
  const d = db || getDatabase();
11084
- const id = uuid();
11085
- const machineId = currentStorageMachineId(d);
11086
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
11087
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
11088
- id,
11089
- input.name,
11090
- input.title_pattern,
11091
- input.description || null,
11092
- input.priority || "medium",
11093
- JSON.stringify(input.tags || []),
11094
- JSON.stringify(input.variables || []),
11095
- input.project_id || null,
11096
- input.plan_id || null,
11097
- JSON.stringify(input.metadata || {}),
11098
- now(),
11099
- machineId
11100
- ]);
11101
- if (input.tasks && input.tasks.length > 0) {
11102
- addTemplateTasks(id, input.tasks, d);
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);
11103
10977
  }
11104
- return getTemplate(id, d);
10978
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
11105
10979
  }
11106
- function getTemplate(id, db) {
10980
+ function removeDependency(taskId, dependsOn, db) {
11107
10981
  const d = db || getDatabase();
11108
- const resolved = resolveTemplateId(id, d);
11109
- if (!resolved)
11110
- return null;
11111
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
11112
- return row ? rowToTemplate(row) : null;
10982
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
10983
+ return result.changes > 0;
11113
10984
  }
11114
- function listTemplates(db) {
10985
+ function getTaskDependencies(taskId, db) {
11115
10986
  const d = db || getDatabase();
11116
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
10987
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
11117
10988
  }
11118
- function deleteTemplate(id, db) {
10989
+ function getTaskDependents(taskId, db) {
11119
10990
  const d = db || getDatabase();
11120
- const resolved = resolveTemplateId(id, d);
11121
- if (!resolved)
11122
- return false;
11123
- const template = getTemplate(resolved, d);
11124
- if (!template)
11125
- return false;
11126
- recordStorageTombstone({
11127
- object_type: "templates",
11128
- object_id: resolved,
11129
- payload: template,
11130
- version: template.version
11131
- }, d);
11132
- return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
10991
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
11133
10992
  }
11134
- function updateTemplate(id, updates, db) {
10993
+ function cloneTask(taskId, overrides, db) {
11135
10994
  const d = db || getDatabase();
11136
- const resolved = resolveTemplateId(id, d);
11137
- if (!resolved)
11138
- return null;
11139
- const current = getTemplateWithTasks(resolved, d);
11140
- if (current) {
11141
- const snapshot = JSON.stringify({
11142
- name: current.name,
11143
- title_pattern: current.title_pattern,
11144
- description: current.description,
11145
- priority: current.priority,
11146
- tags: current.tags,
11147
- variables: current.variables,
11148
- project_id: current.project_id,
11149
- plan_id: current.plan_id,
11150
- metadata: current.metadata,
11151
- tasks: current.tasks
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";
11152
11026
  });
11153
- d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
11154
- }
11155
- const sets = ["version = version + 1"];
11156
- const values = [];
11157
- if (updates.name !== undefined) {
11158
- sets.push("name = ?");
11159
- values.push(updates.name);
11160
- }
11161
- if (updates.title_pattern !== undefined) {
11162
- sets.push("title_pattern = ?");
11163
- values.push(updates.title_pattern);
11164
- }
11165
- if (updates.description !== undefined) {
11166
- sets.push("description = ?");
11167
- values.push(updates.description);
11027
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
11168
11028
  }
11169
- if (updates.priority !== undefined) {
11170
- sets.push("priority = ?");
11171
- values.push(updates.priority);
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);
11172
11040
  }
11173
- if (updates.tags !== undefined) {
11174
- sets.push("tags = ?");
11175
- values.push(JSON.stringify(updates.tags));
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);
11176
11052
  }
11177
- if (updates.variables !== undefined) {
11178
- sets.push("variables = ?");
11179
- values.push(JSON.stringify(updates.variables));
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) {
11066
+ sets.push("task_list_id = ?");
11067
+ params.push(target.task_list_id);
11180
11068
  }
11181
- if (updates.project_id !== undefined) {
11069
+ if (target.project_id !== undefined) {
11182
11070
  sets.push("project_id = ?");
11183
- values.push(updates.project_id);
11071
+ params.push(target.project_id);
11184
11072
  }
11185
- if (updates.plan_id !== undefined) {
11073
+ if (target.plan_id !== undefined) {
11186
11074
  sets.push("plan_id = ?");
11187
- values.push(updates.plan_id);
11075
+ params.push(target.plan_id);
11188
11076
  }
11189
- if (updates.metadata !== undefined) {
11190
- sets.push("metadata = ?");
11191
- values.push(JSON.stringify(updates.metadata));
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
+ }
11192
11095
  }
11193
- values.push(resolved);
11194
- d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
11195
- return getTemplate(resolved, d);
11096
+ return false;
11196
11097
  }
11197
- function taskFromTemplate(templateId, overrides = {}, db) {
11198
- const t = getTemplate(templateId, db);
11199
- if (!t)
11200
- throw new Error(`Template not found: ${templateId}`);
11201
- const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
11202
- return {
11203
- title: cleanOverrides.title || t.title_pattern,
11204
- description: cleanOverrides.description ?? t.description ?? undefined,
11205
- priority: cleanOverrides.priority ?? t.priority,
11206
- tags: cleanOverrides.tags ?? t.tags,
11207
- project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
11208
- plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
11209
- metadata: cleanOverrides.metadata ?? t.metadata,
11210
- ...cleanOverrides
11211
- };
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();
11212
11127
  }
11213
- function addTemplateTasks(templateId, tasks, db) {
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) {
11214
11136
  const d = db || getDatabase();
11215
- const template = getTemplate(templateId, d);
11216
- if (!template)
11217
- throw new Error(`Template not found: ${templateId}`);
11218
- d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
11219
- const results = [];
11220
- for (let i = 0;i < tasks.length; i++) {
11221
- const task = tasks[i];
11222
- const id = uuid();
11223
- 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)
11224
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
11225
- id,
11226
- templateId,
11227
- i,
11228
- task.title_pattern,
11229
- task.description || null,
11230
- task.priority || "medium",
11231
- JSON.stringify(task.tags || []),
11232
- task.task_type || null,
11233
- task.condition || null,
11234
- task.include_template_id || null,
11235
- JSON.stringify(task.depends_on || []),
11236
- JSON.stringify(task.metadata || {}),
11237
- now()
11238
- ]);
11239
- const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
11240
- if (row)
11241
- results.push(rowToTemplateTask(row));
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);
11242
11145
  }
11243
- return results;
11146
+ return blocking;
11244
11147
  }
11245
- function getTemplateWithTasks(id, db) {
11148
+ function startTask(id, agentId, db) {
11246
11149
  const d = db || getDatabase();
11247
- const template = getTemplate(id, d);
11248
- if (!template)
11249
- return null;
11250
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
11251
- const tasks = rows.map(rowToTemplateTask);
11252
- return { ...template, tasks };
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}`);
11169
+ }
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]);
11174
+ if (result.changes === 0) {
11175
+ const current = getTask(id, 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`);
11183
+ }
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;
11253
11191
  }
11254
- function getTemplateTasks(templateId, db) {
11192
+ function completeTask(id, agentId, db, options) {
11255
11193
  const d = db || getDatabase();
11256
- const resolved = resolveTemplateId(templateId, d);
11257
- if (!resolved)
11258
- return [];
11259
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
11260
- return rows.map(rowToTemplateTask);
11261
- }
11262
- function evaluateCondition(condition, variables) {
11263
- if (!condition || condition.trim() === "")
11264
- return true;
11265
- const trimmed = condition.trim();
11266
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
11267
- if (eqMatch) {
11268
- const varName = eqMatch[1];
11269
- const expected = eqMatch[2].trim();
11270
- return (variables[varName] ?? "") === expected;
11194
+ const databasePath = databasePathFromDatabase(d);
11195
+ const task = getTask(id, d);
11196
+ if (!task)
11197
+ throw new TaskNotFoundError(id);
11198
+ if (task.status === "completed") {
11199
+ return task;
11271
11200
  }
11272
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
11273
- if (neqMatch) {
11274
- const varName = neqMatch[1];
11275
- const expected = neqMatch[2].trim();
11276
- return (variables[varName] ?? "") !== expected;
11201
+ if (task.status === "cancelled") {
11202
+ throw new Error(`Task ${id} is cancelled and cannot be completed`);
11277
11203
  }
11278
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
11279
- if (falsyMatch) {
11280
- const varName = falsyMatch[1];
11281
- const val = variables[varName];
11282
- return !val || val === "" || val === "false";
11204
+ if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
11205
+ throw new LockError(id, task.locked_by);
11283
11206
  }
11284
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
11285
- if (truthyMatch) {
11286
- const varName = truthyMatch[1];
11287
- const val = variables[varName];
11288
- return !!val && val !== "" && val !== "false";
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 };
11215
+ }
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
+ }
11262
+ }
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
+ }
11280
+ }
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 };
11284
+ }
11285
+ if (spawnedFromTemplate) {
11286
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
11287
+ }
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 });
11305
+ }
11306
+ }
11307
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
11308
+ }
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
+ };
11319
+ }
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) };
11325
+ }
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
+ };
11339
+ }
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
+ };
11289
11352
  }
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) };
11355
+ }
11356
+ function unlockTask(id, agentId, db) {
11357
+ const d = db || getDatabase();
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);
11363
+ }
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]);
11290
11367
  return true;
11291
11368
  }
11292
- function exportTemplate(id, db) {
11369
+ function getTaskLockStatus(id, db) {
11293
11370
  const d = db || getDatabase();
11294
- const template = getTemplateWithTasks(id, d);
11295
- if (!template)
11296
- throw new Error(`Template not found: ${id}`);
11371
+ const task = getTask(id, d);
11372
+ if (!task)
11373
+ throw new TaskNotFoundError(id);
11374
+ const expired = isLockExpired(task.locked_at);
11297
11375
  return {
11298
- name: template.name,
11299
- title_pattern: template.title_pattern,
11300
- description: template.description,
11301
- priority: template.priority,
11302
- tags: template.tags,
11303
- variables: template.variables,
11304
- project_id: template.project_id,
11305
- plan_id: template.plan_id,
11306
- metadata: template.metadata,
11307
- tasks: template.tasks.map((t) => ({
11308
- position: t.position,
11309
- title_pattern: t.title_pattern,
11310
- description: t.description,
11311
- priority: t.priority,
11312
- tags: t.tags,
11313
- task_type: t.task_type,
11314
- condition: t.condition,
11315
- include_template_id: t.include_template_id,
11316
- depends_on_positions: t.depends_on_positions,
11317
- metadata: t.metadata
11318
- }))
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
11319
11382
  };
11320
11383
  }
11321
- function importTemplate(json, db) {
11322
- const d = db || getDatabase();
11323
- const taskInputs = (json.tasks || []).map((t) => ({
11324
- title_pattern: t.title_pattern,
11325
- description: t.description ?? undefined,
11326
- priority: t.priority,
11327
- tags: t.tags,
11328
- task_type: t.task_type ?? undefined,
11329
- condition: t.condition ?? undefined,
11330
- include_template_id: t.include_template_id ?? undefined,
11331
- depends_on: t.depends_on_positions,
11332
- metadata: t.metadata
11333
- }));
11334
- return createTemplate({
11335
- name: json.name,
11336
- title_pattern: json.title_pattern,
11337
- description: json.description ?? undefined,
11338
- priority: json.priority,
11339
- tags: json.tags,
11340
- variables: json.variables,
11341
- project_id: json.project_id ?? undefined,
11342
- plan_id: json.plan_id ?? undefined,
11343
- metadata: json.metadata,
11344
- tasks: taskInputs
11345
- }, d);
11346
- }
11347
- function getTemplateVersion(id, version, db) {
11384
+ function claimNextTask(agentId, filters, db) {
11348
11385
  const d = db || getDatabase();
11349
- const resolved = resolveTemplateId(id, d);
11350
- if (!resolved)
11351
- return null;
11352
- const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
11353
- return row || null;
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;
11354
11406
  }
11355
- function listTemplateVersions(id, db) {
11407
+ function getNextTask(agentId, filters, db) {
11356
11408
  const d = db || getDatabase();
11357
- const resolved = resolveTemplateId(id, d);
11358
- if (!resolved)
11359
- return [];
11360
- return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
11361
- }
11362
- function resolveVariables(templateVars, provided) {
11363
- const merged = { ...provided };
11364
- for (const v of templateVars) {
11365
- if (merged[v.name] === undefined && v.default !== undefined) {
11366
- merged[v.name] = v.default;
11367
- }
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);
11368
11415
  }
11369
- const missing = [];
11370
- for (const v of templateVars) {
11371
- if (v.required && merged[v.name] === undefined) {
11372
- missing.push(v.name);
11373
- }
11416
+ if (filters?.task_list_id) {
11417
+ conditions.push("task_list_id = ?");
11418
+ params.push(filters.task_list_id);
11374
11419
  }
11375
- if (missing.length > 0) {
11376
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
11420
+ if (filters?.plan_id) {
11421
+ conditions.push("plan_id = ?");
11422
+ params.push(filters.plan_id);
11377
11423
  }
11378
- return merged;
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);
11428
+ }
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);
11435
+ }
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);
11440
+ }
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);
11445
+ }
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;
11379
11449
  }
11380
- function substituteVars(text, variables) {
11381
- let result = text;
11382
- for (const [key, val] of Object.entries(variables)) {
11383
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
11450
+ function getActiveWork(filters, db) {
11451
+ const d = db || getDatabase();
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);
11384
11458
  }
11385
- return result;
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;
11386
11468
  }
11387
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
11469
+ function getTasksChangedSince(since, filters, db) {
11388
11470
  const d = db || getDatabase();
11389
- const template = getTemplateWithTasks(templateId, d);
11390
- if (!template)
11391
- throw new Error(`Template not found: ${templateId}`);
11392
- const visited = _visitedTemplateIds || new Set;
11393
- if (visited.has(template.id)) {
11394
- throw new Error(`Circular template reference detected: ${template.id}`);
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);
11395
11476
  }
11396
- visited.add(template.id);
11397
- const resolved = resolveVariables(template.variables, variables);
11398
- if (template.tasks.length === 0) {
11399
- const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
11400
- const task = createTask(input, d);
11401
- return [task];
11477
+ if (filters?.task_list_id) {
11478
+ conditions.push("task_list_id = ?");
11479
+ params.push(filters.task_list_id);
11402
11480
  }
11403
- const createdTasks = [];
11404
- const positionToId = new Map;
11405
- const skippedPositions = new Set;
11406
- for (const tt of template.tasks) {
11407
- if (tt.include_template_id) {
11408
- const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
11409
- createdTasks.push(...includedTasks);
11410
- if (includedTasks.length > 0) {
11411
- positionToId.set(tt.position, includedTasks[0].id);
11412
- } else {
11413
- skippedPositions.add(tt.position);
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);
11484
+ }
11485
+ function failTask(id, agentId, reason, options, db) {
11486
+ const d = db || getDatabase();
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
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);
11414
11540
  }
11415
- continue;
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]);
11416
11556
  }
11417
- if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
11418
- skippedPositions.add(tt.position);
11419
- continue;
11557
+ }
11558
+ return { task: failedTask, retryTask };
11559
+ }
11560
+ function getStaleTasks(staleQuery = 30, filters, db) {
11561
+ const d = db || getDatabase();
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);
11581
+ }
11582
+ function stealTask(agentId, opts, db) {
11583
+ const d = db || getDatabase();
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)
11588
+ return 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;
11606
+ }
11607
+ function claimOrSteal(agentId, filters, db) {
11608
+ const d = db || getDatabase();
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 };
11420
11614
  }
11421
- let title = tt.title_pattern;
11422
- let desc = tt.description;
11423
- title = substituteVars(title, resolved);
11424
- if (desc)
11425
- desc = substituteVars(desc, resolved);
11426
- const task = createTask({
11427
- title,
11428
- description: desc ?? undefined,
11429
- priority: tt.priority,
11430
- tags: tt.tags,
11431
- task_type: tt.task_type ?? undefined,
11432
- project_id: projectId,
11433
- task_list_id: taskListId,
11434
- metadata: tt.metadata
11435
- }, d);
11436
- createdTasks.push(task);
11437
- positionToId.set(tt.position, task.id);
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();
11621
+ }
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);
11438
11628
  }
11439
- for (const tt of template.tasks) {
11440
- if (skippedPositions.has(tt.position))
11441
- continue;
11442
- if (tt.include_template_id)
11443
- continue;
11444
- const deps = tt.depends_on_positions;
11445
- for (const depPos of deps) {
11446
- if (skippedPositions.has(depPos))
11447
- continue;
11448
- const taskId = positionToId.get(tt.position);
11449
- const depId = positionToId.get(depPos);
11450
- if (taskId && depId) {
11451
- addDependency(taskId, depId, d);
11452
- }
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);
11646
+ }
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);
11681
+ }
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}`);
11453
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));
11454
11696
  }
11455
- return createdTasks;
11456
11697
  }
11457
- function previewTemplate(templateId, variables, db) {
11698
+ function createTask(input, db) {
11458
11699
  const d = db || getDatabase();
11459
- const template = getTemplateWithTasks(templateId, d);
11460
- if (!template)
11461
- throw new Error(`Template not found: ${templateId}`);
11462
- const resolved = resolveVariables(template.variables, variables);
11463
- const tasks = [];
11464
- if (template.tasks.length === 0) {
11465
- tasks.push({
11466
- position: 0,
11467
- title: substituteVars(template.title_pattern, resolved),
11468
- description: template.description ? substituteVars(template.description, resolved) : null,
11469
- priority: template.priority,
11470
- tags: template.tags,
11471
- task_type: null,
11472
- depends_on_positions: []
11473
- });
11474
- } else {
11475
- for (const tt of template.tasks) {
11476
- 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();
11477
11753
  continue;
11478
- tasks.push({
11479
- position: tt.position,
11480
- title: substituteVars(tt.title_pattern, resolved),
11481
- description: tt.description ? substituteVars(tt.description, resolved) : null,
11482
- priority: tt.priority,
11483
- tags: tt.tags,
11484
- task_type: tt.task_type,
11485
- depends_on_positions: tt.depends_on_positions
11486
- });
11754
+ }
11755
+ throw e;
11487
11756
  }
11488
11757
  }
11489
- return {
11490
- template_id: template.id,
11491
- template_name: template.name,
11492
- description: template.description,
11493
- variables: template.variables,
11494
- resolved_variables: resolved,
11495
- tasks
11496
- };
11497
- }
11498
- var init_templates = __esm(() => {
11499
- init_database();
11500
- init_tasks();
11501
- init_storage_tombstones();
11502
- });
11503
-
11504
- // src/db/task-graph.ts
11505
- function addDependency(taskId, dependsOn, db) {
11506
- const d = db || getDatabase();
11507
- if (!getTask(taskId, d))
11508
- throw new TaskNotFoundError(taskId);
11509
- if (!getTask(dependsOn, d))
11510
- throw new TaskNotFoundError(dependsOn);
11511
- if (wouldCreateCycle(taskId, dependsOn, d)) {
11512
- throw new DependencyCycleError(taskId, dependsOn);
11758
+ if (tags.length > 0) {
11759
+ insertTaskTags(id, tags, d);
11513
11760
  }
11514
- d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
11515
- }
11516
- function removeDependency(taskId, dependsOn, db) {
11517
- const d = db || getDatabase();
11518
- const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
11519
- return result.changes > 0;
11520
- }
11521
- function getTaskDependencies(taskId, db) {
11522
- const d = db || getDatabase();
11523
- 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;
11524
11768
  }
11525
- function getTaskDependents(taskId, db) {
11769
+ function getTask(id, db) {
11526
11770
  const d = db || getDatabase();
11527
- 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);
11528
11775
  }
11529
- function cloneTask(taskId, overrides, db) {
11776
+ function getTaskWithRelations(id, db) {
11530
11777
  const d = db || getDatabase();
11531
- const source = getTask(taskId, d);
11532
- if (!source)
11533
- throw new TaskNotFoundError(taskId);
11534
- const input = {
11535
- title: overrides?.title ?? source.title,
11536
- description: overrides?.description ?? source.description ?? undefined,
11537
- priority: overrides?.priority ?? source.priority,
11538
- project_id: overrides?.project_id ?? source.project_id ?? undefined,
11539
- parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
11540
- plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
11541
- task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
11542
- status: overrides?.status ?? "pending",
11543
- agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
11544
- assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
11545
- tags: overrides?.tags ?? source.tags,
11546
- metadata: overrides?.metadata ?? source.metadata,
11547
- estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
11548
- 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
11549
11802
  };
11550
- return createTask(input, d);
11551
11803
  }
11552
- function getTaskGraph(taskId, direction = "both", db) {
11804
+ function listTasks(filter = {}, db) {
11553
11805
  const d = db || getDatabase();
11554
- const task = getTask(taskId, d);
11555
- if (!task)
11556
- throw new TaskNotFoundError(taskId);
11557
- function toNode(t) {
11558
- const deps = getTaskDependencies(t.id, d);
11559
- const hasUnfinishedDeps = deps.some((dep) => {
11560
- const depTask = getTask(dep.depends_on, d);
11561
- return depTask && depTask.status !== "completed";
11562
- });
11563
- 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);
11564
11813
  }
11565
- function buildUp(id, visited) {
11566
- if (visited.has(id))
11567
- return [];
11568
- visited.add(id);
11569
- const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
11570
- return deps.map((dep) => {
11571
- const depTask = getTask(dep.depends_on, d);
11572
- if (!depTask)
11573
- return null;
11574
- return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
11575
- }).filter(Boolean);
11814
+ if (filter.ids && filter.ids.length > 0) {
11815
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
11816
+ params.push(...filter.ids);
11576
11817
  }
11577
- function buildDown(id, visited) {
11578
- if (visited.has(id))
11579
- return [];
11580
- visited.add(id);
11581
- const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
11582
- return dependents.map((dep) => {
11583
- const depTask = getTask(dep.task_id, d);
11584
- if (!depTask)
11585
- return null;
11586
- return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
11587
- }).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
+ }
11588
11825
  }
11589
- const rootNode = toNode(task);
11590
- const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
11591
- const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
11592
- return { task: rootNode, depends_on, blocks };
11593
- }
11594
- function moveTask(taskId, target, db) {
11595
- const d = db || getDatabase();
11596
- const task = getTask(taskId, d);
11597
- if (!task)
11598
- throw new TaskNotFoundError(taskId);
11599
- const sets = ["updated_at = ?", "version = version + 1"];
11600
- const params = [now()];
11601
- if (target.task_list_id !== undefined) {
11602
- sets.push("task_list_id = ?");
11603
- 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
+ }
11604
11834
  }
11605
- if (target.project_id !== undefined) {
11606
- sets.push("project_id = ?");
11607
- 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
+ }
11608
11843
  }
11609
- if (target.plan_id !== undefined) {
11610
- sets.push("plan_id = ?");
11611
- params.push(target.plan_id);
11844
+ if (filter.assigned_to) {
11845
+ conditions.push("assigned_to = ?");
11846
+ params.push(filter.assigned_to);
11612
11847
  }
11613
- params.push(taskId);
11614
- d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
11615
- return getTask(taskId, d);
11616
- }
11617
- function wouldCreateCycle(taskId, dependsOn, db) {
11618
- const visited = new Set;
11619
- const queue = [dependsOn];
11620
- while (queue.length > 0) {
11621
- const current = queue.shift();
11622
- if (current === taskId)
11623
- return true;
11624
- if (visited.has(current))
11625
- continue;
11626
- visited.add(current);
11627
- const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
11628
- for (const dep of deps) {
11629
- 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);
11630
11881
  }
11631
11882
  }
11632
- 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);
11633
11907
  }
11634
- var init_task_graph = __esm(() => {
11635
- init_types();
11636
- init_database();
11637
- init_task_crud();
11638
- });
11639
-
11640
- // src/db/task-lifecycle.ts
11641
- function lockExpiresAt(lockedAt) {
11642
- if (!lockedAt)
11643
- return null;
11644
- 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;
11645
11911
  }
11646
- function assertStartable(task, agentId) {
11647
- if (task.status === "pending")
11648
- return;
11649
- if (task.status === "in_progress")
11650
- return;
11651
- 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
+ };
11652
11918
  }
11653
- function getBlockingDeps(id, db) {
11919
+ function upsertTaskByFingerprint(input, db) {
11654
11920
  const d = db || getDatabase();
11655
- const deps = getTaskDependencies(id, d);
11656
- if (deps.length === 0)
11657
- return [];
11658
- const blocking = [];
11659
- for (const dep of deps) {
11660
- const task = getTask(dep.depends_on, d);
11661
- if (task && task.status !== "completed")
11662
- blocking.push(task);
11663
- }
11664
- 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();
11665
11958
  }
11666
- function startTask(id, agentId, db) {
11959
+ function countTasks(filter = {}, db) {
11667
11960
  const d = db || getDatabase();
11668
- const databasePath = databasePathFromDatabase(d);
11669
- const task = getTask(id, d);
11670
- if (!task)
11671
- throw new TaskNotFoundError(id);
11672
- assertStartable(task, agentId);
11673
- const blocking = getBlockingDeps(id, d);
11674
- if (blocking.length > 0) {
11675
- const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
11676
- emitLocalEventHooksQuiet({
11677
- type: "task.blocked",
11678
- payload: {
11679
- id,
11680
- agent_id: agentId,
11681
- title: task.title,
11682
- blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
11683
- },
11684
- databasePath
11685
- });
11686
- throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
11687
- }
11688
- const cutoff = lockExpiryCutoff();
11689
- const timestamp = now();
11690
- 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 = ?
11691
- 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]);
11692
- if (result.changes === 0) {
11693
- const current = getTask(id, d);
11694
- if (!current)
11695
- throw new TaskNotFoundError(id);
11696
- assertStartable(current, agentId);
11697
- if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
11698
- throw new LockError(id, current.locked_by);
11699
- }
11700
- 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);
11701
11966
  }
11702
- logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
11703
- 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 };
11704
- const payload = taskEventData(startedTask, { agent_id: agentId });
11705
- dispatchWebhook2("task.started", payload, d).catch(() => {});
11706
- emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
11707
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
11708
- return startedTask;
11709
- }
11710
- function completeTask(id, agentId, db, options) {
11711
- const d = db || getDatabase();
11712
- const databasePath = databasePathFromDatabase(d);
11713
- const task = getTask(id, d);
11714
- if (!task)
11715
- throw new TaskNotFoundError(id);
11716
- if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
11717
- 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);
11718
11970
  }
11719
- checkCompletionGuard(task, agentId || null, d);
11720
- 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;
11721
- const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
11722
- const completionMeta = {};
11723
- if (hasEvidence)
11724
- completionMeta._evidence = evidence;
11725
- if (options?.confidence !== undefined) {
11726
- 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
+ }
11727
11978
  }
11728
- const hasMeta = Object.keys(completionMeta).length > 0;
11729
- const timestamp = options?.completed_at || now();
11730
- const confidence = options?.confidence !== undefined ? options.confidence : null;
11731
- const tx = d.transaction(() => {
11732
- if (hasMeta) {
11733
- const meta2 = { ...task.metadata, ...completionMeta };
11734
- const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
11735
- if (metaResult.changes === 0) {
11736
- const current = getTask(id, d);
11737
- throw new VersionConflictError(id, task.version, current?.version ?? -1);
11738
- }
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);
11739
11986
  }
11740
- d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
11741
- WHERE id = ?`, [timestamp, confidence, timestamp, id]);
11742
- });
11743
- tx();
11744
- logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
11745
- const completedTaskForEvent = {
11746
- ...task,
11747
- status: "completed",
11748
- locked_by: null,
11749
- locked_at: null,
11750
- completed_at: timestamp,
11751
- confidence,
11752
- version: task.version + 1,
11753
- updated_at: timestamp,
11754
- metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
11755
- };
11756
- const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
11757
- dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
11758
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
11759
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
11760
- let spawnedTask = null;
11761
- if (task.recurrence_rule && !options?.skip_recurrence) {
11762
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
11763
11987
  }
11764
- let spawnedFromTemplate = null;
11765
- if (task.spawns_template_id) {
11766
- const spawnDepth = task.metadata?._spawn_depth || 0;
11767
- if (spawnDepth >= MAX_SPAWN_DEPTH) {
11768
- 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);
11769
11992
  } else {
11770
- try {
11771
- const input = taskFromTemplate(task.spawns_template_id, {
11772
- project_id: task.project_id ?? undefined,
11773
- plan_id: task.plan_id ?? undefined,
11774
- task_list_id: task.task_list_id ?? undefined,
11775
- assigned_to: task.assigned_to ?? undefined
11776
- }, d);
11777
- input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
11778
- spawnedFromTemplate = createTask(input, d);
11779
- } catch {}
11993
+ conditions.push("priority = ?");
11994
+ params.push(filter.priority);
11780
11995
  }
11781
11996
  }
11782
- const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
11783
- if (spawnedTask) {
11784
- 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);
11785
12000
  }
11786
- if (spawnedFromTemplate) {
11787
- 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);
11788
12004
  }
11789
- const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
11790
- JOIN task_dependencies td ON td.task_id = t.id
11791
- WHERE td.depends_on = ? AND t.status = 'pending'
11792
- AND NOT EXISTS (
11793
- SELECT 1 FROM task_dependencies td2
11794
- JOIN tasks dep2 ON dep2.id = td2.depends_on
11795
- WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
11796
- )`).all(id, id);
11797
- if (unblockedDeps.length > 0) {
11798
- meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
11799
- for (const dep of unblockedDeps) {
11800
- const depTask = getTask(dep.id, d);
11801
- const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
11802
- dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
11803
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
11804
- if (depTask)
11805
- 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);
11806
12034
  }
11807
12035
  }
11808
- 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;
11809
12043
  }
11810
- function lockTask(id, agentId, db) {
12044
+ function updateTask(id, input, db) {
11811
12045
  const d = db || getDatabase();
11812
12046
  const task = getTask(id, d);
11813
12047
  if (!task)
11814
12048
  throw new TaskNotFoundError(id);
11815
- if (task.status === "completed" || task.status === "cancelled") {
11816
- return {
11817
- success: false,
11818
- error: `Task is ${task.status} and cannot be locked`
11819
- };
11820
- }
11821
- if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
11822
- const timestamp2 = now();
11823
- d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
11824
- logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
11825
- 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);
11826
12051
  }
11827
- const cutoff = lockExpiryCutoff();
11828
12052
  const timestamp = now();
11829
- const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
11830
- 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]);
11831
- if (result.changes === 0) {
11832
- const current = getTask(id, d);
11833
- if (!current)
11834
- throw new TaskNotFoundError(id);
11835
- if (current.status === "completed" || current.status === "cancelled") {
11836
- return {
11837
- success: false,
11838
- error: `Task is ${current.status} and cannot be locked`
11839
- };
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);
11840
12067
  }
11841
- if (current.locked_by && !isLockExpired(current.locked_at)) {
11842
- return {
11843
- success: false,
11844
- locked_by: current.locked_by,
11845
- locked_at: current.locked_at,
11846
- error: `Task is locked by ${current.locked_by}`
11847
- };
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");
11848
12077
  }
11849
- return {
11850
- success: false,
11851
- error: `Task ${id} could not be locked because it changed during lock acquisition`
11852
- };
11853
12078
  }
11854
- logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
11855
- return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
11856
- }
11857
- function unlockTask(id, agentId, db) {
11858
- const d = db || getDatabase();
11859
- const task = getTask(id, d);
11860
- if (!task)
11861
- throw new TaskNotFoundError(id);
11862
- if (agentId && task.locked_by && task.locked_by !== agentId) {
11863
- throw new LockError(id, task.locked_by);
12079
+ if (input.priority !== undefined) {
12080
+ sets.push("priority = ?");
12081
+ params.push(input.priority);
11864
12082
  }
11865
- const timestamp = now();
11866
- d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
11867
- WHERE id = ?`, [timestamp, id]);
11868
- return true;
11869
- }
11870
- function getTaskLockStatus(id, db) {
11871
- const d = db || getDatabase();
11872
- const task = getTask(id, d);
11873
- if (!task)
11874
- throw new TaskNotFoundError(id);
11875
- const expired = isLockExpired(task.locked_at);
11876
- return {
11877
- task_id: id,
11878
- locked: !!task.locked_by && !expired,
11879
- locked_by: task.locked_by,
11880
- locked_at: task.locked_at,
11881
- expires_at: lockExpiresAt(task.locked_at),
11882
- expired
11883
- };
11884
- }
11885
- function claimNextTask(agentId, filters, db) {
11886
- const d = db || getDatabase();
11887
- const tx = d.transaction(() => {
11888
- const task = getNextTask(agentId, filters, d);
11889
- if (!task)
11890
- return null;
11891
- return startTask(task.id, agentId, d);
11892
- });
11893
- return tx();
11894
- }
11895
- function getNextTask(agentId, filters, db) {
11896
- const d = db || getDatabase();
11897
- clearExpiredLocks(d);
11898
- const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
11899
- const params = [lockExpiryCutoff()];
11900
- if (filters?.project_id) {
11901
- conditions.push("project_id = ?");
11902
- params.push(filters.project_id);
12083
+ if (input.project_id !== undefined) {
12084
+ sets.push("project_id = ?");
12085
+ params.push(input.project_id);
11903
12086
  }
11904
- if (filters?.task_list_id) {
11905
- conditions.push("task_list_id = ?");
11906
- params.push(filters.task_list_id);
12087
+ if (input.assigned_to !== undefined) {
12088
+ sets.push("assigned_to = ?");
12089
+ params.push(input.assigned_to);
11907
12090
  }
11908
- if (filters?.plan_id) {
11909
- conditions.push("plan_id = ?");
11910
- params.push(filters.plan_id);
12091
+ if (input.working_dir !== undefined) {
12092
+ sets.push("working_dir = ?");
12093
+ params.push(input.working_dir);
11911
12094
  }
11912
- if (filters?.tags && filters.tags.length > 0) {
11913
- const placeholders = filters.tags.map(() => "?").join(",");
11914
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
11915
- params.push(...filters.tags);
12095
+ if (input.tags !== undefined) {
12096
+ sets.push("tags = ?");
12097
+ params.push(JSON.stringify(input.tags));
11916
12098
  }
11917
- 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')");
11918
- const where = conditions.join(" AND ");
11919
- let recentProjectIds = [];
11920
- if (agentId) {
11921
- 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);
11922
- recentProjectIds = recentRows.map((r) => r.project_id);
12099
+ if (input.metadata !== undefined) {
12100
+ sets.push("metadata = ?");
12101
+ params.push(JSON.stringify(input.metadata));
11923
12102
  }
11924
- let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
11925
- if (agentId) {
11926
- sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
11927
- params.push(agentId);
12103
+ if (input.plan_id !== undefined) {
12104
+ sets.push("plan_id = ?");
12105
+ params.push(input.plan_id);
11928
12106
  }
11929
- if (recentProjectIds.length > 0) {
11930
- const placeholders = recentProjectIds.map(() => "?").join(",");
11931
- sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
11932
- params.push(...recentProjectIds);
12107
+ if (input.task_list_id !== undefined) {
12108
+ sets.push("task_list_id = ?");
12109
+ params.push(input.task_list_id);
11933
12110
  }
11934
- 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`;
11935
- const row = d.query(sql).get(...params);
11936
- return row ? rowToTask(row) : null;
11937
- }
11938
- function getActiveWork(filters, db) {
11939
- const d = db || getDatabase();
11940
- clearExpiredLocks(d);
11941
- const conditions = ["status = 'in_progress'"];
11942
- const params = [];
11943
- if (filters?.project_id) {
11944
- conditions.push("project_id = ?");
11945
- params.push(filters.project_id);
12111
+ if (input.due_at !== undefined) {
12112
+ sets.push("due_at = ?");
12113
+ params.push(input.due_at);
11946
12114
  }
11947
- if (filters?.task_list_id) {
11948
- conditions.push("task_list_id = ?");
11949
- params.push(filters.task_list_id);
12115
+ if (input.estimated_minutes !== undefined) {
12116
+ sets.push("estimated_minutes = ?");
12117
+ params.push(input.estimated_minutes);
11950
12118
  }
11951
- const where = conditions.join(" AND ");
11952
- const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
11953
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
11954
- updated_at DESC`).all(...params);
11955
- return rows;
11956
- }
11957
- function getTasksChangedSince(since, filters, db) {
11958
- const d = db || getDatabase();
11959
- const conditions = ["updated_at > ?"];
11960
- const params = [since];
11961
- if (filters?.project_id) {
11962
- conditions.push("project_id = ?");
11963
- params.push(filters.project_id);
12119
+ if (input.sla_minutes !== undefined) {
12120
+ sets.push("sla_minutes = ?");
12121
+ params.push(input.sla_minutes);
11964
12122
  }
11965
- if (filters?.task_list_id) {
11966
- conditions.push("task_list_id = ?");
11967
- params.push(filters.task_list_id);
12123
+ if (input.actual_minutes !== undefined) {
12124
+ sets.push("actual_minutes = ?");
12125
+ params.push(input.actual_minutes);
11968
12126
  }
11969
- const where = conditions.join(" AND ");
11970
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
11971
- return rows.map(rowToTask);
11972
- }
11973
- function failTask(id, agentId, reason, options, db) {
11974
- const d = db || getDatabase();
11975
- const databasePath = databasePathFromDatabase(d);
11976
- const task = getTask(id, d);
11977
- if (!task)
11978
- throw new TaskNotFoundError(id);
11979
- const meta = {
11980
- ...task.metadata,
11981
- _failure: {
11982
- reason: reason || "Unknown failure",
11983
- error_code: options?.error_code || null,
11984
- failed_by: agentId || null,
11985
- failed_at: now(),
11986
- 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)}`);
11987
12181
  }
11988
- };
11989
- const timestamp = now();
11990
- d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
11991
- WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
11992
- 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 = {
11993
12199
  ...task,
11994
- status: "failed",
11995
- locked_by: null,
11996
- locked_at: null,
11997
- metadata: meta,
12200
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
12201
+ tags: input.tags ?? task.tags,
12202
+ metadata: input.metadata ?? task.metadata,
11998
12203
  version: task.version + 1,
11999
- 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
12000
12217
  };
12001
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
12002
- const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
12003
- dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
12004
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
12005
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
12006
- let retryTask;
12007
- if (options?.retry) {
12008
- const retryCount = (task.retry_count || 0) + 1;
12009
- const maxRetries = task.max_retries || 3;
12010
- if (retryCount > maxRetries) {
12011
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
12012
- JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
12013
- id
12014
- ]);
12015
- } else {
12016
- const backoffMinutes = Math.pow(5, retryCount - 1);
12017
- const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
12018
- let title = task.title;
12019
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
12020
- title = title.slice(task.short_id.length + 2);
12021
- }
12022
- retryTask = createTask({
12023
- title,
12024
- description: task.description ?? undefined,
12025
- priority: task.priority,
12026
- project_id: task.project_id ?? undefined,
12027
- task_list_id: task.task_list_id ?? undefined,
12028
- plan_id: task.plan_id ?? undefined,
12029
- assigned_to: task.assigned_to ?? undefined,
12030
- tags: task.tags,
12031
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
12032
- estimated_minutes: task.estimated_minutes ?? undefined,
12033
- recurrence_rule: task.recurrence_rule ?? undefined,
12034
- due_at: retryAfter
12035
- }, d);
12036
- d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
12037
- }
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 });
12038
12224
  }
12039
- return { task: failedTask, retryTask };
12040
- }
12041
- function getStaleTasks(staleQuery = 30, filters, db) {
12042
- const d = db || getDatabase();
12043
- const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
12044
- const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
12045
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
12046
- const conditions = [
12047
- "status = 'in_progress'",
12048
- "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
12049
- ];
12050
- const params = [cutoff, cutoff];
12051
- if (effectiveFilters?.project_id) {
12052
- conditions.push("project_id = ?");
12053
- 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 });
12054
12230
  }
12055
- if (effectiveFilters?.task_list_id) {
12056
- conditions.push("task_list_id = ?");
12057
- 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 });
12058
12233
  }
12059
- const where = conditions.join(" AND ");
12060
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
12061
- return rows.map(rowToTask);
12062
- }
12063
- function stealTask(agentId, opts, db) {
12064
- const d = db || getDatabase();
12065
- const databasePath = databasePathFromDatabase(d);
12066
- const staleMinutes = opts?.stale_minutes ?? 30;
12067
- const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
12068
- if (staleTasks.length === 0)
12069
- return null;
12070
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
12071
- staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
12072
- const target = staleTasks[0];
12073
- const timestamp = now();
12074
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
12075
- const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
12076
- 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]);
12077
- if (result.changes === 0)
12078
- return null;
12079
- logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
12080
- logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
12081
- const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
12082
- const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
12083
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
12084
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
12085
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
12086
- 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;
12087
12239
  }
12088
- function claimOrSteal(agentId, filters, db) {
12240
+ function deleteTask(id, db) {
12089
12241
  const d = db || getDatabase();
12090
- const tx = d.transaction(() => {
12091
- const next = getNextTask(agentId, filters, d);
12092
- if (next) {
12093
- const started = startTask(next.id, agentId, d);
12094
- return { task: started, stolen: false };
12095
- }
12096
- const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
12097
- if (stolen)
12098
- return { task: stolen, stolen: true };
12099
- return null;
12100
- });
12101
- return tx();
12102
- }
12103
- function spawnNextRecurrence(completedTask, db, completedAt) {
12104
- const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
12105
- const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
12106
- let title = completedTask.title;
12107
- if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
12108
- title = title.slice(completedTask.short_id.length + 2);
12109
- }
12110
- const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
12111
- return createTask({
12112
- title,
12113
- description: completedTask.description ?? undefined,
12114
- priority: completedTask.priority,
12115
- project_id: completedTask.project_id ?? undefined,
12116
- task_list_id: completedTask.task_list_id ?? undefined,
12117
- plan_id: completedTask.plan_id ?? undefined,
12118
- assigned_to: completedTask.assigned_to ?? undefined,
12119
- tags: completedTask.tags,
12120
- metadata: completedTask.metadata,
12121
- estimated_minutes: completedTask.estimated_minutes ?? undefined,
12122
- sla_minutes: completedTask.sla_minutes ?? undefined,
12123
- recurrence_rule: completedTask.recurrence_rule,
12124
- recurrence_parent_id: recurrenceParentId,
12125
- due_at: dueAt
12126
- }, 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;
12127
12253
  }
12128
- var MAX_SPAWN_DEPTH = 10;
12129
- var init_task_lifecycle = __esm(() => {
12254
+ var init_task_crud = __esm(() => {
12130
12255
  init_types();
12131
12256
  init_database();
12132
12257
  init_completion_guard();
@@ -12134,11 +12259,9 @@ var init_task_lifecycle = __esm(() => {
12134
12259
  init_event_hooks();
12135
12260
  init_shared_events();
12136
12261
  init_audit();
12137
- init_recurrence();
12138
12262
  init_webhooks();
12139
- init_templates();
12140
- init_task_crud();
12141
- init_task_graph();
12263
+ init_checklists();
12264
+ init_storage_tombstones();
12142
12265
  });
12143
12266
 
12144
12267
  // src/db/task-status.ts
@@ -12228,6 +12351,15 @@ function setTaskStatus(id, status, _agentId, db) {
12228
12351
  throw new TaskNotFoundError(id);
12229
12352
  if (task.status === status)
12230
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
+ }
12231
12363
  try {
12232
12364
  return updateTask(id, { status, version: task.version }, d);
12233
12365
  } catch (e) {
@@ -15613,10 +15745,8 @@ var init_token_utils = __esm(() => {
15613
15745
  "cancel_task",
15614
15746
  "check_task_done_contract",
15615
15747
  "claim_task",
15616
- "clone_task",
15617
15748
  "delete_task",
15618
15749
  "extend_task",
15619
- "get_active_work",
15620
15750
  "get_archived_tasks",
15621
15751
  "get_blocked_tasks",
15622
15752
  "get_blocking_tasks",
@@ -15652,7 +15782,8 @@ var init_token_utils = __esm(() => {
15652
15782
  "task_context",
15653
15783
  "unlock_task",
15654
15784
  "unarchive_task",
15655
- "update_task"
15785
+ "update_task",
15786
+ "upsert_task"
15656
15787
  ],
15657
15788
  projects: [
15658
15789
  "bootstrap_project",
@@ -15911,12 +16042,8 @@ var init_token_utils = __esm(() => {
15911
16042
  "delete_tag",
15912
16043
  "get_label",
15913
16044
  "get_activity_timeline",
15914
- "get_recent_activity",
15915
16045
  "get_tag",
15916
16046
  "get_task_fields",
15917
- "get_task_graph",
15918
- "get_task_history",
15919
- "get_task_stats",
15920
16047
  "list_workflow_states",
15921
16048
  "list_labels",
15922
16049
  "list_tags",
@@ -15927,6 +16054,10 @@ var init_token_utils = __esm(() => {
15927
16054
  "describe_tools",
15928
16055
  "set_task_workflow_state",
15929
16056
  "set_task_fields",
16057
+ "assign_label_to_task",
16058
+ "create_custom_field",
16059
+ "set_task_custom_field",
16060
+ "set_task_priority_meta",
15930
16061
  "update_label",
15931
16062
  "update_tag"
15932
16063
  ],
@@ -15952,7 +16083,6 @@ var init_token_utils = __esm(() => {
15952
16083
  "update_template",
15953
16084
  "write_template_library"
15954
16085
  ],
15955
- webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
15956
16086
  machines: [
15957
16087
  "machines_archive",
15958
16088
  "machines_delete",
@@ -38526,6 +38656,11 @@ function safeEqualHex(a, b) {
38526
38656
  return false;
38527
38657
  return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
38528
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
+ }
38529
38664
  function hasActiveApiKeys(db) {
38530
38665
  const d = db || getDatabase();
38531
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());
@@ -38698,6 +38833,37 @@ function parseBoundedLimit(value, fallback, max) {
38698
38833
  return fallback;
38699
38834
  return Math.min(parsed, max);
38700
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
+ }
38701
38867
  function handleSseEvents(_req, url, ctx) {
38702
38868
  const agentId = url.searchParams.get("agent_id") || undefined;
38703
38869
  const projectId = url.searchParams.get("project_id") || undefined;
@@ -38776,35 +38942,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
38776
38942
  });
38777
38943
  }
38778
38944
  function handleHealth(_ctx, json2) {
38779
- const all = listTasks({ limit: 1e4 });
38780
- const stale = all.filter((t) => t.status === "in_progress" && new Date(t.updated_at).getTime() < Date.now() - 30 * 60 * 1000);
38781
- const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < new Date().toISOString());
38782
- 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
+ });
38783
38955
  }
38784
38956
  function handleHeadlessBoundary(_ctx, json2) {
38785
38957
  const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
38786
38958
  return json2(getHeadlessBoundaryManifest2());
38787
38959
  }
38788
38960
  function handleStats(_ctx, json2) {
38789
- const all = listTasks({ limit: 1e4 });
38961
+ const stats2 = getTaskStats();
38962
+ const byStatus = stats2.by_status;
38790
38963
  const projects = listProjects();
38791
38964
  const agents = listAgents();
38792
- const staleItems = getStaleTasks(30);
38793
- const nowStr = new Date().toISOString();
38794
- const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
38795
- 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;
38796
38967
  return json2({
38797
- total_tasks: all.length,
38798
- pending: all.filter((t) => t.status === "pending").length,
38799
- in_progress: all.filter((t) => t.status === "in_progress").length,
38800
- completed: all.filter((t) => t.status === "completed").length,
38801
- failed: all.filter((t) => t.status === "failed").length,
38802
- 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,
38803
38974
  projects: projects.length,
38804
38975
  agents: agents.length,
38805
- stale_count: staleItems.length,
38976
+ stale_count: staleCount,
38806
38977
  overdue_recurring: overdueRecurring,
38807
- recurring_tasks: recurringTasks
38978
+ recurring_tasks: countRecurringTasks()
38808
38979
  });
38809
38980
  }
38810
38981
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
@@ -38883,27 +39054,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
38883
39054
  const summaries = tasks.map((t) => taskToSummary2(t));
38884
39055
  if (format === "csv") {
38885
39056
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
38886
- const rows = summaries.map((t) => headers.map((h) => {
38887
- const val = t[h];
39057
+ const csvCell = (val) => {
38888
39058
  if (val === null || val === undefined)
38889
39059
  return "";
38890
- const str = String(val);
38891
- return str.includes(",") || str.includes('"') || str.includes(`
38892
- `) ? `"${str.replace(/"/g, '""')}"` : str;
38893
- }).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(","));
38894
39070
  const csv = [headers.join(","), ...rows].join(`
38895
39071
  `);
38896
39072
  return new Response(csv, {
38897
39073
  headers: {
38898
39074
  "Content-Type": "text/csv",
38899
- "Content-Disposition": "attachment; filename=tasks.csv"
39075
+ "Content-Disposition": "attachment; filename=tasks.csv",
39076
+ ...SECURITY_HEADERS
38900
39077
  }
38901
39078
  });
38902
39079
  }
38903
39080
  return new Response(JSON.stringify(summaries, null, 2), {
38904
39081
  headers: {
38905
39082
  "Content-Type": "application/json",
38906
- "Content-Disposition": "attachment; filename=tasks.json"
39083
+ "Content-Disposition": "attachment; filename=tasks.json",
39084
+ ...SECURITY_HEADERS
38907
39085
  }
38908
39086
  });
38909
39087
  }
@@ -39077,12 +39255,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
39077
39255
  if (ALLOWED.has(key))
39078
39256
  safeBody[key] = value;
39079
39257
  }
39258
+ const clientVersion = typeof body["version"] === "number" ? body["version"] : task2.version;
39080
39259
  const updated = updateTask(id, {
39081
39260
  ...safeBody,
39082
- version: task2.version
39261
+ version: clientVersion
39083
39262
  });
39084
39263
  return json2(taskToSummary2(updated));
39085
39264
  } catch (e) {
39265
+ const mapped = mapTaskError(e, json2);
39266
+ if (mapped)
39267
+ return mapped;
39086
39268
  return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
39087
39269
  }
39088
39270
  }
@@ -39098,6 +39280,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
39098
39280
  ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "started", agent_id: "dashboard", project_id: task2.project_id });
39099
39281
  return json2(taskToSummary2(task2));
39100
39282
  } catch (e) {
39283
+ const mapped = mapTaskError(e, json2);
39284
+ if (mapped)
39285
+ return mapped;
39101
39286
  return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
39102
39287
  }
39103
39288
  }
@@ -39117,6 +39302,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
39117
39302
  ctx.broadcastEvent({ type: "task", task_id: task2.id, action: "completed", agent_id: "dashboard", project_id: task2.project_id });
39118
39303
  return json2(taskToSummary2(task2));
39119
39304
  } catch (e) {
39305
+ const mapped = mapTaskError(e, json2);
39306
+ if (mapped)
39307
+ return mapped;
39120
39308
  return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
39121
39309
  }
39122
39310
  }
@@ -39441,6 +39629,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
39441
39629
  }
39442
39630
  var init_routes = __esm(() => {
39443
39631
  init_tasks();
39632
+ init_database();
39633
+ init_types();
39444
39634
  init_projects();
39445
39635
  init_agents();
39446
39636
  init_plans();
@@ -39498,7 +39688,7 @@ function checkAuth(req, apiKey) {
39498
39688
  if (!apiKey && !generatedKeysEnabled)
39499
39689
  return null;
39500
39690
  const provided = getProvidedApiKey(req);
39501
- const matchesEnvKey = Boolean(apiKey && provided && provided === apiKey);
39691
+ const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
39502
39692
  const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
39503
39693
  if (!matchesEnvKey && !matchesGeneratedKey) {
39504
39694
  return new Response(JSON.stringify({ error: "Unauthorized" }), {
@@ -39508,6 +39698,15 @@ function checkAuth(req, apiKey) {
39508
39698
  }
39509
39699
  return null;
39510
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
+ }
39511
39710
  function checkRateLimit(ip) {
39512
39711
  const now4 = Date.now();
39513
39712
  const entry = rateLimitMap.get(ip);
@@ -39635,7 +39834,7 @@ Dashboard not found at: ${dashboardDir}`);
39635
39834
  const server = Bun.serve({
39636
39835
  port,
39637
39836
  hostname: hostname3,
39638
- async fetch(req) {
39837
+ async fetch(req, server2) {
39639
39838
  const url = new URL(req.url);
39640
39839
  const path = url.pathname;
39641
39840
  const method = req.method;
@@ -39647,15 +39846,6 @@ Dashboard not found at: ${dashboardDir}`);
39647
39846
  Vary: "Origin"
39648
39847
  } : undefined;
39649
39848
  const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
39650
- if (path === "/health" && method === "GET") {
39651
- const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
39652
- return healthResponse2("todos");
39653
- }
39654
- if (path === "/mcp") {
39655
- const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
39656
- const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
39657
- return handleMcpHttpRequest2(req, buildServer2);
39658
- }
39659
39849
  if (method === "OPTIONS") {
39660
39850
  return new Response(null, {
39661
39851
  headers: corsHeaders || {
@@ -39663,7 +39853,7 @@ Dashboard not found at: ${dashboardDir}`);
39663
39853
  }
39664
39854
  });
39665
39855
  }
39666
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
39856
+ const ip = resolveClientIp(req, server2);
39667
39857
  const rl = checkRateLimit(ip);
39668
39858
  if (!rl.allowed) {
39669
39859
  return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
@@ -39671,6 +39861,18 @@ Dashboard not found at: ${dashboardDir}`);
39671
39861
  headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
39672
39862
  });
39673
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
+ }
39674
39876
  if (path.startsWith("/api/")) {
39675
39877
  const authError = checkAuth(req, apiKey);
39676
39878
  if (authError)
@@ -39950,14 +40152,16 @@ function printHelp() {
39950
40152
  Start the @hasna/todos MCP server.
39951
40153
 
39952
40154
  Options:
39953
- --stdio Use stdio transport
39954
- --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)
39955
40158
  -V, --version output the version number
39956
40159
  -h, --help display help for command
39957
40160
 
39958
40161
  Environment:
39959
- TODOS_MCP_STDIO=true Force stdio transport
39960
- 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
39961
40165
  TODOS_PROFILE=<profile> Tool profile filter
39962
40166
  TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
39963
40167
  }
@@ -40046,8 +40250,22 @@ function formatError(error) {
40046
40250
  function resolveId(partialId, table = "tasks") {
40047
40251
  const db = getDatabase();
40048
40252
  const id = resolvePartialId(db, table, partialId);
40049
- if (!id)
40050
- 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
+ }
40051
40269
  return id;
40052
40270
  }
40053
40271
  function formatTask(task2) {
@@ -40136,8 +40354,9 @@ function buildServer() {
40136
40354
  return server;
40137
40355
  }
40138
40356
  async function main() {
40139
- const { isStdioMode: isStdioMode2, resolveHttpPort: resolveHttpPort2 } = await Promise.resolve().then(() => (init_http(), exports_http));
40140
- 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) {
40141
40360
  const server = buildServer();
40142
40361
  const transport = new StdioServerTransport;
40143
40362
  await server.connect(transport);