@hasna/todos 0.11.72 → 0.11.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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());
@@ -5739,1767 +5759,1858 @@ var init_checklists = __esm(() => {
5739
5759
  init_database();
5740
5760
  });
5741
5761
 
5742
- // src/db/task-crud.ts
5743
- 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) {
5744
5882
  return {
5745
5883
  ...row,
5746
5884
  tags: JSON.parse(row.tags || "[]"),
5885
+ variables: JSON.parse(row.variables || "[]"),
5747
5886
  metadata: JSON.parse(row.metadata || "{}"),
5748
- status: row.status,
5749
- priority: row.priority,
5750
- requires_approval: !!row.requires_approval
5887
+ priority: row.priority || "medium",
5888
+ version: row.version ?? 1
5751
5889
  };
5752
5890
  }
5753
- function insertTaskTags(taskId, tags, db) {
5754
- if (tags.length === 0)
5755
- return;
5756
- const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
5757
- for (const tag of tags) {
5758
- if (tag)
5759
- stmt.run(taskId, tag);
5760
- }
5761
- }
5762
- function replaceTaskTags(taskId, tags, db) {
5763
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
5764
- 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
+ };
5765
5901
  }
5766
- function addMetadataConditions(metadata, conditions, params) {
5767
- if (!metadata)
5768
- return;
5769
- for (const [key, value] of Object.entries(metadata)) {
5770
- if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
5771
- throw new Error(`Invalid metadata filter key: ${key}`);
5772
- }
5773
- conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
5774
- params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
5775
- }
5902
+ function resolveTemplateId(id, d) {
5903
+ return resolvePartialId(d, "task_templates", id);
5776
5904
  }
5777
- function createTask(input, db) {
5905
+ function createTemplate(input, db) {
5778
5906
  const d = db || getDatabase();
5779
- const timestamp = now();
5780
- const tags = input.tags || [];
5907
+ const id = uuid();
5781
5908
  const machineId = currentStorageMachineId(d);
5782
- const assignedBy = input.assigned_by || input.agent_id;
5783
- const assignedFromProject = input.assigned_from_project || null;
5784
- let id = uuid();
5785
- for (let attempt = 0;attempt < 3; attempt++) {
5786
- try {
5787
- 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)
5788
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5789
- id,
5790
- null,
5791
- input.project_id || null,
5792
- input.parent_id || null,
5793
- input.plan_id || null,
5794
- input.task_list_id || null,
5795
- input.cycle_id || null,
5796
- input.title,
5797
- input.description || null,
5798
- input.status || "pending",
5799
- input.priority || "medium",
5800
- input.agent_id || null,
5801
- input.assigned_to || null,
5802
- input.session_id || null,
5803
- input.working_dir || null,
5804
- JSON.stringify(tags),
5805
- JSON.stringify(input.metadata || {}),
5806
- timestamp,
5807
- timestamp,
5808
- input.due_at || null,
5809
- input.estimated_minutes || null,
5810
- input.sla_minutes ?? null,
5811
- input.confidence ?? null,
5812
- input.retry_count ?? 0,
5813
- input.max_retries ?? 3,
5814
- input.retry_after ?? null,
5815
- input.requires_approval ? 1 : 0,
5816
- null,
5817
- null,
5818
- input.recurrence_rule || null,
5819
- input.recurrence_parent_id || null,
5820
- input.spawns_template_id || null,
5821
- input.reason || null,
5822
- input.spawned_from_session || null,
5823
- assignedBy || null,
5824
- assignedFromProject || null,
5825
- input.task_type || null,
5826
- machineId
5827
- ]);
5828
- break;
5829
- } catch (e) {
5830
- if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
5831
- id = uuid();
5832
- continue;
5833
- }
5834
- throw e;
5835
- }
5836
- }
5837
- if (tags.length > 0) {
5838
- 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);
5839
5926
  }
5840
- const task = getTask(id, d);
5841
- const payload = taskEventData(task);
5842
- const databasePath = databasePathFromDatabase(d);
5843
- dispatchWebhook2("task.created", payload, d).catch(() => {});
5844
- emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
5845
- emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
5846
- return task;
5927
+ return getTemplate(id, d);
5847
5928
  }
5848
- function getTask(id, db) {
5929
+ function getTemplate(id, db) {
5849
5930
  const d = db || getDatabase();
5850
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
5851
- if (!row)
5931
+ const resolved = resolveTemplateId(id, d);
5932
+ if (!resolved)
5852
5933
  return null;
5853
- return rowToTask(row);
5934
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
5935
+ return row ? rowToTemplate(row) : null;
5854
5936
  }
5855
- function getTaskWithRelations(id, db) {
5937
+ function listTemplates(db) {
5856
5938
  const d = db || getDatabase();
5857
- const task = getTask(id, d);
5858
- if (!task)
5859
- return null;
5860
- const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
5861
- const subtasks = subtaskRows.map(rowToTask);
5862
- const depRows = d.query(`SELECT t.* FROM tasks t
5863
- JOIN task_dependencies td ON td.depends_on = t.id
5864
- WHERE td.task_id = ?`).all(id);
5865
- const dependencies = depRows.map(rowToTask);
5866
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
5867
- JOIN task_dependencies td ON td.task_id = t.id
5868
- WHERE td.depends_on = ?`).all(id);
5869
- const blocked_by = blockedByRows.map(rowToTask);
5870
- const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
5871
- const parent = task.parent_id ? getTask(task.parent_id, d) : null;
5872
- const checklist = getChecklist(id, d);
5873
- return {
5874
- ...task,
5875
- subtasks,
5876
- dependencies,
5877
- blocked_by,
5878
- comments,
5879
- parent,
5880
- checklist
5881
- };
5939
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
5882
5940
  }
5883
- function listTasks(filter = {}, db) {
5941
+ function deleteTemplate(id, db) {
5884
5942
  const d = db || getDatabase();
5885
- const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
5886
- clearExpiredLocks2(d);
5887
- const conditions = [];
5888
- const params = [];
5889
- if (filter.project_id) {
5890
- conditions.push("project_id = ?");
5891
- params.push(filter.project_id);
5892
- }
5893
- if (filter.ids && filter.ids.length > 0) {
5894
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
5895
- params.push(...filter.ids);
5896
- }
5897
- if (filter.parent_id !== undefined) {
5898
- if (filter.parent_id === null) {
5899
- conditions.push("parent_id IS NULL");
5900
- } else {
5901
- conditions.push("parent_id = ?");
5902
- params.push(filter.parent_id);
5903
- }
5904
- }
5905
- if (filter.status) {
5906
- if (Array.isArray(filter.status)) {
5907
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
5908
- params.push(...filter.status);
5909
- } else {
5910
- conditions.push("status = ?");
5911
- params.push(filter.status);
5912
- }
5913
- }
5914
- if (filter.priority) {
5915
- if (Array.isArray(filter.priority)) {
5916
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
5917
- params.push(...filter.priority);
5918
- } else {
5919
- conditions.push("priority = ?");
5920
- params.push(filter.priority);
5921
- }
5922
- }
5923
- if (filter.assigned_to) {
5924
- conditions.push("assigned_to = ?");
5925
- params.push(filter.assigned_to);
5926
- }
5927
- if (filter.agent_id) {
5928
- conditions.push("agent_id = ?");
5929
- 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()]);
5930
5977
  }
5931
- if (filter.session_id) {
5932
- conditions.push("session_id = ?");
5933
- 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);
5934
5983
  }
5935
- if (filter.tags && filter.tags.length > 0) {
5936
- const placeholders = filter.tags.map(() => "?").join(",");
5937
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
5938
- params.push(...filter.tags);
5984
+ if (updates.title_pattern !== undefined) {
5985
+ sets.push("title_pattern = ?");
5986
+ values.push(updates.title_pattern);
5939
5987
  }
5940
- if (filter.plan_id) {
5941
- conditions.push("plan_id = ?");
5942
- params.push(filter.plan_id);
5988
+ if (updates.description !== undefined) {
5989
+ sets.push("description = ?");
5990
+ values.push(updates.description);
5943
5991
  }
5944
- if (filter.task_list_id) {
5945
- conditions.push("task_list_id = ?");
5946
- params.push(filter.task_list_id);
5992
+ if (updates.priority !== undefined) {
5993
+ sets.push("priority = ?");
5994
+ values.push(updates.priority);
5947
5995
  }
5948
- if (filter.has_recurrence === true) {
5949
- conditions.push("recurrence_rule IS NOT NULL");
5950
- } else if (filter.has_recurrence === false) {
5951
- conditions.push("recurrence_rule IS NULL");
5996
+ if (updates.tags !== undefined) {
5997
+ sets.push("tags = ?");
5998
+ values.push(JSON.stringify(updates.tags));
5952
5999
  }
5953
- if (filter.task_type) {
5954
- if (Array.isArray(filter.task_type)) {
5955
- conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
5956
- params.push(...filter.task_type);
5957
- } else {
5958
- conditions.push("task_type = ?");
5959
- params.push(filter.task_type);
5960
- }
6000
+ if (updates.variables !== undefined) {
6001
+ sets.push("variables = ?");
6002
+ values.push(JSON.stringify(updates.variables));
5961
6003
  }
5962
- addMetadataConditions(filter.metadata, conditions, params);
5963
- const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
5964
- if (filter.cursor) {
5965
- try {
5966
- const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
5967
- conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
5968
- params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
5969
- } catch {}
6004
+ if (updates.project_id !== undefined) {
6005
+ sets.push("project_id = ?");
6006
+ values.push(updates.project_id);
5970
6007
  }
5971
- if (!filter.include_archived) {
5972
- conditions.push("archived_at IS NULL");
6008
+ if (updates.plan_id !== undefined) {
6009
+ sets.push("plan_id = ?");
6010
+ values.push(updates.plan_id);
5973
6011
  }
5974
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5975
- let limitClause = "";
5976
- if (filter.limit) {
5977
- limitClause = " LIMIT ?";
5978
- params.push(filter.limit);
5979
- if (!filter.cursor && filter.offset) {
5980
- limitClause += " OFFSET ?";
5981
- params.push(filter.offset);
5982
- }
6012
+ if (updates.metadata !== undefined) {
6013
+ sets.push("metadata = ?");
6014
+ values.push(JSON.stringify(updates.metadata));
5983
6015
  }
5984
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
5985
- return rows.map(rowToTask);
5986
- }
5987
- function getTaskByFingerprint(fingerprint, db) {
5988
- const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
5989
- 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);
5990
6019
  }
5991
- 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));
5992
6025
  return {
5993
- ...current,
5994
- ...next ?? {},
5995
- 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
5996
6034
  };
5997
6035
  }
5998
- function upsertTaskByFingerprint(input, db) {
6036
+ function addTemplateTasks(templateId, tasks, db) {
5999
6037
  const d = db || getDatabase();
6000
- const fingerprint = input.fingerprint.trim();
6001
- if (!fingerprint)
6002
- throw new Error("fingerprint is required");
6003
- const existing = getTaskByFingerprint(fingerprint, d);
6004
- const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
6005
- if (!existing) {
6006
- const task2 = createTask({ ...input, metadata }, d);
6007
- return { task: task2, created: true };
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));
6008
6065
  }
6009
- const task = updateTask(existing.id, {
6010
- version: existing.version,
6011
- title: input.title,
6012
- description: input.description,
6013
- status: input.status,
6014
- priority: input.priority,
6015
- project_id: input.project_id,
6016
- assigned_to: input.assigned_to,
6017
- working_dir: input.working_dir,
6018
- plan_id: input.plan_id,
6019
- task_list_id: input.task_list_id,
6020
- tags: input.tags,
6021
- metadata,
6022
- due_at: input.due_at,
6023
- estimated_minutes: input.estimated_minutes,
6024
- sla_minutes: input.sla_minutes,
6025
- confidence: input.confidence,
6026
- retry_count: input.retry_count,
6027
- max_retries: input.max_retries,
6028
- retry_after: input.retry_after,
6029
- requires_approval: input.requires_approval,
6030
- recurrence_rule: input.recurrence_rule,
6031
- task_type: input.task_type
6032
- }, d);
6033
- return { task, created: false };
6066
+ return results;
6034
6067
  }
6035
- function countTasks(filter = {}, db) {
6068
+ function getTemplateWithTasks(id, db) {
6036
6069
  const d = db || getDatabase();
6037
- const conditions = [];
6038
- const params = [];
6039
- if (filter.project_id) {
6040
- conditions.push("project_id = ?");
6041
- params.push(filter.project_id);
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;
6042
6094
  }
6043
- if (filter.ids && filter.ids.length > 0) {
6044
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
6045
- params.push(...filter.ids);
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;
6046
6100
  }
6047
- if (filter.parent_id !== undefined) {
6048
- if (filter.parent_id === null) {
6049
- conditions.push("parent_id IS NULL");
6050
- } else {
6051
- conditions.push("parent_id = ?");
6052
- params.push(filter.parent_id);
6053
- }
6101
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
6102
+ if (falsyMatch) {
6103
+ const varName = falsyMatch[1];
6104
+ const val = variables[varName];
6105
+ return !val || val === "" || val === "false";
6054
6106
  }
6055
- if (filter.status) {
6056
- if (Array.isArray(filter.status)) {
6057
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
6058
- params.push(...filter.status);
6059
- } else {
6060
- conditions.push("status = ?");
6061
- params.push(filter.status);
6062
- }
6107
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
6108
+ if (truthyMatch) {
6109
+ const varName = truthyMatch[1];
6110
+ const val = variables[varName];
6111
+ return !!val && val !== "" && val !== "false";
6063
6112
  }
6064
- if (filter.priority) {
6065
- if (Array.isArray(filter.priority)) {
6066
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
6067
- params.push(...filter.priority);
6068
- } else {
6069
- conditions.push("priority = ?");
6070
- params.push(filter.priority);
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
+ }))
6142
+ };
6143
+ }
6144
+ function importTemplate(json, db) {
6145
+ const d = db || getDatabase();
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
6168
+ }, d);
6169
+ }
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;
6071
6190
  }
6072
6191
  }
6073
- if (filter.assigned_to) {
6074
- conditions.push("assigned_to = ?");
6075
- params.push(filter.assigned_to);
6076
- }
6077
- if (filter.agent_id) {
6078
- conditions.push("agent_id = ?");
6079
- params.push(filter.agent_id);
6080
- }
6081
- if (filter.session_id) {
6082
- conditions.push("session_id = ?");
6083
- params.push(filter.session_id);
6084
- }
6085
- if (filter.tags && filter.tags.length > 0) {
6086
- const placeholders = filter.tags.map(() => "?").join(",");
6087
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
6088
- params.push(...filter.tags);
6089
- }
6090
- if (filter.plan_id) {
6091
- conditions.push("plan_id = ?");
6092
- params.push(filter.plan_id);
6192
+ const missing = [];
6193
+ for (const v of templateVars) {
6194
+ if (v.required && merged[v.name] === undefined) {
6195
+ missing.push(v.name);
6196
+ }
6093
6197
  }
6094
- if (filter.task_list_id) {
6095
- conditions.push("task_list_id = ?");
6096
- params.push(filter.task_list_id);
6198
+ if (missing.length > 0) {
6199
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
6097
6200
  }
6098
- addMetadataConditions(filter.metadata, conditions, params);
6099
- if (!filter.include_archived) {
6100
- conditions.push("archived_at IS NULL");
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);
6101
6207
  }
6102
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
6103
- const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
6104
- return row.count;
6208
+ return result;
6105
6209
  }
6106
- function updateTask(id, input, db) {
6210
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
6107
6211
  const d = db || getDatabase();
6108
- const task = getTask(id, d);
6109
- if (!task)
6110
- throw new TaskNotFoundError(id);
6111
- if (task.version !== input.version) {
6112
- throw new VersionConflictError(id, input.version, task.version);
6113
- }
6114
- const timestamp = now();
6115
- const completionTimestamp = input.completed_at ?? timestamp;
6116
- const sets = ["version = version + 1", "updated_at = ?"];
6117
- const params = [timestamp];
6118
- if (input.title !== undefined) {
6119
- sets.push("title = ?");
6120
- params.push(input.title);
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}`);
6121
6218
  }
6122
- if (input.description !== undefined) {
6123
- sets.push("description = ?");
6124
- params.push(input.description);
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];
6125
6225
  }
