@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
@@ -1856,6 +1856,11 @@ function ensureSchema(db) {
1856
1856
  ensureColumn("tasks", "priority_score", "INTEGER");
1857
1857
  ensureColumn("tasks", "priority_reason", "TEXT");
1858
1858
  ensureColumn("tasks", "archived_at", "TEXT");
1859
+ ensureColumn("tasks", "runner_id", "TEXT");
1860
+ ensureColumn("tasks", "runner_started_at", "TEXT");
1861
+ ensureColumn("tasks", "runner_completed_at", "TEXT");
1862
+ ensureColumn("tasks", "current_step", "TEXT");
1863
+ ensureColumn("tasks", "total_steps", "INTEGER");
1859
1864
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
1860
1865
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
1861
1866
  ensureColumn("agents", "reports_to", "TEXT");
@@ -1863,6 +1868,10 @@ function ensureSchema(db) {
1863
1868
  ensureColumn("agents", "level", "TEXT");
1864
1869
  ensureColumn("agents", "org_id", "TEXT");
1865
1870
  ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
1871
+ ensureColumn("agents", "session_id", "TEXT");
1872
+ ensureColumn("agents", "working_dir", "TEXT");
1873
+ ensureColumn("agents", "active_project_id", "TEXT");
1874
+ ensureColumn("agents", "status", "TEXT NOT NULL DEFAULT 'active'");
1866
1875
  ensureColumn("projects", "org_id", "TEXT");
1867
1876
  ensureColumn("plans", "slug", "TEXT");
1868
1877
  ensureColumn("plans", "task_list_id", "TEXT");
@@ -2780,34 +2789,40 @@ function ensureDir(filePath) {
2780
2789
  mkdirSync(dir, { recursive: true });
2781
2790
  }
2782
2791
  }
2792
+ function openDatabase(path) {
2793
+ ensureDir(path);
2794
+ const db = new Database(path);
2795
+ db.run("PRAGMA journal_mode = WAL");
2796
+ db.run("PRAGMA busy_timeout = 5000");
2797
+ db.run("PRAGMA foreign_keys = ON");
2798
+ runMigrations(db);
2799
+ backfillTaskTags(db);
2800
+ backfillMachineId(db);
2801
+ return db;
2802
+ }
2783
2803
  function getDatabase(dbPath) {
2784
2804
  const path = dbPath || getDbPath();
2785
2805
  if (_db && _dbPath === path)
2786
2806
  return _db;
2787
- if (_db && _dbPath !== path) {
2788
- _db.close();
2789
- _db = null;
2790
- _dbPath = null;
2791
- }
2792
- ensureDir(path);
2793
- _db = new Database(path);
2807
+ _db = openDatabase(path);
2794
2808
  _dbPath = path;
2795
- _db.run("PRAGMA journal_mode = WAL");
2796
- _db.run("PRAGMA busy_timeout = 5000");
2797
- _db.run("PRAGMA foreign_keys = ON");
2798
- runMigrations(_db);
2799
- backfillTaskTags(_db);
2800
- backfillMachineId(_db);
2801
2809
  return _db;
2802
2810
  }
2803
2811
  function closeDatabase() {
2804
2812
  if (_db) {
2805
- _db.close();
2813
+ try {
2814
+ _db.close();
2815
+ } catch {}
2806
2816
  _db = null;
2807
2817
  _dbPath = null;
2808
2818
  }
2809
2819
  }
2810
2820
  function resetDatabase() {
2821
+ if (_db) {
2822
+ try {
2823
+ _db.close();
2824
+ } catch {}
2825
+ }
2811
2826
  _db = null;
2812
2827
  _dbPath = null;
2813
2828
  }
@@ -2912,6 +2927,11 @@ function safeEqualHex(a, b) {
2912
2927
  return false;
2913
2928
  return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
2914
2929
  }
2930
+ function safeEqualStrings(a, b) {
2931
+ const ah = createHash("sha256").update(a, "utf8").digest();
2932
+ const bh = createHash("sha256").update(b, "utf8").digest();
2933
+ return timingSafeEqual(ah, bh);
2934
+ }
2915
2935
  function hasActiveApiKeys(db) {
2916
2936
  const d = db || getDatabase();
2917
2937
  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());
@@ -5060,8 +5080,6 @@ function routeEnabledForTask(task, taskList) {
5060
5080
  const explicit = booleanField(task.metadata.route_enabled);
5061
5081
  if (explicit !== undefined)
5062
5082
  return explicit;
5063
- if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
5064
- return true;
5065
5083
  const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
5066
5084
  if (taskListDefault !== undefined)
5067
5085
  return taskListDefault;
@@ -5080,8 +5098,26 @@ function workflowPointersFromMetadata(metadata) {
5080
5098
  workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
5081
5099
  };
5082
5100
  }
5083
- function classifyProjectKind(path) {
5084
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
5101
+ function metadataStringField(record, keys) {
5102
+ if (!record)
5103
+ return;
5104
+ for (const key of keys) {
5105
+ const value = record[key];
5106
+ if (typeof value === "string" && value.trim())
5107
+ return value.trim();
5108
+ }
5109
+ return;
5110
+ }
5111
+ function projectKindFromMetadata(...records) {
5112
+ for (const record of records) {
5113
+ const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
5114
+ if (value)
5115
+ return value;
5116
+ }
5117
+ return null;
5118
+ }
5119
+ function classifyProjectKind(_path, metadata) {
5120
+ return projectKindFromMetadata(metadata);
5085
5121
  }
5086
5122
  function isWorktreePath(path) {
5087
5123
  return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
@@ -5154,7 +5190,6 @@ function taskEventMetadata(task) {
5154
5190
  metadata.project_canonical_path = projectPath;
5155
5191
  }
5156
5192
  if (projectPath) {
5157
- metadata.project_kind = classifyProjectKind(projectPath);
5158
5193
  metadata.project_is_worktree = isWorktreePath(projectPath);
5159
5194
  metadata.working_dir = task.working_dir ?? projectPath;
5160
5195
  }
@@ -5166,6 +5201,10 @@ function taskEventMetadata(task) {
5166
5201
  metadata.task_list_project_id = taskList.project_id;
5167
5202
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
5168
5203
  }
5204
+ const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
5205
+ if (projectKind) {
5206
+ metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
5207
+ }
5169
5208
  const routeEnabled = routeEnabledForTask(task, taskList);
5170
5209
  if (routeEnabled !== undefined) {
5171
5210
  metadata.route_enabled = routeEnabled;
@@ -5720,1767 +5759,1858 @@ var init_checklists = __esm(() => {
5720
5759
  init_database();
5721
5760
  });
5722
5761
 
5723
- // src/db/task-crud.ts
5724
- function rowToTask(row) {
5762
+ // src/lib/recurrence.ts
5763
+ function parseRecurrenceRule(rule) {
5764
+ const normalized = rule.trim().toLowerCase();
5765
+ if (normalized === "every weekday" || normalized === "every weekdays") {
5766
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
5767
+ }
5768
+ if (normalized === "every day" || normalized === "daily") {
5769
+ return { type: "interval", interval: 1, unit: "day" };
5770
+ }
5771
+ if (normalized === "every week" || normalized === "weekly") {
5772
+ return { type: "interval", interval: 1, unit: "week" };
5773
+ }
5774
+ if (normalized === "every month" || normalized === "monthly") {
5775
+ return { type: "interval", interval: 1, unit: "month" };
5776
+ }
5777
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
5778
+ if (intervalMatch) {
5779
+ return {
5780
+ type: "interval",
5781
+ interval: parseInt(intervalMatch[1], 10),
5782
+ unit: intervalMatch[2]
5783
+ };
5784
+ }
5785
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
5786
+ if (daysMatch) {
5787
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
5788
+ const days = [];
5789
+ for (const part of dayParts) {
5790
+ const dayNum = DAY_NAMES[part];
5791
+ if (dayNum !== undefined) {
5792
+ days.push(dayNum);
5793
+ }
5794
+ }
5795
+ if (days.length > 0) {
5796
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
5797
+ }
5798
+ }
5799
+ 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"`);
5800
+ }
5801
+ function isValidRecurrenceRule(rule) {
5802
+ try {
5803
+ parseRecurrenceRule(rule);
5804
+ return true;
5805
+ } catch {
5806
+ return false;
5807
+ }
5808
+ }
5809
+ function nextOccurrence(rule, from) {
5810
+ const parsed = parseRecurrenceRule(rule);
5811
+ const base = from || new Date;
5812
+ if (parsed.type === "interval") {
5813
+ const next = new Date(base);
5814
+ if (parsed.unit === "day") {
5815
+ next.setDate(next.getDate() + parsed.interval);
5816
+ } else if (parsed.unit === "week") {
5817
+ next.setDate(next.getDate() + parsed.interval * 7);
5818
+ } else if (parsed.unit === "month") {
5819
+ next.setMonth(next.getMonth() + parsed.interval);
5820
+ }
5821
+ return next.toISOString();
5822
+ }
5823
+ if (parsed.type === "specific_days") {
5824
+ const currentDay = base.getDay();
5825
+ const days = parsed.days;
5826
+ let daysToAdd = Infinity;
5827
+ for (const day of days) {
5828
+ let diff = day - currentDay;
5829
+ if (diff <= 0)
5830
+ diff += 7;
5831
+ if (diff < daysToAdd)
5832
+ daysToAdd = diff;
5833
+ }
5834
+ const next = new Date(base);
5835
+ next.setDate(next.getDate() + daysToAdd);
5836
+ return next.toISOString();
5837
+ }
5838
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
5839
+ }
5840
+ var DAY_NAMES;
5841
+ var init_recurrence = __esm(() => {
5842
+ DAY_NAMES = {
5843
+ sunday: 0,
5844
+ sun: 0,
5845
+ monday: 1,
5846
+ mon: 1,
5847
+ tuesday: 2,
5848
+ tue: 2,
5849
+ wednesday: 3,
5850
+ wed: 3,
5851
+ thursday: 4,
5852
+ thu: 4,
5853
+ friday: 5,
5854
+ fri: 5,
5855
+ saturday: 6,
5856
+ sat: 6
5857
+ };
5858
+ });
5859
+
5860
+ // src/db/templates.ts
5861
+ var exports_templates = {};
5862
+ __export(exports_templates, {
5863
+ updateTemplate: () => updateTemplate,
5864
+ tasksFromTemplate: () => tasksFromTemplate,
5865
+ taskFromTemplate: () => taskFromTemplate,
5866
+ resolveVariables: () => resolveVariables,
5867
+ previewTemplate: () => previewTemplate,
5868
+ listTemplates: () => listTemplates,
5869
+ listTemplateVersions: () => listTemplateVersions,
5870
+ importTemplate: () => importTemplate,
5871
+ getTemplateWithTasks: () => getTemplateWithTasks,
5872
+ getTemplateVersion: () => getTemplateVersion,
5873
+ getTemplateTasks: () => getTemplateTasks,
5874
+ getTemplate: () => getTemplate,
5875
+ exportTemplate: () => exportTemplate,
5876
+ evaluateCondition: () => evaluateCondition,
5877
+ deleteTemplate: () => deleteTemplate,
5878
+ createTemplate: () => createTemplate,
5879
+ addTemplateTasks: () => addTemplateTasks
5880
+ });
5881
+ function rowToTemplate(row) {
5725
5882
  return {
5726
5883
  ...row,
5727
5884
  tags: JSON.parse(row.tags || "[]"),
5885
+ variables: JSON.parse(row.variables || "[]"),
5728
5886
  metadata: JSON.parse(row.metadata || "{}"),
5729
- status: row.status,
5730
- priority: row.priority,
5731
- requires_approval: !!row.requires_approval
5887
+ priority: row.priority || "medium",
5888
+ version: row.version ?? 1
5732
5889
  };
5733
5890
  }
5734
- function insertTaskTags(taskId, tags, db) {
5735
- if (tags.length === 0)
5736
- return;
5737
- const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
5738
- for (const tag of tags) {
5739
- if (tag)
5740
- stmt.run(taskId, tag);
5741
- }
5742
- }
5743
- function replaceTaskTags(taskId, tags, db) {
5744
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
5745
- insertTaskTags(taskId, tags, db);
5891
+ function rowToTemplateTask(row) {
5892
+ return {
5893
+ ...row,
5894
+ tags: JSON.parse(row.tags || "[]"),
5895
+ depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
5896
+ metadata: JSON.parse(row.metadata || "{}"),
5897
+ priority: row.priority || "medium",
5898
+ condition: row.condition ?? null,
5899
+ include_template_id: row.include_template_id ?? null
5900
+ };
5746
5901
  }
5747
- function addMetadataConditions(metadata, conditions, params) {
5748
- if (!metadata)
5749
- return;
5750
- for (const [key, value] of Object.entries(metadata)) {
5751
- if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
5752
- throw new Error(`Invalid metadata filter key: ${key}`);
5753
- }
5754
- conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
5755
- params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
5756
- }
5902
+ function resolveTemplateId(id, d) {
5903
+ return resolvePartialId(d, "task_templates", id);
5757
5904
  }
5758
- function createTask(input, db) {
5905
+ function createTemplate(input, db) {
5759
5906
  const d = db || getDatabase();
5760
- const timestamp = now();
5761
- const tags = input.tags || [];
5907
+ const id = uuid();
5762
5908
  const machineId = currentStorageMachineId(d);
5763
- const assignedBy = input.assigned_by || input.agent_id;
5764
- const assignedFromProject = input.assigned_from_project || null;
5765
- let id = uuid();
5766
- for (let attempt = 0;attempt < 3; attempt++) {
5767
- try {
5768
- 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)
5769
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5770
- id,
5771
- null,
5772
- input.project_id || null,
5773
- input.parent_id || null,
5774
- input.plan_id || null,
5775
- input.task_list_id || null,
5776
- input.cycle_id || null,
5777
- input.title,
5778
- input.description || null,
5779
- input.status || "pending",
5780
- input.priority || "medium",
5781
- input.agent_id || null,
5782
- input.assigned_to || null,
5783
- input.session_id || null,
5784
- input.working_dir || null,
5785
- JSON.stringify(tags),
5786
- JSON.stringify(input.metadata || {}),
5787
- timestamp,
5788
- timestamp,
5789
- input.due_at || null,
5790
- input.estimated_minutes || null,
5791
- input.sla_minutes ?? null,
5792
- input.confidence ?? null,
5793
- input.retry_count ?? 0,
5794
- input.max_retries ?? 3,
5795
- input.retry_after ?? null,
5796
- input.requires_approval ? 1 : 0,
5797
- null,
5798
- null,
5799
- input.recurrence_rule || null,
5800
- input.recurrence_parent_id || null,
5801
- input.spawns_template_id || null,
5802
- input.reason || null,
5803
- input.spawned_from_session || null,
5804
- assignedBy || null,
5805
- assignedFromProject || null,
5806
- input.task_type || null,
5807
- machineId
5808
- ]);
5809
- break;
5810
- } catch (e) {
5811
- if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
5812
- id = uuid();
5813
- continue;
5814
- }
5815
- throw e;
5816
- }
5817
- }
5818
- if (tags.length > 0) {
5819
- insertTaskTags(id, tags, d);
5909
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
5910
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5911
+ id,
5912
+ input.name,
5913
+ input.title_pattern,
5914
+ input.description || null,
5915
+ input.priority || "medium",
5916
+ JSON.stringify(input.tags || []),
5917
+ JSON.stringify(input.variables || []),
5918
+ input.project_id || null,
5919
+ input.plan_id || null,
5920
+ JSON.stringify(input.metadata || {}),
5921
+ now(),
5922
+ machineId
5923
+ ]);
5924
+ if (input.tasks && input.tasks.length > 0) {
5925
+ addTemplateTasks(id, input.tasks, d);
5820
5926
  }
5821
- const task = getTask(id, d);
5822
- const payload = taskEventData(task);
5823
- const databasePath = databasePathFromDatabase(d);
5824
- dispatchWebhook2("task.created", payload, d).catch(() => {});
5825
- emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
5826
- emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
5827
- return task;
5927
+ return getTemplate(id, d);
5828
5928
  }
5829
- function getTask(id, db) {
5929
+ function getTemplate(id, db) {
5830
5930
  const d = db || getDatabase();
5831
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
5832
- if (!row)
5931
+ const resolved = resolveTemplateId(id, d);
5932
+ if (!resolved)
5833
5933
  return null;
5834
- return rowToTask(row);
5934
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
5935
+ return row ? rowToTemplate(row) : null;
5835
5936
  }
5836
- function getTaskWithRelations(id, db) {
5937
+ function listTemplates(db) {
5837
5938
  const d = db || getDatabase();
5838
- const task = getTask(id, d);
5839
- if (!task)
5840
- return null;
5841
- const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
5842
- const subtasks = subtaskRows.map(rowToTask);
5843
- const depRows = d.query(`SELECT t.* FROM tasks t
5844
- JOIN task_dependencies td ON td.depends_on = t.id
5845
- WHERE td.task_id = ?`).all(id);
5846
- const dependencies = depRows.map(rowToTask);
5847
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
5848
- JOIN task_dependencies td ON td.task_id = t.id
5849
- WHERE td.depends_on = ?`).all(id);
5850
- const blocked_by = blockedByRows.map(rowToTask);
5851
- const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
5852
- const parent = task.parent_id ? getTask(task.parent_id, d) : null;
5853
- const checklist = getChecklist(id, d);
5854
- return {
5855
- ...task,
5856
- subtasks,
5857
- dependencies,
5858
- blocked_by,
5859
- comments,
5860
- parent,
5861
- checklist
5862
- };
5939
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
5863
5940
  }
5864
- function listTasks(filter = {}, db) {
5941
+ function deleteTemplate(id, db) {
5865
5942
  const d = db || getDatabase();
5866
- const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
5867
- clearExpiredLocks2(d);
5868
- const conditions = [];
5869
- const params = [];
5870
- if (filter.project_id) {
5871
- conditions.push("project_id = ?");
5872
- params.push(filter.project_id);
5873
- }
5874
- if (filter.ids && filter.ids.length > 0) {
5875
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
5876
- params.push(...filter.ids);
5877
- }
5878
- if (filter.parent_id !== undefined) {
5879
- if (filter.parent_id === null) {
5880
- conditions.push("parent_id IS NULL");
5881
- } else {
5882
- conditions.push("parent_id = ?");
5883
- params.push(filter.parent_id);
5884
- }
5885
- }
5886
- if (filter.status) {
5887
- if (Array.isArray(filter.status)) {
5888
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
5889
- params.push(...filter.status);
5890
- } else {
5891
- conditions.push("status = ?");
5892
- params.push(filter.status);
5893
- }
5894
- }
5895
- if (filter.priority) {
5896
- if (Array.isArray(filter.priority)) {
5897
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
5898
- params.push(...filter.priority);
5899
- } else {
5900
- conditions.push("priority = ?");
5901
- params.push(filter.priority);
5902
- }
5903
- }
5904
- if (filter.assigned_to) {
5905
- conditions.push("assigned_to = ?");
5906
- params.push(filter.assigned_to);
5907
- }
5908
- if (filter.agent_id) {
5909
- conditions.push("agent_id = ?");
5910
- params.push(filter.agent_id);
5943
+ const resolved = resolveTemplateId(id, d);
5944
+ if (!resolved)
5945
+ return false;
5946
+ const template = getTemplate(resolved, d);
5947
+ if (!template)
5948
+ return false;
5949
+ recordStorageTombstone({
5950
+ object_type: "templates",
5951
+ object_id: resolved,
5952
+ payload: template,
5953
+ version: template.version
5954
+ }, d);
5955
+ return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
5956
+ }
5957
+ function updateTemplate(id, updates, db) {
5958
+ const d = db || getDatabase();
5959
+ const resolved = resolveTemplateId(id, d);
5960
+ if (!resolved)
5961
+ return null;
5962
+ const current = getTemplateWithTasks(resolved, d);
5963
+ if (current) {
5964
+ const snapshot = JSON.stringify({
5965
+ name: current.name,
5966
+ title_pattern: current.title_pattern,
5967
+ description: current.description,
5968
+ priority: current.priority,
5969
+ tags: current.tags,
5970
+ variables: current.variables,
5971
+ project_id: current.project_id,
5972
+ plan_id: current.plan_id,
5973
+ metadata: current.metadata,
5974
+ tasks: current.tasks
5975
+ });
5976
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
5911
5977
  }
5912
- if (filter.session_id) {
5913
- conditions.push("session_id = ?");
5914
- params.push(filter.session_id);
5978
+ const sets = ["version = version + 1"];
5979
+ const values = [];
5980
+ if (updates.name !== undefined) {
5981
+ sets.push("name = ?");
5982
+ values.push(updates.name);
5915
5983
  }
5916
- if (filter.tags && filter.tags.length > 0) {
5917
- const placeholders = filter.tags.map(() => "?").join(",");
5918
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
5919
- params.push(...filter.tags);
5984
+ if (updates.title_pattern !== undefined) {
5985
+ sets.push("title_pattern = ?");
5986
+ values.push(updates.title_pattern);
5920
5987
  }
5921
- if (filter.plan_id) {
5922
- conditions.push("plan_id = ?");
5923
- params.push(filter.plan_id);
5988
+ if (updates.description !== undefined) {
5989
+ sets.push("description = ?");
5990
+ values.push(updates.description);
5924
5991
  }
5925
- if (filter.task_list_id) {
5926
- conditions.push("task_list_id = ?");
5927
- params.push(filter.task_list_id);
5992
+ if (updates.priority !== undefined) {
5993
+ sets.push("priority = ?");
5994
+ values.push(updates.priority);
5928
5995
  }
5929
- if (filter.has_recurrence === true) {
5930
- conditions.push("recurrence_rule IS NOT NULL");
5931
- } else if (filter.has_recurrence === false) {
5932
- conditions.push("recurrence_rule IS NULL");
5996
+ if (updates.tags !== undefined) {
5997
+ sets.push("tags = ?");
5998
+ values.push(JSON.stringify(updates.tags));
5933
5999
  }
5934
- if (filter.task_type) {
5935
- if (Array.isArray(filter.task_type)) {
5936
- conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
5937
- params.push(...filter.task_type);
5938
- } else {
5939
- conditions.push("task_type = ?");
5940
- params.push(filter.task_type);
5941
- }
6000
+ if (updates.variables !== undefined) {
6001
+ sets.push("variables = ?");
6002
+ values.push(JSON.stringify(updates.variables));
5942
6003
  }
5943
- addMetadataConditions(filter.metadata, conditions, params);
5944
- const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
5945
- if (filter.cursor) {
5946
- try {
5947
- const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
5948
- conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
5949
- params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
5950
- } catch {}
6004
+ if (updates.project_id !== undefined) {
6005
+ sets.push("project_id = ?");
6006
+ values.push(updates.project_id);
5951
6007
  }
5952
- if (!filter.include_archived) {
5953
- conditions.push("archived_at IS NULL");
6008
+ if (updates.plan_id !== undefined) {
6009
+ sets.push("plan_id = ?");
6010
+ values.push(updates.plan_id);
5954
6011
  }
5955
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5956
- let limitClause = "";
5957
- if (filter.limit) {
5958
- limitClause = " LIMIT ?";
5959
- params.push(filter.limit);
5960
- if (!filter.cursor && filter.offset) {
5961
- limitClause += " OFFSET ?";
5962
- params.push(filter.offset);
5963
- }
6012
+ if (updates.metadata !== undefined) {
6013
+ sets.push("metadata = ?");
6014
+ values.push(JSON.stringify(updates.metadata));
5964
6015
  }
5965
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
5966
- return rows.map(rowToTask);
5967
- }
5968
- function getTaskByFingerprint(fingerprint, db) {
5969
- const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
5970
- return tasks[0] ?? null;
6016
+ values.push(resolved);
6017
+ d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
6018
+ return getTemplate(resolved, d);
5971
6019
  }
5972
- function mergeTaskMetadata(current, next, fingerprint) {
6020
+ function taskFromTemplate(templateId, overrides = {}, db) {
6021
+ const t = getTemplate(templateId, db);
6022
+ if (!t)
6023
+ throw new Error(`Template not found: ${templateId}`);
6024
+ const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
5973
6025
  return {
5974
- ...current,
5975
- ...next ?? {},
5976
- fingerprint
6026
+ title: cleanOverrides.title || t.title_pattern,
6027
+ description: cleanOverrides.description ?? t.description ?? undefined,
6028
+ priority: cleanOverrides.priority ?? t.priority,
6029
+ tags: cleanOverrides.tags ?? t.tags,
6030
+ project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
6031
+ plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
6032
+ metadata: cleanOverrides.metadata ?? t.metadata,
6033
+ ...cleanOverrides
5977
6034
  };
5978
6035
  }
5979
- function upsertTaskByFingerprint(input, db) {
5980
- const d = db || getDatabase();
5981
- const fingerprint = input.fingerprint.trim();
5982
- if (!fingerprint)
5983
- throw new Error("fingerprint is required");
5984
- const existing = getTaskByFingerprint(fingerprint, d);
5985
- const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
5986
- if (!existing) {
5987
- const task2 = createTask({ ...input, metadata }, d);
5988
- return { task: task2, created: true };
5989
- }
5990
- const task = updateTask(existing.id, {
5991
- version: existing.version,
5992
- title: input.title,
5993
- description: input.description,
5994
- status: input.status,
5995
- priority: input.priority,
5996
- project_id: input.project_id,
5997
- assigned_to: input.assigned_to,
5998
- working_dir: input.working_dir,
5999
- plan_id: input.plan_id,
6000
- task_list_id: input.task_list_id,
6001
- tags: input.tags,
6002
- metadata,
6003
- due_at: input.due_at,
6004
- estimated_minutes: input.estimated_minutes,
6005
- sla_minutes: input.sla_minutes,
6006
- confidence: input.confidence,
6007
- retry_count: input.retry_count,
6008
- max_retries: input.max_retries,
6009
- retry_after: input.retry_after,
6010
- requires_approval: input.requires_approval,
6011
- recurrence_rule: input.recurrence_rule,
6012
- task_type: input.task_type
6013
- }, d);
6014
- return { task, created: false };
6015
- }
6016
- function countTasks(filter = {}, db) {
6036
+ function addTemplateTasks(templateId, tasks, db) {
6017
6037
  const d = db || getDatabase();
6018
- const conditions = [];
6019
- const params = [];
6020
- if (filter.project_id) {
6021
- conditions.push("project_id = ?");
6022
- params.push(filter.project_id);
6023
- }
6024
- if (filter.ids && filter.ids.length > 0) {
6025
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
6026
- params.push(...filter.ids);
6027
- }
6028
- if (filter.parent_id !== undefined) {
6029
- if (filter.parent_id === null) {
6030
- conditions.push("parent_id IS NULL");
6031
- } else {
6032
- conditions.push("parent_id = ?");
6033
- params.push(filter.parent_id);
6034
- }
6035
- }
6036
- if (filter.status) {
6037
- if (Array.isArray(filter.status)) {
6038
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
6039
- params.push(...filter.status);
6040
- } else {
6041
- conditions.push("status = ?");
6042
- params.push(filter.status);
6043
- }
6044
- }
6045
- if (filter.priority) {
6046
- if (Array.isArray(filter.priority)) {
6047
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
6048
- params.push(...filter.priority);
6049
- } else {
6050
- conditions.push("priority = ?");
6051
- params.push(filter.priority);
6052
- }
6053
- }
6054
- if (filter.assigned_to) {
6055
- conditions.push("assigned_to = ?");
6056
- params.push(filter.assigned_to);
6057
- }
6058
- if (filter.agent_id) {
6059
- conditions.push("agent_id = ?");
6060
- params.push(filter.agent_id);
6061
- }
6062
- if (filter.session_id) {
6063
- conditions.push("session_id = ?");
6064
- params.push(filter.session_id);
6065
- }
6066
- if (filter.tags && filter.tags.length > 0) {
6067
- const placeholders = filter.tags.map(() => "?").join(",");
6068
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
6069
- params.push(...filter.tags);
6070
- }
6071
- if (filter.plan_id) {
6072
- conditions.push("plan_id = ?");
6073
- params.push(filter.plan_id);
6074
- }
6075
- if (filter.task_list_id) {
6076
- conditions.push("task_list_id = ?");
6077
- params.push(filter.task_list_id);
6078
- }
6079
- addMetadataConditions(filter.metadata, conditions, params);
6080
- if (!filter.include_archived) {
6081
- conditions.push("archived_at IS NULL");
6038
+ const template = getTemplate(templateId, d);
6039
+ if (!template)
6040
+ throw new Error(`Template not found: ${templateId}`);
6041
+ d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
6042
+ const results = [];
6043
+ for (let i = 0;i < tasks.length; i++) {
6044
+ const task = tasks[i];
6045
+ const id = uuid();
6046
+ 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)
6047
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6048
+ id,
6049
+ templateId,
6050
+ i,
6051
+ task.title_pattern,
6052
+ task.description || null,
6053
+ task.priority || "medium",
6054
+ JSON.stringify(task.tags || []),
6055
+ task.task_type || null,
6056
+ task.condition || null,
6057
+ task.include_template_id || null,
6058
+ JSON.stringify(task.depends_on || []),
6059
+ JSON.stringify(task.metadata || {}),
6060
+ now()
6061
+ ]);
6062
+ const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
6063
+ if (row)
6064
+ results.push(rowToTemplateTask(row));
6082
6065
  }
6083
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
6084
- const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
6085
- return row.count;
6066
+ return results;
6086
6067
  }
6087
- function updateTask(id, input, db) {
6068
+ function getTemplateWithTasks(id, db) {
6088
6069
  const d = db || getDatabase();
6089
- const task = getTask(id, d);
6090
- if (!task)
6091
- throw new TaskNotFoundError(id);
6092
- if (task.version !== input.version) {
6093
- throw new VersionConflictError(id, input.version, task.version);
6094
- }
6095
- const timestamp = now();
6096
- const completionTimestamp = input.completed_at ?? timestamp;
6097
- const sets = ["version = version + 1", "updated_at = ?"];
6098
- const params = [timestamp];
6099
- if (input.title !== undefined) {
6100
- sets.push("title = ?");
6101
- params.push(input.title);
6102
- }
6103
- if (input.description !== undefined) {
6104
- sets.push("description = ?");
6105
- params.push(input.description);
6106
- }
6107
- if (input.status !== undefined) {
6108
- if (input.status === "completed") {
6109
- checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
6110
- }
6111
- sets.push("status = ?");
6112
- params.push(input.status);
6113
- if (input.status === "completed") {
6114
- sets.push("completed_at = ?");
6115
- params.push(completionTimestamp);
6116
- }
6117
- }
6118
- if (input.priority !== undefined) {
6119
- sets.push("priority = ?");
6120
- params.push(input.priority);
6121
- }
6122
- if (input.project_id !== undefined) {
6123
- sets.push("project_id = ?");
6124
- params.push(input.project_id);
6125
- }
6126
- if (input.assigned_to !== undefined) {
6127
- sets.push("assigned_to = ?");
6128
- params.push(input.assigned_to);
6129
- }
6130
- if (input.working_dir !== undefined) {
6131
- sets.push("working_dir = ?");
6132
- params.push(input.working_dir);
6133
- }
6134
- if (input.tags !== undefined) {
6135
- sets.push("tags = ?");
6136
- params.push(JSON.stringify(input.tags));
6137
- }
6138
- if (input.metadata !== undefined) {
6139
- sets.push("metadata = ?");
6140
- params.push(JSON.stringify(input.metadata));
6141
- }
6142
- if (input.plan_id !== undefined) {
6143
- sets.push("plan_id = ?");
6144
- params.push(input.plan_id);
6145
- }
6146
- if (input.task_list_id !== undefined) {
6147
- sets.push("task_list_id = ?");
6148
- params.push(input.task_list_id);
6149
- }
6150
- if (input.due_at !== undefined) {
6151
- sets.push("due_at = ?");
6152
- params.push(input.due_at);
6153
- }
6154
- if (input.estimated_minutes !== undefined) {
6155
- sets.push("estimated_minutes = ?");
6156
- params.push(input.estimated_minutes);
6157
- }
6158
- if (input.sla_minutes !== undefined) {
6159
- sets.push("sla_minutes = ?");
6160
- params.push(input.sla_minutes);
6161
- }
6162
- if (input.actual_minutes !== undefined) {
6163
- sets.push("actual_minutes = ?");
6164
- params.push(input.actual_minutes);
6165
- }
6166
- if (input.completed_at !== undefined && input.status !== "completed") {
6167
- sets.push("completed_at = ?");
6168
- params.push(input.completed_at);
6169
- }
6170
- if (input.confidence !== undefined) {
6171
- sets.push("confidence = ?");
6172
- params.push(input.confidence);
6173
- }
6174
- if (input.retry_count !== undefined) {
6175
- sets.push("retry_count = ?");
6176
- params.push(input.retry_count);
6177
- }
6178
- if (input.max_retries !== undefined) {
6179
- sets.push("max_retries = ?");
6180
- params.push(input.max_retries);
6181
- }
6182
- if (input.retry_after !== undefined) {
6183
- sets.push("retry_after = ?");
6184
- params.push(input.retry_after);
6185
- }
6186
- if (input.requires_approval !== undefined) {
6187
- sets.push("requires_approval = ?");
6188
- params.push(input.requires_approval ? 1 : 0);
6189
- }
6190
- if (input.approved_by !== undefined) {
6191
- sets.push("approved_by = ?");
6192
- params.push(input.approved_by);
6193
- sets.push("approved_at = ?");
6194
- params.push(now());
6195
- }
6196
- if (input.recurrence_rule !== undefined) {
6197
- sets.push("recurrence_rule = ?");
6198
- params.push(input.recurrence_rule);
6070
+ const template = getTemplate(id, d);
6071
+ if (!template)
6072
+ return null;
6073
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
6074
+ const tasks = rows.map(rowToTemplateTask);
6075
+ return { ...template, tasks };
6076
+ }
6077
+ function getTemplateTasks(templateId, db) {
6078
+ const d = db || getDatabase();
6079
+ const resolved = resolveTemplateId(templateId, d);
6080
+ if (!resolved)
6081
+ return [];
6082
+ const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
6083
+ return rows.map(rowToTemplateTask);
6084
+ }
6085
+ function evaluateCondition(condition, variables) {
6086
+ if (!condition || condition.trim() === "")
6087
+ return true;
6088
+ const trimmed = condition.trim();
6089
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
6090
+ if (eqMatch) {
6091
+ const varName = eqMatch[1];
6092
+ const expected = eqMatch[2].trim();
6093
+ return (variables[varName] ?? "") === expected;
6199
6094
  }
6200
- if (input.task_type !== undefined) {
6201
- sets.push("task_type = ?");
6202
- params.push(input.task_type ?? null);
6095
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
6096
+ if (neqMatch) {
6097
+ const varName = neqMatch[1];
6098
+ const expected = neqMatch[2].trim();
6099
+ return (variables[varName] ?? "") !== expected;
6203
6100
  }
6204
- params.push(id, input.version);
6205
- const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
6206
- if (result.changes === 0) {
6207
- const current = getTask(id, d);
6208
- throw new VersionConflictError(id, input.version, current?.version ?? -1);
6101
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
6102
+ if (falsyMatch) {
6103
+ const varName = falsyMatch[1];
6104
+ const val = variables[varName];
6105
+ return !val || val === "" || val === "false";
6209
6106
  }
6210
- if (input.tags !== undefined) {
6211
- replaceTaskTags(id, input.tags, d);
6107
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
6108
+ if (truthyMatch) {
6109
+ const varName = truthyMatch[1];
6110
+ const val = variables[varName];
6111
+ return !!val && val !== "" && val !== "false";
6212
6112
  }
6213
- const agentId = task.assigned_to || task.agent_id || null;
6214
- if (input.status !== undefined && input.status !== task.status)
6215
- logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
6216
- if (input.priority !== undefined && input.priority !== task.priority)
6217
- logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
6218
- if (input.title !== undefined && input.title !== task.title)
6219
- logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
6220
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
6221
- logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
6222
- if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
6223
- logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
6224
- if (input.approved_by !== undefined)
6225
- logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
6226
- const updatedTask = {
6227
- ...task,
6228
- ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
6229
- tags: input.tags ?? task.tags,
6230
- metadata: input.metadata ?? task.metadata,
6231
- version: task.version + 1,
6232
- updated_at: timestamp,
6233
- completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
6234
- sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
6235
- actual_minutes: input.actual_minutes ?? task.actual_minutes,
6236
- confidence: input.confidence !== undefined ? input.confidence : task.confidence,
6237
- retry_count: input.retry_count ?? task.retry_count,
6238
- max_retries: input.max_retries ?? task.max_retries,
6239
- retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
6240
- requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
6241
- approved_by: input.approved_by ?? task.approved_by,
6242
- approved_at: input.approved_by ? timestamp : task.approved_at
6113
+ return true;
6114
+ }
6115
+ function exportTemplate(id, db) {
6116
+ const d = db || getDatabase();
6117
+ const template = getTemplateWithTasks(id, d);
6118
+ if (!template)
6119
+ throw new Error(`Template not found: ${id}`);
6120
+ return {
6121
+ name: template.name,
6122
+ title_pattern: template.title_pattern,
6123
+ description: template.description,
6124
+ priority: template.priority,
6125
+ tags: template.tags,
6126
+ variables: template.variables,
6127
+ project_id: template.project_id,
6128
+ plan_id: template.plan_id,
6129
+ metadata: template.metadata,
6130
+ tasks: template.tasks.map((t) => ({
6131
+ position: t.position,
6132
+ title_pattern: t.title_pattern,
6133
+ description: t.description,
6134
+ priority: t.priority,
6135
+ tags: t.tags,
6136
+ task_type: t.task_type,
6137
+ condition: t.condition,
6138
+ include_template_id: t.include_template_id,
6139
+ depends_on_positions: t.depends_on_positions,
6140
+ metadata: t.metadata
6141
+ }))
6243
6142
  };
6244
- const databasePath = databasePathFromDatabase(d);
6245
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
6246
- const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
6247
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
6248
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
6249
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
6250
- }
6251
- if (input.status !== undefined && input.status !== task.status) {
6252
- const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
6253
- dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
6254
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
6255
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
6256
- }
6257
- if (input.approved_by !== undefined) {
6258
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
6259
- }
6260
- const updatePayload = taskEventData(updatedTask);
6261
- dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
6262
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
6263
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
6264
- return updatedTask;
6265
6143
  }
6266
- function deleteTask(id, db) {
6144
+ function importTemplate(json, db) {
6267
6145
  const d = db || getDatabase();
6268
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
6269
- if (!row)
6270
- return false;
6271
- recordStorageTombstone({
6272
- object_type: "tasks",
6273
- object_id: id,
6274
- payload: rowToTask(row),
6275
- version: row.version
6146
+ const taskInputs = (json.tasks || []).map((t) => ({
6147
+ title_pattern: t.title_pattern,
6148
+ description: t.description ?? undefined,
6149
+ priority: t.priority,
6150
+ tags: t.tags,
6151
+ task_type: t.task_type ?? undefined,
6152
+ condition: t.condition ?? undefined,
6153
+ include_template_id: t.include_template_id ?? undefined,
6154
+ depends_on: t.depends_on_positions,
6155
+ metadata: t.metadata
6156
+ }));
6157
+ return createTemplate({
6158
+ name: json.name,
6159
+ title_pattern: json.title_pattern,
6160
+ description: json.description ?? undefined,
6161
+ priority: json.priority,
6162
+ tags: json.tags,
6163
+ variables: json.variables,
6164
+ project_id: json.project_id ?? undefined,
6165
+ plan_id: json.plan_id ?? undefined,
6166
+ metadata: json.metadata,
6167
+ tasks: taskInputs
6276
6168
  }, d);
6277
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
6278
- return result.changes > 0;
6279
6169
  }
6280
- var init_task_crud = __esm(() => {
6281
- init_types();
6282
- init_database();
6283
- init_completion_guard();
6284
- init_event_emission_safety();
6285
- init_event_hooks();
6286
- init_shared_events();
6287
- init_audit();
6288
- init_webhooks();
6289
- init_checklists();
6290
- init_storage_tombstones();
6291
- });
6292
-
6293
- // src/lib/recurrence.ts
6294
- function parseRecurrenceRule(rule) {
6295
- const normalized = rule.trim().toLowerCase();
6296
- if (normalized === "every weekday" || normalized === "every weekdays") {
6297
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
6170
+ function getTemplateVersion(id, version, db) {
6171
+ const d = db || getDatabase();
6172
+ const resolved = resolveTemplateId(id, d);
6173
+ if (!resolved)
6174
+ return null;
6175
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
6176
+ return row || null;
6177
+ }
6178
+ function listTemplateVersions(id, db) {
6179
+ const d = db || getDatabase();
6180
+ const resolved = resolveTemplateId(id, d);
6181
+ if (!resolved)
6182
+ return [];
6183
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
6184
+ }
6185
+ function resolveVariables(templateVars, provided) {
6186
+ const merged = { ...provided };
6187
+ for (const v of templateVars) {
6188
+ if (merged[v.name] === undefined && v.default !== undefined) {
6189
+ merged[v.name] = v.default;
6190
+ }
6298
6191
  }
6299
- if (normalized === "every day" || normalized === "daily") {
6300
- return { type: "interval", interval: 1, unit: "day" };
6192
+ const missing = [];
6193
+ for (const v of templateVars) {
6194
+ if (v.required && merged[v.name] === undefined) {
6195
+ missing.push(v.name);
6196
+ }
6301
6197
  }
6302
- if (normalized === "every week" || normalized === "weekly") {
6303
- return { type: "interval", interval: 1, unit: "week" };
6198
+ if (missing.length > 0) {
6199
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
6304
6200
  }
6305
- if (normalized === "every month" || normalized === "monthly") {
6306
- return { type: "interval", interval: 1, unit: "month" };
6201
+ return merged;
6202
+ }
6203
+ function substituteVars(text, variables) {
6204
+ let result = text;
6205
+ for (const [key, val] of Object.entries(variables)) {
6206
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
6307
6207
  }
6308
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
6309
- if (intervalMatch) {
6310
- return {
6311
- type: "interval",
6312
- interval: parseInt(intervalMatch[1], 10),
6313
- unit: intervalMatch[2]
6314
- };
6208
+ return result;
6209
+ }
6210
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
6211
+ const d = db || getDatabase();
6212
+ const template = getTemplateWithTasks(templateId, d);
6213
+ if (!template)
6214
+ throw new Error(`Template not found: ${templateId}`);
6215
+ const visited = _visitedTemplateIds || new Set;
6216
+ if (visited.has(template.id)) {
6217
+ throw new Error(`Circular template reference detected: ${template.id}`);
6315
6218
  }
6316
- const daysMatch = normalized.match(/^every\s+(.+)$/);
6317
- if (daysMatch) {
6318
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
6319
- const days = [];
6320
- for (const part of dayParts) {
6321
- const dayNum = DAY_NAMES[part];
6322
- if (dayNum !== undefined) {
6323
- days.push(dayNum);
6219
+ visited.add(template.id);
6220
+ const resolved = resolveVariables(template.variables, variables);
6221
+ if (template.tasks.length === 0) {
6222
+ const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
6223
+ const task = createTask(input, d);
6224
+ return [task];
6225
+ }
6226
+ const createdTasks = [];
6227
+ const positionToId = new Map;
6228
+ const skippedPositions = new Set;
6229
+ for (const tt of template.tasks) {
6230
+ if (tt.include_template_id) {
6231
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
6232
+ createdTasks.push(...includedTasks);
6233
+ if (includedTasks.length > 0) {
6234
+ positionToId.set(tt.position, includedTasks[0].id);
6235
+ } else {
6236
+ skippedPositions.add(tt.position);
6324
6237
  }
6238
+ continue;
6325
6239
  }
6326
- if (days.length > 0) {
6327
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
6240
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
6241
+ skippedPositions.add(tt.position);
6242
+ continue;
6328
6243
  }
6244
+ let title = tt.title_pattern;
6245
+ let desc = tt.description;
6246
+ title = substituteVars(title, resolved);
6247
+ if (desc)
6248
+ desc = substituteVars(desc, resolved);
6249
+ const task = createTask({
6250
+ title,
6251
+ description: desc ?? undefined,
6252
+ priority: tt.priority,
6253
+ tags: tt.tags,
6254
+ task_type: tt.task_type ?? undefined,
6255
+ project_id: projectId,
6256
+ task_list_id: taskListId,
6257
+ metadata: tt.metadata
6258
+ }, d);
6259
+ createdTasks.push(task);
6260
+ positionToId.set(tt.position, task.id);
6329
6261
  }
6330
- 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"`);
6331
- }
6332
- function isValidRecurrenceRule(rule) {
6333
- try {
6334
- parseRecurrenceRule(rule);
6335
- return true;
6336
- } catch {
6337
- return false;
6338
- }
6339
- }
6340
- function nextOccurrence(rule, from) {
6341
- const parsed = parseRecurrenceRule(rule);
6342
- const base = from || new Date;
6343
- if (parsed.type === "interval") {
6344
- const next = new Date(base);
6345
- if (parsed.unit === "day") {
6346
- next.setDate(next.getDate() + parsed.interval);
6347
- } else if (parsed.unit === "week") {
6348
- next.setDate(next.getDate() + parsed.interval * 7);
6349
- } else if (parsed.unit === "month") {
6350
- next.setMonth(next.getMonth() + parsed.interval);
6262
+ for (const tt of template.tasks) {
6263
+ if (skippedPositions.has(tt.position))
6264
+ continue;
6265
+ if (tt.include_template_id)
6266
+ continue;
6267
+ const deps = tt.depends_on_positions;
6268
+ for (const depPos of deps) {
6269
+ if (skippedPositions.has(depPos))
6270
+ continue;
6271
+ const taskId = positionToId.get(tt.position);
6272
+ const depId = positionToId.get(depPos);
6273
+ if (taskId && depId) {
6274
+ addDependency(taskId, depId, d);
6275
+ }
6351
6276
  }
6352
- return next.toISOString();
6353
6277
  }
6354
- if (parsed.type === "specific_days") {
6355
- const currentDay = base.getDay();
6356
- const days = parsed.days;
6357
- let daysToAdd = Infinity;
6358
- for (const day of days) {
6359
- let diff = day - currentDay;
6360
- if (diff <= 0)
6361
- diff += 7;
6362
- if (diff < daysToAdd)
6363
- daysToAdd = diff;
6278
+ return createdTasks;
6279
+ }
6280
+ function previewTemplate(templateId, variables, db) {
6281
+ const d = db || getDatabase();
6282
+ const template = getTemplateWithTasks(templateId, d);
6283
+ if (!template)
6284
+ throw new Error(`Template not found: ${templateId}`);
6285
+ const resolved = resolveVariables(template.variables, variables);
6286
+ const tasks = [];
6287
+ if (template.tasks.length === 0) {
6288
+ tasks.push({
6289
+ position: 0,
6290
+ title: substituteVars(template.title_pattern, resolved),
6291
+ description: template.description ? substituteVars(template.description, resolved) : null,
6292
+ priority: template.priority,
6293
+ tags: template.tags,
6294
+ task_type: null,
6295
+ depends_on_positions: []
6296
+ });
6297
+ } else {
6298
+ for (const tt of template.tasks) {
6299
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
6300
+ continue;
6301
+ tasks.push({
6302
+ position: tt.position,
6303
+ title: substituteVars(tt.title_pattern, resolved),
6304
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
6305
+ priority: tt.priority,
6306
+ tags: tt.tags,
6307
+ task_type: tt.task_type,
6308
+ depends_on_positions: tt.depends_on_positions
6309
+ });
6364
6310
  }
6365
- const next = new Date(base);
6366
- next.setDate(next.getDate() + daysToAdd);
6367
- return next.toISOString();
6368
6311
  }
6369
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
6370
- }
6371
- var DAY_NAMES;
6372
- var init_recurrence = __esm(() => {
6373
- DAY_NAMES = {
6374
- sunday: 0,
6375
- sun: 0,
6376
- monday: 1,
6377
- mon: 1,
6378
- tuesday: 2,
6379
- tue: 2,
6380
- wednesday: 3,
6381
- wed: 3,
6382
- thursday: 4,
6383
- thu: 4,
6384
- friday: 5,
6385
- fri: 5,
6386
- saturday: 6,
6387
- sat: 6
6388
- };
6389
- });
6390
-
6391
- // src/db/templates.ts
6392
- var exports_templates = {};
6393
- __export(exports_templates, {
6394
- updateTemplate: () => updateTemplate,
6395
- tasksFromTemplate: () => tasksFromTemplate,
6396
- taskFromTemplate: () => taskFromTemplate,
6397
- resolveVariables: () => resolveVariables,
6398
- previewTemplate: () => previewTemplate,
6399
- listTemplates: () => listTemplates,
6400
- listTemplateVersions: () => listTemplateVersions,
6401
- importTemplate: () => importTemplate,
6402
- getTemplateWithTasks: () => getTemplateWithTasks,
6403
- getTemplateVersion: () => getTemplateVersion,
6404
- getTemplateTasks: () => getTemplateTasks,
6405
- getTemplate: () => getTemplate,
6406
- exportTemplate: () => exportTemplate,
6407
- evaluateCondition: () => evaluateCondition,
6408
- deleteTemplate: () => deleteTemplate,
6409
- createTemplate: () => createTemplate,
6410
- addTemplateTasks: () => addTemplateTasks
6411
- });
6412
- function rowToTemplate(row) {
6413
- return {
6414
- ...row,
6415
- tags: JSON.parse(row.tags || "[]"),
6416
- variables: JSON.parse(row.variables || "[]"),
6417
- metadata: JSON.parse(row.metadata || "{}"),
6418
- priority: row.priority || "medium",
6419
- version: row.version ?? 1
6420
- };
6421
- }
6422
- function rowToTemplateTask(row) {
6423
6312
  return {
6424
- ...row,
6425
- tags: JSON.parse(row.tags || "[]"),
6426
- depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
6427
- metadata: JSON.parse(row.metadata || "{}"),
6428
- priority: row.priority || "medium",
6429
- condition: row.condition ?? null,
6430
- include_template_id: row.include_template_id ?? null
6313
+ template_id: template.id,
6314
+ template_name: template.name,
6315
+ description: template.description,
6316
+ variables: template.variables,
6317
+ resolved_variables: resolved,
6318
+ tasks
6431
6319
  };
6432
6320
  }
6433
- function resolveTemplateId(id, d) {
6434
- return resolvePartialId(d, "task_templates", id);
6435
- }
6436
- function createTemplate(input, db) {
6321
+ var init_templates = __esm(() => {
6322
+ init_database();
6323
+ init_tasks();
6324
+ init_storage_tombstones();
6325
+ });
6326
+
6327
+ // src/db/task-graph.ts
6328
+ function addDependency(taskId, dependsOn, db) {
6437
6329
  const d = db || getDatabase();
6438
- const id = uuid();
6439
- const machineId = currentStorageMachineId(d);
6440
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
6441
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6442
- id,
6443
- input.name,
6444
- input.title_pattern,
6445
- input.description || null,
6446
- input.priority || "medium",
6447
- JSON.stringify(input.tags || []),
6448
- JSON.stringify(input.variables || []),
6449
- input.project_id || null,
6450
- input.plan_id || null,
6451
- JSON.stringify(input.metadata || {}),
6452
- now(),
6453
- machineId
6454
- ]);
6455
- if (input.tasks && input.tasks.length > 0) {
6456
- addTemplateTasks(id, input.tasks, d);
6330
+ if (!getTask(taskId, d))
6331
+ throw new TaskNotFoundError(taskId);
6332
+ if (!getTask(dependsOn, d))
6333
+ throw new TaskNotFoundError(dependsOn);
6334
+ if (wouldCreateCycle(taskId, dependsOn, d)) {
6335
+ throw new DependencyCycleError(taskId, dependsOn);
6457
6336
  }
6458
- return getTemplate(id, d);
6337
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
6459
6338
  }
6460
- function getTemplate(id, db) {
6339
+ function removeDependency(taskId, dependsOn, db) {
6461
6340
  const d = db || getDatabase();
6462
- const resolved = resolveTemplateId(id, d);
6463
- if (!resolved)
6464
- return null;
6465
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
6466
- return row ? rowToTemplate(row) : null;
6341
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
6342
+ return result.changes > 0;
6467
6343
  }
6468
- function listTemplates(db) {
6344
+ function getTaskDependencies(taskId, db) {
6469
6345
  const d = db || getDatabase();
6470
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
6346
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
6471
6347
  }
6472
- function deleteTemplate(id, db) {
6348
+ function getTaskDependents(taskId, db) {
6473
6349
  const d = db || getDatabase();
6474
- const resolved = resolveTemplateId(id, d);
6475
- if (!resolved)
6476
- return false;
6477
- const template = getTemplate(resolved, d);
6478
- if (!template)
6479
- return false;
6480
- recordStorageTombstone({
6481
- object_type: "templates",
6482
- object_id: resolved,
6483
- payload: template,
6484
- version: template.version
6485
- }, d);
6486
- return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
6350
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
6487
6351
  }
6488
- function updateTemplate(id, updates, db) {
6352
+ function cloneTask(taskId, overrides, db) {
6489
6353
  const d = db || getDatabase();
6490
- const resolved = resolveTemplateId(id, d);
6491
- if (!resolved)
6492
- return null;
6493
- const current = getTemplateWithTasks(resolved, d);
6494
- if (current) {
6495
- const snapshot = JSON.stringify({
6496
- name: current.name,
6497
- title_pattern: current.title_pattern,
6498
- description: current.description,
6499
- priority: current.priority,
6500
- tags: current.tags,
6501
- variables: current.variables,
6502
- project_id: current.project_id,
6503
- plan_id: current.plan_id,
6504
- metadata: current.metadata,
6505
- tasks: current.tasks
6354
+ const source = getTask(taskId, d);
6355
+ if (!source)
6356
+ throw new TaskNotFoundError(taskId);
6357
+ const input = {
6358
+ title: overrides?.title ?? source.title,
6359
+ description: overrides?.description ?? source.description ?? undefined,
6360
+ priority: overrides?.priority ?? source.priority,
6361
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
6362
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
6363
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
6364
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
6365
+ status: overrides?.status ?? "pending",
6366
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
6367
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
6368
+ tags: overrides?.tags ?? source.tags,
6369
+ metadata: overrides?.metadata ?? source.metadata,
6370
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
6371
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
6372
+ };
6373
+ return createTask(input, d);
6374
+ }
6375
+ function getTaskGraph(taskId, direction = "both", db) {
6376
+ const d = db || getDatabase();
6377
+ const task = getTask(taskId, d);
6378
+ if (!task)
6379
+ throw new TaskNotFoundError(taskId);
6380
+ function toNode(t) {
6381
+ const deps = getTaskDependencies(t.id, d);
6382
+ const hasUnfinishedDeps = deps.some((dep) => {
6383
+ const depTask = getTask(dep.depends_on, d);
6384
+ return depTask && depTask.status !== "completed";
6506
6385
  });
6507
- d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
6508
- }
6509
- const sets = ["version = version + 1"];
6510
- const values = [];
6511
- if (updates.name !== undefined) {
6512
- sets.push("name = ?");
6513
- values.push(updates.name);
6514
- }
6515
- if (updates.title_pattern !== undefined) {
6516
- sets.push("title_pattern = ?");
6517
- values.push(updates.title_pattern);
6518
- }
6519
- if (updates.description !== undefined) {
6520
- sets.push("description = ?");
6521
- values.push(updates.description);
6386
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
6522
6387
  }
6523
- if (updates.priority !== undefined) {
6524
- sets.push("priority = ?");
6525
- values.push(updates.priority);
6388
+ function buildUp(id, visited) {
6389
+ if (visited.has(id))
6390
+ return [];
6391
+ visited.add(id);
6392
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
6393
+ return deps.map((dep) => {
6394
+ const depTask = getTask(dep.depends_on, d);
6395
+ if (!depTask)
6396
+ return null;
6397
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
6398
+ }).filter(Boolean);
6526
6399
  }
6527
- if (updates.tags !== undefined) {
6528
- sets.push("tags = ?");
6529
- values.push(JSON.stringify(updates.tags));
6400
+ function buildDown(id, visited) {
6401
+ if (visited.has(id))
6402
+ return [];
6403
+ visited.add(id);
6404
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
6405
+ return dependents.map((dep) => {
6406
+ const depTask = getTask(dep.task_id, d);
6407
+ if (!depTask)
6408
+ return null;
6409
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
6410
+ }).filter(Boolean);
6530
6411
  }
6531
- if (updates.variables !== undefined) {
6532
- sets.push("variables = ?");
6533
- values.push(JSON.stringify(updates.variables));
6412
+ const rootNode = toNode(task);
6413
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
6414
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
6415
+ return { task: rootNode, depends_on, blocks };
6416
+ }
6417
+ function moveTask(taskId, target, db) {
6418
+ const d = db || getDatabase();
6419
+ const task = getTask(taskId, d);
6420
+ if (!task)
6421
+ throw new TaskNotFoundError(taskId);
6422
+ const sets = ["updated_at = ?", "version = version + 1"];
6423
+ const params = [now()];
6424
+ if (target.task_list_id !== undefined) {
6425
+ sets.push("task_list_id = ?");
6426
+ params.push(target.task_list_id);
6534
6427
  }
6535
- if (updates.project_id !== undefined) {
6428
+ if (target.project_id !== undefined) {
6536
6429
  sets.push("project_id = ?");
6537
- values.push(updates.project_id);
6430
+ params.push(target.project_id);
6538
6431
  }
6539
- if (updates.plan_id !== undefined) {
6432
+ if (target.plan_id !== undefined) {
6540
6433
  sets.push("plan_id = ?");
6541
- values.push(updates.plan_id);
6434
+ params.push(target.plan_id);
6542
6435
  }
6543
- if (updates.metadata !== undefined) {
6544
- sets.push("metadata = ?");
6545
- values.push(JSON.stringify(updates.metadata));
6436
+ params.push(taskId);
6437
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
6438
+ return getTask(taskId, d);
6439
+ }
6440
+ function wouldCreateCycle(taskId, dependsOn, db) {
6441
+ const visited = new Set;
6442
+ const queue = [dependsOn];
6443
+ while (queue.length > 0) {
6444
+ const current = queue.shift();
6445
+ if (current === taskId)
6446
+ return true;
6447
+ if (visited.has(current))
6448
+ continue;
6449
+ visited.add(current);
6450
+ const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
6451
+ for (const dep of deps) {
6452
+ queue.push(dep.depends_on);
6453
+ }
6546
6454
  }
6547
- values.push(resolved);
6548
- d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
6549
- return getTemplate(resolved, d);
6455
+ return false;
6550
6456
  }
6551
- function taskFromTemplate(templateId, overrides = {}, db) {
6552
- const t = getTemplate(templateId, db);
6553
- if (!t)
6554
- throw new Error(`Template not found: ${templateId}`);
6555
- const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
6556
- return {
6557
- title: cleanOverrides.title || t.title_pattern,
6558
- description: cleanOverrides.description ?? t.description ?? undefined,
6559
- priority: cleanOverrides.priority ?? t.priority,
6560
- tags: cleanOverrides.tags ?? t.tags,
6561
- project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
6562
- plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
6563
- metadata: cleanOverrides.metadata ?? t.metadata,
6564
- ...cleanOverrides
6565
- };
6457
+ var init_task_graph = __esm(() => {
6458
+ init_types();
6459
+ init_database();
6460
+ init_task_crud();
6461
+ });
6462
+
6463
+ // src/db/task-lifecycle.ts
6464
+ var exports_task_lifecycle = {};
6465
+ __export(exports_task_lifecycle, {
6466
+ unlockTask: () => unlockTask,
6467
+ stealTask: () => stealTask,
6468
+ startTask: () => startTask,
6469
+ spawnNextRecurrence: () => spawnNextRecurrence,
6470
+ lockTask: () => lockTask,
6471
+ getTasksChangedSince: () => getTasksChangedSince,
6472
+ getTaskLockStatus: () => getTaskLockStatus,
6473
+ getStaleTasks: () => getStaleTasks,
6474
+ getNextTask: () => getNextTask,
6475
+ getBlockingDeps: () => getBlockingDeps,
6476
+ getActiveWork: () => getActiveWork,
6477
+ failTask: () => failTask,
6478
+ completeTask: () => completeTask,
6479
+ claimOrSteal: () => claimOrSteal,
6480
+ claimNextTask: () => claimNextTask
6481
+ });
6482
+ function lockExpiresAt(lockedAt) {
6483
+ if (!lockedAt)
6484
+ return null;
6485
+ return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
6566
6486
  }
6567
- function addTemplateTasks(templateId, tasks, db) {
6487
+ function assertStartable(task, agentId) {
6488
+ if (task.status === "pending")
6489
+ return;
6490
+ if (task.status === "in_progress")
6491
+ return;
6492
+ throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
6493
+ }
6494
+ function getBlockingDeps(id, db) {
6568
6495
  const d = db || getDatabase();
6569
- const template = getTemplate(templateId, d);
6570
- if (!template)
6571
- throw new Error(`Template not found: ${templateId}`);
6572
- d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
6573
- const results = [];
6574
- for (let i = 0;i < tasks.length; i++) {
6575
- const task = tasks[i];
6576
- const id = uuid();
6577
- 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)
6578
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6579
- id,
6580
- templateId,
6581
- i,
6582
- task.title_pattern,
6583
- task.description || null,
6584
- task.priority || "medium",
6585
- JSON.stringify(task.tags || []),
6586
- task.task_type || null,
6587
- task.condition || null,
6588
- task.include_template_id || null,
6589
- JSON.stringify(task.depends_on || []),
6590
- JSON.stringify(task.metadata || {}),
6591
- now()
6592
- ]);
6593
- const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
6594
- if (row)
6595
- results.push(rowToTemplateTask(row));
6496
+ const deps = getTaskDependencies(id, d);
6497
+ if (deps.length === 0)
6498
+ return [];
6499
+ const blocking = [];
6500
+ for (const dep of deps) {
6501
+ const task = getTask(dep.depends_on, d);
6502
+ if (task && task.status !== "completed")
6503
+ blocking.push(task);
6596
6504
  }
6597
- return results;
6505
+ return blocking;
6598
6506
  }
6599
- function getTemplateWithTasks(id, db) {
6507
+ function startTask(id, agentId, db) {
6600
6508
  const d = db || getDatabase();
6601
- const template = getTemplate(id, d);
6602
- if (!template)
6603
- return null;
6604
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
6605
- const tasks = rows.map(rowToTemplateTask);
6606
- return { ...template, tasks };
6509
+ const databasePath = databasePathFromDatabase(d);
6510
+ const task = getTask(id, d);
6511
+ if (!task)
6512
+ throw new TaskNotFoundError(id);
6513
+ assertStartable(task, agentId);
6514
+ const blocking = getBlockingDeps(id, d);
6515
+ if (blocking.length > 0) {
6516
+ const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
6517
+ emitLocalEventHooksQuiet({
6518
+ type: "task.blocked",
6519
+ payload: {
6520
+ id,
6521
+ agent_id: agentId,
6522
+ title: task.title,
6523
+ blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
6524
+ },
6525
+ databasePath
6526
+ });
6527
+ throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
6528
+ }
6529
+ const cutoff = lockExpiryCutoff();
6530
+ const timestamp = now();
6531
+ 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 = ?
6532
+ 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]);
6533
+ if (result.changes === 0) {
6534
+ const current = getTask(id, d);
6535
+ if (!current)
6536
+ throw new TaskNotFoundError(id);
6537
+ assertStartable(current, agentId);
6538
+ if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
6539
+ throw new LockError(id, current.locked_by);
6540
+ }
6541
+ throw new Error(`Task ${id} could not be started because it changed during claim`);
6542
+ }
6543
+ logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
6544
+ 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 };
6545
+ const payload = taskEventData(startedTask, { agent_id: agentId });
6546
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
6547
+ emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
6548
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
6549
+ return startedTask;
6607
6550
  }
6608
- function getTemplateTasks(templateId, db) {
6551
+ function completeTask(id, agentId, db, options) {
6609
6552
  const d = db || getDatabase();
6610
- const resolved = resolveTemplateId(templateId, d);
6611
- if (!resolved)
6612
- return [];
6613
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
6614
- return rows.map(rowToTemplateTask);
6553
+ const databasePath = databasePathFromDatabase(d);
6554
+ const task = getTask(id, d);
6555
+ if (!task)
6556
+ throw new TaskNotFoundError(id);
6557
+ if (task.status === "completed") {
6558
+ return task;
6559
+ }
6560
+ if (task.status === "cancelled") {
6561
+ throw new Error(`Task ${id} is cancelled and cannot be completed`);
6562
+ }
6563
+ if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
6564
+ throw new LockError(id, task.locked_by);
6565
+ }
6566
+ checkCompletionGuard(task, agentId || null, d);
6567
+ 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;
6568
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
6569
+ const completionMeta = {};
6570
+ if (hasEvidence)
6571
+ completionMeta._evidence = evidence;
6572
+ if (options?.confidence !== undefined) {
6573
+ completionMeta._completion = { confidence: options.confidence };
6574
+ }
6575
+ const hasMeta = Object.keys(completionMeta).length > 0;
6576
+ const timestamp = options?.completed_at || now();
6577
+ const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
6578
+ const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
6579
+ const finalVersion = versionBeforeStatus + 1;
6580
+ const tx = d.transaction(() => {
6581
+ if (hasMeta) {
6582
+ const meta2 = { ...task.metadata, ...completionMeta };
6583
+ const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
6584
+ if (metaResult.changes === 0) {
6585
+ const current = getTask(id, d);
6586
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
6587
+ }
6588
+ }
6589
+ const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
6590
+ WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
6591
+ if (statusResult.changes === 0) {
6592
+ const current = getTask(id, d);
6593
+ throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
6594
+ }
6595
+ });
6596
+ tx();
6597
+ logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
6598
+ const completedTaskForEvent = {
6599
+ ...task,
6600
+ status: "completed",
6601
+ locked_by: null,
6602
+ locked_at: null,
6603
+ completed_at: timestamp,
6604
+ confidence,
6605
+ version: finalVersion,
6606
+ updated_at: timestamp,
6607
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
6608
+ };
6609
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
6610
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
6611
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
6612
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
6613
+ let spawnedTask = null;
6614
+ if (task.recurrence_rule && !options?.skip_recurrence) {
6615
+ try {
6616
+ spawnedTask = spawnNextRecurrence(task, d, timestamp);
6617
+ } catch (e) {
6618
+ spawnedTask = null;
6619
+ console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
6620
+ }
6621
+ }
6622
+ let spawnedFromTemplate = null;
6623
+ if (task.spawns_template_id) {
6624
+ const spawnDepth = task.metadata?._spawn_depth || 0;
6625
+ if (spawnDepth >= MAX_SPAWN_DEPTH) {
6626
+ console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
6627
+ } else {
6628
+ try {
6629
+ const input = taskFromTemplate(task.spawns_template_id, {
6630
+ project_id: task.project_id ?? undefined,
6631
+ plan_id: task.plan_id ?? undefined,
6632
+ task_list_id: task.task_list_id ?? undefined,
6633
+ assigned_to: task.assigned_to ?? undefined
6634
+ }, d);
6635
+ input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
6636
+ spawnedFromTemplate = createTask(input, d);
6637
+ } catch {}
6638
+ }
6639
+ }
6640
+ const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
6641
+ if (spawnedTask) {
6642
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
6643
+ }
6644
+ if (spawnedFromTemplate) {
6645
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
6646
+ }
6647
+ const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
6648
+ JOIN task_dependencies td ON td.task_id = t.id
6649
+ WHERE td.depends_on = ? AND t.status = 'pending'
6650
+ AND NOT EXISTS (
6651
+ SELECT 1 FROM task_dependencies td2
6652
+ JOIN tasks dep2 ON dep2.id = td2.depends_on
6653
+ WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
6654
+ )`).all(id, id);
6655
+ if (unblockedDeps.length > 0) {
6656
+ meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
6657
+ for (const dep of unblockedDeps) {
6658
+ const depTask = getTask(dep.id, d);
6659
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
6660
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
6661
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
6662
+ if (depTask)
6663
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
6664
+ }
6665
+ }
6666
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
6615
6667
  }
6616
- function evaluateCondition(condition, variables) {
6617
- if (!condition || condition.trim() === "")
6618
- return true;
6619
- const trimmed = condition.trim();
6620
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
6621
- if (eqMatch) {
6622
- const varName = eqMatch[1];
6623
- const expected = eqMatch[2].trim();
6624
- return (variables[varName] ?? "") === expected;
6668
+ function lockTask(id, agentId, db) {
6669
+ const d = db || getDatabase();
6670
+ const task = getTask(id, d);
6671
+ if (!task)
6672
+ throw new TaskNotFoundError(id);
6673
+ if (task.status === "completed" || task.status === "cancelled") {
6674
+ return {
6675
+ success: false,
6676
+ error: `Task is ${task.status} and cannot be locked`
6677
+ };
6625
6678
  }
6626
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
6627
- if (neqMatch) {
6628
- const varName = neqMatch[1];
6629
- const expected = neqMatch[2].trim();
6630
- return (variables[varName] ?? "") !== expected;
6679
+ if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
6680
+ const timestamp2 = now();
6681
+ d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
6682
+ logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
6683
+ return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
6631
6684
  }
6632
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
6633
- if (falsyMatch) {
6634
- const varName = falsyMatch[1];
6635
- const val = variables[varName];
6636
- return !val || val === "" || val === "false";
6685
+ const cutoff = lockExpiryCutoff();
6686
+ const timestamp = now();
6687
+ const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
6688
+ 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]);
6689
+ if (result.changes === 0) {
6690
+ const current = getTask(id, d);
6691
+ if (!current)
6692
+ throw new TaskNotFoundError(id);
6693
+ if (current.status === "completed" || current.status === "cancelled") {
6694
+ return {
6695
+ success: false,
6696
+ error: `Task is ${current.status} and cannot be locked`
6697
+ };
6698
+ }
6699
+ if (current.locked_by && !isLockExpired(current.locked_at)) {
6700
+ return {
6701
+ success: false,
6702
+ locked_by: current.locked_by,
6703
+ locked_at: current.locked_at,
6704
+ error: `Task is locked by ${current.locked_by}`
6705
+ };
6706
+ }
6707
+ return {
6708
+ success: false,
6709
+ error: `Task ${id} could not be locked because it changed during lock acquisition`
6710
+ };
6637
6711
  }
6638
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
6639
- if (truthyMatch) {
6640
- const varName = truthyMatch[1];
6641
- const val = variables[varName];
6642
- return !!val && val !== "" && val !== "false";
6712
+ logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
6713
+ return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
6714
+ }
6715
+ function unlockTask(id, agentId, db) {
6716
+ const d = db || getDatabase();
6717
+ const task = getTask(id, d);
6718
+ if (!task)
6719
+ throw new TaskNotFoundError(id);
6720
+ if (agentId && task.locked_by && task.locked_by !== agentId) {
6721
+ throw new LockError(id, task.locked_by);
6643
6722
  }
6723
+ const timestamp = now();
6724
+ d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
6725
+ WHERE id = ?`, [timestamp, id]);
6644
6726
  return true;
6645
6727
  }
6646
- function exportTemplate(id, db) {
6728
+ function getTaskLockStatus(id, db) {
6647
6729
  const d = db || getDatabase();
6648
- const template = getTemplateWithTasks(id, d);
6649
- if (!template)
6650
- throw new Error(`Template not found: ${id}`);
6730
+ const task = getTask(id, d);
6731
+ if (!task)
6732
+ throw new TaskNotFoundError(id);
6733
+ const expired = isLockExpired(task.locked_at);
6651
6734
  return {
6652
- name: template.name,
6653
- title_pattern: template.title_pattern,
6654
- description: template.description,
6655
- priority: template.priority,
6656
- tags: template.tags,
6657
- variables: template.variables,
6658
- project_id: template.project_id,
6659
- plan_id: template.plan_id,
6660
- metadata: template.metadata,
6661
- tasks: template.tasks.map((t) => ({
6662
- position: t.position,
6663
- title_pattern: t.title_pattern,
6664
- description: t.description,
6665
- priority: t.priority,
6666
- tags: t.tags,
6667
- task_type: t.task_type,
6668
- condition: t.condition,
6669
- include_template_id: t.include_template_id,
6670
- depends_on_positions: t.depends_on_positions,
6671
- metadata: t.metadata
6672
- }))
6735
+ task_id: id,
6736
+ locked: !!task.locked_by && !expired,
6737
+ locked_by: task.locked_by,
6738
+ locked_at: task.locked_at,
6739
+ expires_at: lockExpiresAt(task.locked_at),
6740
+ expired
6673
6741
  };
6674
6742
  }
6675
- function importTemplate(json, db) {
6676
- const d = db || getDatabase();
6677
- const taskInputs = (json.tasks || []).map((t) => ({
6678
- title_pattern: t.title_pattern,
6679
- description: t.description ?? undefined,
6680
- priority: t.priority,
6681
- tags: t.tags,
6682
- task_type: t.task_type ?? undefined,
6683
- condition: t.condition ?? undefined,
6684
- include_template_id: t.include_template_id ?? undefined,
6685
- depends_on: t.depends_on_positions,
6686
- metadata: t.metadata
6687
- }));
6688
- return createTemplate({
6689
- name: json.name,
6690
- title_pattern: json.title_pattern,
6691
- description: json.description ?? undefined,
6692
- priority: json.priority,
6693
- tags: json.tags,
6694
- variables: json.variables,
6695
- project_id: json.project_id ?? undefined,
6696
- plan_id: json.plan_id ?? undefined,
6697
- metadata: json.metadata,
6698
- tasks: taskInputs
6699
- }, d);
6700
- }
6701
- function getTemplateVersion(id, version, db) {
6743
+ function claimNextTask(agentId, filters, db) {
6702
6744
  const d = db || getDatabase();
6703
- const resolved = resolveTemplateId(id, d);
6704
- if (!resolved)
6705
- return null;
6706
- const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
6707
- return row || null;
6745
+ const MAX_ATTEMPTS = 25;
6746
+ const tried = new Set;
6747
+ for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
6748
+ const outcome = d.transaction(() => {
6749
+ const task = getNextTask(agentId, filters, d);
6750
+ if (!task)
6751
+ return { done: true, task: null };
6752
+ if (tried.has(task.id))
6753
+ return { done: true, task: null };
6754
+ tried.add(task.id);
6755
+ try {
6756
+ return { done: true, task: startTask(task.id, agentId, d) };
6757
+ } catch {
6758
+ return { done: false, task: null };
6759
+ }
6760
+ })();
6761
+ if (outcome.done)
6762
+ return outcome.task;
6763
+ }
6764
+ return null;
6708
6765
  }
6709
- function listTemplateVersions(id, db) {
6766
+ function getNextTask(agentId, filters, db) {
6710
6767
  const d = db || getDatabase();
6711
- const resolved = resolveTemplateId(id, d);
6712
- if (!resolved)
6713
- return [];
6714
- return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
6715
- }
6716
- function resolveVariables(templateVars, provided) {
6717
- const merged = { ...provided };
6718
- for (const v of templateVars) {
6719
- if (merged[v.name] === undefined && v.default !== undefined) {
6720
- merged[v.name] = v.default;
6721
- }
6768
+ clearExpiredLocks(d);
6769
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
6770
+ const params = [lockExpiryCutoff()];
6771
+ if (filters?.project_id) {
6772
+ conditions.push("project_id = ?");
6773
+ params.push(filters.project_id);
6722
6774
  }
6723
- const missing = [];
6724
- for (const v of templateVars) {
6725
- if (v.required && merged[v.name] === undefined) {
6726
- missing.push(v.name);
6727
- }
6775
+ if (filters?.task_list_id) {
6776
+ conditions.push("task_list_id = ?");
6777
+ params.push(filters.task_list_id);
6728
6778
  }
6729
- if (missing.length > 0) {
6730
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
6779
+ if (filters?.plan_id) {
6780
+ conditions.push("plan_id = ?");
6781
+ params.push(filters.plan_id);
6731
6782
  }
6732
- return merged;
6783
+ if (filters?.tags && filters.tags.length > 0) {
6784
+ const placeholders = filters.tags.map(() => "?").join(",");
6785
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
6786
+ params.push(...filters.tags);
6787
+ }
6788
+ 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')");
6789
+ const where = conditions.join(" AND ");
6790
+ let recentProjectIds = [];
6791
+ if (agentId) {
6792
+ 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);
6793
+ recentProjectIds = recentRows.map((r) => r.project_id);
6794
+ }
6795
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
6796
+ if (agentId) {
6797
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
6798
+ params.push(agentId);
6799
+ }
6800
+ if (recentProjectIds.length > 0) {
6801
+ const placeholders = recentProjectIds.map(() => "?").join(",");
6802
+ sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
6803
+ params.push(...recentProjectIds);
6804
+ }
6805
+ 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`;
6806
+ const row = d.query(sql).get(...params);
6807
+ return row ? rowToTask(row) : null;
6733
6808
  }
6734
- function substituteVars(text, variables) {
6735
- let result = text;
6736
- for (const [key, val] of Object.entries(variables)) {
6737
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
6809
+ function getActiveWork(filters, db) {
6810
+ const d = db || getDatabase();
6811
+ clearExpiredLocks(d);
6812
+ const conditions = ["status = 'in_progress'"];
6813
+ const params = [];
6814
+ if (filters?.project_id) {
6815
+ conditions.push("project_id = ?");
6816
+ params.push(filters.project_id);
6738
6817
  }
6739
- return result;
6818
+ if (filters?.task_list_id) {
6819
+ conditions.push("task_list_id = ?");
6820
+ params.push(filters.task_list_id);
6821
+ }
6822
+ const where = conditions.join(" AND ");
6823
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
6824
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
6825
+ updated_at DESC`).all(...params);
6826
+ return rows;
6740
6827
  }
6741
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
6828
+ function getTasksChangedSince(since, filters, db) {
6742
6829
  const d = db || getDatabase();
6743
- const template = getTemplateWithTasks(templateId, d);
6744
- if (!template)
6745
- throw new Error(`Template not found: ${templateId}`);
6746
- const visited = _visitedTemplateIds || new Set;
6747
- if (visited.has(template.id)) {
6748
- throw new Error(`Circular template reference detected: ${template.id}`);
6830
+ const conditions = ["updated_at > ?"];
6831
+ const params = [since];
6832
+ if (filters?.project_id) {
6833
+ conditions.push("project_id = ?");
6834
+ params.push(filters.project_id);
6749
6835
  }
6750
- visited.add(template.id);
6751
- const resolved = resolveVariables(template.variables, variables);
6752
- if (template.tasks.length === 0) {
6753
- const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
6754
- const task = createTask(input, d);
6755
- return [task];
6836
+ if (filters?.task_list_id) {
6837
+ conditions.push("task_list_id = ?");
6838
+ params.push(filters.task_list_id);
6756
6839
  }
6757
- const createdTasks = [];
6758
- const positionToId = new Map;
6759
- const skippedPositions = new Set;
6760
- for (const tt of template.tasks) {
6761
- if (tt.include_template_id) {
6762
- const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
6763
- createdTasks.push(...includedTasks);
6764
- if (includedTasks.length > 0) {
6765
- positionToId.set(tt.position, includedTasks[0].id);
6766
- } else {
6767
- skippedPositions.add(tt.position);
6840
+ const where = conditions.join(" AND ");
6841
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
6842
+ return rows.map(rowToTask);
6843
+ }
6844
+ function failTask(id, agentId, reason, options, db) {
6845
+ const d = db || getDatabase();
6846
+ const databasePath = databasePathFromDatabase(d);
6847
+ const task = getTask(id, d);
6848
+ if (!task)
6849
+ throw new TaskNotFoundError(id);
6850
+ const meta = {
6851
+ ...task.metadata,
6852
+ _failure: {
6853
+ reason: reason || "Unknown failure",
6854
+ error_code: options?.error_code || null,
6855
+ failed_by: agentId || null,
6856
+ failed_at: now(),
6857
+ retry_requested: options?.retry || false
6858
+ }
6859
+ };
6860
+ const timestamp = now();
6861
+ const failTx = d.transaction(() => {
6862
+ const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
6863
+ WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
6864
+ if (res.changes === 0) {
6865
+ const current = getTask(id, d);
6866
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
6867
+ }
6868
+ });
6869
+ failTx();
6870
+ const failedTask = {
6871
+ ...task,
6872
+ status: "failed",
6873
+ locked_by: null,
6874
+ locked_at: null,
6875
+ metadata: meta,
6876
+ version: task.version + 1,
6877
+ updated_at: timestamp
6878
+ };
6879
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
6880
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
6881
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
6882
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
6883
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
6884
+ let retryTask;
6885
+ if (options?.retry) {
6886
+ const retryCount = (task.retry_count || 0) + 1;
6887
+ const maxRetries = task.max_retries || 3;
6888
+ if (retryCount > maxRetries) {
6889
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
6890
+ JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
6891
+ id
6892
+ ]);
6893
+ } else {
6894
+ const backoffMinutes = Math.pow(5, retryCount - 1);
6895
+ const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
6896
+ let title = task.title;
6897
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
6898
+ title = title.slice(task.short_id.length + 2);
6768
6899
  }
6769
- continue;
6900
+ retryTask = createTask({
6901
+ title,
6902
+ description: task.description ?? undefined,
6903
+ priority: task.priority,
6904
+ project_id: task.project_id ?? undefined,
6905
+ task_list_id: task.task_list_id ?? undefined,
6906
+ plan_id: task.plan_id ?? undefined,
6907
+ assigned_to: task.assigned_to ?? undefined,
6908
+ tags: task.tags,
6909
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
6910
+ estimated_minutes: task.estimated_minutes ?? undefined,
6911
+ recurrence_rule: task.recurrence_rule ?? undefined,
6912
+ due_at: retryAfter
6913
+ }, d);
6914
+ d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
6770
6915
  }
6771
- if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
6772
- skippedPositions.add(tt.position);
6773
- continue;
6916
+ }
6917
+ return { task: failedTask, retryTask };
6918
+ }
6919
+ function getStaleTasks(staleQuery = 30, filters, db) {
6920
+ const d = db || getDatabase();
6921
+ const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
6922
+ const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
6923
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
6924
+ const conditions = [
6925
+ "status = 'in_progress'",
6926
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
6927
+ ];
6928
+ const params = [cutoff, cutoff];
6929
+ if (effectiveFilters?.project_id) {
6930
+ conditions.push("project_id = ?");
6931
+ params.push(effectiveFilters.project_id);
6932
+ }
6933
+ if (effectiveFilters?.task_list_id) {
6934
+ conditions.push("task_list_id = ?");
6935
+ params.push(effectiveFilters.task_list_id);
6936
+ }
6937
+ const where = conditions.join(" AND ");
6938
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
6939
+ return rows.map(rowToTask);
6940
+ }
6941
+ function stealTask(agentId, opts, db) {
6942
+ const d = db || getDatabase();
6943
+ const databasePath = databasePathFromDatabase(d);
6944
+ const staleMinutes = opts?.stale_minutes ?? 30;
6945
+ const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
6946
+ if (staleTasks.length === 0)
6947
+ return null;
6948
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
6949
+ staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
6950
+ const target = staleTasks[0];
6951
+ const timestamp = now();
6952
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
6953
+ const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
6954
+ 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]);
6955
+ if (result.changes === 0)
6956
+ return null;
6957
+ logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
6958
+ logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
6959
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
6960
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
6961
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
6962
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
6963
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
6964
+ return stolenTask;
6965
+ }
6966
+ function claimOrSteal(agentId, filters, db) {
6967
+ const d = db || getDatabase();
6968
+ const tx = d.transaction(() => {
6969
+ const next = getNextTask(agentId, filters, d);
6970
+ if (next) {
6971
+ const started = startTask(next.id, agentId, d);
6972
+ return { task: started, stolen: false };
6774
6973
  }
6775
- let title = tt.title_pattern;
6776
- let desc = tt.description;
6777
- title = substituteVars(title, resolved);
6778
- if (desc)
6779
- desc = substituteVars(desc, resolved);
6780
- const task = createTask({
6781
- title,
6782
- description: desc ?? undefined,
6783
- priority: tt.priority,
6784
- tags: tt.tags,
6785
- task_type: tt.task_type ?? undefined,
6786
- project_id: projectId,
6787
- task_list_id: taskListId,
6788
- metadata: tt.metadata
6789
- }, d);
6790
- createdTasks.push(task);
6791
- positionToId.set(tt.position, task.id);
6974
+ const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
6975
+ if (stolen)
6976
+ return { task: stolen, stolen: true };
6977
+ return null;
6978
+ });
6979
+ return tx();
6980
+ }
6981
+ function spawnNextRecurrence(completedTask, db, completedAt) {
6982
+ const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
6983
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
6984
+ let title = completedTask.title;
6985
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
6986
+ title = title.slice(completedTask.short_id.length + 2);
6792
6987
  }
6793
- for (const tt of template.tasks) {
6794
- if (skippedPositions.has(tt.position))
6795
- continue;
6796
- if (tt.include_template_id)
6797
- continue;
6798
- const deps = tt.depends_on_positions;
6799
- for (const depPos of deps) {
6800
- if (skippedPositions.has(depPos))
6801
- continue;
6802
- const taskId = positionToId.get(tt.position);
6803
- const depId = positionToId.get(depPos);
6804
- if (taskId && depId) {
6805
- addDependency(taskId, depId, d);
6806
- }
6988
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
6989
+ return createTask({
6990
+ title,
6991
+ description: completedTask.description ?? undefined,
6992
+ priority: completedTask.priority,
6993
+ project_id: completedTask.project_id ?? undefined,
6994
+ task_list_id: completedTask.task_list_id ?? undefined,
6995
+ plan_id: completedTask.plan_id ?? undefined,
6996
+ assigned_to: completedTask.assigned_to ?? undefined,
6997
+ tags: completedTask.tags,
6998
+ metadata: completedTask.metadata,
6999
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
7000
+ sla_minutes: completedTask.sla_minutes ?? undefined,
7001
+ recurrence_rule: completedTask.recurrence_rule,
7002
+ recurrence_parent_id: recurrenceParentId,
7003
+ due_at: dueAt
7004
+ }, db);
7005
+ }
7006
+ var MAX_SPAWN_DEPTH = 10;
7007
+ var init_task_lifecycle = __esm(() => {
7008
+ init_types();
7009
+ init_database();
7010
+ init_completion_guard();
7011
+ init_event_emission_safety();
7012
+ init_event_hooks();
7013
+ init_shared_events();
7014
+ init_audit();
7015
+ init_recurrence();
7016
+ init_webhooks();
7017
+ init_templates();
7018
+ init_task_crud();
7019
+ init_task_graph();
7020
+ });
7021
+
7022
+ // src/db/task-crud.ts
7023
+ function rowToTask(row) {
7024
+ return {
7025
+ ...row,
7026
+ tags: JSON.parse(row.tags || "[]"),
7027
+ metadata: JSON.parse(row.metadata || "{}"),
7028
+ status: row.status,
7029
+ priority: row.priority,
7030
+ requires_approval: !!row.requires_approval
7031
+ };
7032
+ }
7033
+ function insertTaskTags(taskId, tags, db) {
7034
+ if (tags.length === 0)
7035
+ return;
7036
+ const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
7037
+ for (const tag of tags) {
7038
+ if (tag)
7039
+ stmt.run(taskId, tag);
7040
+ }
7041
+ }
7042
+ function replaceTaskTags(taskId, tags, db) {
7043
+ db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
7044
+ insertTaskTags(taskId, tags, db);
7045
+ }
7046
+ function addMetadataConditions(metadata, conditions, params) {
7047
+ if (!metadata)
7048
+ return;
7049
+ for (const [key, value] of Object.entries(metadata)) {
7050
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
7051
+ throw new Error(`Invalid metadata filter key: ${key}`);
6807
7052
  }
7053
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
7054
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
6808
7055
  }
6809
- return createdTasks;
6810
7056
  }
6811
- function previewTemplate(templateId, variables, db) {
7057
+ function createTask(input, db) {
6812
7058
  const d = db || getDatabase();
6813
- const template = getTemplateWithTasks(templateId, d);
6814
- if (!template)
6815
- throw new Error(`Template not found: ${templateId}`);
6816
- const resolved = resolveVariables(template.variables, variables);
6817
- const tasks = [];
6818
- if (template.tasks.length === 0) {
6819
- tasks.push({
6820
- position: 0,
6821
- title: substituteVars(template.title_pattern, resolved),
6822
- description: template.description ? substituteVars(template.description, resolved) : null,
6823
- priority: template.priority,
6824
- tags: template.tags,
6825
- task_type: null,
6826
- depends_on_positions: []
6827
- });
6828
- } else {
6829
- for (const tt of template.tasks) {
6830
- if (tt.condition && !evaluateCondition(tt.condition, resolved))
6831
- continue;
6832
- tasks.push({
6833
- position: tt.position,
6834
- title: substituteVars(tt.title_pattern, resolved),
6835
- description: tt.description ? substituteVars(tt.description, resolved) : null,
6836
- priority: tt.priority,
6837
- tags: tt.tags,
6838
- task_type: tt.task_type,
6839
- depends_on_positions: tt.depends_on_positions
6840
- });
7059
+ const timestamp = now();
7060
+ const tags = input.tags || [];
7061
+ const machineId = currentStorageMachineId(d);
7062
+ const assignedBy = input.assigned_by || input.agent_id;
7063
+ const assignedFromProject = input.assigned_from_project || null;
7064
+ let id = uuid();
7065
+ for (let attempt = 0;attempt < 3; attempt++) {
7066
+ try {
7067
+ 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)
7068
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7069
+ id,
7070
+ null,
7071
+ input.project_id || null,
7072
+ input.parent_id || null,
7073
+ input.plan_id || null,
7074
+ input.task_list_id || null,
7075
+ input.cycle_id || null,
7076
+ input.title,
7077
+ input.description || null,
7078
+ input.status || "pending",
7079
+ input.priority || "medium",
7080
+ input.agent_id || null,
7081
+ input.assigned_to || null,
7082
+ input.session_id || null,
7083
+ input.working_dir || null,
7084
+ JSON.stringify(tags),
7085
+ JSON.stringify(input.metadata || {}),
7086
+ timestamp,
7087
+ timestamp,
7088
+ input.due_at || null,
7089
+ input.estimated_minutes || null,
7090
+ input.sla_minutes ?? null,
7091
+ input.confidence ?? null,
7092
+ input.retry_count ?? 0,
7093
+ input.max_retries ?? 3,
7094
+ input.retry_after ?? null,
7095
+ input.requires_approval ? 1 : 0,
7096
+ null,
7097
+ null,
7098
+ input.recurrence_rule || null,
7099
+ input.recurrence_parent_id || null,
7100
+ input.spawns_template_id || null,
7101
+ input.reason || null,
7102
+ input.spawned_from_session || null,
7103
+ assignedBy || null,
7104
+ assignedFromProject || null,
7105
+ input.task_type || null,
7106
+ machineId
7107
+ ]);
7108
+ break;
7109
+ } catch (e) {
7110
+ if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
7111
+ id = uuid();
7112
+ continue;
7113
+ }
7114
+ throw e;
6841
7115
  }
6842
7116
  }
6843
- return {
6844
- template_id: template.id,
6845
- template_name: template.name,
6846
- description: template.description,
6847
- variables: template.variables,
6848
- resolved_variables: resolved,
6849
- tasks
6850
- };
6851
- }
6852
- var init_templates = __esm(() => {
6853
- init_database();
6854
- init_tasks();
6855
- init_storage_tombstones();
6856
- });
6857
-
6858
- // src/db/task-graph.ts
6859
- function addDependency(taskId, dependsOn, db) {
6860
- const d = db || getDatabase();
6861
- if (!getTask(taskId, d))
6862
- throw new TaskNotFoundError(taskId);
6863
- if (!getTask(dependsOn, d))
6864
- throw new TaskNotFoundError(dependsOn);
6865
- if (wouldCreateCycle(taskId, dependsOn, d)) {
6866
- throw new DependencyCycleError(taskId, dependsOn);
7117
+ if (tags.length > 0) {
7118
+ insertTaskTags(id, tags, d);
6867
7119
  }
6868
- d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
6869
- }
6870
- function removeDependency(taskId, dependsOn, db) {
6871
- const d = db || getDatabase();
6872
- const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
6873
- return result.changes > 0;
6874
- }
6875
- function getTaskDependencies(taskId, db) {
6876
- const d = db || getDatabase();
6877
- return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
7120
+ const task = getTask(id, d);
7121
+ const payload = taskEventData(task);
7122
+ const databasePath = databasePathFromDatabase(d);
7123
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
7124
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
7125
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
7126
+ return task;
6878
7127
  }
6879
- function getTaskDependents(taskId, db) {
7128
+ function getTask(id, db) {
6880
7129
  const d = db || getDatabase();
6881
- return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
7130
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
7131
+ if (!row)
7132
+ return null;
7133
+ return rowToTask(row);
6882
7134
  }
6883
- function cloneTask(taskId, overrides, db) {
7135
+ function getTaskWithRelations(id, db) {
6884
7136
  const d = db || getDatabase();
6885
- const source = getTask(taskId, d);
6886
- if (!source)
6887
- throw new TaskNotFoundError(taskId);
6888
- const input = {
6889
- title: overrides?.title ?? source.title,
6890
- description: overrides?.description ?? source.description ?? undefined,
6891
- priority: overrides?.priority ?? source.priority,
6892
- project_id: overrides?.project_id ?? source.project_id ?? undefined,
6893
- parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
6894
- plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
6895
- task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
6896
- status: overrides?.status ?? "pending",
6897
- agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
6898
- assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
6899
- tags: overrides?.tags ?? source.tags,
6900
- metadata: overrides?.metadata ?? source.metadata,
6901
- estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
6902
- recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
7137
+ const task = getTask(id, d);
7138
+ if (!task)
7139
+ return null;
7140
+ const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
7141
+ const subtasks = subtaskRows.map(rowToTask);
7142
+ const depRows = d.query(`SELECT t.* FROM tasks t
7143
+ JOIN task_dependencies td ON td.depends_on = t.id
7144
+ WHERE td.task_id = ?`).all(id);
7145
+ const dependencies = depRows.map(rowToTask);
7146
+ const blockedByRows = d.query(`SELECT t.* FROM tasks t
7147
+ JOIN task_dependencies td ON td.task_id = t.id
7148
+ WHERE td.depends_on = ?`).all(id);
7149
+ const blocked_by = blockedByRows.map(rowToTask);
7150
+ const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
7151
+ const parent = task.parent_id ? getTask(task.parent_id, d) : null;
7152
+ const checklist = getChecklist(id, d);
7153
+ return {
7154
+ ...task,
7155
+ subtasks,
7156
+ dependencies,
7157
+ blocked_by,
7158
+ comments,
7159
+ parent,
7160
+ checklist
6903
7161
  };
6904
- return createTask(input, d);
6905
7162
  }
6906
- function getTaskGraph(taskId, direction = "both", db) {
7163
+ function listTasks(filter = {}, db) {
6907
7164
  const d = db || getDatabase();
6908
- const task = getTask(taskId, d);
6909
- if (!task)
6910
- throw new TaskNotFoundError(taskId);
6911
- function toNode(t) {
6912
- const deps = getTaskDependencies(t.id, d);
6913
- const hasUnfinishedDeps = deps.some((dep) => {
6914
- const depTask = getTask(dep.depends_on, d);
6915
- return depTask && depTask.status !== "completed";
6916
- });
6917
- return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
7165
+ const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
7166
+ clearExpiredLocks2(d);
7167
+ const conditions = [];
7168
+ const params = [];
7169
+ if (filter.project_id) {
7170
+ conditions.push("project_id = ?");
7171
+ params.push(filter.project_id);
6918
7172
  }
6919
- function buildUp(id, visited) {
6920
- if (visited.has(id))
6921
- return [];
6922
- visited.add(id);
6923
- const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
6924
- return deps.map((dep) => {
6925
- const depTask = getTask(dep.depends_on, d);
6926
- if (!depTask)
6927
- return null;
6928
- return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
6929
- }).filter(Boolean);
7173
+ if (filter.ids && filter.ids.length > 0) {
7174
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
7175
+ params.push(...filter.ids);
6930
7176
  }
6931
- function buildDown(id, visited) {
6932
- if (visited.has(id))
6933
- return [];
6934
- visited.add(id);
6935
- const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
6936
- return dependents.map((dep) => {
6937
- const depTask = getTask(dep.task_id, d);
6938
- if (!depTask)
6939
- return null;
6940
- return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
6941
- }).filter(Boolean);
7177
+ if (filter.parent_id !== undefined) {
7178
+ if (filter.parent_id === null) {
7179
+ conditions.push("parent_id IS NULL");
7180
+ } else {
7181
+ conditions.push("parent_id = ?");
7182
+ params.push(filter.parent_id);
7183
+ }
6942
7184
  }
6943
- const rootNode = toNode(task);
6944
- const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
6945
- const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
6946
- return { task: rootNode, depends_on, blocks };
6947
- }
6948
- function moveTask(taskId, target, db) {
6949
- const d = db || getDatabase();
6950
- const task = getTask(taskId, d);
6951
- if (!task)
6952
- throw new TaskNotFoundError(taskId);
6953
- const sets = ["updated_at = ?", "version = version + 1"];
6954
- const params = [now()];
6955
- if (target.task_list_id !== undefined) {
6956
- sets.push("task_list_id = ?");
6957
- params.push(target.task_list_id);
7185
+ if (filter.status) {
7186
+ if (Array.isArray(filter.status)) {
7187
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
7188
+ params.push(...filter.status);
7189
+ } else {
7190
+ conditions.push("status = ?");
7191
+ params.push(filter.status);
7192
+ }
6958
7193
  }
6959
- if (target.project_id !== undefined) {
6960
- sets.push("project_id = ?");
6961
- params.push(target.project_id);
7194
+ if (filter.priority) {
7195
+ if (Array.isArray(filter.priority)) {
7196
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
7197
+ params.push(...filter.priority);
7198
+ } else {
7199
+ conditions.push("priority = ?");
7200
+ params.push(filter.priority);
7201
+ }
6962
7202
  }
6963
- if (target.plan_id !== undefined) {
6964
- sets.push("plan_id = ?");
6965
- params.push(target.plan_id);
7203
+ if (filter.assigned_to) {
7204
+ conditions.push("assigned_to = ?");
7205
+ params.push(filter.assigned_to);
6966
7206
  }
6967
- params.push(taskId);
6968
- d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
6969
- return getTask(taskId, d);
6970
- }
6971
- function wouldCreateCycle(taskId, dependsOn, db) {
6972
- const visited = new Set;
6973
- const queue = [dependsOn];
6974
- while (queue.length > 0) {
6975
- const current = queue.shift();
6976
- if (current === taskId)
6977
- return true;
6978
- if (visited.has(current))
6979
- continue;
6980
- visited.add(current);
6981
- const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
6982
- for (const dep of deps) {
6983
- queue.push(dep.depends_on);
7207
+ if (filter.agent_id) {
7208
+ conditions.push("agent_id = ?");
7209
+ params.push(filter.agent_id);
7210
+ }
7211
+ if (filter.session_id) {
7212
+ conditions.push("session_id = ?");
7213
+ params.push(filter.session_id);
7214
+ }
7215
+ if (filter.tags && filter.tags.length > 0) {
7216
+ const placeholders = filter.tags.map(() => "?").join(",");
7217
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
7218
+ params.push(...filter.tags);
7219
+ }
7220
+ if (filter.plan_id) {
7221
+ conditions.push("plan_id = ?");
7222
+ params.push(filter.plan_id);
7223
+ }
7224
+ if (filter.task_list_id) {
7225
+ conditions.push("task_list_id = ?");
7226
+ params.push(filter.task_list_id);
7227
+ }
7228
+ if (filter.has_recurrence === true) {
7229
+ conditions.push("recurrence_rule IS NOT NULL");
7230
+ } else if (filter.has_recurrence === false) {
7231
+ conditions.push("recurrence_rule IS NULL");
7232
+ }
7233
+ if (filter.task_type) {
7234
+ if (Array.isArray(filter.task_type)) {
7235
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
7236
+ params.push(...filter.task_type);
7237
+ } else {
7238
+ conditions.push("task_type = ?");
7239
+ params.push(filter.task_type);
6984
7240
  }
6985
7241
  }
6986
- return false;
7242
+ addMetadataConditions(filter.metadata, conditions, params);
7243
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
7244
+ if (filter.cursor) {
7245
+ try {
7246
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
7247
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
7248
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
7249
+ } catch {}
7250
+ }
7251
+ if (!filter.include_archived) {
7252
+ conditions.push("archived_at IS NULL");
7253
+ }
7254
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7255
+ let limitClause = "";
7256
+ if (filter.limit) {
7257
+ limitClause = " LIMIT ?";
7258
+ params.push(filter.limit);
7259
+ if (!filter.cursor && filter.offset) {
7260
+ limitClause += " OFFSET ?";
7261
+ params.push(filter.offset);
7262
+ }
7263
+ }
7264
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
7265
+ return rows.map(rowToTask);
6987
7266
  }
6988
- var init_task_graph = __esm(() => {
6989
- init_types();
6990
- init_database();
6991
- init_task_crud();
6992
- });
6993
-
6994
- // src/db/task-lifecycle.ts
6995
- function lockExpiresAt(lockedAt) {
6996
- if (!lockedAt)
6997
- return null;
6998
- return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
7267
+ function getTaskByFingerprint(fingerprint, db) {
7268
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
7269
+ return tasks[0] ?? null;
6999
7270
  }
7000
- function assertStartable(task, agentId) {
7001
- if (task.status === "pending")
7002
- return;
7003
- if (task.status === "in_progress")
7004
- return;
7005
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
7271
+ function mergeTaskMetadata(current, next, fingerprint) {
7272
+ return {
7273
+ ...current,
7274
+ ...next ?? {},
7275
+ fingerprint
7276
+ };
7006
7277
  }
7007
- function getBlockingDeps(id, db) {
7278
+ function upsertTaskByFingerprint(input, db) {
7008
7279
  const d = db || getDatabase();
7009
- const deps = getTaskDependencies(id, d);
7010
- if (deps.length === 0)
7011
- return [];
7012
- const blocking = [];
7013
- for (const dep of deps) {
7014
- const task = getTask(dep.depends_on, d);
7015
- if (task && task.status !== "completed")
7016
- blocking.push(task);
7017
- }
7018
- return blocking;
7280
+ const fingerprint = input.fingerprint.trim();
7281
+ if (!fingerprint)
7282
+ throw new Error("fingerprint is required");
7283
+ const tx = d.transaction(() => {
7284
+ const existing = getTaskByFingerprint(fingerprint, d);
7285
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
7286
+ if (!existing) {
7287
+ const task2 = createTask({ ...input, metadata }, d);
7288
+ return { task: task2, created: true };
7289
+ }
7290
+ const task = updateTask(existing.id, {
7291
+ version: existing.version,
7292
+ title: input.title,
7293
+ description: input.description,
7294
+ status: input.status,
7295
+ priority: input.priority,
7296
+ project_id: input.project_id,
7297
+ assigned_to: input.assigned_to,
7298
+ working_dir: input.working_dir,
7299
+ plan_id: input.plan_id,
7300
+ task_list_id: input.task_list_id,
7301
+ tags: input.tags,
7302
+ metadata,
7303
+ due_at: input.due_at,
7304
+ estimated_minutes: input.estimated_minutes,
7305
+ sla_minutes: input.sla_minutes,
7306
+ confidence: input.confidence,
7307
+ retry_count: input.retry_count,
7308
+ max_retries: input.max_retries,
7309
+ retry_after: input.retry_after,
7310
+ requires_approval: input.requires_approval,
7311
+ recurrence_rule: input.recurrence_rule,
7312
+ task_type: input.task_type
7313
+ }, d);
7314
+ return { task, created: false };
7315
+ });
7316
+ return tx();
7019
7317
  }
7020
- function startTask(id, agentId, db) {
7318
+ function countTasks(filter = {}, db) {
7021
7319
  const d = db || getDatabase();
7022
- const databasePath = databasePathFromDatabase(d);
7023
- const task = getTask(id, d);
7024
- if (!task)
7025
- throw new TaskNotFoundError(id);
7026
- assertStartable(task, agentId);
7027
- const blocking = getBlockingDeps(id, d);
7028
- if (blocking.length > 0) {
7029
- const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
7030
- emitLocalEventHooksQuiet({
7031
- type: "task.blocked",
7032
- payload: {
7033
- id,
7034
- agent_id: agentId,
7035
- title: task.title,
7036
- blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
7037
- },
7038
- databasePath
7039
- });
7040
- throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
7041
- }
7042
- const cutoff = lockExpiryCutoff();
7043
- const timestamp = now();
7044
- 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 = ?
7045
- 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]);
7046
- if (result.changes === 0) {
7047
- const current = getTask(id, d);
7048
- if (!current)
7049
- throw new TaskNotFoundError(id);
7050
- assertStartable(current, agentId);
7051
- if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
7052
- throw new LockError(id, current.locked_by);
7053
- }
7054
- throw new Error(`Task ${id} could not be started because it changed during claim`);
7320
+ const conditions = [];
7321
+ const params = [];
7322
+ if (filter.project_id) {
7323
+ conditions.push("project_id = ?");
7324
+ params.push(filter.project_id);
7055
7325
  }
7056
- logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
7057
- 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 };
7058
- const payload = taskEventData(startedTask, { agent_id: agentId });
7059
- dispatchWebhook2("task.started", payload, d).catch(() => {});
7060
- emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
7061
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
7062
- return startedTask;
7063
- }
7064
- function completeTask(id, agentId, db, options) {
7065
- const d = db || getDatabase();
7066
- const databasePath = databasePathFromDatabase(d);
7067
- const task = getTask(id, d);
7068
- if (!task)
7069
- throw new TaskNotFoundError(id);
7070
- if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
7071
- throw new LockError(id, task.locked_by);
7326
+ if (filter.ids && filter.ids.length > 0) {
7327
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
7328
+ params.push(...filter.ids);
7072
7329
  }
7073
- checkCompletionGuard(task, agentId || null, d);
7074
- 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;
7075
- const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
7076
- const completionMeta = {};
7077
- if (hasEvidence)
7078
- completionMeta._evidence = evidence;
7079
- if (options?.confidence !== undefined) {
7080
- completionMeta._completion = { confidence: options.confidence };
7330
+ if (filter.parent_id !== undefined) {
7331
+ if (filter.parent_id === null) {
7332
+ conditions.push("parent_id IS NULL");
7333
+ } else {
7334
+ conditions.push("parent_id = ?");
7335
+ params.push(filter.parent_id);
7336
+ }
7081
7337
  }
7082
- const hasMeta = Object.keys(completionMeta).length > 0;
7083
- const timestamp = options?.completed_at || now();
7084
- const confidence = options?.confidence !== undefined ? options.confidence : null;
7085
- const tx = d.transaction(() => {
7086
- if (hasMeta) {
7087
- const meta2 = { ...task.metadata, ...completionMeta };
7088
- const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
7089
- if (metaResult.changes === 0) {
7090
- const current = getTask(id, d);
7091
- throw new VersionConflictError(id, task.version, current?.version ?? -1);
7092
- }
7338
+ if (filter.status) {
7339
+ if (Array.isArray(filter.status)) {
7340
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
7341
+ params.push(...filter.status);
7342
+ } else {
7343
+ conditions.push("status = ?");
7344
+ params.push(filter.status);
7093
7345
  }
7094
- d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
7095
- WHERE id = ?`, [timestamp, confidence, timestamp, id]);
7096
- });
7097
- tx();
7098
- logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
7099
- const completedTaskForEvent = {
7100
- ...task,
7101
- status: "completed",
7102
- locked_by: null,
7103
- locked_at: null,
7104
- completed_at: timestamp,
7105
- confidence,
7106
- version: task.version + 1,
7107
- updated_at: timestamp,
7108
- metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
7109
- };
7110
- const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
7111
- dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
7112
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
7113
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
7114
- let spawnedTask = null;
7115
- if (task.recurrence_rule && !options?.skip_recurrence) {
7116
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
7117
7346
  }
7118
- let spawnedFromTemplate = null;
7119
- if (task.spawns_template_id) {
7120
- const spawnDepth = task.metadata?._spawn_depth || 0;
7121
- if (spawnDepth >= MAX_SPAWN_DEPTH) {
7122
- console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
7347
+ if (filter.priority) {
7348
+ if (Array.isArray(filter.priority)) {
7349
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
7350
+ params.push(...filter.priority);
7123
7351
  } else {
7124
- try {
7125
- const input = taskFromTemplate(task.spawns_template_id, {
7126
- project_id: task.project_id ?? undefined,
7127
- plan_id: task.plan_id ?? undefined,
7128
- task_list_id: task.task_list_id ?? undefined,
7129
- assigned_to: task.assigned_to ?? undefined
7130
- }, d);
7131
- input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
7132
- spawnedFromTemplate = createTask(input, d);
7133
- } catch {}
7352
+ conditions.push("priority = ?");
7353
+ params.push(filter.priority);
7134
7354
  }
7135
7355
  }
7136
- const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
7137
- if (spawnedTask) {
7138
- meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
7356
+ if (filter.assigned_to) {
7357
+ conditions.push("assigned_to = ?");
7358
+ params.push(filter.assigned_to);
7139
7359
  }
7140
- if (spawnedFromTemplate) {
7141
- meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
7360
+ if (filter.agent_id) {
7361
+ conditions.push("agent_id = ?");
7362
+ params.push(filter.agent_id);
7142
7363
  }
7143
- const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
7144
- JOIN task_dependencies td ON td.task_id = t.id
7145
- WHERE td.depends_on = ? AND t.status = 'pending'
7146
- AND NOT EXISTS (
7147
- SELECT 1 FROM task_dependencies td2
7148
- JOIN tasks dep2 ON dep2.id = td2.depends_on
7149
- WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
7150
- )`).all(id, id);
7151
- if (unblockedDeps.length > 0) {
7152
- meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
7153
- for (const dep of unblockedDeps) {
7154
- const depTask = getTask(dep.id, d);
7155
- const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
7156
- dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
7157
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
7158
- if (depTask)
7159
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
7364
+ if (filter.session_id) {
7365
+ conditions.push("session_id = ?");
7366
+ params.push(filter.session_id);
7367
+ }
7368
+ if (filter.tags && filter.tags.length > 0) {
7369
+ const placeholders = filter.tags.map(() => "?").join(",");
7370
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
7371
+ params.push(...filter.tags);
7372
+ }
7373
+ if (filter.plan_id) {
7374
+ conditions.push("plan_id = ?");
7375
+ params.push(filter.plan_id);
7376
+ }
7377
+ if (filter.task_list_id) {
7378
+ conditions.push("task_list_id = ?");
7379
+ params.push(filter.task_list_id);
7380
+ }
7381
+ if (filter.has_recurrence === true) {
7382
+ conditions.push("recurrence_rule IS NOT NULL");
7383
+ } else if (filter.has_recurrence === false) {
7384
+ conditions.push("recurrence_rule IS NULL");
7385
+ }
7386
+ if (filter.task_type) {
7387
+ if (Array.isArray(filter.task_type)) {
7388
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
7389
+ params.push(...filter.task_type);
7390
+ } else {
7391
+ conditions.push("task_type = ?");
7392
+ params.push(filter.task_type);
7160
7393
  }
7161
7394
  }
7162
- return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
7395
+ addMetadataConditions(filter.metadata, conditions, params);
7396
+ if (!filter.include_archived) {
7397
+ conditions.push("archived_at IS NULL");
7398
+ }
7399
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7400
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
7401
+ return row.count;
7163
7402
  }
7164
- function lockTask(id, agentId, db) {
7403
+ function updateTask(id, input, db) {
7165
7404
  const d = db || getDatabase();
7166
7405
  const task = getTask(id, d);
7167
7406
  if (!task)
7168
7407
  throw new TaskNotFoundError(id);
7169
- if (task.status === "completed" || task.status === "cancelled") {
7170
- return {
7171
- success: false,
7172
- error: `Task is ${task.status} and cannot be locked`
7173
- };
7174
- }
7175
- if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
7176
- const timestamp2 = now();
7177
- d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
7178
- logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
7179
- return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
7408
+ if (task.version !== input.version) {
7409
+ throw new VersionConflictError(id, input.version, task.version);
7180
7410
  }
7181
- const cutoff = lockExpiryCutoff();
7182
7411
  const timestamp = now();
7183
- const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
7184
- 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]);
7185
- if (result.changes === 0) {
7186
- const current = getTask(id, d);
7187
- if (!current)
7188
- throw new TaskNotFoundError(id);
7189
- if (current.status === "completed" || current.status === "cancelled") {
7190
- return {
7191
- success: false,
7192
- error: `Task is ${current.status} and cannot be locked`
7193
- };
7412
+ const completionTimestamp = input.completed_at ?? timestamp;
7413
+ const sets = ["version = version + 1", "updated_at = ?"];
7414
+ const params = [timestamp];
7415
+ if (input.title !== undefined) {
7416
+ sets.push("title = ?");
7417
+ params.push(input.title);
7418
+ }
7419
+ if (input.description !== undefined) {
7420
+ sets.push("description = ?");
7421
+ params.push(input.description);
7422
+ }
7423
+ if (input.status !== undefined) {
7424
+ if (input.status === "completed") {
7425
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
7194
7426
  }
7195
- if (current.locked_by && !isLockExpired(current.locked_at)) {
7196
- return {
7197
- success: false,
7198
- locked_by: current.locked_by,
7199
- locked_at: current.locked_at,
7200
- error: `Task is locked by ${current.locked_by}`
7201
- };
7427
+ sets.push("status = ?");
7428
+ params.push(input.status);
7429
+ if (input.status === "completed") {
7430
+ sets.push("completed_at = ?");
7431
+ params.push(completionTimestamp);
7432
+ sets.push("locked_by = NULL");
7433
+ sets.push("locked_at = NULL");
7434
+ } else if (task.status === "completed" && input.completed_at === undefined) {
7435
+ sets.push("completed_at = NULL");
7202
7436
  }
7203
- return {
7204
- success: false,
7205
- error: `Task ${id} could not be locked because it changed during lock acquisition`
7206
- };
7207
7437
  }
7208
- logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
7209
- return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
7210
- }
7211
- function unlockTask(id, agentId, db) {
7212
- const d = db || getDatabase();
7213
- const task = getTask(id, d);
7214
- if (!task)
7215
- throw new TaskNotFoundError(id);
7216
- if (agentId && task.locked_by && task.locked_by !== agentId) {
7217
- throw new LockError(id, task.locked_by);
7438
+ if (input.priority !== undefined) {
7439
+ sets.push("priority = ?");
7440
+ params.push(input.priority);
7218
7441
  }
7219
- const timestamp = now();
7220
- d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
7221
- WHERE id = ?`, [timestamp, id]);
7222
- return true;
7223
- }
7224
- function getTaskLockStatus(id, db) {
7225
- const d = db || getDatabase();
7226
- const task = getTask(id, d);
7227
- if (!task)
7228
- throw new TaskNotFoundError(id);
7229
- const expired = isLockExpired(task.locked_at);
7230
- return {
7231
- task_id: id,
7232
- locked: !!task.locked_by && !expired,
7233
- locked_by: task.locked_by,
7234
- locked_at: task.locked_at,
7235
- expires_at: lockExpiresAt(task.locked_at),
7236
- expired
7237
- };
7238
- }
7239
- function claimNextTask(agentId, filters, db) {
7240
- const d = db || getDatabase();
7241
- const tx = d.transaction(() => {
7242
- const task = getNextTask(agentId, filters, d);
7243
- if (!task)
7244
- return null;
7245
- return startTask(task.id, agentId, d);
7246
- });
7247
- return tx();
7248
- }
7249
- function getNextTask(agentId, filters, db) {
7250
- const d = db || getDatabase();
7251
- clearExpiredLocks(d);
7252
- const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
7253
- const params = [lockExpiryCutoff()];
7254
- if (filters?.project_id) {
7255
- conditions.push("project_id = ?");
7256
- params.push(filters.project_id);
7442
+ if (input.project_id !== undefined) {
7443
+ sets.push("project_id = ?");
7444
+ params.push(input.project_id);
7257
7445
  }
7258
- if (filters?.task_list_id) {
7259
- conditions.push("task_list_id = ?");
7260
- params.push(filters.task_list_id);
7446
+ if (input.assigned_to !== undefined) {
7447
+ sets.push("assigned_to = ?");
7448
+ params.push(input.assigned_to);
7261
7449
  }
7262
- if (filters?.plan_id) {
7263
- conditions.push("plan_id = ?");
7264
- params.push(filters.plan_id);
7450
+ if (input.working_dir !== undefined) {
7451
+ sets.push("working_dir = ?");
7452
+ params.push(input.working_dir);
7265
7453
  }
7266
- if (filters?.tags && filters.tags.length > 0) {
7267
- const placeholders = filters.tags.map(() => "?").join(",");
7268
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
7269
- params.push(...filters.tags);
7454
+ if (input.tags !== undefined) {
7455
+ sets.push("tags = ?");
7456
+ params.push(JSON.stringify(input.tags));
7270
7457
  }
7271
- 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')");
7272
- const where = conditions.join(" AND ");
7273
- let recentProjectIds = [];
7274
- if (agentId) {
7275
- 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);
7276
- recentProjectIds = recentRows.map((r) => r.project_id);
7458
+ if (input.metadata !== undefined) {
7459
+ sets.push("metadata = ?");
7460
+ params.push(JSON.stringify(input.metadata));
7277
7461
  }
7278
- let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
7279
- if (agentId) {
7280
- sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
7281
- params.push(agentId);
7462
+ if (input.plan_id !== undefined) {
7463
+ sets.push("plan_id = ?");
7464
+ params.push(input.plan_id);
7282
7465
  }
7283
- if (recentProjectIds.length > 0) {
7284
- const placeholders = recentProjectIds.map(() => "?").join(",");
7285
- sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
7286
- params.push(...recentProjectIds);
7466
+ if (input.task_list_id !== undefined) {
7467
+ sets.push("task_list_id = ?");
7468
+ params.push(input.task_list_id);
7287
7469
  }
7288
- 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`;
7289
- const row = d.query(sql).get(...params);
7290
- return row ? rowToTask(row) : null;
7291
- }
7292
- function getActiveWork(filters, db) {
7293
- const d = db || getDatabase();
7294
- clearExpiredLocks(d);
7295
- const conditions = ["status = 'in_progress'"];
7296
- const params = [];
7297
- if (filters?.project_id) {
7298
- conditions.push("project_id = ?");
7299
- params.push(filters.project_id);
7470
+ if (input.due_at !== undefined) {
7471
+ sets.push("due_at = ?");
7472
+ params.push(input.due_at);
7300
7473
  }
7301
- if (filters?.task_list_id) {
7302
- conditions.push("task_list_id = ?");
7303
- params.push(filters.task_list_id);
7474
+ if (input.estimated_minutes !== undefined) {
7475
+ sets.push("estimated_minutes = ?");
7476
+ params.push(input.estimated_minutes);
7304
7477
  }
7305
- const where = conditions.join(" AND ");
7306
- const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
7307
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
7308
- updated_at DESC`).all(...params);
7309
- return rows;
7310
- }
7311
- function getTasksChangedSince(since, filters, db) {
7312
- const d = db || getDatabase();
7313
- const conditions = ["updated_at > ?"];
7314
- const params = [since];
7315
- if (filters?.project_id) {
7316
- conditions.push("project_id = ?");
7317
- params.push(filters.project_id);
7478
+ if (input.sla_minutes !== undefined) {
7479
+ sets.push("sla_minutes = ?");
7480
+ params.push(input.sla_minutes);
7318
7481
  }
7319
- if (filters?.task_list_id) {
7320
- conditions.push("task_list_id = ?");
7321
- params.push(filters.task_list_id);
7482
+ if (input.actual_minutes !== undefined) {
7483
+ sets.push("actual_minutes = ?");
7484
+ params.push(input.actual_minutes);
7322
7485
  }
7323
- const where = conditions.join(" AND ");
7324
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
7325
- return rows.map(rowToTask);
7326
- }
7327
- function failTask(id, agentId, reason, options, db) {
7328
- const d = db || getDatabase();
7329
- const databasePath = databasePathFromDatabase(d);
7330
- const task = getTask(id, d);
7331
- if (!task)
7332
- throw new TaskNotFoundError(id);
7333
- const meta = {
7334
- ...task.metadata,
7335
- _failure: {
7336
- reason: reason || "Unknown failure",
7337
- error_code: options?.error_code || null,
7338
- failed_by: agentId || null,
7339
- failed_at: now(),
7340
- retry_requested: options?.retry || false
7486
+ if (input.completed_at !== undefined && input.status !== "completed") {
7487
+ sets.push("completed_at = ?");
7488
+ params.push(input.completed_at);
7489
+ }
7490
+ if (input.confidence !== undefined) {
7491
+ sets.push("confidence = ?");
7492
+ params.push(input.confidence);
7493
+ }
7494
+ if (input.retry_count !== undefined) {
7495
+ sets.push("retry_count = ?");
7496
+ params.push(input.retry_count);
7497
+ }
7498
+ if (input.max_retries !== undefined) {
7499
+ sets.push("max_retries = ?");
7500
+ params.push(input.max_retries);
7501
+ }
7502
+ if (input.retry_after !== undefined) {
7503
+ sets.push("retry_after = ?");
7504
+ params.push(input.retry_after);
7505
+ }
7506
+ if (input.requires_approval !== undefined) {
7507
+ sets.push("requires_approval = ?");
7508
+ params.push(input.requires_approval ? 1 : 0);
7509
+ }
7510
+ if (input.approved_by !== undefined) {
7511
+ sets.push("approved_by = ?");
7512
+ params.push(input.approved_by);
7513
+ sets.push("approved_at = ?");
7514
+ params.push(now());
7515
+ }
7516
+ if (input.recurrence_rule !== undefined) {
7517
+ sets.push("recurrence_rule = ?");
7518
+ params.push(input.recurrence_rule);
7519
+ }
7520
+ if (input.task_type !== undefined) {
7521
+ sets.push("task_type = ?");
7522
+ params.push(input.task_type ?? null);
7523
+ }
7524
+ params.push(id, input.version);
7525
+ const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
7526
+ if (result.changes === 0) {
7527
+ const current = getTask(id, d);
7528
+ throw new VersionConflictError(id, input.version, current?.version ?? -1);
7529
+ }
7530
+ if (input.tags !== undefined) {
7531
+ replaceTaskTags(id, input.tags, d);
7532
+ }
7533
+ const transitionedToCompleted = input.status === "completed" && task.status !== "completed";
7534
+ if (transitionedToCompleted && task.recurrence_rule) {
7535
+ try {
7536
+ const { spawnNextRecurrence: spawnNextRecurrence2 } = (init_task_lifecycle(), __toCommonJS(exports_task_lifecycle));
7537
+ spawnNextRecurrence2(task, d, completionTimestamp);
7538
+ } catch (e) {
7539
+ console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
7341
7540
  }
7342
- };
7343
- const timestamp = now();
7344
- d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
7345
- WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
7346
- const failedTask = {
7541
+ }
7542
+ const agentId = task.assigned_to || task.agent_id || null;
7543
+ if (input.status !== undefined && input.status !== task.status)
7544
+ logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
7545
+ if (input.priority !== undefined && input.priority !== task.priority)
7546
+ logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
7547
+ if (input.title !== undefined && input.title !== task.title)
7548
+ logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
7549
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
7550
+ logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
7551
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
7552
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
7553
+ if (input.approved_by !== undefined)
7554
+ logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
7555
+ const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
7556
+ const completedNow = input.status === "completed";
7557
+ const updatedTask = {
7347
7558
  ...task,
7348
- status: "failed",
7349
- locked_by: null,
7350
- locked_at: null,
7351
- metadata: meta,
7559
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
7560
+ tags: input.tags ?? task.tags,
7561
+ metadata: input.metadata ?? task.metadata,
7352
7562
  version: task.version + 1,
7353
- updated_at: timestamp
7563
+ updated_at: timestamp,
7564
+ locked_by: completedNow ? null : task.locked_by,
7565
+ locked_at: completedNow ? null : task.locked_at,
7566
+ completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
7567
+ sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
7568
+ actual_minutes: input.actual_minutes ?? task.actual_minutes,
7569
+ confidence: input.confidence !== undefined ? input.confidence : task.confidence,
7570
+ retry_count: input.retry_count ?? task.retry_count,
7571
+ max_retries: input.max_retries ?? task.max_retries,
7572
+ retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
7573
+ requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
7574
+ approved_by: input.approved_by ?? task.approved_by,
7575
+ approved_at: input.approved_by ? timestamp : task.approved_at
7354
7576
  };
7355
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
7356
- const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
7357
- dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
7358
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
7359
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
7360
- let retryTask;
7361
- if (options?.retry) {
7362
- const retryCount = (task.retry_count || 0) + 1;
7363
- const maxRetries = task.max_retries || 3;
7364
- if (retryCount > maxRetries) {
7365
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
7366
- JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
7367
- id
7368
- ]);
7369
- } else {
7370
- const backoffMinutes = Math.pow(5, retryCount - 1);
7371
- const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
7372
- let title = task.title;
7373
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
7374
- title = title.slice(task.short_id.length + 2);
7375
- }
7376
- retryTask = createTask({
7377
- title,
7378
- description: task.description ?? undefined,
7379
- priority: task.priority,
7380
- project_id: task.project_id ?? undefined,
7381
- task_list_id: task.task_list_id ?? undefined,
7382
- plan_id: task.plan_id ?? undefined,
7383
- assigned_to: task.assigned_to ?? undefined,
7384
- tags: task.tags,
7385
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
7386
- estimated_minutes: task.estimated_minutes ?? undefined,
7387
- recurrence_rule: task.recurrence_rule ?? undefined,
7388
- due_at: retryAfter
7389
- }, d);
7390
- d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
7391
- }
7577
+ const databasePath = databasePathFromDatabase(d);
7578
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
7579
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
7580
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7581
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
7582
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
7392
7583
  }
7393
- return { task: failedTask, retryTask };
7394
- }
7395
- function getStaleTasks(staleQuery = 30, filters, db) {
7396
- const d = db || getDatabase();
7397
- const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
7398
- const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
7399
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
7400
- const conditions = [
7401
- "status = 'in_progress'",
7402
- "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
7403
- ];
7404
- const params = [cutoff, cutoff];
7405
- if (effectiveFilters?.project_id) {
7406
- conditions.push("project_id = ?");
7407
- params.push(effectiveFilters.project_id);
7584
+ if (input.status !== undefined && input.status !== task.status) {
7585
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
7586
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
7587
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
7588
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
7408
7589
  }
7409
- if (effectiveFilters?.task_list_id) {
7410
- conditions.push("task_list_id = ?");
7411
- params.push(effectiveFilters.task_list_id);
7590
+ if (input.approved_by !== undefined) {
7591
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
7412
7592
  }
7413
- const where = conditions.join(" AND ");
7414
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
7415
- return rows.map(rowToTask);
7416
- }
7417
- function stealTask(agentId, opts, db) {
7418
- const d = db || getDatabase();
7419
- const databasePath = databasePathFromDatabase(d);
7420
- const staleMinutes = opts?.stale_minutes ?? 30;
7421
- const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
7422
- if (staleTasks.length === 0)
7423
- return null;
7424
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
7425
- staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
7426
- const target = staleTasks[0];
7427
- const timestamp = now();
7428
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
7429
- const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
7430
- 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]);
7431
- if (result.changes === 0)
7432
- return null;
7433
- logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
7434
- logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
7435
- const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
7436
- const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
7437
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7438
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
7439
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
7440
- return stolenTask;
7593
+ const updatePayload = taskEventData(updatedTask);
7594
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
7595
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
7596
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
7597
+ return updatedTask;
7441
7598
  }
7442
- function claimOrSteal(agentId, filters, db) {
7599
+ function deleteTask(id, db) {
7443
7600
  const d = db || getDatabase();
7444
- const tx = d.transaction(() => {
7445
- const next = getNextTask(agentId, filters, d);
7446
- if (next) {
7447
- const started = startTask(next.id, agentId, d);
7448
- return { task: started, stolen: false };
7449
- }
7450
- const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
7451
- if (stolen)
7452
- return { task: stolen, stolen: true };
7453
- return null;
7454
- });
7455
- return tx();
7456
- }
7457
- function spawnNextRecurrence(completedTask, db, completedAt) {
7458
- const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
7459
- const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
7460
- let title = completedTask.title;
7461
- if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
7462
- title = title.slice(completedTask.short_id.length + 2);
7463
- }
7464
- const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
7465
- return createTask({
7466
- title,
7467
- description: completedTask.description ?? undefined,
7468
- priority: completedTask.priority,
7469
- project_id: completedTask.project_id ?? undefined,
7470
- task_list_id: completedTask.task_list_id ?? undefined,
7471
- plan_id: completedTask.plan_id ?? undefined,
7472
- assigned_to: completedTask.assigned_to ?? undefined,
7473
- tags: completedTask.tags,
7474
- metadata: completedTask.metadata,
7475
- estimated_minutes: completedTask.estimated_minutes ?? undefined,
7476
- sla_minutes: completedTask.sla_minutes ?? undefined,
7477
- recurrence_rule: completedTask.recurrence_rule,
7478
- recurrence_parent_id: recurrenceParentId,
7479
- due_at: dueAt
7480
- }, db);
7601
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
7602
+ if (!row)
7603
+ return false;
7604
+ recordStorageTombstone({
7605
+ object_type: "tasks",
7606
+ object_id: id,
7607
+ payload: rowToTask(row),
7608
+ version: row.version
7609
+ }, d);
7610
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
7611
+ return result.changes > 0;
7481
7612
  }
7482
- var MAX_SPAWN_DEPTH = 10;
7483
- var init_task_lifecycle = __esm(() => {
7613
+ var init_task_crud = __esm(() => {
7484
7614
  init_types();
7485
7615
  init_database();
7486
7616
  init_completion_guard();
@@ -7488,11 +7618,9 @@ var init_task_lifecycle = __esm(() => {
7488
7618
  init_event_hooks();
7489
7619
  init_shared_events();
7490
7620
  init_audit();
7491
- init_recurrence();
7492
7621
  init_webhooks();
7493
- init_templates();
7494
- init_task_crud();
7495
- init_task_graph();
7622
+ init_checklists();
7623
+ init_storage_tombstones();
7496
7624
  });
7497
7625
 
7498
7626
  // src/db/task-status.ts
@@ -7582,6 +7710,15 @@ function setTaskStatus(id, status, _agentId, db) {
7582
7710
  throw new TaskNotFoundError(id);
7583
7711
  if (task.status === status)
7584
7712
  return task;
7713
+ if (status === "completed") {
7714
+ try {
7715
+ return completeTask(id, _agentId, d);
7716
+ } catch (e) {
7717
+ if (e instanceof VersionConflictError && attempt < 2)
7718
+ continue;
7719
+ throw e;
7720
+ }
7721
+ }
7585
7722
  try {
7586
7723
  return updateTask(id, { status, version: task.version }, d);
7587
7724
  } catch (e) {
@@ -11538,6 +11675,37 @@ function parseBoundedLimit(value, fallback, max) {
11538
11675
  return fallback;
11539
11676
  return Math.min(parsed, max);
11540
11677
  }
11678
+ function mapTaskError(e, json2) {
11679
+ if (e instanceof VersionConflictError) {
11680
+ return json2({
11681
+ error: e.message,
11682
+ code: VersionConflictError.code,
11683
+ expected_version: e.expectedVersion,
11684
+ current_version: e.actualVersion
11685
+ }, 409);
11686
+ }
11687
+ if (e instanceof TaskNotFoundError) {
11688
+ return json2({ error: e.message, code: TaskNotFoundError.code }, 404);
11689
+ }
11690
+ if (e instanceof LockError) {
11691
+ return json2({ error: e.message, code: LockError.code }, 409);
11692
+ }
11693
+ if (e instanceof CompletionGuardError) {
11694
+ return json2({
11695
+ error: e.message,
11696
+ code: CompletionGuardError.code,
11697
+ retry_after: e.retryAfterSeconds ?? null
11698
+ }, 409);
11699
+ }
11700
+ if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
11701
+ return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
11702
+ }
11703
+ return null;
11704
+ }
11705
+ function countRecurringTasks() {
11706
+ 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();
11707
+ return row?.count ?? 0;
11708
+ }
11541
11709
  function handleSseEvents(_req, url, ctx) {
11542
11710
  const agentId = url.searchParams.get("agent_id") || undefined;
11543
11711
  const projectId = url.searchParams.get("project_id") || undefined;
@@ -11616,35 +11784,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
11616
11784
  });
11617
11785
  }
11618
11786
  function handleHealth(_ctx, json2) {
11619
- const all = listTasks({ limit: 1e4 });
11620
- const stale = all.filter((t) => t.status === "in_progress" && new Date(t.updated_at).getTime() < Date.now() - 30 * 60 * 1000);
11621
- const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < new Date().toISOString());
11622
- 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() });
11787
+ const stats = getTaskStats();
11788
+ const staleCount = getStaleTasks(30).length;
11789
+ const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
11790
+ return json2({
11791
+ status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
11792
+ tasks: stats.total,
11793
+ stale: staleCount,
11794
+ overdue_recurring: overdueRecurring,
11795
+ timestamp: new Date().toISOString()
11796
+ });
11623
11797
  }
11624
11798
  function handleHeadlessBoundary(_ctx, json2) {
11625
11799
  const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
11626
11800
  return json2(getHeadlessBoundaryManifest2());
11627
11801
  }
11628
11802
  function handleStats(_ctx, json2) {
11629
- const all = listTasks({ limit: 1e4 });
11803
+ const stats = getTaskStats();
11804
+ const byStatus = stats.by_status;
11630
11805
  const projects = listProjects();
11631
11806
  const agents = listAgents();
11632
- const staleItems = getStaleTasks(30);
11633
- const nowStr = new Date().toISOString();
11634
- const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
11635
- const recurringTasks = all.filter((t) => t.recurrence_rule).length;
11807
+ const staleCount = getStaleTasks(30).length;
11808
+ const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
11636
11809
  return json2({
11637
- total_tasks: all.length,
11638
- pending: all.filter((t) => t.status === "pending").length,
11639
- in_progress: all.filter((t) => t.status === "in_progress").length,
11640
- completed: all.filter((t) => t.status === "completed").length,
11641
- failed: all.filter((t) => t.status === "failed").length,
11642
- cancelled: all.filter((t) => t.status === "cancelled").length,
11810
+ total_tasks: stats.total,
11811
+ pending: byStatus["pending"] ?? 0,
11812
+ in_progress: byStatus["in_progress"] ?? 0,
11813
+ completed: byStatus["completed"] ?? 0,
11814
+ failed: byStatus["failed"] ?? 0,
11815
+ cancelled: byStatus["cancelled"] ?? 0,
11643
11816
  projects: projects.length,
11644
11817
  agents: agents.length,
11645
- stale_count: staleItems.length,
11818
+ stale_count: staleCount,
11646
11819
  overdue_recurring: overdueRecurring,
11647
- recurring_tasks: recurringTasks
11820
+ recurring_tasks: countRecurringTasks()
11648
11821
  });
11649
11822
  }
11650
11823
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
@@ -11723,27 +11896,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
11723
11896
  const summaries = tasks.map((t) => taskToSummary2(t));
11724
11897
  if (format === "csv") {
11725
11898
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
11726
- const rows = summaries.map((t) => headers.map((h) => {
11727
- const val = t[h];
11899
+ const csvCell = (val) => {
11728
11900
  if (val === null || val === undefined)
11729
11901
  return "";
11730
- const str = String(val);
11731
- return str.includes(",") || str.includes('"') || str.includes(`
11732
- `) ? `"${str.replace(/"/g, '""')}"` : str;
11733
- }).join(","));
11902
+ let str = String(val);
11903
+ if (/^[=+\-@\t\r]/.test(str))
11904
+ str = `'${str}`;
11905
+ if (str.includes(",") || str.includes('"') || str.includes(`
11906
+ `) || str.includes("\r")) {
11907
+ str = `"${str.replace(/"/g, '""')}"`;
11908
+ }
11909
+ return str;
11910
+ };
11911
+ const rows = summaries.map((t) => headers.map((h) => csvCell(t[h])).join(","));
11734
11912
  const csv = [headers.join(","), ...rows].join(`
11735
11913
  `);
11736
11914
  return new Response(csv, {
11737
11915
  headers: {
11738
11916
  "Content-Type": "text/csv",
11739
- "Content-Disposition": "attachment; filename=tasks.csv"
11917
+ "Content-Disposition": "attachment; filename=tasks.csv",
11918
+ ...SECURITY_HEADERS
11740
11919
  }
11741
11920
  });
11742
11921
  }
11743
11922
  return new Response(JSON.stringify(summaries, null, 2), {
11744
11923
  headers: {
11745
11924
  "Content-Type": "application/json",
11746
- "Content-Disposition": "attachment; filename=tasks.json"
11925
+ "Content-Disposition": "attachment; filename=tasks.json",
11926
+ ...SECURITY_HEADERS
11747
11927
  }
11748
11928
  });
11749
11929
  }
@@ -11917,12 +12097,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
11917
12097
  if (ALLOWED.has(key))
11918
12098
  safeBody[key] = value;
11919
12099
  }
12100
+ const clientVersion = typeof body["version"] === "number" ? body["version"] : task.version;
11920
12101
  const updated = updateTask(id, {
11921
12102
  ...safeBody,
11922
- version: task.version
12103
+ version: clientVersion
11923
12104
  });
11924
12105
  return json2(taskToSummary2(updated));
11925
12106
  } catch (e) {
12107
+ const mapped = mapTaskError(e, json2);
12108
+ if (mapped)
12109
+ return mapped;
11926
12110
  return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
11927
12111
  }
11928
12112
  }
@@ -11938,6 +12122,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
11938
12122
  ctx.broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
11939
12123
  return json2(taskToSummary2(task));
11940
12124
  } catch (e) {
12125
+ const mapped = mapTaskError(e, json2);
12126
+ if (mapped)
12127
+ return mapped;
11941
12128
  return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
11942
12129
  }
11943
12130
  }
@@ -11957,6 +12144,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
11957
12144
  ctx.broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
11958
12145
  return json2(taskToSummary2(task));
11959
12146
  } catch (e) {
12147
+ const mapped = mapTaskError(e, json2);
12148
+ if (mapped)
12149
+ return mapped;
11960
12150
  return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
11961
12151
  }
11962
12152
  }
@@ -12281,6 +12471,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
12281
12471
  }
12282
12472
  var init_routes = __esm(() => {
12283
12473
  init_tasks();
12474
+ init_database();
12475
+ init_types();
12284
12476
  init_projects();
12285
12477
  init_agents();
12286
12478
  init_plans();
@@ -15665,7 +15857,7 @@ class JSONSchemaGenerator {
15665
15857
  if (val === undefined) {
15666
15858
  if (this.unrepresentable === "throw") {
15667
15859
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
15668
- }
15860
+ } else {}
15669
15861
  } else if (typeof val === "bigint") {
15670
15862
  if (this.unrepresentable === "throw") {
15671
15863
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -33299,10 +33491,8 @@ var init_token_utils = __esm(() => {
33299
33491
  "cancel_task",
33300
33492
  "check_task_done_contract",
33301
33493
  "claim_task",
33302
- "clone_task",
33303
33494
  "delete_task",
33304
33495
  "extend_task",
33305
- "get_active_work",
33306
33496
  "get_archived_tasks",
33307
33497
  "get_blocked_tasks",
33308
33498
  "get_blocking_tasks",
@@ -33338,7 +33528,8 @@ var init_token_utils = __esm(() => {
33338
33528
  "task_context",
33339
33529
  "unlock_task",
33340
33530
  "unarchive_task",
33341
- "update_task"
33531
+ "update_task",
33532
+ "upsert_task"
33342
33533
  ],
33343
33534
  projects: [
33344
33535
  "bootstrap_project",
@@ -33597,12 +33788,8 @@ var init_token_utils = __esm(() => {
33597
33788
  "delete_tag",
33598
33789
  "get_label",
33599
33790
  "get_activity_timeline",
33600
- "get_recent_activity",
33601
33791
  "get_tag",
33602
33792
  "get_task_fields",
33603
- "get_task_graph",
33604
- "get_task_history",
33605
- "get_task_stats",
33606
33793
  "list_workflow_states",
33607
33794
  "list_labels",
33608
33795
  "list_tags",
@@ -33613,6 +33800,10 @@ var init_token_utils = __esm(() => {
33613
33800
  "describe_tools",
33614
33801
  "set_task_workflow_state",
33615
33802
  "set_task_fields",
33803
+ "assign_label_to_task",
33804
+ "create_custom_field",
33805
+ "set_task_custom_field",
33806
+ "set_task_priority_meta",
33616
33807
  "update_label",
33617
33808
  "update_tag"
33618
33809
  ],
@@ -33638,7 +33829,6 @@ var init_token_utils = __esm(() => {
33638
33829
  "update_template",
33639
33830
  "write_template_library"
33640
33831
  ],
33641
- webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
33642
33832
  machines: [
33643
33833
  "machines_archive",
33644
33834
  "machines_delete",
@@ -58753,7 +58943,7 @@ var require_to_json_schema = __commonJS((exports) => {
58753
58943
  if (val === undefined) {
58754
58944
  if (this.unrepresentable === "throw") {
58755
58945
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
58756
- }
58946
+ } else {}
58757
58947
  } else if (typeof val === "bigint") {
58758
58948
  if (this.unrepresentable === "throw") {
58759
58949
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -79492,14 +79682,16 @@ function printHelp() {
79492
79682
  Start the @hasna/todos MCP server.
79493
79683
 
79494
79684
  Options:
79495
- --stdio Use stdio transport
79496
- --port <port> Use Streamable HTTP on the given port
79685
+ --stdio Use stdio transport (default)
79686
+ --http Use Streamable HTTP transport
79687
+ --port <port> Use Streamable HTTP on the given port (implies --http)
79497
79688
  -V, --version output the version number
79498
79689
  -h, --help display help for command
79499
79690
 
79500
79691
  Environment:
79501
- TODOS_MCP_STDIO=true Force stdio transport
79502
- TODOS_MCP_PORT=<port> HTTP port when not using stdio
79692
+ MCP_STDIO=1 Force stdio transport
79693
+ MCP_HTTP=1 Use Streamable HTTP transport
79694
+ MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
79503
79695
  TODOS_PROFILE=<profile> Tool profile filter
79504
79696
  TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
79505
79697
  }
@@ -79588,8 +79780,22 @@ function formatError2(error2) {
79588
79780
  function resolveId(partialId, table = "tasks") {
79589
79781
  const db = getDatabase();
79590
79782
  const id = resolvePartialId(db, table, partialId);
79591
- if (!id)
79592
- throw new Error(`Could not resolve ID: ${partialId}`);
79783
+ if (!id) {
79784
+ switch (table) {
79785
+ case "tasks":
79786
+ throw new TaskNotFoundError(partialId);
79787
+ case "projects":
79788
+ throw new ProjectNotFoundError(partialId);
79789
+ case "plans":
79790
+ throw new PlanNotFoundError(partialId);
79791
+ case "task_lists":
79792
+ throw new TaskListNotFoundError(partialId);
79793
+ case "agents":
79794
+ throw new AgentNotFoundError(partialId);
79795
+ default:
79796
+ throw new TaskNotFoundError(partialId);
79797
+ }
79798
+ }
79593
79799
  return id;
79594
79800
  }
79595
79801
  function formatTask(task2) {
@@ -79678,8 +79884,9 @@ function buildServer() {
79678
79884
  return server;
79679
79885
  }
79680
79886
  async function main() {
79681
- const { isStdioMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http(), exports_http));
79682
- if (isStdioMode()) {
79887
+ const { isHttpMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http(), exports_http));
79888
+ const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
79889
+ if (!isHttpMode() && !portRequested) {
79683
79890
  const server = buildServer();
79684
79891
  const transport = new StdioServerTransport;
79685
79892
  await server.connect(transport);
@@ -79857,7 +80064,7 @@ function checkAuth(req, apiKey) {
79857
80064
  if (!apiKey && !generatedKeysEnabled)
79858
80065
  return null;
79859
80066
  const provided = getProvidedApiKey(req);
79860
- const matchesEnvKey = Boolean(apiKey && provided && provided === apiKey);
80067
+ const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
79861
80068
  const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
79862
80069
  if (!matchesEnvKey && !matchesGeneratedKey) {
79863
80070
  return new Response(JSON.stringify({ error: "Unauthorized" }), {
@@ -79867,6 +80074,15 @@ function checkAuth(req, apiKey) {
79867
80074
  }
79868
80075
  return null;
79869
80076
  }
80077
+ function resolveClientIp(req, server) {
80078
+ const trustProxy = process.env["TODOS_TRUST_PROXY"] === "1" || process.env["TODOS_TRUST_PROXY"] === "true";
80079
+ if (trustProxy) {
80080
+ const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim();
80081
+ if (forwarded)
80082
+ return forwarded;
80083
+ }
80084
+ return server.requestIP(req)?.address || "unknown";
80085
+ }
79870
80086
  function checkRateLimit(ip) {
79871
80087
  const now4 = Date.now();
79872
80088
  const entry = rateLimitMap.get(ip);
@@ -79994,7 +80210,7 @@ Dashboard not found at: ${dashboardDir}`);
79994
80210
  const server = Bun.serve({
79995
80211
  port,
79996
80212
  hostname: hostname4,
79997
- async fetch(req) {
80213
+ async fetch(req, server2) {
79998
80214
  const url = new URL(req.url);
79999
80215
  const path = url.pathname;
80000
80216
  const method = req.method;
@@ -80006,15 +80222,6 @@ Dashboard not found at: ${dashboardDir}`);
80006
80222
  Vary: "Origin"
80007
80223
  } : undefined;
80008
80224
  const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
80009
- if (path === "/health" && method === "GET") {
80010
- const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
80011
- return healthResponse2("todos");
80012
- }
80013
- if (path === "/mcp") {
80014
- const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
80015
- const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp3(), exports_mcp));
80016
- return handleMcpHttpRequest2(req, buildServer2);
80017
- }
80018
80225
  if (method === "OPTIONS") {
80019
80226
  return new Response(null, {
80020
80227
  headers: corsHeaders || {
@@ -80022,7 +80229,7 @@ Dashboard not found at: ${dashboardDir}`);
80022
80229
  }
80023
80230
  });
80024
80231
  }
80025
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
80232
+ const ip = resolveClientIp(req, server2);
80026
80233
  const rl = checkRateLimit(ip);
80027
80234
  if (!rl.allowed) {
80028
80235
  return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
@@ -80030,6 +80237,18 @@ Dashboard not found at: ${dashboardDir}`);
80030
80237
  headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
80031
80238
  });
80032
80239
  }
80240
+ if (path === "/health" && method === "GET") {
80241
+ const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
80242
+ return healthResponse2("todos");
80243
+ }
80244
+ if (path === "/mcp") {
80245
+ const authError = checkAuth(req, apiKey);
80246
+ if (authError)
80247
+ return authError;
80248
+ const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
80249
+ const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp3(), exports_mcp));
80250
+ return handleMcpHttpRequest2(req, buildServer2);
80251
+ }
80033
80252
  if (path.startsWith("/api/")) {
80034
80253
  const authError = checkAuth(req, apiKey);
80035
80254
  if (authError)