@hasna/todos 0.11.71 → 0.11.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/index.js +2162 -1450
  7. package/dist/cli-mcp-parity.d.ts.map +1 -1
  8. package/dist/contracts.js +6526 -6258
  9. package/dist/db/api-keys.d.ts +9 -0
  10. package/dist/db/api-keys.d.ts.map +1 -1
  11. package/dist/db/database.d.ts.map +1 -1
  12. package/dist/db/schema.d.ts.map +1 -1
  13. package/dist/db/task-crud.d.ts.map +1 -1
  14. package/dist/db/task-lifecycle.d.ts +1 -0
  15. package/dist/db/task-lifecycle.d.ts.map +1 -1
  16. package/dist/db/task-status.d.ts.map +1 -1
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +10121 -9282
  20. package/dist/lib/db-backup.d.ts.map +1 -1
  21. package/dist/lib/shared-events.d.ts.map +1 -1
  22. package/dist/lib/task-route-contract.d.ts +2 -1
  23. package/dist/lib/task-route-contract.d.ts.map +1 -1
  24. package/dist/lib/task-route-sources.d.ts +68 -0
  25. package/dist/lib/task-route-sources.d.ts.map +1 -0
  26. package/dist/lib/task-routing.d.ts.map +1 -1
  27. package/dist/mcp/index.d.ts.map +1 -1
  28. package/dist/mcp/index.js +1869 -1650
  29. package/dist/mcp/token-utils.d.ts.map +1 -1
  30. package/dist/mcp.js +6 -8
  31. package/dist/registry.js +6526 -6258
  32. package/dist/release-provenance.json +3 -3
  33. package/dist/sdk/client.d.ts.map +1 -1
  34. package/dist/sdk/index.js +6 -4
  35. package/dist/server/index.js +1871 -1652
  36. package/dist/server/routes.d.ts +2 -1
  37. package/dist/server/routes.d.ts.map +1 -1
  38. package/dist/server/serve.d.ts.map +1 -1
  39. package/dist/storage/postgres-sync.d.ts.map +1 -1
  40. package/dist/storage.js +3324 -3029
  41. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1011
1011
  this._exitCallback = (err) => {
1012
1012
  if (err.code !== "commander.executeSubCommandAsync") {
1013
1013
  throw err;
1014
- }
1014
+ } else {}
1015
1015
  };
1016
1016
  }
1017
1017
  return this;