6126
- if (input.status !== undefined) {
6127
- if (input.status === "completed") {
6128
- checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
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);
6237
+ }
6238
+ continue;
6129
6239
  }
6130
- sets.push("status = ?");
6131
- params.push(input.status);
6132
- if (input.status === "completed") {
6133
- sets.push("completed_at = ?");
6134
- params.push(completionTimestamp);
6240
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
6241
+ skippedPositions.add(tt.position);
6242
+ continue;
6135
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);
6136
6261
  }
6137
- if (input.priority !== undefined) {
6138
- sets.push("priority = ?");
6139
- params.push(input.priority);
6140
- }
6141
- if (input.project_id !== undefined) {
6142
- sets.push("project_id = ?");
6143
- params.push(input.project_id);
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
+ }
6276
+ }
6144
6277
  }
6145
- if (input.assigned_to !== undefined) {
6146
- sets.push("assigned_to = ?");
6147
- params.push(input.assigned_to);
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
+ });
6310
+ }
6148
6311
  }
6149
- if (input.working_dir !== undefined) {
6150
- sets.push("working_dir = ?");
6151
- params.push(input.working_dir);
6312
+ return {
6313
+ template_id: template.id,
6314
+ template_name: template.name,
6315
+ description: template.description,
6316
+ variables: template.variables,
6317
+ resolved_variables: resolved,
6318
+ tasks
6319
+ };
6320
+ }
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) {
6329
+ const d = db || getDatabase();
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);
6152
6336
  }
6153
- if (input.tags !== undefined) {
6154
- sets.push("tags = ?");
6155
- params.push(JSON.stringify(input.tags));
6337
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
6338
+ }
6339
+ function removeDependency(taskId, dependsOn, db) {
6340
+ const d = db || getDatabase();
6341
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
6342
+ return result.changes > 0;
6343
+ }
6344
+ function getTaskDependencies(taskId, db) {
6345
+ const d = db || getDatabase();
6346
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
6347
+ }
6348
+ function getTaskDependents(taskId, db) {
6349
+ const d = db || getDatabase();
6350
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
6351
+ }
6352
+ function cloneTask(taskId, overrides, db) {
6353
+ const d = db || getDatabase();
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";
6385
+ });
6386
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
6156
6387
  }
6157
- if (input.metadata !== undefined) {
6158
- sets.push("metadata = ?");
6159
- params.push(JSON.stringify(input.metadata));
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);
6160
6399
  }
6161
- if (input.plan_id !== undefined) {
6162
- sets.push("plan_id = ?");
6163
- params.push(input.plan_id);
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);
6164
6411
  }
6165
- if (input.task_list_id !== undefined) {
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) {
6166
6425
  sets.push("task_list_id = ?");
6167
- params.push(input.task_list_id);
6168
- }
6169
- if (input.due_at !== undefined) {
6170
- sets.push("due_at = ?");
6171
- params.push(input.due_at);
6172
- }
6173
- if (input.estimated_minutes !== undefined) {
6174
- sets.push("estimated_minutes = ?");
6175
- params.push(input.estimated_minutes);
6176
- }
6177
- if (input.sla_minutes !== undefined) {
6178
- sets.push("sla_minutes = ?");
6179
- params.push(input.sla_minutes);
6180
- }
6181
- if (input.actual_minutes !== undefined) {
6182
- sets.push("actual_minutes = ?");
6183
- params.push(input.actual_minutes);
6184
- }
6185
- if (input.completed_at !== undefined && input.status !== "completed") {
6186
- sets.push("completed_at = ?");
6187
- params.push(input.completed_at);
6188
- }
6189
- if (input.confidence !== undefined) {
6190
- sets.push("confidence = ?");
6191
- params.push(input.confidence);
6192
- }
6193
- if (input.retry_count !== undefined) {
6194
- sets.push("retry_count = ?");
6195
- params.push(input.retry_count);
6426
+ params.push(target.task_list_id);
6196
6427
  }
6197
- if (input.max_retries !== undefined) {
6198
- sets.push("max_retries = ?");
6199
- params.push(input.max_retries);
6428
+ if (target.project_id !== undefined) {
6429
+ sets.push("project_id = ?");
6430
+ params.push(target.project_id);
6200
6431
  }
6201
- if (input.retry_after !== undefined) {
6202
- sets.push("retry_after = ?");
6203
- params.push(input.retry_after);
6432
+ if (target.plan_id !== undefined) {
6433
+ sets.push("plan_id = ?");
6434
+ params.push(target.plan_id);
6204
6435
  }
6205
- if (input.requires_approval !== undefined) {
6206
- sets.push("requires_approval = ?");
6207
- params.push(input.requires_approval ? 1 : 0);
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
+ }
6208
6454
  }
6209
- if (input.approved_by !== undefined) {
6210
- sets.push("approved_by = ?");
6211
- params.push(input.approved_by);
6212
- sets.push("approved_at = ?");
6213
- params.push(now());
6214
- }
6215
- if (input.recurrence_rule !== undefined) {
6216
- sets.push("recurrence_rule = ?");
6217
- params.push(input.recurrence_rule);
6455
+ return false;
6456
+ }
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();
6486
+ }
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) {
6495
+ const d = db || getDatabase();
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);
6218
6504
  }
