@hasna/todos 0.9.38 → 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);
1326
+ if (input.due_at !== undefined) {
1327
+ sets.push("due_at = ?");
1328
+ params.push(input.due_at);
1329
+ }
1330
+ if (input.estimated_minutes !== undefined) {
1331
+ sets.push("estimated_minutes = ?");
1332
+ params.push(input.estimated_minutes);
1333
+ }
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(() => {});
1373
+ }
1374
+ return {
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
1385
+ };
958
1386
  }
959
- function getOrg(id, db) {
1387
+ function deleteTask(id, db) {
960
1388
  const d = db || getDatabase();
961
- const row = d.query("SELECT * FROM orgs WHERE id = ?").get(id);
962
- return row ? rowToOrg(row) : null;
1389
+ const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
1390
+ return result.changes > 0;
963
1391
  }
964
- function getOrgByName(name, db) {
1392
+ function getBlockingDeps(id, db) {
965
1393
  const d = db || getDatabase();
966
- const row = d.query("SELECT * FROM orgs WHERE name = ?").get(name);
967
- return row ? rowToOrg(row) : null;
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);
1402
+ }
1403
+ return blocking;
968
1404
  }
969
- function listOrgs(db) {
1405
+ function startTask(id, agentId, db) {
970
1406
  const d = db || getDatabase();
971
- return d.query("SELECT * FROM orgs ORDER BY name").all().map(rowToOrg);
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}`);
1414
+ }
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);
1422
+ }
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 };
972
1427
  }
973
- function updateOrg(id, input, db) {
1428
+ function completeTask(id, agentId, db, options) {
974
1429
  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);
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);
983
1435
  }
984
- if (input.description !== undefined) {
985
- sets.push("description = ?");
986
- params.push(input.description);
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]);
987
1442
  }
988
- if (input.metadata !== undefined) {
989
- sets.push("metadata = ?");
990
- params.push(JSON.stringify(input.metadata));
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);
991
1451
  }
992
- params.push(id);
993
- d.run(`UPDATE orgs SET ${sets.join(", ")} WHERE id = ?`, params);
994
- return getOrg(id, d);
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 };
1455
+ }
1456
+ return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
995
1457
  }
996
- function deleteOrg(id, db) {
1458
+ function lockTask(id, agentId, db) {
997
1459
  const d = db || getDatabase();
998
- return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
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 };
1465
+ }
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
+ };
1481
+ }
1482
+ }
1483
+ return { success: true, locked_by: agentId, locked_at: timestamp };
999
1484
  }
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
- return {
1015
- ...row,
1016
- tags: JSON.parse(row.tags || "[]"),
1017
- metadata: JSON.parse(row.metadata || "{}"),
1018
- priority: row.priority || "medium"
1019
- };
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;
1020
1497
  }
1021
- function createTemplate(input, db) {
1498
+ function addDependency(taskId, dependsOn, db) {
1022
1499
  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);
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);
1506
+ }
1507
+ d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
1038
1508
  }
1039
- function getTemplate(id, db) {
1509
+ function removeDependency(taskId, dependsOn, db) {
1040
1510
  const d = db || getDatabase();
1041
- const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
1042
- return row ? rowToTemplate(row) : null;
1511
+ const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
1512
+ return result.changes > 0;
1043
1513
  }
1044
- function listTemplates(db) {
1514
+ function getTaskDependencies(taskId, db) {
1045
1515
  const d = db || getDatabase();
1046
- return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
1516
+ return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
1047
1517
  }
1048
- function deleteTemplate(id, db) {
1518
+ function getTaskDependents(taskId, db) {
1049
1519
  const d = db || getDatabase();
1050
- return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
1520
+ return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
1051
1521
  }
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
1522
+ function cloneTask(taskId, overrides, db) {
1523
+ const d = db || getDatabase();
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
1065
1542
  };
1543
+ return createTask(input, d);
1066
1544
  }
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;
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);
1096
1597
  }
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;
1598
+ if (target.project_id !== undefined) {
1599
+ sets.push("project_id = ?");
1600
+ params.push(target.project_id);
1108
1601
  }
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);
1602
+ if (target.plan_id !== undefined) {
1603
+ sets.push("plan_id = ?");
1604
+ params.push(target.plan_id);
1112
1605
  }
1113
- cached = config;
1114
- return cached;
1606
+ params.push(taskId);
1607
+ d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
1608
+ return getTask(taskId, d);
1115
1609
  }
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 };
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);
1128
1615
  }
1129
- return global;
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);
1130
1632
  }
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);
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);
1141
1651
  }
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.`);
1652
+ if (filters?.task_list_id) {
1653
+ conditions.push("task_list_id = ?");
1654
+ params.push(filters.task_list_id);
1146
1655
  }
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
- }
1656
+ if (filters?.plan_id) {
1657
+ conditions.push("plan_id = ?");
1658
+ params.push(filters.plan_id);
1155
1659
  }
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
- }
1660
+ if (filters?.tags && filters.tags.length > 0) {
1661
+ const placeholders = filters.tags.map(() => "?").join(",");
1662
+ conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1663
+ params.push(...filters.tags);
1163
1664
  }
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
- }
1173
- }
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);
1174
1671
  }
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;
1175
1675
  }
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" };
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);
1204
1684
  }