@@ -3890,6 +3890,11 @@ function ensureSchema(db) {
3890
3890
  ensureColumn("tasks", "priority_score", "INTEGER");
3891
3891
  ensureColumn("tasks", "priority_reason", "TEXT");
3892
3892
  ensureColumn("tasks", "archived_at", "TEXT");
3893
+ ensureColumn("tasks", "runner_id", "TEXT");
3894
+ ensureColumn("tasks", "runner_started_at", "TEXT");
3895
+ ensureColumn("tasks", "runner_completed_at", "TEXT");
3896
+ ensureColumn("tasks", "current_step", "TEXT");
3897
+ ensureColumn("tasks", "total_steps", "INTEGER");
3893
3898
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
3894
3899
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
3895
3900
  ensureColumn("agents", "reports_to", "TEXT");
@@ -3897,6 +3902,10 @@ function ensureSchema(db) {
3897
3902
  ensureColumn("agents", "level", "TEXT");
3898
3903
  ensureColumn("agents", "org_id", "TEXT");
3899
3904
  ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
3905
+ ensureColumn("agents", "session_id", "TEXT");
3906
+ ensureColumn("agents", "working_dir", "TEXT");
3907
+ ensureColumn("agents", "active_project_id", "TEXT");
3908
+ ensureColumn("agents", "status", "TEXT NOT NULL DEFAULT 'active'");
3900
3909
  ensureColumn("projects", "org_id", "TEXT");
3901
3910
  ensureColumn("plans", "slug", "TEXT");
3902
3911
  ensureColumn("plans", "task_list_id", "TEXT");
@@ -4819,34 +4828,40 @@ function ensureDir(filePath) {
4819
4828
  mkdirSync(dir, { recursive: true });
4820
4829
  }
4821
4830
  }
4831
+ function openDatabase(path) {
4832
+ ensureDir(path);
4833
+ const db = new Database(path);
4834
+ db.run("PRAGMA journal_mode = WAL");
4835
+ db.run("PRAGMA busy_timeout = 5000");
4836
+ db.run("PRAGMA foreign_keys = ON");
4837
+ runMigrations(db);
4838
+ backfillTaskTags(db);
4839
+ backfillMachineId(db);
4840
+ return db;
4841
+ }
4822
4842
  function getDatabase(dbPath) {
4823
4843
  const path = dbPath || getDbPath();
4824
4844
  if (_db && _dbPath === path)
4825
4845
  return _db;
4826
- if (_db && _dbPath !== path) {
4827
- _db.close();
4828
- _db = null;
4829
- _dbPath = null;
4830
- }
4831
- ensureDir(path);
4832
- _db = new Database(path);
4846
+ _db = openDatabase(path);
4833
4847
  _dbPath = path;
4834
- _db.run("PRAGMA journal_mode = WAL");
4835
- _db.run("PRAGMA busy_timeout = 5000");
4836
- _db.run("PRAGMA foreign_keys = ON");
4837
- runMigrations(_db);
4838
- backfillTaskTags(_db);
4839
- backfillMachineId(_db);
4840
4848
  return _db;
4841
4849
  }
4842
4850
  function closeDatabase() {
4843
4851
  if (_db) {
4844
- _db.close();
4852
+ try {
4853
+ _db.close();
4854
+ } catch {}
4845
4855
  _db = null;
4846
4856
  _dbPath = null;
4847
4857
  }
4848
4858
  }
4849
4859
  function resetDatabase() {
4860
+ if (_db) {
4861
+ try {
4862
+ _db.close();
4863
+ } catch {}
4864
+ }
4850
4865
  _db = null;
4851
4866
  _dbPath = null;
4852
4867
  }
@@ -7215,8 +7230,6 @@ function routeEnabledForTask(task, taskList) {
7215
7230
  const explicit = booleanField(task.metadata.route_enabled);
7216
7231
  if (explicit !== undefined)
7217
7232
  return explicit;
7218
- if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
7219
- return true;
7220
7233
  const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
7221
7234
  if (taskListDefault !== undefined)
7222
7235
  return taskListDefault;
@@ -7238,8 +7251,26 @@ function workflowPointersFromMetadata(metadata) {
7238
7251
  function compactWorkflowPointers(pointers) {
7239
7252
  return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
7240
7253
  }
7241
- function classifyProjectKind(path) {
7242
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
7254
+ function metadataStringField(record, keys) {
7255
+ if (!record)
7256
+ return;
7257
+ for (const key of keys) {
7258
+ const value = record[key];
7259
+ if (typeof value === "string" && value.trim())
7260
+ return value.trim();
7261
+ }
7262
+ return;
7263
+ }
7264
+ function projectKindFromMetadata(...records) {
7265
+ for (const record of records) {
7266
+ const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
7267
+ if (value)
7268
+ return value;
7269
+ }
7270
+ return null;
7271
+ }
7272
+ function classifyProjectKind(_path, metadata) {
7273
+ return projectKindFromMetadata(metadata);
7243
7274
  }
7244
7275
  function isWorktreePath(path) {
7245
7276
  return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
@@ -7312,7 +7343,6 @@ function taskEventMetadata(task) {
7312
7343
  metadata.project_canonical_path = projectPath;
7313
7344
  }
7314
7345
  if (projectPath) {
7315
- metadata.project_kind = classifyProjectKind(projectPath);
7316
7346
  metadata.project_is_worktree = isWorktreePath(projectPath);
7317
7347
  metadata.working_dir = task.working_dir ?? projectPath;
7318
7348
  }
@@ -7324,6 +7354,10 @@ function taskEventMetadata(task) {
7324
7354
  metadata.task_list_project_id = taskList.project_id;
7325
7355
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
7326
7356
  }
7357
+ const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
7358
+ if (projectKind) {
7359
+ metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
7360
+ }
7327
7361
  const routeEnabled = routeEnabledForTask(task, taskList);
7328
7362
  if (routeEnabled !== undefined) {
7329
7363
  metadata.route_enabled = routeEnabled;
@@ -7911,851 +7945,281 @@ var init_checklists = __esm(() => {
7911
7945
  init_database();
7912
7946
  });
7913
7947
 
7914
- // src/db/task-crud.ts
7915
- function rowToTask(row) {
7948
+ // src/lib/recurrence.ts
7949
+ function parseRecurrenceRule(rule) {
7950
+ const normalized = rule.trim().toLowerCase();
7951
+ if (normalized === "every weekday" || normalized === "every weekdays") {
7952
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
7953
+ }
7954
+ if (normalized === "every day" || normalized === "daily") {
7955
+ return { type: "interval", interval: 1, unit: "day" };
7956
+ }
7957
+ if (normalized === "every week" || normalized === "weekly") {
7958
+ return { type: "interval", interval: 1, unit: "week" };
7959
+ }
7960
+ if (normalized === "every month" || normalized === "monthly") {
7961
+ return { type: "interval", interval: 1, unit: "month" };
7962
+ }
7963
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
7964
+ if (intervalMatch) {
7965
+ return {
7966
+ type: "interval",
7967
+ interval: parseInt(intervalMatch[1], 10),
7968
+ unit: intervalMatch[2]
7969
+ };
7970
+ }
7971
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
7972
+ if (daysMatch) {
7973
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
7974
+ const days = [];
7975
+ for (const part of dayParts) {
7976
+ const dayNum = DAY_NAMES[part];
7977
+ if (dayNum !== undefined) {
7978
+ days.push(dayNum);
7979
+ }
7980
+ }
7981
+ if (days.length > 0) {
7982
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
7983
+ }
7984
+ }
7985
+ 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"`);
7986
+ }
7987
+ function isValidRecurrenceRule(rule) {
7988
+ try {
7989
+ parseRecurrenceRule(rule);
7990
+ return true;
7991
+ } catch {
7992
+ return false;
7993
+ }
7994
+ }
7995
+ function nextOccurrence(rule, from) {
7996
+ const parsed = parseRecurrenceRule(rule);
7997
+ const base = from || new Date;
7998
+ if (parsed.type === "interval") {
7999
+ const next = new Date(base);
8000
+ if (parsed.unit === "day") {
8001
+ next.setDate(next.getDate() + parsed.interval);
8002
+ } else if (parsed.unit === "week") {
8003
+ next.setDate(next.getDate() + parsed.interval * 7);
8004
+ } else if (parsed.unit === "month") {
8005
+ next.setMonth(next.getMonth() + parsed.interval);
8006
+ }
8007
+ return next.toISOString();
8008
+ }
8009
+ if (parsed.type === "specific_days") {
8010
+ const currentDay = base.getDay();
8011
+ const days = parsed.days;
8012
+ let daysToAdd = Infinity;
8013
+ for (const day of days) {
8014
+ let diff = day - currentDay;
8015
+ if (diff <= 0)
8016
+ diff += 7;
8017
+ if (diff < daysToAdd)
8018
+ daysToAdd = diff;
8019
+ }
8020
+ const next = new Date(base);
8021
+ next.setDate(next.getDate() + daysToAdd);
8022
+ return next.toISOString();
8023
+ }
8024
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
8025
+ }
8026
+ var DAY_NAMES;
8027
+ var init_recurrence = __esm(() => {
8028
+ DAY_NAMES = {
8029
+ sunday: 0,
8030
+ sun: 0,
8031
+ monday: 1,
8032
+ mon: 1,
8033
+ tuesday: 2,
8034
+ tue: 2,
8035
+ wednesday: 3,
8036
+ wed: 3,
8037
+ thursday: 4,
8038
+ thu: 4,
8039
+ friday: 5,
8040
+ fri: 5,
8041
+ saturday: 6,
8042
+ sat: 6
8043
+ };
8044
+ });
8045
+
8046
+ // src/db/templates.ts
8047
+ var exports_templates = {};
8048
+ __export(exports_templates, {
8049
+ updateTemplate: () => updateTemplate,
8050
+ tasksFromTemplate: () => tasksFromTemplate,
8051
+ taskFromTemplate: () => taskFromTemplate,
8052
+ resolveVariables: () => resolveVariables,
8053
+ previewTemplate: () => previewTemplate,
8054
+ listTemplates: () => listTemplates,
8055
+ listTemplateVersions: () => listTemplateVersions,
8056
+ importTemplate: () => importTemplate,
8057
+ getTemplateWithTasks: () => getTemplateWithTasks,
8058
+ getTemplateVersion: () => getTemplateVersion,
8059
+ getTemplateTasks: () => getTemplateTasks,
8060
+ getTemplate: () => getTemplate,
8061
+ exportTemplate: () => exportTemplate,
8062
+ evaluateCondition: () => evaluateCondition,
8063
+ deleteTemplate: () => deleteTemplate,
8064
+ createTemplate: () => createTemplate,
8065
+ addTemplateTasks: () => addTemplateTasks
8066
+ });
8067
+ function rowToTemplate(row) {
7916
8068
  return {
7917
8069
  ...row,
7918
8070
  tags: JSON.parse(row.tags || "[]"),
8071
+ variables: JSON.parse(row.variables || "[]"),
7919
8072
  metadata: JSON.parse(row.metadata || "{}"),
7920
- status: row.status,
7921
- priority: row.priority,
7922
- requires_approval: !!row.requires_approval
8073
+ priority: row.priority || "medium",
8074
+ version: row.version ?? 1
7923
8075
  };
7924
8076
  }
7925
- function insertTaskTags(taskId, tags, db) {
7926
- if (tags.length === 0)
7927
- return;
7928
- const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
7929
- for (const tag of tags) {
7930
- if (tag)
7931
- stmt.run(taskId, tag);
7932
- }
7933
- }
7934
- function replaceTaskTags(taskId, tags, db) {
7935
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
7936
- insertTaskTags(taskId, tags, db);
8077
+ function rowToTemplateTask(row) {
8078
+ return {
8079
+ ...row,
8080
+ tags: JSON.parse(row.tags || "[]"),
8081
+ depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
8082
+ metadata: JSON.parse(row.metadata || "{}"),
8083
+ priority: row.priority || "medium",
8084
+ condition: row.condition ?? null,
8085
+ include_template_id: row.include_template_id ?? null
8086
+ };
7937
8087
  }
7938
- function addMetadataConditions(metadata, conditions, params) {
7939
- if (!metadata)
7940
- return;
7941
- for (const [key, value] of Object.entries(metadata)) {
7942
- if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
7943
- throw new Error(`Invalid metadata filter key: ${key}`);
7944
- }
7945
- conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
7946
- params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
7947
- }
8088
+ function resolveTemplateId(id, d) {
8089
+ return resolvePartialId(d, "task_templates", id);
7948
8090
  }
7949
- function createTask(input, db) {
8091
+ function createTemplate(input, db) {
7950
8092
  const d = db || getDatabase();
7951
- const timestamp = now();
7952
- const tags = input.tags || [];
8093
+ const id = uuid();
7953
8094
  const machineId = currentStorageMachineId(d);
7954
- const assignedBy = input.assigned_by || input.agent_id;
7955
- const assignedFromProject = input.assigned_from_project || null;
7956
- let id = uuid();
7957
- for (let attempt = 0;attempt < 3; attempt++) {
7958
- try {
7959
- 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)
7960
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7961
- id,
7962
- null,
7963
- input.project_id || null,
7964
- input.parent_id || null,
7965
- input.plan_id || null,
7966
- input.task_list_id || null,
7967
- input.cycle_id || null,
7968
- input.title,
7969
- input.description || null,
7970
- input.status || "pending",
7971
- input.priority || "medium",
7972
- input.agent_id || null,
7973
- input.assigned_to || null,
7974
- input.session_id || null,
7975
- input.working_dir || null,
7976
- JSON.stringify(tags),
7977
- JSON.stringify(input.metadata || {}),
7978
- timestamp,
7979
- timestamp,
7980
- input.due_at || null,
7981
- input.estimated_minutes || null,
7982
- input.sla_minutes ?? null,
7983
- input.confidence ?? null,
7984
- input.retry_count ?? 0,
7985
- input.max_retries ?? 3,
7986
- input.retry_after ?? null,
7987
- input.requires_approval ? 1 : 0,
7988
- null,
7989
- null,
7990
- input.recurrence_rule || null,
7991
- input.recurrence_parent_id || null,
7992
- input.spawns_template_id || null,
7993
- input.reason || null,
7994
- input.spawned_from_session || null,
7995
- assignedBy || null,
7996
- assignedFromProject || null,
7997
- input.task_type || null,
7998
- machineId
7999
- ]);
8000
- break;
8001
- } catch (e) {
8002
- if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
8003
- id = uuid();
8004
- continue;
8005
- }
8006
- throw e;
8007
- }
8008
- }
8009
- if (tags.length > 0) {
8010
- insertTaskTags(id, tags, d);
8095
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
8096
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8097
+ id,
8098
+ input.name,
8099
+ input.title_pattern,
8100
+ input.description || null,
8101
+ input.priority || "medium",
8102
+ JSON.stringify(input.tags || []),
8103
+ JSON.stringify(input.variables || []),
8104
+ input.project_id || null,
8105
+ input.plan_id || null,
8106
+ JSON.stringify(input.metadata || {}),
8107
+ now(),
8108
+ machineId
8109
+ ]);
8110
+ if (input.tasks && input.tasks.length > 0) {
8111
+ addTemplateTasks(id, input.tasks, d);
8011
8112
  }
8012
- const task = getTask(id, d);
8013
- const payload = taskEventData(task);
8014
- const databasePath = databasePathFromDatabase(d);
8015
- dispatchWebhook2("task.created", payload, d).catch(() => {});
8016
- emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
8017
- emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
8018
- return task;
8113
+ return getTemplate(id, d);
8019
8114
  }
8020
- function getTask(id, db) {
8115
+ function getTemplate(id, db) {
8021
8116
  const d = db || getDatabase();
8022
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
8023
- if (!row)
8117
+ const resolved = resolveTemplateId(id, d);
8118
+ if (!resolved)
8024
8119
  return null;
8025
- return rowToTask(row);
8120
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
8121
+ return row ? rowToTemplate(row) : null;
8026
8122
  }
8027
- function getTaskWithRelations(id, db) {
8123
+ function listTemplates(db) {
8028
8124
  const d = db || getDatabase();
8029
- const task = getTask(id, d);
8030
- if (!task)
8031
- return null;
8032
- const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
8033
- const subtasks = subtaskRows.map(rowToTask);
8034
- const depRows = d.query(`SELECT t.* FROM tasks t
8035
- JOIN task_dependencies td ON td.depends_on = t.id
8036
- WHERE td.task_id = ?`).all(id);
8037
- const dependencies = depRows.map(rowToTask);
8038
- const blockedByRows = d.query(`SELECT t.* FROM tasks t
8039
- JOIN task_dependencies td ON td.task_id = t.id
8040
- WHERE td.depends_on = ?`).all(id);
8041
- const blocked_by = blockedByRows.map(rowToTask);
8042
- const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
8043
- const parent = task.parent_id ? getTask(task.parent_id, d) : null;
8044
- const checklist = getChecklist(id, d);
8045
- return {
8046
- ...task,
8047
- subtasks,
8048
- dependencies,
8049
- blocked_by,
8050
- comments,
8051
- parent,
8052
- checklist
8053
- };
8125
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
8054
8126
  }
8055
- function listTasks(filter = {}, db) {
8127
+ function deleteTemplate(id, db) {
8056
8128
  const d = db || getDatabase();
8057
- const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
8058
- clearExpiredLocks2(d);
8059
- const conditions = [];
8060
- const params = [];
8061
- if (filter.project_id) {
8062
- conditions.push("project_id = ?");
8063
- params.push(filter.project_id);
8064
- }
8065
- if (filter.ids && filter.ids.length > 0) {
8066
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
8067
- params.push(...filter.ids);
8068
- }
8069
- if (filter.parent_id !== undefined) {
8070
- if (filter.parent_id === null) {
8071
- conditions.push("parent_id IS NULL");
8072
- } else {
8073
- conditions.push("parent_id = ?");
8074
- params.push(filter.parent_id);
8075
- }
8076
- }
8077
- if (filter.status) {
8078
- if (Array.isArray(filter.status)) {
8079
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
8080
- params.push(...filter.status);
8081
- } else {
8082
- conditions.push("status = ?");
8083
- params.push(filter.status);
8084
- }
8085
- }
8086
- if (filter.priority) {
8087
- if (Array.isArray(filter.priority)) {
8088
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
8089
- params.push(...filter.priority);
8090
- } else {
8091
- conditions.push("priority = ?");
8092
- params.push(filter.priority);
8093
- }
8094
- }
8095
- if (filter.assigned_to) {
8096
- conditions.push("assigned_to = ?");
8097
- params.push(filter.assigned_to);
8098
- }
8099
- if (filter.agent_id) {
8100
- conditions.push("agent_id = ?");
8101
- params.push(filter.agent_id);
8129
+ const resolved = resolveTemplateId(id, d);
8130
+ if (!resolved)
8131
+ return false;
8132
+ const template = getTemplate(resolved, d);
8133
+ if (!template)
8134
+ return false;
8135
+ recordStorageTombstone({
8136
+ object_type: "templates",
8137
+ object_id: resolved,
8138
+ payload: template,
8139
+ version: template.version
8140
+ }, d);
8141
+ return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
8142
+ }
8143
+ function updateTemplate(id, updates, db) {
8144
+ const d = db || getDatabase();
8145
+ const resolved = resolveTemplateId(id, d);
8146
+ if (!resolved)
8147
+ return null;
8148
+ const current = getTemplateWithTasks(resolved, d);
8149
+ if (current) {
8150
+ const snapshot = JSON.stringify({
8151
+ name: current.name,
8152
+ title_pattern: current.title_pattern,
8153
+ description: current.description,
8154
+ priority: current.priority,
8155
+ tags: current.tags,
8156
+ variables: current.variables,
8157
+ project_id: current.project_id,
8158
+ plan_id: current.plan_id,
8159
+ metadata: current.metadata,
8160
+ tasks: current.tasks
8161
+ });
8162
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
8102
8163
  }
8103
- if (filter.session_id) {
8104
- conditions.push("session_id = ?");
8105
- params.push(filter.session_id);
8164
+ const sets = ["version = version + 1"];
8165
+ const values = [];
8166
+ if (updates.name !== undefined) {
8167
+ sets.push("name = ?");
8168
+ values.push(updates.name);
8106
8169
  }
8107
- if (filter.tags && filter.tags.length > 0) {
8108
- const placeholders = filter.tags.map(() => "?").join(",");
8109
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
8110
- params.push(...filter.tags);
8170
+ if (updates.title_pattern !== undefined) {
8171
+ sets.push("title_pattern = ?");
8172
+ values.push(updates.title_pattern);
8111
8173
  }
8112
- if (filter.plan_id) {
8113
- conditions.push("plan_id = ?");
8114
- params.push(filter.plan_id);
8174
+ if (updates.description !== undefined) {
8175
+ sets.push("description = ?");
8176
+ values.push(updates.description);
8115
8177
  }
8116
- if (filter.task_list_id) {
8117
- conditions.push("task_list_id = ?");
8118
- params.push(filter.task_list_id);
8178
+ if (updates.priority !== undefined) {
8179
+ sets.push("priority = ?");
8180
+ values.push(updates.priority);
8119
8181
  }
8120
- if (filter.has_recurrence === true) {
8121
- conditions.push("recurrence_rule IS NOT NULL");
8122
- } else if (filter.has_recurrence === false) {
8123
- conditions.push("recurrence_rule IS NULL");
8182
+ if (updates.tags !== undefined) {
8183
+ sets.push("tags = ?");
8184
+ values.push(JSON.stringify(updates.tags));
8124
8185
  }
8125
- if (filter.task_type) {
8126
- if (Array.isArray(filter.task_type)) {
8127
- conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
8128
- params.push(...filter.task_type);
8129
- } else {
8130
- conditions.push("task_type = ?");
8131
- params.push(filter.task_type);
8132
- }
8186
+ if (updates.variables !== undefined) {
8187
+ sets.push("variables = ?");
8188
+ values.push(JSON.stringify(updates.variables));
8133
8189
  }
8134
- addMetadataConditions(filter.metadata, conditions, params);
8135
- const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
8136
- if (filter.cursor) {
8137
- try {
8138
- const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
8139
- conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
8140
- params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
8141
- } catch {}
8190
+ if (updates.project_id !== undefined) {
8191
+ sets.push("project_id = ?");
8192
+ values.push(updates.project_id);
8142
8193
  }
8143
- if (!filter.include_archived) {
8144
- conditions.push("archived_at IS NULL");
8194
+ if (updates.plan_id !== undefined) {
8195
+ sets.push("plan_id = ?");
8196
+ values.push(updates.plan_id);
8145
8197
  }
8146
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
8147
- let limitClause = "";
8148
- if (filter.limit) {
8149
- limitClause = " LIMIT ?";
8150
- params.push(filter.limit);
8151
- if (!filter.cursor && filter.offset) {
8152
- limitClause += " OFFSET ?";
8153
- params.push(filter.offset);
8154
- }
8198
+ if (updates.metadata !== undefined) {
8199
+ sets.push("metadata = ?");
8200
+ values.push(JSON.stringify(updates.metadata));
8155
8201
  }
8156
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
8157
- return rows.map(rowToTask);
8158
- }
8159
- function getTaskByFingerprint(fingerprint, db) {
8160
- const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
8161
- return tasks[0] ?? null;
8202
+ values.push(resolved);
8203
+ d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
8204
+ return getTemplate(resolved, d);
8162
8205
  }
8163
- function mergeTaskMetadata(current, next, fingerprint) {
8206
+ function taskFromTemplate(templateId, overrides = {}, db) {
8207
+ const t = getTemplate(templateId, db);
8208
+ if (!t)
8209
+ throw new Error(`Template not found: ${templateId}`);
8210
+ const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
8164
8211
  return {
8165
- ...current,
8166
- ...next ?? {},
8167
- fingerprint
8212
+ title: cleanOverrides.title || t.title_pattern,
8213
+ description: cleanOverrides.description ?? t.description ?? undefined,
8214
+ priority: cleanOverrides.priority ?? t.priority,
8215
+ tags: cleanOverrides.tags ?? t.tags,
8216
+ project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
8217
+ plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
8218
+ metadata: cleanOverrides.metadata ?? t.metadata,
8219
+ ...cleanOverrides
8168
8220
  };
8169
8221
  }
8170
- function upsertTaskByFingerprint(input, db) {
8171
- const d = db || getDatabase();
8172
- const fingerprint = input.fingerprint.trim();
8173
- if (!fingerprint)
8174
- throw new Error("fingerprint is required");
8175
- const existing = getTaskByFingerprint(fingerprint, d);
8176
- const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
8177
- if (!existing) {
8178
- const task2 = createTask({ ...input, metadata }, d);
8179
- return { task: task2, created: true };
8180
- }
8181
- const task = updateTask(existing.id, {
8182
- version: existing.version,
8183
- title: input.title,
8184
- description: input.description,
8185
- status: input.status,
8186
- priority: input.priority,
8187
- project_id: input.project_id,
8188
- assigned_to: input.assigned_to,
8189
- working_dir: input.working_dir,
8190
- plan_id: input.plan_id,
8191
- task_list_id: input.task_list_id,
8192
- tags: input.tags,
8193
- metadata,
8194
- due_at: input.due_at,
8195
- estimated_minutes: input.estimated_minutes,
8196
- sla_minutes: input.sla_minutes,
8197
- confidence: input.confidence,
8198
- retry_count: input.retry_count,
8199
- max_retries: input.max_retries,
8200
- retry_after: input.retry_after,
8201
- requires_approval: input.requires_approval,
8202
- recurrence_rule: input.recurrence_rule,
8203
- task_type: input.task_type
8204
- }, d);
8205
- return { task, created: false };
8206
- }
8207
- function countTasks(filter = {}, db) {
8208
- const d = db || getDatabase();
8209
- const conditions = [];
8210
- const params = [];
8211
- if (filter.project_id) {
8212
- conditions.push("project_id = ?");
8213
- params.push(filter.project_id);
8214
- }
8215
- if (filter.ids && filter.ids.length > 0) {
8216
- conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
8217
- params.push(...filter.ids);
8218
- }
8219
- if (filter.parent_id !== undefined) {
8220
- if (filter.parent_id === null) {
8221
- conditions.push("parent_id IS NULL");
8222
- } else {
8223
- conditions.push("parent_id = ?");
8224
- params.push(filter.parent_id);
8225
- }
8226
- }
8227
- if (filter.status) {
8228
- if (Array.isArray(filter.status)) {
8229
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
8230
- params.push(...filter.status);
8231
- } else {
8232
- conditions.push("status = ?");
8233
- params.push(filter.status);
8234
- }
8235
- }
8236
- if (filter.priority) {
8237
- if (Array.isArray(filter.priority)) {
8238
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
8239
- params.push(...filter.priority);
8240
- } else {
8241
- conditions.push("priority = ?");
8242
- params.push(filter.priority);
8243
- }
8244
- }
8245
- if (filter.assigned_to) {
8246
- conditions.push("assigned_to = ?");
8247
- params.push(filter.assigned_to);
8248
- }
8249
- if (filter.agent_id) {
8250
- conditions.push("agent_id = ?");
8251
- params.push(filter.agent_id);
8252
- }
8253
- if (filter.session_id) {
8254
- conditions.push("session_id = ?");
8255
- params.push(filter.session_id);
8256
- }
8257
- if (filter.tags && filter.tags.length > 0) {
8258
- const placeholders = filter.tags.map(() => "?").join(",");
8259
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
8260
- params.push(...filter.tags);
8261
- }
8262
- if (filter.plan_id) {
8263
- conditions.push("plan_id = ?");
8264
- params.push(filter.plan_id);
8265
- }
8266
- if (filter.task_list_id) {
8267
- conditions.push("task_list_id = ?");
8268
- params.push(filter.task_list_id);
8269
- }
8270
- addMetadataConditions(filter.metadata, conditions, params);
8271
- if (!filter.include_archived) {
8272
- conditions.push("archived_at IS NULL");
8273
- }
8274
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
8275
- const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
8276
- return row.count;
8277
- }
8278
- function updateTask(id, input, db) {
8279
- const d = db || getDatabase();
8280
- const task = getTask(id, d);
8281
- if (!task)
8282
- throw new TaskNotFoundError(id);
8283
- if (task.version !== input.version) {
8284
- throw new VersionConflictError(id, input.version, task.version);
8285
- }
8286
- const timestamp = now();
8287
- const completionTimestamp = input.completed_at ?? timestamp;
8288
- const sets = ["version = version + 1", "updated_at = ?"];
8289
- const params = [timestamp];
8290
- if (input.title !== undefined) {
8291
- sets.push("title = ?");
8292
- params.push(input.title);
8293
- }
8294
- if (input.description !== undefined) {
8295
- sets.push("description = ?");
8296
- params.push(input.description);
8297
- }
8298
- if (input.status !== undefined) {
8299
- if (input.status === "completed") {
8300
- checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
8301
- }
8302
- sets.push("status = ?");
8303
- params.push(input.status);
8304
- if (input.status === "completed") {
8305
- sets.push("completed_at = ?");
8306
- params.push(completionTimestamp);
8307
- }
8308
- }
8309
- if (input.priority !== undefined) {
8310
- sets.push("priority = ?");
8311
- params.push(input.priority);
8312
- }
8313
- if (input.project_id !== undefined) {
8314
- sets.push("project_id = ?");
8315
- params.push(input.project_id);
8316
- }
8317
- if (input.assigned_to !== undefined) {
8318
- sets.push("assigned_to = ?");
8319
- params.push(input.assigned_to);
8320
- }
8321
- if (input.working_dir !== undefined) {
8322
- sets.push("working_dir = ?");
8323
- params.push(input.working_dir);
8324
- }
8325
- if (input.tags !== undefined) {
8326
- sets.push("tags = ?");
8327
- params.push(JSON.stringify(input.tags));
8328
- }
8329
- if (input.metadata !== undefined) {
8330
- sets.push("metadata = ?");
8331
- params.push(JSON.stringify(input.metadata));
8332
- }
8333
- if (input.plan_id !== undefined) {
8334
- sets.push("plan_id = ?");
8335
- params.push(input.plan_id);
8336
- }
8337
- if (input.task_list_id !== undefined) {
8338
- sets.push("task_list_id = ?");
8339
- params.push(input.task_list_id);
8340
- }
8341
- if (input.due_at !== undefined) {
8342
- sets.push("due_at = ?");
8343
- params.push(input.due_at);
8344
- }
8345
- if (input.estimated_minutes !== undefined) {
8346
- sets.push("estimated_minutes = ?");
8347
- params.push(input.estimated_minutes);
8348
- }
8349
- if (input.sla_minutes !== undefined) {
8350
- sets.push("sla_minutes = ?");
8351
- params.push(input.sla_minutes);
8352
- }
8353
- if (input.actual_minutes !== undefined) {
8354
- sets.push("actual_minutes = ?");
8355
- params.push(input.actual_minutes);
8356
- }
8357
- if (input.completed_at !== undefined && input.status !== "completed") {
8358
- sets.push("completed_at = ?");
8359
- params.push(input.completed_at);
8360
- }
8361
- if (input.confidence !== undefined) {
8362
- sets.push("confidence = ?");
8363
- params.push(input.confidence);
8364
- }
8365
- if (input.retry_count !== undefined) {
8366
- sets.push("retry_count = ?");
8367
- params.push(input.retry_count);
8368
- }
8369
- if (input.max_retries !== undefined) {
8370
- sets.push("max_retries = ?");
8371
- params.push(input.max_retries);
8372
- }
8373
- if (input.retry_after !== undefined) {
8374
- sets.push("retry_after = ?");
8375
- params.push(input.retry_after);
8376
- }
8377
- if (input.requires_approval !== undefined) {
8378
- sets.push("requires_approval = ?");
8379
- params.push(input.requires_approval ? 1 : 0);
8380
- }
8381
- if (input.approved_by !== undefined) {
8382
- sets.push("approved_by = ?");
8383
- params.push(input.approved_by);
8384
- sets.push("approved_at = ?");
8385
- params.push(now());
8386
- }
8387
- if (input.recurrence_rule !== undefined) {
8388
- sets.push("recurrence_rule = ?");
8389
- params.push(input.recurrence_rule);
8390
- }
8391
- if (input.task_type !== undefined) {
8392
- sets.push("task_type = ?");
8393
- params.push(input.task_type ?? null);
8394
- }
8395
- params.push(id, input.version);
8396
- const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
8397
- if (result.changes === 0) {
8398
- const current = getTask(id, d);
8399
- throw new VersionConflictError(id, input.version, current?.version ?? -1);
8400
- }
8401
- if (input.tags !== undefined) {
8402
- replaceTaskTags(id, input.tags, d);
8403
- }
8404
- const agentId = task.assigned_to || task.agent_id || null;
8405
- if (input.status !== undefined && input.status !== task.status)
8406
- logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
8407
- if (input.priority !== undefined && input.priority !== task.priority)
8408
- logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
8409
- if (input.title !== undefined && input.title !== task.title)
8410
- logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
8411
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
8412
- logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
8413
- if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
8414
- logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
8415
- if (input.approved_by !== undefined)
8416
- logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
8417
- const updatedTask = {
8418
- ...task,
8419
- ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
8420
- tags: input.tags ?? task.tags,
8421
- metadata: input.metadata ?? task.metadata,
8422
- version: task.version + 1,
8423
- updated_at: timestamp,
8424
- completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
8425
- sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
8426
- actual_minutes: input.actual_minutes ?? task.actual_minutes,
8427
- confidence: input.confidence !== undefined ? input.confidence : task.confidence,
8428
- retry_count: input.retry_count ?? task.retry_count,
8429
- max_retries: input.max_retries ?? task.max_retries,
8430
- retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
8431
- requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
8432
- approved_by: input.approved_by ?? task.approved_by,
8433
- approved_at: input.approved_by ? timestamp : task.approved_at
8434
- };
8435
- const databasePath = databasePathFromDatabase(d);
8436
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
8437
- const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
8438
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
8439
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
8440
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
8441
- }
8442
- if (input.status !== undefined && input.status !== task.status) {
8443
- const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
8444
- dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
8445
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
8446
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
8447
- }
8448
- if (input.approved_by !== undefined) {
8449
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
8450
- }
8451
- const updatePayload = taskEventData(updatedTask);
8452
- dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
8453
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
8454
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
8455
- return updatedTask;
8456
- }
8457
- function deleteTask(id, db) {
8458
- const d = db || getDatabase();
8459
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
8460
- if (!row)
8461
- return false;
8462
- recordStorageTombstone({
8463
- object_type: "tasks",
8464
- object_id: id,
8465
- payload: rowToTask(row),
8466
- version: row.version
8467
- }, d);
8468
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
8469
- return result.changes > 0;
8470
- }
8471
- var init_task_crud = __esm(() => {
8472
- init_types();
8473
- init_database();
8474
- init_completion_guard();
8475
- init_event_emission_safety();
8476
- init_event_hooks();
8477
- init_shared_events();
8478
- init_audit();
8479
- init_webhooks();
8480
- init_checklists();
8481
- init_storage_tombstones();
8482
- });
8483
-
8484
- // src/lib/recurrence.ts
8485
- function parseRecurrenceRule(rule) {
8486
- const normalized = rule.trim().toLowerCase();
8487
- if (normalized === "every weekday" || normalized === "every weekdays") {
8488
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
8489
- }
8490
- if (normalized === "every day" || normalized === "daily") {
8491
- return { type: "interval", interval: 1, unit: "day" };
8492
- }
8493
- if (normalized === "every week" || normalized === "weekly") {
8494
- return { type: "interval", interval: 1, unit: "week" };
8495
- }
8496
- if (normalized === "every month" || normalized === "monthly") {
8497
- return { type: "interval", interval: 1, unit: "month" };
8498
- }
8499
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
8500
- if (intervalMatch) {
8501
- return {
8502
- type: "interval",
8503
- interval: parseInt(intervalMatch[1], 10),
8504
- unit: intervalMatch[2]
8505
- };
8506
- }
8507
- const daysMatch = normalized.match(/^every\s+(.+)$/);
8508
- if (daysMatch) {
8509
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
8510
- const days = [];
8511
- for (const part of dayParts) {
8512
- const dayNum = DAY_NAMES[part];
8513
- if (dayNum !== undefined) {
8514
- days.push(dayNum);
8515
- }
8516
- }
8517
- if (days.length > 0) {
8518
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
8519
- }
8520
- }
8521
- 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"`);
8522
- }
8523
- function isValidRecurrenceRule(rule) {
8524
- try {
8525
- parseRecurrenceRule(rule);
8526
- return true;
8527
- } catch {
8528
- return false;
8529
- }
8530
- }
8531
- function nextOccurrence(rule, from) {
8532
- const parsed = parseRecurrenceRule(rule);
8533
- const base = from || new Date;
8534
- if (parsed.type === "interval") {
8535
- const next = new Date(base);
8536
- if (parsed.unit === "day") {
8537
- next.setDate(next.getDate() + parsed.interval);
8538
- } else if (parsed.unit === "week") {
8539
- next.setDate(next.getDate() + parsed.interval * 7);
8540
- } else if (parsed.unit === "month") {
8541
- next.setMonth(next.getMonth() + parsed.interval);
8542
- }
8543
- return next.toISOString();
8544
- }
8545
- if (parsed.type === "specific_days") {
8546
- const currentDay = base.getDay();
8547
- const days = parsed.days;
8548
- let daysToAdd = Infinity;
8549
- for (const day of days) {
8550
- let diff = day - currentDay;
8551
- if (diff <= 0)
8552
- diff += 7;
8553
- if (diff < daysToAdd)
8554
- daysToAdd = diff;
8555
- }
8556
- const next = new Date(base);
8557
- next.setDate(next.getDate() + daysToAdd);
8558
- return next.toISOString();
8559
- }
8560
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
8561
- }
8562
- var DAY_NAMES;
8563
- var init_recurrence = __esm(() => {
8564
- DAY_NAMES = {
8565
- sunday: 0,
8566
- sun: 0,
8567
- monday: 1,
8568
- mon: 1,
8569
- tuesday: 2,
8570
- tue: 2,
8571
- wednesday: 3,
8572
- wed: 3,
8573
- thursday: 4,
8574
- thu: 4,
8575
- friday: 5,
8576
- fri: 5,
8577
- saturday: 6,
8578
- sat: 6
8579
- };
8580
- });
8581
-
8582
- // src/db/templates.ts
8583
- var exports_templates = {};
8584
- __export(exports_templates, {
8585
- updateTemplate: () => updateTemplate,
8586
- tasksFromTemplate: () => tasksFromTemplate,
8587
- taskFromTemplate: () => taskFromTemplate,
8588
- resolveVariables: () => resolveVariables,
8589
- previewTemplate: () => previewTemplate,
8590
- listTemplates: () => listTemplates,
8591
- listTemplateVersions: () => listTemplateVersions,
8592
- importTemplate: () => importTemplate,
8593
- getTemplateWithTasks: () => getTemplateWithTasks,
8594
- getTemplateVersion: () => getTemplateVersion,
8595
- getTemplateTasks: () => getTemplateTasks,
8596
- getTemplate: () => getTemplate,
8597
- exportTemplate: () => exportTemplate,
8598
- evaluateCondition: () => evaluateCondition,
8599
- deleteTemplate: () => deleteTemplate,
8600
- createTemplate: () => createTemplate,
8601
- addTemplateTasks: () => addTemplateTasks
8602
- });
8603
- function rowToTemplate(row) {
8604
- return {
8605
- ...row,
8606
- tags: JSON.parse(row.tags || "[]"),
8607
- variables: JSON.parse(row.variables || "[]"),
8608
- metadata: JSON.parse(row.metadata || "{}"),
8609
- priority: row.priority || "medium",
8610
- version: row.version ?? 1
8611
- };
8612
- }
8613
- function rowToTemplateTask(row) {
8614
- return {
8615
- ...row,
8616
- tags: JSON.parse(row.tags || "[]"),
8617
- depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
8618
- metadata: JSON.parse(row.metadata || "{}"),
8619
- priority: row.priority || "medium",
8620
- condition: row.condition ?? null,
8621
- include_template_id: row.include_template_id ?? null
8622
- };
8623
- }
8624
- function resolveTemplateId(id, d) {
8625
- return resolvePartialId(d, "task_templates", id);
8626
- }
8627
- function createTemplate(input, db) {
8628
- const d = db || getDatabase();
8629
- const id = uuid();
8630
- const machineId = currentStorageMachineId(d);
8631
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
8632
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8633
- id,
8634
- input.name,
8635
- input.title_pattern,
8636
- input.description || null,
8637
- input.priority || "medium",
8638
- JSON.stringify(input.tags || []),
8639
- JSON.stringify(input.variables || []),
8640
- input.project_id || null,
8641
- input.plan_id || null,
8642
- JSON.stringify(input.metadata || {}),
8643
- now(),
8644
- machineId
8645
- ]);
8646
- if (input.tasks && input.tasks.length > 0) {
8647
- addTemplateTasks(id, input.tasks, d);
8648
- }
8649
- return getTemplate(id, d);
8650
- }
8651
- function getTemplate(id, db) {
8652
- const d = db || getDatabase();
8653
- const resolved = resolveTemplateId(id, d);
8654
- if (!resolved)
8655
- return null;
8656
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
8657
- return row ? rowToTemplate(row) : null;
8658
- }
8659
- function listTemplates(db) {
8660
- const d = db || getDatabase();
8661
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
8662
- }
8663
- function deleteTemplate(id, db) {
8664
- const d = db || getDatabase();
8665
- const resolved = resolveTemplateId(id, d);
8666
- if (!resolved)
8667
- return false;
8668
- const template = getTemplate(resolved, d);
8669
- if (!template)
8670
- return false;
8671
- recordStorageTombstone({
8672
- object_type: "templates",
8673
- object_id: resolved,
8674
- payload: template,
8675
- version: template.version
8676
- }, d);
8677
- return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
8678
- }
8679
- function updateTemplate(id, updates, db) {
8680
- const d = db || getDatabase();
8681
- const resolved = resolveTemplateId(id, d);
8682
- if (!resolved)
8683
- return null;
8684
- const current = getTemplateWithTasks(resolved, d);
8685
- if (current) {
8686
- const snapshot = JSON.stringify({
8687
- name: current.name,
8688
- title_pattern: current.title_pattern,
8689
- description: current.description,
8690
- priority: current.priority,
8691
- tags: current.tags,
8692
- variables: current.variables,
8693
- project_id: current.project_id,
8694
- plan_id: current.plan_id,
8695
- metadata: current.metadata,
8696
- tasks: current.tasks
8697
- });
8698
- d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
8699
- }
8700
- const sets = ["version = version + 1"];
8701
- const values = [];
8702
- if (updates.name !== undefined) {
8703
- sets.push("name = ?");
8704
- values.push(updates.name);
8705
- }
8706
- if (updates.title_pattern !== undefined) {
8707
- sets.push("title_pattern = ?");
8708
- values.push(updates.title_pattern);
8709
- }
8710
- if (updates.description !== undefined) {
8711
- sets.push("description = ?");
8712
- values.push(updates.description);
8713
- }
8714
- if (updates.priority !== undefined) {
8715
- sets.push("priority = ?");
8716
- values.push(updates.priority);
8717
- }
8718
- if (updates.tags !== undefined) {
8719
- sets.push("tags = ?");
8720
- values.push(JSON.stringify(updates.tags));
8721
- }
8722
- if (updates.variables !== undefined) {
8723
- sets.push("variables = ?");
8724
- values.push(JSON.stringify(updates.variables));
8725
- }
8726
- if (updates.project_id !== undefined) {
8727
- sets.push("project_id = ?");
8728
- values.push(updates.project_id);
8729
- }
8730
- if (updates.plan_id !== undefined) {
8731
- sets.push("plan_id = ?");
8732
- values.push(updates.plan_id);
8733
- }
8734
- if (updates.metadata !== undefined) {
8735
- sets.push("metadata = ?");
8736
- values.push(JSON.stringify(updates.metadata));
8737
- }
8738
- values.push(resolved);
8739
- d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
8740
- return getTemplate(resolved, d);
8741
- }
8742
- function taskFromTemplate(templateId, overrides = {}, db) {
8743
- const t = getTemplate(templateId, db);
8744
- if (!t)
8745
- throw new Error(`Template not found: ${templateId}`);
8746
- const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
8747
- return {
8748
- title: cleanOverrides.title || t.title_pattern,
8749
- description: cleanOverrides.description ?? t.description ?? undefined,
8750
- priority: cleanOverrides.priority ?? t.priority,
8751
- tags: cleanOverrides.tags ?? t.tags,
8752
- project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
8753
- plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
8754
- metadata: cleanOverrides.metadata ?? t.metadata,
8755
- ...cleanOverrides
8756
- };
8757
- }
8758
- function addTemplateTasks(templateId, tasks, db) {
8222
+ function addTemplateTasks(templateId, tasks, db) {
8759
8223
  const d = db || getDatabase();
8760
8224
  const template = getTemplate(templateId, d);
8761
8225
  if (!template)
@@ -9151,527 +8615,1188 @@ function moveTask(taskId, target, db) {
9151
8615
  sets.push("project_id = ?");
9152
8616
  params.push(target.project_id);
9153
8617
  }
9154
- if (target.plan_id !== undefined) {
9155
- sets.push("plan_id = ?");
9156
- params.push(target.plan_id);
8618
+ if (target.plan_id !== undefined) {
8619
+ sets.push("plan_id = ?");
8620
+ params.push(target.plan_id);
8621
+ }
8622
+ params.push(taskId);
8623
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
8624
+ return getTask(taskId, d);
8625
+ }
8626
+ function wouldCreateCycle(taskId, dependsOn, db) {
8627
+ const visited = new Set;
8628
+ const queue = [dependsOn];
8629
+ while (queue.length > 0) {
8630
+ const current = queue.shift();
8631
+ if (current === taskId)
8632
+ return true;
8633
+ if (visited.has(current))
8634
+ continue;
8635
+ visited.add(current);
8636
+ const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
8637
+ for (const dep of deps) {
8638
+ queue.push(dep.depends_on);
8639
+ }
8640
+ }
8641
+ return false;
8642
+ }
8643
+ var init_task_graph = __esm(() => {
8644
+ init_types();
8645
+ init_database();
8646
+ init_task_crud();
8647
+ });
8648
+
8649
+ // src/db/task-lifecycle.ts
8650
+ var exports_task_lifecycle = {};
8651
+ __export(exports_task_lifecycle, {
8652
+ unlockTask: () => unlockTask,
8653
+ stealTask: () => stealTask,
8654
+ startTask: () => startTask,
8655
+ spawnNextRecurrence: () => spawnNextRecurrence,
8656
+ lockTask: () => lockTask,
8657
+ getTasksChangedSince: () => getTasksChangedSince,
8658
+ getTaskLockStatus: () => getTaskLockStatus,
8659
+ getStaleTasks: () => getStaleTasks,
8660
+ getNextTask: () => getNextTask,
8661
+ getBlockingDeps: () => getBlockingDeps,
8662
+ getActiveWork: () => getActiveWork,
8663
+ failTask: () => failTask,
8664
+ completeTask: () => completeTask,
8665
+ claimOrSteal: () => claimOrSteal,
8666
+ claimNextTask: () => claimNextTask
8667
+ });
8668
+ function lockExpiresAt(lockedAt) {
8669
+ if (!lockedAt)
8670
+ return null;
8671
+ return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
8672
+ }
8673
+ function assertStartable(task, agentId) {
8674
+ if (task.status === "pending")
8675
+ return;
8676
+ if (task.status === "in_progress")
8677
+ return;
8678
+ throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
8679
+ }
8680
+ function getBlockingDeps(id, db) {
8681
+ const d = db || getDatabase();
8682
+ const deps = getTaskDependencies(id, d);
8683
+ if (deps.length === 0)
8684
+ return [];
8685
+ const blocking = [];
8686
+ for (const dep of deps) {
8687
+ const task = getTask(dep.depends_on, d);
8688
+ if (task && task.status !== "completed")
8689
+ blocking.push(task);
8690
+ }
8691
+ return blocking;
8692
+ }
8693
+ function startTask(id, agentId, db) {
8694
+ const d = db || getDatabase();
8695
+ const databasePath = databasePathFromDatabase(d);
8696
+ const task = getTask(id, d);
8697
+ if (!task)
8698
+ throw new TaskNotFoundError(id);
8699
+ assertStartable(task, agentId);
8700
+ const blocking = getBlockingDeps(id, d);
8701
+ if (blocking.length > 0) {
8702
+ const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
8703
+ emitLocalEventHooksQuiet({
8704
+ type: "task.blocked",
8705
+ payload: {
8706
+ id,
8707
+ agent_id: agentId,
8708
+ title: task.title,
8709
+ blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
8710
+ },
8711
+ databasePath
8712
+ });
8713
+ throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
8714
+ }
8715
+ const cutoff = lockExpiryCutoff();
8716
+ const timestamp = now();
8717
+ 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 = ?
8718
+ 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]);
8719
+ if (result.changes === 0) {
8720
+ const current = getTask(id, d);
8721
+ if (!current)
8722
+ throw new TaskNotFoundError(id);
8723
+ assertStartable(current, agentId);
8724
+ if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
8725
+ throw new LockError(id, current.locked_by);
8726
+ }
8727
+ throw new Error(`Task ${id} could not be started because it changed during claim`);
8728
+ }
8729
+ logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
8730
+ 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 };
8731
+ const payload = taskEventData(startedTask, { agent_id: agentId });
8732
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
8733
+ emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
8734
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
8735
+ return startedTask;
8736
+ }
8737
+ function completeTask(id, agentId, db, options) {
8738
+ const d = db || getDatabase();
8739
+ const databasePath = databasePathFromDatabase(d);
8740
+ const task = getTask(id, d);
8741
+ if (!task)
8742
+ throw new TaskNotFoundError(id);
8743
+ if (task.status === "completed") {
8744
+ return task;
8745
+ }
8746
+ if (task.status === "cancelled") {
8747
+ throw new Error(`Task ${id} is cancelled and cannot be completed`);
8748
+ }
8749
+ if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
8750
+ throw new LockError(id, task.locked_by);
8751
+ }
8752
+ checkCompletionGuard(task, agentId || null, d);
8753
+ 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;
8754
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
8755
+ const completionMeta = {};
8756
+ if (hasEvidence)
8757
+ completionMeta._evidence = evidence;
8758
+ if (options?.confidence !== undefined) {
8759
+ completionMeta._completion = { confidence: options.confidence };
8760
+ }
8761
+ const hasMeta = Object.keys(completionMeta).length > 0;
8762
+ const timestamp = options?.completed_at || now();
8763
+ const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
8764
+ const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
8765
+ const finalVersion = versionBeforeStatus + 1;
8766
+ const tx = d.transaction(() => {
8767
+ if (hasMeta) {
8768
+ const meta2 = { ...task.metadata, ...completionMeta };
8769
+ const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
8770
+ if (metaResult.changes === 0) {
8771
+ const current = getTask(id, d);
8772
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
8773
+ }
8774
+ }
8775
+ const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
8776
+ WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
8777
+ if (statusResult.changes === 0) {
8778
+ const current = getTask(id, d);
8779
+ throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
8780
+ }
8781
+ });
8782
+ tx();
8783
+ logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
8784
+ const completedTaskForEvent = {
8785
+ ...task,
8786
+ status: "completed",
8787
+ locked_by: null,
8788
+ locked_at: null,
8789
+ completed_at: timestamp,
8790
+ confidence,
8791
+ version: finalVersion,
8792
+ updated_at: timestamp,
8793
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
8794
+ };
8795
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
8796
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
8797
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
8798
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
8799
+ let spawnedTask = null;
8800
+ if (task.recurrence_rule && !options?.skip_recurrence) {
8801
+ try {
8802
+ spawnedTask = spawnNextRecurrence(task, d, timestamp);
8803
+ } catch (e) {
8804
+ spawnedTask = null;
8805
+ console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
8806
+ }
8807
+ }
8808
+ let spawnedFromTemplate = null;
8809
+ if (task.spawns_template_id) {
8810
+ const spawnDepth = task.metadata?._spawn_depth || 0;
8811
+ if (spawnDepth >= MAX_SPAWN_DEPTH) {
8812
+ console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
8813
+ } else {
8814
+ try {
8815
+ const input = taskFromTemplate(task.spawns_template_id, {
8816
+ project_id: task.project_id ?? undefined,
8817
+ plan_id: task.plan_id ?? undefined,
8818
+ task_list_id: task.task_list_id ?? undefined,
8819
+ assigned_to: task.assigned_to ?? undefined
8820
+ }, d);
8821
+ input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
8822
+ spawnedFromTemplate = createTask(input, d);
8823
+ } catch {}
8824
+ }
8825
+ }
8826
+ const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
8827
+ if (spawnedTask) {
8828
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
8829
+ }
8830
+ if (spawnedFromTemplate) {
8831
+ meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
8832
+ }
8833
+ const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
8834
+ JOIN task_dependencies td ON td.task_id = t.id
8835
+ WHERE td.depends_on = ? AND t.status = 'pending'
8836
+ AND NOT EXISTS (
8837
+ SELECT 1 FROM task_dependencies td2
8838
+ JOIN tasks dep2 ON dep2.id = td2.depends_on
8839
+ WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
8840
+ )`).all(id, id);
8841
+ if (unblockedDeps.length > 0) {
8842
+ meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
8843
+ for (const dep of unblockedDeps) {
8844
+ const depTask = getTask(dep.id, d);
8845
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
8846
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
8847
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
8848
+ if (depTask)
8849
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
8850
+ }
8851
+ }
8852
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
8853
+ }
8854
+ function lockTask(id, agentId, db) {
8855
+ const d = db || getDatabase();
8856
+ const task = getTask(id, d);
8857
+ if (!task)
8858
+ throw new TaskNotFoundError(id);
8859
+ if (task.status === "completed" || task.status === "cancelled") {
8860
+ return {
8861
+ success: false,
8862
+ error: `Task is ${task.status} and cannot be locked`
8863
+ };
8864
+ }
8865
+ if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
8866
+ const timestamp2 = now();
8867
+ d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
8868
+ logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
8869
+ return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
8870
+ }
8871
+ const cutoff = lockExpiryCutoff();
8872
+ const timestamp = now();
8873
+ const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
8874
+ 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]);
8875
+ if (result.changes === 0) {
8876
+ const current = getTask(id, d);
8877
+ if (!current)
8878
+ throw new TaskNotFoundError(id);
8879
+ if (current.status === "completed" || current.status === "cancelled") {
8880
+ return {
8881
+ success: false,
8882
+ error: `Task is ${current.status} and cannot be locked`
8883
+ };
8884
+ }
8885
+ if (current.locked_by && !isLockExpired(current.locked_at)) {
8886
+ return {
8887
+ success: false,
8888
+ locked_by: current.locked_by,
8889
+ locked_at: current.locked_at,
8890
+ error: `Task is locked by ${current.locked_by}`
8891
+ };
8892
+ }
8893
+ return {
8894
+ success: false,
8895
+ error: `Task ${id} could not be locked because it changed during lock acquisition`
8896
+ };
8897
+ }
8898
+ logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
8899
+ return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
8900
+ }
8901
+ function unlockTask(id, agentId, db) {
8902
+ const d = db || getDatabase();
8903
+ const task = getTask(id, d);
8904
+ if (!task)
8905
+ throw new TaskNotFoundError(id);
8906
+ if (agentId && task.locked_by && task.locked_by !== agentId) {
8907
+ throw new LockError(id, task.locked_by);
8908
+ }
8909
+ const timestamp = now();
8910
+ d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
8911
+ WHERE id = ?`, [timestamp, id]);
8912
+ return true;
8913
+ }
8914
+ function getTaskLockStatus(id, db) {
8915
+ const d = db || getDatabase();
8916
+ const task = getTask(id, d);
8917
+ if (!task)
8918
+ throw new TaskNotFoundError(id);
8919
+ const expired = isLockExpired(task.locked_at);
8920
+ return {
8921
+ task_id: id,
8922
+ locked: !!task.locked_by && !expired,
8923
+ locked_by: task.locked_by,
8924
+ locked_at: task.locked_at,
8925
+ expires_at: lockExpiresAt(task.locked_at),
8926
+ expired
8927
+ };
8928
+ }
8929
+ function claimNextTask(agentId, filters, db) {
8930
+ const d = db || getDatabase();
8931
+ const MAX_ATTEMPTS = 25;
8932
+ const tried = new Set;
8933
+ for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
8934
+ const outcome = d.transaction(() => {
8935
+ const task = getNextTask(agentId, filters, d);
8936
+ if (!task)
8937
+ return { done: true, task: null };
8938
+ if (tried.has(task.id))
8939
+ return { done: true, task: null };
8940
+ tried.add(task.id);
8941
+ try {
8942
+ return { done: true, task: startTask(task.id, agentId, d) };
8943
+ } catch {
8944
+ return { done: false, task: null };
8945
+ }
8946
+ })();
8947
+ if (outcome.done)
8948
+ return outcome.task;
8949
+ }
8950
+ return null;
8951
+ }
8952
+ function getNextTask(agentId, filters, db) {
8953
+ const d = db || getDatabase();
8954
+ clearExpiredLocks(d);
8955
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
8956
+ const params = [lockExpiryCutoff()];
8957
+ if (filters?.project_id) {
8958
+ conditions.push("project_id = ?");
8959
+ params.push(filters.project_id);
8960
+ }
8961
+ if (filters?.task_list_id) {
8962
+ conditions.push("task_list_id = ?");
8963
+ params.push(filters.task_list_id);
8964
+ }
8965
+ if (filters?.plan_id) {
8966
+ conditions.push("plan_id = ?");
8967
+ params.push(filters.plan_id);
8968
+ }
8969
+ if (filters?.tags && filters.tags.length > 0) {
8970
+ const placeholders = filters.tags.map(() => "?").join(",");
8971
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
8972
+ params.push(...filters.tags);
8973
+ }
8974
+ 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')");
8975
+ const where = conditions.join(" AND ");
8976
+ let recentProjectIds = [];
8977
+ if (agentId) {
8978
+ 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);
8979
+ recentProjectIds = recentRows.map((r) => r.project_id);
8980
+ }
8981
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
8982
+ if (agentId) {
8983
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
8984
+ params.push(agentId);
8985
+ }
8986
+ if (recentProjectIds.length > 0) {
8987
+ const placeholders = recentProjectIds.map(() => "?").join(",");
8988
+ sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
8989
+ params.push(...recentProjectIds);
8990
+ }
8991
+ 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`;
8992
+ const row = d.query(sql).get(...params);
8993
+ return row ? rowToTask(row) : null;
8994
+ }
8995
+ function getActiveWork(filters, db) {
8996
+ const d = db || getDatabase();
8997
+ clearExpiredLocks(d);
8998
+ const conditions = ["status = 'in_progress'"];
8999
+ const params = [];
9000
+ if (filters?.project_id) {
9001
+ conditions.push("project_id = ?");
9002
+ params.push(filters.project_id);
9003
+ }
9004
+ if (filters?.task_list_id) {
9005
+ conditions.push("task_list_id = ?");
9006
+ params.push(filters.task_list_id);
9007
+ }
9008
+ const where = conditions.join(" AND ");
9009
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
9010
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
9011
+ updated_at DESC`).all(...params);
9012
+ return rows;
9013
+ }
9014
+ function getTasksChangedSince(since, filters, db) {
9015
+ const d = db || getDatabase();
9016
+ const conditions = ["updated_at > ?"];
9017
+ const params = [since];
9018
+ if (filters?.project_id) {
9019
+ conditions.push("project_id = ?");
9020
+ params.push(filters.project_id);
9021
+ }
9022
+ if (filters?.task_list_id) {
9023
+ conditions.push("task_list_id = ?");
9024
+ params.push(filters.task_list_id);
9025
+ }
9026
+ const where = conditions.join(" AND ");
9027
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
9028
+ return rows.map(rowToTask);
9029
+ }
9030
+ function failTask(id, agentId, reason, options, db) {
9031
+ const d = db || getDatabase();
9032
+ const databasePath = databasePathFromDatabase(d);
9033
+ const task = getTask(id, d);
9034
+ if (!task)
9035
+ throw new TaskNotFoundError(id);
9036
+ const meta = {
9037
+ ...task.metadata,
9038
+ _failure: {
9039
+ reason: reason || "Unknown failure",
9040
+ error_code: options?.error_code || null,
9041
+ failed_by: agentId || null,
9042
+ failed_at: now(),
9043
+ retry_requested: options?.retry || false
9044
+ }
9045
+ };
9046
+ const timestamp = now();
9047
+ const failTx = d.transaction(() => {
9048
+ const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
9049
+ WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
9050
+ if (res.changes === 0) {
9051
+ const current = getTask(id, d);
9052
+ throw new VersionConflictError(id, task.version, current?.version ?? -1);
9053
+ }
9054
+ });
9055
+ failTx();
9056
+ const failedTask = {
9057
+ ...task,
9058
+ status: "failed",
9059
+ locked_by: null,
9060
+ locked_at: null,
9061
+ metadata: meta,
9062
+ version: task.version + 1,
9063
+ updated_at: timestamp
9064
+ };
9065
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9066
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9067
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9068
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
9069
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
9070
+ let retryTask;
9071
+ if (options?.retry) {
9072
+ const retryCount = (task.retry_count || 0) + 1;
9073
+ const maxRetries = task.max_retries || 3;
9074
+ if (retryCount > maxRetries) {
9075
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
9076
+ JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
9077
+ id
9078
+ ]);
9079
+ } else {
9080
+ const backoffMinutes = Math.pow(5, retryCount - 1);
9081
+ const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
9082
+ let title = task.title;
9083
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
9084
+ title = title.slice(task.short_id.length + 2);
9085
+ }
9086
+ retryTask = createTask({
9087
+ title,
9088
+ description: task.description ?? undefined,
9089
+ priority: task.priority,
9090
+ project_id: task.project_id ?? undefined,
9091
+ task_list_id: task.task_list_id ?? undefined,
9092
+ plan_id: task.plan_id ?? undefined,
9093
+ assigned_to: task.assigned_to ?? undefined,
9094
+ tags: task.tags,
9095
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
9096
+ estimated_minutes: task.estimated_minutes ?? undefined,
9097
+ recurrence_rule: task.recurrence_rule ?? undefined,
9098
+ due_at: retryAfter
9099
+ }, d);
9100
+ d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
9101
+ }
9102
+ }
9103
+ return { task: failedTask, retryTask };
9104
+ }
9105
+ function getStaleTasks(staleQuery = 30, filters, db) {
9106
+ const d = db || getDatabase();
9107
+ const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
9108
+ const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
9109
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
9110
+ const conditions = [
9111
+ "status = 'in_progress'",
9112
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
9113
+ ];
9114
+ const params = [cutoff, cutoff];
9115
+ if (effectiveFilters?.project_id) {
9116
+ conditions.push("project_id = ?");
9117
+ params.push(effectiveFilters.project_id);
9118
+ }
9119
+ if (effectiveFilters?.task_list_id) {
9120
+ conditions.push("task_list_id = ?");
9121
+ params.push(effectiveFilters.task_list_id);
9157
9122
  }
9158
- params.push(taskId);
9159
- d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
9160
- return getTask(taskId, d);
9123
+ const where = conditions.join(" AND ");
9124
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
9125
+ return rows.map(rowToTask);
9161
9126
  }
9162
- function wouldCreateCycle(taskId, dependsOn, db) {
9163
- const visited = new Set;
9164
- const queue = [dependsOn];
9165
- while (queue.length > 0) {
9166
- const current = queue.shift();
9167
- if (current === taskId)
9168
- return true;
9169
- if (visited.has(current))
9170
- continue;
9171
- visited.add(current);
9172
- const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
9173
- for (const dep of deps) {
9174
- queue.push(dep.depends_on);
9127
+ function stealTask(agentId, opts, db) {
9128
+ const d = db || getDatabase();
9129
+ const databasePath = databasePathFromDatabase(d);
9130
+ const staleMinutes = opts?.stale_minutes ?? 30;
9131
+ const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
9132
+ if (staleTasks.length === 0)
9133
+ return null;
9134
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
9135
+ staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
9136
+ const target = staleTasks[0];
9137
+ const timestamp = now();
9138
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
9139
+ const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
9140
+ 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]);
9141
+ if (result.changes === 0)
9142
+ return null;
9143
+ logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
9144
+ logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
9145
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9146
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9147
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9148
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
9149
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
9150
+ return stolenTask;
9151
+ }
9152
+ function claimOrSteal(agentId, filters, db) {
9153
+ const d = db || getDatabase();
9154
+ const tx = d.transaction(() => {
9155
+ const next = getNextTask(agentId, filters, d);
9156
+ if (next) {
9157
+ const started = startTask(next.id, agentId, d);
9158
+ return { task: started, stolen: false };
9175
9159
  }
9160
+ const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
9161
+ if (stolen)
9162
+ return { task: stolen, stolen: true };
9163
+ return null;
9164
+ });
9165
+ return tx();
9166
+ }
9167
+ function spawnNextRecurrence(completedTask, db, completedAt) {
9168
+ const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
9169
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
9170
+ let title = completedTask.title;
9171
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
9172
+ title = title.slice(completedTask.short_id.length + 2);
9176
9173
  }
9177
- return false;
9174
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
9175
+ return createTask({
9176
+ title,
9177
+ description: completedTask.description ?? undefined,
9178
+ priority: completedTask.priority,
9179
+ project_id: completedTask.project_id ?? undefined,
9180
+ task_list_id: completedTask.task_list_id ?? undefined,
9181
+ plan_id: completedTask.plan_id ?? undefined,
9182
+ assigned_to: completedTask.assigned_to ?? undefined,
9183
+ tags: completedTask.tags,
9184
+ metadata: completedTask.metadata,
9185
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
9186
+ sla_minutes: completedTask.sla_minutes ?? undefined,
9187
+ recurrence_rule: completedTask.recurrence_rule,
9188
+ recurrence_parent_id: recurrenceParentId,
9189
+ due_at: dueAt
9190
+ }, db);
9178
9191
  }
9179
- var init_task_graph = __esm(() => {
9192
+ var MAX_SPAWN_DEPTH = 10;
9193
+ var init_task_lifecycle = __esm(() => {
9180
9194
  init_types();
9181
9195
  init_database();
9196
+ init_completion_guard();
9197
+ init_event_emission_safety();
9198
+ init_event_hooks();
9199
+ init_shared_events();
9200
+ init_audit();
9201
+ init_recurrence();
9202
+ init_webhooks();
9203
+ init_templates();
9182
9204
  init_task_crud();
9205
+ init_task_graph();
9183
9206
  });
9184
9207
 
9185
- // src/db/task-lifecycle.ts
9186
- function lockExpiresAt(lockedAt) {
9187
- if (!lockedAt)
9188
- return null;
9189
- return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
9208
+ // src/db/task-crud.ts
9209
+ function rowToTask(row) {
9210
+ return {
9211
+ ...row,
9212
+ tags: JSON.parse(row.tags || "[]"),
9213
+ metadata: JSON.parse(row.metadata || "{}"),
9214
+ status: row.status,
9215
+ priority: row.priority,
9216
+ requires_approval: !!row.requires_approval
9217
+ };
9190
9218
  }
9191
- function assertStartable(task, agentId) {
9192
- if (task.status === "pending")
9193
- return;
9194
- if (task.status === "in_progress")
9219
+ function insertTaskTags(taskId, tags, db) {
9220
+ if (tags.length === 0)
9195
9221
  return;
9196
- throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
9222
+ const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
9223
+ for (const tag of tags) {
9224
+ if (tag)
9225
+ stmt.run(taskId, tag);
9226
+ }
9197
9227
  }
9198
- function getBlockingDeps(id, db) {
9199
- const d = db || getDatabase();
9200
- const deps = getTaskDependencies(id, d);
9201
- if (deps.length === 0)
9202
- return [];
9203
- const blocking = [];
9204
- for (const dep of deps) {
9205
- const task = getTask(dep.depends_on, d);
9206
- if (task && task.status !== "completed")
9207
- blocking.push(task);
9228
+ function replaceTaskTags(taskId, tags, db) {
9229
+ db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
9230
+ insertTaskTags(taskId, tags, db);
9231
+ }
9232
+ function addMetadataConditions(metadata, conditions, params) {
9233
+ if (!metadata)
9234
+ return;
9235
+ for (const [key, value] of Object.entries(metadata)) {
9236
+ if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
9237
+ throw new Error(`Invalid metadata filter key: ${key}`);
9238
+ }
9239
+ conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
9240
+ params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
9208
9241
  }
9209
- return blocking;
9210
9242
  }
9211
- function startTask(id, agentId, db) {
9243
+ function createTask(input, db) {
9212
9244
  const d = db || getDatabase();
9213
- const databasePath = databasePathFromDatabase(d);
9214
- const task = getTask(id, d);
9215
- if (!task)
9216
- throw new TaskNotFoundError(id);
9217
- assertStartable(task, agentId);
9218
- const blocking = getBlockingDeps(id, d);
9219
- if (blocking.length > 0) {
9220
- const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
9221
- emitLocalEventHooksQuiet({
9222
- type: "task.blocked",
9223
- payload: {
9224
- id,
9225
- agent_id: agentId,
9226
- title: task.title,
9227
- blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
9228
- },
9229
- databasePath
9230
- });
9231
- throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
9232
- }
9233
- const cutoff = lockExpiryCutoff();
9234
9245
  const timestamp = now();
9235
- 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 = ?
9236
- 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]);
9237
- if (result.changes === 0) {
9238
- const current = getTask(id, d);
9239
- if (!current)
9240
- throw new TaskNotFoundError(id);
9241
- assertStartable(current, agentId);
9242
- if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
9243
- throw new LockError(id, current.locked_by);
9246
+ const tags = input.tags || [];
9247
+ const machineId = currentStorageMachineId(d);
9248
+ const assignedBy = input.assigned_by || input.agent_id;
9249
+ const assignedFromProject = input.assigned_from_project || null;
9250
+ let id = uuid();
9251
+ for (let attempt = 0;attempt < 3; attempt++) {
9252
+ try {
9253
+ 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)
9254
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
9255
+ id,
9256
+ null,
9257
+ input.project_id || null,
9258
+ input.parent_id || null,
9259
+ input.plan_id || null,
9260
+ input.task_list_id || null,
9261
+ input.cycle_id || null,
9262
+ input.title,
9263
+ input.description || null,
9264
+ input.status || "pending",
9265
+ input.priority || "medium",
9266
+ input.agent_id || null,
9267
+ input.assigned_to || null,
9268
+ input.session_id || null,
9269
+ input.working_dir || null,
9270
+ JSON.stringify(tags),
9271
+ JSON.stringify(input.metadata || {}),
9272
+ timestamp,
9273
+ timestamp,
9274
+ input.due_at || null,
9275
+ input.estimated_minutes || null,
9276
+ input.sla_minutes ?? null,
9277
+ input.confidence ?? null,
9278
+ input.retry_count ?? 0,
9279
+ input.max_retries ?? 3,
9280
+ input.retry_after ?? null,
9281
+ input.requires_approval ? 1 : 0,
9282
+ null,
9283
+ null,
9284
+ input.recurrence_rule || null,
9285
+ input.recurrence_parent_id || null,
9286
+ input.spawns_template_id || null,
9287
+ input.reason || null,
9288
+ input.spawned_from_session || null,
9289
+ assignedBy || null,
9290
+ assignedFromProject || null,
9291
+ input.task_type || null,
9292
+ machineId
9293
+ ]);
9294
+ break;
9295
+ } catch (e) {
9296
+ if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
9297
+ id = uuid();
9298
+ continue;
9299
+ }
9300
+ throw e;
9244
9301
  }
9245
- throw new Error(`Task ${id} could not be started because it changed during claim`);
9246
9302
  }
9247
- logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
9248
- 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 };
9249
- const payload = taskEventData(startedTask, { agent_id: agentId });
9250
- dispatchWebhook2("task.started", payload, d).catch(() => {});
9251
- emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
9252
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
9253
- return startedTask;
9303
+ if (tags.length > 0) {
9304
+ insertTaskTags(id, tags, d);
9305
+ }
9306
+ const task = getTask(id, d);
9307
+ const payload = taskEventData(task);
9308
+ const databasePath = databasePathFromDatabase(d);
9309
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
9310
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
9311
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
9312
+ return task;
9254
9313
  }
9255
- function completeTask(id, agentId, db, options) {
9314
+ function getTask(id, db) {
9315
+ const d = db || getDatabase();
9316
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
9317
+ if (!row)
9318
+ return null;
9319
+ return rowToTask(row);
9320
+ }
9321
+ function getTaskWithRelations(id, db) {
9256
9322
  const d = db || getDatabase();
9257
- const databasePath = databasePathFromDatabase(d);
9258
9323
  const task = getTask(id, d);
9259
9324
  if (!task)
9260
- throw new TaskNotFoundError(id);
9261
- if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
9262
- throw new LockError(id, task.locked_by);
9325
+ return null;
9326
+ const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
9327
+ const subtasks = subtaskRows.map(rowToTask);
9328
+ const depRows = d.query(`SELECT t.* FROM tasks t
9329
+ JOIN task_dependencies td ON td.depends_on = t.id
9330
+ WHERE td.task_id = ?`).all(id);
9331
+ const dependencies = depRows.map(rowToTask);
9332
+ const blockedByRows = d.query(`SELECT t.* FROM tasks t
9333
+ JOIN task_dependencies td ON td.task_id = t.id
9334
+ WHERE td.depends_on = ?`).all(id);
9335
+ const blocked_by = blockedByRows.map(rowToTask);
9336
+ const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
9337
+ const parent = task.parent_id ? getTask(task.parent_id, d) : null;
9338
+ const checklist = getChecklist(id, d);
9339
+ return {
9340
+ ...task,
9341
+ subtasks,
9342
+ dependencies,
9343
+ blocked_by,
9344
+ comments,
9345
+ parent,
9346
+ checklist
9347
+ };
9348
+ }
9349
+ function listTasks(filter = {}, db) {
9350
+ const d = db || getDatabase();
9351
+ const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
9352
+ clearExpiredLocks2(d);
9353
+ const conditions = [];
9354
+ const params = [];
9355
+ if (filter.project_id) {
9356
+ conditions.push("project_id = ?");
9357
+ params.push(filter.project_id);
9263
9358
  }
9264
- checkCompletionGuard(task, agentId || null, d);
9265
- 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;
9266
- const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
9267
- const completionMeta = {};
9268
- if (hasEvidence)
9269
- completionMeta._evidence = evidence;
9270
- if (options?.confidence !== undefined) {
9271
- completionMeta._completion = { confidence: options.confidence };
9359
+ if (filter.ids && filter.ids.length > 0) {
9360
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
9361
+ params.push(...filter.ids);
9272
9362
  }
9273
- const hasMeta = Object.keys(completionMeta).length > 0;
9274
- const timestamp = options?.completed_at || now();
9275
- const confidence = options?.confidence !== undefined ? options.confidence : null;
9276
- const tx = d.transaction(() => {
9277
- if (hasMeta) {
9278
- const meta2 = { ...task.metadata, ...completionMeta };
9279
- const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
9280
- if (metaResult.changes === 0) {
9281
- const current = getTask(id, d);
9282
- throw new VersionConflictError(id, task.version, current?.version ?? -1);
9283
- }
9363
+ if (filter.parent_id !== undefined) {
9364
+ if (filter.parent_id === null) {
9365
+ conditions.push("parent_id IS NULL");
9366
+ } else {
9367
+ conditions.push("parent_id = ?");
9368
+ params.push(filter.parent_id);
9284
9369
  }
9285
- d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
9286
- WHERE id = ?`, [timestamp, confidence, timestamp, id]);
9287
- });
9288
- tx();
9289
- logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
9290
- const completedTaskForEvent = {
9291
- ...task,
9292
- status: "completed",
9293
- locked_by: null,
9294
- locked_at: null,
9295
- completed_at: timestamp,
9296
- confidence,
9297
- version: task.version + 1,
9298
- updated_at: timestamp,
9299
- metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
9300
- };
9301
- const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
9302
- dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
9303
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
9304
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
9305
- let spawnedTask = null;
9306
- if (task.recurrence_rule && !options?.skip_recurrence) {
9307
- spawnedTask = spawnNextRecurrence(task, d, timestamp);
9308
9370
  }
9309
- let spawnedFromTemplate = null;
9310
- if (task.spawns_template_id) {
9311
- const spawnDepth = task.metadata?._spawn_depth || 0;
9312
- if (spawnDepth >= MAX_SPAWN_DEPTH) {
9313
- console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
9371
+ if (filter.status) {
9372
+ if (Array.isArray(filter.status)) {
9373
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
9374
+ params.push(...filter.status);
9314
9375
  } else {
9315
- try {
9316
- const input = taskFromTemplate(task.spawns_template_id, {
9317
- project_id: task.project_id ?? undefined,
9318
- plan_id: task.plan_id ?? undefined,
9319
- task_list_id: task.task_list_id ?? undefined,
9320
- assigned_to: task.assigned_to ?? undefined
9321
- }, d);
9322
- input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
9323
- spawnedFromTemplate = createTask(input, d);
9324
- } catch {}
9376
+ conditions.push("status = ?");
9377
+ params.push(filter.status);
9325
9378
  }
9326
9379
  }
9327
- const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
9328
- if (spawnedTask) {
9329
- meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
9380
+ if (filter.priority) {
9381
+ if (Array.isArray(filter.priority)) {
9382
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
9383
+ params.push(...filter.priority);
9384
+ } else {
9385
+ conditions.push("priority = ?");
9386
+ params.push(filter.priority);
9387
+ }
9330
9388
  }
9331
- if (spawnedFromTemplate) {
9332
- meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
9389
+ if (filter.assigned_to) {
9390
+ conditions.push("assigned_to = ?");
9391
+ params.push(filter.assigned_to);
9333
9392
  }
9334
- const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
9335
- JOIN task_dependencies td ON td.task_id = t.id
9336
- WHERE td.depends_on = ? AND t.status = 'pending'
9337
- AND NOT EXISTS (
9338
- SELECT 1 FROM task_dependencies td2
9339
- JOIN tasks dep2 ON dep2.id = td2.depends_on
9340
- WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
9341
- )`).all(id, id);
9342
- if (unblockedDeps.length > 0) {
9343
- meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
9344
- for (const dep of unblockedDeps) {
9345
- const depTask = getTask(dep.id, d);
9346
- const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
9347
- dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
9348
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
9349
- if (depTask)
9350
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
9351
- }
9393
+ if (filter.agent_id) {
9394
+ conditions.push("agent_id = ?");
9395
+ params.push(filter.agent_id);
9352
9396
  }
9353
- return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
9354
- }
9355
- function lockTask(id, agentId, db) {
9356
- const d = db || getDatabase();
9357
- const task = getTask(id, d);
9358
- if (!task)
9359
- throw new TaskNotFoundError(id);
9360
- if (task.status === "completed" || task.status === "cancelled") {
9361
- return {
9362
- success: false,
9363
- error: `Task is ${task.status} and cannot be locked`
9364
- };
9397
+ if (filter.session_id) {
9398
+ conditions.push("session_id = ?");
9399
+ params.push(filter.session_id);
9365
9400
  }
9366
- if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
9367
- const timestamp2 = now();
9368
- d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
9369
- logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
9370
- return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
9401
+ if (filter.tags && filter.tags.length > 0) {
9402
+ const placeholders = filter.tags.map(() => "?").join(",");
9403
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
9404
+ params.push(...filter.tags);
9371
9405
  }
9372
- const cutoff = lockExpiryCutoff();
9373
- const timestamp = now();
9374
- const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
9375
- 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]);
9376
- if (result.changes === 0) {
9377
- const current = getTask(id, d);
9378
- if (!current)
9379
- throw new TaskNotFoundError(id);
9380
- if (current.status === "completed" || current.status === "cancelled") {
9381
- return {
9382
- success: false,
9383
- error: `Task is ${current.status} and cannot be locked`
9384
- };
9385
- }
9386
- if (current.locked_by && !isLockExpired(current.locked_at)) {
9387
- return {
9388
- success: false,
9389
- locked_by: current.locked_by,
9390
- locked_at: current.locked_at,
9391
- error: `Task is locked by ${current.locked_by}`
9392
- };
9406
+ if (filter.plan_id) {
9407
+ conditions.push("plan_id = ?");
9408
+ params.push(filter.plan_id);
9409
+ }
9410
+ if (filter.task_list_id) {
9411
+ conditions.push("task_list_id = ?");
9412
+ params.push(filter.task_list_id);
9413
+ }
9414
+ if (filter.has_recurrence === true) {
9415
+ conditions.push("recurrence_rule IS NOT NULL");
9416
+ } else if (filter.has_recurrence === false) {
9417
+ conditions.push("recurrence_rule IS NULL");
9418
+ }
9419
+ if (filter.task_type) {
9420
+ if (Array.isArray(filter.task_type)) {
9421
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
9422
+ params.push(...filter.task_type);
9423
+ } else {
9424
+ conditions.push("task_type = ?");
9425
+ params.push(filter.task_type);
9393
9426
  }
9394
- return {
9395
- success: false,
9396
- error: `Task ${id} could not be locked because it changed during lock acquisition`
9397
- };
9398
9427
  }
9399
- logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
9400
- return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
9401
- }
9402
- function unlockTask(id, agentId, db) {
9403
- const d = db || getDatabase();
9404
- const task = getTask(id, d);
9405
- if (!task)
9406
- throw new TaskNotFoundError(id);
9407
- if (agentId && task.locked_by && task.locked_by !== agentId) {
9408
- throw new LockError(id, task.locked_by);
9428
+ addMetadataConditions(filter.metadata, conditions, params);
9429
+ const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
9430
+ if (filter.cursor) {
9431
+ try {
9432
+ const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
9433
+ conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
9434
+ params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
9435
+ } catch {}
9436
+ }
9437
+ if (!filter.include_archived) {
9438
+ conditions.push("archived_at IS NULL");
9439
+ }
9440
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
9441
+ let limitClause = "";
9442
+ if (filter.limit) {
9443
+ limitClause = " LIMIT ?";
9444
+ params.push(filter.limit);
9445
+ if (!filter.cursor && filter.offset) {
9446
+ limitClause += " OFFSET ?";
9447
+ params.push(filter.offset);
9448
+ }
9409
9449
  }
9410
- const timestamp = now();
9411
- d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
9412
- WHERE id = ?`, [timestamp, id]);
9413
- return true;
9450
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
9451
+ return rows.map(rowToTask);
9414
9452
  }
9415
- function getTaskLockStatus(id, db) {
9416
- const d = db || getDatabase();
9417
- const task = getTask(id, d);
9418
- if (!task)
9419
- throw new TaskNotFoundError(id);
9420
- const expired = isLockExpired(task.locked_at);
9453
+ function getTaskByFingerprint(fingerprint, db) {
9454
+ const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
9455
+ return tasks[0] ?? null;
9456
+ }
9457
+ function mergeTaskMetadata(current, next, fingerprint) {
9421
9458
  return {
9422
- task_id: id,
9423
- locked: !!task.locked_by && !expired,
9424
- locked_by: task.locked_by,
9425
- locked_at: task.locked_at,
9426
- expires_at: lockExpiresAt(task.locked_at),
9427
- expired
9459
+ ...current,
9460
+ ...next ?? {},
9461
+ fingerprint
9428
9462
  };
9429
9463
  }
9430
- function claimNextTask(agentId, filters, db) {
9464
+ function upsertTaskByFingerprint(input, db) {
9431
9465
  const d = db || getDatabase();
9466
+ const fingerprint = input.fingerprint.trim();
9467
+ if (!fingerprint)
9468
+ throw new Error("fingerprint is required");
9432
9469
  const tx = d.transaction(() => {
9433
- const task = getNextTask(agentId, filters, d);
9434
- if (!task)
9435
- return null;
9436
- return startTask(task.id, agentId, d);
9470
+ const existing = getTaskByFingerprint(fingerprint, d);
9471
+ const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
9472
+ if (!existing) {
9473
+ const task2 = createTask({ ...input, metadata }, d);
9474
+ return { task: task2, created: true };
9475
+ }
9476
+ const task = updateTask(existing.id, {
9477
+ version: existing.version,
9478
+ title: input.title,
9479
+ description: input.description,
9480
+ status: input.status,
9481
+ priority: input.priority,
9482
+ project_id: input.project_id,
9483
+ assigned_to: input.assigned_to,
9484
+ working_dir: input.working_dir,
9485
+ plan_id: input.plan_id,
9486
+ task_list_id: input.task_list_id,
9487
+ tags: input.tags,
9488
+ metadata,
9489
+ due_at: input.due_at,
9490
+ estimated_minutes: input.estimated_minutes,
9491
+ sla_minutes: input.sla_minutes,
9492
+ confidence: input.confidence,
9493
+ retry_count: input.retry_count,
9494
+ max_retries: input.max_retries,
9495
+ retry_after: input.retry_after,
9496
+ requires_approval: input.requires_approval,
9497
+ recurrence_rule: input.recurrence_rule,
9498
+ task_type: input.task_type
9499
+ }, d);
9500
+ return { task, created: false };
9437
9501
  });
9438
9502
  return tx();
9439
9503
  }
9440
- function getNextTask(agentId, filters, db) {
9504
+ function countTasks(filter = {}, db) {
9441
9505
  const d = db || getDatabase();
9442
- clearExpiredLocks(d);
9443
- const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
9444
- const params = [lockExpiryCutoff()];
9445
- if (filters?.project_id) {
9506
+ const conditions = [];
9507
+ const params = [];
9508
+ if (filter.project_id) {
9446
9509
  conditions.push("project_id = ?");
9447
- params.push(filters.project_id);
9510
+ params.push(filter.project_id);
9448
9511
  }
9449
- if (filters?.task_list_id) {
9450
- conditions.push("task_list_id = ?");
9451
- params.push(filters.task_list_id);
9512
+ if (filter.ids && filter.ids.length > 0) {
9513
+ conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
9514
+ params.push(...filter.ids);
9452
9515
  }
9453
- if (filters?.plan_id) {
9454
- conditions.push("plan_id = ?");
9455
- params.push(filters.plan_id);
9516
+ if (filter.parent_id !== undefined) {
9517
+ if (filter.parent_id === null) {
9518
+ conditions.push("parent_id IS NULL");
9519
+ } else {
9520
+ conditions.push("parent_id = ?");
9521
+ params.push(filter.parent_id);
9522
+ }
9456
9523
  }
9457
- if (filters?.tags && filters.tags.length > 0) {
9458
- const placeholders = filters.tags.map(() => "?").join(",");
9459
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
9460
- params.push(...filters.tags);
9524
+ if (filter.status) {
9525
+ if (Array.isArray(filter.status)) {
9526
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
9527
+ params.push(...filter.status);
9528
+ } else {
9529
+ conditions.push("status = ?");
9530
+ params.push(filter.status);
9531
+ }
9461
9532
  }
9462
- 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')");
9463
- const where = conditions.join(" AND ");
9464
- let recentProjectIds = [];
9465
- if (agentId) {
9466
- 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);
9467
- recentProjectIds = recentRows.map((r) => r.project_id);
9533
+ if (filter.priority) {
9534
+ if (Array.isArray(filter.priority)) {
9535
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
9536
+ params.push(...filter.priority);
9537
+ } else {
9538
+ conditions.push("priority = ?");
9539
+ params.push(filter.priority);
9540
+ }
9468
9541
  }
9469
- let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
9470
- if (agentId) {
9471
- sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
9472
- params.push(agentId);
9542
+ if (filter.assigned_to) {
9543
+ conditions.push("assigned_to = ?");
9544
+ params.push(filter.assigned_to);
9473
9545
  }
9474
- if (recentProjectIds.length > 0) {
9475
- const placeholders = recentProjectIds.map(() => "?").join(",");
9476
- sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
9477
- params.push(...recentProjectIds);
9546
+ if (filter.agent_id) {
9547
+ conditions.push("agent_id = ?");
9548
+ params.push(filter.agent_id);
9478
9549
  }
9479
- 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`;
9480
- const row = d.query(sql).get(...params);
9481
- return row ? rowToTask(row) : null;
9482
- }
9483
- function getActiveWork(filters, db) {
9484
- const d = db || getDatabase();
9485
- clearExpiredLocks(d);
9486
- const conditions = ["status = 'in_progress'"];
9487
- const params = [];
9488
- if (filters?.project_id) {
9489
- conditions.push("project_id = ?");
9490
- params.push(filters.project_id);
9550
+ if (filter.session_id) {
9551
+ conditions.push("session_id = ?");
9552
+ params.push(filter.session_id);
9491
9553
  }
9492
- if (filters?.task_list_id) {
9493
- conditions.push("task_list_id = ?");
9494
- params.push(filters.task_list_id);
9554
+ if (filter.tags && filter.tags.length > 0) {
9555
+ const placeholders = filter.tags.map(() => "?").join(",");
9556
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
9557
+ params.push(...filter.tags);
9495
9558
  }
9496
- const where = conditions.join(" AND ");
9497
- const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
9498
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
9499
- updated_at DESC`).all(...params);
9500
- return rows;
9501
- }
9502
- function getTasksChangedSince(since, filters, db) {
9503
- const d = db || getDatabase();
9504
- const conditions = ["updated_at > ?"];
9505
- const params = [since];
9506
- if (filters?.project_id) {
9507
- conditions.push("project_id = ?");
9508
- params.push(filters.project_id);
9559
+ if (filter.plan_id) {
9560
+ conditions.push("plan_id = ?");
9561
+ params.push(filter.plan_id);
9509
9562
  }
9510
- if (filters?.task_list_id) {
9563
+ if (filter.task_list_id) {
9511
9564
  conditions.push("task_list_id = ?");
9512
- params.push(filters.task_list_id);
9565
+ params.push(filter.task_list_id);
9513
9566
  }
9514
- const where = conditions.join(" AND ");
9515
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
9516
- return rows.map(rowToTask);
9567
+ if (filter.has_recurrence === true) {
9568
+ conditions.push("recurrence_rule IS NOT NULL");
9569
+ } else if (filter.has_recurrence === false) {
9570
+ conditions.push("recurrence_rule IS NULL");
9571
+ }
9572
+ if (filter.task_type) {
9573
+ if (Array.isArray(filter.task_type)) {
9574
+ conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
9575
+ params.push(...filter.task_type);
9576
+ } else {
9577
+ conditions.push("task_type = ?");
9578
+ params.push(filter.task_type);
9579
+ }
9580
+ }
9581
+ addMetadataConditions(filter.metadata, conditions, params);
9582
+ if (!filter.include_archived) {
9583
+ conditions.push("archived_at IS NULL");
9584
+ }
9585
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
9586
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
9587
+ return row.count;
9517
9588
  }
9518
- function failTask(id, agentId, reason, options, db) {
9589
+ function updateTask(id, input, db) {
9519
9590
  const d = db || getDatabase();
9520
- const databasePath = databasePathFromDatabase(d);
9521
9591
  const task = getTask(id, d);
9522
9592
  if (!task)
9523
9593
  throw new TaskNotFoundError(id);
9524
- const meta = {
9525
- ...task.metadata,
9526
- _failure: {
9527
- reason: reason || "Unknown failure",
9528
- error_code: options?.error_code || null,
9529
- failed_by: agentId || null,
9530
- failed_at: now(),
9531
- retry_requested: options?.retry || false
9532
- }
9533
- };
9594
+ if (task.version !== input.version) {
9595
+ throw new VersionConflictError(id, input.version, task.version);
9596
+ }
9534
9597
  const timestamp = now();
9535
- d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
9536
- WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
9537
- const failedTask = {
9598
+ const completionTimestamp = input.completed_at ?? timestamp;
9599
+ const sets = ["version = version + 1", "updated_at = ?"];
9600
+ const params = [timestamp];
9601
+ if (input.title !== undefined) {
9602
+ sets.push("title = ?");
9603
+ params.push(input.title);
9604
+ }
9605
+ if (input.description !== undefined) {
9606
+ sets.push("description = ?");
9607
+ params.push(input.description);
9608
+ }
9609
+ if (input.status !== undefined) {
9610
+ if (input.status === "completed") {
9611
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
9612
+ }
9613
+ sets.push("status = ?");
9614
+ params.push(input.status);
9615
+ if (input.status === "completed") {
9616
+ sets.push("completed_at = ?");
9617
+ params.push(completionTimestamp);
9618
+ sets.push("locked_by = NULL");
9619
+ sets.push("locked_at = NULL");
9620
+ } else if (task.status === "completed" && input.completed_at === undefined) {
9621
+ sets.push("completed_at = NULL");
9622
+ }
9623
+ }
9624
+ if (input.priority !== undefined) {
9625
+ sets.push("priority = ?");
9626
+ params.push(input.priority);
9627
+ }
9628
+ if (input.project_id !== undefined) {
9629
+ sets.push("project_id = ?");
9630
+ params.push(input.project_id);
9631
+ }
9632
+ if (input.assigned_to !== undefined) {
9633
+ sets.push("assigned_to = ?");
9634
+ params.push(input.assigned_to);
9635
+ }
9636
+ if (input.working_dir !== undefined) {
9637
+ sets.push("working_dir = ?");
9638
+ params.push(input.working_dir);
9639
+ }
9640
+ if (input.tags !== undefined) {
9641
+ sets.push("tags = ?");
9642
+ params.push(JSON.stringify(input.tags));
9643
+ }
9644
+ if (input.metadata !== undefined) {
9645
+ sets.push("metadata = ?");
9646
+ params.push(JSON.stringify(input.metadata));
9647
+ }
9648
+ if (input.plan_id !== undefined) {
9649
+ sets.push("plan_id = ?");
9650
+ params.push(input.plan_id);
9651
+ }
9652
+ if (input.task_list_id !== undefined) {
9653
+ sets.push("task_list_id = ?");
9654
+ params.push(input.task_list_id);
9655
+ }
9656
+ if (input.due_at !== undefined) {
9657
+ sets.push("due_at = ?");
9658
+ params.push(input.due_at);
9659
+ }
9660
+ if (input.estimated_minutes !== undefined) {
9661
+ sets.push("estimated_minutes = ?");
9662
+ params.push(input.estimated_minutes);
9663
+ }
9664
+ if (input.sla_minutes !== undefined) {
9665
+ sets.push("sla_minutes = ?");
9666
+ params.push(input.sla_minutes);
9667
+ }
9668
+ if (input.actual_minutes !== undefined) {
9669
+ sets.push("actual_minutes = ?");
9670
+ params.push(input.actual_minutes);
9671
+ }
9672
+ if (input.completed_at !== undefined && input.status !== "completed") {
9673
+ sets.push("completed_at = ?");
9674
+ params.push(input.completed_at);
9675
+ }
9676
+ if (input.confidence !== undefined) {
9677
+ sets.push("confidence = ?");
9678
+ params.push(input.confidence);
9679
+ }
9680
+ if (input.retry_count !== undefined) {
9681
+ sets.push("retry_count = ?");
9682
+ params.push(input.retry_count);
9683
+ }
9684
+ if (input.max_retries !== undefined) {
9685
+ sets.push("max_retries = ?");
9686
+ params.push(input.max_retries);
9687
+ }
9688
+ if (input.retry_after !== undefined) {
9689
+ sets.push("retry_after = ?");
9690
+ params.push(input.retry_after);
9691
+ }
9692
+ if (input.requires_approval !== undefined) {
9693
+ sets.push("requires_approval = ?");
9694
+ params.push(input.requires_approval ? 1 : 0);
9695
+ }
9696
+ if (input.approved_by !== undefined) {
9697
+ sets.push("approved_by = ?");
9698
+ params.push(input.approved_by);
9699
+ sets.push("approved_at = ?");
9700
+ params.push(now());
9701
+ }
9702
+ if (input.recurrence_rule !== undefined) {
9703
+ sets.push("recurrence_rule = ?");
9704
+ params.push(input.recurrence_rule);
9705
+ }
9706
+ if (input.task_type !== undefined) {
9707
+ sets.push("task_type = ?");
9708
+ params.push(input.task_type ?? null);
9709
+ }
9710
+ params.push(id, input.version);
9711
+ const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
9712
+ if (result.changes === 0) {
9713
+ const current = getTask(id, d);
9714
+ throw new VersionConflictError(id, input.version, current?.version ?? -1);
9715
+ }
9716
+ if (input.tags !== undefined) {
9717
+ replaceTaskTags(id, input.tags, d);
9718
+ }
9719
+ const transitionedToCompleted = input.status === "completed" && task.status !== "completed";
9720
+ if (transitionedToCompleted && task.recurrence_rule) {
9721
+ try {
9722
+ const { spawnNextRecurrence: spawnNextRecurrence2 } = (init_task_lifecycle(), __toCommonJS(exports_task_lifecycle));
9723
+ spawnNextRecurrence2(task, d, completionTimestamp);
9724
+ } catch (e) {
9725
+ console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
9726
+ }
9727
+ }
9728
+ const agentId = task.assigned_to || task.agent_id || null;
9729
+ if (input.status !== undefined && input.status !== task.status)
9730
+ logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
9731
+ if (input.priority !== undefined && input.priority !== task.priority)
9732
+ logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
9733
+ if (input.title !== undefined && input.title !== task.title)
9734
+ logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
9735
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
9736
+ logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
9737
+ if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
9738
+ logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
9739
+ if (input.approved_by !== undefined)
9740
+ logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
9741
+ const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
9742
+ const completedNow = input.status === "completed";
9743
+ const updatedTask = {
9538
9744
  ...task,
9539
- status: "failed",
9540
- locked_by: null,
9541
- locked_at: null,
9542
- metadata: meta,
9745
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
9746
+ tags: input.tags ?? task.tags,
9747
+ metadata: input.metadata ?? task.metadata,
9543
9748
  version: task.version + 1,
9544
- updated_at: timestamp
9749
+ updated_at: timestamp,
9750
+ locked_by: completedNow ? null : task.locked_by,
9751
+ locked_at: completedNow ? null : task.locked_at,
9752
+ completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
9753
+ sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
9754
+ actual_minutes: input.actual_minutes ?? task.actual_minutes,
9755
+ confidence: input.confidence !== undefined ? input.confidence : task.confidence,
9756
+ retry_count: input.retry_count ?? task.retry_count,
9757
+ max_retries: input.max_retries ?? task.max_retries,
9758
+ retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
9759
+ requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
9760
+ approved_by: input.approved_by ?? task.approved_by,
9761
+ approved_at: input.approved_by ? timestamp : task.approved_at
9545
9762
  };
9546
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
9547
- const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
9548
- dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
9549
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
9550
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
9551
- let retryTask;
9552
- if (options?.retry) {
9553
- const retryCount = (task.retry_count || 0) + 1;
9554
- const maxRetries = task.max_retries || 3;
9555
- if (retryCount > maxRetries) {
9556
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
9557
- JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
9558
- id
9559
- ]);
9560
- } else {
9561
- const backoffMinutes = Math.pow(5, retryCount - 1);
9562
- const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
9563
- let title = task.title;
9564
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
9565
- title = title.slice(task.short_id.length + 2);
9566
- }
9567
- retryTask = createTask({
9568
- title,
9569
- description: task.description ?? undefined,
9570
- priority: task.priority,
9571
- project_id: task.project_id ?? undefined,
9572
- task_list_id: task.task_list_id ?? undefined,
9573
- plan_id: task.plan_id ?? undefined,
9574
- assigned_to: task.assigned_to ?? undefined,
9575
- tags: task.tags,
9576
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
9577
- estimated_minutes: task.estimated_minutes ?? undefined,
9578
- recurrence_rule: task.recurrence_rule ?? undefined,
9579
- due_at: retryAfter
9580
- }, d);
9581
- d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
9582
- }
9763
+ const databasePath = databasePathFromDatabase(d);
9764
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
9765
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
9766
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9767
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
9768
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
9583
9769
  }
9584
- return { task: failedTask, retryTask };
9585
- }
9586
- function getStaleTasks(staleQuery = 30, filters, db) {
9587
- const d = db || getDatabase();
9588
- const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
9589
- const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
9590
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
9591
- const conditions = [
9592
- "status = 'in_progress'",
9593
- "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
9594
- ];
9595
- const params = [cutoff, cutoff];
9596
- if (effectiveFilters?.project_id) {
9597
- conditions.push("project_id = ?");
9598
- params.push(effectiveFilters.project_id);
9770
+ if (input.status !== undefined && input.status !== task.status) {
9771
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
9772
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
9773
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
9774
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
9599
9775
  }
9600
- if (effectiveFilters?.task_list_id) {
9601
- conditions.push("task_list_id = ?");
9602
- params.push(effectiveFilters.task_list_id);
9776
+ if (input.approved_by !== undefined) {
9777
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
9603
9778
  }
9604
- const where = conditions.join(" AND ");
9605
- const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
9606
- return rows.map(rowToTask);
9607
- }
9608
- function stealTask(agentId, opts, db) {
9609
- const d = db || getDatabase();
9610
- const databasePath = databasePathFromDatabase(d);
9611
- const staleMinutes = opts?.stale_minutes ?? 30;
9612
- const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
9613
- if (staleTasks.length === 0)
9614
- return null;
9615
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
9616
- staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
9617
- const target = staleTasks[0];
9618
- const timestamp = now();
9619
- const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
9620
- const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
9621
- 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]);
9622
- if (result.changes === 0)
9623
- return null;
9624
- logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
9625
- logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
9626
- const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
9627
- const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
9628
- dispatchWebhook2("task.assigned", payload, d).catch(() => {});
9629
- emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
9630
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
9631
- return stolenTask;
9779
+ const updatePayload = taskEventData(updatedTask);
9780
+ dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
9781
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
9782
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
9783
+ return updatedTask;
9632
9784
  }
9633
- function claimOrSteal(agentId, filters, db) {
9785
+ function deleteTask(id, db) {
9634
9786
  const d = db || getDatabase();
9635
- const tx = d.transaction(() => {
9636
- const next = getNextTask(agentId, filters, d);
9637
- if (next) {
9638
- const started = startTask(next.id, agentId, d);
9639
- return { task: started, stolen: false };
9640
- }
9641
- const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
9642
- if (stolen)
9643
- return { task: stolen, stolen: true };
9644
- return null;
9645
- });
9646
- return tx();
9647
- }
9648
- function spawnNextRecurrence(completedTask, db, completedAt) {
9649
- const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
9650
- const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
9651
- let title = completedTask.title;
9652
- if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
9653
- title = title.slice(completedTask.short_id.length + 2);
9654
- }
9655
- const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
9656
- return createTask({
9657
- title,
9658
- description: completedTask.description ?? undefined,
9659
- priority: completedTask.priority,
9660
- project_id: completedTask.project_id ?? undefined,
9661
- task_list_id: completedTask.task_list_id ?? undefined,
9662
- plan_id: completedTask.plan_id ?? undefined,
9663
- assigned_to: completedTask.assigned_to ?? undefined,
9664
- tags: completedTask.tags,
9665
- metadata: completedTask.metadata,
9666
- estimated_minutes: completedTask.estimated_minutes ?? undefined,
9667
- sla_minutes: completedTask.sla_minutes ?? undefined,
9668
- recurrence_rule: completedTask.recurrence_rule,
9669
- recurrence_parent_id: recurrenceParentId,
9670
- due_at: dueAt
9671
- }, db);
9787
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
9788
+ if (!row)
9789
+ return false;
9790
+ recordStorageTombstone({
9791
+ object_type: "tasks",
9792
+ object_id: id,
9793
+ payload: rowToTask(row),
9794
+ version: row.version
9795
+ }, d);
9796
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
9797
+ return result.changes > 0;
9672
9798
  }
9673
- var MAX_SPAWN_DEPTH = 10;
9674
- var init_task_lifecycle = __esm(() => {
9799
+ var init_task_crud = __esm(() => {
9675
9800
  init_types();
9676
9801
  init_database();
9677
9802
  init_completion_guard();
@@ -9679,11 +9804,9 @@ var init_task_lifecycle = __esm(() => {
9679
9804
  init_event_hooks();
9680
9805
  init_shared_events();
9681
9806
  init_audit();
9682
- init_recurrence();
9683
9807
  init_webhooks();
9684
- init_templates();
9685
- init_task_crud();
9686
- init_task_graph();
9808
+ init_checklists();
9809
+ init_storage_tombstones();
9687
9810
  });
9688
9811
 
9689
9812
  // src/db/task-status.ts
@@ -9773,6 +9896,15 @@ function setTaskStatus(id, status, _agentId, db) {
9773
9896
  throw new TaskNotFoundError(id);
9774
9897
  if (task.status === status)
9775
9898
  return task;
9899
+ if (status === "completed") {
9900
+ try {
9901
+ return completeTask(id, _agentId, d);
9902
+ } catch (e) {
9903
+ if (e instanceof VersionConflictError && attempt < 2)
9904
+ continue;
9905
+ throw e;
9906
+ }
9907
+ }
9776
9908
  try {
9777
9909
  return updateTask(id, { status, version: task.version }, d);
9778
9910
  } catch (e) {
@@ -12742,11 +12874,6 @@ __export(exports_task_routing, {
12742
12874
  setTaskWorkflowPointers: () => setTaskWorkflowPointers,
12743
12875
  getTaskRouteState: () => getTaskRouteState
12744
12876
  });
12745
- function classifyProjectKind2(path) {
12746
- if (!path)
12747
- return null;
12748
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
12749
- }
12750
12877
  function machineLocalPath(project, db) {
12751
12878
  const machineId = process.env["TODOS_MACHINE_ID"];
12752
12879
  if (!machineId)
@@ -12794,6 +12921,7 @@ function getTaskRouteState(taskOrId, db) {
12794
12921
  const automation = routingAutomationMetadata(task, taskList) ?? {};
12795
12922
  const routeEnabled = routeEnabledForTask(task, taskList) === true;
12796
12923
  const tagOptIn = task.tags.includes("auto:route") || task.tags.includes("route:enabled");
12924
+ const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
12797
12925
  const locked = Boolean(task.locked_by && !isLockExpired(task.locked_at));
12798
12926
  const blockers = getBlockingDeps(task.id, d);
12799
12927
  const blocked = blockers.length > 0;
@@ -12856,7 +12984,7 @@ function getTaskRouteState(taskOrId, db) {
12856
12984
  project_id: project?.id ?? task.project_id,
12857
12985
  project_path: projectPath,
12858
12986
  working_dir: task.working_dir ?? projectPath,
12859
- project_kind: classifyProjectKind2(projectPath),
12987
+ project_kind: projectKind,
12860
12988
  task_list_id: taskList?.id ?? task.task_list_id,
12861
12989
  task_list_slug: taskList?.slug ?? null,
12862
12990
  task_list_name: taskList?.name ?? null,
@@ -12925,20 +13053,56 @@ import chalk2 from "chalk";
12925
13053
  import { basename as basename3, resolve as resolve9 } from "path";
12926
13054
  function resolveProjectIdOrSlug(input) {
12927
13055
  const db = getDatabase();
13056
+ if (isPathLike(input)) {
13057
+ const projectPath = resolve9(input);
13058
+ const byPath2 = getProjectByPath(projectPath, db);
13059
+ return (byPath2 ?? ensureProject(basename3(projectPath), projectPath, db)).id;
13060
+ }
13061
+ const byPath = getProjectByPath(resolve9(input), db);
13062
+ if (byPath)
13063
+ return byPath.id;
12928
13064
  const byId = getProject(input, db);
12929
13065
  if (byId)
12930
13066
  return byId.id;
12931
- const row = db.query("SELECT id FROM projects WHERE name LIKE ? LIMIT 1").get(`%${input}%`);
13067
+ const partial = resolvePartialId(db, "projects", input);
13068
+ if (partial)
13069
+ return partial;
13070
+ const exact = db.query("SELECT id FROM projects WHERE lower(name) = lower(?) OR task_list_id = ? ORDER BY name LIMIT 1").get(input, input);
13071
+ if (exact)
13072
+ return exact.id;
13073
+ const inputSlug = slugify(input);
13074
+ if (inputSlug) {
13075
+ const all = db.query("SELECT id, name FROM projects ORDER BY name").all();
13076
+ const bySlug = all.find((p) => slugify(p.name) === inputSlug);
13077
+ if (bySlug)
13078
+ return bySlug.id;
13079
+ }
13080
+ const row = db.query("SELECT id FROM projects WHERE name LIKE ? ORDER BY name LIMIT 1").get(`%${input}%`);
12932
13081
  if (row)
12933
13082
  return row.id;
12934
- if (isPathLike(input)) {
12935
- const projectPath = resolve9(input);
12936
- const byPath = getProjectByPath(projectPath, db);
12937
- return (byPath ?? ensureProject(basename3(projectPath), projectPath, db)).id;
12938
- }
12939
13083
  console.error(chalk2.red(`Project not found: ${input}`));
12940
13084
  process.exit(1);
12941
13085
  }
13086
+ function parseStatus(value) {
13087
+ if (!value)
13088
+ return;
13089
+ const normalized = normalizeStatus(value);
13090
+ if (!TASK_STATUSES.includes(normalized)) {
13091
+ console.error(chalk2.red(`--status must be one of: ${TASK_STATUSES.join(", ")}`));
13092
+ process.exit(1);
13093
+ }
13094
+ return normalized;
13095
+ }
13096
+ function parseIntOption(value, flag) {
13097
+ if (value === undefined)
13098
+ return;
13099
+ const n = parseInt(value, 10);
13100
+ if (!Number.isFinite(n)) {
13101
+ console.error(chalk2.red(`${flag} must be a number`));
13102
+ process.exit(1);
13103
+ }
13104
+ return n;
13105
+ }
12942
13106
  function isPathLike(input) {
12943
13107
  return input.startsWith(".") || input.includes("/") || input.includes("\\");
12944
13108
  }
@@ -13037,27 +13201,32 @@ function registerTaskCommands(program2) {
13037
13201
  }
13038
13202
  return id;
13039
13203
  })() : undefined;
13040
- const task2 = createTask({
13041
- title,
13042
- description: opts.description,
13043
- priority: parsePriority(opts.priority),
13044
- parent_id: opts.parent ? resolveTaskId(opts.parent) : undefined,
13045
- tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
13046
- plan_id: opts.plan ? resolvePlanId(opts.plan) : undefined,
13047
- assigned_to: opts.assign,
13048
- status: opts.status ? normalizeStatus(opts.status) : undefined,
13049
- task_list_id: taskListId,
13050
- agent_id: globalOpts.agent,
13051
- session_id: globalOpts.session,
13052
- project_id: projectId,
13053
- working_dir: process.cwd(),
13054
- estimated_minutes: opts.estimated ? parseInt(opts.estimated, 10) : undefined,
13055
- sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseInt(opts.slaMinutes ?? opts.sla, 10) : undefined,
13056
- requires_approval: opts.approval || false,
13057
- recurrence_rule: opts.recurrence,
13058
- due_at: opts.due ? opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
13059
- reason: opts.reason
13060
- });
13204
+ let task2;
13205
+ try {
13206
+ task2 = createTask({
13207
+ title,
13208
+ description: opts.description,
13209
+ priority: parsePriority(opts.priority),
13210
+ parent_id: opts.parent ? resolveTaskId(opts.parent) : undefined,
13211
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
13212
+ plan_id: opts.plan ? resolvePlanId(opts.plan) : undefined,
13213
+ assigned_to: opts.assign,
13214
+ status: parseStatus(opts.status),
13215
+ task_list_id: taskListId,
13216
+ agent_id: globalOpts.agent,
13217
+ session_id: globalOpts.session,
13218
+ project_id: projectId,
13219
+ working_dir: process.cwd(),
13220
+ estimated_minutes: parseIntOption(opts.estimated, "--estimated"),
13221
+ sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
13222
+ requires_approval: opts.approval || false,
13223
+ recurrence_rule: opts.recurrence,
13224
+ due_at: opts.due ? opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
13225
+ reason: opts.reason
13226
+ });
13227
+ } catch (e) {
13228
+ handleError(e);
13229
+ }
13061
13230
  if (globalOpts.json) {
13062
13231
  output(task2, true);
13063
13232
  } else {
@@ -13088,7 +13257,7 @@ function registerTaskCommands(program2) {
13088
13257
  title: opts.title,
13089
13258
  description: opts.description,
13090
13259
  priority: parsePriority(opts.priority),
13091
- status: opts.status ? normalizeStatus(opts.status) : undefined,
13260
+ status: parseStatus(opts.status),
13092
13261
  task_list_id: taskListId,
13093
13262
  tags: parseTags(opts.tags),
13094
13263
  metadata: buildExpectationMetadata(opts),
@@ -13565,7 +13734,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13565
13734
  console.log(` ${chalk2.dim(h.created_at)} ${chalk2.bold(h.action)}${field}${change}${agent}`);
13566
13735
  }
13567
13736
  });
13568
- program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list").option("--task-list <id>", "Move to a task list (alias for --list)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").action((id, opts) => {
13737
+ program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list").option("--task-list <id>", "Move to a task list (alias for --list)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").option("--clear-approval", "Remove the approval requirement").action((id, opts) => {
13569
13738
  const globalOpts = program2.opts();
13570
13739
  opts.tags = opts.tags || opts.tag;
13571
13740
  opts.list = opts.list || opts.taskList;
@@ -13579,6 +13748,10 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13579
13748
  console.error(chalk2.red("Use either --plan or --clear-plan, not both."));
13580
13749
  process.exit(1);
13581
13750
  }
13751
+ if (opts.approval && opts.clearApproval) {
13752
+ console.error(chalk2.red("Use either --approval or --clear-approval, not both."));
13753
+ process.exit(1);
13754
+ }
13582
13755
  const taskListId = opts.list ? (() => {
13583
13756
  const db = getDatabase();
13584
13757
  const resolved = resolvePartialId(db, "task_lists", opts.list);
@@ -13595,17 +13768,17 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13595
13768
  version: current.version,
13596
13769
  title: opts.title,
13597
13770
  description: opts.description,
13598
- status: opts.status ? normalizeStatus(opts.status) : undefined,
13599
- priority: opts.priority,
13771
+ status: parseStatus(opts.status),
13772
+ priority: parsePriority(opts.priority),
13600
13773
  assigned_to: opts.assign,
13601
13774
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
13602
13775
  plan_id: planId,
13603
13776
  task_list_id: taskListId,
13604
- estimated_minutes: opts.estimated !== undefined ? parseInt(opts.estimated, 10) : undefined,
13605
- sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseInt(opts.slaMinutes ?? opts.sla, 10) : undefined,
13777
+ estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
13778
+ sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
13606
13779
  due_at: opts.due !== undefined ? opts.due === "" ? null : opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
13607
13780
  recurrence_rule: opts.recurrence !== undefined ? opts.recurrence === "" ? null : opts.recurrence : undefined,
13608
- requires_approval: opts.approval !== undefined ? true : undefined
13781
+ requires_approval: opts.clearApproval ? false : opts.approval !== undefined ? true : undefined
13609
13782
  });
13610
13783
  } catch (e) {
13611
13784
  handleError(e);
@@ -13622,7 +13795,14 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
13622
13795
  const resolvedId = resolveTaskId(id);
13623
13796
  const attachmentIds = opts.attachIds ? opts.attachIds.split(",").map((s) => s.trim()) : undefined;
13624
13797
  const filesChanged = opts.filesChanged ? opts.filesChanged.split(",").map((s) => s.trim()) : undefined;
13625
- const confidence = opts.confidence !== undefined ? parseFloat(opts.confidence) : undefined;
13798
+ let confidence;
13799
+ if (opts.confidence !== undefined) {
13800
+ confidence = parseFloat(opts.confidence);
13801
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
13802
+ console.error(chalk2.red("--confidence must be a number between 0.0 and 1.0"));
13803
+ process.exit(1);
13804
+ }
13805
+ }
13626
13806
  const evidence = attachmentIds || filesChanged || opts.testResults || opts.commitHash || opts.notes ? { attachment_ids: attachmentIds, files_changed: filesChanged, test_results: opts.testResults, commit_hash: opts.commitHash, notes: opts.notes } : undefined;
13627
13807
  let task2;
13628
13808
  try {
@@ -23254,12 +23434,21 @@ function registerProjectCommands(program2) {
23254
23434
  handleError(e);
23255
23435
  }
23256
23436
  });
23257
- program2.command("comment <id> <text>").description("Add a comment to a task").action((id, text) => {
23437
+ program2.command("comment <id> <text>").alias("log-progress").description("Add a comment to a task (alias: log-progress, for recording intermediate progress)").option("--pct <percent>", "Progress percentage (0-100) to record alongside the note").action((id, text, opts) => {
23258
23438
  const globalOpts = program2.opts();
23259
23439
  const resolvedId = resolveTaskId(id);
23440
+ let content = text;
23441
+ if (opts.pct !== undefined) {
23442
+ const pct = parseInt(opts.pct, 10);
23443
+ if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
23444
+ console.error(chalk4.red("--pct must be a number between 0 and 100"));
23445
+ process.exit(1);
23446
+ }
23447
+ content = `[progress ${pct}%] ${text}`;
23448
+ }
23260
23449
  const comment = addComment({
23261
23450
  task_id: resolvedId,
23262
- content: text,
23451
+ content,
23263
23452
  agent_id: globalOpts.agent,
23264
23453
  session_id: globalOpts.session
23265
23454
  });
@@ -25391,10 +25580,8 @@ var init_token_utils = __esm(() => {
25391
25580
  "cancel_task",
25392
25581
  "check_task_done_contract",
25393
25582
  "claim_task",
25394
- "clone_task",
25395
25583
  "delete_task",
25396
25584
  "extend_task",
25397
- "get_active_work",
25398
25585
  "get_archived_tasks",
25399
25586
  "get_blocked_tasks",
25400
25587
  "get_blocking_tasks",
@@ -25430,7 +25617,8 @@ var init_token_utils = __esm(() => {
25430
25617
  "task_context",
25431
25618
  "unlock_task",
25432
25619
  "unarchive_task",
25433
- "update_task"
25620
+ "update_task",
25621
+ "upsert_task"
25434
25622
  ],
25435
25623
  projects: [
25436
25624
  "bootstrap_project",
@@ -25689,12 +25877,8 @@ var init_token_utils = __esm(() => {
25689
25877
  "delete_tag",
25690
25878
  "get_label",
25691
25879
  "get_activity_timeline",
25692
- "get_recent_activity",
25693
25880
  "get_tag",
25694
25881
  "get_task_fields",
25695
- "get_task_graph",
25696
- "get_task_history",
25697
- "get_task_stats",
25698
25882
  "list_workflow_states",
25699
25883
  "list_labels",
25700
25884
  "list_tags",
@@ -25705,6 +25889,10 @@ var init_token_utils = __esm(() => {
25705
25889
  "describe_tools",
25706
25890
  "set_task_workflow_state",
25707
25891
  "set_task_fields",
25892
+ "assign_label_to_task",
25893
+ "create_custom_field",
25894
+ "set_task_custom_field",
25895
+ "set_task_priority_meta",
25708
25896
  "update_label",
25709
25897
  "update_tag"
25710
25898
  ],
@@ -25730,7 +25918,6 @@ var init_token_utils = __esm(() => {
25730
25918
  "update_template",
25731
25919
  "write_template_library"
25732
25920
  ],
25733
- webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
25734
25921
  machines: [
25735
25922
  "machines_archive",
25736
25923
  "machines_delete",
@@ -27288,6 +27475,11 @@ function safeEqualHex(a, b) {
27288
27475
  return false;
27289
27476
  return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
27290
27477
  }
27478
+ function safeEqualStrings(a, b) {
27479
+ const ah = createHash6("sha256").update(a, "utf8").digest();
27480
+ const bh = createHash6("sha256").update(b, "utf8").digest();
27481
+ return timingSafeEqual3(ah, bh);
27482
+ }
27291
27483
  function generatePlaintextKey() {
27292
27484
  return `tdos_${randomBytes2(32).toString("base64url")}`;
27293
27485
  }
@@ -27895,6 +28087,37 @@ function parseBoundedLimit(value, fallback, max) {
27895
28087
  return fallback;
27896
28088
  return Math.min(parsed, max);
27897
28089
  }
28090
+ function mapTaskError(e, json2) {
28091
+ if (e instanceof VersionConflictError) {
28092
+ return json2({
28093
+ error: e.message,
28094
+ code: VersionConflictError.code,
28095
+ expected_version: e.expectedVersion,
28096
+ current_version: e.actualVersion
28097
+ }, 409);
28098
+ }
28099
+ if (e instanceof TaskNotFoundError) {
28100
+ return json2({ error: e.message, code: TaskNotFoundError.code }, 404);
28101
+ }
28102
+ if (e instanceof LockError) {
28103
+ return json2({ error: e.message, code: LockError.code }, 409);
28104
+ }
28105
+ if (e instanceof CompletionGuardError) {
28106
+ return json2({
28107
+ error: e.message,
28108
+ code: CompletionGuardError.code,
28109
+ retry_after: e.retryAfterSeconds ?? null
28110
+ }, 409);
28111
+ }
28112
+ if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
28113
+ return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
28114
+ }
28115
+ return null;
28116
+ }
28117
+ function countRecurringTasks() {
28118
+ 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();
28119
+ return row?.count ?? 0;
28120
+ }
27898
28121
  function handleSseEvents(_req, url, ctx) {
27899
28122
  const agentId = url.searchParams.get("agent_id") || undefined;
27900
28123
  const projectId = url.searchParams.get("project_id") || undefined;
@@ -27973,35 +28196,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
27973
28196
  });
27974
28197
  }
27975
28198
  function handleHealth(_ctx, json2) {
27976
- const all = listTasks({ limit: 1e4 });
27977
- const stale = all.filter((t) => t.status === "in_progress" && new Date(t.updated_at).getTime() < Date.now() - 30 * 60 * 1000);
27978
- const overdue = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < new Date().toISOString());
27979
- 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() });
28199
+ const stats = getTaskStats();
28200
+ const staleCount = getStaleTasks(30).length;
28201
+ const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
28202
+ return json2({
28203
+ status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
28204
+ tasks: stats.total,
28205
+ stale: staleCount,
28206
+ overdue_recurring: overdueRecurring,
28207
+ timestamp: new Date().toISOString()
28208
+ });
27980
28209
  }
27981
28210
  function handleHeadlessBoundary(_ctx, json2) {
27982
28211
  const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
27983
28212
  return json2(getHeadlessBoundaryManifest2());
27984
28213
  }
27985
28214
  function handleStats(_ctx, json2) {
27986
- const all = listTasks({ limit: 1e4 });
28215
+ const stats = getTaskStats();
28216
+ const byStatus = stats.by_status;
27987
28217
  const projects = listProjects();
27988
28218
  const agents = listAgents();
27989
- const staleItems = getStaleTasks(30);
27990
- const nowStr = new Date().toISOString();
27991
- const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
27992
- const recurringTasks = all.filter((t) => t.recurrence_rule).length;
28219
+ const staleCount = getStaleTasks(30).length;
28220
+ const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
27993
28221
  return json2({
27994
- total_tasks: all.length,
27995
- pending: all.filter((t) => t.status === "pending").length,
27996
- in_progress: all.filter((t) => t.status === "in_progress").length,
27997
- completed: all.filter((t) => t.status === "completed").length,
27998
- failed: all.filter((t) => t.status === "failed").length,
27999
- cancelled: all.filter((t) => t.status === "cancelled").length,
28222
+ total_tasks: stats.total,
28223
+ pending: byStatus["pending"] ?? 0,
28224
+ in_progress: byStatus["in_progress"] ?? 0,
28225
+ completed: byStatus["completed"] ?? 0,
28226
+ failed: byStatus["failed"] ?? 0,
28227
+ cancelled: byStatus["cancelled"] ?? 0,
28000
28228
  projects: projects.length,
28001
28229
  agents: agents.length,
28002
- stale_count: staleItems.length,
28230
+ stale_count: staleCount,
28003
28231
  overdue_recurring: overdueRecurring,
28004
- recurring_tasks: recurringTasks
28232
+ recurring_tasks: countRecurringTasks()
28005
28233
  });
28006
28234
  }
28007
28235
  async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
@@ -28080,27 +28308,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
28080
28308
  const summaries = tasks.map((t) => taskToSummary2(t));
28081
28309
  if (format === "csv") {
28082
28310
  const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
28083
- const rows = summaries.map((t) => headers.map((h) => {
28084
- const val = t[h];
28311
+ const csvCell = (val) => {
28085
28312
  if (val === null || val === undefined)
28086
28313
  return "";
28087
- const str = String(val);
28088
- return str.includes(",") || str.includes('"') || str.includes(`
28089
- `) ? `"${str.replace(/"/g, '""')}"` : str;
28090
- }).join(","));
28314
+ let str = String(val);
28315
+ if (/^[=+\-@\t\r]/.test(str))
28316
+ str = `'${str}`;
28317
+ if (str.includes(",") || str.includes('"') || str.includes(`
28318
+ `) || str.includes("\r")) {
28319
+ str = `"${str.replace(/"/g, '""')}"`;
28320
+ }
28321
+ return str;
28322
+ };
28323
+ const rows = summaries.map((t) => headers.map((h) => csvCell(t[h])).join(","));
28091
28324
  const csv = [headers.join(","), ...rows].join(`
28092
28325
  `);
28093
28326
  return new Response(csv, {
28094
28327
  headers: {
28095
28328
  "Content-Type": "text/csv",
28096
- "Content-Disposition": "attachment; filename=tasks.csv"
28329
+ "Content-Disposition": "attachment; filename=tasks.csv",
28330
+ ...SECURITY_HEADERS
28097
28331
  }
28098
28332
  });
28099
28333
  }
28100
28334
  return new Response(JSON.stringify(summaries, null, 2), {
28101
28335
  headers: {
28102
28336
  "Content-Type": "application/json",
28103
- "Content-Disposition": "attachment; filename=tasks.json"
28337
+ "Content-Disposition": "attachment; filename=tasks.json",
28338
+ ...SECURITY_HEADERS
28104
28339
  }
28105
28340
  });
28106
28341
  }
@@ -28274,12 +28509,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
28274
28509
  if (ALLOWED.has(key))
28275
28510
  safeBody[key] = value;
28276
28511
  }
28512
+ const clientVersion = typeof body["version"] === "number" ? body["version"] : task.version;
28277
28513
  const updated = updateTask(id, {
28278
28514
  ...safeBody,
28279
- version: task.version
28515
+ version: clientVersion
28280
28516
  });
28281
28517
  return json2(taskToSummary2(updated));
28282
28518
  } catch (e) {
28519
+ const mapped = mapTaskError(e, json2);
28520
+ if (mapped)
28521
+ return mapped;
28283
28522
  return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
28284
28523
  }
28285
28524
  }
@@ -28295,6 +28534,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
28295
28534
  ctx.broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
28296
28535
  return json2(taskToSummary2(task));
28297
28536
  } catch (e) {
28537
+ const mapped = mapTaskError(e, json2);
28538
+ if (mapped)
28539
+ return mapped;
28298
28540
  return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
28299
28541
  }
28300
28542
  }
@@ -28314,6 +28556,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
28314
28556
  ctx.broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
28315
28557
  return json2(taskToSummary2(task));
28316
28558
  } catch (e) {
28559
+ const mapped = mapTaskError(e, json2);
28560
+ if (mapped)
28561
+ return mapped;
28317
28562
  return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
28318
28563
  }
28319
28564
  }
@@ -28638,6 +28883,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
28638
28883
  }