6219
- if (input.task_type !== undefined) {
6220
- sets.push("task_type = ?");
6221
- params.push(input.task_type ?? null);
6505
+ return blocking;
6506
+ }
6507
+ function startTask(id, agentId, db) {
6508
+ const d = db || getDatabase();
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}`);
6222
6528
  }
6223
- params.push(id, input.version);
6224
- const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
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]);
6225
6533
  if (result.changes === 0) {
6226
6534
  const current = getTask(id, d);
6227
- throw new VersionConflictError(id, input.version, current?.version ?? -1);
6228
- }
6229
- if (input.tags !== undefined) {
6230
- replaceTaskTags(id, input.tags, 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`);
6231
6542
  }
6232
- const agentId = task.assigned_to || task.agent_id || null;
6233
- if (input.status !== undefined && input.status !== task.status)
6234
- logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
6235
- if (input.priority !== undefined && input.priority !== task.priority)
6236
- logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
6237
- if (input.title !== undefined && input.title !== task.title)
6238
- logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
6239
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
6240
- logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
6241
- if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
6242
- logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
6243
- if (input.approved_by !== undefined)
6244
- logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
6245
- const updatedTask = {
6246
- ...task,
6247
- ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
6248
- tags: input.tags ?? task.tags,
6249
- metadata: input.metadata ?? task.metadata,
6250
- version: task.version + 1,
6251
- updated_at: timestamp,
6252
- completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
6253
- sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
6254
- actual_minutes: input.actual_minutes ?? task.actual_minutes,
6255
- confidence: input.confidence !== undefined ? input.confidence : task.confidence,
6256
- retry_count: input.retry_count ?? task.retry_count,
6257
- max_retries: input.max_retries ?? task.max_retries,
6258
- retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
6259
- requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
6260
- approved_by: input.approved_by ?? task.approved_by,
6261
- approved_at: input.approved_by ? timestamp : task.approved_at
6262
- };
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;
6550
+ }
6551
+ function completeTask(id, agentId, db, options) {
6552
+ const d = db || getDatabase();
6263
6553
  const databasePath = databasePathFromDatabase(d);
6264
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
6265
- const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
6266
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
6267
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
6268
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
6554
+ const task = getTask(id, d);
6555
+ if (!task)
6556
+ throw new TaskNotFoundError(id);
6557
+ if (task.status === "completed") {
6558
+ return task;
6269
6559
  }
6270
- if (input.status !== undefined && input.status !== task.status) {
6271
- const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
6272
- dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
6273
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
6274
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
6560
+ if (task.status === "cancelled") {
6561
+ throw new Error(`Task ${id} is cancelled and cannot be completed`);
6275
6562
  }
6276
- if (input.approved_by !== undefined) {
6277
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
6563
+ if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
6564
+ throw new LockError(id, task.locked_by);
6278
6565
  }
6279
- const updatePayload = taskEventData(updatedTask);
6280
- dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
6281
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
6282
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
6283
- return updatedTask;
6284
- }
6285
- function deleteTask(id, db) {
6286
- const d = db || getDatabase();
6287
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
6288
- if (!row)
6289
- return false;
6290
- recordStorageTombstone({
6291
- object_type: "tasks",
6292
- object_id: id,
6293
- payload: rowToTask(row),
6294
- version: row.version
6295
- }, d);
6296
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
6297
- return result.changes > 0;
6298
- }
6299
- var init_task_crud = __esm(() => {
6300
- init_types();
6301
- init_database();
6302
- init_completion_guard();
6303
- init_event_emission_safety();
6304
- init_event_hooks();
6305
- init_shared_events();
6306
- init_audit();
6307
- init_webhooks();
6308
- init_checklists();
6309
- init_storage_tombstones();
6310
- });
6311
-
6312
- // src/lib/recurrence.ts
6313
- function parseRecurrenceRule(rule) {
6314
- const normalized = rule.trim().toLowerCase();
6315
- if (normalized === "every weekday" || normalized === "every weekdays") {
6316
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
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 };
6317
6574
  }
6318
- if (normalized === "every day" || normalized === "daily") {
6319
- return { type: "interval", interval: 1, unit: "day" };
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
+ }
6320
6621
  }
6321
- if (normalized === "every week" || normalized === "weekly") {
6322
- return { type: "interval", interval: 1, unit: "week" };
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
+ }
6323
6639
  }
6324
- if (normalized === "every month" || normalized === "monthly") {
6325
- return { type: "interval", interval: 1, unit: "month" };
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 };
6326
6643
  }
6327
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
6328
- if (intervalMatch) {
6329
- return {
6330
- type: "interval",
6331
- interval: parseInt(intervalMatch[1], 10),
6332
- unit: intervalMatch[2]
6333
- };
6644
+ if (spawnedFromTemplate) {
6645
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
6334
6646
  }
6335
- const daysMatch = normalized.match(/^every\s+(.+)$/);
6336
- if (daysMatch) {
6337
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
6338
- const days = [];
6339
- for (const part of dayParts) {
6340
- const dayNum = DAY_NAMES[part];
6341
- if (dayNum !== undefined) {
6342
- days.push(dayNum);
6343
- }
6344
- }
6345
- if (days.length > 0) {
6346
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
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 });
6347
6664
  }
6348
6665
  }
6349
- 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"`);
6666
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
6350
6667
  }
6351
- function isValidRecurrenceRule(rule) {
6352
- try {
6353
- parseRecurrenceRule(rule);
6354
- return true;
6355
- } catch {
6356
- return false;
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
+ };
6357
6678
  }
6358
- }
6359
- function nextOccurrence(rule, from) {
6360
- const parsed = parseRecurrenceRule(rule);
6361
- const base = from || new Date;
6362
- if (parsed.type === "interval") {
6363
- const next = new Date(base);
6364
- if (parsed.unit === "day") {
6365
- next.setDate(next.getDate() + parsed.interval);
6366
- } else if (parsed.unit === "week") {
6367
- next.setDate(next.getDate() + parsed.interval * 7);
6368
- } else if (parsed.unit === "month") {
6369
- next.setMonth(next.getMonth() + parsed.interval);
6370
- }
6371
- return next.toISOString();
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) };
6372
6684
  }
6373
- if (parsed.type === "specific_days") {
6374
- const currentDay = base.getDay();
6375
- const days = parsed.days;
6376
- let daysToAdd = Infinity;
6377
- for (const day of days) {
6378
- let diff = day - currentDay;
6379
- if (diff <= 0)
6380
- diff += 7;
6381
- if (diff < daysToAdd)
6382
- daysToAdd = diff;
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
+ };
6383
6698
  }
6384
- const next = new Date(base);
6385
- next.setDate(next.getDate() + daysToAdd);
6386
- return next.toISOString();
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
+ };
6387
6711
  }
6388
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
6389
- }
6390
- var DAY_NAMES;
6391
- var init_recurrence = __esm(() => {
6392
- DAY_NAMES = {
6393
- sunday: 0,
6394
- sun: 0,
6395
- monday: 1,
6396
- mon: 1,
6397
- tuesday: 2,
6398
- tue: 2,
6399
- wednesday: 3,
6400
- wed: 3,
6401
- thursday: 4,
6402
- thu: 4,
6403
- friday: 5,
6404
- fri: 5,
6405
- saturday: 6,
6406
- sat: 6
6407
- };
6408
- });
6409
-
6410
- // src/db/templates.ts
6411
- var exports_templates = {};
6412
- __export(exports_templates, {
6413
- updateTemplate: () => updateTemplate,
6414
- tasksFromTemplate: () => tasksFromTemplate,
6415
- taskFromTemplate: () => taskFromTemplate,
6416
- resolveVariables: () => resolveVariables,
6417
- previewTemplate: () => previewTemplate,
6418
- listTemplates: () => listTemplates,
6419
- listTemplateVersions: () => listTemplateVersions,
6420
- importTemplate: () => importTemplate,
6421
- getTemplateWithTasks: () => getTemplateWithTasks,
6422
- getTemplateVersion: () => getTemplateVersion,
6423
- getTemplateTasks: () => getTemplateTasks,
6424
- getTemplate: () => getTemplate,
6425
- exportTemplate: () => exportTemplate,
6426
- evaluateCondition: () => evaluateCondition,
6427
- deleteTemplate: () => deleteTemplate,
6428
- createTemplate: () => createTemplate,
6429
- addTemplateTasks: () => addTemplateTasks
6430
- });
6431
- function rowToTemplate(row) {
6432
- return {
6433
- ...row,
6434
- tags: JSON.parse(row.tags || "[]"),
6435
- variables: JSON.parse(row.variables || "[]"),
6436
- metadata: JSON.parse(row.metadata || "{}"),
6437
- priority: row.priority || "medium",
6438
- version: row.version ?? 1
6439
- };
6440
- }
6441
- function rowToTemplateTask(row) {
6442
- return {
6443
- ...row,
6444
- tags: JSON.parse(row.tags || "[]"),
6445
- depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
6446
- metadata: JSON.parse(row.metadata || "{}"),
6447
- priority: row.priority || "medium",
6448
- condition: row.condition ?? null,
6449
- include_template_id: row.include_template_id ?? null
6450
- };
6451
- }
6452
- function resolveTemplateId(id, d) {
6453
- return resolvePartialId(d, "task_templates", id);
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) };
6454
6714
  }
6455
- function createTemplate(input, db) {
6715
+ function unlockTask(id, agentId, db) {
6456
6716
  const d = db || getDatabase();
6457
- const id = uuid();
6458
- const machineId = currentStorageMachineId(d);
6459
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
6460
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6461
- id,
6462
- input.name,
6463
- input.title_pattern,
6464
- input.description || null,
6465
- input.priority || "medium",
6466
- JSON.stringify(input.tags || []),
6467
- JSON.stringify(input.variables || []),
6468
- input.project_id || null,
6469
- input.plan_id || null,
6470
- JSON.stringify(input.metadata || {}),
6471
- now(),
6472
- machineId
6473
- ]);
6474
- if (input.tasks && input.tasks.length > 0) {
6475
- addTemplateTasks(id, input.tasks, d);
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);
6476
6722
  }