1205
- if (normalized === "every week" || normalized === "weekly") {
1206
- return { type: "interval", interval: 1, unit: "week" };
1685
+ if (filters?.task_list_id) {
1686
+ conditions.push("task_list_id = ?");
1687
+ params.push(filters.task_list_id);
1207
1688
  }
1208
- if (normalized === "every month" || normalized === "monthly") {
1209
- return { type: "interval", interval: 1, unit: "month" };
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);
1210
1702
  }
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
- };
1703
+ if (filters?.task_list_id) {
1704
+ conditions.push("task_list_id = ?");
1705
+ params.push(filters.task_list_id);
1218
1706
  }
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
- }
1707
+ const where = conditions.join(" AND ");
1708
+ const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
1709
+ return rows.map(rowToTask);
1710
+ }
1711
+ function failTask(id, agentId, reason, options, db) {
1712
+ const d = db || getDatabase();
1713
+ const task = getTask(id, d);
1714
+ if (!task)
1715
+ throw new TaskNotFoundError(id);
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
1228
1724
  }
1229
- if (days.length > 0) {
1230
- return { type: "specific_days", days: days.sort((a, b) => a - b) };
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);
1231
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);
1232
1774
  }
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"`);
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);
1234
1782
  }
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();
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 < ?"];
1793
+ const params = [now()];
1794
+ if (filters?.project_id) {
1795
+ conditions.push("project_id = ?");
1796
+ params.push(filters.project_id);
1248
1797
  }
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;
1259
- }
1260
- const next = new Date(base);
1261
- next.setDate(next.getDate() + daysToAdd);
1262
- return next.toISOString();
1798
+ if (filters?.task_list_id) {
1799
+ conditions.push("task_list_id = ?");
1800
+ params.push(filters.task_list_id);
1263
1801
  }
1264
- throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
1265
- }
1266
-
1267
- // src/db/tasks.ts
1268
- init_webhooks();
1269
- function rowToTask(row) {
1802
+ const overdueRow = d.query(`SELECT COUNT(*) as count FROM tasks WHERE ${conditions.join(" AND ")}`).get(...params);
1270
1803
  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
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
1277
1812
  };
1278
1813
  }
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);
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);
1827
+ }
1286
1828
  }
1829
+ return false;
1287
1830
  }
1288
- function replaceTaskTags(taskId, tags, db) {
1289
- db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
1290
- insertTaskTags(taskId, tags, db);
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);
1838
+ }
1839
+ if (filters?.task_list_id) {
1840
+ conditions.push("task_list_id = ?");
1841
+ params.push(filters.task_list_id);
1842
+ }
1843
+ if (filters?.agent_id) {
1844
+ conditions.push("(agent_id = ? OR assigned_to = ?)");
1845
+ params.push(filters.agent_id, filters.agent_id);
1846
+ }
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 };
1291
1893
  }