28639
28884
  var init_routes = __esm(() => {
28640
28885
  init_tasks();
28886
+ init_database();
28887
+ init_types();
28641
28888
  init_projects();
28642
28889
  init_agents();
28643
28890
  init_plans();
@@ -51837,14 +52084,16 @@ function printHelp() {
51837
52084
  Start the @hasna/todos MCP server.
51838
52085
 
51839
52086
  Options:
51840
- --stdio Use stdio transport
51841
- --port <port> Use Streamable HTTP on the given port
52087
+ --stdio Use stdio transport (default)
52088
+ --http Use Streamable HTTP transport
52089
+ --port <port> Use Streamable HTTP on the given port (implies --http)
51842
52090
  -V, --version output the version number
51843
52091
  -h, --help display help for command
51844
52092
 
51845
52093
  Environment:
51846
- TODOS_MCP_STDIO=true Force stdio transport
51847
- TODOS_MCP_PORT=<port> HTTP port when not using stdio
52094
+ MCP_STDIO=1 Force stdio transport
52095
+ MCP_HTTP=1 Use Streamable HTTP transport
52096
+ MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
51848
52097
  TODOS_PROFILE=<profile> Tool profile filter
51849
52098
  TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
51850
52099
  }
@@ -51933,8 +52182,22 @@ function formatError(error) {
51933
52182
  function resolveId(partialId, table = "tasks") {
51934
52183
  const db = getDatabase();
51935
52184
  const id = resolvePartialId(db, table, partialId);
51936
- if (!id)
51937
- throw new Error(`Could not resolve ID: ${partialId}`);
52185
+ if (!id) {
52186
+ switch (table) {
52187
+ case "tasks":
52188
+ throw new TaskNotFoundError(partialId);
52189
+ case "projects":
52190
+ throw new ProjectNotFoundError(partialId);
52191
+ case "plans":
52192
+ throw new PlanNotFoundError(partialId);
52193
+ case "task_lists":
52194
+ throw new TaskListNotFoundError(partialId);
52195
+ case "agents":
52196
+ throw new AgentNotFoundError(partialId);
52197
+ default:
52198
+ throw new TaskNotFoundError(partialId);
52199
+ }
52200
+ }
51938
52201
  return id;
51939
52202
  }
51940
52203
  function formatTask(task2) {
@@ -52023,8 +52286,9 @@ function buildServer() {
52023
52286
  return server;
52024
52287
  }
52025
52288
  async function main() {
52026
- const { isStdioMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http(), exports_http));
52027
- if (isStdioMode()) {
52289
+ const { isHttpMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http(), exports_http));
52290
+ const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
52291
+ if (!isHttpMode() && !portRequested) {
52028
52292
  const server = buildServer();
52029
52293
  const transport = new StdioServerTransport;
52030
52294
  await server.connect(transport);
@@ -52200,7 +52464,7 @@ function checkAuth(req, apiKey) {
52200
52464
  if (!apiKey && !generatedKeysEnabled)
52201
52465
  return null;
52202
52466
  const provided = getProvidedApiKey(req);
52203
- const matchesEnvKey = Boolean(apiKey && provided && provided === apiKey);
52467
+ const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
52204
52468
  const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
52205
52469
  if (!matchesEnvKey && !matchesGeneratedKey) {
52206
52470
  return new Response(JSON.stringify({ error: "Unauthorized" }), {
@@ -52210,6 +52474,15 @@ function checkAuth(req, apiKey) {
52210
52474
  }
52211
52475
  return null;
52212
52476
  }
52477
+ function resolveClientIp(req, server) {
52478
+ const trustProxy = process.env["TODOS_TRUST_PROXY"] === "1" || process.env["TODOS_TRUST_PROXY"] === "true";
52479
+ if (trustProxy) {
52480
+ const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim();
52481
+ if (forwarded)
52482
+ return forwarded;
52483
+ }
52484
+ return server.requestIP(req)?.address || "unknown";
52485
+ }
52213
52486
  function checkRateLimit(ip) {
52214
52487
  const now4 = Date.now();
52215
52488
  const entry = rateLimitMap.get(ip);
@@ -52337,7 +52610,7 @@ Dashboard not found at: ${dashboardDir}`);
52337
52610
  const server = Bun.serve({
52338
52611
  port,
52339
52612
  hostname: hostname3,
52340
- async fetch(req) {
52613
+ async fetch(req, server2) {
52341
52614
  const url = new URL(req.url);
52342
52615
  const path = url.pathname;
52343
52616
  const method = req.method;
@@ -52349,15 +52622,6 @@ Dashboard not found at: ${dashboardDir}`);
52349
52622
  Vary: "Origin"
52350
52623
  } : undefined;
52351
52624
  const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
52352
- if (path === "/health" && method === "GET") {
52353
- const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
52354
- return healthResponse2("todos");
52355
- }
52356
- if (path === "/mcp") {
52357
- const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
52358
- const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
52359
- return handleMcpHttpRequest2(req, buildServer2);
52360
- }
52361
52625
  if (method === "OPTIONS") {
52362
52626
  return new Response(null, {
52363
52627
  headers: corsHeaders || {
@@ -52365,7 +52629,7 @@ Dashboard not found at: ${dashboardDir}`);
52365
52629
  }
52366
52630
  });
52367
52631
  }
52368
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
52632
+ const ip = resolveClientIp(req, server2);
52369
52633
  const rl = checkRateLimit(ip);
52370
52634
  if (!rl.allowed) {
52371
52635
  return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
@@ -52373,6 +52637,18 @@ Dashboard not found at: ${dashboardDir}`);
52373
52637
  headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
52374
52638
  });
52375
52639
  }
52640
+ if (path === "/health" && method === "GET") {
52641
+ const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
52642
+ return healthResponse2("todos");
52643
+ }
52644
+ if (path === "/mcp") {
52645
+ const authError = checkAuth(req, apiKey);
52646
+ if (authError)
52647
+ return authError;
52648
+ const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
52649
+ const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
52650
+ return handleMcpHttpRequest2(req, buildServer2);
52651
+ }
52376
52652
  if (path.startsWith("/api/")) {
52377
52653
  const authError = checkAuth(req, apiKey);
52378
52654
  if (authError)
@@ -55219,6 +55495,339 @@ var init_config_serve_commands = __esm(() => {
55219
55495
  init_helpers();
55220
55496
  });
55221
55497
 
55498
+ // src/lib/task-route-sources.ts
55499
+ var exports_task_route_sources = {};
55500
+ __export(exports_task_route_sources, {
55501
+ discoverTaskRouteSources: () => discoverTaskRouteSources,
55502
+ TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION: () => TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION
55503
+ });
55504
+ import { Database as Database3 } from "bun:sqlite";
55505
+ import { createHash as createHash13 } from "crypto";
55506
+ import { existsSync as existsSync21, readdirSync as readdirSync5, statSync as statSync9 } from "fs";
55507
+ import { basename as basename9, dirname as dirname12, join as join21, resolve as resolve21 } from "path";
55508
+ function normalizePath5(input) {
55509
+ return resolve21(input);
55510
+ }
55511
+ function sourceStoreId(sourceDbPath) {
55512
+ const digest = createHash13("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
55513
+ return `sqlite:${digest}`;
55514
+ }
55515
+ function inferSourceRepoPath(sourceDbPath) {
55516
+ const normalized = normalizePath5(sourceDbPath);
55517
+ if (normalized.endsWith(TODO_STORE_RELATIVE_PATH)) {
55518
+ return dirname12(dirname12(dirname12(normalized)));
55519
+ }
55520
+ return dirname12(normalized);
55521
+ }
55522
+ function createStoreRef(sourceDbPath) {
55523
+ const normalized = normalizePath5(sourceDbPath);
55524
+ return {
55525
+ source_store_id: sourceStoreId(normalized),
55526
+ source_repo_path: inferSourceRepoPath(normalized),
55527
+ source_db_path: normalized
55528
+ };
55529
+ }
55530
+ function normalizePatterns(patterns) {
55531
+ return (patterns ?? []).map((pattern) => pattern.trim()).filter(Boolean);
55532
+ }
55533
+ function escapeRegExp(value) {
55534
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
55535
+ }
55536
+ function globPatternToRegExp(pattern) {
55537
+ let source3 = "";
55538
+ for (const char of pattern) {
55539
+ if (char === "*")
55540
+ source3 += ".*";
55541
+ else if (char === "?")
55542
+ source3 += ".";
55543
+ else
55544
+ source3 += escapeRegExp(char);
55545
+ }
55546
+ return new RegExp(`^${source3}$`);
55547
+ }
55548
+ function matchesPattern4(value, pattern) {
55549
+ const normalizedValue = value.replace(/\\/g, "/");
55550
+ const normalizedPattern = pattern.replace(/\\/g, "/");
55551
+ if (normalizedPattern.includes("*") || normalizedPattern.includes("?")) {
55552
+ return globPatternToRegExp(normalizedPattern).test(normalizedValue);
55553
+ }
55554
+ return normalizedValue.includes(normalizedPattern);
55555
+ }
55556
+ function storeMatchesAny(ref, patterns) {
55557
+ if (patterns.length === 0)
55558
+ return false;
55559
+ const paths = [ref.source_db_path, ref.source_repo_path].filter((value) => Boolean(value));
55560
+ const values = paths.flatMap((value) => [value, basename9(value)]);
55561
+ return patterns.some((pattern) => values.some((value) => matchesPattern4(value, pattern)));
55562
+ }
55563
+ function shouldIncludeStore(ref, include, exclude) {
55564
+ const included = include.length === 0 || storeMatchesAny(ref, include);
55565
+ return included && !storeMatchesAny(ref, exclude);
55566
+ }
55567
+ function discoverStoresUnderRoot(sourceRoot) {
55568
+ const rootPath = normalizePath5(sourceRoot);
55569
+ const errors2 = [];
55570
+ const stores = [];
55571
+ if (!existsSync21(rootPath)) {
55572
+ const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
55573
+ errors2.push({
55574
+ ...ref,
55575
+ code: "SOURCE_ROOT_MISSING",
55576
+ message: `Source root does not exist: ${rootPath}`
55577
+ });
55578
+ return { stores, errors: errors2 };
55579
+ }
55580
+ let rootStat;
55581
+ try {
55582
+ rootStat = statSync9(rootPath);
55583
+ } catch (error) {
55584
+ const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
55585
+ errors2.push({
55586
+ ...ref,
55587
+ code: "SOURCE_ROOT_UNREADABLE",
55588
+ message: error instanceof Error ? error.message : `Unable to read source root: ${rootPath}`
55589
+ });
55590
+ return { stores, errors: errors2 };
55591
+ }
55592
+ if (rootStat.isFile()) {
55593
+ stores.push(createStoreRef(rootPath));
55594
+ return { stores, errors: errors2 };
55595
+ }
55596
+ function scanDirectory(dir, depth) {
55597
+ const candidate = join21(dir, TODO_STORE_RELATIVE_PATH);
55598
+ if (existsSync21(candidate)) {
55599
+ stores.push(createStoreRef(candidate));
55600
+ }
55601
+ if (depth >= ROOT_SCAN_MAX_DEPTH)
55602
+ return;
55603
+ let entries;
55604
+ try {
55605
+ entries = readdirSync5(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
55606
+ } catch (error) {
55607
+ const ref = createStoreRef(candidate);
55608
+ errors2.push({
55609
+ ...ref,
55610
+ code: "SOURCE_ROOT_UNREADABLE",
55611
+ message: error instanceof Error ? error.message : `Unable to read source root: ${dir}`
55612
+ });
55613
+ return;
55614
+ }
55615
+ for (const entry of entries) {
55616
+ if (!entry.isDirectory() || SKIPPED_SCAN_DIRS.has(entry.name))
55617
+ continue;
55618
+ scanDirectory(join21(dir, entry.name), depth + 1);
55619
+ }
55620
+ }
55621
+ scanDirectory(rootPath, 0);
55622
+ return { stores, errors: errors2 };
55623
+ }
55624
+ function collectStoreRefs(input) {
55625
+ const byPath = new Map;
55626
+ const errors2 = [];
55627
+ for (const storePath of input.sourceStores ?? []) {
55628
+ const ref = createStoreRef(storePath);
55629
+ byPath.set(ref.source_db_path, ref);
55630
+ }
55631
+ for (const sourceRoot of input.sourceRoots ?? []) {
55632
+ const discovered = discoverStoresUnderRoot(sourceRoot);
55633
+ for (const ref of discovered.stores) {
55634
+ byPath.set(ref.source_db_path, ref);
55635
+ }
55636
+ errors2.push(...discovered.errors);
55637
+ }
55638
+ return {
55639
+ stores: [...byPath.values()].sort((a, b) => a.source_db_path.localeCompare(b.source_db_path)),
55640
+ errors: errors2.sort((a, b) => a.source_db_path.localeCompare(b.source_db_path))
55641
+ };
55642
+ }
55643
+ function openReadonlyStore(ref) {
55644
+ if (!existsSync21(ref.source_db_path)) {
55645
+ throw Object.assign(new Error(`Store does not exist: ${ref.source_db_path}`), { code: "STORE_MISSING" });
55646
+ }
55647
+ return new Database3(ref.source_db_path, { readonly: true, create: false });
55648
+ }
55649
+ function hasTable2(db, tableName) {
55650
+ const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
55651
+ return Boolean(row);
55652
+ }
55653
+ function tableColumns2(db, tableName) {
55654
+ const rows = db.query(`PRAGMA table_info(${tableName})`).all();
55655
+ return new Set(rows.map((row) => row.name));
55656
+ }
55657
+ function listPendingTasksReadonly(db) {
55658
+ if (!hasTable2(db, "tasks")) {
55659
+ throw Object.assign(new Error("Store does not contain a tasks table"), { code: "STORE_INVALID" });
55660
+ }
55661
+ const columns = tableColumns2(db, "tasks");
55662
+ const conditions = ["status = 'pending'"];
55663
+ if (columns.has("archived_at"))
55664
+ conditions.push("archived_at IS NULL");
55665
+ const rows = db.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")}
55666
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at DESC`).all();
55667
+ return rows.map(rowToTask);
55668
+ }
55669
+ function isReadyTask(task2, db) {
55670
+ if (task2.locked_by && !isLockExpired(task2.locked_at))
55671
+ return false;
55672
+ return getBlockingDeps(task2.id, db).length === 0;
55673
+ }
55674
+ function metadataFingerprint(metadata) {
55675
+ const value = metadata.fingerprint;
55676
+ if (typeof value === "string" && value.trim())
55677
+ return value;
55678
+ if (typeof value === "number" && Number.isFinite(value))
55679
+ return String(value);
55680
+ return null;
55681
+ }
55682
+ function boundedMetadataValue(value, depth = 0) {
55683
+ if (depth > 6)
55684
+ return "[TRUNCATED]";
55685
+ if (typeof value === "string") {
55686
+ return value.length > 2000 ? `${value.slice(0, 2000)}[TRUNCATED]` : value;
55687
+ }
55688
+ if (Array.isArray(value)) {
55689
+ return value.slice(0, 50).map((item) => boundedMetadataValue(item, depth + 1));
55690
+ }
55691
+ if (value && typeof value === "object") {
55692
+ const result = {};
55693
+ for (const [key, child] of Object.entries(value).slice(0, 80)) {
55694
+ const normalized = key.toLowerCase();
55695
+ if (normalized === "comment" || normalized === "comments" || normalized === "task_comments") {
55696
+ result[key] = "[REDACTED_COMMENT]";
55697
+ continue;
55698
+ }
55699
+ result[key] = boundedMetadataValue(child, depth + 1);
55700
+ }
55701
+ return result;
55702
+ }
55703
+ return value;
55704
+ }
55705
+ function discoveryMetadata(metadata) {
55706
+ return redactValue(boundedMetadataValue(metadata));
55707
+ }
55708
+ function sourceCandidate(ref, task2, db) {
55709
+ const routeState = getTaskRouteState(task2, db);
55710
+ const autoRoute = task2.tags.includes("auto:route") || task2.tags.includes("route:enabled");
55711
+ return {
55712
+ source_store_id: ref.source_store_id,
55713
+ source_repo_path: ref.source_repo_path,
55714
+ source_db_path: ref.source_db_path,
55715
+ source_task_key: `${ref.source_store_id}:${task2.id}`,
55716
+ source_selected_by_input: true,
55717
+ task_id: task2.id,
55718
+ task_short_id: task2.short_id,
55719
+ title: task2.title,
55720
+ status: task2.status,
55721
+ priority: task2.priority,
55722
+ project_path: routeState.route.project_path ?? task2.working_dir ?? ref.source_repo_path,
55723
+ task_version: task2.version,
55724
+ task_updated_at: task2.updated_at,
55725
+ task_fingerprint: metadataFingerprint(task2.metadata),
55726
+ tags: task2.tags,
55727
+ task_intent: {
55728
+ auto_route: autoRoute
55729
+ },
55730
+ metadata: discoveryMetadata(task2.metadata),
55731
+ route_state: routeState
55732
+ };
55733
+ }
55734
+ function discoveryError(ref, code, error) {
55735
+ return {
55736
+ ...ref,
55737
+ code,
55738
+ message: error instanceof Error ? error.message : String(error)
55739
+ };
55740
+ }
55741
+ function errorCode(error) {
55742
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_MISSING") {
55743
+ return "STORE_MISSING";
55744
+ }
55745
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_INVALID") {
55746
+ return "STORE_INVALID";
55747
+ }
55748
+ return "STORE_UNREADABLE";
55749
+ }
55750
+ function discoverTaskRouteSources(input) {
55751
+ const include = normalizePatterns(input.include);
55752
+ const exclude = normalizePatterns(input.exclude);
55753
+ const sourceRoots = (input.sourceRoots ?? []).map(normalizePath5).sort();
55754
+ const sourceStores = (input.sourceStores ?? []).map(normalizePath5).sort();
55755
+ const limit = Number.isFinite(input.limit ?? NaN) && (input.limit ?? 0) >= 0 ? Math.floor(input.limit ?? 0) : null;
55756
+ const collected = collectStoreRefs(input);
55757
+ const stores = [];
55758
+ const errors2 = [...collected.errors];
55759
+ const candidates = [];
55760
+ let totalCandidateCount = 0;
55761
+ for (const ref of collected.stores) {
55762
+ if (!shouldIncludeStore(ref, include, exclude))
55763
+ continue;
55764
+ const storeErrors = [];
55765
+ let db = null;
55766
+ try {
55767
+ db = openReadonlyStore(ref);
55768
+ const readyTasks = listPendingTasksReadonly(db).filter((task2) => isReadyTask(task2, db));
55769
+ totalCandidateCount += readyTasks.length;
55770
+ const remaining = limit === null ? readyTasks.length : Math.max(0, limit - candidates.length);
55771
+ const selectedTasks = limit === null ? readyTasks : readyTasks.slice(0, remaining);
55772
+ candidates.push(...selectedTasks.map((task2) => sourceCandidate(ref, task2, db)));
55773
+ stores.push({
55774
+ ...ref,
55775
+ status: "ok",
55776
+ candidate_count: readyTasks.length,
55777
+ returned_candidate_count: selectedTasks.length,
55778
+ errors: []
55779
+ });
55780
+ } catch (error) {
55781
+ const storeError = discoveryError(ref, errorCode(error), error);
55782
+ storeErrors.push(storeError);
55783
+ errors2.push(storeError);
55784
+ stores.push({
55785
+ ...ref,
55786
+ status: storeError.code === "STORE_MISSING" ? "missing" : "error",
55787
+ candidate_count: 0,
55788
+ returned_candidate_count: 0,
55789
+ errors: storeErrors
55790
+ });
55791
+ } finally {
55792
+ db?.close();
55793
+ }
55794
+ }
55795
+ return {
55796
+ schema_version: TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION,
55797
+ sourceRoots,
55798
+ sourceStores,
55799
+ include,
55800
+ exclude,
55801
+ limit,
55802
+ total_candidate_count: totalCandidateCount,
55803
+ returned_candidate_count: candidates.length,
55804
+ truncated: limit !== null && totalCandidateCount > candidates.length,
55805
+ stores,
55806
+ candidates,
55807
+ errors: errors2
55808
+ };
55809
+ }
55810
+ var TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION = "todos.task_route_sources.v1", TODO_STORE_RELATIVE_PATH, ROOT_SCAN_MAX_DEPTH = 5, SKIPPED_SCAN_DIRS;
55811
+ var init_task_route_sources = __esm(() => {
55812
+ init_database();
55813
+ init_task_lifecycle();
55814
+ init_task_crud();
55815
+ init_redaction();
55816
+ init_task_routing();
55817
+ TODO_STORE_RELATIVE_PATH = join21(".hasna", "todos", "todos.db");
55818
+ SKIPPED_SCAN_DIRS = new Set([
55819
+ ".git",
55820
+ ".hg",
55821
+ ".svn",
55822
+ "node_modules",
55823
+ "dist",
55824
+ "build",
55825
+ ".next",
55826
+ ".turbo",
55827
+ ".cache"
55828
+ ]);
55829
+ });
55830
+
55222
55831
  // src/lib/tester-issue-reports.ts
55223
55832
  var exports_tester_issue_reports = {};
55224
55833
  __export(exports_tester_issue_reports, {
@@ -55231,7 +55840,7 @@ __export(exports_tester_issue_reports, {
55231
55840
  TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
55232
55841
  TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION
55233
55842
  });
55234
- import { createHash as createHash13 } from "crypto";
55843
+ import { createHash as createHash14 } from "crypto";
55235
55844
  function asObject3(value) {
55236
55845
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
55237
55846
  }
@@ -55403,7 +56012,7 @@ function fingerprintTesterIssueReport(report) {
55403
56012
  normalizeText4(report.failure?.message || report.summary || report.title).slice(0, 240),
55404
56013
  normalizeText4(stackTop).slice(0, 160)
55405
56014
  ].join("::");
55406
- return `testers:${createHash13("sha256").update(raw).digest("hex").slice(0, 16)}`;
56015
+ return `testers:${createHash14("sha256").update(raw).digest("hex").slice(0, 16)}`;
55407
56016
  }
55408
56017
  function priorityForSeverity(severity, fallback) {
55409
56018
  return PRIORITIES5.includes(severity) ? severity : fallback;
@@ -55722,6 +56331,15 @@ function parseCsvOption(value) {
55722
56331
  const values = value.split(",").map((item) => item.trim()).filter(Boolean);
55723
56332
  return values.length > 0 ? values : undefined;
55724
56333
  }
56334
+ function collectOption2(value, previous = []) {
56335
+ return [...previous, value];
56336
+ }
56337
+ function expandRepeatedCsvOption(value) {
56338
+ if (!value || value.length === 0)
56339
+ return;
56340
+ const values = value.flatMap((item) => item.split(",").map((part) => part.trim()).filter(Boolean));
56341
+ return values.length > 0 ? values : undefined;
56342
+ }
55725
56343
  function resolveOptionalId(table, value) {
55726
56344
  if (!value)
55727
56345
  return;
@@ -55880,6 +56498,7 @@ function registerQueryCommands(program2) {
55880
56498
  });
55881
56499
  program2.command("next").description("Show the best pending task to work on next").option("--agent <id>", "Prefer tasks assigned to this agent").option("--project <id>", "Filter to project").option("-j, --json", "Output as JSON").action(async (opts) => {
55882
56500
  const globalOpts = program2.opts();
56501
+ const json2 = opts.json || globalOpts.json;
55883
56502
  const db = getDatabase();
55884
56503
  const filters = {};
55885
56504
  const projectInput = opts.project || globalOpts.project;
@@ -55890,10 +56509,14 @@ function registerQueryCommands(program2) {
55890
56509
  }
55891
56510
  const task2 = getNextTask(opts.agent, Object.keys(filters).length ? filters : undefined, db);
55892
56511
  if (!task2) {
56512
+ if (json2) {
56513
+ console.log(JSON.stringify(null));
56514
+ return;
56515
+ }
55893
56516
  console.log(chalk7.dim("No tasks available."));
55894
56517
  return;
55895
56518
  }
55896
- if (opts.json) {
56519
+ if (json2) {
55897
56520
  console.log(JSON.stringify(task2, null, 2));
55898
56521
  return;
55899
56522
  }
@@ -55903,16 +56526,26 @@ function registerQueryCommands(program2) {
55903
56526
  console.log(chalk7.dim(` ${task2.description.slice(0, 100)}`));
55904
56527
  });
55905
56528
  program2.command("claim <agent>").description("Atomically claim the best pending task for an agent").option("--project <id>", "Filter to project").option("--steal-stale", "Steal the highest-priority stale task when no pending task is available").option("--stale-minutes <n>", "How long a task must be stale before stealing (default: 30)", "30").option("-j, --json", "Output as JSON").action(async (agent, opts) => {
56529
+ const globalOpts = program2.opts();
56530
+ const json2 = opts.json || globalOpts.json;
55906
56531
  const db = getDatabase();
55907
56532
  const filters = {};
55908
- if (opts.project)
55909
- filters.project_id = opts.project;
56533
+ const projectInput = opts.project || globalOpts.project;
56534
+ if (projectInput) {
56535
+ const pid = autoProject({ project: projectInput }) || resolvePartialId(db, "projects", projectInput) || db.query("SELECT id FROM projects WHERE path = ? OR name = ? OR task_list_id = ?").get(projectInput, projectInput, projectInput)?.id;
56536
+ if (pid)
56537
+ filters.project_id = pid;
56538
+ }
55910
56539
  const task2 = opts.stealStale ? (await Promise.resolve().then(() => (init_tasks(), exports_tasks))).claimOrSteal(agent, { ...filters, stale_minutes: parseInt(opts.staleMinutes, 10) }, db)?.task ?? null : claimNextTask(agent, Object.keys(filters).length ? filters : undefined, db);
55911
56540
  if (!task2) {
56541
+ if (json2) {
56542
+ console.log(JSON.stringify(null));
56543
+ return;
56544
+ }
55912
56545
  console.log(chalk7.dim("No tasks available to claim."));
55913
56546
  return;
55914
56547
  }
55915
- if (opts.json) {
56548
+ if (json2) {
55916
56549
  console.log(JSON.stringify(task2, null, 2));
55917
56550
  return;
55918
56551
  }
@@ -55933,12 +56566,14 @@ function registerQueryCommands(program2) {
55933
56566
  console.log(chalk7.green(`Stolen: ${task2.short_id || task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}`));
55934
56567
  });
55935
56568
  program2.command("status").description("Show full project health snapshot").option("--agent <id>", "Include next task for this agent").option("--project <id>", "Filter to project").option("-j, --json", "Output as JSON").action(async (opts) => {
56569
+ const globalOpts = program2.opts();
56570
+ const json2 = opts.json || globalOpts.json;
55936
56571
  const db = getDatabase();
55937
56572
  const filters = {};
55938
56573
  if (opts.project)
55939
56574
  filters.project_id = opts.project;
55940
56575
  const s = getStatus(Object.keys(filters).length ? filters : undefined, opts.agent, undefined, db);
55941
- if (opts.json) {
56576
+ if (json2) {
55942
56577
  console.log(JSON.stringify(s, null, 2));
55943
56578
  return;
55944
56579
  }
@@ -56075,6 +56710,8 @@ Blocked:`));
56075
56710
  console.log();
56076
56711
  });
56077
56712
  program2.command("fail <id>").description("Mark a task as failed with optional reason and retry").option("--reason <text>", "Why it failed").option("--agent <id>", "Agent reporting the failure").option("--retry", "Auto-create a retry copy").option("-j, --json", "Output as JSON").action(async (id, opts) => {
56713
+ const globalOpts = program2.opts();
56714
+ const json2 = opts.json || globalOpts.json;
56078
56715
  const db = getDatabase();
56079
56716
  const resolvedId = resolvePartialId(db, "tasks", id);
56080
56717
  if (!resolvedId) {
@@ -56082,7 +56719,7 @@ Blocked:`));
56082
56719
  process.exit(1);
56083
56720
  }
56084
56721
  const result = failTask(resolvedId, opts.agent, opts.reason, { retry: opts.retry }, db);
56085
- if (opts.json) {
56722
+ if (json2) {
56086
56723
  console.log(JSON.stringify(result, null, 2));
56087
56724
  return;
56088
56725
  }
@@ -56093,12 +56730,14 @@ Blocked:`));
56093
56730
  console.log(chalk7.yellow(`Retry created: ${result.retryTask.short_id || result.retryTask.id.slice(0, 8)} | ${result.retryTask.title}`));
56094
56731
  });
56095
56732
  program2.command("active").description("Show all currently in-progress tasks").option("--project <id>", "Filter to project").option("-j, --json", "Output as JSON").action(async (opts) => {
56733
+ const globalOpts = program2.opts();
56734
+ const json2 = opts.json || globalOpts.json;
56096
56735
  const db = getDatabase();
56097
56736
  const filters = {};
56098
56737
  if (opts.project)
56099
56738
  filters.project_id = opts.project;
56100
56739
  const work = getActiveWork(Object.keys(filters).length ? filters : undefined, db);
56101
- if (opts.json) {
56740
+ if (json2) {
56102
56741
  console.log(JSON.stringify(work, null, 2));
56103
56742
  return;
56104
56743
  }
@@ -56114,12 +56753,14 @@ Blocked:`));
56114
56753
  }
56115
56754
  });
56116
56755
  program2.command("stale").description("Find tasks stuck in_progress with no recent activity").option("--minutes <n>", "Stale threshold in minutes", "30").option("--project <id>", "Filter to project").option("-j, --json", "Output as JSON").action(async (opts) => {
56756
+ const globalOpts = program2.opts();
56757
+ const json2 = opts.json || globalOpts.json;
56117
56758
  const db = getDatabase();
56118
56759
  const filters = {};
56119
56760
  if (opts.project)
56120
56761
  filters.project_id = opts.project;
56121
56762
  const tasks = getStaleTasks(parseInt(opts.minutes, 10), Object.keys(filters).length ? filters : undefined, db);
56122
- if (opts.json) {
56763
+ if (json2) {
56123
56764
  console.log(JSON.stringify(tasks, null, 2));
56124
56765
  return;
56125
56766
  }
@@ -56143,7 +56784,7 @@ Blocked:`));
56143
56784
  project_id: projectId,
56144
56785
  limit: opts.limit ? parseInt(opts.limit, 10) : undefined
56145
56786
  }, db);
56146
- if (opts.json) {
56787
+ if (opts.json || globalOpts.json) {
56147
56788
  console.log(JSON.stringify(result, null, 2));
56148
56789
  return;
56149
56790
  }
@@ -56360,13 +57001,13 @@ Repairs`));
56360
57001
  try {
56361
57002
  const db = getDatabase();
56362
57003
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
56363
- const { statSync: statSync9 } = await import("fs");
56364
- const { join: join21 } = await import("path");
57004
+ const { statSync: statSync10 } = await import("fs");
57005
+ const { join: join22 } = await import("path");
56365
57006
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
56366
- const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join21(home, ".hasna", "todos", "todos.db");
57007
+ const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join22(home, ".hasna", "todos", "todos.db");
56367
57008
  let size = "unknown";
56368
57009
  try {
56369
- size = `${(statSync9(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
57010
+ size = `${(statSync10(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
56370
57011
  } catch {}
56371
57012
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk7.dim(dbPath)}` });
56372
57013
  } catch (e) {
@@ -56856,8 +57497,42 @@ Repairs`));
56856
57497
  console.log(` ${chalk7.dim(time2)} ${chalk7.cyan(entry.source)} ${chalk7.dim(ref)} ${entry.event_type}${message}${agent}`);
56857
57498
  }
56858
57499
  });
56859
- program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").action(async (opts) => {
57500
+ program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").option("--source-root <path>", "Read-only source root to scan for .hasna/todos/todos.db (repeatable)", collectOption2, []).option("--source-store <path>", "Read-only todos SQLite store path to scan (repeatable)", collectOption2, []).option("--include <pattern>", "Include source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).option("--exclude <pattern>", "Exclude source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).action(async (opts) => {
56860
57501
  const globalOpts = program2.opts();
57502
+ const sourceRoots = expandRepeatedCsvOption(opts.sourceRoot);
57503
+ const sourceStores = expandRepeatedCsvOption(opts.sourceStore);
57504
+ const include = expandRepeatedCsvOption(opts.include);
57505
+ const exclude = expandRepeatedCsvOption(opts.exclude);
57506
+ if (sourceRoots || sourceStores || include || exclude) {
57507
+ const { discoverTaskRouteSources: discoverTaskRouteSources2 } = await Promise.resolve().then(() => (init_task_route_sources(), exports_task_route_sources));
57508
+ const result = discoverTaskRouteSources2({
57509
+ sourceRoots,
57510
+ sourceStores,
57511
+ include,
57512
+ exclude,
57513
+ limit: parseInt(opts.limit, 10)
57514
+ });
57515
+ if (opts.json || globalOpts.json) {
57516
+ console.log(JSON.stringify(result));
57517
+ return;
57518
+ }
57519
+ if (result.candidates.length === 0) {
57520
+ console.log(chalk7.dim(" No source tasks ready to claim."));
57521
+ } else {
57522
+ console.log(chalk7.bold(`Ready source tasks (${result.candidates.length}):
57523
+ `));
57524
+ for (const candidate of result.candidates) {
57525
+ const source3 = candidate.source_repo_path ?? candidate.source_db_path;
57526
+ const pri = candidate.priority === "critical" ? chalk7.bgRed.white(" CRIT ") : candidate.priority === "high" ? chalk7.red("[high]") : candidate.priority === "medium" ? chalk7.yellow("[med]") : "";
57527
+ console.log(` ${chalk7.cyan(candidate.task_short_id || candidate.task_id.slice(0, 8))} ${candidate.title} ${pri}${chalk7.dim(` ${source3}`)}`);
57528
+ }
57529
+ }
57530
+ if (result.errors.length > 0) {
57531
+ console.log(chalk7.yellow(`
57532
+ ${result.errors.length} source error${result.errors.length === 1 ? "" : "s"} isolated; rerun with --json for details.`));
57533
+ }
57534
+ return;
57535
+ }
56861
57536
  const db = getDatabase();
56862
57537
  const { getBlockingDeps: getBlockingDeps2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
56863
57538
  const { isLockExpired: isLockExpired2 } = await Promise.resolve().then(() => (init_database(), exports_database));
@@ -58179,21 +58854,21 @@ __export(exports_mcp_hooks_commands, {
58179
58854
  });
58180
58855
  import chalk8 from "chalk";
58181
58856
  import { execSync as execSync3 } from "child_process";
58182
- import { existsSync as existsSync21, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
58183
- import { dirname as dirname12, join as join21 } from "path";
58857
+ import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
58858
+ import { dirname as dirname13, join as join22 } from "path";
58184
58859
  function getMcpBinaryPath() {
58185
58860
  try {
58186
58861
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
58187
58862
  if (p)
58188
58863
  return p;
58189
58864
  } catch {}
58190
- const bunBin = join21(HOME2, ".bun", "bin", "todos-mcp");
58191
- if (existsSync21(bunBin))
58865
+ const bunBin = join22(HOME2, ".bun", "bin", "todos-mcp");
58866
+ if (existsSync22(bunBin))
58192
58867
  return bunBin;
58193
58868
  return "todos-mcp";
58194
58869
  }
58195
58870
  function readJsonFile2(path) {
58196
- if (!existsSync21(path))
58871
+ if (!existsSync22(path))
58197
58872
  return {};
58198
58873
  try {
58199
58874
  return JSON.parse(readFileSync17(path, "utf-8"));
@@ -58202,20 +58877,20 @@ function readJsonFile2(path) {
58202
58877
  }
58203
58878
  }
58204
58879
  function writeJsonFile2(path, data) {
58205
- const dir = dirname12(path);
58206
- if (!existsSync21(dir))
58880
+ const dir = dirname13(path);
58881
+ if (!existsSync22(dir))
58207
58882
  mkdirSync11(dir, { recursive: true });
58208
58883
  writeFileSync10(path, JSON.stringify(data, null, 2) + `
58209
58884
  `);
58210
58885
  }
58211
58886
  function readTomlFile(path) {
58212
- if (!existsSync21(path))
58887
+ if (!existsSync22(path))
58213
58888
  return "";
58214
58889
  return readFileSync17(path, "utf-8");
58215
58890
  }
58216
58891
  function writeTomlFile(path, content) {
58217
- const dir = dirname12(path);
58218
- if (!existsSync21(dir))
58892
+ const dir = dirname13(path);
58893
+ if (!existsSync22(dir))
58219
58894
  mkdirSync11(dir, { recursive: true });
58220
58895
  writeFileSync10(path, content);
58221
58896
  }
@@ -58262,7 +58937,7 @@ function envOption(value) {
58262
58937
  }
58263
58938
  function registerClaude(binPath, global) {
58264
58939
  const scope = global ? "user" : "project";
58265
- const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath}`;
58940
+ const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath} --stdio`;
58266
58941
  try {
58267
58942
  execSync3(cmd, { stdio: "pipe" });
58268
58943
  console.log(chalk8.green(`Claude Code (${scope}): registered via 'claude mcp add'`));
@@ -58281,13 +58956,13 @@ function unregisterClaude(_global) {
58281
58956
  }
58282
58957
  }
58283
58958
  function registerCodex(binPath) {
58284
- const configPath = join21(HOME2, ".codex", "config.toml");
58959
+ const configPath = join22(HOME2, ".codex", "config.toml");
58285
58960
  let content = readTomlFile(configPath);
58286
58961
  content = removeTomlBlock(content, "mcp_servers.todos");
58287
58962
  const block = `
58288
58963
  [mcp_servers.todos]
58289
58964
  command = "${binPath}"
58290
- args = []
58965
+ args = ["--stdio"]
58291
58966
  `;
58292
58967
  content = content.trimEnd() + `
58293
58968
  ` + block;
@@ -58295,7 +58970,7 @@ args = []
58295
58970
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
58296
58971
  }
58297
58972
  function unregisterCodex() {
58298
- const configPath = join21(HOME2, ".codex", "config.toml");
58973
+ const configPath = join22(HOME2, ".codex", "config.toml");
58299
58974
  let content = readTomlFile(configPath);
58300
58975
  if (!content.includes("[mcp_servers.todos]")) {
58301
58976
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -58307,7 +58982,7 @@ function unregisterCodex() {
58307
58982
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
58308
58983
  }
58309
58984
  function registerGemini(binPath) {
58310
- const configPath = join21(HOME2, ".gemini", "settings.json");
58985
+ const configPath = join22(HOME2, ".gemini", "settings.json");
58311
58986
  const config = readJsonFile2(configPath);
58312
58987
  if (!config["mcpServers"]) {
58313
58988
  config["mcpServers"] = {};
@@ -58315,13 +58990,13 @@ function registerGemini(binPath) {
58315
58990
  const servers = config["mcpServers"];
58316
58991
  servers["todos"] = {
58317
58992
  command: binPath,
58318
- args: []
58993
+ args: ["--stdio"]
58319
58994
  };
58320
58995
  writeJsonFile2(configPath, config);
58321
58996
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
58322
58997
  }
58323
58998
  function unregisterGemini() {
58324
- const configPath = join21(HOME2, ".gemini", "settings.json");
58999
+ const configPath = join22(HOME2, ".gemini", "settings.json");
58325
59000
  const config = readJsonFile2(configPath);
58326
59001
  const servers = config["mcpServers"];
58327
59002
  if (!servers || !("todos" in servers)) {
@@ -58378,8 +59053,8 @@ function registerMcpHooksCommands(program2) {
58378
59053
  if (p)
58379
59054
  todosBin = p;
58380
59055
  } catch {}
58381
- const hooksDir = join21(process.cwd(), ".claude", "hooks");
58382
- if (!existsSync21(hooksDir))
59056
+ const hooksDir = join22(process.cwd(), ".claude", "hooks");
59057
+ if (!existsSync22(hooksDir))
58383
59058
  mkdirSync11(hooksDir, { recursive: true });
58384
59059
  const hookScript = `#!/usr/bin/env bash
58385
59060
  # Auto-generated by: todos hooks install
@@ -58404,11 +59079,11 @@ esac
58404
59079
 
58405
59080
  exit 0
58406
59081
  `;
58407
- const hookPath = join21(hooksDir, "todos-sync.sh");
59082
+ const hookPath = join22(hooksDir, "todos-sync.sh");
58408
59083
  writeFileSync10(hookPath, hookScript);
58409
59084
  execSync3(`chmod +x "${hookPath}"`);
58410
59085
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
58411
- const settingsPath = join21(process.cwd(), ".claude", "settings.json");
59086
+ const settingsPath = join22(process.cwd(), ".claude", "settings.json");
58412
59087
  const settings = readJsonFile2(settingsPath);
58413
59088
  if (!settings["hooks"]) {
58414
59089
  settings["hooks"] = {};
@@ -58446,7 +59121,11 @@ exit 0
58446
59121
  unregisterMcp(opts.unregister, opts.global);
58447
59122
  return;
58448
59123
  }
58449
- await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
59124
+ const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
59125
+ const { StdioServerTransport: StdioServerTransport2 } = await import("@modelcontextprotocol/sdk/server/stdio.js");
59126
+ const server = buildServer2();
59127
+ const transport = new StdioServerTransport2;
59128
+ await server.connect(transport);
58450
59129
  });
58451
59130
  program2.command("import <url>").description("Import a GitHub issue as a task").option("--project <id>", "Project ID").option("--list <id>", "Task list ID").action(async (url, opts) => {
58452
59131
  const globalOpts = program2.opts();
@@ -59277,7 +59956,7 @@ Artifacts:`));
59277
59956
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
59278
59957
  const hookPath = `${gitDir}/hooks/post-commit`;
59279
59958
  const marker = "# todos-auto-link";
59280
- if (existsSync21(hookPath)) {
59959
+ if (existsSync22(hookPath)) {
59281
59960
  const existing = readFileSync17(hookPath, "utf-8");
59282
59961
  if (existing.includes(marker)) {
59283
59962
  console.log(chalk8.yellow("Hook already installed."));
@@ -59305,7 +59984,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
59305
59984
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
59306
59985
  const hookPath = `${gitDir}/hooks/post-commit`;
59307
59986
  const marker = "# todos-auto-link";
59308
- if (!existsSync21(hookPath)) {
59987
+ if (!existsSync22(hookPath)) {
59309
59988
  console.log(chalk8.dim("No post-commit hook found."));
59310
59989
  return;
59311
59990
  }
@@ -59491,7 +60170,7 @@ import chalk10 from "chalk";
59491
60170
  import { execSync as execSync4 } from "child_process";
59492
60171
  import { readFileSync as readFileSync18, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
59493
60172
  import { tmpdir as tmpdir4 } from "os";
59494
- import { join as join22 } from "path";
60173
+ import { join as join23 } from "path";
59495
60174
  function getOrCreateLocalMachineName() {
59496
60175
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
59497
60176
  }
@@ -59529,7 +60208,7 @@ function remoteTempPath(sshAddress) {
59529
60208
  }
59530
60209
  function readRemoteBridgeBundle(sshAddress) {
59531
60210
  const remotePath = remoteTempPath(sshAddress);
59532
- const localPath = join22(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
60211
+ const localPath = join23(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
59533
60212
  try {
59534
60213
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
59535
60214
  scpFromRemote(sshAddress, remotePath, localPath);
@@ -59544,7 +60223,7 @@ function readRemoteBridgeBundle(sshAddress) {
59544
60223
  }
59545
60224
  }
59546
60225
  function writeLocalBridgeBundle() {
59547
- const localPath = join22(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
60226
+ const localPath = join23(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
59548
60227
  writeFileSync11(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
59549
60228
  return localPath;
59550
60229
  }
@@ -60608,7 +61287,7 @@ __export(exports_onboarding_commands, {
60608
61287
  registerOnboardingCommands: () => registerOnboardingCommands
60609
61288
  });
60610
61289
  import chalk17 from "chalk";
60611
- import { resolve as resolve21 } from "path";
61290
+ import { resolve as resolve22 } from "path";
60612
61291
  function registerOnboardingCommands(program2) {
60613
61292
  program2.command("onboarding").alias("demo-fixtures").description("List, show, write, or import bundled local onboarding fixtures").option("--show <name>", "Show one fixture bridge bundle as JSON").option("--write <dir>", "Write all bundled fixture bridge bundles to a directory").option("--import <name>", "Dry-run or apply an onboarding fixture import").option("--apply", "Apply an onboarding fixture import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge existing local tasks while preserving divergent fields").action(async (opts) => {
60614
61293
  const globalOpts = program2.opts();
@@ -60624,7 +61303,7 @@ function registerOnboardingCommands(program2) {
60624
61303
  return;
60625
61304
  }
60626
61305
  if (opts.write) {
60627
- const result = writeOnboardingFixtureFiles2(resolve21(opts.write));
61306
+ const result = writeOnboardingFixtureFiles2(resolve22(opts.write));
60628
61307
  if (globalOpts.json) {
60629
61308
  output(result, true);
60630
61309
  return;
@@ -60838,7 +61517,6 @@ var init_cli_mcp_parity = __esm(() => {
60838
61517
  "bulk_create_tasks",
60839
61518
  "get_next_task",
60840
61519
  "claim_next_task",
60841
- "get_active_work",
60842
61520
  "get_stale_tasks",
60843
61521
  "get_my_tasks",
60844
61522
  "get_blocked_tasks",
@@ -61870,11 +62548,8 @@ var init_cli_mcp_parity = __esm(() => {
61870
62548
  "delete_search_view",
61871
62549
  "get_status",
61872
62550
  "standup",
61873
- "get_task_stats",
61874
62551
  "get_context",
61875
- "task_context",
61876
- "get_task_graph",
61877
- "get_recent_activity"
62552
+ "task_context"
61878
62553
  ],
61879
62554
  jsonContracts: ["task", "saved_search_view", "saved_search_run_result", "status_summary", "audit_history", "structured_error", "api_error"],
61880
62555
  errorContracts: ["structured_error", "api_error"],
@@ -64073,7 +64748,7 @@ __export(exports_sdk_integration_fixtures, {
64073
64748
  TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT: () => TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT
64074
64749
  });
64075
64750
  import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
64076
- import { join as join23 } from "path";
64751
+ import { join as join24 } from "path";
64077
64752
  function source5(version) {
64078
64753
  return {
64079
64754
  packageName: "@hasna/todos",
@@ -64180,7 +64855,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
64180
64855
  ];
64181
64856
  const written = [];
64182
64857
  for (const [name, payload] of files) {
64183
- const file = join23(directory, name);
64858
+ const file = join24(directory, name);
64184
64859
  writeFileSync12(file, `${JSON.stringify(payload, null, 2)}
64185
64860
  `, "utf-8");
64186
64861
  written.push(file);
@@ -64204,7 +64879,7 @@ __export(exports_sdk_fixture_commands, {
64204
64879
  registerSdkFixtureCommands: () => registerSdkFixtureCommands
64205
64880
  });
64206
64881
  import chalk19 from "chalk";
64207
- import { resolve as resolve22 } from "path";
64882
+ import { resolve as resolve23 } from "path";
64208
64883
  function registerSdkFixtureCommands(program2) {
64209
64884
  program2.command("sdk-fixtures").description("List, show, or write local SDK integration fixtures").option("--show", "Print the full fixture pack JSON").option("--write <dir>", "Write fixture pack, bridge fixture, contract snapshots, and example index to a directory").action(async (opts) => {
64210
64885
  const globalOpts = program2.opts();
@@ -64215,7 +64890,7 @@ function registerSdkFixtureCommands(program2) {
64215
64890
  writeSdkIntegrationFixtures: writeSdkIntegrationFixtures2
64216
64891
  } = await Promise.resolve().then(() => (init_sdk_integration_fixtures(), exports_sdk_integration_fixtures));
64217
64892
  if (opts.write) {
64218
- const result = writeSdkIntegrationFixtures2(resolve22(opts.write));
64893
+ const result = writeSdkIntegrationFixtures2(resolve23(opts.write));
64219
64894
  if (globalOpts.json) {
64220
64895
  console.log(JSON.stringify(result));
64221
64896
  return;
@@ -65037,7 +65712,7 @@ __export(exports_local_backup_commands, {
65037
65712
  registerLocalBackupCommands: () => registerLocalBackupCommands
65038
65713
  });
65039
65714
  import chalk26 from "chalk";
65040
- import { resolve as resolve23 } from "path";
65715
+ import { resolve as resolve24 } from "path";
65041
65716
  function globalOptions6(program2) {
65042
65717
  const command = program2;
65043
65718
  return command.optsWithGlobals?.() ?? program2.opts();
@@ -65059,10 +65734,10 @@ function registerLocalBackupCommands(program2) {
65059
65734
  const projectId = opts.projectId ?? autoProject(globalOpts);
65060
65735
  const backupBundle = createLocalBackup2({
65061
65736
  project_id: projectId,
65062
- output_path: opts.output ? resolve23(opts.output) : undefined
65737
+ output_path: opts.output ? resolve24(opts.output) : undefined
65063
65738
  });
65064
65739
  const result = {
65065
- output_path: opts.output ? resolve23(opts.output) : null,
65740
+ output_path: opts.output ? resolve24(opts.output) : null,
65066
65741
  backup: backupBundle
65067
65742
  };
65068
65743
  if (opts.json || globalOpts.json) {
@@ -65387,7 +66062,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
65387
66062
  const placeholders2 = presentColumns.map(() => "?").join(", ");
65388
66063
  const values = presentColumns.map((column) => valueForColumn(column, row[column]));
65389
66064
  const updateColumns = presentColumns.filter((column) => column !== "id");
65390
- const updateSet = updateColumns.map((column) => `${column} = excluded.${column}`).join(", ");
66065
+ const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
65391
66066
  const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
65392
66067
  const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})
65393
66068
  ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})`;
@@ -65831,7 +66506,9 @@ class PostgresTodosSyncStore {
65831
66506
  deleted_at = EXCLUDED.deleted_at,
65832
66507
  source_machine_id = EXCLUDED.source_machine_id,
65833
66508
  version = EXCLUDED.version
65834
- WHERE ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
66509
+ WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
66510
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
66511
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
65835
66512
  this.service,
65836
66513
  entry.type,
65837
66514
  entry.id,
@@ -66184,7 +66861,7 @@ class PostgresJsonRecordStore {
66184
66861
  async upsert(type, value, context = {}) {
66185
66862
  await this.ensureSchema();
66186
66863
  const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
66187
- await this.options.client.query(`INSERT INTO ${this.tableName} (
66864
+ const result = await this.options.client.query(`INSERT INTO ${this.tableName} (
66188
66865
  service, object_type, object_id, payload, updated_at,
66189
66866
  deleted_at, source_machine_id, version
66190
66867
  ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, $7)
@@ -66194,7 +66871,11 @@ class PostgresJsonRecordStore {
66194
66871
  deleted_at = NULL,
66195
66872
  source_machine_id = EXCLUDED.source_machine_id,
66196
66873
  version = EXCLUDED.version
66197
- WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
66874
+ WHERE ${this.tableName}.updated_at IS NULL
66875
+ OR ${this.tableName}.updated_at < EXCLUDED.updated_at
66876
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
66877
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
66878
+ RETURNING object_id`, [
66198
66879
  this.service,
66199
66880
  type,
66200
66881
  value.id,
@@ -66203,8 +66884,27 @@ class PostgresJsonRecordStore {
66203
66884
  context.requestId ?? this.sourceMachineId ?? null,
66204
66885
  numberValue3(value.version)
66205
66886
  ]);
66887
+ if (result.rows.length === 0) {
66888
+ const current = await this.get(type, value.id);
66889
+ if (current)
66890
+ return current;
66891
+ }
66206
66892
  return value;
66207
66893
  }
66894
+ async incrementProjectTaskCounter(projectId, _context = {}) {
66895
+ await this.ensureSchema();
66896
+ const result = await this.options.client.query(`UPDATE ${this.tableName}
66897
+ SET payload = jsonb_set(payload, '{task_counter}',
66898
+ to_jsonb(COALESCE((payload->>'task_counter')::bigint, 0) + 1)),
66899
+ updated_at = $3::timestamptz,
66900
+ version = COALESCE(version, 0) + 1
66901
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL
66902
+ RETURNING payload->>'task_counter' AS counter`, [this.service, projectId, new Date().toISOString()]);
66903
+ const row = result.rows[0];
66904
+ if (!row || row.counter === null || row.counter === undefined)
66905
+ return null;
66906
+ return Number(row.counter);
66907
+ }
66208
66908
  async delete(type, id, context = {}) {
66209
66909
  await this.ensureSchema();
66210
66910
  const existing = await this.get(type, id);
@@ -66360,6 +67060,9 @@ async function updateTask2(id, input, store) {
66360
67060
  }
66361
67061
  async function startTask2(id, agentId, store) {
66362
67062
  const task2 = await requireRecord("tasks", id, store);
67063
+ if (task2.status !== "pending" && task2.status !== "in_progress") {
67064
+ throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
67065
+ }
66363
67066
  return patchTask(task2, {
66364
67067
  status: "in_progress",
66365
67068
  assigned_to: task2.assigned_to ?? agentId,
@@ -66429,8 +67132,20 @@ async function getNextTask2(filters, store) {
66429
67132
  return (await listTasks3({ ...filters, status: "pending", limit: 1 }, store))[0] ?? null;
66430
67133
  }
66431
67134
  async function claimNextTask2(agentId, filters, store) {
66432
- const task2 = await getNextTask2(filters, store);
66433
- return task2 ? startTask2(task2.id, agentId, store) : null;
67135
+ const MAX_ATTEMPTS = 25;
67136
+ const tried = new Set;
67137
+ for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
67138
+ const task2 = await getNextTask2(filters, store);
67139
+ if (!task2)
67140
+ return null;
67141
+ if (tried.has(task2.id))
67142
+ return null;
67143
+ tried.add(task2.id);
67144
+ try {
67145
+ return await startTask2(task2.id, agentId, store);
67146
+ } catch {}
67147
+ }
67148
+ return null;
66434
67149
  }
66435
67150
  async function getActiveWork2(filters, store) {
66436
67151
  const tasks = await listTasks3({ ...filters, status: "in_progress" }, store);
@@ -66703,12 +67418,9 @@ async function nextTaskShortId2(projectId, store, context) {
66703
67418
  const project = await store.get("projects", projectId);
66704
67419
  if (!project?.task_prefix)
66705
67420
  return null;
66706
- const counter = project.task_counter + 1;
66707
- await store.upsert("projects", {
66708
- ...project,
66709
- task_counter: counter,
66710
- updated_at: new Date().toISOString()
66711
- }, context);
67421
+ const counter = await store.incrementProjectTaskCounter(projectId, context);
67422
+ if (counter === null)
67423
+ return null;
66712
67424
  return `${project.task_prefix}-${String(counter).padStart(5, "0")}`;
66713
67425
  }
66714
67426
  async function generateProjectPrefix(name, store) {
@@ -66866,7 +67578,7 @@ var init_factory = __esm(() => {
66866
67578
  });
66867
67579
 
66868
67580
  // src/storage/s3-artifacts.ts
66869
- import { createHash as createHash14, createHmac as createHmac2 } from "crypto";
67581
+ import { createHash as createHash15, createHmac as createHmac2 } from "crypto";
66870
67582
  function createTodosS3ArtifactStore(options) {
66871
67583
  const requestFetch = options.fetch ?? fetch;
66872
67584
  const now4 = options.now ?? (() => new Date);
@@ -67038,7 +67750,7 @@ function toAmzDate(date) {
67038
67750
  return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
67039
67751
  }
67040
67752
  function sha256Hex(value) {
67041
- return createHash14("sha256").update(value).digest("hex");
67753
+ return createHash15("sha256").update(value).digest("hex");
67042
67754
  }
67043
67755
  function hmac(key, value) {
67044
67756
  return createHmac2("sha256", key).update(value).digest();