6477
- return getTemplate(id, d);
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]);
6726
+ return true;
6478
6727
  }
6479
- function getTemplate(id, db) {
6728
+ function getTaskLockStatus(id, db) {
6480
6729
  const d = db || getDatabase();
6481
- const resolved = resolveTemplateId(id, d);
6482
- if (!resolved)
6483
- return null;
6484
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
6485
- return row ? rowToTemplate(row) : null;
6730
+ const task = getTask(id, d);
6731
+ if (!task)
6732
+ throw new TaskNotFoundError(id);
6733
+ const expired = isLockExpired(task.locked_at);
6734
+ return {
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
6741
+ };
6486
6742
  }
6487
- function listTemplates(db) {
6743
+ function claimNextTask(agentId, filters, db) {
6488
6744
  const d = db || getDatabase();
6489
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
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;
6490
6765
  }
6491
- function deleteTemplate(id, db) {
6766
+ function getNextTask(agentId, filters, db) {
6492
6767
  const d = db || getDatabase();
6493
- const resolved = resolveTemplateId(id, d);
6494
- if (!resolved)
6495
- return false;
6496
- const template = getTemplate(resolved, d);
6497
- if (!template)
6498
- return false;
6499
- recordStorageTombstone({
6500
- object_type: "templates",
6501
- object_id: resolved,
6502
- payload: template,
6503
- version: template.version
6504
- }, d);
6505
- return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
6506
- }
6507
- function updateTemplate(id, updates, db) {
6508
- const d = db || getDatabase();
6509
- const resolved = resolveTemplateId(id, d);
6510
- if (!resolved)
6511
- return null;
6512
- const current = getTemplateWithTasks(resolved, d);
6513
- if (current) {
6514
- const snapshot = JSON.stringify({
6515
- name: current.name,
6516
- title_pattern: current.title_pattern,
6517
- description: current.description,
6518
- priority: current.priority,
6519
- tags: current.tags,
6520
- variables: current.variables,
6521
- project_id: current.project_id,
6522
- plan_id: current.plan_id,
6523
- metadata: current.metadata,
6524
- tasks: current.tasks
6525
- });
6526
- d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
6527
- }
6528
- const sets = ["version = version + 1"];
6529
- const values = [];
6530
- if (updates.name !== undefined) {
6531
- sets.push("name = ?");
6532
- values.push(updates.name);
6533
- }
6534
- if (updates.title_pattern !== undefined) {
6535
- sets.push("title_pattern = ?");
6536
- values.push(updates.title_pattern);
6537
- }
6538
- if (updates.description !== undefined) {
6539
- sets.push("description = ?");
6540
- values.push(updates.description);
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);
6541
6774
  }
6542
- if (updates.priority !== undefined) {
6543
- sets.push("priority = ?");
6544
- values.push(updates.priority);
6775
+ if (filters?.task_list_id) {
6776
+ conditions.push("task_list_id = ?");
6777
+ params.push(filters.task_list_id);
6545
6778
  }
6546
- if (updates.tags !== undefined) {
6547
- sets.push("tags = ?");
6548
- values.push(JSON.stringify(updates.tags));
6779
+ if (filters?.plan_id) {
6780
+ conditions.push("plan_id = ?");
6781
+ params.push(filters.plan_id);
6549
6782
  }
6550
- if (updates.variables !== undefined) {
6551
- sets.push("variables = ?");
6552
- values.push(JSON.stringify(updates.variables));
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);
6553
6787
  }
6554
- if (updates.project_id !== undefined) {
6555
- sets.push("project_id = ?");
6556
- values.push(updates.project_id);
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);
6557
6794
  }
6558
- if (updates.plan_id !== undefined) {
6559
- sets.push("plan_id = ?");
6560
- values.push(updates.plan_id);
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);
6561
6799
  }
6562
- if (updates.metadata !== undefined) {
6563
- sets.push("metadata = ?");
6564
- values.push(JSON.stringify(updates.metadata));
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);
6565
6804
  }
6566
- values.push(resolved);
6567
- d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
6568
- return getTemplate(resolved, d);
6569
- }
6570
- function taskFromTemplate(templateId, overrides = {}, db) {
6571
- const t = getTemplate(templateId, db);
6572
- if (!t)
6573
- throw new Error(`Template not found: ${templateId}`);
6574
- const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
6575
- return {
6576
- title: cleanOverrides.title || t.title_pattern,
6577
- description: cleanOverrides.description ?? t.description ?? undefined,
6578
- priority: cleanOverrides.priority ?? t.priority,
6579
- tags: cleanOverrides.tags ?? t.tags,
6580
- project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
6581
- plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
6582
- metadata: cleanOverrides.metadata ?? t.metadata,
6583
- ...cleanOverrides
6584
- };
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;
6585
6808
  }
6586
- function addTemplateTasks(templateId, tasks, db) {
6809
+ function getActiveWork(filters, db) {
6587
6810
  const d = db || getDatabase();
6588
- const template = getTemplate(templateId, d);
6589
- if (!template)
6590
- throw new Error(`Template not found: ${templateId}`);
6591
- d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
6592
- const results = [];
6593
- for (let i = 0;i < tasks.length; i++) {
6594
- const task = tasks[i];
6595
- const id = uuid();
6596
- 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)
6597
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6598
- id,
6599
- templateId,
6600
- i,
6601
- task.title_pattern,
6602
- task.description || null,
6603
- task.priority || "medium",
6604
- JSON.stringify(task.tags || []),
6605
- task.task_type || null,
6606
- task.condition || null,
6607
- task.include_template_id || null,
6608
- JSON.stringify(task.depends_on || []),
6609
- JSON.stringify(task.metadata || {}),
6610
- now()
6611
- ]);
6612
- const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
6613
- if (row)
6614
- results.push(rowToTemplateTask(row));
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);
6615
6817
  }
6616
- return results;
6617
- }
6618
- function getTemplateWithTasks(id, db) {
6619
- const d = db || getDatabase();
6620
- const template = getTemplate(id, d);
6621
- if (!template)
6622
- return null;
6623
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
6624
- const tasks = rows.map(rowToTemplateTask);
6625
- return { ...template, tasks };
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;
6626
6827
  }
6627
- function getTemplateTasks(templateId, db) {
6828
+ function getTasksChangedSince(since, filters, db) {
6628
6829
  const d = db || getDatabase();
6629
- const resolved = resolveTemplateId(templateId, d);
6630
- if (!resolved)
6631
- return [];
6632
- const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
6633
- return rows.map(rowToTemplateTask);
6634
- }
6635
- function evaluateCondition(condition, variables) {
6636
- if (!condition || condition.trim() === "")
6637
- return true;
6638
- const trimmed = condition.trim();
6639
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
6640
- if (eqMatch) {
6641
- const varName = eqMatch[1];
6642
- const expected = eqMatch[2].trim();
6643
- return (variables[varName] ?? "") === expected;
6644
- }
6645
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
6646
- if (neqMatch) {
6647
- const varName = neqMatch[1];
6648
- const expected = neqMatch[2].trim();
6649
- return (variables[varName] ?? "") !== expected;
6650
- }
6651
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
6652
- if (falsyMatch) {
6653
- const varName = falsyMatch[1];
6654
- const val = variables[varName];
6655
- return !val || val === "" || val === "false";
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);
6656
6835
  }
6657
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
6658
- if (truthyMatch) {
6659
- const varName = truthyMatch[1];
6660
- const val = variables[varName];
6661
- return !!val && val !== "" && val !== "false";
6836
+ if (filters?.task_list_id) {
6837
+ conditions.push("task_list_id = ?");
6838
+ params.push(filters.task_list_id);
6662
6839
  }
6663
- return true;
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);
6664
6843
  }
6665
- function exportTemplate(id, db) {
6844
+ function failTask(id, agentId, reason, options, db) {
6666
6845
  const d = db || getDatabase();
6667
- const template = getTemplateWithTasks(id, d);
6668
- if (!template)
6669
- throw new Error(`Template not found: ${id}`);
6670
- return {
6671
- name: template.name,
6672
- title_pattern: template.title_pattern,
6673
- description: template.description,
6674
- priority: template.priority,
6675
- tags: template.tags,
6676
- variables: template.variables,
6677
- project_id: template.project_id,
6678
- plan_id: template.plan_id,
6679
- metadata: template.metadata,
6680
- tasks: template.tasks.map((t) => ({
6681
- position: t.position,
6682
- title_pattern: t.title_pattern,
6683
- description: t.description,
6684
- priority: t.priority,
6685
- tags: t.tags,
6686
- task_type: t.task_type,
6687
- condition: t.condition,
6688
- include_template_id: t.include_template_id,
6689
- depends_on_positions: t.depends_on_positions,
6690
- metadata: t.metadata
6691
- }))
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
6692
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);
6899
+ }
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]);
6915
+ }
6916
+ }
6917
+ return { task: failedTask, retryTask };
6693
6918
  }
6694
- function importTemplate(json, db) {
6919
+ function getStaleTasks(staleQuery = 30, filters, db) {
6695
6920
  const d = db || getDatabase();
6696
- const taskInputs = (json.tasks || []).map((t) => ({
6697
- title_pattern: t.title_pattern,
6698
- description: t.description ?? undefined,
6699
- priority: t.priority,
6700
- tags: t.tags,
6701
- task_type: t.task_type ?? undefined,
6702
- condition: t.condition ?? undefined,
6703
- include_template_id: t.include_template_id ?? undefined,
6704
- depends_on: t.depends_on_positions,
6705
- metadata: t.metadata
6706
- }));
6707
- return createTemplate({
6708
- name: json.name,
6709
- title_pattern: json.title_pattern,
6710
- description: json.description ?? undefined,
6711
- priority: json.priority,
6712
- tags: json.tags,
6713
- variables: json.variables,
6714
- project_id: json.project_id ?? undefined,
6715
- plan_id: json.plan_id ?? undefined,
6716
- metadata: json.metadata,
6717
- tasks: taskInputs
6718
- }, d);
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);
6719
6940
  }
6720
- function getTemplateVersion(id, version, db) {
6941
+ function stealTask(agentId, opts, db) {
6721
6942
  const d = db || getDatabase();
6722
- const resolved = resolveTemplateId(id, d);
6723
- if (!resolved)
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)
6724
6947
  return null;
6725
- const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
6726
- return row || 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;
6727
6965
  }
