@hasna/todos 0.9.34 → 0.9.35
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.
- package/dist/cli/index.js +1262 -102
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +113 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +625 -12
- package/dist/lib/recurrence.d.ts +10 -0
- package/dist/lib/recurrence.d.ts.map +1 -0
- package/dist/lib/search.d.ts +14 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/mcp/index.js +1271 -101
- package/dist/server/index.js +176 -6
- package/dist/types/index.d.ts +26 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -283,6 +283,13 @@ var MIGRATIONS = [
|
|
|
283
283
|
ALTER TABLE agents ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
|
|
284
284
|
ALTER TABLE projects ADD COLUMN org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
|
|
285
285
|
INSERT OR IGNORE INTO _migrations (id) VALUES (12);
|
|
286
|
+
`,
|
|
287
|
+
`
|
|
288
|
+
ALTER TABLE tasks ADD COLUMN recurrence_rule TEXT;
|
|
289
|
+
ALTER TABLE tasks ADD COLUMN recurrence_parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL;
|
|
290
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id);
|
|
291
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL;
|
|
292
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (13);
|
|
286
293
|
`
|
|
287
294
|
];
|
|
288
295
|
var _db = null;
|
|
@@ -409,6 +416,8 @@ function ensureSchema(db) {
|
|
|
409
416
|
ensureColumn("tasks", "requires_approval", "INTEGER NOT NULL DEFAULT 0");
|
|
410
417
|
ensureColumn("tasks", "approved_by", "TEXT");
|
|
411
418
|
ensureColumn("tasks", "approved_at", "TEXT");
|
|
419
|
+
ensureColumn("tasks", "recurrence_rule", "TEXT");
|
|
420
|
+
ensureColumn("tasks", "recurrence_parent_id", "TEXT REFERENCES tasks(id) ON DELETE SET NULL");
|
|
412
421
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
413
422
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
414
423
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -433,6 +442,8 @@ function ensureSchema(db) {
|
|
|
433
442
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
434
443
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
435
444
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id)");
|
|
445
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id)");
|
|
446
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL");
|
|
436
447
|
}
|
|
437
448
|
function backfillTaskTags(db) {
|
|
438
449
|
try {
|
|
@@ -533,6 +544,8 @@ class VersionConflictError extends Error {
|
|
|
533
544
|
taskId;
|
|
534
545
|
expectedVersion;
|
|
535
546
|
actualVersion;
|
|
547
|
+
static code = "VERSION_CONFLICT";
|
|
548
|
+
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
536
549
|
constructor(taskId, expectedVersion, actualVersion) {
|
|
537
550
|
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
538
551
|
this.taskId = taskId;
|
|
@@ -544,6 +557,8 @@ class VersionConflictError extends Error {
|
|
|
544
557
|
|
|
545
558
|
class TaskNotFoundError extends Error {
|
|
546
559
|
taskId;
|
|
560
|
+
static code = "TASK_NOT_FOUND";
|
|
561
|
+
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
547
562
|
constructor(taskId) {
|
|
548
563
|
super(`Task not found: ${taskId}`);
|
|
549
564
|
this.taskId = taskId;
|
|
@@ -553,6 +568,8 @@ class TaskNotFoundError extends Error {
|
|
|
553
568
|
|
|
554
569
|
class ProjectNotFoundError extends Error {
|
|
555
570
|
projectId;
|
|
571
|
+
static code = "PROJECT_NOT_FOUND";
|
|
572
|
+
static suggestion = "Use list_projects to see available projects.";
|
|
556
573
|
constructor(projectId) {
|
|
557
574
|
super(`Project not found: ${projectId}`);
|
|
558
575
|
this.projectId = projectId;
|
|
@@ -562,6 +579,8 @@ class ProjectNotFoundError extends Error {
|
|
|
562
579
|
|
|
563
580
|
class PlanNotFoundError extends Error {
|
|
564
581
|
planId;
|
|
582
|
+
static code = "PLAN_NOT_FOUND";
|
|
583
|
+
static suggestion = "Use list_plans to see available plans.";
|
|
565
584
|
constructor(planId) {
|
|
566
585
|
super(`Plan not found: ${planId}`);
|
|
567
586
|
this.planId = planId;
|
|
@@ -572,6 +591,8 @@ class PlanNotFoundError extends Error {
|
|
|
572
591
|
class LockError extends Error {
|
|
573
592
|
taskId;
|
|
574
593
|
lockedBy;
|
|
594
|
+
static code = "LOCK_ERROR";
|
|
595
|
+
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
575
596
|
constructor(taskId, lockedBy) {
|
|
576
597
|
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
577
598
|
this.taskId = taskId;
|
|
@@ -582,6 +603,8 @@ class LockError extends Error {
|
|
|
582
603
|
|
|
583
604
|
class AgentNotFoundError extends Error {
|
|
584
605
|
agentId;
|
|
606
|
+
static code = "AGENT_NOT_FOUND";
|
|
607
|
+
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
585
608
|
constructor(agentId) {
|
|
586
609
|
super(`Agent not found: ${agentId}`);
|
|
587
610
|
this.agentId = agentId;
|
|
@@ -591,6 +614,8 @@ class AgentNotFoundError extends Error {
|
|
|
591
614
|
|
|
592
615
|
class TaskListNotFoundError extends Error {
|
|
593
616
|
taskListId;
|
|
617
|
+
static code = "TASK_LIST_NOT_FOUND";
|
|
618
|
+
static suggestion = "Use list_task_lists to see available lists.";
|
|
594
619
|
constructor(taskListId) {
|
|
595
620
|
super(`Task list not found: ${taskListId}`);
|
|
596
621
|
this.taskListId = taskListId;
|
|
@@ -601,6 +626,8 @@ class TaskListNotFoundError extends Error {
|
|
|
601
626
|
class DependencyCycleError extends Error {
|
|
602
627
|
taskId;
|
|
603
628
|
dependsOn;
|
|
629
|
+
static code = "DEPENDENCY_CYCLE";
|
|
630
|
+
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
604
631
|
constructor(taskId, dependsOn) {
|
|
605
632
|
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
606
633
|
this.taskId = taskId;
|
|
@@ -612,6 +639,8 @@ class DependencyCycleError extends Error {
|
|
|
612
639
|
class CompletionGuardError extends Error {
|
|
613
640
|
reason;
|
|
614
641
|
retryAfterSeconds;
|
|
642
|
+
static code = "COMPLETION_BLOCKED";
|
|
643
|
+
static suggestion = "Wait for the cooldown period, then retry.";
|
|
615
644
|
constructor(reason, retryAfterSeconds) {
|
|
616
645
|
super(reason);
|
|
617
646
|
this.reason = reason;
|
|
@@ -891,6 +920,101 @@ function getRecentActivity(limit = 50, db) {
|
|
|
891
920
|
return d.query("SELECT * FROM task_history ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
892
921
|
}
|
|
893
922
|
|
|
923
|
+
// src/lib/recurrence.ts
|
|
924
|
+
var DAY_NAMES = {
|
|
925
|
+
sunday: 0,
|
|
926
|
+
sun: 0,
|
|
927
|
+
monday: 1,
|
|
928
|
+
mon: 1,
|
|
929
|
+
tuesday: 2,
|
|
930
|
+
tue: 2,
|
|
931
|
+
wednesday: 3,
|
|
932
|
+
wed: 3,
|
|
933
|
+
thursday: 4,
|
|
934
|
+
thu: 4,
|
|
935
|
+
friday: 5,
|
|
936
|
+
fri: 5,
|
|
937
|
+
saturday: 6,
|
|
938
|
+
sat: 6
|
|
939
|
+
};
|
|
940
|
+
function parseRecurrenceRule(rule) {
|
|
941
|
+
const normalized = rule.trim().toLowerCase();
|
|
942
|
+
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
943
|
+
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
944
|
+
}
|
|
945
|
+
if (normalized === "every day" || normalized === "daily") {
|
|
946
|
+
return { type: "interval", interval: 1, unit: "day" };
|
|
947
|
+
}
|
|
948
|
+
if (normalized === "every week" || normalized === "weekly") {
|
|
949
|
+
return { type: "interval", interval: 1, unit: "week" };
|
|
950
|
+
}
|
|
951
|
+
if (normalized === "every month" || normalized === "monthly") {
|
|
952
|
+
return { type: "interval", interval: 1, unit: "month" };
|
|
953
|
+
}
|
|
954
|
+
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
955
|
+
if (intervalMatch) {
|
|
956
|
+
return {
|
|
957
|
+
type: "interval",
|
|
958
|
+
interval: parseInt(intervalMatch[1], 10),
|
|
959
|
+
unit: intervalMatch[2]
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
963
|
+
if (daysMatch) {
|
|
964
|
+
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
965
|
+
const days = [];
|
|
966
|
+
for (const part of dayParts) {
|
|
967
|
+
const dayNum = DAY_NAMES[part];
|
|
968
|
+
if (dayNum !== undefined) {
|
|
969
|
+
days.push(dayNum);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (days.length > 0) {
|
|
973
|
+
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
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"`);
|
|
977
|
+
}
|
|
978
|
+
function isValidRecurrenceRule(rule) {
|
|
979
|
+
try {
|
|
980
|
+
parseRecurrenceRule(rule);
|
|
981
|
+
return true;
|
|
982
|
+
} catch {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function nextOccurrence(rule, from) {
|
|
987
|
+
const parsed = parseRecurrenceRule(rule);
|
|
988
|
+
const base = from || new Date;
|
|
989
|
+
if (parsed.type === "interval") {
|
|
990
|
+
const next = new Date(base);
|
|
991
|
+
if (parsed.unit === "day") {
|
|
992
|
+
next.setDate(next.getDate() + parsed.interval);
|
|
993
|
+
} else if (parsed.unit === "week") {
|
|
994
|
+
next.setDate(next.getDate() + parsed.interval * 7);
|
|
995
|
+
} else if (parsed.unit === "month") {
|
|
996
|
+
next.setMonth(next.getMonth() + parsed.interval);
|
|
997
|
+
}
|
|
998
|
+
return next.toISOString();
|
|
999
|
+
}
|
|
1000
|
+
if (parsed.type === "specific_days") {
|
|
1001
|
+
const currentDay = base.getDay();
|
|
1002
|
+
const days = parsed.days;
|
|
1003
|
+
let daysToAdd = Infinity;
|
|
1004
|
+
for (const day of days) {
|
|
1005
|
+
let diff = day - currentDay;
|
|
1006
|
+
if (diff <= 0)
|
|
1007
|
+
diff += 7;
|
|
1008
|
+
if (diff < daysToAdd)
|
|
1009
|
+
daysToAdd = diff;
|
|
1010
|
+
}
|
|
1011
|
+
const next = new Date(base);
|
|
1012
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
1013
|
+
return next.toISOString();
|
|
1014
|
+
}
|
|
1015
|
+
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
894
1018
|
// src/db/tasks.ts
|
|
895
1019
|
function rowToTask(row) {
|
|
896
1020
|
return {
|
|
@@ -922,8 +1046,8 @@ function createTask(input, db) {
|
|
|
922
1046
|
const tags = input.tags || [];
|
|
923
1047
|
const shortId = input.project_id ? nextTaskShortId(input.project_id, d) : null;
|
|
924
1048
|
const title = shortId ? `${shortId}: ${input.title}` : input.title;
|
|
925
|
-
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)
|
|
926
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1049
|
+
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)
|
|
1050
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
927
1051
|
id,
|
|
928
1052
|
shortId,
|
|
929
1053
|
input.project_id || null,
|
|
@@ -946,7 +1070,9 @@ function createTask(input, db) {
|
|
|
946
1070
|
input.estimated_minutes || null,
|
|
947
1071
|
input.requires_approval ? 1 : 0,
|
|
948
1072
|
null,
|
|
949
|
-
null
|
|
1073
|
+
null,
|
|
1074
|
+
input.recurrence_rule || null,
|
|
1075
|
+
input.recurrence_parent_id || null
|
|
950
1076
|
]);
|
|
951
1077
|
if (tags.length > 0) {
|
|
952
1078
|
insertTaskTags(id, tags, d);
|
|
@@ -1046,6 +1172,11 @@ function listTasks(filter = {}, db) {
|
|
|
1046
1172
|
conditions.push("task_list_id = ?");
|
|
1047
1173
|
params.push(filter.task_list_id);
|
|
1048
1174
|
}
|
|
1175
|
+
if (filter.has_recurrence === true) {
|
|
1176
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
1177
|
+
} else if (filter.has_recurrence === false) {
|
|
1178
|
+
conditions.push("recurrence_rule IS NULL");
|
|
1179
|
+
}
|
|
1049
1180
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1050
1181
|
let limitClause = "";
|
|
1051
1182
|
if (filter.limit) {
|
|
@@ -1061,6 +1192,69 @@ function listTasks(filter = {}, db) {
|
|
|
1061
1192
|
created_at DESC${limitClause}`).all(...params);
|
|
1062
1193
|
return rows.map(rowToTask);
|
|
1063
1194
|
}
|
|
1195
|
+
function countTasks(filter = {}, db) {
|
|
1196
|
+
const d = db || getDatabase();
|
|
1197
|
+
const conditions = [];
|
|
1198
|
+
const params = [];
|
|
1199
|
+
if (filter.project_id) {
|
|
1200
|
+
conditions.push("project_id = ?");
|
|
1201
|
+
params.push(filter.project_id);
|
|
1202
|
+
}
|
|
1203
|
+
if (filter.parent_id !== undefined) {
|
|
1204
|
+
if (filter.parent_id === null) {
|
|
1205
|
+
conditions.push("parent_id IS NULL");
|
|
1206
|
+
} else {
|
|
1207
|
+
conditions.push("parent_id = ?");
|
|
1208
|
+
params.push(filter.parent_id);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (filter.status) {
|
|
1212
|
+
if (Array.isArray(filter.status)) {
|
|
1213
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
1214
|
+
params.push(...filter.status);
|
|
1215
|
+
} else {
|
|
1216
|
+
conditions.push("status = ?");
|
|
1217
|
+
params.push(filter.status);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (filter.priority) {
|
|
1221
|
+
if (Array.isArray(filter.priority)) {
|
|
1222
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
1223
|
+
params.push(...filter.priority);
|
|
1224
|
+
} else {
|
|
1225
|
+
conditions.push("priority = ?");
|
|
1226
|
+
params.push(filter.priority);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (filter.assigned_to) {
|
|
1230
|
+
conditions.push("assigned_to = ?");
|
|
1231
|
+
params.push(filter.assigned_to);
|
|
1232
|
+
}
|
|
1233
|
+
if (filter.agent_id) {
|
|
1234
|
+
conditions.push("agent_id = ?");
|
|
1235
|
+
params.push(filter.agent_id);
|
|
1236
|
+
}
|
|
1237
|
+
if (filter.session_id) {
|
|
1238
|
+
conditions.push("session_id = ?");
|
|
1239
|
+
params.push(filter.session_id);
|
|
1240
|
+
}
|
|
1241
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
1242
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
1243
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
1244
|
+
params.push(...filter.tags);
|
|
1245
|
+
}
|
|
1246
|
+
if (filter.plan_id) {
|
|
1247
|
+
conditions.push("plan_id = ?");
|
|
1248
|
+
params.push(filter.plan_id);
|
|
1249
|
+
}
|
|
1250
|
+
if (filter.task_list_id) {
|
|
1251
|
+
conditions.push("task_list_id = ?");
|
|
1252
|
+
params.push(filter.task_list_id);
|
|
1253
|
+
}
|
|
1254
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1255
|
+
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
1256
|
+
return row.count;
|
|
1257
|
+
}
|
|
1064
1258
|
function updateTask(id, input, db) {
|
|
1065
1259
|
const d = db || getDatabase();
|
|
1066
1260
|
const task = getTask(id, d);
|
|
@@ -1132,6 +1326,10 @@ function updateTask(id, input, db) {
|
|
|
1132
1326
|
sets.push("approved_at = ?");
|
|
1133
1327
|
params.push(now());
|
|
1134
1328
|
}
|
|
1329
|
+
if (input.recurrence_rule !== undefined) {
|
|
1330
|
+
sets.push("recurrence_rule = ?");
|
|
1331
|
+
params.push(input.recurrence_rule);
|
|
1332
|
+
}
|
|
1135
1333
|
params.push(id, input.version);
|
|
1136
1334
|
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
1137
1335
|
if (result.changes === 0) {
|
|
@@ -1205,7 +1403,7 @@ function startTask(id, agentId, db) {
|
|
|
1205
1403
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
1206
1404
|
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, version: task.version + 1, updated_at: timestamp };
|
|
1207
1405
|
}
|
|
1208
|
-
function completeTask(id, agentId, db,
|
|
1406
|
+
function completeTask(id, agentId, db, options) {
|
|
1209
1407
|
const d = db || getDatabase();
|
|
1210
1408
|
const task = getTask(id, d);
|
|
1211
1409
|
if (!task)
|
|
@@ -1214,7 +1412,9 @@ function completeTask(id, agentId, db, evidence) {
|
|
|
1214
1412
|
throw new LockError(id, task.locked_by);
|
|
1215
1413
|
}
|
|
1216
1414
|
checkCompletionGuard(task, agentId || null, d);
|
|
1217
|
-
|
|
1415
|
+
const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes } : undefined;
|
|
1416
|
+
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes);
|
|
1417
|
+
if (hasEvidence) {
|
|
1218
1418
|
const meta2 = { ...task.metadata, _evidence: evidence };
|
|
1219
1419
|
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [JSON.stringify(meta2), id]);
|
|
1220
1420
|
}
|
|
@@ -1222,7 +1422,14 @@ function completeTask(id, agentId, db, evidence) {
|
|
|
1222
1422
|
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, version = version + 1, updated_at = ?
|
|
1223
1423
|
WHERE id = ?`, [timestamp, timestamp, id]);
|
|
1224
1424
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
1225
|
-
|
|
1425
|
+
let spawnedTask = null;
|
|
1426
|
+
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
1427
|
+
spawnedTask = spawnNextRecurrence(task, d);
|
|
1428
|
+
}
|
|
1429
|
+
const meta = hasEvidence ? { ...task.metadata, _evidence: evidence } : task.metadata;
|
|
1430
|
+
if (spawnedTask) {
|
|
1431
|
+
meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
|
|
1432
|
+
}
|
|
1226
1433
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
1227
1434
|
}
|
|
1228
1435
|
function lockTask(id, agentId, db) {
|
|
@@ -1289,6 +1496,266 @@ function getTaskDependents(taskId, db) {
|
|
|
1289
1496
|
const d = db || getDatabase();
|
|
1290
1497
|
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
1291
1498
|
}
|
|
1499
|
+
function cloneTask(taskId, overrides, db) {
|
|
1500
|
+
const d = db || getDatabase();
|
|
1501
|
+
const source = getTask(taskId, d);
|
|
1502
|
+
if (!source)
|
|
1503
|
+
throw new TaskNotFoundError(taskId);
|
|
1504
|
+
const input = {
|
|
1505
|
+
title: overrides?.title ?? source.title,
|
|
1506
|
+
description: overrides?.description ?? source.description ?? undefined,
|
|
1507
|
+
priority: overrides?.priority ?? source.priority,
|
|
1508
|
+
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
1509
|
+
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
1510
|
+
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
1511
|
+
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
1512
|
+
status: overrides?.status ?? "pending",
|
|
1513
|
+
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
1514
|
+
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
1515
|
+
tags: overrides?.tags ?? source.tags,
|
|
1516
|
+
metadata: overrides?.metadata ?? source.metadata,
|
|
1517
|
+
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
1518
|
+
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
1519
|
+
};
|
|
1520
|
+
return createTask(input, d);
|
|
1521
|
+
}
|
|
1522
|
+
function getTaskGraph(taskId, direction = "both", db) {
|
|
1523
|
+
const d = db || getDatabase();
|
|
1524
|
+
const task = getTask(taskId, d);
|
|
1525
|
+
if (!task)
|
|
1526
|
+
throw new TaskNotFoundError(taskId);
|
|
1527
|
+
function toNode(t) {
|
|
1528
|
+
const deps = getTaskDependencies(t.id, d);
|
|
1529
|
+
const hasUnfinishedDeps = deps.some((dep) => {
|
|
1530
|
+
const depTask = getTask(dep.depends_on, d);
|
|
1531
|
+
return depTask && depTask.status !== "completed";
|
|
1532
|
+
});
|
|
1533
|
+
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
1534
|
+
}
|
|
1535
|
+
function buildUp(id, visited) {
|
|
1536
|
+
if (visited.has(id))
|
|
1537
|
+
return [];
|
|
1538
|
+
visited.add(id);
|
|
1539
|
+
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
1540
|
+
return deps.map((dep) => {
|
|
1541
|
+
const depTask = getTask(dep.depends_on, d);
|
|
1542
|
+
if (!depTask)
|
|
1543
|
+
return null;
|
|
1544
|
+
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
1545
|
+
}).filter(Boolean);
|
|
1546
|
+
}
|
|
1547
|
+
function buildDown(id, visited) {
|
|
1548
|
+
if (visited.has(id))
|
|
1549
|
+
return [];
|
|
1550
|
+
visited.add(id);
|
|
1551
|
+
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
1552
|
+
return dependents.map((dep) => {
|
|
1553
|
+
const depTask = getTask(dep.task_id, d);
|
|
1554
|
+
if (!depTask)
|
|
1555
|
+
return null;
|
|
1556
|
+
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
1557
|
+
}).filter(Boolean);
|
|
1558
|
+
}
|
|
1559
|
+
const rootNode = toNode(task);
|
|
1560
|
+
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
1561
|
+
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
1562
|
+
return { task: rootNode, depends_on, blocks };
|
|
1563
|
+
}
|
|
1564
|
+
function moveTask(taskId, target, db) {
|
|
1565
|
+
const d = db || getDatabase();
|
|
1566
|
+
const task = getTask(taskId, d);
|
|
1567
|
+
if (!task)
|
|
1568
|
+
throw new TaskNotFoundError(taskId);
|
|
1569
|
+
const sets = ["updated_at = ?", "version = version + 1"];
|
|
1570
|
+
const params = [now()];
|
|
1571
|
+
if (target.task_list_id !== undefined) {
|
|
1572
|
+
sets.push("task_list_id = ?");
|
|
1573
|
+
params.push(target.task_list_id);
|
|
1574
|
+
}
|
|
1575
|
+
if (target.project_id !== undefined) {
|
|
1576
|
+
sets.push("project_id = ?");
|
|
1577
|
+
params.push(target.project_id);
|
|
1578
|
+
}
|
|
1579
|
+
if (target.plan_id !== undefined) {
|
|
1580
|
+
sets.push("plan_id = ?");
|
|
1581
|
+
params.push(target.plan_id);
|
|
1582
|
+
}
|
|
1583
|
+
params.push(taskId);
|
|
1584
|
+
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1585
|
+
return getTask(taskId, d);
|
|
1586
|
+
}
|
|
1587
|
+
function spawnNextRecurrence(completedTask, db) {
|
|
1588
|
+
const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
|
|
1589
|
+
let title = completedTask.title;
|
|
1590
|
+
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
1591
|
+
title = title.slice(completedTask.short_id.length + 2);
|
|
1592
|
+
}
|
|
1593
|
+
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
1594
|
+
return createTask({
|
|
1595
|
+
title,
|
|
1596
|
+
description: completedTask.description ?? undefined,
|
|
1597
|
+
priority: completedTask.priority,
|
|
1598
|
+
project_id: completedTask.project_id ?? undefined,
|
|
1599
|
+
task_list_id: completedTask.task_list_id ?? undefined,
|
|
1600
|
+
plan_id: completedTask.plan_id ?? undefined,
|
|
1601
|
+
assigned_to: completedTask.assigned_to ?? undefined,
|
|
1602
|
+
tags: completedTask.tags,
|
|
1603
|
+
metadata: completedTask.metadata,
|
|
1604
|
+
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
1605
|
+
recurrence_rule: completedTask.recurrence_rule,
|
|
1606
|
+
recurrence_parent_id: recurrenceParentId,
|
|
1607
|
+
due_at: dueAt
|
|
1608
|
+
}, db);
|
|
1609
|
+
}
|
|
1610
|
+
function claimNextTask(agentId, filters, db) {
|
|
1611
|
+
const d = db || getDatabase();
|
|
1612
|
+
const tx = d.transaction(() => {
|
|
1613
|
+
const task = getNextTask(agentId, filters, d);
|
|
1614
|
+
if (!task)
|
|
1615
|
+
return null;
|
|
1616
|
+
return startTask(task.id, agentId, d);
|
|
1617
|
+
});
|
|
1618
|
+
return tx();
|
|
1619
|
+
}
|
|
1620
|
+
function getNextTask(agentId, filters, db) {
|
|
1621
|
+
const d = db || getDatabase();
|
|
1622
|
+
clearExpiredLocks(d);
|
|
1623
|
+
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
1624
|
+
const params = [lockExpiryCutoff()];
|
|
1625
|
+
if (filters?.project_id) {
|
|
1626
|
+
conditions.push("project_id = ?");
|
|
1627
|
+
params.push(filters.project_id);
|
|
1628
|
+
}
|
|
1629
|
+
if (filters?.task_list_id) {
|
|
1630
|
+
conditions.push("task_list_id = ?");
|
|
1631
|
+
params.push(filters.task_list_id);
|
|
1632
|
+
}
|
|
1633
|
+
if (filters?.plan_id) {
|
|
1634
|
+
conditions.push("plan_id = ?");
|
|
1635
|
+
params.push(filters.plan_id);
|
|
1636
|
+
}
|
|
1637
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
1638
|
+
const placeholders = filters.tags.map(() => "?").join(",");
|
|
1639
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
1640
|
+
params.push(...filters.tags);
|
|
1641
|
+
}
|
|
1642
|
+
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')");
|
|
1643
|
+
const where = conditions.join(" AND ");
|
|
1644
|
+
let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
|
|
1645
|
+
if (agentId) {
|
|
1646
|
+
sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
|
|
1647
|
+
params.push(agentId);
|
|
1648
|
+
}
|
|
1649
|
+
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`;
|
|
1650
|
+
const row = d.query(sql).get(...params);
|
|
1651
|
+
return row ? rowToTask(row) : null;
|
|
1652
|
+
}
|
|
1653
|
+
function getActiveWork(filters, db) {
|
|
1654
|
+
const d = db || getDatabase();
|
|
1655
|
+
clearExpiredLocks(d);
|
|
1656
|
+
const conditions = ["status = 'in_progress'"];
|
|
1657
|
+
const params = [];
|
|
1658
|
+
if (filters?.project_id) {
|
|
1659
|
+
conditions.push("project_id = ?");
|
|
1660
|
+
params.push(filters.project_id);
|
|
1661
|
+
}
|
|
1662
|
+
if (filters?.task_list_id) {
|
|
1663
|
+
conditions.push("task_list_id = ?");
|
|
1664
|
+
params.push(filters.task_list_id);
|
|
1665
|
+
}
|
|
1666
|
+
const where = conditions.join(" AND ");
|
|
1667
|
+
const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
|
|
1668
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
1669
|
+
updated_at DESC`).all(...params);
|
|
1670
|
+
return rows;
|
|
1671
|
+
}
|
|
1672
|
+
function getTasksChangedSince(since, filters, db) {
|
|
1673
|
+
const d = db || getDatabase();
|
|
1674
|
+
const conditions = ["updated_at > ?"];
|
|
1675
|
+
const params = [since];
|
|
1676
|
+
if (filters?.project_id) {
|
|
1677
|
+
conditions.push("project_id = ?");
|
|
1678
|
+
params.push(filters.project_id);
|
|
1679
|
+
}
|
|
1680
|
+
if (filters?.task_list_id) {
|
|
1681
|
+
conditions.push("task_list_id = ?");
|
|
1682
|
+
params.push(filters.task_list_id);
|
|
1683
|
+
}
|
|
1684
|
+
const where = conditions.join(" AND ");
|
|
1685
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
|
|
1686
|
+
return rows.map(rowToTask);
|
|
1687
|
+
}
|
|
1688
|
+
function failTask(id, agentId, reason, options, db) {
|
|
1689
|
+
const d = db || getDatabase();
|
|
1690
|
+
const task = getTask(id, d);
|
|
1691
|
+
if (!task)
|
|
1692
|
+
throw new TaskNotFoundError(id);
|
|
1693
|
+
const meta = {
|
|
1694
|
+
...task.metadata,
|
|
1695
|
+
_failure: {
|
|
1696
|
+
reason: reason || "Unknown failure",
|
|
1697
|
+
error_code: options?.error_code || null,
|
|
1698
|
+
failed_by: agentId || null,
|
|
1699
|
+
failed_at: now(),
|
|
1700
|
+
retry_requested: options?.retry || false
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
const timestamp = now();
|
|
1704
|
+
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
1705
|
+
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
1706
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
1707
|
+
const failedTask = {
|
|
1708
|
+
...task,
|
|
1709
|
+
status: "failed",
|
|
1710
|
+
locked_by: null,
|
|
1711
|
+
locked_at: null,
|
|
1712
|
+
metadata: meta,
|
|
1713
|
+
version: task.version + 1,
|
|
1714
|
+
updated_at: timestamp
|
|
1715
|
+
};
|
|
1716
|
+
let retryTask;
|
|
1717
|
+
if (options?.retry) {
|
|
1718
|
+
let title = task.title;
|
|
1719
|
+
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
1720
|
+
title = title.slice(task.short_id.length + 2);
|
|
1721
|
+
}
|
|
1722
|
+
retryTask = createTask({
|
|
1723
|
+
title,
|
|
1724
|
+
description: task.description ?? undefined,
|
|
1725
|
+
priority: task.priority,
|
|
1726
|
+
project_id: task.project_id ?? undefined,
|
|
1727
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
1728
|
+
plan_id: task.plan_id ?? undefined,
|
|
1729
|
+
assigned_to: task.assigned_to ?? undefined,
|
|
1730
|
+
tags: task.tags,
|
|
1731
|
+
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
|
|
1732
|
+
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
1733
|
+
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
1734
|
+
due_at: options.retry_after || task.due_at || undefined
|
|
1735
|
+
}, d);
|
|
1736
|
+
}
|
|
1737
|
+
return { task: failedTask, retryTask };
|
|
1738
|
+
}
|
|
1739
|
+
function getStaleTasks(staleMinutes = 30, filters, db) {
|
|
1740
|
+
const d = db || getDatabase();
|
|
1741
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
1742
|
+
const conditions = [
|
|
1743
|
+
"status = 'in_progress'",
|
|
1744
|
+
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
1745
|
+
];
|
|
1746
|
+
const params = [cutoff, cutoff];
|
|
1747
|
+
if (filters?.project_id) {
|
|
1748
|
+
conditions.push("project_id = ?");
|
|
1749
|
+
params.push(filters.project_id);
|
|
1750
|
+
}
|
|
1751
|
+
if (filters?.task_list_id) {
|
|
1752
|
+
conditions.push("task_list_id = ?");
|
|
1753
|
+
params.push(filters.task_list_id);
|
|
1754
|
+
}
|
|
1755
|
+
const where = conditions.join(" AND ");
|
|
1756
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
1757
|
+
return rows.map(rowToTask);
|
|
1758
|
+
}
|
|
1292
1759
|
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
1293
1760
|
const visited = new Set;
|
|
1294
1761
|
const queue = [dependsOn];
|
|
@@ -1306,6 +1773,91 @@ function wouldCreateCycle(taskId, dependsOn, db) {
|
|
|
1306
1773
|
}
|
|
1307
1774
|
return false;
|
|
1308
1775
|
}
|
|
1776
|
+
function getTaskStats(filters, db) {
|
|
1777
|
+
const d = db || getDatabase();
|
|
1778
|
+
const conditions = [];
|
|
1779
|
+
const params = [];
|
|
1780
|
+
if (filters?.project_id) {
|
|
1781
|
+
conditions.push("project_id = ?");
|
|
1782
|
+
params.push(filters.project_id);
|
|
1783
|
+
}
|
|
1784
|
+
if (filters?.task_list_id) {
|
|
1785
|
+
conditions.push("task_list_id = ?");
|
|
1786
|
+
params.push(filters.task_list_id);
|
|
1787
|
+
}
|
|
1788
|
+
if (filters?.agent_id) {
|
|
1789
|
+
conditions.push("(agent_id = ? OR assigned_to = ?)");
|
|
1790
|
+
params.push(filters.agent_id, filters.agent_id);
|
|
1791
|
+
}
|
|
1792
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1793
|
+
const totalRow = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
1794
|
+
const statusRows = d.query(`SELECT status, COUNT(*) as count FROM tasks ${where} GROUP BY status`).all(...params);
|
|
1795
|
+
const by_status = {};
|
|
1796
|
+
for (const r of statusRows)
|
|
1797
|
+
by_status[r.status] = r.count;
|
|
1798
|
+
const priorityRows = d.query(`SELECT priority, COUNT(*) as count FROM tasks ${where} GROUP BY priority`).all(...params);
|
|
1799
|
+
const by_priority = {};
|
|
1800
|
+
for (const r of priorityRows)
|
|
1801
|
+
by_priority[r.priority] = r.count;
|
|
1802
|
+
const agentRows = d.query(`SELECT COALESCE(assigned_to, agent_id, 'unassigned') as agent, COUNT(*) as count FROM tasks ${where} GROUP BY agent`).all(...params);
|
|
1803
|
+
const by_agent = {};
|
|
1804
|
+
for (const r of agentRows)
|
|
1805
|
+
by_agent[r.agent] = r.count;
|
|
1806
|
+
const completed = by_status["completed"] || 0;
|
|
1807
|
+
const completion_rate = totalRow.count > 0 ? Math.round(completed / totalRow.count * 100) : 0;
|
|
1808
|
+
return { total: totalRow.count, by_status, by_priority, completion_rate, by_agent };
|
|
1809
|
+
}
|
|
1810
|
+
function bulkCreateTasks(inputs, db) {
|
|
1811
|
+
const d = db || getDatabase();
|
|
1812
|
+
const tempIdToRealId = new Map;
|
|
1813
|
+
const created = [];
|
|
1814
|
+
const tx = d.transaction(() => {
|
|
1815
|
+
for (const input of inputs) {
|
|
1816
|
+
const { temp_id, depends_on_temp_ids: _deps, ...createInput } = input;
|
|
1817
|
+
const task = createTask(createInput, d);
|
|
1818
|
+
if (temp_id)
|
|
1819
|
+
tempIdToRealId.set(temp_id, task.id);
|
|
1820
|
+
created.push({ temp_id: temp_id || null, id: task.id, short_id: task.short_id, title: task.title });
|
|
1821
|
+
}
|
|
1822
|
+
for (const input of inputs) {
|
|
1823
|
+
if (input.depends_on_temp_ids && input.depends_on_temp_ids.length > 0) {
|
|
1824
|
+
const taskId = input.temp_id ? tempIdToRealId.get(input.temp_id) : null;
|
|
1825
|
+
if (!taskId)
|
|
1826
|
+
continue;
|
|
1827
|
+
for (const depTempId of input.depends_on_temp_ids) {
|
|
1828
|
+
const depRealId = tempIdToRealId.get(depTempId);
|
|
1829
|
+
if (depRealId) {
|
|
1830
|
+
addDependency(taskId, depRealId, d);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
tx();
|
|
1837
|
+
return { created };
|
|
1838
|
+
}
|
|
1839
|
+
function bulkUpdateTasks(taskIds, updates, db) {
|
|
1840
|
+
const d = db || getDatabase();
|
|
1841
|
+
let updated = 0;
|
|
1842
|
+
const failed = [];
|
|
1843
|
+
const tx = d.transaction(() => {
|
|
1844
|
+
for (const id of taskIds) {
|
|
1845
|
+
try {
|
|
1846
|
+
const task = getTask(id, d);
|
|
1847
|
+
if (!task) {
|
|
1848
|
+
failed.push({ id, error: "Task not found" });
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
updateTask(id, { ...updates, version: task.version }, d);
|
|
1852
|
+
updated++;
|
|
1853
|
+
} catch (e) {
|
|
1854
|
+
failed.push({ id, error: e instanceof Error ? e.message : String(e) });
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
tx();
|
|
1859
|
+
return { updated, failed };
|
|
1860
|
+
}
|
|
1309
1861
|
// src/db/plans.ts
|
|
1310
1862
|
function createPlan(input, db) {
|
|
1311
1863
|
const d = db || getDatabase();
|
|
@@ -1811,19 +2363,64 @@ function rowToTask2(row) {
|
|
|
1811
2363
|
requires_approval: Boolean(row.requires_approval)
|
|
1812
2364
|
};
|
|
1813
2365
|
}
|
|
1814
|
-
function searchTasks(
|
|
2366
|
+
function searchTasks(options, projectId, taskListId, db) {
|
|
2367
|
+
const opts = typeof options === "string" ? { query: options, project_id: projectId, task_list_id: taskListId } : options;
|
|
1815
2368
|
const d = db || getDatabase();
|
|
1816
2369
|
clearExpiredLocks(d);
|
|
1817
|
-
const pattern = `%${query}%`;
|
|
2370
|
+
const pattern = `%${opts.query}%`;
|
|
1818
2371
|
let sql = `SELECT * FROM tasks WHERE (title LIKE ? OR description LIKE ? OR EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = tasks.id AND tag LIKE ?))`;
|
|
1819
2372
|
const params = [pattern, pattern, pattern];
|
|
1820
|
-
if (
|
|
2373
|
+
if (opts.project_id) {
|
|
1821
2374
|
sql += " AND project_id = ?";
|
|
1822
|
-
params.push(
|
|
2375
|
+
params.push(opts.project_id);
|
|
1823
2376
|
}
|
|
1824
|
-
if (
|
|
2377
|
+
if (opts.task_list_id) {
|
|
1825
2378
|
sql += " AND task_list_id = ?";
|
|
1826
|
-
params.push(
|
|
2379
|
+
params.push(opts.task_list_id);
|
|
2380
|
+
}
|
|
2381
|
+
if (opts.status) {
|
|
2382
|
+
if (Array.isArray(opts.status)) {
|
|
2383
|
+
sql += ` AND status IN (${opts.status.map(() => "?").join(",")})`;
|
|
2384
|
+
params.push(...opts.status);
|
|
2385
|
+
} else {
|
|
2386
|
+
sql += " AND status = ?";
|
|
2387
|
+
params.push(opts.status);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
if (opts.priority) {
|
|
2391
|
+
if (Array.isArray(opts.priority)) {
|
|
2392
|
+
sql += ` AND priority IN (${opts.priority.map(() => "?").join(",")})`;
|
|
2393
|
+
params.push(...opts.priority);
|
|
2394
|
+
} else {
|
|
2395
|
+
sql += " AND priority = ?";
|
|
2396
|
+
params.push(opts.priority);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
if (opts.assigned_to) {
|
|
2400
|
+
sql += " AND assigned_to = ?";
|
|
2401
|
+
params.push(opts.assigned_to);
|
|
2402
|
+
}
|
|
2403
|
+
if (opts.agent_id) {
|
|
2404
|
+
sql += " AND agent_id = ?";
|
|
2405
|
+
params.push(opts.agent_id);
|
|
2406
|
+
}
|
|
2407
|
+
if (opts.created_after) {
|
|
2408
|
+
sql += " AND created_at > ?";
|
|
2409
|
+
params.push(opts.created_after);
|
|
2410
|
+
}
|
|
2411
|
+
if (opts.updated_after) {
|
|
2412
|
+
sql += " AND updated_at > ?";
|
|
2413
|
+
params.push(opts.updated_after);
|
|
2414
|
+
}
|
|
2415
|
+
if (opts.has_dependencies === true) {
|
|
2416
|
+
sql += " AND id IN (SELECT task_id FROM task_dependencies)";
|
|
2417
|
+
} else if (opts.has_dependencies === false) {
|
|
2418
|
+
sql += " AND id NOT IN (SELECT task_id FROM task_dependencies)";
|
|
2419
|
+
}
|
|
2420
|
+
if (opts.is_blocked === true) {
|
|
2421
|
+
sql += " AND id IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
|
|
2422
|
+
} else if (opts.is_blocked === false) {
|
|
2423
|
+
sql += " AND id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')";
|
|
1827
2424
|
}
|
|
1828
2425
|
sql += ` ORDER BY
|
|
1829
2426
|
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
@@ -2368,8 +2965,11 @@ export {
|
|
|
2368
2965
|
resetDatabase,
|
|
2369
2966
|
removeDependency,
|
|
2370
2967
|
registerAgent,
|
|
2968
|
+
parseRecurrenceRule,
|
|
2371
2969
|
now,
|
|
2372
2970
|
nextTaskShortId,
|
|
2971
|
+
nextOccurrence,
|
|
2972
|
+
moveTask,
|
|
2373
2973
|
logTaskChange,
|
|
2374
2974
|
lockTask,
|
|
2375
2975
|
loadConfig,
|
|
@@ -2383,15 +2983,20 @@ export {
|
|
|
2383
2983
|
listOrgs,
|
|
2384
2984
|
listComments,
|
|
2385
2985
|
listAgents,
|
|
2986
|
+
isValidRecurrenceRule,
|
|
2386
2987
|
getWebhook,
|
|
2387
2988
|
getTemplate,
|
|
2989
|
+
getTasksChangedSince,
|
|
2388
2990
|
getTaskWithRelations,
|
|
2991
|
+
getTaskStats,
|
|
2389
2992
|
getTaskListBySlug,
|
|
2390
2993
|
getTaskList,
|
|
2391
2994
|
getTaskHistory,
|
|
2995
|
+
getTaskGraph,
|
|
2392
2996
|
getTaskDependents,
|
|
2393
2997
|
getTaskDependencies,
|
|
2394
2998
|
getTask,
|
|
2999
|
+
getStaleTasks,
|
|
2395
3000
|
getSession,
|
|
2396
3001
|
getRecentActivity,
|
|
2397
3002
|
getProjectByPath,
|
|
@@ -2400,6 +3005,7 @@ export {
|
|
|
2400
3005
|
getOrgChart,
|
|
2401
3006
|
getOrgByName,
|
|
2402
3007
|
getOrg,
|
|
3008
|
+
getNextTask,
|
|
2403
3009
|
getDirectReports,
|
|
2404
3010
|
getDatabase,
|
|
2405
3011
|
getCompletionGuardConfig,
|
|
@@ -2407,6 +3013,8 @@ export {
|
|
|
2407
3013
|
getBlockingDeps,
|
|
2408
3014
|
getAgentByName,
|
|
2409
3015
|
getAgent,
|
|
3016
|
+
getActiveWork,
|
|
3017
|
+
failTask,
|
|
2410
3018
|
ensureTaskList,
|
|
2411
3019
|
ensureProject,
|
|
2412
3020
|
dispatchWebhook,
|
|
@@ -2429,9 +3037,14 @@ export {
|
|
|
2429
3037
|
createProject,
|
|
2430
3038
|
createPlan,
|
|
2431
3039
|
createOrg,
|
|
3040
|
+
countTasks,
|
|
2432
3041
|
completeTask,
|
|
2433
3042
|
closeDatabase,
|
|
3043
|
+
cloneTask,
|
|
3044
|
+
claimNextTask,
|
|
2434
3045
|
checkCompletionGuard,
|
|
3046
|
+
bulkUpdateTasks,
|
|
3047
|
+
bulkCreateTasks,
|
|
2435
3048
|
addDependency,
|
|
2436
3049
|
addComment,
|
|
2437
3050
|
VersionConflictError,
|