@hasna/todos 0.9.39 → 0.9.40

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.
@@ -29,7 +29,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
29
  var __require = import.meta.require;
30
30
 
31
31
  // src/types/index.ts
32
- var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, CompletionGuardError;
32
+ var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, DependencyCycleError, CompletionGuardError;
33
33
  var init_types = __esm(() => {
34
34
  VersionConflictError = class VersionConflictError extends Error {
35
35
  taskId;
@@ -87,6 +87,18 @@ var init_types = __esm(() => {
87
87
  this.name = "LockError";
88
88
  }
89
89
  };
90
+ DependencyCycleError = class DependencyCycleError extends Error {
91
+ taskId;
92
+ dependsOn;
93
+ static code = "DEPENDENCY_CYCLE";
94
+ static suggestion = "Check the dependency chain with get_task to avoid circular references.";
95
+ constructor(taskId, dependsOn) {
96
+ super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
97
+ this.taskId = taskId;
98
+ this.dependsOn = dependsOn;
99
+ this.name = "DependencyCycleError";
100
+ }
101
+ };
90
102
  CompletionGuardError = class CompletionGuardError extends Error {
91
103
  reason;
92
104
  retryAfterSeconds;
@@ -715,6 +727,107 @@ var init_projects = __esm(() => {
715
727
  init_database();
716
728
  });
717
729
 
730
+ // src/lib/sync-utils.ts
731
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
732
+ function readJsonFile(path) {
733
+ try {
734
+ return JSON.parse(readFileSync(path, "utf-8"));
735
+ } catch {
736
+ return null;
737
+ }
738
+ }
739
+ var HOME;
740
+ var init_sync_utils = __esm(() => {
741
+ HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
742
+ });
743
+
744
+ // src/lib/config.ts
745
+ import { existsSync as existsSync3 } from "fs";
746
+ import { join as join2 } from "path";
747
+ function loadConfig() {
748
+ if (cached)
749
+ return cached;
750
+ if (!existsSync3(CONFIG_PATH)) {
751
+ cached = {};
752
+ return cached;
753
+ }
754
+ const config = readJsonFile(CONFIG_PATH) || {};
755
+ if (typeof config.sync_agents === "string") {
756
+ config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
757
+ }
758
+ cached = config;
759
+ return cached;
760
+ }
761
+ function getCompletionGuardConfig(projectPath) {
762
+ const config = loadConfig();
763
+ const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
764
+ if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
765
+ return { ...global, ...config.project_overrides[projectPath].completion_guard };
766
+ }
767
+ return global;
768
+ }
769
+ var CONFIG_PATH, cached = null, GUARD_DEFAULTS;
770
+ var init_config = __esm(() => {
771
+ init_sync_utils();
772
+ CONFIG_PATH = join2(HOME, ".todos", "config.json");
773
+ GUARD_DEFAULTS = {
774
+ enabled: false,
775
+ min_work_seconds: 30,
776
+ max_completions_per_window: 5,
777
+ window_minutes: 10,
778
+ cooldown_seconds: 60
779
+ };
780
+ });
781
+
782
+ // src/lib/completion-guard.ts
783
+ function checkCompletionGuard(task, agentId, db, configOverride) {
784
+ let config;
785
+ if (configOverride) {
786
+ config = configOverride;
787
+ } else {
788
+ const projectPath = task.project_id ? getProject(task.project_id, db)?.path : null;
789
+ config = getCompletionGuardConfig(projectPath);
790
+ }
791
+ if (!config.enabled)
792
+ return;
793
+ if (task.status !== "in_progress") {
794
+ throw new CompletionGuardError(`Task must be in 'in_progress' status before completing (current: '${task.status}'). Use start_task first.`);
795
+ }
796
+ const agent = agentId || task.assigned_to || task.agent_id;
797
+ if (config.min_work_seconds && task.locked_at) {
798
+ const startedAt = new Date(task.locked_at).getTime();
799
+ const elapsedSeconds = (Date.now() - startedAt) / 1000;
800
+ if (elapsedSeconds < config.min_work_seconds) {
801
+ const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
802
+ throw new CompletionGuardError(`Too fast: task was started ${Math.floor(elapsedSeconds)}s ago. Minimum work duration is ${config.min_work_seconds}s. Wait ${remaining}s.`, remaining);
803
+ }
804
+ }
805
+ if (agent && config.max_completions_per_window && config.window_minutes) {
806
+ const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
807
+ const result = db.query(`SELECT COUNT(*) as count FROM tasks
808
+ WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
809
+ if (result.count >= config.max_completions_per_window) {
810
+ throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
811
+ }
812
+ }
813
+ if (agent && config.cooldown_seconds) {
814
+ const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
815
+ WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
816
+ if (result.last_completed) {
817
+ const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
818
+ if (elapsedSeconds < config.cooldown_seconds) {
819
+ const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
820
+ throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
821
+ }
822
+ }
823
+ }
824
+ }
825
+ var init_completion_guard = __esm(() => {
826
+ init_types();
827
+ init_config();
828
+ init_projects();
829
+ });
830
+
718
831
  // src/db/audit.ts
719
832
  var exports_audit = {};
720
833
  __export(exports_audit, {
@@ -742,6 +855,96 @@ var init_audit = __esm(() => {
742
855
  init_database();
743
856
  });
744
857
 
858
+ // src/lib/recurrence.ts
859
+ function parseRecurrenceRule(rule) {
860
+ const normalized = rule.trim().toLowerCase();
861
+ if (normalized === "every weekday" || normalized === "every weekdays") {
862
+ return { type: "specific_days", days: [1, 2, 3, 4, 5] };
863
+ }
864
+ if (normalized === "every day" || normalized === "daily") {
865
+ return { type: "interval", interval: 1, unit: "day" };
866
+ }
867
+ if (normalized === "every week" || normalized === "weekly") {
868
+ return { type: "interval", interval: 1, unit: "week" };
869
+ }
870
+ if (normalized === "every month" || normalized === "monthly") {
871
+ return { type: "interval", interval: 1, unit: "month" };
872
+ }
873
+ const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
874
+ if (intervalMatch) {
875
+ return {
876
+ type: "interval",
877
+ interval: parseInt(intervalMatch[1], 10),
878
+ unit: intervalMatch[2]
879
+ };
880
+ }
881
+ const daysMatch = normalized.match(/^every\s+(.+)$/);
882
+ if (daysMatch) {
883
+ const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
884
+ const days = [];
885
+ for (const part of dayParts) {
886
+ const dayNum = DAY_NAMES[part];
887
+ if (dayNum !== undefined) {
888
+ days.push(dayNum);
889
+ }
890
+ }
891
+ if (days.length > 0) {
892
+ return { type: "specific_days", days: days.sort((a, b) => a - b) };
893
+ }
894
+ }
895
+ 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"`);
896
+ }
897
+ function nextOccurrence(rule, from) {
898
+ const parsed = parseRecurrenceRule(rule);
899
+ const base = from || new Date;
900
+ if (parsed.type === "interval") {
901
+ const next = new Date(base);
902
+ if (parsed.unit === "day") {
903
+ next.setDate(next.getDate() + parsed.interval);
904
+ } else if (parsed.unit === "week") {
905
+ next.setDate(next.getDate() + parsed.interval * 7);
906
+ } else if (parsed.unit === "month") {
907
+ next.setMonth(next.getMonth() + parsed.interval);
908
+ }
909
+ return next.toISOString();
910
+ }
911
+ if (parsed.type === "specific_days") {
912
+ const currentDay = base.getDay();
913
+ const days = parsed.days;
914
+ let daysToAdd = Infinity;
915
+ for (const day of days) {
916
+ let diff = day - currentDay;
917
+ if (diff <= 0)
918
+ diff += 7;
919
+ if (diff < daysToAdd)
920
+ daysToAdd = diff;
921
+ }
922
+ const next = new Date(base);
923
+ next.setDate(next.getDate() + daysToAdd);
924
+ return next.toISOString();
925
+ }
926
+ throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
927
+ }
928
+ var DAY_NAMES;
929
+ var init_recurrence = __esm(() => {
930
+ DAY_NAMES = {
931
+ sunday: 0,
932
+ sun: 0,
933
+ monday: 1,
934
+ mon: 1,
935
+ tuesday: 2,
936
+ tue: 2,
937
+ wednesday: 3,
938
+ wed: 3,
939
+ thursday: 4,
940
+ thu: 4,
941
+ friday: 5,
942
+ fri: 5,
943
+ saturday: 6,
944
+ sat: 6
945
+ };
946
+ });
947
+
745
948
  // src/db/webhooks.ts
746
949
  var exports_webhooks = {};
747
950
  __export(exports_webhooks, {
@@ -793,847 +996,1215 @@ var init_webhooks = __esm(() => {
793
996
  init_database();
794
997
  });
795
998
 
796
- // src/db/agents.ts
797
- var exports_agents = {};
798
- __export(exports_agents, {
799
- updateAgentActivity: () => updateAgentActivity,
800
- updateAgent: () => updateAgent,
801
- registerAgent: () => registerAgent,
802
- listAgents: () => listAgents,
803
- getOrgChart: () => getOrgChart,
804
- getDirectReports: () => getDirectReports,
805
- getAgentByName: () => getAgentByName,
806
- getAgent: () => getAgent,
807
- deleteAgent: () => deleteAgent
999
+ // src/db/tasks.ts
1000
+ var exports_tasks = {};
1001
+ __export(exports_tasks, {
1002
+ updateTask: () => updateTask,
1003
+ unlockTask: () => unlockTask,
1004
+ startTask: () => startTask,
1005
+ removeDependency: () => removeDependency,
1006
+ moveTask: () => moveTask,
1007
+ lockTask: () => lockTask,
1008
+ listTasks: () => listTasks,
1009
+ getTasksChangedSince: () => getTasksChangedSince,
1010
+ getTaskWithRelations: () => getTaskWithRelations,
1011
+ getTaskStats: () => getTaskStats,
1012
+ getTaskGraph: () => getTaskGraph,
1013
+ getTaskDependents: () => getTaskDependents,
1014
+ getTaskDependencies: () => getTaskDependencies,
1015
+ getTask: () => getTask,
1016
+ getStatus: () => getStatus,
1017
+ getStaleTasks: () => getStaleTasks,
1018
+ getNextTask: () => getNextTask,
1019
+ getBlockingDeps: () => getBlockingDeps,
1020
+ getActiveWork: () => getActiveWork,
1021
+ failTask: () => failTask,
1022
+ deleteTask: () => deleteTask,
1023
+ createTask: () => createTask,
1024
+ countTasks: () => countTasks,
1025
+ completeTask: () => completeTask,
1026
+ cloneTask: () => cloneTask,
1027
+ claimNextTask: () => claimNextTask,
1028
+ bulkUpdateTasks: () => bulkUpdateTasks,
1029
+ bulkCreateTasks: () => bulkCreateTasks,
1030
+ addDependency: () => addDependency
808
1031
  });
809
- function shortUuid() {
810
- return crypto.randomUUID().slice(0, 8);
811
- }
812
- function rowToAgent(row) {
1032
+ function rowToTask(row) {
813
1033
  return {
814
1034
  ...row,
815
- permissions: JSON.parse(row.permissions || '["*"]'),
816
- metadata: JSON.parse(row.metadata || "{}")
1035
+ tags: JSON.parse(row.tags || "[]"),
1036
+ metadata: JSON.parse(row.metadata || "{}"),
1037
+ status: row.status,
1038
+ priority: row.priority,
1039
+ requires_approval: !!row.requires_approval
817
1040
  };
818
1041
  }
819
- function registerAgent(input, db) {
820
- const d = db || getDatabase();
821
- const normalizedName = input.name.trim().toLowerCase();
822
- const existing = getAgentByName(normalizedName, d);
823
- if (existing) {
824
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
825
- return getAgent(existing.id, d);
1042
+ function insertTaskTags(taskId, tags, db) {
1043
+ if (tags.length === 0)
1044
+ return;
1045
+ const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
1046
+ for (const tag of tags) {
1047
+ if (tag)
1048
+ stmt.run(taskId, tag);
826
1049
  }
827
- const id = shortUuid();
1050
+ }
1051
+ function replaceTaskTags(taskId, tags, db) {
1052
+ db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
1053
+ insertTaskTags(taskId, tags, db);
1054
+ }
1055
+ function createTask(input, db) {
1056
+ const d = db || getDatabase();
1057
+ const id = uuid();
828
1058
  const timestamp = now();
829
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, org_id, metadata, created_at, last_seen_at)
830
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1059
+ const tags = input.tags || [];
1060
+ const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
1061
+ const title = shortId ? `${shortId}: ${input.title}` : input.title;
1062
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id)
1063
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
831
1064
  id,
832
- normalizedName,
1065
+ shortId,
1066
+ input.project_id || null,
1067
+ input.parent_id || null,
1068
+ input.plan_id || null,
1069
+ input.task_list_id || null,
1070
+ title,
833
1071
  input.description || null,
834
- input.role || "agent",
835
- input.title || null,
836
- input.level || null,
837
- JSON.stringify(input.permissions || ["*"]),
838
- input.reports_to || null,
839
- input.org_id || null,
1072
+ input.status || "pending",
1073
+ input.priority || "medium",
1074
+ input.agent_id || null,
1075
+ input.assigned_to || null,
1076
+ input.session_id || null,
1077
+ input.working_dir || null,
1078
+ JSON.stringify(tags),
840
1079
  JSON.stringify(input.metadata || {}),
841
1080
  timestamp,
842
- timestamp
1081
+ timestamp,
1082
+ input.due_at || null,
1083
+ input.estimated_minutes || null,
1084
+ input.requires_approval ? 1 : 0,
1085
+ null,
1086
+ null,
1087
+ input.recurrence_rule || null,
1088
+ input.recurrence_parent_id || null
843
1089
  ]);
844
- return getAgent(id, d);
1090
+ if (tags.length > 0) {
1091
+ insertTaskTags(id, tags, d);
1092
+ }
1093
+ const task = getTask(id, d);
1094
+ dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
1095
+ return task;
845
1096
  }
846
- function getAgent(id, db) {
1097
+ function getTask(id, db) {
847
1098
  const d = db || getDatabase();
848
- const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
849
- return row ? rowToAgent(row) : null;
1099
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
1100
+ if (!row)
1101
+ return null;
1102
+ return rowToTask(row);
850
1103
  }
851
- function getAgentByName(name, db) {
1104
+ function getTaskWithRelations(id, db) {
852
1105
  const d = db || getDatabase();
853
- const normalizedName = name.trim().toLowerCase();
854
- const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
855
- return row ? rowToAgent(row) : null;
1106
+ const task = getTask(id, d);
1107
+ if (!task)
1108
+ return null;
1109
+ const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
1110
+ const subtasks = subtaskRows.map(rowToTask);
1111
+ const depRows = d.query(`SELECT t.* FROM tasks t
1112
+ JOIN task_dependencies td ON td.depends_on = t.id
1113
+ WHERE td.task_id = ?`).all(id);
1114
+ const dependencies = depRows.map(rowToTask);
1115
+ const blockedByRows = d.query(`SELECT t.* FROM tasks t
1116
+ JOIN task_dependencies td ON td.task_id = t.id
1117
+ WHERE td.depends_on = ?`).all(id);
1118
+ const blocked_by = blockedByRows.map(rowToTask);
1119
+ const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
1120
+ const parent = task.parent_id ? getTask(task.parent_id, d) : null;
1121
+ return {
1122
+ ...task,
1123
+ subtasks,
1124
+ dependencies,
1125
+ blocked_by,
1126
+ comments,
1127
+ parent
1128
+ };
856
1129
  }
857
- function listAgents(db) {
1130
+ function listTasks(filter = {}, db) {
858
1131
  const d = db || getDatabase();
859
- return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
1132
+ clearExpiredLocks(d);
1133
+ const conditions = [];
1134
+ const params = [];
1135
+ if (filter.project_id) {
1136
+ conditions.push("project_id = ?");
1137
+ params.push(filter.project_id);
1138
+ }
1139
+ if (filter.parent_id !== undefined) {
1140
+ if (filter.parent_id === null) {
1141
+ conditions.push("parent_id IS NULL");
1142
+ } else {
1143
+ conditions.push("parent_id = ?");
1144
+ params.push(filter.parent_id);
1145
+ }
1146
+ }
1147
+ if (filter.status) {
1148
+ if (Array.isArray(filter.status)) {
1149
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1150
+ params.push(...filter.status);
1151
+ } else {
1152
+ conditions.push("status = ?");
1153
+ params.push(filter.status);
1154
+ }
1155
+ }
1156
+ if (filter.priority) {
1157
+ if (Array.isArray(filter.priority)) {
1158
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
1159
+ params.push(...filter.priority);
1160
+ } else {
1161
+ conditions.push("priority = ?");
1162
+ params.push(filter.priority);
1163
+ }
1164
+ }
1165
+ if (filter.assigned_to) {
1166
+ conditions.push("assigned_to = ?");
1167
+ params.push(filter.assigned_to);
1168
+ }
1169
+ if (filter.agent_id) {
1170
+ conditions.push("agent_id = ?");
1171
+ params.push(filter.agent_id);
1172
+ }
1173
+ if (filter.session_id) {
1174
+ conditions.push("session_id = ?");
1175
+ params.push(filter.session_id);
1176
+ }
1177
+ if (filter.tags && filter.tags.length > 0) {
1178
+ const placeholders = filter.tags.map(() => "?").join(",");
1179
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1180
+ params.push(...filter.tags);
1181
+ }
1182
+ if (filter.plan_id) {
1183
+ conditions.push("plan_id = ?");
1184
+ params.push(filter.plan_id);
1185
+ }
1186
+ if (filter.task_list_id) {
1187
+ conditions.push("task_list_id = ?");
1188
+ params.push(filter.task_list_id);
1189
+ }
1190
+ if (filter.has_recurrence === true) {
1191
+ conditions.push("recurrence_rule IS NOT NULL");
1192
+ } else if (filter.has_recurrence === false) {
1193
+ conditions.push("recurrence_rule IS NULL");
1194
+ }
1195
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1196
+ let limitClause = "";
1197
+ if (filter.limit) {
1198
+ limitClause = " LIMIT ?";
1199
+ params.push(filter.limit);
1200
+ if (filter.offset) {
1201
+ limitClause += " OFFSET ?";
1202
+ params.push(filter.offset);
1203
+ }
1204
+ }
1205
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
1206
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
1207
+ created_at DESC${limitClause}`).all(...params);
1208
+ return rows.map(rowToTask);
860
1209
  }
861
- function updateAgentActivity(id, db) {
1210
+ function countTasks(filter = {}, db) {
862
1211
  const d = db || getDatabase();
863
- d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
1212
+ const conditions = [];
1213
+ const params = [];
1214
+ if (filter.project_id) {
1215
+ conditions.push("project_id = ?");
1216
+ params.push(filter.project_id);
1217
+ }
1218
+ if (filter.parent_id !== undefined) {
1219
+ if (filter.parent_id === null) {
1220
+ conditions.push("parent_id IS NULL");
1221
+ } else {
1222
+ conditions.push("parent_id = ?");
1223
+ params.push(filter.parent_id);
1224
+ }
1225
+ }
1226
+ if (filter.status) {
1227
+ if (Array.isArray(filter.status)) {
1228
+ conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1229
+ params.push(...filter.status);
1230
+ } else {
1231
+ conditions.push("status = ?");
1232
+ params.push(filter.status);
1233
+ }
1234
+ }
1235
+ if (filter.priority) {
1236
+ if (Array.isArray(filter.priority)) {
1237
+ conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
1238
+ params.push(...filter.priority);
1239
+ } else {
1240
+ conditions.push("priority = ?");
1241
+ params.push(filter.priority);
1242
+ }
1243
+ }
1244
+ if (filter.assigned_to) {
1245
+ conditions.push("assigned_to = ?");
1246
+ params.push(filter.assigned_to);
1247
+ }
1248
+ if (filter.agent_id) {
1249
+ conditions.push("agent_id = ?");
1250
+ params.push(filter.agent_id);
1251
+ }
1252
+ if (filter.session_id) {
1253
+ conditions.push("session_id = ?");
1254
+ params.push(filter.session_id);
1255
+ }
1256
+ if (filter.tags && filter.tags.length > 0) {
1257
+ const placeholders = filter.tags.map(() => "?").join(",");
1258
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1259
+ params.push(...filter.tags);
1260
+ }
1261
+ if (filter.plan_id) {
1262
+ conditions.push("plan_id = ?");
1263
+ params.push(filter.plan_id);
1264
+ }
1265
+ if (filter.task_list_id) {
1266
+ conditions.push("task_list_id = ?");
1267
+ params.push(filter.task_list_id);
1268
+ }
1269
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1270
+ const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
1271
+ return row.count;
864
1272
  }
865
- function updateAgent(id, input, db) {
1273
+ function updateTask(id, input, db) {
866
1274
  const d = db || getDatabase();
867
- const agent = getAgent(id, d);
868
- if (!agent)
869
- throw new Error(`Agent not found: ${id}`);
870
- const sets = ["last_seen_at = ?"];
1275
+ const task = getTask(id, d);
1276
+ if (!task)
1277
+ throw new TaskNotFoundError(id);
1278
+ if (task.version !== input.version) {
1279
+ throw new VersionConflictError(id, input.version, task.version);
1280
+ }
1281
+ const sets = ["version = version + 1", "updated_at = ?"];
871
1282
  const params = [now()];
872
- if (input.name !== undefined) {
873
- sets.push("name = ?");
874
- params.push(input.name.trim().toLowerCase());
1283
+ if (input.title !== undefined) {
1284
+ sets.push("title = ?");
1285
+ params.push(input.title);
875
1286
  }
876
1287
  if (input.description !== undefined) {
877
1288
  sets.push("description = ?");
878
1289
  params.push(input.description);
879
1290
  }
880
- if (input.role !== undefined) {
881
- sets.push("role = ?");
882
- params.push(input.role);
883
- }
884
- if (input.permissions !== undefined) {
885
- sets.push("permissions = ?");
886
- params.push(JSON.stringify(input.permissions));
887
- }
888
- if (input.title !== undefined) {
889
- sets.push("title = ?");
890
- params.push(input.title);
1291
+ if (input.status !== undefined) {
1292
+ if (input.status === "completed") {
1293
+ checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
1294
+ }
1295
+ sets.push("status = ?");
1296
+ params.push(input.status);
1297
+ if (input.status === "completed") {
1298
+ sets.push("completed_at = ?");
1299
+ params.push(now());
1300
+ }
891
1301
  }
892
- if (input.level !== undefined) {
893
- sets.push("level = ?");
894
- params.push(input.level);
1302
+ if (input.priority !== undefined) {
1303
+ sets.push("priority = ?");
1304
+ params.push(input.priority);
895
1305
  }
896
- if (input.reports_to !== undefined) {
897
- sets.push("reports_to = ?");
898
- params.push(input.reports_to);
1306
+ if (input.assigned_to !== undefined) {
1307
+ sets.push("assigned_to = ?");
1308
+ params.push(input.assigned_to);
899
1309
  }
900
- if (input.org_id !== undefined) {
901
- sets.push("org_id = ?");
902
- params.push(input.org_id);
1310
+ if (input.tags !== undefined) {
1311
+ sets.push("tags = ?");
1312
+ params.push(JSON.stringify(input.tags));
903
1313
  }
904
1314
  if (input.metadata !== undefined) {
905
1315
  sets.push("metadata = ?");
906
1316
  params.push(JSON.stringify(input.metadata));
907
1317
  }
908
- params.push(id);
909
- d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
910
- return getAgent(id, d);
911
- }
912
- function deleteAgent(id, db) {
913
- const d = db || getDatabase();
914
- return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
915
- }
916
- function getDirectReports(agentId, db) {
917
- const d = db || getDatabase();
918
- return d.query("SELECT * FROM agents WHERE reports_to = ? ORDER BY name").all(agentId).map(rowToAgent);
919
- }
920
- function getOrgChart(db) {
921
- const agents = listAgents(db);
922
- const byManager = new Map;
923
- for (const a of agents) {
924
- const key = a.reports_to;
925
- if (!byManager.has(key))
926
- byManager.set(key, []);
927
- byManager.get(key).push(a);
1318
+ if (input.plan_id !== undefined) {
1319
+ sets.push("plan_id = ?");
1320
+ params.push(input.plan_id);
928
1321
  }
929
- function buildTree(parentId) {
930
- const children = byManager.get(parentId) || [];
931
- return children.map((a) => ({ agent: a, reports: buildTree(a.id) }));
1322
+ if (input.task_list_id !== undefined) {
1323
+ sets.push("task_list_id = ?");
1324
+ params.push(input.task_list_id);
932
1325
  }
933
- return buildTree(null);
934
- }
935
- var init_agents = __esm(() => {
936
- init_database();
937
- });
938
-
939
- // src/db/orgs.ts
940
- var exports_orgs = {};
941
- __export(exports_orgs, {
942
- updateOrg: () => updateOrg,
943
- listOrgs: () => listOrgs,
944
- getOrgByName: () => getOrgByName,
945
- getOrg: () => getOrg,
946
- deleteOrg: () => deleteOrg,
947
- createOrg: () => createOrg
948
- });
949
- function rowToOrg(row) {
950
- return { ...row, metadata: JSON.parse(row.metadata || "{}") };
951
- }
952
- function createOrg(input, db) {
953
- const d = db || getDatabase();
954
- const id = uuid();
955
- const timestamp = now();
956
- d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
957
- return getOrg(id, d);
958
- }
959
- function getOrg(id, db) {
960
- const d = db || getDatabase();
961
- const row = d.query("SELECT * FROM orgs WHERE id = ?").get(id);
962
- return row ? rowToOrg(row) : null;
963
- }
964
- function getOrgByName(name, db) {
965
- const d = db || getDatabase();
966
- const row = d.query("SELECT * FROM orgs WHERE name = ?").get(name);
967
- return row ? rowToOrg(row) : null;
968
- }
969
- function listOrgs(db) {
970
- const d = db || getDatabase();
971
- return d.query("SELECT * FROM orgs ORDER BY name").all().map(rowToOrg);
972
- }
973
- function updateOrg(id, input, db) {
974
- const d = db || getDatabase();
975
- const org = getOrg(id, d);
976
- if (!org)
977
- throw new Error(`Org not found: ${id}`);
978
- const sets = ["updated_at = ?"];
979
- const params = [now()];
980
- if (input.name !== undefined) {
981
- sets.push("name = ?");
982
- params.push(input.name);
1326
+ if (input.due_at !== undefined) {
1327
+ sets.push("due_at = ?");
1328
+ params.push(input.due_at);
983
1329
  }
984
- if (input.description !== undefined) {
985
- sets.push("description = ?");
986
- params.push(input.description);
1330
+ if (input.estimated_minutes !== undefined) {
1331
+ sets.push("estimated_minutes = ?");
1332
+ params.push(input.estimated_minutes);
987
1333
  }
988
- if (input.metadata !== undefined) {
989
- sets.push("metadata = ?");
990
- params.push(JSON.stringify(input.metadata));
1334
+ if (input.requires_approval !== undefined) {
1335
+ sets.push("requires_approval = ?");
1336
+ params.push(input.requires_approval ? 1 : 0);
1337
+ }
1338
+ if (input.approved_by !== undefined) {
1339
+ sets.push("approved_by = ?");
1340
+ params.push(input.approved_by);
1341
+ sets.push("approved_at = ?");
1342
+ params.push(now());
1343
+ }
1344
+ if (input.recurrence_rule !== undefined) {
1345
+ sets.push("recurrence_rule = ?");
1346
+ params.push(input.recurrence_rule);
1347
+ }
1348
+ params.push(id, input.version);
1349
+ const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
1350
+ if (result.changes === 0) {
1351
+ const current = getTask(id, d);
1352
+ throw new VersionConflictError(id, input.version, current?.version ?? -1);
1353
+ }
1354
+ if (input.tags !== undefined) {
1355
+ replaceTaskTags(id, input.tags, d);
1356
+ }
1357
+ const agentId = task.assigned_to || task.agent_id || null;
1358
+ if (input.status !== undefined && input.status !== task.status)
1359
+ logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
1360
+ if (input.priority !== undefined && input.priority !== task.priority)
1361
+ logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
1362
+ if (input.title !== undefined && input.title !== task.title)
1363
+ logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
1364
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
1365
+ logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
1366
+ if (input.approved_by !== undefined)
1367
+ logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
1368
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
1369
+ dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
1370
+ }
1371
+ if (input.status !== undefined && input.status !== task.status) {
1372
+ dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
991
1373
  }
992
- params.push(id);
993
- d.run(`UPDATE orgs SET ${sets.join(", ")} WHERE id = ?`, params);
994
- return getOrg(id, d);
995
- }
996
- function deleteOrg(id, db) {
997
- const d = db || getDatabase();
998
- return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
999
- }
1000
- var init_orgs = __esm(() => {
1001
- init_database();
1002
- });
1003
-
1004
- // src/db/templates.ts
1005
- var exports_templates = {};
1006
- __export(exports_templates, {
1007
- taskFromTemplate: () => taskFromTemplate,
1008
- listTemplates: () => listTemplates,
1009
- getTemplate: () => getTemplate,
1010
- deleteTemplate: () => deleteTemplate,
1011
- createTemplate: () => createTemplate
1012
- });
1013
- function rowToTemplate(row) {
1014
1374
  return {
1015
- ...row,
1016
- tags: JSON.parse(row.tags || "[]"),
1017
- metadata: JSON.parse(row.metadata || "{}"),
1018
- priority: row.priority || "medium"
1375
+ ...task,
1376
+ ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
1377
+ tags: input.tags ?? task.tags,
1378
+ metadata: input.metadata ?? task.metadata,
1379
+ version: task.version + 1,
1380
+ updated_at: now(),
1381
+ completed_at: input.status === "completed" ? now() : task.completed_at,
1382
+ requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
1383
+ approved_by: input.approved_by ?? task.approved_by,
1384
+ approved_at: input.approved_by ? now() : task.approved_at
1019
1385
  };
1020
1386
  }
1021
- function createTemplate(input, db) {
1022
- const d = db || getDatabase();
1023
- const id = uuid();
1024
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
1025
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1026
- id,
1027
- input.name,
1028
- input.title_pattern,
1029
- input.description || null,
1030
- input.priority || "medium",
1031
- JSON.stringify(input.tags || []),
1032
- input.project_id || null,
1033
- input.plan_id || null,
1034
- JSON.stringify(input.metadata || {}),
1035
- now()
1036
- ]);
1037
- return getTemplate(id, d);
1038
- }
1039
- function getTemplate(id, db) {
1040
- const d = db || getDatabase();
1041
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
1042
- return row ? rowToTemplate(row) : null;
1043
- }
1044
- function listTemplates(db) {
1387
+ function deleteTask(id, db) {
1045
1388
  const d = db || getDatabase();
1046
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
1389
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
1390
+ return result.changes > 0;
1047
1391
  }
1048
- function deleteTemplate(id, db) {
1392
+ function getBlockingDeps(id, db) {
1049
1393
  const d = db || getDatabase();
1050
- return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
1051
- }
1052
- function taskFromTemplate(templateId, overrides = {}, db) {
1053
- const t = getTemplate(templateId, db);
1054
- if (!t)
1055
- throw new Error(`Template not found: ${templateId}`);
1056
- return {
1057
- title: overrides.title || t.title_pattern,
1058
- description: overrides.description ?? t.description ?? undefined,
1059
- priority: overrides.priority ?? t.priority,
1060
- tags: overrides.tags ?? t.tags,
1061
- project_id: overrides.project_id ?? t.project_id ?? undefined,
1062
- plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
1063
- metadata: overrides.metadata ?? t.metadata,
1064
- ...overrides
1065
- };
1066
- }
1067
- var init_templates = __esm(() => {
1068
- init_database();
1069
- });
1070
-
1071
- // src/server/serve.ts
1072
- import { existsSync as existsSync4 } from "fs";
1073
- import { join as join3, dirname as dirname2, extname } from "path";
1074
- import { fileURLToPath } from "url";
1075
-
1076
- // src/db/tasks.ts
1077
- init_types();
1078
- init_database();
1079
- init_projects();
1080
-
1081
- // src/lib/completion-guard.ts
1082
- init_types();
1083
-
1084
- // src/lib/config.ts
1085
- import { existsSync as existsSync3 } from "fs";
1086
- import { join as join2 } from "path";
1087
-
1088
- // src/lib/sync-utils.ts
1089
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
1090
- var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
1091
- function readJsonFile(path) {
1092
- try {
1093
- return JSON.parse(readFileSync(path, "utf-8"));
1094
- } catch {
1095
- return null;
1096
- }
1097
- }
1098
-
1099
- // src/lib/config.ts
1100
- var CONFIG_PATH = join2(HOME, ".todos", "config.json");
1101
- var cached = null;
1102
- function loadConfig() {
1103
- if (cached)
1104
- return cached;
1105
- if (!existsSync3(CONFIG_PATH)) {
1106
- cached = {};
1107
- return cached;
1108
- }
1109
- const config = readJsonFile(CONFIG_PATH) || {};
1110
- if (typeof config.sync_agents === "string") {
1111
- config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
1112
- }
1113
- cached = config;
1114
- return cached;
1115
- }
1116
- var GUARD_DEFAULTS = {
1117
- enabled: false,
1118
- min_work_seconds: 30,
1119
- max_completions_per_window: 5,
1120
- window_minutes: 10,
1121
- cooldown_seconds: 60
1122
- };
1123
- function getCompletionGuardConfig(projectPath) {
1124
- const config = loadConfig();
1125
- const global = { ...GUARD_DEFAULTS, ...config.completion_guard };
1126
- if (projectPath && config.project_overrides?.[projectPath]?.completion_guard) {
1127
- return { ...global, ...config.project_overrides[projectPath].completion_guard };
1394
+ const deps = getTaskDependencies(id, d);
1395
+ if (deps.length === 0)
1396
+ return [];
1397
+ const blocking = [];
1398
+ for (const dep of deps) {
1399
+ const task = getTask(dep.depends_on, d);
1400
+ if (task && task.status !== "completed")
1401
+ blocking.push(task);
1128
1402
  }
1129
- return global;
1403
+ return blocking;
1130
1404
  }
1131
-
1132
- // src/lib/completion-guard.ts
1133
- init_projects();
1134
- function checkCompletionGuard(task, agentId, db, configOverride) {
1135
- let config;
1136
- if (configOverride) {
1137
- config = configOverride;
1138
- } else {
1139
- const projectPath = task.project_id ? getProject(task.project_id, db)?.path : null;
1140
- config = getCompletionGuardConfig(projectPath);
1141
- }
1142
- if (!config.enabled)
1143
- return;
1144
- if (task.status !== "in_progress") {
1145
- throw new CompletionGuardError(`Task must be in 'in_progress' status before completing (current: '${task.status}'). Use start_task first.`);
1146
- }
1147
- const agent = agentId || task.assigned_to || task.agent_id;
1148
- if (config.min_work_seconds && task.locked_at) {
1149
- const startedAt = new Date(task.locked_at).getTime();
1150
- const elapsedSeconds = (Date.now() - startedAt) / 1000;
1151
- if (elapsedSeconds < config.min_work_seconds) {
1152
- const remaining = Math.ceil(config.min_work_seconds - elapsedSeconds);
1153
- throw new CompletionGuardError(`Too fast: task was started ${Math.floor(elapsedSeconds)}s ago. Minimum work duration is ${config.min_work_seconds}s. Wait ${remaining}s.`, remaining);
1154
- }
1155
- }
1156
- if (agent && config.max_completions_per_window && config.window_minutes) {
1157
- const windowStart = new Date(Date.now() - config.window_minutes * 60 * 1000).toISOString();
1158
- const result = db.query(`SELECT COUNT(*) as count FROM tasks
1159
- WHERE completed_at > ? AND (assigned_to = ? OR agent_id = ?)`).get(windowStart, agent, agent);
1160
- if (result.count >= config.max_completions_per_window) {
1161
- throw new CompletionGuardError(`Rate limit: ${result.count} tasks completed in the last ${config.window_minutes} minutes (max ${config.max_completions_per_window}). Slow down.`);
1162
- }
1405
+ function startTask(id, agentId, db) {
1406
+ const d = db || getDatabase();
1407
+ const task = getTask(id, d);
1408
+ if (!task)
1409
+ throw new TaskNotFoundError(id);
1410
+ const blocking = getBlockingDeps(id, d);
1411
+ if (blocking.length > 0) {
1412
+ const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
1413
+ throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
1163
1414
  }
1164
- if (agent && config.cooldown_seconds) {
1165
- const result = db.query(`SELECT MAX(completed_at) as last_completed FROM tasks
1166
- WHERE completed_at IS NOT NULL AND (assigned_to = ? OR agent_id = ?) AND id != ?`).get(agent, agent, task.id);
1167
- if (result.last_completed) {
1168
- const elapsedSeconds = (Date.now() - new Date(result.last_completed).getTime()) / 1000;
1169
- if (elapsedSeconds < config.cooldown_seconds) {
1170
- const remaining = Math.ceil(config.cooldown_seconds - elapsedSeconds);
1171
- throw new CompletionGuardError(`Cooldown: last completion was ${Math.floor(elapsedSeconds)}s ago. Wait ${remaining}s between completions.`, remaining);
1172
- }
1415
+ const cutoff = lockExpiryCutoff();
1416
+ const timestamp = now();
1417
+ const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
1418
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
1419
+ if (result.changes === 0) {
1420
+ if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
1421
+ throw new LockError(id, task.locked_by);
1173
1422
  }
1174
1423
  }
1424
+ logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
1425
+ dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
1426
+ return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
1175
1427
  }
1176
-
1177
- // src/db/tasks.ts
1178
- init_audit();
1179
-
1180
- // src/lib/recurrence.ts
1181
- var DAY_NAMES = {
1182
- sunday: 0,
1183
- sun: 0,
1184
- monday: 1,
1185
- mon: 1,
1186
- tuesday: 2,
1187
- tue: 2,
1188
- wednesday: 3,
1189
- wed: 3,
1190
- thursday: 4,
1191
- thu: 4,
1192
- friday: 5,
1193
- fri: 5,
1194
- saturday: 6,
1195
- sat: 6
1196
- };
1197
- function parseRecurrenceRule(rule) {
1198
- const normalized = rule.trim().toLowerCase();
1199
- if (normalized === "every weekday" || normalized === "every weekdays") {
1200
- return { type: "specific_days", days: [1, 2, 3, 4, 5] };
1201
- }
1202
- if (normalized === "every day" || normalized === "daily") {
1203
- return { type: "interval", interval: 1, unit: "day" };
1204
- }
1205
- if (normalized === "every week" || normalized === "weekly") {
1206
- return { type: "interval", interval: 1, unit: "week" };
1428
+ function completeTask(id, agentId, db, options) {
1429
+ const d = db || getDatabase();
1430
+ const task = getTask(id, d);
1431
+ if (!task)
1432
+ throw new TaskNotFoundError(id);
1433
+ if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
1434
+ throw new LockError(id, task.locked_by);
1207
1435
  }
1208
- if (normalized === "every month" || normalized === "monthly") {
1209
- return { type: "interval", interval: 1, unit: "month" };
1436
+ checkCompletionGuard(task, agentId || null, d);
1437
+ 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;
1438
+ const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
1439
+ if (hasEvidence) {
1440
+ const meta2 = { ...task.metadata, _evidence: evidence };
1441
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
1210
1442
  }
1211
- const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
1212
- if (intervalMatch) {
1213
- return {
1214
- type: "interval",
1215
- interval: parseInt(intervalMatch[1], 10),
1216
- unit: intervalMatch[2]
1217
- };
1443
+ const timestamp = now();
1444
+ d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
1445
+ WHERE id = ?`, [timestamp, timestamp, id]);
1446
+ logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
1447
+ dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
1448
+ let spawnedTask = null;
1449
+ if (task.recurrence_rule && !options?.skip_recurrence) {
1450
+ spawnedTask = spawnNextRecurrence(task, d);
1218
1451
  }
1219
- const daysMatch = normalized.match(/^every\s+(.+)$/);
1220
- if (daysMatch) {
1221
- const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
1222
- const days = [];
1223
- for (const part of dayParts) {
1224
- const dayNum = DAY_NAMES[part];
1225
- if (dayNum !== undefined) {
1226
- days.push(dayNum);
1227
- }
1228
- }
1229
- if (days.length > 0) {
1230
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
1231
- }
1452
+ const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
1453
+ if (spawnedTask) {
1454
+ meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
1232
1455
  }
1233
- 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"`);
1456
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
1234
1457
  }
1235
- function nextOccurrence(rule, from) {
1236
- const parsed = parseRecurrenceRule(rule);
1237
- const base = from || new Date;
1238
- if (parsed.type === "interval") {
1239
- const next = new Date(base);
1240
- if (parsed.unit === "day") {
1241
- next.setDate(next.getDate() + parsed.interval);
1242
- } else if (parsed.unit === "week") {
1243
- next.setDate(next.getDate() + parsed.interval * 7);
1244
- } else if (parsed.unit === "month") {
1245
- next.setMonth(next.getMonth() + parsed.interval);
1246
- }
1247
- return next.toISOString();
1458
+ function lockTask(id, agentId, db) {
1459
+ const d = db || getDatabase();
1460
+ const task = getTask(id, d);
1461
+ if (!task)
1462
+ throw new TaskNotFoundError(id);
1463
+ if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
1464
+ return { success: true, locked_by: agentId, locked_at: task.locked_at };
1248
1465
  }
1249
- if (parsed.type === "specific_days") {
1250
- const currentDay = base.getDay();
1251
- const days = parsed.days;
1252
- let daysToAdd = Infinity;
1253
- for (const day of days) {
1254
- let diff = day - currentDay;
1255
- if (diff <= 0)
1256
- diff += 7;
1257
- if (diff < daysToAdd)
1258
- daysToAdd = diff;
1466
+ const cutoff = lockExpiryCutoff();
1467
+ const timestamp = now();
1468
+ const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
1469
+ WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
1470
+ if (result.changes === 0) {
1471
+ const current = getTask(id, d);
1472
+ if (!current)
1473
+ throw new TaskNotFoundError(id);
1474
+ if (current.locked_by && !isLockExpired(current.locked_at)) {
1475
+ return {
1476
+ success: false,
1477
+ locked_by: current.locked_by,
1478
+ locked_at: current.locked_at,
1479
+ error: `Task is locked by ${current.locked_by}`
1480
+ };
1259
1481
  }
1260
- const next = new Date(base);
1261
- next.setDate(next.getDate() + daysToAdd);
1262
- return next.toISOString();
1263
1482
  }
1264
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
1483
+ return { success: true, locked_by: agentId, locked_at: timestamp };
1265
1484
  }
1266
-
1267
- // src/db/tasks.ts
1268
- init_webhooks();
1269
- function rowToTask(row) {
1270
- return {
1271
- ...row,
1272
- tags: JSON.parse(row.tags || "[]"),
1273
- metadata: JSON.parse(row.metadata || "{}"),
1274
- status: row.status,
1275
- priority: row.priority,
1276
- requires_approval: !!row.requires_approval
1277
- };
1485
+ function unlockTask(id, agentId, db) {
1486
+ const d = db || getDatabase();
1487
+ const task = getTask(id, d);
1488
+ if (!task)
1489
+ throw new TaskNotFoundError(id);
1490
+ if (agentId && task.locked_by && task.locked_by !== agentId) {
1491
+ throw new LockError(id, task.locked_by);
1492
+ }
1493
+ const timestamp = now();
1494
+ d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
1495
+ WHERE id = ?`, [timestamp, id]);
1496
+ return true;
1278
1497
  }
1279
- function insertTaskTags(taskId, tags, db) {
1280
- if (tags.length === 0)
1281
- return;
1282
- const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
1283
- for (const tag of tags) {
1284
- if (tag)
1285
- stmt.run(taskId, tag);
1498
+ function addDependency(taskId, dependsOn, db) {
1499
+ const d = db || getDatabase();
1500
+ if (!getTask(taskId, d))
1501
+ throw new TaskNotFoundError(taskId);
1502
+ if (!getTask(dependsOn, d))
1503
+ throw new TaskNotFoundError(dependsOn);
1504
+ if (wouldCreateCycle(taskId, dependsOn, d)) {
1505
+ throw new DependencyCycleError(taskId, dependsOn);
1286
1506
  }
1507
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
1287
1508
  }
1288
- function replaceTaskTags(taskId, tags, db) {
1289
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
1290
- insertTaskTags(taskId, tags, db);
1509
+ function removeDependency(taskId, dependsOn, db) {
1510
+ const d = db || getDatabase();
1511
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
1512
+ return result.changes > 0;
1291
1513
  }
1292
- function createTask(input, db) {
1514
+ function getTaskDependencies(taskId, db) {
1293
1515
  const d = db || getDatabase();
1294
- const id = uuid();
1295
- const timestamp = now();
1296
- const tags = input.tags || [];
1297
- const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
1298
- const title = shortId ? `${shortId}: ${input.title}` : input.title;
1299
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id)
1300
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1301
- id,
1302
- shortId,
1303
- input.project_id || null,
1304
- input.parent_id || null,
1305
- input.plan_id || null,
1306
- input.task_list_id || null,
1307
- title,
1308
- input.description || null,
1309
- input.status || "pending",
1310
- input.priority || "medium",
1311
- input.agent_id || null,
1312
- input.assigned_to || null,
1313
- input.session_id || null,
1314
- input.working_dir || null,
1315
- JSON.stringify(tags),
1316
- JSON.stringify(input.metadata || {}),
1317
- timestamp,
1318
- timestamp,
1319
- input.due_at || null,
1320
- input.estimated_minutes || null,
1321
- input.requires_approval ? 1 : 0,
1322
- null,
1323
- null,
1324
- input.recurrence_rule || null,
1325
- input.recurrence_parent_id || null
1326
- ]);
1327
- if (tags.length > 0) {
1328
- insertTaskTags(id, tags, d);
1329
- }
1330
- const task = getTask(id, d);
1331
- dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
1332
- return task;
1516
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
1333
1517
  }
1334
- function getTask(id, db) {
1518
+ function getTaskDependents(taskId, db) {
1335
1519
  const d = db || getDatabase();
1336
- const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
1337
- if (!row)
1338
- return null;
1339
- return rowToTask(row);
1520
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
1340
1521
  }
1341
- function listTasks(filter = {}, db) {
1522
+ function cloneTask(taskId, overrides, db) {
1342
1523
  const d = db || getDatabase();
1343
- clearExpiredLocks(d);
1344
- const conditions = [];
1345
- const params = [];
1346
- if (filter.project_id) {
1347
- conditions.push("project_id = ?");
1348
- params.push(filter.project_id);
1524
+ const source = getTask(taskId, d);
1525
+ if (!source)
1526
+ throw new TaskNotFoundError(taskId);
1527
+ const input = {
1528
+ title: overrides?.title ?? source.title,
1529
+ description: overrides?.description ?? source.description ?? undefined,
1530
+ priority: overrides?.priority ?? source.priority,
1531
+ project_id: overrides?.project_id ?? source.project_id ?? undefined,
1532
+ parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
1533
+ plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
1534
+ task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
1535
+ status: overrides?.status ?? "pending",
1536
+ agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
1537
+ assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
1538
+ tags: overrides?.tags ?? source.tags,
1539
+ metadata: overrides?.metadata ?? source.metadata,
1540
+ estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
1541
+ recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
1542
+ };
1543
+ return createTask(input, d);
1544
+ }
1545
+ function getTaskGraph(taskId, direction = "both", db) {
1546
+ const d = db || getDatabase();
1547
+ const task = getTask(taskId, d);
1548
+ if (!task)
1549
+ throw new TaskNotFoundError(taskId);
1550
+ function toNode(t) {
1551
+ const deps = getTaskDependencies(t.id, d);
1552
+ const hasUnfinishedDeps = deps.some((dep) => {
1553
+ const depTask = getTask(dep.depends_on, d);
1554
+ return depTask && depTask.status !== "completed";
1555
+ });
1556
+ return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
1557
+ }
1558
+ function buildUp(id, visited) {
1559
+ if (visited.has(id))
1560
+ return [];
1561
+ visited.add(id);
1562
+ const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
1563
+ return deps.map((dep) => {
1564
+ const depTask = getTask(dep.depends_on, d);
1565
+ if (!depTask)
1566
+ return null;
1567
+ return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
1568
+ }).filter(Boolean);
1569
+ }
1570
+ function buildDown(id, visited) {
1571
+ if (visited.has(id))
1572
+ return [];
1573
+ visited.add(id);
1574
+ const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
1575
+ return dependents.map((dep) => {
1576
+ const depTask = getTask(dep.task_id, d);
1577
+ if (!depTask)
1578
+ return null;
1579
+ return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
1580
+ }).filter(Boolean);
1581
+ }
1582
+ const rootNode = toNode(task);
1583
+ const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
1584
+ const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
1585
+ return { task: rootNode, depends_on, blocks };
1586
+ }
1587
+ function moveTask(taskId, target, db) {
1588
+ const d = db || getDatabase();
1589
+ const task = getTask(taskId, d);
1590
+ if (!task)
1591
+ throw new TaskNotFoundError(taskId);
1592
+ const sets = ["updated_at = ?", "version = version + 1"];
1593
+ const params = [now()];
1594
+ if (target.task_list_id !== undefined) {
1595
+ sets.push("task_list_id = ?");
1596
+ params.push(target.task_list_id);
1349
1597
  }
1350
- if (filter.parent_id !== undefined) {
1351
- if (filter.parent_id === null) {
1352
- conditions.push("parent_id IS NULL");
1353
- } else {
1354
- conditions.push("parent_id = ?");
1355
- params.push(filter.parent_id);
1356
- }
1598
+ if (target.project_id !== undefined) {
1599
+ sets.push("project_id = ?");
1600
+ params.push(target.project_id);
1357
1601
  }
1358
- if (filter.status) {
1359
- if (Array.isArray(filter.status)) {
1360
- conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
1361
- params.push(...filter.status);
1362
- } else {
1363
- conditions.push("status = ?");
1364
- params.push(filter.status);
1365
- }
1602
+ if (target.plan_id !== undefined) {
1603
+ sets.push("plan_id = ?");
1604
+ params.push(target.plan_id);
1366
1605
  }
1367
- if (filter.priority) {
1368
- if (Array.isArray(filter.priority)) {
1369
- conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
1370
- params.push(...filter.priority);
1371
- } else {
1372
- conditions.push("priority = ?");
1373
- params.push(filter.priority);
1374
- }
1606
+ params.push(taskId);
1607
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
1608
+ return getTask(taskId, d);
1609
+ }
1610
+ function spawnNextRecurrence(completedTask, db) {
1611
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
1612
+ let title = completedTask.title;
1613
+ if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
1614
+ title = title.slice(completedTask.short_id.length + 2);
1375
1615
  }
1376
- if (filter.assigned_to) {
1377
- conditions.push("assigned_to = ?");
1378
- params.push(filter.assigned_to);
1616
+ const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
1617
+ return createTask({
1618
+ title,
1619
+ description: completedTask.description ?? undefined,
1620
+ priority: completedTask.priority,
1621
+ project_id: completedTask.project_id ?? undefined,
1622
+ task_list_id: completedTask.task_list_id ?? undefined,
1623
+ plan_id: completedTask.plan_id ?? undefined,
1624
+ assigned_to: completedTask.assigned_to ?? undefined,
1625
+ tags: completedTask.tags,
1626
+ metadata: completedTask.metadata,
1627
+ estimated_minutes: completedTask.estimated_minutes ?? undefined,
1628
+ recurrence_rule: completedTask.recurrence_rule,
1629
+ recurrence_parent_id: recurrenceParentId,
1630
+ due_at: dueAt
1631
+ }, db);
1632
+ }
1633
+ function claimNextTask(agentId, filters, db) {
1634
+ const d = db || getDatabase();
1635
+ const tx = d.transaction(() => {
1636
+ const task = getNextTask(agentId, filters, d);
1637
+ if (!task)
1638
+ return null;
1639
+ return startTask(task.id, agentId, d);
1640
+ });
1641
+ return tx();
1642
+ }
1643
+ function getNextTask(agentId, filters, db) {
1644
+ const d = db || getDatabase();
1645
+ clearExpiredLocks(d);
1646
+ const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
1647
+ const params = [lockExpiryCutoff()];
1648
+ if (filters?.project_id) {
1649
+ conditions.push("project_id = ?");
1650
+ params.push(filters.project_id);
1379
1651
  }
1380
- if (filter.agent_id) {
1381
- conditions.push("agent_id = ?");
1382
- params.push(filter.agent_id);
1652
+ if (filters?.task_list_id) {
1653
+ conditions.push("task_list_id = ?");
1654
+ params.push(filters.task_list_id);
1383
1655
  }
1384
- if (filter.session_id) {
1385
- conditions.push("session_id = ?");
1386
- params.push(filter.session_id);
1656
+ if (filters?.plan_id) {
1657
+ conditions.push("plan_id = ?");
1658
+ params.push(filters.plan_id);
1387
1659
  }
1388
- if (filter.tags && filter.tags.length > 0) {
1389
- const placeholders = filter.tags.map(() => "?").join(",");
1660
+ if (filters?.tags && filters.tags.length > 0) {
1661
+ const placeholders = filters.tags.map(() => "?").join(",");
1390
1662
  conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1391
- params.push(...filter.tags);
1663
+ params.push(...filters.tags);
1392
1664
  }
1393
- if (filter.plan_id) {
1394
- conditions.push("plan_id = ?");
1395
- params.push(filter.plan_id);
1665
+ 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')");
1666
+ const where = conditions.join(" AND ");
1667
+ let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
1668
+ if (agentId) {
1669
+ sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
1670
+ params.push(agentId);
1396
1671
  }
1397
- if (filter.task_list_id) {
1672
+ 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`;
1673
+ const row = d.query(sql).get(...params);
1674
+ return row ? rowToTask(row) : null;
1675
+ }
1676
+ function getActiveWork(filters, db) {
1677
+ const d = db || getDatabase();
1678
+ clearExpiredLocks(d);
1679
+ const conditions = ["status = 'in_progress'"];
1680
+ const params = [];
1681
+ if (filters?.project_id) {
1682
+ conditions.push("project_id = ?");
1683
+ params.push(filters.project_id);
1684
+ }
1685
+ if (filters?.task_list_id) {
1398
1686
  conditions.push("task_list_id = ?");
1399
- params.push(filter.task_list_id);
1687
+ params.push(filters.task_list_id);
1400
1688
  }
1401
- if (filter.has_recurrence === true) {
1402
- conditions.push("recurrence_rule IS NOT NULL");
1403
- } else if (filter.has_recurrence === false) {
1404
- conditions.push("recurrence_rule IS NULL");
1689
+ const where = conditions.join(" AND ");
1690
+ const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
1691
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
1692
+ updated_at DESC`).all(...params);
1693
+ return rows;
1694
+ }
1695
+ function getTasksChangedSince(since, filters, db) {
1696
+ const d = db || getDatabase();
1697
+ const conditions = ["updated_at > ?"];
1698
+ const params = [since];
1699
+ if (filters?.project_id) {
1700
+ conditions.push("project_id = ?");
1701
+ params.push(filters.project_id);
1405
1702
  }
1406
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1407
- let limitClause = "";
1408
- if (filter.limit) {
1409
- limitClause = " LIMIT ?";
1410
- params.push(filter.limit);
1411
- if (filter.offset) {
1412
- limitClause += " OFFSET ?";
1413
- params.push(filter.offset);
1414
- }
1703
+ if (filters?.task_list_id) {
1704
+ conditions.push("task_list_id = ?");
1705
+ params.push(filters.task_list_id);
1415
1706
  }
1416
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY
1417
- CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
1418
- created_at DESC${limitClause}`).all(...params);
1707
+ const where = conditions.join(" AND ");
1708
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
1419
1709
  return rows.map(rowToTask);
1420
1710
  }
1421
- function updateTask(id, input, db) {
1711
+ function failTask(id, agentId, reason, options, db) {
1422
1712
  const d = db || getDatabase();
1423
1713
  const task = getTask(id, d);
1424
1714
  if (!task)
1425
1715
  throw new TaskNotFoundError(id);
1426
- if (task.version !== input.version) {
1427
- throw new VersionConflictError(id, input.version, task.version);
1716
+ const meta = {
1717
+ ...task.metadata,
1718
+ _failure: {
1719
+ reason: reason || "Unknown failure",
1720
+ error_code: options?.error_code || null,
1721
+ failed_by: agentId || null,
1722
+ failed_at: now(),
1723
+ retry_requested: options?.retry || false
1724
+ }
1725
+ };
1726
+ const timestamp = now();
1727
+ d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
1728
+ WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
1729
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
1730
+ dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
1731
+ const failedTask = {
1732
+ ...task,
1733
+ status: "failed",
1734
+ locked_by: null,
1735
+ locked_at: null,
1736
+ metadata: meta,
1737
+ version: task.version + 1,
1738
+ updated_at: timestamp
1739
+ };
1740
+ let retryTask;
1741
+ if (options?.retry) {
1742
+ let title = task.title;
1743
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
1744
+ title = title.slice(task.short_id.length + 2);
1745
+ }
1746
+ retryTask = createTask({
1747
+ title,
1748
+ description: task.description ?? undefined,
1749
+ priority: task.priority,
1750
+ project_id: task.project_id ?? undefined,
1751
+ task_list_id: task.task_list_id ?? undefined,
1752
+ plan_id: task.plan_id ?? undefined,
1753
+ assigned_to: task.assigned_to ?? undefined,
1754
+ tags: task.tags,
1755
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
1756
+ estimated_minutes: task.estimated_minutes ?? undefined,
1757
+ recurrence_rule: task.recurrence_rule ?? undefined,
1758
+ due_at: options.retry_after || task.due_at || undefined
1759
+ }, d);
1760
+ }
1761
+ return { task: failedTask, retryTask };
1762
+ }
1763
+ function getStaleTasks(staleMinutes = 30, filters, db) {
1764
+ const d = db || getDatabase();
1765
+ const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
1766
+ const conditions = [
1767
+ "status = 'in_progress'",
1768
+ "(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
1769
+ ];
1770
+ const params = [cutoff, cutoff];
1771
+ if (filters?.project_id) {
1772
+ conditions.push("project_id = ?");
1773
+ params.push(filters.project_id);
1428
1774
  }
1429
- const sets = ["version = version + 1", "updated_at = ?"];
1775
+ if (filters?.task_list_id) {
1776
+ conditions.push("task_list_id = ?");
1777
+ params.push(filters.task_list_id);
1778
+ }
1779
+ const where = conditions.join(" AND ");
1780
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
1781
+ return rows.map(rowToTask);
1782
+ }
1783
+ function getStatus(filters, agentId, db) {
1784
+ const d = db || getDatabase();
1785
+ const pending = countTasks({ ...filters, status: "pending" }, d);
1786
+ const in_progress = countTasks({ ...filters, status: "in_progress" }, d);
1787
+ const completed = countTasks({ ...filters, status: "completed" }, d);
1788
+ const total = countTasks(filters || {}, d);
1789
+ const active_work = getActiveWork(filters, d);
1790
+ const next_task = getNextTask(agentId, filters, d);
1791
+ const stale = getStaleTasks(30, filters, d);
1792
+ const conditions = ["recurrence_rule IS NOT NULL", "status = 'pending'", "due_at < ?"];
1430
1793
  const params = [now()];
1431
- if (input.title !== undefined) {
1432
- sets.push("title = ?");
1433
- params.push(input.title);
1794
+ if (filters?.project_id) {
1795
+ conditions.push("project_id = ?");
1796
+ params.push(filters.project_id);
1434
1797
  }
1435
- if (input.description !== undefined) {
1436
- sets.push("description = ?");
1437
- params.push(input.description);
1798
+ if (filters?.task_list_id) {
1799
+ conditions.push("task_list_id = ?");
1800
+ params.push(filters.task_list_id);
1438
1801
  }
1439
- if (input.status !== undefined) {
1440
- if (input.status === "completed") {
1441
- checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
1442
- }
1443
- sets.push("status = ?");
1444
- params.push(input.status);
1445
- if (input.status === "completed") {
1446
- sets.push("completed_at = ?");
1447
- params.push(now());
1802
+ const overdueRow = d.query(`SELECT COUNT(*) as count FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params);
1803
+ return {
1804
+ pending,
1805
+ in_progress,
1806
+ completed,
1807
+ total,
1808
+ active_work,
1809
+ next_task,
1810
+ stale_count: stale.length,
1811
+ overdue_recurring: overdueRow.count
1812
+ };
1813
+ }
1814
+ function wouldCreateCycle(taskId, dependsOn, db) {
1815
+ const visited = new Set;
1816
+ const queue = [dependsOn];
1817
+ while (queue.length > 0) {
1818
+ const current = queue.shift();
1819
+ if (current === taskId)
1820
+ return true;
1821
+ if (visited.has(current))
1822
+ continue;
1823
+ visited.add(current);
1824
+ const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
1825
+ for (const dep of deps) {
1826
+ queue.push(dep.depends_on);
1448
1827
  }
1449
1828
  }
1450
- if (input.priority !== undefined) {
1451
- sets.push("priority = ?");
1452
- params.push(input.priority);
1453
- }
1454
- if (input.assigned_to !== undefined) {
1455
- sets.push("assigned_to = ?");
1456
- params.push(input.assigned_to);
1457
- }
1458
- if (input.tags !== undefined) {
1459
- sets.push("tags = ?");
1460
- params.push(JSON.stringify(input.tags));
1829
+ return false;
1830
+ }
1831
+ function getTaskStats(filters, db) {
1832
+ const d = db || getDatabase();
1833
+ const conditions = [];
1834
+ const params = [];
1835
+ if (filters?.project_id) {
1836
+ conditions.push("project_id = ?");
1837
+ params.push(filters.project_id);
1461
1838
  }
1462
- if (input.metadata !== undefined) {
1463
- sets.push("metadata = ?");
1464
- params.push(JSON.stringify(input.metadata));
1839
+ if (filters?.task_list_id) {
1840
+ conditions.push("task_list_id = ?");
1841
+ params.push(filters.task_list_id);
1465
1842
  }
1466
- if (input.plan_id !== undefined) {
1467
- sets.push("plan_id = ?");
1468
- params.push(input.plan_id);
1843
+ if (filters?.agent_id) {
1844
+ conditions.push("(agent_id = ? OR assigned_to = ?)");
1845
+ params.push(filters.agent_id, filters.agent_id);
1469
1846
  }
1470
- if (input.task_list_id !== undefined) {
1471
- sets.push("task_list_id = ?");
1472
- params.push(input.task_list_id);
1847
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1848
+ const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
1849
+ const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
1850
+ const by_status = {};
1851
+ for (const r of statusRows)
1852
+ by_status[r.status] = r.count;
1853
+ const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
1854
+ const by_priority = {};
1855
+ for (const r of priorityRows)
1856
+ by_priority[r.priority] = r.count;
1857
+ const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
1858
+ const by_agent = {};
1859
+ for (const r of agentRows)
1860
+ by_agent[r.agent] = r.count;
1861
+ const completed = by_status["completed"] || 0;
1862
+ const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
1863
+ return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
1864
+ }
1865
+ function bulkCreateTasks(inputs, db) {
1866
+ const d = db || getDatabase();
1867
+ const tempIdToRealId = new Map;
1868
+ const created = [];
1869
+ const tx = d.transaction(() => {
1870
+ for (const input of inputs) {
1871
+ const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
1872
+ const task = createTask(createInput, d);
1873
+ if (temp_id)
1874
+ tempIdToRealId.set(temp_id, task.id);
1875
+ created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
1876
+ }
1877
+ for (const input of inputs) {
1878
+ if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
1879
+ const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
1880
+ if (!taskId)
1881
+ continue;
1882
+ for (const depTempId of input.depends_on_temp_ids) {
1883
+ const depRealId = tempIdToRealId.get(depTempId);
1884
+ if (depRealId) {
1885
+ addDependency(taskId, depRealId, d);
1886
+ }
1887
+ }
1888
+ }
1889
+ }
1890
+ });
1891
+ tx();
1892
+ return { created };
1893
+ }
1894
+ function bulkUpdateTasks(taskIds, updates, db) {
1895
+ const d = db || getDatabase();
1896
+ let updated = 0;
1897
+ const failed = [];
1898
+ const tx = d.transaction(() => {
1899
+ for (const id of taskIds) {
1900
+ try {
1901
+ const task = getTask(id, d);
1902
+ if (!task) {
1903
+ failed.push({ id, error: "Task not found" });
1904
+ continue;
1905
+ }
1906
+ updateTask(id, { ...updates, version: task.version }, d);
1907
+ updated++;
1908
+ } catch (e) {
1909
+ failed.push({ id, error: e instanceof Error ? e.message : String(e) });
1910
+ }
1911
+ }
1912
+ });
1913
+ tx();
1914
+ return { updated, failed };
1915
+ }
1916
+ var init_tasks = __esm(() => {
1917
+ init_types();
1918
+ init_database();
1919
+ init_projects();
1920
+ init_completion_guard();
1921
+ init_audit();
1922
+ init_recurrence();
1923
+ init_webhooks();
1924
+ });
1925
+
1926
+ // src/db/agents.ts
1927
+ var exports_agents = {};
1928
+ __export(exports_agents, {
1929
+ updateAgentActivity: () => updateAgentActivity,
1930
+ updateAgent: () => updateAgent,
1931
+ registerAgent: () => registerAgent,
1932
+ listAgents: () => listAgents,
1933
+ getOrgChart: () => getOrgChart,
1934
+ getDirectReports: () => getDirectReports,
1935
+ getAgentByName: () => getAgentByName,
1936
+ getAgent: () => getAgent,
1937
+ deleteAgent: () => deleteAgent
1938
+ });
1939
+ function shortUuid() {
1940
+ return crypto.randomUUID().slice(0, 8);
1941
+ }
1942
+ function rowToAgent(row) {
1943
+ return {
1944
+ ...row,
1945
+ permissions: JSON.parse(row.permissions || '["*"]'),
1946
+ metadata: JSON.parse(row.metadata || "{}")
1947
+ };
1948
+ }
1949
+ function registerAgent(input, db) {
1950
+ const d = db || getDatabase();
1951
+ const normalizedName = input.name.trim().toLowerCase();
1952
+ const existing = getAgentByName(normalizedName, d);
1953
+ if (existing) {
1954
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), existing.id]);
1955
+ return getAgent(existing.id, d);
1473
1956
  }
1474
- if (input.due_at !== undefined) {
1475
- sets.push("due_at = ?");
1476
- params.push(input.due_at);
1957
+ const id = shortUuid();
1958
+ const timestamp = now();
1959
+ d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, reports_to, org_id, metadata, created_at, last_seen_at)
1960
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1961
+ id,
1962
+ normalizedName,
1963
+ input.description || null,
1964
+ input.role || "agent",
1965
+ input.title || null,
1966
+ input.level || null,
1967
+ JSON.stringify(input.permissions || ["*"]),
1968
+ input.reports_to || null,
1969
+ input.org_id || null,
1970
+ JSON.stringify(input.metadata || {}),
1971
+ timestamp,
1972
+ timestamp
1973
+ ]);
1974
+ return getAgent(id, d);
1975
+ }
1976
+ function getAgent(id, db) {
1977
+ const d = db || getDatabase();
1978
+ const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
1979
+ return row ? rowToAgent(row) : null;
1980
+ }
1981
+ function getAgentByName(name, db) {
1982
+ const d = db || getDatabase();
1983
+ const normalizedName = name.trim().toLowerCase();
1984
+ const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
1985
+ return row ? rowToAgent(row) : null;
1986
+ }
1987
+ function listAgents(db) {
1988
+ const d = db || getDatabase();
1989
+ return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
1990
+ }
1991
+ function updateAgentActivity(id, db) {
1992
+ const d = db || getDatabase();
1993
+ d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
1994
+ }
1995
+ function updateAgent(id, input, db) {
1996
+ const d = db || getDatabase();
1997
+ const agent = getAgent(id, d);
1998
+ if (!agent)
1999
+ throw new Error(`Agent not found: ${id}`);
2000
+ const sets = ["last_seen_at = ?"];
2001
+ const params = [now()];
2002
+ if (input.name !== undefined) {
2003
+ sets.push("name = ?");
2004
+ params.push(input.name.trim().toLowerCase());
1477
2005
  }
1478
- if (input.estimated_minutes !== undefined) {
1479
- sets.push("estimated_minutes = ?");
1480
- params.push(input.estimated_minutes);
2006
+ if (input.description !== undefined) {
2007
+ sets.push("description = ?");
2008
+ params.push(input.description);
1481
2009
  }
1482
- if (input.requires_approval !== undefined) {
1483
- sets.push("requires_approval = ?");
1484
- params.push(input.requires_approval ? 1 : 0);
2010
+ if (input.role !== undefined) {
2011
+ sets.push("role = ?");
2012
+ params.push(input.role);
1485
2013
  }
1486
- if (input.approved_by !== undefined) {
1487
- sets.push("approved_by = ?");
1488
- params.push(input.approved_by);
1489
- sets.push("approved_at = ?");
1490
- params.push(now());
2014
+ if (input.permissions !== undefined) {
2015
+ sets.push("permissions = ?");
2016
+ params.push(JSON.stringify(input.permissions));
1491
2017
  }
1492
- if (input.recurrence_rule !== undefined) {
1493
- sets.push("recurrence_rule = ?");
1494
- params.push(input.recurrence_rule);
2018
+ if (input.title !== undefined) {
2019
+ sets.push("title = ?");
2020
+ params.push(input.title);
1495
2021
  }
1496
- params.push(id, input.version);
1497
- const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
1498
- if (result.changes === 0) {
1499
- const current = getTask(id, d);
1500
- throw new VersionConflictError(id, input.version, current?.version ?? -1);
2022
+ if (input.level !== undefined) {
2023
+ sets.push("level = ?");
2024
+ params.push(input.level);
1501
2025
  }
1502
- if (input.tags !== undefined) {
1503
- replaceTaskTags(id, input.tags, d);
2026
+ if (input.reports_to !== undefined) {
2027
+ sets.push("reports_to = ?");
2028
+ params.push(input.reports_to);
1504
2029
  }
1505
- const agentId = task.assigned_to || task.agent_id || null;
1506
- if (input.status !== undefined && input.status !== task.status)
1507
- logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
1508
- if (input.priority !== undefined && input.priority !== task.priority)
1509
- logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
1510
- if (input.title !== undefined && input.title !== task.title)
1511
- logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
1512
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
1513
- logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
1514
- if (input.approved_by !== undefined)
1515
- logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
1516
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
1517
- dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
2030
+ if (input.org_id !== undefined) {
2031
+ sets.push("org_id = ?");
2032
+ params.push(input.org_id);
1518
2033
  }
1519
- if (input.status !== undefined && input.status !== task.status) {
1520
- dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
2034
+ if (input.metadata !== undefined) {
2035
+ sets.push("metadata = ?");
2036
+ params.push(JSON.stringify(input.metadata));
1521
2037
  }
1522
- return {
1523
- ...task,
1524
- ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
1525
- tags: input.tags ?? task.tags,
1526
- metadata: input.metadata ?? task.metadata,
1527
- version: task.version + 1,
1528
- updated_at: now(),
1529
- completed_at: input.status === "completed" ? now() : task.completed_at,
1530
- requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
1531
- approved_by: input.approved_by ?? task.approved_by,
1532
- approved_at: input.approved_by ? now() : task.approved_at
1533
- };
2038
+ params.push(id);
2039
+ d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
2040
+ return getAgent(id, d);
1534
2041
  }
1535
- function deleteTask(id, db) {
2042
+ function deleteAgent(id, db) {
1536
2043
  const d = db || getDatabase();
1537
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
1538
- return result.changes > 0;
2044
+ return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
1539
2045
  }
1540
- function getBlockingDeps(id, db) {
2046
+ function getDirectReports(agentId, db) {
1541
2047
  const d = db || getDatabase();
1542
- const deps = getTaskDependencies(id, d);
1543
- if (deps.length === 0)
1544
- return [];
1545
- const blocking = [];
1546
- for (const dep of deps) {
1547
- const task = getTask(dep.depends_on, d);
1548
- if (task && task.status !== "completed")
1549
- blocking.push(task);
2048
+ return d.query("SELECT * FROM agents WHERE reports_to = ? ORDER BY name").all(agentId).map(rowToAgent);
2049
+ }
2050
+ function getOrgChart(db) {
2051
+ const agents = listAgents(db);
2052
+ const byManager = new Map;
2053
+ for (const a of agents) {
2054
+ const key = a.reports_to;
2055
+ if (!byManager.has(key))
2056
+ byManager.set(key, []);
2057
+ byManager.get(key).push(a);
1550
2058
  }
1551
- return blocking;
2059
+ function buildTree(parentId) {
2060
+ const children = byManager.get(parentId) || [];
2061
+ return children.map((a) => ({ agent: a, reports: buildTree(a.id) }));
2062
+ }
2063
+ return buildTree(null);
1552
2064
  }
1553
- function startTask(id, agentId, db) {
2065
+ var init_agents = __esm(() => {
2066
+ init_database();
2067
+ });
2068
+
2069
+ // src/db/orgs.ts
2070
+ var exports_orgs = {};
2071
+ __export(exports_orgs, {
2072
+ updateOrg: () => updateOrg,
2073
+ listOrgs: () => listOrgs,
2074
+ getOrgByName: () => getOrgByName,
2075
+ getOrg: () => getOrg,
2076
+ deleteOrg: () => deleteOrg,
2077
+ createOrg: () => createOrg
2078
+ });
2079
+ function rowToOrg(row) {
2080
+ return { ...row, metadata: JSON.parse(row.metadata || "{}") };
2081
+ }
2082
+ function createOrg(input, db) {
1554
2083
  const d = db || getDatabase();
1555
- const task = getTask(id, d);
1556
- if (!task)
1557
- throw new TaskNotFoundError(id);
1558
- const blocking = getBlockingDeps(id, d);
1559
- if (blocking.length > 0) {
1560
- const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
1561
- throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
1562
- }
1563
- const cutoff = lockExpiryCutoff();
2084
+ const id = uuid();
1564
2085
  const timestamp = now();
1565
- const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
1566
- WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, id, agentId, cutoff]);
1567
- if (result.changes === 0) {
1568
- if (task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
1569
- throw new LockError(id, task.locked_by);
1570
- }
1571
- }
1572
- logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
1573
- dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
1574
- return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
2086
+ d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
2087
+ return getOrg(id, d);
1575
2088
  }
1576
- function completeTask(id, agentId, db, options) {
2089
+ function getOrg(id, db) {
1577
2090
  const d = db || getDatabase();
1578
- const task = getTask(id, d);
1579
- if (!task)
1580
- throw new TaskNotFoundError(id);
1581
- if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
1582
- throw new LockError(id, task.locked_by);
1583
- }
1584
- checkCompletionGuard(task, agentId || null, d);
1585
- 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;
1586
- const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
1587
- if (hasEvidence) {
1588
- const meta2 = { ...task.metadata, _evidence: evidence };
1589
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
2091
+ const row = d.query("SELECT * FROM orgs WHERE id = ?").get(id);
2092
+ return row ? rowToOrg(row) : null;
2093
+ }
2094
+ function getOrgByName(name, db) {
2095
+ const d = db || getDatabase();
2096
+ const row = d.query("SELECT * FROM orgs WHERE name = ?").get(name);
2097
+ return row ? rowToOrg(row) : null;
2098
+ }
2099
+ function listOrgs(db) {
2100
+ const d = db || getDatabase();
2101
+ return d.query("SELECT * FROM orgs ORDER BY name").all().map(rowToOrg);
2102
+ }
2103
+ function updateOrg(id, input, db) {
2104
+ const d = db || getDatabase();
2105
+ const org = getOrg(id, d);
2106
+ if (!org)
2107
+ throw new Error(`Org not found: ${id}`);
2108
+ const sets = ["updated_at = ?"];
2109
+ const params = [now()];
2110
+ if (input.name !== undefined) {
2111
+ sets.push("name = ?");
2112
+ params.push(input.name);
1590
2113
  }
1591
- const timestamp = now();
1592
- d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
1593
- WHERE id = ?`, [timestamp, timestamp, id]);
1594
- logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
1595
- dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
1596
- let spawnedTask = null;
1597
- if (task.recurrence_rule && !options?.skip_recurrence) {
1598
- spawnedTask = spawnNextRecurrence(task, d);
2114
+ if (input.description !== undefined) {
2115
+ sets.push("description = ?");
2116
+ params.push(input.description);
1599
2117
  }
1600
- const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
1601
- if (spawnedTask) {
1602
- meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
2118
+ if (input.metadata !== undefined) {
2119
+ sets.push("metadata = ?");
2120
+ params.push(JSON.stringify(input.metadata));
1603
2121
  }
1604
- return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
2122
+ params.push(id);
2123
+ d.run(`UPDATE orgs SET ${sets.join(", ")} WHERE id = ?`, params);
2124
+ return getOrg(id, d);
1605
2125
  }
1606
- function getTaskDependencies(taskId, db) {
2126
+ function deleteOrg(id, db) {
1607
2127
  const d = db || getDatabase();
1608
- return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
2128
+ return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
1609
2129
  }
1610
- function spawnNextRecurrence(completedTask, db) {
1611
- const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
1612
- let title = completedTask.title;
1613
- if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
1614
- title = title.slice(completedTask.short_id.length + 2);
1615
- }
1616
- const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
1617
- return createTask({
1618
- title,
1619
- description: completedTask.description ?? undefined,
1620
- priority: completedTask.priority,
1621
- project_id: completedTask.project_id ?? undefined,
1622
- task_list_id: completedTask.task_list_id ?? undefined,
1623
- plan_id: completedTask.plan_id ?? undefined,
1624
- assigned_to: completedTask.assigned_to ?? undefined,
1625
- tags: completedTask.tags,
1626
- metadata: completedTask.metadata,
1627
- estimated_minutes: completedTask.estimated_minutes ?? undefined,
1628
- recurrence_rule: completedTask.recurrence_rule,
1629
- recurrence_parent_id: recurrenceParentId,
1630
- due_at: dueAt
1631
- }, db);
2130
+ var init_orgs = __esm(() => {
2131
+ init_database();
2132
+ });
2133
+
2134
+ // src/db/templates.ts
2135
+ var exports_templates = {};
2136
+ __export(exports_templates, {
2137
+ taskFromTemplate: () => taskFromTemplate,
2138
+ listTemplates: () => listTemplates,
2139
+ getTemplate: () => getTemplate,
2140
+ deleteTemplate: () => deleteTemplate,
2141
+ createTemplate: () => createTemplate
2142
+ });
2143
+ function rowToTemplate(row) {
2144
+ return {
2145
+ ...row,
2146
+ tags: JSON.parse(row.tags || "[]"),
2147
+ metadata: JSON.parse(row.metadata || "{}"),
2148
+ priority: row.priority || "medium"
2149
+ };
2150
+ }
2151
+ function createTemplate(input, db) {
2152
+ const d = db || getDatabase();
2153
+ const id = uuid();
2154
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
2155
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2156
+ id,
2157
+ input.name,
2158
+ input.title_pattern,
2159
+ input.description || null,
2160
+ input.priority || "medium",
2161
+ JSON.stringify(input.tags || []),
2162
+ input.project_id || null,
2163
+ input.plan_id || null,
2164
+ JSON.stringify(input.metadata || {}),
2165
+ now()
2166
+ ]);
2167
+ return getTemplate(id, d);
2168
+ }
2169
+ function getTemplate(id, db) {
2170
+ const d = db || getDatabase();
2171
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
2172
+ return row ? rowToTemplate(row) : null;
2173
+ }
2174
+ function listTemplates(db) {
2175
+ const d = db || getDatabase();
2176
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
2177
+ }
2178
+ function deleteTemplate(id, db) {
2179
+ const d = db || getDatabase();
2180
+ return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
2181
+ }
2182
+ function taskFromTemplate(templateId, overrides = {}, db) {
2183
+ const t = getTemplate(templateId, db);
2184
+ if (!t)
2185
+ throw new Error(`Template not found: ${templateId}`);
2186
+ return {
2187
+ title: overrides.title || t.title_pattern,
2188
+ description: overrides.description ?? t.description ?? undefined,
2189
+ priority: overrides.priority ?? t.priority,
2190
+ tags: overrides.tags ?? t.tags,
2191
+ project_id: overrides.project_id ?? t.project_id ?? undefined,
2192
+ plan_id: overrides.plan_id ?? t.plan_id ?? undefined,
2193
+ metadata: overrides.metadata ?? t.metadata,
2194
+ ...overrides
2195
+ };
1632
2196
  }
2197
+ var init_templates = __esm(() => {
2198
+ init_database();
2199
+ });
1633
2200
 
1634
2201
  // src/server/serve.ts
2202
+ init_tasks();
1635
2203
  init_projects();
1636
2204
  init_agents();
2205
+ import { existsSync as existsSync4 } from "fs";
2206
+ import { join as join3, dirname as dirname2, extname } from "path";
2207
+ import { fileURLToPath } from "url";
1637
2208
 
1638
2209
  // src/db/plans.ts
1639
2210
  init_types();
@@ -1954,6 +2525,62 @@ Dashboard not found at: ${dashboardDir}`);
1954
2525
  return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
1955
2526
  }
1956
2527
  }
2528
+ if (path === "/api/tasks/status" && method === "GET") {
2529
+ try {
2530
+ const projectId = url.searchParams.get("project_id") || undefined;
2531
+ const agentId = url.searchParams.get("agent_id") || undefined;
2532
+ const { getStatus: getStatus2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
2533
+ const status = getStatus2(projectId ? { project_id: projectId } : undefined, agentId);
2534
+ return json(status, 200, port);
2535
+ } catch (e) {
2536
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
2537
+ }
2538
+ }
2539
+ if (path === "/api/tasks/next" && method === "GET") {
2540
+ try {
2541
+ const projectId = url.searchParams.get("project_id") || undefined;
2542
+ const agentId = url.searchParams.get("agent_id") || undefined;
2543
+ const { getNextTask: getNextTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
2544
+ const task = getNextTask2(agentId, projectId ? { project_id: projectId } : undefined);
2545
+ return json({ task: task ? taskToSummary(task) : null }, 200, port);
2546
+ } catch (e) {
2547
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
2548
+ }
2549
+ }
2550
+ if (path === "/api/tasks/active" && method === "GET") {
2551
+ try {
2552
+ const projectId = url.searchParams.get("project_id") || undefined;
2553
+ const { getActiveWork: getActiveWork2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
2554
+ const work = getActiveWork2(projectId ? { project_id: projectId } : undefined);
2555
+ return json({ active: work, count: work.length }, 200, port);
2556
+ } catch (e) {
2557
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
2558
+ }
2559
+ }
2560
+ if (path === "/api/tasks/stale" && method === "GET") {
2561
+ try {
2562
+ const projectId = url.searchParams.get("project_id") || undefined;
2563
+ const minutes = parseInt(url.searchParams.get("minutes") || "30", 10);
2564
+ const { getStaleTasks: getStaleTasks2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
2565
+ const tasks = getStaleTasks2(minutes, projectId ? { project_id: projectId } : undefined);
2566
+ return json({ tasks: tasks.map((t) => taskToSummary(t)), count: tasks.length }, 200, port);
2567
+ } catch (e) {
2568
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
2569
+ }
2570
+ }
2571
+ if (path === "/api/tasks/changed" && method === "GET") {
2572
+ try {
2573
+ const since = url.searchParams.get("since");
2574
+ if (!since)
2575
+ return json({ error: "since parameter required (ISO date string)" }, 400, port);
2576
+ const projectId = url.searchParams.get("project_id") || undefined;
2577
+ const { getTasksChangedSince: getTasksChangedSince2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
2578
+ const tasks = getTasksChangedSince2(since, projectId ? { project_id: projectId } : undefined);
2579
+ return json({ tasks: tasks.map((t) => taskToSummary(t)), count: tasks.length, since }, 200, port);
2580
+ } catch (e) {
2581
+ return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
2582
+ }
2583
+ }
1957
2584
  const taskMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
1958
2585
  if (taskMatch) {
1959
2586
  const id = taskMatch[1];
@@ -2048,25 +2675,9 @@ Dashboard not found at: ${dashboardDir}`);
2048
2675
  try {
2049
2676
  const body = await req.json();
2050
2677
  const agentId = body.agent_id || "anonymous";
2051
- const pending = listTasks({ status: "pending", project_id: body.project_id });
2052
- const available = pending.filter((t) => !t.locked_by);
2053
- if (available.length === 0)
2054
- return json({ task: null }, 200, port);
2055
- const order = { critical: 0, high: 1, medium: 2, low: 3 };
2056
- available.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4));
2057
- const target = available[0];
2058
- try {
2059
- const claimed = startTask(target.id, agentId);
2060
- return json({ task: taskToSummary(claimed) }, 200, port);
2061
- } catch (e) {
2062
- const next = available[1] || null;
2063
- return json({
2064
- task: null,
2065
- locked_by: target.locked_by,
2066
- locked_since: target.locked_at,
2067
- suggested_task: next ? taskToSummary(next) : null
2068
- }, 200, port);
2069
- }
2678
+ const { claimNextTask: claimNextTask2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
2679
+ const task = claimNextTask2(agentId, body.project_id ? { project_id: body.project_id } : undefined);
2680
+ return json({ task: task ? taskToSummary(task) : null }, 200, port);
2070
2681
  } catch (e) {
2071
2682
  return json({ error: e instanceof Error ? e.message : "Failed to claim" }, 500, port);
2072
2683
  }