6728
- function listTemplateVersions(id, db) {
6966
+ function claimOrSteal(agentId, filters, db) {
6729
6967
  const d = db || getDatabase();
6730
- const resolved = resolveTemplateId(id, d);
6731
- if (!resolved)
6732
- return [];
6733
- return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
6734
- }
6735
- function resolveVariables(templateVars, provided) {
6736
- const merged = { ...provided };
6737
- for (const v of templateVars) {
6738
- if (merged[v.name] === undefined && v.default !== undefined) {
6739
- merged[v.name] = v.default;
6740
- }
6741
- }
6742
- const missing = [];
6743
- for (const v of templateVars) {
6744
- if (v.required && merged[v.name] === undefined) {
6745
- missing.push(v.name);
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 };
6746
6973
  }
6747
- }
6748
- if (missing.length > 0) {
6749
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
6750
- }
6751
- return merged;
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();
6752
6980
  }
6753
- function substituteVars(text, variables) {
6754
- let result = text;
6755
- for (const [key, val] of Object.entries(variables)) {
6756
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
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);
6757
6987
  }
6758
- return result;
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);
6759
7005
  }
6760
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
6761
- const d = db || getDatabase();
6762
- const template = getTemplateWithTasks(templateId, d);
6763
- if (!template)
6764
- throw new Error(`Template not found: ${templateId}`);
6765
- const visited = _visitedTemplateIds || new Set;
6766
- if (visited.has(template.id)) {
6767
- throw new Error(`Circular template reference detected: ${template.id}`);
6768
- }
6769
- visited.add(template.id);
6770
- const resolved = resolveVariables(template.variables, variables);
6771
- if (template.tasks.length === 0) {
6772
- const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
6773
- const task = createTask(input, d);
6774
- return [task];
6775
- }
6776
- const createdTasks = [];
6777
- const positionToId = new Map;
6778
- const skippedPositions = new Set;
6779
- for (const tt of template.tasks) {
6780
- if (tt.include_template_id) {
6781
- const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
6782
- createdTasks.push(...includedTasks);
6783
- if (includedTasks.length > 0) {
6784
- positionToId.set(tt.position, includedTasks[0].id);
6785
- } else {
6786
- skippedPositions.add(tt.position);
6787
- }
6788
- continue;
6789
- }
6790
- if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
6791
- skippedPositions.add(tt.position);
6792
- continue;
6793
- }
6794
- let title = tt.title_pattern;
6795
- let desc = tt.description;
6796
- title = substituteVars(title, resolved);
6797
- if (desc)
6798
- desc = substituteVars(desc, resolved);
6799
- const task = createTask({
6800
- title,
6801
- description: desc ?? undefined,
6802
- priority: tt.priority,
6803
- tags: tt.tags,
6804
- task_type: tt.task_type ?? undefined,
6805
- project_id: projectId,
6806
- task_list_id: taskListId,
6807
- metadata: tt.metadata
6808
- }, d);
6809
- createdTasks.push(task);
6810
- positionToId.set(tt.position, task.id);
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);
6811
7040
  }
6812
- for (const tt of template.tasks) {
6813
- if (skippedPositions.has(tt.position))
6814
- continue;
6815
- if (tt.include_template_id)
6816
- continue;
6817
- const deps = tt.depends_on_positions;
6818
- for (const depPos of deps) {
6819
- if (skippedPositions.has(depPos))
6820
- continue;
6821
- const taskId = positionToId.get(tt.position);
6822
- const depId = positionToId.get(depPos);
6823
- if (taskId && depId) {
6824
- addDependency(taskId, depId, d);
6825
- }
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}`);
6826
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));
6827
7055
  }
6828
- return createdTasks;
6829
7056
  }
6830
- function previewTemplate(templateId, variables, db) {
7057
+ function createTask(input, db) {
6831
7058
  const d = db || getDatabase();
6832
- const template = getTemplateWithTasks(templateId, d);
6833
- if (!template)
6834
- throw new Error(`Template not found: ${templateId}`);
6835
- const resolved = resolveVariables(template.variables, variables);
6836
- const tasks = [];
6837
- if (template.tasks.length === 0) {
6838
- tasks.push({
6839
- position: 0,
6840
- title: substituteVars(template.title_pattern, resolved),
6841
- description: template.description ? substituteVars(template.description, resolved) : null,
6842
- priority: template.priority,
6843
- tags: template.tags,
6844
- task_type: null,
6845
- depends_on_positions: []
6846
- });
6847
- } else {
6848
- for (const tt of template.tasks) {
6849
- if (tt.condition && !evaluateCondition(tt.condition, resolved))
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();
6850
7112
  continue;
6851
- tasks.push({
6852
- position: tt.position,
6853
- title: substituteVars(tt.title_pattern, resolved),
6854
- description: tt.description ? substituteVars(tt.description, resolved) : null,
6855
- priority: tt.priority,
6856
- tags: tt.tags,
6857
- task_type: tt.task_type,
6858
- depends_on_positions: tt.depends_on_positions
6859
- });
7113
+ }
7114
+ throw e;
6860
7115
  }
6861
7116
  }
6862
- return {
6863
- template_id: template.id,
6864
- template_name: template.name,
6865
- description: template.description,
6866
- variables: template.variables,
6867
- resolved_variables: resolved,
6868
- tasks
6869
- };
6870
- }
6871
- var init_templates = __esm(() => {
6872
- init_database();
6873
- init_tasks();
6874
- init_storage_tombstones();
6875
- });
6876
-
6877
- // src/db/task-graph.ts
6878
- function addDependency(taskId, dependsOn, db) {
6879
- const d = db || getDatabase();
6880
- if (!getTask(taskId, d))
6881
- throw new TaskNotFoundError(taskId);
6882
- if (!getTask(dependsOn, d))
6883
- throw new TaskNotFoundError(dependsOn);
6884
- if (wouldCreateCycle(taskId, dependsOn, d)) {
6885
- throw new DependencyCycleError(taskId, dependsOn);
7117
+ if (tags.length > 0) {
7118
+ insertTaskTags(id, tags, d);
6886
7119
  }
6887
- d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
6888
- }
6889
- function removeDependency(taskId, dependsOn, db) {
6890
- const d = db || getDatabase();
6891
- const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
6892
- return result.changes > 0;
6893
- }
6894
- function getTaskDependencies(taskId, db) {
6895
- const d = db || getDatabase();
6896
- 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;
6897
7127
  }
6898
- function getTaskDependents(taskId, db) {
7128
+ function getTask(id, db) {
6899
7129
  const d = db || getDatabase();
6900
- 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);
6901
7134
  }
6902
- function cloneTask(taskId, overrides, db) {
7135
+ function getTaskWithRelations(id, db) {
6903
7136
  const d = db || getDatabase();
6904
- const source = getTask(taskId, d);
6905
- if (!source)
6906
- throw new TaskNotFoundError(taskId);
6907
- const input = {
6908
- title: overrides?.title ?? source.title,
6909
- description: overrides?.description ?? source.description ?? undefined,
6910
- priority: overrides?.priority ?? source.priority,
6911
- project_id: overrides?.project_id ?? source.project_id ?? undefined,
6912
- parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
6913
- plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
6914
- task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
6915
- status: overrides?.status ?? "pending",
6916
- agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
6917
- assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
6918
- tags: overrides?.tags ?? source.tags,
6919
- metadata: overrides?.metadata ?? source.metadata,
6920
- estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
6921
- 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
6922
7161
  };
6923
- return createTask(input, d);
6924
7162
  }
6925
- function getTaskGraph(taskId, direction = "both", db) {
7163
+ function listTasks(filter = {}, db) {
6926
7164
  const d = db || getDatabase();
6927
- const task = getTask(taskId, d);
6928
- if (!task)
6929
- throw new TaskNotFoundError(taskId);
6930
- function toNode(t) {
6931
- const deps = getTaskDependencies(t.id, d);
6932
- const hasUnfinishedDeps = deps.some((dep) => {
6933
- const depTask = getTask(dep.depends_on, d);
6934
- return depTask && depTask.status !== "completed";
6935
- });
6936
- 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);
6937
7172
  }
6938
- function buildUp(id, visited) {
6939
- if (visited.has(id))
6940
- return [];
6941
- visited.add(id);
6942
- const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
6943
- return deps.map((dep) => {
6944
- const depTask = getTask(dep.depends_on, d);
6945
- if (!depTask)
6946
- return null;
6947
- return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
6948
- }).filter(Boolean);
7173
+ if (filter.ids && filter.ids.length > 0) {
7174
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
7175
+ params.push(...filter.ids);
6949
7176
  }
6950
- function buildDown(id, visited) {
6951
- if (visited.has(id))
6952
- return [];
6953
- visited.add(id);
6954
- const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
6955
- return dependents.map((dep) => {
6956
- const depTask = getTask(dep.task_id, d);
6957
- if (!depTask)
6958
- return null;
6959
- return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
6960
- }).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
+ }
6961
7184
  }
6962
- const rootNode = toNode(task);
6963
- const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
6964
- const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
6965
- return { task: rootNode, depends_on, blocks };
6966
- }
6967
- function moveTask(taskId, target, db) {
6968
- const d = db || getDatabase();
6969
- const task = getTask(taskId, d);
6970
- if (!task)
6971
- throw new TaskNotFoundError(taskId);
6972
- const sets = ["updated_at = ?", "version = version + 1"];
6973
- const params = [now()];
6974
- if (target.task_list_id !== undefined) {
6975
- sets.push("task_list_id = ?");
6976
- 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
+ }
6977
7193
  }
6978
- if (target.project_id !== undefined) {
6979
- sets.push("project_id = ?");
6980
- 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
+ }
6981
7202
  }
6982
- if (target.plan_id !== undefined) {
6983
- sets.push("plan_id = ?");
6984
- params.push(target.plan_id);
7203
+ if (filter.assigned_to) {
7204
+ conditions.push("assigned_to = ?");
7205
+ params.push(filter.assigned_to);
6985
7206
  }
6986
- params.push(taskId);
6987
- d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
6988
- return getTask(taskId, d);
6989
- }
6990
- function wouldCreateCycle(taskId, dependsOn, db) {
6991
- const visited = new Set;
6992
- const queue = [dependsOn];
6993
- while (queue.length > 0) {
6994
- const current = queue.shift();
6995
- if (current === taskId)
6996
- return true;
6997
- if (visited.has(current))
6998
- continue;
6999
- visited.add(current);
7000
- const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
7001
- for (const dep of deps) {
7002
- 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);
7003
7240
  }
7004
7241
  }
7005
- 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);
7006
7266
  }
7007
- var init_task_graph = __esm(() => {
7008
- init_types();
7009
- init_database();
7010
- init_task_crud();
7011
- });
7012
-
7013
- // src/db/task-lifecycle.ts
7014
- function lockExpiresAt(lockedAt) {
7015
- if (!lockedAt)
7016
- return null;
7017
- 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;
7018
7270
  }
7019
- function assertStartable(task, agentId) {
7020
- if (task.status === "pending")
7021
- return;
7022
- if (task.status === "in_progress")
7023
- return;
7024
- 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
+ };
7025
7277
  }
7026
- function getBlockingDeps(id, db) {
7278
+ function upsertTaskByFingerprint(input, db) {
7027
7279
  const d = db || getDatabase();
7028
- const deps = getTaskDependencies(id, d);
7029
- if (deps.length === 0)
7030
- return [];
7031
- const blocking = [];
7032
- for (const dep of deps) {
7033
- const task = getTask(dep.depends_on, d);
7034
- if (task && task.status !== "completed")
7035
- blocking.push(task);
7036
- }
7037
- 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();
7038
7317
  }
7039
- function startTask(id, agentId, db) {
7318
+ function countTasks(filter = {}, db) {
7040
7319
  const d = db || getDatabase();
7041
- const databasePath = databasePathFromDatabase(d);
7042
- const task = getTask(id, d);
7043
- if (!task)
7044
- throw new TaskNotFoundError(id);
7045
- assertStartable(task, agentId);
7046
- const blocking = getBlockingDeps(id, d);
7047
- if (blocking.length > 0) {
7048
- const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
7049
- emitLocalEventHooksQuiet({
7050
- type: "task.blocked",
7051
- payload: {
7052
- id,
7053
- agent_id: agentId,
7054
- title: task.title,
7055
- blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
7056
- },
7057
- databasePath
7058
- });
7059
- throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
7060
- }
7061
- const cutoff = lockExpiryCutoff();
7062
- const timestamp = now();
7063
- 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 = ?
7064
- 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]);
7065
- if (result.changes === 0) {
7066
- const current = getTask(id, d);
7067
- if (!current)
7068
- throw new TaskNotFoundError(id);
7069
- assertStartable(current, agentId);
7070
- if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
7071
- throw new LockError(id, current.locked_by);
7072
- }
7073
- 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);
7074
7325
  }
7075
- logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
7076
- 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 };
7077
- const payload = taskEventData(startedTask, { agent_id: agentId });
7078
- dispatchWebhook2("task.started", payload, d).catch(() => {});
7079
- emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
7080
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
7081
- return startedTask;
7082
- }
7083
- function completeTask(id, agentId, db, options) {
7084
- const d = db || getDatabase();
7085
- const databasePath = databasePathFromDatabase(d);
7086
- const task = getTask(id, d);
7087
- if (!task)
7088
- throw new TaskNotFoundError(id);
7089
- if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
7090
- 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);
7091
7329
  }
7092
- checkCompletionGuard(task, agentId || null, d);
7093
- 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;
7094
- const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
7095
- const completionMeta = {};
7096
- if (hasEvidence)
7097
- completionMeta._evidence = evidence;
7098
- if (options?.confidence !== undefined) {
7099
- 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
+ }
7100
7337
  }
7101
- const hasMeta = Object.keys(completionMeta).length > 0;
7102
- const timestamp = options?.completed_at || now();
7103
- const confidence = options?.confidence !== undefined ? options.confidence : null;
7104
- const tx = d.transaction(() => {
7105
- if (hasMeta) {
7106
- const meta2 = { ...task.metadata, ...completionMeta };
7107
- const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
7108
- if (metaResult.changes === 0) {
7109
- const current = getTask(id, d);
7110
- throw new VersionConflictError(id, task.version, current?.version ?? -1);
7111
- }
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);
7112
7345
  }
7113
- d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
7114
- WHERE id = ?`, [timestamp, confidence, timestamp, id]);
7115
- });
7116
- tx();
7117
- logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
7118
- const completedTaskForEvent = {
7119
- ...task,
7120
- status: "completed",
7121
- locked_by: null,
7122
- locked_at: null,
7123
- completed_at: timestamp,
7124
- confidence,
7125
- version: task.version + 1,
7126
- updated_at: timestamp,
7127
- metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
7128
- };
7129
- const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
7130
- dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
7131
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
7132
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
7133
- let spawnedTask = null;
7134
- if (task.recurrence_rule && !options?.skip_recurrence) {
7135
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
7136
7346
  }
