@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.
- package/dist/cli/index.js +111 -26
- package/dist/db/tasks.d.ts +1 -0
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/mcp/index.js +11 -5
- package/dist/server/index.js +1337 -726
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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/
|
|
797
|
-
var
|
|
798
|
-
__export(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
|
810
|
-
return crypto.randomUUID().slice(0, 8);
|
|
811
|
-
}
|
|
812
|
-
function rowToAgent(row) {
|
|
1032
|
+
function rowToTask(row) {
|
|
813
1033
|
return {
|
|
814
1034
|
...row,
|
|
815
|
-
|
|
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
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
830
|
-
|
|
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
|
-
|
|
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.
|
|
835
|
-
input.
|
|
836
|
-
input.
|
|
837
|
-
|
|
838
|
-
input.
|
|
839
|
-
input.
|
|
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
|
-
|
|
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
|
|
1097
|
+
function getTask(id, db) {
|
|
847
1098
|
const d = db || getDatabase();
|
|
848
|
-
const row = d.query("SELECT * FROM
|
|
849
|
-
|
|
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
|
|
1104
|
+
function getTaskWithRelations(id, db) {
|
|
852
1105
|
const d = db || getDatabase();
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
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
|
|
1130
|
+
function listTasks(filter = {}, db) {
|
|
858
1131
|
const d = db || getDatabase();
|
|
859
|
-
|
|
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
|
|
1210
|
+
function countTasks(filter = {}, db) {
|
|
862
1211
|
const d = db || getDatabase();
|
|
863
|
-
|
|
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
|
|
1273
|
+
function updateTask(id, input, db) {
|
|
866
1274
|
const d = db || getDatabase();
|
|
867
|
-
const
|
|
868
|
-
if (!
|
|
869
|
-
throw new
|
|
870
|
-
|
|
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.
|
|
873
|
-
sets.push("
|
|
874
|
-
params.push(input.
|
|
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.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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.
|
|
893
|
-
sets.push("
|
|
894
|
-
params.push(input.
|
|
1302
|
+
if (input.priority !== undefined) {
|
|
1303
|
+
sets.push("priority = ?");
|
|
1304
|
+
params.push(input.priority);
|
|
895
1305
|
}
|
|
896
|
-
if (input.
|
|
897
|
-
sets.push("
|
|
898
|
-
params.push(input.
|
|
1306
|
+
if (input.assigned_to !== undefined) {
|
|
1307
|
+
sets.push("assigned_to = ?");
|
|
1308
|
+
params.push(input.assigned_to);
|
|
899
1309
|
}
|
|
900
|
-
if (input.
|
|
901
|
-
sets.push("
|
|
902
|
-
params.push(input.
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1322
|
+
if (input.task_list_id !== undefined) {
|
|
1323
|
+
sets.push("task_list_id = ?");
|
|
1324
|
+
params.push(input.task_list_id);
|
|
932
1325
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
d.run(`
|
|
957
|
-
|
|
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
|
|
1387
|
+
function deleteTask(id, db) {
|
|
960
1388
|
const d = db || getDatabase();
|
|
961
|
-
const
|
|
962
|
-
return
|
|
1389
|
+
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
1390
|
+
return result.changes > 0;
|
|
963
1391
|
}
|
|
964
|
-
function
|
|
1392
|
+
function getBlockingDeps(id, db) {
|
|
965
1393
|
const d = db || getDatabase();
|
|
966
|
-
const
|
|
967
|
-
|
|
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
|
|
1405
|
+
function startTask(id, agentId, db) {
|
|
970
1406
|
const d = db || getDatabase();
|
|
971
|
-
|
|
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
|
|
1428
|
+
function completeTask(id, agentId, db, options) {
|
|
974
1429
|
const d = db || getDatabase();
|
|
975
|
-
const
|
|
976
|
-
if (!
|
|
977
|
-
throw new
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
|
1458
|
+
function lockTask(id, agentId, db) {
|
|
997
1459
|
const d = db || getDatabase();
|
|
998
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
|
1498
|
+
function addDependency(taskId, dependsOn, db) {
|
|
1022
1499
|
const d = db || getDatabase();
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
|
1509
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
1040
1510
|
const d = db || getDatabase();
|
|
1041
|
-
const
|
|
1042
|
-
return
|
|
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
|
|
1514
|
+
function getTaskDependencies(taskId, db) {
|
|
1045
1515
|
const d = db || getDatabase();
|
|
1046
|
-
return d.query("SELECT * FROM
|
|
1516
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
1047
1517
|
}
|
|
1048
|
-
function
|
|
1518
|
+
function getTaskDependents(taskId, db) {
|
|
1049
1519
|
const d = db || getDatabase();
|
|
1050
|
-
return d.
|
|
1520
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
1051
1521
|
}
|
|
1052
|
-
function
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
project_id: overrides
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1602
|
+
if (target.plan_id !== undefined) {
|
|
1603
|
+
sets.push("plan_id = ?");
|
|
1604
|
+
params.push(target.plan_id);
|
|
1112
1605
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1606
|
+
params.push(taskId);
|
|
1607
|
+
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
1608
|
+
return getTask(taskId, d);
|
|
1115
1609
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
|
|
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 (
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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 (
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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 (
|
|
1206
|
-
|
|
1685
|
+
if (filters?.task_list_id) {
|
|
1686
|
+
conditions.push("task_list_id = ?");
|
|
1687
|
+
params.push(filters.task_list_id);
|
|
1207
1688
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
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
|
|
1236
|
-
const
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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 (
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
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
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
|
1894
|
+
function bulkUpdateTasks(taskIds, updates, db) {
|
|
1293
1895
|
const d = db || getDatabase();
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
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
|
|
1976
|
+
function getAgent(id, db) {
|
|
1335
1977
|
const d = db || getDatabase();
|
|
1336
|
-
const row = d.query("SELECT * FROM
|
|
1337
|
-
|
|
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
|
|
1981
|
+
function getAgentByName(name, db) {
|
|
1342
1982
|
const d = db || getDatabase();
|
|
1343
|
-
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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 (
|
|
1359
|
-
|
|
1360
|
-
|
|
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 (
|
|
1368
|
-
|
|
1369
|
-
|
|
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 (
|
|
1377
|
-
|
|
1378
|
-
params.push(
|
|
2014
|
+
if (input.permissions !== undefined) {
|
|
2015
|
+
sets.push("permissions = ?");
|
|
2016
|
+
params.push(JSON.stringify(input.permissions));
|
|
1379
2017
|
}
|
|
1380
|
-
if (
|
|
1381
|
-
|
|
1382
|
-
params.push(
|
|
2018
|
+
if (input.title !== undefined) {
|
|
2019
|
+
sets.push("title = ?");
|
|
2020
|
+
params.push(input.title);
|
|
1383
2021
|
}
|
|
1384
|
-
if (
|
|
1385
|
-
|
|
1386
|
-
params.push(
|
|
2022
|
+
if (input.level !== undefined) {
|
|
2023
|
+
sets.push("level = ?");
|
|
2024
|
+
params.push(input.level);
|
|
1387
2025
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
|
|
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 (
|
|
1394
|
-
|
|
1395
|
-
params.push(
|
|
2030
|
+
if (input.org_id !== undefined) {
|
|
2031
|
+
sets.push("org_id = ?");
|
|
2032
|
+
params.push(input.org_id);
|
|
1396
2033
|
}
|
|
1397
|
-
if (
|
|
1398
|
-
|
|
1399
|
-
params.push(
|
|
2034
|
+
if (input.metadata !== undefined) {
|
|
2035
|
+
sets.push("metadata = ?");
|
|
2036
|
+
params.push(JSON.stringify(input.metadata));
|
|
1400
2037
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
const
|
|
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.
|
|
1432
|
-
sets.push("
|
|
1433
|
-
params.push(input.
|
|
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.
|
|
1459
|
-
sets.push("
|
|
1460
|
-
params.push(
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
|
2126
|
+
function deleteOrg(id, db) {
|
|
1536
2127
|
const d = db || getDatabase();
|
|
1537
|
-
|
|
1538
|
-
return result.changes > 0;
|
|
2128
|
+
return d.run("DELETE FROM orgs WHERE id = ?", [id]).changes > 0;
|
|
1539
2129
|
}
|
|
1540
|
-
|
|
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
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
|
2169
|
+
function getTemplate(id, db) {
|
|
1554
2170
|
const d = db || getDatabase();
|
|
1555
|
-
const
|
|
1556
|
-
|
|
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
|
|
2174
|
+
function listTemplates(db) {
|
|
1577
2175
|
const d = db || getDatabase();
|
|
1578
|
-
|
|
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
|
|
2178
|
+
function deleteTemplate(id, db) {
|
|
1607
2179
|
const d = db || getDatabase();
|
|
1608
|
-
return d.
|
|
2180
|
+
return d.run("DELETE FROM task_templates WHERE id = ?", [id]).changes > 0;
|
|
1609
2181
|
}
|
|
1610
|
-
function
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
|
2052
|
-
const
|
|
2053
|
-
|
|
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
|
}
|