1292
- function createTask(input, db) {
1894
+ function bulkUpdateTasks(taskIds, updates, db) {
1293
1895
  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),
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);
1956
+ }
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,
1316
1970
  JSON.stringify(input.metadata || {}),
1317
1971
  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
1972
+ timestamp
1326
1973
  ]);
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;
1974
+ return getAgent(id, d);
1333
1975
  }
1334
- function getTask(id, db) {
1976
+ function getAgent(id, db) {
1335
1977
  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);
1978
+ const row = d.query("SELECT * FROM agents WHERE id = ?").get(id);
1979
+ return row ? rowToAgent(row) : null;
1340
1980
  }
1341
- function listTasks(filter = {}, db) {
1981
+ function getAgentByName(name, db) {
1342
1982
  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);
1349
- }
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
- }
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());
1357
2005
  }
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
- }
2006
+ if (input.description !== undefined) {
2007
+ sets.push("description = ?");
2008
+ params.push(input.description);
1366
2009
  }
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
- }
2010
+ if (input.role !== undefined) {
2011
+ sets.push("role = ?");
2012
+ params.push(input.role);
1375
2013
  }
1376
- if (filter.assigned_to) {
1377
- conditions.push("assigned_to = ?");
1378
- params.push(filter.assigned_to);
2014
+ if (input.permissions !== undefined) {
2015
+ sets.push("permissions = ?");
2016
+ params.push(JSON.stringify(input.permissions));
1379
2017
  }
1380
- if (filter.agent_id) {
1381
- conditions.push("agent_id = ?");
1382
- params.push(filter.agent_id);
2018
+ if (input.title !== undefined) {
2019
+ sets.push("title = ?");
2020
+ params.push(input.title);
1383
2021
  }
1384
- if (filter.session_id) {
1385
- conditions.push("session_id = ?");
1386
- params.push(filter.session_id);
2022
+ if (input.level !== undefined) {
2023
+ sets.push("level = ?");
2024
+ params.push(input.level);
1387
2025
  }
1388
- if (filter.tags && filter.tags.length > 0) {
1389
- const placeholders = filter.tags.map(() => "?").join(",");
1390
- conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
1391
- params.push(...filter.tags);
2026
+ if (input.reports_to !== undefined) {
2027
+ sets.push("reports_to = ?");
2028
+ params.push(input.reports_to);
1392
2029
  }
1393
- if (filter.plan_id) {
1394
- conditions.push("plan_id = ?");
1395
- params.push(filter.plan_id);
2030
+ if (input.org_id !== undefined) {
2031
+ sets.push("org_id = ?");
2032
+ params.push(input.org_id);
1396
2033
  }
1397
- if (filter.task_list_id) {
1398
- conditions.push("task_list_id = ?");
1399
- params.push(filter.task_list_id);
2034
+ if (input.metadata !== undefined) {
2035
+ sets.push("metadata = ?");
2036
+ params.push(JSON.stringify(input.metadata));
1400
2037
  }
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");
2038
+ params.push(id);
2039
+ d.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, params);
2040
+ return getAgent(id, d);
2041
+ }
2042
+ function deleteAgent(id, db) {
2043
+ const d = db || getDatabase();
2044
+ return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
2045
+ }
2046
+ function getDirectReports(agentId, db) {
2047
+ const d = db || getDatabase();
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);
1405
2058
  }
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
- }
2059
+ function buildTree(parentId) {
2060
+ const children = byManager.get(parentId) || [];
2061
+ return children.map((a) => ({ agent: a, reports: buildTree(a.id) }));
1415
2062
  }
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);
1419
- return rows.map(rowToTask);
2063
+ return buildTree(null);
1420
2064
  }