7137
- let spawnedFromTemplate = null;
7138
- if (task.spawns_template_id) {
7139
- const spawnDepth = task.metadata?._spawn_depth || 0;
7140
- if (spawnDepth >= MAX_SPAWN_DEPTH) {
7141
- 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);
7142
7351
  } else {
7143
- try {
7144
- const input = taskFromTemplate(task.spawns_template_id, {
7145
- project_id: task.project_id ?? undefined,
7146
- plan_id: task.plan_id ?? undefined,
7147
- task_list_id: task.task_list_id ?? undefined,
7148
- assigned_to: task.assigned_to ?? undefined
7149
- }, d);
7150
- input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
7151
- spawnedFromTemplate = createTask(input, d);
7152
- } catch {}
7352
+ conditions.push("priority = ?");
7353
+ params.push(filter.priority);
7153
7354
  }
7154
7355
  }
7155
- const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
7156
- if (spawnedTask) {
7157
- 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);
7158
7359
  }
7159
- if (spawnedFromTemplate) {
7160
- 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);
7161
7363
  }
7162
- const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
7163
- JOIN task_dependencies td ON td.task_id = t.id
7164
- WHERE td.depends_on = ? AND t.status = 'pending'
7165
- AND NOT EXISTS (
7166
- SELECT 1 FROM task_dependencies td2
7167
- JOIN tasks dep2 ON dep2.id = td2.depends_on
7168
- WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
7169
- )`).all(id, id);
7170
- if (unblockedDeps.length > 0) {
7171
- meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
7172
- for (const dep of unblockedDeps) {
7173
- const depTask = getTask(dep.id, d);
7174
- const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
7175
- dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
7176
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
7177
- if (depTask)
7178
- 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);
7179
7393
  }
7180
7394
  }
7181
- 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;
7182
7402
  }
7183
- function lockTask(id, agentId, db) {
7403
+ function updateTask(id, input, db) {
7184
7404
  const d = db || getDatabase();
7185
7405
  const task = getTask(id, d);
7186
7406
  if (!task)
7187
7407
  throw new TaskNotFoundError(id);
7188
- if (task.status === "completed" || task.status === "cancelled") {
7189
- return {
7190
- success: false,
7191
- error: `Task is ${task.status} and cannot be locked`
7192
- };
7193
- }
7194
- if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
7195
- const timestamp2 = now();
7196
- d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
7197
- logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
7198
- 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);
7199
7410
  }
7200
- const cutoff = lockExpiryCutoff();
7201
7411
  const timestamp = now();
7202
- const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
7203
- 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]);
7204
- if (result.changes === 0) {
7205
- const current = getTask(id, d);
7206
- if (!current)
7207
- throw new TaskNotFoundError(id);
7208
- if (current.status === "completed" || current.status === "cancelled") {
7209
- return {
7210
- success: false,
7211
- error: `Task is ${current.status} and cannot be locked`
7212
- };
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);
7213
7426
  }
7214
- if (current.locked_by && !isLockExpired(current.locked_at)) {
7215
- return {
7216
- success: false,
7217
- locked_by: current.locked_by,
7218
- locked_at: current.locked_at,
7219
- error: `Task is locked by ${current.locked_by}`
7220
- };
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");
7221
7436
  }
7222
- return {
7223
- success: false,
7224
- error: `Task ${id} could not be locked because it changed during lock acquisition`
7225
- };
7226
7437
  }
7227
- logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
7228
- return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
7229
- }
7230
- function unlockTask(id, agentId, db) {
7231
- const d = db || getDatabase();
7232
- const task = getTask(id, d);
7233
- if (!task)
7234
- throw new TaskNotFoundError(id);
7235
- if (agentId && task.locked_by && task.locked_by !== agentId) {
7236
- throw new LockError(id, task.locked_by);
7438
+ if (input.priority !== undefined) {
7439
+ sets.push("priority = ?");
7440
+ params.push(input.priority);
7237
7441
  }
7238
- const timestamp = now();
7239
- d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
7240
- WHERE id = ?`, [timestamp, id]);
7241
- return true;
7242
- }
7243
- function getTaskLockStatus(id, db) {
7244
- const d = db || getDatabase();
7245
- const task = getTask(id, d);
7246
- if (!task)
7247
- throw new TaskNotFoundError(id);
7248
- const expired = isLockExpired(task.locked_at);
7249
- return {
7250
- task_id: id,
7251
- locked: !!task.locked_by && !expired,
7252
- locked_by: task.locked_by,
7253
- locked_at: task.locked_at,
7254
- expires_at: lockExpiresAt(task.locked_at),
7255
- expired
7256
- };
7257
- }
7258
- function claimNextTask(agentId, filters, db) {
7259
- const d = db || getDatabase();
7260
- const tx = d.transaction(() => {
7261
- const task = getNextTask(agentId, filters, d);
7262
- if (!task)
7263
- return null;
7264
- return startTask(task.id, agentId, d);
7265
- });
7266
- return tx();
7267
- }
7268
- function getNextTask(agentId, filters, db) {
7269
- const d = db || getDatabase();
7270
- clearExpiredLocks(d);
7271
- const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
7272
- const params = [lockExpiryCutoff()];
7273
- if (filters?.project_id) {
7274
- conditions.push("project_id = ?");
7275
- params.push(filters.project_id);
7442
+ if (input.project_id !== undefined) {
7443
+ sets.push("project_id = ?");
7444
+ params.push(input.project_id);
7276
7445
  }
7277
- if (filters?.task_list_id) {
7278
- conditions.push("task_list_id = ?");
7279
- params.push(filters.task_list_id);
7446
+ if (input.assigned_to !== undefined) {
7447
+ sets.push("assigned_to = ?");
7448
+ params.push(input.assigned_to);
7280
7449
  }
7281
- if (filters?.plan_id) {
7282
- conditions.push("plan_id = ?");
7283
- params.push(filters.plan_id);
7450
+ if (input.working_dir !== undefined) {
7451
+ sets.push("working_dir = ?");
7452
+ params.push(input.working_dir);
7284
7453
  }
7285
- if (filters?.tags && filters.tags.length > 0) {
7286
- const placeholders = filters.tags.map(() => "?").join(",");
7287
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
7288
- params.push(...filters.tags);
7454
+ if (input.tags !== undefined) {
7455
+ sets.push("tags = ?");
7456
+ params.push(JSON.stringify(input.tags));
7289
7457
  }
7290
- 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')");
7291
- const where = conditions.join(" AND ");
7292
- let recentProjectIds = [];
7293
- if (agentId) {
7294
- 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);
7295
- recentProjectIds = recentRows.map((r) => r.project_id);
7458
+ if (input.metadata !== undefined) {
7459
+ sets.push("metadata = ?");
7460
+ params.push(JSON.stringify(input.metadata));
7296
7461
  }
7297
- let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
7298
- if (agentId) {
7299
- sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
7300
- params.push(agentId);
7462
+ if (input.plan_id !== undefined) {
7463
+ sets.push("plan_id = ?");
7464
+ params.push(input.plan_id);
7301
7465
  }
7302
- if (recentProjectIds.length > 0) {
7303
- const placeholders = recentProjectIds.map(() => "?").join(",");
7304
- sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
7305
- params.push(...recentProjectIds);
7466
+ if (input.task_list_id !== undefined) {
7467
+ sets.push("task_list_id = ?");
7468
+ params.push(input.task_list_id);
7306
7469
  }
7307
- 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`;
7308
- const row = d.query(sql).get(...params);
7309
- return row ? rowToTask(row) : null;
7310
- }
7311
- function getActiveWork(filters, db) {
7312
- const d = db || getDatabase();
7313
- clearExpiredLocks(d);
7314
- const conditions = ["status = 'in_progress'"];
7315
- const params = [];
7316
- if (filters?.project_id) {
7317
- conditions.push("project_id = ?");
7318
- params.push(filters.project_id);
7470
+ if (input.due_at !== undefined) {
7471
+ sets.push("due_at = ?");
7472
+ params.push(input.due_at);
7319
7473
  }
7320
- if (filters?.task_list_id) {
7321
- conditions.push("task_list_id = ?");
7322
- params.push(filters.task_list_id);
7474
+ if (input.estimated_minutes !== undefined) {
7475
+ sets.push("estimated_minutes = ?");
7476
+ params.push(input.estimated_minutes);
7323
7477
  }
7324
- const where = conditions.join(" AND ");
7325
- const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
7326
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
7327
- updated_at DESC`).all(...params);
7328
- return rows;
7329
- }
7330
- function getTasksChangedSince(since, filters, db) {
7331
- const d = db || getDatabase();
7332
- const conditions = ["updated_at > ?"];
7333
- const params = [since];
7334
- if (filters?.project_id) {
7335
- conditions.push("project_id = ?");
7336
- params.push(filters.project_id);
7478
+ if (input.sla_minutes !== undefined) {
7479
+ sets.push("sla_minutes = ?");
7480
+ params.push(input.sla_minutes);
7337
7481
  }
7338
- if (filters?.task_list_id) {
7339
- conditions.push("task_list_id = ?");
7340
- params.push(filters.task_list_id);
7482
+ if (input.actual_minutes !== undefined) {
7483
+ sets.push("actual_minutes = ?");
7484
+ params.push(input.actual_minutes);
7341
7485
  }
7342
- const where = conditions.join(" AND ");
7343
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
7344
- return rows.map(rowToTask);
7345
- }
7346
- function failTask(id, agentId, reason, options, db) {
7347
- const d = db || getDatabase();
7348
- const databasePath = databasePathFromDatabase(d);
7349
- const task = getTask(id, d);
7350
- if (!task)
7351
- throw new TaskNotFoundError(id);
7352
- const meta = {
7353
- ...task.metadata,
7354
- _failure: {
7355
- reason: reason || "Unknown failure",
7356
- error_code: options?.error_code || null,
7357
- failed_by: agentId || null,
7358
- failed_at: now(),
7359
- 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)}`);
7360
7540
  }
7361
- };
7362
- const timestamp = now();
7363
- d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
7364
- WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
7365
- 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 = {
7366
7558
  ...task,
7367
- status: "failed",
7368
- locked_by: null,
7369
- locked_at: null,
7370
- metadata: meta,
7559
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
7560
+ tags: input.tags ?? task.tags,
7561
+ metadata: input.metadata ?? task.metadata,
7371
7562
  version: task.version + 1,
7372
- 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
7373
7576
  };
7374
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
7375
- const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
7376
- dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
7377
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
7378
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
7379
- let retryTask;
7380
- if (options?.retry) {
7381
- const retryCount = (task.retry_count || 0) + 1;
7382
- const maxRetries = task.max_retries || 3;
7383
- if (retryCount > maxRetries) {
7384
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
7385
- JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
7386
- id
7387
- ]);
7388
- } else {
7389
- const backoffMinutes = Math.pow(5, retryCount - 1);
7390
- const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
7391
- let title = task.title;
7392
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
7393
- title = title.slice(task.short_id.length + 2);
7394
- }
7395
- retryTask = createTask({
7396
- title,
7397
- description: task.description ?? undefined,
7398
- priority: task.priority,
7399
- project_id: task.project_id ?? undefined,
7400
- task_list_id: task.task_list_id ?? undefined,
7401
- plan_id: task.plan_id ?? undefined,
7402
- assigned_to: task.assigned_to ?? undefined,
7403
- tags: task.tags,
7404
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
7405
- estimated_minutes: task.estimated_minutes ?? undefined,
7406
- recurrence_rule: task.recurrence_rule ?? undefined,
7407
- due_at: retryAfter
7408
- }, d);
7409
- d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
7410
- }
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 });
7411
7583
  }
7412
- return { task: failedTask, retryTask };
7413
- }
7414
- function getStaleTasks(staleQuery = 30, filters, db) {
7415
- const d = db || getDatabase();
7416
- const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
7417
- const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
7418
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
7419
- const conditions = [
7420
- "status = 'in_progress'",
7421
- "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
7422
- ];
7423
- const params = [cutoff, cutoff];
7424
- if (effectiveFilters?.project_id) {
7425
- conditions.push("project_id = ?");
7426
- 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 });
7427
7589
  }
7428
- if (effectiveFilters?.task_list_id) {
7429
- conditions.push("task_list_id = ?");
7430
- 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 });
7431
7592
  }
7432
- const where = conditions.join(" AND ");
7433
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
7434
- return rows.map(rowToTask);
7435
- }
7436
- function stealTask(agentId, opts, db) {
7437
- const d = db || getDatabase();
7438
- const databasePath = databasePathFromDatabase(d);
7439
- const staleMinutes = opts?.stale_minutes ?? 30;
7440
- const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
7441
- if (staleTasks.length === 0)
7442
- return null;
7443
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
7444
- staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
7445
- const target = staleTasks[0];
7446
- const timestamp = now();
7447
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
7448
- const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
7449
- 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]);
7450
- if (result.changes === 0)
7451
- return null;
7452
- logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
7453
- logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
7454
- const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
7455
- const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
7456
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7457
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
7458
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
7459
- 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;
7460
7598
  }
7461
- function claimOrSteal(agentId, filters, db) {
7599
+ function deleteTask(id, db) {
7462
7600
  const d = db || getDatabase();
7463
- const tx = d.transaction(() => {
7464
- const next = getNextTask(agentId, filters, d);
7465
- if (next) {
7466
- const started = startTask(next.id, agentId, d);
7467
- return { task: started, stolen: false };
7468
- }
7469
- const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
7470
- if (stolen)
7471
- return { task: stolen, stolen: true };
7472
- return null;
7473
- });
7474
- return tx();
7475
- }
7476
- function spawnNextRecurrence(completedTask, db, completedAt) {
7477
- const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
7478
- const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
7479
- let title = completedTask.title;
7480
- if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
7481
- title = title.slice(completedTask.short_id.length + 2);
7482
- }
7483
- const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
7484
- return createTask({
7485
- title,
7486
- description: completedTask.description ?? undefined,
7487
- priority: completedTask.priority,
7488
- project_id: completedTask.project_id ?? undefined,
7489
- task_list_id: completedTask.task_list_id ?? undefined,
7490
- plan_id: completedTask.plan_id ?? undefined,
7491
- assigned_to: completedTask.assigned_to ?? undefined,
7492
- tags: completedTask.tags,
7493
- metadata: completedTask.metadata,
7494
- estimated_minutes: completedTask.estimated_minutes ?? undefined,
7495
- sla_minutes: completedTask.sla_minutes ?? undefined,
7496
- recurrence_rule: completedTask.recurrence_rule,
7497
- recurrence_parent_id: recurrenceParentId,
7498
- due_at: dueAt
7499
- }, 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;
7500
7612
  }
7501
- var MAX_SPAWN_DEPTH = 10;
7502
- var init_task_lifecycle = __esm(() => {
7613
+ var init_task_crud = __esm(() => {
7503
7614
  init_types();
7504
7615
  init_database();
7505
7616
  init_completion_guard();
@@ -7507,11 +7618,9 @@ var init_task_lifecycle = __esm(() => {
7507
7618
  init_event_hooks();
7508
7619
  init_shared_events();
7509
7620
  init_audit();
7510
- init_recurrence();
7511
7621
  init_webhooks();
7512
- init_templates();
7513
- init_task_crud();
7514
- init_task_graph();
7622
+ init_checklists();
7623
+ init_storage_tombstones();
7515
7624
  });
7516
7625
 
7517
7626
  // src/db/task-status.ts
@@ -7601,6 +7710,15 @@ function setTaskStatus(id, status, _agentId, db) {
7601
7710
  throw new TaskNotFoundError(id);
7602
7711
  if (task.status === status)
7603
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
+ }
7604
7722
  try {
7605
7723
  return updateTask(id, { status, version: task.version }, d);
7606
7724
  } catch (e) {
@@ -11557,6 +11675,37 @@ function parseBoundedLimit(value, fallback, max) {
11557
11675
  return fallback;
11558
11676
  return Math.min(parsed, max);
11559
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
+ }
11560
11709
  function handleSseEvents(_req, url, ctx) {
11561
11710
  const agentId = url.searchParams.get("agent_id") || undefined;
11562
11711
  const projectId = url.searchParams.get("project_id") || undefined;
@@ -11635,35 +11784,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
11635
11784
  });
11636
11785
  }
11637
11786
  function handleHealth(_ctx, json2) {
11638
- const all = listTasks({ limit: 1e4 });
11639
- const stale = all.filter((t) => t.status === "in_progress" && new Date(t.updated_at).getTime() < Date.now() - 30 * 60 * 1000);
11640
- const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < new Date().toISOString());
11641
- 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
+ });
11642
11797
  }
11643
11798
  function handleHeadlessBoundary(_ctx, json2) {
11644
11799
  const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
11645
11800
  return json2(getHeadlessBoundaryManifest2());
11646
11801
  }
11647
11802
  function handleStats(_ctx, json2) {
11648
- const all = listTasks({ limit: 1e4 });
11803
+ const stats = getTaskStats();
11804
+ const byStatus = stats.by_status;
11649
11805
  const projects = listProjects();
11650
11806
  const agents = listAgents();
11651
- const staleItems = getStaleTasks(30);
11652
- const nowStr = new Date().toISOString();
11653
- const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
11654
- 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;
11655
11809
  return json2({
11656
- total_tasks: all.length,
11657
- pending: all.filter((t) => t.status === "pending").length,
11658
- in_progress: all.filter((t) => t.status === "in_progress").length,
11659
- completed: all.filter((t) => t.status === "completed").length,
11660
- failed: all.filter((t) => t.status === "failed").length,
11661
- 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,
11662
11816
  projects: projects.length,
11663
11817
  agents: agents.length,
11664
- stale_count: staleItems.length,
11818
+ stale_count: staleCount,
11665
11819
  overdue_recurring: overdueRecurring,
11666
- recurring_tasks: recurringTasks
11820
+ recurring_tasks: countRecurringTasks()
11667
11821
  });
11668
11822
  }
11669
11823
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
@@ -11742,27 +11896,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
11742
11896
  const summaries = tasks.map((t) => taskToSummary2(t));
11743
11897
  if (format === "csv") {
11744
11898
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
11745
- const rows = summaries.map((t) => headers.map((h) => {
11746
- const val = t[h];
11899
+ const csvCell = (val) => {
11747
11900
  if (val === null || val === undefined)
11748
11901
  return "";
11749
- const str = String(val);
11750
- return str.includes(",") || str.includes('"') || str.includes(`
11751
- `) ? `"${str.replace(/"/g, '""')}"` : str;
11752
- }).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(","));
11753
11912
  const csv = [headers.join(","), ...rows].join(`
11754
11913
  `);
11755
11914
  return new Response(csv, {
11756
11915
  headers: {
11757
11916
  "Content-Type": "text/csv",
11758
- "Content-Disposition": "attachment; filename=tasks.csv"
11917
+ "Content-Disposition": "attachment; filename=tasks.csv",
11918
+ ...SECURITY_HEADERS
11759
11919
  }
11760
11920
  });
11761
11921
  }
11762
11922
  return new Response(JSON.stringify(summaries, null, 2), {
11763
11923
  headers: {
11764
11924
  "Content-Type": "application/json",
11765
- "Content-Disposition": "attachment; filename=tasks.json"
11925
+ "Content-Disposition": "attachment; filename=tasks.json",
11926
+ ...SECURITY_HEADERS
11766
11927
  }
11767
11928
  });
11768
11929
  }
@@ -11936,12 +12097,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
11936
12097
  if (ALLOWED.has(key))
11937
12098
  safeBody[key] = value;
11938
12099
  }
