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