1421
- function updateTask(id, input, 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) {
1422
2083
  const d = db || getDatabase();
1423
- const task = getTask(id, d);
1424
- if (!task)
1425
- throw new TaskNotFoundError(id);
1426
- if (task.version !== input.version) {
1427
- throw new VersionConflictError(id, input.version, task.version);
1428
- }
1429
- const sets = ["version = version + 1", "updated_at = ?"];
2084
+ const id = uuid();
2085
+ const timestamp = now();
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);
2088
+ }
2089
+ function getOrg(id, db) {
2090
+ const d = db || getDatabase();
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 = ?"];
1430
2109
  const params = [now()];
1431
- if (input.title !== undefined) {
1432
- sets.push("title = ?");
1433
- params.push(input.title);
1434
- }
1435
- if (input.description !== undefined) {
1436
- sets.push("description = ?");
1437
- params.push(input.description);
1438
- }
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());
1448
- }
1449
- }
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);
2110
+ if (input.name !== undefined) {
2111
+ sets.push("name = ?");
2112
+ params.push(input.name);
1457
2113
  }
1458
- if (input.tags !== undefined) {
1459
- sets.push("tags = ?");
1460
- params.push(JSON.stringify(input.tags));
2114
+ if (input.description !== undefined) {
2115
+ sets.push("description = ?");
2116
+ params.push(input.description);
1461
2117
  }
1462
2118
  if (input.metadata !== undefined) {
1463
2119
  sets.push("metadata = ?");
1464
2120
  params.push(JSON.stringify(input.metadata));
1465
2121
  }
1466
- if (input.plan_id !== undefined) {
1467
- sets.push("plan_id = ?");
1468
- params.push(input.plan_id);
1469
- }
1470
- if (input.task_list_id !== undefined) {
1471
- sets.push("task_list_id = ?");
1472
- params.push(input.task_list_id);
1473
- }
1474
- if (input.due_at !== undefined) {
1475
- sets.push("due_at = ?");
1476
- params.push(input.due_at);
1477
- }
1478
- if (input.estimated_minutes !== undefined) {
1479
- sets.push("estimated_minutes = ?");
1480
- params.push(input.estimated_minutes);
1481
- }
1482
- if (input.requires_approval !== undefined) {
1483
- sets.push("requires_approval = ?");
1484
- params.push(input.requires_approval ? 1 : 0);
1485
- }
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());
1491
- }
1492
- if (input.recurrence_rule !== undefined) {
1493
- sets.push("recurrence_rule = ?");
1494
- params.push(input.recurrence_rule);
1495
- }
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);
1501
- }
1502
- if (input.tags !== undefined) {
1503
- replaceTaskTags(id, input.tags, d);
1504
- }
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(() => {});
1518
- }
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(() => {});
1521
- }
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
- };
2122
+ params.push(id);
2123
+ d.run(`UPDATE orgs SET ${sets.join(", ")} WHERE id = ?`, params);
2124
+ return getOrg(id, d);
1534
2125
  }
1535
- function deleteTask(id, db) {
2126
+ function deleteOrg(id, db) {
1536
2127
  const d = db || getDatabase();
1537
- const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
1538
- return result.changes > 0;
2128
+ return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
1539
2129
  }
1540
- function getBlockingDeps(id, 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) {
1541
2152
  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);
1550
- }
1551
- return blocking;
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);
1552
2168
  }
1553
- function startTask(id, agentId, db) {
2169
+ function getTemplate(id, db) {
1554
2170
  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();
1564
- 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 };
2171
+ const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(id);
2172
+ return row ? rowToTemplate(row) : null;
1575
2173
  }
1576
- function completeTask(id, agentId, db, options) {
2174
+ function listTemplates(db) {
1577
2175
  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 } : undefined;
1586
- const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes);
1587
- if (hasEvidence) {
1588
- const meta2 = { ...task.metadata, _evidence: evidence };
1589
- d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
1590
- }
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);
1599
- }
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 };
1603
- }
1604
- return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
2176
+ return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
1605
2177
  }
1606
- function getTaskDependencies(taskId, db) {
2178
+ function deleteTemplate(id, db) {
1607
2179
  const d = db || getDatabase();
1608
- return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
2180
+ return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
1609
2181
  }
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);
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
  }