12100
+ const clientVersion = typeof body["version"] === "number" ? body["version"] : task.version;
11939
12101
  const updated = updateTask(id, {
11940
12102
  ...safeBody,
11941
- version: task.version
12103
+ version: clientVersion
11942
12104
  });
11943
12105
  return json2(taskToSummary2(updated));
11944
12106
  } catch (e) {
12107
+ const mapped = mapTaskError(e, json2);
12108
+ if (mapped)
12109
+ return mapped;
11945
12110
  return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
11946
12111
  }
11947
12112
  }
@@ -11957,6 +12122,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
11957
12122
  ctx.broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
11958
12123
  return json2(taskToSummary2(task));
11959
12124
  } catch (e) {
12125
+ const mapped = mapTaskError(e, json2);
12126
+ if (mapped)
12127
+ return mapped;
11960
12128
  return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
11961
12129
  }
11962
12130
  }
@@ -11976,6 +12144,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
11976
12144
  ctx.broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
11977
12145
  return json2(taskToSummary2(task));
11978
12146
  } catch (e) {
12147
+ const mapped = mapTaskError(e, json2);
12148
+ if (mapped)
12149
+ return mapped;
11979
12150
  return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
11980
12151
  }
11981
12152
  }
@@ -12300,6 +12471,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
12300
12471
  }
12301
12472
  var init_routes = __esm(() => {
12302
12473
  init_tasks();
12474
+ init_database();
12475
+ init_types();
12303
12476
  init_projects();
12304
12477
  init_agents();
12305
12478
  init_plans();
@@ -33318,10 +33491,8 @@ var init_token_utils = __esm(() => {
33318
33491
  "cancel_task",
33319
33492
  "check_task_done_contract",
33320
33493
  "claim_task",
33321
- "clone_task",
33322
33494
  "delete_task",
33323
33495
  "extend_task",
33324
- "get_active_work",
33325
33496
  "get_archived_tasks",
33326
33497
  "get_blocked_tasks",
33327
33498
  "get_blocking_tasks",
@@ -33357,7 +33528,8 @@ var init_token_utils = __esm(() => {
33357
33528
  "task_context",
33358
33529
  "unlock_task",
33359
33530
  "unarchive_task",
33360
- "update_task"
33531
+ "update_task",
33532
+ "upsert_task"
33361
33533
  ],
33362
33534
  projects: [
33363
33535
  "bootstrap_project",
@@ -33616,12 +33788,8 @@ var init_token_utils = __esm(() => {
33616
33788
  "delete_tag",
33617
33789
  "get_label",
33618
33790
  "get_activity_timeline",
33619
- "get_recent_activity",
33620
33791
  "get_tag",
33621
33792
  "get_task_fields",
33622
- "get_task_graph",
33623
- "get_task_history",
33624
- "get_task_stats",
33625
33793
  "list_workflow_states",
33626
33794
  "list_labels",
33627
33795
  "list_tags",
@@ -33632,6 +33800,10 @@ var init_token_utils = __esm(() => {
33632
33800
  "describe_tools",
33633
33801
  "set_task_workflow_state",
33634
33802
  "set_task_fields",
33803
+ "assign_label_to_task",
33804
+ "create_custom_field",
33805
+ "set_task_custom_field",
33806
+ "set_task_priority_meta",
33635
33807
  "update_label",
33636
33808
  "update_tag"
33637
33809
  ],
@@ -33657,7 +33829,6 @@ var init_token_utils = __esm(() => {
33657
33829
  "update_template",
33658
33830
  "write_template_library"
33659
33831
  ],
33660
- webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
33661
33832
  machines: [
33662
33833
  "machines_archive",
33663
33834
  "machines_delete",
@@ -79511,14 +79682,16 @@ function printHelp() {
79511
79682
  Start the @hasna/todos MCP server.
79512
79683
 
79513
79684
  Options:
79514
- --stdio Use stdio transport
79515
- --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)
79516
79688
  -V, --version output the version number
79517
79689
  -h, --help display help for command
79518
79690
 
79519
79691
  Environment:
79520
- TODOS_MCP_STDIO=true Force stdio transport
79521
- 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
79522
79695
  TODOS_PROFILE=<profile> Tool profile filter
79523
79696
  TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
79524
79697
  }
@@ -79607,8 +79780,22 @@ function formatError2(error2) {
79607
79780
  function resolveId(partialId, table = "tasks") {
79608
79781
  const db = getDatabase();
79609
79782
  const id = resolvePartialId(db, table, partialId);
79610
- if (!id)
79611
- 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
+ }
79612
79799
  return id;
79613
79800
  }
79614
79801
  function formatTask(task2) {
@@ -79697,8 +79884,9 @@ function buildServer() {
79697
79884
  return server;
79698
79885
  }
79699
79886
  async function main() {
79700
- const { isStdioMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http(), exports_http));
79701
- 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) {
79702
79890
  const server = buildServer();
79703
79891
  const transport = new StdioServerTransport;
79704
79892
  await server.connect(transport);
@@ -79876,7 +80064,7 @@ function checkAuth(req, apiKey) {
79876
80064
  if (!apiKey && !generatedKeysEnabled)
79877
80065
  return null;
79878
80066
  const provided = getProvidedApiKey(req);
79879
- const matchesEnvKey = Boolean(apiKey && provided && provided === apiKey);
80067
+ const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
79880
80068
  const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
79881
80069
  if (!matchesEnvKey && !matchesGeneratedKey) {
79882
80070
  return new Response(JSON.stringify({ error: "Unauthorized" }), {
@@ -79886,6 +80074,15 @@ function checkAuth(req, apiKey) {
79886
80074
  }
79887
80075
  return null;
79888
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
+ }
79889
80086
  function checkRateLimit(ip) {
79890
80087
  const now4 = Date.now();
79891
80088
  const entry = rateLimitMap.get(ip);
@@ -80013,7 +80210,7 @@ Dashboard not found at: ${dashboardDir}`);
80013
80210
  const server = Bun.serve({
80014
80211
  port,
80015
80212
  hostname: hostname4,
80016
- async fetch(req) {
80213
+ async fetch(req, server2) {
80017
80214
  const url = new URL(req.url);
80018
80215
  const path = url.pathname;
80019
80216
  const method = req.method;
@@ -80025,15 +80222,6 @@ Dashboard not found at: ${dashboardDir}`);
80025
80222
  Vary: "Origin"
80026
80223
  } : undefined;
80027
80224
  const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
80028
- if (path === "/health" && method === "GET") {
80029
- const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
80030
- return healthResponse2("todos");
80031
- }
80032
- if (path === "/mcp") {
80033
- const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
80034
- const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp3(), exports_mcp));
80035
- return handleMcpHttpRequest2(req, buildServer2);
80036
- }
80037
80225
  if (method === "OPTIONS") {
80038
80226
  return new Response(null, {
80039
80227
  headers: corsHeaders || {
@@ -80041,7 +80229,7 @@ Dashboard not found at: ${dashboardDir}`);
80041
80229
  }
80042
80230
  });
80043
80231
  }
80044
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
80232
+ const ip = resolveClientIp(req, server2);
80045
80233
  const rl = checkRateLimit(ip);
80046
80234
  if (!rl.allowed) {
80047
80235
  return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
@@ -80049,6 +80237,18 @@ Dashboard not found at: ${dashboardDir}`);
80049
80237
  headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
80050
80238
  });
80051
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
+ }
80052
80252
  if (path.startsWith("/api/")) {
80053
80253
  const authError = checkAuth(req, apiKey);
80054
80254
  if (authError)